新包发布系统FPub技术调研

架构与术语

名称 说明
FPub Server 包发布系统,包括web服务,git仓库等
File Server 文件下载服务器,生成的包文件放到这服务器上,可与FPub Server部署在同一台机器上
Salt Master Salt控制主机
Salt Minion 需要部署软件的主机

控制主机的协议

目前主流通过ssh远程执行命令方式来操控Minion

框架 实现方案
tars ssh执行命令,无ssh key,直接写死user/pwd,Minion主机必须创建一样的用户
zhiyun ssh执行命令,使用ssh key,支持配置 salt
walle ssh执行命令,使用ssh key
gopub ssh执行命令,使用ssh key,可选ansible

ssh执行命令方式,我觉得一个很大的弊病是FPub Server将有所有主机ssh权限,而且ssh命令执行起来速度较慢,不太好做并行。

由于我们自身已经用了salt来管理所有机器,实在没有必要再使用另一套方案,所以决定直接调用 salt-api来实现minion主机的操控。

包文件分发

框架 实现方案
tars diff两个版本,打包增量文件,生成到FPub Server本地

Minion通过 rsync 命令从FPub Server拉取更新文件

zhiyun diff两个版本,打包增量文件,生成到FPub Server本地

Minion通过HTTP下载方式从FPub Server拉取更新文件

walle git checkout到一个版本。

Minion通过 rsync 同步文件

gopub 原理抄的walle。

增加了大文件通过BT协议传输

首先,通过diff两个版的文件不同,再打一个增加更新的包,再覆盖,这种方式非常不好,操作太复杂,很容易引起Bug,如tars中的很多软链接导致的问题。这种方法唯一有一个好处就是每次更新的文件比较小。

FPub将使用全量更新的方式,把复杂度降低,换来稳定性的提升。也就是包操作只有两种类型:

  1. 卸载包
  2. 安装包

版本升级、降级相当于: 卸载 + 安装 的组合。

也就是说每一次更新包,都是把之前的文件包整个删除再替换,干干净净。不过需要考虑到包文件已经被人修改过的情况,这时候如果贸然删除,可能导致别人的修改丢失掉,应该是发现有修改痕迹就马上报错,中断更新操作。如何判断安装的包是否被人修改过,下面再讲。

由于不使用ssh协议,所以也自然不能使用rsync来同步包文件,可以采用HTTP服务+BT的方式,长远考虑BT传输会有较大优势,是个不错方案,如果实现较复杂第一期可使用HTTP,后面再改成BT。

如果是HTTP方式,那么权限问题也需要值得考虑,因为所有机器都可以通过HTTP下载所有包文件,这样一来,我们在FPub Web上就算做了权限的限制,每个人都只能访问到自己权限内的包文件,也不严谨了,因为理论上他们可以通过HTTP下载到所有包。

BT传输可以避免这个问题,因为只有开始部署时,服务器才开始做种,Minion才能下载到文件,一旦传输完成,服务器就撤种,就再无法下载了。

文件分发流程示意如下图

使用BitTorrent分发软件包

优势对比

Protocol Transfer time
Client/Server t * N
P2P with a single piece t + t * log2(N)
P2P with multiple pieces t + t * log2(N) / R
  • N = Number of nodes
  • R = Number of pieces that the package is split into
  • t = Time to transfer a complete package from one node to another

参考项目 Gopub,可选用BT分发软件包。

跨数据中心传输:先把文件分发到每个数据中心节点的种子服务器,再在每个数据中心内做BT共享传输。

适用场景:大文件和大批量机器文件传输。

这部分功能可直接使用开源项目,所需要的功能基本上都有,不需要自己开发。参考: https://github.com/anacrolix/torrent

判断本地包被修改

据说tars设计成增量更新,就是考虑到有人会在线上环境做修改,为了避免覆盖修改的部分,才只更新变化的文件。

FPub虽然不使用增量,而是全覆盖更新,也还是需要考虑到线上文件被人修改的情况,如果检测到修改,则直接拒绝上线更新,那么就出现一个问题,如何检测文件被修改过?

我提出的方案是,使用git。

具体实现如下,当包发布系统从File Server下载好包,解压到安装位置后,进入程序包的目录,然后执行git init初始化成一个本地仓库,再commit一次。这样,后续若是有任何修改变更,直接通过git status就可以知道。

但是若是有人修改了文件,并且也git commit了,这时候若是git status不会有变更,所以我们需要记下第一次commit时的 commit id,后面判断目录是否被修改时,先看当前commit id是否是初始化时的id,再看git status输出。

包命名及用户权限

目前我们使用的tars采用二级目录命名,也就是 product/name 这样的路径,每一个包分属到一个product命名空间下面。包在服务器上存储的目录结构也是按 product/name这样。

zhiyun则只有一级,所有的包都在一起,名字不能重复。

为了更好地兼容tars,以及考虑到后面跟Gitlab进行CI整合,FPub将采用如下的包组织形式。

