Skip to content

Kubernetes 完全指南:从入门到生产实战 ​

核心主张 ​

Kubernetes 不仅仅是一个容器编排工具,它是云原生时代的"操作系统",是分布式系统基础设施的事实标准。学习K8s不能只停留在"会用"的层面,而要深入理解其设计哲学——声明式API、控制循环、不可变基础设施——这些思想远比具体的API更有价值。从第一性原理出发,建立完整的K8s知识体系,才能在快速变化的云原生生态中立于不败之地。

作者声音标签 ​

硬核但有趣、深入但不晦涩、实用且有对比、既有原理也有实战、故事化表达、工程师视角

目标读者 ​

  • 初学者:有一定Linux和Docker基础,想系统学习K8s的开发/运维工程师
  • 中级工程师:正在使用K8s但知识碎片化,想补全体系、深入理解原理
  • 资深工程师:有多年运维经验,希望了解K8s前沿生态、最佳实践和性能调优
  • 技术讲师:需要高质量教学材料的高校或培训机构讲师

阅读本书需要的前置知识:熟悉Linux基本操作、了解Docker容器概念、有一定编程基础。

章节规划 ​

章节标题核心问题用户收获目标字数
ch01容器编排的前世今生:为什么我们需要K8s容器解决了什么问题?为什么还需要容器编排?K8s为什么赢了?理解K8s诞生的历史背景和技术必然性,建立技术演进的大局观3000
ch02K8s架构全景图:上帝视角看集群K8s集群由哪些组件组成?它们各自负责什么?如何协同工作?建立K8s架构的完整认知,理解控制平面与数据平面的分工3500
ch03Pod:K8s世界的原子单位为什么是Pod而不是直接用容器?Pod的本质是什么?深入理解Pod设计理念,掌握容器设计模式3500
ch04工作负载控制器:从Pod到应用为什么不直接用Pod?各种控制器分别适用于什么场景?掌握Deployment、StatefulSet、DaemonSet、Job等控制器的使用场景和原理3000
ch05Service与Ingress:服务发现与流量入口Pod IP不持久怎么办?外部流量怎么进集群?理解Service和Ingress的底层实现,掌握服务发现机制3000
ch06存储管理:有状态应用的基石容器存储为什么难?各种存储方案怎么选?掌握PV/PVC/StorageClass体系,了解主流存储方案对比3500
ch07配置与密钥管理:ConfigMap与Secret配置为什么要与镜像分离?Secret真的安全吗?掌握配置管理最佳实践,了解密钥管理方案3000
ch08调度器深度解析:Pod去哪儿了调度器是怎么工作的?如何影响Pod的调度决策?深入理解调度算法,掌握节点选择、亲和性、污点等调度策略3500
ch09K8s网络模型:容器网络的黑盒揭秘Pod网络是怎么通的?各种CNI有什么区别?深入理解容器网络底层原理,掌握主流CNI方案对比4000
ch10安全与权限:RBAC、NetworkPolicy与Pod SecurityK8s安全怎么做?如何防范各种安全风险?建立K8s安全体系认知,掌握RBAC、网络策略等安全机制3000
ch11可观测性三支柱:日志、监控与告警生产环境怎么监控?出了问题怎么排查?掌握Prometheus、Grafana、EFK/Loki等可观测性工具栈3500
ch12应用生命周期管理:升级、回滚与自愈怎么零停机发布应用?应用挂了怎么办?掌握滚动更新、蓝绿部署、金丝雀发布、自愈机制3000
ch13生产环境实战:集群部署与高可用生产集群怎么搭?怎么保证高可用?掌握集群部署方案、高可用架构、备份恢复策略3500
ch14性能调优与大规模集群:万节点集群的秘密K8s性能瓶颈在哪?大规模集群怎么优化?掌握K8s各组件性能调优方法,了解大规模集群挑战3500
ch15故障排查方法论:从入门到精通出了问题怎么快速定位?有什么系统化的方法?建立故障排查思维框架,掌握常见问题排查思路3500
ch16高级资源:CRD、Operator与自定义控制器K8s怎么扩展?Operator是什么?理解K8s的扩展能力,掌握CRD和Operator模式3500
ch17Gateway API:下一代流量治理标准Ingress有什么问题?Gateway API好在哪?了解Gateway API的设计理念和核心概念3000
ch18K8s生态全景:CNCF项目地图云原生生态有哪些项目?怎么选?了解CNCF生态全景,建立技术选型能力3000
ch19GitOps与持续交付:Argo CD与FluxGitOps是什么?为什么是最佳实践?掌握GitOps理念和工具,实现声明式持续交付3500
ch20Serverless与弹性伸缩:KEDA、Knative与Virtual KubeletK8s上怎么做Serverless?怎么自动伸缩?了解Serverless在K8s上的实现,掌握各种自动伸缩方案3500
ch21Service Mesh:Istio与eBPF时代的服务治理服务网格是什么?需要吗?选哪个?理解Service Mesh理念,掌握Istio、Linkerd、Cilium对比3500
ch22AI与K8s:GPU调度与大模型训练AI训练为什么需要K8s?GPU怎么调度?了解K8s在AI/ML场景的应用,掌握GPU调度方案3000
ch23K8s发行版对比:选哪个发行版好为什么有这么多发行版?怎么选?了解主流发行版特点,掌握选型决策方法2500
ch24面试与认证指南:CKA/CKAD/CKS通关秘籍K8s认证怎么考?面试常考什么?掌握CKA/CKAD/CKS备考方法,了解面试高频考点2500
ch25未来展望:K8s的下一个十年K8s未来会怎么发展?有什么趋势?了解云原生前沿趋势,建立技术发展预判能力2000

章节依赖关系 ​

章节前置章节
ch01无
ch02ch01
ch03ch02
ch04ch03
ch05ch04
ch06ch04
ch07ch04
ch08ch03
ch09ch05
ch10ch03, ch05, ch09
ch11ch03, ch05
ch12ch04
ch13ch02, ch10
ch14ch02, ch08, ch09
ch15ch11, ch13
ch16ch02, ch04
ch17ch05
ch18ch02
ch19ch04, ch12
ch20ch04, ch08
ch21ch05, ch09, ch10
ch22ch08, ch06
ch23ch02, ch13
ch24ch01, ch02, ch03, ch04, ch05, ch06, ch07, ch08, ch09, ch10, ch11, ch12, ch13, ch14, ch15, ch16, ch17, ch18, ch19, ch20, ch21
ch25ch18, ch22

总字数目标 ​

全书目标字数:80,000 字

各篇字数分配:

  • 基础篇(ch01-ch05):16,000 字
  • 进阶篇(ch06-ch10):17,000 字
  • 运维篇(ch11-ch15):17,000 字
  • 高级篇(ch16-ch21):20,000 字
  • 前沿篇(ch22-ch25):10,000 字

第1章 容器编排的前世今生:为什么我们需要K8s ​

1.1 从物理机到容器:基础设施的三次革命 ​

要理解K8s为什么会出现,我们得先把时间往回拨,看看基础设施这几十年是怎么演进的。这不是在讲历史故事,而是理解技术发展规律的必经之路——所有新技术的诞生,都是为了解决旧技术的痛点。

1.1.1 物理机时代:笨重的"铁盒子" ​

在2000年之前,互联网还处于蛮荒时代。那时候部署一个应用是什么样的?

你得先买一台服务器——对,就是那种又大又重、放机房里嗡嗡响的"铁盒子"。然后装操作系统、装依赖库、装应用、配置网络、配置防火墙……一套流程下来,少则几天,多则几周。

这时候的问题很明显:

  • 部署慢:上线一个新应用,先等采购服务器,等机器到了再装环境,黄花菜都凉了
  • 资源利用率低:一台服务器跑一个应用,CPU、内存大部分时间都闲着,但你不敢往上堆别的应用——怕冲突啊
  • 扩容难:流量突增怎么办?再买几台服务器呗,等买回来流量高峰都过去了
  • 迁移成本高:要换个机房?把所有机器搬过去,重新部署,想想都头大

那时候的运维工程师,本质上是"服务器搬运工",每天跟硬件打交道。

1.1.2 虚拟机时代:第一次"解耦" ​

2000年左右,VMware把虚拟化技术带进了数据中心。这是基础设施的第一次革命。

什么是虚拟化?简单说,就是在一台物理服务器上,模拟出多台"虚拟服务器"。每台虚拟机都有自己的操作系统、CPU、内存、磁盘,互相隔离,就像独立的物理机一样。

这一下就解决了很多问题:

  • 资源利用率提高了:一台物理机可以跑好多台虚拟机,CPU、内存不浪费了
  • 部署快了:不用等采购了,直接克隆一个虚拟机镜像,几分钟就能启动一台新的
  • 隔离性好了:应用跑在各自的虚拟机里,互不影响
  • 迁移方便了:虚拟机就是个文件,想挪到哪就挪到哪,vMotion技术甚至能做到热迁移

但是,虚拟机也有它的问题:

  • 太重了:每个虚拟机都要跑一个完整的操作系统,内核、用户态库、系统服务……光操作系统就占了好几个G内存,启动也要几分钟
  • 资源开销大:Hypervisor层(虚拟机监控器)本身也要占资源,虚拟化有性能损耗
  • 镜像分发难:几个G的镜像,传来传去不方便
  • 环境一致性问题还是没彻底解决:你说"我这能跑啊",他说"我这怎么不行"——因为虚拟机里的环境可能还是不一样

这就像你搬家,把整个房子(包括墙壁、地板、水电)都打包搬走——当然能搬,但太重了。

1.1.3 容器时代:第二次"解耦" ​

2013年,Docker横空出世,容器技术突然火了。这是基础设施的第二次革命。

容器和虚拟机有什么区别?简单说:

  • 虚拟机:硬件级别的虚拟化,每个虚拟机有自己的完整操作系统
  • 容器:操作系统级别的虚拟化,所有容器共享宿主机的内核

打个比方:

  • 虚拟机就像独栋别墅,每家都有自己的院子、厨房、卫生间——独立,但占地大、成本高
  • 容器就像公寓楼里的套房,大家共享大楼的地基、水电、公共设施——每家有自己的独立空间,但共享基础设施,成本低、密度高

容器的好处太明显了:

  • 轻量:容器镜像只有几十M甚至几M,秒级启动
  • 资源利用率高:没有Hypervisor开销,没有多余的操作系统,一台物理机能跑几百个容器
  • 环境一致性:"Build once, run anywhere"——镜像里包含了应用和所有依赖,在哪跑都一样
  • 快速部署和回滚:镜像就是版本,想部署哪个版本就用哪个镜像,回滚也是秒级

Docker的出现,让"微服务"架构真正落地成为可能。以前一个大应用拆成几十个小服务,部署运维成本太高,现在有了容器,拆就拆呗,反正部署都一样。

但是,问题又来了——

1.2 容器带来的新问题:谁来管这一大堆容器? ​

容器解决了"单个应用怎么部署"的问题,但当你有几百、几千个容器的时候,新的问题就出现了:

1.2.1 调度问题:这么多容器,放哪台机器上? ​

假设你有100台服务器,500个容器要部署。每个容器需要的CPU、内存不一样,每台服务器的剩余资源也不一样。

你怎么安排?

  • 哪个容器放哪台机器上?
  • 怎么保证机器资源不超卖?
  • 怎么提高资源利用率,别让有的机器闲死、有的机器忙死?
  • 新的容器来了,往哪放?

这就是经典的装箱问题(Bin Packing)——把不同大小的"物品"(容器)放进不同容量的"箱子"(服务器)里,目标是用最少的箱子装下所有物品。

人工来做?500个容器你排排试试,排到怀疑人生。

1.2.2 自愈问题:容器挂了怎么办? ​

容器是"易逝的"——它可能随时挂掉:

  • 应用崩溃了
  • OOM(内存不足)被内核杀了
  • 所在的服务器宕机了
  • 有人不小心删了

以前用虚拟机的时候,虚拟机挂了,运维工程师收到告警,手动重启,或者迁移到别的机器上。

但容器呢?几百个容器,每天挂几个太正常了。总不能让运维工程师24小时盯着,挂一个手动起一个吧?

我们需要一个系统,能自动监控容器状态,挂了就自动重启,机器挂了就把上面的容器自动迁移到别的机器上。

1.2.3 服务发现与负载均衡:怎么找到这些容器? ​

在微服务架构里,服务之间要互相调用。比如订单服务要调用用户服务。

但容器的IP是不固定的啊!

  • 容器重启了,IP就变了
  • 扩容了,又多了几个IP
  • 缩容了,少了几个IP

那订单服务怎么找到用户服务?总不能把IP写死在配置里吧?

这就需要服务发现——服务启动的时候自动注册,调用的时候自动发现。

而且,用户服务有10个实例,订单服务调用哪个?这就需要负载均衡——把请求均匀地分到各个实例上。

1.2.4 弹性伸缩:流量突增怎么办? ​

搞活动、上热搜、突发流量……请求量瞬间涨了10倍。

以前用虚拟机的时候,扩容要几分钟甚至几十分钟,等你扩完,流量高峰都过去了,用户早就走了。

容器启动快,能不能自动扩容?

  • 监控CPU使用率,超过80%就自动加实例
  • 流量降下来了,就自动减实例
  • 既能扛住高峰,又能省钱

这就是自动伸缩。

1.2.5 滚动更新:怎么零停机发布? ​

以前发布新版本,怎么发布?

  • 停掉旧版本,启动新版本——中间有停机时间,用户体验差
  • 先启动新版本,再停掉旧版本——中间两个版本同时跑,会不会有问题?
  • 一台一台更新——怎么保证更新过程中服务可用?

我们需要滚动更新——逐步替换旧版本的容器,整个过程服务不中断,出了问题还能快速回滚。

1.2.6 配置管理:这么多容器,配置怎么管? ​

500个容器,每个都有配置文件。

  • 配置改了,怎么同步到所有容器?
  • 不同环境(开发、测试、生产)的配置不一样,怎么管理?
  • 敏感信息(密码、密钥)怎么安全地传给容器?

总不能每个镜像里都塞配置文件吧——改个配置还要重新构建镜像?那也太蠢了。

……

你看,容器解决了部署的问题,但带来了编排的问题。当容器数量多到一定程度,人工管理就不现实了,必须有一个系统来自动管理这些容器。

这个系统,就叫容器编排系统。

1.3 容器编排三巨头:三国杀时代 ​

2014-2016年,容器编排领域是"三国杀"的局面,三个主要玩家:

1.3.1 Docker Swarm:Docker官方的"亲儿子" ​

Docker公司自己搞的编排工具,叫Swarm。

它的优势很明显:

  • 简单:跟Docker CLI无缝集成,学Docker的时候顺便就学了
  • 轻量:架构简单,部署容易
  • 官方出品:Docker公司背书,大家觉得"正统"

但是它的问题也很明显:

  • 功能太简单:只能满足基本的编排需求,复杂场景就不行了
  • 扩展性差:想加个自定义功能?很难
  • 社区不活跃:Docker公司自己控制,社区参与度低

简单说,Swarm就是个"玩具级"的编排工具,小打小闹还行,生产环境大规模用就差点意思。

1.3.2 Apache Mesos + Marathon:大数据派的"降维打击" ​

Mesos是什么?它其实不是专门给容器做的,它是一个更通用的"集群资源管理器"。

你可以把Mesos理解成"数据中心的操作系统内核"——它把整个数据中心的资源(CPU、内存、磁盘)统一管理起来,然后分配给上面跑的各种框架。

Marathon就是跑在Mesos上的一个框架,专门用来编排容器。

Mesos的优势:

  • 成熟稳定:Mesos是2010年就有的项目,Twitter、Airbnb这些大厂都在用,经过了大规模生产环境验证
  • 通用性强:不光能跑容器,还能跑Hadoop、Spark、Storm这些大数据任务
  • 性能好:万级节点的集群都能扛住

但是它的问题:

  • 太重了:架构复杂,学习成本高,部署运维都麻烦
  • 容器不是一等公民:Mesos本来就不是为容器设计的,容器只是它支持的一种任务类型,总感觉有点"别扭"
  • 社区在走下坡路:随着K8s的崛起,Mesos的社区越来越冷清

1.3.3 Kubernetes:Google的"秘密武器"开源了 ​

2014年,Google宣布开源Kubernetes(简称K8s)。

K8s是什么来头?它是Google基于自己内部的Borg系统设计的。Borg是什么?那是Google用了十几年的内部容器编排系统,Google的所有服务——搜索、Gmail、YouTube、MapReduce……全都跑在Borg上。

Google在容器编排领域有多少年的经验?说出来吓人——从2000年初就开始搞了,比Docker早了十几年。Google内部每年要启动几十亿个容器,什么大风大浪没见过?

所以K8s不是拍脑袋设计出来的,它是Google十几年运维经验的结晶。

