查看原文
其他

Kubernetes 1.28:边车复仇记?

CNCF CNCF 2023-09-21

作者:William Morgan,文章最初在Buoyant 博客[1]上发布

Kubernetes 边车指南:它们是什么,为什么存在,以及 Kubernetes 1.28 的变化

如果你正在使用 Kubernetes,你可能听过边车( sidecar )这个术语。这个概念是云原生生态系统中几个重要构建模块的基础,尤其是服务网格。然而,你可能会感到惊讶的是,Kubernetes 本身没有内置的边车概念——至少直到现在为止。在 Kubernetes 1.28 中,“边车 KEP”的合并终于改变了这一点,正式将边车引入 Kubernetes API。

但是这个 KEP 实际上意味着什么?在本指南中,我们将为你介绍所有你需要了解的内容:边车是什么,什么是好的,什么是不好的,以及 Kubernetes 1.28 中有哪些变化。

Pods、容器和初始化容器

在我们深入了解边车之前,让我们确保我们理解 Kubernetes 如何组织其应用组件的基础知识。

Kubernetes 中执行和定址的核心单位是 Pod。一个 Pod 包含一个或多个容器。每个 Pod 被分配一个唯一的 IP 地址,Pod 中的所有容器共享此 IP 地址。这些容器可以监听不同的端口,但是 Pod 中的所有容器都绑定到该 IP,并且是该 Pod 的“一部分”——当 Pod 死亡时,它们也会死亡。

使用 Pod 作为其基本构建模块,Kubernetes 提供了大量的构造,构建在其之上。有许多方法可以复制 Pod,组织它们,对它们进行更改管理,为它们命名,允许它们彼此通信等等。但是对于我们今天的目的来说,我们不需要真正了解 Pod 的使用方式;只需要知道它们是大多数 Kubernetes 构造的核心构建块即可。

虽然不同的 Pod 可能具有不同的属性,但就 Kubernetes 而言,并没有不同的“类型”Pod。Pod 就是 Pod。容器不是如此。Kubernetes 识别两种类型的容器:普通容器和初始化容器( init container )。

初始化容器,顾名思义,在 Pod 初始化时运行,即在 Pod 准备好运行之前。初始化容器有望终止,并且一旦 Pod 中的所有初始化容器终止,普通的非初始化容器开始运行。普通(非初始化)容器在初始化容器全部完成后启动,并通常持续运行直到 Pod 本身终止。(对于预期在完成后终止的工作负载,如 Job,此行为有一些例外,但这是最常见的模式。)

Kubernetes Pod 中的容器排序。初始化容器按顺序首先运行,然后普通容器并行运行直到终止。

在 Kubernetes 1.28 中,初始化容器和普通容器之间的这种区别将对新的边车功能至关重要。但在深入讨论之前,让我们最后来看看边车是什么。

什么是边车呢?

在 Kubernetes 的整个历史中,边车一直只是一种模式的名称,而不是 Kubernetes 的实际功能。边车是指在主应用容器“旁边”运行的容器,就像摩托车旁边附着的边车一样。这仅仅是约定俗成的一个概念——“边车容器”与其他容器的唯一区别就在于它的功能。

但是为什么一个容器可以在另一个容器旁边运行的想法如此有趣,以至于值得特别命名?因为边车模式提供了一种非常强大的方式:在运行时增强应用程序而无需进行代码更改,同时在与应用程序本身相同的操作和安全环境中运行 。这个观点是理解边车的力量的关键,也是我认为理解 Kubernetes 的 Pod 模型的力量。

一种在运行时增强应用程序而无需代码更改的方式 。边车为你提供了一种在不需要重新编译应用程序、链接新库或以任何方式更改应用程序的情况下添加功能的方法。这意味着开发人员不仅不需要参与,而且边车可以与使用任何语言编写的应用程序一起工作。

同时与应用程序本身在相同的操作和安全环境中运行 。这是真正强大的部分。由于边车在同一个 Pod 中运行(而不是在系统的其他地方),它们被视为应用程序的一部分。如果边车失败,相当于 Pod 本身失败。如果边车存在安全漏洞,该漏洞仅限于 Pod 内。如果边车消耗大量资源,就好像应用程序本身消耗了这些资源。所有处理这些情况的 Kubernetes 机制对于启用边车的应用程序都适用,无需进行任何更改。

