跳转至

容器底层原理

学习目标

学完本章后,学习者应该能够:

  1. 理解容器不是虚拟机,而是 Linux 隔离与限制机制的组合。
  2. 解释 namespace、cgroups、OverlayFS、capabilities、seccomp 的作用。
  3. 理解容器资源限制如何影响 Go 服务。
  4. 能把容器底层知识连接到 Kubernetes 排障。

容器到底是什么

容器本质上是宿主机上的进程,只是这个进程被隔离、限制,并使用了特定文件系统视图。

简化理解:

容器 = 进程 + namespace 隔离 + cgroups 限制 + 镜像文件系统 + 安全策略

它比虚拟机轻量,因为容器共享宿主机内核。

namespace

namespace 用于隔离进程看到的系统资源。

常见 namespace:

namespace 隔离内容
PID 进程 ID 视图
NET 网络设备、IP、端口
MNT 挂载点和文件系统视图
UTS hostname
IPC 进程间通信资源
USER 用户和用户组映射

容器内看到的 PID 1,通常只是该 PID namespace 中的一号进程。

cgroups

cgroups 用于限制和统计资源:

  • CPU。
  • 内存。
  • IO。
  • 进程数量。

Kubernetes 的 CPU limit、memory limit 底层依赖 cgroups。容器超过 memory limit 时,可能被 OOMKilled。

OverlayFS

容器镜像由多层文件系统组成。OverlayFS 把只读镜像层和可写容器层组合成一个统一视图。

这解释了:

  • 镜像为什么可以复用层。
  • 容器写文件为什么写在可写层。
  • 容器删除后,容器层数据通常也会消失。
  • 日志和持久化数据不应该随便写在容器本地层。

capabilities 与 seccomp

Linux capabilities 把 root 权限拆成更细粒度的能力。容器可以只保留需要的能力。

seccomp 可以限制进程能使用哪些系统调用,减少攻击面。

生产容器应该尽量:

  • 使用非 root 用户运行。
  • 删除不必要 capabilities。
  • 避免特权容器。
  • 使用只读根文件系统时谨慎设计临时目录。

Go 后端实际应用例子

例子一:容器内处理 SIGTERM

Kubernetes 停止 Pod 时会发送 SIGTERM。Go 服务应该优雅退出:

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()

go func() {
    <-ctx.Done()
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    _ = server.Shutdown(shutdownCtx)
}()

这能减少发布或扩缩容时的请求中断。

例子二:尊重容器内存限制

Go 新版本可以配合 GOMEMLIMIT 控制运行时目标内存:

$env:GOMEMLIMIT="512MiB"
go run .\main.go
GOMEMLIMIT=512MiB go run ./main.go

在 Kubernetes 中,可以通过环境变量把内存限制传递给 Go runtime,减少 GC 与 OOM 风险。

常见误区

  • 误区一:容器是小型虚拟机。

容器共享宿主机内核,不是完整虚拟机。内核参数、文件系统、网络和安全边界都要用容器视角理解。

  • 误区二:容器内写文件就是持久化。

容器可写层随容器生命周期变化。需要持久化的数据应使用 volume、对象存储或外部数据库。

  • 误区三:容器里进程退出无所谓。

容器主进程退出,容器就会停止。PID 1 的信号处理和子进程回收也需要注意。

线上问题案例

某 Go 服务在 Kubernetes 中没有处理 SIGTERM。滚动发布时 Pod 收到终止信号后直接退出,正在处理的请求被中断,调用方出现大量 502。

修复方式是实现 graceful shutdown,配置 readiness probe、preStop hook 和合理的 terminationGracePeriodSeconds

实战任务

为 Go HTTP 服务设计容器友好的运行方案:

  1. 非 root 用户运行。
  2. 正确处理 SIGTERM
  3. 日志输出到 stdout。
  4. 配置 CPU 和内存限制。
  5. 思考本地临时文件写到哪里。
参考答案

Dockerfile 中可以创建非 root 用户,并使用 USER 切换;服务启动后监听 SIGTERM 并调用 http.Server.Shutdown;日志输出到 stdout 交给容器运行时采集;Kubernetes 中配置 requests 和 limits,并根据内存限制设置 GOMEMLIMIT

临时文件可以写到明确挂载的临时目录,并设置容量和清理策略。不能把容器可写层当作长期持久化存储。

面试题

1. 容器和虚拟机有什么区别?

参考答案

虚拟机通常有独立操作系统内核,隔离边界更重。容器共享宿主机内核,通过 namespace、cgroups、文件系统和安全策略实现隔离与限制。

容器更轻量、启动快、镜像分层复用好,但安全边界和内核依赖需要特别关注。

2. namespace 和 cgroups 分别解决什么问题?

参考答案

namespace 解决“看见什么”的问题,例如进程、网络、挂载点、hostname 的隔离。cgroups 解决“能用多少”的问题,例如 CPU、内存、IO、进程数量的限制与统计。

容器隔离通常依赖 namespace,资源限制通常依赖 cgroups。

3. 为什么容器中的 Go 服务要处理 SIGTERM?

参考答案

Kubernetes 删除或更新 Pod 时,会先发送 SIGTERM,给进程机会优雅退出。如果服务不处理信号,可能直接退出,导致正在处理的请求中断。

正确做法是停止接收新请求,等待已有请求完成,并在超时时间内释放连接、刷新必要状态,然后退出。