从18年公司基础架构开始转向 Kubernetes

共 6907字,需浏览 14分钟

 ·

2021-07-14 20:28

翻译:公众号 - 架构师头条
原文链接:https://medium.com/zendesk-engineering

几年前,人们认为,对于 Zendesk 而言,Kubernetes 是个不可能的挑战。那时,人们普遍认为:

在 Kubernetes 中,我们可以做一些小事情,但是对于我们最初的单体 Ruby on Rails 产品来说,这样做并没有什么意义。即便我们这么做,我们也不知道 Kubernetes 是否能够应付得来。

但是一切都是可以改变的,改变的种子始于 2017 年的 Zendesk Dev Leads 峰会,Jon Moter 在那里举办了一场“Zendesk 的 Kubernetes”的研讨会。我萌生了进一步探索的想法。

1入坑

一开始,我在本地开发环境中使用 Kubernetes。我们已经将它引入到了基于 Docker 的环境中,我只想在 Kubernetes 中运行时启动应用程序。然而,应用程序经常崩溃,使我陷入困境:

  • Kubernetes 上的各种错误是什么意思?

  • 怎样查看日志?

  • 怎样知道失败的原因?

最初的几个月,变成了要学会如何使用 Kubernetes,而非在上面运行应用程序。我完全没想到会这样,但是这很有意思。

在本地开发环境中启动应用程序,而不立即抛出错误,是我的首要目标。一旦完成,我开始测试其他的 API。异常会被抛出,而我无法找出原因,这时我就可以开始解决配置问题了。我认识到需要解决配置问题,这样才能在部署环境中加入 Kubernetes。

2配置问题

有必要介绍一下配置的背景:Zendesk 遵循的路径对于 Ruby on Rails 应用程序非常典型:首先要有几个 YAML 文件……最后才是 YAML 文件和环境变量……然后再根据这些文件定制特定于公司的内容。这一切我们都有。使用 Chef 在 EC2 实例和数据中心中“烘焙”出 YAML 文件和环境变量。其结果是:配置管理混乱。

首先要尝试做的是将所有配置注入到环境变量中。这是我们开始时所做的,并且非常适合许多事情。接着我们进入结构化数据,事情就开始变得有趣了。现在回想起来,第一个挑战是网络配置。环境变量的值在 UNIX 系统中可以是有限的。在多数系统中,一个变量实际上并没有限制,但是整个环境却有限制。这有几十兆甚至更多,但是我们这里也讨论兆字节的结构化数据。此外,我们的部署工具,即注入环境变量的工具,在一个文本列中存储数值,长度不超过 2048 个字符。事实上,我们不能将所有结构化数据放在某些环境变量中。所以我们必须把它们分开。这就是说将一个数据块放在一个键中。我们设置的是 20 个键都具有相同的前缀,后缀表示数据所包含结构的哪一部分。

原始 YAML 的定义如下:

联网 YAML 文件示例

尝试用环境变量来代替:

网络环境变量示例

刚开始的时候,这样做非常好,因为它涵盖了 80% 的场景。然而,仅有 40 个环境变量的配置并非正确的答案,而且无法将我们引入预生产环境,因此我们开始使用 Kubernetes ConfigMap。

“网络配置”是我们处理的第一个大地图。在 Kubernetes 集群的 ConfigMap 中,我们编写了一个小型服务,将 Chef 维护的 YAML 文件放入其中。在最初的成功之后,我们开始加载更多的 ConfigMap。

我们得到了一些相当有趣的边缘案例,在这些案例中,我们有涉及到文件的秘密值。在此之前,我们并不反对将秘密以限制访问模式保存在磁盘上。但是在 ConfigMap 中,我们不想保持这种习惯。根据菜谱的评估位置,我们创建了一个菜谱,它会产生“数据形状”,并在所有地方共享。当运行我们的单体 EC2 实例时对其进行评估,它会以某种方式填充秘密值。若是其他应用程序,则使用不同的方法对其进行填充。基本上配置的形状是一样的,但是每个应用程序的秘密都不一样。

3缩小范围,获得信心

