Docker 在 B 站的实施之路

2,389 阅读13分钟
原文链接: mp.weixin.qq.com


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。需要重启或者迭代应用才能恢复。

Q&A

Q:你好 问下贵公司的自动扩容是针对应用的吧 有没有针对Mesos资源池监控并做Mesos Agent的扩容?

A:目前的自动扩容是针对应用的。Mesos Agent扩容时,先把物理机信息录入PaaS平台,手动在PaaS平台点击扩容,后台会调用Ansible,分钟快速级扩Mesos Agent。

Q:现在是确定Nginx+UpSync+Upsteam check是无法一起用的么?贵公司的Nginx版本是多少哇?

A:测试过Nginx 1.8和1.10,确认无法一同编译。我们用的最多的Nginx(SLB)是Tengine 2.1.1,部署在Docker上。

Q:既然是封装, 那底层用Mesos比Kubernets并没有太大的灵活性吧?

A:对于PaaS平台,目前我们希望的只需要资源调度这个功能,其他功能我们还是希望可以自己实现,而Mesos是专注于调度资源,而且已经历经了大量级的考验。而Kubernetes目前提供的很多服务,我们并不需要,所以选择了Mesos。

Q:容器是采用Monitor Agent监控,那容器内的呢?也还是内部埋点?还是EFK吗?监控是采用Prometheus吗?

A:Prometheus没有使用,我们是用自己的监控Agent -> InfluxDB。容器内有多种监控方式。有用ELK,也有其他埋点,比如StatsD,基于Dapper论文实现的全链路追踪。

Q:网络选型这块,还调研过其他网络方案吗?譬如Calico、Weave等,为什么会选用Macvlan?

A:我们的选型第一步是先选择标准的,要从CoreOS主导的cni还是Docker官方主导cnm里面选择,目前由于我们容器方案还是走的Docker,所以选择了cnm,那从cnm的标准里面的选择基本是:1. 基于XVLAN的Overlay;2. 基于三层路由的Calico;3. 基于二层隔离的Macvlan,实际以上的方案我们都调研使用过,基于希望尽量简单的原则最终选型还是Macvlan。

Q:Bili PaaS平台,自动扩容和手动扩容,应用多的是哪种方式?自动扩容后,资源会重调度么?是否会中断已有业务呢?

A:用的更多的是根据制定好的策略,自动扩容。通过Nginx 动态Upstream对外提供服务,不会中断业务。

Q:关于日志收集每个容器里都跑一个Logstash吗?好像ELK不能搜索展示上下文的啊?

A:容器里面没有跑Logstash。目前是在远端的Logstash集群上监听一个UDP端口,应用直接把日志推送到Logstash的UDP端口,然后Logstash把日志推送到Kafka,Kafka的消费者有两个,一个是Elasticsearch,一个是HDFS。一般用ELK足以。需要详细日志时,由运维通过HDFS查询。

Q:我想请教下Nginx的一些动态配置文件是封装在容器内部了?还是通过volume的方式挂载了?有没有配置中心类似的服务?这块想了解下是怎么实现的?

A:Nginx的Upstream是从Consul动态获取生成在本地的,通过Volume挂载,持久化到宿主机。有配置中心。业务Docker化时,就会推动业务配置接配置中心,Docker中不在保存业务依赖的配置。

查看图片
查看图片