使用Kubernetes两年的经验教训
大约两年前,我们决定放弃基于Ansible的安装配置方式,以便在EC2上部署应用程序的方式,并转向使用Kubernetes实现应用程序的容器化和编排。我们已经将大部分基础设施迁移到了Kubernetes。这是一项艰巨的任务以及挑战——从混合部署方式直到大部分迁移完成,再到培训整个团队学习全新的操作范式等等。
在这篇文章中,我们想回顾一下我们的经验,并与你分享我们在这段旅程中所学到的东西,以帮助你做出更好的决策,增加成功的机会。
清楚你迁移到Kubernetes的原因
无服务和容器化是很好的概念。如果你正在创建一个新的业务并从头开始构建所有东西,那么一定要使用容器部署应用程序,如果你有信心(或者可能没有,请继续阅读)和技术能力来配置和操作Kubernetes,以及在Kubernetes上部署应用程序,那么就请使用Kubernetes来部署应用程序。
即使你使用托管的Kubernetes服务,如EKS、GKE或AKS,在Kubernetes上正确部署和操作应用程序也有它的学习曲线。你的开发团队应该准备好迎接挑战。只有当你的团队遵循DevOps理念,团队才能从中获益。如果你让专门的运维团队为开发团队编写应用程序清单,那么从DevOps的角度来看,我们个人认为你将从Kubernetes中获益较少。当然,选择Kubernetes还有很多其他的好处,例如成本、更快的实验、更快的自动伸缩性、弹性等等。
如果你已经在云虚拟机或其他PaaS平台上部署应用程序,请认真考虑你为什么还要迁移到Kubernetes?你相信Kubernetes是解决问题的唯一方法吗?你必须清楚自己的动机,因为将现有基础设施迁移到Kubernetes是一项艰巨的任务。
我们在这方面犯了一些错误。我们迁移到Kubernetes的主要原因是构建一个持续集成平台。在这些年中,我们的应用程序累积了大量的技术债务,我们希望Kubernetes可以帮助我们快速地重新构建我们的微服务。大部分的新功能开发都需要修改多处的代码才能完成,这严重拖慢了我们新特性的开发速度。我们觉得有必要为每个开发人员和每个变更提供一个集成环境,以帮助加快开发和测试周期,而不需要协调,在同一套环境上进行开发。
下图是我们的持续集成管道之一,它提供了一个新的集成环境和所有的微服务,并运行自动化测试。
我们现在做得很好。我们今天在Kubernetes上,可以在8分钟内部署一个具有21个微服务的集成环境。任何开发人员都可以使用我们自己开发的工具来实现这一点。针对每个pull request我们亦可创建一个集成环境。整个测试周期(部署环境、配置环境以及运行测试)不到12分钟。你可能觉得12分钟很长,但它确实帮助我们有效验证了每一次发布变更。
下图为持续集成管道的执行报告。
太好了!我们怎么做到的?我们花了将近1年半的时间来完成这一切。值得吗?
我们花了将近1年半的时间,通过构建额外的工具、自动化和重构每个应用程序来稳定这个复杂的持续集成。为了实现开发环境和生产环境的一致性,我们必须将所有微服务部署到生产环境中,否则,基础设施和部署设置之间的偏差将使开发人员难以理解和操作应用程序。
我们对这个话题有着复杂的感情。回顾过去,我们认为我们让解决持续集成的问题变得更糟,因为将所有微服务推到生产环境中以实现开发环境和生产环境的一致性,这使持续集成变得更复杂和困难,也大大降低了持续集成的速度。在Kubernetes之前,我们使用Ansible,Hashicorp Consul和Vault进行基础设施部署以及配置。它慢吗?是的,当然慢。但我们认为我们可以引入Consul服务发现,并对Ansible部署进行优化,以便在较短的时间内接近我们的目标。
我们应该迁移到Kubernetes吗?是的,当然。使用Kubernetes有几个好处——服务发现、更好的成本管理、弹性、治理、云基础设施等等。我们今天也收获了所有这些好处。但这并不是我们刚开始时的主要目标,我们自己强加给我们的压力和付出的痛苦也许是不必要的。
对我们来说,一个重大的教训是,我们本可以采取一种不同的、阻力较小的方式来采用Kubernetes。但我们被Kubernetes收购了,Kubernetes是我们唯一的选择,甚至不需要去评估其他解决方案。
在这篇文章中,我们将看到Kubernetes上的迁移和操作与部署在云上或裸机上是不一样的。你的工程师团队会有一个学校曲线。让你的团队经历一下也许是值得的。但问题是你现在需要这么做吗?你一定要想清楚。
开箱即用的Kubernetes对任何人来说都是远远不够的
很多人会把Kubernetes当作PaaS解决方案——它不是PaaS解决方案。它是构建PaaS解决方案的平台。例如OpenShift就是一个基于Kubernetes的PaaS的平台。
开箱即用的Kubernetes对几乎任何人来说都是远远不够的。这是一个学习和探索的好地方。但是,你很可能需要更多的基础设施组件,并将它们作为应用程序的一部分,有机的结合在一起,使其对你的开发人员更有意义。通常这种带有附加基础设施组件和策略的Kubernetes平台,我们称其为“Internal Kubernetes Platform[1]”。有几种方法可以扩展Kubernetes平台[2]。
在度量指标监控、日志、服务发现、分布式链路追踪、配置和秘钥管理、CI/CD、本地开发经验、自动伸缩等方面,我们都需要考虑和作出决策。以上这些只是我们能想到的。当然还有更多的决策要做,更多的基础设施需要建立。一个重要的方面是你的开发人员将如何使用Kubernetes资源和清单,在稍后的文章中我们将对此进行详细介绍。
以下是我们的作出的一些决策和理由。
度量指标监控(Metrics)
我们最终决定使用Prometheus。在度量指标监控方面,Prometheus几乎是一个事实标准。CNCF和Kubernetes非常喜欢它。它在Grafana生态系统中非常有效。我们也很喜欢Grafana!我们唯一的问题是以前用的是InfluxDB。我们决定离开InfluxDB,完全投入Prometheus。
日志
日志一直是我们的大问题。我们一直在努力使用ELK创建一个稳定的日志记录平台。我们发现在ELK中充满了对我们团队没有实际使用的功能。这些功能是有代价的。另外,我们认为在日志中使用Elasticsearch存在固有的挑战,这使得它成为一个昂贵的日志解决方案。我们最后决定使用Loki。它很简单,具有满足我们团队需要的必要功能。最重要的是,它的查询语言与PromQL非常相似,具有良好的用户体验。而且,它与Grafana配合得很好。更可以将整个度量指标监控和日志记录平台集中在一个用户界面中。
下图是Grafana仪表板的一个示例,它可以同时显示度量指标和相应的日志。
配置管理和秘钥管理
在Kubernetes中,你会发现大多数文章使用ConfigMap和Secret对象。我们的经验是它们可以满足你的基本需求。在现有服务中使用ConfigMap是要付出一定代价的。ConfigMap可以以某种方式挂载到Pod中,使用环境变量是最常见的方式。如果你有大量的遗留微服务,从配置管理工具(如Puppet、Chef或Ansible)中读取配置文件,则必须重构现有的代码,以便从环境变量中读取。我们没有找到足够的理由去做这件事。另外,配置文件的更改意味着您必须重启服务。
为了避免这一切,我们决定使用Consul、Vault和Consul Template进行配置管理和秘钥管理。我们将Consul Template作为init容器运行,并在Pod中添加一个sidecar容器,以便它可以监视Consul Template中的配置更改,刷新Vault中过期的秘钥信息,并优雅地重新加载应用程序进程。
CI/CD
在迁移到Kubernetes之前,我们使用Jenkins做CI/CD。迁移到Kubernetes之后,我们决定依然使用Jenkins。到目前为止,我们的经验是,Jenkins并不是云场景下的最佳解决方案。我们用Python、Bash、Docker和脚本化/声明性的Jenkins管道做了大量的工作,使CI/CD可以工作。编写和维护这些工具和管道开始变得很昂贵。我们现在正在探索Tekton和Argo Workflows工作流作为我们新的CI/CD平台。你也有其他更多的选择,如Jenkins X,Screwdriver,Keptn等。
开发经验
在开发中,有很多种方法使用Kubernetes。我们主要集中在两个选项上——Telepresence.io和Skaffold。Skaffold能够监视你的本地更改,并不断地将它们部署到Kubernetes集群中。另一方面,Telepresence允许你在本地运行服务,同时使用Kubernetes集群设置一个透明的网络代理,以便你的本地服务可以像部署在集群中一样与Kubernetes中的其他服务通信。这是个人意见和喜好的问题。很难决定哪一种工具更好。我们现在主要是在尝试Telepresence,但我们没有放弃Skaffold 成为更好工具的可能性。只有时间会告诉我们到底使用,或者两者同时使用。当然还有其他的解决方案,比如Draft。
分布式链路追踪
我们还没有实现分布式链路追踪。不过,我们已经在计划中。与日志一样,我们希望也能将分布式链路追踪集成在Grafana中,以便为我们的开发团队提供更佳的可观察性体验。
应用程序打包、部署和工具
Kubernetes的一个重要方面是考虑开发人员将如何与集群交互并部署他们的工作负载。我们想让事情变得简单,易于扩展。我们正在尝试Kustomize,Skaffold和一些自行开发的CRDs,作为开发人员部署和管理应用程序的方式。尽管如此,任何团队都可以自由地使用他们想使用的任何工具来与集群交互,只要他们是开源的,并且构建在开放标准之上。
操作Kubernetes集群很困难
我们的业务主要在新加坡地区运营。在我们开始Kubernetes的旅程时,EKS还没有在新加坡地区提供服务。所以我们不得不在EC2上使用kops建立自己的Kubernetes集群。
建立一个开箱即用的集群也许没有那么困难。我们能够在一周内启动我们的第一个集群。大多数问题发生在开始部署工作负载时。如何将集群调整到最佳状态,自动扩缩容该如何设置,网络该怎么配置?你必须自己进行研究和配置。默认值在大多数情况下都不适用于生产(或者至少对我们不起作用)。
经过两年的生产经验,我们认为操作Kubernetes是很复杂的。Kubernetes有很多组件组成。你更关心的是你的核心业务,而不是如果操作Kubernetes,尽可能地将这些事情交给云服务提供商,比如EKS、GKE、AKS等。你不能从操作Kubernetes中获得任何价值。
你还得考虑升级
Kubernetes非常复杂,即使你使用的是托管服务,升级也不会一帆风顺。
即使使用了托管Kubernetes服务,也应尽早将灾难恢复和升级的过程自动化,能够在灾难面临前快速做出反应。
如果你愿意,你可以尝试GitOps理念。如果不能做到这一点,那么将手动步骤减少到最低限度也是一个很好的开始。我们使用eksctl、terraform和我们的集群配置清单(包括平台服务的清单)组合来建立我们的Kubernetes平台 ——“Grofers Kubernetes Platform”。为了使部署和配置过程更简单和可重复,我们构建了一个自动化管道来设置新的集群并将更改部署到现有集群中。
资源请求和限制
在开始迁移之后,我们发现集群中由于配置不正确而出现了许多性能和功能问题。其效果之一是在资源请求和限制中添加了大量的缓冲区,以消除资源限制作为性能降低的可能性。
最初的现象是由于节点内存不足而导致Pod被迫逐出。原因是与Pod的资源请求相比,资源限制过高。随着流量的激增,内存消耗的增加可能会导致节点上的内存饱和,进而导致Pod被迫逐出。
经过两年的生产经验,我们认为保持资源请求足够高,但不要太高,以防止在低流量时间浪费资源,并将资源限制相对接近资源请求,以便为尖峰流量提供一些喘息空间,而不会因节点内存压力而导致Pod被迫逐出。资源限制与资源请求的具体数值取决于你的流量模式。
但以上经验并不适用于非生产环境,如开发环境。这些环境下的流量不会出现任何峰值。理论上,如果将CPU请求设置为零,并为容器设置足够高的CPU限制,则可以运行无限的容器。如果你的容器开始大量使用CPU,它们将被限制。对于内存请求和限制也可以这样做。然而,达到内存限制的行为与CPU不同。如果使用的内存超过了设置的内存限制,那么容器就会被杀死并重新启动。如果内存限制异常高(假设高于节点的容量),则可以继续使用内存,最终当节点的可用内存不足时,调度程序将开始逐出Pod。
在非生产环境中,我们通过保持资源请求极低而限制极高,来尽可能安全地过度提交资源。在这种情况下,限制因素是内存,也就是说,无论内存请求有多低,内存限制有多高,Pod逐出,都是一个节点上所有容器使用的内存总和的函数。
安全与治理
Kubernetes旨在为开发者打造一个云平台,使他们更加独立,并推动DevOps文化。向开发人员开放平台,减少云工程师团队(或系统管理员)的干预,使开发团队更独立,应该是重要目标之一。
有时这种独立性可能带来严重的风险。例如,在EKS中使用LoadBalancer类型的服务,默认情况下提供面向公共网络的ELB。添加某个注释,确保创建的是内部ELB,而不是面向公共网络的。我们很早就犯了这些错误。
我们使用Open Policy Agent来降低这些安全风险,以及与成本、安全和技术债务相关的风险。
使用Open Policy Agent来构建正确的自动化变更管理过程,并为我们的开发人员构建正确的安全网。使用Open Policy Agent,我们可以限制服务对象的创建,除非出现正确的注释,这样开发人员就不会意外地创建面向公共网络的ELB。
成本
我们在迁移后看到了巨大的成本效益。然而,并不是所有的好处都是立竿见影的。我们正在整理一份关于成本效益的更详细的帖子。
更好地利用资源容量
这是最明显的一个。我们使用了更少的计算、内存和存储资源,提供了和以前一样的基础设施能力。除了因为容器和流程变得更好了,我们更好地利用了我们的共享服务,比如可观察性流程(度量指标监控和日志)。
然而,最初我们在迁移的时候浪费了大量的资源。由于我们无法以正确的方式优化自我管理的Kubernetes集群,这导致了大量的性能问题,我们最终在Pod中添加了大量的资源请求作为缓冲,更像是保险,以减少由于计算或内存不足而导致的停机或性能问题的可能性。
由于大量的资源缓冲,基础设施成本高是一个大问题。由于Kubernetes的原因,我们并没有意识到Kubernetes带来的产能好处。在迁移到EKS之后,Kubernetes的稳定性帮助我们变得更加自信,帮助我们采取必要的步骤来纠正资源请求,并大幅减少资源浪费。
Spot实例
在Kubernetes中使用Spot实例会变得容易得多。在使用Spot实例时,Spot实例随时可能被回收,对于一般应用程序而言,如何确保应用程序一直正常运行,这可能会变得更复杂。但对于Kubernetes来说,对于中断的容器,可以被快速的重新调度。
Spot实例同时帮助我们节省了大量资金。今天,我们的测试集群运行在Spot实例上。
对我们来说,下一步优化是如何将生产集群运行在Spot实例上。在另一篇博文中有更多关于这个主题的文章。
ELB合并
我们使用Ingress来整合测试环境中的ELB,并大幅降低ELB的支出成本。为了避免在代码中,实现开发与测试/生产环境的差异,我们决定实现一个控制器,它将LoadBalancer类型的服务与测试/生产环境中的一个Ingres对象一起变更为NodePort类型的服务。
对于我们来说,迁移到Nginx ingress相对简单,由于我们的控制器方法,不需要太多更改。如果我们在生产中也使用Ingress,可以节省更多的成本。这不是一个简单的改变。在以正确的方式为生产环境配置Ingress时,必须考虑几个问题,并且需要从安全性和API管理的角度来考虑。这是我们打算在不久的将来开展工作的领域。
增加跨AZ数据传输
虽然我们节省了大量的基础设施开支,但有一个基础设施领域的成本会增加——跨AZ数据传输。
Pod可以被分配到任何节点。即使你控制了Pod在集群中的分配方式,也没有简单的方法来控制服务如何发现彼此,即一个服务的Pod与同一AZ中的另一个服务的Pod进行通信,以减少跨AZ的数据传输。
在与其他公司的同行进行了大量的研究和交谈之后,我们了解到,通过引入服务网格来控制从一个Pod到另一个Pod的流量是如何路由的,这是可以实现的。我们还没有准备好仅仅为了节省跨AZ数据传输的成本而自己承担操作服务网格的复杂性。
CRD、Operators和控制器——简化操作
每个组织都有自己的工作流程和运维挑战。我们也有我们的。
在两年的Kubernetes之旅中,我们了解到Kubernetes很棒,当你使用它的功能,如控制器、Operators和CRD,来简化日常操作,并为开发人员提供更集成的体验时,它简直太棒了。
我们已经开始投资创建一系列Operators和CRD。例如,LoadBalancer服务类型到Ingress的转换是一个控制器操作。类似地,每当部署新服务时,我们使用控制器在DNS服务器中自动创建CNAME记录。这是几个例子。我们还有5个单独的用例,我们依赖内部控制器来简化日常操作并减少工作量。
我们还建造了一些CRD。其中一种被广泛用于在Grafana上生成监控仪表板,声明性地指定应该使用什么样的监控仪表板。这使得开发人员可以在应用程序代码库旁边签入监视仪表板,并使用相同的工作流kubectl apply -f … 部署所有内容。
我们看到了控制器和CRD的巨大优势。当我们与云供应商AWS密切合作以简化集群基础设施操作时,我们将更多精力放在构建“the Grofers Kubernetes platform”上,该平台架构的宗旨是以最佳方式支持我们的开发团队。
相关链接:
- https://itnext.io/why-everyone-builds-internal-kubernetes-platforms-284c2cf76226
- https://speakerdeck.com/gianarb/cloud-native-ambassador-day-extending-kubernetes
原文链接:https://lambda.grofers.com/learnings-from-two-years-of-kubernetes-in-production-b0ec21aa2814