“我可以增强应用程序而无需开发人员参与”与“我可以像处理应用程序一样处理这些增强功能”这种组合对于平台所有者来说非常有用——特别是因为像服务网格这样基于边车的项目提供的大多数增强功能都是平台特性,而不是业务逻辑。

Kubernetes 中常见的边车示例包括用于仪表化应用程序容器并将数据报告到集中的指标存储的指标代理,或捕获容器日志并将其中继到其他地方的日志聚合器。但可能最大的边车示例是服务网格。

服务网格使用边车模型为每个 Pod 添加网络代理。这些边车拦截与应用程序之间的所有 TCP 连接,并对这些连接上的请求进行“处理”,以实现一系列令人印象深刻的功能——这些代理可以提供诸如 Pod 之间的相互 TLS、每个组件的细粒度 HTTP 指标、请求重试、透明的跨集群通信等功能。这些代理的具体形式因实现而异。许多服务网格使用流行的 Envoy 项目,但例如 Linkerd 使用轻量级的用 Rust 编写的“微代理”方法,这种方法提高了性能和安全性,同时大大降低了复杂性和资源消耗。(你可以阅读更多关于我们决定采用 Rust[2]的信息。)

边车模型的强大之处在于,不仅像 Linkerd 这样的服务网格可以透明地添加这些功能,而且它们还可以随应用程序扩展,并避免在集群中引入单点故障。

我们真的需要边车吗?难道网络不能做到这一点吗?

尽管边车很受欢迎,但它们一直是一个有争议的话题。对于那些从应用程序或平台所有者的背景来看服务网格的人来说,边车模式的价值是明显的:你可以获得共享库的好处,几乎没有任何缺点。这基本上是我上面所分享的观点。

然而,对于那些认为服务网格是一个网络层的人来说,将 L7 网络逻辑放置在靠近应用程序而不是“在内核中”,或者至少“在查看应用程序时看不到它的地方”这一想法与应用程序和网络之间的传统关系相悖。毕竟,每次看到一个 Pod 时,服务网格边车就会在那里凝视着你!从网络的角度来看,这可能感觉很奇怪和不受欢迎。

实际上,这个分歧已经引发了“无边车”服务网格的出现。撇开那些用诸如 eBPF 或 HBONE 之类的神奇技术来“解决服务网格”的令人屏息的营销(我们将在每个节点或每个命名空间的代理中移动核心逻辑,而不是边车),这些服务网格最终都是通过将核心逻辑移到每个节点或每个命名空间的代理中来工作的。换句话说,代理仍然存在,只是不再在你的 Pod 中。

我们仔细研究了这些方法,最终没有在 Linkerd 中采用它们。关于这方面的权衡取舍有很多可以讲,如果你对这个特定话题感兴趣,我在我的关于 eBPF 及其在服务网格中的实用性的详细评估[3]中涵盖了大部分内容。总之,L7 流量与 L4 流量不同,通过不采用边车,你将失去关键的隔离和安全保证,我认为从操作和安全角度来看,这是一大步退步。

但是对于本文的目的,我们可以忽略我众多的意见。我们来谈谈边车!但边车世界并不完美。边车确实存在一些真正的缺点和烦恼。让我们看看为什么。

边车的一些问题是什么?

使得 Kubernetes 能够提供 Pod 的强大模型也是边车周围一些讨厌问题的根源。主要问题包括:

  1. 升级边车需要重新启动整个 Pod。由于 Kubernetes 的 Pod 是不可变的(immutable),我们无法在不重新启动整个 Pod(包括应用程序)的情况下更改一个容器。现在,Kubernetes 应用程序应该能够处理任意的 Pod 重新启动和关闭,并且 Kubernetes 提供了各种机制,如滚动重启,以确保整个工作负载在此过程中无停机时间,但仍然很烦人。
  2. 边车可能阻止 Job 的终止。还记得我之前提到的 Job 吗?Job 是 Kubernetes 中一种预期自行完成的工作负载类型。不幸的是,Kubernetes 没有提供一种信号给边车,告诉它应该终止,这意味着默认情况下 Job 的 Pod 将永远运行。虽然有一些解决方法来解决这个问题(我们的解决方法叫做linkerd-await[4]),但它们需要对应用程序容器进行更改,违反了服务网格的透明性。
  3. 边车可能与应用程序在启动时发生竞争。虽然 Kubernetes 按顺序运行初始化容器,但对于常规容器,它不提供顺序保证。这意味着在启动过程中,应用程序不能保证边车已准备就绪,如果你需要边车与外部进行网络连接,这可能会有问题。(Linkerd 使用了一些技巧来解决这个问题。)

