Kubernetes 原理¶
一句话
Kubernetes(K8s)的核心只有三句: ① 写下期望状态,控制器一直去接近它(reconcile);② 对象之间靠 Label 关联;③ K8s 是抽象层,背后干活的是云资源(LB、Block Volume、IAM)。 觉得乱的时候,回到这三句。
本页整理自 Phase 11 的讲义(第 1〜8 课)和 Phase 6 的实验。在 OCI 上怎么建、实测看到了什么,见 OKE 的设计和实践。
1. 先认识这些对象¶
| K8s 对象 | 一句话 | 类比(OCI / 之前做过的) |
|---|---|---|
| Pod | 最小单位:1 个或多个容器,共用一个 IP | — |
| Deployment / ReplicaSet | 维持"N 个同样的 Pod",负责换版本 | Phase 4 的"VM ×2 + LB 健康检查",但自动补 |
| Service | 给一直在变的 Pod 一个固定的 IP 和名字 | LB 的 Backend Set |
| Ingress | HTTP 的分流规则(域名、路径 → 哪个 Service) | LB 的 L7 路由规则 |
| ConfigMap / Secret | 放在镜像外面的配置 / 机密 | Function 的 config / Vault 的 Secret |
| PersistentVolumeClaim(PVC) | "要一块 50GB 的盘"的申请 | 由 CSI 建出 Block Volume |
| Namespace | 集群里的分区 | Compartment |
| Role / RoleBinding(RBAC) | 谁在哪个 Namespace 能做什么 | IAM Policy |
| ServiceAccount | Pod 自己的身份 | Resource Principal / Instance Principal |
| Job / CronJob | 跑完就结束的任务 / 定时任务 | Function / cron |
2. 原理¶
2.1 容器:被"罩住"的进程¶
容器不是小 VM,它就是宿主机上的一个进程,只是被罩住了,看不到外面。
| VM | 容器 | |
|---|---|---|
| 隔离的是什么 | 整台机器(各自有内核) | 进程(所有容器共用宿主机的内核) |
| 启动 | 几十秒〜几分钟 | 几十毫秒〜几秒 |
| 大小 | 几 GB | 几 MB〜几百 MB |
| 隔离强度 | 强 | 比 VM 弱(内核有漏洞会影响所有容器) |
靠 Linux 的三个功能罩住:namespace 决定"能看到什么"(自己的 PID、IP、文件系统),cgroups 决定"最多能用多少"(超过内存上限 → OOMKilled),分层文件系统让镜像的只读层共享、容器只在最上面加一层可写层。
和镜像有关的三件事:
- tag 会变,digest 不会:
nginx:1.27的 tag 可以被重新指向别的镜像;要绝对固定用nginx@sha256:... - CPU 架构:OKE 的 node 是 A1(arm64)。官方镜像大多是"多架构"的,pull 时自动选;只有 amd64 的镜像在 A1 上跑不起来
- 数据和配置放外面:容器删掉可写层就没了;同一个镜像在开发 / 生产用不同的配置
这背后的原则叫 cattle, not pets(把服务器当牲口,不当宠物):坏了就换,不去修。不在运行中的容器里改东西,要改就 build 新镜像替换。
2.2 声明式 + 持续修正(reconcile)¶
你告诉 K8s"想要什么状态",K8s 一直盯着,一发现实际不一样就自动修正。
| Terraform | K8s | |
|---|---|---|
| 写法 | 声明式(.tf) |
声明式(YAML manifest) |
| 什么时候修正 drift | 执行 apply 的时候(一次) |
一直在修,不用人触发 |
| 期望状态存在哪 | state 文件 | etcd |
所有对象都是同样的结构:spec 是你写的期望,status 是 K8s 写的实际,控制器让 status 追上 spec。
2.3 架构¶
flowchart TB
U["kubectl"] -->|"HTTPS :6443"| API
subgraph CP["Control Plane(OKE 上由 Oracle 管)"]
API["kube-apiserver<br/>唯一的入口"]
ETCD[("etcd<br/>全部状态")]
CTL["scheduler<br/>controller-manager<br/>cloud-controller-manager"]
CTL <-->|"盯着变化、写回结果"| API
API --- ETCD
end
subgraph N1["Worker Node(每台都有)"]
K1["kubelet"] --> R1["容器运行时<br/>CRI-O"]
R1 --> P1["Pod"] & P2["Pod"]
KP1["kube-proxy<br/>flannel"]
end
API <-->|"分配给我的 Pod?"| K1
| 组件 | 在哪 | 做什么 |
|---|---|---|
| kube-apiserver | Control Plane | 所有操作的唯一入口:认证 → 权限检查 → 存进 etcd。组件之间不直接对话,都通过它 |
| etcd | Control Plane | 集群全部状态的键值数据库 |
| scheduler | Control Plane | 新 Pod 放哪个 node。只做决定,不负责启动 |
| controller-manager | Control Plane | Deployment、ReplicaSet、Node、Job 等几十个控制器,各跑各的 reconcile loop |
| cloud-controller-manager(CCM) | Control Plane | 和云打交道:Service type=LoadBalancer → 建 OCI LB |
| kubelet | 每个 node | 问 apiserver"分配给我的 Pod",让运行时启动容器,把状态报回去 |
| kube-proxy | 每个 node | 把"访问 Service → 实际 Pod"的转发规则写进 node |
| CNI 插件 | 每个 node | 给 Pod 分 IP,打通 node 之间的 Pod 网络(本仓库:flannel) |
2.4 组件链:kubectl apply 之后发生了什么¶
以"2 个副本的 nginx Deployment"为例:
| 步骤 | 谁 | 做了什么 |
|---|---|---|
| ① | kubectl → apiserver | 把期望写进 etcd(认证用 OCI IAM 的 token → RBAC 检查)。kubectl 到这里就返回了 |
| ② | Deployment 控制器 | 建一个 ReplicaSet |
| ③ | ReplicaSet 控制器 | 0 个、要 2 个 → 建 2 个 Pod 对象(这时还没有 node) |
| ④ | scheduler | 给没有 node 的 Pod 挑 node,把 node 名写进去 |
| ⑤ | 那台 node 的 kubelet | 发现分配给自己的 Pod → 让 CRI-O 拉镜像、启动容器;让 CNI 分配 IP |
| ⑥ | kubelet | 报告"Running、IP 是 10.244.x.x" |
| ⑦ | 其他控制器 / kube-proxy | Pod 就绪 → 更新 Service 的转发目标 |
最容易搞错:容器是谁启动的
controller 建的只是 Pod 对象(etcd 里的一条记录)。真正启动容器(进程)的是那台 node 上的 kubelet。 controller 建对象 → scheduler 选 node → kubelet 干活,三者互不调用,都是"盯着 apiserver、发现差距、改数据"。
Phase 11 里亲眼看到了这条链:node 还没加入时,coredns 的 Pod 对象已经存在(controller 做完了),但状态是 Pending、NODE 是 <none>,Events 是 no nodes available to schedule pods(scheduler 卡住),也没有 kubelet 来启动它。node 加入后它自动变成 Running。
控制平面停了 ≠ 服务停了:
| scheduler 停了 5 分钟 | 结果 |
|---|---|
| 已经在跑的 Pod | 不受影响(scheduler 只在"放哪"那一刻有用) |
| 运行中的容器崩溃 | kubelet 在原地重启 |
| 新 Pod / 自愈要补的 Pod | 停在 Pending,scheduler 恢复后自动调度 |
用户的请求走 LB → node → Pod(数据平面),不经过控制平面。
3. 核心对象:Pod、Label、Deployment、Service¶
Pod¶
- 一个 Pod 里的容器共用 IP 和 Volume。绝大多数是一个 Pod 一个容器;放日志转发等辅助容器时叫 sidecar
- Pod 也是用完即扔的:死了换新的,名字和 IP 都会变(Phase 6:
10.244.1.4 → .5)→ 不要直接用 Pod IP,也不要直接建单个 Pod
| 状态 | 意思 | 常见原因 |
|---|---|---|
Pending |
还没分到 node | 资源不够、没有 node |
ImagePullBackOff |
拉镜像失败 | 名字写错、要认证、没有对应架构 |
CrashLoopBackOff |
一启动就崩溃 | 程序报错、配置错、exec format error(架构不对) |
OOMKilled |
超过内存上限被杀 | limits.memory 太小 |
Label 和 Selector:K8s 的粘合剂¶
对象之间不靠名字,靠标签关联:Deployment 管理 app=nginx 的 Pod,Service 把流量转给 app=nginx 的 Pod。
和 OCI 的 freeform_tags 不同:OCI 的标签只是备注,K8s 的标签直接决定"谁管谁、流量给谁"。手动给别的 Pod 贴上 app=nginx,ReplicaSet 会认为多了一个而删掉一个,Service 也会把流量转给它。
Deployment → ReplicaSet → Pod¶
# 06-oke/k8s/nginx.yaml(摘录)
spec:
replicas: 2 # 期望:保持 2 个
selector:
matchLabels:
app: nginx # ① 我管"带 app=nginx 的 Pod"
template:
metadata:
labels:
app: nginx # ② 新 Pod 贴这个标签(必须和 ① 一致,否则 apply 时被拒绝)
spec:
containers:
- name: nginx
image: nginx:1.27 # 改这里 → 滚动更新
为什么分两层?ReplicaSet 管"数量",Deployment 管"版本"。改了 image,Deployment 建一个新 ReplicaSet,新的一点点加、旧的一点点减(滚动更新);旧 ReplicaSet 以副本数 0 保留,kubectl rollout undo 马上能退回。
Pod 名 nginx-598968dd58-xxxxx = Deployment 名 - 模板的 hash - 随机字符串。
Service:固定入口¶
| type | 从哪里访问 | 实际做了什么 |
|---|---|---|
ClusterIP(默认) |
只能集群内部 | 分配虚拟 IP(本仓库从 10.96.0.0/16) |
NodePort |
外部用 node IP:30000〜32767 |
ClusterIP + 每个 node 开同一个端口 |
LoadBalancer |
外部通过云 LB | NodePort + CCM 建 OCI LB |
大多数 Service 是 ClusterIP(Web → API 用 http://api 这样的名字访问,CoreDNS 解析),对外入口通常只有 1 个。
Service 能访问但没响应时,先看 kubectl get endpointslices -l kubernetes.io/service-name=nginx:转发目标为空 = selector 没匹配到 Pod。
其他工作负载¶
| 类型 | 用途 | 和 Deployment 的区别 |
|---|---|---|
| StatefulSet | 有状态(DB、消息队列) | Pod 名固定(db-0)、每个 Pod 有专用存储、按顺序启停 |
| DaemonSet | 每个 node 跑 1 个 | 不指定副本数(kube-proxy、flannel、csi-oci-node) |
| Job / CronJob | 跑完就结束 / 定时 | 成功后不重启 |
实际项目里 DB 一般不放进 K8s,用托管 DB(ADB 等),K8s 里只跑无状态应用。
4. 网络¶
集群里的 4 种地址(06-oke 的配置)¶
| 地址 | 范围 | 谁分配 | 在 VCN 里真实存在吗 |
|---|---|---|---|
| node 的 IP | 10.6.10.0/24 |
OCI(VCN) | ✅ |
| Pod 的 IP | 10.244.0.0/16 |
flannel(CNI) | ❌(overlay,VCN 不知道) |
| Service 的 IP(ClusterIP) | 10.96.0.0/16 |
K8s | ❌ 哪里都不存在,只是 node 上的转发规则 |
| LB 的公网 IP | — | OCI(CCM 去申请) | ✅ |
Pod 网络:K8s 只规定"每个 Pod 有自己的 IP、Pod 之间不经过 NAT 直接通",怎么实现交给 CNI 插件。
| flannel(本仓库) | VCN-Native(OCI 专有) | |
|---|---|---|
| Pod 的 IP | overlay(10.244.x.x) |
VCN subnet 的 IP |
| 跨 node | 外面再套一层 node → node 的包(VXLAN,UDP 8472) | 不封包 |
| VCN 能看到 Pod 吗 | 不能 | 能(按 Pod 设 NSG、LB 直接转给 Pod) |
| 消耗 VCN 的 IP | 不消耗 | 有多少 Pod 就消耗多少 |
Service 的真面目:ClusterIP 没有任何网卡持有。kube-proxy 在每个 node 上写 iptables 规则:"发往 ClusterIP:80 的包 → 随机选一个 Pod,改掉目的地址(DNAT)"。所以 ClusterIP ping 不通是正常的(规则只匹配 TCP / UDP),确认 Service 要用 curl。
从浏览器到 Pod:两次独立的选择¶
flowchart TB
PC["浏览器"] -->|":80"| LB["OCI LB"]
LB -->|"① LB 选 node<br/>只认识 node 的 IP"| NA["node A<br/>:NodePort"]
LB -.-> NB["node B<br/>:NodePort"]
NA -->|"② node 选 Pod<br/>候选是全集群的 Pod"| PA["node A 上的 Pod"]
NA -->|"选中别的 node 的 Pod<br/>flannel 封包"| PB["node B 上的 Pod"]
- LB 选 node:LB 不认识 Pod 的 IP(overlay),只能把 node 的 NodePort 登记为后端
- node 选 Pod:kube-proxy 的规则包含整个集群的 Pod,所以某台 node 上一个 Pod 都没有,请求也能成功(默认
externalTrafficPolicy: Cluster)。代价是多跳一次,Pod 看到的来源 IP 是 node 的 - LB 的健康检查打的是 kube-proxy 的
:10256:只有 kube-proxy 知道"本 node 能不能处理这个 Service"(externalTrafficPolicy: Local时就靠它把没有 Pod 的 node 排除掉)
L4 和 L7:LB 和 Ingress¶
| 能看到什么 | 能按什么分流 | |
|---|---|---|
| L4(传输层) | IP、端口 | "80 端口的流量交给这组服务器" |
| L7(应用层) | HTTP 的域名、路径、header | "a.example.com 给 A、/api 给 API" |
Service 本身是 L4 的概念(YAML 里只能写端口)→ K8s 另外设计了 Ingress 来表达 L7。OKE 用 Service type=LoadBalancer 建的 OCI LB 产品本身支持 L7,但 Listener 是 TCP,当 L4 在用(实测确认)。
LB 是"大门",Ingress 是"进门以后的分流规则",两者一起用:
| LB(OCI LB) | Ingress | |
|---|---|---|
| 是什么 | 云的产品,在集群外 | K8s 的对象 = 一份路由规则 |
| 谁干活 | OCI | Ingress Controller(另外安装的 Pod) |
| 在哪配置 | OCI(Terraform、Console) | K8s 的 YAML(和应用放一起) |
- 实际项目里 HTTP(S) 应用共用 1 个入口,LB 的数量按"入口的种类"决定(公网 / 内网、非 HTTP),不是每个应用一个
- 后继标准是 Gateway API。用 OCI 的哪种 Ingress 实现还待确认
为什么 LB 让 K8s 建,而不是 Terraform:后端(所有 node 的 NodePort)一直在变,CCM 盯着变化自动更新;应用写一个 Service 就能公开;同一份 Service 在别的云上也能用。代价:LB 不在 Terraform 的 state 里,删除时要先 kubectl delete(见 OKE 页)。一般的分工是:LB 让 K8s 建,预留 IP、DNS、证书这类"固定的东西"由 Terraform 管。
flannel 不实现 NetworkPolicy
K8s 默认所有 Pod 之间都能互相访问(跨 Namespace 也一样)。NetworkPolicy 可以限制,但 flannel 不实现它:写了会被接受,但不生效,也不报错。要用就装 Calico 等,或者用 VCN-Native + NSG。
5. 配置和存储¶
ConfigMap 和 Secret¶
把配置放到镜像外面 → 同一个镜像,开发和生产用不同的 ConfigMap。可以作为环境变量或文件注入。
改了 ConfigMap,Pod 不一定跟着变
环境变量:不会更新(进程启动时读一次)。文件:约 1 分钟内更新,但应用要自己重新读。
→ 改完一般 kubectl rollout restart deployment <名字>。
Secret 的用法和 ConfigMap 几乎一样,区别是能用 RBAC 单独限制谁能读、etcd 里可以加密、挂成文件时放在内存里。
Secret 默认只是 base64,不是加密
有读取权限就能 base64 -d 看到明文。写着值的 Secret YAML 不要提交到 Git(和"机密不进 Terraform state"同样的原则)。
| K8s Secret | OCI Vault Secret | |
|---|---|---|
| 存在哪 | etcd | Vault |
| 加密 | 默认没有(OKE 可以用自己的 Vault 密钥加密 etcd) | 一定加密 |
| 版本管理 / 轮换 | 没有 | 有 |
| 权限 | RBAC | IAM Policy |
实际项目常见的做法:机密在 Vault 里管理,运行时同步到 K8s(External Secrets Operator 等)。
存储:PV / PVC / StorageClass¶
Pod 会换 node → 数据要放在"跟着 Pod 走"的存储里。
| 对象 | 意思 | 和 Terraform 对比 |
|---|---|---|
| PVC | 应用的申请:"要 50GB,挂到 1 个 node 读写" | .tf 里的 resource |
| StorageClass | 怎么建。OKE 默认 oci-bv(CSI → OCI Block Volume) |
provider |
| PV | 实际建出来的存储(自动创建) | apply 后的实际资源 |
在 OKE 上要注意:
- 最小 50GB:申请
1Gi也建 50GB;Always Free 的 Block Volume 合计 200GB 里,2 台 node 的 boot volume 已经用了 100GB → PVC 最多 2 个 - 不在 Terraform 的 state 里:不删 PVC 就
terraform destroy,Block Volume 会留下来占额度 reclaimPolicy:Delete= 删 PVC 时盘也删;Retain= 保留(忘了删就一直占额度)。oci-bv默认是哪个待确认(讲义按文档写的是Delete)
6. 可靠性¶
probe / resources 写在哪里
写在 manifest(Deployment 的 containers[])里,不是镜像里。镜像管"跑什么",manifest 管"怎么跑"。Dockerfile 的 HEALTHCHECK K8s 会忽略。
三种 probe¶
| probe | 问什么 | 失败了 |
|---|---|---|
| startupProbe | 启动完了吗? | 完成之前不执行另外两种(防止启动慢的应用被误判) |
| livenessProbe | 还活着吗?(要不要重启) | kubelet 在同一个 Pod 里重启容器(RESTARTS 增加) |
| readinessProbe | 现在能接请求吗?(要不要给流量) | 从 Service 的转发目标里摘掉(不重启),恢复后加回来 |
startupProbe:
httpGet: {path: /, port: 80}
periodSeconds: 10
failureThreshold: 12 # 最多等 10 秒 × 12 = 120 秒
readinessProbe:
httpGet: {path: /, port: 80}
periodSeconds: 5
livenessProbe:
httpGet: {path: /, port: 80}
periodSeconds: 10
failureThreshold: 3 # 连续 3 次失败才重启
两个常见错误
- liveness 检查外部依赖(例:能不能连 DB):DB 停一下 → 所有 Pod 一起反复重启,重启了也连不上。liveness 只检查自己;依赖放在 readiness
- 启动要 60 秒的应用只写 liveness:30 秒就被判定"死了" → 重启 → 一直起不来。把开始检查的时间往后推也不够稳 → 用 startupProbe
resources:requests 和 limits¶
- requests = 预约,给 scheduler 看的:"node 可分配的量 − 已有 Pod 的 requests 合计"够不够。看的不是实际用量;所有 node 都约满了 →
Pending - limits = 上限,由 cgroups 强制:超过 CPU → 限速(不会被杀);超过内存 →
OOMKilled - 常见做法:内存设上限,CPU 不设(本仓库的 nginx.yaml 就是这样:
requestscpu 50m / memory 64Mi,limitsmemory 128Mi)
滚动更新:靠 readiness 保证不中断¶
maxSurge(最多多出几个)和 maxUnavailable(最多允许几个不可用)决定节奏,默认都是 25%。
滚动更新等新 Pod 的 readiness 成功才继续。新版本有 bug 起不来 → readiness 一直失败 → 更新卡住不往下走,旧 Pod 继续服务 → kubectl rollout undo。不写 readiness 的话,坏版本也会被当成正常的,旧 Pod 被一个个删掉。
调度:要的是「分散」,不是「指定 node」¶
scheduler 只在放置的那一刻决定,事后不会自动调整。Phase 6 的实验:drain 一台 → Pod 全到另一台 → uncordon 后再缩回 2 个 → 2 个都在同一台(没有冗余)。
# 把 app=web 的 Pod 按 node 分散(各 node 数量差不超过 1,做不到就不调度)
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname # 分散的单位:node(也可以是 FD 等)
whenUnsatisfiable: DoNotSchedule # 做不到时:不调度(ScheduleAnyway = 尽量)
labelSelector:
matchLabels: {app: web}
| 想做的事 | 用什么 |
|---|---|
| 把 Pod 分散到不同 node / FD | topologySpreadConstraints |
| 只放到某种 node(例:arm64) | nodeSelector / nodeAffinity |
| node 拒绝一般的 Pod(例:GPU node) | node 上 taint,Pod 写 toleration |
| 维护(drain)时保底 | PodDisruptionBudget(PDB) |
不要用 nodeSelector 指定某一台 node:要么全挤在那一台,要么那台被替换(升级时 node 名也变)后 Pod 无处可去。
PDB(Pod Disruption Budget,和 DB 没关系)= "维护时至少保持几个可用"。kubectl drain 时先赶一个、等新 Pod 就绪再赶下一个。但它只管 drain、升级这类自愿的中断;node 突然宕机靠的是分散配置。
| 可靠性的部件 | 防的是什么 |
|---|---|
| probe | 应用卡死、没准备好就接请求 |
| resources | 资源被抢光、node 被塞满 |
| 滚动更新 + readiness | 换版本时中断、坏版本扩散 |
| 分散 + PDB | 一台挂了全挂、维护时全停 |
7. 权限和运维¶
Namespace¶
集群里的分区,和 OCI 的 Compartment 很像:分组、按它写权限、设配额。
- 不是网络隔离:不同 Namespace 的 Pod 默认能互相访问
- 删掉 Namespace = 里面全部删掉(包括 PVC 背后的 Block Volume)
RBAC:谁能做什么¶
OCI 的 Policy 是一句话,K8s 拆成两个对象:Role(能做什么)+ RoleBinding(给谁)。拆开是为了同一个 Role 能绑给很多人。
| 范围 | |
|---|---|
| Role + RoleBinding | 一个 Namespace 内 |
| ClusterRole + ClusterRoleBinding | 整个集群 |
自带的 ClusterRole:view(只读,不含 Secret)、edit、admin、cluster-admin。
复用自带的 view:RoleBinding 也能绑 ClusterRole
想让 developers 组只在 dev 里只读,不用自己写 Role:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding # RoleBinding → 只在这个 Namespace 有效
metadata:
name: dev-view
namespace: dev
subjects:
- kind: Group
name: developers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole # 绑的是集群级的 ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
权限的范围由 Binding 决定:ClusterRole 只是"能做什么"的定义,用 RoleBinding 绑就只在这个 Namespace 生效。
OKE 的权限是两层:① OCI IAM:能不能拿到 kubeconfig / token;② K8s RBAC:在集群里能做什么。tenancy 管理员是否自动是 cluster-admin,待确认。
ServiceAccount:Pod 自己的身份¶
Pod 里的程序调用 K8s API 时用的身份。每个 Namespace 自动有 default(几乎没有权限)。
- 一般的 Web 应用不调用 K8s API →
automountServiceAccountToken: false(被攻破时攻击者拿不到 token) - 要调用 API 的程序:单独建 ServiceAccount,只给最小权限
- Pod 要调用 OCI API:用 node 的 Instance Principal 的话,这个 node 上所有 Pod 权限都一样(范围太大);按 ServiceAccount 写 IAM Policy 的 OKE Workload Identity 只有 Enhanced cluster(收费)能用
Job、CronJob 和 Helm¶
- Job:进程正常结束 = 完成,不重启。重试或 node 故障时可能执行两次 → 要设计成幂等。CronJob 不写
timeZone就是 UTC - Helm = K8s 版的
dnf:Chart(模板 + 默认参数)≒ Terraform 的 module,values ≒terraform.tfvars,helm template≒ plan - chart 里的 LoadBalancer / PVC 也会建 OCI 资源 → 先
helm uninstall,再terraform destroy
8. 设计决策一览¶
| 决策 | 选了 | 没选 | 理由 |
|---|---|---|---|
| 运行 Pod 的方式 | Deployment | 直接建 Pod | 删了没人补;换版本要靠 ReplicaSet |
| 镜像 | 固定 tag(nginx:1.27),多架构 |
latest |
tag 不固定就不知道跑的是哪个版本;A1 要 arm64。digest 是之后的练习 |
| resources | 内存设 limits,CPU 不设 | 都不写 / CPU 也设上限 | 防止吃光 node 内存;CPU 设上限反而被不必要地限速 |
| 健康检查 | liveness 只查自己,依赖放 readiness | liveness 查 DB | 依赖一停所有 Pod 一起重启 |
| 冗余 | topologySpreadConstraints + PDB |
nodeSelector 指定 node | node 是牲口,会被替换 |
| 权限 | RoleBinding + 自带 view / edit |
都给 cluster-admin |
最小权限;不用重复造 Role |
| 机密 | Vault 管理,运行时同步 | Secret YAML 进 Git | Secret 只是 base64 |
| DB | 托管 DB(K8s 外) | StatefulSet 自己跑 | 备份、高可用交给云 |
| 对外入口 | 1 个 LB(+ Ingress) | 每个应用 1 个 LB | 费用、管理;Always Free 只有 1 个 LB |
本仓库的 06-oke/k8s/nginx.yaml 是最小示例:probe、PDB、显式分散、独立 Namespace、镜像 digest 都还没写,是后续的练习。
9. 验证(实验中观察到的)¶
| 想确认的 | 做了什么 | 看到的 |
|---|---|---|
| 自愈(reconcile) | kubectl delete pod |
马上补一个新 Pod,名字和 IP 都变了 |
| 组件链 | node 加入前看 coredns | Pod 对象存在、Pending、NODE <none>;node 加入后自动 Running |
| 组件链 | kubectl get events(Pod 自己的用 kubectl events --for pod/<name>) |
Deployment controller(ScalingReplicaSet)→ ReplicaSet controller(SuccessfulCreate)→ kubelet(Pulling / Started);scheduler 那一环没有单独记下来 |
| 调度只在放置时发生 | drain → uncordon → 缩回 2 个 | Pod 不迁回,2 个都在同一台 |
10. 坑¶
| 现象 | 原因 | 对策 |
|---|---|---|
exec format error / no matching manifest for linux/arm64 |
镜像没有 arm64 版(A1 是 arm64) | 用多架构镜像,或 docker buildx --platform |
| 运行中的容器里修好的 bug 又回来了 | Pod 重建时可写层没了 | 改 Dockerfile,build 新镜像 |
改了 Deployment 的 selector 报 field is immutable |
selector 建好后不能改 | delete 后重建 |
| 改了 ConfigMap,应用没变 | 环境变量只在启动时读 | kubectl rollout restart |
启动慢的应用一直 CrashLoopBackOff |
只写了 liveness,启动中就被重启 | 加 startupProbe |
| 写了 NetworkPolicy 但没效果 | flannel 不实现 | Calico 等 / VCN-Native + NSG |
| uncordon 后 Pod 还是挤在一台 | scheduler 不会事后调整 | kubectl rollout restart;平时写 topologySpreadConstraints |
| Service 访问没响应 | selector 没匹配到 Pod | 看 endpointslices |
kubectl delete pod 10.6.10.x → NotFound |
那是 node 名(OKE 的 node 名 = 私有 IP) | Pod 名是 nginx-xxxx-xxxxx 格式 |
11. 自测问题¶
1. Deployment 的控制器建了 Pod,那 Pod 里的容器是谁启动的?
那台 node 上的 kubelet(让 CRI-O 去拉镜像、启动)。控制器只建 Pod 对象(etcd 里的记录),scheduler 只决定放哪个 node。
2. 按顺序说出 kubectl apply 一个 Deployment 之后,各组件做了什么
apiserver 存进 etcd → Deployment 控制器建 ReplicaSet → ReplicaSet 控制器建 Pod 对象 → scheduler 给 Pod 选 node → kubelet 启动容器并报告状态 → Service 的转发目标更新。各组件互不调用,都通过 apiserver。
3. 集群里还没有任何 node 时,Pod 是什么状态?卡在哪个组件?
Pending、NODE 是 <none>。Pod 对象已经由控制器建好了,卡在 scheduler(没有 node 可选),所以也没有 kubelet 来启动它。
4. 一个应用启动要 60 秒,只写了 liveness(10 秒一次、3 次失败重启),会怎样?怎么改?
30 秒就被判定为"死了"并重启,永远起不来(CrashLoopBackOff)。加 startupProbe,让 periodSeconds × failureThreshold 比启动时间长;它成功之前 liveness 不执行。
5. 滚动更新时,K8s 用哪个 probe 判断新 Pod 准备好了?liveness 失败会发生什么?
readinessProbe。liveness 失败是 kubelet 在同一个 Pod 里重启容器,不是新建 Pod,也不影响滚动更新的判断。
6. liveness 里检查「能不能连上 DB」有什么问题?
DB 一停,所有 Pod 的 liveness 都失败 → 全部反复重启,重启了也连不上,问题更大。liveness 只检查自己;依赖的检查放 readiness(连不上就先不接请求)。
7. 2 个副本的 Pod 都跑在同一台 node 上。写出让它们分到不同 node 的配置要点
topologySpreadConstraints:topologyKey: kubernetes.io/hostname(以 node 为单位)、maxSkew: 1、whenUnsatisfiable: DoNotSchedule,labelSelector 选中这组 Pod。已经偏了的话 kubectl rollout restart 重新分配。
8. 为什么不用 nodeSelector 把两个 Pod 分别指定到两台 node?
node 是会被替换的(升级时换新 node,名字也变),指定了特定 node,那台没了 Pod 就无处可去。要写的是"分散"的规则,不是"放哪台"。
9. 想让 developers 组在 dev Namespace 里只读,最省事的写法是?
在 dev 里建一个 RoleBinding,roleRef 指向自带的 ClusterRole view。不用自己写 Role;因为是 RoleBinding,权限只在 dev 里有效。
10. 自带的 view 能看 Secret 吗?Secret 是加密存的吗?
view 不含 Secret。Secret 默认只是 base64,有读取权限就能看到明文;etcd 加密要另外配置(OKE 可以用自己的 Vault 密钥)。
11. Pod 超过了 CPU 的 limits 和超过内存的 limits,结果有什么不同?
CPU:被限速,变慢但不会被杀。内存:OOMKilled,容器被重启。
12. PDB 能防止 node 突然宕机导致服务中断吗?
不能。PDB 只管 drain、node pool 升级这类自愿的中断。突然宕机要靠把 Pod 分散到不同 node / FD。
13. ClusterIP ping 不通,说明 Service 坏了吗?
不说明。ClusterIP 只是 kube-proxy 写在 node 上的 iptables 规则,只匹配 TCP / UDP 端口,不回应 ICMP。要用 curl 确认。