剑客
关注科技互联网

DockOne微信分享(八十):Docker在B站的实施之路

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

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

DockOne微信分享(八十):Docker在B站的实施之路

为什么选择Mesos?

K8s太重,功能繁多。我们主要看中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。如下图

DockOne微信分享(八十):Docker在B站的实施之路

ipam plugin在每台宿主机上都存在,通过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的方式有三种:

a.sock files are UNIX domain sockets.

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

c.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包和镜像。

DockOne微信分享(八十):Docker在B站的实施之路

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

DockOne微信分享(八十):Docker在B站的实施之路

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

DockOne微信分享(八十):Docker在B站的实施之路

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

DockOne微信分享(八十):Docker在B站的实施之路

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

DockOne微信分享(八十):Docker在B站的实施之路

这个设置是指,每次迭代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。这里着重讲解下基于规则的自动扩缩容。

自动扩缩容依赖总架构图中的几个组件:monitor 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 + upsync替换为tengine + dyups。Dyups的弊端就是upstream信息是保存在内存里的。Reload/Restart tenginx时就会丢失。需要自己同步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的配置。如下图

DockOne微信分享(八十):Docker在B站的实施之路

我们规定所有的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

DockOne微信分享(八十):Docker在B站的实施之路

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

Q&A

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

A1: 目前的自动扩容是针对应用的。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比k8s并没有太大的灵活性吧?

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

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

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

Q:网络选型这块,还调研过其他网络方案吗?譬如 calico,weave等.为什么会选用macvlan? **

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

Q:我想请教一下关于volume的管理尤其是b站,您那边具体是用什么方式去实现的,在这方面遇到过哪些坑和有什么经验可以传授么?**

A:我们目前覆盖的场景主要是无状态的业务。对于volume,我们暂时没有使用。容器里的日志也都打到了远端的ELK。

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

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

Q3: 关于日志收集每个容器里都跑一个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中不在保存业务依赖的配置。

以上内容根据2016年9月27日晚微信群分享内容整理。分享人 武安闯,Bilibili 运维工程师,目前主要负责B站运维和业务Docker化的实施。一直关注于Docker的发展,Docker与Mesos结合的容器平台实施 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesz,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址