每个用户创建的包都只挂在自己用户名下面,如kylechen创建了一个包叫 ftrace_alarm,则包路径为 kylechen/ftrace_alarm。这个与Gitlab的项目类似。

可以创建项目,项目与包是多对多的关系。项目关联包,只是在数据库层面有一条纪录,实际上文件系统只以用户名空间为准。也就是说,现在创建了一个项目叫ftrace,把ftrace_alarm关联到项目中去,这时候浏览ftrace这个项目能快速找到下面所有的包,但是项目下面的包实际上还是在所属用户那里的。

示例图

至于权限,每个用户对自己创建的包有权限,默认登录后只能看到自己创建的,以及某个项目。对用户授权只在项目级别上授权,这样操作比较简单。也就是说你的项目,要让别人能看到,只有加入到某个项目,然后那个人有项目的权限。

发布时,只能发布哪些ip的机器,这些细分的权限后期可与CMDB联动,属于后期迭代开发的功能,需要再讨论,这里不赘述。

包版本管理

目前tars的包版本命名为 v1.0.2 这种,然后安装到Minion上面的软件包命名为 name-1.0 ,这个1.0是固定死的,无意义,按照奥卡姆剃刀原理,应该去掉。

FPub将支持两种类型的版本号,v1.3 和 v2.0-r7。v1.3这种就没什么好说的了,每个版本递增就好了,v1.1、v1.2、v1.3……在每个这种版本号下面,可以有若干 release candidate版本,也就是带有r1这样的后缀。

比如 v1.1.r3,表示v1.1版本前的第3个候选版本。

候选版本可用于发布到测试环境进行测试,正式环境不允许发布候选版本号的包。

如果候选版本稳定了,可以升级成正式版本,这时可使用版本克隆功能。版本克隆可以将多个不同版本号指向包的同一文件系统,目前只支持将候选版本克隆为正式版本。

FPub Server 上面包文件通过 git 来实现版本管理,不同的版本对应一个git tag。那么版本号之间转换,可用如下图示意

包安装到Minion上的路径上去掉版本号,也就是现在的路径 /usr/local/services/ftrace_alarm-1.0 ,改成 /usr/local/services/ftrace_alarm。

那么这样一来,我们怎么知道机器上面安装的是哪一个版本呢?还记得上面“判断本地包被修改”一节中,我们在安装后git init了一下,然后我们再 git tag -a v1.4.r2 生成一个tag,后面要查看版本号,直接git tag命令就可以了。当然FPub Server上面保存了每个Minion上面安装包版本号的。

包进程及日志管理

tars的进程监控是直接用ps命令,grep用户设置的程序名,这种方式极其不可靠,比如像java类程序,根本无法正常监控。

FPub将使用业界广泛使用的supervisor来管理进程,包括启动、停止、重启、自动拉起、日志rotate等功能。

用户需要在FPub上面配置好自己程序的启动配置文件,示例

[program:ftrace_alarm]
command=/home/vagrant/projects/futu/ftrace_alarm/ftrace_alarm
process_name=%(program_name)s
numprocs=1
directory=/home/vagrant/projects/futu/ftrace_alarm/
autostart=true
stdout_logfile=/home/vagrant/log/supervisor/ftrace_alarm_stdout.log
stderr_logfile=/home/vagrant/log/supervisor/ftrace_alarm_stderr.log

这里只列出了核心几个参数,还有大量其它参数,可参考supervisor文档。

当安装包时,会将上面的配置文件写入到 /usr/local/services/supervisor/etc/conf.d/ftrace_alarm.ini 中,然后启动supervisor,进行进程管理。

其中 ftrace_alarm.ini 文件名字,等于 FPub 相应项目名,这里有个问题就是如果两个用户有两个相同名字的包,这时如果在同一台机器上安装,就会冲突。这种情况比较极端,暂时不考虑,应该人为避免。

删除包时,将 conf.d 文件夹下面对应的 init 配置文件删除,再 supervisorctl update 即可。

supervisor自带有 xml-rpc 接口,我们直接通过程序调用接口就可以实现进程管理,后面有示例代码。

supervisor本身有 events 事件,可以自己对接,能够实现进程挂掉后报警通知等自定义逻辑,参考文档:https://github.com/Supervisor/supervisor/blob/master/docs/events.rst。

每个项目包可以添加多个程序入口配置,也就是多个supervisor program节点。如

[program:ftrace_alarm_0]
....
[program:ftrace_alarm_1]
....
[program:ftrace_alarm_2]
....

python依赖及pip仓库

tars上传python项目的包一直是个痛点,现在做法是把整个虚拟环境目录 env上传上去,导致包文件数量和体积都很大,不好维护。

FPub将抛弃这种方式,改成业界通用的做法,那就是维护python包依赖文件 requirements.txt,包依赖必须指名准确的版本号,以便FPub在安装依赖时判断是否有依赖包版本的变化。示例文件

elasticsearch==6.3.0
elasticsearch_dsl==6.2.1
pyspark==2.3.1
http://pypi.server.com/monitor-1.0.1.tar.gz

