第 2 章

容器技术基础

·约 13 分钟

第 1 章推过一遍"为什么会有容器",这一章往下钻一层:容器具体是怎么实现的、Docker 的架构长什么样、多个容器为什么又需要 Kubernetes 这样的编排系统。

容器的隔离与资源限制

第 1 章说容器"共享宿主机内核,所以启动快、开销小",具体是靠 Linux 内核的两个机制实现的:

  • Namespace(命名空间):负责"隔离"。让每个容器都觉得自己独占了一套资源——有自己的进程树(PID Namespace)、自己的网络栈(Network Namespace)、自己的文件系统挂载点(Mount Namespace)、自己的主机名(UTS Namespace)等,实际上大家都跑在同一个内核上,只是互相看不见。
  • Cgroups(控制组):负责"限制"。规定每个容器最多能用多少 CPU、内存、磁盘 IO,防止一个容器把整台机器的资源吃光。

一句话总结:Namespace 让容器觉得自己独享世界,Cgroups 防止它真的把资源都占了。这两个是操作系统层面的原语,Docker 只是把它们封装成了好用的命令行工具。

从镜像到容器

先把三个名字放进同一个过程里:构建得到镜像,通过仓库分发,再从镜像创建运行中的容器。同一个镜像可以启动多个实例。

Dockerfile 与构建上下文生成只读镜像,镜像通过 push 和 pull 在本地与仓库之间分发;运行同一镜像得到多个容器,各有可写层并复用只读镜像层。

仓库公共的有 Docker Hub,阿里云对应的产品是 ACR(第 3 章展开)。图里画的是容器自身的文件层;写入卷或绑定挂载的数据由挂载的存储承接,不能都算进容器的可写层。镜像与容器的关系可对照 Docker 官方概览。

镜像分层的好处是复用:如果两个镜像都基于同一个 ubuntu:22.04 基础层构建,本地只需要存一份基础层,上层各自的差异部分才会重复占用空间。docker pull 拉镜像的时候也是按层拉取,已有的层会跳过。

Docker 客户端与后台服务

Docker 是 C/S(客户端-服务端)架构:

  • Docker Client:接收命令(docker run、docker build……),把请求发给 Daemon
  • Docker Daemon(dockerd):真正干活的后台进程,负责构建镜像、运行容器、管理网络和存储,对外暴露 REST API
  • 客户端和 Daemon 既可以在同一台机器上(最常见),也可以通过网络远程调用

底层的容器运行时早年是 Docker 自己基于 LXC(Linux Container)实现,后来演化出自己的 libcontainer。现在整个行业遵循 OCI(开放容器标准,Open Container Initiative),runc 是 OCI 标准的参考实现。不同厂商做的容器工具之所以能互相兼容,就是因为大家底层都对齐了同一份标准。

顺带一个常考的冷知识:Docker 是用 Go 语言写的。不止 Docker,云原生生态的核心项目(Kubernetes、etcd、Prometheus、containerd)几乎全是 Go。

构建与运行

日常最常用的一批命令:

docker pull nginx:1.25       # 从仓库拉镜像
docker images                # 查看本地已有的镜像
docker run -d -p 80:80 nginx # 后台启动一个容器,把容器 80 端口映射到宿主机 80
docker ps                    # 查看正在运行的容器
docker exec -it <容器ID> sh  # 进到容器里执行命令,排查问题常用
docker logs <容器ID>         # 看容器的输出日志
docker stop <容器ID>         # 停止容器
docker rm <容器ID>           # 删除已停止的容器
docker rmi <镜像ID>          # 删除镜像

自己构建镜像要写 Dockerfile,一个最基础的例子:

FROM node:20-alpine   # 基础镜像
WORKDIR /app           # 设置工作目录
COPY . .               # 把当前目录内容复制进镜像
RUN npm install         # 构建时执行的命令
EXPOSE 3000              # 声明容器对外监听的端口
CMD ["node", "index.js"] # 容器启动时执行的命令

docker build -t myapp:1.0 . 会按 Dockerfile 里的指令顺序逐层构建出镜像。

为什么需要容器编排

单机上用 Docker 管几个容器没问题,但生产环境的容器数量往往是几百上千个,分布在几十上百台机器上,这时候单机 Docker 就不够用了,会遇到一串新问题:

  • 这么多容器,该起在哪台机器上才能让资源分布均匀?
  • 某台机器上的容器挂了,谁负责发现并重新拉起?
  • 一个服务的多个容器实例分布在不同机器上,IP 还会变,别的服务怎么找到它?
  • 流量涨了要扩容,跌了要缩容,谁来做这个决策和执行?
  • 发布新版本的时候,怎么平滑替换旧容器而不中断服务?

