Kubernetes 完全指南:从入门到生产实战
核心主张
Kubernetes 不仅仅是一个容器编排工具,它是云原生时代的"操作系统",是分布式系统基础设施的事实标准。学习K8s不能只停留在"会用"的层面,而要深入理解其设计哲学——声明式API、控制循环、不可变基础设施——这些思想远比具体的API更有价值。从第一性原理出发,建立完整的K8s知识体系,才能在快速变化的云原生生态中立于不败之地。
作者声音标签
硬核但有趣、深入但不晦涩、实用且有对比、既有原理也有实战、故事化表达、工程师视角
目标读者
- 初学者:有一定Linux和Docker基础,想系统学习K8s的开发/运维工程师
- 中级工程师:正在使用K8s但知识碎片化,想补全体系、深入理解原理
- 资深工程师:有多年运维经验,希望了解K8s前沿生态、最佳实践和性能调优
- 技术讲师:需要高质量教学材料的高校或培训机构讲师
阅读本书需要的前置知识:熟悉Linux基本操作、了解Docker容器概念、有一定编程基础。
章节规划
| 章节 | 标题 | 核心问题 | 用户收获 | 目标字数 |
|---|---|---|---|---|
| ch01 | 容器编排的前世今生:为什么我们需要K8s | 容器解决了什么问题?为什么还需要容器编排?K8s为什么赢了? | 理解K8s诞生的历史背景和技术必然性,建立技术演进的大局观 | 3000 |
| ch02 | K8s架构全景图:上帝视角看集群 | K8s集群由哪些组件组成?它们各自负责什么?如何协同工作? | 建立K8s架构的完整认知,理解控制平面与数据平面的分工 | 3500 |
| ch03 | Pod:K8s世界的原子单位 | 为什么是Pod而不是直接用容器?Pod的本质是什么? | 深入理解Pod设计理念,掌握容器设计模式 | 3500 |
| ch04 | 工作负载控制器:从Pod到应用 | 为什么不直接用Pod?各种控制器分别适用于什么场景? | 掌握Deployment、StatefulSet、DaemonSet、Job等控制器的使用场景和原理 | 3000 |
| ch05 | Service与Ingress:服务发现与流量入口 | Pod IP不持久怎么办?外部流量怎么进集群? | 理解Service和Ingress的底层实现,掌握服务发现机制 | 3000 |
| ch06 | 存储管理:有状态应用的基石 | 容器存储为什么难?各种存储方案怎么选? | 掌握PV/PVC/StorageClass体系,了解主流存储方案对比 | 3500 |
| ch07 | 配置与密钥管理:ConfigMap与Secret | 配置为什么要与镜像分离?Secret真的安全吗? | 掌握配置管理最佳实践,了解密钥管理方案 | 3000 |
| ch08 | 调度器深度解析:Pod去哪儿了 | 调度器是怎么工作的?如何影响Pod的调度决策? | 深入理解调度算法,掌握节点选择、亲和性、污点等调度策略 | 3500 |
| ch09 | K8s网络模型:容器网络的黑盒揭秘 | Pod网络是怎么通的?各种CNI有什么区别? | 深入理解容器网络底层原理,掌握主流CNI方案对比 | 4000 |
| ch10 | 安全与权限:RBAC、NetworkPolicy与Pod Security | K8s安全怎么做?如何防范各种安全风险? | 建立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 |
| ch17 | Gateway API:下一代流量治理标准 | Ingress有什么问题?Gateway API好在哪? | 了解Gateway API的设计理念和核心概念 | 3000 |
| ch18 | K8s生态全景:CNCF项目地图 | 云原生生态有哪些项目?怎么选? | 了解CNCF生态全景,建立技术选型能力 | 3000 |
| ch19 | GitOps与持续交付:Argo CD与Flux | GitOps是什么?为什么是最佳实践? | 掌握GitOps理念和工具,实现声明式持续交付 | 3500 |
| ch20 | Serverless与弹性伸缩:KEDA、Knative与Virtual Kubelet | K8s上怎么做Serverless?怎么自动伸缩? | 了解Serverless在K8s上的实现,掌握各种自动伸缩方案 | 3500 |
| ch21 | Service Mesh:Istio与eBPF时代的服务治理 | 服务网格是什么?需要吗?选哪个? | 理解Service Mesh理念,掌握Istio、Linkerd、Cilium对比 | 3500 |
| ch22 | AI与K8s:GPU调度与大模型训练 | AI训练为什么需要K8s?GPU怎么调度? | 了解K8s在AI/ML场景的应用,掌握GPU调度方案 | 3000 |
| ch23 | K8s发行版对比:选哪个发行版好 | 为什么有这么多发行版?怎么选? | 了解主流发行版特点,掌握选型决策方法 | 2500 |
| ch24 | 面试与认证指南:CKA/CKAD/CKS通关秘籍 | K8s认证怎么考?面试常考什么? | 掌握CKA/CKAD/CKS备考方法,了解面试高频考点 | 2500 |
| ch25 | 未来展望:K8s的下一个十年 | K8s未来会怎么发展?有什么趋势? | 了解云原生前沿趋势,建立技术发展预判能力 | 2000 |
章节依赖关系
| 章节 | 前置章节 |
|---|---|
| ch01 | 无 |
| ch02 | ch01 |
| ch03 | ch02 |
| ch04 | ch03 |
| ch05 | ch04 |
| ch06 | ch04 |
| ch07 | ch04 |
| ch08 | ch03 |
| ch09 | ch05 |
| ch10 | ch03, ch05, ch09 |
| ch11 | ch03, ch05 |
| ch12 | ch04 |
| ch13 | ch02, ch10 |
| ch14 | ch02, ch08, ch09 |
| ch15 | ch11, ch13 |
| ch16 | ch02, ch04 |
| ch17 | ch05 |
| ch18 | ch02 |
| ch19 | ch04, ch12 |
| ch20 | ch04, ch08 |
| ch21 | ch05, ch09, ch10 |
| ch22 | ch08, ch06 |
| ch23 | ch02, ch13 |
| ch24 | ch01, ch02, ch03, ch04, ch05, ch06, ch07, ch08, ch09, ch10, ch11, ch12, ch13, ch14, ch15, ch16, ch17, ch18, ch19, ch20, ch21 |
| ch25 | ch18, 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 控制平面组件
控制平面有四个核心组件:
- kube-apiserver:所有请求的唯一入口,集群的"前台"
- etcd:集群的数据库,存所有集群数据,"记忆中枢"
- kube-scheduler:调度器,决定Pod放哪台机器上,"调度大脑"
- kube-controller-manager:控制器管理器,跑各种控制器,"控制循环"
还有一个可选的:
- cloud-controller-manager:跟云厂商对接的控制器,用云服务的时候才需要
2.1.2 数据平面组件
每个工作节点(Worker Node)上,都有这些组件:
- kubelet:节点上的"管家",跟控制平面通信,管理本节点的Pod
- kube-proxy:节点上的"网络管理员",负责Service的网络规则
- 容器运行时(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的存储:
强一致性:etcd基于Raft共识算法,能保证数据的强一致性。这对K8s太重要了——所有组件看到的状态必须是一样的,不然就乱套了。
高可用:etcd可以部署成集群,只要大部分节点活着,就能正常工作。
Watch机制:etcd支持Watch——你可以监听某个key的变化,一旦变化就会收到通知。这对K8s的控制器模式太重要了——控制器不用轮询,等着通知就行,效率高。
简单:就是个键值存储,API简单,性能好。
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,一定要注意:
- 一定要高可用:至少3个节点,别用单节点
- 磁盘性能很重要:etcd对磁盘IO延迟非常敏感,一定要用SSD,别用机械硬盘
- 要定期备份:etcd的数据要定期备份,万一挂了还能恢复
- 别直接改etcd的数据:所有变更都应该通过apiserver做,别直接操作etcd——容易把数据搞坏
面试常考点:etcd用的什么共识算法?Raft。为什么etcd节点数是奇数?因为要多数派,奇数个性价比最高。
2.3 kube-apiserver:所有请求的唯一入口
2.3.1 apiserver是干什么的?
kube-apiserver是控制平面的"前台",是所有请求的唯一入口。
不管你是用kubectl命令,还是控制器、调度器、kubelet要读写集群数据——所有请求都要经过apiserver。
为什么要搞个统一入口?直接让各个组件去读写etcd不行吗?
不行,因为apiserver做了很多重要的事情:
- 认证(Authentication):你是谁?验证你的身份
- 授权(Authorization):你能干什么?你有没有权限做这个操作
- 准入控制(Admission Control):这个请求合不合规?比如能不能用特权容器、资源限制够不够
- API注册与发现:有哪些API、哪些资源类型,都由apiserver管理
- CRUD操作:对etcd里的数据进行增删改查
- 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的工作流程
一个请求从发出来到完成,经过哪些步骤?
- 认证:验证请求者的身份(证书、token、用户名密码等)
- 授权:验证这个身份有没有权限做这个操作(RBAC)
- 准入控制(Mutating):修改请求——比如给Pod加默认的资源限制、注入Sidecar
- 准入控制(Validating):验证请求合不合规——比如资源限制有没有超过配额、格式对不对
- 写入etcd:验证都通过了,把数据写到etcd里
- 返回结果:告诉客户端操作成功
你看,一个请求要经过这么多关卡,才能真正写入集群。这就是为什么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 控制器是怎么工作的?
所有控制器的工作模式都一样:
- Watch:通过apiserver的Watch机制,监听它负责的资源的变化
- 对比:拿到当前状态,跟期望状态对比
- 调整:如果不一致,就做调整(创建、删除、更新资源)
- 循环:不断重复上面的步骤
这个模式有什么好处?
- 自愈:出了问题自动修,不用人工干预
- 最终一致:不管中间出什么问题,最终都会达到期望状态
- 解耦:每个控制器只负责自己的事儿,互不影响
- 可扩展:你可以自己写控制器,管理自己的资源
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是每个节点上的"管家",它是控制平面在节点上的代理人。
它的主要工作:
- 跟apiserver通信:注册节点,上报节点状态,接收任务
- 管理Pod生命周期:创建、启动、停止、重启Pod
- 监控Pod状态:监控Pod的健康状态,上报给apiserver
- 执行探针:执行Liveness、Readiness探针
- 管理存储卷:挂载Volume,处理存储相关的事儿
- 上报指标:上报节点和Pod的资源使用情况
简单说,控制平面下达的命令,到了节点上,都是kubelet来执行的。
2.6.2 kubelet是怎么创建Pod的?
Pod的创建流程大概是这样的:
- 用户通过apiserver创建Pod
- 调度器给Pod分配节点
- apiserver更新Pod的NodeName字段
- 对应节点上的kubelet Watch到有新的Pod分配到自己这了
- kubelet告诉容器运行时:"给我创建这些容器"
- 容器运行时拉取镜像、创建容器、启动容器
- kubelet监控容器状态,上报给apiserver
你看,kubelet不直接跑容器,它是告诉容器运行时去跑。
2.6.3 Pod清单(Pod Manifest)
kubelet怎么知道要跑哪些Pod?
主要有两个来源:
- 从apiserver来的:这是主要来源,就是我们正常创建的Pod
- 本地静态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定义了两个标准:
- 镜像标准(Image Specification):容器镜像应该是什么格式
- 运行时标准(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 为什么这个模式这么强大?
这个模式为什么这么牛?我觉得有几个原因:
自愈能力强:出了问题自动修,不用人管。机器挂了?没关系,控制器会把Pod调度到别的机器上。Pod挂了?没关系,控制器会重启它。
容错性好:控制器偶尔挂了没关系,重启了接着来——反正它只看当前状态和期望状态,不管之前发生了什么。
可扩展性好:想加新功能?写个新控制器就行,不用改现有代码。想管理新资源?定义个CRD,写个控制器就行。
易于推理:系统的行为是可预测的——它总是朝着期望状态走。你不用担心中间状态,只要保证期望状态是对的就行。
理解了声明式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这么麻烦。"
把多个进程塞到一个容器里,有很多问题:
日志混乱:多个进程的日志都混在一起,很难区分。Docker的日志收集是收集容器的stdout/stderr,多个进程的输出混在一起,你怎么分?
资源管理困难:你没法单独给某个进程限制资源,只能给整个容器限制。如果某个进程内存泄漏了,会把整个容器的内存吃光,影响其他进程。
监控困难:你没法单独监控某个进程的状态,只能监控整个容器。哪个进程挂了?你不知道。
生命周期管理困难:一个进程挂了,其他进程怎么办?是整个容器重启还是怎样?容器的重启策略是针对整个容器的,不是单个进程。
职责不清:容器的设计哲学就是单进程,你硬要塞多个,就违背了这个设计,各种问题都会来。
而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,经历了什么?
- 用户创建Pod:通过apiserver提交Pod定义
- apiserver写入etcd:验证通过后,把Pod数据写入etcd
- 调度器调度:scheduler Watch到新的Pod,给它分配节点,更新Pod的NodeName
- kubelet接收任务:对应节点的kubelet Watch到有新的Pod分配到自己这了
- 执行Init Container:按顺序执行所有Init Container,都成功了才继续
- 创建主容器:创建所有主容器的容器
- 启动容器:启动容器,开始运行
- 执行postStart钩子:容器启动后,执行postStart钩子(如果有的话)
- Running状态:所有容器都启动了,Pod进入Running状态
这个过程中,如果哪一步出了问题,Pod就会卡在对应的状态。排查Pod问题的时候,你就可以按这个流程一步步查。
3.3.4 Pod的终止过程
Pod的终止过程也很重要,很多人都搞不懂为什么Pod删了半天还在。
- 用户删除Pod:apiserver收到删除请求,把Pod的删除时间戳设上
- kubelet收到删除通知:开始终止Pod
- 执行preStop钩子:给容器发SIGTERM之前,先执行preStop钩子(如果有的话)
- 发送SIGTERM信号:给容器里的进程发SIGTERM信号,告诉它"该退出了"
- 等待优雅终止:等待terminationGracePeriodSeconds这么长时间,默认30秒
- 发送SIGKILL信号:如果到时间容器还没退出,就发SIGKILL强杀
- 删除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 探针的检查方式
探针有几种检查方式:
- HTTP GET:发一个HTTP GET请求,返回200-399之间的状态码就算成功
- TCP Socket:尝试连接容器的某个端口,能连上就算成功
- Exec:在容器里执行一个命令,退出码0就算成功
- 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才合理?
- 不要不设资源:不设的话就是BestEffort,随时可能被杀,生产环境绝对不能这样
- 重要应用设成Guaranteed:核心业务,requests和limits设成一样的,保证资源
- 一般应用用Burstable:requests设成平时的用量,limits设成高峰的用量,留一定余量
- requests要合理:设太小了调度不进去,设太大了浪费资源
- limits不要设得太离谱:跟requests差太多的话,超卖严重,容易出问题
- 根据监控调整:看实际的资源使用情况,不断调整requests和limits
资源设置是个技术活,需要根据实际情况不断调优。
3.6 扩展知识:Linux命名空间与cgroups深度解析
前面我们说了,容器的本质是Linux的命名空间(Namespace)和cgroups。Pod的共享,本质上也是命名空间的共享。这一节我们深入讲讲这两个东西。
3.6.1 命名空间(Namespace):隔离的魔法
命名空间是什么?它是Linux内核提供的一种机制——让进程觉得自己拥有整个系统的资源。
就像给进程戴上了"有色眼镜"——它看到的世界,跟真实的世界不一样。
Linux有好几种命名空间:
PID命名空间:隔离进程ID。在不同的PID命名空间里,进程ID是独立的——你在容器里看到的PID 1,在宿主机上可能是PID 12345。而且容器里的进程看不到宿主机的进程。
网络命名空间:隔离网络栈。每个网络命名空间有自己的网络设备、IP地址、端口空间、路由表、iptables规则。Pod里的容器共享同一个网络命名空间,所以它们的IP是一样的,localhost就能互相访问。
挂载命名空间:隔离文件系统挂载点。每个挂载命名空间有自己的挂载树——容器里的根目录是镜像里的根目录,跟宿主机的根目录不一样。
UTS命名空间:隔离主机名和域名。每个容器可以有自己的主机名。
IPC命名空间:隔离System V IPC和POSIX消息队列。不同命名空间里的进程不能直接用IPC通信。
用户命名空间:隔离用户和组ID。容器里的root用户,在宿主机上可能是个普通用户——这样更安全。
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的工作原理很简单:
- 观察当前有多少个Pod在运行
- 跟期望的副本数对比
- 少了就创建新的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的镜像版本的时候:
- Deployment创建一个新的ReplicaSet(用新镜像)
- 新的ReplicaSet慢慢增加副本数
- 旧的ReplicaSet慢慢减少副本数
- 最后新的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)
所以更新的过程大概是这样的:
- 先启动2个新Pod(现在12个:10旧 + 2新)
- 然后停掉2个旧Pod(现在10个:8旧 + 2新)
- 再启动2个新Pod(12个:8旧 + 4新)
- 再停掉2个旧Pod(10个:6旧 + 4新)
- ……
- 直到全部替换完
这样整个过程中,服务始终是可用的,而且资源使用也不会超太多。
这两个参数可以根据你的需求调整:
- 如果你想更新快一点,就把maxSurge设大一点——多启动一些新的,替换得快
- 如果你想保证可用性,就把maxUnavailable设小一点——保证更多的Pod可用
- 如果你资源紧张,就把maxSurge设小一点——不要同时跑太多Pod
4.2.4 回滚
Deployment更新出问题了怎么办?回滚啊!
Deployment的回滚非常简单——因为旧的ReplicaSet还在。
回滚的时候:
- 把旧的ReplicaSet的副本数加起来
- 把新的ReplicaSet的副本数减下去
- 就回到旧版本了
就是这么简单。而且你可以查看历史版本,回滚到任意一个版本。
这就是声明式的好处——所有版本都记录在案,想回滚就回滚。
4.2.5 什么时候用Deployment?
Deployment适合什么场景?
- 无状态应用——Web服务、API服务、微服务……这些都是无状态的,Pod随时可以替换,哪个实例都一样
- 需要滚动更新、回滚的应用
- 需要自动伸缩的应用
大部分应用都是无状态的,所以Deployment是最常用的控制器。
4.3 StatefulSet:有状态应用的解决方案
Deployment很好用,但它有个前提——应用是无状态的,Pod是可以随便替换的。
但有些应用是有状态的——比如数据库、消息队列、分布式存储……这些应用的Pod不是随便就能替换的。
4.3.1 有状态应用的痛点
有状态应用有什么特点?为什么Deployment搞不定?
稳定的网络标识:每个Pod要有固定的名字、固定的DNS名字——因为其他Pod要通过固定的地址找到它。比如数据库的主从,从库要知道主库的地址,如果主库的名字变了,那就乱了。
稳定的持久化存储:每个Pod要有自己的存储,Pod重启或者迁移到别的节点,存储还要跟着它。如果Pod是随便替换的,存储跟Pod对不上,那就完了——数据都丢了。
有序的部署和伸缩:有状态应用一般不能同时启动所有实例——要按顺序来,一个一个启动。比如数据库集群,要先启主库,再启从库。伸缩的时候也是,要按顺序加,按顺序删。
有序的滚动更新:更新的时候也要按顺序来,不能随便更新。
这些Deployment都做不到——Deployment里的Pod都是一样的,名字是随机的,存储也是共享的(如果有的话),启动也是一起启动的。
所以就有了StatefulSet——专门为有状态应用设计的控制器。
4.3.2 StatefulSet的特点
StatefulSet有什么特点?
稳定的Pod名字:StatefulSet里的Pod,名字是固定的,格式是
{statefulset名}-{序号}。比如叫mysql的StatefulSet,有3个副本,那Pod名字就是mysql-0、mysql-1、mysql-2。序号从0开始。而且Pod重启或者重建,名字不变——mysql-0挂了,重建出来还是叫mysql-0。
稳定的DNS名字:每个Pod都有固定的DNS域名,格式是
{pod名}.{service名}.{namespace}.svc.cluster.local。其他Pod可以通过这个固定的域名找到它。这就解决了服务发现的问题——不管Pod在哪,IP怎么变,域名是不变的。
稳定的持久化存储:每个Pod对应自己的PVC(PersistentVolumeClaim)。Pod重建的时候,PVC还在,所以数据不会丢。Pod迁移到别的节点,存储也跟着走。
而且PVC的名字也是固定的——
{pvc模板名}-{pod名},跟Pod一一对应。有序的部署和伸缩:部署的时候,按序号从小到大,一个一个来——0号启动好了再启1号,1号好了再启2号。伸缩的时候也是,加的话从大到小加,删的话从大到小删——保证序号是连续的。
有序的滚动更新:更新的时候,从大到小,一个一个更新——先更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?
典型的使用场景:
- 日志采集:比如Fluentd、Filebeat、Logstash——每个节点都要跑一个,收集本节点的日志
- 监控Agent:比如Prometheus Node Exporter、Zabbix Agent——每个节点都要跑一个,采集本节点的指标
- 网络插件:比如Calico、Cilium的Agent——每个节点都要跑,负责本节点的网络
- 存储插件:比如Ceph的RBD插件、CSI插件——每个节点都要跑,负责本节点的存储挂载
- 安全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要注意几个问题:
任务要幂等:因为Job可能会重试,同一个任务可能跑多次——所以你的任务要保证幂等,跑多次结果一样,不会出问题。
并发策略:CronJob有并发策略——如果上一个任务还没跑完,下一个时间点到了,怎么办?
- Allow:允许同时跑(默认)
- Forbid:不允许,跳过这次
- Replace:替换掉上一个,用新的
根据你的任务类型选合适的。
历史记录限制:CronJob会保留一些历史的Job,太多了会占资源,可以设置保留多少个。
4.6 控制器对比:怎么选?
这么多控制器,怎么选?我给你总结一下:
| 控制器 | 适用场景 | Pod特点 | 存储 | 网络标识 | 有序性 |
|---|---|---|---|---|---|
| Deployment + ReplicaSet | 无状态应用 | 完全一样,可随便替换 | 共享存储(可选) | 随机名字,Service负载均衡 | 无序 |
| StatefulSet | 有状态应用 | 有固定序号,一一对应 | 每个Pod独立存储 | 固定DNS名字 | 有序部署、更新、伸缩 |
| DaemonSet | 每个节点跑一个 | 每个节点一个 | 可选 | 节点级 | 无 |
| Job | 一次性任务 | 跑完就退出 | 可选 | 无 | 可并行 |
| CronJob | 定时任务 | 定时跑,跑完退出 | 可选 | 无 | 定时触发 |
选型决策树:
- 是长期运行的服务吗?
- 是 → 继续
- 否 → 是一次性的?→ Job
- 否 → 是定时的?→ CronJob
- 每个节点都要跑一个吗?
- 是 → DaemonSet
- 否 → 继续
- 是有状态应用吗?
- 是 → 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的工作流程:
- 先List一次,把所有资源都拉下来,存在本地缓存
- 然后Watch,有变化就更新本地缓存
- 有变化的时候,把事件放到队列里
- 控制器从队列里拿事件,处理
这样既高效,又可靠——就算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 为什么这个模式这么强大?
控制循环模式为什么这么牛?我觉得有几个原因:
自愈能力强:不管出什么问题,只要控制器还在,它就会不断地调整,最终达到期望状态。Pod挂了?没关系,控制器会重建。节点挂了?没关系,控制器会把Pod调度到别的节点。
容错性好:控制器偶尔挂了没关系,重启了接着来——它只看当前状态和期望状态,不管之前发生了什么。就算中间丢了几个事件也没关系,因为最终状态是对的就行。
可组合性好:控制器可以嵌套——Deployment管ReplicaSet,ReplicaSet管Pod。每层只做自己的事,职责清晰。
可扩展性好:你可以自己写控制器,管理自己的资源。不用改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的时候:
- 你的数据包到了节点的网络栈
- iptables(或者IPVS)匹配到这个目的IP是Service IP
- 做DNAT(目的地址转换),把目的IP改成某个后端Pod的IP
- 数据包就发给了那个Pod
返回的数据包再做SNAT(源地址转换),这样Pod看到的源IP是节点的IP,返回的时候能正确返回。
整个过程对应用是透明的——应用不知道背后有多少个Pod,也不知道自己访问的是哪个Pod,它只知道访问Service IP就行了。
这就是Service的实现原理——通过iptables/IPVS做DNAT,实现负载均衡。
5.1.5 kube-proxy的三种模式对比
前面我们提过kube-proxy有三种模式:userspace、iptables、IPVS。这里我们详细对比一下:
| 模式 | 工作方式 | 性能 | 功能 | 适用场景 |
|---|---|---|---|---|
| userspace | kube-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能做很多事情:
基于域名的虚拟主机:同一个IP,不同的域名转发到不同的服务
a.example.com→ Service Ab.example.com→ Service B
基于路径的路由:同一个域名,不同的路径转发到不同的服务
example.com/api→ 后端服务example.com/→ 前端服务
SSL/TLS终止:HTTPS证书在Ingress层配置,后端服务不用管
负载均衡:Ingress Controller本身做负载均衡
限流、熔断、重试:各种流量治理功能
认证、授权:统一的入口认证
WAF、日志、监控:统一的安全和可观测性
总之,Ingress就是集群的统一流量入口——所有外部流量都先经过Ingress,再转发到各个服务。
5.3.3 Ingress Controller对比
Ingress Controller有很多种,主流的有:
| Ingress Controller | 特点 | 性能 | 功能 | 适用场景 |
|---|---|---|---|---|
| Nginx Ingress | 官方维护,基于Nginx,最常用 | 好 | 丰富 | 通用场景,大部分人用这个 |
| Traefik | Go写的,原生支持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是怎么工作的?
- 你创建一个Ingress资源,定义路由规则
- Ingress Controller Watch到有新的Ingress
- Ingress Controller根据Ingress规则,配置自己的反向代理
- 外部流量进来,到达Ingress Controller
- Ingress Controller根据规则,把流量转发到对应的Service
- Service再负载均衡到后端Pod
整个流程:
用户 → Ingress Controller → Service → PodIngress Controller本身也是以Pod的形式跑在集群里的——一般用Deployment或者DaemonSet部署,前面用LoadBalancer或者NodePort暴露出去。
5.4 对比:Service vs Ingress
很多人搞不清Service和Ingress的区别,我给你对比一下:
| 维度 | Service | Ingress |
|---|---|---|
| 层级 | 四层(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
反向代理的作用:
- 负载均衡:把请求分到多个后端
- SSL终止:HTTPS在代理层解密,后端不用管
- 缓存:静态内容缓存,减轻后端压力
- 压缩:压缩响应,节省带宽
- 安全:WAF、限流、防攻击
- 路由:根据域名、路径转发到不同后端
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虽然好用,但有几个问题:
- 跟Pod绑定:Volume是Pod的一部分,Pod删了Volume就没了(有些类型数据还在,但Volume对象没了)
- 用户需要知道存储细节:你要用NFS,就得知道NFS服务器地址、路径;你要用云盘,就得知道云盘ID——这对用户不友好
- 管理混乱:存储资源怎么管理?谁都能随便申请?怎么计费?怎么配额?
为了解决这些问题,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 怎么选?
选型决策树:
- 数据是临时的、不重要的?→ 本地存储(emptyDir)
- 需要多个Pod同时读写共享?→ 网络文件存储(NFS/CephFS)
- 是数据库类应用,对性能要求高?→ 块存储(RBD/云盘)
- 存海量非结构化数据,比如图片视频?→ 对象存储(S3/MinIO)
- 用云厂商的话,优先用云厂商的托管存储——省心,不用自己维护
还有个原则:能不用分布式存储就不用——分布式存储复杂,运维成本高。如果云厂商有托管的,就用托管的,别自己搭。
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有什么好处?
- 解耦:存储驱动跟K8s核心代码分开——K8s不用管存储怎么实现,存储厂商不用管K8s怎么实现
- 独立迭代:存储驱动可以自己发版、自己更新,不用等K8s发版
- 统一标准:所有存储都用同一套接口,用户用起来都一样
- 功能更丰富: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部署有状态应用,要注意什么?
- 存储要支持:你的存储得支持动态供给,或者提前创建好PV——不然PVC会Pending
- 数据备份:重要数据一定要定期备份——PV不是万能的,存储集群也可能挂
- 应用本身的集群逻辑:StatefulSet只是提供了稳定的标识和存储,应用的集群逻辑(比如主从切换、数据同步)还要应用自己处理
- 不要随便删PVC:删了PVC数据可能就没了(取决于回收策略)
- 扩容缩容要小心:有状态应用的扩容缩容不像无状态那么简单,要考虑数据、集群状态
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 存储的三大指标:容量、性能、可靠性
选存储的时候,主要看三个指标:
- 容量:能存多少数据
- 性能:IOPS(每秒读写次数)、吞吐量(每秒读写多少数据)、延迟(IO需要多久)
- 可靠性:数据会不会丢,可用性怎么样
这三个指标,跟成本是相关的——越好越贵。你要根据你的业务需求,在这三者之间做平衡。
比如:
- 数据库:要性能、要可靠性,容量可以不用太大 → 用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。