Service 与 kube-proxy:虚拟 IP、iptables 与 IPVS
Service 的 ClusterIP 是一个不存在于任何网卡的虚拟 IP。在节点上运行 ip addr,看不到它;用 ping 测试,收不到回应;但对它发起 TCP 连接,数据却能稳定到达某个后端 Pod。这个"幽灵 IP"能够工作,完全依赖 kube-proxy 写入每个节点内核的 iptables 或 IPVS 规则——规则在,转发在;规则消失,连接消失。 理解这一机制的起点是区分两种截然不同的角色:kube-proxy 是"规则维护者",内核的 netfilter/IPVS 子系统才是"数据面执行者"。kube-proxy 进程本身从不处理任何用户请求,它只监听 kube-apiserver 上 Service 和 EndpointSlice 的变化,把这些变化翻译成内核规则,然后等待下一次变化。真正处理数据包的是内核——每个到达某 ClusterIP:port 的数据包,在经过 iptables nat 表的 PREROUTING 链时,被 DNAT 规则改写目标地址,转向某个真实的 Pod IP:port,整...
网络模型与 CNI:每个 Pod 一个 IP 背后的实现
Kubernetes 对网络做出了一个看似简单的承诺:每个 Pod 拥有一个集群范围内唯一的可路由 IP,Pod 之间无需 NAT 可直接通信,节点访问 Pod 也无需 NAT。三条规则,没有一条涉及实现细节。这种"定义行为、不定义机制"的风格与 Kubernetes 整体的设计哲学一致,但也意味着没有任何一行核心代码实现了这个承诺——它完全由 CNI 插件负责兑现。 理解 CNI 的切入点不是插件本身,而是 Linux 网络命名空间带来的问题。每个容器拥有独立的网络栈,包含独立的路由表、ARP 缓存、iptables 规则集。新建的命名空间里只有一个回环接口 lo,对外完全不可达——这正是容器隔离的本意。CNI 插件的全部工作就是在这种隔离性上凿出一条通道:创建 veth pair,把一端移入 Pod 命名空间,另一端挂到宿主机网桥,配置 IP 地址和路由,让 Pod 从完全隔离的孤岛变成集群网络里的一个节点。 跨节点通信是第二层复杂性。同节点内的 Pod 通过共享网桥即可互通,但当数据包需要离开宿主机的物理网卡,穿越真实的交换机和路由器,抵达另一台节点,再...
kubelet 与容器运行时:节点上到底发生了什么
前两篇讲了调度(选节点)和 Pod 生命周期(状态机)。这一篇下到节点层:kubelet 在节点上把一个 Pod spec 变成真实运行的进程。 容器运行时曾经等于 Docker。Kubernetes 1.24 移除了 dockershim,此后所有节点必须使用实现了 CRI(Container Runtime Interface)的运行时:containerd、CRI-O 或其他。这次迁移对用户基本无感,但原理上有重要变化——kubelet 和运行时之间的通信从内嵌的特定实现变成了标准 gRPC 接口。 本文只问一个问题:从 pod.spec.nodeName 被写入,到容器进程在节点上跑起来,发生了什么? 节点内部全景图 123456789101112131415161718192021222324API Server → etcd (Pod.spec.nodeName = this-node) │ ▼kubelet (watches own node's Pods via Informer)┌───────────────────────────...
调度器深入:打分、抢占与拓扑约束
调度器(kube-scheduler)是 Kubernetes 控制平面里最容易被低估的组件。它做的事情看起来简单:把没有 nodeName 的 Pod 绑定到某个节点。但"选哪个节点"这个问题背后,隐藏着资源感知、亲和性约束、拓扑分散、优先级抢占等多维度的决策过程。 从 Kubernetes 1.19 起,调度器的内部逻辑通过 Scheduling Framework 对外暴露为一组扩展点。原来散落在代码各处的调度逻辑被重新组织成插件,每个插件只负责一个扩展点。这让调度行为既可预测,又可扩展。 本文的核心问题是:Kubernetes 调度器用什么框架选节点,当资源不足时如何通过抢占让高优先级 Pod 运行? 调度框架全景图 1234567891011121314151617181920212223242526Pod pending (no nodeName) │ ▼ Scheduler(from ActiveQ by priority) ┌──────────────────────────────────────────────────...
Pod 生命周期:从 Pending 到 Running 的完整路径
一个 kubectl apply -f pod.yaml 之后,终端里出现了 pod/my-app created。这行输出只意味着 API Server 接受了资源定义并写入 etcd——Pod 还没有运行在任何节点上,甚至还没有被任何节点知晓。从这个瞬间到容器真正响应流量,Pod 经历了一条由多个组件接力完成的状态转换链。 这条链不是黑盒。Kubernetes 把每个阶段都编码进 status.phase、status.conditions、status.containerStatuses 三个字段里,并通过 Events 记录关键节点的时间戳。理解这条链,就能在 Pod 卡住时精确定位是调度失败、镜像拉取失败、探针失败,还是 OOM Kill。 本文的核心问题是:一个 Pod 从 kubectl apply 到 Running 经历了哪些状态转换,每个状态背后是哪个组件在操作? 状态转换全景图 12345678910111213141516171819202122232425kubectl apply │ ▼ etcd (Pod phase=Pending...
控制器模式与 Informer 机制:调谐循环的工程实现
Kubernetes 的控制平面由一批控制器组成,每个控制器负责把某种资源的实际状态收敛到期望状态。Deployment 控制器确保 ReplicaSet 数量正确,ReplicaSet 控制器确保 Pod 数量正确,StatefulSet 控制器维护有序 Pod 的标识和存储绑定。这些控制器全部运行在 kube-controller-manager 进程中,共享同一个 Go runtime,但逻辑上彼此独立。 控制器的核心模式是调谐循环(Reconcile Loop):读取资源当前状态,与期望状态对比,对差异执行操作,然后等待下一次触发。这个循环不是基于轮询的——每隔几秒查一次 etcd 的方式在规模上不可持续。实际实现依赖 Informer 机制:Informer 在本地维护一份与 etcd 同步的缓存,用 Watch 接收增量变更,把变更转换成事件分发给控制器。控制器从工作队列取出事件,执行 Reconcile,写回 API Server。 这篇的核心问题是:从 etcd 的一次写入,到控制器的一次 Reconcile 调用,中间经历了哪些层次,每层解决什么问题? Info...
etcd 与持久化:集群状态的唯一事实来源
上一篇讲完了 API Server 的请求管道。管道的终点是 etcd。这一篇进入 etcd 内部。 etcd 经常被简单地描述为"分布式 KV 存储"。这个描述成立,但掩盖了一个关键点:etcd 不是 Redis,也不是 Zookeeper,它是一个强一致的、以 Raft 为共识机制的、支持 Watch 的版本化存储。Kubernetes 之所以选择 etcd,正是因为这三个性质缺一不可——缺少任何一个,API Server 的 List-Watch 机制就无法建立在它之上。 本文只问一个问题:etcd 的哪些设计让 Kubernetes 的 List-Watch 机制成为可能? etcd 在集群中的位置 123456789101112131415161718192021┌─────────────────────────────────────────────────────┐│ Kubernetes Control Plane ││ ...
API Server 与声明式 API:一切皆资源
上一篇(核心概念导读)描述了 Kubernetes 的对象体系和控制器循环。这一篇进入 API Server 内部。API Server 不是普通的 HTTP 代理;它是整个集群的唯一写入路径,也是所有控制器、kubelet、用户工具共享的信息总线。 声明式 API 经常被描述为"说你想要什么,而不是怎么做"。这个描述准确,但不完整。Kubernetes 的声明式 API 背后有一套精确的语义:冲突检测、字段所有权、幂等写入。理解这些语义,才能解释为什么 kubectl apply 不是简单的 HTTP PUT。 本文只问一个问题:一个 apply 请求经过了哪些关卡? 请求管道全貌 一个 kubectl apply 请求从客户端到 etcd 的完整路径: 12345678910111213141516171819202122232425262728kubectl apply -f deployment.yaml │ │ HTTPS (mTLS) ▼┌─────────────────────────────────...
导读:为什么 Kafka 的核心是一根日志
Kafka 不是一个消息队列。更准确的说法是:Kafka 是一个分布式追加日志(append-only log)系统,消息队列的语义只是日志操作的一种投影。 本文只抓一个问题:追加日志这个数据结构,如何成为 Kafka 全部机制的起点。 追加日志:一种最朴素的数据结构 追加日志的定义可以压成三条规则: 写入只能追加到尾部(append)。 每条记录获得一个单调递增的序号(offset)。 已写入的记录不可修改(immutable)。 用 ASCII 表示一根日志的状态: 123456offset: 0 1 2 3 4 5 6 +----+----+----+----+----+----+----+ | m0 | m1 | m2 | m3 | m4 | m5 | | ← 写入点 +----+----+----+----+----+----+----+ ^ ...
从零配置 GitHub Actions 自动部署 Hexo 博客
Hexo 博客最容易卡住的地方,通常不是 hexo generate 本身,而是生成后的 public/ 怎么稳定发布。手动 hexo deploy 能跑,但它把构建环境、网络、Git 凭据都绑在本地机器上。换一台电脑、换一个网络、换一个 Node 版本,部署结果就可能变得不可预测。 更稳的做法是把部署流程写进仓库:源码仓库只保存 Markdown、主题配置、依赖锁和 workflow;每次 push 到 main,GitHub Actions 在云端安装依赖、生成静态站点,再把 public/ 推送到 GitHub Pages 仓库。 这篇文章记录从零配置这套流程的完整路径。例子以当前博客为准: 源码仓库:magicliang/hexo-blog 站点仓库:magicliang/magicliang.github.io Hexo 版本:8.1.2(Butterfly 5.x 主题) 包管理器:Yarn 1 构建命令:npx hexo clean && npx hexo generate 部署方式:peaceiris/actions-gh-pages@v4 推...
