1. Linux 内核三大件

Namespace(环境隔离)

Namespace 负责“视觉隔离”,让容器进程觉得自己拥有独立的资源栈。

  • PID Namespace:容器内的进程 ID 从 1 开始,看不到宿主机的其他进程。

  • Net Namespace:拥有独立的网络设备、IP 地址、端口范围。

  • Mount Namespace:挂载独立的文件系统,容器内的 /etc/usr 与宿主机隔离。

  • UTS/IPC/User Namespace:分别隔离主机名、进程间通信和用户权限。

Cgroups (Control Groups,资源限制)

防止某个容器耗尽宿主机的全部资源。

  • CPU 限制:控制容器占用的 CPU 时间片比例。

  • 内存限制:限制容器使用的内存上限(OOM 机制)。

  • I/O 限制:控制磁盘读写带宽。

UnionFS (联合文件系统,镜像存储)

容器镜像(Image)之所以能做到分层复用和秒级启动,全靠 Copy-on-Write (CoW) 机制。

  • 分层存储:镜像是由多个只读层叠在一起的。当你修改容器文件时,它会把底层文件复制到最顶部的“可读写层”进行修改。

  • 存储驱动:如 Overlay2、AUFS。

2. 容器 VS 虚拟机

核心区别在于:是否拥有独立的内核。

特性

容器 (Container)

虚拟机 (VM)

内核

共享宿主机内核

拥有独立 Guest OS 内核

启动速度

秒级(进程启动)

分钟级(系统引导)

资源损耗

极低(几乎无额外开销)

较高(Hypervisor 及系统占用)

隔离性

逻辑隔离(安全性相对稍弱)

硬件级强隔离

密度

单机可运行成百上千个

单机运行几十个已是极限

3. Docker

架构

Docker 并非一个单一的二进制程序,它是一个典型的 C/S 架构 系统:

  • Docker Client:我们常用的 docker 命令行工具。它不负责创建容器,只负责把你的指令(如 run, build)翻译成 REST API 调用。

  • Docker Daemon (dockerd):守护进程,运行在宿主机上。它监听 API 请求,管理镜像、容器、网络和磁盘卷。

  • containerd & runc:这是更底层的组件。Docker 为了符合 OCI(开放容器标准),将容器运行时拆分了出来。runc 负责真正调用内核接口创建容器,containerd 负责管理容器的生命周期。

为什么 Docker 比 VM 快

这源于其独特的文件系统和进程隔离。

联合文件系统:

Docker 镜像是由一系列只读层 (Read-only Layers) 组成的。

  • 分层设计:如果你有 10 个 Java 应用镜像,它们都基于 openjdk:17,那么在磁盘上,这份基础镜像只会被存储一份。

  • 写时复制 (Copy-on-Write):当你启动容器时,Docker 会在镜像层顶部加一个极薄的可读写层 (Container Layer)。所有对文件的修改都发生在这里,原始镜像毫发无伤。

进程隔离(共享内核与 Cgroups 限制):

由于 Docker 共享宿主机内核,它不需要像 VM 那样加载复杂的 BIOS、引导程序和内核镜像。它只是宿主机上的一个进程,通过 Cgroups 强制规定了:

  • 这个进程组最多只能用 512MB 内存。

  • 这个进程组最多占用 10% 的 CPU 时间片。

三大核心对象

镜像 (Image) —— 静态的“类”

镜像是一个包含了运行程序所需的所有依赖(库、环境变量、配置)的压缩包。它是不可变的。

容器 (Container) —— 动态的“实例”

容器是镜像运行时的表现。一个镜像可以启动多个容器,每个容器相互隔离。

仓库 (Registry) —— “代码托管所”

类似于 Maven 仓库或 GitHub,用于存放和分发镜像。

网络模式

在单机环境下,Docker 提供四种主要网络模式,这是后端排查网络连接问题的关键:

  1. Bridge (默认):每个容器分配一个虚拟 IP,通过宿主机的 docker0 网桥进行通信,并利用 NAT(网络地址转换)访问外网。

  2. Host:容器直接共享宿主机的网络栈(没有自己的 IP)。性能最高,但端口容易冲突。

  3. None:容器只有本地回环网卡(lo),不联网。适用于高安全要求的计算任务。

  4. Container (联合网络):新容器共享一个已存在容器的网络命名空间。K8s 的 Pod 内部多个容器通信就是基于这种原理。

如何合理使用

  • 减少镜像层数:每一个 RUN, COPY, ADD 指令都会产生一层。尽量用 && 合并命令。

  • 使用 .dockerignore:防止把 target/ 目录或 .git 等无用大文件打包进镜像,减少镜像体积。

  • 多阶段构建 (Multi-stage Builds):

    • 阶段一:用带有 Maven 的镜像编译代码。

    • 阶段二:只把编译好的 .jar 拷入只有 JRE 的精简镜像。

    • 结果:镜像体积可能从 800MB 缩减到 100MB。

  • 信号处理:容器内的进程 PID 必须为 1 才能正确接收 SIGTERM 信号。如果使用 sh -c "java -jar ..." 启动,Java 进程将无法优雅停机。