2026-01-20 22:39
kubernetes在当初学docker的时候就接触过,那时的认知是这东西就是把docker容器分散在各个机器上运行的工具:集群版docker。后来发现它的行为如此,但是设计理念或者说要解决的问题完全不是如此。
kubernetes算是一个集群操作系统。一个典型的操作系统是硬件设备、资源接口的抽象者和管理者,提供了一个完整的心智模型简化应用程序的开发。用它比喻k8s是因为k8s也类似,将集群中的一切资源抽象为了RD——Resource Definition,这些定义和传统操作系统中的进程、可执行文件、应用配置、网络接口、内存、存储等都可以对应的上,从而借助它,我们可以在集群的角度导入可执行文件、启动前者得到若干进程、管理和限制它们使用的网络、存储、算力等资源,同时也可以借助几种模式保证进程始终运行、崩溃后自动重启等等。
除此之外,k8s允许自定义资源类型:Custom Resource Definition,你可以自定义一些数据结构,推送到k8s集群中,就可以操作和删除这种自定义的资源类型了。至于用这种资源类型干什么,你完全可以自由发挥:k8s提供了一些能力,比如说事件驱动的Resource队列和对应的消费者、对某个节点上容器的管理功能等;比如说你可以创建一个自定义资源类型叫做日志,然后当容器运行出错时就调用k8s api创建一个对应的Resource,同时再启动一个进程或者容器,在其中调用k8s api消耗日志,来给出集群服务大盘等信息。
而这种”定义资源类型 + 监听资源变化并执行对应操作”的模式,就是 Operator 模式。Operator = CRD + Controller,Controller 监听 CRD 资源的增删改,不断将实际状态”调和”(reconcile)到用户声明的期望状态。典型例子如 etcd-operator:用户提交一个 EtcdCluster CR,operator 自动创建 Pod、Service,处理备份恢复、滚动升级等运维操作,将运维经验写成代码交给 k8s 执行。
所以,它提供的这组工具实际上类似于:单个节点的控制能力+分布式事件总线的感觉;二者组合就成为了一个云操作系统。
那它解决的问题,或者说能解决的问题就是:运行时实例的管理、应用程序升级版本、版本过渡、错误恢复、容器-资源的调度分配问题等。
说到这里,就是为什么它适合解放运维?在一般操作系统里,它自身不但扮演资源的配发者,更多的是,扮演资源的控制者:对于声明在进程树中,要求作为服务运行的进程,一般都会尽力保证其运行;同时升级软件也是使用工具安装一下的事,oom kill也是自动触发的。而运维需要启动服务并保证服务长久正常运转,这有点类似os的能力。
k8s最强的,就是实现了这个可靠的分布式事件-资源总线,它的informer实现了良好的性能-可靠性。其中的kubelet也提供了良好的节点、容器控制能力。
因此,它适合自动化部署、回滚:推送新镜像时自动触发蓝绿发版、崩溃时自动在本节点/换节点创建新容器实现高可用等。此外还能编写规则+CRD实现各种自动化流水线,比如自动换掉故障服务器等。
CRD (资源定义) → 用户提交 CR 实例
↓
Controller (控制循环)
├── Informer/Watch ── 监听 CR 及关联资源变化
├── Reconciler ── 核心调和逻辑
└── 操作 API ── 创建/更新/删除 k8s 资源 (Pod, Service, PVC...)
核心是一个事件驱动的调和循环。Reconciler 接口只需一个方法:
func (r *Reconciler) Reconcile(ctx, req) (Result, error) {
// 1. 获取用户声明的期望状态
cr := r.GetCR(req.NamespacedName)
// 2. 观测当前实际状态
pods := r.ListPods(ownerRef(cr))
// 3. 比较 → 执行动作:多了删、少了建、版本不对就更新
if len(pods) < cr.Spec.Replicas {
r.CreatePod(cr.Spec.Template)
}
if pods[0].Image != cr.Spec.Image {
r.RollingUpdate(cr.Spec.Image)
}
// 4. 更新状态
cr.Status.ReadyReplicas = readyCount
r.UpdateStatus(cr)
return ctrl.Result{RequeueAfter: 30s}, nil
}常用框架:Kubebuilder / Operator SDK,底层依赖
sigs.k8s.io/controller-runtime。Operator 本身也是一个
Deployment 跑在集群内。
① 画出运维流程图
以 MySQL 为例,整理人工运维的每个操作:
创建实例 → 创建 Pod → 创建 Service → 初始化主从 → 接入监控
版本升级 → 逐节点替换 → 检查同步延迟 → 切主 → 完成
节点故障 → 检测到挂 → 重新调度 → 重建从库 → 重建主库
备份恢复 → 定时备份 → 存储到 S3 → 按需还原
② 抽象为 CRD Spec / Status
Spec = 用户声明什么,Status = 用户观测什么:
spec:
replicas: 3
version: "8.0.32"
storage: 100Gi
backup:
schedule: "0 3 * * *"
status:
readyReplicas: 3
phase: Running
master: mysql-0③ 转为条件分支
每个人工决策变成一个 if 判断,构成 Reconciler
的调和逻辑:
if 不存在 StatefulSet → 创建
if 版本 ≠ spec.version → 滚动升级
if readyReplicas < replicas → 等待/重建
if 存在故障节点 → 驱逐 Pod
if 到了备份时间 → 执行备份 Job
④ 终态收敛 + 重试
每次 Reconcile 只推进一小步,出错就 return error
触发指数退避重试。RequeueAfter
控制定期轮询,确保无事件时也能持续调和。
把 SRE 的 runbook 翻译成代码,每个 if-else 对应一个运维决策节点,Operator 框架提供标准封装模版。
k8s 集群存在三层网络:
| 网络层 | 范围 | 谁分配 | 说明 |
|---|---|---|---|
| Node 网络 | 节点物理 IP (eth0) | 物理网络 / DHCP | 节点间互通的物理基础 |
| Pod 网络 | 每个 Pod 一个 IP (如 10.42.x.x) | CNI 插件 (Flannel/Calico) | 所有 Pod 能直接互通,无论是否同节点 |
| Service 网络 | 虚拟 IP (如 10.43.x.x) | k8s 自身 (kube-proxy) | 不存在于任何网卡,纯 iptables/IPVS 规则 |
关键理解: - eth0
是物理网卡,连外部局域网;cni0 是虚拟网桥,连 Pod - Pod IP
(10.42.x.x) 只在集群节点内部路由可达,外部设备没有路由表 -
Service 的 ClusterIP 是一个存在 etcd 里的编号 + kube-proxy 写的 iptables
DNAT 规则,没有进程监听它
| 类型 | 暴露在哪 | 原理 |
|---|---|---|
| ClusterIP | 集群内部 | kube-proxy iptables DNAT 到 Pod |
| NodePort | 所有 Node 的 0.0.0.0:NodePort |
iptables PREROUTING 拦截端口 → DNAT |
| LoadBalancer | NodePort + 云厂商 LB | LB 公网 IP → NodeIP:NodePort |
| Ingress | L7 域名路由 | Ingress Controller (如 k3s 自带的 Traefik) 按 host/path 分发到 Service |
kube-proxy 的 DNAT 规则示例:
# 拦截 NodePort 30080 的流量
-A PREROUTING -p tcp --dport 30080 -j KUBE-NODEPORT
# 随机选后端 Pod
-A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.5 \
-j KUBE-SEP-PODA
# 改写目标 IP:port 为 Pod 的 IP:port
-A KUBE-SEP-PODA -p tcp -j DNAT --to-destination 10.42.0.5:80DNAT (Destination NAT) 即目标地址转换——包进入 Node 时,内核 netfilter 在 PREROUTING 链改目标 IP:port。DNAT 有状态,回复包自动逆转换,外部看到的是 NodeIP 回的包,Pod IP 完全透明。
客户端 → NodeIP:NodePort (eth0 可达)
→ iptables PREROUTING 匹配 → DNAT 改写目标为 Pod IP
→ 路由决策 → FORWARD → cni0 网桥
→ veth pair → Pod netns → nginx 进程
局域网内访问服务: - 少量设备:改 /etc/hosts - 不用配
DNS:用 nip.io,Ingress host 填
app.192.168.1.100.nip.io - 多设备多域名:自建
dnsmasq(占用极小,跑在 53 端口)
远程访问(公网/跨 NAT)—— WireGuard Hub-and-Spoke:
VPS (公网 IP, 做 Hub)
10.0.0.1/24
/ \
k3s 节点 (你) 朋友
10.0.0.2/24 10.0.0.3/24
10.42.0.0/16
核心配置,VPS 侧:
[Peer]
PublicKey = <k3s节点公钥>
AllowedIPs = 10.0.0.2/32, 10.42.0.0/16关键就在 AllowedIPs 带上 Pod CIDR
10.42.0.0/16——WireGuard 的 AllowedIPs 本质就是路由表,VPS
看到目标 10.42.x.x 的包就知道该转发给 k3s 节点。k3s
节点上再加一条路由即可让朋友直接访问 Pod:
ip route add 10.42.0.0/16 dev wg0
# 之后朋友直接 curl http://10.42.0.5:8000 就能访问 Pod好处:比 NodePort/Ingress 少一层 DNAT,全程 WireGuard 加密,不需要公网端口暴露。
那总而言之,k8s就是一个择机在某个节点上运行容器的系统。而这发生的时机是:系统中存在未分配到具体节点运行的容器。即,大部分情况下只有新的容器会经过调度器计算,被指定给一个节点来创建;对应的还有不太常见的情况:正在运行的容器因为某些原因挂了,或者是被终止等,这时如果它们在k8s的蓝图中还是“规划中应该存在的”容器,那它们就会进入调度队列,即,触发重调度动作。
重调度则是一种运行时负载均衡的实用手段,运行时如果某个节点负载过重或者出现异常需要维护,就可以由维护者程序触发重调度,停止容器并由调度器将它分配到其他合适的节点。
对应到程序上。我们创建程序时需要指定程序使用的硬件资源,包括使用哪个处理器核心、内存区域等;容器也是一样,启动时会经过调度器判断,将它分配给合适的节点执行。
虽然这和单机操作系统这种响应时间级别的场景相比,它工作的时间尺度远大于前者,但是一些典型调度器还是可以供后者借鉴使用的。