K8s一出来,就展现出了"降维打击"的实力:

  • 设计先进:声明式API、控制器模式、Pod概念……这些设计思想领先同行一个时代
  • 功能强大:调度、自愈、服务发现、负载均衡、配置管理、存储编排……你能想到的功能它都有
  • 扩展性极强:CRD、Operator、CNI、CSI、CRI……各种扩展接口,想加什么功能加什么功能
  • 社区强大:Google、Red Hat、IBM、微软……各大厂商都参与,社区超级活跃

但是K8s也有缺点:

  • 太复杂了:概念多、组件多、学习曲线陡峭,新手入门容易劝退
  • 部署运维难:搭一个生产可用的K8s集群,不是件容易的事

不过,这些缺点跟它的优点比起来,都不算什么——功能强大才是硬道理。

1.4 K8s为什么赢了?技术、生态与社区的三重胜利 ​

2017年之后,战局逐渐明朗——K8s赢了,而且是碾压式的胜利。

Docker Swarm基本凉了,Docker公司后来都宣布把K8s集成到Docker里了;Mesos也不行了,社区越来越冷清,很多原来用Mesos的公司都迁到K8s了。

为什么K8s能赢?我总结了三个原因:

1.4.1 技术上的"降维打击" ​

K8s的设计思想,真的比竞争对手先进太多了。

举几个例子:

声明式API vs 命令式API:

  • Swarm、Mesos都是命令式的——你告诉它"做什么"(给我启动3个容器)
  • K8s是声明式的——你告诉它"要什么状态"(我要3个副本运行这个镜像),然后K8s自己想办法达到这个状态

别小看这一点区别,这是本质上的不同。声明式API意味着:

  • 系统是自愈的——当前状态跟期望状态不一样,系统就自动调整
  • 配置是版本化的——期望状态存在YAML文件里,可以用Git管理
  • 操作是幂等的——同一个配置 apply 多少次结果都一样

这就是**基础设施即代码(IaC)**的思想,K8s从设计之初就贯彻了这个理念。

Pod的设计:

  • 别的编排系统,最小单位都是容器
  • K8s搞了个Pod的概念——Pod是一组容器的集合,共享网络命名空间

为什么要这么设计?因为在真实场景中,一个应用往往不是只有一个容器。比如你有个主容器跑业务,还有个Sidecar容器帮它收集日志、做代理……这些容器是紧密耦合的,应该放在一起调度、一起生命周期管理。

Pod的设计,完美地解决了这个问题。这就是十几年经验沉淀出来的设计——你没遇到过这些场景,你就想不到要这么设计。

控制器模式: K8s的核心逻辑就是"控制循环"——不断地观察当前状态,跟期望状态对比,如果不一样就做调整。

这个模式太强大了,而且是可扩展的——你可以自己写控制器,自己定义资源类型,让K8s管理你自己的东西。

这就是为什么K8s的生态这么繁荣——因为它的扩展能力太强了,大家都能在上面做东西。

1.4.2 生态上的"赢者通吃" ​

K8s的厉害之处,不只是它本身,更是它周围的生态。

CNCF(云原生计算基金会)是什么?2015年Google联合Red Hat等公司成立了CNCF,把K8s捐给了CNCF。然后CNCF就成了云原生领域的"联合国",各种项目都往里面挤。

现在CNCF有多少项目?几十个——

  • 可观测性:Prometheus、Grafana、Jaeger、Fluentd……
  • 网络:Calico、Cilium、Istio、Linkerd……
  • 存储:Rook、Longhorn……
  • 安全:Falco、OPA、cert-manager……
  • 部署:Helm、Argo CD、Flux……

这些项目,几乎都是围绕K8s的。你用了K8s,就能用这一整套工具,形成一个完整的技术栈。

这就是网络效应——用的人越多,生态越好;生态越好,用的人就越多。形成正向循环,最后赢者通吃。

1.4.3 社区上的"众望所归" ​

K8s的社区是真的强。

首先,大厂背书:Google、Red Hat、IBM、微软、亚马逊……几乎所有你叫得上名字的科技公司,都在参与K8s的开发,都在支持K8s。

为什么?因为大家都怕被某一家公司控制。Docker Swarm是Docker公司控制的,Mesos主要是Mesosphere公司在搞——大家都怕"被绑架"。而K8s在CNCF手里,是中立的,大家都放心。

其次,社区活跃:K8s是GitHub上最活跃的项目之一,几千个贡献者,每个版本都有几百个功能特性。你遇到的问题,别人早就遇到过了;你想要的功能,社区早就有人在做了。

最后,人才市场:现在招运维、招后端,不会K8s你都不好意思投简历。会K8s的人越来越多,公司用K8s的成本就越来越低——因为招人容易啊。

技术、生态、社区,三重优势叠加,K8s想不赢都难。

1.5 Borg:K8s的精神祖先,Google十年运维经验的结晶 ​

前面提到了Borg,我觉得有必要多讲几句——因为理解了Borg,你就能理解K8s为什么是现在这个样子。

1.5.1 Borg是什么? ​

Borg是Google内部的容器编排系统,从2003年左右就开始搞了,到现在已经20多年了。Google的几乎所有服务,都跑在Borg上。

Borg有多厉害?

  • 一个Borg集群可以有几万台机器
  • 每年启动几十亿个容器
  • 资源利用率比行业平均水平高很多——Google靠这个省了不知道多少钱

1.5.2 Borg给K8s留下了什么遗产? ​

K8s的几个核心设计,都是从Borg来的:

Pod:Borg里叫"alloc",跟K8s的Pod是一个概念——一组任务共享资源,一起调度。

Job/Task模型:Borg里Job对应K8s里的Deployment/ReplicaSet,Task对应Pod。

声明式API:Borg也是声明式的,用户提交Job配置,Borg负责达到期望状态。

调度器:Borg的调度器经过了十几年的优化,K8s的调度器设计也借鉴了很多经验。

资源隔离:Google在容器资源隔离方面的经验,直接影响了Linux cgroups的发展,也影响了K8s的资源管理设计。

可以说,K8s就是Google把自己十几年的运维经验,以开源项目的形式分享给了全世界。

这就是为什么K8s一出来就这么成熟——它不是从零开始的,它是站在巨人的肩膀上。

1.6 扩展知识:分布式系统的CAP定理与最终一致性 ​

讲到分布式系统,就不得不提CAP定理——这是分布式系统的"相对论",所有分布式系统都绕不开它。

1.6.1 CAP定理是什么? ​

CAP定理由计算机科学家Eric Brewer提出,它说的是:一个分布式系统,不可能同时满足以下三个特性,最多只能同时满足两个:

  • 一致性(Consistency):所有节点在同一时间看到的数据是一样的
  • 可用性(Availability):每个请求都能得到响应(不一定是最新的数据)
  • 分区容错性(Partition tolerance):系统因为网络问题分成了几个区,系统还能继续工作

为什么不能同时满足三个?我给你举个例子:

假设你有两个节点,A和B,它们都存了同一份数据。

现在网络断了,A和B之间没法通信了(这就是"分区")。

这时候,如果有用户往A写了新数据,A这边更新了,但B不知道。

现在有用户从B读数据——

  • 如果你要保证一致性,那B就不能返回数据,因为它的数据不是最新的——这样就牺牲了可用性
  • 如果你要保证可用性,那B就返回它自己的数据,但这数据不是最新的——这样就牺牲了一致性

看到了吧?分区发生的时候,你要么选一致性,要么选可用性,不能都要。

1.6.2 K8s是怎么取舍的? ​

K8s的核心数据存在etcd里。etcd是什么?它是一个分布式键值存储,基于Raft共识算法。

etcd属于CP系统——它优先保证一致性,牺牲可用性。

为什么?因为etcd存的是集群的"期望状态",这个数据必须是一致的,不然各个组件看到的状态不一样,就乱套了。

那什么时候不可用?比如etcd集群有3个节点,挂了2个,剩下1个,这时候没法达成共识了,etcd就不能写了——这时候集群就不能做变更了,但已经跑着的Pod还能继续跑。

这是合理的取舍——控制平面的一致性,比可用性更重要。

1.6.3 最终一致性是什么? ​

那有没有办法,既要有可用性,又要尽量一致?有,那就是最终一致性。

最终一致性的意思是:我不保证你立刻读到最新的数据,但我保证,过一段时间后,数据最终会变成一致的。

很多分布式系统都是最终一致性的——比如DNS、比如Amazon的DynamoDB、比如很多微服务架构。

K8s里有没有最终一致性?当然有。比如你创建一个Deployment,期望有3个副本。apiserver把这个状态存到etcd里了,但这时候Pod可能还没创建出来——当前状态跟期望状态不一致。但控制器会不断地努力,最终会把3个Pod都创建出来。

这就是一种最终一致性——最终,系统会达到你期望的状态。

理解了CAP和最终一致性,你就能更好地理解K8s的设计——为什么它是声明式的?为什么它是控制循环?因为它就是一个最终一致性的系统,不断地朝着期望状态收敛。

1.7 本章小结 ​

这一章我们从历史讲起,梳理了从物理机到虚拟机到容器的演进过程,然后讲了容器带来的新问题——也就是容器编排要解决的问题。

然后我们回顾了容器编排"三国杀"时代的三个玩家:Docker Swarm、Mesos、K8s,分析了为什么K8s最后赢了——技术上的先进性、生态上的网络效应、社区上的众望所归。

最后我们讲了K8s的精神祖先Borg,以及分布式系统的CAP定理。

这一章的目的,不是让你记住多少历史事件,而是让你建立一个大局观——

  • K8s不是凭空出现的,它是技术发展的必然产物
  • K8s的设计不是拍脑袋的,它是十几年经验的沉淀
  • 学习K8s,不只是学一个工具,而是学一种分布式系统的设计思想

理解了这些,你再去学K8s的具体概念,就不会觉得"为什么要这么设计"了——因为你知道它要解决什么问题。

下一章,我们就正式进入K8s的世界,看看它的架构到底是什么样的。


第2章 K8s架构全景图:上帝视角看集群 ​

学K8s的第一个门槛,就是它的组件太多了——etcd、apiserver、scheduler、controller-manager、kubelet、kube-proxy、containerd……光听名字就头大。

这一章,我们就从上帝视角,把K8s的架构彻底讲清楚。你会发现,这些组件看似复杂,其实分工很明确,就像一个公司的各个部门,各司其职,协同工作。

2.1 整体架构:控制平面与数据平面 ​

K8s集群从大的层面分,就两部分:

  • 控制平面(Control Plane):集群的"大脑",负责管理整个集群——做决策、调度、监控、响应事件
  • 数据平面(Data Plane):集群的"手脚",负责具体干活——跑容器、处理网络、提供存储

打个比方:

  • 控制平面就像公司的总部——CEO、CTO、HR、财务……他们不直接做业务,但管理整个公司的运转
  • 数据平面就像各个业务部门——销售、研发、生产……他们直接干活,产出价值

控制平面的组件可以跑在任意节点上,但生产环境一般会专门用几台机器跑控制平面,跟工作节点分开,保证稳定性。

2.1.1 控制平面组件 ​

控制平面有四个核心组件:

  1. kube-apiserver:所有请求的唯一入口,集群的"前台"
  2. etcd:集群的数据库,存所有集群数据,"记忆中枢"
  3. kube-scheduler:调度器,决定Pod放哪台机器上,"调度大脑"
  4. kube-controller-manager:控制器管理器,跑各种控制器,"控制循环"

还有一个可选的:

  • cloud-controller-manager:跟云厂商对接的控制器,用云服务的时候才需要

2.1.2 数据平面组件 ​

每个工作节点(Worker Node)上,都有这些组件:

  1. kubelet:节点上的"管家",跟控制平面通信,管理本节点的Pod
  2. kube-proxy:节点上的"网络管理员",负责Service的网络规则
  3. 容器运行时(Container Runtime):真正跑容器的东西,比如containerd、CRI-O

还有一些附加组件(Addons),不是必须的,但一般都会装:

  • CoreDNS:集群内的DNS服务,服务发现用的
  • Ingress Controller:七层流量入口
  • Metrics Server:集群指标采集
  • Prometheus:监控告警
  • ……

好,有了整体概念,我们一个个来讲。

2.2 etcd:集群的"记忆中枢" ​

2.2.1 etcd是什么? ​

etcd是一个分布式的、一致的键值存储。简单说,它就是K8s集群的数据库——所有集群数据都存在etcd里。

什么数据?

  • 有哪些节点
  • 有哪些Pod、Deployment、Service
  • 各种配置
  • 密钥
  • ……

总之,K8s集群的所有状态,都存在etcd里。etcd里的数据就是集群的"唯一事实来源"(Single Source of Truth)。

2.2.2 为什么选etcd? ​

K8s为什么用etcd,不用MySQL、Redis什么的?因为etcd有几个特点,特别适合做K8s的存储:

  1. 强一致性:etcd基于Raft共识算法,能保证数据的强一致性。这对K8s太重要了——所有组件看到的状态必须是一样的,不然就乱套了。

  2. 高可用:etcd可以部署成集群,只要大部分节点活着,就能正常工作。

  3. Watch机制:etcd支持Watch——你可以监听某个key的变化,一旦变化就会收到通知。这对K8s的控制器模式太重要了——控制器不用轮询,等着通知就行,效率高。

  4. 简单:就是个键值存储,API简单,性能好。

  5. Go语言写的:跟K8s技术栈一致,集成方便。

2.2.3 etcd的工作原理:Raft共识算法 ​

etcd的一致性靠Raft算法保证。Raft是什么?它是一种分布式共识算法——就是让分布式系统里的各个节点,对同一件事达成一致的算法。

Raft的核心思想是领导者(Leader):

  • etcd集群里有一个Leader节点,其他是Follower节点
  • 所有写请求都由Leader处理,Leader把数据同步给Follower
  • 只要大部分节点(半数以上)都确认收到了,这次写入就算成功了
  • 如果Leader挂了,Follower会自动选举出新的Leader

为什么是奇数个节点?因为要"大多数"——3个节点最多挂1个,5个节点最多挂2个。如果是4个节点,最多也只能挂1个(因为挂2个就剩2个,不到一半了)——所以奇数个节点性价比最高。

这就是为什么生产环境etcd一般部署3个、5个、7个节点——奇数个。

2.2.4 生产环境注意事项 ​

etcd是集群的命根子——etcd挂了,整个集群就废了。所以生产环境用etcd,一定要注意:

  1. 一定要高可用:至少3个节点,别用单节点
  2. 磁盘性能很重要:etcd对磁盘IO延迟非常敏感,一定要用SSD,别用机械硬盘
  3. 要定期备份:etcd的数据要定期备份,万一挂了还能恢复
  4. 别直接改etcd的数据:所有变更都应该通过apiserver做,别直接操作etcd——容易把数据搞坏

面试常考点:etcd用的什么共识算法?Raft。为什么etcd节点数是奇数?因为要多数派,奇数个性价比最高。

2.3 kube-apiserver:所有请求的唯一入口 ​

2.3.1 apiserver是干什么的? ​

kube-apiserver是控制平面的"前台",是所有请求的唯一入口。

不管你是用kubectl命令,还是控制器、调度器、kubelet要读写集群数据——所有请求都要经过apiserver。

为什么要搞个统一入口?直接让各个组件去读写etcd不行吗?

不行,因为apiserver做了很多重要的事情:

  1. 认证(Authentication):你是谁?验证你的身份
  2. 授权(Authorization):你能干什么?你有没有权限做这个操作
  3. 准入控制(Admission Control):这个请求合不合规?比如能不能用特权容器、资源限制够不够
  4. API注册与发现:有哪些API、哪些资源类型,都由apiserver管理
  5. CRUD操作:对etcd里的数据进行增删改查
  6. Watch机制:客户端可以Watch资源变化,apiserver会推送通知

你看,apiserver就像一个公司的前台+保安+行政——所有人进来都要经过它,它检查你身份、检查你权限、登记你的请求,然后再帮你办事。

2.3.2 声明式API:K8s的灵魂 ​

apiserver最重要的设计,就是声明式API。

什么是声明式?什么是命令式?

  • 命令式:你告诉系统"做什么"——"给我启动3个容器"、"把镜像换成v2"
  • 声明式:你告诉系统"我要什么状态"——"我要这个应用有3个副本,用v2版本的镜像",然后系统自己想办法达到这个状态

别小看这一点区别,这是本质上的不同。

声明式API意味着:

  • 系统是自愈的:如果当前状态跟期望状态不一样,系统会自动调整
  • 配置是可版本化的:期望状态就是YAML文件,可以用Git管理,想回滚就回滚
  • 操作是幂等的:同一个配置apply多少次,结果都一样
  • 扩展性好:你可以自定义资源类型(CRD),让K8s管理你自己的东西

这就是K8s的灵魂——声明式 + 控制循环。你定义期望状态,控制器不断地把当前状态往期望状态靠拢。

2.3.3 apiserver的工作流程 ​

