2017年.我们构建了第一个kubernetes集群,版本是1.9.4。有两个集群,一个用裸机RHEL VMs运行,另一个用的是AWS EC2。时至今日,我们的Kubernetes基础架构团队有超过400个虚拟机,遍布多个数据中心。这个平台有高度可用且关键的软件应用和系统,能管理运行近四百万个活跃机器的大型实时网络。
虽然kubernetes使我们的生活更轻松了,但过程会蛮艰辛,要经过范式的改变。不仅要完全更迭我们的技能和工具,还有设计和思想。我们必须掌握许多新的科技,大幅扩充提升团队和基础设施。
回首用kubernetes产出的这三年,我们得出以下关键经验。
1. 应用的奇怪案例
涉及到微服务和容器化,工程师们倾向于不使用Java,主要是因为它糟糕的内存管理。然而Java已不同往日,它的容器适配性在几年里已有提高。毕竟大多数系统都用Java运行,如Apache Kafka和Elasticsearch。
在2017-2018年,只有一些应用在Java8运行。这些应用通常很难适应Docker内存系统,并且很容易因为堆内存问题和异常的垃圾收集趋势而崩溃。这是由于Java虚拟机不能遵守Linux cgroup和namespace造成的,而他们是容器化技术的核心。
然而甲骨文公司在这之后不断提升Java容器化的适配性。Java8的后续补丁也引入了实验性的Java虚拟机flag标示来解决这些问题:
- XX:+UnlockExperimentalVMOptions
- XX:+UseCGroupMemoryLimitForHeap
但Java仍名声不佳,对比同行的python和Go,Java占用内存且启动速度慢。主要是由Java虚拟机的内存管理及类加载器造成的。
现在如果必须选择Java,我们会确保版本为11及以上,并且我们的Kubernetes内存设定为Java虚拟机最大堆内存( -Xmx )之上的1GB,以提供余量。也就是说,如果JVM使用8GB的堆内存,则该应用的Kubernetes资源限制就是9GB。有了它生活会更好。
图源:unsplash
2. Kubernetes的生命周期升级
Kubernetes的生命周期管理很繁杂,比如它的升级和加强,特别体现在用裸机或者VM搭建的集群。在升级时,我们发现搭建新集群最简单的方式就是用最新的版本并将工作负载从旧版本转移到新版本。努力和计划在模型内进行升级是不值得的。
Kubernetes有多个运行插件,需要与升级同步。Docker、Calico或Flannel之类的CNI插件都需要仔细地将它们组合在一起才能正常工作。虽然一些项目可以使它变得更容易运行,如Kubespray、Kubeone、Kops和Kubeaws,但它们都有缺点。
我们在RHEL VM上使用Kubespray搭建了集群。Kubespray有关于搭建、添加、删除新节点、版本升级的指导手册,基本覆盖了我们使用Kubernetes产出需要的所有内容。但升级手册包含了免责声明,提醒我们即使变更很小也不要忽略任何版本,也就是说要更新所有中间版本才能使用目标版本。
诀窍就是,当你计划使用或已经使用了Kubernetes,想想生命周期活动以及你的方案如何这些问题。相对来说用它来搭建和运行集群是比较简单的,但生命周期的维护仍是有着诸多活动部件的全新领域。
3. 构建和部署
图源:unsplash
要做好重新设计整个搭建和部署管道的准备。我们的搭建过程和部署必须经历Kubernetes的完全转型,不仅对Jenkins管道进行了大量的重组,还使用了Helm等新工具,策划了新的git流程和构建,标记了docker镜像,并对helm部署图表进行了版本控制。
不仅需要维护代码,还需要策略来维护Kubernetes部署文件、Docker文件、Docker镜像和Helm图表,并设计一种将它们链接起来的方法。
在几次迭代后我们有了如下设计:放置应用程序代码及其Helm图表于单独的git存储库中,这使我们可以分别对它们进行版本控制。(语义版本号)
然后,我们将图表版本的地图与应用程序版本一起保存,并使用它来跟踪发布。例如,将app-1.2.0与charts-1.1.0一起部署。如果仅更改Helm值文件,则仅更改图表的补丁程序版本(例如,从1.1.0到1.1.1)。所有这些版本均由每个存储库RELEASE.txt发行说明。
Apache Kafka或Redis的代码之类的系统应用程序的工作方式有所不同,我们未构建或修改其代码。也就是说,由于Docker标签只是Helm chart版本控制的一部分,我们没有两个git存储库。如果我们曾经更改了docker标签进行升级,我们将在图表标签中增加主要版本。
4. 存活和就绪探针(双刃剑)
Kubernetes的存活和就绪探针是能自动解决系统问题的出色功能点。可以在发生故障时重新启动容器,并将流量从异常事故中转移。但在某些故障情况下,探针可能是双刃剑,并影响应用程序的启动和恢复,尤其是有状态应用程序,例如消息平台或数据库。
Kafka系统就是受害者。我们运行了一个有replicationFactor of 3 和minInSyncReplica of 2的 3 Broker 3 Zookeeper状态集,当系统意外故障或崩溃导致Kafka启动时会发生此问题。这导致它在启动期间运行其他脚本来修复损坏的索引,根据严重程度不同,该过程可能需要10到30分钟。
由于时间耗费增加,存活探针将不断失败,从而向Kafka发出终止信号以重新启动,这一并阻止了Kafka系统修改索引和完全启动。
唯一的解决方案是在存活探针设置中设置 initialDelaySeconds,从而在容器启动后延迟探针评估。但是问题在于当然很难给出具体时间,有些恢复甚至需要一个小时,因此我们需要留足空间来计算时间。但是initialDelaySeconds增加的时间越多,速度就越慢,因为启动失败时Kubernetes需要更长的时间来重新启动容器。
最新的几个版本中,Kubernetes引入了第三种探针“启动探针”来解决这个问题。在alpha from 1.16 和 beta from 1.18 中都可用。启动探针可以使存活和就绪探针在容器启动前停止运行,保证应用程序的启动不受干扰。
5. 公开外部IP
图源:unsplash
我们了解到,使用静态外部IP公开服务会极大损害内核的连接跟踪机制。除非详细计划,否则它只会大规模崩溃。
集群在Calico for CNI和BGP运行,作为Kubernetes内部路由协议,并与边缘路由器搭配。而Kubeproxy,我们使用IP Tables模式。我们在Kubernetes有着庞大的服务,该服务通过外部IP公开,每天处理数百万个连接。
所有的源网络地址转换(NAT)和伪装来自软件定义网络,Kubernetes需要一个机制来跟踪所有逻辑流。为此,它使用内核的Conntrack and netfilter工具来管理与静态IP的外部连接,接着将其转换为内部服务IP,再转为pod IP。这些都通过conntrack表和IP表完成。
但这个conntrack表有限制。当达到极限时,Kubernetes集群(下有OS内核)将不能接受新连接。在RHEL上,可以通过这种方式进行检查:
- $ sysctlnet.netfilter.nf_conntrack_countnet.netfilter.nf_conntrack_maxnet.netfilter.nf_conntrack_count = 167012
- net.netfilter.nf_conntrack_max = 262144
解决该问题的方法是匹配带有边缘路由器的多个节点,使你的静态IP的传入连接遍及整个群集。因此,如果集群中有大量虚拟机,累积起来就可以拥有一个大的conntrack表来处理大量的传入连接。
在2017年刚开始使用的时候,这个问题完全难住了我们。但2019年Calico发布了一份相关的详尽研究,题目就是《为什么conntrack不再友好》。
我们是否一定需要Kubernetes?
图源:debian
三年来,我们仍在每天继续探索和学习新东西。这个复杂的平台中存在着一系列挑战,尤其是构建和维护环境的开销很大。这会改变你设计、思考、构建的方式,并且会需要团队不断提升扩充来契合转变。
然而,如果你在云端并且使用Kubernetes作为“服务”,那么大部分平台维护的开销都可以下降,例如“如何扩展内部网络CIDR?”或“如何升级我的Kubernetes版本?”
应该问的第一个问题是:“是否一定需要Kubernetes?”这可以帮助你评估问题,判断用Kubernetes来解决它们是否必要。Kubernetes的升级转换等并不便宜。你的用例一定要真正配得上这笔支出并且值得使用这个平台。如果值得,那么Kubernetes可以极大地提高生产力。
记住,为了使用科技而使用是没有意义的。
本文转载自微信公众号「读芯术」,可以通过以下二维码关注。转载本文请联系读芯术公众号。