DockOne微信分享(一零九):中小型团队的容器化之路


【编者的话】GrowingIO是基于用户行为的新一代数据分析产品,提供全球领先的数据采集和分析技术。企业无需在网站或APP中埋点,即可获取并分析全面、用数据驱动用户和营收的增长。为了应对高速变化的业务增长,我们在系统设计之初就采用了微服务的架构,获得良好的可扩展性。随着时间的推移,微服务的缺点也渐渐体现,人肉运维的成本太高,导致研发效率下降。作为一个中小型团队,我们经过了半年的探索,使用容器相关技术来搭建团队内部的私有PaaS,取得了良好的效果。

作为一个创业公司我们时刻面临着业务的快速变化和规模的快速增长,为了获得良好的可扩展性,团队在系统设计之初就采用了微服务的架构。但是随着时间的推移,微服务的缺点也渐渐体现:
  1. 系统架构过于复杂,随着时间推移运维成本会逐渐增加。
  2. 传统的运维方式可复制性不高,环境迁移和横向扩展带来的成本过高。
  3. 资源利用率低,大量的专有机器。


这些问题都会导致研发效率下降。为了解决这些问题,经过了半年的探索,团队使用容器相关技术来运行微服务的应用,取得了良好的效果。在继续分享前,我先简单介绍一下我们的技术背景,我们是一家大数据创业公司,整个技术组织架构分为三层,分别是前端,服务端,数据端。前端使用react框架,服务端和数据端统一采用scala技术栈,服务端采取微服务的架构。

迈出第一步

容器化的第一步当然是把应用打包成一个镜像,我们尝试了两种方案。第一种是比较常见的做法,把编译环境放到容器里,比如下面这个镜像的Dockerfile。这个方案的好处是能做到百分之百复现一个镜像,因为没有任何外部依赖。坏处也很明显,对于依赖非常庞大的语言,比如Java和Scala,这种方案的编译速度非常慢,尤其是Scala本身的编译速度并不快,这种方案对我们来说太重了。
屏幕快照_2017-03-09_下午10.05_.45_.png

第二种方案也是我们现在采用的方案是在宿主机打包,镜像只提供基础的运行环境。这种做法的优点是速度和原生编译一样,缺点是对宿主机的环境会有一定的要求。因为我们的团队规模不大,所以固定配置几台打包的机器完全是可行的,相反如果开发人员因为要把应用放到容器里运行,就得牺牲大量的时间在编译上,肯定是无法让人接受的。
屏幕快照_2017-03-09_下午9.55_.46_.png

管理配置

应用容器化后碰到的第一个问题肯定是配置管理问题。总结一下我们尝试过的两种方案:
  1. 使用专门的配置管理中心,比如etcd,Consul。应用需要编写配置管理逻辑,需要额外开发工作。应用和环境多了以后使用界面来回切换编辑非常笨拙。好处是能够做到实时配置的更改。
  2. 使用环境变量,大部分语言和工具都会对环境变量提供支持,比如Typesafe的config和Openresty的conf,就算不支持也可以在应用启动的时候通过entrypoint脚本对配置文件进行渲染替换。启动后无法再更新配置,配置更改后需要重新启动应用。好处是工作量比较轻,结合具体的编排工具可以很方便的管理不同环境的配置。


最后我们结合了两种方案的优点,Consul管理需要热更新的配置项,大部分固定的配置通过环境变量注入。
屏幕快照_2017-03-09_下午9.55_.52_.png

test.jpg

选择一个编排工具

刚开始测试容器的时候,大部分情况都是开发人员自己编写一些脚本来控制容器的分发,创建和销毁。没过多久就发现亟需一个统一的编排工具来规范操作,减少自己造轮子的情况。我们把目光放在了业内最流行的三款编排工具上,他们分别是Docker官方支持的Swarm,源自Google的Kubernetes、Mesos资源调度框架Marathon。

选工具是非常主观的事情,不管一个开源项目本身宣传的理念有多好,最终都要实际上手一遍才会知道和自身的实际情况是否匹配。这里我们先抓取了这三个项目在GitHub上的一些信息,结合自身的实践来做一个以我们团队视角的主观评价。
屏幕快照_2017-03-09_下午9.56_.03_.png