一个请求从发出来到完成,经过哪些步骤?

  1. 认证:验证请求者的身份(证书、token、用户名密码等)
  2. 授权:验证这个身份有没有权限做这个操作(RBAC)
  3. 准入控制(Mutating):修改请求——比如给Pod加默认的资源限制、注入Sidecar
  4. 准入控制(Validating):验证请求合不合规——比如资源限制有没有超过配额、格式对不对
  5. 写入etcd:验证都通过了,把数据写到etcd里
  6. 返回结果:告诉客户端操作成功

你看,一个请求要经过这么多关卡,才能真正写入集群。这就是为什么K8s的API这么安全、这么灵活。

2.4 kube-scheduler:调度决策的"大脑" ​

2.4.1 调度器是干什么的? ​

kube-scheduler,顾名思义,就是负责调度的——决定新创建的Pod应该放到哪台节点上。

听起来很简单?不,这事儿可复杂了。

假设你有100台节点,要创建一个Pod。你怎么选?

  • 首先,哪些节点有足够的资源(CPU、内存)跑这个Pod?
  • 然后,这些节点里,哪个最合适?
  • 要不要考虑亲和性?比如这个Pod要跟另一个Pod放一起,或者不能放一起
  • 要不要考虑污点?有些节点是专门给特定Pod用的
  • 要不要考虑数据局部性?比如Pod需要的数据在某台节点上,最好就放那台
  • 怎么平衡各个节点的负载?别让有的节点闲死、有的忙死

这就是调度器要解决的问题。

2.4.2 调度流程:Filter → Score → Bind ​

调度的过程分三步:

第一步:预选(Filter) 先把所有不符合条件的节点过滤掉。

  • 资源不够的?去掉
  • 有污点但Pod不能容忍的?去掉
  • 节点选择器不匹配的?去掉
  • 端口冲突的?去掉
  • ……

预选之后,剩下的就是"有资格"运行这个Pod的节点。

第二步:优选(Score) 在合格的节点里,给每个节点打分,选出分数最高的。

  • 资源利用率怎么样?越均衡分越高
  • 有没有亲和性匹配?匹配的话加分
  • 是不是已经有同应用的Pod了?如果是反亲和的话减分
  • 数据局部性怎么样?有需要的数据加分
  • ……

第三步:绑定(Bind) 分数最高的节点,就是最终的选择。调度器把Pod跟这个节点绑定——也就是告诉apiserver:"这个Pod就放这台节点了"。

然后kubelet就会在那台节点上启动这个Pod。

调度器的设计很巧妙,它是可插拔的——你可以自己写调度插件,自定义调度策略。甚至你可以跑多个调度器,不同的Pod用不同的调度器。

2.4.3 调度器的扩展性 ​

K8s的调度器不是一个黑盒,它是高度可扩展的。

调度器框架(Scheduler Framework)定义了很多扩展点:

  • PreFilter:预选前的预处理
  • Filter:预选
  • PreScore:优选前的预处理
  • Score:优选
  • Reserve:预留资源
  • Permit:批准调度
  • PreBind:绑定前的操作
  • Bind:绑定
  • PostBind:绑定后的操作
  • ……

你可以在每个扩展点插入自己的逻辑,实现自定义的调度策略。

比如:

  • 你想让Pod优先放到有GPU的节点?写个Score插件
  • 你想让同一应用的Pod分散到不同机架?写个Filter插件
  • 你想支持自定义资源(比如FPGA)?写个扩展

这就是K8s的强大之处——它不是一个成品,而是一个平台,你可以在上面构建自己的东西。

2.5 kube-controller-manager:永不休息的"控制循环" ​

2.5.1 控制器是什么? ​

kube-controller-manager,控制器管理器——它里面跑了一大堆控制器。

什么是控制器?控制器就是一个控制循环:

  • 不断地观察系统的当前状态
  • 跟期望状态对比
  • 如果不一样,就做调整,让当前状态趋近于期望状态

这就是K8s的核心工作模式——控制循环(Control Loop)。

打个比方:

  • 期望状态就是你设定的空调温度(比如26度)
  • 当前状态就是房间的实际温度
  • 控制器就是空调的温控器——温度高了就制冷,温度低了就制热,一直保持在你设定的温度

K8s里有各种各样的控制器,每个控制器负责一种资源:

  • Deployment控制器:管理Deployment,保证副本数正确
  • ReplicaSet控制器:管理ReplicaSet
  • StatefulSet控制器:管理StatefulSet
  • DaemonSet控制器:管理DaemonSet
  • Job控制器:管理Job
  • Node控制器:管理节点,节点挂了做处理
  • Endpoint控制器:管理Endpoint,Service跟Pod关联
  • Namespace控制器:管理命名空间
  • ServiceAccount控制器:管理服务账号
  • ……

数都数不过来,K8s里有几十种控制器。

2.5.2 控制器是怎么工作的? ​

所有控制器的工作模式都一样:

  1. Watch:通过apiserver的Watch机制,监听它负责的资源的变化
  2. 对比:拿到当前状态,跟期望状态对比
  3. 调整:如果不一致,就做调整(创建、删除、更新资源)
  4. 循环:不断重复上面的步骤

这个模式有什么好处?

  • 自愈:出了问题自动修,不用人工干预
  • 最终一致:不管中间出什么问题,最终都会达到期望状态
  • 解耦:每个控制器只负责自己的事儿,互不影响
  • 可扩展:你可以自己写控制器,管理自己的资源

2.5.3 为什么叫"控制器管理器"? ​

因为有这么多控制器,不可能每个都单独跑一个进程——太浪费资源了。所以把它们都打包到一个进程里,就是kube-controller-manager。

这样既节省资源,又方便管理。

还有一个cloud-controller-manager,是跟云厂商对接的——比如你用阿里云的K8s,要创建LoadBalancer类型的Service,就需要cloud-controller-manager去调用阿里云的API,创建负载均衡器。

用云厂商托管的K8s,这个组件一般是云厂商帮你管的,你看不到。

2.6 kubelet:每个节点上的"管家" ​

讲完了控制平面,我们来讲数据平面的组件。第一个就是kubelet。

2.6.1 kubelet是干什么的? ​

kubelet是每个节点上的"管家",它是控制平面在节点上的代理人。

它的主要工作:

  1. 跟apiserver通信:注册节点,上报节点状态,接收任务
  2. 管理Pod生命周期:创建、启动、停止、重启Pod
  3. 监控Pod状态:监控Pod的健康状态,上报给apiserver
  4. 执行探针:执行Liveness、Readiness探针
  5. 管理存储卷:挂载Volume,处理存储相关的事儿
  6. 上报指标:上报节点和Pod的资源使用情况

简单说,控制平面下达的命令,到了节点上,都是kubelet来执行的。

2.6.2 kubelet是怎么创建Pod的? ​

Pod的创建流程大概是这样的:

  1. 用户通过apiserver创建Pod
  2. 调度器给Pod分配节点
  3. apiserver更新Pod的NodeName字段
  4. 对应节点上的kubelet Watch到有新的Pod分配到自己这了
  5. kubelet告诉容器运行时:"给我创建这些容器"
  6. 容器运行时拉取镜像、创建容器、启动容器
  7. kubelet监控容器状态,上报给apiserver

你看,kubelet不直接跑容器,它是告诉容器运行时去跑。

2.6.3 Pod清单(Pod Manifest) ​

kubelet怎么知道要跑哪些Pod?

主要有两个来源:

  1. 从apiserver来的:这是主要来源,就是我们正常创建的Pod
  2. 本地静态Pod(Static Pod):kubelet会监控本地的一个目录,目录里的YAML文件定义的Pod,就是静态Pod

静态Pod有什么用?

  • 控制平面的组件(apiserver、scheduler、controller-manager),很多时候就是以静态Pod的方式跑的——因为控制平面自己还没起来的时候,没法通过apiserver创建Pod啊
  • 所以kubeadm部署的集群,Master节点上的控制平面组件,都是静态Pod

这是个很巧妙的设计——用K8s的方式来跑K8s自己。

2.7 kube-proxy:服务网络的"交通警察" ​

2.7.1 kube-proxy是干什么的? ​

kube-proxy是每个节点上的"网络管理员",负责实现Service的网络规则。

什么意思?Service是K8s里的服务发现和负载均衡机制——一个Service对应一组Pod,访问Service的IP,就会被负载均衡到后端的Pod上。

那这个负载均衡是怎么实现的?就是kube-proxy干的。

kube-proxy在每个节点上,监听Service和Endpoint的变化,然后在节点上配置相应的网络规则(iptables、IPVS等),让访问Service的流量能正确地转发到后端Pod。

2.7.2 kube-proxy的三种模式 ​

kube-proxy有三种工作模式:

1. userspace模式(最老的,已经不用了) 最早的模式,kube-proxy自己在用户空间做代理——流量先到kube-proxy进程,再由kube-proxy转发到Pod。

  • 缺点:性能差,因为要经过用户态内核态切换,还有额外的网络开销
  • 现在基本没人用了

2. iptables模式(目前最常用的) kube-proxy通过配置iptables规则来实现Service的负载均衡。

  • 流量直接由内核的iptables处理,不经过kube-proxy进程
  • 性能比userspace模式好很多
  • 缺点:Service太多的话,iptables规则会非常多,匹配效率下降,更新也慢

3. IPVS模式(性能最好的) IPVS(IP Virtual Server)是Linux内核里专门做负载均衡的模块,性能比iptables好很多。

  • 支持更多的负载均衡算法(轮询、加权轮询、最小连接、源地址哈希……)
  • 性能更好,特别是Service数量多的时候
  • 缺点:配置稍微复杂一点,内核要加载IPVS模块

生产环境如果集群规模大、Service多,建议用IPVS模式。

面试常考点:kube-proxy有哪几种模式?区别是什么?iptables和IPVS哪个性能好?为什么?

2.8 容器运行时:从Docker到containerd ​

2.8.1 容器运行时是什么? ​

容器运行时(Container Runtime),就是真正负责跑容器的软件——拉取镜像、创建容器、启动容器、管理容器生命周期……

早期K8s用的是Docker作为容器运行时。后来K8s推出了CRI(Container Runtime Interface,容器运行时接口)标准,只要实现了CRI接口,就能作为K8s的容器运行时。

现在主流的容器运行时:

  • containerd:Docker公司捐给CNCF的,现在是最主流的容器运行时
  • CRI-O:Red Hat搞的,轻量级的K8s容器运行时
  • Docker:早期的默认选项,现在K8s已经弃用了dockershim

2.8.2 为什么不用Docker了? ​

很多人刚听到K8s弃用Docker的时候都很惊讶——Docker不是跟K8s绑定的吗?

其实这里有个误会。Docker不是一个东西,它是一堆东西的集合:

  • Docker CLI:命令行工具
  • Docker daemon:后台守护进程
  • containerd:真正管理容器的运行时
  • runc:真正创建容器的底层工具

K8s需要的只是容器运行时——也就是containerd那部分。Docker那套(daemon、CLI、build、volume、network……)对K8s来说都是多余的。

而且,Docker不实现CRI接口——K8s为了兼容Docker,专门搞了个dockershim,在中间做转换。这增加了复杂度,也增加了维护成本。

所以K8s后来就弃用了dockershim,直接用containerd作为容器运行时——反正Docker底层用的也是containerd。

那我们平时用Docker构建的镜像,还能用吗?当然能!因为镜像格式是标准的(OCI标准),containerd也能跑。

一句话总结:K8s弃用的是Docker作为容器运行时,不是弃用Docker镜像。Docker镜像还是可以正常用的。

2.8.3 OCI标准 ​

说到容器,就不得不提OCI(Open Container Initiative,开放容器倡议)。

OCI定义了两个标准:

  1. 镜像标准(Image Specification):容器镜像应该是什么格式
  2. 运行时标准(Runtime Specification):容器运行时应该怎么工作

有了标准,大家就不用各自为政了——你用Docker build的镜像,我用containerd也能跑;你用CRI-O运行时,也能跑OCI标准的镜像。

这就是开源的好处——标准先行,生态繁荣。

2.9 扩展知识:声明式API与控制循环模式 ​

前面我们多次提到了"声明式API"和"控制循环",我觉得有必要展开讲讲——因为这是K8s最核心的设计思想,理解了它,你就理解了K8s的本质。

2.9.1 命令式 vs 声明式 ​

我们再深入对比一下命令式和声明式:

命令式编程:

  • 你告诉计算机"怎么做"
  • 你写的是步骤:第一步做什么,第二步做什么
  • 例子:Ansible脚本、Shell脚本、Dockerfile
  • 优点:直观,容易理解
  • 缺点:复杂场景下代码量很大,不容易维护,状态管理麻烦

声明式编程:

  • 你告诉计算机"要什么结果"
  • 你写的是目标状态,具体怎么达到这个状态,由系统自己决定
  • 例子:SQL、HTML、K8s YAML
  • 优点:简洁,关注点分离(你只管要什么,系统管怎么实现),容易做状态管理
  • 缺点:学习曲线陡,底层复杂

K8s选择声明式,是因为——在分布式系统里,状态管理是最难的。

如果是命令式,你发了个"创建3个副本"的命令,执行到一半网络断了,现在有几个副本?2个?还是2.5个?你不知道。系统当前状态是什么?你也不知道。下次再执行,是接着来还是从头来?

而声明式呢?你不管中间过程,你只知道你要3个副本。系统不管中间出了什么问题,它都会不断地调整,最终达到3个副本。

这就是最终一致性——我不保证过程,但我保证最终结果。

2.9.2 控制循环:K8s的发动机 ​

控制循环(Control Loop)是实现声明式的机制。

控制循环的逻辑非常简单:

while True:
    当前状态 = 获取当前状态()
    期望状态 = 获取期望状态()
    if 当前状态 == 期望状态:
        继续等待
    else:
        执行调整操作,让当前状态趋近期望状态

就这么简单的逻辑,但是威力巨大。

为什么?因为它是收敛的——不管当前状态离期望状态有多远,只要不断地调整,最终总会达到期望状态。

而且,这个模式是可组合的——你可以有很多个控制循环,每个管自己的一摊事儿,互不干扰。

K8s就是由一大堆控制循环组成的:

  • Deployment控制器管Deployment的副本数
  • ReplicaSet控制器管ReplicaSet
  • Node控制器管节点状态
  • Endpoint控制器管Service和Pod的关联
  • ……

每个控制器都只做一件事,都只跟apiserver交互,互相之间不直接通信。这就是关注点分离,是非常优雅的设计。

2.9.3 为什么这个模式这么强大? ​

这个模式为什么这么牛?我觉得有几个原因:

  1. 自愈能力强:出了问题自动修,不用人管。机器挂了?没关系,控制器会把Pod调度到别的机器上。Pod挂了?没关系,控制器会重启它。

  2. 容错性好:控制器偶尔挂了没关系,重启了接着来——反正它只看当前状态和期望状态,不管之前发生了什么。

  3. 可扩展性好:想加新功能?写个新控制器就行,不用改现有代码。想管理新资源?定义个CRD,写个控制器就行。

  4. 易于推理:系统的行为是可预测的——它总是朝着期望状态走。你不用担心中间状态,只要保证期望状态是对的就行。

理解了声明式API和控制循环,你就理解了K8s的灵魂。后面所有的概念,都是在这个基础上延伸出来的。

2.10 本章小结 ​

这一章我们从整体到局部,把K8s的架构讲了一遍。

总结一下:

  • K8s分为控制平面和数据平面
  • 控制平面有四大组件:etcd(存储)、apiserver(API入口)、scheduler(调度)、controller-manager(控制器)
  • 数据平面每个节点有三个组件:kubelet(节点管家)、kube-proxy(网络)、容器运行时(跑容器)
  • K8s的核心设计思想是声明式API + 控制循环

学架构不要死记硬背每个组件是干什么的,要理解它们为什么存在、解决什么问题、怎么协同工作。

下一章,我们来讲K8s里最核心、最基础的概念——Pod。


第3章 Pod:K8s世界的原子单位 ​

学K8s,第一个要搞懂的概念就是Pod。很多初学者会问:"我们已经有容器了,为什么还要搞个Pod出来?直接用容器不行吗?"

这是个好问题。搞懂了这个问题,你就理解了K8s设计的精髓。

3.1 为什么是Pod?——从容器到Pod的设计哲学 ​

3.1.1 容器的"单进程"模型 ​

Docker容器的设计哲学是"一个容器一个进程"——每个容器只跑一个进程(或者说一个应用)。

这个设计有很多好处:

  • 职责单一,容易管理
  • 日志好收集,每个容器的日志就是那个进程的输出
  • 资源隔离好,每个应用的资源使用是独立的
  • 扩缩容方便,哪个应用需要扩容就加哪个容器

但是,真实世界里的应用,真的都是单进程的吗?

不是的。很多应用都是由多个紧密协作的进程组成的。

