基于 Docker 1.12 Swarm 的集群管理开发实践

KarrySpearm 3年前
   <p>Mesos/Marathon 折腾久了,我们一直希望有机会深入到 Swarm 内部一探究竟。 另外, Mesos 这一套东西虽然是久经企业级考验的, 但是安装、部署和使用相对复杂,上手有门槛。同时,在今年的 DockerCon 上,内置了Swarm 功能的 Docker 1.12 发布。基于以上背景,数人云计划围绕 Docker 1.12 Swarm 开发一版轻量级的集群管理工具,也借此与 Mesos/Marathon 对比下。目前,我们第一版数人云容器管理面板 <strong>Crane</strong> 已经开发完毕,过程也是磕磕绊绊,这里趁机总结几篇技术分享。</p>    <p>正文开始前先八卦一下,关注 Docker 技术的小伙伴们应该清楚 Docker 1.12 的 Swarm mode 颇受争议:首先是有人认为 Docker 公司 Market Drive Develop,违背了 Linux 信徒恪守的哲学——一个工具只干一件事情;其次, 有人认为 Swarm mode 的功能不及 Mesos 和 K8S,还不适合生产环境使用,这一点我倒认为稳定性而不是功能才是 Swarm 目前不适合生产环境的原因;最后, Docker 的向后兼容性不足也引来口水无数,毕竟 Docker 还在 active develop。其它的像容器网络标准的争议, Runc 的争议这些都把 Docker 推到了风口浪尖。当然,不辩不明,相信 Docker 给我们提供的不止眼前这些。</p>    <p>我们首先从 <strong>应用编排( Application Stack)</strong> 谈起,应用编排是 Docker 1.12 引入的概念,目前还是 experimental 的功能,必须得安装 experimental 的包才可以尝试。除去编排 (stack), Docker 1.12 还引入了 <strong>服务 (service) 和任务 (task)</strong> 的概念, Docker 借此重新阐述了应用与容器 (container) 之间的关系。上述几个概念的关系如下图所示:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/d5509c8f289f6be44d025c9eb53d8b27.jpg"></p>    <p>即:一个应用编排代表一组有依赖关系的服务,服务之间可以相互发现(后面详细介绍),每个服务由多个任务组成,任务的数量可以扩缩 (scale),而任务则物化为一个具体的 Docker 容器及其配置。</p>    <p>Docker 通过扩展名为 dab( Distributed application bundles) 的文件来描述一个应用编排,下图是一个带有两个服务的 dab 文件例子:</p>    <p><img src="https://simg.open-open.com/show/1f0bae68909e8b70f6a84019a70bf7b7.png"></p>    <p>这里有dab文件的详细介绍:</p>    <p>https://github.com/docker/docker/blob/master/experimental/docker-stacks-and-bundles.md</p>    <p>其中 Image 这里推荐使用 image@digest 而不是 image:tag,原因是为了避免这种情况 : 在集群中部署服务时, image:tag 无法保证镜像是全局一致的,本地的 image:tag 可能与镜像仓库里面的 image:tag 数据不一致,这个问题在跨机环境中被放大。而 image@digest 这种方式通过中心化仓库设置全局唯一的 digest 值,避免了上述问题。</p>    <p>除此之外,还有下述几个关键特性值得分享:</p>    <ul>     <li> <p>滚动更新: 服务的镜像更新是一个基本诉求。 Docker 可以通过关键词 update-parallelism 和 update-delay 来控制并行更新的频率。 Marathon 也提供了类似的功能。这个特性很关键,如果无法控制更新频率,成百上千的镜像拉取和任务调度会导致严重的资源波峰。 Docker 文档还声称支持更新失败回滚,尝试了下,目前没发现怎么玩,还没来得及看底层代码。</p> </li>     <li> <p>服务模式 (service mode): Docker 1.12 提供了两种方式控制任务数量—— replicated 和 global,在 replicated 方式下,我们需要提供期望的任务数量, Swarm 将一直尝试维护这个任务数;而在 global 方式下, Swarm 尝试在每个节点上启动一个任务,这种方式特别适合向每个节点下发 agent 的场景。</p> </li>     <li> <p>stop-grace-period 参数: 在服务缩容时,我们比较关心容器被强制 kill 而带来的事务 (transaction) 问题,配合该参数 stop-grace-period( 强制杀死容器前的等待时间 ) 和容器内部的退出信号监听,可以达到容忍程序友好退出和快速回收资源之间的平衡。 Mesos / Marathon 也采用了类似的策略来解决这个问题。</p> </li>     <li> <p>with-registry-auth 参数: 在服务创建时,该参数声明将管理节点 (Swarm manager) 上的 registry authentication 信息带到工作节点 (worker node) 上,从而为工作节点提供从 registry 拉取镜像的认证信息。在 Docker 的非 Swarm 场景下, Docker-client 负责 registry 认证信息的管理,但在 Swarm 方式下,不再是 Docker-client 触发镜像拉取动作,所以服务无法使用工作节点本地的 registry 认证信息了,必须要通过上述方式从管理节点分发认证信息。同时节点间的加密通信也保证了认证信息传输的安全性。</p> </li>     <li> <p>任务的生命周期: 在容器的生命周期之上, Docker 1.12 引入了任务 (task) 的生命周期。某任务下的容器异常退出时,带有同样任务编号 (slot) 的新容器将会被尝试启动。不同于容器的生命周期只囿于一台固定主机上,任务的生命周期是与主机无关的,我们可以依此对容器的日志进行聚合得到任务的日志。这一点正好是 Mesos / Marathon 所欠缺的。</p> </li>     <li> <p>重启策略: Docker 1.12 提供了三种重启条件 -any, none, on-failure,其中 none 指的是退出不重启, on-failure 指的是失败( exit code 不是零)时重启,而 any 指的是无论任务正常或是异常退出,都重启。 any 方式配合参数重启间隔 (restart-delay) 可以满足定时任务的场景; none 方式则可以满足批处理任务场景。另外参数评估间隔 (restart-window) +参数尝试次数 (restart-max-attempts) 可以控制在一段时间内的任务重启次数,避免异常任务频繁重启带来的集群资源失控。</p> </li>    </ul>    <p>当然,Swarm mode 还有很多问题亟待解决:</p>    <ul>     <li> <p>dab 文件的表达能力有限: 当前版本的 dab 文件只有寥寥数个关键词,服务 (service) 创建的诸多参数 dab 都无法支持。在我们的实践中,为了解决这个问题, team 不得不二次开发引入服务的其它参数,以期对服务的参数全量支持。</p> </li>     <li> <p>容器回收问题: 按照目前的设计,只要服务 (service) 还在,退出的容器是不会被自动回收掉的,在某些场景下,这会导致集群失控,各主机的文件描述符被耗尽。</p> </li>     <li> <p>容器的健康检查 (healthcheck): 在我看来,这是 Swarm mode 之外, Docker 1.12 引入的最关键功能,有了 healthcheck 这个 feature,我们可以将业务内部真正的健康状况暴露出来了。这个功能落后于 Marathon 足足一年的时间。但可惜的是,服务( service)创建目前还不支持这个关键词,这就限制了服务 (service) 异常重启的能力,从而间接降低了服务容错能力。这一点 Marathon 做的特别好。</p> </li>     <li> <p>资源控制: 1.12 目前支持单个任务 CPU / mem 的资源控制。还无法像 Mesos 那样,自由的配置管理磁盘, GPU,端口等资源。</p> </li>     <li> <p>无法使用主机网络: 通过服务( service)启动的容器是无法使用主机网络的 (network host is not eligible for Docker services),但同时按照网络上 Percona 提供的压测结果, overlay 网络相较于主机网络有 60% 的性能损耗。这严重局限了 Swarm 的应用场景,我们可以认为这是编排功能带来的架构副作用。而 Mesos 从资源维度管理集群很好的规避了这个问题。</p> </li>    </ul>    <p>接下来让我们看看下一层: Docker 是怎样在 stack, service, task container 之间建立联系的?同一个 stack 内的 service 又是如何相互发现的?</p>    <p>第一个问题很好回答, service label 和 container name, Docker 通过在 service 上添加 label: com.Docker.stack.namespace=XXX 来标示这个 service 属于哪一个 stack。我们可以 inspect 一个 service 看下:</p>    <p><img src="https://simg.open-open.com/show/9b8c7b5a4328cbc0ddb172965916580f.jpg"></p>    <p>Docker 通过特定格式的 container name 标示这个 container 隶属于哪一个 service 下面,如下图所示:</p>    <p><img src="https://simg.open-open.com/show/c6dd787eef96ba44ed90a674aa075ac7.png"></p>    <p>容器名称 merryfox_mysql.1.by842qrj7xhsne93yzfpjp367 代表该容器是服务 merryfox_mysql 的任务 1 的容器。Docker 在很多地方使用了这种技巧来处理数据。而第二个问题就引出了我们下面的——服务发现。</p>    <h2><strong>服务发现</strong></h2>    <p>在谈服务发现之前,我们简单讨论下 Docker overlay 网络的性能问题,根据 https://www.percona.com/blog/2016/08/03/testing-Docker-multi-host-network-performance/ 的网络压测结果, <strong>相较于 host 网络, overlay 有 60% 的网络性能损耗</strong> ,问题主要出在多 CPU 下网络负载不均。同时容器无法在 Swarm 编排模式下使用 host 网络,这带来的问题就是: <strong>在 Docker 1.12 Swarm mode 下网络性能损耗无法避免</strong> 。</p>    <p>与 Marathon / Mesos 的 Mesos-DNS、 bamboo 类似, Swarm 的服务发现也分为内部服务发现 (internal service discovery) 与外部服务发现 (ingress service discovery),这两种服务发现使用了不同的技术。</p>    <p>如果想让一个服务暴露到集群之外,我们需要借助 service create 的参数 publish,该参数显式的声明将集群特定端口 PORT_N(集群端口 PORT_N 代表集群中所有主机的端口 PORT_N)分配给这个服务。这样无论该服务的容器运行在哪台主机上,我们都可以通过集群中任何主机的 PORT_N 端口访问这个服务了。下图可以形象的描述这个场景:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/cae6b69556bfcd203220cc285521b26a.jpg"></p>    <p>接下来,我们就可以把集群中部分或所有主机的 IP 加上述端口配置到我们的前置负载均衡器上对公网提供服务了。一般称这种分布式的负载均衡策略为 routing mesh,在 Calico 网络方案中也提到了类似的概念。由于没有了中心化的负载均衡器,集群不会因某台机器异常而导致整个服务对外不可用,很好的避免了单点问题,同时也带了可扩展性。关于routing mesh这个概念的详细介绍可以参考该链接( https://en.wikipedia.org/wiki/Mesh_networking )。</p>    <p>这里摘抄一个简短解释:</p>    <p>A mesh network is a network topology in which each node relays data for the network. All mesh nodes cooperate in the distribution of data in the network.</p>    <p>上述就是 Swarm 的外部负载均衡(也可以称为routing mesh),那么 Docker 在底层做了什么来实现上述功能的呢?如下图所示:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/d847f20a130e0a7f03737956a9b0f19e.png"></p>    <ol>     <li> <p>在 Swarm 集群初始化时, Docker 会创建一个 overlay 网络 ingress,同时在每个节点上创建与 ingress 关联的 sandbox 网络命名空间,这样集群中的每个主机都变为了 ingress 网络的一部分;</p> </li>     <li> <p>当我们创建 service 申请一个 publish port 时, Docker 会通过 Iptables rules 建立 主机 IP:Port 到 sandbox IP:Port 间的映射,即 : 将对应端口的包转发给 ingress 网络;</p> </li>     <li> <p>同时在 sandbox 网络命名空间内, Docker 通过 Iptables rules + IPVS 控制对应 端口/ Port 的包负载均衡到不同的容器。这里 IPVS(IP virtual server) 的功能与 HAproxy 类似,承担着 Swarm 集群内部的负载均衡工作。</p> </li>    </ol>    <p>对于只供集群内部访问的服务,无需使用上述 routing mesh,Swarm 还提供了一套内部服务发现 + 负载均衡。如下图所示(这里我们只讨论基于 VIP 的负载均衡方法):</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/d791bbd0d4bc1057e2e17490546294d9.png"></p>    <ol>     <li> <p>manager 会为该 service 分配一个 VIP(Virtual IP),并在内部 DNS 上建立一条 VIP 与 service name 的映射记录。注意这里的 DNS server 也是分布式的,每一个主机上都有一个 DNS server ;</p> </li>     <li> <p>Iptables 配合 IPVS 将 VIP 请求负载到 service 下面的一个具体容器上。</p> </li>    </ol>    <p>另外,主机间 routing mesh,load balancing rule 等信息是利用gossip协议进行数据同步的,限于篇幅,这里不再深究这个问题。</p>    <p>最后友情提示几个雷区:</p>    <p>Q1:为什么我的机器无法加入 (join) 到 Swarm 集群中?终端报错:加入集群超时。</p>    <p>A1:这个问题极有可能是主机间时钟不同步导致的,建议开启 ntp 服务,同步主机时间。</p>    <p>Q2:为什么我在 manager A 上通过命令 Docker network create 的 overlay 网络无法在集群另外的机器 B 上通过 Docker network ls 发现?</p>    <p>A2: 这有可能是正常的。按 Swarm 当前的设计,只有使用相应网络的容器被调度到了机器 B 上, overlay 网络信息才会更新到机器 B 上去。</p>    <p>Q3: 为什么我的服务的 publish port 在有些机器上不生效?我使用 netstat – lnp 方式看不到端口监听。</p>    <p>A3: 与问题 1 一样,这也可能是时钟不同步导致的问题。</p>    <p> </p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s?__biz=MzA3MDg4Nzc2NQ==&mid=2652134137&idx=1&sn=cea9f4f5cf604670c3868e9a94f3e363&scene=4</p>    <p> </p>