Python使用Kerberos认证查询Impala

最近做QoS报告,数据来源于Impala,客户端认证采用的是Kerberos。
Impala是Cloudera公司开发并开源的一款基于HDFS/Hbase的MPP SQL引擎,它提供SQL语义,能够查询存储在Hadoop的HDFS和HBase中的PB级大数据。Kerberous本身是一个网络认证授权协议,借由中心服务器认证,对通信双方的客户端/服务端进行授权而不需要传递双方的密码。Kerberos的认证流程比较有意思,分为三个阶段

  • 客户端认证
    • 1 客户端发送自己用户名
    • 2 认证服务器返回使用客户端密钥加密的Client/TGS会话密钥和使用票据授权服务器密钥加密的TGT, 包括sessions key,用户信息及有效期
    • 3 客户端使用自己的密钥解密出Client/TGS会话密钥
  • 服务授权
    • 1 客户端发送两条消息:接收到的TGT和所请求的服务ID;使用Client/TGS会话密钥加密的用户ID和时间戳
    • 2 票据授权服务器使用自己的密钥解密TGT得到客户端的Client/TGS会话密钥,然后使用它解密出用户ID并进行认证。返回使用所请求服务端密钥加密的client-server票据和使用Client/TGS会话密钥加密的Client/Server会话密钥
    • 3 客户端使用Client/TGS会话密钥(Client/TGS Session Key)解密出Client/Server会话密钥
  • 服务请求
    • 1 客户端发送两条消息:使用所请求服务端密钥加密的client-server票据及使用Client/Server会话密钥加密的用户ID和时间戳
    • 2 服务端使用自己的密钥解密client-server票据从而得到Client/Server会话密钥,使用该密钥解密获得用户信息并认证。返回使用Client/Server会话密钥的新时间戳
    • 3 客户端使用Client/Server会话密钥解密该消息,认证结束并请求服务
    • 4 服务端提供服务

在CentOS上安装Kerberos

yum install krb5-devel pam_krb5 krb5-libs krb5-workstation

编辑配置

vim /etc/krb5.conf

配置KDC,认证服务器

[logging]
default = FILE:/var/log/krb5libs.log
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmind.log
[libdefaults]
default_realm = EXAMPLE.COM
dns_lookup_realm = false
dns_lookup_kdc = true
ticket_lifetime = 24h
renew_lifetime = 7d
forwardable = true
default_tkt_enctypes = rc4-hmac
default_tgs_enctypes = rc4-hmac
permitted_enctypes = rc4-hmac
[realms]
EXAMPLE.COM = {
default_domain = example.com
kdc = kdc01.example.com
kdc = kdc02.example.com
admin_server = adc01.example.com
admin_server = adc02.example.com
}
[domain_realm]
.example.com = EXAMPLE.COM
example.com = EXAMPLE.COM

测试一下

[root@localhost rc]# kinit abc.xyz@EXAMPLE.COM
Password for abc.xyz@EXAMPLE.COM:

注意这个配置文件每行前面的空格被删掉,是因为在VirtualBox里面每行开头有莫名其妙的乱码,Linux下并不可见,在EditPlus下面才发现,否则会乱报错

kinit: Improper format of Kerberos configuration file while initializing Kerberos 5 library
kinit: Cannot find KDC for realm "EXAMPLE.COM" while getting initial credentials

查看一下认证的ticket

[root@localhost vagrant]# klist
Ticket cache: FILE:/tmp/krb5cc_0
Default principal: abc.xyz@EXAMPLE.COM

Valid starting       Expires              Service principal
09/21/2017 08:30:50  09/21/2017 18:30:50  krbtgt/EXAMPLE.COM@EXAMPLE.COM
        renew until 09/28/2017 08:30:42

这个ticket在28号就会过期了,到时候又要输入密码,这样也不便于自动化程序使用。可以使用ktutil创建keytab文件

$ ktutil
ktutil:  addent -password -p abc.xyz@EXAMPLE.COM -k 1 -e RC4-HMAC
Password for abc.xyz@EXAMPLE.COM:
ktutil:  wkt abc.xyz.keytab
ktutil:  q
$ ls
abc.xyz.keytab

测试一下

$ kinit -kt abc.xyz.keytab abc.xyz@EXAMPLE.COM
$ klist -k abc.xyz.keytab
Keytab name: FILE:abc.xyz.keytab
KVNO Principal
---- --------------------------------------------------------------------------
  1 abc.xyz@EXAMPLE.COM

之后便可以使用kinit自动更新ticket了。注意,如果更换了密码,需要重新生成新的keytab。
另外,相同用户生成的授权ticket在任意一台机器上都是相同的, kinit时会自动同步回来的。
公司的大数据平台使用Hue来提供基于web界面的查询,Impala也支持使用ODBC方式查询。在Python里使用的是impyla来查询,首先安装sasl的依赖

yum install libgsasl-devel cyrus-sasl-devel cyrus-sasl-gssapi
pip install impyla thrift_sasl

测试脚本

from impala.dbapi import connect
conn = connect(host="impalad.example.com", port=21050, auth_mechanism='GSSAPI', kerberos_service_name='impala', database='acme')
cur =  conn.cursor()
cur.execute(r'SELECT * FROM acme WHERE dt="2017-09-12" LIMIT 5')
print(cur.fetchall())

运行下

python test.py

如下报错,则是服务器不能连接,检查一下网络,DNS/hosts及VPN

thriftpy.transport.TTransportException: TTransportException(type=1, message="Could not connect to ('impalad.example.com', 21050)")

如下报错,CentOS则是需要cyrus-sasl-gssapi模块

thriftpy.transport.TTransportException: TTransportException(type=1, message="Could not start SASL: b'Error in sasl_client_start (-4) SASL(-4): no mechanism available: No worthy mechs found'")

参考链接:
Impala:新一代开源大数据分析引擎
大数据时代快速SQL引擎-Impala
CDH 5.2中Impala认证集成LDAP和Kerberos
Kerberos
Configuring Kerberos Authentication for Windows
Speaking Kerberos with KNIME Big Data Extensions

使用Supervisor 管理监控进程

Supervisor是用Python写的一款应用程监控管理工具,能够启动,停止,重启死进程,提供web管理界面,XML-RPC接口及事件监听。通常我们写了一些脚本都不会带有daemon功能,而是加&或者nohub,screen什么的丢到后台去运行,同时使用corntab定时检测脚本是否存活,以便重新运行脚本。使用Supervisor可以将这些脚本,程序转为守护进程,自动重启它们;还可以监控机器的进程运行状况,输出警报等。
Supervisor只能运行于Python 2.x的环境,但子进程可以为其他任意程序,比如Python 3,PHP等。这里使用pip来安装

$ wget https://bootstrap.pypa.io/get-pip.py
$ python -V
$ sudo python get-pip.py

生成配置文件及日志目录

$ sudo echo_supervisord_conf > /etc/supervisord.conf
$ mkdir /var/log/supervisor
$ chmod 655 /var/log/supervisor

启动supervisord

$ sudo supervisord -c /etc/supervisor
$ supervisorctl
$ Server requires authentication
$ Username:user
$ Password:

$ supervisor> status
$ supervisor> help

default commands (type help <topic>):
=====================================
add    exit      open  reload  restart   start   tail
avail  fg        pid   remove  shutdown  status  update
clear  maintail  quit  reread  signal    stop    version
$ supervisor> quit

