第7章 配置与密钥管理:ConfigMap与Secret
前面我们讲了存储——持久化的数据存在哪里。这一章我们讲另一种"数据"——应用的配置。
配置跟普通数据不一样——它不是业务数据,而是应用运行需要的参数、环境变量、配置文件等等。
配置管理是个老问题了——怎么管理不同环境的配置?怎么安全地管理密码、密钥这些敏感信息?怎么热更新配置?
K8s给我们提供了两个专门的资源——ConfigMap和Secret,用来管理配置和敏感信息。
7.1 为什么要把配置跟镜像分开?
在讲ConfigMap之前,我们先想想:为什么不直接把配置文件打到镜像里?为什么要单独管理配置?
7.1.1 十二要素应用方法论
这个问题,"十二要素应用"(The Twelve-Factor App)里早就讲过了——配置要严格地和代码分离。
什么是十二要素应用?它是一套现代应用(特别是SaaS应用)的最佳实践,总结了应用开发应该遵循的12条原则。其中第三条就是"配置"——配置存储在环境变量里,跟代码分开。
为什么?
不同环境的配置不一样——开发、测试、生产环境的数据库地址、密码、各种参数都不一样。如果配置在镜像里,那每个环境都要打不同的镜像——太麻烦了。
配置会变,代码不会经常变——改个配置还要重新构建镜像、重新部署?那也太重了。
敏感信息不能进代码库——密码、密钥这些东西,不能打到镜像里,更不能提交到Git里——太危险了。
同一个镜像要能在多个环境运行——build once, run anywhere。同一个镜像,在开发、测试、生产都能跑,只是配置不一样。
所以,配置和代码分离,这是最佳实践。
7.1.2 传统配置管理的问题
传统的配置管理有哪些方式?
- 配置文件跟代码一起放Git里——敏感信息不安全,改配置要重新发版
- 环境变量——简单,但多了不好管理,而且不能热更新
- 配置中心(Apollo、Nacos等)——功能强大,但要额外部署,增加复杂度
- 直接把配置文件挂进去——麻烦,不好管理
K8s的ConfigMap和Secret,提供了一种原生的、声明式的配置管理方式——配置是K8s的资源对象,可以用YAML管理,可以跟应用一起部署。
7.2 ConfigMap:普通配置的管家
7.2.1 ConfigMap是什么?
ConfigMap是什么?简单说,它就是K8s里的一个资源对象,用来存普通的配置数据——不敏感的、可以明文存的配置。
ConfigMap里存的是键值对——key是配置名,value是配置内容。value可以是一个字符串,也可以是整个配置文件的内容。
ConfigMap是命名空间级别的——每个命名空间里的ConfigMap只能在这个命名空间里用。
7.2.2 ConfigMap的四种使用方式
ConfigMap怎么用?有四种方式:
1. 环境变量 把ConfigMap里的值作为环境变量注入到容器里。
比如ConfigMap里有个key叫LOG_LEVEL,值是info——你可以把它变成容器里的环境变量LOG_LEVEL=info,应用直接读环境变量就行。
适合:简单的配置项、少量的配置。
2. 命令行参数 把ConfigMap里的值作为容器启动命令的参数。
跟环境变量类似,只是用在命令行参数里。
3. Volume挂载(整个文件) 把ConfigMap里的每个key-value,变成挂载目录里的一个文件——key是文件名,value是文件内容。
比如ConfigMap里有个key叫nginx.conf,value是Nginx配置文件的内容——挂载之后,容器里就有一个nginx.conf文件,内容就是配置。
适合:配置文件、多个配置项、需要用文件的应用。
4. subPath(单个文件) 上面那种方式是把整个ConfigMap都挂载成一个目录,目录里有多个文件。
如果你只想挂载其中一个文件,不想覆盖整个目录——就用subPath。
比如容器里有个/etc/nginx/conf.d/目录,里面已经有一些文件了,你只想加一个my.conf进去,不想把整个目录覆盖掉——就用subPath,只挂载这一个文件。
这是个很实用的技巧——很多人刚开始用的时候不知道subPath,一挂载把整个目录覆盖了,原来的文件都没了,应用就挂了。
7.2.3 配置热更新?——看情况
很多人问:ConfigMap改了,应用里的配置会自动更新吗?
答案是:看你怎么用的。
如果是Volume挂载的:会自动更新。ConfigMap改了,过一会儿(大概几十秒),挂载的文件内容就会更新。
- 但是!应用会不会重新加载配置?那要看应用自己——大部分应用不会自动重新加载配置文件,得重启或者发信号让它 reload。
- 所以ConfigMap更新了,文件内容是更新了,但应用可能还用的旧配置——除非应用支持热加载。
如果是环境变量或者命令行参数:不会自动更新。环境变量是容器启动的时候注入的,启动之后就不会变了。要更新的话,得重启Pod。
这是个很常见的坑——很多人以为改了ConfigMap应用就自动用新配置了,结果不是。
所以:
- 如果你需要配置热更新,要么用Volume挂载 + 支持热加载的应用,要么用专门的配置中心
- 如果用环境变量,改了ConfigMap之后要重启Pod才能生效
7.3 Secret:敏感信息的保险箱
ConfigMap是存普通配置的,那敏感信息呢?比如密码、密钥、证书这些——不能明文存的东西。
那就用Secret。
7.3.1 Secret是什么?
Secret跟ConfigMap很像,也是键值对,也是命名空间级别的,也可以用环境变量或者Volume挂载。
区别是:
- Secret是专门用来存敏感信息的——密码、token、密钥、证书等等
- Secret的内容是Base64编码的(注意:不是加密!)
- Secret有专门的类型,不同类型用途不一样
- K8s对Secret有一些特殊的处理——比如不会随便输出到日志里
等等,Base64编码?那不是跟明文差不多吗?Base64解码一下就看到了。
没错!Secret默认只是Base64编码,不是加密的。很多人有这个误解,以为Secret是加密的,很安全——其实不是。
为什么叫Secret?因为它是"敏感信息",K8s会尽量避免把它泄露出去——比如不会在日志里随便打,不会在kubectl describe里直接显示内容——但它本质上还是明文存在etcd里的。
所以,不要以为用了Secret就绝对安全了。它只是比ConfigMap稍微安全一点,但不是加密存储。
7.3.2 Secret的类型
Secret有好几种类型,不同类型用途不一样:
1. Opaque(默认类型)
- 普通的Secret,存任意的敏感数据
- 最常用的类型
2. kubernetes.io/dockerconfigjson
- 用来存私有镜像仓库的认证信息
- 拉私有镜像的时候用
- Pod的imagePullSecrets字段指定这个Secret,就能拉私有仓库的镜像了
3. kubernetes.io/tls
- 用来存TLS证书和私钥
- Ingress做HTTPS的时候用的就是这种Secret
4. kubernetes.io/service-account-token
- ServiceAccount的token
- 这个是K8s自动创建的,用来给Pod访问apiserver用的
- 我们后面讲安全的时候会详细讲
还有一些其他类型,用得不多。
7.3.3 Secret的使用方式
Secret的使用方式跟ConfigMap差不多:
- 环境变量注入
- Volume挂载
- 镜像拉取认证(dockerconfigjson类型)
- Ingress TLS(tls类型)
用法跟ConfigMap几乎一样,只是资源类型不一样。
7.3.4 Secret真的安全吗?
这是个很重要的问题——Secret到底安不安全?
我们来分析一下:
默认情况下,Secret的安全性:
- 存在etcd里——是明文的(Base64,跟明文差不多)
- 有权限的人(比如集群管理员)可以看到所有Secret
- 节点上——kubelet会把Secret的内容存在节点上(挂载的话就是文件,环境变量的话就在进程环境里)
- 如果etcd没加密,那etcd备份泄露了,所有Secret就都泄露了
所以,默认的Secret不是强安全的,只是"比明文好一点"。
那怎么加强Secret的安全性?
- etcd加密:开启etcd的静态加密——Secret存在etcd里是加密的,就算etcd备份泄露了,没有密钥也解不开
- RBAC权限控制:严格控制谁能访问Secret——不是所有人都能看Secret的,用RBAC限制权限
- Pod Security:限制特权容器,防止有人从容器里偷Secret
- 外部密钥管理:用专门的密钥管理服务——比如HashiCorp Vault、云厂商的KMS、AWS Secrets Manager
- Sealed Secrets:加密的Secret,可以安全地存在Git里
生产环境,如果对安全要求高,建议用外部的密钥管理服务,不要只用默认的Secret。
7.4 配置管理最佳实践
讲完了ConfigMap和Secret,我们来总结一下配置管理的最佳实践。
7.4.1 什么配置放哪里?
- 普通配置、非敏感的 → ConfigMap
- 敏感配置、密码、密钥、证书 → Secret
- 非常敏感、对安全要求极高的 → 外部密钥管理服务(Vault、KMS等)
- 跟应用代码一起的、不会变的 → 可以打在镜像里(但不推荐,最好还是分离)
7.4.2 配置与代码分离
严格遵循十二要素应用原则——配置和代码分离:
- 不要把配置写在代码里
- 不要把配置打到镜像里
- 配置用ConfigMap/Secret管理,跟应用分开部署
- 同一个镜像可以在不同环境用不同配置运行
7.4.3 敏感信息不要进Git
密码、密钥这些敏感信息,绝对不能提交到Git里——不管是公开仓库还是私有仓库。
- 公共仓库就不用说了,谁都能看到
- 私有仓库也不安全——仓库可能泄露,历史记录里有,离职员工可能带走
那GitOps怎么办?配置都要存在Git里啊——用Sealed Secrets或者External Secrets,加密之后再存Git里。
7.4.4 配置热更新的问题
前面说了,ConfigMap/Secret更新了,应用不一定会自动更新配置。
如果需要热更新,有几种方案:
- 应用自己支持热加载——比如Nginx可以reload,Java应用可以用Spring Cloud Config之类的
- 用配置中心——Apollo、Nacos、Consul等,专门做配置管理,支持热更新
- Reloader之类的工具——监听ConfigMap/Secret变化,自动滚动更新Pod(相当于重启应用)
根据你的需求选。
7.4.5 配置的版本管理
配置也要做版本管理——改了什么、什么时候改的、谁改的,都要有记录。
- 用GitOps的话,配置存在Git里,天然有版本历史
- 不用GitOps的话,也要有变更记录——不然出了问题都不知道改了什么
7.5 外部配置中心与K8s的集成
ConfigMap和Secret虽然好用,但功能还是比较基础——没有界面、没有版本管理、没有灰度发布、没有权限管理……
如果你的配置管理需求比较复杂,可能还是需要专门的配置中心——比如Apollo、Nacos、Consul、Spring Cloud Config等。
那这些配置中心怎么跟K8s集成?
7.5.1 集成方式
主要有几种集成方式:
1. 应用直接集成
- 应用直接连配置中心,拉配置
- 跟K8s没关系,应用该怎么用怎么用
- 优点:简单,跟以前一样
- 缺点:每个应用都要集成SDK,有语言绑定
2. Sidecar模式
- 给Pod加一个Sidecar容器,Sidecar负责跟配置中心通信,把配置拉下来写到文件里
- 主应用直接读文件就行,不用改代码
- 优点:应用不用改,无侵入
- 缺点:多了个Sidecar,增加资源开销
3. 同步到ConfigMap/Secret
- 有个工具把配置中心的配置,自动同步成K8s的ConfigMap/Secret
- 应用还是用ConfigMap/Secret,跟普通用法一样
- 优点:应用不用改,用K8s原生方式
- 缺点:多了一层同步,有延迟
根据你的情况选合适的方式。
7.5.2 什么时候用配置中心,什么时候用ConfigMap?
- 简单场景、配置不多 → 用ConfigMap/Secret就够了,简单,K8s原生,不用额外部署
- 复杂场景、配置很多、需要热更新、需要权限管理、需要灰度发布 → 用配置中心,功能更强大
- 微服务多、多语言、配置管理需求复杂 → 配置中心更合适
没有最好的,只有最合适的——根据你的需求来。
7.6 扩展知识:信息安全基础与加密算法简介
讲到Secret和敏感信息,我们扩展一下信息安全的基础知识——了解了这些,你对安全的理解会更深。
7.6.1 对称加密 vs 非对称加密
加密算法分两大类:
对称加密:
- 加密和解密用同一个密钥
- 例子:AES、DES、3DES
- 优点:速度快,适合加密大量数据
- 缺点:密钥分发困难——怎么把密钥安全地传给对方?
非对称加密(公钥加密):
- 有一对密钥:公钥和私钥
- 公钥加密,私钥解密;或者私钥签名,公钥验签
- 例子:RSA、ECC
- 优点:不用分发密钥,公钥可以公开
- 缺点:速度慢,不适合加密大量数据
实际应用中,一般是两者结合:
- 用非对称加密交换对称密钥
- 然后用对称密钥加密数据
比如HTTPS就是这样的——握手的时候用非对称加密交换密钥,之后用对称密钥加密通信内容。
7.6.2 哈希算法
哈希算法是什么?它是把任意长度的数据,转换成固定长度的字符串(哈希值)。
特点:
- 单向的——只能从数据算哈希值,不能从哈希值反推出数据
- 雪崩效应——输入改一点点,输出就完全不一样
- 碰撞概率低——很难找到两个不同的数据有相同的哈希值
例子:MD5、SHA-1、SHA-256、SHA-512
用途:
- 数据完整性校验——下载文件后算哈希,跟官方的对比,看文件有没有被篡改
- 密码存储——存密码的哈希值,不存明文(但要加盐,防止彩虹表攻击)
- 数字签名——签名的是数据的哈希值
注意:MD5和SHA-1已经不安全了,现在推荐用SHA-256及以上的。
7.6.3 数字证书与PKI
数字证书是什么?它是用来证明"这个公钥确实属于某个人/某个组织"的。
为什么需要证书?因为公钥是公开的,你怎么知道你拿到的公钥真的是对方的,而不是中间人伪造的?
这就需要第三方权威机构(CA,证书颁发机构)来做担保——CA用自己的私钥给你的公钥签名,签出来的就是证书。
你拿到证书,用CA的公钥验证签名——验证通过,就说明这个公钥确实是证书里那个人的。
这就是PKI(公钥基础设施)——一套基于证书的信任体系。
我们平时用的HTTPS,就是基于PKI的——网站有证书,浏览器验证证书,确认网站身份,然后建立加密连接。
K8s里也大量用到证书——apiserver、etcd、kubelet、kube-proxy……所有组件之间的通信,都是用TLS加密的,都需要证书。
7.6.4 安全的基本原则
最后,讲几个安全的基本原则:
- 最小权限原则:每个用户、每个服务、每个程序,只给它需要的最小权限——能不给的就不给
- 纵深防御:多层防护——一层被攻破了还有下一层,不要把所有鸡蛋放在一个篮子里
- 默认安全:默认就是安全的配置,不安全的功能默认关闭——要用户主动开才能开
- 不要自己造轮子:加密、安全这些东西,不要自己实现,用成熟的、经过验证的方案
- 安全是持续的:不是做一次就完了,要持续监控、持续更新、持续改进
安全是个系统工程,不是靠某一个工具就能解决的。Secret只是其中一环,还要配合RBAC、网络策略、运行时安全等等,才能真正做好安全。
7.7 本章小结
这一章我们讲了配置与密钥管理——ConfigMap和Secret。
总结一下重点:
- 配置要跟代码分离——十二要素应用原则,build once run anywhere
- ConfigMap:存普通配置,支持环境变量、命令行参数、Volume挂载、subPath四种用法
- Secret:存敏感信息,用法跟ConfigMap类似,但只是Base64编码,不是加密的
- ConfigMap/Secret更新后,Volume挂载的会自动更新文件,但应用不一定会重新加载;环境变量的不会自动更新
- Secret的安全性:默认只是Base64,要加强安全可以用etcd加密、RBAC、外部密钥管理服务等
- 配置中心:复杂场景可以用Apollo/Nacos等配置中心,跟K8s有多种集成方式
- 安全基础:对称加密、非对称加密、哈希、证书、PKI
配置管理看起来简单,但其实水很深——怎么管理不同环境的配置、怎么安全管理敏感信息、怎么做热更新、怎么做版本管理……都是学问。理解了这些工具的原理和特点,你才能选对方案、用好工具。
下一章,我们来讲调度器——Pod到底是怎么被放到节点上的?
第8章 调度器深度解析:Pod去哪儿了
你有没有想过:当你创建一个Pod的时候,它是怎么被放到某个节点上的?是谁决定的?根据什么决定的?
这就是**调度器(Scheduler)**干的事儿。调度器是K8s的"大脑"之一——它负责给Pod找一个合适的家(节点)。
这一章我们就深入讲讲调度器——它是怎么工作的?有哪些调度策略?怎么影响调度决策?
8.1 调度器是干什么的?
8.1.1 调度的本质
调度的本质是什么?就是给每个Pod找一个最合适的节点。
说起来简单,但做起来难——因为"最合适"的定义很复杂:
- 节点资源够不够?
- 节点有没有污点?
- Pod有没有节点选择器?
- Pod跟其他Pod的亲和性/反亲和性满足吗?
- 怎么平衡各个节点的负载?
- 怎么提高资源利用率?
- 怎么保证高可用?
这是一个经典的装箱问题(Bin Packing)——把不同大小的物品(Pod)放进不同容量的箱子(节点)里,目标是用最少的箱子装下所有物品,同时还要满足各种约束条件。
装箱问题是NP难的——没有完美的最优解,只能找尽量好的近似解。
K8s的调度器,就是在各种约束条件下,给每个Pod找一个尽量好的节点。
8.1.2 调度器的位置
调度器在K8s架构里是什么位置?
- 调度器是控制平面的组件之一,叫kube-scheduler
- 它是独立的进程,跑在Master节点上
- 它通过apiserver Watch新创建的、还没分配节点的Pod
- 它给每个Pod选一个最合适的节点,然后告诉apiserver(绑定)
- 然后对应节点的kubelet就会在那台节点上启动Pod
调度器只做一件事——给Pod分配节点。它不负责启动Pod,也不负责监控Pod,它只负责调度决策。
8.2 调度流程:Filter → Score → Bind
调度的过程分三步:预选(Filter)→ 优选(Score)→ 绑定(Bind)。
8.2.1 第一步:预选(Filter)
预选是什么?就是先把所有不符合条件的节点过滤掉。
比如:
- 节点资源不够的?去掉
- 节点有污点,Pod不能容忍的?去掉
- 节点不满足Pod的nodeSelector的?去掉
- 端口冲突的?去掉
- 节点不满足Pod亲和性/反亲和性的?去掉
- 节点磁盘不够的?去掉
- 节点网络不可用的?去掉
- ……
预选之后,剩下的节点就是"有资格"运行这个Pod的节点。
如果预选之后一个节点都没剩下——那Pod就一直处于Pending状态,调度失败。
8.2.2 第二步:优选(Score)
优选是什么?就是在剩下的合格节点里,给每个节点打分,选出分数最高的。
打分的维度有很多:
- 资源利用率怎么样?——资源越均衡分数越高(或者说,剩下的资源越多分数越高?取决于调度策略)
- 节点有没有跟Pod匹配的亲和性?——匹配的话加分
- 节点上有没有同应用的Pod?——如果是反亲和的话减分
- 节点是不是已经有很多Pod了?——负载高的减分
- 数据局部性怎么样?——Pod需要的数据在这台节点上,加分
- ……
每个维度打一个分,然后加权加起来,就是这个节点的总分。
分数最高的节点,就是最终的选择——如果有多个节点分数一样,就随机选一个。
8.2.3 第三步:绑定(Bind)
绑定是什么?就是调度器告诉apiserver:"这个Pod就放这台节点了"。
具体来说,调度器调用apiserver的API,把Pod的nodeName字段设成选中的节点名——这就叫绑定。
绑定之后,Pod就有了节点归属,对应节点的kubelet就会开始创建Pod。
8.2.4 为什么分两步?
你可能会问:为什么要分预选和优选两步?直接给所有节点打分不行吗?
不行,因为节点可能很多——几百上千个节点,如果每个都要算一遍优选的分数,那太慢了。
预选的作用就是先快速过滤掉大部分不合格的节点,剩下少数几个合格的,再仔细算优选分数——这样效率高多了。
这就像找工作——先看学历、工作经验这些硬条件,不符合的直接pass(预选);剩下符合条件的,再仔细面试、比较,选最好的(优选)。
8.3 影响调度的因素:怎么控制Pod调度到哪?
默认的调度策略是通用的,但很多时候我们想自己控制Pod的调度——比如:
- 我想让Pod跑到有GPU的节点上
- 我想让同一个应用的Pod分散到不同节点,提高可用性
- 我想让两个紧密协作的Pod放在同一个节点,提高性能
- 我想把某些节点专门给特定的应用用
- ……
K8s提供了很多方式来影响调度决策,我们一个个来讲。
8.3.1 nodeSelector:最简单的节点选择
nodeSelector是最简单的方式——你给Pod指定一个nodeSelector,就是一组标签。Pod只能调度到有这些标签的节点上。
比如:
- 你给某些节点打上标签
disktype=ssd - 然后Pod的nodeSelector设成
disktype: ssd - 那这个Pod就只能调度到有SSD的节点上
简单粗暴,容易理解。但功能有限——只能做"等于"的匹配,不能做复杂的逻辑。
8.3.2 节点亲和性(Node Affinity)
节点亲和性(Node Affinity)是nodeSelector的升级版——功能更强大,支持更复杂的规则。
节点亲和性支持两种:
- requiredDuringSchedulingIgnoredDuringExecution:必须满足,不满足就调度不了——跟nodeSelector类似,但规则更丰富
- preferredDuringSchedulingIgnoredDuringExecution:尽量满足,不满足也没关系——软限制,优先选满足的节点
名字很长,什么意思?
- required:必须的,硬限制
- preferred:偏好的,软限制
- DuringScheduling:调度的时候生效
- IgnoredDuringExecution:运行期间不生效——就是说Pod已经跑起来了,节点标签变了,Pod也不会被赶跑
节点亲和性支持哪些操作?
- In:标签值在列表里
- NotIn:标签值不在列表里
- Exists:标签存在就行,不管值是什么
- DoesNotExist:标签不存在
- Gt:大于
- Lt:小于
比nodeSelector强大太多了——你可以写很复杂的规则。
8.3.3 Pod亲和性与反亲和性(Pod Affinity / Anti-Affinity)
节点亲和性是Pod跟节点的关系,那Pod跟Pod之间的关系呢?
比如:
- 我想让这两个Pod尽量放在同一个节点——因为它们通信很多,放一起网络快
- 我想让这几个Pod尽量分散在不同节点——提高可用性,一个节点挂了还有别的
这就是Pod亲和性(Pod Affinity)和Pod反亲和性(Pod Anti-Affinity)。
跟节点亲和性类似,也分硬限制和软限制:
- requiredDuringSchedulingIgnoredDuringExecution:必须满足
- preferredDuringSchedulingIgnoredDuringExecution:尽量满足
Pod亲和性:这个Pod跟哪些Pod要放一起——比如前端Pod跟缓存Pod放一起,减少网络延迟。
Pod反亲和性:这个Pod跟哪些Pod不要放一起——比如同一个应用的多个副本,分散到不同节点,提高可用性;或者不同租户的Pod不要放一起,安全隔离。
Pod亲和性/反亲和性是通过标签选择器来匹配的——匹配哪些Pod,是通过标签来选的。
这是个非常有用的功能——特别是反亲和性,生产环境常用它来提高应用的可用性,避免单点故障。
8.3.4 污点与容忍(Taint & Toleration)
前面讲的都是Pod选节点——Pod说"我要去什么样的节点"。
那反过来呢?节点能不能选Pod?——节点说"我不想要什么样的Pod"。
可以!这就是污点(Taint)和容忍(Toleration)。
**污点(Taint)**是打在节点上的——节点有了污点,普通的Pod就调度不上去了。就像节点上有"臭味",普通Pod受不了,不愿意来。
**容忍(Toleration)**是Pod上的——Pod如果能容忍这个污点,就能调度到有这个污点的节点上。
打个比方:
- 节点上的污点就像"隔离区"——普通人不能进
- Pod的容忍就像"通行证"——有通行证的才能进
污点有什么用?
- 专用节点:把某些节点专门给特定的应用用——给节点打上污点,只有对应的应用有容忍,才能调度上去
- 特殊硬件节点:比如GPU节点、高内存节点——打上污点,只有需要这些硬件的Pod才能调度上去,不浪费资源
- 节点维护:要维护节点了,给节点打上污点,把Pod都赶出去(当然还要配合cordon和drain)
- 节点问题:节点有问题,先打上污点,不让新的Pod调度上来
污点有三种效果(Effect):
- NoSchedule:不能调度上去——硬限制,已经在上面的Pod不受影响
- PreferNoSchedule:尽量不要调度上去——软限制
- NoExecute:不仅不能调度上去,已经在上面的Pod,如果不能容忍,也会被赶出去
NoExecute比较狠——节点打上NoExecute的污点,上面所有不能容忍这个污点的Pod都会被立刻驱逐。
污点和容忍是个很强大的机制——用来做节点隔离、专用节点、特殊硬件节点,非常方便。
8.3.5 节点名称(nodeName)
最简单粗暴的方式——直接指定Pod的nodeName,说"我就要跑在这个节点上"。
调度器直接跳过,Pod就直接跑在指定节点上。
但这种方式不推荐用——太硬了,节点挂了Pod就跑不了了。一般只有特殊场景才用。
8.3.6 优先级与抢占(Priority & Preemption)
前面讲的都是"怎么选节点",那如果节点资源不够了呢?——所有节点都满了,新的Pod调度不上去怎么办?
这时候就有了优先级(Priority)和抢占(Preemption)。
优先级:Pod有优先级——优先级高的Pod比优先级低的重要。
抢占:如果一个高优先级的Pod调度不上去——所有节点都满了,那调度器会把某个节点上低优先级的Pod删掉(抢占),腾出资源来跑高优先级的Pod。
就像飞机升舱——你是白金卡会员,经济舱满了,把普通乘客赶下去,给你腾位置?(当然现实中不会这样,但K8s里会)
什么时候用优先级和抢占?
- 核心业务Pod设高优先级——资源不够的时候,先保证核心业务跑起来
- 非核心、批处理任务设低优先级——资源不够的时候,先把这些停掉,给核心业务腾地方
- 系统组件设最高优先级——比如kube-proxy、CNI插件这些,绝对不能被抢占
生产环境建议给不同的Pod设置不同的优先级——保证重要的应用在资源紧张的时候能优先运行。
8.4 调度器框架:可扩展的调度器
K8s的调度器不是一个黑盒——它是高度可扩展的。这就是调度器框架(Scheduler Framework)。
8.4.1 扩展点
调度器框架定义了很多扩展点——在调度流程的不同阶段,你可以插入自己的逻辑。
主要的扩展点有:
- PreFilter:预选之前的预处理——比如做一些计算、缓存一些数据
- Filter:预选——过滤掉不合格的节点
- PreScore:优选之前的预处理
- Score:优选——给节点打分
- Reserve:预留资源——选中节点后,先预留资源
- Permit:批准——批准还是拒绝这个调度
- PreBind:绑定之前的操作——比如挂载存储什么的
- Bind:绑定——把Pod绑定到节点
- PostBind:绑定之后的操作
- Reserve失败的时候:Unreserve——释放预留的资源
每个扩展点,你都可以写自己的插件,实现自定义的调度逻辑。
8.4.2 怎么扩展调度器?
扩展调度器有几种方式:
- 写调度插件:用调度器框架,写自己的插件,编译进调度器——功能最强大,但要改调度器代码
- 调度器配置文件:通过配置文件,启用/禁用某些插件,调整插件的参数——不用改代码,配置就行
- 多个调度器:跑多个调度器,不同的Pod用不同的调度器——比如默认调度器管普通Pod,你自己写的调度器管特殊Pod
- 调度器扩展器(Scheduler Extender):老的扩展方式,通过HTTP接口扩展——现在推荐用调度器框架
K8s的调度器设计得非常灵活——你几乎可以自定义任何调度逻辑。
8.4.3 常用的调度插件
K8s自带了很多调度插件,常用的有:
- NodeResourcesFit:检查节点资源够不够
- NodeName:检查nodeName匹配
- NodeSelector:检查nodeSelector匹配
- NodeAffinity:检查节点亲和性
- PodAffinity:检查Pod亲和性
- PodAntiAffinity:检查Pod反亲和性
- TaintToleration:检查污点容忍
- NodeResourcesBalancedAllocation:优选的时候,让节点资源更均衡
- ImageLocality:优选的时候,节点上已经有镜像的加分
- InterPodAffinity:Pod亲和性优选
- ……
大部分调度逻辑,都是通过这些插件实现的。
8.5 调度策略:LeastRequested vs MostRequested
优选阶段,给节点打分的时候,有个很重要的策略——怎么算资源分数?
有两种常见的策略:
8.5.1 LeastRequested(最少请求)
LeastRequested策略——剩余资源越多的节点,分数越高。
什么意思?就是优先把Pod放到资源比较空闲的节点上——让各个节点的负载都比较均衡,不要有的节点忙死、有的闲死。
这是默认的策略吗?以前是,现在默认是BalancedAllocation——更均衡的分配。
这种策略的好处:
- 节点负载均衡
- 每个节点都有较多剩余资源,突发流量的时候扛得住
- 单个节点故障影响小
缺点:
- 资源利用率低——很多节点都有剩余资源,但加起来很多,浪费了
- 需要更多的节点
8.5.2 MostRequested(最多请求)/ BinPacking
MostRequested策略——资源用得越多的节点,分数越高。
什么意思?就是尽量把Pod往已经用了很多资源的节点上塞——尽量把节点填满,减少需要的节点数。
这就是**装箱问题(Bin Packing)**的目标——用最少的箱子装下所有物品。
这种策略的好处:
- 资源利用率高——尽量把节点填满,用更少的节点,省钱
- 节点数量少,管理成本低
缺点:
- 节点负载不均衡——有的节点满了,有的可能很空
- 突发流量的时候可能扛不住——节点已经满了,没余量
- 单个节点故障影响大——一个节点满了,挂了影响的Pod多
8.5.3 怎么选?
两种策略各有优劣,怎么选?
- 对可用性要求高、有突发流量 → LeastRequested / BalancedAllocation——留有余量,更安全
- 对成本敏感、流量平稳 → MostRequested / BinPacking——提高利用率,省钱
- 生产环境一般用BalancedAllocation——平衡型的,既不太浪费,也留有余量
这是个权衡——成本和可用性之间的权衡。根据你的业务特点来选。
8.6 扩展知识:装箱问题与资源利用率优化
讲到调度,就不得不提装箱问题(Bin Packing Problem)——这是调度的核心问题。我们扩展讲讲。
8.6.1 什么是装箱问题?
装箱问题是经典的组合优化问题:
- 有若干个容量相同的箱子(节点)
- 有若干个不同大小的物品(Pod)
- 目标:用最少的箱子装下所有物品
这个问题看起来简单,但其实是NP难问题——没有多项式时间的最优解,物品多了只能用启发式算法找近似解。
什么是NP难?就是说,物品数量多了,你根本算不出最优解——因为可能的组合太多了,算到天荒地老也算不完。只能用一些启发式的算法,找一个"尽量好"的解。
8.6.2 常见的装箱算法
常见的启发式装箱算法有:
1. 首次适应(First Fit)
- 物品按顺序来,找到第一个能放进去的箱子就放进去
- 优点:简单,快
- 缺点:结果不是最优的,可能浪费空间
2. 最佳适应(Best Fit)
- 物品按顺序来,找到能放进去的、剩余空间最小的箱子放进去
- 尽量把箱子填满
- 比首次适应好一点,但也不是最优的
3. 首次适应递减(First Fit Decreasing)
- 先把物品按从大到小排序
- 然后用首次适应算法放
- 大的先放,小的见缝插针
- 效果比前两个好很多
4. 最佳适应递减(Best Fit Decreasing)
- 先排序,再最佳适应
- 效果最好的启发式算法之一
K8s的调度器,本质上就是一个在线的装箱算法——Pod一个一个来,给每个找个合适的节点。
8.6.3 资源利用率优化
调度的最终目标之一,就是提高资源利用率——资源是花钱买的,利用率高了就能省钱。
怎么提高资源利用率?
合理设置requests:很多人把requests设得很大,怕不够用——结果大部分时间资源都闲着,浪费钱。合理设置requests,根据实际用量调,能省很多钱。
用BinPacking调度策略:尽量把节点填满,减少需要的节点数。
超卖(Overcommit):limits比requests大——平时用不了那么多,高峰的时候可以用。但要注意风险,超卖太多容易出问题。
自动伸缩:节点自动伸缩(Cluster Autoscaler)——节点不够了自动加,多了自动减。不用一直留着很多节点待命。
混部:在线业务和离线业务混部——在线业务白天忙,离线业务晚上跑,错峰使用资源,提高利用率。
但要注意:资源利用率不是越高越好——利用率太高了,就没有余量了,出点问题就扛不住。要在成本和可用性之间找平衡。
一般来说,集群整体CPU利用率在60-70%左右是比较健康的——既不浪费,又留有余量。
8.7 本章小结
这一章我们深入讲了K8s的调度器。
总结一下重点:
- 调度器负责给Pod分配节点,是K8s的"大脑"之一
- 调度流程分三步:预选(Filter)→ 优选(Score)→ 绑定(Bind)
- 影响调度的因素:nodeSelector、节点亲和性、Pod亲和性/反亲和性、污点与容忍、优先级与抢占
- 调度器框架是高度可扩展的——有很多扩展点,可以写自定义插件
- 调度策略:LeastRequested(均衡)vs MostRequested(装箱),各有优劣
- 装箱问题是调度的核心问题,是NP难的,只能用启发式算法
- 资源利用率优化是调度的重要目标,但要跟可用性做平衡
调度是K8s里比较深入的一块——大部分时候你不用管它,默认的调度策略就够用了。但当你有特殊需求的时候,知道怎么影响调度决策、怎么扩展调度器,就很有用了。
下一章,我们来讲K8s的网络模型——这是K8s里最复杂、也最容易出问题的一块。
第9章 K8s网络模型:容器网络的黑盒揭秘
如果说K8s里哪一块最复杂、最容易出问题、最让新手头疼,那一定是网络。
Pod之间怎么通信?Pod的IP是哪来的?Service的ClusterIP是怎么回事?网络插件是干什么的?为什么有这么多种CNI?
这一章我们就来揭开K8s网络的黑盒——从基础原理到主流方案,彻底搞懂K8s网络。
9.1 K8s网络模型:四大基本要求
在讲具体实现之前,我们先搞清楚:K8s对网络有什么要求?
K8s的网络模型有四个基本要求——这是K8s的网络"宪法",所有网络方案都必须满足:
9.1.1 要求一:Pod之间可以直接通信,不用NAT
同一个集群里,所有Pod之间都可以直接用IP通信,不需要做网络地址转换(NAT)。
什么意思?就是说:
- Pod A访问Pod B,Pod B看到的源IP就是Pod A的真实IP
- 中间不会被改源地址、改目的地址
- 不管Pod在哪个节点上,都是这样
这是K8s网络最基本的要求——Pod的IP是端到端的,中间不做NAT。
为什么要有这个要求?因为很多应用需要知道客户端的真实IP——比如日志、审计、安全策略,都需要真实的源IP。如果中间做了NAT,源IP就丢了。
9.1.2 要求二:节点上的组件可以跟Pod通信
节点本身(比如kubelet、kube-proxy)可以跟所有Pod直接通信。
这个很自然——节点上的组件要管理Pod,肯定要能跟Pod通信。
9.1.3 要求三:每个Pod有自己的IP
每个Pod都有一个独立的IP地址,Pod里的所有容器共享这个IP。
前面我们讲过,Pod里的容器共享网络命名空间——所以它们的IP是一样的,端口空间也是一样的。
这就意味着,在K8s的世界里,Pod就像一台虚拟机(或者物理机)——有自己的IP,有自己的端口空间,跟其他Pod是平等的。
这就是K8s网络的核心思想——IP-per-Pod:每个Pod一个IP,Pod之间平等通信,就像物理机之间通信一样。
9.1.4 要求四:不指定怎么实现
K8s只规定了网络应该是什么样的(上面三条),但不规定怎么实现。
具体怎么实现这个网络模型?K8s不管——你用什么方案都行,只要满足这几个要求就行。
这就是为什么有这么多网络插件——Calico、Flannel、Cilium、Weave……都是实现K8s网络模型的不同方案。
这种设计的好处是——灵活,你可以根据自己的需求选合适的网络方案。坏处是——方案太多了,新手不知道怎么选。
9.2 容器网络基础:Docker网络模型
在讲K8s网络之前,我们先回顾一下Docker的网络模型——K8s的网络是在容器网络基础上发展来的。
9.2.1 Docker的默认网络:bridge模式
Docker默认的网络模式是bridge模式:
- 宿主机上有一个docker0的网桥(虚拟交换机)
- 每个容器创建的时候,会创建一对veth设备(虚拟网卡对)——一端在容器里(eth0),一端连到docker0网桥上
- 所有容器都连到docker0上,它们之间可以通过docker0互相通信
- 容器访问外部网络的时候,通过docker0到宿主机的网卡,然后做SNAT(源地址转换)出去
这种模式下,容器的IP是docker0网段的内部IP——外部网络看不到,容器之间可以通信,但容器访问外部要做NAT。
9.2.2 Docker网络的问题
Docker的默认网络有什么问题?
- 跨主机容器不能直接通信——不同宿主机上的容器,IP是各自docker0网段的,互相不通。要通信得通过宿主机端口映射(-p),做NAT。
- NAT带来的问题——源IP丢失,性能损耗,配置复杂。
这在单机的时候没问题,但在集群里就不行了——K8s是集群,Pod分布在很多节点上,必须跨节点也能直接通信。
所以K8s的网络,必须解决跨节点Pod通信的问题。
9.3 CNI:容器网络接口标准
前面说了,K8s不规定网络怎么实现,只要满足模型就行。那这么多网络方案,怎么跟K8s对接?
答案是——CNI(Container Network Interface,容器网络接口)。
9.3.1 什么是CNI?
CNI是什么?它是一个标准接口——定义了容器网络应该怎么配置、怎么调用。
就像CSI是存储的标准接口一样,CNI是网络的标准接口——只要你的网络方案实现了CNI接口,就能跟K8s对接。
CNI定义了什么?
- 容器网络的配置格式
- 插件的调用方式(命令行参数、输入输出)
- 插件应该实现的操作:ADD(添加网络)、DEL(删除网络)、CHECK(检查)……
K8s的kubelet在创建Pod的时候,会调用CNI插件——CNI插件负责给Pod配置网络、分配IP、设置路由等等。
这样,K8s本身不用管网络具体怎么实现——只要是CNI插件,就能用。
9.3.2 CNI插件的分类
CNI插件有很多种,大致可以分几类:
1. 基础插件
- 最基础的网络功能,比如bridge、vlan、macvlan、ipvlan……
- 这些是构成复杂网络方案的基础组件
2. 元插件(Meta Plugin)
- 调用其他插件的插件,比如flannel、calico这些
- 或者是一些辅助插件,比如portmap(端口映射)、bandwidth(限流)、firewall(防火墙)……
3. IPAM插件
- 负责IP地址管理的插件,比如host-local(本地分配)、dhcp、calico-ipam……
一般来说,你用的网络方案(比如Calico、Flannel),都会自带自己的CNI插件——你安装了网络插件,CNI就配置好了,不用自己管。
9.4 跨节点通信的两种思路
跨节点的Pod怎么通信?主要有两种思路:Overlay网络和Underlay网络。
9.4.1 Overlay网络:隧道封装
Overlay网络是什么?就是在现有网络之上,再建一层虚拟网络。
怎么建?用隧道封装——把Pod的网络包,封装在宿主机的网络包里,然后通过宿主机的网络传过去,到了对端再解封装,把里面的Pod包拿出来,发给Pod。
就像寄快递——你要寄一个东西(Pod的包),你把它装在快递盒里(封装),写上收件人地址(宿主机地址),然后通过快递网络(宿主机网络)寄过去,对方收到了拆开快递盒(解封装),拿出里面的东西。
常用的封装协议:
- VXLAN:最常用的,把二层帧封装在UDP包里
- IPIP:把IP包封装在IP包里,简单,性能比VXLAN好一点,但只能传IP包
- Geneve:比较新的,跟VXLAN类似,更灵活
Overlay网络的优点:
- 对底层网络没有要求——只要宿主机之间能通就行,不管底层是什么网络
- 部署简单——不用改底层网络配置
- 隔离性好——虚拟网络跟物理网络分开
缺点:
- 有封装解封装的开销——性能损失,大概10-20%左右
- 排错困难——包是封装的,抓包看到的都是封装后的包,不好排查问题
- MTU问题——封装之后包变大了,要调小MTU,不然会分片或者丢包
代表方案:Flannel(VXLAN模式)、Calico(IPIP模式)、Weave
9.4.2 Underlay网络:路由打通
Underlay网络是什么?就是不做封装,直接通过路由打通——Pod的IP在物理网络里是可路由的,直接通过路由转发。
怎么实现?
- 每个节点分配一个Pod网段
- 在物理网络的路由器上,加路由——某个Pod网段,下一跳是某个节点
- 这样Pod的包直接通过路由转发到目标节点,不用封装
或者用BGP协议——每个节点跟物理路由器建立BGP邻居,把自己的Pod网段宣告出去,路由器自动学习路由。
Underlay网络的优点:
- 性能好——没有封装开销,跟物理网络差不多
- 排错简单——就是普通的IP路由,跟物理网络一样
- 没有MTU问题
缺点:
- 对底层网络有要求——需要能加路由,或者支持BGP
- 部署复杂——要配置物理网络设备
- IP地址消耗大——Pod IP要占用物理网络的地址空间
- 隔离性差——Pod网络跟物理网络是通的
代表方案:Calico(BGP模式)、Macvlan、SR-IOV
9.4.3 怎么选?
两种方案各有优劣,怎么选?
- 简单、快速部署、对底层网络没要求 → Overlay(Flannel VXLAN、Calico IPIP)
- 对性能要求高、底层网络可控 → Underlay(Calico BGP)
- 公有云环境 → 一般用Overlay,或者云厂商的CNI(比如AWS VPC CNI、阿里云Terway)
- 一般场景 → Calico就挺好,功能全,性能也不错
没有最好的,只有最合适的——根据你的环境和需求来。
9.5 主流网络插件对比
CNI插件太多了,我们来对比几个主流的。
9.5.1 Flannel
Flannel是CoreOS开发的,是最早、最简单的CNI插件之一。
特点:
- 简单,容易部署
- 功能单一——只管网络连通,不管别的
- 支持多种后端:VXLAN、host-gw、UDP
- 没有网络策略(NetworkPolicy)支持
适用场景:
- 测试环境、简单场景
- 只需要网络通,不需要高级功能
优点:简单,轻量。 缺点:功能少,性能一般,没有网络策略。
9.5.2 Calico
Calico是现在最流行的CNI插件之一,功能非常强大。
特点:
- 支持多种模式:IPIP(Overlay)、BGP(Underlay)、VXLAN
- 支持网络策略(NetworkPolicy)——而且比K8s原生的功能还强
- 性能好——特别是BGP模式,几乎没有性能损失
- 支持大规模集群
- 功能丰富:网络策略、BGP、IPAM、服务网格集成……
适用场景:
- 生产环境
- 需要网络策略的场景
- 对性能要求高的场景
- 大规模集群
优点:功能强大,性能好,社区活跃。 缺点:稍微复杂一点,学习成本高一点。
9.5.3 Cilium
Cilium是比较新的CNI插件,基于eBPF技术,最近非常火。
特点:
- 基于eBPF——内核级的可编程网络
- 性能非常好——eBPF在内核里处理,比iptables快多了
- 功能强大:网络策略、负载均衡、可观测性、服务网格(Cilium Service Mesh)……
- 支持 Hubble——网络可观测性工具,能看到所有网络流量
- 可以替代kube-proxy——用eBPF做Service负载均衡,比iptables/IPVS还快
适用场景:
- 对性能要求极高的场景
- 需要高级网络可观测性的场景
- 大规模集群
- 想用eBPF技术的
优点:性能极高,功能强大,可观测性好,未来方向。 缺点:比较新,社区和生态不如Calico成熟,对内核版本有要求(需要比较新的内核支持eBPF)。
9.5.4 其他方案
还有一些其他方案:
- Weave:简单易用,支持加密,但是性能一般,用的人越来越少了
- Macvlan:直接用物理网卡的MAC地址,性能好,但是需要底层网络支持,用得不多
- SR-IOV:硬件级的网络虚拟化,性能极高,但是需要特殊网卡,一般用于高性能计算场景
- 云厂商CNI:比如AWS VPC CNI、阿里云Terway、Azure CNI——跟云厂商的VPC集成,Pod IP就是VPC里的IP,性能好,跟云服务集成好
9.5.5 选型建议
怎么选CNI插件?我的建议:
- 新手、测试环境 → Flannel,简单,容易上手
- 生产环境、通用场景 → Calico,功能全,成熟稳定,社区好
- 追求极致性能、想用eBPF → Cilium,未来的方向
- 公有云环境 → 优先用云厂商的CNI,跟云集成更好
现在Calico是事实标准,大部分生产环境都用Calico——功能全,性能好,成熟稳定。
Cilium是后起之秀,发展很快——特别是eBPF火了之后,Cilium越来越受欢迎。如果你的环境比较新,内核版本够高,可以试试Cilium。
9.6 Service的实现:kube-proxy的工作原理
前面我们讲Service的时候提到了kube-proxy——它负责实现Service的负载均衡。这一节我们深入讲讲它是怎么工作的。
9.6.1 ClusterIP是怎么回事?
首先,ClusterIP是什么?它是一个虚拟IP——不是真实存在的网卡上的IP,是iptables/IPVS里的规则。
你在节点上ping ClusterIP是ping不通的——因为它不是一个真实的地址,只是iptables里的一个规则,匹配到了就做DNAT。
9.6.2 iptables模式的工作原理
kube-proxy的iptables模式,是怎么实现Service的?
简单说:
- kube-proxy Watch Service和Endpoint的变化
- 为每个Service,在iptables里加一系列规则
- 当访问Service的ClusterIP + Port的时候,iptables匹配到规则
- 做DNAT——把目的IP改成某个后端Pod的IP,目的端口改成Pod的端口
- 包就发给了那个Pod
返回的包呢?做SNAT——把源IP改成节点的IP,这样Pod返回的包能回到节点,再由iptables改回去,发给客户端。
整个过程是完全在Netfilter(内核的网络过滤框架)里做的——不需要经过用户态的kube-proxy进程,所以性能还不错。
但是iptables有个问题——Service多了,iptables规则会非常多,而且iptables是线性匹配的——规则多了性能会下降。
9.6.3 IPVS模式的工作原理
IPVS模式跟iptables模式类似,也是内核里做负载均衡。
区别是:
- IPVS是内核里专门做负载均衡的模块,性能更好
- 支持多种负载均衡算法:轮询、加权轮询、最小连接、源地址哈希……
- 规则更新是增量的,比iptables快
- 大规模下性能稳定,不会像iptables那样规则多了就慢
所以大规模集群,推荐用IPVS模式。
9.6.4 eBPF模式:Cilium的做法
前面说了Cilium——它可以不用kube-proxy,直接用eBPF来实现Service的负载均衡。
eBPF是什么?它是Linux内核里的一个虚拟机——可以在内核里运行用户写的小程序,做各种事情(网络、监控、安全……)。
Cilium用eBPF在内核里做负载均衡——比iptables和IPVS都快,因为更灵活,不需要遍历规则。
这是未来的方向——用eBPF替代iptables/IPVS,性能更好,功能更强。
9.7 网络策略:NetworkPolicy
前面讲了网络连通性——Pod之间怎么通信。但有时候我们不想让所有Pod之间都能通信——我们想做网络隔离,只有允许的才能通。
这就是NetworkPolicy(网络策略)。
9.7.1 什么是NetworkPolicy?
NetworkPolicy是什么?它是K8s的一个资源对象,用来定义Pod之间的网络访问规则——就像防火墙规则。
比如:
- 只有前端Pod能访问后端Pod的8080端口
- 只有某个命名空间的Pod能访问数据库
- 某个Pod只能访问特定的外部地址
默认情况下,K8s的网络是全通的——所有Pod之间都能互相访问。配置了NetworkPolicy之后,匹配的Pod就会被隔离——只有规则允许的流量才能通过。
9.7.2 NetworkPolicy的类型
NetworkPolicy有两种类型的规则:
- 入站规则(Ingress):控制哪些流量能进来——哪些源能访问这个Pod的哪些端口
- 出站规则(Egress):控制哪些流量能出去——这个Pod能访问哪些目的地址、哪些端口
可以分别配置,也可以都配置。
9.7.3 怎么选择Pod?
NetworkPolicy是作用在哪些Pod上的?通过标签选择器——跟Service一样,选择匹配某个标签的Pod。
然后规则里的源/目的,也可以用标签选择器选Pod,或者选命名空间,或者指定IP段。
非常灵活——你可以精确控制谁能访问谁。
9.7.4 注意:需要网络插件支持
重要提醒:NetworkPolicy不是K8s默认就支持的,需要网络插件支持。
也就是说,不是你装了K8s就能用NetworkPolicy——你的CNI插件得支持NetworkPolicy才行。
哪些支持?
- Calico:支持,而且功能比原生的还强
- Cilium:支持,基于eBPF,性能好
- Weave:支持
- Flannel:不支持!Flannel只管连通,不管网络策略
这也是为什么生产环境一般不用Flannel——功能太少了,连网络策略都没有。
9.8 扩展知识:Linux网络虚拟化基础
K8s的网络,本质上是Linux网络虚拟化的应用——veth、网桥、路由、iptables、eBPF……都是Linux内核的特性。
我们扩展讲讲这些基础概念,理解了这些,K8s网络就不神秘了。
9.8.1 veth:虚拟网卡对
veth(virtual ethernet)是什么?它是一对虚拟网卡——两个网卡是连在一起的,从一个进去的包,会从另一个出来。
就像一根虚拟的网线——两头各有一个网卡,插在不同的地方。
veth有什么用?用来连接不同的网络命名空间——比如容器的网络命名空间和宿主机的网络命名空间,用一对veth连起来,这样容器就能跟宿主机通信了。
Pod里的eth0,就是veth的一端——另一端在宿主机上,连到网桥上。
9.8.2 网桥(Bridge):虚拟交换机
网桥(Bridge)是什么?它是Linux内核里的虚拟交换机——可以把多个网络接口连起来,它们之间可以互相转发数据包。
就像物理交换机一样——把多台机器插在交换机上,它们之间就能通信了。
Docker的docker0、CNI的cni0,都是网桥——所有容器的veth都连到网桥上,容器之间就能通过网桥通信了。
9.8.3 网络命名空间(Network Namespace)
前面讲过网络命名空间——隔离网络栈。每个网络命名空间有自己的网络设备、IP地址、路由表、iptables规则。
容器的网络隔离,就是靠网络命名空间实现的——每个容器(或者说每个Pod)有自己的网络命名空间,跟其他容器隔离开。
9.8.4 iptables / Netfilter
Netfilter是什么?它是Linux内核里的网络过滤框架——可以在网络包处理的各个阶段,做过滤、修改、NAT等操作。
iptables是什么?它是用户态的工具,用来配置Netfilter的规则。
我们常说的iptables,其实指的是Netfilter + iptables工具。
K8s里的Service、SNAT、网络策略(部分实现),都是通过iptables实现的。
9.8.5 eBPF:内核可编程
eBPF(extended Berkeley Packet Filter)是什么?它是Linux内核里的一个虚拟机——可以在内核里安全地运行用户写的小程序。
eBPF能做什么?
- 网络:包过滤、负载均衡、流量控制……
- 可观测性:跟踪内核函数、统计性能指标……
- 安全:系统调用过滤、安全审计……
eBPF的好处是——不用改内核代码,不用加载内核模块,就能在内核里实现各种功能,而且性能很好。
Cilium就是基于eBPF的——用eBPF做网络、负载均衡、网络策略、可观测性,比传统的iptables/IPVS方案性能更好、功能更强。
eBPF是现在云原生领域最火的技术之一——被称为"Linux的JavaScript",因为它让内核变得可编程了。未来很多网络、可观测性、安全的功能,都会用eBPF来实现。
9.9 本章小结
这一章我们深入讲了K8s的网络模型。
总结一下重点:
- K8s网络模型的四个基本要求:Pod之间直接通信不用NAT、节点跟Pod能通信、每个Pod有自己的IP、不指定实现方式
- CNI:容器网络接口标准,所有网络方案都通过CNI跟K8s对接
- 跨节点通信的两种思路:Overlay(隧道封装,简单但有性能损失)和Underlay(路由打通,性能好但要求高)
- 主流网络插件对比:Flannel(简单)、Calico(功能强大,生产首选)、Cilium(基于eBPF,未来方向)
- kube-proxy的工作原理:iptables模式、IPVS模式、eBPF模式
- NetworkPolicy:网络策略,做Pod之间的网络隔离,需要网络插件支持
- Linux网络虚拟化基础:veth、网桥、网络命名空间、iptables、eBPF
网络是K8s里最复杂的一块,也是生产环境最容易出问题的一块。理解了网络的基本原理和各种方案的特点,你才能选对网络插件,出了问题也能排查。
下一章,我们来讲安全与权限——RBAC、NetworkPolicy、Pod Security。
第10章 安全与权限:RBAC、ServiceAccount与Pod Security
K8s很强大,但也很复杂——复杂就意味着安全风险。如果配置不当,K8s集群可能会成为安全重灾区。
这一章我们就来讲讲K8s的安全体系——怎么控制谁能访问集群、能做什么、Pod能有什么权限、网络怎么隔离。
10.1 K8s安全的四层防线
K8s的安全,可以分成四层,从外到内:
- 认证(Authentication):你是谁?——验证用户的身份
- 授权(Authorization):你能做什么?——验证用户有没有权限做某个操作
- 准入控制(Admission Control):这个请求合不合规?——在请求生效之前做检查
- 运行时安全:Pod运行的时候安全吗?——Pod的权限、网络隔离、运行时防护
我们一层层来讲。
10.2 认证:你是谁?
10.2.1 K8s的用户类型
首先,K8s里有两种"用户":
1. 普通用户(Human User)
- 真实的人,比如运维工程师、开发人员
- K8s本身不管理用户——没有用户数据库,没有用户表
- 用户是在外部管理的——比如证书、LDAP、OIDC、云厂商的IAM
2. 服务账号(ServiceAccount)
- 给Pod用的账号——Pod里的进程访问apiserver的时候用的身份
- K8s管理的资源——你可以创建ServiceAccount,给它分配权限
- 每个命名空间都有一个默认的ServiceAccount
为什么分两种?因为人和程序的认证方式不一样——人用用户名密码、证书、SSO,程序用token、证书。
10.2.2 认证方式
K8s支持很多种认证方式:
1. 客户端证书(X509 Client Certs)
- 最常用的方式——用SSL证书认证
- apiserver配置CA证书,客户端用CA签发的证书来认证
- 证书里的CN字段就是用户名,O字段是用户组
- kubeadm部署的集群,默认就是这种方式
2. 静态Token文件
- 一个静态的CSV文件,里面存着token、用户名、用户组
- 简单,但不安全——token是静态的,不能过期,改了要重启apiserver
- 现在很少用了
3. ServiceAccount Token
- ServiceAccount的token,给Pod用的
- Pod里自动挂载,进程可以用这个token访问apiserver
4. OIDC(OpenID Connect)
- 基于OAuth2的认证方式,支持SSO
- 可以对接企业的身份提供商,比如Google、Azure AD、Keycloak
- 企业级环境常用
5. Webhook Token
- 调用外部的webhook来认证token
- 灵活,可以对接自己的认证系统
6. 匿名认证(Anonymous)
- 没有认证信息的请求,当作匿名用户
- 默认是开启的——但匿名用户默认没什么权限
- 生产环境建议关掉,或者严格限制匿名用户的权限
还有其他一些方式,比如Bootstrap Token、Keystone Password……用得不多。
一般来说:
- 小集群、个人用 → 客户端证书就行
- 企业级、多用户 → OIDC对接企业身份系统
- Pod用 → ServiceAccount
10.3 授权:你能做什么?——RBAC
认证解决了"你是谁"的问题,接下来是"你能做什么"——授权。
K8s的授权有好几种模式:ABAC、RBAC、Webhook、Node……现在最常用的是RBAC(Role-Based Access Control,基于角色的访问控制)。
10.3.1 什么是RBAC?
RBAC是什么?简单说:
- 定义角色(Role)——角色有一组权限
- 把角色绑定给用户/ServiceAccount——用户就有了这个角色的权限
就像公司里的岗位:
- "运维工程师"这个岗位,有服务器的权限
- "开发工程师"这个岗位,有代码仓库的权限
- 你是运维工程师,你就有运维的权限
这种方式的好处是——灵活、可复用、好管理。用户多了,不用一个个给权限,只要给角色,然后把人放到角色里就行。
10.3.2 Role和ClusterRole
RBAC里有两种角色:
1. Role(角色)
- 命名空间级别的——只能管某个命名空间里的资源
- 比如你在default命名空间里创建一个Role,它只能管default里的资源
2. ClusterRole(集群角色)
- 集群级别的——管整个集群的资源
- 比如节点、PV、所有命名空间的资源……
为什么分两种?因为权限的范围不一样——有些权限是整个集群的,有些是某个命名空间的。
10.3.3 RoleBinding和ClusterRoleBinding
角色定义好了,怎么跟用户绑定?用Binding:
1. RoleBinding
- 把Role绑定给用户/ServiceAccount
- 绑定的是命名空间里的Role,权限只在这个命名空间生效
- 也可以绑定ClusterRole——但权限只在这个命名空间生效(相当于把ClusterRole的权限限定在某个命名空间)
2. ClusterRoleBinding
- 把ClusterRole绑定给用户/ServiceAccount
- 权限在整个集群生效
举个例子:
- 你想让张三在default命名空间里有Pod的查看权限 → 建一个Role(看Pod),然后RoleBinding绑给张三,在default命名空间
- 你想让李四在整个集群里都有Pod的查看权限 → 建一个ClusterRole(看Pod),然后ClusterRoleBinding绑给李四
10.3.4 权限怎么定义?
角色里的权限是怎么定义的?用规则(Rules):
每条规则包含:
- apiGroups:API组——比如apps、batch、extensions……核心资源的API组是空字符串
- resources:资源类型——比如pods、deployments、services……
- verbs:操作——get、list、watch、create、update、patch、delete、deletecollection……
- (可选)resourceNames:具体的资源名——限制只能操作某个具体的资源
- (可选)nonResourceURLs:非资源URL——比如/healthz、/version……
比如:
apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]这条规则的意思是——可以查看(get/list/watch)核心API组里的pods资源。
非常灵活——你可以精确控制谁能对什么资源做什么操作。
10.3.5 ServiceAccount:Pod的身份
前面提到了ServiceAccount——它是给Pod用的账号。
ServiceAccount是什么?
- 它是K8s的一个资源对象,命名空间级别的
- 每个ServiceAccount对应一个Secret——里面存着token
- Pod可以指定用哪个ServiceAccount
- Pod里的进程,可以用这个ServiceAccount的token访问apiserver
- apiserver通过RBAC判断这个ServiceAccount有没有权限
默认情况下,每个命名空间都有一个叫default的ServiceAccount——如果你不指定,Pod就用这个默认的。
默认的ServiceAccount有什么权限?——基本没有,只能访问一些公开的API。
生产环境最佳实践:
- 每个应用用自己的ServiceAccount,不要用默认的
- 给ServiceAccount分配最小权限——需要什么权限给什么,不要多给
- 不要给ServiceAccount集群管理员权限——太危险了
10.3.6 RBAC最佳实践
RBAC的最佳实践:
- 最小权限原则:只给需要的权限,能不给的就不给
- 角色复用:尽量用角色,不要给用户直接加权限——好管理
- 用组:用户加到组里,给组授权——比给单个用户授权好管理
- 定期审计:定期检查权限,清理不用的权限和账号
- 不要用cluster-admin随便给人——集群管理员权限太大了,慎用
- ServiceAccount也要管:不要给Pod的ServiceAccount太多权限
10.4 准入控制:请求合不合规?
认证和授权之后,还有一关——准入控制(Admission Control)。
10.4.1 什么是准入控制?
准入控制是什么?它是apiserver里的一些插件——在请求被认证授权之后、对象被保存到etcd之前,对请求进行检查和修改。
如果准入控制插件拒绝了这个请求,整个请求就失败了,返回错误。
打个比方:
- 认证:查身份证,看你是不是本人
- 授权:看你有没有权限进这个楼
- 准入控制:安检——看你带的东西合不合规,有没有违禁品
准入控制分两种:
- Mutating Admission:修改型的——可以修改请求的内容,比如给Pod加默认的资源限制
- Validating Admission:验证型的——只能检查,不能修改,不合规就拒绝
先执行Mutating的,再执行Validating的——先改,再检查。
10.4.2 常用的准入控制插件
K8s有很多内置的准入控制插件,常用的有:
- NamespaceLifecycle:防止删除正在使用的命名空间,防止在不存在的命名空间里创建资源
- LimitRanger:给Pod设置默认的资源限制——如果Pod没设requests/limits,自动加上
- ResourceQuota:限制命名空间的资源配额——总资源不能超过多少
- ServiceAccount:自动给Pod挂载ServiceAccount
- DefaultStorageClass:给没指定StorageClass的PVC自动加上默认的
- PodSecurity:Pod安全策略——检查Pod的安全配置
- AlwaysPullImages:总是拉取镜像——防止用本地缓存的镜像
- ……还有很多
apiserver有个参数--enable-admission-plugins,可以配置启用哪些插件。
10.4.3 动态准入控制:Admission Webhook
内置的插件不够用?没关系,K8s支持动态准入控制——你可以自己写webhook,apiserver在处理请求的时候调用你的webhook,你来决定接受还是拒绝,或者修改请求。
这就是Admission Webhook——也分Mutating和Validating两种。
Admission Webhook非常强大——你可以实现各种自定义的策略:
- 镜像必须来自指定的镜像仓库
- Pod必须设置资源限制
- 必须有某些标签
- 不能用特权容器
- 自动注入Sidecar(比如Istio就是这么干的)
- ……
很多高级功能都是通过Admission Webhook实现的——Istio、Knative、各种Operator……
10.5 Pod安全:Pod能做什么?
前面讲的是访问apiserver的权限,那Pod本身的权限呢?Pod里的进程能做什么?能不能访问宿主机的资源?
这就是Pod安全——控制Pod的权限和行为。
10.5.1 Pod Security Standards:Pod安全标准
K8s官方定义了Pod Security Standards(PSS,Pod安全标准)——三个安全等级:
1. Privileged(特权级)
- 没有限制,Pod想干嘛干嘛
- 跟直接跑在宿主机上差不多
- 风险最高
- 适用场景:系统组件、需要特权的应用(比如网络插件、存储插件)
2. Baseline(基线级)
- 基本的安全限制,防止特权提升
- 禁止特权容器、禁止HostNetwork、禁止HostPID、禁止HostIPC……
- 大部分普通应用都能满足
- 风险中等
- 适用场景:大部分普通应用
3. Restricted(严格限制级)
- 最严格的安全限制
- 除了Baseline的限制,还要求:必须以非root用户运行、不能提权、只读根文件系统、不能加危险的capabilities……
- 最安全,但有些应用跑不起来
- 适用场景:安全要求高的应用、不可信的应用
这三个等级是递进的——Privileged最松,Baseline中等,Restricted最严。
10.5.2 Pod Security Admission:内置的Pod安全准入
K8s 1.25之后,内置了Pod Security Admission(PSA)——一个准入控制插件,用来强制实施Pod安全标准。
怎么用?给命名空间打标签——指定这个命名空间用哪个安全等级:
pod-security.kubernetes.io/enforce: baseline—— 强制实施baseline标准,不符合的Pod会被拒绝pod-security.kubernetes.io/audit: restricted—— 审计模式,不符合的会记日志,但不拒绝pod-security.kubernetes.io/warn: restricted—— 警告模式,不符合的会给用户警告,但不拒绝
非常简单——给命名空间打个标签就行了,不用装额外的组件。
10.5.3 已废弃的PodSecurityPolicy(PSP)
以前K8s有个东西叫PodSecurityPolicy(PSP)——也是做Pod安全策略的。
但PSP有很多问题:
- 难用,配置复杂
- 容易踩坑——默认开启的话可能把所有Pod都拦了
- 权限模型有问题
所以K8s 1.21开始废弃PSP,1.25正式移除了——取而代之的是Pod Security Standards + Pod Security Admission。
如果你还在用老版本的K8s,注意PSP已经废弃了,新的用PSA。
10.5.4 第三方方案:OPA/Gatekeeper、Kyverno
内置的PSA比较简单,如果需要更复杂的策略怎么办?
可以用第三方的策略引擎——比如OPA Gatekeeper和Kyverno。
OPA Gatekeeper:
- 基于OPA(Open Policy Agent),用Rego语言写策略
- 功能非常强大,非常灵活
- 学习曲线比较陡——Rego语言需要学
- CNCF毕业项目,社区活跃
Kyverno:
- 专门为K8s设计的策略引擎
- 策略用YAML写,跟K8s的风格一致
- 容易上手,不用学新语言
- 功能也很强大
- CNCF孵化项目
这两个都是通过Admission Webhook实现的——比内置的PSA功能强大多了,可以实现各种复杂的自定义策略。
生产环境如果对安全要求高,建议用OPA Gatekeeper或者Kyverno。
10.5.5 Pod安全最佳实践
Pod安全的最佳实践:
- 不要用特权容器——除非万不得已,特权容器跟root没区别,太危险了
- 以非root用户运行——容器里的进程不要用root用户,用普通用户
- 只读根文件系统——容器的根文件系统设为只读,防止篡改
- 不要用HostNetwork/HostPID/HostIPC——除非必须,不要共享宿主机的命名空间
- drop所有capabilities,只加需要的——Linux capabilities,默认的太多了,能去掉的都去掉
- 设置seccomp/AppArmor/SELinux——限制系统调用,减少攻击面
- 不要挂载宿主机的敏感目录——比如/var/run/docker.sock、/proc、/sys这些
10.6 网络安全:NetworkPolicy
前面讲网络的时候提到了NetworkPolicy——网络策略,用来做Pod之间的网络隔离。
这也是安全的重要一环——就算攻击者攻破了一个Pod,也不能随便访问其他Pod。
10.6.1 默认拒绝,最小开放
网络安全的最佳实践是——默认拒绝,只开放需要的。
就是说,默认情况下,所有流量都拒绝——然后一条条加规则,只允许需要的流量通过。
这样最安全——就算有漏洞,攻击面也小。
很多人图省事,网络全通——这是很危险的。生产环境建议开启NetworkPolicy,做网络隔离。
10.6.2 分段隔离
怎么设计网络策略?可以按应用、按环境、按租户分段隔离:
- 不同环境(开发、测试、生产)之间隔离
- 不同租户之间隔离
- 不同应用之间隔离
- 前端、后端、数据库之间隔离——只有前端能访问后端,只有后端能访问数据库
分层隔离,一层一层的,就算一层被攻破了,还有下一层。
10.7 镜像安全
还有一块安全——镜像安全。镜像是应用的载体,如果镜像本身有问题,那跑起来的Pod也不安全。
10.7.1 镜像安全要点
镜像安全要注意什么:
- 用基础镜像要小心:不要用不知名的基础镜像,可能有后门、有漏洞。用官方的、可信的基础镜像。
- 最小化镜像:镜像里只放需要的东西——不要装一堆没用的工具,减少攻击面。用Distroless、Alpine之类的精简镜像。
- 扫描镜像漏洞:用镜像扫描工具(比如Trivy、Clair、Aqua)扫描镜像里的漏洞,及时修复。
- 镜像签名:对镜像进行签名,确保镜像没有被篡改——比如用Cosign。
- 只允许可信镜像仓库:只允许从指定的镜像仓库拉镜像,防止拉到恶意镜像。
- 不要用latest标签:latest标签不稳定,不知道拉的是什么版本。用具体的版本号。
- 不要在镜像里放敏感信息:密码、密钥这些不要打到镜像里,用Secret。
10.7.2 供应链安全
最近软件供应链安全很火——SolarWinds事件、Log4j事件……供应链攻击越来越多。
K8s生态里的供应链安全也很重要——你用的镜像、用的Helm Chart、用的Operator,是不是可信的?有没有被篡改?
CNCF有个项目叫Sigstore——专门做软件供应链安全,给软件签名、验证。还有SLSA——供应链安全等级框架。
这块是现在的热点,越来越受重视。
10.8 扩展知识:零信任安全模型
讲到安全,我们扩展讲讲现在很火的**零信任(Zero Trust)**安全模型。
10.8.1 什么是零信任?
传统的安全模型是边界安全——假设内网是安全的,外网是危险的。在边界建防火墙,把坏人挡在外面,里面的都是好人。
但现在这个模型不行了——
- 云原生时代,没有明确的边界了
- 攻击者一旦攻破边界,内网里横着走
- 内部威胁也越来越多——内鬼、账号泄露
零信任的理念是——从不信任,始终验证(Never trust, always verify)。
什么意思?就是:
- 不管你是从内网来的还是外网来的,都不信任
- 每次访问都要验证身份、验证权限
- 没有"内网就是安全的"这种说法
- 最小权限,只给需要的
- 持续验证,不是一次验证就完了
10.8.2 零信任在K8s里的体现
零信任在K8s里怎么落地?
- 身份认证:所有访问都要认证——人要认证,服务也要认证(mTLS)
- RBAC最小权限:只给需要的权限
- NetworkPolicy网络隔离:Pod之间默认不通,只开放需要的
- Pod安全:限制Pod的权限,减少攻击面
- mTLS:服务之间通信用mTLS加密,双向认证——服务网格(Istio、Cilium)可以做这个
- 持续监控:持续监控所有访问,异常行为告警
零信任不是一个产品,而是一种理念、一套体系——需要从多个层面一起做,才能实现真正的零信任。
云原生时代,零信任是必然的趋势——因为传统的边界安全模型已经不适用了。
10.9 本章小结
这一章我们讲了K8s的安全体系。
总结一下重点:
- K8s安全四层防线:认证 → 授权 → 准入控制 → 运行时安全
- 认证:验证身份,支持证书、Token、OIDC等多种方式
- RBAC:基于角色的访问控制,Role/ClusterRole + RoleBinding/ClusterRoleBinding
- ServiceAccount:Pod的身份,Pod用它访问apiserver
- 准入控制:在请求生效前做检查和修改,支持内置插件和动态Webhook
- Pod安全:Pod Security Standards三个等级(Privileged/Baseline/Restricted),Pod Security Admission强制实施
- 第三方策略引擎:OPA Gatekeeper、Kyverno,实现更复杂的策略
- 网络安全:NetworkPolicy做网络隔离,默认拒绝最小开放
- 镜像安全:镜像扫描、镜像签名、可信镜像仓库
- 零信任安全模型:从不信任,始终验证,云原生安全的趋势
安全是个系统工程,不是靠某一个工具就能解决的——需要从认证、授权、网络、运行时、供应链等多个层面一起做,层层防护,才能真正做好安全。
下一章,我们来讲可观测性——日志、监控、告警,怎么知道集群里发生了什么。
第11章 可观测性三支柱:日志、监控与告警
K8s集群跑起来了,应用也部署了——但你怎么知道集群运行得好不好?应用有没有问题?出了问题怎么排查?
这就是**可观测性(Observability)**要解决的问题。
可观测性是什么?简单说——通过系统的外部输出,就能理解系统的内部状态。不用进系统里面看,看日志、指标、链路追踪这些外部输出,就能知道系统里面发生了什么。
可观测性有三大支柱——日志(Logging)、指标(Metrics)、链路追踪(Tracing)。这一章我们就来讲讲K8s里的可观测性体系。
11.1 可观测性三支柱概述
先简单介绍一下三支柱:
11.1.1 日志(Logging)
日志是什么?就是应用或者系统输出的、记录发生了什么事情的文本信息。
比如:
- 应用打印的日志:"用户登录成功"、"数据库连接失败"
- 系统日志:内核日志、kubelet日志、apiserver日志
- 访问日志:Nginx的access log
日志的特点:
- 记录具体的事件——发生了什么事,什么时候发生的,详情是什么
- 是离散的、非结构化的(或者半结构化的)
- 数据量大——尤其是高并发应用,日志量非常大
- 适合排查具体问题——出了什么错,错误信息是什么
11.1.2 指标(Metrics)
指标是什么?就是可度量的、数值型的数据,按时间序列存储。
比如:
- CPU使用率、内存使用率
- QPS(每秒请求数)、延迟、错误率
- Pod数量、节点数量
指标的特点:
- 是数值型的、结构化的
- 按时间序列存储——时间 + 数值 + 标签
- 数据量相对小——因为是聚合过的
- 适合监控趋势、告警——看系统整体状态怎么样,有没有异常
11.1.3 链路追踪(Tracing)
链路追踪是什么?就是追踪一个请求经过了哪些服务,每个服务花了多长时间。
比如一个用户请求,经过了网关 → 前端 → 后端 → 数据库——链路追踪能把整个链路串起来,告诉你每个环节花了多久,哪里慢了。
链路追踪的特点:
- 记录请求的完整链路
- 适合排查性能问题、分布式系统的问题
- 微服务架构下特别有用——服务多了,不知道请求卡在哪了
11.1.4 三者的关系
三者是什么关系?怎么配合用?
一般排查问题的流程是:
- 先看指标——发现异常,比如错误率升高了、延迟变大了
- 再看链路追踪——找到是哪个服务、哪个环节出了问题
- 最后看日志——看具体的错误信息,定位根因
指标告诉你"有没有问题",链路追踪告诉你"哪里有问题",日志告诉你"为什么有问题"。
三者配合,才能完整地观测系统。
11.2 监控体系:Prometheus + Grafana
K8s生态里,监控的事实标准是Prometheus + Grafana——Prometheus负责采集和存储指标,Grafana负责可视化展示。
11.2.1 Prometheus是什么?
Prometheus是什么?它是一个开源的监控系统和时序数据库——专门用来存指标数据的。
Prometheus是CNCF的毕业项目,现在是云原生监控的事实标准。
Prometheus的特点:
- 多维数据模型:时序数据由指标名和一组键值对标签组成——非常灵活
- PromQL查询语言:强大的查询语言,可以做各种聚合、计算
- 拉模型(Pull):Prometheus主动去拉指标,不是客户端推过来——这样Prometheus可以控制拉取频率,也可以做服务发现
- 服务发现:自动发现监控目标——在K8s里,自动发现Pod、Service、节点,不用手动配置
- 强大的告警:Alertmanager,支持灵活的告警规则和通知
11.2.2 Prometheus的工作原理
Prometheus是怎么工作的?
- 服务发现:Prometheus通过服务发现机制,找到要监控的目标(target)
- 拉取指标:定期去拉取每个目标的/metrics接口,获取指标数据
- 存储:把拉到的指标数据存在时序数据库里
- 查询:用户或者Grafana通过PromQL查询数据
- 告警:根据告警规则计算,触发的告警发给Alertmanager
- 通知:Alertmanager把告警发给用户(邮件、钉钉、企业微信……)
整个过程是拉模型——Prometheus主动去拉,不是客户端推。
为什么用拉模型?
- Prometheus可以控制拉取频率
- 可以做服务发现——自动发现目标
- 方便调试——你可以手动curl目标的metrics接口,看对不对
- Prometheus挂了不影响应用——应用不用管监控的事儿
11.2.3 监控什么?——指标分类
在K8s里,我们一般监控哪些东西?
1. 基础设施层
- 节点的CPU、内存、磁盘、网络——用Node Exporter采集
- 节点的状态、数量
2. K8s组件层
- apiserver、scheduler、controller-manager、etcd、kubelet、kube-proxy这些组件的指标
- K8s资源的状态——Pod数量、Deployment状态、PVC状态……——用kube-state-metrics采集
3. 容器/ Pod层
- 每个Pod/容器的CPU、内存、网络、磁盘——用cAdvisor采集(kubelet自带)
- Pod的状态、重启次数
4. 应用层
- 应用自己的业务指标——QPS、延迟、错误率、业务量……
- 需要应用自己暴露metrics接口,Prometheus去拉
5. 黑盒监控
- 从外部探测服务是否可用——比如探测网站能不能访问、端口通不通
- 用Blackbox Exporter
一层层下来,从基础设施到应用,全覆盖。
11.2.4 Grafana:可视化
Prometheus存了数据,怎么展示?用Grafana。
Grafana是什么?它是一个开源的可视化工具——可以接各种数据源(Prometheus、MySQL、Elasticsearch……),然后做漂亮的Dashboard。
Grafana的特点:
- 支持多种数据源
- 丰富的图表类型——折线图、柱状图、饼图、表格、热力图……
- 灵活的Dashboard——可以自己拼,也可以用别人做好的
- 变量、模板——Dashboard可以复用
- 也支持告警
一般的用法是:Prometheus存数据,Grafana做Dashboard展示——运维人员平时看Grafana大盘,知道系统运行得怎么样。
K8s相关的Dashboard,Grafana官网上有很多现成的——节点监控、Pod监控、Deployment监控……直接导入就能用,不用自己从零做。
11.2.5 Alertmanager:告警
监控了,发现异常怎么办?告警啊!
Prometheus的告警是通过Alertmanager实现的:
- Prometheus负责计算告警规则——满足条件就触发告警
- Alertmanager负责管理告警——去重、分组、抑制、静默、通知
Alertmanager的功能:
- 分组:把相关的告警分到一组,一起发——避免告警风暴
- 抑制:某个告警触发了,抑制其他相关的告警——比如节点挂了,节点上的Pod告警就不用发了
- 静默:临时屏蔽某些告警——比如维护的时候
- 多种通知方式:邮件、钉钉、企业微信、Slack、PagerDuty……
告警是监控最重要的功能之一——光有Dashboard没用,你不可能24小时盯着大盘看。有问题要自动通知你。
11.3 日志体系:EFK / PLG
监控指标告诉你"出问题了",但要知道"为什么出问题",还得看日志。
K8s里的日志体系,主流的有两套:EFK和PLG。
11.3.1 EFK:Elasticsearch + Fluentd + Kibana
EFK是传统的ELK(Elasticsearch + Logstash + Kibana)的K8s版本——把Logstash换成了Fluentd。
为什么换?因为Fluentd更轻量,更适合容器环境——Logstash比较重,资源消耗大。
EFK各组件的作用:
- Fluentd:日志采集器——每个节点上跑一个(DaemonSet),收集节点上所有容器的日志,处理之后发给Elasticsearch
- Elasticsearch:日志存储和搜索引擎——存日志,支持全文检索
- Kibana:日志可视化和查询界面——查日志、做Dashboard
EFK是比较成熟的方案,用的人很多。但Elasticsearch比较重——资源消耗大,运维复杂,成本高。
11.3.2 PLG:Promtail + Loki + Grafana
PLG是Grafana Labs推出的轻量级日志方案——也叫Grafana Loki。
PLG各组件的作用:
- Promtail:日志采集器——跟Fluentd类似,收集日志,发给Loki
- Loki:日志存储——但它跟Elasticsearch不一样,它不索引日志内容,只索引标签
- Grafana:可视化——在Grafana里查日志,跟指标、链路放在一起看
Loki的设计理念是——日志就像日志,不要像搜索引擎。
什么意思?Elasticsearch把日志内容都索引了,所以能全文检索,但代价是存储和计算成本高。
Loki的思路是——大部分时候你查日志,都是先知道哪个服务、哪个Pod有问题,然后去查那个Pod的日志。所以Loki只索引标签(比如Pod名、命名空间、容器名),日志内容存在对象存储里,不索引。
这样成本就低多了——存储成本只有Elasticsearch的几分之一。而且跟Grafana集成得很好,跟指标、链路追踪在同一个界面里看,非常方便。
Loki是后起之秀,发展很快——如果你的日志量很大,想省成本,或者已经在用Grafana了,可以试试Loki。
11.3.3 日志采集的方式
K8s里的日志怎么采集?主要有几种方式:
1. DaemonSet方式(最常用)
- 每个节点上跑一个日志采集Agent(Fluentd、Promtail、Filebeat……)
- Agent收集节点上所有容器的日志
- 优点:部署简单,每个节点一个,资源开销可控
- 缺点:只能收集标准输出的日志——应用要把日志打到stdout/stderr
2. Sidecar方式
- 每个Pod里加一个Sidecar容器,负责收集日志
- 应用把日志写到文件,Sidecar收集文件里的日志
- 优点:可以收集文件日志,应用不用改
- 缺点:每个Pod都有Sidecar,资源开销大
3. 应用直接发
- 应用直接把日志发给日志系统
- 优点:不用Agent
- 缺点:应用要改代码,耦合,而且应用挂了日志可能丢
最常用的是第一种——DaemonSet方式,应用把日志打到标准输出,节点上的Agent统一收集。这也是K8s推荐的方式。
11.3.4 结构化日志
日志最好是结构化的——比如JSON格式,每个字段都有名字。
为什么?因为结构化日志好查询、好分析——你可以按字段过滤、聚合,比如查某个用户的所有日志,查错误码的分布……
如果是纯文本的非结构化日志,就很难做这些分析——只能全文搜索,而且不准。
所以最佳实践是——应用输出结构化日志(JSON格式),日志系统直接解析字段,方便查询和分析。
11.4 链路追踪:Jaeger / Zipkin / Tempo
微服务架构下,一个请求会经过很多个服务——出了问题,或者变慢了,你不知道是哪个服务的问题。
这时候就需要链路追踪(Distributed Tracing)。
11.4.1 链路追踪的基本概念
链路追踪的几个基本概念:
- Trace:一次完整的请求链路——从请求开始到结束,经过的所有服务
- Span:链路中的一个单元——一个服务里的处理过程,或者一次RPC调用
- 每个Span有:开始时间、结束时间、名称、标签、父Span ID
- 整个Trace就是一棵Span树——父Span下面有子Span
通过Trace,你可以看到:
- 请求经过了哪些服务
- 每个服务花了多长时间
- 哪个服务最慢
- 有没有错误
11.4.2 主流的链路追踪工具
主流的链路追踪工具有:
1. Jaeger
- Uber开源的,CNCF毕业项目
- 功能强大,UI好看
- 支持多种存储:Elasticsearch、Cassandra、Kafka……
- 用的人很多
2. Zipkin
- Twitter开源的,比较老牌
- 简单,轻量
- 社区也挺活跃
3. Grafana Tempo
- Grafana Labs的,跟Loki、Prometheus一套的
- 只存Trace,不索引——成本低
- 跟Grafana集成好
现在Jaeger用得比较多——功能全,社区好,CNCF毕业。
11.4.3 怎么实现链路追踪?
链路追踪不是自动就有的——需要应用配合:
- 注入Trace ID:请求进来的时候,生成一个Trace ID,然后在服务之间传递——HTTP的话放在Header里,RPC的话放在上下文里
- 生成Span:每个服务处理请求的时候,生成Span,记录开始结束时间、标签
- 上报Span:把Span数据发给链路追踪系统
这个传递Trace ID和生成Span的工作,一般用OpenTelemetry来做。
11.4.4 OpenTelemetry:统一标准
以前链路追踪的标准很乱——Zipkin有Zipkin的格式,Jaeger有Jaeger的格式,OpenTracing和OpenCensus各搞各的。
现在好了,有了OpenTelemetry(OTel)——CNCF的项目,把OpenTracing和OpenCensus合并了,成为统一的标准。
OpenTelemetry是什么?它是一套可观测性数据的标准和工具集——指标、日志、链路追踪,三者统一。
特点:
- 统一的API和SDK——多种语言都有
- 统一的数据格式——OTLP协议
- 支持指标、日志、链路追踪三支柱
- 厂商中立——你可以把数据发给任何支持OTLP的后端
现在新的项目,都推荐用OpenTelemetry来做可观测性的埋点——一次埋点,三支柱的数据都有了,而且后端可以随便换,不用改代码。
这是未来的方向——统一的可观测性标准。
11.5 告警最佳实践
监控和告警是分不开的。但告警不是越多越好——告警太多了,人就麻木了,真正重要的告警反而被淹没了。
11.5.1 告警的原则
好的告警应该遵循什么原则?
- 可行动性:每个告警都应该是需要人去处理的——如果一个告警触发了,不需要人管,那这个告警就不该存在。
- 严重性分级:告警要有级别——紧急的、重要的、一般的。不同级别不同的通知方式。
- 减少噪音:避免告警风暴——相关的告警合并,重复的告警抑制。
- 精准:告警要准确——不要误报,也不要漏报。误报多了大家就不信了。
- 有上下文:告警信息里要有足够的上下文——哪个集群、哪个服务、什么问题、相关的Dashboard链接……收到告警就能知道怎么回事,不用再去查半天。
11.5.2 告警分级
一般告警分几级:
- P0 / 紧急:系统不可用,影响大量用户——需要立刻处理,24小时oncall
- P1 / 高:重要功能不可用,影响部分用户——工作时间尽快处理
- P2 / 中:一般问题,不影响核心功能——可以排期处理
- P3 / 低:提示性的,不影响使用——看看就行
不同级别不同的通知方式:P0打电话、发短信;P1发钉钉/企业微信@所有人;P2发群消息;P3记下来就行。
11.5.3 常见的告警指标
K8s里常见的告警有哪些?
基础设施层:
- 节点宕机
- 节点CPU/内存/磁盘使用率过高
- 节点磁盘满了
- 节点网络问题
K8s层:
- Pod CrashLoopBackOff
- Pod一直Pending
- Deployment不可用副本数
- PVC挂载失败
- 组件不正常(apiserver、etcd等)
应用层:
- 错误率过高
- 延迟过高
- QPS异常
- 进程挂了
根据你的实际情况配置——不要太多,但关键的一定要有。
11.6 扩展知识:可观测性 vs 监控
很多人搞不清"可观测性"和"监控"的区别——不就是一回事吗?
其实不是。我们来辨析一下。
11.6.1 监控是什么?
**监控(Monitoring)**是什么?它是——你知道你要关注什么,你去监控它。
比如:
- 你知道CPU使用率很重要,所以你监控CPU使用率
- 你知道错误率很重要,所以你监控错误率
- 你设置阈值,超过了就告警
监控的前提是——你知道你要找什么。你知道哪些指标重要,哪些问题会发生。
11.6.2 可观测性是什么?
**可观测性(Observability)**是什么?它是——你不知道会出什么问题,但你能通过系统的输出,搞清楚发生了什么。
系统是复杂的,你不可能提前想到所有可能出问题的地方。可观测性的目标是——不管出什么你没预料到的问题,你都能通过日志、指标、链路追踪这些数据,快速定位和理解问题。
所以可观测性比监控的范围更大——监控是可观测性的一部分。监控是"已知的未知"——你知道你要监控什么;可观测性还包括"未知的未知"——你不知道会出什么问题,但你能排查出来。
11.6.3 为什么现在可观测性这么火?
因为系统越来越复杂了——微服务、云原生、分布式系统……系统太复杂了,你不可能预料到所有问题。
以前单体应用的时候,系统简单,监控几个关键指标就够了——出了问题也容易排查。
现在微服务,几十个上百个服务,请求链路很长——出了问题,你都不知道是哪个服务的问题。这时候就需要可观测性——通过三支柱的数据,快速定位问题。
这就是为什么可观测性在云原生时代这么重要——系统越复杂,越需要可观测性。
11.7 本章小结
这一章我们讲了可观测性三支柱——日志、监控、告警。
总结一下重点:
- 可观测性三支柱:日志(Logging)、指标(Metrics)、链路追踪(Tracing)——指标告诉你有没有问题,链路追踪告诉你哪里有问题,日志告诉你为什么有问题
- 监控体系:Prometheus + Grafana + Alertmanager——Prometheus采集存储指标,Grafana可视化,Alertmanager告警
- 日志体系:EFK(Elasticsearch + Fluentd + Kibana)和PLG(Promtail + Loki + Grafana)——前者成熟功能强,后者轻量成本低
- 链路追踪:Jaeger、Zipkin、Tempo——微服务下排查性能问题必备
- OpenTelemetry:统一的可观测性标准,未来的方向
- 告警最佳实践:可行动、分级、减少噪音、精准
- 可观测性 vs 监控:监控是已知的未知,可观测性还包括未知的未知
可观测性是生产环境必不可少的——没有可观测性,出了问题两眼一抹黑,根本不知道怎么回事。建好可观测性体系,是运维K8s最重要的工作之一。
下一章,我们来讲应用生命周期管理——升级、回滚、自愈。
第12章 应用生命周期管理:升级、回滚与自愈
应用部署上去不是就完事儿了——你还要升级、回滚、故障自愈、扩缩容……这些都是应用生命周期管理的内容。
K8s在这方面非常强大——很多以前需要人工做的事情,现在K8s自动帮你做了。
这一章我们就来讲讲应用的全生命周期管理——从部署到升级到回滚到自愈,K8s是怎么帮你搞定的。
12.1 部署策略:怎么发布新版本?
发布新版本是最常见的运维操作之一。怎么发布?有很多种策略,各有优劣。
12.1.1 重建发布(Recreate)
重建发布:先把旧版本全停了,再启动新版本。
- 优点:简单,不会有新旧版本同时存在的问题
- 缺点:有停机时间——旧的停了新的还没起来,中间服务不可用
- 适用场景:停机不影响的、测试环境、不重要的服务
这是最简单的方式,但生产环境一般不用——因为有停机时间。
12.1.2 滚动发布(Rolling Update)
滚动发布:逐步替换旧版本——启动几个新版本,停掉几个旧版本,慢慢全部替换完。
- 优点:没有停机时间,服务不中断
- 缺点:发布过程中新旧版本同时存在,要考虑兼容性
- 适用场景:大部分无状态应用,生产环境最常用
这是K8s Deployment的默认发布方式——也是最常用的。
12.1.3 蓝绿发布(Blue/Green)
蓝绿发布:准备两套完全一样的环境——蓝环境跑旧版本,绿环境跑新版本。新版本测试没问题了,把流量切到绿环境。
- 优点:切换快,回滚也快——切流量就行;不会有新旧版本同时存在的问题
- 缺点:需要两套资源,成本高
- 适用场景:对发布质量要求高、资源充足的场景
K8s里怎么实现蓝绿?可以用两个Deployment,一个蓝一个绿,然后Service切换selector——把selector从蓝的标签改成绿的标签,流量就切过去了。
12.1.4 金丝雀发布(Canary)
金丝雀发布:先让一小部分流量走新版本,观察没问题了再逐步扩大比例,最后全量。
为什么叫金丝雀?以前矿工下井,会带一只金丝雀——如果有毒气,金丝雀先死,矿工就知道有危险,赶紧撤。金丝雀发布就是这个意思——先小范围试,没问题再全量。
- 优点:风险小——出问题只影响一小部分用户;可以观察新版本的表现
- 缺点:发布过程慢;需要流量控制
- 适用场景:重要的服务、大版本更新、对稳定性要求高的
K8s原生的Deployment不支持金丝雀——Deployment只能控制滚动更新的速度,不能按比例控制流量。要做金丝雀发布,需要用Ingress Controller、服务网格(Istio)、或者专门的发布工具(Argo Rollouts、Flagger)。
12.1.5 A/B测试
A/B测试:跟金丝雀类似,也是一部分用户走A版本,一部分走B版本。但目的不一样——金丝雀是为了验证稳定性,A/B测试是为了验证产品效果(比如转化率、用户体验)。
A/B测试一般是按用户特征分流——比如新用户走B版本,老用户走A版本;或者按地域、按设备分流。
A/B测试需要更复杂的流量控制——一般用服务网格或者专门的A/B测试平台来做。
12.1.6 怎么选?
发布策略怎么选?
- 简单、不重要的服务 → 滚动发布就够了
- 对稳定性要求高 → 金丝雀发布
- 资源充足,要求快速回滚 → 蓝绿发布
- 产品验证 → A/B测试
大部分场景,滚动发布就够了——简单,够用。重要的服务可以用金丝雀,更稳妥。
12.2 Deployment的滚动更新与回滚
前面讲Deployment的时候提到了滚动更新,这一节我们再深入讲讲。
12.2.1 滚动更新的参数
Deployment的滚动更新有两个关键参数:
maxSurge:更新过程中最多可以比期望副本数多几个maxUnavailable:更新过程中最多可以有几个不可用
这两个参数决定了滚动更新的速度和安全性:
- maxSurge越大,更新越快——同时启动更多新版本的Pod
- maxUnavailable越小,可用性越高——不会有太多Pod不可用
- 两个都大,更新最快,但可用性差
- 两个都小,更新最慢,但可用性高
根据你的需求调整——对可用性要求高,就把maxUnavailable设小一点;想更新快,就把maxSurge设大一点。
12.2.2 回滚机制
Deployment的回滚非常方便——因为旧的ReplicaSet还在。
回滚的时候:
- 把旧的ReplicaSet的副本数加起来
- 把新的ReplicaSet的副本数减下去
- 就回到旧版本了
跟滚动更新一样,也是逐步替换的——不会一下子全换回去,保证服务不中断。
你可以查看历史版本,回滚到任意一个版本——Deployment默认保留10个历史版本,可以通过revisionHistoryLimit配置。
12.2.3 发布暂停与继续
Deployment还支持暂停和继续——你可以在发布过程中暂停,观察一下,没问题再继续。
这个功能很实用——比如你想做手动的金丝雀发布,先更新一个Pod,看看没问题,再继续更新剩下的。
12.2.4 就绪探针的重要性
滚动更新的时候,就绪探针(Readiness Probe)非常重要。
为什么?因为新版本的Pod启动了,不代表它就准备好了——可能还在初始化、加载数据、预热缓存……这时候还不能接流量。
如果没有就绪探针,Pod一启动就加入Service的后端列表,开始接流量——这时候请求就会失败,导致发布过程中有5xx错误。
有了就绪探针,Pod启动后,就绪探针成功了,才会加入Service后端,才开始接流量——这样发布过程就是平滑的,用户无感知。
生产环境一定要配置就绪探针——不然发布的时候会有抖动。
12.3 自愈能力:K8s怎么帮你"修"应用
K8s最强大的功能之一就是自愈——应用出问题了,K8s自动帮你恢复,不用人工干预。
K8s的自愈能力体现在哪些方面?
12.3.1 进程级自愈:容器重启
应用进程挂了怎么办?——kubelet自动重启容器。
这是最基础的自愈——容器退出了,根据重启策略(RestartPolicy),自动重启。
重启策略有三种:
- Always:总是重启——不管什么原因退出都重启(默认)
- OnFailure:只有失败的时候才重启——正常退出(退出码0)不重启
- Never:从不重启
大部分长期运行的服务,用Always就对了——挂了就重启。
但注意:重启不是无限的——重启次数多了,会有指数退避——第一次重启等10秒,第二次20秒,第三次40秒……最长等5分钟。防止崩溃循环一直重启,把资源耗光。
12.3.2 应用级自愈:存活探针
进程还在,但应用死锁了、挂死了、不响应了——怎么办?进程没退出,所以不会自动重启。
这时候就需要存活探针(Liveness Probe)——定期检查应用是不是健康的,不健康就杀掉容器,重启。
前面讲过存活探针——它的作用就是应用级的自愈。
有了存活探针,就算应用没崩溃,但已经不工作了,K8s也能发现,然后重启它——让应用恢复正常。
12.3.3 Pod级自愈:控制器重建Pod
Pod挂了、节点挂了——怎么办?控制器自动重建Pod。
比如Deployment管理的Pod——如果某个Pod挂了,ReplicaSet发现副本数不够了,就自动创建一个新的Pod补上。
如果节点挂了呢?——节点上的所有Pod都没了。控制器发现副本数不够了,就会在其他节点上重建这些Pod。
这就是Pod级的自愈——Pod没了,自动重建,保证副本数始终是你期望的数量。
12.3.4 节点级自愈:节点问题怎么办?
节点出问题了——比如节点宕机了、节点网络断了、节点NotReady了——上面的Pod怎么办?
K8s的处理是:
- 节点NotReady之后,等待一段时间(默认5分钟)
- 如果节点还是没恢复,就把这个节点上的Pod标记为删除
- 然后控制器在其他节点上重建这些Pod
为什么要等5分钟?因为可能只是临时的网络波动——等一会儿节点就恢复了,这时候Pod还在节点上,不用重建。如果一发现NotReady就立刻重建,可能会造成重复的Pod。
这个等待时间可以通过pod-eviction-timeout参数配置。
12.3.5 集群级自愈:节点自动修复
节点本身出问题了——比如系统挂了、磁盘坏了——怎么办?
K8s本身不会修节点——它只会把Pod调度到别的健康节点上。
但如果你在云上,配合云厂商的功能,可以做到节点级自愈:
- 节点不健康了,自动把这个节点删掉
- 自动创建一个新的节点加入集群
- 这就是集群自动伸缩(Cluster Autoscaler) + 云厂商的自动修复功能
这样从应用到节点到集群,都有自愈能力——大部分故障,K8s都能自动恢复,不用人工干预。
这就是K8s的强大之处——自愈能力,大大减少了运维工作量。
12.4 自动伸缩:HPA、VPA、CA
前面讲的自愈是"出了问题自动恢复",那负载变化了呢?——流量涨了,自动扩容;流量降了,自动缩容。
这就是自动伸缩(Auto Scaling)。
K8s里有三种自动伸缩:
12.4.1 HPA:水平Pod自动伸缩
HPA(Horizontal Pod Autoscaler):水平Pod自动伸缩——根据Pod的CPU使用率(或者其他指标),自动增加或减少Pod的副本数。
比如:
- 平均CPU使用率超过80%,就自动扩容,增加Pod数量
- 平均CPU使用率低于30%,就自动缩容,减少Pod数量
HPA是最常用的自动伸缩方式——大部分无状态应用都能用HPA。
HPA能基于哪些指标伸缩?
- 资源指标:CPU、内存使用率——最常用的
- 自定义指标:应用自己的指标,比如QPS、队列长度——通过Prometheus Adapter之类的工具接入
- 外部指标:外部系统的指标,比如消息队列的消息堆积数
HPA的工作原理:
- 定期采集指标
- 跟目标值对比
- 计算需要的副本数
- 修改Deployment/ReplicaSet的replicas
- 然后控制器去调整Pod数量
HPA本身不直接创建或删除Pod——它只是改replicas,然后控制器去做具体的调整。
12.4.2 VPA:垂直Pod自动伸缩
VPA(Vertical Pod Autoscaler):垂直Pod自动伸缩——自动调整Pod的资源requests和limits。
比如:
- Pod的CPU使用率一直很高,说明给的CPU不够——VPA自动把CPU requests调大
- Pod的内存使用率一直很低,说明给多了——VPA自动把内存requests调小
VPA有什么用?
- 自动优化资源配置——不用人工去调requests/limits
- 提高资源利用率——不会给太多浪费,也不会给太少OOM
- 适合不知道该设多少资源的应用
但VPA有个问题——调整资源需要重启Pod——因为Pod的资源是创建的时候指定的,运行中不能改(至少现在还不行)。所以VPA调整资源的时候,会重启Pod。
所以VPA一般用在:
- 不知道该设多少资源的应用
- 对重启不敏感的应用
- 跟HPA配合——VPA调资源大小,HPA调副本数量
VPA现在还是beta阶段,生产环境慎用。
12.4.3 CA:集群自动伸缩
CA(Cluster Autoscaler):集群自动伸缩——自动增加或减少节点数量。
什么时候扩容节点?——Pod调度不上去了,所有节点都满了,Pending了,CA就自动加节点。
什么时候缩容节点?——某个节点上的Pod很少,这些Pod都能调度到其他节点上,CA就把这个节点删掉。
CA的工作原理:
- 定期检查有没有Pending的Pod——有就扩容
- 定期检查节点是不是利用率太低——是就缩容,把Pod调度走,然后删掉节点
CA需要跟云厂商配合——因为要创建和删除节点,所以需要调用云厂商的API。如果是自建集群,就需要自己实现节点的增减。
12.4.4 三层伸缩的配合
这三种伸缩,是配合工作的:
- 流量涨了 → HPA先增加Pod副本数
- Pod多了,节点资源不够了 → 有些Pod Pending → CA增加节点
- 流量降了 → HPA减少Pod副本数
- Pod少了,节点利用率太低 → CA减少节点
从Pod到节点,三层自动伸缩——完全自动化,不用人工干预。
这就是云原生的弹性——用多少拿多少,按需付费,省钱又省心。
12.5 优雅终止:怎么让应用优雅地退出?
前面讲Pod生命周期的时候提到了优雅终止——这一节我们再深入讲讲。
12.5.1 为什么需要优雅终止?
为什么不能直接杀进程?因为直接杀的话:
- 正在处理的请求会中断——用户体验差
- 数据可能没保存——比如写文件写到一半,数据库事务没提交
- 资源可能没释放——比如连接没断开,临时文件没删
所以我们希望应用能优雅地退出——收到退出信号后,做完该做的事情,再退出。
12.5.2 优雅终止的过程
K8s删除Pod的时候,优雅终止的过程是这样的:
- Pod被标记为Terminating状态
- Service摘掉这个Pod——不再把新流量发给它
- 执行preStop钩子——如果配置了preStop,先执行
- 发送SIGTERM信号——给容器里的进程发SIGTERM
- 等待优雅终止时间——等terminationGracePeriodSeconds这么久,默认30秒
- 发送SIGKILL信号——如果到时间还没退出,就强杀
- 删除Pod
整个过程,应用有最多30秒的时间来做清理工作——处理完正在处理的请求、保存数据、关闭连接、释放资源……然后正常退出。
12.5.3 preStop钩子
preStop钩子是什么?它是容器终止前执行的一个操作——可以是执行一个命令,也可以是发一个HTTP请求。
preStop有什么用?
- 让应用优雅退出——比如给Nginx发-s quit,让它优雅退出
- 等待一下,等Service把这个Pod摘掉——因为Service更新后端列表有延迟,preStop等几秒,确保没有新流量进来了再退出
- 做一些清理工作
很多人忽略了preStop——结果删除Pod的时候,还有请求发过来,就失败了。因为Service更新iptables有延迟,Pod已经收到SIGTERM开始退出了,但Service还没把它摘掉,还有流量过来。
最佳实践:配置一个preStop,等个几秒(比如5-10秒),再开始退出——确保Service已经把这个Pod摘掉了,没有新流量了,再优雅退出。
12.5.4 应用要支持优雅退出
当然,光K8s支持没用——你的应用也要支持优雅退出:
- 能正确处理SIGTERM信号
- 收到信号后,停止接收新请求
- 把正在处理的请求处理完
- 保存数据、释放资源
- 然后正常退出
很多应用默认就支持——比如Nginx、Tomcat、Go的HTTP服务……但有些可能需要自己配置。
生产环境一定要配置优雅终止——不然发布、缩容、节点维护的时候,都会有请求失败,用户体验差。
12.6 扩展知识:DevOps与GitOps
讲到应用生命周期管理,就不得不提DevOps和GitOps——这是现在应用交付的主流理念。
12.6.1 DevOps是什么?
DevOps是什么?它是一种理念——开发(Dev)和运维(Ops)紧密合作,通过自动化工具,实现快速、可靠的软件交付。
DevOps的核心是——自动化和持续交付。
- 代码提交 → 自动构建 → 自动测试 → 自动部署
- 整个流程自动化,减少人工操作,提高效率和可靠性
K8s是DevOps的好帮手——声明式API、自动化部署、自愈、自动伸缩……这些都跟DevOps的理念完美契合。
12.6.2 GitOps是什么?
GitOps是DevOps的进一步发展——以Git为唯一真相来源(Single Source of Truth)。
什么意思?就是:
- 所有的基础设施和应用配置,都存在Git仓库里
- 所有的变更都通过Git提交——PR、review、合并
- 有个工具自动把Git里的状态同步到集群里
- 集群的实际状态,跟Git里的声明保持一致
GitOps的好处:
- 版本控制:所有变更都有历史,谁改了什么、什么时候改的,一清二楚
- 可审计:所有变更都有记录,符合合规要求
- 可回滚:出了问题,Git回滚一下,集群就回滚了
- 一致的环境:开发、测试、生产环境的配置都在Git里,保持一致
- 声明式:跟K8s的声明式API完美契合
GitOps的工具,主流的有两个:
- Argo CD:CNCF毕业项目,功能强大,UI好用,用的人多
- Flux:CNCF孵化项目,轻量,跟GitOps的理念更贴近
GitOps是现在云原生应用交付的主流方式——越来越多的团队在用。如果你还在用手动kubectl apply,或者用CI脚本直接部署,建议试试GitOps——体验好太多了。
12.7 本章小结
这一章我们讲了应用生命周期管理。
总结一下重点:
- 发布策略:重建、滚动、蓝绿、金丝雀、A/B测试——各有优劣,根据场景选
- Deployment的滚动更新与回滚:maxSurge和maxUnavailable控制速度和可用性,回滚方便
- 自愈能力:进程级(容器重启)、应用级(存活探针)、Pod级(控制器重建)、节点级(迁移到其他节点)——层层自愈
- 自动伸缩:HPA(水平Pod伸缩)、VPA(垂直Pod伸缩)、CA(集群节点伸缩)——三层配合,全自动弹性
- 优雅终止:preStop钩子 + SIGTERM + 优雅终止时间——让应用平滑退出,不影响用户
- DevOps与GitOps:Git为真相来源,自动化交付,是云原生应用交付的主流方式
K8s把应用生命周期的大部分事情都自动化了——部署、升级、回滚、自愈、伸缩……运维人员不用再做这些重复劳动,可以把精力放在更有价值的事情上。
下一章,我们来讲生产环境实战——集群部署与高可用。
第13章 生产环境实战:集群部署与高可用
前面讲了很多K8s的概念和原理,这一章我们来讲实战——生产环境怎么部署K8s集群?怎么做高可用?有哪些坑?
部署K8s说难不难,说简单也不简单——测试环境随便搭一个很容易,但生产环境的高可用集群,要考虑的事情就多了。
13.1 集群部署方式:选哪种?
部署K8s有很多种方式,怎么选?
13.1.1 托管K8s(云上)
如果你在云上,最简单的方式就是用云厂商的托管K8s——比如:
- 阿里云 ACK
- 腾讯云 TKE
- 华为云 CCE
- AWS EKS
- Google GKE
- Azure AKS
什么是托管K8s?就是——控制平面(Master节点)云厂商帮你管,你只管Worker节点,管你上面的应用。
优点:
- 省心——控制平面不用你管,高可用、升级、备份,云厂商都搞定了
- 稳定——云厂商有专业团队维护,比你自己搭的靠谱
- 跟云服务集成好——负载均衡、存储、网络、监控,都跟云原生集成
- 不用自己维护Master节点,省钱——Master节点不用你出钱
缺点:
- 贵——比自己搭的贵
- 不自由——有些配置不能改,受云厂商限制
- 绑定云厂商——迁移成本高
生产环境,如果在云上,优先用托管K8s——省心,稳定,出了问题找云厂商。自己搭控制平面,出了问题你得自己扛,不值得。
13.1.2 自动化部署工具(自建)
如果要自建集群,用什么工具?
主流的工具有:
1. kubeadm
- 官方推荐的集群部署工具
- 简单、易用、符合最佳实践
- 适合大多数场景
- 但只负责部署,不负责运维——升级、备份这些还要自己搞
2. kops
- Kubernetes Operations,专门用来在云上部署生产级K8s集群
- 功能强大,支持高可用、升级、备份
- 主要支持AWS,其他云支持一般
- 国外用得多,国内用得少
3. Kubespray
- 基于Ansible的部署工具
- 支持多种部署方式,可定制性强
- 支持裸金属、各种云
- 比较复杂,学习成本高
4. Rancher
- 不仅是部署工具,还是K8s管理平台
- 可以部署和管理多个K8s集群
- 有UI,操作简单
- 功能丰富,适合管理多集群
5. 二进制部署
- 手动下载二进制文件,一个个组件部署
- 最麻烦,最灵活
- 适合想深入理解K8s的,或者有特殊需求的
怎么选?
- 简单快速 → kubeadm
- 管理多集群 → Rancher
- 高度定制 → Kubespray
- 学习研究 → 二进制部署
大部分自建场景,kubeadm就够了——官方推荐,简单可靠。
13.1.3 轻量级发行版
还有一些轻量级的K8s发行版,适合资源受限的场景:
- k3s:Rancher出的轻量级K8s,二进制才不到100MB,资源占用小,适合边缘计算、IoT、开发测试
- minikube:单节点K8s,主要用来本地开发测试
- kind:Kubernetes IN Docker,把K8s跑在Docker里,适合本地开发、CI测试
这些不适合生产环境(k3s可以用于生产边缘场景),但开发测试用很方便。
13.2 高可用架构:怎么做到不宕机?
生产环境最重要的是什么?——高可用。不能有单点故障。
K8s集群的高可用,主要是控制平面的高可用——Master节点不能挂。
13.2.1 控制平面高可用
控制平面的组件:apiserver、scheduler、controller-manager、etcd。
怎么做到高可用?
etcd高可用:
- etcd本身就是分布式的,部署3个或者5个节点,组成集群
- Raft共识算法,只要半数以上节点活着,就能正常工作
- 3个节点可以容忍1个故障,5个节点可以容忍2个故障
- 生产环境至少3个etcd节点,重要的用5个
apiserver高可用:
- apiserver是无状态的,可以部署多个实例
- 前面放一个负载均衡(或者VIP),把请求分发到多个apiserver
- apiserver挂了一个没关系,还有别的
scheduler和controller-manager高可用:
- 这两个是有状态的——不能同时有多个在工作,不然会冲突
- 用选主(Leader Election)机制——多个实例,只有一个Leader在工作,其他的待命
- Leader挂了,其他的自动选一个新的Leader顶上
- 所以部署多个实例就行,自动选主
总结一下,控制平面高可用架构:
- 3个或5个Master节点
- etcd集群部署在Master节点上(或者独立部署)
- apiserver多实例,前面负载均衡
- scheduler和controller-manager多实例,自动选主
这样,任何一个Master节点挂了,都不影响集群正常工作。
13.2.2 负载均衡:apiserver前面的入口
多个apiserver,前面需要一个负载均衡——把请求分发到健康的apiserver上。
负载均衡怎么搞?
- 云上的话,用云厂商的负载均衡(SLB/ELB)
- 自建的话,可以用HAProxy、Nginx、Keepalived + VIP
- 也可以用硬件负载均衡
负载均衡本身也要高可用——不能负载均衡自己成了单点。所以一般是两个负载均衡节点,做HA,用Keepalived飘VIP,或者用云厂商的托管LB。
13.2.3 Worker节点高可用
Worker节点呢?——Worker节点本身就是多的,一个挂了,Pod会调度到其他节点上。
但要注意:
- 节点数量要足够——不能只有一两个,挂一个就影响很大
- 应用要多副本——单副本的话,节点挂了应用就挂了
- 用反亲和性——把同一个应用的Pod分散到不同节点,避免单点故障
- 跨可用区部署——如果在云上,把节点分布在不同可用区,一个可用区挂了还有别的
13.2.4 集群级高可用的最佳实践
生产环境高可用最佳实践:
- 控制平面3节点起步:至少3个Master节点,etcd也3节点
- etcd独立部署(可选):如果集群很大,etcd可以独立部署在专门的节点上,不跟其他控制平面组件混部,性能更好,更稳定
- apiserver前面有负载均衡:多apiserver实例 + LB
- Worker节点跨可用区:分布在不同AZ,提高可用性
- 关键应用多副本 + 反亲和:避免单点故障
- 存储高可用:用分布式存储,不要用本地存储——本地存储节点挂了数据就没了
- 备份:定期备份etcd——这是K8s的"数据库",丢了就全完了
13.3 集群规划:多大的集群?多少节点?
部署集群之前,要做规划——集群多大?多少节点?节点什么配置?
13.3.1 集群规模
首先,集群规模多大?——几个节点?几十个?几百个?几千个?
K8s官方说,单个集群最多支持:
- 5000个节点
- 15万个Pod
- 30万个容器
- 每个节点最多110个Pod
但这是理论最大值——实际生产环境,不建议搞这么大的单集群——太大了管理复杂,出问题影响面大。
一般建议:
- 小集群:几个到几十个节点——没问题
- 中等集群:几百个节点——没问题
- 大集群:上千个节点——要做性能调优,而且建议按业务、按环境分集群
- 超大集群:几千个——建议用联邦(Federation)或者多集群管理
不要把所有东西都塞到一个集群里——按环境(开发/测试/生产)、按业务线、按重要性分集群,隔离故障域。
13.3.2 节点配置
节点用什么配置?——CPU、内存、磁盘多大?
这取决于你的应用——不同应用需求不一样。
一般来说:
- Master节点:不需要太高配置——4核8G起步,大集群8核16G、16核32G
- Worker节点:看应用——一般8核16G、16核32G、32核64G比较常见
- etcd节点:对磁盘IO要求高——一定要用SSD,最好NVMe,etcd对延迟很敏感
节点不是越大越好——大节点的话,一个节点挂了影响大;小节点的话,节点数量多,管理成本高。根据你的情况选合适的。
13.3.3 网络规划
网络也要规划:
- Pod CIDR:Pod的IP段——选一个大的私网段,比如10.244.0.0/16
- Service CIDR:Service的IP段——另一个私网段,比如10.96.0.0/12
- 节点IP段:跟你现有网络规划一致
注意:这几个网段不能重叠,也不能跟你现有网络的网段重叠——不然会有路由冲突。
13.3.4 命名空间规划
命名空间怎么规划?——怎么分命名空间?
常见的分法:
- 按环境分:dev、test、staging、prod——每个环境一个命名空间
- 按团队/业务线分:每个团队一个命名空间
- 按应用分:每个大应用一个命名空间
一般是组合——比如按环境 + 按团队。不要太粗也不要太细——太少了隔离不够,太多了管理麻烦。
13.4 生产环境检查清单:上线前要检查什么?
集群搭好了,应用部署了,能不能上线?——上线前要做检查。
我给你列一个生产环境检查清单:
13.4.1 集群层面
- [ ] 控制平面高可用:3个Master节点,etcd集群正常
- [ ] apiserver前面有负载均衡
- [ ] 节点跨可用区部署
- [ ] 网络插件安装配置正确
- [ ] CoreDNS正常
- [ ] Ingress Controller部署好了,高可用
- [ ] 存储类配置好了,动态供给正常
- [ ] 监控告警系统部署好了(Prometheus + Grafana + Alertmanager)
- [ ] 日志系统部署好了(EFK/Loki)
- [ ] RBAC配置好了,权限最小化
- [ ] Pod安全策略配置了(PSA / OPA)
- [ ] NetworkPolicy配置了(可选,但建议有)
- [ ] etcd定期备份,并且验证过备份能恢复
- [ ] 集群升级方案准备好了
- [ ] 灾备方案准备好了
13.4.2 应用层面
- [ ] 应用有多副本,至少2个
- [ ] 配置了Pod反亲和性,分散在不同节点
- [ ] 配置了资源requests和limits
- [ ] 配置了存活探针和就绪探针
- [ ] 配置了优雅终止(preStop + 合理的优雅终止时间)
- [ ] 配置了HPA(如果需要自动伸缩)
- [ ] 配置用ConfigMap和Secret,不打在镜像里
- [ ] 敏感信息用Secret,或者外部密钥管理
- [ ] 镜像来自可信仓库,扫描过漏洞
- [ ] 应用日志输出到标准输出
- [ ] 应用暴露了metrics接口,接入了监控
- [ ] 发布策略配置合理(滚动更新参数)
- [ ] 回滚方案验证过
13.4.3 运维层面
- [ ] 有完整的监控大盘
- [ ] 关键告警配置好了,告警通道正常
- [ ] 日志能正常查询
- [ ] 故障排查文档有了
- [ ] 运维人员培训过
- [ ] 有oncall机制
- [ ] 定期演练——故障演练、灾备演练
这个清单可以根据你的实际情况增减——但关键的项一定要检查,不然上线了出问题就麻烦了。
13.5 生产环境常见的坑
最后,讲讲生产环境常见的坑——很多人都踩过,你可以避开。
13.5.1 资源相关的坑
坑1:不设资源requests/limits
- 后果:Pod随便用资源,一个应用把节点资源吃光了,影响其他应用;调度也不准
- 解决:所有Pod都设requests和limits,用LimitRange给命名空间设默认值
坑2:requests设太大,浪费资源
- 后果:资源利用率低,节点很多但跑不了几个Pod,浪费钱
- 解决:根据实际用量合理设置requests,用VPA或者监控数据调优
坑3:内存limits设得太接近实际用量
- 后果:稍微涨一点就OOM Kill,频繁重启
- 解决:给内存留一定余量,监控内存使用率,及时调整
13.5.2 网络相关的坑
坑4:Service没配就绪探针,发布有抖动
- 后果:发布的时候,Pod还没准备好就接流量,有5xx错误
- 解决:所有对外服务都配就绪探针,就绪了再接流量
坑5:没配置优雅终止,删除Pod有5xx
- 后果:缩容、发布、节点维护的时候,请求失败
- 解决:配置preStop + 优雅终止,应用支持优雅退出
坑6:网络插件选得不对
- 后果:性能差、功能不够、出问题不好排查
- 解决:根据需求选合适的CNI,生产环境优先选Calico这种成熟的
13.5.3 存储相关的坑
坑7:用本地存储跑有状态应用
- 后果:节点挂了数据就没了,Pod迁移到别的节点数据跟不上
- 解决:用分布式存储或者云存储,不要用本地存储跑重要数据
坑8:PV的回收策略是Delete,误删PVC数据没了
- 后果:手滑删了PVC,数据就没了,哭都来不及
- 解决:重要数据的PV回收策略设为Retain
坑9:不备份数据
- 后果:存储集群挂了、误操作了,数据全丢
- 解决:定期备份,并且验证备份能恢复——没验证过的备份等于没有备份
13.5.4 安全相关的坑
坑10:默认ServiceAccount权限太大
- 后果:Pod被攻破了,攻击者能用ServiceAccount的权限搞事情
- 解决:每个应用用自己的ServiceAccount,最小权限原则
坑11:容器用root用户运行
- 后果:容器被攻破了,攻击者就是root,危害大
- 解决:用非root用户运行容器,配置Pod安全策略
坑12:镜像里有敏感信息
- 后果:密码、密钥打到镜像里了,谁都能看到
- 解决:用Secret,不要把敏感信息打到镜像里
13.5.5 运维相关的坑
坑13:不备份etcd
- 后果:etcd挂了数据丢了,整个集群就没了
- 解决:定期备份etcd,并且测试恢复
坑14:直接在生产环境改东西,不记录
- 后果:出了问题不知道改了什么,回滚都不知道怎么回
- 解决:用GitOps,所有变更走Git,有记录,可回滚
坑15:没有监控告警
- 后果:出了问题不知道,用户先发现了
- 解决:建好监控告警体系,关键指标都要告警
这些坑都是前人踩过的——你知道了,就能避开。
13.6 扩展知识:K8s发行版与生态选择
前面提到了很多工具和方案——K8s生态太丰富了,有时候选择太多也是一种烦恼。
怎么选?我给你一些建议:
网络插件:
- 通用生产场景 → Calico
- 追求性能、eBPF → Cilium
- 简单测试 → Flannel
存储:
- 云上 → 用云厂商的云盘、云NAS
- 自建块存储 → Ceph RBD
- 自建文件存储 → CephFS / NFS
- 对象存储 → MinIO / 云OSS
Ingress Controller:
- 通用 → Nginx Ingress
- 云原生、简单 → Traefik
- 服务网格 → Istio Gateway / Cilium Gateway
监控:
- 指标 → Prometheus + Grafana
- 日志 → Loki(轻量) / Elasticsearch(功能强)
- 链路追踪 → Jaeger
- 统一标准 → OpenTelemetry
发布/GitOps:
- GitOps → Argo CD
- 高级发布策略(金丝雀、蓝绿) → Argo Rollouts / Flagger
安全策略:
- 简单 → Pod Security Admission
- 复杂 → OPA Gatekeeper / Kyverno
服务网格:
- 功能全面 → Istio
- 轻量、eBPF → Cilium Service Mesh / Linkerd
没有最好的,只有最合适的——根据你的需求、团队能力、环境来选。不要追求技术时髦,选成熟的、团队能hold住的。
13.7 本章小结
这一章我们讲了生产环境的集群部署与高可用。
总结一下重点:
- 部署方式:云上优先用托管K8s,自建用kubeadm/Rancher/Kubespray
- 高可用架构:控制平面3节点起步,etcd集群,apiserver负载均衡,scheduler/controller-manager选主
- 集群规划:规模、节点配置、网络、命名空间,提前规划好
- 生产环境检查清单:集群层面、应用层面、运维层面,上线前都要检查
- 常见的坑:资源、网络、存储、安全、运维,避开这些坑
- 生态选择:根据需求选合适的工具,不要追求时髦,选成熟稳定的
生产环境跟测试环境不一样——测试环境怎么折腾都行,生产环境要稳。高可用、监控、备份、安全,这些都不能少。上线前多检查,上线后少踩坑。
下一章,我们来讲性能调优与大规模集群——万节点集群的秘密。
第14章 性能调优与大规模集群:万节点集群的秘密
小集群怎么都好说——几个节点,几十个Pod,怎么跑都没问题。但集群大了——几百上千个节点,几万个Pod——性能问题就出来了。
K8s能支持多大的集群?官方说单集群最多5000节点、15万Pod——但这是调优过的。你要是默认配置跑,可能几百节点就扛不住了。
这一章我们就来讲讲K8s的性能调优——怎么让集群跑得更快、更稳、更大规模。
14.1 性能瓶颈在哪?——K8s的性能分析
集群慢了,卡了,瓶颈在哪?我们先分析一下K8s各个组件的性能特点。
14.1.1 etcd:K8s的心脏,最容易成瓶颈
etcd是K8s的"数据库"——所有数据都存在etcd里。etcd的性能,直接决定了整个集群的性能。
etcd容易成为瓶颈的原因:
- 所有组件都要跟apiserver打交道,apiserver又要读写etcd
- etcd是强一致的,写操作要过半节点确认,延迟相对高
- etcd对磁盘IO非常敏感——磁盘慢的话,etcd性能会很差
- 集群大了,数据量多了,etcd的压力也大
etcd的常见性能问题:
- 磁盘IO不够——用机械盘的话,etcd会非常慢
- 网络延迟高——etcd节点之间网络不好,Raft共识慢
- 数据量太大——存了太多没用的资源,历史版本太多
- 写请求太多——大量的创建、更新操作
etcd是K8s性能的重中之重——etcd慢了,整个集群都慢。
14.1.2 apiserver:集群的入口,压力最大
apiserver是所有请求的入口——kubectl、控制器、调度器、kubelet、用户……都要访问apiserver。
集群大了,apiserver的压力会非常大:
- 大量的读请求——各种控制器、kubelet都在Watch资源
- 大量的写请求——Pod创建、更新、状态上报
- 大的List请求——比如全量拉取所有Pod
apiserver的常见性能问题:
- 请求太多,QPS太高
- 大查询太多——比如一次list所有Pod,数据量大,耗内存耗CPU
- 认证授权慢——插件太多,每个请求都要走一遍
- etcd慢——apiserver读写etcd慢
14.1.3 调度器:大规模下的挑战
调度器的压力主要在大规模集群——节点多了,Pod多了,调度的开销就大了。
调度器的性能问题:
- 预选阶段:节点多了,每个Pod都要遍历所有节点,开销大
- 优选阶段:要给所有合格节点打分,开销也大
- 调度吞吐量:每秒能调度多少个Pod?大规模下调度速度跟不上
14.1.4 kubelet:节点上的管家
kubelet的压力主要在节点上Pod多了的时候:
- 节点上Pod多了,kubelet要管理的容器就多
- 状态上报频繁,给apiserver造成压力
- Pod生命周期管理的开销
默认每个节点最多110个Pod——一般也不会跑这么多,但跑个五六十个是常有的。
14.1.5 控制器:控制循环的压力
各种控制器——Deployment、ReplicaSet、Endpoint、Node controller……
控制器的压力主要在:
- 资源多了,控制循环的开销大
- Watch事件多了,处理不过来
- 比如Endpoint控制器,Service和Pod多了,Endpoint更新非常频繁
14.2 etcd性能调优
etcd是重中之重,我们先讲etcd的调优。
14.2.1 用SSD,最好NVMe
etcd对磁盘IO延迟非常非常敏感——磁盘慢,etcd就慢,整个集群都慢。
etcd一定要用SSD,最好是NVMe SSD——绝对不要用机械盘。
怎么看etcd的磁盘性能?看etcd的wal_fsync_duration_seconds和backend_commit_duration_seconds指标——
- wal_fsync正常应该在10ms以内,超过50ms就有问题了
- backend_commit正常应该在100ms以内,超过1s就有问题了
如果这两个指标很高,说明磁盘IO不够,赶紧换SSD。
14.2.2 etcd节点规划
etcd节点怎么规划?
- 数量:3个或5个——不要太多,也不要太少。3个可以容忍1个故障,5个可以容忍2个。再多的话,写性能反而下降——因为要更多节点确认。
- 配置:CPU不用太多,4核8核就行;内存要够,能把数据都放内存里最好;磁盘一定要快。
- 独立部署:大集群建议etcd独立部署——不跟其他控制平面组件混部,专门的节点,专门的磁盘。这样etcd的性能更稳定,不受其他组件影响。
- 网络:etcd节点之间网络要好,延迟低——最好在同一个机房,同一个交换机。跨地域的话延迟太高,etcd性能会很差。
14.2.3 压缩和碎片整理
etcd是MVCC的——会保留历史版本。时间长了,历史版本越来越多,数据量越来越大,性能就下降了。
怎么办?
- 自动压缩:开启自动压缩——定期删除老的历史版本。比如保留最近1小时的历史,之前的都删掉。
- 碎片整理:压缩之后,磁盘空间不会自动释放,会有碎片——要定期做碎片整理,把空间收回来。
K8s默认会做一些,但大集群建议你自己调优一下压缩策略,定期做碎片整理。
14.2.4 控制etcd的数据量
etcd里存的东西越多,性能越差。所以要控制etcd的数据量:
- 不要在etcd里存大的Secret、ConfigMap——太大的东西别往etcd里放
- 定期清理没用的资源——旧的ReplicaSet、没用的Pod、过期的Job……
- Event保留时间不要太长——Event默认只保留1小时,不要改太大,Event很占空间
- 不要用etcd存应用数据——etcd是K8s的元数据存储,不是应用数据库
控制etcd的数据量,是保持etcd性能的关键。
14.3 apiserver性能调优
apiserver是集群的入口,压力大,也要调优。
14.3.1 水平扩展apiserver
apiserver是无状态的,可以水平扩展——部署多个apiserver实例,前面放负载均衡。
集群大了,apiserver的CPU、内存不够了,加实例就行——横向扩展。
一般3-5个apiserver实例就够很大的集群用了。
14.3.2 开启API优先级与公平性
K8s 1.20之后,有个功能叫API Priority and Fairness(API优先级与公平性)——给不同的请求分优先级,重要的请求优先处理,防止不重要的请求把apiserver占满了。
比如:
- 控制平面的请求(控制器、调度器)优先级高
- 普通用户的请求优先级中等
- 事件上报、领导选举之类的优先级低
这样就算请求很多,也不会把重要的请求饿死——保证控制平面正常工作。
大集群一定要开启这个功能——不然请求多了,apiserver容易被打挂。
14.3.3 优化List请求
大的List请求非常耗资源——比如一次list所有Pod,要从etcd里把所有Pod都读出来,序列化,返回给客户端——非常耗CPU和内存。
怎么优化?
- 用分页(Limit + Continue)——不要一次全拉出来,分页拉
- 用Watch代替List——不要频繁全量List,用Watch增量更新
- 用Selector过滤——只拉需要的,不要全拉
客户端也要注意——不要动不动就全量List,尽量用Watch。
14.3.4 缓存与Watch
apiserver前面可以加缓存——比如用apiserver的watch cache,或者用API Aggregation,或者用第三方的缓存层。
大部分读请求,其实可以从缓存里读——不用每次都去etcd读。这样既能减轻etcd的压力,也能提高apiserver的性能。
14.4 调度器性能调优
大规模集群,调度器的性能也很重要。
14.4.1 调度器调优参数
调度器有一些参数可以调:
- kube-api-qps / kube-api-burst:调度器跟apiserver通信的QPS限制——大集群可以调大一点,不然调度速度上不去
- numThreads:调度线程数——默认是1,大集群可以调大,并行调度多个Pod
- percentageOfNodesToScore:优选阶段,只给一定比例的节点打分,不是所有节点都打分——节点多了之后,不用所有节点都打分,选一部分就行,能大大提高调度速度
比如percentageOfNodesToScore默认是动态的——节点越多,比例越低。5000节点的话,只给10%的节点打分——这样调度速度快很多,而且对调度质量影响不大。
14.4.2 调度器框架优化
调度器框架本身也有优化——比如:
- 预选阶段并行化——多个节点并行检查
- 缓存优化——节点信息缓存,不用每次都去apiserver拉
- 增量更新——只有变化的才更新,不用全量算
新版本的K8s调度器性能已经很好了——几千个节点的集群,调度延迟也能接受。
14.4.3 多调度器
如果调度吞吐量还是不够,可以用多个调度器——不同的Pod用不同的调度器,分散压力。
但一般不需要——单个调度器就能处理很大的集群了。
14.5 kubelet与节点性能调优
节点上的kubelet也有调优空间。
14.5.1 kubelet调优参数
kubelet的一些参数:
- kube-api-qps / kube-api-burst:kubelet跟apiserver通信的QPS——节点上Pod多的话可以调大一点
- podsPerCore:每个核最多跑多少个Pod——限制节点上的Pod密度
- maxPods:节点最多跑多少个Pod——默认110,可以根据节点配置调整
- eventRecordQPS:事件上报的QPS——限制事件上报的频率,减轻apiserver压力
- syncFrequency:Pod同步频率——多久同步一次Pod状态,不用太频繁
14.5.2 节点资源预留
节点上的资源,不能全部分给Pod——还要留一部分给系统进程、kubelet、Docker这些用。
如果不留,Pod把资源用光了,系统进程、kubelet就没资源了——节点就会出问题。
所以要配置资源预留——给系统和kubelet预留一部分CPU和内存:
system-reserved:给系统进程预留的资源kube-reserved:给kubelet等K8s组件预留的资源eviction-threshold:驱逐阈值——节点资源不够了,就驱逐Pod,保证节点稳定
生产环境一定要配置资源预留——不然节点容易出问题。
14.5.3 容器运行时调优
容器运行时(containerd、Docker)也可以调优:
- 镜像拉取的并发数
- 镜像清理策略
- 日志大小限制——容器日志不要无限增长,不然磁盘会满
- 容器的资源限制
特别是日志——一定要限制容器日志的大小,不然跑久了日志把磁盘占满了,节点就挂了。
14.6 大规模集群的架构优化
集群大了,光调优单个组件不够,还要从架构上优化。
14.6.1 分片与分区
怎么应对超大规模?——分片。
比如:
- 按节点分片:把节点分成多个组,每个调度器只管一组节点——这样每个调度器要管的节点数就少了,调度更快
- 按命名空间分片:不同的命名空间用不同的控制器——分散压力
- etcd分片:不同的资源存在不同的etcd集群里——比如事件存在单独的etcd集群,不跟核心资源混
K8s的一些新特性,比如调度器框架、API分组,都是为了支持更大规模的。
14.6.2 事件分离
Event(事件)是非常占etcd空间的——而且Event不重要,丢了也没关系。
大集群建议把Event分离出去——存在单独的etcd集群里,或者存在其他存储里,不跟核心资源共用etcd。
这样既能减轻核心etcd的压力,也能减少核心etcd的数据量。
14.6.3 控制平面与数据平面分离
大集群一定要把控制平面和数据平面分开——Master节点专门跑控制平面组件,不跑业务Pod。
为什么?
- 控制平面很重要,不能受业务Pod影响——业务Pod把资源吃光了,影响控制平面就麻烦了
- 控制平面的性能有保障——专门的节点,资源有保证
- 安全——控制平面跟业务隔离,更安全
生产环境,Master节点一定要打污点,不让业务Pod调度上去。
14.6.4 什么时候该分集群?
不是所有情况都要搞单集群超大规模——集群太大了,管理复杂,出问题影响面大。
什么时候应该分集群?
- 不同环境(开发/测试/生产)——必须分开
- 不同业务线——重要的跟不重要的分开
- 不同地域——不同地域的分开,延迟低
- 不同安全等级——高安全的跟普通的分开
- 单集群太大了——超过几千节点了,建议分
一般来说,单集群几千节点是比较合适的规模——再大的话,管理成本和风险都会上升。
不要追求"一个集群管所有"——多集群,故障隔离,反而更稳。
14.7 性能监控:怎么知道性能好不好?
调优的前提是——你知道性能好不好,瓶颈在哪。
怎么监控K8s的性能?
14.7.1 关键性能指标
要监控哪些指标?
etcd指标:
- 读写延迟(wal_fsync_duration、backend_commit_duration)
- QPS
- 数据库大小
- Raft提案延迟
- 节点之间的网络延迟
apiserver指标:
- 请求QPS
- 请求延迟(按API、按方法、按状态码)
- 工作队列长度
- 认证授权延迟
- etcd请求延迟
调度器指标:
- 调度延迟(预选、优选、绑定各阶段的时间)
- 调度吞吐量(每秒调度多少个Pod)
- 调度失败的数量
- 调度队列长度
kubelet指标:
- Pod同步延迟
- PLEG(Pod生命周期事件生成器)延迟
- 节点上Pod数量
- 容器运行时延迟
整体指标:
- API请求成功率、错误率
- 各种控制器的工作队列长度、同步延迟
- Pod启动时间——从创建到Running要多久
这些指标都能从Prometheus里拿到——各个组件都暴露了metrics接口。
14.7.2 性能基准测试
怎么测集群的性能?——用基准测试工具。
常用的工具有:
- kubemark:K8s官方的性能测试工具,可以模拟大量节点和Pod
- clusterloader2:K8s SIG Scalability的测试工具,用来测集群的性能和规模
- knb:K8s网络基准测试,测网络性能
通过基准测试,你可以知道你的集群能扛多大规模,性能瓶颈在哪。
14.8 扩展知识:K8s的性能演进历史
最后,我们聊聊K8s的性能演进——看看K8s这些年在性能上做了哪些优化。
14.8.1 早期的K8s:1.0时代
K8s 1.0的时候,性能其实不怎么样——
- 单集群最多支持几百个节点
- etcd用的是v2 API,性能一般
- 调度器比较简单,性能一般
- 各种控制器也比较简单
那时候大家都说K8s重、慢,不适合小集群。
14.8.2 etcd v3:性能飞跃
K8s 1.6左右,切换到了etcd v3 API——这是个大的性能飞跃。
etcd v3比v2好太多了:
- 性能更好,延迟更低
- 支持更多的并发连接
- 存储更高效
- 有gRPC接口,比HTTP JSON快多了
切换到etcd v3之后,K8s的性能上了一个大台阶——支持的集群规模大了很多。
14.8.3 调度器优化
调度器也做了很多优化:
- 预选并行化
- 优选只给部分节点打分(percentageOfNodesToScore)
- 调度器框架重构,更高效
- 调度吞吐量大大提高
现在的调度器,几千个节点的集群,调度一个Pod也就几十毫秒——非常快。
14.8.4 API Server优化
apiserver也做了很多优化:
- Watch缓存优化
- 分页支持
- API优先级与公平性
- 等等
现在的apiserver,单实例就能扛很高的QPS——多实例水平扩展,能支持更大的规模。
14.8.5 未来的方向
K8s性能优化的未来方向:
- eBPF:用eBPF优化网络、负载均衡,替代iptables/IPVS
- 更高效的存储:etcd继续优化,或者探索新的存储后端
- 更智能的调度:AI辅助调度,更高效的装箱
- 分片架构:控制平面分片,支持更大规模
K8s的性能一直在进步——现在能支持5000节点,未来可能能支持更大的规模。
但还是那句话——不是集群越大越好。适合你的规模,才是最好的。
14.9 本章小结
这一章我们讲了K8s的性能调优与大规模集群。
总结一下重点:
- K8s的性能瓶颈主要在etcd、apiserver、调度器、kubelet这几个组件
- etcd是重中之重——一定要用SSD,独立部署,控制数据量,定期压缩
- apiserver可以水平扩展,开启API优先级与公平性,优化大查询
- 调度器可以调优线程数、打分节点比例,提高调度吞吐量
- kubelet要配置资源预留,限制Pod密度,优化跟apiserver的通信
- 大规模集群要从架构上优化——控制平面与数据平面分离、事件分离、分片
- 不是集群越大越好——到了一定规模就该分集群了
- 要监控关键性能指标,知道瓶颈在哪,针对性调优
性能调优是个持续的过程——不是调一次就完了。集群在变,应用在变,性能也会变。持续监控,持续优化,才能让集群始终保持良好的性能。
下一章,我们来讲故障排查方法论——出了问题怎么查。