举几个例子:

  • 一个Web应用,主进程跑业务,还有个日志收集进程帮它收集日志、转发到日志系统
  • 一个数据库,主进程跑数据库,还有个备份进程定期做备份
  • 一个应用,主进程跑业务,还有个代理进程(Envoy、Linkerd)帮它处理网络流量、做mTLS
  • 一个应用,主进程跑业务,还有个初始化进程,启动前先做一些准备工作(比如加载配置、初始化数据)

这些进程有什么特点?

  • 它们是紧密耦合的——必须一起运行,少一个都不行
  • 它们需要共享资源——共享网络、共享磁盘、共享IPC
  • 它们需要一起调度——必须放在同一台机器上
  • 它们的生命周期是一致的——一起启动,一起停止

如果只有容器,你怎么处理这种情况?

你可以把多个进程塞到一个容器里——但这样就破坏了容器的单进程模型,日志、资源管理、监控都变得很麻烦。

你也可以用两个容器——但怎么保证它们总是在同一台机器上?怎么共享网络和存储?怎么协调生命周期?

这就是Pod要解决的问题。

3.1.2 Pod的本质:共享上下文的容器组 ​

Pod是什么?Pod是一组容器的集合,这些容器共享网络、存储等资源,作为一个整体被调度和管理。

打个比方:

  • 容器就像一个"单间",里面住一个人(进程)
  • Pod就像一个"套间",里面住好几个人(多个容器),他们共享客厅、厨房、卫生间(共享网络、存储等资源)

Pod里的容器:

  • 共享同一个网络命名空间——它们的IP地址、端口空间是一样的,localhost就能互相访问
  • 共享存储卷——Volume可以被Pod里的所有容器挂载
  • 共享IPC命名空间——可以用System V IPC或者POSIX消息队列通信
  • 共享UTS命名空间——主机名一样
  • 一起被调度——整个Pod作为一个调度单元,要么都在这台机器,要么都在那台机器
  • 生命周期一致——一起创建,一起销毁

这就是Pod的设计哲学——把紧密耦合的一组容器,作为一个整体来管理。

3.1.3 为什么不直接在容器里跑多个进程? ​

有人可能会说:"我在一个容器里跑多个进程不就行了?搞Pod这么麻烦。"

把多个进程塞到一个容器里,有很多问题:

  1. 日志混乱:多个进程的日志都混在一起,很难区分。Docker的日志收集是收集容器的stdout/stderr,多个进程的输出混在一起,你怎么分?

  2. 资源管理困难:你没法单独给某个进程限制资源,只能给整个容器限制。如果某个进程内存泄漏了,会把整个容器的内存吃光,影响其他进程。

  3. 监控困难:你没法单独监控某个进程的状态,只能监控整个容器。哪个进程挂了?你不知道。

  4. 生命周期管理困难:一个进程挂了,其他进程怎么办?是整个容器重启还是怎样?容器的重启策略是针对整个容器的,不是单个进程。

  5. 职责不清:容器的设计哲学就是单进程,你硬要塞多个,就违背了这个设计,各种问题都会来。

而Pod的方式就优雅多了——每个容器还是单进程,保持了容器的优点;同时又通过Pod把它们组织在一起,共享资源、一起调度。

这就是"分而治之"的智慧——既能分开,又能合在一起。

3.2 容器设计模式:Pod的正确打开方式 ​

Pod不是随便把几个容器塞在一起就行的,它有一些经典的使用模式。这些模式是Google十几年运维经验的总结,非常有用。

3.2.1 Sidecar模式(边车模式) ​

这是最常用的模式。主容器跑业务,旁边有个Sidecar容器辅助它。

就像摩托车的边车——主车是主要的,边车辅助它,坐个乘客或者放点东西。

常见的Sidecar:

  • 日志收集Sidecar:主容器把日志写到文件,Sidecar容器收集日志、转发到日志系统(比如Fluentd、Filebeat)
  • 代理Sidecar:比如Istio的Envoy代理,帮主容器处理网络流量、做mTLS、做负载均衡、做监控
  • 配置同步Sidecar:帮主容器同步配置,配置变了自动更新
  • 加密Sidecar:帮主容器加解密数据

为什么不直接把这些功能做到主容器里?

  • 解耦——业务就是业务,跟日志、代理这些无关
  • 复用——同一个Sidecar可以给很多不同的应用用
  • 独立升级——Sidecar可以单独升级,不用动业务应用
  • 不同语言的应用都能用——Sidecar跟业务语言无关

这就是Sidecar模式的好处——关注点分离,复用性强。

3.2.2 Ambassador模式(大使模式) ​

Ambassador(大使)模式,就是用一个代理容器,代表主容器跟外部通信。

比如:

  • 主容器要访问数据库,但数据库有多个副本(主库、从库),你用一个Ambassador容器来做数据库代理,主容器只需要连localhost,Ambassador帮你路由到正确的数据库
  • 主容器要访问外部服务,但服务发现很复杂,你用Ambassador容器来做服务发现和负载均衡
  • 主容器要访问缓存,你用Ambassador容器来做缓存代理,主容器只需要简单地访问,Ambassador帮你处理缓存逻辑

为什么叫"大使"?因为大使代表国家跟外国打交道——Ambassador容器代表主容器跟外部世界打交道。

跟Sidecar有什么区别?其实Ambassador是Sidecar的一种特殊情况——Sidecar更通用,Ambassador特指做网络代理的Sidecar。

3.2.3 Adapter模式(适配器模式) ​

Adapter(适配器)模式,就是把主容器的输出,转换成标准格式。

比如:

  • 不同应用的日志格式不一样,你用Adapter容器把它们都转换成统一的格式,再发给日志系统
  • 不同应用的监控指标格式不一样,你用Adapter容器把它们都转换成Prometheus格式
  • 不同应用的健康检查接口不一样,你用Adapter容器统一成标准的健康检查接口

为什么叫"适配器"?就像电源适配器——把不同的电压转换成标准的电压,你的电器就能用了。

Adapter模式的好处是——标准化。不管你是什么应用,经过Adapter之后,输出都是标准格式,后面的系统(日志、监控)就不用管你是什么应用了,统一处理就行。

3.2.4 Init Container模式(初始化容器模式) ​

前面三个都是跟主容器一起运行的,Init Container不一样——它是在主容器启动之前运行的,做完自己的事儿就退出。

Init Container的作用是做初始化工作:

  • 等待依赖的服务就绪——比如主容器依赖数据库,Init Container就等数据库启动好了再退出,然后主容器再启动
  • 初始化配置——比如从配置中心拉配置、生成配置文件
  • 初始化数据——比如下载数据、预热缓存
  • 做一些权限相关的准备工作

Init Container有什么特点?

  • 按顺序执行——多个Init Container的话,一个执行完了下一个才开始
  • 必须都执行成功——有一个失败,Pod就不会启动
  • 执行完就退出——不跟主容器一起运行
  • 不接受探针——因为它是一次性的

为什么不把初始化逻辑做到主容器的启动脚本里?

  • 解耦——初始化逻辑跟业务逻辑分开
  • 镜像可以复用——业务镜像不用包含初始化的工具
  • 安全——初始化的时候可以用更高的权限,主容器运行的时候用低权限
  • 可以有多个初始化步骤,每个步骤一个容器,职责清晰

这四种模式是Pod最经典的使用方式。掌握了这些,你就知道什么时候该用多容器Pod了。

面试常考点:Pod里有哪几种容器设计模式?Sidecar、Ambassador、Adapter、Init Container,分别是干什么的?

3.3 Pod的生命周期:从出生到死亡 ​

Pod不是一成不变的,它有自己的生命周期。理解Pod的生命周期,对排查问题非常重要。

3.3.1 Pod的状态 ​

Pod有几种主要的状态(Phase):

  • Pending:Pod已经创建了,但还没调度到节点上,或者镜像还在拉取中
  • Running:Pod已经调度到节点上了,所有容器都创建了,至少有一个容器在运行
  • Succeeded:Pod里所有容器都正常退出了(退出码0),而且不会再重启了
  • Failed:Pod里所有容器都退出了,至少有一个容器是异常退出的(退出码非0)
  • Unknown:不知道Pod是什么状态——一般是节点失联了,apiserver联系不上kubelet

这些状态是Pod的"大状态",但具体到每个容器,还有更细的状态。

3.3.2 容器的状态 ​

每个容器有三种状态:

  • Waiting:等待中——比如正在拉镜像、正在等待Init Container完成
  • Running:运行中——容器正常运行
  • Terminated:终止了——容器退出了

容器终止的时候,会有退出码,你可以通过退出码判断容器是正常退出还是异常退出。

3.3.3 Pod的创建过程 ​

一个Pod从创建到Running,经历了什么?

  1. 用户创建Pod:通过apiserver提交Pod定义
  2. apiserver写入etcd:验证通过后,把Pod数据写入etcd
  3. 调度器调度:scheduler Watch到新的Pod,给它分配节点,更新Pod的NodeName
  4. kubelet接收任务:对应节点的kubelet Watch到有新的Pod分配到自己这了
  5. 执行Init Container:按顺序执行所有Init Container,都成功了才继续
  6. 创建主容器:创建所有主容器的容器
  7. 启动容器:启动容器,开始运行
  8. 执行postStart钩子:容器启动后,执行postStart钩子(如果有的话)
  9. Running状态:所有容器都启动了,Pod进入Running状态

这个过程中,如果哪一步出了问题,Pod就会卡在对应的状态。排查Pod问题的时候,你就可以按这个流程一步步查。

3.3.4 Pod的终止过程 ​

Pod的终止过程也很重要,很多人都搞不懂为什么Pod删了半天还在。

  1. 用户删除Pod:apiserver收到删除请求,把Pod的删除时间戳设上
  2. kubelet收到删除通知:开始终止Pod
  3. 执行preStop钩子:给容器发SIGTERM之前,先执行preStop钩子(如果有的话)
  4. 发送SIGTERM信号:给容器里的进程发SIGTERM信号,告诉它"该退出了"
  5. 等待优雅终止:等待terminationGracePeriodSeconds这么长时间,默认30秒
  6. 发送SIGKILL信号:如果到时间容器还没退出,就发SIGKILL强杀
  7. 删除Pod:所有容器都退出了,kubelet告诉apiserver删除Pod

这里有个很重要的点——你的应用要能正确处理SIGTERM信号,实现优雅退出。

很多应用收到SIGTERM就直接挂了,请求处理到一半就断了,用户体验很差。正确的做法是:收到SIGTERM后,停止接收新请求,把正在处理的请求处理完,然后再退出。

这就是优雅终止(Graceful Shutdown)。

3.4 探针机制:怎么知道Pod健康不健康? ​

Pod跑起来了,不代表它就是健康的。比如:

  • 应用启动了,但还没初始化完,这时候还不能接流量
  • 应用跑着跑着死锁了,进程还在,但已经不响应请求了
  • 应用内存泄漏,快OOM了

这时候就需要探针(Probe)——定期检查容器的健康状态。

K8s有三种探针:

3.4.1 Liveness Probe(存活探针) ​

存活探针是用来判断容器是不是还活着的。

如果存活探针失败了,kubelet就会杀掉容器,然后根据重启策略决定要不要重启。

什么时候用存活探针?

  • 应用可能会死锁、挂死——进程还在,但已经不工作了
  • 这时候重启一下可能就好了

存活探针就是——"你还活着吗?不说话我就当你死了,重启你啊"。

3.4.2 Readiness Probe(就绪探针) ​

就绪探针是用来判断容器是不是准备好了,可以接流量了。

如果就绪探针失败了,Service就会把这个Pod从后端列表里摘掉,不再把流量发给它。但容器不会被杀掉,还会继续跑。

什么时候用就绪探针?

  • 应用启动需要时间——启动过程中不能接流量
  • 应用运行过程中可能会暂时不可用——比如加载数据、缓存失效,这时候不能接流量,但过一会儿就好了
  • 这时候你不想杀它,只是不想给它发流量

就绪探针就是——"你准备好了吗?没准备好我就先不把流量发给你"。

3.4.3 Startup Probe(启动探针) ​

启动探针是用来判断容器是不是启动完成了。

启动探针成功之前,存活探针和就绪探针都不会生效。

为什么需要启动探针?因为有些应用启动很慢——比如Java应用,启动可能要一两分钟。如果你用存活探针,initialDelaySeconds设短了,应用还没启动完探针就失败了,然后就被重启了,陷入死循环;设长了,运行时如果真挂了,要等很久才能发现。

启动探针就解决了这个问题——启动的时候用启动探针,给它足够长的时间;启动成功后,就用存活探针,这时候超时时间可以设短一点,能快速发现运行时的故障。

三种探针的区别:

  • Liveness:活着吗?不活就重启
  • Readiness:准备好了吗?没准备好就不给流量
  • Startup:启动完了吗?没启动完其他探针都不算数

3.4.4 探针的检查方式 ​

探针有几种检查方式:

  1. HTTP GET:发一个HTTP GET请求,返回200-399之间的状态码就算成功
  2. TCP Socket:尝试连接容器的某个端口,能连上就算成功
  3. Exec:在容器里执行一个命令,退出码0就算成功
  4. gRPC:gRPC健康检查,gRPC应用用的

根据你的应用情况选合适的检查方式。

3.4.5 探针的配置参数 ​

探针有几个重要的参数:

  • initialDelaySeconds:初始延迟——容器启动后等多久才开始第一次检查
  • periodSeconds:检查周期——多久检查一次
  • timeoutSeconds:超时时间——每次检查的超时时间
  • successThreshold:成功阈值——连续成功多少次才算健康
  • failureThreshold:失败阈值——连续失败多少次才算不健康

这些参数要根据你的应用情况合理设置,不能太灵敏也不能太迟钝。

生产环境最佳实践:

  • 一定要配就绪探针——不然启动过程中就会有流量进来,导致502
  • 慢启动的应用一定要配启动探针——避免启动慢导致被误杀
  • 存活探针不要太敏感——不然会导致频繁重启,反而影响可用性

3.5 资源请求与限制:requests vs limits ​

Pod里的容器可以指定资源请求和限制——主要是CPU和内存。

很多人搞不懂requests和limits的区别,也不知道QoS等级是怎么回事。这一节我们彻底讲清楚。

3.5.1 requests vs limits ​

首先,什么是requests,什么是limits?

  • requests(请求):容器需要的最少资源。调度的时候,scheduler会看节点的剩余可分配资源,如果不够,就不会把Pod调度到这台节点上。
  • limits(限制):容器最多能用多少资源。超过了会怎么样?CPU的话会被限流,内存的话会被OOM Kill。

打个比方:

  • requests就像你订酒店房间——你说你要一间房,酒店就给你留一间,保证你有地方住
  • limits就像酒店的房间容量——一间房最多住4个人,你不能住5个

CPU和内存的处理方式不一样:

CPU是可压缩资源:

  • CPU不够了,进程只会变慢,不会死掉
  • 所以超过limits的话,就是被限流,用不到更多CPU,但不会被杀

内存是不可压缩资源:

  • 内存不够了,进程就OOM了,会死掉
  • 所以超过limits的话,容器就会被OOM Kill

这一点很重要——内存超了会死,CPU超了只会慢。

3.5.2 为什么要有requests和limits两个? ​

为什么不直接设一个值?搞两个多麻烦。

因为这两个作用不一样:

  • requests是给调度器用的——决定Pod能不能调度到这台节点上
  • limits是给内核用的——决定容器最多能用多少资源

而且,有了两个值,你可以做超卖(Overcommit)。

什么是超卖?就是所有Pod的limits加起来,超过了节点的总资源。

为什么要超卖?因为不是所有Pod都会同时用到limits那么多资源——大部分时候,它们用的比limits少。

比如:

  • 节点有16G内存
  • 跑了10个Pod,每个limits是2G——总共20G,超过了16G
  • 但正常情况下,每个Pod只用1G,总共才10G,完全够用
  • 这样资源利用率就高了

超卖能提高资源利用率,但也有风险——如果所有Pod同时都用到limits那么多,节点资源就不够了,就会有Pod被OOM Kill。

所以超卖是一把双刃剑——用得好能省钱,用不好会出事故。

3.5.3 QoS等级 ​

根据requests和limits的设置,Pod会被分成三个QoS(Quality of Service,服务质量)等级:

1. Guaranteed(保证级)

  • 条件:每个容器都设置了CPU和内存的requests和limits,而且requests == limits
  • 特点:资源有保证,一般不会被杀死,除非节点资源真的不够了,而且它是优先级最低的
  • 适用场景:核心业务、重要应用,需要保证资源的

2. Burstable(突发级)

  • 条件:设置了requests和limits,但requests != limits;或者只有部分容器设置了
  • 特点:平时能用requests保证的资源,高峰的时候可以用到limits的量,但可能会被杀死
  • 适用场景:大部分应用,平时用量稳定,偶尔有高峰