要让 Kubernetes 最终进入我们的 staging 环境,我们就必须把范围缩小到最小,以便获得自信。我们主要关注 unicorn 服务的流量。我们这么做的理由有很多。第一个原因是纯粹的可观察性。到目前为止,这是我们仪器化程度最高的过程,并且我们的观测越来越好。可以看到响应时间指标、错误率指标和各端点指标等等。它使得衡量成功更加容易。“这东西和以前一样管用吗?还是我破坏了什么?”

第二个原因,之所以选择 HTTP 请求,是因为它们的工作单元很小:你得到一个请求,你做了些什么,然后你发送一个响应。这样最容易让它们回滚。在推出东西的时候,我们可以观察这些指标,然后再用最小的影响将其翻转回来。这是我们必须考虑的因素。

到目前为止,我们对可观察性有信心。通过 ConfigMap,Kubernetes 突然在我们最初的 Ruby on Rails 单体产品(Classic)的 staging 环境中运行。开始后,我们可以可靠地部署它,并向它发送流量。我记得那是一个平常的日子,我们说:“噢,这确实管用。大家好,Classic 现在可以在 Kubernetes 上运行了。”

4更多配置问题

接着是什么呢?我们在 staging 中运行了一段时间,发现了许多 bug,并对它们进行了修复,一些类型的 bug 也开始出现。举例来说,开发者 A 改变了配置,他们有与此相适应的代码片段。这样很好。开发者 B 随后将代码部署到我们的 staging 环境中,并从开发者 A 那里得到配置的更改,而不知道相关的代码更改……我们以某种形式的大脑分裂告终:我们的配置输入和我们更新代码的方式不同步。这一局面变得非常混乱,非常迅速。我们开始看到事情发生一些变化,不是由于 Kubernetes 的变化,而是由于配置的变化。

这里发生了一件特殊的事情,是一个转折点。更改被合并进 README 文件。它会自动部署到我们的 staging 环境中,然后……突然之间,staging 环境不起作用了。大家感到困惑:“我们修改了一个 Markdown 文件,这是我们所能做的最无伤大雅的一件事。这怎么会破坏 staging 呢?”我们立刻回滚了以前的标签,但……这并不能解决问题。这个经验告诉我们,在复杂的环境下,或者任何环境下的配置版本都是至关重要的。这说明了配置是多么困难,并最终引导我们在整个公司实现配置的标准化。

5分配负载

迄今为止,我们已进入了一个阶段,而且感觉良好,希望进入生产阶段。但是,在此之前,我们需要了解新的基础设施在负载情况下的性能情况。负载均衡器是否足够好?代理层是否能够均匀地代理所有这些事情,或者我们最终会遇到瓶颈和请求排队的情况?

这非常有趣,因为它与 Zendesk 的历史有关。在我们以前的基础设施中,我们有一个 Ruby 单体应用,并且垂直扩展以适应增长。到了这一地步,我们购买了专用硬件,是 256GB 内存和 64 核 CPU 的裸机实例。接下来我们将运行 100 个 unicorn 服务。于是我们决定在它前面安装一个不可思议的高性能负载均衡器。那时候,我们的代理层不需要均衡,它也不在乎,因为我们有专门的硬件。部分原因是有意识的决定,部分原因是当时的技术不足。这种结构甚至贯穿于我们的 AWS 转型过程中,在 EC2 实例之前还有一个 AWS 弹性负载均衡器(应用程序或网络均衡器出现之前)。

但是在 Kubernetes 世界里,这个选项并不简单。为了使该应用程序稳定,我们必须了解配置和所有不同的特性。要找出不同的垂直和水平扩展特性。现在我们回顾一下,unicorn 是如何工作的,以及 NGINX 层中负载的分布。重新审视 8 年前的这些决定是痛苦的,但也是十分宝贵的。重新审视这些决定还会带来一些很好的优化,比如改变 NGINX 的负载均衡策略,这会极大地影响我们的资源利用率。

Kubernetes Pod 在改变 NGINX 上游配置前后的 CPU 使用情况

在开始向 Kubernetes 迁移的时候,我们尝试让它的部署与之前的环境相匹配。如果有可能的话,我们会尽量让它们在垂直方向上变大,但是不幸的是,这仅仅是以前的四分之一。在开始均衡这些请求的时候,我们担心的是会遇到一个完全繁忙的实例。这就是名为“经典 API 流量生成器”的笨拙工具。主要是对生产环境中的前 20 个请求进行快照,并查看每个请求的数据和所占比例。流量生成器在 staging 环境中生成这些请求,并将其相应地加权以使其与生产环境大致一致。搜索是我们最大的动力之一。结果,生成器会对搜索终端产生很大的影响,请求形状也大致相似。