这些问题都不是致命问题,但它们都很烦人,并且在某些情况下需要进行技巧或变通方法。

那些技巧是什么?给你一点提示:为了防止竞争条件,事实上容器规范中有一个postStart钩子,即使这不在规范中,它将阻止下一个容器启动,直到钩子完成。因此,Linkerd 将其代理作为 Pod 中的第一个容器,并使用 postStart 钩子确保它在其他应用程序之前已经启动并运行。竞争条件得到避免!然而,由于 Linkerd 现在是 Pod 规范中的第一个容器,诸如kubectl logs之类的工具默认会输出 Linkerd 代理的日志而不是应用程序日志。这对用户来说非常烦人!但是不用担心,在 Kubernetes 1.27 版本中引入了默认容器的概念,可以在应用程序容器上设置它,这有助于 kubectl 做正确的事情。但并不是每个集群都升级到了 1.27 版本...等等。整个设置过程混乱而脆弱,但这就是目前的技术水平。

就像我说的:技巧。

迄今为止,在 Kubernetes 中使用边车一直是这样的情况——大部分工作都正常进行,但偶尔会遇到一些棘手的问题。但现在,Kubernetes 1.28 来拯救我们!

Kubernetes 1.28 有什么新变化?

最后,让我们谈谈 Kubernetes 1.28 版本和sidecar KEP[5](Kubernetes Enhancement Proposal)。这个 KEP 最初在 2019 年启动,并经历了数年的争论、停滞、重新设计和险些失败。它已经成为 Kubernetes 世界的一种内部笑话!

Reddit 的 Kubernetes 社区对 KEP 再次错过 Kubernetes 版本做出反应。

如今,四年过去了,边车 KEP 终于在 1.28 版本中成为了一个 Alpha 级功能。这个 KEP 的实际变化与初始化容器有关。

KEP 为初始化容器规范引入了一个新的RestartPolicy字段。下面是该字段的描述:

RestartPolicy 定义了 Pod 中各个容器的重启行为。该字段只能为初始化容器设置,并且唯一允许的值是"Always"。对于非初始化容器或未指定此字段的情况,重启行为由 Pod 的重启策略和容器类型来定义。为初始化容器设置 RestartPolicy 为"Always"将产生以下效果:该初始化容器将在退出后持续重启,直到所有常规容器终止。一旦所有常规容器完成,具有 RestartPolicy 为"Always"的所有初始化容器将被关闭。此生命周期与正常的初始化容器不同,通常被称为"边车"容器。尽管该初始化容器仍按照初始化容器序列启动,但它不会等待容器完成后再继续下一个初始化容器。相反,下一个初始化容器会在该初始化容器启动后立即开始,或在任何 startupProbe 成功完成后开始。

让我们来详细解释一下:

  1. 现在,你可以为初始化容器指定新的 RestartPolicy: Always 配置。
  2. 如果添加了这个新的配置,你现在就有了一个边车容器。
  3. 边车容器在所有普通容器之前启动(因为它是一个初始化容器),并且最重要的是,在所有普通容器终止后它会终止。
  4. 如果由于某种原因你的边车容器在普通容器运行时死掉,它将自动重新启动。(这就是"Always"的作用。)
  5. 最后,与每个正常的初始化容器依次等待完成后再启动不同,其他初始化容器在边车容器完成之前不会等待。这很好,因为它们要等到很久以后才会完成。

从本质上讲,这个 KEP 引入了一种几乎类似于"后台容器"的模式(或者古老的称之为"TSR[6]容器")。这些新的边车容器在普通容器之前启动,并在普通容器终止后终止。这一举措解决了边车问题清单中的 2 和 3 号问题:现在我们可以在 Job 终止时自动终止,而且不再存在与普通容器的启动竞争条件,也不再需要 postStart 钩子和默认容器的一切麻烦事。

