第一章:Kubernetes (K8s) 入门:容器化应用的智能管理系统

你正在构建一个应用程序,它最终会服务成千上万的用户。当你的应用程序只是一个简单的“程序包”时,你可能可以手动管理它。但当这个“程序包”需要运行几十、几百甚至几千份,并且要保证它们随时在线、协同工作时,手动管理就变得不可能。

这时候,你会想创建一个系统,它能像一个智能机器人一样,自动帮你管理这些应用程序副本。

让我们回到 K8s 诞生的地方——Google

1.1 K8s 的前世今生:Google 的解决方案

在 K8s 出现之前,也就是本世纪初的 Google,一群顶尖的工程师也面临着同样的问题,只不过规模要大得多:他们有数以万计的应用程序和数十万台服务器。手动部署、管理、扩缩这些应用程序简直是天文数字般的挑战。

于是,Google 内部诞生了一个名为 Borg 的系统。Borg 是一个高度自动化的集群管理系统,它能高效地在 Google 的服务器集群上调度和运行应用程序。Borg 的成功,使得 Google 能够以惊人的效率和规模运行其各项服务,从搜索到 Gmail,无一不是受益于其强大的自动化能力。

随着时间的推移,容器技术开始普及。Google 意识到,Borg 的核心理念和实践可以被抽象出来,并推广给更广泛的开发者社区。于是,他们借鉴 Borg 的经验,重新设计并开源了一个全新的项目——这就是 Kubernetes (K8s)。K8s 就像是 Borg 的“开源精华版”,将 Google 内部大规模系统管理的宝贵经验带给了全世界。

1.2 K8s 的核心价值:为什么我们需要它?

当你的应用程序需要在大规模分布式环境中稳定运行,并具备高可用性、弹性伸缩能力时,手动运维会迅速变得复杂且不可靠。K8s 的存在,正是为了解决这些痛点,它提供了一套完整的解决方案,让你的应用程序能够“活下来、跑得好”:

  • 自动化运维: K8s 能够自动化处理应用程序的部署、更新、扩缩容、故障恢复等几乎所有运维任务。你无需手动干预,只需定义应用程序的期望状态,K8s 就会自动使其达到并维持该状态。这极大地减少了人工错误和运维负担。
  • 高可用性与自愈: K8s 持续监控应用程序和其运行环境的健康状况。一旦发现故障(如某个应用程序实例崩溃、某个服务器宕机),它会自动进行故障转移、重启受损实例或重新调度,确保服务不中断,实现系统的高可用性。
  • 弹性伸缩: 随着业务负载的变化,K8s 能够根据预设规则自动增加或减少应用程序的运行实例数量。这意味着你的应用程序可以根据实际需求动态调整资源,高峰期应对自如,低谷期节约成本。
  • 资源高效利用: K8s 能够智能地将应用程序调度到集群中资源最适合的服务器上,优化资源分配。它就像一个精明的“调度员”,确保每台服务器的计算资源都得到充分利用。

简而言之,K8s 的核心用处在于将复杂的、手动的、易出错的分布式应用管理过程,转变为自动化、可靠且高效的模式。它让开发者和运维人员能够专注于应用程序本身,而不是底层的基础设施管理。

1.3 K8s 与 Docker Compose 有何不同?:适用场景的差异

那么,有人就问了:既然 Docker Compose 也能管理多个容器,那 K8s 又有何不同?

关键在于它们设计的规模和场景

  • Docker Compose 更适用于在单台机器上管理和运行少量相互关联的容器。它像一个“小团队的指挥官”,适合本地开发和测试环境。
  • Kubernetes (K8s) 则专为分布式、大规模的生产环境而生。它能将应用程序部署到多台服务器(集群)上,提供自动故障转移、负载均衡、水平伸缩等高级功能。K8s 就像一个“大型舰队的总司令”,能协调成百上千甚至上万个容器在不同服务器上高效运作,即便有服务器宕机,也能保证服务不受影响。

1.4 K8s 的核心理念:声明式控制

K8s 的强大之处在于它的“声明式”管理模式。

  • 你声明目标,K8s 负责实现: 你不需要告诉 K8s 如何一步步执行任务(“先启动服务器 A,再部署程序 B”)。你只需声明你希望的最终状态(“我需要 5 个应用程序实例在运行,版本为 2.0”)。
  • 自动调整: K8s 会持续检查当前状态是否符合你的声明。如果发现不符,它会主动采取行动,直到达到你定义的目标状态。并且,它会一直维护这个状态。

当然,很多读者可能难以理解上述所说的“声明式”管理模式到底是怎么样的一个机制,不用着急,听我继续娓娓道来。


第二章:深入 K8s 世界——核心概念与组件

在第一章中,我们了解了 K8s 的诞生背景、其核心价值以及声明式管理理念。现在,是时候揭开 K8s 这座“智能机器人”的内部构造了。我们将从 K8s 的基本组成单位开始,逐步认识那些协同工作,共同支撑你的应用程序稳定运行的关键概念。

2.1 K8s 的“骨架”:集群 (Cluster)

K8s 并非单台机器就能运行,它是一个由多台服务器组成的集群 (Cluster)。你可以将这个集群想象成一个大型的、统一的计算资源池,K8s 负责在这个池子里高效地管理你的应用程序。一个典型的 K8s 集群包含两种主要类型的节点——主节点和工作节点

2.1.1 主节点 (Master Node) / 控制平面 (Control Plane):集群的“大脑”

