专栏名称: 分布式实验室
最专业的Docker文章,最权威的Docker新闻。关注容器生态圈的发展。
目录
相关文章推荐
51好读  ›  专栏  ›  分布式实验室

Docker在B站的实施之路

分布式实验室  · 公众号  · 后端  · 2016-10-09 07:44

正文


B站一直在关注Docker的发展,去年成功在核心SLB(Tengine)集群上实施了Docker。今年我们对比了各种Docker实施方案后选择了Mesos。结合CI&CD,打通了整个业务的Docker上线流程,并实现了全自动扩缩容。这次结合我们的实施之路,分享一下遇到的重点与难点:

  1. 自研Docker网络插件的介绍;

  2. Bili PaaS平台中的CD实现与优化;

  3. 应用全自动扩缩容的实现方案;

  4. Nginx动态Upstream跟Docker的配合使用。

B站一直在关注Docker的发展,去年成功在核心SLB(Tengine)集群上实施了Docker,规模不大但访问量大、没有CI & CD的工作。随着业务量的增长,应用扩缩需求越来越多。但在没有Docker标准化的情况下,应用扩容需要扩服务器,操作繁重。同时为了减少因测试、线上环境不一致导致的问题,我们计划将业务全部Docker化,并配合CI & CD,打通整个业务上线流程,达到秒级动态扩缩容。

下面是我们的实施之路,整体架构图如下:



为什么选择Mesos?

Kubernetes太重,功能繁多。我们主要看中Mesos的调度功能,且轻量更易维护。另外和我们选择了Macvlan的网络有关。

Docker网络选择

Docker自带的网络都不能满足我们的需求。

Bridger :Docker分配私有IP,本机通过bridge跟容器通信。不同宿主机如果要通信需要iptables映射端口。随着容器的增多,端口管理会很混乱,iptables规则也越来越多。

Host :使用宿主机的网络,不同容器不能监听相同端口。

None :Docker不给容器分配网络,手动分配。

正当我们无法选定Docker的网络方案时,发现最新的Docker 1.12版本提供了另外两种网络驱动:Overlay和Macvlan。

Overlay :在原来的TCP/IP数据包基础上再封装成UDP的数据包传输。当网络出现问题需要抓包时,会比较麻烦。而且,Overlay依靠服务器的CPU来解UDP数据包,会导致Docker网络性能非常不稳定,性能损耗比较严重,在生产环境中难以使用。

Macvlan :在交换机上配置VLAN,然后在宿主机上配置物理网卡,使其接收对应的VLAN。Docker在创建Network时driver指定Macvlan。对Docker的Macvlan网络进行压测,跑在Macvlan网络的容器比跑在host网络的容器性能损失10~15%左右,但总体性能很稳定,没有抖动。这是能接受的。

基于Macvlan,我们开发了自己的IPAM Driver Plugin—底层基于Consul。
Docker在创建Macvlan网络时,驱动指定为自己研发的Consul。Consul中会记录free和used的IP。如下图:


IPAM Driver在每台宿主机上都存在,通过Socket的方式暴露给Docker调用。Docker在创建容器时,IPAM Plugin会从Consul申请一个free的IP地址。删除容器时,IPAM Plugin会释放这个IP到Consul。因为所有宿主机上的IPAM Plugin连接到的是同一个Consul,就保证了所有容器的IP地址唯一性。

我们用IPAM Plugin遇到的问题:

1) Consul IPAM Plugin在每台宿主机上都存在,通过Socket方式调用,目前使用容器启动。

当Docker daemon重启加载Network时,因为容器还未启动,会找不到Consul IPAM Plugin的Socket文件,导致Docker daemon会去重试请求IPAM,延长daemon的启动时间,报错如下:

level=warning msg="Unable to locate plugin: consul, retrying in 1s"
level=warning msg="Unable to locate plugin: consul, retrying in 2s"
level=warning msg="Unable to locate plugin: consul, retrying in 4s"
level=warning msg="Unable to locate plugin: consul, retrying in 8s"
level=warning msg="Failed to retrieve ipam driver for network \"vlan1062\"