如果依赖的是公司内部的包,如monitor,cmlb等,则通过http方式添加依赖,如上文件中的monitor。

对于公司内部的包,将通过nginx提供http服务供pip安装下载,同时,FPub提供管理入口,供用户上传、更新包文件。标准的包将通过腾讯云内网的pypi源安装。

用户在创建项目时,需要指定项目类型,如python,spp等。若为python项目,且项目根目录下面存在requirements.txt时,FPub在安装、更新包的时候,会自动完成 pip 包依赖的更新。每个项目都拥有属于自己的虚拟环境 env,虚拟环境目录 与包目录在同一级别,命名为 pacakge-env,比如我有个包名字叫 ftrace_alarm ,那么对应的虚拟环境目录名为 ftrace_alarm-env ,这样就可以在 supervisor 配置文件中指定相应python路径。

$ tree /usr/local/services/
├── ftrace_alarm
│   ├── main.go
│   └── ……
├── ftrace_alarm-env
│   └── bin
│   ├── python2.7
│   ├── lib
│   ├── site-pacakges

在每次升级、降级包版本时,FPub会diff两个版本间requirements.txt变化,把新增、删除、变化的依赖包找出来,然后通过pip执行相应操作。

对于同一台server,不同包项目需要用到不同的python版本,比如有个程序用的是2.7,另一个用python3.5写的,这种情况FPub将提供支持。首先用户在创建包的时候需要指定python版本号,然后FPub检测目标机器是否安装对应版本python,若没有安装,则通过pyenv安装。

pyenv 是社区使用比较多的python版本管理工具,可以非常方便地在同一个机器上实现不同python版本切换使用。

实时查看应用日志

发现大家在发布包的时候,都会ssh登录到机器上去,tail -f一下日志,看看程序启动的情况。新系统将支持日志实时查看功能,直接在FPub Web上就可以看,不需要再ssh到目标机器上去。

首先如果程序通过stdout输出的日志,supervisor有标准的api,可以拿到日志,如果应用有自己输出日志到文件,则需要另做配置读取,实现类似于系统tail -f的命令。

读取到日志后,通过websocket协议将日志实时发送给FPub Server,再转发给Web端用户展示。流程如图所示

注意这种日志流并不是任何时候都上传,只有当有用户需要查看日志的时候,才开启,当没有人查看时,就发送命令停止收集日志,同时,日志只通过网络传输,并不做持久化存储。

这个功能定位在于实时查看日志,并不是查找历史日志。

Crontab管理

一般的包发布系统是不包含这个功能的,但是我们之前已经用了tars在管理很多机器上的crontab,所以为了兼容,这个功能也还是需要有。

实现上crontab还是采用Linux系统标准的写crontab配置方式,暂时没计划换其它方案。但是tars用的是直接执行shell脚本,非常不稳定,难以维护。FPub将通过python第三方库 plan(https://github.com/fengsp/plan) 来管理crontab配置,示例代码

# coding=utf-8
from plan import Plan

cron = Plan()
cron.command('ls /tmp', every='1.day', at='12:00')
cron.command('pwd', every='2.month')
cron.command('date', every='weekend')

if __name__ == '__main__':
cron.run()

以上代码将输出如下的crontab配置

# Begin Plan generated jobs for: main
0 12 * * * ls /tmp
0 0 1 1,3,5,7,9,11 * pwd
0 0 * * 6,0 date
# End Plan generated jobs for: main

注意上面的 # Begin plan 和 # End plan,这些标记是用来管理不同crontab空间的。

同时,在让用户设置crontab任务的web交互上,也需要设计得更好,不要让他们直接写 0 12 * * * ls /tmp 这种形式的配置,容易出错。

上线任务编排

这一部分我还没有完全想清楚。核心理念就是,尽量减少大家发起上线流程的次数,因为发生变更的次数越多,产生事故的概率就越大。

比如一次大的版本上线,需要同时更新十几个包,并且有先后依赖顺序,需要事先编排好上线的流程,哪个包先发,发完验证OK再发另一个包,诸如此类,这种叫上线任务Pipeline。当写好一个Pipeline后,需要审核及演练(用rc版本的包在测试环境中演练),没有问题才正式上线。也就是说,十几个包的上线发布,最终体现在FPub系统里面的,只是一次上线任务。

在上线流程Pipeline方面,Jenkins已经做得足够好了,我们可以从中学习和吸取灵感,再应用到我们的系统中。后期等我有了完善的想法再来更新文档。

持续集成 CI & CD

Gitlab自带的CI功能已经够用,后期将与FPub整合,Gitlab实现版本构建的触发,以及项目编译的Runner,生成的清单文件,再通过接口的形式发送给FPub,形成包。流程如下图

FPub要做的,就是预留一个接口,给Gitlab Runner提交文件之用,收到文件后将自动创建rc版本的包,CI核心事情都交给Gitlab实现了,确实没有太多可讲的。

部分原型图

新建项目时设置项目属性

管理项目文件

设置项目启动项

设置项目Crontab