这里没有任何进程。以下为常用命令:

  • supervisorctl stop program 停止某个进程
  • supervisorctl start program 启动某个进程
  • supervisorctl restart program 重启某个进程
  • supervisorctl stop group 重启属于group分组的所有进程(start,restart同理)
  • supervisorctl stop all 停止全部进程,注:start、restart、stop都不会载入最新的配置文件
  • supervisorctl reload 载入最新配置文件,停止原有进程并按新配置启动进程
  • supervisorctl update 根据最新配置文件,启动新配置或有改动的进程,没有改动的进程不受影响
  • 编辑supervisord.conf启用web界面,账号密码为web及supervisorctl共用,必须更改

    $ sudo vim /etc/supervisord.conf
    #取消以下行的注释
    [inet_http_server]         ; inet (TCP) server disabled by default
    port=*:9002           ; ip_address:port specifier, *:port for all iface
    username=user              ; default is no username (open server)
    password=123               ; default is no password (open server)
    
    #添加新应用qrd
    [program:qrd]
    command = /usr/bin/gunicorn --access-logfile - --workers 3 --bind 127.0.0.1:8000 qrd.wsgi
    directory = /home/qrd/qrd/
    user = root
    autostart = true
    autorestart = true
    startsecs = 5
    startretries = 3
    stdout_logfile = /var/log/supervisor/qrd.log
    stderr_logfile = /var/log/supervisor/qrd.log
    

    关于supervisord.conf配置项,命令释义可以参考这里

  • command 为要运行的脚本或程序
  • directory 为脚本或程序运行时的工作目录
  • user 为脚本或程序运行时的用户
  • autostart 随supervisord启动
  • startsecs 启动后等待时间,过了这个时间没起来就重新启动
  • startretries 启动失败后重试的次数
  • stdout_logfile,stderr_logfile 为输出日志
  • 重新加载所有应用

    $ supervisorctl
    Server requires authentication
    Username:user
    Password:
    
    supervisor> reload
    Really restart the remote supervisord process y/N? y
    Restarted supervisord
    supervisor> status
    qrd                              RUNNING   pid 3861, uptime 0:00:22
    

    可以看到定义的qrd程序已经起来了。如果qrd程序意外退出了,那么supervisord将会重启它。如果杀掉了supervisord,那么qrd对应的进程也将被杀死。也可以去web界面查看http://127.0.0.1:9002/

    可以通过web页面来启动,停止进程,查看日志等。
    再增加下Celery的Worker,Beat配置

    ; ==================================
    ;  celery worker supervisor example
    ; ==================================
    
    [program:qrdworker]
    ; Set full path to celery program if using virtualenv
    command=/usr/bin/celery worker -A qrd --loglevel=INFO
    
    ; Alternatively,
    ;command=celery --app=your_app.celery:app worker --loglevel=INFO -n worker.%%h
    ; Or run a script
    ;command=celery.sh
    
    directory=/home/qrd/qrd/
    user=nobody
    numprocs=1
    stdout_logfile=/var/log/supervisor/qrdworker.log
    stderr_logfile=/var/log/supervisor/qrdworker.log
    autostart=true
    autorestart=true
    startsecs=10
    
    ; Need to wait for currently executing tasks to finish at shutdown.
    ; Increase this if you have very long running tasks.
    stopwaitsecs = 600
    
    ; Causes supervisor to send the termination signal (SIGTERM) to the whole process group.
    stopasgroup=true
    
    ; Set Celery priority higher than default (999)
    ; so, if rabbitmq is supervised, it will start first.
    priority=1000
    
    ; ================================
    ;  celery beat supervisor example
    ; ================================
    
    [program:qrdbeat]
    ; Set full path to celery program if using virtualenv
    command=/usr/bin/celery -A qrd beat -l info -s /tmp/celerybeat-schedule
    
    ; remove the -A myapp argument if you aren't using an app instance
    
    directory=/home/qrd/qrd/
    user=nobody
    numprocs=1
    stdout_logfile=/var/log/supervisor/beat.log
    stderr_logfile=/var/log/supervisor/beat.log
    autostart=true
    autorestart=true
    startsecs=10
    
    ; Causes supervisor to send the termination signal (SIGTERM) to the whole process group.
    stopasgroup=true
    
    ; if rabbitmq is supervised, set its priority higher
    ; so it starts first
    priority=999
    

    然后启用它们

    supervisor> update
    qrdbeat: added process group
    supervisor> status
    qrd                              RUNNING   pid 4468, uptime 0:03:49
    qrdbeat                          BACKOFF   Exited too quickly (process log may have details)
    qrdworker                        RUNNING   pid 4469, uptime 0:03:49
    

    查看下进程

    $ sudo ps aux | grep python
    root      1038  0.0  3.1 562392 15720 ?        Ssl  01:49   0:01 /usr/bin/python -Es /usr/sbin/tuned -l -P
    root      3992  0.1  3.0 222124 15224 ?        Ss   03:49   0:00 /bin/python /bin/supervisord -c /etc/supervisord.conf
    root      3993  0.3  3.8 211868 19296 ?        S    03:49   0:00 /usr/local/python3/bin/python3.6 /usr/bin/gunicorn --access-logfile - --workers 3 --bind 127.0.0.1:8000 qrd.wsgi
    root      3996  0.3  6.9 264048 34780 ?        S    03:49   0:00 /usr/local/python3/bin/python3.6 /usr/bin/gunicorn --access-logfile - --workers 3 --bind 127.0.0.1:8000 qrd.wsgi
    root      3998  0.3  6.9 264056 34784 ?        S    03:49   0:00 /usr/local/python3/bin/python3.6 /usr/bin/gunicorn --access-logfile - --workers 3 --bind 127.0.0.1:8000 qrd.wsgi
    root      3999  0.4  6.9 264024 34784 ?        S    03:49   0:00 /usr/local/python3/bin/python3.6 /usr/bin/gunicorn --access-logfile - --workers 3 --bind 127.0.0.1:8000 qrd.wsgi
    root      4014  0.0  0.1 112660   976 pts/0    R+   03:50   0:00 grep --color=auto python
    

    除了program标签,Supervisor也支持group标签,用来启动一系列的program,比如先启动数据库,再启动web服务器。
    Supervisor也可以用来管理PHP进程,比如使用 Supervisor 管理 Laravel 队列进程
    Docker官方推荐一个docker容器只运行一个服务,如果你想启动多个脚本或程序,可以使用Supervisor会更简单点
    如果需要Supervisor开启启动,可以使用GitHub上的脚本和配置。CentOS 7上可以配置为service,由systemctl来管理:

    $ sudo vim /etc/systemd/system/supervisord.service
    

    内容如下:

    # supervisord service for systemd (CentOS 7.0+)
    # by ET-CS (https://github.com/ET-CS)
    [Unit]
    Description=Supervisor daemon
    
    [Service]
    Type=forking
    ExecStart=/usr/bin/supervisord
    ExecStop=/usr/bin/supervisorctl $OPTIONS shutdown
    ExecReload=/usr/bin/supervisorctl $OPTIONS reload
    KillMode=process
    Restart=on-failure
    RestartSec=42s
    
    [Install]
    WantedBy=multi-user.target
    

    然后启用

    $ sudo systemctl enable supervisord.service
    Created symlink from /etc/systemd/system/multi-user.target.wants/supervisord.service to /etc/systemd/system/supervisord.service.
    $ sudo systemctl start supervisord.service
    $ sudo ps aux | grep python
    

    虽然supervisorctl及web界面都只能管理本机,但是Supervisor提供了XML-RPC接口,可以获取进程运行信息,因此诞生了许多监控平台,以便监控服务器集群的进程的运行状况。
    Supervisor还提供了event listener配置,在这里Supervisor将会通知脚本其他进程的运行状态变更的事件,可以用来发送警报监控等。

    参考链接:
    使用Supervisor3.2.1基于Mac10.10.3对系统进程进行管理
    supervisor的配置浅析
    Python 进程管理工具 Supervisor 使用教程
    Supervisor进程监护使用指南
    supervisord 的 XML-RPC API 使用说明
    Supervisor Event Listener
    Dockerizing Nginx and SSH using Supervisord

    PHP MongoDB Replica Set应用

    MongoDB是个面向文档管理的NoSQL数据库,能够直接存取JSON数据,支持海量数据。公司内部的一个单点登录系统使用MongoDB来存储用户的session,以便在不同应用,服务器之间共享登录信息,解决了服务器切换用户状态丢失(被登出)的问题。单点登录系统前端采用PHP,与LDAP交互认证用户信息,提供登录界面,API等。MongoDB则采用Replica Set模式以便支持高可用。
    在CentOS 6.5上安装MongoDB,首先添加源仓库

    $ suod vim /etc/yum.repos.d/mongodb.repo
    [mongodb]
    name=MongoDB Repository
    baseurl=http://downloads-distro.mongodb.org/repo/redhat/os/x86_64/
    gpgcheck=0
    enabled=0
    

    安装,启动,连接MongoDB

    $ sudo yum --enablerepo=mongodb install mongodb-org
    $ sudo /sbin/chkconfig --levels 235 mongod on
    $ sudo mongod --port 27017 --dbpath /data/db1
    $ mongo --port 27017
    

    创建用户SSOReplUser

    > use admin 
    > db.createUser( { user: "SSOReplUser", pwd: "<password>", roles: [ { role: "root", db: "admin" } ] });
    

    创建集群要用到的key

    $ sudo openssl rand -base64 741 > /home/sso/mongodb-keyfile 
    $ sudo chmod 600 /home/sso/mongodb-keyfile
    

    编辑MongoDB配置文件

    $ sudo vim /etc/mongod.conf
    # Replication Options 
    # in replicated mongo databases, specify the replica set name here 
    replSet=SSOReplSet 
    # maximum size in megabytes for replication operation log 
    #oplogSize=1024 
    # path to a key file storing authentication info for connections # between replica set members keyFile=/home/sso/mongodb-keyfile
    

    重启mongod服务

    $ sudo /etc/init.d/mongod stop   
    $ sudo /usr/bin/mongod -f /etc/mongod.conf
    

    使用SSOReplUser登录

    > use admin 
    > db.auth("SSOReplUser", "<password>");
    

    初始化集群

    > rs.initiate()
    > rs.conf()   
    {
      "_id": "SSOReplSet",
      "version": 1,
      "members": [
        {
          "_id": 1,
          "host": "192.168.33.10:27017"
        }
      ]
    }
    

    在其他机器上按照上面步骤安装MongoDB,更改配置,复制对应的mongodb-keyfile并启动。
    回到192.168.33.10并添加机器

    SSOReplSet:PRIMARY> rs.add("192.168.33.11:27017") 
    SSOReplSet:PRIMARY> rs.add("192.168.33.12:27017") 
    SSOReplSet:PRIMARY> rs.add("192.168.33.13:27017")
    #SSOReplSet:PRIMARY> rs.remove("192.168.33.13:27017")
    

    在从机上认证查看

    SSOReplSet:SECONDARY> use admin 
    SSOReplSet:SECONDARY> db.auth("SSOReplUser", "<password>");   
    SSOReplSet:SECONDARY> rs.status()
    

    MongoDB的Replica set模式最少要求3台机器,以便在PRIMARY机器故障时,能够自我选举出新的PRIMARY,但需要n/2+1的机器投票同意。例如刚才配置了4台机器,那么需要4/2+1=3台机器投票,能够承受一台机器故障。如果是集群有5台机器,则能够承受2台机器故障。因此再配置一台不存储数据的Arbiter机器比较合理。
    按照上面的步骤安装,复制mongodb-keyfile文件,但需要更改mongod.conf

    $sudo vim /etc/mongod.conf
    nojournal=true
    

    然后启动mongod服务并在PRIMARY机器上将这台机器加入机器

    SSOReplSet:PRIMARY> use admin   
    SSOReplSet:PRIMARY> db.auth("SSOReplUser", "<password>");   
    SSOReplSet:PRIMARY> rs.addArb("<ip of arbiter>");
    

    PHP的mongo扩展安装比较简单,下载对应版本,启用即可。然而在应用过程中发现登录有点慢,启用看PHP mongo扩展profiling功能,查看PHP日志

    <?php
    \MongoLog::setLevel(\MongoLog::ALL);
    \MongoLog::setModule(\MongoLog::ALL);
    try{
        echo  microtime(true) . PHP_EOL;
        echo "aaa01(primary)" . PHP_EOL;
        $m = new \MongoClient("mongodb://admin:XXXXXXXXX@192.168.33.10:27017/?replicaSet=SSOReplSet ");
        echo  microtime(true) . PHP_EOL;
        echo "aaa01(primay), bbb01(secondary),ccc01(secondary),ddd01(secondary)" . PHP_EOL;
        $m = new \MongoClient("mongodb://admin:XXXXXXXXX@192.168.33.10:27017,192.168.33.11:27017,192.168.33.12:27017,192.168.33.13:27017/?replicaSet=SSOReplSet ");
        echo  microtime(true) . PHP_EOL;
    } catch (\MongoConnectionException $e) {
        var_dump($e);
    }
    

    发现PHP连接MongoDB集群是这样子的

    即:

  • 1)在连接里面配置了几个MongoDB连接服务器,PHP每个都会创建连接去查询
  • 2)从每台服务器上查询出整个集群的服务器列表,再分别ping和连接这些服务器,如果连接不存在或不匹配则创建,无效的则销毁
  • 3)汇总所有服务器返回集群列表
  • 4)选择离自己最近的服务器并使用该与该服务器的连接
  • MongoDB 的写操作默认在Primary节点上操作完成即返回成功;读操作默认是也是从Primary上读取,所以需要去查询服务器。由于SSO应用部署在多个数据中心,网络抖动会造成较大影响,跨数据中心的查询并不会很快,如果每次都去连接并查询整个列表是比较耗时。另外如果在php里面配置的是IP而MongoDB Replica Set里面配置的是域名,则连接名会出现不匹配,而创建新的连接并销毁旧连接,也耗时。如果配置的是域名,则需要DNS解析。由于每台PHP服务器均已配置HA检测,最终每个应用只配置了一台服务器,并统一配置MongoDB集群为IP。而连接最快的是在Primary机器上,可以根据本机是否Primary来做HA:

    #!/usr/bin/env bash
    count=`ps -fe |grep "mongod" | grep -v "grep" | wc -l`
    FILE="/home/scripts-bits/master.conf"
    SERVER=`hostname`
    
    if [ $count -lt 1 ]; then
        rm -f $FILE
    else
        PRIMAY=`/usr/bin/mongo ${SERVER}:27017 --quiet --eval 'printjson(db.isMaster().ismaster);'`
        if [ "$PRIMAY" == "true" ]; then
        	if [ ! -f "$FILE" ]; then
        		touch "$FILE"
        	fi
        	REMOVE=`/usr/bin/mongo ${SERVER}:27017/admin --quiet /home/scripts-bits/mongo_status.js`
        fi
    fi
    

    删除故障节点(not reachable/healthy)的脚本mongo_status.js:

    db.auth('admin','<password>');
    conf=rs.status();
    members=conf["members"];
    for(i in members){
    	if(members[i]["state"] == 8){
    		rs.remove(members[i]["name"]);
    	}
    }
    

    这中间也出现过因为网络/机器故障,导致PHP等待连接超时的情况的,将该机器从集群中移除即可。然而当服务器是启动的(可以到达)情况下,MongoDB故障,则不会超时。可以更改connectTimeoutMS,以便减少等待。
    MongoDB的日志默认都是记录在/var/log/mongodb/下面并且会越来越大。创建Python脚本来定时清除它:

    #!/bin/env python
    import commands
    import datetime,time
    
    def rotate_log(path, expire = 30):
        str_now = time.strftime("%Y-%m-%d")
        dat_now = time.strptime(str_now, "%Y-%m-%d")
        array_dat_now = datetime.datetime(dat_now[0], dat_now[1], dat_now[2])
        lns = commands.getoutput("/bin/ls --full-time %s|awk '{print $6, $9}'" % path)
        for ln in lns.split('\n'):
            ws = ln.split()
            if len(ws) != 2:
                continue
            ws1 = time.strptime(ws[0], "%Y-%m-%d")
            ws2 = datetime.datetime(ws1[0], ws1[1], ws1[2])
            if (array_dat_now - ws2).days > expire:
                v_del = commands.getoutput("/bin/rm -rf %s/%s" % (path, ws[1]))
    
    
    def rotate_mongo():
        # get mongo pid
        mongo_pid = commands.getoutput("/sbin/pidof mongod")
        #print mongo_pid
        # send Sig to mongo
        if mongo_pid != '':
            cmd = "/bin/kill -USR1 %s" % (mongo_pid)
            # print cmd
            mongo_rotate = commands.getoutput(cmd)
        else:
            print "mongod is not running..."
    
    if __name__ == "__main__":
        log_path = "/var/log/mongodb/"
        expire = 30
        rotate_mongo()
        rotate_log(log_path, expire)
    
    

    加入到crontab里面去执行

    10 1 * * * /usr/bin/python /home/sso/mongo_rotate.py > /dev/null 2>&1
    

    参考连接
    mongodb与mysql相比的优缺点
    PHP MongoDB 复制集合
    MongoDB Replication
    MongoDB Enable Auth
    MongoDB Rotate Log Files
    Blocking connect() leads to cumulative timeouts for multiple inaccessible servers
    How Raft consensus algorithm will make replication even better in MongoDB 3.2
    Write Concern for Replica Sets
    MongoDB Read Preference

    Vagrant CentOS 共享目录挂载问题解决

    一直以来都是使用Vagrant与VirtualBox运行CentOS系统来搭建环境,然而有一天突然出现Windows下面的目录无法映射进去了,报错:

    D:\project\vagrant\centos64php56>vagrant up
    Bringing machine 'default' up with 'virtualbox' provider...
    ==> default: Clearing any previously set forwarded ports...
    ==> default: Clearing any previously set network interfaces...
    ==> default: Preparing network interfaces based on configuration...
      default: Adapter 1: nat
      default: Adapter 2: hostonly
    ==> default: Forwarding ports...
      default: 80 (guest) => 8080 (host) (adapter 1)
      default: 22 (guest) => 2222 (host) (adapter 1)
    ==> default: Booting VM...
    ==> default: Waiting for machine to boot. This may take a few minutes...
      default: SSH address: 127.0.0.1:2222
      default: SSH username: vagrant
      default: SSH auth method: private key
      default: Warning: Connection reset. Retrying...
      default: Warning: Connection aborted. Retrying...
      default: Warning: Connection reset. Retrying...
      default: Warning: Remote connection disconnect. Retrying...
    ==> default: Machine booted and ready!
    [default] GuestAdditions versions on your host (5.1.26) and guest (4.3.6) do not matc
    Loaded plugins: fastestmirror
    Setting up Install Process
    Loading mirror speeds from cached hostfile
     * base: mirrors.btte.net
     * epel: repo.fedoralinux.ir
     * extras: mirrors.btte.net
     * updates: mirrors.btte.net
    No package kernel-devel-2.6.32-358.23.2.el6.x86_64 available.
    Package gcc-4.4.7-18.el6.x86_64 already installed and latest version
    Package binutils-2.20.51.0.2-5.47.el6_9.1.x86_64 already installed and latest version
    Package 1:make-3.81-23.el6.x86_64 already installed and latest version
    Package 4:perl-5.10.1-144.el6.x86_64 already installed and latest version
    Package bzip2-1.0.5-7.el6_0.x86_64 already installed and latest version
    Nothing to do
    Copy iso file C:\Program Files\Oracle\VirtualBox\VBoxGuestAdditions.iso into the box
    Installing Virtualbox Guest Additions 5.1.26 - guest version is 4.3.6
    Verifying archive integrity... All good.
    Uncompressing VirtualBox 5.1.26 Guest Additions for Linux...........
    VirtualBox Guest Additions installer
    Removing installed version 5.1.26 of VirtualBox Guest Additions...
    vboxadd.sh: Stopping VirtualBox Additions.
    Copying additional installer modules ...
    Installing additional modules ...
    vboxadd.sh: Starting the VirtualBox Guest Additions.
    Failed to set up service vboxadd, please check the log file
    /var/log/VBoxGuestAdditions.log for details.
    An error occurred during installation of VirtualBox Guest Additions 5.1.26. Some func
    In most cases it is OK that the "Window System drivers" installation failed.
    vboxadd.sh: Starting the VirtualBox Guest Additions.
    vboxadd.sh: failed: Look at /var/log/vboxadd-install.log to find out what went wrong.
    vboxadd.sh: failed: modprobe vboxguest failed.
    ==> default: Checking for guest additions in VM...
      default: The guest additions on this VM do not match the installed version of
      default: VirtualBox! In most cases this is fine, but in rare cases it can
      default: prevent things such as shared folders from working properly. If you see
      default: shared folder errors, please make sure the guest additions within the
      default: virtual machine match the version of VirtualBox you have installed on
      default: your host and reload your VM.
      default:
      default: Guest Additions Version: 4.3.6
      default: VirtualBox Version: 5.1
    ==> default: Configuring and enabling network interfaces...
      default: SSH address: 127.0.0.1:2222
      default: SSH username: vagrant
      default: SSH auth method: private key
    ==> default: Mounting shared folders...
      default: /home/rc => D:/project/vagrant/centos64php56
    Vagrant was unable to mount VirtualBox shared folders. This is usually
    because the filesystem "vboxsf" is not available. This filesystem is
    made available via the VirtualBox Guest Additions and kernel module.
    Please verify that these guest additions are properly installed in the
    guest. This is not a bug in Vagrant and is usually caused by a faulty
    Vagrant box. For context, the command attempted was:
    
    mount -t vboxsf -o uid=500,gid=500 home_rc_ /home/rc
    
    The error output from the command was:
    
    /sbin/mount.vboxsf: mounting failed with the error: No such device
    

    这里提示虚拟机里CentOS的VBoxGuestAdditions与VirtualBox的版本匹配,需要升级:

    [default] GuestAdditions versions on your host (5.1.26) and guest (4.3.6) do not matc
    

    应该是升级了VirtualBox导致的。然而自动安装新插件失败:

    vboxadd.sh: Starting the VirtualBox Guest Additions.
    Failed to set up service vboxadd, please check the log file
    /var/log/VBoxGuestAdditions.log for details.
    An error occurred during installation of VirtualBox Guest Additions 5.1.26. Some func
    In most cases it is OK that the "Window System drivers" installation failed.
    vboxadd.sh: Starting the VirtualBox Guest Additions.
    vboxadd.sh: failed: Look at /var/log/vboxadd-install.log to find out what went wrong.
    vboxadd.sh: failed: modprobe vboxguest failed.
    

    导致共享目录无法映射。但是虚拟机仍然是启动成功的,可以ssh进去,或者使用sftp挂载。
    Google了下有说是VirtualBox bug的,也有说是Windows问题的,各种折腾不能解决。升级插件也无效:vagrant plugin install vagrant-vbguest。重新安装VirtualBox和Vagrant,并不会影响现有虚拟机及网络配置,但不能解决问题。
    直到看到这篇文章,决定从CentOS入手解决。
    启动虚拟机后,使用sftp上传VBoxGuestAdditions.iso,ssh进入手动安装:

    $ sudo
    $ mount VBoxGuestAdditions.iso -o loop /mnt
    $ cd /mnt
    $ sh VBoxLinuxAdditions.run
    

    安装失败,查看日志:

    Building the main Guest Additions module                   [FAILED]
    (Look at /var/log/vboxadd-install.log to find out what went wrong)
    

    vboxadd-install.log日志:

    /tmp/vbox.0/Makefile.include.header:97: *** Error: unable to find the sources of your current Linux kernel. Specify KERN_DIR=<directory> and run Make again.  Stop.
    

    参照这篇文章,查看kernel版本

    $ rpm -qa kernel\* | sort
    kernel-2.6.32-358.23.2.el6.x86_64
    kernel-devel-2.6.32-696.10.2.el6.x86_64
    kernel-firmware-2.6.32-358.10.2.el6.noarch
    kernel-headers-2.6.32-696.10.2.el6.x86_64
    
    $ uname -r
    2.6.32-358.10.2.el6.x86_64
    

    其实一开始是更多版本不匹配的,尝试更新kernel:

    yum update
    yum install kernel-headers kernel-devel
    

    结果部分更新失败:

    Warning: No matches found for: kernel-devel
    No Matches found
    

    参照这里解除版本锁定,设置enabled = 0:

    $ vim /etc/yum/pluginconf.d/versionlock.conf
    

    再次运行升级kernel就可以了。安装成功后,重启后,再次运行sh VBoxLinuxAdditions.run 就可以了。事实上Vagrant启动时就会自动安装VBoxGuestAdditions:

    [default] GuestAdditions 5.1.26 running --- OK.
    

    Vagrant升级到后发现vagrant up初始化下载box卡住了,那是你的vagrant版本太高与对应的powershell版本对应不上,可以下载最新的powershell安装即可。
    如果box下载很慢,可以参照这里的方法自我映射取得url单独下载:

    https://app.vagrantup.com/box-cutter/boxes/centos73
    ==> https://atlas.hashicorp.com/box-cutter/boxes/centos73/versions/2.0.21/providers/virtualbox.box
    

    参考链接:
    Guest Additions Version error on VirtualBox5
    VirtualBox下挂载共享文件目录问题处理
    Resolving GuestAdditions version mismatch in vagrant/homestead vm (failed to mount shared folders / modprobe vboxsf failed)
    Problem installing virtualBox guest additions
    Warning: No matches found for: kernel-devel
    Vagrant up hangs forever on Windows 7, Vagrant 1.9.7, VirtualBox 5.1.22.r11512

    TCP UDP探索

    最近开发新接口,为实现更高的性能,深入考察了下TCP,UDP通信协议。之前的开发当中都是基于HTTP的接口开发,简单易用,各个编程语言都支持;web服务器、中间件成熟,方便扩展;支持安全加密等等。与自定义Socket通信相比,HTTP仍然不够高效。

    TCP协议,也叫传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。每建立一个TCP连接需要三次握手,关闭时又需要四次握手;传输过程当中,每发送一个包,都要接收一个确认回复,如果在一定时间内未收到,则重发,如果是同一块数据分包传输,则回复所需要的包序号;在发送和接收时都要计算校验和。由于有严格的数据包传输顺序和校验,因为接收到的数据将会是顺序。

    UDP协议,也叫用户数据报协议(User Datagram Protocol)是一个简单的面向数据报的传输层协议。它只管数据包发送,并不没有TCP的连接控制、回复确认,并不知道对方是否接收成功。因为并不等待接收方回复,发送端可以连续发送数据,接收端接收到数据并不一定是顺序的。因此相比TCP,UDP的有更高的传输效率,更低的可靠性。如果需要可靠性,则需要发送确认包,类似TCP那样子。

    通常HTTP协议基于TCP实现,保证收到的内容是完整、顺序的。在HTTP 1.0时,每一次请求都需要建立TCP连接,关闭TCP连接;在HTTP 1.1时,同一TCP连接则可以在活跃(Keep-Live)间期内复用。HTTP是无连接的,每次处理完成请求即断开连接,一方面可以节省服务器资源,另一方面却浪费连接时间。如果需要维持客户端的Keep-Alive状态,则又需要消耗更多服务器资源,需要权衡。

    WebSocket是基于TCP 连接上进行全双工通讯的协议,允许服务端直接向浏览器发送消息,不再需要ajax轮询。它也是在TCP上面的实现,使用类似HTTP请求的握手协议,进行TCP通信切换。WebRTC,也叫Web即时通信(Web Real-Time Communication),则是一个支持浏览器之间进行实时语音对话或视频对话的API。它是建立于TCP/UDP之上的点对点通信,客户端基于上层RTCDataChannel进行通信而非底层TCP/UDP。

    Google推出了基于UDP的QUIC协议(快速UDP网络连接)实现的HTTP传输,可以快速在两个端点间创建连接,支持多路复用连接。对于丢包问题,则采用冗余传输,能够根据接收到的包,对缺失的包进行恢复(类似RAID)。对于一个TCP连接,需要四个参数:源IP/端口,目的IP/端口,任何一个改变都需要重新建立连接。QUIC则不需要这些,因为UDP并不需要来源信;它使用UUID来标识一个连接,只要这个UUID在,那么这个连接会话就能够继续(与HTTP会话有点像)。因此在不同网络,特别是移动网络之间的切换,都将保持稳定(参考:Mosh)。目前Google的网站已经支持,并可以在Chrome浏览器的里面查看连接情况:chrome://net-internals/#quic。

    通常说TCP是可靠连接,可用于数据流,复杂网络情况;UDP由于缺少确认机制等,属不可靠连接,通常应用于网络情况良好(内部)或者允许丢包的场景。事实上UDP也应用于游戏,VoIP等需要高效传输的场景。当网络状况糟糕时,使用TCP通信反而可能更差,因为需要更多步骤建立连接和确认。参考这里:QQ 为什么采用 UDP 协议,而不采用 TCP 协议实现?。TCP连接是有状态的,服务端需要去维持,及TIME_WAIT问题。如果连接一段时间没用,TCP并不能感知连接是否仍然有效,需要自定义心跳包,维持连接。UDP则是无连接状态的,只要知道目标IP及端口即可以发送。

    UDP通信简单许多,不需要socket_connect和socket_connect,主动暴露一端(服务端)需要socket_bind。例如,服务端:

    <?php
    $socket = stream_socket_server("udp://127.0.0.1:1113", $errno, $errstr, STREAM_SERVER_BIND);
    if (!$socket) {
        die("$errstr ($errno)");
    }
    
    do {
        $pkt = stream_socket_recvfrom($socket, 1024, 0, $peer);
        echo "$peer,$pkt\n";
        //客户端互相通信时,注释下面一行
        stream_socket_sendto($socket, date("D M j H:i:s Y\r\n"), 0, $peer);
    } while ($pkt !== false);
    
    

    客户端:

    <?php
    $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
    
    $msg = "Ping !";
    $len = strlen($msg);
    
    socket_sendto($sock, $msg, $len, 0, '127.0.0.1', 1113);
    
    //sleep(30)
    $res = socket_recv( $sock , $reply , 2045 , MSG_WAITALL);
    echo $reply;
    
    socket_close($sock);
    
    

    服务端的代码里面有个stream_socket_sendto(或者socket_sendto),向客户端发送消息。UDP并不需要向客户端回复消息,这并像TCP在同一个连接里面回复,而是另一次UDP通信罢了。实际上,两个客户端之间就能够互相通信(分别使用不同端口),任何能够拿到对方IP和端口,都能够向其发送消息。复制客户端代码,更改目标IP和端口为服务端打印出来的信息,然后运行,原先的客户端也将收到消息。利用中间服务器交换两个客户端的IP和端口,以便直接通信,是NAT穿透常用的方法。

    综上,使用TCP可以保证数据可靠传输,复用连接,比如请求-应答模型;使用UDP则可以更高效发送数据,但能容忍部分丢包的场景,比如视频画面;或者只发送不需响应的数据,比如日志;或者无需连接状态的场景(类似HTTP会话),减少服务器压力和客户端等待。当然,也可以在UDP基础上自定义传输规则,实现譬如Mosh或则TCP那样的应用。选择TCP或者UDP,应该综合考虑使用场景,网络,数据包大小,效率,安全等,而不是盲从字面上的“面向可靠的”或者“不可靠的”或者“高效的”。

    参考链接:
    如何理解HTTP协议的 “无连接,无状态” 特点?
    Google’s QUIC protocol: moving the web from TCP to UDP(Google QUIC协议:从TCP到UDP的Web平台
    如何看待谷歌 Google 打算用 QUIC 协议替代 TCP/UDP? – 回答作者: Trotyl Yu
    How Mosh works
    使用TCP协议的NAT穿透技术
    PHP Socket通信
    日志系统设计
    可靠 UDP 传输
    DDOS(拒绝服务攻击)
    php使用socket感悟–tcp和udp
    UDP socket programming in php
    Does WebRTC use TCP or UDP?

    日志系统设计

    日志系统是项目开发/运维当中非常重要的一部分,提供产品使用情况,跟踪调试信息等。以前都是各个项目各自实现日志记录,比如PHP、JAVA各自实现一套,如果要跨项目/服务器进行查询/跟踪/统计,则比较麻烦,比如Web后台–>业务模块–>基础组件,客户端–>公共接口–>业务模块等等。
    目前的项目都是将日志写入本地,首先需要定义统一的规范:

    格式
    DateTime | ServerIP | ClientIP | PID | RequestID|  Type | Level | Message|  Code
    
    解释
    DateTime:记录日志的当前时间戳
    ServerIP:当前服务器IP
    ClientIP:客户端IP
    PID:进程ID
    RequestID:交易号,即请求的唯一标识,使用uniqid(+UserCode)或UUID或,一次请求会有多条日志,,用于关联本次请求内的相关日志
    Type:日志类型,比如统计,操作(审计),业务等
    Level:日志等级
    Message:日志内容,当为值为数组或者对象时转为JSON
    Type为RUNTIME时,表示运行日志,属性:自定义
    Type为HTTP时,表示来源请求,属性:Url,Method(Get/Post),Params,RemoteIP,UserAgent,ReferUrl[,Action,Method]
    Type为REST时,表示外部调用,属性:Type(Http/Https),Url,Port,RequestParams,Response,RunTime;
    Type为SQL时,表示SQL执行,属性:Sql,RunTime
    Code:标识码,记录错误码、响应码、统计码、版本、自定义信息,方便统计
    

    这里的RequestID由入口处自动产生,用于标识一次请求,关联所产生的所有日志,需要在各个项目之间传递。为了减少日志,Level通常为Info级别,避免产生过多日志。为了方便调试追踪,RequestID和Level也可以在其他参数中指明,比如HTTP头里面附加。
    然后是日志收集:客户端收走日志系统,发送给日志系统服务端。
    然后分析处理呈现:服务端将接收到的日志,发给处理其他组件分析处理,提供Web界面的查询系统。研发人员,可以错误信息,定位问题;获悉程序运行情况进行调优;大数据分析日志,得出产品使用情况;运维平台则可以进行业务报警。

    日志产生由个语言依照规范自行实现,收集、保持则由FlumeKafka实现。Flume是一个分布式的日志收集、合并、移动系统,能够监控文件变化,将变化部分传输出去。Kafka是一个分布式的发布/订阅的消息流平台,包括Broker,Consumer,Producer都支持分布式,依赖Zookeeper实现。

    在PHP上面的实现,一开始使用log4php,看起来很美好,但是性能很差,对业务影响较大。决定再次简化,砍掉不必要的东西(Socket,邮件等等),在C语言开发的PHP日志扩展SeasLog基础上在做开发,将日志文件保存在本地。为了减少日志所占用内存,每超过一定的大小的日志即进行保存,否则在最后进行保存,利用了Nginx的fastcgi_finish_request特性。生产上发现,每天产生的文件日志太大了,需要控制日志信息大小、等级,并及时清理。
    对于Web后台,还结合FirePHP,将日志直接输出到浏览器,方便边运行变调试。

    参考链接:
    最佳日志实践
    Optimal Logging
    Flume+Kafka收集Docker容器内分布式日志应用实践
    基于Flume的美团日志收集系统(一)架构和设计
    Twitter是如何构建高性能分布式日志的
    开源日志系统比较
    Kafka剖析(一):Kafka背景及架构介绍
    基于Flume的野狗实时日志系统的演进和优化
    EVODelavega/phpkafka
    有赞统一日志平台初探
    RabbitMQ和kafka从几个角度简单的对比
    Flume-ng的原理和使用
    利用flume+kafka+storm+mysql构建大数据实时系统

    HTTP文件分块上传下载

    在开发中经常需要上传下载文件,涉及web页面,手机应用,线下服务器等,文件传输方式有HTTP,FTP,BT。。于是基于HTTP开发了统一文件上传下载接口。
    由于上传涉及多端,网络状况复杂,需要能够支持断点续传,因此需要对文件进行分块和校验。
    请求上传:

    • chunk:当前块编号
    • chunks:文件分块总数
    • md5:整个文件md5
    • content:文件块内容
    • size:文件块大小

    服务端返回:

    • statu:状态,0失败,非0成功
    • chunk:需要上传的块编号
    • message:操作信息

    服务端有几种情况

    • 1. 上传成功,md5已经存在,说明文件已存在,则返回上传成功,并结束上传,类似秒传
    • 2. 上传成功,md5已经不存在,但当前文件块已经存在,说明该文件已经上传过一部分,则返回成功,并给出需要开始上传的块号,类似断点续传
    • 3. 上传成功,文件块不存在,当前块号小于总块数减一,则返回上传成功,并给出下一块块号
    • 4. 上传成功,文件块不存在,当前块号等于总块数减一,进行合并,校验成功,则返回上传成功,并结束上传
    • 5. 上传成功,文件块不存在,当前块号等于总块数减一,进行合并,校验成功,则返回上传失败,从第一块重新开始上传
    • 6. 上传失败,服务端返回失败,则根据错误信息重传
    • 7. 上传失败,服务端没有返回,则重传当前块

    客户端依据自定义的大小对文件进行切割,每次传递一个块,当服务端接收到当前块号为总块数减一则认为全部上传完毕,进行文件块合并,清除临时文件块,计算MD5,如果MD5与传递过来的相等则认为上传成功,否则失败,要求客户端从第一块重传。
    这个是基于客户端顺序上传的,假如是并发上传呢?那么就需要在每个分块上传结束后触发合并,需要借助锁来管理。
    又拍云表单分块上传则分为3步骤:

    • 1.初始化,上报文件信息
    • 2.上传分块
    • 3.上传结束,触发合并

    百度的WebUploader则支持浏览器向服务端的断点续传,利用HTML5或Flash对文件进行分块,计算MD5。
    HTTP分块下载,也就是断点续传下载,是根据HTTP1.1协议(RFC2616)中定义的HTTP头 Range和Content-Range字段来控制的:

    • 1. 客户端在HTTP请求头里面指明Range,即开始下载位置
    • 2. 服务端在HTTP响应头里面返回Content-Range,告知下载其实点和范围

    服务端可以将文件MD5等加入Etag或自定义Header字段里面,HTTP客户端便可以分开请求数据,最后合并;否则永远都是从头开始。其实也可以把大文件切成多个小文件,再一个个下载回来合并。
    有些web服务器直接支持文件上传和下载的断点续传,比如Nginx。

    刚才的文件分块传输过程,有点像TCP通信,也有建立连接,分片,校验,重发,断开连接。
    而这个将大任务分解为多个小任务进行处理的思想,即准备–>循环(并行)处理–>结束,可以应用到很多项目里面,比如大数据,爬虫。为Web管理系统写了jQuery插件用来处理的耗时任务,将一个耗时任务分解为多个循环请求,避免超时

    • 1. prepare:服务端返任务标示ID,总步骤,及附加参数
    • 2. process:根据返回参数进行循环请求
    • 3. complete:循环完成,触发结束

    后面又延伸出另外一个插件,支持任意顺序步骤的处理。

    参考链接:
    聊聊大文件上传
    md5 javascript
    HTTP协议--断点续传
    http断点续传的秘密
    nginx-upload-module模块实现文件断点续传
    通过curl测试服务器是否支持断点续传

    jQuery 插件开发

    最近开发一个前端图表模块,使用jQuery和Highchart开发,然后提供给其他人用,发现调用时要写太多代码,能不能一行搞定。之前开发过几个jQuery扩展,于是也做成jQuery扩展,调用时类似$(‘#chart’).warning(options)这样子,简洁了不少。
    jQuery扩展有两种,一种是$(‘#element’).func(),使用了选择器,定义是:

    (function($) {
        //Plugin fucntions
        $.fn.PluginName = function(options) {
            // Plugin initial
        }
    })(jQuery);
    

    将PluginName这个插件注册到$.fn上面,另外一种$.func(options),不绑定元素的,定义是:

    (function($) {
        //Plugin fucntions
        $.PluginName = function(options) {
            // Plugin initial
        }
    })(jQuery);
    

    将PluginName直接挂在了$对象下面。
    jQeury.extend方法可以用来扩展对象,也可以用来增加插件:

    //$('#el').PluginName方式
    jQuery.fn.extend({
        PluginName: function(options) {
            // Plugin initial
        }
    });
    
    
    //对原有的PluginName插件再进一步扩展,也可用于升级插件而不修改原有代码
    jQuery.fn.PluginName.extend({
        doSomething: function(options) {
            // Plugin extend
        }
    });
    
    jQuery.extend(true, jQuery.fn.PluginName.prototype, {});
    
    //$.PluginName方式
    jQuery.extend({
        PluginName: function(options) {
            // Plugin initial
        }
    });
    
    jQuery.PluginName.extend({
        doSomething: function(options) {
            // Plugin extend
        }
    });
    

    以下是一个简单的jQuery插件模板,来自这里

    /*
     *  Project: 
     *  Description: 
     *  Author: 
     *  License: 
     */
    
    // the semi-colon before function invocation is a safety net against concatenated
    // scripts and/or other plugins which may not be closed properly.
    ;(function ( $, window, document, undefined ) {
    
        // undefined is used here as the undefined global variable in ECMAScript 3 is
        // mutable (ie. it can be changed by someone else). undefined isn't really being
        // passed in so we can ensure the value of it is truly undefined. In ES5, undefined
        // can no longer be modified.
    
        // window is passed through as local variable rather than global
        // as this (slightly) quickens the resolution process and can be more efficiently
        // minified (especially when both are regularly referenced in your plugin).
    
        // Create the defaults once
        var pluginName = 'defaultPluginName',
            defaults = {
                propertyName: "value"
            };
    
        // The actual plugin constructor
        function Plugin( element, options ) {
            this.element = element;
    
            // jQuery has an extend method which merges the contents of two or
            // more objects, storing the result in the first object. The first object
            // is generally empty as we don't want to alter the default options for
            // future instances of the plugin
            this.options = $.extend( {}, defaults, options) ;
    
            this._defaults = defaults;
            this._name = pluginName;
    
            this.init();
        }
    
        Plugin.prototype.init = function () {
            // Place initialization logic here
            // You already have access to the DOM element and the options via the instance,
            // e.g., this.element and this.options
        };
    
        // You don't need to change something below:
        // A really lightweight plugin wrapper around the constructor,
        // preventing against multiple instantiations and allowing any
        // public function (ie. a function whose name doesn't start
        // with an underscore) to be called via the jQuery plugin,
        // e.g. $(element).defaultPluginName('functionName', arg1, arg2)
        $.fn[pluginName] = function ( options ) {
            var args = arguments;
    
            // Is the first parameter an object (options), or was omitted,
            // instantiate a new instance of the plugin.
            if (options === undefined || typeof options === 'object') {
                return this.each(function () {
    
                    // Only allow the plugin to be instantiated once,
                    // so we check that the element has no plugin instantiation yet
                    if (!$.data(this, 'plugin_' + pluginName)) {
    
                        // if it has no instance, create a new one,
                        // pass options to our plugin constructor,
                        // and store the plugin instance
                        // in the elements jQuery data object.
                        $.data(this, 'plugin_' + pluginName, new Plugin( this, options ));
                    }
                });
    
            // If the first parameter is a string and it doesn't start
            // with an underscore or "contains" the `init`-function,
            // treat this as a call to a public method.
            } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') {
    
                // Cache the method call
                // to make it possible
                // to return a value
                var returns;
    
                this.each(function () {
                    var instance = $.data(this, 'plugin_' + pluginName);
    
                    // Tests that there's already a plugin-instance
                    // and checks that the requested public method exists
                    if (instance instanceof Plugin && typeof instance[options] === 'function') {
    
                        // Call the method of our plugin instance,
                        // and pass it the supplied arguments.
                        returns = instance[options].apply( instance, Array.prototype.slice.call( args, 1 ) );
                    }
    
                    // Allow instances to be destroyed via the 'destroy' method
                    if (options === 'destroy') {
                      $.data(this, 'plugin_' + pluginName, null);
                    }
                });
    
                // If the earlier cached method
                // gives a value back return the value,
                // otherwise return this to preserve chainability.
                return returns !== undefined ? returns : this;
            }
        };
    
    }(jQuery, window, document));
    

    根据这个就可以方便的开发jQuery插件了。
    也可以使用jQuery.widget来开发一个插件,参考这里

    //新插件
    $.widget( "custom.superDialog", {} );
    //继承原有jQuery UI插件
    $.widget( "custom.superDialog", $.ui.dialog, {} );
    

    除此之外,也可以自己写插件/模块:

    (function () {
    	// 将插件/模块定义在闭包私有空间里,加载时候就会立即执行,初始化
    	this.PluginName = function(options) {
    	}
    	PluginName.prototype.doSomething = function(options) {
    	}
    }());
    

    然后就可以调用了

    var plugin = new PluginName(options);
    plugin.doSomething();
    

    也可以这样子:

    var MODULE = (function (my) {
    	my.moduleMethod = function () {
    		// method override, has access to old through old_moduleMethod...
    	};
    	return my;
    }(MODULE || {}));
    
    #子模块,挂在MODULE这个对象/命名空间下面
    MODULE.sub = (function () {
    	var my = {};
    	// ...
    
    	return my;
    }());
    

    模块多了就需要模块管理工具了,比如RequireJS

    //定义
    define('myModule', 
        ['foo', 'bar'], 
        // module definition function
        // dependencies (foo and bar) are mapped to function parameters
        function ( foo, bar ) {
            // return a value that defines the module export
            // (i.e the functionality we want to expose for consumption)
        
            // create your module here
            var myModule = {
                doStuff:function(){
                    console.log('Yay! Stuff');
                }
            }
     
            return myModule;
    });
    #使用
    require(['app/myModule'], 
        function( myModule ){
            // start the main module which in-turn
            // loads other modules
            var module = new myModule();
            module.doStuff();
    });
    

    Javascript模块话加载可以参考这里

    参考链接:
    JavaScript Module Pattern: In-Depth
    Advanced Plugin Concepts
    Learning JavaScript Design Patterns
    jQuery Plugin Pattern
    理解jquery的$.extend()、$.fn和$.fn.extend()
    Making Use of jQuery UI’s Widget Factory
    Building Your Own JavaScript Modal Plugin
    Writing Modular JavaScript With AMD, CommonJS & ES Harmony
    Javascript文件加载:LABjs和RequireJS
    jQuery Boilerplate——流行的jQuery插件开发模板

    在Raspberry Pi上面运行Docker

    公司有个项目是运行在Raspberry Pi上面,这个项目涉及到Perl,Python,Java Applet,并且未来Pi的数量可能达到上千个。最近碰到了一样的代码遵循一样的部署脚本,在不同的树莓派上运行竟然到不同的结果。于是也想将Docker应用到树莓派上面去,使用docker主要优点:
    1 消除环境差异,程序运行所依赖的环境已经一起打包,随身携带了
    2 简化配置,只有docker相关配置,不再需要每个语言单独配置
    3 重复利用
    4 方便升级,之前有差异时,是重写镜像。。
    5 方便管理,可以使用docker已有的工具来管理
    6 性能接近原生应用

    Raspberry Pi在jessie这个版本就已经支持了docker,检查版本:

    $ lsb_release -da
    No LSB modules are available.
    Distributor ID: Raspbian
    Description:    Raspbian GNU/Linux 8.0 (jessie)
    Release:    8.0
    Codename:  jessie
    

    如果你的版本已经支持,直接执行以下命令就可以了:

    curl -sSL https://get.docker.com | sh
    

    如果你的版本不支持,你需要升级系统或重新刷镜像,可以采用hypriot

    #下载
    wget https://downloads.hypriot.com/hypriotos-rpi-v1.0.0.img.zip
    #解压
    unzip hypriotos-rpi-v1.0.0.img.zip
    #查找 SD card ID
    lsblk
    #查找类似/dev/mmcblk0
    
    #弹出SD卡
    umount /run/media/mac/8734-1E4C
    #将镜像写入 SD card
    sudo dd if=hypriotos-rpi-v1.0.0.img of=/dev/mmcblk0 bs=1m
    
    

    然后启动,默认的用户名/密码是pirate/hypriot。
    当然也可以采用Debian或者是Alpine Linux定制(Raspberry Pi仅支持 arm版本的系统,并且只能在arm平台上构建,或者这个)。
    运行测试一下:

    docker run -d -p 80:80 hypriot/rpi-busybox-httpd
    

    访问以下树莓派的IP,就会看到一个非常神器的网页,而这个容器仅仅几M。在这里可以看到基于hypriot的好几个镜像,也可以搜索其他的raspberry pi镜像。
    现在我们使用hypriot/rpi-python来构建一个使用Python Selenium测试带有Java Applet的网页:

    docker run -idt -P --name web hypriot/rpi-python
    docker attach web
    

    进入到容器里面,执行以下命令:

    sudo apt-get update
    #安装openjdk
    sudo apt-get install openjdk-7-jdk
    #安装Firefox
    sudo apt-get install iceweasel
    #安装Java Applet插件
    sudo apt-get install icedtea-7-plugin
    #安装虚拟桌面
    sudo apt-get install xvfb
    #安装Python包管理工具
    sudo apt-get install python-pip
    #安装selenium
    sudo pip install selenium
    #安装Python虚拟桌面库
    sudo pip install pyvirtualdisplay
    

    创建测试test.py:

    from selenium import webdriver
    from selenium.webdriver.common.keys import Keys
    import time
    from pyvirtualdisplay import Display
    from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
    from selenium.webdriver.firefox.firefox_binary import FirefoxBinary
    display = Display(visible=0, size=(800, 600))
    display.start()
    caps = DesiredCapabilities.FIREFOX
    caps["marionette"] = False
    #caps["binary"] = "/usr/bin/firefox"
    firefox_binary = FirefoxBinary("/usr/bin/firefox")
    fp = webdriver.FirefoxProfile()
    fp.set_preference("security.enable_java", True )
    fp.set_preference("plugin.state.java", 2)
    driver = webdriver.Firefox(firefox_profile=fp,firefox_binary=firefox_binary)
    driver.maximize_window()
    driver.get("http://businessconnect.telus.com/bandwidth/en_us/index.html?voip=on&voiplines=1&testlength=15&codec=g711&_=1470641590&startTest=on")
    time.sleep(120)
    mos = driver.find_element_by_class_name("sectiontitle18").find_element_by_tag_name("span").get_attribute("innerHTML")
    print mos
    driver.close()
    display.stop()
    

    运行测试用例:

    python test.py
    

    如果仅仅是这样子,是行不通的,Java Applet的安全询问弹窗超出了浏览器范围是点击不到的,需要更为强大的组件来做,比如。
    这里采用取巧的方法,首先把pyvirtualdisplay相关代码注释掉,在界面上观察具体的执行效果,并点击安全询问,运行通过。然后取消注释,再次运行,就可以了。最后将运行通过的配置文件/root/.icedtea目录拷贝出来,放到其他的树莓派里面就可以了。
    如果碰到报错”selenium.common.exceptions.WebDriverException: Can’t load the profile”,通常是说Firefox或selenium版本太低,如果检查都不是,需要修改WebDriver的超时时间

    nano /usr/local/lib/python2.7/dist-packages/selenium/webdriver/firefox/webdriver.py
    

    timeout从30秒改为120秒:

    class WebDriver(RemoteWebDriver):
    
    
        # There is no native event support on Mac
        NATIVE_EVENTS_ALLOWED = sys.platform != "darwin"
    
    
        def __init__(self, firefox_profile=None, firefox_binary=None, timeout=120,
    

    修改为容器后,提交变更:

    root@black-pearl:/home/pirate# docker ps -l
    CONTAINER ID        IMAGE                COMMAND            CREATED            STATUS              PORTS              NAMES
    04762770df2d        hypriot/rpi-python  "bash"              2 days ago          Up 2 days                              web
    root@black-pearl:/home/pirate# docker commit 04762770df2d hypriot/rpi-python
    root@black-pearl:/home/pirate# docker rm web
    

    运行新镜像

    #docker run -idt -P --name web hypriot/rpi-python
    #映射程序目录和配置文件进去
    docker run -idt -P --name web -v /home/pi:/home/pi -v /home/pi/.icedtea:/root/.icedtea hypriot/rpi-python
    

    程序正常运行后,可以将这个镜像打包:

    docker save -o hypriot.tar hypriot/rpi-python:latest
    

    分发给其他树莓派运行:

    docker load --input hypriot.tar
    docker images
    docker run -idt -P --name web -v /home/pi:/home/pi -v /home/pi/.icedtea:/root/.icedtea hypriot/rpi-python
    

    这样子便可以利用docker工具来管理这个应用程序,比起原本复杂的环境配置要简单很多。

    在查找Pi可用镜像的时候,顺便发现了一个IOT(internet of things)管理平台:resin

    参考链接:
    add support to install Docker on raspbian/jessie
    Getting started with Docker on your Raspberry Pi
    How to get Docker running on your Raspberry Pi using Linux
    Getting selenium python to work on Raspberry Pi Model B

    使用Docker Compose管理Docker容器

    上一篇创建了一个PHP的运行环境,现在需要一个MySQL服务,直接运行:

    root@thinkpad:~# docker run --name rocket-mysql -v /home/rocketfish/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7
    

    就自动下载并得到一个mysql版本为5.7的服务镜像。
    这时候,要运行一个Web服务我们需要运行两次的docker run才可以,如果还有更多的Web容器或其他的服务容器呢?
    Docker官方提倡一个容器仅提供一个服务,多个服务/容器可以使用docker-compose来管理。
    docker-compose本身是一个Python写的工具,可以直接通过pip安装:

    root@thinkpad:~# sudo pip install --upgrade pip
    

    如果你本地并没有Python环境,也可以采用docker-compose的docker镜像来运行:

    root@thinkpad:/home/compose-web# curl -L https://github.com/docker/compose/releases/download/1.8.0/run.sh > /usr/local/bin/docker-compose
    root@thinkpad:/home/compose-web# docker-compose --version
    #开始下载镜像,建议用第一种
    

    查看帮助

    root@thinkpad:~# docker-compose -h
    Define and run multi-container applications with Docker.
    
    Usage:
      docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]
      docker-compose -h|--help
    
    Options:
      -f, --file FILE             Specify an alternate compose file (default: docker-compose.yml)
      -p, --project-name NAME     Specify an alternate project name (default: directory name)
      --verbose                   Show more output
      -v, --version               Print version and exit
      -H, --host HOST             Daemon socket to connect to
    
      --tls                       Use TLS; implied by --tlsverify
      --tlscacert CA_PATH         Trust certs signed only by this CA
      --tlscert CLIENT_CERT_PATH  Path to TLS certificate file
      --tlskey TLS_KEY_PATH       Path to TLS key file
      --tlsverify                 Use TLS and verify the remote
      --skip-hostname-check       Don't check the daemon's hostname against the name specified
                                  in the client certificate (for example if your docker host
                                  is an IP address)
    
    Commands:
      build              Build or rebuild services
      bundle             Generate a Docker bundle from the Compose file
      config             Validate and view the compose file
      create             Create services
      down               Stop and remove containers, networks, images, and volumes
      events             Receive real time events from containers
      exec               Execute a command in a running container
      help               Get help on a command
      kill               Kill containers
      logs               View output from containers
      pause              Pause services
      port               Print the public port for a port binding
      ps                 List containers
      pull               Pulls service images
      push               Push service images
      restart            Restart services
      rm                 Remove stopped containers
      run                Run a one-off command
      scale              Set number of containers for a service
      start              Start services
      stop               Stop services
      unpause            Unpause services
      up                 Create and start containers
      version            Show the Docker-Compose version information
    

    这些命令包括了创建/启动/停止/暂停/继续运行容器。
    首先要创建docker-compose.yml,这是一个YAML格式的文档

    mkdir docker
    cd docker
    mkdir web
    mkdir db
    vim docker-compose.yml
    

    内容如下:

    version: '2'
    services:
      db:
        image: mysql:5.7
        ports:
        - "3306:3306"
        restart: always
        environment:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: web
          MYSQL_USER: root
          MYSQL_PASSWORD: rootweb
        volumes:
        - ./db/data:/var/lib/mysql
      web:
        depends_on:
          - db
        image: nginx-php-fpm:phalcon
        ports:
        - "80:80"
        restart: always
        environment:
          WEB_DB_HOST: db:3306
          WEB_DB_PASSWORD: root
        volumes:
        - ./web/html:/var/www/html/
        links:
        - db
    

    这里的environment定义了环境变量,比如host的MAC,就可以在docker容器的系统变量里面得到,各个编程语言都有获取系统环境变量的方法。这些变量也可以组织起来放在文件里面加载进去,参考这里
    这里的volumes定义了要映射进容器的文件或文件夹或者数据容器,可以参考这里。注意,多个容器共享同一个目录,会出现写冲突,比如MySQL,多个实例,数据目录需要分开。所以设计好你的程序,哪一部分是需要只读的(可以共享),哪一部分是需要写的;写的部分是否可以放到各自临时目录,或者其他公共服务里面。
    运行并查看

    root@thinkpad:/home/compose-web# docker-compose up -d
    Creating network "composeweb_default" with the default driver
    Creating composeweb_db_1
    Creating composeweb_web_1
    root@thinkpad:/home/compose-web# docker-compose ps
          Name                   Command             State              Ports            
    ------------------------------------------------------------------------------------
    composeweb_db_1    docker-entrypoint.sh mysqld   Up      0.0.0.0:3306->3306/tcp      
    composeweb_web_1   /start.sh                     Up      443/tcp, 0.0.0.0:80->80/tcp 
    #也可以使用原来的命令
    root@thinkpad:/home/compose-web# docker ps
    CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                         NAMES
    efbdaf257748        nginx-php-fpm:phalcon   "/start.sh"              13 seconds ago      Up 11 seconds       0.0.0.0:80->80/tcp, 443/tcp   composeweb_web_1
    a6935d20911e        mysql:5.7               "docker-entrypoint.sh"   14 seconds ago      Up 13 seconds       0.0.0.0:3306->3306/tcp        composeweb_db_1
    
    

    docker-compose up -d ,将会在后台启动并运行所有的容器。
    docker-compose仅仅提供了对多个docker服务的管理,仍然可以在这些容器上运行原来的docker命令。检查下web容器的IP:

    root@thinkpad:/home/compose-web# docker inspect composeweb_web_1 | grep IPAddress
                "SecondaryIPAddresses": null,
                "IPAddress": "",
                        "IPAddress": "172.18.0.3",
    

    然后访问:http://172.18.0.3/ 就可以看到web页面了。
    如果想要停止服务,可以用docker-compose stop:

    root@thinkpad:/home/compose-web# docker-compose stop db
    Stopping composeweb_db_1 ... done
    root@thinkpad:/home/compose-web# docker-compose stop web
    Stopping composeweb_web_1 ... done
    

    再次启动,得到相同名称的服务:

    root@thinkpad:/home/compose-web# docker-compose up -d
    Starting composeweb_db_1
    Starting composeweb_web_1
    

    各个容器的名称也可以在配置文件里面通过参数container_name来指定。
    docker-compose的配置还有很多其他的参数,可以参考这里。比如刚才我们通过docker inspect来查找容器IP,也可以配置成静态IP:

    version: '2'
    
    services:
      app:
        image: nginx-php-fpm:phalcon
        networks:
          app_net:
            ipv4_address: 172.18.0.10
            ipv6_address: 2001:3984:3989::10
    
    networks:
      app_net:
        driver: bridge
        driver_opts:
          com.docker.network.enable_ipv6: "true"
        ipam:
          driver: default
          config:
          - subnet: 172.18.0.0/24
            gateway: 172.18.0.1
          - subnet: 2001:3984:3989::/64
            gateway: 2001:3984:3989::1
    

    也可以自定义网络
    刚才我们的配置文件里面直接指定了镜像,也可以指定Dockerfile镜像构建,假如Dockerfile在web目录下面:

    build:
      context: ./web
    

    或者直接指明Dockerfile:

    build:
      context: .
      dockerfile: Dockerfile-alternate
    

    实际上,docker-compose的配置已经覆盖了Dockerfile的配置项,也可以直接用于构建多个docker服务,比如指定运行的命令

    command: [/bin/bash]
    

    这里的入口程序,如果不能持续运行的话,运行完成后,docker就会退出。所以如果你需要可以后台运行的docker容器,那么入口程序,就必须保持一个程序在前台运行,不退出,比如运行crontab的镜像,入口程序可以是:

    cron && bash
    

    cron这个程序是在后台运行的,如果不配合前台程序bash,容器执行玩cron就会退出了。其他运行crontab的容器,大都是强制一个程序在前台不退出,比如这个

    cron && tail -f /var/log/cron.log
    

    这里需要注意一下,通过docker设置的environment values在容器内的cli下面执行时取不到的,比如printenv,使用cron后台运行时取不到外部设置的变量,但是手动执行时又可以。这些外部环境变量只能在ENTRYPOINT或者CMD所执行的shell里面去设置,比如在CMD执行start.sh:

    printenv | grep -E "^HOST" > /root/env.conf && cron && bash
    

    Docker compose还可以跟Docker Swarm结合。

    参考链接:
    Install Docker Compose
    Quickstart: Docker Compose and WordPress
    YAML 模板文件
    Introduction to Docker Compose Tool for Multi-Container Applications
    Dockerfile基本结构
    How to Get Environment Variables Passed Through docker-compose to the Containers
    Access environment variables from crontab into a docker container
    How can I access Docker set Environment Variables From a Cron Job
    Dockerfile里指定执行命令用ENTRYPOING和用CMD有何不同?
    What is the difference between CMD and ENTRYPOINT in a Dockerfile?
    Docker difference between run, cmd, entrypoint commands