主节点是 K8s 集群的“大脑”和“指挥中心”。它负责管理整个集群,调度应用程序,响应用户请求。所有与 K8s 集群的交互,都是通过主节点进行的。主节点本身不直接运行用户应用程序,而是运行着 K8s 的核心组件,包括:

  • API Server: 这是 K8s 集群的“门面”和“前端接口”。所有与 K8s 集群的通信(无论是来自用户、CLI 工具还是其他组件)都通过 API Server 进行。它验证请求、处理数据,并将集群状态信息持久化。
  • etcd: K8s 集群的“记忆库”或“数据库”。它是一个高可用、强一致性的键值存储系统,用于保存 K8s 集群的所有配置数据、状态信息以及元数据。etcd 的稳定性对整个 K8s 集群至关重要。
  • Scheduler (调度器): 负责将新创建的 Pod(我们稍后会介绍)调度到集群中合适的“工作节点”上。调度器会根据资源需求(CPU、内存)、可用性、策略约束等因素,选择最佳的运行节点。
  • Controller Manager (控制器管理器): K8s 的“巡逻员”。它包含一系列控制器,持续监控集群的当前状态,并尝试将其驱动到用户期望的“声明式”状态。例如,如果某个应用程序副本数量不足,ReplicaSet Controller 就会启动一个新的 Pod。

通常我们会分别在三台以上机器上部署 Master Node 形成控制平面集群,如果 Master 全部死亡(宕机或停止运转),Worker Node 将失去“调度总线”,即无法再创建新的 Pod。但是 Worker Node 仍然可以保持自主运行,只是不再拥有“自我修复”与动态扩容机制。

2.1.2 工作节点 (Worker Node):集群的“四肢”

工作节点是 K8s 集群中真正运行应用程序(容器)的机器。它们是集群的“执行者”,接收来自主节点的指令,并负责容器的生命周期管理。每个工作节点上都运行着以下核心组件:

  • Kubelet: 这是运行在每个工作节点上的“代理”,也是一位合格的程序“监察官”。它负责与 API Server 通信,接收主节点的指令,并确保节点上的容器按照 Pod 规范运行。Kubelet 会监控 Pod 的状态,并向 API Server 报告。
  • Kube-proxy: K8s 的“网络代理”——节点网络的守门员。它负责为 K8s 中的 Service(服务)实现网络代理和负载均衡功能。它会根据 Service 的定义,在节点上配置网络规则,确保对服务的请求能够正确路由到后端 Pod。
  • 容器运行时 (Container Runtime): 这是真正执行容器、管理容器生命周期的软件。例如,我们熟知的 Docker、containerd 或 CRI-O 都属于容器运行时。Kubelet 会通过容器运行时来启动、停止、删除容器以及管理容器的镜像。它是 Pod 中应用程序得以运行的基石。

2.2 K8s 的“居民”:Pod——应用程序的最小单位

如果说 K8s Node 是容器世界的管理者,那么 K8s Pod 就是容器世界的合法公民。

在 K8s 集群中,你的应用程序(容器)并非直接运行在工作节点上,而是被封装在一个名为 Pod 的抽象层中。Pod 是 K8s 中最小的可部署和可调度的计算单元。

  • 容器的“家”: 一个 Pod 通常包含一个应用程序容器。但在某些特定场景下,一个 Pod 也可以包含多个紧密耦合、需要共享资源(如网络、存储)的容器。例如,一个主应用程序容器和一个用于日志收集的辅助容器。
  • 共享资源: 同一个 Pod 内的所有容器共享相同的网络命名空间(即它们共享一个 IP 地址和端口空间)和存储卷。它们可以像在同一台机器上一样通过 localhost 互相通信。
  • 短暂性: Pod 被设计为相对短暂的。如果一个 Pod 崩溃或其所在的节点出现故障,K8s 会自动创建并启动一个新的 Pod 来替代它,而不会尝试“修复”旧的 Pod。这意味着 Pod 是可替换的,不应存储持久性数据。

2.3 K8s 的“自动化管家”:Deployment——管理 Pod 的“生产线”

哎~,说到底现在还是只能手动管理 Pod 的创建与运行,说好的自动化管理呢?

咱们继续往下看——这不就来了?

手动管理大量的 Pod 显然是不现实的。为了实现 Pod 的自动化部署、扩展和更新,K8s 引入了 Deployment 这个概念。

  • 管理一组 Pods: Deployment 负责管理一组相同 Pod 的“副本”。你只需声明你希望运行多少个 Pod 副本,Deployment 就会确保集群中始终有这么多数量的 Pod 在运行。
  • 弹性伸缩: 当应用程序负载变化时,你可以轻松修改 Deployment 中的 Pod 副本数量,K8s 会自动进行扩容(增加 Pod)或缩容(减少 Pod)。
  • 无缝更新与回滚: Deployment 提供了强大的更新策略,最常用的是滚动更新 (Rolling Update)。这意味着当你发布新版本的应用程序时,K8s 会逐步替换旧版本的 Pod,确保服务在更新过程中不中断。如果新版本出现问题,Deployment 也支持快速回滚到上一个稳定版本。
  • 自愈能力: 如果 Deployment 管理的 Pod 发生故障,它会自动替换掉故障 Pod,维持所需的副本数量。

2.4 K8s 的“联系方式”:Service——应用程序的稳定入口