Swarm:架构最简单,部署最快,关注度随着Docker的官方支持上升较快。缺点是Docker团队刚投入研发没多久,没有大规模经过生产考验。同时最新的Swarm和Docker绑定在了一起,升级是个问题。

Kubernetes:过去的一年里编排工具最闪耀的明星肯定是Kubernetes,Kubernetes受到的关注度和他的竞争对手完全不在一个量级。Kubernetes的理念非常适合微服务的架构,Pod,Service,Cluster IP,ReplicationController。然而我们在实际使用后发现复杂度过高,组件过于分散,调试起来非常麻烦。去年Kubernetes推出了kubeadm,很大程度的降低了部署的复杂度。但易用度上和Swarm以及Marathon都还是有差距。让我们放弃Kubernetes的另一个原因是Kubernetes对大数据的支持力度不够,从长远来看我们需要做大数据应用的容器化。虽然最近听到社区有Kubernetes-Mesos这种企图将两者的长处合并的项目,不过现在还完全不成气候。

Marathon:与单台主机的调度问题相比,跨集群之间的调度会更加复杂。单台主机的调度重点关注少数CPU上如何运行尽可能多线程和进程的问题,保证单个进程不会运行太长时间并且确保进程能命中资源。分布式背景下的调度问题要复杂的多,因为主机之间的网络交互会有延迟,和Kubernetes不同,Mesos是以资源为基础进行调度,关注点在资源分配上。Mesos本身只维护集群资源,然后基于优势资源公平算法决定把资源分配给哪个服务框架。所以Mesos不关心资源分配的细节,细节都交给二次调度框架。Marathon就是一个构建在Mesos之上的二次资源调度系统,Marathon的架构非常简单,是典型的master-slave架构,本身依赖ZooKeeper实现HA。
屏幕快照_2017-03-09_下午9.56_.07_.png

Marathon的开发时间是最早的,在2013年就开始研发。同时也是最早到达生产可用的,使用Marathon的大公司最著名就是推特了,稳定性方面有保证。另一个很重要的原因就是之前提到的,Mesos和大数据应用,比如Spark集成非常友好,Mesosphere最近推出基于Marathon的DCOS就是抓住了这个卖点,将业务系统和大数据系统跑在同一个集群上。这对成本压缩非常有好处,因为这两类系统的资源利用时间正好错峰,业务系统一般在白天达到请求的峰值,而大数据系统则在夜晚才会开始计算当天的数据。同时Marathon本身是Scala编写的,UI是React编写的,前面也介绍过我们团队的技术背景了,和我们的技术栈非常吻合,有利于二次开发。

Marathon特性介绍

在Marathon上部署一个应用非常简单,你可以选择在界面上编辑,也可以使用它提供的REST接口。实际测试下来ui还是有些不稳定的bug,虽然在UI上操作很直观但在生产环境肯定不能使用有bug的UI。所以我们还是选择使用脚本来控制应用的更新,使用JSON文件存储应用的定义。
屏幕快照_2017-03-09_下午9.56_.12_.png

这里有个坑提一下,新手在部署的时候可能会经常碰到应用的状态显示waitting。这是因为Mesos无法响应Marathon的资源offer,这个时候要检查资源配置,比如机器的CPU是不是不够用,另一个常见的原因是Mesos把端口当成一种资源,如果Mesos Slave节点在启动的时候没有申明自己的端口范围,很可能应用指定的端口不在默认端口范围里。所以需要在启动的时候指定端口资源范围--resources='ports(*):[1-32000]'

约束功能

为了防止应用在你的资源池里随意漂移,Marathon提供了约束功能。Hostname是一种常见的约束,你可以指定应用跑在一台特定IP的机器上。这点在团队正在向容器化过渡的期间非常有用,我们只是把原来部署在机器上的应用跑在容器里,其他的条件还是和原来一致。
屏幕快照_2017-03-09_下午9.56_.18_.png