这样就可以在 staging 环境中找到很多有趣的发现。首先,我们没有对资源进行正确的测量。在单个 Kubernetes pod 中运行一定数量的 unicorn 进程,而该 pod 可以使用一定数量的 CPU。除非负载过重,否则没有问题。大量请求排在一起,pod 会崩溃,因此我们必须进行深入研究,“我只给它发送了 10 次请求。为何这个应用现在失败了?”最终,CPU 内存资源的上限对于这个应用程序的配置文件来说还不够高。

随着负载的增加,与现有的基础设施相比,服务器的利用率和排队情况
6缩短部署时间

对于我们来说,另一个巨大的挑战是将 Kubernetes 实际部署到我们的生产环境中,换句话说,把一组新的代码传递给 Kubernetes 集群中运行的东西。与在磁盘上更改文件相比,部署一个不可变的代码库实际上会做得更少,也更耗时。刚开始进行大规模试验时,它的速度非常缓慢。去年,我们花费了大量的时间和精力,通过添加工具来提前加载构件构建,从而降低了我们的部署速度。这就是说,在 EC2 专用硬件中运行新代码只需几分钟:我们会更新代码,unicorn 会顺利完成请求,工作器进程会终止,然后再重新运行新代码。

在 Kubernetes 环境中,情况就完全不同了。不能连接到 Kubernetes,也不能发出信号来执行类似 unicorn 重载的事情。即便我们能够,我们也不能将新代码放入已有的容器中,因为我们将损失所有的热重载。为了部署完整规模的 Kubernetes pod 集群,需要下载所有容器并创建所有 pod。部署时间从 2~3 分钟到 30 分钟不等,简直吓死人了。没有什么灵丹妙药,但是我们已经采取了一些重要措施来提高部署速度。

我们在 Kubernetes 浪费时间的一个原因就是对所有实例进行轮换。我们将扩展集群,但是我们没有足够的空间来运行如此大型的应用程序。一个显而易见的解决方案是“只需让集群永久性地变大”。我们的梦想当然是要运行两个完整的版本:启动一个全新的集群,将流量转移到新的集群,在旧的集群仍然存在的情况下验证它,如果有任何问题,只需拨动开关,就可以将流量送回。有时,我们确实开始怀疑是否需要将 EC2 的开销增加一倍,以确保可以进行部署。但是在当时,加倍的开支是行不通的,因此我们必须寻找其他办法来推进。

在 Kubernetes 专家的帮助下,我们花了大量时间讨论是否可以利用滚动部署。当我们开始这样做时,我们的主要关注点就变成了实际的容器,“我们如何帮助它自己更快地部署?如何才能让它在 Kubernetes 领域中更出色呢?”

容器里的工作很多。首先我们要了解的是 unicorn 工作器进程的启动顺序。从 Kubernetes 调度我们的工作负载,到工作负载对我们有用时,这个时间花了多少?结果我们发现,这个启动要用 60~90 秒钟的时间才完成。这就是说,在这个滚动部署期间,我们必须等待 90 秒才能杀死旧的一批,然后再开始新的一批:当你谈论几百个批时,这可不是件好事。

我们首先在本地绘制了启动过程的火焰图,并在各种模式下运行了启动程序。在 Rails,我们研究了“eager load”和“not eager load”。当构建 Docker 容器时,我们尝试预先加载尽可能多的内容。就是在这里,我们找到了一种最有影响力的方法,它可以减少部署时间,这样我还是觉得很有趣。在 unicorn 的启动过程中,在 unicorn 工作器被分叉后,它将回收所有连接到数据库或缓存存储的连接。正是在这里,我们找到了……一个休眠。在连接回收命令发出后,会有一秒钟的休眠,旁边会有一条注释:“给连接重置的时间”。但我们在一个容器中运行了 20~30 个 unicorn 进程。连接回收就像是一个原子操作,调用该方法回收连接,就会发生。你不必等待任何事情。如果把这条休眠线去掉,我们马上就能省下 30 秒钟。当编写任何应用程序时,如果你决定只需等待的话,这就是一个错误的选择。从规模上讲,这绝对是一个错误的选择。