Pod 的 IP 地址是动态的,当 Pod 被 K8s 重启、替换或重新调度时,它们的 IP 地址会发生变化。这使得外部用户或集群内的其他应用程序难以稳定地访问到这些 Pod。这时,Service 就发挥了作用。

  • 稳定的网络入口: Service 为一组 Pod 提供了一个固定且稳定的网络入口(通常是一个固定的 IP 地址和端口)。无论其背后 Pod 的 IP 地址如何变化,Service 的地址都不会变。
  • 负载均衡: 当一个 Service 关联了多个后端 Pod 时,它会自动将进入的请求均匀地分发到这些 Pod 上,实现负载均衡。这确保了单个 Pod 不会被请求压垮,提高了应用程序的可用性。
  • 服务发现: K8s 内部的服务发现机制允许其他 Pod 通过 Service 的名称来访问它,而无需知道具体的 IP 地址。这使得应用程序间的通信更加灵活和解耦。
  • 多种类型:

    Service 有多种类型,以适应不同的访问需求,例如:

    • ClusterIP: 默认类型,Service 只能在 K8s 集群内部被访问。
    • NodePort: 通过每个工作节点上的特定端口,将 Service 暴露给集群外部。
    • LoadBalancer: 在云环境中,为 Service 创建一个外部负载均衡器,提供公网访问能力。

第三章:K8s 如何运转?——深入其工作机制

在第二章中,我们认识了 K8s 世界里的主要“角色”:主节点、工作节点、Pod、Deployment 和 Service。现在,我们将把这些角色串联起来,揭示 K8s 这个“智能机器人”是如何通过巧妙的协作机制,让你的应用程序在集群中高效、稳定地运行的。

3.1 K8s 基本架构概览

在先前的章节中我们得知,K8s 集群从逻辑上可以划分为两个主要部分:控制平面 (Control Plane)工作节点 (Worker Nodes)。它们通过网络相互通信,共同完成任务。

+---------------------+                      +---------------------+
|                     |                      |                     |
|  **控制平面 (Master)** |                      |  **工作节点 (Worker)** |
|                     |                      |                     |
|  +--------------+   |     API 调用         |  +--------------+   |
|  | API Server   |<---------------------->|  | Kubelet      |   |
|  +------+-------+   |                      |  +------+-------+   |
|         |           |                      |         |           |
|  +------v-------+   |                      |  +------v-------+   |
|  | etcd         |   |                      |  | Kube-proxy   |   |
|  +--------------+   |                      |  +--------------++----+
|  +--------------+   |                      |  | Container    |   |  |
|  | Scheduler    |   |                      |  | Runtime      |<--+  |
|  +--------------+   |                      |  +--------------+      |
|  +--------------+   |                      |  | Your Apps    |      |
|  | Controller   |   |                      |  | (Pods)       |      |
|  | Manager      |   |                      |  +--------------+      |
|  +--------------+   |                      |                     |
|                     |                      |                     |
+---------------------+                      +---------------------+
       ^       ^                                       ^
       |       |                                       |
       |       |                                       |
       |       +---------------------------------------+
       |               集群网络 (Cluster Network)
       +-----------------------------------------------
              用户/管理员 (kubectl CLI)

控制平面 (Master): 负责集群的全局决策和管理。它接收用户的指令,维护集群的期望状态。

工作节点 (Worker): 负责运行实际的应用程序容器。它们接收控制平面的指令并执行。

它们之间通过 Master API Server 和 Worker Kubelet 之间的 API 通信机制来进行调度上的沟通,Master API Server 和 Worker Kubelet 都是负责向其他组件/模块下达内部指令。

3.2 K8s 的启动与运行流程

当你在 K8s 中部署一个应用程序时,背后会发生一系列精密的协作。我们以部署一个新的 Deployment 为例:

  1. 用户提交意图 (Desired State): 你通过 kubectl 命令行工具(或 API)向 K8s 提交一个 Deployment 的配置文件(YAML 文件)。这个文件声明了你希望应用程序运行的副本数量、镜像版本等。

    用户 -> kubectl -> API Server (提交 Deployment YAML)
  2. API Server 接收并保存: API Server 接收到这个请求,进行验证,并将其保存在 etcd 中。此时,集群的“期望状态”发生了变化。

    API Server -> etcd (保存 Deployment 信息)
  3. Controller Manager 发现变化: Controller Manager 中的 Deployment 控制器持续监控 API Server。它发现 etcd 中的 Deployment 数量与实际运行的 Pod 数量不符。它会计算出需要创建的 Pod 数量,并请求 API Server 创建这些 Pod。

    Controller Manager (发现不符) -> API Server (创建 Pod 对象)
  4. Scheduler 调度 Pod: Scheduler 持续监控 API Server,发现有新的 Pod 对象(它们还没有被分配到任何工作节点)。它会根据 Pod 的资源需求、节点负载、亲和性等因素,选择一个最合适的工作节点,并将这个“分配决定”更新回 API Server

    Scheduler (选择节点) -> API Server (更新 Pod 状态,绑定到工作节点) -> Worker Node Kubelet
  5. Kubelet 启动 Pod: 被选中的工作节点上的 Kubelet 持续监控 API Server。它发现有一个 Pod 被调度到自己身上了。Kubelet 随即指示本地的容器运行时 (Container Runtime)(如 Docker)拉取镜像并启动 Pod 中的容器。只有这时候,Pod 才是被真正的创建,在 Master 上的 Pod 只是一个标识,并非实体。

    Kubelet (收到调度信息) -> Container Runtime (拉取镜像,启动容器) 
  6. Kubelet 汇报状态: Pod 启动后,Kubelet 会持续监控 Pod 的健康状况,并将 Pod 的实时状态(如运行中、就绪、失败等)汇报给 API Server
  7. Service 流量转发 (可选): 如果你的应用程序需要对外提供服务,你还会创建一个 ServiceKube-proxy 会在各个工作节点上配置网络规则,将流向 Service IP 的请求负载均衡地转发到后端健康的 Pod 上。

    外部请求 -> Kube-proxy -> 应用程序 Pod