3. BestEffort(尽力而为级)

  • 条件:什么资源都没设置
  • 特点:优先级最低,节点资源不够的时候最先被杀
  • 适用场景:不重要的任务、测试环境

QoS等级有什么用?当节点内存不够的时候,kubelet会杀Pod——优先级低的先杀。

  • BestEffort最先被杀
  • 然后是Burstable
  • 最后是Guaranteed

所以重要的应用,一定要设成Guaranteed级别——这样不容易被杀。

面试常考点:QoS有哪几个等级?分别是什么条件?节点内存不足的时候先杀哪个等级的Pod?

3.5.4 资源设置的最佳实践 ​

怎么设置requests和limits才合理?

  1. 不要不设资源:不设的话就是BestEffort,随时可能被杀,生产环境绝对不能这样
  2. 重要应用设成Guaranteed:核心业务,requests和limits设成一样的,保证资源
  3. 一般应用用Burstable:requests设成平时的用量,limits设成高峰的用量,留一定余量
  4. requests要合理:设太小了调度不进去,设太大了浪费资源
  5. limits不要设得太离谱:跟requests差太多的话,超卖严重,容易出问题
  6. 根据监控调整:看实际的资源使用情况,不断调整requests和limits

资源设置是个技术活,需要根据实际情况不断调优。

3.6 扩展知识:Linux命名空间与cgroups深度解析 ​

前面我们说了,容器的本质是Linux的命名空间(Namespace)和cgroups。Pod的共享,本质上也是命名空间的共享。这一节我们深入讲讲这两个东西。

3.6.1 命名空间(Namespace):隔离的魔法 ​

命名空间是什么?它是Linux内核提供的一种机制——让进程觉得自己拥有整个系统的资源。

就像给进程戴上了"有色眼镜"——它看到的世界,跟真实的世界不一样。

Linux有好几种命名空间:

  1. PID命名空间:隔离进程ID。在不同的PID命名空间里,进程ID是独立的——你在容器里看到的PID 1,在宿主机上可能是PID 12345。而且容器里的进程看不到宿主机的进程。

  2. 网络命名空间:隔离网络栈。每个网络命名空间有自己的网络设备、IP地址、端口空间、路由表、iptables规则。Pod里的容器共享同一个网络命名空间,所以它们的IP是一样的,localhost就能互相访问。

  3. 挂载命名空间:隔离文件系统挂载点。每个挂载命名空间有自己的挂载树——容器里的根目录是镜像里的根目录,跟宿主机的根目录不一样。

  4. UTS命名空间:隔离主机名和域名。每个容器可以有自己的主机名。

  5. IPC命名空间:隔离System V IPC和POSIX消息队列。不同命名空间里的进程不能直接用IPC通信。

  6. 用户命名空间:隔离用户和组ID。容器里的root用户,在宿主机上可能是个普通用户——这样更安全。

  7. cgroup命名空间:隔离cgroup的视图。

……

你看,容器的"隔离",本质上就是通过这些命名空间实现的——进程被关在各种命名空间里,以为自己拥有整个系统,其实只是看到了系统的一部分。

那Pod的共享呢?Pod里的容器,共享哪些命名空间?

  • 共享网络命名空间——所以网络是通的
  • 共享UTS命名空间——所以主机名一样
  • 共享IPC命名空间——所以可以用IPC通信
  • 不共享PID命名空间——默认情况下,每个容器有自己的PID命名空间(当然也可以设成共享的)
  • 不共享挂载命名空间——每个容器有自己的文件系统,Volume是通过挂载共享的

这就是Pod的本质——一组容器,共享部分命名空间。

3.6.2 cgroups:资源限制的魔法 ​

光有隔离还不够——你还得限制每个容器能用多少资源,不然一个容器把所有资源都吃光了,其他容器怎么办?

这就是cgroups(Control Groups)干的事儿。

cgroups是什么?它是Linux内核提供的一种机制——把进程分组,然后对组进行资源限制、统计、控制。

cgroups能限制什么资源?

  • CPU:限制CPU使用率、CPU核数、CPU优先级
  • 内存:限制内存使用量、swap使用量
  • 磁盘IO:限制IO速率
  • 磁盘空间:限制磁盘使用量(通过quota)
  • 设备:限制能访问哪些设备
  • ……

cgroups是怎么工作的?它把进程组织成树状的结构——每个节点是一个控制组,下面可以有子组。每个组可以设置资源限制,组里的所有进程都受这个限制。

容器的资源限制,本质上就是把容器里的所有进程放到一个cgroup里,然后给这个cgroup设置资源限制。

比如你给容器设了内存limits是1G——实际上就是给这个容器的cgroup设置了内存限制,超过了就OOM Kill。

再比如CPU limits——就是给cgroup设置CPU带宽限制,每秒最多能用多少CPU时间。

所以你看,容器的资源限制,本质上就是cgroups的资源限制。

3.6.3 容器 = 命名空间 + cgroups + 镜像 ​

总结一下:

  • 命名空间:实现了"隔离"——进程看不到外面的世界
  • cgroups:实现了"限制"——进程能用的资源是有限的
  • 镜像:实现了"打包"——进程运行的文件系统环境

这三个东西加起来,就是容器。

是不是很简单?容器不是什么黑科技,就是Linux内核的两个老特性(命名空间和cgroups),加上一个文件系统打包(镜像),组合在一起就成了容器。

理解了这个,你就理解了容器的本质。再看Pod,就更好理解了——Pod就是一组容器,它们共享部分命名空间,作为一个整体被调度和管理。

3.7 本章小结 ​

这一章我们讲了K8s最核心的概念——Pod。

总结一下重点:

  • Pod是K8s的最小调度单位,是一组共享资源的容器
  • 为什么要有Pod?因为真实应用往往由多个紧密耦合的进程组成,需要一起调度、共享资源
  • 四种经典容器设计模式:Sidecar、Ambassador、Adapter、Init Container
  • Pod的生命周期:从Pending到Running到Succeeded/Failed
  • 三种探针:Liveness(存活)、Readiness(就绪)、Startup(启动)
  • 资源requests和limits的区别,以及三个QoS等级:Guaranteed、Burstable、BestEffort
  • 容器的本质:命名空间 + cgroups + 镜像

理解了Pod,你就理解了K8s的基础。后面的所有概念——Deployment、Service、Volume……都是围绕Pod展开的。

下一章,我们来讲工作负载控制器——怎么用控制器来管理Pod。


第4章 工作负载控制器:从Pod到应用 ​

上一章我们讲了Pod——K8s的最小单位。但你有没有想过:为什么我们平时很少直接创建Pod?而是创建Deployment、StatefulSet这些东西?

因为直接用Pod有很多问题——Pod挂了没人管,扩容要手动加,更新要一个个改……太麻烦了。

所以K8s设计了控制器(Controller)——帮你管理Pod的生命周期。这一章我们就来讲讲各种工作负载控制器,它们分别适用于什么场景,底层是怎么工作的。

4.1 为什么需要控制器?——直接用Pod的痛点 ​

我们先想想,如果K8s只有Pod,没有控制器,会怎么样?

4.1.1 痛点一:没有自愈能力 ​

Pod挂了怎么办?比如:

  • 应用崩溃了,容器退出了
  • 节点宕机了,上面的Pod都没了
  • OOM了,Pod被杀了

如果只有Pod,那挂了就挂了,不会自动恢复。你得手动再创建一个新的Pod。

这哪行?生产环境,Pod挂了是常事儿,总不能24小时盯着吧?

我们需要——Pod挂了自动重启,节点挂了自动把Pod迁移到别的节点。这就是自愈能力。

4.1.2 痛点二:扩容缩容麻烦 ​

流量涨了,要扩容——原来3个Pod不够了,要加到10个。你怎么办?手动创建7个新的Pod?

流量降了,要缩容——10个太多了,减到3个。你手动删7个?

太蠢了。而且手动操作容易出错——删错了怎么办?

我们需要——一条命令就能扩容缩容,甚至能根据负载自动伸缩。

4.1.3 痛点三:更新发布麻烦 ​

应用要发布新版本,怎么发布?

  • 全量停掉旧的,启动新的——中间有停机时间,用户体验差
  • 一台一台更新——手动操作,累死人,还容易出错
  • 出了问题怎么回滚?再一个个改回去?

太麻烦了。我们需要——滚动更新,零停机发布,一键回滚。

4.1.4 痛点四:管理麻烦 ​

一个应用有几十个Pod,你怎么管理?

  • 怎么知道哪些Pod是这个应用的?
  • 怎么统一操作?
  • 怎么保证数量正确?

总不能一个个记吧?

……

这些问题,控制器都能帮你解决。

控制器是什么?它就是一个控制循环——你告诉它你想要什么状态(比如3个副本、用v2镜像),它就不断地调整,让当前状态达到你想要的状态。

这就是声明式的好处——你只管说要什么,不用管怎么实现。

K8s里有好几种工作负载控制器,分别适用于不同的场景:

  • Deployment + ReplicaSet:无状态应用,最常用
  • StatefulSet:有状态应用
  • DaemonSet:每个节点跑一个
  • Job + CronJob:一次性任务和定时任务

我们一个个来讲。

4.2 Deployment + ReplicaSet:无状态应用的标配 ​

Deployment是最常用的控制器,没有之一。大部分无状态应用,都是用Deployment部署的。

4.2.1 什么是ReplicaSet? ​

讲Deployment之前,得先讲ReplicaSet(副本集)。

ReplicaSet是什么?它就是用来保证一组Pod的副本数始终保持在你指定的数量。

比如你说要3个副本,ReplicaSet就会保证一直有3个Pod在跑——少了就补,多了就删。

ReplicaSet的工作原理很简单:

  1. 观察当前有多少个Pod在运行
  2. 跟期望的副本数对比
  3. 少了就创建新的Pod,多了就删除多余的Pod

就这么简单。ReplicaSet只关心一件事——副本数对不对。

那为什么我们不直接用ReplicaSet,还要用Deployment?

因为ReplicaSet只管副本数,不管别的——比如更新镜像、滚动更新、回滚……这些它都不管。

而Deployment就是在ReplicaSet之上,又加了一层——管理ReplicaSet,实现滚动更新、回滚等功能。

4.2.2 Deployment和ReplicaSet的关系 ​

Deployment和ReplicaSet是什么关系?

  • Deployment不直接管理Pod
  • Deployment管理ReplicaSet
  • ReplicaSet管理Pod

打个比方:

  • Deployment就像产品经理——他说"我们要做这个功能,用v2版本"
  • ReplicaSet就像技术组长——他管具体的开发人员(Pod),保证人数够
  • Pod就像开发人员——真正干活的

当你更新Deployment的镜像版本的时候:

  1. Deployment创建一个新的ReplicaSet(用新镜像)
  2. 新的ReplicaSet慢慢增加副本数
  3. 旧的ReplicaSet慢慢减少副本数
  4. 最后新的ReplicaSet有全部副本,旧的ReplicaSet副本数为0

这就是滚动更新——整个过程服务不中断,因为新旧版本同时存在,逐步替换。

而且,旧的ReplicaSet还留着——你想回滚?一句话的事儿,把旧的ReplicaSet再扩起来,新的缩下去就行了。

这就是为什么要用Deployment——它在ReplicaSet的基础上,提供了更高级的功能。

4.2.3 滚动更新策略 ​

Deployment的滚动更新,有两个重要参数:

  • maxSurge:更新过程中,最多可以比期望副本数多几个
  • maxUnavailable:更新过程中,最多可以有几个不可用

这两个参数怎么理解?举个例子:

假设你有10个副本,maxSurge=2,maxUnavailable=2。

更新的时候:

  • 最多可以有12个Pod(10 + 2)
  • 最少要有8个可用的Pod(10 - 2)

所以更新的过程大概是这样的:

  1. 先启动2个新Pod(现在12个:10旧 + 2新)
  2. 然后停掉2个旧Pod(现在10个:8旧 + 2新)
  3. 再启动2个新Pod(12个:8旧 + 4新)
  4. 再停掉2个旧Pod(10个:6旧 + 4新)
  5. ……
  6. 直到全部替换完

这样整个过程中,服务始终是可用的,而且资源使用也不会超太多。

这两个参数可以根据你的需求调整:

  • 如果你想更新快一点,就把maxSurge设大一点——多启动一些新的,替换得快
  • 如果你想保证可用性,就把maxUnavailable设小一点——保证更多的Pod可用
  • 如果你资源紧张,就把maxSurge设小一点——不要同时跑太多Pod

4.2.4 回滚 ​

Deployment更新出问题了怎么办?回滚啊!

Deployment的回滚非常简单——因为旧的ReplicaSet还在。

回滚的时候:

  1. 把旧的ReplicaSet的副本数加起来
  2. 把新的ReplicaSet的副本数减下去
  3. 就回到旧版本了

就是这么简单。而且你可以查看历史版本,回滚到任意一个版本。

这就是声明式的好处——所有版本都记录在案,想回滚就回滚。

4.2.5 什么时候用Deployment? ​

Deployment适合什么场景?

  • 无状态应用——Web服务、API服务、微服务……这些都是无状态的,Pod随时可以替换,哪个实例都一样
  • 需要滚动更新、回滚的应用
  • 需要自动伸缩的应用

大部分应用都是无状态的,所以Deployment是最常用的控制器。

4.3 StatefulSet:有状态应用的解决方案 ​

Deployment很好用,但它有个前提——应用是无状态的,Pod是可以随便替换的。

但有些应用是有状态的——比如数据库、消息队列、分布式存储……这些应用的Pod不是随便就能替换的。

4.3.1 有状态应用的痛点 ​

有状态应用有什么特点?为什么Deployment搞不定?

  1. 稳定的网络标识:每个Pod要有固定的名字、固定的DNS名字——因为其他Pod要通过固定的地址找到它。比如数据库的主从,从库要知道主库的地址,如果主库的名字变了,那就乱了。

  2. 稳定的持久化存储:每个Pod要有自己的存储,Pod重启或者迁移到别的节点,存储还要跟着它。如果Pod是随便替换的,存储跟Pod对不上,那就完了——数据都丢了。

  3. 有序的部署和伸缩:有状态应用一般不能同时启动所有实例——要按顺序来,一个一个启动。比如数据库集群,要先启主库,再启从库。伸缩的时候也是,要按顺序加,按顺序删。

  4. 有序的滚动更新:更新的时候也要按顺序来,不能随便更新。

这些Deployment都做不到——Deployment里的Pod都是一样的,名字是随机的,存储也是共享的(如果有的话),启动也是一起启动的。

所以就有了StatefulSet——专门为有状态应用设计的控制器。

4.3.2 StatefulSet的特点 ​

StatefulSet有什么特点?

  1. 稳定的Pod名字:StatefulSet里的Pod,名字是固定的,格式是{statefulset名}-{序号}。比如叫mysql的StatefulSet,有3个副本,那Pod名字就是mysql-0、mysql-1、mysql-2。序号从0开始。

    而且Pod重启或者重建,名字不变——mysql-0挂了,重建出来还是叫mysql-0。

  2. 稳定的DNS名字:每个Pod都有固定的DNS域名,格式是{pod名}.{service名}.{namespace}.svc.cluster.local。其他Pod可以通过这个固定的域名找到它。

    这就解决了服务发现的问题——不管Pod在哪,IP怎么变,域名是不变的。

  3. 稳定的持久化存储:每个Pod对应自己的PVC(PersistentVolumeClaim)。Pod重建的时候,PVC还在,所以数据不会丢。Pod迁移到别的节点,存储也跟着走。

    而且PVC的名字也是固定的——{pvc模板名}-{pod名},跟Pod一一对应。

  4. 有序的部署和伸缩:部署的时候,按序号从小到大,一个一个来——0号启动好了再启1号,1号好了再启2号。伸缩的时候也是,加的话从大到小加,删的话从大到小删——保证序号是连续的。

  5. 有序的滚动更新:更新的时候,从大到小,一个一个更新——先更2号,再更1号,再更0号。

这些特点,完美解决了有状态应用的需求。

4.3.3 Headless Service ​

StatefulSet需要配合Headless Service使用。

什么是Headless Service?就是没有ClusterIP的Service——它不会做负载均衡,而是直接返回后端Pod的DNS记录。

为什么需要Headless Service?因为StatefulSet里的每个Pod都要有自己的DNS名字——通过Headless Service,每个Pod都会有一个DNS记录,其他Pod就能通过域名找到它了。

如果是普通的Service,那所有Pod共享一个ClusterIP,做负载均衡——这对有状态应用没用,因为有状态应用要找的是具体的某个Pod,不是随便一个。

4.3.4 什么时候用StatefulSet? ​

StatefulSet适合什么场景?

  • 数据库:MySQL、PostgreSQL、MongoDB……这些有状态的数据库
  • 消息队列:Kafka、RabbitMQ、RocketMQ……
  • 分布式存储:Redis集群、Elasticsearch、Ceph、MinIO……
  • 任何需要稳定标识、稳定存储、有序部署的应用

