DockOne微信分享(一零四):艺龙部署体系的演进


【编者的话】随着公司业务规模的逐渐扩大,传统的基于机器层面的部署系统在面对服务扩缩容、故障迁移、成本控制等方面已经越来越力不从心,于是,我们开始将容器技术与当前公司内部已有的自动化运维体系相结合,来实现一套艺龙的容器云平台,以期解决上述问题。

原有部署平台的架构设计

在讲解部署系统之前,我先给大家讲一下目前艺龙自动化运维的业务模型。早期艺龙的运维,因为规模较小,通常人工处理,当规模大了以后,就开始尝试基于Puppet等脚本的方式操作。随着规模的进一步扩大,逻辑需求的增加,使得原有的这种基于行为的模型很难更高效的进行管理。

因此,我们抽象出服务这个概念,用户对服务做出定义,服务之间采用树形关联,服务节点上挂载元信息,而机器也是元信息之一,因此,新的模型就变为人去定义服务,而服务去关联机器。无论上线,监控,还是日志收集,都是基于服务粒度的,而不再需要关注底层的机器。

在服务树的元信息之下,是这套系统的核心部分,控制系统。控制系统有两部分,一个是由erlang-OTP编写的控制中心,它负责将任务下发给像相应的客户端,并控制任务运行的流程,比如串并行、暂停恢复等,一个是每台机器上运行的客户端,负责具体执行逻辑。

控制中心采用了我们自主研发的一套基于Raft的分布式库进行开发,整体采用主从结构,主写从读,保证强一致性,当主节点宕机后,执行中的任务仍然能无缝的迁移到从机。客户端执行的任务是以插件形式开放的任何可执行应用,控制系统整体平台化,依赖于这个特点,我们在控制系统之上开发了很多业务系统,比如部署系统,就是通过部署插件来完成的。
屏幕快照_2017-01-17_下午10.10_.25_.jpg

控制系统图