3.3 K8s 内部的调度与通信机制

3.3.1 调度机制:K8s 如何选择“家”?

K8s 的调度器 (Scheduler) 是一个非常智能的“房产中介”。当一个新的 Pod 需要运行时,调度器会经历两个阶段来选择最合适的“家”(工作节点):

  • 预选 (Predicates): 过滤掉不符合条件的节点。例如,如果一个节点没有足够的内存,或者没有所需的特定硬件(如 GPU),它就会被排除。这就像中介先筛选掉不满足基本要求的房子。
  • 优选 (Priorities): 对剩下的节点进行打分排名。例如,资源利用率较低的节点可能得分更高,或者与 Pod 有亲和性(自定义权重)的节点会得到加分。这就像中介根据你的偏好,给符合条件的房子打分。

最终,得分最高的节点被选中,Pod 就被调度到该节点上。

3.3.2 内部通信:K8s 各组件如何“说话”?

K8s 内部的各个组件之间并非直接“手拉手”通信,它们的核心通信方式是:

  • API Server 为核心: 所有的组件都通过 API Server 进行通信。它们不会直接访问 etcd,也不会直接调用其他组件的函数。
  • “拉取”而非“推送”: 大多数组件都是通过“拉取”的方式获取信息。例如,Kubelet 会周期性地向 API Server 查询分配给自己的 Pod 列表;Controller Manager 会监听 API Server 的事件流来发现集群状态的变化。
  • 状态同步: 组件通过在 etcd 中读写对象(通过 API Server 中转)来同步彼此的状态和信息。这确保了集群状态的最终一致性。

这种设计模式使得 K8s 具有极高的可伸缩性、弹性和解耦性。即使某个组件暂时离线,只要 etcd 中的状态不变,它恢复后依然可以从 API Server 获取最新状态并继续工作,不会影响整个集群的稳定性。


第四章:K8s 实战入门——部署有状态应用

在本章节开始之前,交给大家一个任务,自行部署一个可用的 K8s 集群(例如通过 Minikube、Kind 或云服务商提供的 K8s 服务或者本地计算机部署)以及一个配置好 kubectl 命令行的本地环境。如果你尚未准备好,请参考相关文档完成 K8s 和 Docker 的安装与配置( Windows 或者 Macos 可以通过Docker Desktop来简易运行 K8s 服务)。

在前三章中,我们详细探讨了 K8s 的基本概念、架构以及工作原理。现在,是时候将这些理论知识付诸实践了!本章将带你完成两个常见的实战部署任务:在 K8s 集群中部署一个 Redis 集群和一个 PostgreSQL 数据库。

4.1 理解有状态应用与持久化存储

在开始部署之前,我们需要明确有状态应用持久化存储的概念。

  • 无状态应用 (Stateless Application): 这类应用不存储任何会话数据或用户数据。每次请求都是独立的,不依赖之前的任何状态。例如,一个简单的网页服务器。这类应用在 K8s 中部署起来相对简单,因为它们的 Pod 可以随时被创建、销毁或替换,而不会丢失数据。
  • 有状态应用 (Stateful Application): 这类应用需要持久化地存储数据或者需要存储部分运行时数据,其行为依赖于之前的状态或数据。例如,数据库(PostgreSQL, Redis)、消息队列等。这类应用在 K8s 中部署更复杂,因为 Pod 的销毁意味着数据的丢失,我们必须确保数据能够独立于 Pod 的生命周期而存在。

通常在分布式架构环境的前提下,我们的程序都会尽可能设计为无状态应用(服务)。

为了支持有状态应用,K8s 也引入了持久化存储 (Persistent Storage) 的概念。它允许应用程序将数据存储在独立于 Pod 的存储卷上,即使 Pod 被删除或重新创建,数据也不会丢失。

4.2 部署必备的“工具箱”:K8s 的三个 YAML 文件

在 K8s 中部署任何应用,都需要通过 YAML 文件来描述你期望的集群状态。我们将主要使用以下三种类型的 YAML 文件作为我们的“工具”:

ConfigMap (配置映射):配置的“百宝箱”

  • 作用: 这是一个 K8s 对象,专门用来存储非敏感的配置数据,例如应用程序的配置文件、启动参数、环境变量等。
  • 为什么用它: 它将应用程序的配置与代码分离,让你可以在不修改、不重新构建应用镜像的情况下,灵活地修改配置。就像给你的应用程序准备了一个外部的“操作手册”,随时可以更新。

Service (服务):应用程序的“稳定入口”

  • 作用: Service 为一组 Pod 提供一个稳定、不变的网络地址,无论底层的 Pod IP 如何变化,Service 的地址始终不变。它还负责将流入的流量负载均衡到后端健康的 Pod 上。
  • 为什么用它: Pod 的生命周期是短暂且动态的,IP 地址会变。Service 就像是你的应用程序在 K8s 内部的“电话总机”或“固定门牌号”,保证了其他应用或外部流量总能找到它。

