做后端的人基本都经历过这个过程:先在单机上把 Docker 玩得飞起,镜像一打包到处跑,爽是爽,但容器数量一多,手工维护就开始翻车。机器要腾挪、端口要改、依赖要重新理顺,新服务上线一次像打仗一样。这时候你才有机会真正理解,为什么社区里所有人都在聊 K8s 架构。Kubernetes 这套架构设计的核心,就是把“一堆孤立的容器”变成“一个有调度、有自愈、有负载均衡能力的操作系统”。往大了说,K8s 就是分布式架构里的“机房操作系统”,微服务架构落地时,服务注册、弹性伸缩、滚动发布这些让人头疼的能力,它都封装成了标准接口。这篇文章就把 K8s 架构从控制平面到工作节点拆开讲,再给出一份可以直接照着做的 k8s 安装部署思路,适合刚入门的同学建立整体视图,也适合在生产环境折腾过一轮、想回头把原理补齐的运维和开发。
1. 先从宏观视角理解 K8s 架构的设计意图
1.1 从“养容器”到“编排容器”
单机 Docker 时代,大家玩的其实是进程管理:镜像 build 一次,run 起来是个容器,logs 能看日志,exec 能进容器,这跟直接用 systemd 跑进程没有本质区别。真正把人逼疯的,是容器多到一定程度之后的“运维失控”。
举个例子,你有 10 台机器,上面跑了 50 个容器,每个容器需要独立配置环境变量、挂载存储、映射端口。今天 A 机器磁盘满了,你得把一批容器迁到 B 机器;明天某个容器把 CPU 吃满,你得手动给它限流;后天发新版,你得先停掉老容器再起新容器,中间稍有差池就是一段服务中断。这些工作不是不能做,但每一次都靠人肉操作,成本太高,而且以 100% 的精力去维护 100 个容器的运行状态,根本不是人能干的事。
K8s 架构解决的就是这个“失控”问题。它的核心思想是声明式管理,你只需要告诉它“我要 3 个副本的 nginx”,它就负责把 3 个副本调起来,放在哪台机器、怎么放、容器挂了怎么补,统统不用你操心。这种模式一旦建立起来,你会发现机器的概念被淡化了,集群看起来就像一个巨大的“资源池”,你的服务只是这个池子里的租户。
我印象最深的一次经历是刚接触 K8s 时,把一个跑了两年的单体服务拆成 12 个微服务。以前服务启动顺序、依赖关系、端口占用都需要人工维护,迁到 K8s 之后,整套服务通过清单文件定义,一条命令就能在全新环境里拉起整个体系。那种“从钢丝上走下来”的感觉,确实是架构演进带来的最大红利。
1.2 控制平面与数据平面:架构的第一次切分
K8s 架构最容易被忽略、但其实最重要的设计,是它从一开始就把集群分成两个平面:控制平面(Control Plane)和数据平面(Data Plane)。控制平面就是集群的“大脑”,负责决策,比如哪个 Pod 该调度到哪个节点、某个 Deployment 需要几个副本、服务发现怎么走。数据平面则是“肌肉”,负责真正跑容器、转发流量、存储数据。
这个切分思路和网络设备里的控制面/转发面分离很像,核心好处有三点。第一是全局视野,调度器能看到所有节点的资源水位,才能做出全局最优的放置决策,如果每台节点各自为政,很容易出现“这台机器挤爆、那台机器闲着”的失衡。第二是安全边界,普通用户永远不可能直接操作节点内核,所有人只能通过 API Server 这个统一入口提交请求,权限模型才能闭环。第三是故障域隔离,控制平面挂了,已运行的业务暂时还能跑,只是无法做变更;反过来,某个节点崩了,控制平面也能立刻感知并重新调度,把损失限制在局部。
理解了这一层切分,再看 K8s 架构的细节就会很顺。控制平面是一组组件,etcd、API Server、Scheduler、Controller Manager;工作节点是另一组组件,kubelet、kube-proxy、容器运行时。搞清楚每个组件是谁、负责什么、跟谁通信,K8s 对你就不再是一个黑盒,而是一台可以随意拆装的机器。
2. 控制平面核心组件拆解:K8s 的大脑
2.1 API Server:所有请求都从这扇门进
API Server 是 K8s 里最核心的组件,没有之一。你可以把它理解成公司前台:所有外部请求、内部组件请求,都必须先经过它,再由它转发给后台系统。你去 kubectl apply 一个 Deployment,这条命令最终就是通过 HTTP 请求打到 API Server,由它校验、存储,再通知其他组件执行。
为什么不能绕过 API Server?因为 K8s 的所有状态变更都需要走“先写 etcd,再让控制器去干活的链路”。如果允许各个组件直接改数据,整个集群的一致性就被破坏。API Server 承担了认证、授权、准入控制、校验、持久化等一系列工作。举个例子,准入控制(Admission Control)会在对象写入 etcd 之前拦截请求,检查你是否给 Pod 设置了资源限制、是否使用了被禁止的镜像仓库,这一层是很多安全策略落地的关键位置。
实际排障时,大量问题都能在 API Server 日志里找到线索。我刚上手那会儿经常遇到 Deployment 创建没反应,第一反应是看 kubelet,其实正确姿势是先kubectl describe deployment,看 Events 和 Conditions。如果显示no matches for kind "Deployment"这类错误,十有八九是 API Server 侧的 apiVersion 写得不对,或者是自定义资源没注册。API Server 的 watch 机制也很值得了解,所有控制器都不是“定时去查询”,而是 watch 资源变化,收到事件推送再做处理,这才能做到秒级响应。
2.2 etcd:集群状态的“银行账本”
etcd 是 K8s 架构里唯一一个存储组件,保存了集群的完整状态。哪个节点是 Ready 的、哪个 Pod 跑在哪台机器上、哪个 Service 关联了哪些 Endpoint,全部以键值对的形式存在这里。如果你把 K8s 想象成一家银行,etcd 就是那本终极账本,任何一笔“存款”和“取款”都会被记下来。
第一次接触 K8s 的人经常问:为什么不用 MySQL 或者 Redis?因为 etcd 要的其实是强一致性、低延迟和 watch 能力。强一致性靠的是 Raft 共识算法,写请求必须多数节点确认才算成功;watch 能力则是让控制器能实时感知数据变化,而不是轮询数据库。MySQL 虽然也能做这些,但性能和模型都不匹配,Redis 又放弃了强一致性,都不合适。
关于 etcd 有几个实操心得,我必须多说几句。首先是备份,etcd 的备份几乎是集群唯一可信的备份手段,一定要定期做,建议至少每天一次快照。其次是性能,etcd 对磁盘 IO 非常敏感,生产环境别用网络存储,放本地 SSD,并且避免和其他业务抢 IO。最后是容量,etcd 不适合存大对象,比如 ConfigMap 超过 1MB 就明显感觉 API 变慢,原因就是每次变更都要走 Raft 全量同步。我踩过最大的坑是没给 etcd 单独做 IO 隔离,高峰期 API 响应从几十毫秒涨到几秒,后来把 etcd 挪到独立节点才解决。
2.3 Scheduler 与 Controller Manager:决策与纠偏
Scheduler 负责回答一个核心问题:新创建的 Pod 应该放在哪个节点上?它的决策过程分成两步:过滤和打分。过滤阶段会剔除掉资源不足、有污点、不满足亲和性条件的节点,剩下的是候选;打分阶段再根据资源利用率、镜像是否已存在于节点、亲和性等因素给候选节点排序,选最优的胖子,然后把 Pod 绑定到那个节点。
Controller Manager 则是 K8s 架构里的“纠偏机”。它内部是一堆小控制器,DeploymentController、ReplicaSetController、NodeController、NamespaceController 等等,每个控制器都遵循同一个循环模式:查看期望状态,查看当前状态,发现偏差,发出指令纠正。拿我经常用的 Deployment 举例,你声明replicas: 3,Controller Manager 发现当前只有 2 个副本,就会创建第 3 个;如果你把镜像版本从 v1 改成 v2,它还会驱动滚动更新,先停一个再起一个,默认maxUnavailable: 25%。这个模式有一个正经名字,叫控制器循环,也是 K8s 架构里最值得借鉴的设计思维之一。
我建议所有做后端的人认真理解这个“期望状态驱动”的模型。它最大的价值是让运维变得可预测,你不需要关心中间过程中某个时刻集群长什么样,只需要明确“最终要变成什么样”。很多同学刚学 K8s 时喜欢手动改副本数、手动删除 Pod,其实这些都不符合声明式管理的思路,正确做法是改 YAML,让控制器帮你收敛。
3. 工作节点与数据面:真正干活的地方
3.1 kubelet:节点上的驻场经理
控制平面做了决策之后,真正在节点上执行的组件是 kubelet。每台工作节点上都有一个 kubelet,它的职责可以概括成一句话:确保当前节点上的 Pod 都按 PodSpec 定义的状态运行。API Server 通过 watch 机制告诉 kubelet“这个节点上应该跑哪几个 Pod”,kubelet 就调用容器运行时把容器创建出来,然后持续监控容器健康状态,并把状态回报给 API Server。
kubelet 还负责探针检查。Liveness Probe 决定容器要不要重启,Readiness Probe 决定流量要不要进这个 Pod,Startup Probe 则给慢启动的应用留时间。很多同学部署 Java 服务老是被反复重启,就是因为默认探针没配 Startup Probe,应用启动要 40 秒,Liveness 却设成 10 秒一次,自然误杀。实际配置中建议对冷启动时间长的服务单独配 Startup Probe,期间 Liveness 不会生效。
kubelet 与容器运行时通信走的是 CRI(Container Runtime Interface),这让 K8s 可以同时支持 containerd、CRI-O 等运行时。现在没有必要再用 Docker 作为运行时,containerd 是更轻量的选择,镜像兼容性也很好,我迁移到 containerd 之后节点内存占用明显下降了。如果你用kubectl get nodes看到节点状态是 NotReady,第一步应该检查 kubelet 服务状态和日志,大概率是证书过期、磁盘压力或网络插件挂掉。
3.2 kube-proxy 与 Service 抽象
Service 是 K8s 架构里最妙的设计之一。Pod 是会被随时创建和销毁的,它们的 IP 也在不断变化,你不可能让其他服务记死 Pod IP。Service 就像是一个稳定的“前台号码”,用户只需要访问 Service 的虚拟 IP,内容由 kube-proxy 转发到背后的 Pod。
kube-proxy 在很长一段时间默认用 iptables 模式工作,把 Service 流量转发规则写成一组 iptables 链,数据包到了节点,由内核按规则转发到某个 Pod。iptables 模式实现简单,但规则一多性能会下降。后来出现了 IPVS 模式,哈希表查找比线性规则匹配快得多,吞吐量高,我已经把所有集群都切到 IPVS 了。如果你也想切,只需要在 kube-proxy 的 ConfigMap 里把mode改成ipvs,实测在大量 Service 的场景下,延迟和 CPU 消耗都有明显改善。
这里要提醒一点:Service 的 ClusterIP 是一个虚拟 IP,你永远 ping 不通它,因为它没有绑定任何网卡。排障时如果发现 Service 不通,不要先去 ping ClusterIP,而是检查三件事:Service 的 selector 是否匹配 Pod 标签、Endpoints 列表是否为空、kube-proxy 是否在正常同步规则。流量绕来绕去的问题,多数时候都出在标签匹配这层,kubectl get endpoints一目了然。
3.3 容器运行时与 Pod 生命周期
Pod 是 K8s 架构里最小的调度和部署单元,它跟容器不是一回事。一个 Pod 里可以有一个或多个容器,这些容器共享同一个网络命名空间和存储卷,它们之间的通信走 localhost 就行,访问同一个磁盘目录也不需要额外配置。这个设计很像“同一个业务进程的多个子进程”,最适合的场景是一个主容器做业务,旁边挂一个 sidecar 容器做日志采集或流量代理。
容器的生命周期管理也有一套完整语言。每个容器的状态可能是 Waiting、Running 或 Terminated;Pod 层面还有完整状态机。最重要的是重启策略,可选 Always、OnFailure 和 Never。绝大多数时候你用的是 Always,意味着只要容器退出,kubelet 就会把它重新拉起来,这也是 K8s 自愈能力的来源。但要警惕,如果一个容器始终崩溃,kubelet 会按指数退避重启,从 10 秒、20 秒涨到 300 秒封顶,看着像“卡住了”,实际是保护机制在起作用。
我经常跟团队说一句话:Pod 是无状态的,不要把任何业务数据写在容器文件系统里,因为在节点故障时,Pod 可能被调度到别的机器,原来写入的本地文件全都消失。持久化数据必须走 PersistentVolume 和 PersistentVolumeClaim 挂载到 Pod 上,由底层存储撑腰,才能做到漂移不丢数据。
4. 实操:一套可落地的 k8s 安装部署方案
4.1 环境规划与版本选型
在动手装 K8s 之前,先把环境想清楚。最省心的方式是用托管服务,云厂商帮你维护控制平面,你只需要管工作节点,适合不想研究 kubeadm 细节的团队。但如果你的目标是学习架构,或者有离线部署、私有化交付需求,那自己用 kubeadm 装一遍非常值得,一次动手对架构理解的提升是看十篇文章都比不上的。
版本选型要遵循一个原则:不要盲目追新。Kubernetes 每个版本只维护大约一年,你可以选前一个稳定版本,比如当前最新稳定版往前退一两个次版本,既保证安全补丁可用,又避开了新版本早期的坑。三个节点的控制平面至少要 4 核 8G,工作节点根据业务量定,单个节点内存建议至少 4G,磁盘要用 SSD 才有安全余量。
规划网络时要提前定好 Pod CIDR 和 Service CIDR。Pod CIDR 决定集群内容器的 IP 段,比如10.244.0.0/16,Service CIDR 设为10.96.0.0/12。这两个网段必须跟物理网络、现有业务网段错开,否则路由冲突会让你排查到天亮。节点之间要保证 6443、10250、2379 等端口互通,防火墙规则先放行。
4.2 初始化与组件部署的关键步骤
我以 kubeadm 为例说一套可复现的过程。准备阶段要做的三件事:关闭 swap、加载内核模块、设置 sysctl 参数。关闭 swap 是因为 kubelet 需要强依赖内存的严格限制,swap 会导致容器性能和资源判断失真。内核模块方面,需要 br_netfilter 和 overlayfs。
初始化命令大概是这样的:
sudo kubeadm init \ --control-plane-endpoint=k8s-master:6443 \ --pod-network-cidr=10.244.0.0/16 \ --service-cidr=10.96.0.0/12 \ --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers--pod-network-cidr这个参数尤其重要,它要跟后续安装网络插件时用的网段一致。如果选了 Calico 并且用默认配置,Pod 网段通常用10.244.0.0/16,后面别乱改,否则网络起不来。初始化完成后会生成一段 join 命令,工作节点拿到 token 就能加进来,token 默认有效期是 24 小时,过期后可以用kubeadm token create --print-join-command重新生成。
控制平面初始化好了,第一件事是装网络插件。K8s 本身不解决 Pod 间跨节点通信的问题,需要通过 CNI 插件实现。很多刚接触的人在这步翻车,不装网络插件只看kubectl get nodes,控制平面显示 NotReady,其实根因就是 CNI 组件没启动。我比较推荐 Calico,功能全面,支持 NetworkPolicy,缺点是组件稍重;纯追求轻量可以选 Flannel,但做不了 NetworkPolicy。安装命令一般是:
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/<版本>/manifests/calico.yaml装完网络插件,等kubectl get pods -n kube-system里网络相关 Pod 全部 Running,节点状态就会变成 Ready。
4.3 集群验证与第一个应用
安装完成后建议先部署一个简单应用验证整条链路。我会创建一个 Deployment,镜像用 nginx:
apiVersion: apps/v1 kind: Deployment metadata: name: demo-nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:latest ports: - containerPort: 80然后执行:
kubectl apply -f demo-nginx.yaml kubectl get pods -o wide正常情况你会看到 3 个 Pod 分布在不同的工作节点上,状态都是 Running。接着创建一个 Service 把它们暴露出来:
kubectl expose deployment demo-nginx --port=80 --type=NodePort用kubectl get svc查看分配的 NodePort,然后通过任意节点 IP 加端口访问。这条链路走完,你基本就能把控制平面、工作节点、Service 转发、容器运行时的关系串起来了。到这一步,集群真正能干活了。
5. 排错与避坑:我在生产环境里踩过的坑
5.1 高频故障速查表
我整理了一份实战中最高频的故障对照表,基本上每一条都是我亲眼见过的:
| 症状 | 常见原因 | 优先排查命令 |
|---|---|---|
| Pod 一直 Pending | 节点资源不足、没有匹配标签、存在污点 | kubectl describe pod <name>看 Events |
| CrashLoopBackOff | 启动命令失败、依赖配置缺失、探针误杀 | kubectl logs <pod>配合--previous |
| ImagePullBackOff | 镜像名写错、私有仓库凭据缺失 | kubectl describe pod看 Events 里的具体错误 |
| 节点 NotReady | kubelet 异常、CNI 插件挂掉、磁盘压力 | journalctl -u kubelet -f |
| Service 不通 | selector 不匹配、Endpoints 为空、kube-proxy 异常 | kubectl get endpoints <service> |
| API Server 变慢 | etcd 磁盘 IO 超载、大量大对象写入 | kubectl get --raw /metrics |
排查这件事,最重要的不是背命令,而是养成看 Events 的习惯。kubectl describe输出的 Events 字段会把调度失败、镜像拉取失败、探针失败的原因直接告诉你,多数问题根本不需要翻日志。我见过太多人上来就kubectl logs,其实很多调度类问题根本不会出现在容器日志里。
5.2 资源配置:不设 limits 等于裸奔
这是我在生产环境里交学费最多的一课。Kubernetes 调度器看的是 Pod 的 requests 值,它决定这个 Pod 能“坐在”哪台节点上;而 limits 是硬约束,容器超过 limits 会被直接杀掉或限制 CPU。很多团队只设 requests 不设 limits,意味着某个 Pod 可以把整台节点的内存吃光,其他 Pod 全部遭殃。
正确做法是两者都给,而且建议 requests 略低于实际使用,limits 留出一定余量。比如一个服务平时用 512M 内存,高峰期 900M,你可以设置requests: memory: 512Mi、limits: memory: 1Gi。这样调度器能准确分配资源,高峰期又有缓冲,同时避免单点吃垮宿主。
还有一个容易被忽略的点:CPU 的 requests 和 limits 会导致 QoS 分级。如果 Pod 没设 limits,它的 QoS 类是 BestEffort,内存紧张时会被第一个杀掉;如果 requests 等于 limits,是 Guaranteed,节点压力大时会尽量保住。如果你有核心业务,务必让它达到 Guaranteed 级别,否则某天节点内存告警,核心服务反而走在最前头。
5.3 高可用不是第一优先项
很多初学者刚装完集群就急着做高可用,三个控制平面、负载均衡、多层存储全上,结果反而把自己淹没在复杂度里。我的建议是:先单控制平面跑通业务、做好监控和备份,再逐步演进高可用。说实话,一个稳定运行的单控制平面集群,加上每日 etcd 备份和离线部署包,可靠性已经超过绝大多数中小团队的真实运维水平。等业务规模上来了,再引入多控制平面,注意 etcd 节点一定要配奇数,3 个起步、5 个封顶,这是 Raft 算法决定的,偶数节点不仅不增加可用性,反而白白增加同步开销。
横向扩展方面,多用标签和节点亲和性来规划拓扑。开发环境、测试环境、生产环境尽量用不同的命名空间隔离,再按团队或服务打标签。污点和容忍度是控制调度的重要工具,给专用节点打上污点后,除非业务显式容忍,否则普通 Pod 不会调度上去,这对跑数据库、AI 训练这种强资源独占的场景非常实用。
我在实际使用中还有一个体会:K8s 架构的学习曲线并不是线性的,很多人卡在中间是因为想一口气吃透所有组件。正确的顺序是先弄懂控制平面和数据平面这对概念,再亲手部署一套集群,把 nginx 跑起来,接着用 Service 做流量转发,最后才是折腾存储、网络策略和扩展调度。这样一步步下来,之前觉得晦涩的概念会自然串起来。如果你正准备做 k8s 安装部署,别急着抄复杂配置,先把最小可用集群跑稳,再根据业务量逐步加码,这条路我走下来,是最省时间的。