但是,注意——不是所有有状态应用都适合直接用StatefulSet。

为什么?因为StatefulSet只是提供了稳定的标识、存储和有序部署,但它不理解应用的逻辑。比如:

  • 数据库的主从切换,StatefulSet不会帮你做
  • 集群的成员管理,StatefulSet不会帮你做
  • 数据的备份恢复,StatefulSet不会帮你做

这些都需要你自己处理,或者用Operator来做。

StatefulSet只是基础,真正的有状态应用部署,最好还是用对应的Operator——后面我们会讲Operator。

4.4 DaemonSet:每个节点跑一个 ​

Deployment是你要几个副本就跑几个,Pod放在哪由调度器决定。

但有些场景,你需要每个节点都跑一个Pod——比如日志采集、监控Agent、网络插件……这些东西,每个节点都要有一个。

这时候就需要DaemonSet。

4.4.1 DaemonSet是什么? ​

DaemonSet的作用就是——确保每个(或者符合条件的)节点上,都有一个Pod在运行。

节点加入集群,DaemonSet就自动在上面启动一个Pod;节点离开集群,那个Pod就被自动删掉。

就像每个节点上的"常驻进程"——每个节点都要有一个。

4.4.2 什么时候用DaemonSet? ​

典型的使用场景:

  1. 日志采集:比如Fluentd、Filebeat、Logstash——每个节点都要跑一个,收集本节点的日志
  2. 监控Agent:比如Prometheus Node Exporter、Zabbix Agent——每个节点都要跑一个,采集本节点的指标
  3. 网络插件:比如Calico、Cilium的Agent——每个节点都要跑,负责本节点的网络
  4. 存储插件:比如Ceph的RBD插件、CSI插件——每个节点都要跑,负责本节点的存储挂载
  5. 安全Agent:比如入侵检测、安全扫描——每个节点都要跑

总之,每个节点都需要跑一个的东西,就用DaemonSet。

4.4.3 节点选择器 ​

不是所有节点都要跑?没关系,DaemonSet支持节点选择器——你可以指定只在某些节点上跑。

比如:

  • 你有GPU节点,只想在GPU节点上跑GPU监控Agent
  • 你有专门的存储节点,只想在存储节点上跑存储插件
  • 你想灰度发布,先在部分节点上跑

通过nodeSelector、nodeAffinity、taint & toleration这些,你可以精确控制DaemonSet在哪些节点上跑。

4.5 Job与CronJob:一次性任务与定时任务 ​

前面讲的控制器,都是跑长期运行的服务的——Deployment、StatefulSet、DaemonSet,里面的Pod都是一直跑的,除非你删了它们。

但有些任务是一次性的——跑完就结束了,不需要一直跑。比如:

  • 数据处理任务
  • 批量计算任务
  • 备份任务
  • 迁移任务

这时候就需要Job。

4.5.1 Job:一次性任务 ​

Job是什么?它负责管理一次性任务——创建一个或多个Pod,确保它们成功完成任务,然后Job就结束了。

Job的特点:

  • Pod跑完就退出,不会一直运行
  • 如果Pod失败了,Job会重新创建一个Pod,直到成功
  • 你可以指定并行数——同时跑几个Pod
  • 你可以指定完成数——总共要成功完成几个

Job适合什么场景?

  • 批量数据处理
  • 一次性的计算任务
  • 数据迁移
  • 备份任务
  • CI/CD的构建任务

4.5.2 CronJob:定时任务 ​

Job是一次性的,那如果我想定时执行呢?比如每天凌晨2点跑一次备份,每小时跑一次数据同步。

这时候就需要CronJob。

CronJob是什么?它就是定时执行的Job——就像Linux里的crontab,到了指定时间,就创建一个Job来执行任务。

CronJob的时间格式跟crontab一样:

分 时 日 月 周

比如:

  • 0 2 * * *:每天凌晨2点
  • */5 * * * *:每5分钟
  • 0 0 * * 0:每周日凌晨0点

CronJob适合什么场景?

  • 定时备份
  • 定时数据同步
  • 定时报表
  • 定时清理任务
  • 任何需要定时执行的任务

4.5.3 注意事项 ​

用Job和CronJob要注意几个问题:

  1. 任务要幂等:因为Job可能会重试,同一个任务可能跑多次——所以你的任务要保证幂等,跑多次结果一样,不会出问题。

  2. 并发策略:CronJob有并发策略——如果上一个任务还没跑完,下一个时间点到了,怎么办?

    • Allow:允许同时跑(默认)
    • Forbid:不允许,跳过这次
    • Replace:替换掉上一个,用新的

    根据你的任务类型选合适的。

  3. 历史记录限制:CronJob会保留一些历史的Job,太多了会占资源,可以设置保留多少个。

4.6 控制器对比:怎么选? ​

这么多控制器,怎么选?我给你总结一下:

控制器适用场景Pod特点存储网络标识有序性
Deployment + ReplicaSet无状态应用完全一样,可随便替换共享存储(可选)随机名字,Service负载均衡无序
StatefulSet有状态应用有固定序号,一一对应每个Pod独立存储固定DNS名字有序部署、更新、伸缩
DaemonSet每个节点跑一个每个节点一个可选节点级无
Job一次性任务跑完就退出可选无可并行
CronJob定时任务定时跑,跑完退出可选无定时触发

选型决策树:

  1. 是长期运行的服务吗?
    • 是 → 继续
    • 否 → 是一次性的?→ Job
    • 否 → 是定时的?→ CronJob
  2. 每个节点都要跑一个吗?
    • 是 → DaemonSet
    • 否 → 继续
  3. 是有状态应用吗?
    • 是 → StatefulSet
    • 否 → Deployment

大部分情况下,你用Deployment就对了——大部分应用都是无状态的。

4.7 扩展知识:控制循环与水平扩展原理 ​

前面我们多次提到"控制循环",这一章我们再深入讲讲——控制器到底是怎么工作的?水平扩展又是怎么实现的?

4.7.1 控制器的工作模式 ​

所有的控制器,工作模式都差不多——都是控制循环(Control Loop):

while True:
    期望状态 = 从apiserver获取期望状态()
    当前状态 = 从apiserver获取当前状态()
    if 期望状态 == 当前状态:
        继续等待
    else:
        执行调整操作,让当前状态趋近期望状态

就这么简单的逻辑,但非常强大。

以ReplicaSet为例:

  • 期望状态:3个副本
  • 当前状态:现在有2个Pod在跑
  • 调整操作:创建1个新的Pod

再比如Deployment的滚动更新:

  • 期望状态:用v2镜像,3个副本
  • 当前状态:现在有2个v1的Pod,1个v2的Pod
  • 调整操作:再创建1个v2的Pod,删掉1个v1的Pod

就这么不断地循环,直到当前状态跟期望状态一致。

4.7.2 Informer:高效的状态同步 ​

你可能会问:控制器不断地去apiserver查状态,会不会给apiserver造成很大压力?

不会,因为K8s用了Informer机制。

Informer是什么?它不是每次都去查,而是Watchapiserver的变化——有变化的时候apiserver会推过来,Informer把状态存在本地缓存里。

控制器读状态的时候,直接读本地缓存,不用每次都去查apiserver——这样效率就高多了。

而且,Informer还会做去重、排序、限流之类的处理,保证控制器的工作是高效的。

Informer的工作流程:

  1. 先List一次,把所有资源都拉下来,存在本地缓存
  2. 然后Watch,有变化就更新本地缓存
  3. 有变化的时候,把事件放到队列里
  4. 控制器从队列里拿事件,处理

这样既高效,又可靠——就算Watch断了,重连之后也能从断点继续,不会丢事件。

4.7.3 水平扩展的原理 ​

水平扩展(Horizontal Scaling)是什么?就是增加或减少副本数。

水平扩展是怎么实现的?其实非常简单——就是改一下控制器的期望副本数,然后控制器自己调整。

比如你有一个Deployment,3个副本,你想扩容到5个——你只需要把replicas改成5,Deployment控制器发现期望状态变了,就会让ReplicaSet增加2个副本,然后ReplicaSet就创建2个新的Pod。

就这么简单。

那自动伸缩(HPA)呢?HPA就是自动改replicas——它监控Pod的CPU使用率(或者其他指标),如果高了,就自动把replicas改大;如果低了,就自动改小。

HPA本身不直接创建或删除Pod——它只是改Deployment的replicas,然后Deployment和ReplicaSet去做具体的调整。

这就是K8s的设计哲学——每个组件只做一件事,通过组合来实现复杂功能。

4.7.4 为什么这个模式这么强大? ​

控制循环模式为什么这么牛?我觉得有几个原因:

  1. 自愈能力强:不管出什么问题,只要控制器还在,它就会不断地调整,最终达到期望状态。Pod挂了?没关系,控制器会重建。节点挂了?没关系,控制器会把Pod调度到别的节点。

  2. 容错性好:控制器偶尔挂了没关系,重启了接着来——它只看当前状态和期望状态,不管之前发生了什么。就算中间丢了几个事件也没关系,因为最终状态是对的就行。

  3. 可组合性好:控制器可以嵌套——Deployment管ReplicaSet,ReplicaSet管Pod。每层只做自己的事,职责清晰。

  4. 可扩展性好:你可以自己写控制器,管理自己的资源。不用改K8s的核心代码,就能扩展功能。

这就是K8s最核心的设计思想——声明式API + 控制循环。理解了这个,你就理解了K8s的灵魂。

4.8 本章小结 ​

这一章我们讲了各种工作负载控制器。

总结一下:

  • 为什么需要控制器?因为直接用Pod没有自愈、扩容、更新等能力
  • Deployment + ReplicaSet:无状态应用的标配,支持滚动更新、回滚
  • StatefulSet:有状态应用的解决方案,稳定的标识、存储、有序部署
  • DaemonSet:每个节点跑一个,适合日志、监控、网络插件等
  • Job + CronJob:一次性任务和定时任务
  • 控制器的核心是控制循环——不断地把当前状态调整到期望状态

选控制器的时候,根据你的应用场景来——大部分无状态应用用Deployment,有状态的用StatefulSet,每个节点都要的用DaemonSet,任务型的用Job/CronJob。

下一章,我们来讲Service和Ingress——服务发现和流量入口。


第5章 Service与Ingress:服务发现与流量入口 ​

前面我们讲了Pod、讲了控制器——应用跑起来了。但新的问题来了:怎么访问这些应用?

Pod的IP是不固定的——Pod重启、迁移、扩容缩容,IP都会变。而且Pod是"易逝的",随时可能挂掉。那其他应用怎么找到它?外部用户怎么访问它?

这就是这一章要解决的问题——服务发现和流量入口。

5.1 Service:服务发现与负载均衡 ​

5.1.1 为什么需要Service? ​

我们先想想,如果没有Service,会怎么样?

假设你有一个Deployment,跑了3个Pod,提供Web服务。另一个应用要调用这个Web服务。

你怎么调用?直接用Pod IP?

  • Pod重启了,IP变了怎么办?
  • 扩容了,多了几个Pod,你怎么知道?
  • 缩容了,少了几个Pod,你怎么知道?
  • 3个Pod,你调用哪个?怎么做负载均衡?

太麻烦了。你需要一个固定的访问入口,还有服务发现和负载均衡。

Service就是干这个的。

5.1.2 Service是什么? ​

Service是什么?简单说:

  • Service是K8s里的一个资源对象
  • 它定义了一组Pod的访问方式
  • 它有一个固定的IP(ClusterIP)和DNS名字
  • 访问Service的IP,会被负载均衡到后端的Pod上

打个比方:

  • Pod就像公司里的员工,员工会离职、会入职,人数会变
  • Service就像公司的前台电话——你打前台电话,前台帮你转到具体的员工
  • 不管员工怎么变,前台电话是不变的

Service的后端Pod是怎么选的?通过标签选择器(Label Selector)——Service选择所有匹配某个标签的Pod,作为它的后端。

比如你的Pod都有标签app=web,那Service的selector就设成app=web——所有带这个标签的Pod,都是这个Service的后端。

Pod增增减减没关系,只要标签匹配,Service就会自动更新后端列表——这就是服务发现。

5.1.3 Service的四种类型 ​

Service有四种类型,分别适用于不同场景:

1. ClusterIP(默认类型)

  • 只有集群内部可以访问的IP
  • 集群内的其他Pod可以通过这个IP访问服务
  • 外部访问不到
  • 这是默认类型,最常用

适用场景:集群内部的服务之间互相调用。

2. NodePort

  • 在每个节点上开一个端口(NodePort)
  • 访问任何一个节点的这个端口,都会被转发到Service
  • 外部可以通过节点IP:NodePort访问服务

适用场景:临时测试、或者在没有云负载均衡的环境下对外提供服务。

缺点:端口范围有限(默认30000-32767),而且要知道节点IP,节点挂了就不行了。

3. LoadBalancer

  • 使用云厂商的负载均衡器(比如阿里云SLB、AWS ELB)
  • 云厂商会给你分配一个公网IP,访问这个IP就会转发到Service
  • 底层是NodePort + 云负载均衡

适用场景:在云上部署,需要对外提供服务的时候。

优点:有公网IP,有负载均衡,不用管节点IP。 缺点:要钱,每个LoadBalancer都要花钱。

4. ExternalName

  • 把Service映射到一个外部域名上
  • 集群内访问这个Service的DNS,会解析到外部的域名
  • 相当于给外部服务起了个别名

适用场景:集群内的应用要访问外部服务,用ExternalName起个别名,方便管理。

5.1.4 Service是怎么实现的?kube-proxy的作用 ​

Service听起来很神奇——一个固定的IP,访问它就自动负载均衡到后端Pod。这是怎么实现的?

答案是——kube-proxy。

我们前面讲过,每个节点上都有一个kube-proxy。kube-proxy的作用就是:

  • Watch Service和Endpoint的变化
  • 在节点上配置相应的网络规则(iptables/IPVS)
  • 让访问Service IP的流量,能正确地转发到后端Pod

具体来说,当你访问Service的ClusterIP的时候:

  1. 你的数据包到了节点的网络栈
  2. iptables(或者IPVS)匹配到这个目的IP是Service IP
  3. 做DNAT(目的地址转换),把目的IP改成某个后端Pod的IP
  4. 数据包就发给了那个Pod

返回的数据包再做SNAT(源地址转换),这样Pod看到的源IP是节点的IP,返回的时候能正确返回。

整个过程对应用是透明的——应用不知道背后有多少个Pod,也不知道自己访问的是哪个Pod,它只知道访问Service IP就行了。

这就是Service的实现原理——通过iptables/IPVS做DNAT,实现负载均衡。

5.1.5 kube-proxy的三种模式对比 ​

前面我们提过kube-proxy有三种模式:userspace、iptables、IPVS。这里我们详细对比一下:

模式工作方式性能功能适用场景
userspacekube-proxy在用户空间做代理差(要经过用户态内核态切换)基础已经淘汰,基本不用了
iptables内核的iptables做DNAT较好(内核态处理)基础负载均衡中小规模集群,Service数量不多
IPVS内核的IPVS模块做负载均衡好(专门的负载均衡模块,性能高)支持多种负载均衡算法大规模集群,Service数量多

iptables模式的问题:

  • Service多了之后,iptables规则会非常多——几千条甚至几万条
  • iptables是线性匹配的,规则多了匹配效率下降
  • 更新规则的时候要全量更新,慢

IPVS模式的优势:

  • IPVS是专门设计来做负载均衡的,性能更好
  • 支持更多的负载均衡算法:轮询、加权轮询、最小连接、源地址哈希、目标地址哈希……
  • 规则更新是增量的,更快
  • 大规模下性能稳定

所以,生产环境如果集群规模比较大,建议用IPVS模式。

5.2 服务发现:DNS与CoreDNS ​

Service有了固定的IP,但IP地址不好记啊——人记域名比记IP方便多了。

而且,我们希望服务之间通过名字互相调用,而不是IP——这样IP变了也没关系。

这就是服务发现——通过服务名找到服务的IP。

K8s里的服务发现,主要是通过DNS实现的。

5.2.1 CoreDNS:集群内的DNS服务器 ​

K8s集群里有一个DNS服务——以前叫kube-dns,现在叫CoreDNS。

CoreDNS是什么?它就是跑在集群里的DNS服务器,集群里的Pod默认都用它做DNS解析。

当你创建一个Service的时候,CoreDNS会自动给它添加一条DNS记录——格式是:

{service名}.{namespace}.svc.cluster.local

比如:default命名空间里有个叫web的Service,那它的DNS名字就是web.default.svc.cluster.local。

集群里的Pod,直接用这个名字就能访问到Service——DNS解析出来就是Service的ClusterIP。

这太方便了!你不用记IP,也不用管Service的IP会不会变——只要名字不变,就能访问。

5.2.2 不同命名空间的服务怎么访问? ​