解决方案:Docker识别Plugin的方式有三种:

  1. sock files are UNIX domain sockets.

  2. spec files are text files containing a URL, such as unix:///other.sock or tcp://localhost:8080.

  3. json files are text files containing a full json specification for the plugin.

最早我们是通过.sock的方式识别IPAM Plugin。现在通过.spec文件的方式调用非本地的IPAM Plugin。这样Docker daemon在重启时就不受IPAM Plugin的影响。

2) 在通过Docker network rm 删除用Consul IPAM创建的网络时,会把网关地址释放给Consul,下次创建容器申请IP时会获取到网关的IP,导致网关IP地址冲突。

解决方案:在删除容器释放IP时,检测下IP地址,如果是网关IP,则不允许添加到Consul的free列表。

基于以上背景,我们刚开始选型的时候,测试过Docker 1.11 + Swarm 和Docker 1.12集成的SwarmKit。Docker 1.11 + Swarm网络没有Macvlan驱动,而Docker 1.12集成的SwarmKit只能使用Overlay网络,Overlay的性能太差。最终我们采用了Docker 1.12 + Mesos。

CI & CD

对于CI,我们采用了目前公司中正在大量使用的Jenkins。 Jenkins通过Pipeline分为多个step,step 1 build出要构建war包。Step 2 build Docker 镜像并push到仓库中。

第一步: build出想要的war,并把war包保存到固定的目录。第二步:build docker镜像,会自动发现前面build出的war包,并通过写好的Dockerfile build镜像,镜像名即为应用名。镜像构建成功后会push到我们的私有仓库。每次镜像构建都会打上一个tag,tag即为发布版本号。后续我们计划把CI从jenkins独立出来,通过自建的Paas平台来build war包和镜像。


我们自研了基于Docker的PaaS平台(持续开发中)。该平台的功能主要包括信息录入、应用部署、监控、容器管理、应用扩缩等。CD就在PaaS上。


当要部署一个新的业务系统时,要先在PaaS系统上录入应用相关信息,比如基础镜像地址、容器资源配置、容器数量、网络、健康检查等


CD时,需要选择镜像的版本号,即上文提到的tag


我们同时支持控制迭代速度,即迭代比例的设置


这个设置是指,每次迭代20%的容器,同时存活的容器不能低于迭代前的100%。

我们遇到的问题:控制迭代比例。

Marathon有两个参数控制迭代比例:

  • minimumHealthCapacity(Optional. Default: 1.0)处于health状态的最少容器比例;

  • maximumOverCapacity(Optional. Default: 1.0)可同时迭代的容器比例。

假如有个Java应用通过Tomcat部署,分配了四个容器,默认配置下迭代,Marathon可以同时启动四个新的容器,启动成功后删除四个老的容器。四个新的Tomcat容器在对外提供服务的瞬间,因为请求量太大,需要立即扩线程数预热,导致刚进来的请求处理时间延长甚至超时(B站因为请求量大,请求设置的超时时间很短,在这种情况下,请求会504超时)。

解决方法

对于请求量很大需要预热的应用,严格控制迭代比例,比如设置maximumOverCapacity为0.1,则迭代时只能同时新建10%的容器,这10%的容器启动成功并删除对应老的容器后才会再新建10%的容器继续迭代。

对于请求量不大的应用,可适当调大maximumOverCapacity,加快迭代速度。

动态扩缩容

节假日或做活动时,为了应对临时飙高的QPS,需要对应用临时扩容。或者当监控到某个业务的平均资源使用率超过一定限制时,自动扩容。我们的扩容方式有两种:1、手动扩容;2、制定一定的规则,当触发到规则时,自动扩容。我们的Bili PaaS平台同时提供了这两种方式,底层是基于Marathon的Scale API。这里着重讲解下基于规则的自动扩缩容。

自动扩缩容依赖总架构图中的几个组件:Monitoring Agent、Nginx+UpSync+Consul、Marathon Hook、Bili PaaS。

