多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从chroot到Kubernetes:容器隔离与编排技术演进及DevOps避坑指南

从chroot到Kubernetes:容器隔离与编排技术演进及DevOps避坑指南 简介这份文档面向希望深入理解容器与Kubernetes底层逻辑的开发者、运维人员及架构学习者从开发过程、应用架构、部署打包三条主线梳理容器技术的发展脉络帮助读者弄清容器真正解决什么问题、在技术演进中处于何种历史定位。资源包内含1个docx文档约311KB以图文结合的方式展开叙述便于通读与查阅。内容从瀑布式、敏捷式到DevOps的开发过程演变切入分析容器在各阶段扮演的角色再延伸至单体架构、多层架构与微服务架构的变迁说明容器为何成为微服务的理想载体最后落到Dockerfile标准化构建与Kubernetes编排管理讲清容器化打包如何解决版本依赖与环境一致性问题。目前已有241人学习适合作为理解容器技术来龙去脉、建立知识框架的入门与进阶参考。1. 容器这二十年从 chroot 到 Kubernetes一条被反复重写的路2006 年前后Google 内部已经在用 cgroups 做进程组的资源限制那时候没人管它叫「容器」。真正让这个词出圈的是 2013 年 Docker 把镜像、仓库、运行时打包成一套开发者能上手的工具链。再往后 Kubernetes 在 2015 年开源容器编排从「自己写脚本」变成「声明式 API」。这条线看起来是工具迭代本质是隔离边界和交付契约两件事被反复重写。今天你打开任何一个 Java 微服务项目大概率会看到 Dockerfile、docker-compose.yml、k8s 的 deployment.yaml 三件套。容器已经不是「要不要用」的问题而是「怎么用得不翻车」的问题。这篇笔记按时间线拆开每一代容器解决了什么、留下了什么坑、今天做 DevOps 落地时哪些历史包袱还在影响你。适合正在把单体拆微服务、或者第一次搭 Kubernetes 集群的工程师。2. 从 chroot 到 Docker隔离技术是怎么一层层堆出来的2.1 chroot、namespace、cgroups 各自解决什么问题容器不是一项技术是三项 Linux 内核能力的组合。理解这一点后面所有「容器里为什么看不到宿主机进程」「为什么内存限制不生效」的问题都能自己推出来。chroot1979Version 7 Unix只改根目录。它把进程看到的/换成另一个目录但进程仍然共享网络、PID、用户。所以 chroot 逃逸是经典问题——只要进程有 root 权限就能chroot回真实根目录。它解决的是「文件系统视图」不是隔离。namespace2002 起Linux 2.4.19 引入 mount namespace解决「看到什么」。每个 namespace 类型隔离一类全局资源namespace隔离内容内核版本Mount挂载点2.4.19UTShostname/domain2.6.19IPC信号量、消息队列2.6.19PID进程 ID 空间2.6.24Network网卡、路由、端口2.6.29User用户/组 ID 映射3.8Cgroupcgroup 根目录视图4.6cgroups2007Linux 2.6.24解决「能用多少」。CPU、内存、IO、PID 数量都能限。注意 cgroups v1 和 v2 的差异v1 每种资源一个层级v2 统一成单一层级树。Kubernetes 1.25 之后默认走 v2这也是为什么老教程里的--cgroup-driversystemd参数在新集群上行为不一样。三者叠加才是「容器」。Docker 早期用 libcontainer后来拆出 runc本质就是「调这三样内核能力 打包镜像」。2.2 手写一个最小容器不用 Docker 也能跑起来想真正理解容器最快的办法是绕开 Docker用unshare和chroot手动拼一个。下面这段在 Ubuntu 22.04、内核 5.15 上验证过。# 准备一个最小根文件系统用 busybox 静态编译版最省事 mkdir -p /tmp/miniroot/{bin,proc,sys,dev} cp /bin/busybox /tmp/miniroot/bin/ cd /tmp/miniroot ./bin/busybox --install -s ./bin # 用 unshare 创建新的 PID/Mount/UTS/IPC namespace # --fork 让 unshare 自己 fork--mount-proc 自动挂载 /proc sudo unshare --pid --mount --uts --ipc --fork --mount-proc \ chroot /tmp/miniroot /bin/sh # 进入后验证hostname 是独立的ps 只看到自己 hostname mini-container hostname # 输出 mini-container宿主机不受影响 ps -ef # 只有 sh 和 ps 两个进程逻辑说明unshare负责创建 namespace--fork是关键——不 fork 的话 PID namespace 里第一个进程还是当前 shellps会看到宿主机进程。--mount-proc让新 PID namespace 里的/proc正确反映隔离后的进程树否则ps读到的还是宿主机的。参数说明--pid隔离进程号--mount隔离挂载点--uts隔离 hostname--ipc隔离进程间通信。没加--net是因为手动配 veth 网卡比较绕Docker 帮你做了这部分。想加资源限制在unshare外面套systemd-run --scope -p MemoryMax128M这就是 cgroups 的入口。跑通这个你就明白 Docker 的docker run背后大概做了哪些 syscall。后面遇到「容器里 PID 是 1 的进程为什么不能随便 kill」这类问题答案就在 PID namespace 的 init 语义里。2.3 Docker 做对了什么镜像分层与 OCI 标准内核能力 2008 年就齐了为什么容器 2013 年才火因为 Docker 解决了交付问题。镜像分层UnionFS/AUFS/OverlayFS让「基础镜像 应用层」可以复用拉取时只传增量。Dockerfile 的每条指令生成一层层是只读的容器启动时在最上面加一个可写层。这个设计带来两个后果一是镜像层数太多会拖慢启动二是可写层的数据随容器删除而消失——这就是为什么必须挂 volume。2015 年 OCIOpen Container Initiative成立把镜像格式image-spec和运行时runtime-spec标准化。今天 containerd、CRI-O、Podman 都能跑同一份镜像Docker 只是其中一个实现。Kubernetes 1.24 移除 dockershim 就是这个标准化的结果——kubelet 直接通过 CRI 调 containerd不再经过 Docker daemon。# 看镜像分层理解为什么你的镜像 2GB docker history --no-trunc myapp:latest # 用 dive 工具逐层看哪些文件被加进来 docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock \ wagoodman/dive:latest myapp:latest常见做法是把RUN apt-get install和RUN apt-get clean写在同一层否则清理动作在下一层前一层的大小照样算进镜像。这是镜像瘦身最容易被忽略的一条。3. 编排时代Kubernetes 把容器从「能跑」推到「能管」3.1 为什么单机 Docker 撑不住微服务一台机器跑十几个容器没问题但微服务架构下你会遇到容器挂了谁重启、扩容时新容器调度到哪台、服务之间怎么发现、滚动更新怎么保证不中断。这些是编排问题Docker Compose 只能解决单机Swarm 没打赢最后 Kubernetes 成了事实标准。Kubernetes 的核心抽象是声明式你写 YAML 描述「我要 3 个副本、镜像版本 v2、暴露 80 端口」控制器循环对比期望状态和实际状态差多少补多少。这跟 Docker 的「我执行 run 命令」是两种思路。声明式的好处是自愈和幂等代价是调试时你得理解控制器在干什么——「为什么 Pod 一直 Pending」这类问题答案往往在调度器或 PVC 绑定逻辑里不在你的 YAML 表面。3.2 一个能跑的最小 Deployment从 YAML 到 Service下面这份 YAML 在 Kubernetes 1.26 上验证过包含 Deployment、Service、探针三个最常被写错的点。apiVersion: apps/v1 kind: Deployment metadata: name: demo-api spec: replicas: 3 selector: matchLabels: app: demo-api template: metadata: labels: app: demo-api spec: containers: - name: api image: demo-api:v1 ports: - containerPort: 8080 resources: requests: # 调度依据必须设 cpu: 100m memory: 128Mi limits: # 上限超了 OOMKilled cpu: 500m memory: 512Mi readinessProbe: # 就绪探针没过不接流量 httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: # 存活探针没过重启容器 httpGet: path: /healthz port: 8080 initialDelaySeconds: 15 periodSeconds: 20 --- apiVersion: v1 kind: Service metadata: name: demo-api-svc spec: selector: app: demo-api ports: - port: 80 targetPort: 8080 type: ClusterIP逻辑说明requests决定调度到哪台节点limits决定 cgroup 上限。CPU 超 limits 会被 throttle不杀进程内存超 limits 直接 OOMKilled。readinessProbe 和 livenessProbe 的区别是新手最容易翻车的地方——readiness 没过只是不接流量liveness 没过会重启。把 liveness 的initialDelaySeconds设太短应用还没启动完就被反复重启日志里看到的就是 CrashLoopBackOff。参数说明cpu: 100m是 0.1 核memory: 128Mi是 128 MiB。生产环境 requests 和 limits 不要设成一样除非你确定应用内存曲线平稳。periodSeconds和timeoutSeconds要配合应用启动时间调Java 应用冷启动 30 秒很常见initialDelaySeconds至少给 20。# 部署并观察 kubectl apply -f demo-api.yaml kubectl get pods -w # 看 Pod 从 Pending 到 Running kubectl describe pod pod-name # Pending 时看 Events 段 kubectl logs pod-name --previous # 容器重启后看上一次的日志kubectl describe的 Events 段是排查第一现场调度失败、镜像拉取失败、探针失败都会写在这里。养成先看 Events 再看日志的习惯能省一半时间。3.3 微服务拆分与容器粒度的对应关系微服务架构图里画的服务边界落到 Kubernetes 上就是 Deployment 的边界。一个常见误区是按「一个进程一个容器」拆得太细结果 20 个微服务对应 20 个 Deployment每个都要配 Service、ConfigMap、HPA运维成本爆炸。我一般按团队边界 数据边界来定容器粒度同一个团队维护、共享同一个数据库 schema 的模块先放一个 Deployment 里用多容器 Pod 或者干脆一个进程多模块。等团队和流量都涨起来再拆。微服务拆分不是越细越好拆分成本包括网络调用、分布式事务、链路追踪这些在单体里是不存在的。容器资源隔离在这里有个实际影响如果两个微服务放同一个 Pod它们共享 network namespacelocalhost能互通但 CPU/内存 limits 是各自独立的。想省资源可以放一起想故障隔离就分开。这个取舍没有标准答案取决于你的故障域要求。4. 避坑与排查容器落地时最常翻车的 5 个场景4.1 镜像越做越大拉取慢到超时现象kubectl describe pod显示ImagePullBackOff或者拉取耗时几分钟。docker history看到镜像 2GB。原因基础镜像选了ubuntu:latest而不是alpine或distrolessRUN apt-get update apt-get install和RUN rm -rf /var/lib/apt/lists/*分成两层清理不生效构建时把.git、node_modules、测试文件都 COPY 进去了。解决用多阶段构建构建阶段用完整镜像运行阶段只 COPY 产物。加.dockerignore排除无关文件。基础镜像优先gcr.io/distroless或alpineJava 应用可以用eclipse-temurin:17-jre-alpine。改完镜像通常能从 2GB 降到 200MB 以内。4.2 容器内存超限被 OOMKilled但监控看不到峰值现象Pod 反复重启kubectl describe显示Last State: Terminated, Reason: OOMKilled。但应用自己的监控面板内存才用了 60%。原因JVM 在容器里默认按宿主机内存算堆大小-Xmx没设或者设得比 limits 大。Java 8u191 之前不认 cgroup limits之后需要-XX:UseContainerSupport默认开。另外堆外内存Metaspace、直接内存、线程栈不算在堆里但算在 cgroup 内存里。解决显式设-XX:MaxRAMPercentage75.0让 JVM 按容器 limits 的 75% 算堆。limits 设 512Mi 的话堆大概 384Mi剩下留给堆外。用kubectl top pod看实际用量配合-XX:NativeMemoryTrackingsummary排查堆外泄漏。4.3 时区不对日志时间差 8 小时现象容器里date显示 UTC应用日志时间戳跟宿主机对不上排查问题时时间线错乱。原因基础镜像默认 UTC容器不继承宿主机时区。/etc/localtime在镜像里是 UTC 版本。解决Dockerfile 里加ENV TZAsia/Shanghai并RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime。或者在 Pod spec 里挂载宿主机的/etc/localtime。注意 Alpine 镜像要先apk add tzdata否则/usr/share/zoneinfo是空的。4.4 Service 访问不通但 Pod 本身正常现象kubectl exec进 Pod 能 curl 通自己但从另一个 Pod 访问 Service 的 ClusterIP 超时。原因Service 的selector和 Pod 的labels对不上Endpoints 为空。或者targetPort写错指向了容器没监听的端口。也可能是 NetworkPolicy 挡了。解决kubectl get endpoints svc-name看有没有后端。为空就是 selector 问题。kubectl get svc svc-name -o yaml核对 targetPort 和 containerPort。NetworkPolicy 用kubectl describe networkpolicy看规则。DNS 问题用nslookup svc-name.namespace.svc.cluster.local验证。4.5 容器里改的文件重启就没了现象进容器改了配置文件docker restart或 Pod 重建后改动消失。原因容器的可写层随容器生命周期存在删除即丢。Kubernetes 里 Pod 重建会创建新容器可写层是全新的。解决配置文件用 ConfigMap 挂载敏感信息用 Secret持久数据用 PVC。改 ConfigMap 后需要kubectl rollout restart deployment让 Pod 重建才能生效除非用了 reloader 这类工具。记住一个原则容器里除了临时文件什么都不该写。5. 进阶用 kind 在本地复现一套多节点集群验证你的 YAML生产集群不好随便试错本地用 kindKubernetes in Docker起多节点集群是最省事的验证方式。它把每个节点跑成一个 Docker 容器支持多节点、端口映射、本地镜像加载。# 安装 kindmacOS/Linux brew install kind # 或 go install sigs.k8s.io/kindlatest # 写一个三节点集群配置1 control-plane 2 worker cat kind-config.yaml EOF kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane extraPortMappings: - containerPort: 30080 # 映射到宿主机方便访问 NodePort hostPort: 30080 - role: worker - role: worker EOF # 创建集群指定 Kubernetes 版本 kind create cluster --name demo --config kind-config.yaml \ --image kindest/node:v1.26.0 # 把本地构建的镜像直接加载进集群不用推仓库 docker build -t demo-api:v1 . kind load docker-image demo-api:v1 --name demo # 部署并验证 kubectl apply -f demo-api.yaml kubectl get nodes -o wide kubectl get pods -o wide # 看 Pod 分布到两个 worker 上逻辑说明extraPortMappings把集群内 NodePort 映射到宿主机本地浏览器能直接访问。kind load docker-image解决本地镜像拉取问题——kind 节点是 Docker 容器默认拉不到你本地 build 的镜像必须先 load 进去。--image指定节点镜像版本对应 Kubernetes 版本v1.26.0 的节点镜像就是kindest/node:v1.26.0。参数说明--name给集群命名多集群共存时用kubectl config use-context kind-demo切换。kind delete cluster --name demo清理。节点数按需加但注意每个节点是一个 Docker 容器内存占用不小8GB 内存的机器建议不超过 3 个节点。验证 YAML 时重点看三件事Pod 是否调度到不同节点验证亲和性和资源 requests、Service 是否能跨节点访问验证 kube-proxy 和网络插件、滚动更新是否平滑kubectl rollout status deployment/demo-api观察。kind 默认用 kindnet CNI够用但不支持 NetworkPolicy要测网络策略得换 Calico。我自己的习惯是任何要上生产的 YAML先在 kind 里跑一遍kubectl apply --dry-runserver再实际部署看 Events。本地翻车比生产翻车便宜太多。这套流程跑熟之后从写完 YAML 到验证通过大概 5 分钟比直接改生产配置再回滚快得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表