Skip to content

容器和编排

应用部署这件事,最让人头疼的就是同一份代码在不同机器上跑出不同结果——测试机能跑,推到生产就报缺库;A 机器有环境变量,B 机器没有;回滚的时候上一版文件和配置怎么都对不上。这种"环境漂移"问题困扰了运维很多年,容器就是为了彻底解决它而生的。

容器说白了就是把应用、依赖、启动命令、文件系统全打成一个包,叫镜像。镜像搬到哪里跑出来都是一样的,不会再有"我这能跑你那不能跑"这种事。镜像运行起来就是容器编排系统(Kubernetes 是代表)再负责把大量容器放到集群里运行,处理调度、重启、扩缩容、服务发现、滚动发布这些事。

一、部署环境问题

手工部署的流程大概是:登录机器、装运行时(Java/Python/Node)、复制文件、改配置、启动进程,再把日志路径、进程管理方式记到文档里。机器少的时候这么搞还行,机器一多就完全失控。

问题现场表现
依赖不一致缺动态库、Python 包版本不同、JDK 版本不同
权限不一致某台机器目录不能写,上传或者写日志失败
发布不可重复同样的步骤换个人执行,结果就不一样
回滚困难上一版文件、配置、启动参数都对不上
扩容慢新加机器要重新装运行环境,半天搞不定

虚拟机也能解决一部分问题——它能隔离一整套操作系统,但镜像体积大(几个 GB)、启动慢(几十秒)、分发成本高。容器走的另一条路:不隔离整个操作系统,只隔离进程运行环境,让应用直接跑在宿主机内核上,但通过 namespace、cgroup 这些机制让它"看起来"独占系统。所以容器镜像只有几十到几百 MB,启动只要几百毫秒,分发也快。

二、镜像与容器

这两个概念最容易混。简单说:镜像是静态制品,容器是镜像跑起来后的实例。类比一下,镜像是程序安装包,容器是按这个安装包装好之后正在运行的那个程序实例。一个镜像可以同时启动多个容器,就像一个安装包可以装到多台机器上一样。

对象是什么
镜像打包好的应用、依赖、启动命令、文件系统
容器镜像跑起来后的进程环境
镜像仓库保存和分发镜像的地方,比如 Harbor、Docker Hub
容器运行时创建和管理容器的组件,比如 containerd、CRI-O

一次完整的发布流程大概是:开发提交代码 → CI 系统构建镜像 → 把镜像推到仓库 → 服务器或者集群从仓库拉镜像 → 启动容器。

这里有个非常容易绕进去的点:容器不是小型虚拟机。容器里的进程其实还是跑在宿主机内核上的,只是通过 namespace(隔离视图,比如进程列表、网络、文件系统)、cgroup(隔离资源,比如 CPU、内存、IO 限额)这些机制,让它感觉自己是独占系统的。

排查的时候这个特性经常被踩到:容器里 ps 看到的进程、ip addr 看到的网卡、df 看到的文件系统,跟宿主机都不一样——但内核参数(/proc/sys/)、节点资源、磁盘 IO、宿主机时间这些底层东西,还是宿主机那一套。比如容器里 top 看到 CPU 占用很低,实际宿主机可能早就被打满了——因为 cgroup 限制的是容器自己,宿主机层面的整体压力容器内根本看不到。

三、Docker 和 Kubernetes 的关系

很多人刚学容器的时候分不清 Docker 和 Kubernetes 是什么关系,以为 K8s 是 Docker 的升级版。其实不是——它俩是两个层次的东西。

Docker 是一个工具集,把镜像构建、容器运行、网络、卷、命令行这些事包装得特别顺手,所以大家学容器、做本地调试基本都从 Docker 开始。

Kubernetes 是集群级别的编排系统,管的是"一堆机器上跑的大量容器"。在 K8s 节点上,kubelet 通过 CRI 接口调用底层容器运行时(现在主流是 containerd 或 CRI-O),真正启动容器的不是 Docker 而是 containerd。Docker 在生产 K8s 集群里其实已经不太常用了,但在本地构建、开发调试、学习容器命令这些场景里仍然是主流。

中间有个标准叫 OCI(Open Container Initiative),定义了镜像格式和运行时接口。这个标准让"构建工具"和"运行时"可以解耦——docker buildbuildahkanikonerdctl 构建出来的镜像,只要符合 OCI 规范,就能推到任何仓库,再被任何运行时(containerd、CRI-O、Docker)拉取运行。所以你不必从头到尾都用 Docker 一家,工具链可以自由组合。

