手把手教你度过运维 Kubernetes 瓶颈期
翻译:Ashley,夏天
校对:岑鹏浩
最近,基于 Kubernetes 我们创建了一个分布式的 cron job 调度系统,这是一个令人兴奋的容器编排新平台。目前,Kubernetes 非常受欢迎,并且给人们许下了一系列承诺,其中最让人兴奋的一个就是,以后工程师们再也不需要关心或了解他们的应用程序正运行在哪个机器上了。
分布式系统非常难,而管理分布式系统上的服务又是运维团队面临的最大困难之一。因此,如何在生产环境中部署新软件,以及学习如何可靠地运维对我们来说非常重要。想学好如何运维 Kubernetes,多看例子非常重要(为什么就那么难!),我们这正好有一个由 Kubernetes 中的一个 bug, 引起 1 小时停机的小故事。接下来,本文将解释我们为什么会选择部署在 Kubernetes 之上;如何将 Kubernetes 集成到现有的基础设施中;如何建立并提高对 Kubernetes 集群可靠性的信心,以及我们是怎样在 Kubernetes 之上建立抽象层的 。
Kubernetes 是什么?
Kubernetes 是一个在集群中调度应用程序的分布式系统。你可以让 Kubernetes 运行一个程序的五个副本,它就会动态地在你的工作节点上调度这些副本。
自动调度的容器可以有效提高利用率,并节省资金,强大的部署基元(即 Workloads API,例如 Pod,Deployment 等) 帮你逐步升级新的代码,安全上下文和网络策略可以让你安全的运行多租户任务。
Kubernetes 内置多种不同的调度功能。它可以调度长时间运行的 HTTP 服务,集群中每台机器上运行的守护进程(DaemonSet),每小时定期运行的 cron jobs 等等。Kubernetes 有趣之处还有很多。如果你想了解更多,Kelsey Hightower 曾经做过很多优秀的演讲。比如,Kubernetes for Sysadmins 和 Healthz: Stop Reverse Engineering Applications and Start Monitoring from the Inside ,对入门者来说,这是两个非常好的演讲。另外,Slack 上也有一个非常棒的相关社区。
为什么用 Kubernetes?
每个基础设施项目都是从业务需求开始的,我们的目标是,提高现有分布式 cron job 系统的可靠性和安全性。
我们需要建立和运作一个相对较小的团队(只有 2 人是全职工作)
我们需要在大约 20 台机器上调度 500 多个不同的 cron job
我们希望建立在现有的开源项目之上
Kubernetes 包含一个分布式 cron job 调度器,所以我们不必自己写一个
Kubernetes 是一个非常活跃的项目,并且社区在持续对他贡献
Kubernetes 是用 Go 语言写的,很容易学习。几乎所有的 Kubernetes bug 修复都是由我们团队中具有初级经验的 Go 程序员完成的
如果我们能够成功的运行 Kubernetes,将来就可以在 Kubernetes 的基础上进行构建(例如,我们正在研究一个基于 Kubernetes 的用来训练机器学习模型的系统)
我们之前一直在使用 Chronos 作为 cron job 的调度系统,但是现在它已经不再满足我们的可靠性要求,而且大部分是无人维护的(在过去的 9 个月中只有 1 次代码提交,而且最后一次代码合并的时间是 2016 年 3 月) 由于 Chronos 没人维护,因此我们觉得它不值得继续贡献以改善现有的集群。
如果你正在考虑使用 Kubernetes,请记住一点:千万不要仅仅因为其他公司正在使用 Kubernetes 而使用 Kubernetes。建立一个可靠的集群需要花费大量的时间,过于商业的案例使用 Kubernetes 产出效果并不明显。友情提示,还是按需分配有限的时间比较明智。
究竟什么是可靠性?
说到运维一个服务,“可靠性”这个词语本身就没有意义。可是要谈论可靠性,首先你需要建立一个 SLO(服务水平目标)。
99.9% 的 cron jobs 应该在 20 分钟内被调度并开始运行。20 分钟之内其实是非常不精确的时间,但我们采访了我们的内部客户,他们对时间都没有更精确要求
99.99% 的 jobs 应该在一定的时间内完成(没有被主动终止)
向 Kubernetes 做的迁移应该是对客户无感的
Kubernetes API 中的短暂停机时间是可以接受的(如果停机十分钟,只要五分钟内恢复,就可以)
调度的 bug(cron job 完全没有被调度,并且根本无法运行)是不可接受的。我们非常重视调度错误的报告
对于 Pod 驱逐我们要小心,并安全地终止实例,以免 job 过于频繁地被终止
我们需要一个好的迁移计划
构建一个 K8S 的集群
我们构建第一个 Kubernetes 集群的基本方法是,从零开始,而不是使用像 Kubeadm 或 Kops(使用 Kubernetes The Hard Way 作为参考)之类的工具。我们配置了常用的配置管理工具 Puppet。从零开始部署其实非常好,原因有二:其一,我们能够将 Kubernetes 深入整合到我们的架构中;其二我们可以深入了解其内部。
从头开始让我们将 Kubernetes 整合到我们现有的基础设施中。我们希望 Kubernetes 能与现有的日志,证书管理,秘钥,网络安全,监控,AWS 实例管理,部署,数据库代理,内部 DNS 服务器,配置管理等系统进行无缝集成。整合所有这些系统有时需要一点创造力,但总的来说,比使用 Kubeadm / Kops 去做更容易。
我们已经知道如何运维所有这些现有的系统,所以我们希望继续在新的 Kubernetes 集群中使用它们。例如,安全证书管理是一个非常困难的问题,我们已经有一种方法来颁发和管理证书。这能够避免为 Kubernetes 创建一个新的证书。
在这过程中,我们不得不了解,我们设置的参数究竟是如何影响 Kubernetes 设置的。例如,在配置用于认证的证书时使用了十几个参数。了解所有这些参数可以让我们在遇到与身份验证有关的问题时,更容易调试设置。
对 K8S 建立信心的 7 个策略
在我们的 Kubernetes 工作开始之前,团队中并没有人曾使用过 Kubernetes(除了在某些情况下看过几眼)。面对这样的情况,你要如何从 “我们从来没使用过 Kubernetes” 转变到“ 我们有信心在生产环境使用 Kubernetes”? 接下来,我来分享以下 7 个策略!
与其他公司的人交谈
我们向其他公司的一些人询问了他们使用 Kubernetes 的经历。这些人的共同点是,都曾以不同的方式或者在不同的环境下使用过 Kubernetes(运行过 HTTP 服务,在裸机或 GKE 上使用过,等等)。
特别是当谈到像 Kubernetes 这样庞大而复杂的系统时,对我们来说,重要的是要认真思考自己的应用实例,做自己的实验,建立对自己运行环境的信心,最后做出自己的决定。举例来说,你不应该读完这个博客文章,就得出结论:“好吧,Stripe 既然已经成功使用 Kubernetes,所以它也适用于我们!”
以下是我们在与几家运营 Kubernetes 集群的公司进行对话之后所学到的知识:
优先考虑你的 etcd 集群的可靠性(etcd 是你存储所有 Kubernetes 集群状态的地方)
Kubernetes 的有些功能比另一些功能更稳定,所以要小心 Alpha 版本的功能。一些公司只有在稳定版本发布多个版本之后才会使用那些稳定的功能(例如,如果某个功能在 1.8 版本中是稳定,他们则会在 1.9 或 1.10 版本开始使用)
考虑使用像 GKE / AKS / EKS 这样的托管 Kubernetes 系统。我们都知道自己建立一个高可用性的 Kubernetes 系统是一项巨大的工程。AWS 在这个项目中没有托管的 Kubernetes 服务,所以它并不是我们的选择
请注意因为 overlay 网络/软件定义网络引入的额外网络延迟
虽然与其他公司的谈话并没有给我们一个明确的答案,或者告诉我们 Kubernetes 是否会为我所用,但这样的谈话确实给我们提供了需要注意的点以及新的启迪。
阅读代码
当我们计划大规模使用 Kubernetes 的一个组件 cron job 控制器时。这个组件当时还是 Alpha 版本,这让我们有点担心。虽然我们在一个测试集群中试了一下,但是我们怎么知道它是否会在我们的生产环境中工作呢?
值得庆幸的是,所有的 cronjobs 控制器的核心功能只有 400 行。通过源代码快速读取显示:
cron job 控制器是一个无状态的服务(与其他 Kubernetes 组件一样,除了etcd)
每十秒钟,这个控制器就调用一次 syncAll 函数:
go wait.Until(jm.syncAll, 10*time.Second, stopCh)
该 syncAll 函数从 Kubernetes API 中获取所有 cron job,遍历该列表,确定下一个应该运行的 jobs,然后启动这些 jobs
核心逻辑似乎相对容易理解。更重要的是,我们觉得如果在这个控制器中有一个 bug ,这就有可是我们可以修复的东西。
做负载测试(压力测试)
在我们开始认真构建集群之前,我们做了一些负载测试。我们并不担心 Kubernetes 集群究竟能够处理多少个节点(我们打算部署大约 20 个节点),但是我们确实希望让某些 Kubernetes 能够处理和我们想要的一样多的 cron jobs(每分钟大约 50 个)。
我们在一个 3 节点的集群中进行测试,创建了 1000 个 cron jobs,每个 jobs 每分钟运行一次。所有这些工作只是跑了 bash -c 'echo hello world'.。我们选择简单的 job 是因为我们想测试集群的调度和编排能力,而不是集群的总计算能力。
我们的测试集群每分钟无法处理 1000 个 cron jobs。我们观察到每个节点每秒最多只能启动一个 Pod,群集每分钟能正常运行 200 个 cron jobs。由于我们只想每分钟运行大约 50 个 cron jobs ,所以我们认为这些限制并不是一个阻碍因素(如果需要的话,我们可以在以后找出这些限制)。所有测试结果已经 OK,准备全力以赴!
优先构建和测试高可用性 etcd 集群
在设置 Kubernetes 时最重要的一件事就是运行 etcd。etcd 是 Kubernetes 集群的核心,它是存储集群中所有数据的地方。etcd 以外的东西都是无状态的。如果 etcd 未运行,你就不能对 Kubernetes 集群进行任何更改(尽管现有服务将继续运行!)。
下图显示了 etcd 是如何担当 Kubernetes 集群的核心的。 API server 是 etcd 前面的无状态 REST 服务/鉴权终端,然后每个其他组件通过 API server 与 etcd 通信。
运行时,有两个重要的点要牢记在心:
设置副本,以便在丢失节点时不会造成群集宕机。我们现在有三个 etcd 副本
确保你有足够的 I/O 带宽可用。我们的 etcd 版本有一个问题,一个高 fsync 延迟的节点可能会引发持续的 leader elections,从而导致集群不可用。通过确保所有节点的 I/O 带宽高于 etcd 执行的写入次数可以修复此问题
设置副本不是“一劳永逸”运维。我们仔细测试过,实际上我们可能会丢失一个 etcd 节点,并且集群可以正常恢复。
以下是我们设置 etcd 集群所做的一些工作:
设置副本
监控 etcd 服务是否可用(一旦 etcd 停机,我们就要马上知道)
写一些简单的工具,以便我们可以轻松地启动新的 etcd 节点,并将它们加入到群集中
升级 etcd 的 Consul 集成,以便我们可以在我们的生产环境中运行多于 1 个 etcd 集群
尝试从 etcd 的备份数据中恢复
测试是否可以在没有停机的情况下重建整个集群
我们很高兴很早就做了这个测试。在我们的生产集群过程中的一个星期五上午,其中一个 etcd 节点突然停止了对 ping 的响应。我们收到警报后,终止了 node,重建了一个新的 node,并把它加入到集群中,Kubernetes 继续运行,没有发生任何事故。这简直太棒了!
逐步将 jobs 迁移到 Kubernetes
我们的主要目标之一是将我们的 jobs 迁移到 Kubernetes 而不造成任何中断。成功迁移的秘密不是避免犯错(这是不可能的),而是通过精心设计,降低错误对迁移的影响。
很幸运,我们已经让很多种 jobs 迁移到我们的新集群上,所以,我们可以迁移一些对整体影响较低的 jobs,就算有 1-2 个失败也在容错范围内。
迁移之前,我们新建了易于使用的工具,可以让我们在不到五分钟的时间内,实现新旧系统之间来回切换。这个简单的工具减少了错误的影响。如果我们转移了一个计划之外有依赖性的 jobs,也没有什么大不了的!我们可以将其移回原处,解决掉,再来一次。
以下是我们所采用的整体迁移策略:
粗略地按照重要程度给他们排序
反复把部分 jobs 转移到 Kubernetes。如果我们发现新的极端案例,快速回滚,修复问题,然后重试
调查 Kubernetes 漏洞(并修复)
在项目开始的时候我们制定了一个规则:如果 Kubernetes 做了一些出人意料的事情,我们必须进行调查,找出原因并提出补救措施。
调查每个问题很耗时,但却非常重要。如果我们根据分布式系统的复杂程度简单地将 Kubernetes 中的片状和奇怪的行为排除在外,我们估计不得不因为成堆的漏洞而随时待命。采取这种方法后,我们发现了 Kubernetes 的几个漏洞。
以下是我们在这些测试中发现的一些问题:
名字超过 52 个字符的 Cronjob 无法安排工作
Pod 有时会永久卡在待定状态
调度程序每 3 小时崩溃一次
Flannel 的 hostgw 后端没有替换过时的路由表条目
修复这些错误将提升 Kubernetes 的项目使用体验。不仅能运转良好,也接受补丁,并有良好的 PR 审查流程。
像所有软件一样,Kubernetes 肯定有漏洞。特别是,当我们使用调度程序非常频繁时(因为我们的cron job 在不断创建新的 Pod),调度程序使用缓存有时会导致漏洞,回归和崩溃。缓存很难!但是,代码库容易访问,我们已经能够处理我们遇到的错误。
另一个值得一提的问题是 Kubernetes 的 Pod 驱逐逻辑。Kubernetes 有一个称为节点控制器(node controller)的组件,如果节点没有响应的话负责将 Pod 从该节点驱逐,并迁移到另一个节点。很有可能所有节点,均暂无响应(比如,网络或配置出现问题的时候),在这种情况下,Kubernetes 可以终止群集中的所有 Pod。这一般会发生在测试早期。
如果你运行的是大型 Kubernetes 群集,请仔细阅读节点控制器(node controller)文档,仔细考虑设置并进行广泛的测试。每当我们通过创建网络分区测试,对这些设置的配置进行更改的时候,就会发生令人吃惊的事情(例如--pod-eviction-timeout)。在测试中发现这些情况,总比在凌晨 3 点发现要好的多。
有意引起 Kubernetes 集群问题
之前我们已经讨论过在 Stripe 上运行游戏日的练习,而且我们还是经常这样做。这个想法是要想出你最终会在生产中发生的情况(例如,丢失一个 Kubernetes API server),然后在生产中(在工作日期间有警告地)故意造成这些情况,以确保你能够处理它们。
在我们的集群上进行了几个试验之后,他们经常发现监控或配置错误等问题。我们很高兴能够在六个月后以在可控范围内尽早发现这些问题,而不是发生在意料之外。
以下是我们运行的一些游戏日的练习:
终止一个 Kubernetes API server
终止所有的 Kubernetes API server,并把它们恢复(令我们惊讶的是,这运行得很好)
终止一个 etcd 节点
从 API server 中切断我们的 Kubernetes 集群中的工作节点(致使它们不能通信)。使得这些节点上的所有 Pod 被移动到其他节点。
我们很高兴看到 Kubernetes 如何应对我们设置的大量干扰。 Kubernetes 的设计是为了适应错误。它有一个存储所有状态的 etcd 集群,一个简单的 REST 接口的 API server 和一个协调所有集群管理的无状态控制器集合。
如果任何 Kubernetes 核心组件(API server,controller manager 或 scheduler)被中断或重新启动,一旦它们出现,就会从 etcd 中读取相关的状态并继续无缝运行。这是我们所希望的事情之一,在实践中也运作良好。
以下是我们在这些测试中发现的一些问题:
奇怪,该分页的地方没有分页,让我们来修理监控
当我们销毁我们的 API server 实例并将其恢复时,他们需要人工干预。我们最好解决这个问题
有时,当我们执行 etcd 故障转移时,API server 会启动超时请求,直到我们重新启动
运行这些测试之后,我们针对所发现的问题做了补救措施:改进了监控,修复了我们发现的配置问题,并提交了 Kubernetes 的漏洞。
使 corn jobs 易于使用
让我们简单地探讨一下如何让基于 Kubernetes 的系统更好用。
我们最初的目标是设计一个运行 cron jobs 的系统,我们的团队有信心运行和维护。一旦我们建立了对 Kubernetes 的信心,我们需要让同事们轻松配置和添加新的 cron job 。我们开发了一个简单的 YAML 配置格式,方便我们的用户即使不了解有关 Kubernetes 内部的任何信息,也能顺利使用该系统。
这是我们开发的格式:
我们并没有做任何特别的事情。仅仅是编写了一个简单的程序来采取这种格式,并将其转换成我们使用 Kubectl 应用的 Kubernetes cron job 配置。
此外,我们还编写了测试套件,以确保 job 名称不会太长(Kubernetes cron job 名称不能超过 52 个字符),并且所有名称都是唯一的。我们目前不使用 cgroup 来强化我们大部分工作的内存限制,但是这是我们计划在未来推出的。
我们的简单格式很容易使用,而且由于自动生成了 Chronos 和 Kubernetes cron job 定义格式相同的格式,因此在两个系统之间移动 job 非常简单。这是使我们的增量迁移工作顺利运转的关键因素。无论何时将 jobs 转移到 Kubernetes 产生了任何问题,我们都可以在不到十分钟的时间内通过简单的三行配置更改将其移回。
监控 Kubernetes
监控我们的 Kubernetes 集群的内部状态是非常顺利的。我们使用 kube-state-metrics 软件包进行监控,并使用名为 veneur-prometheus 的小型 Go 程序来抓取 Prometheus 度量指标 kube-state-metrics,并将它们作为统计指标发布到我们的监控系统。
例如,以下是过去一小时内群集中待处理群的数量的图表。
Pending 意味着他们正在等待分配一个工作节点来运行。你可以看到这个数字在上午 11 点抵达峰值,因为我们很多的 cron job 都是在每个小时的第 0 分钟运行的。
我们还有一个监控器,用来检查没有挂在 Pending 状态的 Pods,我们检查每个 Pod 是否在 5 分钟内就能开始在 worker 节点上运行,否则将会收到警报。
Kubernetes 的未来
设置 Kubernetes 到合适的地方运行生产环境的代码,将所有的 cron jobs 迁移到新的集群,花了三名全职工程师五个月的时间。我们投资学习 Kubernetes 的一个重要原因是我们希望能够在 Stripe 中更广泛地使用 Kubernetes。
以下是适用于运行 Kubernetes(或任何其他复杂分布式系统)的原则:
为你的 Kubernetes 项目(以及所有基础设施项目)定义明确的商业原因!了解业务案例和我们用户的需求使我们的项目变得更加容易
积极削减范围。我们决定避免使用许多 Kubernetes 的基本特性来简化我们的集群。这让我们可以更快地运行。例如,由于 pod-to-pod 联网不是我们项目的必要条件,我们可以阻断节点之间的所有网络连接,并将 Kubernetes 的网络安全性放到未来的项目
花大量时间学习如何正确运维 Kubernetes 集群。仔细测试极端案例。分布式系统是非常复杂的,有很多潜在的问题容易出错。以我们之前描述的例子为例:根据你的配置,节点控制器(node controller)在与 API server 失去联系时终止集群中的所有的 Pod。需要花时间和精力了解 Kubernetes 在每次配置更改后的表现如何
注意到这些原则后,我们可以放心地使用 Kubernetes。随着时间的推移,我们将继续发展和演进 Kubernetes 的使用,例如,我们正在关注 AWS 的 EKS 版本,完成另一个系统的 job 来训练机器学习模型,同时也在研究将一些 HTTP 服务移动到 Kubernetes 的工作。随着在生产中持续使用 Kubernetes,我们也将计划持续贡献开源项目。
点击“阅读原文”查看原文
推荐阅读:
Q & A | 袁琳峰:使用 Operator 来扩展 Kubernetes
END