Day 2:控制面 deep dive + etcd 内核 + chaos drill
Day 1 把 5 节点 K8s 集群拉到 Ready,但「Ready」离「敢动」还差一截。Day 2 干三件事:
- 打开 apiserver 的 audit log,记住每个改过 Secret 的人
- 把 etcd 的 5 个核心概念(raft / MVCC / compact / defrag / snapshot)逐个跑一遍真实命令
- 故意把 etcd 杀掉看 K8s 怎么挂、怎么救 —— 包括从 snapshot restore 的灾难恢复演练
整个 Day 2 的隐藏主线是:控制面崩了,数据面(worker 上跑的业务 Pod)会不会跟着崩?这一题的答案直接决定面试里你对 K8s 架构理解的深度。
A. apiserver 启用 audit
kubeadm 拉起来的集群默认不开 audit log,apiserver 既不知道谁删了 Secret,也不知道哪个 SA 在轮询整个集群。生产合规(PCI / SOC2 / HIPAA)这一项必过,自己排查 incident 也得靠它。
A.1 写 audit policy
audit 有 4 个 level,从轻到重:
| level | 记什么 | 用在哪 |
|---|---|---|
None | 什么都不记 | 高频低价值 noise 过滤(configmap / events / leases 的 get/list/watch) |
Metadata | user / verb / resource / name / namespace / response code | 默认级别,最常用 |
Request | 上面 + request body | 改写类操作,但 GET 看不到回参 |
RequestResponse | 上面 + response body | Secret 写这种高敏感操作,占盘最大 |
策略写法是先排除 noise,再单点抬高 Secret 的级别,最后全局兜底 Metadata:
# /etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages: ["RequestReceived"] # 只记 ResponseStarted/Complete,省一半行数
rules:
# 1. 高频低价值 noise 不记
- level: None
resources:
- group: ""
resources: ["configmaps", "endpoints", "events"]
- group: "coordination.k8s.io"
resources: ["leases"]
verbs: ["get", "list", "watch"]
# 2. 系统 SA 的自查不记
- level: None
users: ["system:kube-proxy", "system:kube-scheduler", "system:kube-controller-manager"]
verbs: ["get", "list", "watch"]
# 3. Secret 写操作 full RequestResponse(含 body)
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
verbs: ["create", "update", "patch", "delete"]
# 4. 兜底 Metadata
- level: Metadata
A.2 patch apiserver static pod manifest
apiserver 是 static pod,配置文件在 /etc/kubernetes/manifests/kube-apiserver.yaml。加 4 个 flag + 1 个 hostPath volume mount:
spec:
containers:
- command:
- kube-apiserver
# ... 原有 flag
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=7 # 保留 7 天
- --audit-log-maxbackup=3 # rotate 3 个
- --audit-log-maxsize=100 # 单文件 100MB 切
volumeMounts:
- mountPath: /etc/kubernetes/audit-policy.yaml
name: audit-policy
readOnly: true
- mountPath: /var/log/kubernetes
name: audit-log
volumes:
- hostPath:
path: /etc/kubernetes/audit-policy.yaml
type: File
name: audit-policy
- hostPath:
path: /var/log/kubernetes
type: DirectoryOrCreate
name: audit-log
改完 不需要 systemctl restart kubelet。kubelet inotify-watch 着 /etc/kubernetes/manifests/ 目录,文件修改 ~10s 内自动 reconcile 重起 apiserver pod。
A.3 为什么这么写
| 决策 | 不做的后果 |
|---|---|
| omitStages 排掉 RequestReceived | 每个请求两条日志(接收 + 完成),盘量翻倍 |
| configmap/event/lease 的 get/list/watch 不记 | controller-manager / kubelet 每秒几十次轮询,单机几小时就上 GB |
| Secret 写 RequestResponse 含 body | 不记 body 出事时只知道「有人改过」,不知道改成了什么 |
| log rotate 100MB × 3 + 7 天 | audit.log 长成几十 GB 把 /var 撑爆,整个节点 NotReady |
--audit-log-maxsize 是单位 MB 不是 byte | 写 100 = 100MB,写 100000000 当字面值用反而立刻切 |
真坑:system:anonymous 高频 get 暴露 anonymous auth 默认开
audit.log 滚起来后 grep 关键字:
jq -r 'select(.user.username == "system:anonymous") | .verb + " " + .objectRef.resource' \
/var/log/kubernetes/audit.log | sort | uniq -c | sort -rn | head
输出有几百行 get healthz、get readyz 来自 system:anonymous:HAProxy 健康检查、Cilium operator probe 都没带 token,K8s 默认开 anonymous auth,把这些请求当匿名用户放行。
不算严重漏洞但属于潜在隐患 —— anonymous 在某些版本能列到 /api discovery 信息。生产关掉:
- --anonymous-auth=false
关之前先把 HAProxy 健康检查改成带 token 的,否则 LB 把 apiserver 全标 unhealthy。教训:
- kubeadm 默认不开 audit + 默认开 anonymous auth,两条都是面试常问的「默认值不安全」题
- 改 static pod manifest 后不要 restart kubelet,kubelet 自动 reconcile;手动 restart 反而让 apiserver 冷启动多停 30 秒
- 生产 audit 进阶:fluent-bit 把
audit.log转发到 Elasticsearch / Splunk 做长期查询,本地落盘只 short retention(盘量 / IO 都顶不住几年) - 高 QPS 集群下 audit 写盘吃 IO 5-10%,audit 盘单独挂 SSD,不要跟 etcd 抢
B. etcd 内核 5 概念
etcd 是 K8s 唯一的真实状态存储,理解它就等于理解了 K8s 的 ACID 边界。这一段把 5 个核心概念逐个跑一遍真命令。
先把 etcdctl 别名定下,下面所有命令都用它:
ETCDCTL='kubectl -n kube-system exec etcd-k8s-cp-1 -- etcdctl
--cacert=/etc/kubernetes/pki/etcd/ca.crt
--cert=/etc/kubernetes/pki/etcd/server.crt
--key=/etc/kubernetes/pki/etcd/server.key
--endpoints=https://127.0.0.1:2379'
B.1 raft:谁是 leader,term 多少
$ETCDCTL endpoint status --cluster -w table
$ETCDCTL member list -w table
输出:
| ENDPOINT | ID | DB SIZE | IS LEADER | RAFT TERM | RAFT INDEX |
| cp-1:2379 | e4dd | 12 MB | false | 23 | 156162 |
| cp-2:2379 | c144 | 12 MB | false | 23 | 156162 |
| cp-3:2379 | 953b | 12 MB | true | 23 | 156162 |
记住 3 件事:
- quorum = N/2 + 1。3 节点 quorum=2,容忍 1 故障;5 节点 quorum=3,容忍 2;7 节点 quorum=4,容忍 3
- leader 接所有写,follower 同步。读默认也走 leader(linearizable read),可以加
--consistency=s退到 serializable read 直接从本地 follower 读 - term 在每次选举时 +1,是判断「这之间发生过 leader 变更」的最直接信号
raft 选主关键 timeout(etcd 默认值):
| 参数 | 默认 | 含义 |
|---|---|---|
heartbeat-interval | 100ms | leader 发心跳间隔 |
election-timeout | 1000ms | follower 多久收不到心跳就发起选举 |
这就是 Day 0 装 chrony 的原因:节点间时差 > 1s,follower 误判 leader 失联反复选主,集群整天不稳定。
B.2 MVCC:每次写都产生新 revision
etcd 不覆盖写,每个 key 的每次修改产生一个新 revision,revision 是 cluster-wide 单调递增 int64。
# 当前 revision
$ETCDCTL endpoint status -w json | jq '.[0].Status.header.revision'
# 126876
# 看某个 key 的所有历史 revision
$ETCDCTL get /registry/configmaps/default/foo --rev=126800 --keys-only
K8s 把 etcd revision 直接当 resourceVersion 用:
kubectl get pods --watch拿到 resourceVersion=126876- watch 中断 → 用这个 rv 重连 → etcd 从 rev 126877 开始重放变更
- 这就是 K8s informer / list-watch 一致性的内核来源
B.3 compact:删历史,逻辑空间释放
历史 revision 不清理,DB 会无限涨。kube-apiserver 默认每 5 分钟跑一次 auto-compact(--etcd-compaction-interval=5m),手动跑:
CURRENT_REV=$($ETCDCTL endpoint status -w json | jq -r '.[0].Status.header.revision')
$ETCDCTL compact $CURRENT_REV
# compacted revision 126876
效果:删 rev 126876 之前的所有历史版本,释放 ~90% logical 空间。但 du 看磁盘文件没变 —— BoltDB 只是内部标记为 free,不还盘。
B.4 defrag:物理重组,把盘真的还回去
# 看 logical vs physical
$ETCDCTL endpoint status --cluster -w table
# DB SIZE 12 MB ← 物理文件
# DB SIZE IN USE 2.1 MB ← compact 后逻辑占用
$ETCDCTL defrag --cluster
# Finished defragmenting etcd member[https://10.0.24.31:2379]
# Finished defragmenting etcd member[https://10.0.24.29:2379]
# Finished defragmenting etcd member[https://10.0.24.32:2379]
实测物理回收 ~30%(12MB → 8MB)。
defrag 是阻塞操作:单个 member 期间不响应读写,3 节点集群被串行处理,每节点 1-3s(大集群可能 1-5 分钟)。期间 raft 可能把 leader 切走。
| 操作 | 影响 | 何时跑 |
|---|---|---|
| compact | 删 logical history,不阻塞,不还盘 | apiserver 自动跑,无需关心 |
| defrag | 物理重组文件,阻塞写,还盘 | 手动 / cron 错峰,一次一个 member |
记住口诀:先 compact 再 defrag 才有用。defrag 不会去清还没 compact 掉的 history。
B.5 snapshot save:冷备
$ETCDCTL snapshot save /var/lib/etcd/snapshot-day2.db
$ETCDCTL snapshot status /var/lib/etcd/snapshot-day2.db -w table
# | HASH | REVISION | TOTAL KEYS | TOTAL SIZE |
# | ae0dc55f | 126876 | 521 | 2.1 MB |
snapshot save 在 line consistency 下导出整个 keyspace,在 leader 上跑最快(避免 cross-node 流量)。落地一份 + scp 到本地一份,restore 时用。
生产实践:cron 每天 + 异地存储(S3 / OSS),跟集群 etcd 数据完全解耦的物理位置。
真坑:etcd db 默认上限 2GB,超了拒写
$ETCDCTL endpoint status --cluster -w table
# 某个 member DB SIZE: 1.9 GB ← 接近上限
--quota-backend-bytes 默认 2GB。超了之后 etcd 进入 alarm: NOSPACE 状态,拒所有写,apiserver 全集群只读。kubectl 报:
etcdserver: mvcc: database space exceeded
排查:
$ETCDCTL alarm list
# memberID:e4dd alarm:NOSPACE
修复:
# 1. 强制 compact 到当前 rev
CUR=$($ETCDCTL endpoint status -w json | jq -r '.[0].Status.header.revision')
$ETCDCTL compact $CUR
# 2. defrag 物理回收
$ETCDCTL defrag --cluster
# 3. 解除 alarm
$ETCDCTL alarm disarm
教训:
- 大集群(>500 节点 / 万级 Pod)必须把
--quota-backend-bytes调到 8GB,同时把--etcd-compaction-interval调短(默认 5m,可以 2m) - K8s
Event资源是 etcd 大户(大量短命 event 频繁写),生产建议把 event 分流到独立 etcd 集群(--etcd-servers-overrides=/events#...) - 监控必加 etcd
db_size指标,到 80% 报警,不要等到 NOSPACE 才发现
C. chaos drill:杀 etcd 看集群怎么挂、怎么救
kubeadm 的 etcd 是 stacked 模式(etcd 跟 apiserver 同节点跑 static pod)。杀 etcd 的姿势就是把 manifest 文件移走:
# kubelet 10s 内检测到 manifest 不见,停 pod
mv /etc/kubernetes/manifests/etcd.yaml /root/etcd.bak
# 恢复
mv /root/etcd.bak /etc/kubernetes/manifests/etcd.yaml
这比 systemd 单元的 stop 更干净 —— kubelet 完全不知道你想干啥,按 manifest 状态 reconcile。
C.1 三阶段演练
| 阶段 | 动作 | etcd up | quorum | apiserver 行为 |
|---|---|---|---|---|
| 0 | baseline | 3/3 | OK | kubectl 正常 |
| 1 | kill cp-1 etcd | 2/3 | OK(2 ≥ 2) | kubectl 仍正常 |
| 2 | 再 kill cp-2 etcd | 1/3 | LOSS(1 < 2) | kubectl get nodes → etcdserver: request timed out |
| 3 | 恢复 cp-1 + cp-2 | 3/3 | OK | kubectl 全恢复,raft term +1 |
阶段 2 的关键观察:
kubectl get nodes
# Error: etcdserver: request timed out
# 但 worker 上业务 Pod 完全正常
kubectl exec -it test-pod -- ps # 这条 exec 会卡(要经过 apiserver)
ssh m4 'crictl ps' # 但直接到节点看,Pod 全在跑
这就是 K8s 控制面 vs 数据面解耦:
- 控制面挂(apiserver 拒写)= 不能调度新 Pod、不能 kubectl 操作、controller 不能 reconcile
- 数据面(业务 Pod 流量)完全不受影响,因为 kubelet 本地缓存了 Pod spec,CNI / kube-proxy 的 dataplane 也在节点本地
阶段 3 恢复后 raft term: 24(baseline 是 23),+1 是因为期间没 leader 触发了一次重选。
真坑:quorum loss 时千万别 force-new-cluster
阶段 2 quorum loss 时,新手第一反应是「赶紧把单节点改成 force-new-cluster 救起来」。别这么做。
正确处理顺序:
- 先尝试恢复 etcd member:重启 pod / 修网络 / 把 manifest 移回来
- 真的全恢复不了 → 才走 snapshot restore 灾难恢复流程
- 期间 apiserver 写 unavailable 但 worker 上的 Pod 继续跑,业务流量不受影响
如果直接 force-new-cluster 把单节点跑起来当新 cluster,等其他 member 网络恢复回来,会出现两个独立的 etcd cluster 互相不认 —— split-brain 数据分叉,比 quorum loss 还难救。
C.2 灾难恢复:3 个 etcd 全挂,从 snapshot restore
最坏情况:3 节点 etcd 数据目录全损坏(盘炸 / 误删 / ransomware)。流程:
# 1. 在某节点 etcdctl snapshot restore(生成新 data dir)
etcdctl snapshot restore /backup/snapshot-day2.db \
--name k8s-cp-1 \
--initial-cluster k8s-cp-1=https://10.0.24.31:2380 \
--initial-advertise-peer-urls https://10.0.24.31:2380 \
--data-dir /var/lib/etcd-restored
# 2. 改 etcd manifest 指向新 data dir + 单节点 initial-cluster
# /etc/kubernetes/manifests/etcd.yaml 里改两处:
# --data-dir=/var/lib/etcd-restored
# --initial-cluster=k8s-cp-1=https://10.0.24.31:2380
# 3. 把 hostPath volume 挂到 /var/lib/etcd-restored
# 4. kubelet reconcile 起单节点 etcd,验证 apiserver 能连
kubectl get nodes # 应该能拉到 snapshot 里的所有 node 信息
# 5. 加 cp-2 / cp-3 作为新 member
etcdctl member add k8s-cp-2 --peer-urls=https://10.0.24.29:2380
# 然后在 cp-2 上跑同样的 manifest 调整,带 --initial-cluster-state=existing
这个流程的关键是「单节点 restore → 验证 → 再加 member」,不要试图一次 restore 出 3 节点 cluster。
snapshot save/restore vs etcd member remove 的边界
| 场景 | 用什么 |
|---|---|
| 1 个 member 数据坏了,其他 2 个 OK | etcdctl member remove + 重新 add,从 leader 同步 |
| 1 个 member ghost / unstarted 状态 | etcdctl member remove 清掉 ghost |
| 2 个以上 member 数据坏了,quorum loss 且救不回来 | snapshot restore + 重建 cluster |
| 整个 etcd 数据被误删 / ransomware | snapshot restore,没其他路 |
记住边界:有 quorum 时永远不用 snapshot restore,损失 snapshot 到现在之间的所有变更。
Lesson
- kubeadm + static pod 让杀 etcd 极简:移走 manifest 就行,不像 systemd 要
stop。面试常问「K8s static pod 是什么」→ kubelet 直接管理、不通过 apiserver、manifest 在/etc/kubernetes/manifests/、kubelet inotify watch 这个目录 - 3 节点 etcd 是真 HA 起步:容忍 1 失败 + 留运维空间(滚动升级一台一台来)。5 节点容忍 2 但同步开销大、写延迟变高,不是默认推荐
- 数据面不依赖 etcd:worker 上的 Pod 在 control plane 全挂时继续跑,这是 K8s 设计的精髓和面试金句
- snapshot save 必须冷备:跟集群完全解耦的物理位置(S3 / OSS / 异地)。snapshot 跟 etcd 在同一台机器等于没备份
面试常见题
Q1:apiserver / etcd / kubelet 启动顺序是什么?谁依赖谁?
stacked 模式的启动链:
- kubelet 是 systemd 服务,先起。它扫
/etc/kubernetes/manifests/发现 4 个 static pod manifest - 拉起 etcd static pod(先于 apiserver,因为 apiserver 启动要连 etcd)
- 拉起 apiserver static pod,apiserver
--etcd-servers=https://127.0.0.1:2379,连本机 etcd - 拉起 controller-manager + scheduler(这两个连 apiserver,不直连 etcd)
故事性追问:为什么 health probe 失败重启 apiserver 不影响 etcd? —— static pod 互相独立,apiserver crash kubelet 只重起 apiserver,etcd pod 不动。这是 stacked 模式抗故障的关键设计。
Q2:etcd raft 选主 timeout 默认多少?为什么 K8s 节点必须装 chrony?
heartbeat-interval默认 100ms,leader 给 follower 发心跳的间隔election-timeout默认 1000ms(1s),follower 多久收不到心跳就发起选举
节点之间时差 > election-timeout,follower 会误判 leader 失联触发重新选主。生产见过节点时间漂移 5 秒导致 etcd 反复选主、apiserver 几小时不稳定。chrony 是 Day 0 装机第一天就装好不能省的原因。
故事性追问:为什么不用 systemd-timesyncd? —— 只支持 SNTP 无 drift 校准,VM 时间易漂移环境不稳。
Q3:MVCC、compact、defrag 是什么关系?
三层:
| 概念 | 干啥 | 阻塞? | 还盘? |
|---|---|---|---|
| MVCC | 每次写产生新 revision,不覆盖 | 否 | — |
| compact | 删某 rev 之前所有历史,释放 logical 空间 | 否 | 否(BoltDB 内部 free) |
| defrag | 物理重组 BoltDB 文件 | 是,单 member 1-5min | 是 |
口诀:先 compact 再 defrag 才有用。defrag --cluster 串行处理 3 个 member,每次 raft 可能切 leader。
故事性追问:etcd db 涨到 2GB 怎么救? —— alarm list 看到 NOSPACE → compact 到当前 rev → defrag → alarm disarm 解锁写。生产把 quota 调到 8GB + event 分流到独立 etcd。
Q4:3 个 etcd 全挂了怎么救?K8s 集群数据丢吗?
如果有 snapshot:
etcdctl snapshot restore到新 data dir,单节点--initial-cluster- 改 etcd manifest 指新 data dir,kubelet 拉起单节点 etcd
- apiserver 连上验证
kubectl get nodes拉得到 snapshot 里的状态 etcdctl member add把 cp-2 / cp-3 加回来,从 leader sync- 数据只丢 snapshot 到挂前之间的写,业务 Pod 完全不受影响(kubelet 缓存 spec 继续跑)
如果没 snapshot:完蛋,整个 K8s 状态丢失,要从头 kubeadm init。所以 snapshot save 是 K8s 运维第一公民,cron 每天 + 异地存储不可省。
故事性追问:quorum loss 时能不能 force-new-cluster 救起来? —— 千万不要。其他 member 网络恢复后会形成两个独立 cluster,split-brain 比 quorum loss 还难救。正确顺序是先尝试恢复 member,真的救不回来再走 snapshot restore。
Q5:K8s 控制面全挂,业务 Pod 会挂吗?
不会。这是 K8s 设计的精髓:
- 控制面(apiserver / controller-manager / scheduler / etcd)挂 → 不能调度新 Pod、不能 kubectl 操作、controller 不 reconcile
- 数据面(业务 Pod 流量)完全不受影响:
- kubelet 本地缓存 Pod spec,按缓存继续保活
- CNI dataplane(Cilium eBPF / Calico iptables)在节点本地,不查 apiserver
- kube-proxy 维护的 Service rule 已经下发到节点 iptables/ipvs,新的 Service 改不了但旧的还在工作
故事性追问:那 chaos 时 kubectl exec 进 Pod 看为什么会卡? —— 因为 exec 要经过 apiserver 中转,apiserver 挂了 exec 通道断了。直接 ssh 到节点 crictl exec 进容器看,业务进程全在跑。这是区分「Pod 真挂了」vs「只是看不见」的关键。
下一步
Day 2 结束后集群多了 3 件事:apiserver 写入了 audit 链路、etcd 5 个核心命令都跑过真数据、snapshot 在异地存了一份且演练过 restore 流程。下一步进 Day 3 看调度器:scheduler 怎么选节点、taint/toleration / nodeAffinity / topologySpreadConstraints 各自的边界,以及 descheduler 怎么处理「调度结果过时」的问题。