这篇文章已超过一年。较旧的文章可能包含过时内容。请检查页面中的信息自发布以来是否已发生变化。

Modernizing the Skytap Cloud Micro-Service Architecture with Kubernetes

Skytap 是一家全球性的公有云提供商,它让我们的客户能够保存和克隆任意状态下复杂的虚拟化环境。我们的客户包括在混合云中运行应用的企业组织、提供虚拟培训实验室的教育组织、需要易于维护的开发和测试实验室的用户,以及各种拥有不同 DevOps 工作流程的组织。

一段时间前,我们开始加速发展业务——我们的用户群和工程组织同步增长。这些是令人兴奋、充满挑战的任务!然而,顺畅地扩展应用和组织是很困难的,我们正在谨慎地对待这项任务。当我们最初开始寻求改进工具集以实现扩展目标时,很明显传统的操作系统虚拟化不会是有效的方式。我们发现虚拟机持久化的特性鼓励工程师构建和维护定制的“宠物”虚拟机;这与我们构建具有稳定、可预测状态的可复用运行时环境的愿望不符。幸运的是,Docker 和 Kubernetes 社区的发展与我们的增长同步,社区参与度的并行爆发(从我们的角度来看)帮助这些工具走向成熟。

在这篇文章中,我们将探讨 Skytap 如何将 Kubernetes 作为服务中的关键组件,这些服务处理着不断发展的 Skytap Cloud 中的生产工作负载。

随着我们增加工程师,我们希望保持敏捷性并持续让工程师拥有软件开发生命周期中组件的所有权。这需要在我们流程的关键方面进行大量模块化和保持一致性。以前,我们通过虚拟机和环境模板进行系统级打包来实现复用,但随着规模的扩大,容器作为一种打包机制变得越来越重要,因为它们相对轻量,并且能精确控制运行时环境。

除了这种打包的灵活性之外,容器还帮助我们建立更高效的资源利用率,并且它们阻止了因团队倾向于将资源混合到大型、高度专业化的虚拟机中而产生的日益增长的复杂性。例如,我们的运维团队会安装监控健康和资源利用率的工具,开发团队会部署一个服务,安全团队可能会安装流量监控;将所有这些组合到一个虚拟机中会大大增加测试负担,并且常常会带来意外——糟糕,你拉入了新的系统级 Ruby gem!

使用 Docker 对服务中的单个组件进行容器化相当简单。入门很容易,但任何构建过拥有不止少量组件的分布式系统的人都知道,真正的难点在于部署、扩缩容、可用性、一致性以及集群中每个单元之间的通信。

让我们容器化吧!

我们开始将许多深受喜爱的“宠物”虚拟机换成了,就像俗话说的,“牲畜”。

_____
/ Moo \
\---- /
       \   ^__^
        \  (oo)\_______
           (__)\       )\/\
               ||-----w |
               ||     ||

然而,创建一大群散养的容器并不能简化分布式系统的挑战。当我们开始使用容器时,我们认识到需要一个容器管理框架。我们评估了 Docker Swarm、Mesosphere 和 Kubernetes,但我们发现 Mesosphere 的使用模型不符合我们的需求——我们需要管理独立的虚拟机;这与 Mesosphere 的“分布式操作系统”模型不符——而且 Docker Swarm 尚不够成熟。因此,我们选择了 Kubernetes。

启动 Kubernetes 并构建新的分布式服务相对容易(就这类服务而言:你无法逃避CAP 定理)。然而,我们需要将容器管理与我们现有的平台和基础设施集成。平台的一些组件更适合虚拟机,我们需要能够迭代地将服务容器化。

我们将这个集成问题分解为四个类别:

  1. 1. 服务控制和部署
  2. 2. 服务间通信
  3. 3. 基础设施集成
  4. 4. 工程支持和教育

服务控制和部署

我们使用 Capistrano 的定制扩展(我们称之为“Skycap”)来部署服务并在运行时管理这些服务。对我们来说,通过一个单一的、成熟的框架管理容器化和传统服务非常重要。我们还需要将 Skycap 与 Kubernetes 这样活跃开发工具中不可避免的破坏性变更隔离开来。

为了处理这个问题,我们在服务控制框架中使用包装器,将 kubectl 隔离在 Skycap 后面,并处理诸如忽略无关紧要的日志消息等问题。