同一个命名空间里的服务,直接用服务名就能访问——比如同是default命名空间的Pod,访问web服务,直接curl web就行。

不同命名空间的呢?要写全:

  • web.default —— default命名空间的web服务
  • web.production —— production命名空间的web服务

或者写全限定名:web.default.svc.cluster.local。

所以命名空间不仅能隔离资源,还能隔离DNS域名空间——不同命名空间可以有同名的服务,互不影响。

5.2.3 Headless Service的DNS ​

前面讲StatefulSet的时候提到了Headless Service——它的DNS有什么不一样?

普通的Service,DNS解析出来是ClusterIP——一个IP。

Headless Service呢?它没有ClusterIP,DNS解析出来的是所有后端Pod的IP列表。

而且,每个Pod还有自己的DNS记录——格式是{pod名}.{service名}.{namespace}.svc.cluster.local。

比如StatefulSet的Pod叫mysql-0,Headless Service叫mysql,那Pod的DNS就是mysql-0.mysql.default.svc.cluster.local。

这就是StatefulSet的稳定网络标识——通过DNS名字找到具体的Pod。

5.2.4 服务发现的其他方式 ​

除了DNS,还有没有别的服务发现方式?

有,比如环境变量——Pod启动的时候,K8s会把同命名空间的所有Service的信息,以环境变量的形式注入到Pod里。

但这个方式有问题:

  • 必须是Pod启动之前就有的Service,后面创建的没有
  • Service多了环境变量一大堆,很乱
  • 不如DNS方便

所以现在基本都用DNS,环境变量的方式很少用了。

5.3 Ingress:七层流量入口 ​

Service解决了集群内的服务发现和负载均衡问题。但外部流量怎么进集群?

你可能会说:用NodePort或者LoadBalancer啊。

但这两个都是四层的(TCP/UDP),而且有很多问题:

  • NodePort端口太多,不好管理
  • LoadBalancer每个服务一个,太贵了
  • 都是四层的,做不了七层的路由(根据域名、路径转发)

我们需要一个统一的七层入口——根据域名、路径把流量转发到不同的服务,还能做SSL终止、限流、认证等等。

这就是Ingress。

5.3.1 Ingress是什么? ​

Ingress是什么?简单说:

  • Ingress是K8s里的一个资源对象
  • 它定义了外部流量怎么进入集群的规则
  • 比如:域名a.example.com转发到服务A,域名b.example.com转发到服务B
  • 比如:/api路径转发到后端服务,/路径转发到前端服务

但是——Ingress本身只是规则,它不干活。真正干活的是Ingress Controller。

Ingress Controller是什么?它就是一个跑在集群里的反向代理(比如Nginx、Traefik),它Watch Ingress规则,然后根据规则配置自己的代理逻辑。

打个比方:

  • Ingress就像"路由规则"——写在纸上的规则
  • Ingress Controller就像"路由器"——真正执行规则的设备
  • 你把规则写好,路由器根据规则转发流量

这是很多初学者搞混的地方——Ingress和Ingress Controller是两回事。光有Ingress没用,你得有Ingress Controller才行。

5.3.2 Ingress能做什么? ​

Ingress能做很多事情:

  1. 基于域名的虚拟主机:同一个IP,不同的域名转发到不同的服务

    • a.example.com → Service A
    • b.example.com → Service B
  2. 基于路径的路由:同一个域名,不同的路径转发到不同的服务

    • example.com/api → 后端服务
    • example.com/ → 前端服务
  3. SSL/TLS终止:HTTPS证书在Ingress层配置,后端服务不用管

  4. 负载均衡:Ingress Controller本身做负载均衡

  5. 限流、熔断、重试:各种流量治理功能

  6. 认证、授权:统一的入口认证

  7. WAF、日志、监控:统一的安全和可观测性

总之,Ingress就是集群的统一流量入口——所有外部流量都先经过Ingress,再转发到各个服务。

5.3.3 Ingress Controller对比 ​

Ingress Controller有很多种,主流的有:

Ingress Controller特点性能功能适用场景
Nginx Ingress官方维护,基于Nginx,最常用好丰富通用场景,大部分人用这个
TraefikGo写的,原生支持K8s,配置简单较好丰富,有Dashboard云原生场景,喜欢简单配置的
HAProxy Ingress基于HAProxy,性能好很好丰富对性能要求高的
Envoy-based(比如Istio Gateway)基于Envoy,功能强大好非常丰富,服务网格集成用服务网格的场景
Kong基于Nginx + Lua,插件丰富较好非常丰富,API网关功能API网关场景

怎么选?

  • 一般场景,用Nginx Ingress就够了——最成熟,资料最多,用的人最多
  • 喜欢简单、云原生的,用Traefik——配置简单,自动发现,有Dashboard
  • 对性能要求极高的,用HAProxy
  • 已经用了Istio服务网格的,直接用Istio Gateway

5.3.4 Ingress的工作原理 ​

Ingress是怎么工作的?

  1. 你创建一个Ingress资源,定义路由规则
  2. Ingress Controller Watch到有新的Ingress
  3. Ingress Controller根据Ingress规则,配置自己的反向代理
  4. 外部流量进来,到达Ingress Controller
  5. Ingress Controller根据规则,把流量转发到对应的Service
  6. Service再负载均衡到后端Pod

整个流程:

用户 → Ingress Controller → Service → Pod

Ingress Controller本身也是以Pod的形式跑在集群里的——一般用Deployment或者DaemonSet部署,前面用LoadBalancer或者NodePort暴露出去。

5.4 对比:Service vs Ingress ​

很多人搞不清Service和Ingress的区别,我给你对比一下:

维度ServiceIngress
层级四层(TCP/UDP)七层(HTTP/HTTPS)
作用服务发现、负载均衡统一入口、路由、SSL终止
范围集群内为主(也可以对外)主要是外部流量进入
每个服务一个?一般每个服务一个可以一个Ingress管多个服务
实现kube-proxy(iptables/IPVS)Ingress Controller(Nginx/Traefik等)

简单说:

  • Service是服务级别的——每个服务一个,做服务发现和四层负载均衡
  • Ingress是入口级别的——整个集群一个或者几个,做统一的七层流量入口

两者不是替代关系,是配合关系——Ingress把流量转发给Service,Service再转发给Pod。

5.5 扩展知识:负载均衡算法与反向代理原理 ​

讲到负载均衡和反向代理,我们扩展一下知识——负载均衡有哪些算法?反向代理是怎么工作的?

5.5.1 常见的负载均衡算法 ​

负载均衡算法有很多种,各有适用场景:

1. 轮询(Round Robin)

  • 按顺序一个一个来,每个请求轮流分给不同的后端
  • 优点:简单,公平
  • 缺点:没考虑后端的性能差异、负载差异
  • 适用场景:后端性能差不多,请求也差不多

2. 加权轮询(Weighted Round Robin)

  • 给每个后端设置权重,权重大的分到的请求多
  • 优点:能根据后端性能调整
  • 缺点:还是没考虑实时负载
  • 适用场景:后端性能不一样

3. 最小连接(Least Connections)

  • 哪个后端当前连接数最少,就分给哪个
  • 优点:能根据实时负载调整,负载更均衡
  • 缺点:实现稍微复杂一点
  • 适用场景:请求处理时间差异大,长连接场景

4. 源地址哈希(Source IP Hash)

  • 根据客户端IP哈希,同一个IP的请求总是分到同一个后端
  • 优点:能保持会话(Session)
  • 缺点:负载可能不均衡
  • 适用场景:需要会话保持的场景

5. 最小响应时间(Least Response Time)

  • 哪个后端响应最快,就分给哪个
  • 优点:用户体验好
  • 缺点:实现复杂
  • 适用场景:对响应时间要求高的

6. 随机(Random)

  • 随机选一个后端
  • 优点:最简单
  • 缺点:可能不均衡
  • 适用场景:简单场景

不同的场景选不同的算法。K8s的Service默认是轮询(其实是随机的,因为iptables是随机选的),IPVS模式可以选不同的算法。

5.5.2 反向代理是什么? ​

反向代理(Reverse Proxy)是什么?

简单说:

  • 正向代理:代理客户端,帮客户端访问服务器——比如VPN、翻墙
  • 反向代理:代理服务器,帮服务器接收请求——比如Nginx、Ingress Controller

反向代理的作用:

  1. 负载均衡:把请求分到多个后端
  2. SSL终止:HTTPS在代理层解密,后端不用管
  3. 缓存:静态内容缓存,减轻后端压力
  4. 压缩:压缩响应,节省带宽
  5. 安全:WAF、限流、防攻击
  6. 路由:根据域名、路径转发到不同后端

Ingress Controller本质上就是一个反向代理——只不过它是专门为K8s设计的,能自动根据Ingress规则配置自己。

5.5.3 四层 vs 七层负载均衡 ​

我们常说四层负载均衡、七层负载均衡,是什么意思?

这是按OSI网络模型的层级来分的:

  • 四层:传输层(TCP/UDP)——根据IP和端口做负载均衡
  • 七层:应用层(HTTP/HTTPS)——根据域名、路径、Header等做负载均衡

四层负载均衡:

  • 工作在TCP/UDP层
  • 只看源IP、目的IP、源端口、目的端口
  • 不看具体内容
  • 性能好,因为处理的层次浅
  • 例子:LVS、云厂商的四层LB、K8s的Service

七层负载均衡:

  • 工作在HTTP层
  • 能看到HTTP的内容——域名、路径、Header、Cookie……
  • 能做更复杂的路由和处理
  • 性能比四层差一点,因为要解析HTTP
  • 例子:Nginx、Traefik、Ingress Controller、云厂商的七层LB

什么时候用四层?什么时候用七层?

  • 简单的TCP/UDP服务,用四层
  • HTTP/HTTPS服务,需要根据域名路径路由的,用七层
  • 一般是四层+七层配合——前面四层做入口,后面七层做路由

5.6 本章小结 ​

这一章我们讲了Service和Ingress——服务发现和流量入口。

总结一下重点:

  • Service解决了服务发现和负载均衡的问题——固定的IP和DNS名字,访问它自动转发到后端Pod
  • Service有四种类型:ClusterIP(内部)、NodePort(节点端口)、LoadBalancer(云负载均衡)、ExternalName(外部别名)
  • Service是通过kube-proxy实现的——iptables或IPVS做DNAT
  • kube-proxy有三种模式:userspace(淘汰)、iptables(常用)、IPVS(高性能)
  • 服务发现主要通过DNS实现——CoreDNS给每个Service分配DNS名字
  • Ingress是统一的七层流量入口——根据域名、路径转发,支持SSL终止、限流等
  • Ingress本身只是规则,真正干活的是Ingress Controller(Nginx、Traefik等)
  • Service是四层的,Ingress是七层的,两者配合使用

理解了Service和Ingress,你就理解了K8s的网络入口——应用跑起来了,也能访问到了。

下一章,我们来讲存储——有状态应用的基石。


第6章 存储管理:有状态应用的基石 ​

前面我们讲的Pod、控制器、Service,主要都是针对无状态应用的——Pod随时可以替换,数据不重要。

但真实世界里,很多应用是有状态的——数据库、消息队列、文件存储……这些应用的数据是命根子,丢了就完了。

容器是"易逝的"——容器删了,里面的数据就没了。那数据怎么办?这就是这一章要解决的问题——存储管理。

6.1 容器存储的难题:为什么存储这么难? ​

在讲K8s存储之前,我们先想想:为什么容器存储是个难题?

6.1.1 容器的临时性 vs 数据的持久性 ​

容器的设计哲学是"不可变基础设施"——镜像不可变,容器随时可以销毁、重建。

但数据是持久的——你不能说容器删了,数据也跟着没了。数据库删了,数据还得在啊。

这就有了矛盾——容器是临时的,数据是持久的。怎么把持久的数据,挂到临时的容器上?

6.1.2 容器迁移 vs 数据跟随 ​

Pod可能被调度到任意节点上,也可能从一个节点迁移到另一个节点。

那数据呢?如果数据存在本地节点上,Pod迁移到别的节点,数据就跟不上了——Pod在新节点上启动,看不到原来的数据了。

这就麻烦了。Pod可以飘,数据得跟着飘——怎么让数据跟着Pod走?

6.1.3 多种多样的存储后端 ​

存储的种类太多了:

  • 本地存储:本地磁盘、SSD
  • 网络文件存储:NFS、CephFS、GlusterFS
  • 块存储:Ceph RBD、iSCSI、云盘
  • 对象存储:S3、MinIO、OSS

不同的存储有不同的接口、不同的特性、不同的性能。K8s怎么统一管理这么多种存储?

如果每种存储都要单独写代码适配,那也太麻烦了——而且用户用起来也麻烦,每种存储用法都不一样。

6.1.4 存储的管理问题 ​

还有管理的问题:

  • 存储怎么分配?用户需要存储的时候,怎么申请?
  • 存储怎么回收?不用了怎么释放?
  • 不同的存储类型怎么管理?
  • 存储的配额怎么控制?

这些都是问题。

K8s的存储体系,就是为了解决这些问题设计的。

6.2 Volume:最基础的存储 ​

K8s里最基础的存储概念是Volume(卷)。

6.2.1 Volume是什么? ​

Volume是什么?简单说:

  • Volume就是Pod里的容器可以挂载的一个目录
  • 这个目录的背后,可以是各种各样的存储
  • Volume的生命周期跟Pod一样——Pod存在,Volume就存在;Pod删了,Volume也就没了(取决于Volume类型)

Docker也有Volume的概念,K8s的Volume跟Docker的类似,但更强大——支持更多种存储类型,而且是Pod级别的(Pod里的容器可以共享Volume)。

6.2.2 常用的Volume类型 ​

K8s支持几十种Volume类型,我们讲几个常用的:

1. emptyDir

  • 空目录,Pod创建的时候创建,Pod删了就没了
  • 存在节点的本地磁盘上
  • Pod里的容器可以共享这个目录
  • 用途:临时空间、缓存、容器之间共享文件

2. hostPath

  • 把节点上的某个目录挂载到Pod里
  • Pod删了,节点上的目录还在
  • 用途:需要访问节点上文件的场景(比如kube-proxy要用iptables,需要访问节点的iptables文件)
  • 注意:慎用!因为Pod调度到哪个节点是不确定的,不同节点上的hostPath内容可能不一样

3. nfs

  • NFS网络文件系统
  • 把NFS的某个目录挂载到Pod里
  • Pod删了,NFS上的数据还在
  • Pod不管调度到哪个节点,都能挂载同一个NFS目录——数据是共享的
  • 优点:简单,支持读写很多Pod同时挂载
  • 缺点:性能一般,单点故障(NFS服务器挂了就都完了)

4. configMap、secret

  • 把ConfigMap或Secret作为Volume挂载到Pod里
  • 这个我们下一章讲

5. persistentVolumeClaim

  • 把PVC(PersistentVolumeClaim)挂载到Pod里
  • 这是最常用的方式,我们后面详细讲

还有很多其他类型:cephfs、rbd、glusterfs、iscsi、awsElasticBlockStore、gcePersistentDisk、azureDisk……基本上你能想到的存储,K8s都支持。

6.2.3 Volume的问题 ​

Volume虽然好用,但有几个问题:

  1. 跟Pod绑定:Volume是Pod的一部分,Pod删了Volume就没了(有些类型数据还在,但Volume对象没了)
  2. 用户需要知道存储细节:你要用NFS,就得知道NFS服务器地址、路径;你要用云盘,就得知道云盘ID——这对用户不友好
  3. 管理混乱:存储资源怎么管理?谁都能随便申请?怎么计费?怎么配额?

为了解决这些问题,K8s设计了PV/PVC体系——把存储的管理和使用分开。

6.3 PV、PVC、StorageClass:持久化存储三剑客 ​

这是K8s存储最核心的三个概念,很多人搞不清它们的关系。我们一个个来讲。

6.3.1 PersistentVolume(PV):存储资源 ​

**PersistentVolume(简称PV)**是什么?它是集群里的一块存储资源——就像节点是计算资源一样,PV是存储资源。

比如:

  • 你有一个NFS共享目录,你可以把它定义成一个PV
  • 你有一个Ceph RBD块,你可以把它定义成一个PV
  • 你有一块云盘,你可以把它定义成一个PV

PV是集群级别的资源,不属于某个命名空间——所有人都可以用。

PV里定义了什么?

  • 存储的大小(容量)
  • 存储的类型(访问模式)
  • 存储后端的信息(NFS地址、Ceph monitor地址等等)

PV就像"存储池子"里的一块存储——管理员提前把存储准备好,定义成PV,放在池子里。

6.3.2 PersistentVolumeClaim(PVC):存储申请 ​

PersistentVolumeClaim(简称PVC)是什么?它是用户对存储的申请——用户说"我要一块10G的存储,能读写的",这就是一个PVC。

PVC是命名空间级别的——每个命名空间里的用户可以创建自己的PVC。

