cloud-init代码调试方法

新做的centos7.4镜像的cloud-init安装好之后,修改密码失败,但是同样的配置文件在7.2上的是正常的,对比了一下版本,centos7.4上的是0.7.9,7.2上的是0.7.5,经过调试发现是0.7.9版本的cloud-init有bug导致的,发现问题之后通过降级到0.7.5版本解决。之前也加断点调试过几次,但没有记录下来,这里记录下调试方法,因为默认直接加pdb断点是没法调试的。

首先要知道如何手工运行cloud-init工具,可以命令行执行: cloud-init -h ,看到有多个命令供选择,但我们只需要执行 cloud-init init 命令和 cloud-init init --local 命令即可,这两个命令就是开机自启动服务中会执行的,差异就是 --local 仅读取本地数据源(如config drive数据源),不加这个参数,可能尝试读取EC2等网络数据源(http://169.254.169.254)。

执行的时候要注意,cloud-init很多操作都是默认仅第一次开机的时候执行的(once-per-instance),也就是说默认情况下,每个云主机仅在第一次创建后启动的时候执行一次初始化操作,后面无论你怎么重启都不会执行的(当然不是全部初始化操作都不执行,要看具体的配置,但绝大部分操作都是once-per-instance)。所以为了保证每次手工执行的时候,都执行所有初始化操作,可以手工删除cloud-init的缓存目录,cloud-init是根据缓存中的云主机id(instance-id,一般就是云主机uuid)和数据源中获取的id来对比,确认是否是新建云主机第一次启动的。该缓存目录一般位于(/var/lib/cloud),直接删除整个目录即可清空缓存,调试过程中每次执行cloud-init命令前都需要删除一次。

直接加pdb断点到源码里是无法调试的,因为cloud-init会重定向标准输入stdin,所以还要注释掉这行代码才行:

上面是cloud-init init命令需要注释掉代码的示例(基于0.7.9版本源码,0.7.5版本的入口在/usr/bin/cloud-init或者/bin/cloud-init,可使用 which cloud-init 命令查看具体路径,需要在这个文件里注释掉这行代码),如果你要调试其他命令如cloud-init modules还有其他地方需要注释:

之后在main_init方法的入口处加入断点(0.7.5的不加会进入不了调试模式,0.7.9的可以不加,为了保险,可以加上,然后执行c命令跳到你真正想调试的断点处即可),然后再在你想要的任何地方加断点即可调试。

 

VisionStack(TM)私有云平台功能介绍

当前主要功能

后续发力方向

  • 混合云:继续完善阿里云的混合云解决方案,并逐步支持其他公有云厂商
  • 多机房:完善多机房支持,支持跨机房的资源池(计算、存储)
  • 平台运维自动化:进一步提高云平台远程及自动化运维服务能力,为支撑大量用户做技术准备
  • DevOps能力集成:深入理解用户业务开发测试上线等流程,基于云平台开发相关工具帮助用户进一步提升快速业务交付能力
  • 安全能力集成:对接安全厂商,补齐安全能力短板(网络、病毒、漏洞、审计等方面)
  • 云平台用户体验提升:持续改进

一次devstack环境创建vm失败的调试过程

问题比较简单,就是搭好了Mitaka版本devstack环境后(nova+neutron+glance+keystone+cinder),nova boot创建vm,nova-api一直报错:

看错误信息是发送的RabbitMQ消息没有收到consumer回复,最后超时报错了。

看代码是为了验证创建vm使用的网络是否有足够的port可用,然后nova做不了判断,需要发rpc出去给“neutron”帮忙确认,注意这里的“neutron”加了引号。

刚开始我以为是RabbitMQ的配置不对,比如用户名密码或者用户权限没配置好,就修改配置文件改用MQ默认的guest、guest用户密码来发送rpc消息,结果还是超时。

然后就对比了看了物理环境的MQ里的queue和exchange,没发现异常,决定下ipdb断点调试,单步调试了很久,看到使用的exchange是nova,topic是network,通过RabbitMQ的web插件(rabbitmq-plugins enable rabbitmq_management启用该插件,默认端口号是15672)看了下nova这个exchange,里面没有network队列,其他正常的环境也没有。

neutron那边没有注册上来?但是重启了neutron-server也没看到异常日志,并且显示MQ已正常连接了。

只能去正常的环境里打断点调试对比了,结果发现走的代码流程不一样,应该是走到nova.network.neutronv2.api.API#validate_networks,而不是nova.network.api.API#validate_networks,这时才恍然大悟,原来是nova配置文件有问题,少配置了use_neutron = True(默认值是False),导致走到了nova-network流程,nova-api试图发rpc请求给nova-network服务来验证网络参数。

修改nova.conf加上use_neutron = True之后,重启nova-api服务,再进行nova boot创建vm,一切正常了。。。。

 

nova添加neutron安全组代码流程分析

依赖知识点

  • neutron-server启动流程(包含处理HTTP请求的core plugin和extensions注册流程)
  • neutron-openvswitch-agent启动流程
  • neutron openvswitch(或Linux bridge)firewall安全组配置方案
  • neutron安全组创建、安全组规则创建流程
  • neutron rpc及callback机制
  • nova client(或openstack client)命令行及代码流程
  • nova-api启动流程(包含处理HTTP请求的controller注册流程)
  • nova-compute启动流程
  • nova云主机创建流程
  • WSGI路由规则
  • paste deploy WSGI框架

我们可以使用nova命令行给一台云主机(或者叫虚拟机、实例,下面统一叫云主机)添加安全组(前提是云主机已经成功创建,neutron安全组和规则也都创建完毕),其命令行如下:

本文的目的就是分析一下“nova add-secgroup”这个操作在nova项目和neutron项目中,具体执行到的代码流程,分析的相关项目的OpenStack版本是Mitaka。

本人水平有限,如有谬误,请不吝指正!

Nova项目代码流程

nova部分的代码流程比较简单,nova add-secgroup命令会通过client封装并发送HTTP请求到nova-api服务,对应的curl命令发送方式为(可以通过nova –debug add-secgroup $UUID $SGID获得):

nova-api接受到请求后,会交给nova.api.openstack.compute.security_groups.SecurityGroupActionController进行实际的处理,对应的处理方法为:

这段代码比较简单,_invoke没什么好说的,看一眼就明白,主要是self.security_group_api这个是什么要搞清楚,因为它影响接下来的代码流程。一般来讲,类里面的属性都是在__init__里面初始化的,我们首先去那边找找看:

是的,就是在这里初始化的。这里又涉及到openstack_driver是什么从哪儿来的问题(我看代码的三问:是什么?从哪儿来?到哪儿去?)。在nova\network\security_group\openstack_driver.py找到get_openstack_security_group_driver:

可以看到,_get_openstack_security_group_driver这个方法会import对应的类,这里还有一个is_neutron_security_groups方法,CONF.security_group_api.lower() == ‘neutron’这半个条件容易迷惑人,因为这个配置项的默认值是’nova’,因此实际上是后面的nova.network.is_neutron()条件才有效,找到nova\network\__init__.py中的is_neutron:

可以看到network_api_class、use_neutron这两个配置项,根据我们实际环境的配置(/etc/nova/nova.conf),network_api_class未配置为默认值,use_neutron=True,因此is_neutron()返回True。再回到openstack_driver.py和SecurityGroupActionController,最终确定security_group_api是NEUTRON_DRIVER = (‘nova.network.security_group.neutron_driver.SecurityGroupAPI’)。因此self.security_group_api.add_to_instance调用的是nova.network.security_group.neutron_driver.SecurityGroupAPI#add_to_instance方法:

这段代码也比较简单清晰,先根据安全组的名称或id找到安全组,之后根据instance uuid找到云主机上的ports,最后遍历ports调用update_port接口将安全组更新到每个port上。

nova项目代码流程到此结束。

Neutron项目代码流程

使用nova命令行添加安全组到云主机的主要流程实际是在neutron项目中,neutron client会封装update_port并发送HTTP请求给neutron-server,neutron-server收到后转发给base controller,然后根据之前注册好的controller和url映射关系,以及HTTP method(method这里是PUT,url是/port,映射过来就是update_port),再动态找到处理请求的core plugin和extensions(这段代码流程需要分析neutron-server启动流程),我们配置的core plugin是ml2,因此首先转到neutron.plugins.ml2.plugin.Ml2Plugin#update_port:

这个流程就比较复杂了,但我们要谨记一点,nova调用的update_port方法,传入的参数只有2个,一个是port_id,一个是包含security_groups属性的port信息:

知道这些信息后,代码流程就可以忽略很多无关的分支,整理后的相关代码分支如下:

check_and_notify_security_group_member_changed这个方法是关键流程,它是在基类neutron.db.securitygroups_rpc_base.SecurityGroupServerRpcMixin实现的,用途是通过rpc发送port安全组更新通知给neutron-openvswitch-agent服务,我们单独分析下:

走到这里,就又遇到了代码三问,notifier是什么?从哪儿来?security_groups_member_updated到哪儿去找?因为上面的代码流程有很多基类和派生类之间的跳转,还是得回到最初调用过来的地方,然后一路再找回来,我们目前分析过的类的跳转关系如下:

  1. neutron.plugins.ml2.plugin.Ml2Plugin#update_port
  2. neutron.db.securitygroups_rpc_base.SecurityGroupServerRpcMixin#check_and_notify_security_group_member_changed

其中SecurityGroupServerRpcMixin是Ml2Plugin的基类,因此self.notifier不在SecurityGroupServerRpcMixin这里就肯定在Ml2Plugin里,我们在Ml2Plugin的__init__方法里没有直接找到self.notifier的初始化代码,但看到了self._start_rpc_notifiers()这个方法,里面有self.notifier初始化过程(其实我是直接搜索字符串”self.notifier = “找到的_start_rpc_notifiers,然后再找_start_rpc_notifiers的调用方,就找到了__init__,这也是我常用的·猥琐·查找代码方法,个人感觉比较快速有效):

我们要找的security_groups_member_updated方法在AgentNotifierApi类没有实现,只能从基类里面查找:

上面的代码是发送rpc请求到agent端(neutron-openvswitch-agent端的rpc consumer注册流程,需要分析agent的启动流程),agent端的回调方法在:

接下来是self.sg_agent三问:是什么?从哪儿来?到哪里去?分析neutron-openvswitch-agent启动流程可以得知,self.sg_agent是在neutron.plugins.ml2.drivers.openvswitch.agent.ovs_neutron_agent.OVSNeutronAgent#__init__方法中定义并初始化的:

因此只能等轮询过程调用refresh_firewall了,分析neutron-openvswitch-agent启动流程,可以知道轮询执行流程如下:

  1. neutron.plugins.ml2.drivers.openvswitch.agent.ovs_neutron_agent.main
  2. neutron.plugins.ml2.drivers.openvswitch.agent.ovs_neutron_agent.OVSNeutronAgent#daemon_loop
  3. neutron.plugins.ml2.drivers.openvswitch.agent.ovs_neutron_agent.OVSNeutronAgent#rpc_loop
  4. neutron.plugins.ml2.drivers.openvswitch.agent.ovs_neutron_agent.OVSNeutronAgent#process_network_ports
  5. neutron.agent.securitygroups_rpc.SecurityGroupAgentRpc#setup_port_filters
  6. neutron.agent.securitygroups_rpc.SecurityGroupAgentRpc#refresh_firewall

2017-09-20补充:ipdb加断点调试后确认之前分析的流程是正确的。

上述流程没有仔细分析,但应该差不多就是这样了。

self.delete_all_port_flows(of_port) 、self.initialize_port_flows(of_port) 、self.add_flows_from_rules(of_port)这三个方法分别执行清空port上的openflow流表、初始化流表、根据安全组规则添加流,具体执行流程不再分析,最终是通过ovs-ofctl命令进行br-int网桥上的流表规则的下发,这其中还牵涉到安全组规则到openflow流规则的转换过程。

这部分也没有来得及仔细分析,感觉应该是在启动过程中注册的br-int网桥的driver(neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py:__init__方法有self.int_br = self.br_int_cls(ovs_conf.integration_bridge)初始化)。

关于rpc类的命名和用途的联系,我参考这些RPC类的注释分析相关类的用途后,得到到结论如下(不知道是否正确):

  • SecurityGroupServerRpcApi:注意里面的”ServerRpc”字段,表明这是发送到neutron-server的rpc client,因此调用方(client端)是agent或者extension,neutron-server是server端
  • SecurityGroupServerRpcCallback:对应上面rpc client发送请求的rpc server端的回调方法,用来处理rpc请求并返回结果(返回结果仅限call类型的同步的rpc请求,cast异步请求不返回)
  • SecurityGroupAgentRpcApiMixin:”AgentRpc”表明是发送到agent或者extension的rpc client,因此调用方(rpc client端)neutron-server,rpc server端是neutron-openvswitch-agent或其他extension
  • SecurityGroupAgentRpcCallbackMixin:对应上面rpc client请求的rpc server端的回调处理方法

其它rpc类的命名和用途的联系也是类似情况,知道这个规则,可以更简单的理解rpc调用关系以及代码是在哪个服务里执行的。

 

写给儿子们<13>

转眼间弟弟已经2个月多了,爸爸也在新公司上班2个月了

哥哥在这2个月里表现还可以,暑假里跟着奶奶学数学,100以内的加减法已经基本掌握了

美中不足的是,哥哥太喜欢弟弟了,时不时的要跑过去亲一下弟弟,摸一下弟弟,经常把熟睡中的弟弟吵醒,妈妈辛苦把弟弟哄睡,正想休息一下,被哥哥吵醒又要抱起来哄半天

哥哥还有一个不好的习惯,吃饭、刷牙、洗澡、英语课打卡总是拖拉,虽然最终还是做了,但是不够主动,要大人催促好几次,拖拽着才做

现在哥哥开学了,已经是大班了,感觉昨天还在读小班呢,转眼就长大了

时间过的太快,爸爸已经追不上是你们长大的脚步了,最近换了工作,新公司刚起步,有各种各样的事情要忙,所以陪你们的时间、陪妈妈的时间都少了很多

爸爸也一直在思考,究竟是要把更多的时间花在努力工作赚钱上,还是要花更多的时间陪你们长大,是否能做到兼顾这两者?现在看来是不好做到的,只能尽量平衡吧,工作的时候全心工作,回到家多陪你们玩玩

另外爸爸也在想,已经好久没有看过文学类的书籍了,这几年看的最多的就是技术类的书,感觉有点心理空虚,精神食粮不足,房价飞涨,工作压力大,前途迷茫,还有两个儿子,压力山大,导致生活状态充满了焦虑,一直这么下去可不是什么好事,要积极调整。而以我之前的经验,多读一些文学经典,对平和心态调整状态有很大帮助。

另外爸爸还在想,每天的运动量太少了,这段时间天气太热,中午也不能去外面散步,每天开车上下班,更没有运动的时间,导致整个人精神萎靡,提不起劲,垂垂老矣的感觉,这么下去不是办法。

因此从昨天开始,爸爸已经决定天气合适的时候就坐公交车上下班,然后在离公司2公里的车站下车后,骑车去公司(共享单车确实方便),下班也一样先骑车去公交站,公交车大多数时间也是站一路回家,这样运动量就上去了。

另外之前开车上下班,看着路上的豪车,总是感觉自己太穷,赚钱压力更大了,堵车也导致脾气变差,而骑共享单车坐公交车,则感觉还有那么多人跟我一样骑车坐公交上下班,在公交车上还能看看书、刷刷新闻,感觉生活都变得充实美好了,还能顺便锻炼身体,看来保持心态健康非常重要。

等你们长大识字了,爸爸一定给你们多买书,多读书不光为了考试,读书真的能让人生变得不一样。

弟弟这段时间也不错,吃的可真胖,肉嘟嘟的,而且也不算黑,是个小帅哥,晚上也不怎么闹夜了,大人逗也知道笑了。

我们家生二胎的原因很多,但最重要的一条是,想让你们兄弟以后可以互相照顾、互相帮助,人生难免遇到困难、痛苦,有人分担至少不会感觉那么无助。虽说儿子成家后都是老婆说了算,但是遇到通情达理的女人,兄弟情分还是非常亲密的。

Gerrit搭建遇到的一些问题

参考官方文档:https://gerrit-documentation.storage.googleapis.com/Documentation/2.14.2/install.html

  1. 使用centos6.9系统,注意关闭selinux和iptables防火墙,并创建gerrit2用户:adduser gerrit2
  2. 安装MySQL或者mariadb,修改utf8编码
    [mysqld]  ## 该段下增加如下配置:
    character_set_server = utf8
    character_set_client = utf8
    character_set_filesystem = utf8

    之后重启mysql服务。

  3. 安装jre(版本高于1.7,最低1.8):     rpm -i jre-8u144-linux-x64.rpm,下载地址官方安装文档里面有链接
  4. 安装nginx,在/etc/nginx/conf.d/目录下新建或修改文件:
    virtual.conf,内容如下:
    server {
            listen       80;
            server_name  10.0.30.121;   ## 本机IP
            root         /usr/share/nginx/html;
            location / {
              auth_basic              "Gerrit Code Review";
              auth_basic_user_file    /opt/gerrit2/etc/htpasswd.conf;   ### 登录gerrit使用的认证密码文件
              proxy_pass              http://127.0.0.1:8888;    ### gerrit监听的端口
              proxy_set_header        X-Forwarded-For $remote_addr;
              proxy_set_header        Host $host;
            }
            error_page 404 /404.html;
                location = /40x.html {
            }
            error_page 500 502 503 504 /50x.html;
                location = /50x.html {
            }
    }

    之后重启nginx服务。

  5. 初始化数据库
    CREATE USER 'gerrit2'@'localhost' IDENTIFIED BY '123456';    ### gerrit2为数据库用户名,123456为密码,或者直接用root用户和密码访问,root用户密码修改方法请百度,则只需要创建reviewdb数据库即可
    CREATE DATABASE reviewdb;    ### reviewdb为gerrit使用的数据库名称
    GRANT ALL ON reviewdb.* TO 'gerrit2'@'localhost';
    FLUSH PRIVILEGES;
  6. 安装gitweb:yum install gitweb
  7. 配置用户登录密码(需要安装httpd-tools,yum install httpd-tools):
    cd /opt/gerrit2/etc
    htpasswd -c htpasswd.conf admin   ###之后输入两次密码,-c表示创建密码文件htpasswd.conf
    htpasswd htpasswd.conf user1   ### 添加其他用户
  8. 初始化gerrit,先下载gerrit-2.14.2.war(https://gerrit-releases.storage.googleapis.com/index.html):
    mkdir /opt/gerrit2
    cp gerrit-2.14.2.war /opt/gerrit2
    chown gerrit2.gerrit2 -R /opt/gerrit2
    su gerrit2
    java -jar /opt/gerrit2/gerrit-2.14.2.war init -d /opt/gerrit2/ # 输入选默认,插件全部安装(选Y),后面会覆盖初始化过程生成的配置文件

    初始化完之后会在/opt/gerrit2/etc/目录下生成2个配置文件:

    [root@gerrit etc]# cat gerrit.config
    [gerrit]
            basePath = git
            serverId = a673cba0-8af3-4f69-90c6-b681bca3b638
            canonicalWebUrl = http://10.0.30.121/    ### 访问gerrit的IP,需要与nginx配置端口相同,80可不加
    [database]
            type = mysql
            hostname = localhost
            database = reviewdb?useUnicode=yes&characterEncoding=UTF-8
            username = root   ### 直接用root用户访问数据库
    [index]
            type = LUCENE
    [auth]
        type = HTTP
        emailFormat = {0}@yunrongtech.com
    [receive]
            enableSignedPush = false
    [sendemail]
            smtpServer = localhost
    [container]
            user = gerrit2
            javaHome = /usr/java/jre1.8.0_144
    [sshd]
            listenAddress = *:29418
    [httpd]
            listenUrl = http://*:8888/   ### gerrit服务监听端口,需要与nginx中配置相同
    [cache]
            directory = cache
    [gitweb]
            type = gitweb
            cgi = /var/www/git/gitweb.cgi

    gitweb这里,我一开始参考了很多资料,都没提到要加type这个配置项,都无法显示gitweb链接,浪费了很多时间,后来在官方文档里找到了这个配置项是必须要配置的,否则就相当于关闭了gitweb。https://gerrit-documentation.storage.googleapis.com/Documentation/2.14.2/config-gerrit.html#gitweb

    打开changeid超链接,以及关联JIRA单号的超链接设置(追加到gerrit.config文件即可):

    gerrit自带插件也可以之后单独安装:java -jar gerrit.war init -d <SITE_PATH> [–install-plugin=PLUGIN_NAME]

    [root@gerrit etc]# cat secure.config
    [database]
            password = 123456   ### 数据库密码
    [auth]
            registerEmailPrivateKey = BfxlQzoFQlk/XGGDgrP75mt0PnLXmMXS6Ak=

    之后启动gerrit服务即可:cd /opt/gerrit2/bin, ./gerrit.sh start。
    这里遇到一个问题,gerrit在有些发行版部署(可能跟发行版有关,也可能是其他原因,此问题没找到解决方法)之后启动很慢,要10分钟左右,而默认超时时间是90s,导致一直提示”Starting Gerrit Code Review: FAILED”,只能修改超时时间来处理,修改方法,在gerrit.conf文件中的[container]段下增加startupTimeout=900,启动慢但是至少能用: ( 。

    2017-09-18补充:今天同事网上找到了一个java程序启动慢的解决方法,试了下果然有效果(我用的是kvm虚拟机启动的gerrit),首先找到jre的安装目录,可以从gerrit etc目录下的配置文件gerrit.config中找到javaHome = /usr/java/jre1.8.0_144,然后进入其下的lib/security/目录,vi打开其中的java.security,找到securerandom.source=file:/dev/random,将file:/dev/random改成file:/dev/urandom即可解决gerrit启动慢的问题。

  9. 自启动配置
    nginx:chkconfig nginx on
    gerrit:cd /etc/init.d, ln -s   /opt/gerrit2/bin/gerrit.sh  gerrit, chkconfig –add gerrit

备份

  1. crontab脚本
    [root@gerrit rsync]# cat /etc/cron.d/gerrit
    39 2 * * * root /opt/gerrit-backup/backup.sh
    [root@gerrit rsync]# cat /opt/gerrit-backup/backup.sh
    #! /bin/bash
    dt=date +%Y%m%d
    # mysql backup
    mysqldump -uroot -p123456 reviewdb > /opt/gerrit-backup/reviewdb-${dt}.sql
    # gerrit backup
    tar Pcf /opt/gerrit-backup/gerrit2-${dt}.tar /opt/gerrit2
  2. rsync server
    [root@gerrit rsync]# cat /etc/rc.local
    #!/bin/sh
    touch /var/lock/subsys/local
    rsync --daemon --config=/etc/rsync/rsyncd.conf &   ### 添加这行,自动启动rsync服务
    [root@gerrit rsync]# cat /etc/rsync/rsyncd.conf
    # Minimal configuration file for rsync daemon
    # See rsync(1) and rsyncd.conf(5) man pages for help
    # This line is required by the /etc/init.d/rsyncd script
    pid file = /var/run/rsyncd.pid
    lock file = /var/lock/rsyncd.lock
    port = 10873
    address = 10.0.30.121
    uid = root
    gid = root
    use chroot = no
    read only = no
    #limit access to private LANs
    hosts allow=0.0.0.0/0
    #hosts deny=*
    max connections = 32
    motd file = /etc/rsync/rsyncd.motd
    #This will give you a separate log file
    log file = /var/log/rsync/rsync.log
    log format = %t %a %m %f %b
    syslog facility = local3
    timeout = 300
    [gerrit]
    path = /opt/gerrit-backup
    list = no
    ignore errors
    auth users = gerrit
    secrets file = /etc/rsync/rsync.pas
    #comment = gerrit backup dir
    #exclude =[root@gerrit rsync]# cat /etc/rsync/gerrit.pas ### 注意文件权限为600
    ********* #### rsync密码[root@gerrit rsync]# cat /etc/rsync/rsync.pas ### 注意文件权限为600
    gerrit:******* #### rsync server认证使用的配置文件,包含用户名及密码,这里的密码要与上面gerrit.pas中的完全一致
  3. rsync client  ### 在一台windows7电脑上,配置定时任务,自动执行rsync同步(Linux客户端也一样的配置):
    d:\blog\gerrit.bat   ### rsync client脚本文件
    cd /d c:\Program Files (x86)\ICW\Bin   ### rsync windows 客户端安装目录
    rsync.exe -vzrtopgu --progress --delete  --port=10873 gerrit@10.0.30.121::gerrit /cygdrive/d/blog/gerrit-backup < gerrit.pas   ### gerrit.pas为密码文件,与server端一致

    windows任务计划(每天早上2:33分自动同步rsync server配置的目录/opt/gerrit-backup到d:\blog\gerrit-backup):

    gerrit-backup.xml:  ### 在任务计划程序中导入这个文件即可
    <?xml version="1.0" encoding="UTF-16"?>
      <RegistrationInfo>
        <Date>2017-08-22T12:34:50.8871484</Date>
        <Author>VisionStack-pc\Administrator</Author>
      </RegistrationInfo>
      <Triggers>
        <CalendarTrigger>
          <StartBoundary>2017-08-23T02:33:37</StartBoundary>
          <Enabled>true</Enabled>
          <RandomDelay>PT1H</RandomDelay>
          <ScheduleByDay>
            <DaysInterval>1</DaysInterval>
          </ScheduleByDay>
        </CalendarTrigger>
      </Triggers>
      <Principals>
        <Principal id="Author">
          <UserId>S-1-5-18</UserId>
          <RunLevel>LeastPrivilege</RunLevel>
        </Principal>
      </Principals>
      <Settings>
        <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
        <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
        <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
        <AllowHardTerminate>true</AllowHardTerminate>
        <StartWhenAvailable>true</StartWhenAvailable>
        <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
        <IdleSettings>
          <StopOnIdleEnd>true</StopOnIdleEnd>
          <RestartOnIdle>false</RestartOnIdle>
        </IdleSettings>
        <AllowStartOnDemand>true</AllowStartOnDemand>
        <Enabled>true</Enabled>
        <Hidden>false</Hidden>
        <RunOnlyIfIdle>false</RunOnlyIfIdle>
        <WakeToRun>false</WakeToRun>
        <ExecutionTimeLimit>P3D</ExecutionTimeLimit>
        <Priority>7</Priority>
      </Settings>
      <Actions Context="Author">
        <Exec>
          <Command>D:\blog\gerrit.bat</Command>
        </Exec>
      </Actions>
    </Task>

定时任务服务设计

需求

目前我们平台有多个需要定期执行的任务,举例说明:

  1. 平台定期邮件报表(已经开发的功能)
  2. 弹性伸缩服务的定时伸缩功能(即将开发的功能)
  3. 云主机定期备份(已经开发的功能)
  4. 回收站云主机到期删除(目前永远不删除的逻辑不太合适,放入回收站时可设置一个过期删除时间)

未来可能还会有其他定时任务增加,如云硬盘回收站定期清空功能。

这些任务的执行规律都比较类似,都是由用户设定类似Linux crontab的执行周期(比如每周一早上2点整执行),或者每隔多少时间执行一次(比如每1800s执行一次),然后我们后台检查是否到执行时间,进而执行预设操作。

现状

当前的定时任务执行的代码实现比较多,每个功能都单独实现一种执行方法显然不合适,比较难维护,扩展也比较复杂,高可用等设计也没考虑到。

举例说明,云主机定期备份功能是用crontab功能实现的,只能单节点部署(同时在两个节点部署的话,就可能备份两次)。

另外平台定期报表功能,是新开发的服务,已经考虑到一部分异常、高可用实现,但缺乏通用性,只能给这一个功能使用,如果弹性伸缩服务需要增加定时任务,还需要再单独实现一个类似的后台服务,需求很类似,copy代码再修改实现一个服务显然是比较浪费的,因此可以考虑实现一个通用的定时任务框架,来统一的执行所有定时任务。

方案

python中有比较成熟的定时任务调度库,比如APScheduler(https://apscheduler.readthedocs.io/en/latest/userguide.htmlhttps://nummy.github.io/2016/02/19/aps/https://lz5z.com/Python定时任务执行方式),使用这个库可以轻松实现各种定时任务。

因为我们可以考虑基于APScheduler来实现一个通用的定时任务服务,给所有需要执行定时任务的功能使用。

任务管理

我们可以在用户设置定时任务时,将任务保存到我们自建的数据库中,并区分任务类型,供web查询展示使用,以及定时任务服务分类执行使用。

Job表结构设计如下(可根据实际需要添加索引):

字段名称
类型
用途
ID char Job的唯一标识符,32位UUID表示,索引字段
resource text Job关联的资源,最多65535个字符,json格式存储,如果是平台报表,则可留空,如果是弹性伸缩定时任务,则关联到集群UUID、伸缩规则UUID,如果云主机回收站清理或者定期备份,则关联到云主机UUID
name char Job的名称,用于web展示,255位字符,可考虑支持中文
type char Job的类型,255位字符,用于保存Job的使用方或者说用途,目前考虑支持平台邮件报表(platform_report)、弹性伸缩定时规则(auto_scaling)、云主机定期备份(auto_backup)、清空云主机回收站(empty_vm_recycle_bin)
trigger char Job的触发器类型,255位字符,参考APScheduler中trigger类型,共3种可选:date、interval、cron,分别表示一次性任务、固定时间间隔任务、cron任务
owner char Job所属的租户,用于加入租户内执行各种操作,32位字符
policy char Job执行周期策略,255位字符,根据trigger_type类型不同,这里格式也不同,date为某一时间点,interval为固定时间段如1800,cron则参考crontab语法
executor char Job执行节点的hostname,255为字符
last_exec_time datetime Job上次执行时间,需注意时区问题,建议统一为UTC时间
created_at datetime Job创建时间,需注意时区问题,建议统一为UTC时间
updated_at datetime Job更新时间,需注意时区问题,建议统一为UTC时间
deleted_at datetime Job删除时间,需注意时区问题,建议统一为UTC时间
deleted int Job是否已删除,0表示未删除,其他数字表示已删除,索引字段

任务执行

使用APScheduler来管理所有Job的调度执行任务,设置一个主Job定期扫描DB的Job表,与定时任务服务中正在执行的Job列表比较,检查是否有任务新增或者修改,如有则把新Job加入调度队列(add_job,并将resource_id作为参数传给job function),或者修改已调度Job(modify_job)。

需要为每种Job类型编写对应的job function,用于作为add_job方法的func参数,在任务被触发时具体执行该类任务,如云主机回收站清空任务,则可以编写一个类似如下代码的method:

清空云主机回收站示例任务代码
1
2
3
4
5
6
def empty_vm_recycle_bin(*args, **kwargs):
    # 首先从args、kwargs中获取resource_id,也即云主机uuid
    vm_uuid = kwargs.get('resource_id')
    if vm_uuid is None:
        raise Exception()  # 注意需要raise APScheduler指定的异常
    # 根据配置文件中的keystone管理员账号获取token,然后组装url并发送http请求(建议使用python requests库)给nova,执行删除云主机操作

高可用及负载均衡

为了避免单点故障导致所有定时任务都无法执行,需要考虑多节点部署定时任务服务,为此需要考虑任务的分发和加锁,以及节点异常后其他节点的接管流程。任务分发还需要考虑每个节点执行的总任务数量,防止所有任务都集中在某一个节点,而其他节点没有任务可做的情况发生。

高可用场景需要增加一个executor表,字段设计如下(可根据需要添加索引):

字段
类型
用途
host char Job执行节点hostname,255位字符
last_heartbeat_time datetime Job执行节点上次心跳时间

定时任务服务需要定期上报其心跳到DB的executor表(可用一个独立的job实现,每10s上报一次),用于其他节点检测该节点的服务是否异常,如果心跳超时(超时时间可配置到配置文件中)则表示异常,异常后其他节点需要接管该节点负责的Job(修改Job表的executor字段),该节点恢复上线后可以继续接受新Job。判断一个节点异常的条件可设置为(需全部同时满足):

  1. 节点心跳超时(根据executor表中last_heartbeat_time判断,心跳超时时间可设置到配置文件,比如5分钟)
  2. 节点任务执行时间超时(根据Job表中last_exec_time判断,每种任务可设置不同的超时时间,如平台报表任务执行时间较长可设置为15分钟,云主机删除任务执行时间较短,可设置为5分钟,超时时间保存在配置文件中)

负载均衡问题:可以在每个节点主Job定期扫描DB的Job表时检查Job总量,并与所有存活的节点数量进行比较,保证当前节点执行的Job数量不超过math.ceil(总Job数/总存活节点数),并保证没有Job被遗漏而永远执行不到。

其他事项

  1. 本文档只是描述一种初步考虑的定时任务框架解决方案,还需要实际的编写原型代码进行可行性验证,并进行方案细化。
  2. 以配置文件中的管理员用户权限去操作租户资源时(如为租户创建云主机),需要先把管理员用户加入到租户内(member role),之后用租户+管理员用户获取token,再去执行操作。

OpenStack Dashboard用户登录为啥不需要输入租户名

问题

后端命令执行获取token或者执行如nova list等命令时,需要准备的用户信息文件(admin_rc)通常包含如下内容:

1
2
3
4
5
6
7
8
export OS_PROJECT_DOMAIN_NAME=default
export OS_USER_DOMAIN_NAME=default
export OS_PROJECT_NAME=admin
export OS_USERNAME=admin
export OS_PASSWORD=openstack
export OS_AUTH_URL=http://vs-controller:35357/v3
export OS_IDENTITY_API_VERSION=3
export OS_IMAGE_API_VERSION=2

也就是需要很多信息,比dashboard登录时多了很多,有些信息dashboard可以提前配置好,比如OS_AUTH_URL、OS_IDENTITY_API_VERSION、OS_IMAGE_API_VERSION之类的,但是user、project、domain相关信息则不行,因为每个用户对应的project、domain都是不一样的(目前我们平台没启用domain,默认都是default),这里就有一个问题,为啥project不需要用户输入,就可以获取token操作project的资源?

原因

经过询问相关熟悉keystone的朋友,并google搜索相关逻辑,找到了问题的答案。

原来keystone支持两种token类型:scoped、unscoped,其中unscoped的token就是专门为用户登录准备的,这种token获取时仅需要提供用户名、密码即可,但它的用途就比较有限,仅能用来获取用户所属租户列表(V2版本API示例:http://www.cnblogs.com/liuan/p/3194499.html,V3版本示例:http://blog.csdn.net/chenwei8280/article/details/52461408),通过unscoped token拿到租户列表后,就可以获取默认租户信息,然后通过租户信息+用户名密码,即可获取scoped token,这种token就可以正常操作租户所有资源了,当然用户也可以在web上切换project,实现操作不同project资源的功能。

验证

接下来亲自验证一下相关API,这里只验证我们使用的V3版本keystone api,首先创建一个unscoped token,可以看到我们只提供了用户名密码和default domain,并没有提供租户名信息:

[root@vs-compute-82 ~]# curl -i   -H "Content-Type: application/json"   -d '
"auth": {
    "identity": {
      "methods": ["password"],
      "password": {
        "user": {
          "name""admin",
          "domain": { "name""default" }, 
          "password""openstack"
        }
      }
    }
  }
}'   http://vs-controller:5000/v3/auth/tokens

再看下API返回的响应内容:

HTTP/1.1 201 Created
X-Subject-Token: gAAAAABZgYgpZ18icfEVFbhQ0YoD-Hy0O5rNXgEH1i3FHwTE79jERGE404wKOh9KrpGOnJPVC-Kd0teY8lh441tm_MRVDBph-YDwM-FNkTSkDGKeLSMhk0pF7hRXNPiKsJBpT5HCJW41bO3apS3TBnRIHavuyJqb0ppIvKev17yyniyUf_IEIg8
Vary: X-Auth-Token
Content-Type: application/json
Content-Length: 4198
X-Openstack-Request-Id: req-7cb8c560-d5cd-416c-b5ab-55afdfa0f515
Date: Wed, 02 Aug 2017 08:07:05 GMT
{
    "token": {
        "audit_ids": [
            "ukdlt-mGQtyl6mnNYGYdsQ"
        ],
        "catalog": [
            {
                "endpoints": [
                    {
                        "id""4f5f80c9e87140929c90964d95a9fc0d",
                        "interface""internal",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:5000/v3"
                    },
                    {
                        "id""5bb4f46e9dbf4575b7e8f1244272c178",
                        "interface""public",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:5000/v3"
                    },
                    {
                        "id""8717669ebc0b4299af2750ab282f4d5d",
                        "interface""admin",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:35357/v3"
                    }
                ],
                "id""307e9642e0cf44ca9c7798796cdf9290",
                "name""keystone",
                "type""identity"
            },
            {
                "endpoints": [
                    {
                        "id""64666fafeaea4f87994d8c9ef67f82e2",
                        "interface""internal",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:9292"
                    },
                    {
                        "id""77efcd4ca5f1401dabc5b7ad32e50351",
                        "interface""admin",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:9292"
                    },
                    {
                        "id""9d40878e853c4637a66732da1aa51c89",
                        "interface""public",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:9292"
                    }
                ],
                "id""3456cac60558400191a82ed400e8f196",
                "name""glance",
                "type""image"
            },
            {
                "endpoints": [
                    {
                        "id""274b02e30d1746d0950051242188874d",
                        "interface""public",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:9696"
                    },
                    {
                        "id""38ff891fd7684163a6eb108ba60ff28a",
                        "interface""internal",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:9696"
                    },
                    {
                        "id""3aca19ca07ea4bafadfc789da777179a",
                        "interface""admin",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:9696"
                    }
                ],
                "id""74941df6655d4338a32cd9a40422f676",
                "name""neutron",
                "type""network"
            },
            {
                "endpoints": [
                    {
                        "id""41bfe2c2c1ae40d196dd81049cea2b98",
                        "interface""public",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:8776/v2/1829813dc6e94d95aec1d1ace95b587b"
                    },
                    {
                        "id""61c046be865045f19304ba1d5e160af8",
                        "interface""internal",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:8776/v2/1829813dc6e94d95aec1d1ace95b587b"
                    },
                    {
                        "id""9556e0e7d18c4a518e1239a0cb3a53ee",
                        "interface""admin",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:8776/v2/1829813dc6e94d95aec1d1ace95b587b"
                    }
                ],
                "id""82c4abe7015c4f4d8609bdaff6906187",
                "name""cinderv2",
                "type""volumev2"
            },
            {
                "endpoints": [
                    {
                        "id""4cf84e5ba40c4433a52abfd80fcce279",
                        "interface""public",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:8774/v2.1/1829813dc6e94d95aec1d1ace95b587b"
                    },
                    {
                        "id""e6f0b85808214b6594825bdf5f7453df",
                        "interface""admin",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:8774/v2.1/1829813dc6e94d95aec1d1ace95b587b"
                    },
                    {
                        "id""f831a5a1035d44bfa4d228e49ddbbc33",
                        "interface""internal",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:8774/v2.1/1829813dc6e94d95aec1d1ace95b587b"
                    }
                ],
                "id""b6f480b4504143b4bce292bbb498a68b",
                "name""nova",
                "type""compute"
            },
            {
                "endpoints": [
                    {
                        "id""42118b37e7854851ae7165e585548db2",
                        "interface""internal",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:8776/v1/1829813dc6e94d95aec1d1ace95b587b"
                    },
                    {
                        "id""c11ad2ef852742cdbb69c4bd6d70de33",
                        "interface""admin",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:8776/v1/1829813dc6e94d95aec1d1ace95b587b"
                    },
                    {
                        "id""ef88e5228ca7487fa4b7eace8b366472",
                        "interface""public",
                        "region""RegionOne",
                        "region_id""RegionOne",
                        "url""http://vs-controller:8776/v1/1829813dc6e94d95aec1d1ace95b587b"
                    }
                ],
                "id""d55298be79634b5887fd831ec5989e97",
                "name""cinder",
                "type""volume"
            }
        ],
        "expires_at""2017-08-02T09:07:38.000000Z",
        "issued_at""2017-08-02T08:07:38.000000Z",
        "methods": [
            "password"
        ],
        "project": {
            "domain": {
                "id""a77fc52dc6664cca92e4a18e49dc0375",
                "name""default"
            },
            "id""1829813dc6e94d95aec1d1ace95b587b",
            "name""admin"
        },
        "roles": [
            {
                "id""db9c8afebaaf4870ade55402a8df67ed",
                "name""admin"
            }
        ],
        "user": {
            "domain": {
                "id""a77fc52dc6664cca92e4a18e49dc0375",
                "name""default"
            },
            "id""212a66c41a334b74bffea2cebe14d400",
            "name""admin"
        }
    }
}

可以看到我们已经获取了一个token,但要注意token信息是在http header里面返回的(X-Subject-Token字段),而不是在body里面,body里面返回的只有catalog信息(endpoints和用户信息,以及token过期时间等)。

接下来我们用这个unscoped token获取project列表:

[root@vs-controller ~]# curl -g  -X GET -H "Accept: application/json" -H "X-Auth-Token: gAAAAABZgY5_0w3njAKqWnpo1hteRKGPgsf7yQNKBoLIwwJZ3vLxdzeS2PpIp1uzWf_3D8dSXOy1rwUsW4jyIRHyeWr0amaeshwkoqP1yJ_CNHNzGNyto-3kI1l-KwcqrRkbuU-OeKi62jQ0vkEJqFOqczoUrOCYSo7GJZtObibKPTEloJrvacA"  http://vs-controller:5000/v3/auth/projects | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   432  100   432    0     0   1788      0 --:--:-- --:--:-- --:--:--  1792
{
    "links": {
        "next": null,
        "previous": null,
    },
    "projects": [
        {
            "description""Admin Project",
            "domain_id""a77fc52dc6664cca92e4a18e49dc0375",
            "enabled"true,
            "id""1829813dc6e94d95aec1d1ace95b587b",
            "is_domain"false,
            "links": {
            },
            "name""admin",
            "parent_id""a77fc52dc6664cca92e4a18e49dc0375"
        }
    ]
}

可以看到已经正常获取到projects列表。之后的获取scoped token操作就不再演示,只是http body加上project name字段即可。

我们要试验的是,用unscoped token获取云主机列表是否能成功(预期肯定是失败返回错误)。

root@host-10-0-60-105:~# curl -g -i -X GET http://127.0.0.1:8774/v2.1/551d7771d57c443b88f48034601b72df/servers -H "Accept: application/json" -H "X-Auth-Token: gAAAAABZg95LjPhDyVIWcPLuVuCV_39ugBaq9BXMVID62hH9kqn4325t7LiU-FzXlRsvhVOXsYwcThEFA3b3DkIAzisyNKF5jD543raM1HrxWp9cO671LtQuaaqlZGUJ24VZLnpXMEPRUEAzmAEIMgZmCod4TcThGg"
HTTP/1.1 400 Bad Request
X-Openstack-Nova-Api-Version: 2.1
Vary: X-OpenStack-Nova-API-Version
Content-Type: application/json; charset=UTF-8
Content-Length: 160
X-Compute-Request-Id: req-b0747602-0f22-456f-9038-a844aa7541e1
Date: Fri, 04 Aug 2017 02:42:51 GMT
{"badRequest": {"message""Malformed request URL: URL's project_id '551d7771d57c443b88f48034601b72df' doesn't match Context's project_id 'None'""code": 400}}

可以看到,返回错误400,提示token中包含的project_id是None,与http请求URL中的project id不匹配,实际上你将URL中的project id修改为任何值都不能成功,因为token里面没有project id信息,这也正是unscoped token和scoped token的差异。

OpenStack noVNC Chrome浏览器里鼠标不能用的问题

问题症状

用win10平板+Chrome浏览器(60版本,其他版本没测试过)打开云主机的控制台(VNC窗口),后端使用的是novncproxy,通过websocket协议与qemu的vnc server进行通信,其他一切正常,云主机桌面正常可见,可以输入字符,也能点击控制台右上角的“Send CtrlAltDel”并且云主机桌面有反应,但就是移动鼠标没反应,通过vncviewer windows客户端连接到vnc server,鼠标正常,通过微软edge浏览器打开控制台页面,鼠标也正常可用,因此可以确认是Chrome浏览器的问题,但同事的相同版本的Chrome浏览器打开是正常的,怀疑是浏览器插件问题,禁用所有插件后问题还是存在,一度以为是自己人品问题。

解决方法

google搜索了一把,找到很多类似问题,有些是noVNC项目的bug,有些是配置问题,先看了下bug,根据commit的diff,看了下使用的noVNC源码,确认已经修复了相关问题。

之后看Chrome配置,找到一个https://forum.proxmox.com/threads/mouse-not-working-on-console-novnc.32619/,里面提到

于是试着改了下设置,重启Chrome浏览器后鼠标果然正常了,又回头看了下其他同事的这个配置,也都是默认的Automatic,为啥他们是好用的?想了想应该是我的电脑是平板的,支持触摸屏,通过HDMI外接显示器,然后平板的这个Touch Events配置automatic就等同于enabled,而同事们的都是普通的PC机,所以是disabled效果。

写给儿子们<12>

关于跳槽

5年前,为了有时间陪哥哥长大,给哥哥讲睡前故事,陪他入睡,爸爸选择了离开华为,进入网易这个爸爸在毕业前以及在华为期间都梦寐以求的公司,能有幸进入网易是爸爸一辈子的骄傲。

5年后,弟弟出生了,爸爸深感压力山大,决定再次做出改变,离开网易,加入创业公司。希望能在你们长大后,如果问起来爸爸,“为啥我不是富二代?”这个问题时,爸爸至少可以问心无愧的对你们说,爸爸年轻时尽力了!

5年前爸爸离开华为,没有一丝顾虑和犹豫,只有庆幸和激动,庆幸自己能早一天离开狼厂华为,在面试感觉很差的情况下被网易录用,激动自己能进入网易这个梦寐以求的公司。虽然猪厂网易这5年都没把我养肥,但这并不是猪厂的错,毕竟身边很多同事已经先肥起来了。即使体重不达标,我也决定出栏了。

如果要描述一下决定离开网易的感受,可以用我多年前考研结束,要离开郑州来杭州读研那段时间的感受来形容,我觉得这两段感受非常神似。

离开郑州,就意味着离开还在郑州读书的那个我曾经深切喜欢5年的女同学,本来考上研究生是一件非常值得骄傲和兴奋的事情,可惜因为她,离开郑州变成了一种诀别,那个女同学本就不喜欢我,离开之后就意味着从此以后我俩将天各一方陌路殊途,我俩人生也不再会有哪怕只是见面点头致意的交集。

是的,爸爸今天要发一张好人卡了,在此之前的很多年,爸爸只收不发,从各位阿姨们那里收集来的好人卡,差不多可以凑齐一副扑克牌了。人生第一张卡我决定发给网易,陪伴了我5年,见证了我人生最好的年华,娶妻生子买房买车,当然还有升职加薪。网易很可能是我人生职业生涯中最好的公司,离开他也可能是我今生最失败最错误的决定。

在网易干的很爽,但是感觉干的活不值公司给的薪水钱,与其等公司撵人,还是主动走比较好。

关于失败

截止目前,爸爸的人生还不算失败,好人卡收集太多也不能算作失败,更多的是因为爸爸长的不帅,颜值天注定,爸爸没办法改变,但也算尽力了(为了矫正龅牙爸爸忍受了4年的折磨)。

收集那么多好人卡,一度让爸爸感到绝望,爸爸曾经天真的以为是爸爸不够优秀,所以那些阿姨们才看不上,多年以后我才明白,颜值低到爆表才是更重要的原因。

不过幸好,爸爸保持了这种天真的想法很多年,一直努力的让自己更加优秀,还幻想着能有个阿姨能慧眼识珠,把爸爸带走。还好你们的妈妈注意到了爸爸,才没有让爸爸一直这么怀疑人生直到孤独终老。

爸爸想说的是,失败不一定是成功之母,但至少是让人快速成长的捷径,没有经历失败的人生是不完整的,只有失败者才能更懂得珍惜,就像爸爸失败了那么多次才被你们的妈妈解救,爸爸深知不易,曾暗自发誓今生今世都不会背叛你们的妈妈。

而现在,爸爸做出了离开网易这个抉择,爸爸也曾经犹豫再三,因为新的公司刚刚起步,创业维艰,十之八九将以失败告终,到那时爸爸该如何面对你们和你们的妈妈?是的,你们的妈妈对爸爸真好,爸爸犹豫不决的时候,她只是说不建议你换公司,只怕你以后再也找不到网易这么好的了。但是她还是没有拦着爸爸,她明白爸爸的想法,知道爸爸如果不去尝试失败一次,以后换再多好公司也按捺不住躁动不安的心。得妻如此,夫复何求。

有些人是不见兔子不撒鹰,而另一些则是不见棺材不落泪。

至于失败之后的事,就等失败之后再说吧,大不了一败涂地,从头再来,不过至少爸爸还有你们,还有你们的妈妈,只要一家人在一起没什么好担心的。

风萧萧兮易水寒,SB一去兮不复还。

趁年轻,多失败。

关于股票

如果成功了,那些股票钱就没啥意思了,如果失败了,就算现在不走多拿那些钱对整个人生来说也没啥意思啊,指望这点钱你们也成不了富二代。。。

当然你们的妈妈、奶奶都觉得这几十万的股票还是挺值钱的,尤其是明年应该还能再分100W,但是钱是赚不完的,人生的意义在于奋斗,努力到了钱自然不是问题,就算结果还是没钱,那也只能怪运气或者命运不好,等老了可以说了无遗憾。

不因碌碌无为而悔恨!