一个怀疑者与 Kubernetes 的初次接触(2024)

Hacker News Top 新闻

摘要

一位资深系统工程师兼 Kubernetes 怀疑者分享了他亲身体验 Kubernetes 的过程,他认为 Kubernetes 的价值主要源于其控制循环和声明式期望状态,而不仅仅是一张编排架构图。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/10/17 19:50

# 一个怀疑论者与 Kubernetes 的初次接触 Source: https://blog.davidv.dev/posts/first-contact-with-k8s/ 我从事系统管理 / 工程 / 基础设施开发工作已经很多年了,不知怎的一直避免与 Kubernetes 有任何交集。这种回避并非偶然;我的工作需要对物理环境中的工作负载放置进行非常严格的控制。 随着时间推移,我在没有确凿依据的情况下逐渐对 Kubernetes 形成了怀疑态度,唯一模糊的认知就是它"是一种新的做事方式"以及"不必要的复杂性"。 我觉得是时候基于事实而非印象来形成观点了,于是我写下这篇学习笔记。 虽然有大量教程涵盖 Kubernetes 集群的使用和运维,以及对各组件的基本描述,但对我来说并不太管用。这些资料大多只是浅浅地介绍了各组件*做什么*,却往往缺少背后的原理和权衡取舍。 本文旨在从一个我认为有用的视角来阐述这些概念,但并非追求面面俱到。 ## 一个"标准"的描述 Kubernetes 允许你运行任意工作负载,并提供以下能力: - 指定资源需求(CPU、磁盘、内存、实例数……) - 动态伸缩实例数量 但更重要的是,它为你*做到*的事情: - 选择应用程序运行在哪个主机上 - "自愈"(崩溃的实例会自动重启,实例会自动从故障主机中迁出) - 通过 DNS 暴露服务进行服务发现 而它只*要求*你将工作负载打包为一个 [Docker 镜像](https://kubernetes.io/docs/concepts/containers/images/),这看起来是一个合理的代价。 组成集群的基本构建块可以定义如下: - [Pod](https://kubernetes.io/docs/concepts/workloads/pods/):工作单元,由一组 Docker 镜像及其配置组成 - [Node](https://kubernetes.io/docs/concepts/architecture/nodes/):运行 Kubernetes 节点代理(kubelet)的计算机,负责执行 Pod - [Cluster](https://kubernetes.io/docs/concepts/architecture/):`Node`的逻辑集合,连同控制平面 - [Service](https://kubernetes.io/docs/concepts/services-networking/service/):一组 Pod 的逻辑分组 - [Namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/):`Cluster`的逻辑子划分,为名称提供作用域(类似于 DNS 搜索域) 用图示来看这些概念之间的关系会更直观: 虽然这个解释还算过得去,但对我来说并不太有用——很多基础设施平台看起来都*长得差不多*像这张图,这里并没有什么特别之处。 ## 我认为重要的部分 我的观点是,Kubernetes 的大部分价值源于两个核心概念: ### 控制循环 [工作负载](https://kubernetes.io/docs/concepts/workloads/)由一组[控制循环](https://en.wikipedia.org/wiki/Control_loop)(命名为 [Controller](https://kubernetes.io/docs/concepts/architecture/controller/))来管理。 控制循环会通过传感器观察特定变量,如果有必要,会持续地通过控制元件执行*动作*,以达到*期望状态*。 一个有趣的细节是,控制元件并不一定*直接*影响传感器观察到的内容。 这个描述非常笼统,下面举几个例子: - 一个需要处理队列中事件的工作负载 - 期望状态:队列为空 - 传感器:队列深度 - 控制元件:事件处理服务器的数量 - 一个需要水平扩展以应对用户负载的工作负载 - 期望状态:保持 P90 延迟低于 X 毫秒 - 传感器:后端的延迟指标 - 控制元件:启动新的 Pod 来应对需求 - 通用健康监控 - 期望状态:所有节点健康 - 传感器:节点的健康指标 - 控制元件:将故障节点从集群中移除 ### Service [Service](https://kubernetes.io/docs/concepts/services-networking/service/)是一个网络层面的概念,为一组后端 Pod 提供稳定的访问,它提供: - **稳定标识**:不变的(虚拟)IP 地址和一条 DNS 记录 - 负载均衡:将传入流量分发到各个 Pod - 服务发现:允许其他组件找到并与此 Service 通信 与"经典"集群相比,一个 Service 提供了类似于以下组合的抽象: - N 个运行 [Keepalived](https://www.keepalived.org/) 的节点 + NGINX(带有动态后端集合) - 指向虚拟(Keepalived)IP 的 CNAME 记录 每当创建一个新的 Service 时: 1. 会为它分配一个虚拟 IP 2. 每个节点都会更新其网络配置(`netfilter`),将发往每个 Service 虚拟 IP 的数据包转发到后端 Pod。 网络配置由运行在每个节点上的 Controller 更新:[kube-proxy](https://kubernetes.io/docs/concepts/overview/components/#kube-proxy),其主要职责是监视所有`Service`对象的状态,并在每次变化时更新`netfilter`规则。 这种设计意味着所有节点都充当 Service 流量的负载均衡器,因此 Service 流量转发不存在单点故障,且流量转发能力随集群规模而扩展。 对我来说,这种设计的一个有趣特性是每个节点都会收到**每一个** Service 的规则,这带来了一些影响: - `kube-proxy`的实现非常简单 - 规则数量可能很大,这对于某些流量转发实现可能是个问题(`iptables`会线性评估每条规则) - 在某些场景下流量会被多次转发,以更高的可用性换取集群的额外负载 - 当 Pod 迁移到另一个节点时,在旧的 DNS 条目过期之前,流量会被转发两次 - 不需要在`kube-proxy`层面处理 DNS 条目的缓存失效问题 - 行为异常的客户端(例如不在重连前重新解析 DNS 的客户端)仍能正常工作 我们可以追踪一个从"Pod 1"到"Service 1"的数据包的旅程: ## 工作负载管理 或者叫"东西如何、何时、何地运行" 你可以手动创建一个 Pod,使用如下 YAML 定义: ```yaml apiVersion: v1 kind: Pod metadata: name: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 ``` 但这样做的话,你只是*手动*调度了这个 Pod 的*一次*执行;这很简单,但有一些讨厌的限制: - 如果承载 Pod 的节点宕了,Pod 不会被重新调度 - 如果 Pod 因为节点资源压力而被终止(通过[驱逐](https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/)机制),它同样不会被重新调度 - 你不能说"现在我想要两个这样的 Pod" 你可以改为创建一个 [ReplicaSet](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/),它是一个 Controller,作为其控制循环的一部分,它会确保始终有正确数量的 Pod 副本在运行,这就解决了节点故障/驱逐导致你的 Pod 被删除的问题。 ```yaml apiVersion: apps/v1 kind: ReplicaSet metadata: name: nginx-replicaset spec: replicas: 3 template: spec: containers: - name: nginx image: nginx:1.14.2 ``` ReplicaSet 允许你更新期望的副本数量,*仅此而已*(比如更新`nginx`版本,或者暴露另一个端口)——你需要删除资源再重新创建它。 为了解决这些限制,你可以使用 [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/),它是一个 Controller,会确保正确数量的 Pod **最终**运行着**正确的镜像**。 **最终**这个词非常重要,因为 Deployment 允许你更新 Pod 运行的镜像(或版本),并选择变更速率,涵盖了"版本升级"的标准工作流。 换句话说,当你更新一个 Deployment 时,它会创建一个新的 ReplicaSet 并逐渐将其扩容,同时缩减旧的 ReplicaSet,从而实现滚动更新。 ## 存储管理 或者叫"数据如何持久化并随你的 Pod 一起移动" 默认情况下,Pod 内的容器不共享存储/文件系统。 如果你想在 Pod 内的容器之间共享存储,可以使用[临时卷(Ephemeral Volume)](https://kubernetes.io/docs/concepts/storage/ephemeral-volumes/),其生命周期与 Pod 绑定——Pod 关闭时,数据即被删除。 临时卷看起来没什么用[^1],但它们确实有一些使用场景: 1. 边车容器(sidecar)进行日志采集 2. 边车容器为主应用程序预填缓存 如果你想拥有*真正持久*的存储,可以使用[持久卷(Persistent Volume)](https://kubernetes.io/docs/concepts/storage/persistent-volumes/),其生命周期与 Pod 的生命周期是解耦的[^2]。 你可以用一个[声明(Claim)](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)来定义这样的卷,如下所示: ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nginx-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi ``` 然后你可以在 Pod 中这样使用它: ```yaml apiVersion: v1 kind: Pod metadata: name: nginx-with-pv spec: containers: - name: nginx image: nginx volumeMounts: - name: html-volume mountPath: /usr/share/nginx/html volumes: - name: html-volume persistentVolumeClaim: claimName: nginx-pvc ``` 一些有趣的地方: - 同一个 Claim 的多个使用者(Pod)会共享存储,和以往一样,如果有多个写入者,这是相当有风险的 - PersistentVolume 由不同的驱动支撑(NFS、SMB、iSCSI……),但 Pod 并不清楚使用的是哪一个,所以 Pod 无法依赖某些特性(比如原子重命名) 如果你想要*每个实例*拥有独立的持久存储,可以使用 [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/),它在一个资源中定义了:工作负载(Pod)、副本数量、挂载配置和卷声明*模板*。 `StatefulSet`的一些有趣特性: 1. Pod 拥有稳定的主机名(`nginx-0`、`nginx-1`……) 2. 部署按顺序执行: 1. 按升序创建 Pod(0, 1, …) 2. 按降序删除 Pod(3, 2, …) 3. 按降序更新 Pod(3, 2, …) 这看起来很*奇怪*,我在网上找到的解释说编号较低的实例是"核心的"或"更稳定的"——我无法理解,因为一个频繁崩溃的 Pod 也不会被重新编号。也许按"运行时长"降序更新会更好?也许这种"资历"/"稳定性"的设定是后来追加到 Pod 上的,而 Kubernetes 只是需要*某种*顺序? 在运维层面,到目前为止我仍然不清楚如何手动检查持久卷的内容,除了进入运行中的容器并在里面执行命令;如果我的容器里没有 shell 怎么办? ## 总评 我们已经覆盖了 Kubernetes 的主要部分——控制循环、Service、工作负载和存储。学习这些概念让我形成了期望中的那种"不那么毫无根据的"观点(不过,我仍然没有实际运维或使用过一个集群)。 总的来说,我认为这些**概念**非常有道理,特别是 Service(包括网络模型和 kube-proxy)非常出色,而保持 Controller 模式作为资源管理的引擎是正确的运作方式。 我留下了一些开放式的问题/吐槽,就放在这里供你们品味 [Controller 文档](https://kubernetes.io/docs/concepts/architecture/controller/)中说: > 随着工作在进行中以及控制循环自动修复故障,你的集群可能在任何时刻发生变化。这意味着,你的集群可能永远无法达到稳定状态。只要你的集群的控制器在运行并能做出有用的更改,整体状态是否稳定并不重要。 为什么状态不稳定不重要?如果我在运维一个无法收敛的集群,我想立刻知道! - 既然有 Controller 模式,为什么不支持"云原生"架构? - 我希望能有一个 ReplicaSet 根据队列深度的某种简单计算来自动调整`replicas`(比如:队列深度 / 16 = 副本数) - 定义这类事件(队列深度、打开的连接数、响应延迟)的接口会很棒 - 基本上就是 [Horizontal Pod Autoscaler](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/),但传感器不仅仅是"CPU" - 为什么存储和网络实现是"树外"的(CNI / CSI)? - 我猜是为了"模块化"和"关注点分离" - 既然如此,为什么又对云厂商有显式支持? - 比如:LoadBalancer 支持 AWS/GCP/Azure/…… 而对于最大的开放话题,虽然这不是 Kubernetes 的锅,但我有一番牢骚不吐不快 ### 字符串类型 Kubernetes"社区"似乎铁了心要让自己的生活更艰难,他们居然基于**文本插值**来构建工具。就好像他们看到了 [Ansible](https://www.ansible.com/) 然后想:"嘿,这个看起来好痛苦,我们来搞这个吧!" 以这个 [RabbitMQ scaler](https://keda.sh/docs/2.14/scalers/rabbitmq-queue/)(KEDA,由 Microsoft 维护)为例: > value: 触发时使用的消息积压量或每秒发布速率。(当 mode: MessageRate 时,此值可以是浮点数) `value`的*类型*取决于`mode`的*值*?这是什么鬼?还有`mode`是一个字符串而它本该是枚举?? [Helm](https://github.com/helm/helm)是怎么火起来的??像这样的工具怎么可能*做文本模板*——明明有那么多事情可以做,偏偏选了这个? 随便拿一个 [NGINX chart](https://artifacthub.io/packages/helm/bitnami/nginx?modal=template&template=deployment.yaml) 看看大多数值是怎么用*手动字符串缩进*处理的: ``` {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} 14 {{- end }} ``` 或者 ``` {{- toYaml { "rollingUpdate": {}, "type": "RollingUpdate" } .Values.updateStrategy | nindent 4 }} ``` 为什么我们要用计算机来生成一种结构化语言(YAML),却要靠手动加空格来让语法合法?不应该存在这种中间文本模板表示形式。

相似文章

求职面试教会我的 Kubernetes 知识

Hacker News Top

一位求职者发现,小型初创公司采用 Kubernetes 并非出于技术可扩展性,而是为了组织层面的好处,如一致性、共享知识和可追溯性。这篇文章反思了 Kubernetes 对小型团队的非技术优势。