PVC里定义了什么?

  • 需要多大的存储
  • 需要什么访问模式
  • (可选)指定StorageClass

用户不用管存储具体是哪来的——NFS也好,Ceph也好,云盘也好,用户不关心。用户只需要申请,系统会自动给它分配一个合适的PV。

这就是解耦——用户只管申请,管理员管存储,两者分开。

6.3.3 PV和PVC的绑定 ​

PV和PVC是怎么对应上的?

当你创建一个PVC的时候,K8s会去找有没有合适的PV——满足容量要求、满足访问模式要求、StorageClass匹配。

如果找到了,就把这个PV和PVC绑定起来——这个PV就归这个PVC用了,别人不能用了。

如果没找到呢?PVC就一直处于Pending状态——等着有合适的PV出现。

绑定关系是一一对应的——一个PV只能绑定一个PVC,一个PVC只能绑定一个PV。

打个比方:

  • PV就像停车场里的车位——管理员提前画好的
  • PVC就像停车申请——司机说"我要一个车位"
  • 绑定就是——给你分配这个车位,你停这,别人不能停这

6.3.4 StorageClass:动态存储供给 ​

PV/PVC虽然好,但有个问题——管理员得提前创建好PV,不然用户申请PVC的时候找不到PV,就Pending了。

这叫静态供给——管理员提前准备好PV。

但如果存储很多,用户申请很频繁,管理员得不停地创建PV,累死了。

能不能自动创建PV?用户一申请PVC,系统自动创建对应的PV?

可以!这就是StorageClass——动态存储供给。

**StorageClass是什么?**它定义了"存储的种类"——比如"快速SSD存储"、"普通SATA存储"、"冷存储"等等。

每个StorageClass对应一个Provisioner(供给器)——就是负责创建存储的程序。

当你创建PVC的时候,如果指定了StorageClass,那对应的Provisioner就会自动创建一个PV,然后跟PVC绑定。

整个过程全自动——不用管理员手动创建PV了。

这就是动态供给——按需创建,用多少创多少。

打个比方:

  • 静态供给就像——你要喝水,管理员提前给你倒好一杯杯的水放那,你拿一杯
  • 动态供给就像——你要喝水,按一下按钮,自动给你接一杯

显然动态供给方便多了——现在生产环境基本都用动态供给,很少用静态的了。

6.3.5 三者的关系 ​

总结一下PV、PVC、StorageClass的关系:

用户创建PVC → 指定StorageClass → Provisioner自动创建PV → PV和PVC绑定 → Pod挂载PVC

或者静态的:

管理员创建PV → 用户创建PVC → 找到合适的PV → 绑定 → Pod挂载PVC

三者的职责:

  • PV:具体的存储资源,管理员管
  • PVC:用户的存储申请,用户管
  • StorageClass:存储的类型,定义怎么动态创建PV

这样设计的好处是:

  • 解耦:用户不用管存储细节,管理员不用管用户怎么用
  • 可移植:用户的YAML不用改,换个集群换个StorageClass照样能用
  • 自动化:动态供给,不用手动创建PV
  • 多租户:不同命名空间的用户各自申请,互不影响

6.4 访问模式与回收策略 ​

PV有两个重要的属性——访问模式和回收策略。

6.4.1 访问模式(Access Modes) ​

访问模式是什么?就是这个PV可以怎么挂载——能不能同时被多个节点挂载,是读还是写。

有三种访问模式:

1. ReadWriteOnce(RWO)

  • 读写模式,只能被一个节点挂载
  • 就是说,同一时间,只有一个节点上的Pod能挂载这个PV,进行读写
  • 大部分块存储都是这种模式——比如云盘、RBD

2. ReadOnlyMany(ROX)

  • 只读模式,可以被多个节点挂载
  • 多个节点的Pod都能挂载,但只能读,不能写
  • 适合共享只读数据的场景

3. ReadWriteMany(RWX)

  • 读写模式,可以被多个节点挂载
  • 多个节点的Pod都能挂载,都能读写
  • 文件存储一般支持这种——比如NFS、CephFS

注意:访问模式是PV的能力,不是强制的——存储本身支持什么模式,才能用什么模式。不是你想设成什么就设成什么。

比如块存储一般只支持RWO,因为块设备只能挂载给一台机器。文件存储一般支持RWX,因为多个机器可以同时挂载同一个目录。

6.4.2 回收策略(Reclaim Policy) ​

回收策略是什么?就是PVC删了之后,对应的PV怎么办——数据怎么处理。

有三种回收策略:

1. Retain(保留)

  • PVC删了,PV还在,数据也还在
  • PV变成Released状态,不能再被绑定了
  • 需要管理员手动处理数据,然后手动删除PV
  • 最安全,数据不会丢
  • 适合重要数据

2. Delete(删除)

  • PVC删了,PV也自动删了,存储上的数据也删了
  • 动态供给的PV默认一般是Delete
  • 方便,但危险——删错了数据就没了
  • 适合不重要的数据、临时数据

3. Recycle(回收)

  • PVC删了,PV里的数据被清空(rm -rf),然后PV变成Available状态,可以重新被绑定
  • 现在基本不用了,已经废弃了

生产环境重要数据,建议用Retain——安全,不会误删数据。

6.5 主流存储方案对比 ​

K8s支持的存储方案太多了,怎么选?我们来对比一下主流的几种。

6.5.1 本地存储 ​

代表:emptyDir、hostPath、Local PV

优点:

  • 性能好——本地磁盘,IO延迟低
  • 简单——不用搭额外的存储集群
  • 免费——用节点自带的磁盘就行

缺点:

  • 不能跟着Pod走——Pod调度到别的节点,数据就没了(或者说看不到了)
  • 可用性差——节点挂了,数据就没了
  • 容量有限——节点磁盘多大就多大

适用场景:

  • 临时存储、缓存
  • 对性能要求极高,但数据不重要的
  • DaemonSet类的应用,每个节点用自己的本地存储

6.5.2 网络文件存储(NAS类) ​

代表:NFS、CephFS、GlusterFS、云NAS

优点:

  • 支持RWX——多个Pod同时读写
  • 数据共享方便
  • Pod可以随便飘——不管调度到哪个节点,都能挂载
  • 易用——就像用普通目录一样

缺点:

  • 性能一般——网络文件系统,IO延迟比本地高
  • 元数据操作慢——小文件多的时候性能差
  • 单点故障(NFS的话)——NFS服务器挂了全完了

适用场景:

  • 需要共享存储的应用
  • 多读少写的场景
  • 静态文件、配置文件、日志等
  • 对性能要求不是特别高的有状态应用

6.5.3 块存储(Block Storage) ​

代表:Ceph RBD、iSCSI、云盘(EBS/SSD云盘)

优点:

  • 性能好——块存储,比文件存储性能高
  • 支持快照、克隆——方便备份恢复
  • 可靠性高——一般都有副本机制

缺点:

  • 一般只支持RWO——只能挂载给一个节点
  • 不能直接共享——要共享的话得上面再装文件系统
  • 比文件存储稍微复杂一点

适用场景:

  • 数据库——MySQL、PostgreSQL等
  • 对性能要求高的有状态应用
  • 需要快照、克隆的场景

6.5.4 对象存储(Object Storage) ​

代表:S3、MinIO、OSS、COS

优点:

  • 海量存储——想存多少存多少
  • 便宜——比块存储、文件存储都便宜
  • 高可用、高可靠——一般都是多副本、多AZ
  • 支持HTTP接口—— anywhere都能访问

缺点:

  • 不能直接挂载成目录——要通过S3 API访问
  • 不支持随机写——对象是整个上传下载的
  • 延迟比块存储、文件存储高

适用场景:

  • 图片、视频、备份等静态文件
  • 大数据、日志存储
  • 静态网站托管
  • 海量非结构化数据

6.5.5 怎么选? ​

选型决策树:

  1. 数据是临时的、不重要的?→ 本地存储(emptyDir)
  2. 需要多个Pod同时读写共享?→ 网络文件存储(NFS/CephFS)
  3. 是数据库类应用,对性能要求高?→ 块存储(RBD/云盘)
  4. 存海量非结构化数据,比如图片视频?→ 对象存储(S3/MinIO)
  5. 用云厂商的话,优先用云厂商的托管存储——省心,不用自己维护

还有个原则:能不用分布式存储就不用——分布式存储复杂,运维成本高。如果云厂商有托管的,就用托管的,别自己搭。

6.6 CSI:容器存储接口标准 ​

前面我们说了,K8s支持几十种存储类型。那这些存储的代码都在哪?都在K8s的代码里吗?

最早的时候,是的——所有存储的代码都在K8s的源码里,叫"in-tree"(树内)驱动。

但这样有很多问题:

  • K8s的代码越来越臃肿——几十种存储的代码都塞进去
  • 存储厂商要更新驱动,得等K8s发版——太慢了
  • 出了bug不好修——得改K8s的代码
  • 新的存储要支持,得往K8s里加代码——太麻烦

为了解决这些问题,K8s推出了CSI(Container Storage Interface,容器存储接口)——一个标准的存储接口。

6.6.1 CSI是什么? ​

CSI是什么?简单说,它就是一个标准接口——只要你的存储实现了CSI接口,就能跟K8s对接,K8s就能用你的存储。

就像一个插头标准——只要你的插头符合标准,就能插进插座里,不管你是什么电器。

CSI定义了存储驱动应该实现哪些接口:

  • 创建/删除卷
  • 挂载/卸载卷
  • 快照/克隆
  • 扩容
  • ……

存储厂商只要按照CSI标准写一个驱动,就能对接K8s——不用改K8s的代码。

这就是**"out-of-tree"(树外)**驱动——驱动代码不在K8s源码里,独立的。

6.6.2 CSI的好处 ​

CSI有什么好处?

  1. 解耦:存储驱动跟K8s核心代码分开——K8s不用管存储怎么实现,存储厂商不用管K8s怎么实现
  2. 独立迭代:存储驱动可以自己发版、自己更新,不用等K8s发版
  3. 统一标准:所有存储都用同一套接口,用户用起来都一样
  4. 功能更丰富:CSI支持快照、克隆、扩容等高级功能,in-tree驱动支持的有限

现在新的存储驱动基本都是CSI的了,in-tree的驱动正在逐步被淘汰。K8s的目标是——所有存储都用CSI,把in-tree的都去掉。

6.7 StatefulSet + PV:有状态应用的最佳实践 ​

前面讲StatefulSet的时候提到了,StatefulSet配合PV,是部署有状态应用的常用方式。

6.7.1 VolumeClaimTemplate ​

StatefulSet怎么用PV?用volumeClaimTemplates——就是PVC模板。

什么意思?就是StatefulSet里定义一个PVC的模板,每个Pod创建的时候,会根据这个模板自动创建一个对应的PVC。

比如:

  • StatefulSet名字叫mysql,有3个副本
  • volumeClaimTemplate定义了一个叫data的PVC模板,10G
  • 那创建Pod的时候,会自动创建3个PVC:data-mysql-0、data-mysql-1、data-mysql-2
  • 每个Pod挂载自己对应的PVC

这样每个Pod都有自己独立的存储——Pod重建的时候,PVC还在,数据不会丢。Pod迁移到别的节点,PVC也跟着走(如果存储支持的话)。

这就是StatefulSet的稳定存储——每个Pod对应自己的PVC,一一对应,名字固定。

6.7.2 部署有状态应用的注意事项 ​

用StatefulSet部署有状态应用,要注意什么?

  1. 存储要支持:你的存储得支持动态供给,或者提前创建好PV——不然PVC会Pending
  2. 数据备份:重要数据一定要定期备份——PV不是万能的,存储集群也可能挂
  3. 应用本身的集群逻辑:StatefulSet只是提供了稳定的标识和存储,应用的集群逻辑(比如主从切换、数据同步)还要应用自己处理
  4. 不要随便删PVC:删了PVC数据可能就没了(取决于回收策略)
  5. 扩容缩容要小心:有状态应用的扩容缩容不像无状态那么简单,要考虑数据、集群状态

6.7.3 为什么推荐用Operator? ​

前面我们提到过,真正的有状态应用,最好用Operator来部署,而不是直接用StatefulSet。

为什么?因为StatefulSet太基础了——它只负责稳定的标识、存储、有序部署。但它不懂应用的逻辑:

  • 数据库的主从切换,它不会
  • 集群的成员管理,它不会
  • 备份恢复,它不会
  • 升级策略,它不懂

而Operator是什么?Operator就是把运维专家的知识,编码成软件——它懂这个应用怎么部署、怎么扩容、怎么备份、怎么升级、怎么故障恢复。

比如PostgreSQL Operator,你只要说"我要一个3节点的PostgreSQL集群",Operator就自动帮你部署好、配置好主从、做好监控、做好备份。出问题了它自动修复。

这比你自己写StatefulSet、自己写脚本运维,强太多了。

所以,生产环境部署有状态应用,优先找对应的Operator,别自己裸写StatefulSet——坑太多了。

6.8 扩展知识:分布式存储系统原理 ​

讲到存储,我们扩展一下知识——分布式存储系统的基本原理。了解了这些,你选存储、用存储的时候心里更有数。

6.8.1 数据冗余:副本 vs 纠删码 ​

分布式存储为了保证数据可靠性,都会做数据冗余——一份数据存多份,坏了一份还有别的。

数据冗余主要有两种方式:

1. 副本(Replication)

  • 一份数据存多个副本,比如3副本
  • 优点:简单,读性能好(可以从副本读),恢复快
  • 缺点:空间利用率低——3副本就是3倍的空间开销
  • 适用场景:对性能要求高、数据量不是特别大的

2. 纠删码(Erasure Coding,EC)

  • 把数据分成N块,再加上M块校验块,总共N+M块
  • 只要有任意N块在,就能恢复出完整数据
  • 比如4+2,就是4块数据+2块校验,总共6块,最多允许坏2块
  • 优点:空间利用率高——4+2的话,空间利用率是4/6≈67%,比3副本的33%高多了
  • 缺点:计算开销大(要编码解码),恢复慢,写性能差
  • 适用场景:冷数据、归档数据,对空间利用率要求高,对性能要求不高的

两种方式各有优劣,根据场景选。热数据用副本,冷数据用纠删码。

6.8.2 一致性:强一致 vs 最终一致 ​

分布式存储还有个一致性的问题——数据写到多个副本,怎么保证一致性?

强一致性:

  • 写成功了,所有副本都更新了
  • 任何时候读,读到的都是最新的数据
  • 优点:数据一致,不会有问题
  • 缺点:写延迟高,可用性差(副本挂了可能就写不了了)
  • 例子:Ceph RBD、HDFS

最终一致性:

  • 写成功了,可能只有部分副本更新了,过一段时间后所有副本都会更新
  • 刚写完可能读到旧数据
  • 优点:写性能好,可用性高
  • 缺点:可能读到旧数据
  • 例子:S3、很多对象存储

不同的存储一致性模型不一样,根据你的应用需求选。

6.8.3 存储的三大指标:容量、性能、可靠性 ​

选存储的时候,主要看三个指标:

  1. 容量:能存多少数据
  2. 性能:IOPS(每秒读写次数)、吞吐量(每秒读写多少数据)、延迟(IO需要多久)
  3. 可靠性:数据会不会丢,可用性怎么样

这三个指标,跟成本是相关的——越好越贵。你要根据你的业务需求,在这三者之间做平衡。

比如:

  • 数据库:要性能、要可靠性,容量可以不用太大 → 用SSD块存储,多副本
  • 备份数据:要容量、要可靠性,性能无所谓 → 用纠删码的对象存储
  • 临时缓存:要性能,容量和可靠性都无所谓 → 用本地存储

理解了这些,你选存储的时候就不会盲目了。

6.9 本章小结 ​

这一章我们讲了K8s的存储体系。

总结一下重点:

  • 容器存储的难题:容器临时性 vs 数据持久性、容器迁移 vs 数据跟随、多种存储后端、管理问题
  • Volume:最基础的存储,Pod级别的,支持很多种类型
  • PV/PVC/StorageClass:持久化存储三剑客——PV是存储资源,PVC是用户申请,StorageClass是动态供给
  • 访问模式:RWO(单节点读写)、ROX(多节点只读)、RWX(多节点读写)
  • 回收策略:Retain(保留)、Delete(删除)、Recycle(回收,已废弃)
  • 主流存储方案对比:本地存储、网络文件存储、块存储、对象存储,各有适用场景
  • CSI:容器存储接口标准,把存储驱动跟K8s核心解耦
  • StatefulSet + VolumeClaimTemplate:有状态应用的基础部署方式
  • 生产环境优先用Operator部署有状态应用

存储是K8s里比较复杂的一块,也是生产环境最容易出问题的一块。理解了存储的基本原理和各种方案的特点,你才能根据业务场景选对存储。

下一章,我们来讲配置与密钥管理——ConfigMap和Secret。


基于 Vite 强力驱动 | 纯静态轻量托管