作者:林帆,ThoughtWorks公司DevOps技术咨询师。热衷于对DevOps和容器技术应用的推广,在容器规模化运维方面有较丰富实践经验。
本文节选自程序员杂志,谢绝转载,更多精彩,请 订阅程序员杂志
都说『下层基础决定上层建筑』,然而在互联网软件的实践里,设计规划时往往是至上而下进行的,从用户需求到技术架构,再到框架选型、方案设计、模块拆分,直到了最后才会发现是不是应该也考虑一下服务的运维设施,但毕竟那是上线以后的事情了,先实现能带来业务价值的功能再说。随着业务规模的逐步扩大,基础设施的不完善会让许多线上问题发生得措手不及,磁盘不足、内存泄露、服务器故障,设计上的缺陷以及运行环境的脆弱时刻都可能成为压垮系统的那根稻草。而到此时,由于没有必要的预警措施和预先留置的调试手段,解决故障的过程也处处捉襟见肘。
DevOps的理念一直倡导将运维设施的设计提前到服务设计的阶段,尤其是在规模不大、没有专用运维平台的产品团队中,这种理念的价值尤其显著。在运维基础设施中,服务状态和性能的监控预警是降低潜在故障风险最有效的措施,而对运行日志进行汇总收集则是在意外事故发生后快速定位问题、回溯故障现场的可靠手段。
这次我们就来聊一聊在容器技术架构中实施性能监控和日志管理的话题。
伴随着微服务架构的流行,容器和集群的运用近年来如雨后春笋般的铺开。不过,实践过微服务的企业都不难发现,微服务带来的灵活性和扩展性其实是以基础设施的复杂性作为代价的,这一点尤其体现在服务的部署、依赖和版本管理、线上调试、状态监控等方面。作为微服务的好搭档,容器技术简化了服务的部署和版本管理方面的问题,因此凡谈微服务实施的文章,十有八九也会谈及容器。
容器使得微服务的运维变得高效和轻量,然而根据我们的实际经验,容器对于微服务也是一把双刃剑。微服务系统做到一定程度必然逐步发展成分布式结构,相应的基础设施也会由单机过渡到集群。由于微服务系统中的每个单独服务都是分别部署、独立伸缩的,服务运行的位置、个数、IP地址随时可能变化,对这样的环境进行管理本来就不是容易的事情,引入容器实际上又增加了一层变数。从运维成本来看,容器的运用对于微服务集群的故障调试和监控方面不仅不能带来什么帮助,使用不得当还可能会弄巧成拙:许多曾经在虚拟机集群上广泛使用的监控和管理工具对于容器化的服务不再适用了。
容器的运维有什么玄机?这事情得从容器的原理说起。
容器对服务的隔离运行基于的是内核的Namespace和Chroot等特性,这些特性使得在容器中运行的服务拥有独立的系统环境、网络栈和文件存储空间,几乎就是一个微型虚拟机。使用者在创建容器实例时通常会为每个容器起一个直观且有意义的名字,这个信息对于排查问题时定位出错的服务十分有用。
以性能监控的场景为例,通常的实施方式有在主机上收集数据和在容器内部收集数据两种。若是在主机上收集性能数据,传统的监控工具是以系统进程为对象记录的,由于容器的场景常常会在同一个主机上部署多个独立服务,这些服务对外提供的功能各不相同,可以说是毫不相干的服务类型,但从进程的角度上看却并无差异(例如都是Tomcat进程),因此在监控系统中难以快速的区分并与相应容器实例进行关联。若是从容器内部收集数据,服务与容器的对应关系就很容易找到了,但这与Docker提出的每个容器中只有一个清楚明确任务的哲学相悖,并且大量的数据收集客户端也会增加很多额外系统开销。
日志收集的场景十分相似。用户或者将日志输出到容器的控制台,在主机上的容器日志目录里翻找容器中打印出的数据,或者将日志输出到文件,在容器里收集实例内的日志文件内容。前者会面临日志文件和容器名称对应的难题(以Docker容器为例,日志路径能够推测出容器实例的ID,但无法快速识别出实例名称),后者需要对容器镜像改造,并且有悖于单任务容器的设计原则。
由此看来,直接照搬虚拟机管理的基础设施到容器化服务的集群上是不适合的。那么如何构建一套对容器运维友好的基础设施呢?
我们且从尽可能不要重复造轮子的角度考虑,看看原有方案的问题出在哪里。首先否定在容器实例里增加额外管理服务的思路,这种做法且不说成倍的增加带宽和内存开销,未来升级这些镜像里的各种管理客户端也将非常麻烦。反观基于主机层面的管理思路,欠缺的其实是服务于所属容器的对应关联,而这些信息通常是能够使用容器服务(例如Docker)的API查询出来的。特别是对于Docker容器,在命令行工具『docker stats』或『docker inspect』的输出中就能获取到很多有用的信息。因此,一种比较有效的方法是对运行在主机上的客户端工具进行适当改造,弥补缺失的这部分数据内容。这个改造工作不算太麻烦,但十分琐碎,所幸的是,目前有许多开源的或者企业及的运维工具已经集成了容器信息的预处理,用户需要的只是做好运维工具的正确选型。
下面针对开源工具的方面,介绍几种容器友好的监控和日志管理方案。
在虚拟机运维的时代,Nagios和Zabbix等都是十分经典的性能监控软件。但在容器时代,这些曾经耳熟能详的软件以及其他过去用于虚拟机的监控工具大多不能提供方便的容器化服务的监控体验,社区对开发这类插件和数据收集客户端也并不积极。相反的许多新生的开源监控项目则将对容器的支持放到了关键特性的位置,可以说容器的应用界定了一个全新的监控软件时代,一代新人换旧人。
在这些新生的监控工具中,值得一提的是2015年的Docker欧洲技术大会(Docker EU 2015)上,Swisscom AG的云方案架构师Brian Christner推荐的基于InfluxDB或Prometheus的方案。Christner在阐述完容器监控的基本方法原则后,演示了三种比较流行且成熟的具体方案,分别是『cAdvisor』、『cAdvisor + InfluxDB + Grafana』和『Prometheus』。其中基于InfluxDB的方案是一个多种开源组件的组合方案,而基于Prometheus的方案在现场演示时,是为一种整体解决方案出现的。事实上Prometheus虽然是整体化的开源监控软件,但它本身对容器信息的收集能力以及图表展示能力相比其他专用开源组件较弱,通常在实际实施的时候依然会将它组合为『cAdvisor + Prometheus』或『cAdvisor + Prometheus + Grafana』的方式使用。
这几组方案中的Influxdb和Prometheus都是当下已被广泛认可的性能、状态及其他时间序列数据的存储和检索工具。时间序列数据指的是一类对时间关系比较敏感的数据序列,这类数据是在不同时间点上采用相同方法收集的,通过这些数据可以反映出某种特征的趋势变化,而其中出现的异常数据点往往与某些值得关注的事件直接相关。另外,由于这些数据主要是用于故障告警、趋势分析和事故后的现场回溯,因此对于越接近现在的数据需要的精度越高,有时可以采用对一段时间之前的数据数据间隔抽样丢弃的方法,以降低精度为代价减少存储空间消耗。比如说,一周内的数据进行全量的保存,一周前的数据就把精度丢掉一半,一个月前的数据把精度再丢掉一半,类似这样的处理若是用通用型数据库要做起来就要复杂得多。
cAdvisor对经常使用容器的读者来说应该不陌生,它是谷歌专为监控容器性能状态设计的一个开源工具。cAdvisor只有一个二进制文件,使用起来非常简单,只需直接启动然后在主机的8080端口就会看到一个简洁而内容丰富的监控界面。cAdvisor本身不会持久化存储数据,默认只将保存2分钟的记录在内存,更早的数据会被直接丢弃。为此,若是要对数据做进一步处理,就得定期把数据从cAdvisor采集到持久化存储服务里去。cAdvisor提供有Push和Pull两种获取性能数据的接口。Push接口指的是由cAdvisor主动将数据周期性的推送到远端的存储服务中,Influxdb与cAdvisor的对接就是通过这个接口完成的。而Pull接口则允许外部访问服务随时主动从cAdvisor获取到当时时刻的性能数据,然后自行处理,Prometheus与cAdvisor的对接用的是这种方法。此外,作为一个专用的容器监控工具,cAdvisor不仅支持多种输出方式,在输入方面也颇为灵活,能支持Docker、Systemd-nspawn和Rkt等多个标准的容器监控,具有很好的业界通用性。
Grafana是针对时间序列数据设计的一款开源数据展示面板。对于cAdvisor采集上来的数据,Grafana可以提供主机名称和容器名称两级维度的筛选,例如在一个图表中展示所有『名称包含nginx的容器』的内存变化曲线,或是所有『名称是api-gateway且运行在名字含有middleware的主机上的容器』的网络吞吐量趋势。这样查找特定服务并对其进行监控就会变得十分容易,也能缩短在出现问题之后从性能图表定位具体故障的时间。
传统的日志汇总收集方案主要有商业软件Splunk、开源软件栈ELK和Facebook开源的Scribe等,其中以ELK最为广泛使用。ELK是ElasticSearch、Logstash和Kibana三个工具的简称,经典的ELK架构如图1所示。在每一个节点部署一个Logstash服务,称为Shipper,然后收集的日志经过一个Redis缓存,在Redis后面再用另一个Logstash服务去做索引,称为Indexer,之所以有这样一个架构一方面是因为Logstash在做数据索引时会消耗较多内存和CPU,不适合在每个节点分别运算,另一方面则是集中运算方便对运算规则进行修改,而在两级数据之间若不加缓存则可能会由于Indexer处理不及时而丢失部分日志数据。
然而即使在各个节点上的Logstash Shipper仅仅完成日志采集的工作,依然会带来不可忽略的额外性能消耗,这主要是由于Logstash基于JRuby语言开发,因此在运行效率上和内存使用上存在一定瓶颈。之后Logstash所属的公司Elastic推出了基于Go语言的数据采集服务Beats,其中用于做日志文件收集的Filebeat将替代Logstash Shipper的主要功能,为以示区别,这套开源方案被称为EFK。
除了Filebeat,还有许多来自社区的Logstash替代方案,使用比较广泛的是Apache基金会旗下的Flume和2011年底才刚刚诞生的Fluentd,它们可以同时取代Shipper和Indexer的两级日志汇聚,这两种方案同样需要配合ElasticSearch和Kibana作为日志数据的存储和展示工具,因此都被称作是社区方案中的EFK。其中Fluentd在诞生后不久就由于其易用性、扩展性和高效的数据处理能力受到许多企业的青睐,亚马逊将它称为是最好的集群日志收集工具[1]。
Fluentd并非是专用于日志文件收集的,而是一个通用的信息收集、整理、转发的流式数据处理工具,日志收集只是它十分典型的一个运用场景。重要的是,Fluentd的日志收集功能对容器支持十分完备,远远胜于Logstash等传统日志收集工具。一方面得益于Fluentd社区开发了几种专用于Docker日志文件的收集插件,这些插件能够在Fluentd收集完Docker日志以后自动为它加上容器相关的信息,比较推荐其中的fluent-plugin-docker-metadata-filter这一款插件,它提供的信息颇为齐全。Logstash对于这方面依然比较空缺,GitHub上唯一能够找到的一款社区插件[2]也已经在一年前就停止开发。另一方面,当前Docker官方支持的日志驱动除了默认的使用本地目录,还可以直接发送到远程的日志存储或日志采集服务,而其中日志采集服务目前仅仅支持Splunk和Fluentd,同样没有Logstash等老一辈开源日志工具的踪影。
通过采集本地文件日志和使用Docker的日志驱动的方式各有优劣。默认情况下Docker会将所有容器输出到系统控制台的内容重定向到以容器ID命名的一个本地目录中,只需要定期采集所有这些目录的内容就能一字不漏的将容器的输出捕获出来,这种方式的侵入性很小,但由于是周期性的收集,日志在汇聚端(例如Kibana)的展示会有一定的延时,延时长度与日志收集的周期相关。相反的,如果使用Docker的日志驱动(启动docker后台服务时指定参数–log-driver=fluentd)将获得实时性很好的汇聚端日志展示,但由于日志直接发送到了远端的Fluentd服务,会使得在本地主机上的docker logs命令失效。两种方式的共性在于:不论通过哪一种方式,收集到的日志都能够以容器名称、镜像、标签等对容器使用十分友好的维度进行检索。
新技术的大规模流行往往会带来两面性的变革,云技术如此、大数据如此、容器技术也属此列。作为一项正在被越来越多企业所接受的软件打包、分发和部署标准,容器式服务的运维正在成为传统运维团队面对的新挑战。
容器作为DevOps的催化剂,正在为这场欣欣向荣的运动推波助澜。大浪淘沙,Influxdb、Prometheus、Fluentd…这些诞生不过几年的新生事物,借着容器化的百丈浪花,撼动着曾经扎根深厚的运维设施基石。技术的更迭方式可以是潜移默化的和平演变,亦或是轰轰烈烈的武装革命,容器技术应该归属于后者。可以预见在更高效和轻量化的运维实践之后,容器还将为整个IT领域注入更多新鲜和活力,让我们拭目以待。
[1] https://dzone.com/articles/amazon-recommends-fluentd-best
[2] https://github.com/itzg/logstash-filter-docker_container