跳转到内容
← 返回核心概念
系统与架构计算机科学 · 云计算16 分钟阅读

容器与容器编排

Containers and Orchestration

2013 年 3 月,一家名不见经传的 PaaS 初创公司 dotCloud 在 PyCon 上发布了一个内部工具——Docker。演示视频里,工程师 Solomon Hykes 在台上用几行命令打包了一个应用,把它传到另一台机器上,一个命令运行起来:环境完全一致,没有任何"在我机器上能跑"的问题。 那次演示被后来的开…

容器DockerKubernetesDevOps云原生

2013 年 3 月,一家名不见经传的 PaaS 初创公司 dotCloud 在 PyCon 上发布了一个内部工具——Docker。演示视频里,工程师 Solomon Hykes 在台上用几行命令打包了一个应用,把它传到另一台机器上,一个命令运行起来:环境完全一致,没有任何"在我机器上能跑"的问题。

那次演示被后来的开发者称为"容器革命的起点"。Docker 项目在 GitHub 上发布后,在接下来 18 个月里收获了 1.5 万个 Star——这个速度在当时前所未有。它用一种直观的体验,解决了软件分发和运行环境一致性这个困扰开发者数十年的问题。

破除误解:容器不是轻量级虚拟机

虚拟机(VM)在硬件上模拟一套完整的计算机——包括虚拟 CPU、虚拟内存、虚拟网卡,以及一个完整的操作系统内核。VM 隔离彻底,但每个 VM 通常占用几 GB 内存,启动需要数十秒。

容器共享宿主机的 Linux 内核,只隔离进程的视图——用 Linux 内核的两个核心机制实现:

  • 命名空间(Namespaces):让进程看到一个独立的文件系统、网络栈、进程列表、主机名——实际上是内核提供的"视图隔离"
  • 控制组(cgroups):限制进程能使用的 CPU、内存、磁盘 I/O——资源的硬性上限

结果是:容器镜像通常几十 MB,启动时间毫秒级,单台机器可以运行数百个容器。代价是:容器之间的内核隔离不如 VM 彻底——一个内核漏洞可能被容器内的恶意进程利用。

容器的史前史:Docker 没有发明容器

容器在 2013 年因 Docker 而流行,但构成容器的内核机制早在它之前几十年就已存在。Docker 的贡献是把它们包装得好用,而不是发明了它们。

  • chroot(1979):Unix V7 引入的系统调用,把进程的根目录限定到某个子目录,是"隔离文件系统视图"的远祖;1982 年进入 BSD。它只换了根目录,谈不上真正隔离。
  • 命名空间(Namespaces):第一个 mount namespace 随 Linux 2.4.19 内核(2002 年)登场,由 Al Viro 实现;此后逐步补齐——UTS 与 IPC(2.6.19,2006 年)、PID(2.6.24,2008 年)、network(2.6.29,2009 年),最关键的 user namespace 直到 3.8 内核(2013 年)才完整可用。
  • cgroups(2006–2008):由 Google 工程师 Paul Menage 与 Rohit Seth 在 2006 年发起,最初叫"process containers",2007 年改名 control groups,2008 年 1 月随 2.6.24 内核并入主线。它源自 Google 内部对资源限制的需求。
  • LXC(2008):第一个把 namespaces 与 cgroups 封装成易用工具集的项目,无需给内核打补丁。Docker 早期正是构建在 LXC 之上,后来才用自研的 libcontainer(即 runc 的前身)替换它。

也就是说,Docker 真正的贡献不是隔离技术本身,而是把它包装成一套可移植的开发者体验:镜像分层、Registry 分发、一条命令构建并运行。这正是"在我机器上能跑"问题终于有解的原因——镜像不可变,构建一次,到哪儿跑都一样。

Docker 的三个核心概念

镜像(Image):一个只读的文件系统快照,包含应用代码和所有依赖。镜像是分层的(Union FS)——每条 Dockerfile 指令增加一层,层可以被共享和复用,节省存储和传输。

容器(Container):镜像的运行实例,在镜像基础上增加一个可写层。容器停止后,可写层消失;如需持久化数据,必须挂载外部卷(Volume)。

仓库(Registry):存储和分发镜像的服务。Docker Hub 是默认公共仓库,各云厂商也提供私有仓库(Amazon ECR、Google GCR)。

Kubernetes:编排的兴起

Docker 解决了"打包"问题,但当你有数百个容器需要在几十台机器上运行,新问题来了:

  • 哪个容器运行在哪台机器上?
  • 一台机器宕机,上面的容器如何自动迁移?
  • 如何在流量高峰时自动扩容,低谷时缩容?
  • 容器之间如何互相发现和通信?

