本文发布时间已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。
使用 Kubernetes 实现 Skytap 云微服务架构的现代化
Skytap 是一个全球公共云,为我们的客户提供在任何给定状态下保存和克隆复杂虚拟化环境的能力。我们的客户包括在混合云中运行应用程序的企业组织、提供虚拟培训实验室的教育组织、需要易于维护的开发和测试实验室的用户,以及具有各种 DevOps 工作流程的组织。
前段时间,我们的业务开始加速增长——我们的用户群和我们的工程组织都在同步增长。这些是令人兴奋、有益的挑战!然而,平稳地扩展应用程序和组织是困难的,我们正在谨慎地处理这项任务。当我们最初开始研究如何改进我们的工具集以进行扩展时,很明显传统的操作系统虚拟化将不是实现我们扩展目标的有效方法。我们发现虚拟机的持久性鼓励工程师构建和维护定制的“宠物”虚拟机;这与我们构建具有稳定、可预测状态的可重用运行时环境的愿望不符。幸运的是,Docker 和 Kubernetes 社区的增长与我们的增长相一致,并且社区参与度的同步爆发(从我们的角度来看)帮助这些工具走向成熟。
在本文中,我们将探讨 Skytap 如何使用 Kubernetes 作为处理 Skytap Cloud 中不断增长的生产工作负载的服务的关键组件。
随着我们增加工程师,我们希望保持我们的敏捷性,并继续在整个软件开发生命周期中实现组件的所有权。这需要在我们流程的关键方面进行大量的模块化和一致性。以前,我们通过 VM 和环境模板进行系统级打包来驱动重用,但随着我们扩展,容器作为一种打包机制变得越来越重要,因为它们相对轻量级并且可以精确控制运行时环境。
除了这种打包灵活性外,容器还有助于我们建立更有效的资源利用率,并消除因团队自然倾向于将资源混合到大型、高度专业化的虚拟机中而导致的复杂性日益增加的问题。例如,我们的运营团队会安装用于监控运行状况和资源利用率的工具,开发团队会部署一项服务,安全团队可能会安装流量监控;将所有这些组合到一个虚拟机中会大大增加测试负担,并且经常会导致意外情况——哎呀,你引入了一个新的系统级 Ruby gem!
使用 Docker 对服务中的各个组件进行容器化非常简单。入门很容易,但是任何构建过具有十几个以上组件的分布式系统的人都知道,真正的困难在于部署、扩展、可用性、一致性以及集群中每个单元之间的通信。
让我们容器化!
我们已经开始将我们深爱的许多宠物虚拟机换成,正如俗话所说,牛。
_____
/ Moo \
\---- /
\ ^__^
\ (oo)\_______
(__)\ )\/\
||-----w |
|| ||
创建一大群自由放养的容器并不会简化分布式系统的挑战。当我们开始使用容器时,我们意识到需要一个容器管理框架。我们评估了 Docker Swarm、Mesosphere 和 Kubernetes,但我们发现 Mesosphere 的使用模型与我们的需求不符——我们需要管理独立的虚拟机;这与 Mesosphere 的“分布式操作系统”模型不符——而且 Docker Swarm 仍然不够成熟。因此,我们选择了 Kubernetes。
启动 Kubernetes 并构建新的分布式服务相对容易(就此服务而言:你无法击败 CAP 定理)。但是,我们需要将容器管理与我们现有的平台和基础设施集成。平台的一些组件由虚拟机更好地提供,我们需要能够迭代地容器化服务。
我们将此集成问题分解为四个类别:
- 1. 服务控制和部署
- 2. 服务间通信
- 3. 基础设施集成
- 4. 工程支持和教育
服务控制和部署
我们使用 Capistrano 的自定义扩展(我们称之为“Skycap”)来部署服务并在运行时管理这些服务。对于我们来说,通过一个完善的框架来管理容器化服务和传统服务非常重要。我们还需要将 Skycap 与像 Kubernetes 这样积极开发的工具中固有的不可避免的重大更改隔离开来。
为了处理这个问题,我们在我们的服务控制框架中使用包装器来隔离 Skycap 后面的 kubectl,并处理诸如忽略虚假日志消息之类的问题。
部署为我们增加了一层复杂性。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 以与我们的平台一起使用时面临一些独特的挑战。AWS 和 GCP 提供了开箱即用的解决方案,简化了 Kubernetes 的配置,但对底层基础设施做出了与我们的现实不符的假设。一些组织拥有专门构建的数据中心。此选项将要求我们放弃现有的负载平衡基础设施、基于 Puppet 的配置系统以及我们围绕这些工具建立的专业知识。我们对放弃这些工具或我们已有的经验不感兴趣,因此我们需要一种可以与我们的世界集成而不是重建它的方式来管理 Kubernetes。
因此,我们使用 Puppet 来配置和配置 VM,而 VM 又运行 Skytap 平台。我们编写了自定义部署脚本以将 Kubernetes 安装在这些 VM 上,并与我们的运营团队协调以进行 Kube-master 和 Kube-node 主机的容量规划。
在上一节中,我们提到了基于点对点 TCP 的通信。对于面向客户的服务,pod 需要一种与 Skytap 的第 3 层网络基础设施接口的方式。Skytap 的示例包括我们的 Web 应用程序和 HTTPS 上的 API、Web 套接字上的远程桌面、FTP、TCP/UDP 端口转发服务、完整的公共 IP 等。我们需要仔细管理此外部流量的网络入口和出口,并且从历史上看,我们一直使用 F5 负载平衡器。用于内部服务的 MQ 基础设施不足以处理此工作负载,因为各种客户端(如 Web 浏览器)使用的协议非常具体,而 TCP 是最低的公分母。
为了使我们的负载均衡器与 Kubernetes Pod 通信,我们在每个节点上运行 kube-proxy。负载均衡器将流量路由到节点,而 kube-proxy 负责最终将流量传递到相应的 Pod。
我们不能忘记 Kubernetes 需要在 Pod 之间路由流量(用于基于 TCP 和基于 MQ 的消息传递)。我们使用 Calico 插件进行 Kubernetes 网络连接,并使用一个专门的服务在 Kubernetes 启动或移除 Pod 时重新配置 F5。Calico 使用 BGP 处理路由通告,从而简化了与 F5 的集成。
当 Pod 进入或离开集群时,F5 也需要重新配置其负载均衡池。F5 设备维护一个负载均衡的后端池;进入容器化服务的入口通过此池被定向到托管服务 Pod 的某个节点。对于静态网络配置来说,这很简单——但是由于我们使用 Kubernetes 来管理 Pod 的复制和可用性,我们的网络情况变得动态起来。为了处理变化,我们有一个“负载均衡器” Pod,它监视 Kubernetes svc 对象的变化;如果删除了或添加了 Pod,“负载均衡器” Pod 将通过 svc 对象检测到此变化,然后通过该设备的 Web API 更新 F5 配置。这样,Kubernetes 就透明地处理了复制和故障转移/恢复,而动态负载均衡器配置使得此过程对于发起请求的服务或用户来说保持不可见。类似地,Calico 虚拟网络与 F5 负载均衡器的组合意味着,对于在传统 VM 基础设施上运行的服务,或者已迁移到容器的服务,TCP 连接应该保持一致的行为。
通过网络的动态重新配置,Kubernetes 的复制机制使得水平扩展和(大多数)故障转移/恢复变得非常简单。我们尚未达到反应式扩展的里程碑,但我们已经使用 Kubernetes 和 Calico 基础设施奠定了基础,这使得实现它的途径变得简单。
- 配置服务复制的上限和下限。
- 构建负载分析和扩展服务(很简单,对吧?)。
- 如果负载模式与扩展服务中配置的触发器匹配(例如,请求速率或数量超过某个界限),则发出:kubectl scale --replicas=COUNT rc NAME。
这将使我们能够更精细地控制平台级别的自动缩放,而不是从应用程序本身进行控制——但我们也将评估 Kubernetes 中的 水平 Pod 自动缩放;它可能满足我们的需求而无需自定义服务。
请关注我们的 GitHub 帐户 和 Skytap 博客;随着我们解决这些问题的方案日趋成熟,我们希望与开源社区分享我们所构建的成果。
工程支持
像我们的容器化项目这样的转型需要参与维护和贡献平台的工程师改变其工作流程,并学习创建和排查服务问题的新方法。
由于各种学习风格需要多方面的方法,我们通过以下三种方式处理此问题:文档、直接与工程师沟通(即,午餐学习会或指导团队)以及提供易于访问的临时支持。
我们继续整理一份文档集,提供有关将经典服务过渡到 Kubernetes、创建新服务和操作容器化服务的指导。文档并不适合所有人,而且有时尽管我们尽了最大努力,文档仍然缺失或不完整,因此我们还运行了一个内部 #kube-help Slack 频道,任何人都可以随时寻求帮助或安排更深入的面对面讨论。
我们还有另一个强大的支持工具:我们自动构建和测试包含此 Kubernetes 基础设施的类似生产环境,这使工程师可以自由地进行实验并亲身体验 Kubernetes。在 这篇文章中,我们更详细地探讨了自动化环境交付的细节。
最后想法
我们在 Kubernetes 和容器化方面取得了巨大的成功,但我们当然发现与现有完整堆栈环境的集成带来了许多挑战。尽管从企业生命周期的角度来看,它并非完全即插即用,但 Kubernetes 的灵活性和可配置性仍然是构建我们模块化服务生态系统的非常强大的工具。
我们喜欢应用程序现代化挑战。Skytap 平台非常适合此类迁移工作——我们当然是在 Skytap 中运行 Skytap,这极大地帮助了我们的 Kubernetes 集成项目。如果您正在计划自己的现代化工作,请与我们联系,我们很乐意提供帮助。
- 下载 Kubernetes
- 在 GitHub 上参与 Kubernetes 项目
- 在 Stack Overflow 上发布问题(或回答问题)
- 在 Slack 上与社区联系
- 在 Twitter 上关注我们 @Kubernetesio,获取最新更新