部署为我们增加了一层复杂性。Docker 镜像是打包软件的好方法,但历史上我们是从源代码而不是包进行部署的。我们的工程团队期望修改源代码就足以发布他们的工作;开发者不希望处理额外的打包步骤。为了避免为了容器化而重建整个部署和编排框架,我们为容器化服务使用了持续集成流水线。我们为项目的每次提交自动构建新的 Docker 镜像,然后使用该提交的 Mercurial (Hg) 变更集编号对其进行标记。在 Skycap 端,从特定的 Hg 修订版本进行部署时,将拉取标记有相同修订版本的 Docker 镜像。

我们在多个环境中使用容器镜像。这需要将特定于环境的配置注入到每个容器实例中。直到最近,我们还使用类似的基于源代码的原则来注入这些配置值:每个容器在运行时通过 cURL 从 Hg 拉取原始文件,复制相关的配置文件。然而,网络可用性和可变性是最好避免的挑战,因此我们现在将配置加载到 Kubernetes 的 ConfigMap 功能中。这不仅简化了我们的 Docker 镜像,还使 Pod 启动更快且更可预测(因为容器不必从 Hg 下载文件)。

服务间通信

我们的服务使用两种主要方法进行通信。第一种是消息代理,这在 Skytap 平台内的进程间通信中很典型。第二种是通过直接的点对点 TCP 连接,这对于与外部世界(例如 Web 服务)通信的服务很典型。我们将在下一节中讨论 TCP 方法,作为基础设施集成的一个组件。

以服务可理解的方式管理 Pod 之间的直接连接是复杂的。此外,我们的容器化服务需要与基于传统虚拟机的服务通信。为了减轻这种复杂性,我们主要使用我们现有的消息队列系统。这帮助我们避免编写一个基于 TCP 的服务发现和负载均衡系统来处理 Pod 和非 Kubernetes 服务之间的流量。

这降低了我们的配置负载——服务只需要知道如何与消息队列通信,而不需要知道如何与它们需要交互的每个其他服务通信。我们还具有额外的灵活性,例如管理 Pod 的运行状态;节点重启时,消息在队列中缓冲,我们避免了每次添加或移除 Pod 时重新配置 TCP 端点的开销。此外,MQ 模型允许我们使用更准确的“拉取”方法来管理负载均衡,在这种方法中,接收者决定何时准备好处理新消息,而不是使用简单的统计开放套接字数量来估计负载的启发式方法,例如“最少连接”。

与迁移使用复杂基于 TCP 的直接或负载均衡连接的服务相比,将启用 MQ 的服务迁移到 Kubernetes 相对简单。此外,消息代理提供的隔离意味着从传统服务切换到基于容器的服务对任何其他启用 MQ 的服务来说基本上是透明的。

基础设施集成

作为基础设施提供商,我们在为 Kubernetes 配置以便与我们的平台配合使用方面面临一些独特的挑战。AWSGCP 提供了简化 Kubernetes 供应的开箱即用解决方案,但它们对底层基础设施做出了与我们实际情况不符的假设。一些组织拥有专用的数据中心。这个选项要求我们放弃现有的负载均衡基础设施、基于 Puppet 的供应系统以及我们围绕这些工具积累的专业知识。我们不打算放弃这些工具或我们积累的经验,因此我们需要一种方式来管理 Kubernetes,使其能够与我们的世界集成,而不是重建它。

因此,我们使用 Puppet 来供应和配置运行 Skytap 平台的虚拟机。我们编写了定制的部署脚本,在这些虚拟机上安装 Kubernetes,并与我们的运维团队协调进行 Kube-master 和 Kube-node 主机的容量规划。

在上一节中,我们提到了点对点基于 TCP 的通信。对于面向客户的服务,Pod 需要一种方式与 Skytap 的三层网络基础设施接口。Skytap 的例子包括通过 HTTPS 的 Web 应用和 API、通过 WebSockets 的远程桌面、FTP、TCP/UDP 端口转发服务、完整的公共 IP 等。我们需要仔细管理这种外部流量的网络入口和出口,并且历史上我们一直使用 F5 负载均衡器。对于内部服务的 MQ 基础设施不足以处理这种工作负载,因为各种客户端(如 Web 浏览器)使用的协议非常特定,而 TCP 是最低的公分母。