这些问题属于容器编排的范畴。Google 在 2014 年开源了 Kubernetes(希腊语"舵手"),它脱胎于 Google 内部约从 2003–2004 年起运行的 Borg 系统——管理着 Google 全球数据中心的几百万个任务。Kubernetes 1.0 于 2015 年 7 月 21 日发布,同日 Google 与 Linux 基金会宣布成立 CNCF,Kubernetes 成为其托管的第一个项目。

Kubernetes 的核心设计哲学是声明式配置:用户声明"我想要 10 个运行 nginx:1.20 的副本,其中端口 80 对外暴露",Kubernetes 负责让集群状态收敛到这个声明。用户不命令机器"现在启动一个容器",而是声明"期望状态是什么"——协调循环(Reconciliation Loop)持续把实际状态驱动向期望状态。

关键抽象

抽象含义
Pod一个或多个共享网络和存储的容器,是最小调度单元
Deployment声明 Pod 的副本数和更新策略
Service为一组 Pod 提供稳定的网络访问入口(DNS 名 + 负载均衡)
ConfigMap / Secret配置数据和敏感数据与应用代码分离
Namespace在同一集群内做软隔离

标准化与运行时分层:OCI、runc、containerd

容器爆发后,业界担心格式碎片化,于是 2015 年 6 月 22 日,Docker、CoreOS 等公司在 Linux 基金会下成立 OCI(Open Container Initiative,开放容器倡议),统一两件事:镜像长什么样(image-spec)、运行时怎么启动容器(runtime-spec),后来又补上分发规范(distribution-spec)。Docker 把自研的运行时 runc 捐出来,作为 runtime-spec 的基石实现。

这暴露了一个常被忽略的事实:"Docker"其实是一摞分层组件,不是一个单体程序。 从上到下大致是:

  • Docker、Podman 等:面向开发者的高层工具(构建镜像、命令行)
  • containerd:管理镜像与容器生命周期的高层运行时;它本是 Docker 的内部组件,2017 年 Docker 把它捐给 CNCF,如今是 CNCF 毕业项目
  • runc:真正调用 namespaces 和 cgroups 创建容器的低层 OCI 运行时

正因为分层,Kubernetes 早在 1.5 版就定义了 CRI(Container Runtime Interface,容器运行时接口),让自己不绑定任何单一运行时。后果是:Kubernetes 1.20 弃用、1.24 版(2022 年)彻底移除了对接 Docker 的 dockershim。所以"Kubernetes 运行 Docker"这句话今天已不准确——它直接通过 CRI 对接 containerd 或 CRI-O,而它们最终都调用 runc。你的镜像照样能跑,因为镜像格式遵循 OCI 标准。

云原生生态

Kubernetes 催生了整个云原生(Cloud Native)生态,由 CNCF(云原生计算基金会,2015 年成立)托管:

Helm:Kubernetes 的包管理器,把一组资源打包为"Chart",方便分发和重用。

Prometheus + Grafana:监控和可视化,成为云原生监控的事实标准。

Istio / Linkerd:服务网格,在 Pod 旁注入 Sidecar 代理,提供服务间 mTLS、流量管理、可观测性。

Argo CD / Flux:GitOps 工具,以 Git 仓库为声明式配置的唯一真相源,自动同步到集群。

生产环境真正难在哪里

容器在开发机上显得很简单:docker run 一条命令就能启动服务。但生产环境的问题很少停留在"能不能启动"这一层,而是在事故发生时,系统能不能被解释、恢复和审计。

第一层难点是状态。无状态 Web 服务很适合容器化,数据库、消息队列和搜索索引却不同。它们关心磁盘延迟、复制拓扑、备份恢复点和一致性窗口。Kubernetes 可以用 StatefulSet、PersistentVolume 和 Operator 管理这些系统,但这不是把状态问题消灭了,而是把它们显式写进控制平面。

第二层难点是升级。Deployment 的滚动发布看似自动,实际需要处理数据库迁移、向后兼容 API、长连接、缓存污染和灰度回滚。一个镜像能快速替换,前提是应用本身遵守可替换的工程契约。容器让发布更快,也让错误传播更快。

第三层难点是可观测性。容器生命周期短,Pod 名称会变,节点会漂移。如果日志、指标、追踪没有统一的标签体系,事故现场很快会变成碎片。Prometheus、OpenTelemetry 和集中式日志系统的意义,不只是画图,而是把一次请求从入口、服务网格、业务服务、数据库到外部 API 的路径重新拼起来。

第四层难点是供应链。容器镜像常由基础镜像、系统包、语言依赖和构建脚本叠成多层。一个漏洞可能来自应用代码,也可能来自几个月前继承的 openssl、glibc 或 npm 包。因此现代容器平台开始强调 SBOM、镜像签名、准入控制和最小基础镜像。换句话说,容器把"机器环境"变成了可分发文件,也把机器环境的安全责任带进了代码审查。

