转载

在CoreOS上的应用服务实践(上)

【编者按】在“漫步云端:CoreOS实践指南”系列的前几篇文章中,ThoughtWorks的软件工程师林帆主要介绍了CoreOS及其相关组件和使用,其中已经提到了使用 Unit 文件配置 Systemd 管理的系统服务的方式,本文将结合 CoreOS 的内置特性实现服务高可用的综合案例。

在CoreOS上的应用服务实践(上)

作者简介:

林帆,生在80后尾巴的IT攻城狮,ThoughtWorks成都办公室CloudOps小组成员,平时喜欢在业余时间研究DevOps相关的应用,目前在备考AWS认证和推广Docker相关技术。

截止到这里,CoreOS的基础部分已经全部介绍完毕,回头看看,其实大部分的篇幅都用在了介绍CoreOS内置服务的使用上。这些内置的服务,一方面来说为集群中的服务管理和通信提供了一种简单和规范的操作方式,但另一方面也确实使得应用服务引入了特定的依赖。所幸的是这些依赖并没有依存于CoreOS的生态链,因为所有的这些内置服务都是开源、独立的,也就是说比如Etcd、Fleet、Docker这些服务完全可以运行在任何其他的现代Linux发行版之上。只是,在CoreOS系统中,这些服务已经全部集成,并且会随系统自动升级,可以放心的使用罢了。

正是由于这些内置的服务,有些事,在哪儿都能做,不过在CoreOS上特省心。

在这个系列的最后几篇里,我们会用一些实际的例子来说明通过CoreOS的便利(其实就是Etcd和Fleet这些内置服务的提供的便利啦)能够完成的服务案例。

案例说明

在一个运行着数十上百种应用服务的集群的运维和应用设计中常常会遇到这样的需求:服务状态的收集,如何在集群的任意节点上快速的获取到整个集群里任意一个服务的状态呢?

这是典型的大型分布式服务监控任务。传统的解决方案一般都需要引入一个额外的监控系统,例如Nagios,并且通常会使用一个集中的数据收集和存储节点。利用在每个节点的客户端收集定制数据,然后发送回收集节点统一处理和展示,其余节点如果需要集群状态,就要到这个收集节点上获取。此外,如果所有的应用服务都是自行研发的,还可以在应用中添加一段代码,提供统一的暴露状态接口,简化数据收集。否则监控系统就需要知道哪些节点上运行了哪些服务,以便如何通过不同的接口和方式来分别获取每个服务的信息,而这个过程往往是繁琐而复杂的。

在CoreOS中,数据存储和分发的事情可以交给Etcd包干了,定时采集监控这件事本身直接让Fleet来搞定,其实需要自己费点劲的事情仅仅是定制一下监控数据的收集方法。我们其实可以给每个应用服务配备一个“秘书服务”,让这个服务一边跟着被监控的应用服务在节点间东奔西走,一边记录应用服务的实时状态(呃,这到底是秘书还是间谍…),这样又省去了传统集中收集数据时判断哪个节点当前运行哪些服务的麻烦。

为了不偏颇特别的应用场景,下面的例子采用一个最简单的服务作为监控的目标 —— 一个啥子都没有部署的纯纯的Apache HTTP应用服务,顺带省去设计定制数据的内容,单纯的检查HTTP服务是否可用。收集数据的定制会包含很多针对具体业务场景相关的细节,不具有普遍共性。最后我们会看到为什么这样看似简单的组合搭配Etcd的集群数据同步能力,随着数量的积累,就能够实现大规模的分布式服务状态监控。

容器化服务

将服务容器化能够使得普通的服务通过Fleet的协助获得跨节点调度的能力。这里将重点放在CoreOS本身的使用上,略过制作Docker镜像的过程,因此直接使用一个Docker官方仓库里的Apache镜像 eboraas/apache。下面来看个推荐的CoreOS应用服务Unit模板。

# apache@.service  [Unit] Description=Apache web server service listening on port %i  # Requirements Requires=etcd.service Requires=docker.service  # Dependency ordering After=etcd.service After=docker.service  [Service] # Let processes take a while to start up (for first run Docker containers) TimeoutStartSec=0  # Change killmode from "control-group" to "none" to let Docker remove work correctly. KillMode=none  # Get CoreOS environmental variables EnvironmentFile=/etc/environment  # Pre-start and Start ## Directives with "=-" are allowed to fail without consequence ExecStartPre=-/usr/bin/docker kill apache.%i ExecStartPre=-/usr/bin/docker rm apache.%i ExecStartPre=/usr/bin/docker pull eboraas/apache ExecStart=/usr/bin/docker run --name apache.%i -p ${COREOS_PUBLIC_IPV4}:%i:80 eboraas/apache  # Stop ExecStop=/usr/bin/docker stop apache.%i  [X-Fleet] # Don't schedule on the same machine as other Apache instances Conflicts=apache@*.service

