深入解析Docker架构与K8S实战最近团队在梳理一套老服务的容器化改造方案,我跟很多同行聊下来,发现一个很有意思的现象:大家对docker run、kubectl apply这种日常命令都很熟,但一旦追问“Docker 底层到底怎么创建容器”“Pod 卡在 Pending 时该去哪里查原因”,不少人就开始含糊了。这其实很正常,容器和编排工具发展到今天,已经成了一层套一层的抽象,平时用着顺手,但真要排查问题或者做架构决策,底层那点逻辑就必须补上。这篇文章我想以自己多年的实操经验为切入点,把 Docker 的架构关键角色、镜像和容器的隔离机制讲清楚,再把 K8S 的组件地图、工作负载资源、部署实战和排错思路串起来。适合刚接触 K8S 的后端、运维,也适合已经会写 Dockerfile 但对集群原理还一知半解的朋友。这里不会只贴命令,我会尽量把每个步骤背后的“为什么”一起说清楚。1. Docker到底是怎么跑起来的:架构里的几个关键角色1.1 Client、Daemon 和它们之间的会话很多初学者以为docker run就是 Docker 的全部,其实它只是客户端工具。Docker 的整套架构是典型的 C/S 模式:docker命令行是客户端,后台常驻的dockerd是服务端,两者通过 REST API 通信。我们敲下docker run时,客户端把请求发给 dockerd,dockerd 经过一系列处理后返回结果给你。这个设计的直接好处是:客户端不一定要和守护进程在同一台机器上。生产环境里完全可以通过-H tcp://ip:2375远程管理一台宿主机上的容器。当然,这样裸奔很危险,必须有充分的鉴权策略,否则等于把服务器管理口暴露在网络上。还有个容易忽略的点:docker ps、docker stop这些看起来“本地操作”的命令,本质上也是在和 dockerd 通信。等 daemon 挂掉之后,你执行任何 docker 命令都会报“Cannot connect to the Docker daemon”,哪怕只是想看容器列表也不行,因为列表也存在 daemon 维护的状态里。1.2 containerd、runc 和 shim:被大部分人忽略的底座现代 Docker 版本(20.x 以后)自己并不是直接创建容器的,它下面还有一层更底层的运行时。完整链路大致是这样:docker client → dockerd → containerd → containerd-shim → runc → 内核(命名空间+cgroups)dockerd:负责镜像管理、API 接入、网络配置、存储卷挂载等上层能力。containerd:真正管理容器生命周期、镜像解包、存储等能力,是业界的标准容器运行时之一。containerd-shim:夹在 containerd 和 runc 之间的中间进程,负责维护容器的标准输入输出,同时在容器主进程退出后把状态报告回去。runc:真正调用 Linux 内核特性创建容器的那个小工具,也是 OCI 运行时规范的参考实现。这里必须多说一句:很多人以为 Docker 和 containerd 是竞争关系,其实在新版本里 Docker 自己也已经内置了 containerd。也就是说,你在部署 Kubernetes 集群时遇到“容器运行时用 containerd 还是 Docker”的选择,本质上不是跑两个东西,而是看集群用哪个接口去管理容器。1.3 为什么是“客户端-守护进程”而不是直接执行有一种常见的误解是:Docker 就是个进程管理工具,像 systemd 一样直接拉起进程不就行了?为什么中间要绕这么多层?用生活里的例子类比:你外卖点餐,不是直接绕过平台冲进后厨让厨师开火,而是把订单交给平台,平台再分配到具体商户和骑手。这个平台就是 dockerd,它要负责排队、定位、结算、售后等一系列横切的事。容器也一样,如果每次创建都直接调内核,那镜像管理、网络分配、日志收集、资源统计这些活儿就没地方放了。正因如此,Docker 才能提供分层镜像、数据卷、网络桥接、日志驱动这些“超出一个单独进程”的能力。注意:生产环境排查问题时,第一件事永远是确认 dockerd 和 containerd 进程是否都活着。经常有人纠结“容器突然停了但 dockerd 还在”,结果最后发现是 containerd 卡死了。2. 镜像、容器、隔离机制:把 Docker 最难的部分讲透2.1 镜像分层与联合文件系统很多人把 Docker 镜像理解成一个“打包好的软件”,这没错,但从底层看,镜像是多层只读文件的叠加。每一层对应 Dockerfile 里的一步指令,比如FROM、RUN、COPY。这些层会在容器启动时通过联合文件系统合并成一个统一的文件系统视角。以 OverlayFS 为例:底层叫做 lowerdir,就是镜像的只读层;容器启动后会在最上面加一个可写的 upperdir,也就是容器层。你对容器内文件的修改都发生在这一层,不会动到底层镜像。删除文件也不是真的把底层删掉,而是通过“whiteout”机制在可写层把它遮住。这个设计带来的实际收益是镜像缓存和传输效率。比如你同一个基础镜像构建多个项目,只要基础镜像层不变,宿主机上就只需存一份,pull 的时候也只要拉取新增的层。但反过来,如果 Dockerfile 写得很粗糙,比如把易变的代码 COPY 放在最前面,那后面每次构建都可能重新执行很多层,缓存基本全废。分享一个我常用的多阶段构建示例:FROM golang:1.21 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o app . FROM alpine:3.19 COPY --from=builder /app/app /usr/local/bin/app ENTRYPOINT ["app"]这样最终镜像里只包含编译好的二进制和基础运行环境,不会带着整个 Go 工具链,镜像体积可以从几百兆直接压到二三十兆。2.2 容器的隔离:命名空间与控制组容器看起来像一个独立的小系统,本质上是内核提供的两类能力合在一起:Namespace:负责“看不着别人”。PID、Network、Mount、UTS、IPC、User 这些命名空间隔离了进程、网络、文件系统挂载点、主机名、进程间通信和用户 ID 视图。Cgroups:负责“用不超额度”。通过控制组限制 CPU、内存、PID 数量、IO 带宽。打个比方:命名空间是给每个容器造了一个单独的房间,互相看不到对方的家具;cgroups 是给每个房间装了独立电表和限流阀,防止一个房间把整栋楼的电拉崩。有一点必须提醒:容器隔离不等于安全隔离。容器和宿主机共享同一个内核,如果内核有漏洞,恶意进程有可能逃逸出去。所以生产环境里不要把容器当虚拟机来信任,需要强隔离的场景还是要考虑虚拟机,或者至少配合更强的安全上下文和 seccomp 策略。2.3 数据卷和容器网络容器的可写层生命周期和容器一致,容器一删,没放在卷里的数据就全没了。所以需要持久化的数据一定要通过 volume 或 bind mount 挂出来。volume:由 Docker 管理的本地存储,推荐做法。bind mount:直接把宿主机目录映射进容器,修改宿主机文件容器里立即可见。网络方面,常见模式是 bridge、host、none:模式特点适合场景bridge默认,容器之间通过虚拟网桥通信,有独立 IP单机多容器、需要端口映射host容器直接用宿主机网络栈,不隔离端口对网络性能要求极高的场景none无网络,只有 loopback离线计算、特殊安全需求一个容易踩的坑是端口映射和绑定地址。docker run -p 8080:80会把宿主机所有网卡的 8080 转发到容器 80,如果只想在内网某个地址暴露,一定要写完整:-p 192.168.1.10:8080:80,否则外部流量也可能直接到达服务。3. K8S 集群的组件地图:别被各种概念绕晕3.1 控制平面的四个核心组件Kubernetes 集群从逻辑上分为控制平面和工作节点两部分。控制平面负责“决策”,工作节点负责“执行”。控制平面主要四个组件:API Server:所有 kubectl 操作、组件间通信的唯一入口,负责认证、授权、准入控制和数据持久化转发。etcd:保存集群所有状态数据的分布式键值存储。可以理解成控制平面的“数据库”,但它是强一致性的。Scheduler:只负责一个新 Pod 该部署到哪个节点,通过一系列过滤和打分策略做决策。它不负责真正运行 Pod。Controller Manager:一堆控制器的集合,比如节点控制器、副本控制器、服务账号控制器。它们通过 watch 机制不断把“实际状态”拉回“期望状态”。为什么拆这么多组件?核心是职责单一和可扩展性。API Server 不再管调度逻辑,Scheduler 也不管维持副本数,每个组件可以做独立的水平扩展。而且任何一个组件出问题,不至于整个集群瘫痪。3.2 工作节点上的三件套每个工作节点上至少有这几样东西在跑:kubelet:和 API Server 保持通信,负责 Pod 的真正创建、启动、健康检查和回收。kube-proxy:负责实现 Service 的转发规则,常见实现是 iptables、ipvs。容器运行时:真正创建容器的组件,通过 CRI(Container Runtime Interface)接入,常见的有 containerd、CRI-O。这里多讲一下 Docker 和 K8S 的关系。K8S 默认通过 CRI 和容器运行时交互,Docker 本身不直接实现 CRI。早期有 dockershim 这个适配层,1.24 版本已经彻底移除。现在想继续用 Docker 做运行时,需要单独部署一个叫 cri-dockerd 的适配器,但一般没必要,直接用 containerd 更省事。注意:很多教程还在用docker命令查看 K8S 节点上的容器,但在 containerd 环境下docker ps是看不到集群 Pod 的。需要换成crictl ps、crictl logs。3.3 Pod:调度、重启、扩展的最小单位K8S 管理的最小单位不是容器,而是 Pod。一个 Pod 可以包含一个或多个容器,这些容器共享同一个网络命名空间、同一个 IP 地址和存储卷。为什么要把容器再包一层?因为有些场景下多个容器必须紧密协作:主容器写日志,sidecar 容器负责采集转发;主容器提供业务接口,sidecar 容器做本地代理或流量拦截。如果让它们各自拥有独立 IP 和存储,协调起来就很麻烦。Deployment、StatefulSet 这些控制器并不直接创建 Pod,而是通过 ReplicaSet 管理 Pod 副本。这解释了为什么你改了 Deployment 的 Pod 模板后,K8S 不会原地修改已有 Pod,而是创建新的 ReplicaSet,滚动替换旧 Pod。这个设计保证了发布过程可控、可回滚。4. 从部署一个应用开始:核心编排资源的实操角度4.1 把镜像构建和推仓库的注意点部署到集群之前,镜像必须先放到某个 Registry。构建时除了多阶段构建,还有几个经验:镜像版本不要用latest。生产环境我看到太多因为 latest 漂移导致的环境不一致。建议用构建时间和提交哈希组合,比如app-web:20250318-3fa2c91。利用构建缓存。把变化频率低的依赖安装步骤放在 Dockerfile 前面,变化频繁的源码 COPY 放最后。注意架构标签。K8S 集群常见的是 amd64,但现在 ARM 节点越来越多,构建时要留意--platform,或者用 manifest 推送多架构镜像。推送过程很简单:docker build -t registry.example.com/team/app-web:20250318-3fa2c91 . docker push registry.example.com/team/app-web:20250318-3fa2c91如果是私有仓库,需要先docker login registry.example.com,凭据会存在~/.docker/config.json,K8S 节点拉取私有镜像时需要把这个凭据配置成 imagePullSecret,否则拉取直接报权限错误。4.2 用 Deployment 和 Service 跑起来下面是一个最小可用的部署示例,包含 Deployment 和 Service:apiVersion: apps/v1 kind: Deployment metadata: name: app-web namespace: production labels: app: app-web spec: replicas: 3 selector: matchLabels: app: app-web template: metadata: labels: app: app-web spec: containers: - name: app-web image: registry.example.com/team/app-web:20250318-3fa2c91 ports: - containerPort: 8080 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 3 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: app-web namespace: production spec: selector: app: app-web ports: - port: 80 targetPort: 8080几个关键点:resources.requests是调度时用来计算节点是否有足够空间的指标,limits是运行时硬限制。如果把 requests 写得过大,会导致 Pod 即使节点很空闲也调度不上去。readinessProbe决定流量是否进入 Pod。如果应用启动慢却不加这个探针,会有一段时间请求打到还没准备好的实例上,造成 502。Service 的selector必须和 Pod 的 labels 匹配,否则 Endpoints 列表是空的,流量自然不通。4.3 命名空间、标签与日常工作流命名空间(namespace)相当于集群内部的逻辑隔离单元,可以用它区分开发、测试、生产环境,或者按团队、业务线划分。但要注意,命名空间之间网络默认是通的,隔离网络还得靠 NetworkPolicy。标签(label)是 K8S 里最灵活的元数据机制。用标签可以非常精细地筛选资源:kubectl get pods -n production -l app=app-web kubectl get pods -n production -l version=20250318 kubectl logs -n production -l app=app-web --tail=50我习惯给所有资源打三层标签:应用名、版本、环境。比如app=app-web、version=20250318、env=prod。一开始多花一点时间,后面排查问题会非常省力。5. 实战排错:部署过程中一定会踩的坑5.1 镜像拉不下来怎么办这是新集群最常见的问题之一,现象就是 Pod 状态一直ImagePullBackOff。排查顺序:kubectl describe pod <pod-name> -n <namespace>然后看 Events 里的具体报错。常见原因有:镜像不存在或 tag 写错,比如把app-web:20250318写成了app-web:20250318-old。私有仓库没配 imagePullSecret。节点网络到镜像仓库不通,特别是跨云或离线和内网环境。解决办法也分场景。如果集群整体都无法访问外部仓库,最好在能访问外网的机器上先把镜像 pull 下来,再docker save导出、到节点上docker load导入,或者把镜像推到内网私有仓库。5.2 Pod 一直 Pending 的排查思路Pod 一直 Pending,说明调度器还没把 Pod 放到节点上。最常见的原因就是资源不足。执行:kubectl describe pod <pod-name> -n <namespace>Events 里会出现0/3 nodes are available这种提示。需要看节点状态:kubectl get nodes kubectl top nodes另外还要考虑污点和容忍度。如果工作节点有taint,而 Pod 没有对应的tolerations,也不会调度上去。这类问题看 Events 基本都能定位。还有一种隐蔽情况:节点虽然资源够,但 PVC 没绑定成功,Pod 会因为等待存储卷一直 Pending。5.3 CrashLoopBackOff 和网络不通启动后反复崩溃是最折磨人的问题。第一步是看日志:kubectl logs <pod-name> -n <namespace> --previous--previous参数非常重要,因为 Pod 重启后当前容器的日志可能是空的,必须看上一次进程退出前的日志。常见原因包括:启动命令依赖的环境变量没传。配置挂载到了错误路径。健康检查失败导致 K8S 主动杀容器,看起来像是“启动崩溃”。网络不通时,先确认 Service 是否有正确的 Endpoints:kubectl get endpoints <svc-name> -n <namespace>如果列表为空,说明 selector 没匹配上任何 Pod。再检查 Pod 是否全部 Ready,以及 targetPort 是不是写错。5.4 节点磁盘和运行时故障的自救集群跑久了,节点磁盘爆满非常常见。原因往往是镜像堆积、容器日志无限增长、以及 /var/lib/containerd 下面遗留数据。你可以用这些命令快速查看占用:df -h journalctl --disk-usage crictl stats crictl images --digests清理方式按照安全级别来:docker system prune # 如果用 Docker 运行时,清理悬空镜像和停止容器 crictl rmi --prune # containerd 环境删未使用镜像有些公司会直接给 kubelet 配置容器日志轮转,避免单日志文件把磁盘挤满。结尾:一点个人实战体会我自己做过不少容器化迁移和集群运维,最大的体会是:Docker 和 K8S 真正的挑战从来不在某个命令怎么写,而在于你能否把“进程、文件系统、网络、资源限制”这些操作系统底层概念串起来。镜像分层离不开文件系统,容器隔离离不开内核机制,Pod 调度离不开资源模型。谁先把这层窗户纸捅破,谁看问题就能快人一步。最后再分享一个小技巧:排查问题别总盯着 pod log,很多隐藏线索藏在kubectl describe pod的 Events 里,它记录了从调度到拉镜像到启动探针的完整过程。养成“先 describe、再 logs、最后 exec”的排查顺序,你会发现大部分问题其实三分钟内就能定位。希望这篇带着架构视角的实战梳理,能帮你少走一点我当年走过的弯路。
深入解析Docker架构与K8S实战最近团队在梳理一套老服务的容器化改造方案,我跟很多同行聊下来,发现一个很有意思的现象:大家对docker run、kubectl apply这种日常命令都很熟,但一旦追问“Docker 底层到底怎么创建容器”“Pod 卡在 Pending 时该去哪里查原因”,不少人就开始含糊了。这其实很正常,容器和编排工具发展到今天,已经成了一层套一层的抽象,平时用着顺手,但真要排查问题或者做架构决策,底层那点逻辑就必须补上。这篇文章我想以自己多年的实操经验为切入点,把 Docker 的架构关键角色、镜像和容器的隔离机制讲清楚,再把 K8S 的组件地图、工作负载资源、部署实战和排错思路串起来。适合刚接触 K8S 的后端、运维,也适合已经会写 Dockerfile 但对集群原理还一知半解的朋友。这里不会只贴命令,我会尽量把每个步骤背后的“为什么”一起说清楚。1. Docker到底是怎么跑起来的:架构里的几个关键角色1.1 Client、Daemon 和它们之间的会话很多初学者以为docker run就是 Docker 的全部,其实它只是客户端工具。Docker 的整套架构是典型的 C/S 模式:docker命令行是客户端,后台常驻的dockerd是服务端,两者通过 REST API 通信。我们敲下docker run时,客户端把请求发给 dockerd,dockerd 经过一系列处理后返回结果给你。这个设计的直接好处是:客户端不一定要和守护进程在同一台机器上。生产环境里完全可以通过-H tcp://ip:2375远程管理一台宿主机上的容器。当然,这样裸奔很危险,必须有充分的鉴权策略,否则等于把服务器管理口暴露在网络上。还有个容易忽略的点:docker ps、docker stop这些看起来“本地操作”的命令,本质上也是在和 dockerd 通信。等 daemon 挂掉之后,你执行任何 docker 命令都会报“Cannot connect to the Docker daemon”,哪怕只是想看容器列表也不行,因为列表也存在 daemon 维护的状态里。1.2 containerd、runc 和 shim:被大部分人忽略的底座现代 Docker 版本(20.x 以后)自己并不是直接创建容器的,它下面还有一层更底层的运行时。完整链路大致是这样:docker client → dockerd → containerd → containerd-shim → runc → 内核(命名空间+cgroups)dockerd:负责镜像管理、API 接入、网络配置、存储卷挂载等上层能力。containerd:真正管理容器生命周期、镜像解包、存储等能力,是业界的标准容器运行时之一。containerd-shim:夹在 containerd 和 runc 之间的中间进程,负责维护容器的标准输入输出,同时在容器主进程退出后把状态报告回去。runc:真正调用 Linux 内核特性创建容器的那个小工具,也是 OCI 运行时规范的参考实现。这里必须多说一句:很多人以为 Docker 和 containerd 是竞争关系,其实在新版本里 Docker 自己也已经内置了 containerd。也就是说,你在部署 Kubernetes 集群时遇到“容器运行时用 containerd 还是 Docker”的选择,本质上不是跑两个东西,而是看集群用哪个接口去管理容器。1.3 为什么是“客户端-守护进程”而不是直接执行有一种常见的误解是:Docker 就是个进程管理工具,像 systemd 一样直接拉起进程不就行了?为什么中间要绕这么多层?用生活里的例子类比:你外卖点餐,不是直接绕过平台冲进后厨让厨师开火,而是把订单交给平台,平台再分配到具体商户和骑手。这个平台就是 dockerd,它要负责排队、定位、结算、售后等一系列横切的事。容器也一样,如果每次创建都直接调内核,那镜像管理、网络分配、日志收集、资源统计这些活儿就没地方放了。正因如此,Docker 才能提供分层镜像、数据卷、网络桥接、日志驱动这些“超出一个单独进程”的能力。注意:生产环境排查问题时,第一件事永远是确认 dockerd 和 containerd 进程是否都活着。经常有人纠结“容器突然停了但 dockerd 还在”,结果最后发现是 containerd 卡死了。2. 镜像、容器、隔离机制:把 Docker 最难的部分讲透2.1 镜像分层与联合文件系统很多人把 Docker 镜像理解成一个“打包好的软件”,这没错,但从底层看,镜像是多层只读文件的叠加。每一层对应 Dockerfile 里的一步指令,比如FROM、RUN、COPY。这些层会在容器启动时通过联合文件系统合并成一个统一的文件系统视角。以 OverlayFS 为例:底层叫做 lowerdir,就是镜像的只读层;容器启动后会在最上面加一个可写的 upperdir,也就是容器层。你对容器内文件的修改都发生在这一层,不会动到底层镜像。删除文件也不是真的把底层删掉,而是通过“whiteout”机制在可写层把它遮住。这个设计带来的实际收益是镜像缓存和传输效率。比如你同一个基础镜像构建多个项目,只要基础镜像层不变,宿主机上就只需存一份,pull 的时候也只要拉取新增的层。但反过来,如果 Dockerfile 写得很粗糙,比如把易变的代码 COPY 放在最前面,那后面每次构建都可能重新执行很多层,缓存基本全废。分享一个我常用的多阶段构建示例:FROM golang:1.21 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o app . FROM alpine:3.19 COPY --from=builder /app/app /usr/local/bin/app ENTRYPOINT ["app"]这样最终镜像里只包含编译好的二进制和基础运行环境,不会带着整个 Go 工具链,镜像体积可以从几百兆直接压到二三十兆。2.2 容器的隔离:命名空间与控制组容器看起来像一个独立的小系统,本质上是内核提供的两类能力合在一起:Namespace:负责“看不着别人”。PID、Network、Mount、UTS、IPC、User 这些命名空间隔离了进程、网络、文件系统挂载点、主机名、进程间通信和用户 ID 视图。Cgroups:负责“用不超额度”。通过控制组限制 CPU、内存、PID 数量、IO 带宽。打个比方:命名空间是给每个容器造了一个单独的房间,互相看不到对方的家具;cgroups 是给每个房间装了独立电表和限流阀,防止一个房间把整栋楼的电拉崩。有一点必须提醒:容器隔离不等于安全隔离。容器和宿主机共享同一个内核,如果内核有漏洞,恶意进程有可能逃逸出去。所以生产环境里不要把容器当虚拟机来信任,需要强隔离的场景还是要考虑虚拟机,或者至少配合更强的安全上下文和 seccomp 策略。2.3 数据卷和容器网络容器的可写层生命周期和容器一致,容器一删,没放在卷里的数据就全没了。所以需要持久化的数据一定要通过 volume 或 bind mount 挂出来。volume:由 Docker 管理的本地存储,推荐做法。bind mount:直接把宿主机目录映射进容器,修改宿主机文件容器里立即可见。网络方面,常见模式是 bridge、host、none:模式特点适合场景bridge默认,容器之间通过虚拟网桥通信,有独立 IP单机多容器、需要端口映射host容器直接用宿主机网络栈,不隔离端口对网络性能要求极高的场景none无网络,只有 loopback离线计算、特殊安全需求一个容易踩的坑是端口映射和绑定地址。docker run -p 8080:80会把宿主机所有网卡的 8080 转发到容器 80,如果只想在内网某个地址暴露,一定要写完整:-p 192.168.1.10:8080:80,否则外部流量也可能直接到达服务。3. K8S 集群的组件地图:别被各种概念绕晕3.1 控制平面的四个核心组件Kubernetes 集群从逻辑上分为控制平面和工作节点两部分。控制平面负责“决策”,工作节点负责“执行”。控制平面主要四个组件:API Server:所有 kubectl 操作、组件间通信的唯一入口,负责认证、授权、准入控制和数据持久化转发。etcd:保存集群所有状态数据的分布式键值存储。可以理解成控制平面的“数据库”,但它是强一致性的。Scheduler:只负责一个新 Pod 该部署到哪个节点,通过一系列过滤和打分策略做决策。它不负责真正运行 Pod。Controller Manager:一堆控制器的集合,比如节点控制器、副本控制器、服务账号控制器。它们通过 watch 机制不断把“实际状态”拉回“期望状态”。为什么拆这么多组件?核心是职责单一和可扩展性。API Server 不再管调度逻辑,Scheduler 也不管维持副本数,每个组件可以做独立的水平扩展。而且任何一个组件出问题,不至于整个集群瘫痪。3.2 工作节点上的三件套每个工作节点上至少有这几样东西在跑:kubelet:和 API Server 保持通信,负责 Pod 的真正创建、启动、健康检查和回收。kube-proxy:负责实现 Service 的转发规则,常见实现是 iptables、ipvs。容器运行时:真正创建容器的组件,通过 CRI(Container Runtime Interface)接入,常见的有 containerd、CRI-O。这里多讲一下 Docker 和 K8S 的关系。K8S 默认通过 CRI 和容器运行时交互,Docker 本身不直接实现 CRI。早期有 dockershim 这个适配层,1.24 版本已经彻底移除。现在想继续用 Docker 做运行时,需要单独部署一个叫 cri-dockerd 的适配器,但一般没必要,直接用 containerd 更省事。注意:很多教程还在用docker命令查看 K8S 节点上的容器,但在 containerd 环境下docker ps是看不到集群 Pod 的。需要换成crictl ps、crictl logs。3.3 Pod:调度、重启、扩展的最小单位K8S 管理的最小单位不是容器,而是 Pod。一个 Pod 可以包含一个或多个容器,这些容器共享同一个网络命名空间、同一个 IP 地址和存储卷。为什么要把容器再包一层?因为有些场景下多个容器必须紧密协作:主容器写日志,sidecar 容器负责采集转发;主容器提供业务接口,sidecar 容器做本地代理或流量拦截。如果让它们各自拥有独立 IP 和存储,协调起来就很麻烦。Deployment、StatefulSet 这些控制器并不直接创建 Pod,而是通过 ReplicaSet 管理 Pod 副本。这解释了为什么你改了 Deployment 的 Pod 模板后,K8S 不会原地修改已有 Pod,而是创建新的 ReplicaSet,滚动替换旧 Pod。这个设计保证了发布过程可控、可回滚。4. 从部署一个应用开始:核心编排资源的实操角度4.1 把镜像构建和推仓库的注意点部署到集群之前,镜像必须先放到某个 Registry。构建时除了多阶段构建,还有几个经验:镜像版本不要用latest。生产环境我看到太多因为 latest 漂移导致的环境不一致。建议用构建时间和提交哈希组合,比如app-web:20250318-3fa2c91。利用构建缓存。把变化频率低的依赖安装步骤放在 Dockerfile 前面,变化频繁的源码 COPY 放最后。注意架构标签。K8S 集群常见的是 amd64,但现在 ARM 节点越来越多,构建时要留意--platform,或者用 manifest 推送多架构镜像。推送过程很简单:docker build -t registry.example.com/team/app-web:20250318-3fa2c91 . docker push registry.example.com/team/app-web:20250318-3fa2c91如果是私有仓库,需要先docker login registry.example.com,凭据会存在~/.docker/config.json,K8S 节点拉取私有镜像时需要把这个凭据配置成 imagePullSecret,否则拉取直接报权限错误。4.2 用 Deployment 和 Service 跑起来下面是一个最小可用的部署示例,包含 Deployment 和 Service:apiVersion: apps/v1 kind: Deployment metadata: name: app-web namespace: production labels: app: app-web spec: replicas: 3 selector: matchLabels: app: app-web template: metadata: labels: app: app-web spec: containers: - name: app-web image: registry.example.com/team/app-web:20250318-3fa2c91 ports: - containerPort: 8080 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 3 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: app-web namespace: production spec: selector: app: app-web ports: - port: 80 targetPort: 8080几个关键点:resources.requests是调度时用来计算节点是否有足够空间的指标,limits是运行时硬限制。如果把 requests 写得过大,会导致 Pod 即使节点很空闲也调度不上去。readinessProbe决定流量是否进入 Pod。如果应用启动慢却不加这个探针,会有一段时间请求打到还没准备好的实例上,造成 502。Service 的selector必须和 Pod 的 labels 匹配,否则 Endpoints 列表是空的,流量自然不通。4.3 命名空间、标签与日常工作流命名空间(namespace)相当于集群内部的逻辑隔离单元,可以用它区分开发、测试、生产环境,或者按团队、业务线划分。但要注意,命名空间之间网络默认是通的,隔离网络还得靠 NetworkPolicy。标签(label)是 K8S 里最灵活的元数据机制。用标签可以非常精细地筛选资源:kubectl get pods -n production -l app=app-web kubectl get pods -n production -l version=20250318 kubectl logs -n production -l app=app-web --tail=50我习惯给所有资源打三层标签:应用名、版本、环境。比如app=app-web、version=20250318、env=prod。一开始多花一点时间,后面排查问题会非常省力。5. 实战排错:部署过程中一定会踩的坑5.1 镜像拉不下来怎么办这是新集群最常见的问题之一,现象就是 Pod 状态一直ImagePullBackOff。排查顺序:kubectl describe pod <pod-name> -n <namespace>然后看 Events 里的具体报错。常见原因有:镜像不存在或 tag 写错,比如把app-web:20250318写成了app-web:20250318-old。私有仓库没配 imagePullSecret。节点网络到镜像仓库不通,特别是跨云或离线和内网环境。解决办法也分场景。如果集群整体都无法访问外部仓库,最好在能访问外网的机器上先把镜像 pull 下来,再docker save导出、到节点上docker load导入,或者把镜像推到内网私有仓库。5.2 Pod 一直 Pending 的排查思路Pod 一直 Pending,说明调度器还没把 Pod 放到节点上。最常见的原因就是资源不足。执行:kubectl describe pod <pod-name> -n <namespace>Events 里会出现0/3 nodes are available这种提示。需要看节点状态:kubectl get nodes kubectl top nodes另外还要考虑污点和容忍度。如果工作节点有taint,而 Pod 没有对应的tolerations,也不会调度上去。这类问题看 Events 基本都能定位。还有一种隐蔽情况:节点虽然资源够,但 PVC 没绑定成功,Pod 会因为等待存储卷一直 Pending。5.3 CrashLoopBackOff 和网络不通启动后反复崩溃是最折磨人的问题。第一步是看日志:kubectl logs <pod-name> -n <namespace> --previous--previous参数非常重要,因为 Pod 重启后当前容器的日志可能是空的,必须看上一次进程退出前的日志。常见原因包括:启动命令依赖的环境变量没传。配置挂载到了错误路径。健康检查失败导致 K8S 主动杀容器,看起来像是“启动崩溃”。网络不通时,先确认 Service 是否有正确的 Endpoints:kubectl get endpoints <svc-name> -n <namespace>如果列表为空,说明 selector 没匹配上任何 Pod。再检查 Pod 是否全部 Ready,以及 targetPort 是不是写错。5.4 节点磁盘和运行时故障的自救集群跑久了,节点磁盘爆满非常常见。原因往往是镜像堆积、容器日志无限增长、以及 /var/lib/containerd 下面遗留数据。你可以用这些命令快速查看占用:df -h journalctl --disk-usage crictl stats crictl images --digests清理方式按照安全级别来:docker system prune # 如果用 Docker 运行时,清理悬空镜像和停止容器 crictl rmi --prune # containerd 环境删未使用镜像有些公司会直接给 kubelet 配置容器日志轮转,避免单日志文件把磁盘挤满。结尾:一点个人实战体会我自己做过不少容器化迁移和集群运维,最大的体会是:Docker 和 K8S 真正的挑战从来不在某个命令怎么写,而在于你能否把“进程、文件系统、网络、资源限制”这些操作系统底层概念串起来。镜像分层离不开文件系统,容器隔离离不开内核机制,Pod 调度离不开资源模型。谁先把这层窗户纸捅破,谁看问题就能快人一步。最后再分享一个小技巧:排查问题别总盯着 pod log,很多隐藏线索藏在kubectl describe pod的 Events 里,它记录了从调度到拉镜像到启动探针的完整过程。养成“先 describe、再 logs、最后 exec”的排查顺序,你会发现大部分问题其实三分钟内就能定位。希望这篇带着架构视角的实战梳理,能帮你少走一点我当年走过的弯路。