使用 Kubernetes 1.28 中引入的新边车容器在 Kubernetes Pod 内进行容器排序。边车容器运行时不会阻塞非边车初始化容器,并在普通容器终止后终止。

胜利!有了这个新功能,边车又变得很酷了。或者并非如此?

在 Kubernetes 1.28 中等待我们的辉煌未来

我给这篇文章起了个标题叫做“Revenge of the Sidecars”,但显然这有些夸大其词。边车 KEP 确实解决了在 Kubernetes 中使用边车时的两个最大问题:对 Job 的支持以及容器启动时的竞争条件。尽管目前这些问题都有解决方法,但这些解决方法对服务网格的拥有者造成了一些不幸的后果;而在 Kubernetes 1.28 及以后的版本中,我们可以完全消除这些解决方法。

这是否意味着现在在边车领域一切都好了呢?个人认为,要更普遍地接受边车仍然面临一些挑战。其中最大的挑战(具有讽刺意味的是)与可见性有关:因为你可以在每个 Pod 中看到边车,a)这与我们传统的网络观念不符,b)边车的资源消耗非常明显。如果一个共享库占用了 200 兆内存和 5%的 CPU,你可能都不会注意到;但如果一个边车这样做,那就显而易见了。(令人高兴的是,Linkerd 的资源使用非常小,部分原因是由于 Rust 对内存分配的出色控制。)

去年,我开玩笑说我们将从 Linkerd 中删除边车……通过将它们从 kubectl 的输出中隐藏起来[7](使用 eBPF,没错!)。不幸的是,对这个想法的回应如此热情和兴奋,以至于我不得不发推文解释这个笑话[8]。我们实际上并不打算分叉 kubectl。但像所有好的(?)笑话一样,其中有一点真实的内核,我想知道如果边车存在但只是看不见或者在一个标志后面隐藏起来,那么多少“我们需要消灭边车”的情绪会减弱。

话虽如此,我们在 Linkerd 的任务是提供地球上最快、最轻、最简单(即:最低 TCO)的服务网格,迄今为止还没有任何东西能够说服我们边车不是实现这一目标的最佳方式。因此,至少在可预见的未来,等待 Linkerd 用户的辉煌未来将是更多的边车和更少的问题。希望也没有必要分叉 kubectl。

与 Linkerd 一起拥抱服务网格!

如果你正在寻找一个服务网格,我希望你试试 Linkerd。像 Adidas、Plaid、微软(Xbox Cloud)等公司都依赖 Linkerd 来支持他们的关键生产基础设施。无论是通过纯开源方式还是作为商业合作伙伴,我们都乐意帮助你入门。我们已经帮助世界各地的公司摆脱了炒作,将坚如磐石的服务网格部署到生产环境中,无论是用于零信任安全策略、双向 TLS、FIPS-140-2 合规性还是其他用途。立即与我们联系[9]

参考资料

[1]

Buoyant 博客: https://buoyant.io/blog/kubernetes-1-28-revenge-of-the-sidecars

[2]

我们决定采用 Rust: https://linkerd.io/2020/12/03/why-linkerd-doesnt-use-envoy/

[3]

关于 eBPF 及其在服务网格中的实用性的详细评估: https://buoyant.io/blog/ebpf-sidecars-and-the-future-of-the-service-mesh

[4]

linkerd-await: https://github.com/linkerd/linkerd-await

[5]

sidecar KEP: https://github.com/kubernetes/enhancements/issues/753

[6]

TSR: https://en.wikipedia.org/wiki/Terminate-and-stay-resident_program

[7]

从 Linkerd 中删除边车……通过将它们从 kubectl 的输出中隐藏起来: https://twitter.com/wm/status/1577081662848241664

[8]

发推文解释这个笑话: https://twitter.com/wm/status/1577437965969940480?lang=en

[9]

与我们联系: https://buoyant.io/contact

点击【阅读原文】阅读网站原文。


立即扫码注册参会!


CNCF概况(幻灯片)

扫描二维码联系我们!




CNCF (Cloud Native Computing Foundation)成立于2015年12月,隶属于Linux  Foundation,是非营利性组织。 

CNCF云原生计算基金会)致力于培育和维护一个厂商中立的开源生态系统,来推广云原生技术。我们通过将最前沿的模式民主化,让这些创新为大众所用。请关注CNCF微信公众号。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存