这些问题的本质是"跨机器的容器生命周期管理",超出了单机工具能覆盖的范围,于是催生了一批容器编排系统:Docker 官方自带的 Swarm、Apache Mesos + Marathon,以及源自 Google 内部 Borg 系统经验、2014 年开源的 Kubernetes。2017 年前后,包括 Docker 官方在内的主流平台陆续把 Kubernetes 作为默认的编排引擎选项,Kubernetes 也成为 CNCF 首个毕业的项目——编排这条赛道基本尘埃落定。

Kubernetes 如何运行应用

Kubernetes(简称 K8s)的核心理念延续了第 1 章提到的声明式 API:你描述"我要什么样的最终状态"(比如"这个服务要保持 3 个副本在运行"),K8s 自己想办法达成,并且持续监控、一旦偏离就自动纠正。

控制平面与工作节点

Kubernetes 控制平面包含 API Server、etcd、Controller Manager 和 Scheduler;控制器与调度器经 API 观察和更新对象,节点 kubelet 经 API 获取分配给本机的 Pod 配置、报告状态,并调用容器运行时运行 Pod。各组件持续调谐,不是一次性的串行流水线。

提交 YAML 后,API Server 校验并保存对象;控制器根据期望状态创建或调整资源,Scheduler 为待调度的 Pod 选择节点,kubelet 再协调容器运行时把它跑起来。之后这些组件还会不断观察变化、继续调整。容器进程退出时,可能先由 kubelet 按重启策略处理;需要补充 Pod 副本时才由相应控制器处理。图中是控制关系,业务请求并不经过 API Server 转发。参见 Kubernetes 集群架构与控制器机制。

Pod 与常用对象

有一个常考的细节:Kubernetes 调度的最小单位不是容器,是 Pod——一个 Pod 里可以放一个或多个容器,这些容器共享同一个网络命名空间(同一个 IP),也可以共享挂载的存储卷。围绕 Pod,还有几个核心对象概念上要认识:Deployment(管理一组 Pod 副本,负责滚动升级和版本回滚)、Service(给一组变化中的 Pod 提供一个稳定不变的访问入口,并做负载均衡)、Namespace(在同一个集群里做逻辑隔离,划分不同项目或环境)。

Service 如何提供稳定入口

Service 的几种常见访问方式,考试常考:

类型作用
ClusterIP默认类型,只在集群内部可访问,分配一个集群内部 IP
NodePort在每个 Node 上开放一个固定端口,集群外部可以通过 节点IP:端口 访问
LoadBalancer接入云厂商等实现提供的负载均衡器;可以是公网或私网,取决于配置

默认实现中,NodePort 基于 ClusterIP,LoadBalancer 通常还会分配 NodePort;但能直连 Pod 的负载均衡实现可以关闭 NodePort 分配,所以不要记成无条件的包含关系。常规集群由 kube-proxy 配置 Service 转发规则,也有网络插件提供替代实现。具体访问方式见 Kubernetes Service 文档。

这几个对象怎么用 kubectl 操作、YAML 怎么写,留到 CKA 那个合集里深挖。

适用场景与运维代价

这套机制适合几类场景:Web 应用的弹性伸缩(流量涨跌自动加减 Pod)、批处理/定时任务的资源调度、作为微服务架构的运行底座(每个微服务是一组 Pod)、CI/CD 流水线里隔离出干净的构建环境。

和传统的手工运维相比,K8s 的核心价值可以归纳成几点:

  • 自动装箱:根据资源需求和限制自动决定 Pod 放在哪台机器,尽量提高整体资源利用率
  • 自我修复:按策略重启失败的容器;节点故障后,控制器可补建由工作负载管理的 Pod,调度到可用节点
  • 水平扩展:一条命令或一个策略就能让副本数随负载自动增减
  • 服务发现和负载均衡:Service 屏蔽了后端 Pod IP 变化的细节,调用方只认一个固定入口
  • 自动发布和回滚:新版本可以逐步替换旧版本,出问题一键回滚到上一个稳定版本
  • 跨云可移植:Kubernetes 的 API 是云厂商无关的标准接口,同一套 YAML 理论上可以在不同云、甚至自建机房之间迁移,不完全被某一家云厂商锁定

这些能力靠自己从零维护一套高可用的 Kubernetes 集群(尤其是 etcd 的高可用和版本升级),运维成本很高。下一章看阿里云怎么把这件事做成托管产品。

练习这一单元 →