编者按:InfoQ开设新栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自林帆著《CoreOS实践之路》中的章节“激励他人的第二大障碍”,讨论了成功技术领导的一些经验。
6.1.1 案例说明
6.1.1.1 案例背景
在一个运行着数十上百种应用服务的集群的运维和应用设计中常常会遇到这样的需求:服务状态的收集,如何在集群的任意节点上快速地获取到整个集群中任意一个服务的状态呢?
这是典型的大型分布式服务监控任务。传统的解决方案一般都需要引入一个额外的监控系统,例如Nagios。这些监控工具通常会使用一个集中的数据收集和存储节点,然后利用在每个节点上的客户端收集定制数据,并发送回收集节点进行统一处理和展示,其余节点如果需要集群状态,就要到这个收集节点上获取。此外,对于容器化应用,还需要在制作容器时就将收集服务数据的客户端打包到容器里面,从而对容器内容造成侵入性。另一些解决方案是让监控的客户端安装在每个主机节点上,通过Docker服务获取容器的运行状态。这种方法比较适合采集基本的容器数据信息,例如CPU、内存、磁盘访问等,而不便于对具体的服务进行定制信息收集。
在这个案例中,需要考虑如下几个分布式服务监控的具体特性。
- 硬件故障的可能性,监控服务的系统不应该存在单点依赖的情况。
- 被监控服务运行的节点、数量均不确定,因此设计的系统应该具备足够的灵活性。
- 当被监控服务由于故障而转移到新的节点上运行时,监控应该能够继续正常进行。
6.1.1.2 方案分析
在CoreOS系统中,数据存储和分发的任务已经有Etcd能够解决,而定时采集监控的工具可以用Fleet配置一个服务脚本来完成,因此,用户只需要设计好监控数据的收集方法就足够了。
此外,在CoreOS系统中,被监控的分布式服务程序本身可以通过Fleet来管理。为了避免集中收集数据引入单点故障问题,数据的收集也应该采用分布式的方式,在被监控服务所在的节点上就近采集,然后直接保存到Etcd中。为此,可以通过Fleet中的服务依赖管理功能,为每个被监控的服务配备一个额外的“秘书服务”,这个服务就跟随着被监控的应用服务在节点间同时启动,同时迁移,同时记录下被监控服务的实时状态。这样便省去了传统的集中收集数据时判断哪个节点当前运行哪些服务的麻烦。
收集数据的定制会包含很多针对具体业务场景相关的细节,不具有普遍共性。为了不偏颇特别的应用场景,在这个例子中,采用一个最简单的服务作为监控的目标:一个Apache HTTP服务器的默认页面地单纯地检查HTTP服务是否可用。监控这个简单的服务,除了收集数据的方法以外,与监控一个复杂的服务并没有任何差别。通过这个例子,我们将体会到Etcd与Fleet搭配的轻量级服务组合能够为集群管理带来的便利性。
6.1.2 方案实施
6.1.2.1 容器化被监控的服务
由于Docker和Rkt等容器具备了集群镜像分发的特性,将服务容器化能够使得普通的服务通过Fleet的协助获得更好的跨节点调度能力。
特别是对于运行在CoreOS系统上的服务,由于系统的只读分区不可修改,加上系统本身十分精简,许多不常用的依赖库或软件都无法找到。若不通过容器运行,许多服务都难以运行起来。作为模拟被监控服务的目的,我们直接使用Docker官方仓库里的Apache镜像,并编写相应的服务Unit模板文件来运行服务的实例。
将以下文件保存为apache@.service,放到/etc/systemd/system/目录中。
[Unit]
Description=运行在%i端口上的Apache HTTP Web服务
# 需要依赖的服务
Requires=etcd.service
Requires=docker.service
# 依赖的启动顺序
After=etcd.service
After=docker.service
[Service]
# 第一次运行下载镜像较耗时,为了防止误判,关闭启动的超时判定
TimeoutStartSec=0
# 关闭Systemd在结束服务时杀死同CGroup进程的操作
# 这个操作对Docker托管的服务不适用,会误杀Docker后台守护进程
KillMode=none
# 读取CoreOS的公共环境变量文件
EnvironmentFile=/etc/environment
# 启动的命令,使用-=符号表示忽略docker kill和docker rm运行时的错误
ExecStartPre=-/usr/bin/docker kill apache.%i
ExecStartPre=-/usr/bin/docker rm apache.%i
ExecStartPre=/usr/bin/docker pull httpd
ExecStart=/usr/bin/docker run --name apache.%i -p ${COREOS_PUBLIC_IPV4}:%i:80 httpd
# 结束的命令
ExecStop=/usr/bin/docker stop apache.%i
[X-Fleet]
# 不要在同一个服务器节点上运行多个Apache服务
Conflicts=apache@*.service
在第3章中我们已经介绍了Unit模板文件,在上面这个模板文件中,使用@后面的参数作为运行时监听的端口号。在实际的应用中这是一种推荐的做法,因为它能够使得管理服务时可以很方便地通过服务名称找到服务监听的端口号。
上面模板文件中的注释已经比较详细了。这个模板文件可以适用于大多数通过Docker托管的服务。下面再简单介绍一下这个模板中的几个部分,以便用户修改这个模板以适应具体业务场景的需求。
首先,在Unit段中,Requires列出了这个服务需要依赖的其他用户或系统服务的名字,然后用After和Before(这里没有)等关键字指明这些服务的启动顺序。CoreOS会等待所有写在After区域的服务启动完成后再运行当前服务,同时在当前服务启动完成后,唤起所有写在Before区域的其他服务。
然后,在Service段中,设置了两个特别的参数“TimeoutStartSec=0”和“KillMode=none”。前一个配置之前提到过,主要是防止Docker在第一次启动时由于下载镜像时间较长,被Systemd认为失去响应而误杀;后一个配置是因为Docker的每个容器都托管于同一个守护进程下面,默认在服务停止后Systemd会尝试杀死所有属于同一CGroup下的进程,此时它会尝试去清理Docker的后台进程,结果是要么Docker的守护进程意外结束,要么Systemd始终依然认为进程没有清理干净,导致下一次启动同名的容器时出现莫名的问题。
配置“EnvironmentFile=/etc/environment”是属于CoreOS特有的用法,在CoreOS系统中,/etc/environment文件默认包含了服务器节点的公网IP地址和内网IP地址,这个文件中的变量是在每次系统启动时写入的。
$ cat /etc/environment
COREOS_PRIVATE_IPV4=172.31.14.97
COREOS_PUBLIC_IPV4=##.##.##.## <- 演示目的,隐藏实际地址
CoreOS下的许多服务都会使用这个配置文件,这个模板后面用到的变量COREOS_ PUBLIC_IPV4就来自于这个文件。
最后,在X-Fleet段中,可以看到这里配置的“Conflicts=apache@*.service”让与当前服务相同的服务进程不要调度到当前这个节点上。这样做是为了实现服务的高可用性,在出现单节点故障的时候,确保其他相同的服务是运行在集群中不同的节点上的,整个集群仍然能够继续对外提供服务。在实际应用时,还需要加上反向代理作为负载均衡节点,从而屏蔽后端服务调度对用户访问造成的影响。
对于一般的应用服务而言,这个模板的配置基本是通用的。
6.1.2.2 使用Fleet和Etcd监控服务状态
这次内容的主角,秘书服务(暂且想象她是一位貌美如花的女子)出场了。我们来一睹它的芳容。
[Unit]
Description=监控运行在%i端口上的Apache服务运行状态
# 需要依赖的服务
Requires=etcd.service
Requires=apache@%i.service
# 依赖的启动顺序
After=etcd.service
After=apache@%i.service
BindsTo=apache@%i.service
[Service]
# 读取CoreOS的公共环境变量文件
EnvironmentFile=/etc/environment
# 服务意外终止时自动重启
Restart=on-failure
RestartSec=0
# 启动的命令
# 检测被监控的服务是否运行正常,并将检测的结果用etcdctl写入Etcd记录中
ExecStart=/bin/bash -c '\
while true; do \
curl -f ${COREOS_PUBLIC_IPV4}:%i; \
if [ $? -eq 0 ]; then \
etcdctl set /services/apache/${COREOS_PUBLIC_IPV4} \'{"host": "%H", "ipv4_addr": ${COREOS_PUBLIC_IPV4}, "port": %i}\' --ttl 30; \
else \
etcdctl rm /services/apache/${COREOS_PUBLIC_IPV4}; \
fi; \
sleep 20; \
done'
# 结束的命令
ExecStop=/usr/bin/etcdctl rm /services/apache/${COREOS_PUBLIC_IPV4}
[X-Fleet]
# 服务运行在与它所监控的Apache服务相同的主机上
ConditionMachineOf=apache@%i.service
这个文件应该被命名为apache-secretary@%i.service并存放到/etc/systemd/system/目录中。如果读完这个文件依然觉得这个秘书长得有点抽象,下面就具体分析一下这个Unit文件内容。
首先,不难看出,它也是一个Unit模板(使用了 %i 占位符)。
然后,它的Unit段使用了一个特别的限定:BindsTo=apache@%i.service。这个关键字表明,这个秘书服务需要随着它监控的Apache应用服务一起被停止和重启。这一点很必要,否则这个秘书就无法正确地汇报所监控的服务状态了。
接下来,Service段中的ExecStart和ExecStop的内容是需要重点说明的地方,待稍后慢慢道来。
最后,X-Fleet段指明这个服务要与对应的服务始终保持在同一个主机节点上,这使得秘书服务能够直接就近获得服务状态,而不用去查找当前被监控服务运行的节点。
这样看来,最麻烦的地方无非就是上面这段乱糟糟的ExecStart命令了。先大略地打量一下,这个地方和ExecStop里面都使用了etcdctl,它们应该是比较关键的内容,先提出来看看。
在ExecStart中的这个命令,是往 Etcd 数据服务的/services/apache/目录下写入了一个以当前Apache服务所在IP地址命名的键,而键的内容就是秘书所记录的服务信息,包括服务所在的主机名、公网IP地址和监听的公网网卡端口号。最后的 TTL 设置是其中的精彩之处,它确保了当整个服务失效或被迁移到其他节点上时,这条记录会在30秒内被清除。
etcdctl set /services/apache/${COREOS_PUBLIC_IPV4} \'{"host": "%H", "ipv4_addr": ${COREOS_PUBLIC_IPV4}, "port": %i}\' --ttl 30;
下面这个命令在ExecStart和ExecStop中各出现了一次,它的参数比较清晰,就是移除/services/apache/下面刚才写入的那个键。
etcdctl rm /services/apache/${COREOS_PUBLIC_IPV4};
在ExecStart命令的最外层,使用一个“while true”循环将整个执行的命令包裹起来,因此除非外部因素结束这个秘书服务,否则监控的任务就不会停止。在服务本身出现故障失联时,它将会跟随着被监控服务一起停止、一起被迁移,然后又一起在新节点上继续提供服务。
再往下看,有一个“sleep 20”的行,也就是说,在被监控服务运行正常的情况下,每隔20秒秘书就会刷新一次服务状态信息(虽然在这个例子里只有IP地址、端口这些比较固定的信息,但在实际情况中所监控的信息可能不止这些)。这也使得在正常情况下,Etcd中的数据永远不会由于TTL的超时而被清除。
至此,这个ExecStart的意思也就大致清晰了。其中还没有被提到的那行curl命令,以及“etcdctl set”中需要记录的内容就是实际监控服务需要定制的部分。这个例子里的Apache其实是监控的最简单情况。
简单归简单,但稍加思考就不难发现一个问题,要求用户自己管理启动两个有依赖顺序的服务?显然这里有些不合理的地方。
6.1.2.3 关联秘书服务
刚刚在写Apache应用服务时,由于还没有秘书的存在,我们只考虑了应用服务自己的启动依赖。然而,一个合理的需求是,当应用服务启动时,相应的秘书服务也应该自动启动。还记得上面apache-secretary@%i.service文件中的BindsTo那行吗?事实上,单有这个配置只能够实现这个服务随着相应的Apache服务停止和重启,当Apache启动时,由于秘书服务的进程还不存在,这里的 BindsTo 是不会生效的。因此,还需要向Apache服务中加上秘书服务的依赖,以及指定启动顺序。
# Requirements
Requires=etcd.service
Requires=docker.service
Requires=apache-secretary@%i.service # 增加这行,指明依赖服务名称
# Dependency ordering
After=etcd.service
After=docker.service
Before=apache-secretary@%i.service # 增加这行,指定依赖启动顺序
现在只要服务自身启动了,Fleet就会在同一个节点上为它分配一个独立的秘书服务。
6.1.2.4 启动服务
在第4章中,已经介绍过在Fleet中启动Unit模板的方法,只需要在Fleet命令中的模板名参数的@符号后面加上相应的标识字符串就可以运行服务的实例了。由于我们在模板中使用了这个标识字符串作为服务在容器外暴露的端口号(这是一种很常用的技巧),因此这个字符串应该使用一个小于65525的数字表示。例如:
$ fleetctl submit apache@.service apache-secretary@.service
$ fleetctl load apache@8080.service apache-secretary@8080.service
$ fleetctl start apache@8080.service
最后一步执行“fleetctl start”的时候只需要指定一个Apache服务就足够了,它的监控秘书服务会在Apache服务之后自动被Fleet触发。
6.1.2.5 服务状态
启动完成以后,可以用fleetctl检查一下集群中运行的所有服务。
$ fleetctl list-units | grep apache
UNIT MACHINE ACTIVE SUB
apache-secretary@8080.service 14ffe4c3… /172.31.14.97 active running
apache@8080.service 14ffe4c3… /172.31.14.97 active running
还可以再注册一个Apache服务到集群中,它会自动运行到不同的节点上(由于apache@.service文件中的Conflicts配置)。
$ fleetctl load apache@8081.service apache-secretary@8081.service
$ fleetctl start apache@8081.service
$ fleetctl list-units | grep apache
UNIT MACHINE ACTIVE SUB
apache-secretary@8081.service 1af37f7c... /172.31.14.95 active running
apache-secretary@8080.service 14ffe4c3... /172.31.14.97 active running
apache@8081.service 1af37f7c... /172.31.14.95 active running
apache@8080.service 14ffe4c3... /172.31.14.97 active running
现在,在集群中的任意一个节点上,通过Etcd都可以轻松地获得集群中每一个Apache服务的信息。
$ etcdctl ls /services/apache/
/services/apache/172.31.14.97
/services/apache/172.31.14.95
$ etcdctl get /services/apache/172.31.14.95
{
"host": "core01",
"ipv4_addr": "172.31.14.95",
"port": "8081"
}
6.1.2.6 模拟故障情景
最后我们快速地模拟一下这种情况,即有一部分应用服务挂了。
将一个服务停止(或者直接杀死,不过那样它会到别的节点上自动重启,可能观察不到效果),按照上面的设计,记录在 Etcd 中的信息应该在30秒后由于TTL超时而被删除。
$ fleetctl stop apache@8080.service
$ fleetctl list-units
UNIT MACHINE ACTIVE SUB
apache-secretary@8080.service 14ffe4c3... /172.31.14.97 inactive dead
apache-secretary@8081.service 1af37f7c... /172.31.14.95 active running
apache@8080.service 14ffe4c3... /172.31.14.97 inactive dead
apache@8081.service 1af37f7c... /172.31.14.95 active running
现在,再来查询一次服务状态,可以看到记录的Apache服务只剩下172.31.14.95节点的8081端口的那个了。
$ etcdctl ls /services/apache/
/services/apache/172.31.14.95
$ etcdctl get /services/apache/172.31.14.97
Error: 100: Key not found (/services/apache/172.31.14.97)
6.1.3 案例延伸
6.1.3.1 为模板实例建立链接
在做这个案例的测试时,如果手工启动每个服务和相应的秘书服务还是相对比较麻烦的。由于Fleet启动使用了模板服务必须明确地指定标识字符串,因此总是需要在启动命令参数里面明确地写出每个服务的名称和标识,并且管理起来并不十分方便。
相比之下,由于非模板的Unit文件每个都是独立的实体,可以使用通配符(*)来一次启动在同一个目录下的多个Unit服务。因为每个服务对应了磁盘上一个真实的Unit文件,那么可不可以使用链接文件来给同一个模板创建多个带标识字符串的别名来代替呢?
下面我们来做个测试,为每个服务模板创建三个以带标识字符串的完整服务实例名称命名的链接文件。
$ ln -s templates/apache@.service instances/apache@8082.service
$ ln -s templates/apache@.service instances/apache@8083.service
$ ln -s templates/apache@.service instances/apache@8084.service
$ ln -s templates/apache-secretary@.service instances/apache-secretary@8082.service
$ ln -s templates/apache-secretary@.service instances/apache-secretary@8083.service
$ ln -s templates/apache-secretary@.service instances/apache-secretary@8084.service
然后用通配符启动这个目录下的所有链接文件。
$ fleetctl start instances/*
Unit apache@8082.service launched on 14ffe4c3... /172.31.14.97
Unit apache@8083.service launched on 1af37f7c... /172.31.14.95
Unit apache@8084.service launched on 9e389e93... /172.31.14.93
Unit apache-secretary@8082.service launched on 14ffe4c3... /172.31.14.97
Unit apache-secretary@8083.service launched on 1af37f7c... /172.31.14.95
Unit apache-secretary@8084.service launched on 9e389e93... /172.31.14.93
可以看到,Fleet欣然接受了这些通过链接创建的替身文件,并正确地将链接文件的标识字符串用于配置相应的应用服务启动。实际上,这种给每个要启动的具体应用实例创建一个链接到真实模板的方式恰恰是CoreOS官方推荐的使用模板方式,它将原本仅仅体现在启动参数里面的服务标识固化为一个个随时可见、可管理的链接,为管理服务提供了很大的便利。
6.1.3.2 数据的处理与展示
在这个案例的最后,所有的监控数据都还是以键值对的形式保持在Etcd的配置存储空间中的。这样的数据虽然得以持久和方便地查询,但却显得既不直观也不友好。
在实际的应用中,一般会对这类有意义的数据进行进一步的处理和展示。例如计算实时的集群服务健康状况,在特定条件下触发报警,或者将数据以图标的形式更清晰地提供给使用者。这些工作虽然没有在这个案例中进行详细讨论,但却是评判一套数据监控方案实用性的重要方面。CoreOS对数据的处理与展示并没有提供什么特别的工具,所有的工作都需要由用户通过Etcd API额外编码实现。
我们在第5章中已经介绍了Etcd API的使用,基于这些RESTful的接口实现用户自己的Web页面的监控工具并不是什么难事,由于具体界面设计与实际应用的业务场景十分相关,通用性不高,这里不做详述。
6.1.4 案例总结
在这个案例中,虽然我们仅仅设计了一个单纯的HTTP服务的监控,然而随着需要监控的服务数量的增加和集群中服务的流动(节点间迁移)增多,案例中监控策略的复杂度并不会显著地增加,算得上是因为简单所以可靠。分布在各个节点上的秘书服务能够很好地适应被监控服务的动态变化,并且对被监控服务具有很低的入侵性(不会限制服务运行在哪里),因此当应用场景变得复杂化、分布化时,其优势会比传统的集中式管理策略体现得更加明显。从本质上说,这是在用分布式的思想来解决分布式的问题,因此显得得心应手,驾轻就熟。
书籍介绍
本书是一本介绍CoreOS操作系统使用和周边技术的入门实践类书籍。本书内容分为三个主要部分。第一部分 (第1章)主要介绍CoreOS的基本概念和系统的安装,为后续各个组件的使用做好铺垫工作;第二部分(第2~6章)主要介绍CoreOS中*核心的内置 组件,通过这些组件,使用者能够完成大部分CoreOS的日常操作和开发任务;第三部分(第7~9章)主要针对CoreOS中一些比较进阶的话题以及组件 进行更具体的讲解,并介绍一些CoreOS使用技巧。