Monitor Agent :我们自研了Docker的监控Agent,封装成容器,部署在每台Docker宿主机上,通过docker stats的接口获取容器的CPU、内存、IO等信息,信息录入InfluxDB,并在Grafana展示。

Bili PaaS :应用在录入PaaS平台时可以选择扩缩容的规则,比如:平均CPU > 300% OR MEM > 4G。PaaS平台定时轮询判断应用的负载情况,如果达到扩容规则,就按一定的比例增加节点。本质上是调用Marathon的API进行扩缩。

Marathon Hook :通过Marathon提供的/v2/events接口监听Marathon的事件流。当在Bili PaaS平台手动扩容或触发规则自动扩容时,Bili Paas平台会调用Marathon的API。Marathon的每个操作都会产生事件,通过/v2/events接口暴露出来。Marathon Hook程序会把所有容器的信息注册到Consul中。当Marathon删除或创建容器时,Marathon Hook就会更新Consul中的Docker容器信息,保证Consul中的信息和Marathon中的信息是一致的,并且是最新的。

Nginx+UpSync+Consul :当Marathon扩容完成时,新容器的IP:PORT一定要加到SLB(Tengine/Nginx)的Upstream中,并reload SLB后才能对外提供服务。但Tengine/Nginx reload时性能会下降。为了避免频繁reload SLB导致的性能损耗,我们使用了动态Upstream:Nginx + UpSync + Consul。Upsync是Weibo开源的一个模块,使用Consul保存Upstream的server信息。Nginx启动时会从Consul获取最新的Upstream server信息,同时Nginx会建立一个TCP连接hook到Consul,当Consul里的数据有变更时会立即通知到Nginx,Nginx的worker进程更新自己的Upstream server信息。整个过程不需要reload nginx。注意:UpSync的功能是动态更新upstream server,当有vhost的变更时,还是需要reload nginx。

我们遇到的问题

1) Nginx + UpSync 在reload时会产生shutting down。因为Nginx Hook到Consul的链接不能及时断开。曾在GitHub上因这个问题提过issue,作者回复已解决。个人测试发现shuttding down还是存在。并且UpSync和Tengine的http upstream check模块不能同时编译。

解决方案:Tengine + Dyups。我们正在尝试把Nginx + Dyups替换为Tengine + Dyups。Dyups的弊端就是Upstream信息是保存在内存里的。Reload/Restart Tengine时就会丢失。需要自己同步Upstream信息到磁盘中。基于此,我们对Tengine + Dyups做了封装,由一个代理进程Hook Consul,发现有更时则主动更新Tengine,并提供了Web管理界面。目前正在内部测试中。

2)Docker Hook —> Marathon Hook,最早我们是去Hook Docker的events。这需要在每台宿主机上起一个Hook服务。当应用迭代时,Docker会立即产生一个create container的事件。Hook程序监控到后去更新Consul,然后Consul通知Nginx去更新。导致问题就是:容器里的服务还没启动成功(比如Tomcat),就已经对外提供服务了。这会导致很多请求失败,产生重启请求。

解决方案:Marathon Hook。Marathon中有一个health check的配置。如下图


我们规定所有的Web服务必须提供一个Health Check接口,这个接口随着服务一同起来,这个接口任何非200的http code都代表应用异常。Marathon刚启动容器时,显示此容器的Health状态是uknow。当Health Check成功时,Marathon显示此容器的Health状态Healthy,并会产生一个事件。Marathon Hook程序通过Hook这个事件,就能准确捕获容器中应用启动成功的时间,并更新Consul,同步Nginx,对外提供服务。

3)Marathon Failover后会丢失command health check,通过Marathon给容器添加Health Check时,有三种方式可以选择:HTTP TCP COMMAND


当使用HTTP TCP时,Check是由Marathon发起的,无法选择Check时的端口,Marathon会用自己分配的PORT进行Check。实际上我们并未使用marathon映射的端口。我们选择了COMMAND方式,在容器内部发起curl请求来判断容器里的应用状态。当Marathon发生failover后,会丢失COMMAND health check,所有容器状态都显示unknow。需要重启或者迭代应用才能恢复。







请到「今天看啥」查看全文