7逐步投入生产

最终,我们缩短了部署时间,令人满意,并逐渐投入生产。最主要的原因之一就是它的安全性。由于产品代码路径未在 staging 阶段中执行,因此我们不希望发现一些可怕的错误。我们正在转移真实客户产生的流量,不能冒着风险让 Zendesk 变得更加缓慢,也不能让错误率增加一倍。

因为我们无法负担运行 EC2 实例的两个副本,逐步推出也使我们可以一点点地获得信心。我们将推出一小部分流量来对其进行验证,让它静置几天,从我们的仪器获得信心,然后将集群减少 5%。接下来,我们按照分区一个一个来,持续 3~4 个月。

Kubernetes 部署所服务的生产流量逐渐增加

它就像是一场跨越式的游戏,一个棘轮运动,我们会向上走一步,然后向下走一步。它拖慢了推出的速度,但是以一种良好的方式放慢。在开始时,我们会有 1~2% 的增量;在结束时,我们可以随意以 10~20% 的增量移动。

最后,我们达到了这样一个点:可以告诉我们的外部代理层,将 100% 的流量发送到新的基础设施。在将近两年的努力之后,我们准备开放 EC2 实例……但是,仍然需要处理旧的基础设施。结果是我们还没有处理好。这个单体包含了大部分 API,我们有许多外部用户,但是还有内部服务在运行。还有一个未知问题是,这些内部服务如何向单体发出请求。如今,我们必须寻找这些“小精灵”,找出它们为什么要跟我们“说话”,以及我们如何将它们引入新的基础设施。这一步骤令人沮丧,因为我们认为它已经完成了。但我喜欢的是,这最终让我们得到了一个标准,能够真正保证人们以一种一致的方式想请求撸油到我们的单体。对于我们来说,这是架构上的重大改进。

8意想不到的后果

目前,我们已经全面进入 Kubernetes,并看到了一些意想不到的后果。到了新的规模级别,我们必须改变很多东西,包括我们如何使用 Consul、网络、DNS 配置、Etcd 集群、节点更新和集群自动调节。

我们现在已经完全进入了 Kubernetes,并看到了一些意想不到的后果。我们已经达到了新的规模水平,并且不得不改变许多事情,包括使用 Consul 的方式、网络、DNS 配置、Etcd 集群、节点更新和我们的集群自动扩缩器。

对 Kubernetes 集群度量服务器的影响是,最初启用了自动扩缩,然后被迫将其禁用

而且我们还享受着意外的收获。使我们的单体在与所有新服务相同的基础设施上运行,意味着当构建公共组件时,我们现在可以将它与 Kubernetes 绑定。通过更快的迭代,我们可以拥有标准的流程,每个人都将从中受益。就像我们最近已经修改了日志基础设施,并且免费获得了所有这些最新的日志一样。

9总结

我们现在已经 100% 使用 Kubernetes 了,感觉非常棒。在 Kubernetes 世界中,我们已经自动地进行了扩缩,转换到后台工作器,使其更有弹性。而且,Kubernetes 还迫使我们更加一致,这是一件好事。近期,我将内部的 Slack 频道 #classic-k8s-rollout 存档。那是我最喜欢的频道之一,同时也是个苦乐参半的时刻,但现在,是继续前进的时候了。

- END -

 推荐阅读 

最新Kubernetes实战指南:从零到架构师的进阶之路 
互联网公司招聘运维工程师【内推】,7月
使用Go语言,25秒读取16GB文件
用 Python 实现快速 Ping 一个 IP 网段地址!
企业级日志平台新秀Graylog,比ELK轻量~
下一代Docker镜像构建神器 BuildKit
面试数十家Linux运维工程师,总结了这些面试题(含答案)
七年老运维实战中的 Shell 开发经验总结
运维的工作边界,这次真的搞明白了!
搭建一套完整的企业级 K8s 集群(v1.20,二进制方式)
12年资深运维老司机的成长感悟



点亮,服务器三年不宕机

浏览 20
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报