StatefulSet (有状态集):管理有状态应用的“金牌管家”

  • 作用: StatefulSet 是 K8s 专门为有状态应用设计的一种控制器。它能够管理一组 Pod,并为每个 Pod 提供稳定、唯一的网络标识持久化存储
  • 为什么用它: 相比普通的 Deployment,StatefulSet 确保 Pod 具有有序的启动/停止/扩缩容(例如,redis-0 总是第一个启动),并且每个 Pod 都会绑定一个稳定的、唯一的名称和持久化存储卷。这对于数据库等需要数据持久性和身份一致性的应用至关重要。它就像一个“金牌管家”,精心地照顾每一个需要身份和私人空间(持久存储)的应用程序。

感觉没听懂?没有关系,我们来实际的操作一下。

4.2 部署 Redis 集群:键值存储的分布式实践

Redis 是一个高性能的键值存储数据库,常用于缓存、消息队列等场景。为了实现高可用和数据分片,我们通常会部署 Redis 集群。在 K8s 中,我们主要会用到以下 K8s 对象来部署 Redis 集群:

  • Deployment (部署): 用于管理 Redis 节点的 Pod 副本。
  • Service (服务): 为 Redis 节点提供稳定的网络访问入口。
  • ConfigMap (配置映射): 用于存储 Redis 的配置文件。
  • StatefulSet (有状态集): 这是一个专为有状态应用设计的 K8s 对象,它能为每个 Pod 提供稳定的网络标识和持久化存储。Redis 集群的每个主从节点都需要稳定的身份,因此 StatefulSet 是更合适的选择。
  • PersistentVolumeClaim (持久卷声明, PVC): 声明 Pod 需要的持久化存储空间。
  • PersistentVolume (持久卷, PV): 集群管理员提供的实际存储资源。

部署步骤概览:

  1. 创建 Redis 配置: 使用 ConfigMap 来定义 Redis 的集群模式配置。
  2. 定义持久化存储: 准备好 Pod 需要的存储空间(通过 PV/PVC 机制)。
  3. 部署 Redis 主从节点: 使用 StatefulSet 来部署 Redis 的主从 Pod,确保它们具有稳定的身份和持久化存储。
  4. 暴露 Redis 服务: 使用 Service 来为 Redis 节点提供集群内部的访问入口,并可能通过其他 Service 类型暴露给外部。