在系列的上一篇中已经介绍了Unit模板文件,将上面这个配置保存成名为 apache@.service 的模板。在启动服务之前,我们先快速的浏览一下这个模板中的各个部分,以便说明怎样修改这个模板以适应具体业务场景的需求。

首先,[Unit]段中,Requires 列出了这个服务需要依赖的其他用户或系统服务的名字,然后用 After 和 Before(这里没有)等关键字指明这些服务的启动顺序。CoreOS会等待所有写在 After 区域的服务启动完成后再运行当前服务,同时在当前服务启动完成后,唤起所有写在 Before 的其他服务。

然后,[Service]段中,设置了两个特别的参数 TimeoutStartSec=0 和 KillMode=none。前一个之前提到过,主要是防止Docker在第一次启动由于下载镜像时间较长而被Systemd认为失去响应而误杀。后一个配置是因为Docker的每个容器都托管于同一个守护进程下面,从而使得容器停止后Systemd依然认为进程没有清理干净,有时会导致下一次启动同名的容器时出现莫名的问题,具体可以参考 这篇文档 。

接下来,出现了属于CoreOS特别的配置 EnvironmentFile=/etc/environment,这里是指定服务读取系统的环境变量文件,这个文件中的变量是在每次系统启动的时候写入的,后面用到的变量 COREOS_PUBLIC_IPV4 就是来自这个文件。

最后,[X-Fleet]段,可以看到这里配置的 Conflicts=apache@*.service 让与当前服务相同的服务进程不要调度到当前这个节点上,这样做是为了实现服务的高可用,在即使出现单节点严重故障的时候其他节点上的相同进程可以继续提供服务。一般来说为了实现高可用,在前级还需要加上反向代理作为负载均衡节点,这种做法已经是企业级服务的标准配备了。

整体看来,对于单独的应用服务而言,这个模板可以说是考虑得相当充分了。

使用Fleet和Etcd监控服务状态

这次内容的主角,秘书服务(请尽情的将它想象为一个貌美如花的美女)出场了。我们来一睹它的芳容。

# apache-secretary@%i.service  [Unit] Description=Monitoring the Apache web server running on port %i  # Requirements Requires=etcd.service Requires=apache@%i.service  # Dependency ordering and binding After=etcd.service After=apache@%i.service BindsTo=apache@%i.service  [Service] # Get CoreOS environmental variables EnvironmentFile=/etc/environment  # Start ## Test whether service is accessible and then register useful information 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'  # Stop ExecStop=/usr/bin/etcdctl rm /services/apache/${COREOS_PUBLIC_IPV4}  [X-Fleet] # Schedule on the same machine as the associated Apache service ConditionMachineOf=apache@%i.service

感觉现在这个秘书长得有点抽象?呃,那我们来仔细推敲推敲。

首先,不难看出,它也是一个Unit模板(那么多 %i、%H 占位符)。

然后,它的[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};

网上看,整个命令使用一个 while true 循环包裹起来,因此除非外部因素结束这个秘书服务,否则这个它定要跟随着被监控的服务一直这么纠缠下去了。

再往下看,有个 sleep 20 的行,也就是说,如果在被监控服务运行正常的情况下,每隔 20 秒秘书就会刷新一次服务的状态信息(虽然这个例子里只是IP、端口这些比较固定的信息,实际情况中所监控的信息可能不止这些)。这也使得在正常情况下,Etcd中的数据永远不会由于 TTL 的超时而被清除。

至此,这个 ExecStart 的意思也就大致清晰了。其中还没有被提到的那行 curl 命令以及 etcdctl set 中需要记录的内容就是实际监控服务需要定制的部分。例子里的 Apache 其实是监控的最简单情况。

好吧,简单归简单,似乎现在可以启动这两个服务来测试一下效果了吧。但是...要挨个启动两个服务?显然这里有些不合理的地方。

关联秘书服务

刚刚在写 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    # 增加这行,指定依赖启动顺序

启动

啊,终于到了这个“鸡冻人心”的时刻。

在前一节,介绍过启动Unit模板的方法,只需要在 fleet 命令中模板名参数的@符号后面加上相应标识字符串就可以了。由于我们在模板中使用了这个标识字符串作为服务在容器外暴露的端口号(这是一种很常用的技巧),因此这个字符串应该使用一个小于65525的数字表示。例如:

$ fleetctl submit apache@.service apache-secretary@.service $ fleetctl load apache@8080.service apache-secretary@8080.service $ fleetctl start apache@8080.service