另一种约束是基于lable的,你可以在Mesos Slave启动的时候指定这台Slave的lable集合。这样我们就可以在调度的时候指定特定的lable。如图所示,指定这个应用跑在机架编号1-3的机器上。
屏幕快照_2017-03-09_下午9.56_.22_.png

弹性伸缩

弹性伸缩是根据业务需求和策略,自动调整其计算资源,达到优化资源组合的能力。在业务量上升时增加计算能力,当业务量下降时减小计算能力,以此保障业务系统的稳定性和高可用性,同时节约计算资源成本。

弹性伸缩又分为水平伸缩和垂直伸缩,水平伸缩指的是增加更多的机器,增强应用的计算能力来支撑增加的访问量。垂直伸缩指的是增强单机的配置,比如升级CUP核数,或者增加内存,扩展性有限。

无状态的应用,比如一般的Web应用,把数据存储在数据库和缓存中间件里,在容器化后不依赖具体的机器,这种应用非常适合水平扩展。编排工具最重要的特性也正是提供这种水平扩展的能力,配合前面介绍的约束功能,只需更改实例个数的数值就可以达到水平伸缩的目的。
屏幕快照_2017-03-09_下午9.56_.27_.png

服务发现

服务发现通常有两种做法,第一种是基于DNS的实现。第二种是在服务前面挂一个Proxy。对应的Marathon分布提供了Mesos-DNS和Marathon-LB两个工具。Mesos-DNS提供服务名和IP端口号查询服务,没有负载均衡功能。Marathon-LB监听Marathon的事件总线,提供基于端口号的TCP负载均衡。
屏幕快照_2017-03-09_下午9.56_.31_.png

因为DNS需要自己实现负载均衡功能,所以我们选择了LB的方案。lb本身是一个HAProxy,通过监听Marathon的事件总线,LB会动态更新配置文件以达到服务发现的目的。

上面提到lb是基于端口的负载均衡,也就是说LB本身是通过端口来区分不同的服务的。打开LB的9090端口,可以看到LB的负载均衡信息,包括对外的服务端口和对应的后台服务的实际IP和端口号。
屏幕快照_2017-03-09_下午9.56_.37_.png

LB通过Marathon获取服务的具体IP和端口,也就是说我们要保证Marathon能正确获取服务的端口,在网桥模式下我们会指定Container port和host port,但是在host网络模式下没有映射信息,需要我们显示的声明服务需要的端口号,即定义port同时标注我们需要这个端口资源。
屏幕快照_2017-03-09_下午9.56_.42_.png

健康检查

应用的健康检查是保证服务稳定非常重要的一个功能,通常是编写脚本定期的去轮询健康检查的接口,如果发现接口返回异常则采取相应的措施,比如重启。Marathon也提供类似的健康检查功能,功能非常强大。支持多种协议,包括http/https/tcp,也可以自己编写shell命令进行检查。支持设置应用的启动时间,在这段时间内会忽略健康检查。支持超时设置,检查的时间间隔设置,最大失败次数设置。
屏幕快照_2017-03-09_下午9.56_.47_.png

健康检查只是第一步,重要的是如何应对健康检查的结果,Marathon提供了一套非常完整的应对机制,下图是Marathon对于集群不同健康状况采取相应措施的状态转移图。
屏幕快照_2017-03-09_下午9.56_.53_.png

简单解释一下:
  • i代表设置实例数目,r代表集群中正在运行的实例数目,h代表健康的实例数目。
  • "r<i" 表示运行的实例数目少于设置的实例数目,Marathon会进行扩容操作。
  • "h!=r ^ r=i" 表示实例的运行数目和设置数目一致,但是实例中存在不健康的实例,所以Marathon会销毁不健康的实例,此时集群到达 "r<i" 这个状态进行扩容操作。


如果扩容非常顺利,所有实例都是健康的,那么集群会达到 "r=i ^ h=i" 这个整个集群健康的稳态。

稳态并不是一成不变,如果运行的实例因为种种原因变得不健康了,即 "h!=r",那么marathon也会进行销毁操作剔除不健康的实例,集群再次进入扩容态。