四、Kubernetes

单机 Docker 解决的是"一台机器上怎么跑容器"——但生产环境要回答的问题远不止这些:容器放在哪台机器、挂了谁拉起来、服务之间怎么互相访问、配置和密钥怎么注入、发布的时候怎么逐步替换旧版本、流量怎么调度。这些问题 Docker 自己搞不定,所以才有了 Kubernetes 这种编排系统。

K8s 的核心思路是声明式 API——用户提交的不是一串启动命令,而是一份资源描述:这个应用要 3 个副本、用哪个镜像、暴露哪个端口、挂载哪些配置。控制器收到这份描述之后,持续观察集群当前状态,跟期望状态对比,有差异就调整。比如你声明 3 个副本,某个 Pod 挂了,控制器发现当前只有 2 个,就会自动再起一个补上。

问题K8s 用什么对象或机制解决
跑几个副本Deployment
容器放在哪个节点Scheduler(调度器)
容器挂了怎么办kubelet、控制器自动重建
服务怎么访问Service、DNS
HTTP 入口在哪里Ingress、Gateway API
配置怎么注入ConfigMap(普通配置)、Secret(敏感配置)

K8s 的祖师爷是 Google 内部的 Borg 系统——一个用了十几年的大规模集群管理系统。容器生态成熟之后,Google 把 Borg 的设计思想(声明式 API、调度、控制器、服务发现、滚动发布)整理成开源系统,就是现在的 Kubernetes。

五、常见对象

K8s 里有些对象会反复遇到,记一下它们各自管什么:

对象干什么
Pod最小运行单元,里面放一个或多个紧密耦合的容器
Deployment管理无状态应用副本数和滚动发布
Service给一组 Pod 提供稳定的访问入口(Pod IP 会变,Service IP 不变)
ConfigMap保存非敏感配置,挂载到 Pod 里
Secret保存密码、证书、Token 等敏感配置(_base64 编码)
Ingress / Gateway暴露 HTTP/HTTPS 入口,根据域名路径转发
PVC申请持久化存储,容器重启数据不丢

这些对象把原来散在脚本、配置文件、人工操作里的步骤,全部收进了集群 API。带来的最大变化是:排查问题不再看"某个进程是否还在",而是看 Pod 状态、容器日志、Events、Service 后端、Ingress 规则、节点资源

举个例子,用户访问页面返回 502,在传统部署里大概就是"Nginx 到后端不通",但在 K8s 里要拆成好几层看:Ingress 规则有没有匹配到、Service 的 selector 有没有选中 Pod、Endpoints 列表是不是空的、Pod 是不是 Ready 状态、容器里的应用端口是不是真的在监听。任何一层断了,用户看到的现象都是 502。

六、容器化之后典型故障

容器化之后,很多传统故障会表现成 K8s 的状态。看到这些状态基本能直接定位排查方向:

状态或现象大概率问题在哪
ImagePullBackOff镜像名拼错、tag 不存在、仓库地址不通、拉取凭据没配
CrashLoopBackOff应用启动报错、配置缺失、权限不够、健康检查配错
Pod 一直 Pending节点资源不足、调度约束(nodeSelector、affinity)不满足、PVC 绑定失败
Service 没后端(Endpoints 空)label selector 不匹配、Pod 还没 Ready
Ingress 502后端端口、Service、Endpoints、应用健康检查

容器让发布制品本身更稳定了(镜像一致),但排查对象的种类变多了——镜像、运行时、Pod、Service、Ingress、CNI 网络、存储卷、节点资源,任何一个环节都可能成为问题源。这也是为什么 K8s 运维比传统运维门槛高——要理解的对象和层次多得多。

排查的一个实用思路:顺着 K8s 的对象层次一层层查。比如一次 502 故障,先看 Ingress 日志,如果看到 upstream 指向的 Endpoints 是空的,基本能先排除浏览器和 DNS 这两层,问题更接近 Service selector、Pod label 或者 Pod Ready 状态。再看 Service 的 selector 跟 Pod label 是不是真的能匹配上。这种"顺着 API 对象层次"的排查思路,是 K8s 运维的核心技能。