本次分享介绍了 Acttao 技术团队开发、运维进行容器化的方法、过程和经验,包括:GitLab、Drone、Mesos/Marathon 进行持续集成,CI/CD过程中的遇到的问题既解决方法,Docker 的跨主机网络通讯和外部存储实践经验。
- Gitlab、Drone、Mesos/Marathon 进行持续集成
- Docker 容器的跨主机网络通讯
- OpenStack 与 Docker Volume
背景
Acttao 成立于 2008年,起先给客户提供社交网络解决方案,2013年后逐步把重心转移到为客户提供垂直领域的电商平台解决方案。我们的开发模式也由先前的前后端混杂在一起向前后端分离的方式演进。在现有的开发模式下,一个产品至少有两个项目组成,规模稍大些的产品有六七个项目组成。最近我们也在试着走基于业务域的模块化开发方式,这样一个产品的组成部分就会变的更多。公司使用的开发语言有 Python、Node.js、PHP,项目使用服务也比较的多,比如 MySQL、Postgres、Memcached、Redis、ElasticSearch、Rabbitmq等等,在部署上一个项目又分为 Staging 和 Production 环境。
如果公司只有一个产品,人工的去做项目Staging 、Production 环境的搭建和部署,我们可能还顶得住。但公司的实际情况是产品很多,客户的、公司自己的。有一段时间,运维的人手精力实在是不够,我们还规定过一个产品的一个项目每天发布的 Staging 环境的次数限制在 3 次,Production 环境除非重大 bug 需要修复也限制在 3次 。
同时,开发出的产品的质量也渐渐地降低了。这种情况让人很沮丧,我们需要 CI、Code Review、CD。
CI、Code Review
“上帝说要有光,于是有了光”,开发说要有 CI、Code Review,于是有了 CI、Code Review。为了 CI、Code Review 在 2014 年初我们把版本控制系统由 HG 换成了 Git,使用 GitLab 来管理我们的 Git 仓库、 Gitlab CI 来运行 CI,通过 GitLab 的 Merge Request 做 Code Review 。自然而然地,Gitflow 成为我们的开发工作流。一开始我们的 GitLab 和 GitLab CI 就运行在 Docker 容器里,升级更新很方便。这里要感谢 https://github.com/sameersbn/docker-gitlab ,让那时的我们很容易就有了 CI 和 Code Review。
遇上 Mesos/Marathon
在接下来的大半年的时间里,每次 GitLab 有新版本后,我们手动拉取新版本的镜像,备份 Git 仓库,停掉旧版本的 Docker 容器,启动新版本的容器。日子过的还算“舒坦”,除了偶尔对 GitLab CI runner 有些抱怨:有时候运行 CI 时Python 一个包需要的头文件在 GitLab CI runner 中没有安装,我们就需要修改 runner 的镜像重新运行 runner。持续集成对我们而言依然是遥遥无期,那个时候在网上找到的资料依然是前些年基于 VM 的,这个时候 Vagrant 是一种很炫的交付手段。
这半年里我们有一个比较大的变化是给一个客户搭建了 OpenStack 私有云,我们的服务也逐步地向 OpenStack 迁移了,先前在物理机中运行的容器也迁移到了 OpenStack 的 VM 中。
2015年初一次偶然的机会,我们发现了一个叫 Marathon 的东西,它可以管理 Docker 容器,唯一需要我们做的就是调用它的 API。当时花了半天的时间使用 Docker 运行了个 Mesos/Marathon 集群,第一时间对这个东西有了个很直观的感受。
很长一段时间里,我们一直在部署测试 Mesos/Marathon 集群,用的是 Cisco 提供的解决方案 https://github.com/CiscoCloud/mantl (在我们测试这个方案时,它的名字还叫做 microservices-infrastructure)。这个方案里使用 Terraform 调用 OpenStack 的 API 来创建 VM,然后通过 Ansible 来搭建部署系统。通过 Mantl 的指引,我们对 Docker 容器的了解进入到了一个崭新的世界:网络和存储。
OpenStack 中的 Mesos/Marathon
2015年9月份,我们在机房部署了一套公司自己的 OpenStack 系统,10 台机器(后续又增加了 4台 Dell R920 做计算节点)。麻雀虽小,五脏俱全,虽然规模不大,但是 OpenStack 的控制节点、网络节点的HA,OpenStack Swift、Ceph 块存储都有。这套系统使用的是 OpenStack-Ansible ( https://launchpad.net/openstack-ansible 对 OpenStack 赶兴趣的可以了解下)部署的,Ceph 集群使用 https://github.com/ceph/ceph-ansible 部署的。这个时候我们计划在 OpenStack 中搭建一个 Mesos/Marathon 集群,把现有的容器迁移到 Mesos 中去。最快速地方式是把 Mantl 直接部署在 OpenStack ,但是我们在测试 Mantl 的时候发现它有一些问题:
- Mantl 依赖的 Consul 集群不是很稳定,如果 control 节点同时重启后,Consul 集群会启动不了。
- manti 使用的 powerstrip Calico 的方式在 Mesos 中实现 Docker 容器网络互联在 Mesos 0.25 时已不再支持 (https://issues.apache.org/jira/browse/MESOS-3821), 而且 Mesos 官方也没有去修复这个问题想法。而且,PowerStrip 只是 Docker 网络的一个临时的方案, Docker 本身也最计划实现 Network Plugin。
- Mantl 中对容器存储的解决方案不是很好,我们想使用 Docker Volume Plugin,而且现有的 OpenStack 提供的有 Cinder 块存储。
- Mantl 的 LB 组件因为 Consul 的问题有时候会有残留的容器信息。
上面的问题是我们当时直接使用 Mantl 需要解决的(现在的 Mantl 好像把重心转移到 Kubernetes 了,这些可能不再是问题了),没法指望 Cisco 的开发人员立马给我们解决,他们有他们的开发流程,只能自己弄一套类似的东西。
Mantl 中使用 Terraform 和 Ansible 来部署系统的方法我们觉得很好,就直接拿过来用了。
我们保留了 Mantl 中的 ZooKeeper、Mesos、Marathon、GlusterFS 等组件,保留和 Mantl 中相同的功能,采用自己或第三方编写的 Ansible Role ,主要是 Mantl 中的组件和 Consul 耦合太深,没法直接使用。
使用 Mesos-DNS 替代 Consul 做服务发现,使用 Marathon-lb 替代 haproxy-consul 做 LB,使用 Weave 替代 Calico 实现容器间的网络通信。
新增加了 REX-Ray (REX-Ray https://github.com/emccode/rexray) 做 Docker Volume,新增加了自己编写的 Marathon-CloudXNS 把 LB 中的应用在 CloudXNS 中创建一个格式为 appname.example.com 的 A 记录(就是 PaaS 系统中应用自定义域名时添加到对应 cname 的那条记录),新增加了自己编写的 Marathon-DNS 把 LB 中的应用的 DNS 解析到 LB 作为内网 DNS 。
下面是部署系统的 Ansible Playbook:
- hosts: all
sudo: yes
roles:
- { role: apt, tags: ['base', 'apt'] }
- { role: ntp, tags: ['base', 'ntp'] }
- { role: common, tags: ['base', 'common'] }
- { role: hosts, tags: ['base', 'dns', 'hosts'] }
- { role: dnsmasq, tags: ['base', 'dns', 'dnsmasq'] }
- { role: glusterfs, tags: ['glusterfs'] }
- { role: docker, tags: ['base', 'docker'] }
- { role: docker-clean, tags: ['base', 'docker-clean'] }
- { role: mesos-dns, tags: ['base', 'dns', 'mesos-dns'] }
- { role: etcd, tags: ['etcd'] }
- { role: calico, tags: ['calico', 'network'] }
- hosts: control
sudo: yes
roles:
- { role: zookeeper, tags: ['zookeeper', 'platforms'] }
- { role: mesos, tags: ['mesos', 'platforms']}
- { role: marathon, tags: ['marathon', 'platforms'] }
- { role: chronos, tags: ['chronos', 'platforms'] }
- hosts: worker
sudo: yes
roles:
- { role: mesos, tags: ['mesos'] }
- { role: rexray, tags: ['volume', 'rexray'] }
- { role: marathon-cloudxns, tags: ['marathon-cloudxns'] }
- { role: weave, tags: ['network', 'weave'] }
- hosts: edge
sudo: yes
roles:
- { role: mesos, tags: ['mesos'] }
- { role: marathon-lb, tags: ['marathon-lb'] }
- hosts: all
sudo: yes
roles:
- { role: marathon-dns, tags: ['dns', 'marathon-dns'] }
OpenStack 中的 Mesos/Marathon遇到的问题
2015年 9月初公司的 OpenStack 上线,9月底 Mesos/Marathon 系统搭建完毕并投入使用,主要为公司内部的应用提供服务。在使用期间 Mesos/Marathon 遇到的问题主要集中在 Weave和 REX-Ray 两个组件上。Weave 使用 Gossip 协议来同步集群节点间的信息,有时候就会出现容器获取不到 IP 的情况出现,几次查找的原因是集群间的信息同步问题导致分配不了 IP。这个问题在 Weave 1.5 版本开始把一些信息持久化到磁盘后就没有出现过了。
REX-Ray 的问题主要在它的 preempt 功能,使用 OpenStack Cinder 时这个功能是调用 cinder force_detach API 实现的。 cinder 的 force_detach 默认需要管理员权限,如果没有管理员权限,块存储的磁盘会从 libvirt instance中和 OpenStack 的数据库中清理赶紧,但是 instance 系统的 mount 表中还存在磁盘的挂载信息,如果有程序从这个挂载点做读取操作,操作会 hang 掉,没有任何反应。如果有管理员权限 force_detach 在 instance 中的 mount 表中的信息会清理干净,但是 libvirt instance 中和 OpenStack 数据库依然存在对应的磁盘信息,这会导致随着时间的推移 libvirt instance中的磁盘口全部被占用(其实对应的口上根本就没有挂载磁盘)再也不能挂载磁盘了,或者导致 cinder API 中返回的挂载盘符和实际的挂载盘符不一致。我们后来写了个小程序在 OpenStack 的计算节点中定时运行以清理 libvirt instance 中的僵尸磁盘和 OpenStack 数据库中的不一致的磁盘数据。
持续集成
现在 Mesos/Marathon 系统有了,能看到 CD 的曙光了。Gitlab CI 当时是不能很容易的和 Mesos/Marathon 进行持续集成。我们需要一个工具,一个既能做 CI,然后能把代码打包成 Docker 镜像,接着还能调用 Marathon 的 API 来做部署的工具。好在发现了 Drone,一个使用 Docker 来做 CI,官方有一堆的插件来做 CD 的事情,如果没有你需要的还可以自己开发一个自己的插件。
于是我们就从 GitLab CI 切换到 Drone,顺便解决了先前使用 Gitlab CI 时遇到的问题。而且 CI 时也能使用真实的 MySQL、Redis 等等,而不是什么 Sqlite in memory、MockRedis。
当然了,那时的 Drone 还没有和 Marathon 集成的插件,我们需要自己写一个。
我们也想让 QA 团队能自己部署项目,同时对他们屏蔽底层的细节。
于是我们在 Marathon API 的基础上开发了一个系统 (内部命名为 Jujube),这个系统做的事情很简单:管理应用,管理应用的配置,创建应用需要的数据库等资源,通过 Marathon 的 API 来部署应用。这个系统交互方式是仿照 Herkuo 实现的, App、Config、release、addon 等。
jujube 的基本功能实现后,我们又编写了一个 Drone Plugin 和 jujube 进行交互做项目的持续部署。这个 Plugin 编写的很简单,不到 120 行代码。
func main() {
var repo = plugin.Repo{}
var build = plugin.Build{}
var vargs = struct {
URL string `json:"url,omitempty"`
Username string `json:"username"`
Password string `json:"password"`
App string `json:"app"`
Tag string `json:"tag"`
}{}
plugin.Param("repo", &repo)
plugin.Param("build", &build)
plugin.Param("vargs", &vargs)
plugin.Parse()
fmt.Printf("$ jujube login %s ***\n", vargs.Username)
url := defaultURL
if vargs.URL != "" {
url = vargs.URL
}
token, err := getAuthToken(vargs.Username, vargs.Password, url)
if err != nil {
fmt.Printf("%s\n", err)
os.Exit(1)
}
cmd := fmt.Sprintf("jujube releases create %s -p %s --force", vargs.Tag, vargs.App)
fmt.Printf("$ %s\n", cmd)
data, err := doCmd(cmd, token, url)
if err != nil {
fmt.Printf("%s\n", err)
os.Exit(1)
}
rc := data.Get("rc").MustInt()
if rc == 1 {
stderr := data.Get("stderr").MustString()
fmt.Printf("%s\n", stderr)
os.Exit(1)
}
out := data.Get("stdout").MustString()
fmt.Printf("%s\n", out)
}
Drone 中使用自定义的插件也很简单:
build:
image: index.example.com/library/python:2
environment:
- PIP_ARGS=--index-url https://pypi.example.com/prod/pypi/+simple/
commands:
- pip install -U tox ${PIP_ARGS}
- pip install virtualenv==12.1.1 ${PIP_ARGS}
- tox
compose:
database:
image: index.example.com/library/mysql:5.6
environment:
- MYSQL_DATABASE=example
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
command: --character-set-server=utf8mb4
redis:
image: index.example.com/library/redis:2.8
deploy:
jujube:
image: index.example.com/plugins/drone-jujube
url: https://jujube.example.com
username: example
password: $$EXAMPLE_PASS
app: example
tag: $${COMMIT:8}
when:
branch:
- develop
- master
使用 Drone 进行持续集成遇到的问题
到此一切都很顺利,但过了一段时间产品经理都抱怨 CI 运行的太慢,有时候一等等上老半天,严重影响 MR 合并的速度。经过分析发现是 Drone 同一时间里默认只运行有限数量个 CI 任务( 5 个?具体的数量忘记了),超出数量外的 CI 任务会等待。同时,我们进行 CD 时,Docker 镜像打包是用 drone-docker 插件来完成,打包的过程很漫长差不多有一二十分钟。如果同时有 5个任务在打镜像,后续的 CI 任务就需要等待。
针对这个问题摆在我们面前的有两个选择:1、增加 Drone 运行 CI 的 Docker Nodes 节点。2、把打镜像的任务从 Drone 中移走,移到我们的 jujube 系统中去。
我们选择了方案2,在 Jujube 中实现打镜像的功能。没有选择方案 1 的原因是,单独添加的 Docker Nodes 资源不在 Mesos 资源统计内,这样会造成系统实际的负载和 Mesos 中的不一致。同时,作为想成为内部 PaaS 系统的 Jujube,它理应有把项目代码 build 成 Docker 镜像的功能。
我们选用了 Tutum 的 Builder 来实现 git clone 代码打包镜像并推到私有 Docker Registry 的功能,tutum builder 镜像使用起来很简单,我们唯一需要做的是让 Jujube 创建 tutum builder 容器去打镜像。但是不能让 Jujube 直接去调用 Docker API 去实现这个功能,否则的话又面临着类似 Drone 运行 CI 任务时扩展问题。好在这个时候发现了 Eremetic ,一个运行 one-shot 任务的 Mesos Framework,这样打包 Docker 镜像的扩展问题就解决了。有新的打包任务来了,使用 Eremetic 在 Mesos 中启动一个新的任务就可以了。
Tutum Builder 运行时需要 Privileged 权限, Eremetic 默认不支持运行 Privileged 权限的 Docker 容器。我们修改了 Eremetic 的代码添加了对 Docker Privileged 的支持,同时也给它的API 添加了 Basic Auth 认证功能。
锦上添花
Jujube 系统没有 UI 界面,只有一个命令行的工具使用。为了能随时随地的使用 Jujube,我们又部署了 Rocket.Chat ,编写了一个 hubot script通过 Rocket.Chat 与 Jujube 进行交互,这样通过命令行工具可以做的事情也可以通过 Web Chat 来完成。应用打包、部署的状态也没有直观的查看方式,我们就给 Jujube 添加了Webhook 机制,结合 Rocket.Chat 的 Incoming WebHook就可以及时的在 Rocket.Chat 中看到应用的打包、部署情况。
转战阿里云
今年7月份公司开始计划把生产环境迁移到阿里云并撤掉现有的 OpenStack,不是说 OpenStack 不好,我们的 OpenStack 在这一年多的时间里运行稳定,没有出过什么问题,而是公司计划缩减成本,同时缩减运维的人力成本。有了前面一年多的使用经验,我们打算在阿里云中搭建一套类似的系统用来部署我们的应用,数据库等服务直接购买阿里云的服务。为了不手动的配置阿里云中的服务,我们编写了一个 Aliyun 的 Terraform provider,可以管理阿里云的 instance、disk、eip、vpc、switch、subnet、nat_gateway、security-groups、slb 等。
terraform-aliyun 实现后,我们很快就在阿里云上搭建了上述的系统,只是去掉了 weave、rexray 模块。
现在阿里云中系统运行良好,但仍然在测试阶段,我们明天准备正式的切换生产环境到阿里云。
至于测试环境我们的 Mesos 系统现在运行在 KVM VM 中,去掉了 REX-Ray 模块。
未来
前一段时间 Mesos 发布了 1.0 版本,6 天前 Marathon 了发布了支持 Mesos 1.0 的 1.3 版本。我们已经升级到这个两个版本,但仍然使用的是 Mesos 的 Docker Container 。接下来我们可能会现在测试环境中试着切换到 Mesos Container ,同时用 Calico CNI 替换 Weave 。测试环境缺少的 REX-Ray 模块可能会使用能基于 Ceph 的 Docker Volume来替换, Mesos 1.0 支持 dvdcli。
我们的系统也缺少日志收集功能和监控功能,我们也在测试这方面的方案,后续也会添加上这些功能。
Q&A
Q:为什么不采用 Kubernetes 呢?Q:您对容器化 OpenStack 怎么看?
A:我们在 2015年初做选型时,Kubernetes 还不是很成熟。当时我们评估 Mesos 和 Kubernetes 后,觉得 Kubernetes 太复杂,Mesos 的核心功能很简单,其他的功能都由 Framework 来实现。我们很容易就理解了 Mesos,于是就选择了 Mesos。
Q:关于 Weave 很多文章都说性能不行,就你们的项目和实际使用情况来看,能对 Weave 做更多的评价么?
A:OpenStack 在我们看来只是更方便的提供底层资源的平台,容器化后的 OpenStack 只是能使用 OpenStack 的方式管理容器了。但使用 Mesos 来管理容器的话它不关心底层到底是虚拟机还是物理机。
容器化 OpenStack 软件本身后部署升级 OpenStack 也较为方便。我们现在这个系统部署的软件除了 Mesos 外,能使用 Docker 运行的也尽量地采用了 Docker 进行运行。
Q:Container 网络中,Weave 有什么优点,为什么不采用 Flannel?
A:我们自己当时测试在 FastDb 模式下,Weave 的性能还在接受的范围里,大概比 OpenStack 中的 Neutron 网络损失了 10% ~ 20% 。就实际的使用情况看来,Weave 集群搭建起来非常容易,毕竟不依赖于任何外部存储。但是它出现任何问题时,调试不是很方便。
Q:请问目前你们 Staging 和 Product 环境的发布频率如何?两套环境是用的同一套CI/CD系统吗?对于部署在客户的系统即网络不连通的,如何实现镜像或环境同步的?
A:Weave 的优点搭建方便,有 proxy、plugin、cni 模式,自带 DNS 做服务发现。在 Mesos 还没有原生的容器网络支持时,weave proxy 的方式能很容器的让使用 Marathon 创建的 Docker 容器拥有自己的网络。
我们是一个小的团队,优先考虑我们能容易理解的方案。
Q:为什么不直接用阿里云的容器服务而自己搭建呢?
A:Staging 环境的发布频率很快,开发恨不得没 merge 一个 mr 就做一次发布。Product 环境在今天之前依然是运维人工去部署的,明天正式迁移到阿里云后,前期依然会采用人工调用我们的 PaaS 工具去做发布。等大家都接受容器化自动化的部署方式可能会考虑在生产环境也进行自动部署。
都是用的同一套 CI/CD 系统。 我们现在的 Registry 使用的阿里云的 OSS,每个网络的内部部署自己的 Registry。
Q:如果现在选, 会选 Kubernetes 吗? 为什么?
A:我们自己弄这套环境时,没有想过会使用阿里云。当计划迁移到阿里云时,自己的那套东西已经运行很稳定,和开发流程配合的很好。使用阿里云的容器服务可能现有的流程要调整,而且我也不确认公司还会不会从阿里云迁走。
Q:你们的网络似乎经历了 Calico- > Weave- > Calico 的过程,why?
A:依然会选择 Mesos,因为除了容器编排,不排除我们后续会使用其他的服务,比如 Spark 。同时,Kubernetes 有的, Mesos 现在也有,cni、Docker Volume 等等。
Q:这么多应用,数据库分布是怎样的?数据是挂载进 Docker 的?
A:我们从来没有在正式的环境中使用过 Calico。选用 Weave 的原因是 weave proxy 模式在当时能很好的 Mesos/Marathon 进行集成。 现在考虑 Calico 的原因是 Mesos 1.0 后支持 cni 了,Calico cni 能很好的集成进 Mesos,而且 Calico 的性能、稳定性、网络隔离较 Weave 好。
Q:从你说的把你们这套CI/CD移到阿里云上差不多用了接近两个月的时间,这也算比较长了,能说说在迁移过程中都做了哪些事情,有什么需要注意的地方?那一般一个普通的应用系统如果迁移到阿里云上(当然也包括其维护的CI/CD),需要多长时间,有哪些需要注意的地方?
A:使用的 REX-Ray 把 OpenStack 的块存储作为数据盘挂载到 VM 的,然后采用 docker volume plugin 的方式挂载到容器里的。一个 Volume 对应一个 Cinder 块设备。
以上内容根据2016年9月20日晚微信群分享内容整理。分享人何威威,Acttao 技术总监,多年 Python 开发经验,使用 SaltStack、Ansible 倒腾过 OpenStack的部署。从 2014年初开始使用 Docker,2015 年初接触 Mesos 半年后使用 Ansible 在 OpenStack 中部署 Mesos 集群运行到今年 8月初。现在管理维护公司IDC KVM中和阿里云中运行的 Mesos 集群。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesz,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。
A:实际的迁移时间没有用到两个月,在阿里云上搭建这个系统大概花了 2 到 3 周的时间(大部分的时间都在编写 Aliyun 的 terraform provider),真正部署起来用了一天的时间,我记得是在 8月 13,那天我们采用包月的方式买了一批的阿里云资源。后续的时间大多是在做迁移后的测试工作,因为公司的运维对在阿里云中运行 Docker 持怀疑的态度,而且这个迁移后续是用业余时间来完成的,8月中旬我们又忙活了一阵子,这两天刚歇会而。迁移系统应用系统需要注意的事情就是尽量多测试。