更新策略

Marathon 可以通过设置upgradeStrategy里的mininumHealthCapacity和maximumOverCapacity参数调整升级最小和最大的在线实例与预设值的比例。假设你有一个web应用,在升级过程中你希望集群中保持一定比例的健康实例,这个场景你就可以设置mininumHealthCapacity为一个大于0小于1的小数,Marathon会保证集群中存在大于等于这个比例的健康实例。另一个可能在测试环境碰到的问题,很多应用本身会有启动失败的情况,如果不采取措施,很快你的集群里就会充满大量已经挂掉的容器。Marathon提供了一个重启延迟的功能可以避免上述问题,通过设置backoffSeconds和backoffFactor这两个参数,我们可以控制应用的重启时间间隔。
屏幕快照_2017-03-09_下午9.56_.58_.png

基础设施

在容器化之前团队采用Zabbix做基础服务的监控和告警,一部分错误日志也通过Zabbix Agent直接抓取。Zabbix虽然经常发生一些莫名其妙的小问题,不过总体来说是一套可用的低成本方案。最近两年很多新的运维工具渐渐出现,这一代的工具通常都是遵循单一职责设计的,采集,过滤,存储,展示,告警,每个环节都有多种方案可供选择。相比于Zabbix这种单体应用要灵活许多。

日志方案采用了传统的ELK技术栈,在容器日志收集这块使用了一个小工具:Logspout来进行容器日志的转发。Logspout通过监听Docker deamon的socket地址来获取所有运行容器的stdout和stderr信息,它本身不存储日志,准确的讲是一个日志路由工具,对同一条日志可以往多个目标转发。Logspout本身有一套扩展机制,可以自行编写插件。
屏幕快照_2017-03-09_下午9.57_.04_.png

把所有日志当做事件都往stdout和stderr打,然后通过统一的日志处理系统集中处理,这种方案的好处是可以借用工具强大的搜索和分析能力快速得到想要的信息。坏处是如果日志处理不当,很可能会造成大量的垃圾信息,反而阻碍了故障排查。所以一条有效的日志信息应该包含以下几个方面:
  1. level:信息等级 INFO\WARM\ERROR
  2. host:服务器ip
  3. application:应用名
  4. file:代码文件名
  5. line:文件内第几行
  6. date:时间
  7. content:内容


屏幕快照_2017-03-09_下午9.57_.09_.png

指标监控尝试了InfluxDB和Prometheus,两者都非常容易上手,InfluxDB的集群版是收费的,相比而言Prometheus采取支持第三方分布式存储的做法,同时支持告警,开源社区也比较活跃,所以最后还是选择了Prometheus。

Prometheus采取pull的模式周期的去拉export的数据,exporter本身非常简单,只负责把数据转成Prometheus的格式暴露到端口。所以Prometheus和exporter之间完全是松耦合的,任何一个组件挂了都不会对现有的系统造成额外的影响。Prometheus本身提供非常多的exporter,可以监控容器,主机,JVM等信息。对于我们而言不太希望在节点上安装太多的exporter,InfluxDB提供的Telegraf很好的解决了这个问题。telegraf集成了非常多的监控来源,同时支持Prometheus作为输出端。

Prometheus的功能非常强大,可以支持指标的多种数值运算。查询方面提供了一套DSL即promQL,支持任意维度的过滤,过滤规则支持正则表达式式。甚至我们还可以使用各种函数,对指标进行求速率,求和,取整等操作。Prometheus的查询功能非常强大,本身也有一个简单的UI,不做作为监控系统的展示层来说还是太弱了。同时我们也需要把每个系统零散的监控信息汇总起来,这样在排查问题的时候就只需要查看一个平台即可,不需要来回切换浪费时间。

Grafana是一款专注展示的开源软件,Grafana本身支持插件机制,可以通过社区大量的开源插件来完成信息集成。(推荐使用grafana-xxl,可以省去安装插件的过程)下面就是Grafana集成Prometheus的监控界面,同时Grafana支持Elasticsearch的集成,这样就可以将指标信息和日志信息集中到一个看板里。
屏幕快照_2017-03-09_下午9.57_.22_.png