为了让我们的负载均衡器与 Kubernetes Pod 通信,我们在每个节点上运行 kube-proxy。负载均衡器将流量路由到节点,而 kube-proxy 负责最终将流量交给相应的 Pod。

我们不能忘记 Kubernetes 需要在 Pod 之间路由流量(无论是基于 TCP 的还是基于 MQ 的消息传递)。我们使用 Kubernetes 网络的 Calico 插件,并使用一个专门的服务在 Kubernetes 启动或终止 Pod 时重新配置 F5。Calico 使用 BGP 处理路由宣告,这简化了与 F5 的集成。

当 Pod 加入或离开集群时,F5 也需要重新配置其负载均衡池。F5 设备维护一个负载均衡后端池;到容器化服务的入口流量通过此池被导向托管服务 Pod 的节点之一。对于静态网络配置来说,这很简单——但由于我们使用 Kubernetes 管理 Pod 复制和可用性,我们的网络情况变得动态化。为了处理变更,我们有一个“负载均衡器” Pod,它监控 Kubernetes svc 对象的变化;如果 Pod 被移除或添加,“负载均衡器” Pod 将通过 svc 对象检测到此变更,然后通过设备的 Web API 更新 F5 配置。这样,Kubernetes 透明地处理了复制和故障转移/恢复,而动态负载均衡器配置使得这个过程对服务或发起请求的用户保持不可见。类似地,Calico 虚拟网络加上 F5 负载均衡器的组合意味着,对于在传统虚拟机基础设施上运行或已迁移到容器的服务,TCP 连接的行为应该保持一致。

kubernetes_f5_messaging.png

通过网络的动态重新配置,Kubernetes 的复制机制使得水平扩缩容以及(大多数)故障转移/恢复非常简单。我们尚未达到响应式扩缩容的里程碑,但我们已经通过 Kubernetes 和 Calico 基础设施奠定了基础,使其实现途径之一变得直接明了。

  • 配置服务复制的上限和下限
  • 构建一个负载分析和扩缩容服务(很简单,对吧?)
  • 如果负载模式与扩缩容服务中配置的触发器匹配(例如,请求速率或数量超过某个边界),则发出:kubectl scale --replicas=COUNT rc NAME

这将允许我们在平台层面,而不是从应用本身,对自动扩缩容进行细粒度控制——但我们也将评估 Kubernetes 中的Horizontal Pod Autoscaling;它可能无需定制服务即可满足我们的需求。

请关注我们的 GitHub 账号Skytap 博客;随着我们解决这些问题的方法日益成熟,我们希望与开源社区分享我们构建的内容。

工程支持

像我们的容器化项目这样的转型需要参与维护和贡献平台的工程师改变他们的工作流程,并学习创建和排查服务的新方法。

由于不同的学习风格需要多方面的方法,我们通过三种方式来处理这个问题:文档、直接与工程师交流(即非正式讨论会或团队辅导)以及提供易于获取的即时支持。

我们持续整理一套文档,提供将传统服务迁移到 Kubernetes、创建新服务以及运维容器化服务的指导。文档并非适用于所有人,有时尽管我们尽了最大努力,文档仍然缺失或不完整,因此我们还运行一个内部 #kube-help Slack 频道,任何人都可以随时寻求帮助或安排更深入的面对面讨论。

我们还有一个强大的支持工具:我们自动构建和测试包含此 Kubernetes 基础设施的类似生产环境,这让工程师可以自由地进行实验并亲自动手使用 Kubernetes。我们在此文章中更详细地探讨了自动化环境交付的细节。

总结

我们在使用 Kubernetes 和容器化方面总体上取得了巨大成功,但我们确实发现与现有全栈环境集成带来了许多挑战。虽然从企业生命周期的角度来看并非完全即插即用,但 Kubernetes 的灵活性和可配置性仍然是构建我们模块化服务生态系统的强大工具。

我们乐于迎接应用程序现代化方面的挑战。Skytap 平台非常适合此类迁移工作——我们当然是在 Skytap 中运行 Skytap,这在我们的 Kubernetes 集成项目中给予了我们极大帮助。如果您正在规划自己的现代化工作,请联系我们,我们非常乐意提供帮助。