让我们赶紧开始吧~。

  1. 创建 ConfigMap

    我们的目标是部署一个简单的 Redis 集群,包含 3 个主节点和 3 个从节点。为了简化,我们将让每个主节点带一个从节点,共 3 对主从副本。

    apiVersion: v1
    kind: ConfigMap # 申明类型
    metadata:
      name: redis-cluster-config
    data:
      # redis.conf(配置信息)
      redis.conf: |
        cluster-enabled yes
        cluster-config-file /data/nodes.conf
        cluster-node-timeout 5000
        appendonly yes
        protected-mode no
        bind 0.0.0.0
        # 确保日志级别为 notice,方便调试
        loglevel notice
  2. 创建 Headless Service

    为了让 StatefulSet 中的每个 Pod 拥有稳定的网络名称(redis-0.redis-service 等),我们需要一个特殊的 Service,称为 Headless Service (无头服务)。它不会分配一个集群 IP,也不负责负载均衡,而是直接返回所有关联 Pod 的 IP 地址(暴露所有服务的地址)。

    apiVersion: v1
    kind: Service
    metadata:
      name: redis-service  # 这个名称会被 StatefulSet 用作网络域
      labels:
        app: redis-cluster
    spec:
      ports:
      - port: 6379 # Redis 端口
        name: client
      - port: 16379 # 集群总线端口
        name: cluster
      clusterIP: None # 关键:设置为 None 表示这是一个 Headless Service
      selector:
        app: redis-cluster

    name: redis-service 这个名称也会被作为 Pod 的 DNS 域名后缀,例如 redis-0.redis-service.default.svc.cluster.local

  3. 创建 StatefulSet

    这是核心部分,定义了 Redis Pod 如何被管理、存储和网络标识。我们将创建 6 个副本(3 主 3 从)。

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: redis             # StatefulSet 的名称
      labels:                 # StatefulSet 的标签,用于标识
        app: redis-cluster
    spec:
      serviceName: "redis-service" # 必须指定一个 Headless Service 的名称,StatefulSet 会用它来为 Pod 提供稳定的网络标识
      replicas: 6             # 定义 Pod 数量,这里是 6 个 (3 主 + 3 从),它们将自动命名为 redis-0,...,redis-5
      selector:               # Pod 选择器,用于 StatefulSet 查找和管理其所属的 Pod
        matchLabels:
          app: redis-cluster
      template:               # Pod 模板,用于创建 Pod
        metadata:
          labels:             # Pod 的标签,需要与 selector.matchLabels 匹配
            app: redis-cluster
        spec:
          containers:         # 定义 Pod 中运行的容器列表
          - name: redis       # 容器的名称
            image: redis:6.2.7-alpine # 容器使用的镜像及其版本
            command: ["redis-server"] # 容器启动时执行的命令,这里是启动 Redis 服务器
            args: ["/etc/redis/redis.conf"] # 传递给 command 的参数,指定 Redis 配置文件路径
            ports:
            - containerPort: 6379 # 容器暴露的端口号,用于客户端连接
              name: client        # 端口名称
            - containerPort: 16379 # 容器暴露的端口号,用于集群内部通信
              name: cluster
            volumeMounts:         # 容器内卷的挂载点列表
            - name: redis-config  # 引用名为 'redis-config' 的卷
              mountPath: /etc/redis # 将该卷挂载到容器内的 /etc/redis 路径
            - name: redis-data    # 引用名为 'redis-data' 的卷
              mountPath: /data    # 将该卷挂载到容器内的 /data 路径 (Redis 数据目录)
            env:                  # 容器内的环境变量列表
            - name: POD_NAME      # 定义一个名为 POD_NAME 的环境变量
              valueFrom:          # 环境变量的值来源于 K8s 的某个字段
                fieldRef:
                  fieldPath: metadata.name # 获取当前 Pod 的名称作为环境变量的值 (如 redis-0, redis-1)
          volumes:                  # 定义 Pod 中可用的卷列表
          - name: redis-config      # 定义一个名为 'redis-config' 的卷
            configMap:              # 这个卷的数据来源是 ConfigMap
              name: redis-cluster-config # 引用名为 'redis-cluster-config' 的 ConfigMap
      volumeClaimTemplates: # 卷声明模板:StatefulSet 为每个 Pod 自动创建 PVC 的模板
      - metadata:
          name: redis-data    # PVC 的名称,每个 Pod 会是 redis-data-<pod-name> (如 redis-data-redis-0)
        spec:
          accessModes: [ "ReadWriteOnce" ] # 存储的访问模式:ReadWriteOnce 表示只能被单个节点以读写模式挂载
          resources:
            requests:
              storage: 1Gi      # 请求的存储空间大小,每个 Pod 将获得 1GB
          # storageClassName: your-storage-class # (可选) 指定存储类。如果 K8s 集群没有默认 StorageClass,或者需要特定的存储类型,请在此处指定。例如 'standard' 或 'gp2' (AWS)。
  4. 运行 Pod

    拓展:

    • 有时候我们会将 pvc 模版单独划分为一个文件运行,有时候我们也可能将所有配置都放在一个 yaml 中,本质只是将配置分类。
    • 配置分类都是遵照 kind 的定义来划分的。


      好的,我们来详细解释一下 K8s YAML 文件中的 kind 字段。这是一个非常基础但核心的 K8s 概念。


      kind:K8s 对象的“类型标识”

      在 K8s 的 YAML 定义文件中,kind 字段是有严格规定的,不能自定义名称

      1. kind 的定义

      kind(英文单词“种类”的意思)字段用于指定你正在创建或操作的 K8s 资源的类型。每一个 K8s 资源都有一个唯一的 kind

      K8s 是一个高度模块化的系统,它通过各种不同的“对象”(也就是资源)来管理集群中的各种元素。kind 字段就是用来告诉 K8s API Server:“我正在定义一个什么类型的资源”。

      2. 常见的 kind

      以下是一些最常见的 kind 值及其对应的 K8s 对象:

      • Pod: K8s 中最小的部署单元。
      • Service: 用于暴露 Pod 的稳定网络服务。
      • Deployment: 用于管理无状态应用程序的 Pod 副本和更新。
      • StatefulSet: 用于管理有状态应用程序的 Pod 副本,提供稳定的身份和持久化存储。
      • ConfigMap: 用于存储非敏感配置数据。
      • Secret: 用于存储敏感数据(如密码、API 密钥)。
      • PersistentVolumeClaim (PVC): Pod 对持久化存储的请求。
      • PersistentVolume (PV): 集群提供的实际持久化存储资源。
      • Namespace: 用于集群内资源隔离的逻辑分组。
      • Node: 集群中的一台物理或虚拟机(由 K8s 自动发现并创建对应资源)。
      • ReplicaSet: 确保特定数量的 Pod 副本在运行(通常由 Deployment 间接创建和管理)。

      3. kind 的重要性

      • API Server 解析: 当你使用 kubectl apply -f your-file.yaml 命令提交一个 YAML 文件时,K8s 的 API Server 会首先读取 kind 字段,以确定如何解析和处理这个 YAML 文件。如果 kind 写错了,K8s 就不知道你想要创建什么,从而报错。
      • 资源分类: kind 帮助 K8s 内部系统对各种资源进行分类和管理。
      • CLI 操作: 你在执行 kubectl get <kind>kubectl describe <kind> 等命令时,也需要指定 kind 来筛选和查看特定类型的资源。

      4. kind 的来源

      kind 值是 K8s API 定义的一部分,它们由 K8s 的开发者预先定义好,并在 K8s 的官方文档中列出。你可以在 K8s 官方文档的 API 参考中找到所有可用的 kind 类型。

    现在,我们配置都已经定义好了,只需要我们开始传达执行命令即可让整个服务运转起来。

    kubectl 是 Kubernetes 的命令行工具 (Command-Line Interface)。它是你与 K8s 集群进行交互的主要方式,就像一个遥控器,它允许你发送指令给 K8s 集群的 API Server,从而实现对集群中各种资源的创建、管理、查看、删除等操作。

我们需要使用以下命令来应用我们的配置文件:

kubectl apply -f xxx.yaml
kubectl apply -f redis-config.yaml

kubectl apply -f redis-service.yaml