我们基于控制系统开发了一个部署插件,用户将已经打好等待部署的代码上传到FTP上固定的位置,然后调用部署API对某个服务触发一次该版本的部署操作,控制系统就会根据用户定义的控制策略下发给相应的服务器上的客户端,客户端执行部署插件,由插件进行包下载、解压、部署、启停等操作,完成整个上线。
4CVB_1_))`(51RTZ73P{8U.jpg

上线图

我们的整个部署架构如下:
屏幕快照_2017-01-17_下午10.14_.19_.png

部署架构图

基于容器化的部署平台架构设计

当前的自动化部署体系已经能够实现系统的快速部署,但是,由于公司规模的发展,服务的规模越来越大,硬件设备购买和运维的成本也都越来越高。同时,对于电商网站,经常会遇到比如黄金周,双11,抢红包这种请求量的突增的情况,对于系统能够随时快速的扩展,以及过后机器快速回收就提出了要求,因此,为了更进一步,我们要解决两个问题:
  1. 弹性扩缩容
  2. 控制规模化成本


通过解决这两个问题,我们想在最终够实现,在任意云平台或IDC,做到全服务快速迁移的目标,也就是将艺龙的运维体系打造为可以依托任意IaaS的混合云模式。

对于服务的快速迁移部署,最终会落地到两个关键问题的解决上:即上下游依赖,以及环境依赖。我们通过容器化应用以及名字服务来解决上述两个问题。

对于一个已经有一定体量以及基础架构设施层面已经比较完善的公司而言,推广层面的问题要比技术层面的问题更加棘手,我们有万这个级别的机器在线上运行,因此并没有直接使用Kubernetes之类,因为改造、迁移成本过高。因此,我们将Docker与当前自动化运维体系中的各个业务系统打通,从而减少业务的迁移成本。我们希望的是将Docker视为一种打包方式,而不是重度依赖整套体系。我们的目标是在解决业务痛点的情况下,简化设计,最小依赖,降低迁移以及运维复杂度。

最终,我们的系统架构转变如下:
屏幕快照_2017-01-17_下午10.14_.23_.png

部署架构图 2

从图中我们可以看到,我们的改造很简单,在原有体系的基础上,增加了三个主要部分:

第一个是镜像构建系统,用来屏蔽掉底层容器,我们依旧复用了FTP以及全量部署的结构,改变的仅仅是打包方式。用户操作流程不变,只是在触发部署操作时,系统会检测该代码版本是否有已经构建完毕的镜像,如果有则直接部署,如果没有,则进行构建。

另外一个增加的部分是资源调度系统,这个系统只去做资源调度,也就是资源的筛选和分配。它直接和运维的CMDB挂钩,负责管理公司所有的机器。用户部署时描述资源需求,资源调度根据需求分配机器,然后下发控制系统进行部署。

我们并没有做动态调度,也没有去做故障的自动迁移、资源自动调整迁移等,因为对于我们来说痛点是快速迁移。并且目前整个系统处于推广中,稳定性重要于功能性,尤其是网络抖动等层面非常容易导致动态调度工作异常。

第三个就是每个机器上的插件了,我们新开发了容器部署插件,主要功能就是在机器上获取镜像,然后启动容器。

根据我刚才描述的整个流程,我们做到了在业务无感知的情况下,将基于机器的部署转变为基于容器化的部署,当然,在这一过程中我们还打通了监控、配置管理、日志收集、门神(安全登录)等等,就不细说了,有兴趣的可以私下沟通。

上面我主要描述了对于环境依赖我们的解决方案,接下来,我来给大家介绍一下我们的上下游依赖解决方案。

服务发现与名字服务

对于服务发现层面,我们总结了几种常用的手段:
  1. 基于配置文件,类似Confd之类的配置文件模板方式
  2. 基于SDK
  3. 基于getHostByName+DNSd方式


我们起初最先考虑的是基于SDK的方式,服务注册与发现统一由一个类ZooKeeper的服务提供,这里可以稍微提一下的地方就是,我们采用的是一个基于Raft的Pyxis服务,也是我们部门自研的服务,并没有用开源的ZooKeeper以及etcd。

一是由于ZooKeeper基于Java编写,在GC层面存在一定的问题,数据量过大时候会导致服务可用性降低(我们希望基于它同时做配置管理),二是我们阅读了它的源码,它的Recovery在检测不一致时处理也比较暴力,同时ZAB协议较复杂,这个从它的论文上就可以看出来,因此运维起来成本较高。

而etcd在我们调研Paxos以及Raft时还刚刚起步,用户较少,因此在当时我们选择自己开发一套类似于ZooKeeper的分布式协调服务。

回到刚才的话题,如果是基于sdk的方式,那么必然要求对调用方甚至被调用方的程序要有一定的侵入性,这种侵入性存在一定的风险,而且公司内部目前也存在着不同的语言版本(PHO/C#/C++/Java......)、甚至不同的操作系统环境(Windows/Linux),推广起来难度极大,因此,SDK的方式最先被否决。

最终,我们决定采用研发一个名字服务Agent来实现1、3两个方案,即配置文件以及DNSd的方式来解决上下游依赖问题。

我们的名字服务Agent运行在所有的Docker容器以及每一个拥有服务的物理机/虚拟机上,提供服务注册以及发现的功能,每个服务需要编写它的描述文件,其中描述了服务的名字、端口、用户的配置文件存放路径、用户的配置文件模板存放路径等信息。

同时,我们要求每个服务必须有4个脚本文件,即启动脚本、停止脚本、服务健康监测脚本以及服务Reload脚本。当服务启动时,Agent就会根据描述文件的信息将服务作为一个服务实例的Active状态节点注册到相应的服务名字下,同时定期调用健康监测脚本来作为踢出负载和报警。

服务启动时用户会在配置模板内描述要关注的名字,当名字对应的机器列表发生变更时,Agent便会拉到最新的名字对应的实例数据,然后实例化模板为新的配置文件,并调用应用的Reload脚本(类似Confd)。

通过配置文件的方式,我们实现了七层负载均衡与名字服务打通,实现了基于Nginx层面的上下游服务解依赖。

我们提供的另一种方式就是基于DNS的方式,即我们对每一个用户创建的服务名字自动创建一个域名,比如我们有个服务是hotel,该服务的名字也叫做hotel,那么我们就会为该名字生成一个类似域名hotel.ns.corp.elong.com的域名。

我们将名字服务与DNS服务器打通,DNS服务器会根据某个域名对应的服务名字下的实例在Pyxis上的存活状态来变更该域名对应的IP列表,而同时,Agent也在这个里面起到了另一个角色,就是本地的DNSd,他会Cache域名的状态,用户的DNS请求会先落到本地的Agent上,Agent根据名字服务的数据进行解析,如果不存在,在继续访问真实的DNS服务器,如果存在,则返回名字服务上该域名对应的名字的实例列表。

一个访问流程如下,UI是用户直接干预基于名字服务的方式。
屏幕快照_2017-01-17_下午10.25_.39_.png

上述就是我们名字服务的整个工作方式,同时,我们还在这套系统基础上设计诸如同机房流量优先调度、用户强制介入的流量调度、名字服务结合nginx层实现的负载均衡、灰度流量调度等等。

遇到的问题以及解决方案

因为我们对Docker层面的依赖较少,所以就使得我们在这一层面遇到的问题也较少,我整理了两个大家可能都遇到过的问题:

1. 容器rootfs文件系统问题

我们最开始采用的是默认的device-mapper方式作为容器的rootfs,如果应用没有大量的IO操作还好,但是毕竟不是每个用户都能按照最佳实践去做,因此早期经常由于某个服务大量占用IO资源而导致整个宿主机的容器都被影响,

而且由于dm的特性,IO比较差,很容易占满,而且我们也没有那么多的资源去深入调研类似于device-mapper这些分层的文件系统,所以考虑了我们的业务场景,同时我们又是基于ftp的全量拉包,就暂时使用了VFS作为容器的rootfs。

2. 网络问题

我们早期使用了默认的基于NAT的方式,结果遇到了非常多的问题,比如我们在做HTTP的压测时,发现性能无论如何都上不去,看了内核的日志,发现大量的 NAT ip_conntrack: table full,才发现是TCP的Established Connection记录满了。

后来也发生过连接被莫名的关掉,或者是数据包莫名的阻塞很久等问题,debug过程非常耗精力,因此我们就干脆换掉了NAT模式,采用了桥接的模式,不但性能得到提升,而且稳定性更好,同时每个容器一个IP,出现问题也可以直接定位到某个IP,对debug也更加友好。

TO DO

1. 调度层面:我们根据目前的需求并未添加动态调度的功能,等到所有的目前的工作ready,我们会重启这一工作,实现动态扩缩容以及动态故障迁移的功能,让整个平台全自动的运转起来。

2. 名字服务Agent层面:我们目前一个容器内部包含了一个Agent,这会导致每一次名字服务Agent的升级就要牵扯到用户服务也必须跟着升级,这是很难做到的一件事,因此我们现在正在着手将名字服务Agent移到容器的外部。

我们同时也打算支持本地HTTP协议的名字服务Agent作为用户解析名字的一个方式,来代替SDK的方式,watch服务可以用本地Agent回调方式实现。

3. DNS层面的负载均衡:目前的负载均衡我们还是在用户的层面去做的,用户基于我们的系统还只能解析到IP列表,我们之后希望能够将负载均衡也集成到我们的系统中,我们的DNS服务端会做负载均衡的策略,用户解析域名直接就可以得到一个负载均衡后的IP。

4. Windows:公司内部还有一部分服务依旧运行在Windows(.net服务)上,这一方面接下来需要着重考虑。

以上就是我今天分享的全部内容,谢谢大家,大家有自动化运维层面的相关问题,都可以和我交流交流哈~

Q&A

Q:麻烦问一下,桥接的网络是给容器的分配一个物理网络的IP吗?另外依据什么来确定每台主机运行多少容器?

A:是的,我们给每个容器分配了一个物理网络的IP,我们根据目前我们物理机的规格去制定的容器数量,目前每台物理机分配上限是64个。
Q:Docker存储考虑过Overlay当时吗?据说这种构建镜像比较快。

A:考虑过,当时也做过各个方面的测试,这种增量式的构建,肯定最快,但是我们需要有专人从源码级别对其进行维护,成本对于我们还是有点高,我们后期打算采用环境和代码分离的方式,即环境部署一次,代码多次部署来提升效率。
Q:.net不是也可以跑到Linux上了吗?有.net镜像吧?

A:我们调研过,但是这个技术太新了,稳定是我们使用一个新技术的前提,而且我们的.net服务大多已经是很多年前的老服务,折腾起来比较费力,暂时.net方面我们只能搁置,但是也会继续跟进。
Q:请问没有采用ZooKeeper和etcd而选择自研的原因是什么?是否如何进行技术对比的?

A:就是我刚才说的,对于ZooKeeper来说,ZooKeeper基于Java编写,在GC层面存在一定的问题,数据量过大时候会导致服务可用性降低,我们希望用他做配置管理,我们甚至有一些上M的配置文件,最终的配置无论是配置项,还是最终大小,都会是一个比较大的量级。二是我们阅读了它的源码,它的Recovery在检测不一致时处理也比较暴力,同时ZAB协议较复杂,这个从它的论文上就可以看出来,因此运维起来成本较高。


而etcd是基于Raft实现的,Raft是14年出的论文(我没记错的话),我们就是在论文出来第一个月开始弄的,也就是说那个时候etcd也刚起步。
以上内容根据2016年12月20日晚微信群分享内容整理。分享人董春明,艺龙旅行网基础架构部容器云平台负责人,专注于自动化运维体系的技术与产品化方向。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesz,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

0 个评论

要回复文章请先登录注册