告警

Alertmanager是一个Prometheus提供的告警管理组件,Prometheus周期性进行抓取数据,完成抓取后会检查是否有告警规则并进行计算,满足告警规则就会触发告警,发送到Alertmanager。配置报警规则非常简单,使用指标的表达式,需要指定触发报警的最大持续时间,同时可以设置报警自身的标签方便后续的路由规则设置。
屏幕快照_2017-03-09_下午9.57_.29_.png

Alertmanager支持报警规则的路由,可以定义多条路由将不同标签的报警分类发送,支持报警静默规则的配置,报警渠道支持邮件,短信,webhook。我们最常用的是Webhook,接入团队的im工具可以接收到实时的告警信息。
屏幕快照_2017-03-09_下午9.57_.32_.png

下面是监控系统的整体架构示意图:
屏幕快照_2017-03-09_下午9.57_.14_.png

总结

简单小结一下我们团队在进行容器化探索的过程中得到的一些经验:
  1. 在进行技术选型的时候要结合自身的实际情况,最流行的不一定最合适。
  2. 不要盲目相信一个工具能解决所有问题,要给自己留出退路,最好选择开放的,可扩展的工具。
  3. 应用容器化后会带来一些负面影响,团队需要改变以前的运维策略,如果基础设施没有到位不要急着上生产。


Q&A

Q:我有几个问题请教: 1.底层是否使用了虚拟化? 2.技术选项时没考虑Rancher? 3.健康检查i是否可以动态设置?

A:底层使用了虚拟化,考虑过Rancher,但是当时太早Rancher非常不稳定,后来还是放弃了,前几天线下跟他们团队交流过貌似重写了,值得大家试试。健康检查的i是逻辑上的,健康检查本身没有这个配置项,i可以通过Scale功能随时调整。
Q:请问你们是否使用了CI/CD工具?如何贯穿整个应用的周期的?

A:这个问题太大了,可以另开一个头了。 我们使用Jenkins做CI,生产的CD还有达到,发布的时候都是手动发布人盯着。
Q:我们目前在用你们的产品,只要用来统计安卓和IOS端的用户行为数据,我们也有自己的容器,并且编排工具是Kubernetes,目前其他还算稳定,就是偶尔出现资源分发时不同集群的状态不一致,请问能否用Marathon去替换解决,是不是代价有点大?

A:我的建议是继续使用Kubernetes,Marathon并不是完全没有bug,使用过程中同样会有坑在里面,Kubernetes发展很好,只不过Marathon对我们而言更合适。
Q:问题1. 数据库有用到容器吗?需要注意什么?问题2. 生产环境镜像更新,镜像较大,然而每次只更新一个文件,有什么好的建议?

A:问题1. 测试环境有用到数据库的容器,主要是部署比较方便。生产没有用到,还是性能问题吧。问题2. 基础镜像要选好,不要把容器当虚拟机用,JVM的语言打包出来算大了,也可以控制在200M左右。
Q:你们是针对每个环境都打包不同的镜像?还是使用同一个镜像?配置管理怎么做的?可以做到配置动态加载吗?

A:不是针对每个环境打不同的镜像,是把应用本身对环境有要求的选项做成动态的,通过环境变量注入。动态加载需要自己编写工具通过Consul实现。
Q:你们研发的程序是怎么微服务化的呢?怎么用容器提高研发的效率?

A:微服务的话题也很大,我们在拆分服务的时候都会再三思考有没有必要,拆分带来的好处是什么。比如说我们把认证这块单独拆分成一个服务,这是根据功能拆分。还有为了提升性能的拆分,不同资源倾斜的拆分。容器带来的最大好处就是打包环境,能做到快速部署。
以上内容根据2017年3月7日晚微信群分享内容整理。分享人林生生,GrowingIO服务端研发工程师。从事GrowingIO核心系统研发。奉行DevOps,对一切能提高研发效率的技术痴迷。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesz,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

0 个评论

要回复文章请先登录注册