安全边界:容器不是安全沙箱

容器给人的错觉,是它像虚拟机一样提供了完整边界。实际情况更微妙:容器的边界主要由 Linux 内核维护,容器内外共享同一个内核。只要内核攻击面足够大,容器逃逸就不是理论问题。

生产加固通常从几件小事开始:不以 root 用户运行进程;丢弃不需要的 Linux capabilities;启用只读根文件系统;限制 seccomp、AppArmor 或 SELinux 策略;禁止特权容器;控制容器能否挂载宿主机路径;用 NetworkPolicy 明确东西向流量。

这些配置看起来琐碎,却决定了容器平台的真实风险。Kubernetes 的强大之处在于它能统一表达这些约束;危险也在于,如果团队没有认真写这些约束,默认开放的能力会被误认为默认安全。

代价与争议

复杂性被低估:Kubernetes 的学习曲线极陡峭。一个完整的生产 Kubernetes 集群需要理解几十个概念,配置数百个选项。Kelsey Hightower(Google 工程师,Kubernetes 最有影响力的布道者之一)在 2023 年坦言,大多数小团队不需要 Kubernetes,一台配置优良的虚拟机足够了。

资源开销:Kubernetes 的控制平面(API Server、etcd、Scheduler、Controller Manager)本身需要消耗相当资源,对中小规模集群来说比例较高。

安全基线低:Kubernetes 默认配置的安全性较差——Pod 可以访问云厂商元数据 API、容器以 root 运行、网络策略默认是全开放的。生产加固需要大量额外配置。

替代方案复苏:Nomad(HashiCorp)、AWS ECS/Fargate、托管 Lambda(Serverless)都是 Kubernetes 的替代路线,针对 Kubernetes 过于复杂的批评进行了取舍。

跨域连接

  • 可观测性与监控:容器寿命短、名字会变、节点会漂移,靠主机名定位问题的老办法因此失效。必须先有统一的标签与追踪上下文,才能把一次请求从入口、网格、服务到数据库重新拼起来,否则事故现场只剩碎片。推论是:把日志与指标的标签体系留到出事后再补,基本等于没有。
  • 国家能力:声明式配置的关键不是写配置文件,而是控制器能持续观测实际状态并与期望比较。只声明目标却测不到现状,收敛就无从发生。治理同理:看不见的东西无法被调控,可观测性是执行能力的前提而非附属品。推论是:控制平面的价值不在自动化本身,而在它把状态差异变成了可比较的量。
  • 助推与自由家长制:默认值就是事实政策。容器默认以超级用户运行、网络策略默认全通,于是多数集群的真实风险由默认值而非文档决定。可检验推论:把安全基线做成默认配置,比写进规范有效得多。
  • 制度经济学:互相竞争的厂商把镜像与运行时规范交给中立组织托管,是为了避免格式碎片化抬高各方的对接成本。收益是镜像到处能跑,代价是创新被锁在规范的演进节奏上,谁也不能单方面改格式。推论是:标准化收益越大的领域,越难出现颠覆性的替代格式。
  • 忒修斯之船:滚动发布不修补实例,而是整体替换。"同一个服务"的身份因此由声明维持,不再由任何一个进程的连续存在维持——这既是可回滚的前提,也意味着写在容器内的状态默认会被丢掉。推论是:有状态服务上容器平台,难点从来不是启动,而是数据放在哪里。

参考文献

  • Verma, A., Pedrosa, L., Korupolu, M., Oppenheimer, D., Tune, E., Wilkes, J. Large-scale cluster management at Google with Borg. EuroSys, 2015.(Borg 系统的原始论文,Kubernetes 的设计源头)
  • Burns, B., Grant, B., Oppenheimer, D., Brewer, E., Wilkes, J. Borg, Omega, and Kubernetes. ACM Queue 14(1), 2016.(Google 三代集群管理系统的综述)
  • Merkel, D. Docker: Lightweight Linux Containers for Consistent Development and Deployment. Linux Journal, 2014.
  • Open Container Initiative. Image Format SpecificationRuntime Specification. opencontainers.org, 2015 起持续维护.(容器镜像与运行时的开放标准)
  • Kubernetes Documentation. Container Runtime Interface (CRI)Dockershim Removal FAQ. kubernetes.io.(CRI 接口与 dockershim 移除说明)

延伸阅读

  • Hightower, K., Burns, B., Beda, J. Kubernetes: Up and Running. 3rd ed., O'Reilly, 2022.
  • Richardson, C. Pattern: Microservice Architecture. microservices.io.(微服务与容器的模式参考)
  • Turnbull, J. The Docker Book. 自出版,2014.(早期 Docker 实践参考)