kubectl apply -f redis-statefulset.yaml
  1. 验证 Pod 和 PVC 状态

    让我们稍等一段时间,大约半分钟左右,K8s 会自动完成 Pod 的创建。

    # 查看 Pod 状态,你应该会看到 `redis-0` 到 `redis-5` 陆续创建并变为 `Running` 状态。
    kubectl get pods -l app=redis-cluster
    
    # 查看 PVC 状态,你应该会看到为每个 Pod 自动创建的 PVC,例如 `redis-data-redis-0` 到 `redis-data-redis-5`,它们应该都处于 `Bound` 状态。
    kubectl get pvc -l app=redis-cluster
  2. 初始化集群

    嘿!别着急!还没有结束!

    StatefulSet 启动 Redis 实例后,它们仅仅是独立的 Redis 服务器,还没有组成集群。我们需要手动执行 Redis 集群初始化命令。通常,我们会选择一个 Pod(例如 redis-0)来执行这个命令。

    1. 获取所有 Pod 的稳定 DNS 名称:

      先来解释一下即将要执行的命令:

      # 定义一个名为 REDIS_POD_IPS 的 Shell 变量
      # $(...) 表示执行括号内的命令,并将其标准输出作为变量的值
      REDIS_POD_IPS=$( \
        # 使用 kubectl 命令获取 K8s 集群中 Pod 的信息
        kubectl get pods \
        # -l app=redis-cluster 是一个标签选择器,只选择带有 "app: redis-cluster" 标签的 Pod
        -l app=redis-cluster \
        # -o jsonpath='...' 指定输出格式为 JSONPath,用于从 JSON 结构中精确提取数据
        -o jsonpath='{ \
          # {range.items[*]} 遍历 kubectl get pods 返回的 JSON 结果中所有的 Pod 对象
          range.items[*] \
          # {.status.podIP} 提取每个 Pod 的实际 IP 地址
          .status.podIP \
          # :6379 手动拼接 Redis 的默认端口号,因为 Redis 集群创建命令需要 "IP:Port" 格式
          # {end} 在每个 IP:Port 组合后添加一个空格,确保它们作为独立的参数被后续命令识别
          }:6379 {end}' \
      )
      
      # 打印 REDIS_POD_IPS 变量的值,用于验证其内容是否正确
      # 预期输出类似:"10.244.1.4:6379 10.244.2.5:6379 10.244.1.5:6379 10.244.2.6:6379 10.244.1.6:6379 10.244.2.7:6379 "
      echo "Redis Pod IPs: $REDIS_POD_IPS"
      
      ----------
      
      # kubectl exec -it redis-0 -- 用于在指定的 Pod (redis-0) 内部执行命令
      # -i: 保持标准输入打开,允许你进行交互式输入 (例如确认 'yes')
      # -t: 分配一个伪 TTY (终端),让输出更像在真实终端中
      # --: 分隔符,表示其后的内容都是要执行的命令及其参数,而不是 kubectl exec 自身的参数
      kubectl exec -it redis-0 -- \
        # sh -c "..." 启动一个 Shell 进程 (sh) 在 Pod 内部,并让它执行双引号内的字符串命令
        # 这是解决 Shell 变量展开问题的关键:确保 $REDIS_POD_IPS 在容器内部的 Shell 中被正确地展开为多个独立的 IP:Port 参数
        sh -c "\
          # redis-cli --cluster create 是 Redis 命令行工具的集群创建子命令
          redis-cli --cluster create \
          # $REDIS_POD_IPS 变量在此处被容器内部的 sh Shell 展开为一串以空格分隔的 IP:Port 列表
          $REDIS_POD_IPS \
          # --cluster-replicas 1 指定每个主节点带一个从节点
          --cluster-replicas 1 \
        "

      为了更好拷贝内容,我再附上一份没有多余解释的命令:

      REDIS_POD_IPS=$(kubectl get pods -l app=redis-cluster -o jsonpath='{range.items[*]}{.status.podIP}:6379 {end}')
      echo "Redis Pod IPs: $REDIS_POD_IPS"
      
      kubectl exec -it redis-0 -- sh -c "redis-cli --cluster create $REDIS_POD_IPS --cluster-replicas 1"

      你会得到如下类似的执行结果:

      ❯ REDIS_POD_IPS=$(kubectl get pods -l app=redis-cluster -o jsonpath='{range.items[*]}{.status.podIP}:6379 {end}')
      echo "Redis Pod IPs: $REDIS_POD_IPS"
      
      Redis Pod IPs: 10.1.0.18:6379 10.1.0.19:6379 10.1.0.20:6379 10.1.0.21:6379 10.1.0.22:6379 10.1.0.23:6379 
      
      ❯ kubectl exec -it redis-0 -- sh -c "redis-cli --cluster create $REDIS_POD_IPS --cluster-replicas 1"
      
      >>> Performing hash slots allocation on 6 nodes...
      Master[0] -> Slots 0 - 5460
      Master[1] -> Slots 5461 - 10922
      Master[2] -> Slots 10923 - 16383
      Adding replica 10.1.0.22:6379 to 10.1.0.18:6379
      Adding replica 10.1.0.23:6379 to 10.1.0.19:6379
      Adding replica 10.1.0.21:6379 to 10.1.0.20:6379
      M: 1eb3dd6f7712c7661f934ec1c65d36ac4daa9ac3 10.1.0.18:6379
         slots:[0-5460] (5461 slots) master
      M: 5441d7b3cee660294a44436ce5e28f124a8a3013 10.1.0.19:6379
         slots:[5461-10922] (5462 slots) master
      M: bae2c19ca08ff1836e7049ae2680f068f179ab46 10.1.0.20:6379
         slots:[10923-16383] (5461 slots) master
      S: 6478f1d8216315e1442218966ef7653b0f73dce3 10.1.0.21:6379
         replicates bae2c19ca08ff1836e7049ae2680f068f179ab46
      S: d3969c843d26d8f7f69f7756f72cccf7ed250017 10.1.0.22:6379
         replicates 1eb3dd6f7712c7661f934ec1c65d36ac4daa9ac3
      S: 3e136252b41a52ec278911a086f674c21c83c270 10.1.0.23:6379
         replicates 5441d7b3cee660294a44436ce5e28f124a8a3013
      Can I set the above configuration? (type 'yes' to accept): yes
      >>> Nodes configuration updated
      >>> Assign a different config epoch to each node
      >>> Sending CLUSTER MEET messages to join the cluster
      Waiting for the cluster to join
      .
      >>> Performing Cluster Check (using node 10.1.0.18:6379)
      M: 1eb3dd6f7712c7661f934ec1c65d36ac4daa9ac3 10.1.0.18:6379
         slots:[0-5460] (5461 slots) master
         1 additional replica(s)
      M: 5441d7b3cee660294a44436ce5e28f124a8a3013 10.1.0.19:6379
         slots:[5461-10922] (5462 slots) master
         1 additional replica(s)
      S: 6478f1d8216315e1442218966ef7653b0f73dce3 10.1.0.21:6379
         slots: (0 slots) slave
         replicates bae2c19ca08ff1836e7049ae2680f068f179ab46
      M: bae2c19ca08ff1836e7049ae2680f068f179ab46 10.1.0.20:6379
         slots:[10923-16383] (5461 slots) master
         1 additional replica(s)
      S: d3969c843d26d8f7f69f7756f72cccf7ed250017 10.1.0.22:6379
         slots: (0 slots) slave
         replicates 1eb3dd6f7712c7661f934ec1c65d36ac4daa9ac3
      S: 3e136252b41a52ec278911a086f674c21c83c270 10.1.0.23:6379
         slots: (0 slots) slave
         replicates 5441d7b3cee660294a44436ce5e28f124a8a3013
      [OK] All nodes agree about slots configuration.
      >>> Check for open slots...
      >>> Check slots coverage...
      [OK] All 16384 slots covered.

      当你执行 redis-cli --cluster create 命令时,它会提示你是否接受集群配置,输入 yes 确认即可。

    2. 验证集群状态:

      kubectl exec -it redis-0 -- redis-cli cluster info
      kubectl exec -it redis-0 -- redis-cli cluster nodes
      
      ----------
      
      # 执行示例如下:
      ❯ kubectl exec -it redis-0 -- redis-cli cluster info
      cluster_state:ok
      cluster_slots_assigned:16384
      cluster_slots_ok:16384
      cluster_slots_pfail:0
      cluster_slots_fail:0
      cluster_known_nodes:6
      cluster_size:3
      cluster_current_epoch:6
      cluster_my_epoch:1
      cluster_stats_messages_ping_sent:546
      cluster_stats_messages_pong_sent:561
      cluster_stats_messages_sent:1107
      cluster_stats_messages_ping_received:556
      cluster_stats_messages_pong_received:546
      cluster_stats_messages_meet_received:5
      cluster_stats_messages_received:1107
      ❯ kubectl exec -it redis-0 -- redis-cli cluster nodes
      5441d7b3cee660294a44436ce5e28f124a8a3013 10.1.0.19:6379@16379 master - 0 1749203764236 2 connected 5461-10922
      6478f1d8216315e1442218966ef7653b0f73dce3 10.1.0.21:6379@16379 slave bae2c19ca08ff1836e7049ae2680f068f179ab46 0 1749203765000 3 connected
      bae2c19ca08ff1836e7049ae2680f068f179ab46 10.1.0.20:6379@16379 master - 0 1749203764000 3 connected 10923-16383
      d3969c843d26d8f7f69f7756f72cccf7ed250017 10.1.0.22:6379@16379 slave 1eb3dd6f7712c7661f934ec1c65d36ac4daa9ac3 0 1749203763514 1 connected
      3e136252b41a52ec278911a086f674c21c83c270 10.1.0.23:6379@16379 slave 5441d7b3cee660294a44436ce5e28f124a8a3013 0 1749203765252 2 connected
      1eb3dd6f7712c7661f934ec1c65d36ac4daa9ac3 10.1.0.18:6379@16379 myself,master - 0 1749203763000 1 connected 0-5460

      你应该能看到集群的状态为 ok,并且所有节点都已加入集群,并分配了主从角色。


    通过上述步骤,你已经在 K8s 中成功部署了一个具备持久化存储的 Redis 集群。这展示了 K8s 在管理复杂有状态应用方面的强大能力。

4.3 部署 PostgreSQL 数据库:关系型数据的持久化基石

PostgreSQL 是一款功能强大、高度稳定的开源关系型数据库。在 K8s 中部署 PostgreSQL,同样需要依赖持久化存储。

部署步骤概览:

  1. 定义持久化存储: 为 PostgreSQL 数据库文件准备持久卷。
  2. 创建数据库密码: 使用 Secret 来安全地存储数据库密码,避免硬编码。
  3. 部署 PostgreSQL 实例: 使用 DeploymentStatefulSet(通常推荐 StatefulSet 以获得稳定的身份和有序部署)来部署 PostgreSQL Pod。
  4. 暴露数据库服务: 使用 Service 为 PostgreSQL 提供一个稳定的集群内部访问地址。

.........更新中。

最后修改:2025 年 06 月 06 日
如果觉得我的文章对你有用,请随意赞赏