还可以再注册一个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... /10.132.249.206  active  running apache-secretary@8080.service   1af37f7c... /10.132.249.206  active  running apache@8081.service     1af37f7c... /10.132.249.206  active  running apache@8080.service     1af37f7c... /10.132.249.206  active  running

现在在集群中的任意一个节点,通过 Etcd 都可以轻松的获得集群中每一个 Apache 服务的信息。

$ etcdctl ls /services/apache/ /services/apache/10.132.249.212 /services/apache/10.132.249.206 $ etcdctl get /services/apache/10.132.249.206 {"host": "core-01", "ipv4_addr": "10.132.249.206", "port": "8081"}

到目前为止,一切看起来已经很不错了。仔细一想,似乎还有一个美中不足的地方,由于Fleet启动使用了模板的服务必须明确指定标识字符串,因此总是需要在启动命令参数里明确写出每一个服务的名称和标识,并且管理起来并不十分方便。

相比之下,使用非模板的Unit文件则可以使用通配符来一次启动多个服务,因为每个服务对应了磁盘上一个真实的Unit文件。那么可不可以使用 link 文件来给同一个模板文件创建多个带标识字符串的别名来充数呢,我们再创建几个服务链接文件,来试试。

$ 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... /10.132.249.212 Unit apache@8083.service launched on 1af37f7c... /10.132.249.206 Unit apache@8084.service launched on 9e389e93... /10.132.248.177 Unit apache-secretary@8082.service launched on 14ffe4c3… /10.132.249.212 Unit apache-secretary@8083.service launched on 1af37f7c... /10.132.249.206 Unit apache-secretary@8084.service launched on 9e389e93... /10.132.248.177

可以看到,Fleet欣然接受了这些通过链接创建的替身,并正确的将链接文件的标识字符用于配置相应的应用服务启动。实际上,这种给每个要启动的具体应用实例创建一个链接到真实模板的方式恰恰是CoreOS官方推荐的使用模板方式,它将原本仅仅体现在启动参数里面的服务标识固化为一个个随时可见、可管理的链接,确实是值得推荐的。

模拟故障情景

最后我们快速的模拟一下这种情况:如果,应用服务挂了。将一个服务停止掉(或者你也可以把它残忍的用 kill 命令杀掉),按照上面的设计,记录在 Etcd 中的信息应该在 30 后由于 TTL 超时而被删除。

$ fleetctl stop apache@8080.service $ fleetctl list-units UNIT                MACHINE             ACTIVE      SUB apache-secretary@8080.service 14ffe4c3... /10.132.249.212  inactive    dead apache-secretary@8081.service   1af37f7c... /10.132.249.206  active  running apache@8080.service       14ffe4c3... /10.132.249.212  inactive    dead apache@8081.service     1af37f7c... /10.132.249.206  active  running

现在再来查询一次服务状态,可以看到记录的 Apache 服务只剩下 10.132.249.206 节点的 8081 端口那个了

$ etcdctl ls /services/apache/ /services/apache/10.132.249.206 $ etcdctl get /services/apache/10.132.249.212 Error:  100: Key not found (/services/apache/10.132.249.212)

小结

这个案例中,我们虽然仅仅设计了一个单纯的HTTP服务的监控,然而随着需要监控的服务数量增加和集群中服务的流动(节点间迁移)增多,案例中监控策略的复杂度并不会显著的增加,算得上是因为简单所以可靠。分布在各个节点的秘书服务能够很好的适应被监控服务的动态变化,并且对被监控服务具有很低的入侵性(甚至不会限制服务运行在哪里),因此当应用场景变得复杂化、分布化时,其优势会比传统的集中式管理策略体现得更加明显。本质上说,这是在用分布式的思想来解决分布式的问题,自然显得得心应手,驾轻就熟。这种设计思想和微服务架构的精髓相当契合:简单即美。

从这个简单却不失经典的小案例中,可以看出合理的利用CoreOS系统提供的内置服务能够快速的解决不少分布式设计遇到的麻烦。下一篇中,还会再介绍一个同样小巧精致的典型案例在CoreOS中的运用,敬请期待。(作者/林帆 责编/刘亚琼)

系列链接:

漫步云端:CoreOS实践指南(一)

CoreOS实践指南(二):架设CoreOS集群

CoreOS实践指南(三):系统服务管家Systemd

CoreOS实践指南(四):集群的指挥所Fleet

CoreOS实践指南(五):分布式数据存储Etcd(上)

CoreOS实践指南(六):分布式数据存储Etcd(下)

CoreOS实践指南(七):Docker容器管理服务

CoreOS实践指南(八):Unit文件详解

正文到此结束
Loading...