“几乎在所有情况下,在Kubernetes上运行Kafka都不值得折腾,我不会去说这是错的,但离错也不远了。”
卡尔工作的地方是仍然没有向现代基础设施的公司之一,他们有一个大型的传统应用程序,卡尔最近被聘为DevOps经理,帮助他们进行 “数字化转型 “和 “向云原生移动”,就像许多公司在这条路上一样,他与那些相信的人合作,也与那些只是顺应行业趋势的人合作,卡尔相信。他是个彻头彻尾的Kubernetes粉丝,听到我对Kubernetes的评价,他就眯起眼睛。
“在Kubernetes上运行应用程序比以其他方式运行更好。”
但这是吗?
有两种类型的应用程序在Kubernetes内运行:为Kubernetes这样的微服务环境专门设计的云原生应用程序,以及被移植到该环境的应用程序。对于第二组,应用程序的性能差异很大。一个拥有静态前端和PHP后端的网络应用是一个简单的提升。对于那些对其进程的直接可用性有期望的更复杂的应用程序,情况就不同了。
Kubernetes提供了哪些好处?
“一个容器能提供什么好处?” 我问卡尔。
他靠在椅子上,自信地认为他将赢得这场辩论。
“容器让你一次性建立一个应用程序,然后用兼容的容器引擎在任何地方运行它。它们消除了运行时环境中的依赖性问题。”
我点了点头,他抬头看了看我们上方的天井盖,想了一会儿。”他继续说:”它们隔离了进程,而不会比直接在主机上运行应用程序增加明显的延迟。它们提高了应用密度,使你能够更好地利用主机的资源。这为你的基础设施提供了更好的投资回报率。”
“但你不能只在容器中运行Kafka,”他说,以为这是我要建议的。”你需要Kubernetes来协调它。”
Kubernetes将容器运行时接口(CRI)与容器网络接口(CNI)和容器存储接口(CSI)连接起来,然后它提供管道和胶水,将一个或多个容器变成一个应用程序。
一个应用程序不仅仅是在容器或主机上运行的软件。它是让用户流量进入该软件并再次返回的一切。软件本身并不做任何事情。它必须被安装。它必须被配置。它必须连接到外部世界。开发人员不应该关心任何这些事情。他们应该只是能够把代码推送到一个仓库,然后由其他东西来部署它并使其可用。
如何在Kubernetes上建立Kafka模型
“看,”我说。”我们用Kubernetes做的事情并不神奇。我们所做的只是将我们过去在硬件中做事的方式虚拟化。Kubernetes就像一个小型数据中心,每个节点是一个机架,每个Pod是一台服务器。它预装了所有的交换、布线和路由,所以当你部署工作负载时,一切都能正常工作。或者至少它应该如此。”
我拿起一张干净的餐巾纸,开始在上面画画。
Kafka在真正的硬件上是什么样子
Apache Kafka(目前)需要两个组件:Kafka集群本身,以及一个ZooKeeperⓇ集群来处理选举、成员、服务状态、配置数据、ACL和配额。如果我们在真实的硬件上构建这些,我们可能有一个3个节点的Kafka集群和一个3个节点的ZooKeeper集群。每个Kafka代理都会有自己的低延迟磁盘,大量的内存用于缓存,以及合理数量的CPU核心。它也会知道每个ZooKeeper节点,每个生产者和消费者都会知道每个Kafka代理。
这是Kafka工作方式的一个关键部分—生产者和消费者有一个要连接的引导地址列表。当他们开始工作时,他们会连接到这些地址中的一个,然后为他们正在工作的分区提供一个特定的代理来连接。如果该代理服务器发生故障,他们会重新连接到引导节点,并被分配到不同的代理服务器。Kafka和ZooKeeper之间的通信是类似的—每个代理将连接到其列表中的一个ZooKeeper节点,如果该节点发生故障,代理将连接到一个不同的节点。
这种意识和自动愈合意味着你永远不会在Kafka前面或Kafka和ZooKeeper之间放一个负载平衡器。
Kafka在Kubernetes上是什么样子?
如果我们把上面定义的内容映射到Kubernetes资源上,我们最终会有一个用于ZooKeeper的StatefulSet,另一个用于Kafka,每个都有三个副本,StatefulSets是为有状态的应用程序设计的一种Kubernetes资源,它们会保证:
- Pod将有稳定的、唯一的名字和网络标识符,以及稳定的、持久的存储,当Pod被安排到任何其他节点时,它也会随之移动。
- Pod将按顺序创建(0, 1, 2…n),并按相反的顺序终止(n…2, 1, 0)。
- 滚动更新将按终止顺序应用
- 在创建连续的Pod之前,StatefulSet中的所有前置程序必须是运行和准备就绪的。
- 在Kubernetes终止一个Pod之前,它的所有继承者必须完全关闭。
一个StatefulSet需要一个无头服务来控制Pod的域,普通服务提供一个IP,并对其背后的Pod进行负载平衡,而无头服务在响应DNS查询时返回其所有Pod的IP。当无头服务位于StatefulSet前面时,Kubernetes更进一步,允许将Pod名称作为服务域名的一部分进行DNS查询。
例如,想象一下,我们有一个名为kafka的StatefulSet,有三个副本,在名字空间production中运行。这些Pod将被命名为kafka-0、kafka-1和kafka-2。如果我们在它们前面放一个叫kafka的无头服务,那么对kafka.production.svc.cluster.local的DNS请求会返回所有三个Pod的IP。我们也可以通过DNS查询kafka-0.kafka.production.svc.cluster.local来获得kafka-0的IP,以此类推。
➤ kubectl get poNAME READY STATUS RESTARTS AGEkafka-0 1/1 Running 0 9m7skafka-1 1/1 Running 0 8m55skafka-2 1/1 Running 0 8m54s➤ kubectl get serviceNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEkafka ClusterIP None <none> 9092/TCP 11m➤ kubectl run -i --tty --image busybox dns-test --restart=Never --rmIf you don't see a command prompt, try pressing enter./ # nslookup kafka-0.kafkaServer: 10.43.0.10Address 1: 10.43.0.10 kube-dns.kube-system.svc.cluster.local
Name: kafka-0.kafkaAddress 1: 10.42.0.14 kafka-0.kafka.production.svc.cluster.local/ # nslookup kafkaServer: 10.43.0.10Address 1: 10.43.0.10 kube-dns.kube-system.svc.cluster.local
Name: kafkaAddress 1: 10.42.0.15 kafka-1.kafka.production.svc.cluster.localAddress 2: 10.42.0.16 kafka-2.kafka.production.svc.cluster.localAddress 3: 10.42.0.14 kafka-0.kafka.production.svc.cluster.local
Kubernetes cookie 令人崩溃的地方
如果你的Kafka集群(无论是否有ZooKeeper)、生产者和消费者都存在于同一个Kubernetes集群中,”我说,”那么一切都可以与其他一切对话。
“但是,当这些组件之一在集群之外时会发生什么?”
比方说,你有一个生产者在集群之外。它需要知道一些经纪商的地址,这样它就可以连接到它们,并被告知它所使用的分区的连接位置。
你可以为引导服务器使用NodePort或LoadBalancer服务。只要有一个经纪商在线(希望你至少有一个经纪商在线……),那么它就会连接并获得这些信息……只不过这个列表会包含以下内容之一。
- Pod IP – 除非你有一个可以直接访问Pod IP的CNI,否则这将不起作用。
- 运行Pod的节点IP,加上Pod监听的端口 – 节点不会在Pod的同一位置有一个开放的端口,所以你的生产商将无法连接。
- 完全没有IP – 只有一个端口,如:9092 – 这将被翻译成127.0.0.1:9092。你的生产者将试图连接到自己,但会失败。
对于除第一种情况外的所有情况,你的生产者将无法连接到代理。
用Kubernetes服务来解决这个问题是很诱人的,但是服务是负载均衡器。负载平衡器不仅破坏了Kafka的运作方式,即生产者和消费者需要连接到一个特定的经纪人,而且还需要经纪人本身知道服务的外部IP,并指示客户连接到该IP。一些Kafka部署通过initContainers或管理员手动维护的特殊配置来处理这个问题,但这破坏了Kubernetes的动态性质。
只要你手动维护一个带有节点IP的配置文件,用于部署你的Pod……你就失去了保留。这只是一个时间问题,可怕的事情会发生。
尽管有这些问题,在通信路径上添加另一层抽象(特别是如果该层是一个外部LoadBalancer)会增加延迟,而延迟是实时数据应用的大忌。
你也不能为每个Pod创建一个服务。这不是Kubernetes的工作方式。对于世界上几乎所有其他的应用程序来说,拥有多个负载均衡器,每个都有一个单一的后端Pod是没有好处的。Kubernetes希望你建立你的应用程序,使所有从外面进来的东西都能在多个Pod之间进行负载平衡。
Kafka可以使用主机的网络吗?
“那使用主机网络呢?” 卡尔问道。
“设置hostNetwork: true会将容器的端口暴露在外面,”我同意,但当卡尔开始微笑时,我摇了摇头。”它也暴露了主机上每一个已发布的容器端口,而且它使我们无法使用我们的StatefulSet所需要的无头服务。”
卡尔的笑容消失了,他皱起了眉头。我继续说。
“事实上,它阻止了与Kubernetes集群内的Pod的所有通信,有效地将我们的Kubernetes部署变成了一个复杂的物理部署,Kubernetes除了确保Pod被调度和运行之外,什么都不做。你可以在Pod内部用hostPort声明来暴露端口,但是你仍然要处理Pod知道它登陆的主机并将该信息返回给外部客户端的问题。”
“好吧!” 卡尔失败地举起双手,几乎打翻了他的饮料。”这很好。反正我们不会从Kubernetes外部生产或消费,所以我们会没事的。”
我知道,卡尔可能不会。
Kafka依赖文件系统的缓存
现代操作系统将未使用的RAM分配为文件系统的缓存,Kafka依靠这个来提高速度。在一个拥有32GB内存的Kafka节点上,你可能会看到有30GB的内存被用于缓存。这是一件好事……直到它不存在。
在Kubernetes节点上会有很多容器运行,访问文件系统,并使用文件系统缓存。这意味着可供Kafka使用的部分将减少,所以Kafka将不得不更频繁地去访问磁盘。Kubernetes也可以将其中一个StatefulSet副本移动到不同的节点上,在这个过程中使Kafka的文件系统缓存失效。
Kubernetes不理解Kafka
Kubernetes对终止容器采取了强硬的态度,向它们发送SIGTERM信号,等待一段时间(默认为30秒),然后再发送SIGKILL。它对Kafka移动分区、选举领导人和优雅地关闭的过程一无所知。让Kubernetes来处理这个问题,就像通过切断电源来关闭一个经纪人。Kafka最终会恢复,但这并不美好。
并非都是厄运和阴霾
“问题并不完全在于Kubernetes,卡尔。问题还在于Kafka。”
卡尔看着我,嗤之以鼻。
“不,说真的,”我追问道。”如果你能用其他东西代替Kafka,而不改变你现有的代码,并解决我刚才描述的问题呢?”
我引起了他的注意。
“你不可能解决将代理机暴露在集群之外的网络问题,但如果你的生产者和消费者位于同一个Kubernetes集群内,我们可以用Redpanda将其变成一个强大的解决方案。”
与Kafka不同,Redpanda通过直接内存访问(DMA)与RAM进行通信,并绕过文件系统缓存进行读写。它根据文件系统的布局来排列内存,所以很少有数据被冲到实际的物理设备上。在Kubernetes清单中分配给Redpanda的资源是为Redpanda保留的,比依赖文件系统缓存更有效率。连同其不使用Java或ZooKeeper的单二进制架构,这也使得它更适合在容器中运行。(了解如何在Kubernetes中开始使用Redpanda)
Redpanda的Kubernetes清单还将经纪人作为Kubernetes关闭过程的一部分进入和退出维护模式,确保其他经纪人作为分区负责人接管,并确保经纪人在Pod终止前耗尽生产者和消费者。在恢复服务后,经纪人的DMA缓存被激活,因为它在有资格成为领导者之前赶上了分区领导者。
这是使Redpanda成为现代流媒体数据平台的一小部分。
Kubernetes是答案,但不是最终方案
我喜欢Kubernetes。它是一个强大的工具,可以解决无数的问题,但它并不是万能的。不是所有能在Kubernetes上运行的东西都应该在Kubernetes上运行,如果你正在评估是否在Kubernetes上运行Kafka或Redpanda,请考虑你将如何从集群外部与它互动。你可能要采取措施告诉经纪人他们的广告IP是什么,你可能要借助于网络堆栈的手动配置来保留非Kubernetes部署中的行为。
具有讽刺意味的是,如果你在Kubernetes之外运行Kafka或Redpanda,你仍然可以在Kubernetes集群内运行的生产者和消费者中使用它,只是反过来才是一个挑战。