从会用Docker到懂设计:隔离、隐藏与行为限制
发布时间:2026/9/3 12:36:44来源:尧图网络
写在文章开头这是笔者在2022年左右写的一篇关于docker基本概念和实践的教程随着AI的出现开发者对于此类运维工具的学习和上手成本逐步降低真正稀缺的是正确地理解技术而理解它要弄明白三件事它为什么这样设计能解决什么边界和风险在哪而本文也将带读者把这篇旧文重过一遍从软件架构痛点引入docker技术实现内幕结合一次完整的安装与配置步骤让读者在“会用”之外更懂它背后的设计理念、局限与风险。SharkChili· 禅与计算机程序设计的艺术开源贡献mini-redis教学级 Redis 精简实现 · https://github.com/shark-ctrl/mini-redis关注公众号回复【加群】获得笔者联系方式加入技术交流群为什么需要dockerdocker解决了哪些问题开发者常遇到这样一件事本地的系统跑得好好的一部署到服务器就冒出各种环境、配置问题。于是人们想能不能把整个环境连同系统整体打包做到一次构建随处快速部署这也是计算机哲学一贯的主张——可复用。我们最先想到的方案可能是虚拟机它确实能把整套运行环境打包、迁移后直接部署。但缺点很明显——它把宿主机的整套guest内核、lib依赖库工具链以及应用层一并复制一个完整 web 系统动辄几十GB的磁盘空间启动要几十秒一台物理机跑三五个虚拟机就内存告急。于是就有了 docker从系统内核层面看docker容器共享宿主系统内核打包时只带必要的依赖和应用程序磁盘空间占用小启动亚秒级这也是目前环境统一打包部署的主流方案之一。相比虚拟机docker 还带来几个直观的好处快速拉取发送方把程序打包成镜像存到仓库码头其他用户直接拉取即可整个过程就像一只鲸鱼拖着货物来回运输。高效部署改完代码用 docker build 重建镜像、docker run 快速重新部署环境不用手工重装。统一管理使用docker之前各程序启动命令不一——例如 nginx 用nginx、tomcat 用./startup.sh而docker把启动/停止封装成统一命令docker start/stop 容器。安全隔离docker使用基于 OCI 运行时containerd 与 runc实现通过cgroup和namespace限制进程组所用的CPU、内存等资源。而且相对于虚拟机而言该技术创建的隔离空间更快、更高效容器间资源受限、互不干扰部署更安全。上文提到一个 OCI 的概念我们可以把 docker run 想象成一条流水线。当我们键入 docker run 时containerd 负责拉取镜像、看管容器生命周期底层 runc一个底层程序按一套“造容器”的流程按照给定的 cgroup、pivot_root、namespace 约束完成容器构建。链路如下这也回答了 docker run 是怎么把容器造出来的docker 三大核心概念镜像英文名叫image就是我们打包好的应用——一个可复用的文件集合运行文件、环境配置等也是构建容器的模板/原料。容器拿镜像当模板跑出来的一个带有隔离空间的运行实例也就是 docker 的最终产物。仓库我们需要的镜像肯定都保存在某个仓库中每次运输镜像都需要从这个仓库里查找结合我们上述的三大概念假设现在要快速构建一个 redis 容器对应流程为从仓库中拉取官方 redis 镜像结合我们的配置约束将镜像构建成一个隔离在宿主机的容器。从设计层面理解docker的思想隐藏我们都知道传统虚拟机通过完整拷贝原始宿主文件和内核资源给使用者一份完整的使用体验但这种重量级的拷贝操作使得虚拟机系统对于资源占用非常庞大从整体资源利用率的角度来看使用率也不高。设计容器第一个考虑的问题是如何隔离容器与宿主机的之间的文件系统即限定容器活动于给定的物理空间。针对此问题docker采用联合挂载 chroot/pivot_root 换根runc 实际用 pivot_root综合解决这个问题。我们不妨将这两个技术拆开来说先来说说chroot它是Linux内核系统调用这里我们以docker实现的语言go来查看这个函数从函数语义逻辑可以看出改函数逻辑本质将传入的path作为Linux chroot的入参将当前程序文件系统的根目录修改为path由此将进程的文件系统空间局限在我们分配的虚拟根目录funcChroot(pathstring)(errerror){var_p0*byte//解析入参path生成_p0_p0,errBytePtrFromString(path)//......//基于go语言的Syscall调用chroot并传入_p0即需要作为根的地址_,_,e1:Syscall(SYS_CHROOT,uintptr(unsafe.Pointer(_p0)),0,0)ife1!0{errerrnoErr(e1)}return}chroot 切换根目录的机理如下图所示隐藏文件隔离还不够如何让虚拟根文件夹看起来像宿主容器一样即具备/etc、/usr、/bin 这些目录呢答案就是我们上文所说的联合挂载技术docker通过分层技术将基础镜像系统根文件系统、依赖库、应用程序构建为一个个只读镜像。执行docker run的时候这些镜像通过联合挂载到类似于 /var/lib/docker/overlay2/…/merged目录从而构建为一个完整的容器期间对于所有写操作都通过cow技术实现例如修改镜像只读层里已有的文件如 /etc/nginx/nginx.conf、/etc/passwddocker 才通过 copy-up 把整个文件拷到可写层再改只读层没有的文件直接在可写层新建不发生 copy-up。而 /etc/hostname、/etc/hosts、/etc/resolv.conf 是 Docker 单独 bind-mount 进来的不走 OverlayFS。java程序生成写入日志时若只读层没有则直接在写层生成对应文件。如下图所示隔离我们通过chroot约束容器的操作路径但容器依然可以访问宿主资源例如键入ps依然可以访问docker外部容器进程所以我们需要更进一步的隔离手段将其操作空间进行约束。查阅docker官网我们看到这样一段话Docker is written in the Go programming language and takes advantage of several features of the Linux kernel to deliver its functionality. Docker uses a technology called namespaces to provide the isolated workspace called the container. When you run a container, Docker creates a set of namespaces for that container.翻译过来就是Docker是用Go编程语言编写的它利用了Linux内核的多种特性来实现其功能。Docker 使用一种名为namespaces的技术来创建隔离的工作区这个工作区就被称为容器。从底层视角来看真正创建容器进程的是 OCI 运行时 runc它先用 unshare 建新 namespace、setns 加入已有 namespace再用 clone 造出容器进程。docker 引擎只经 containerd 把任务交给 runc。intclone(int(*fn)(void*),void*stack,intflags,void*arg,...);这里我们着重介绍flags那些针对命名空间隔离的标志如下图所示docker在进行clone调用时通过如下标志位结合按位或进行hostname、进程、挂载点隔离CLONE_NEWUTS隔离 hostname容器以为它叫 my-container 而不是宿主机名CLONE_NEWPID隔离进程列表容器内从 PID 1 起看自己的进程树注意只对 clone 之后的子孙进程生效。CLONE_NEWNS独立 mount namespace——容器内挂载/卸载的文件树视图宿主机及它进程看不到、也不受影响不是“宿主机进程对容器不可见”那是 PID namespace 的职责。CLONE_NEWNET隔离网卡/IP/路由/端口对应后文 network namespace。CLONE_NEWIPC / CLONE_NEWUSER分别隔离信号量共享内存等进程间通信设施、以及容器内 root 对宿主机的用户映射。行为限制我们通过隐藏和隔离限制了容器的工作空间和执行范围但并没有对其资源进行限定所以在极端情况下容器依然可以无限制的消耗宿主资源所以就有了cgroup的概念docker通过mem_limit指明容器的上限,如下所示笔者将个人的java程序内存上限设置为4gbackend:container_name:blog-backend mem_limit:4g # 容器总内存上限 environment:JAVA_OPTS:--Xms2g-Xmx2g # 只限制Java堆-XX:MaxMetaspaceSize256m基于cgroup技术当资源达到上限后系统对其进行限流CPU超载CPU 被节流变慢、不杀内存超限则先触发内核回收回收不掉再由 OOM Killer 杀进程退出码 137。100行代码理解docker思路说明docker 的设计核心即联合挂载复用镜像chroot 隐藏宿主目录clone 调用隔离 namespacecgroup 约束容器资源说明本例为最小演示只落地 chroot 与 namespace联合挂载需要成型 rootfs、cgroup 需要内核 cgroup 接口均超出本例范围故不实现——上面四支柱这里只兑现两样先把期望收窄。从设计层面来说docker的设计本质是利用Linux内核函数从操作空间、操作权限、资源占用对其进行隔离我们不妨以一个最小化的例子来复刻学习docker对于资源隔离这一优秀的设计理念。我们例子也很简单即指定一个文件空间以本次为例则是/home/sharkchili/tmp作为我们本次容器目标工作空间通过chroot对宿主主机进行隐藏并通过clone调用限定可操作范围使之具备独立的hostname、进程、挂载目录。我们最终期望达到键入ls /只能看到 tmp 目录下的 rootfs 与预制文件夹dir1、dir2、dir3键入hostname看到自定义主机名说明 UTS 隔离生效键入ps只看到容器内进程PID 1 的/bin/sh与 ps 自身看不到宿主机进程说明 PID namespace 隔离生效前置准备因为要 chroot 换根而我们的“根”里没有 /bin/sh、/etc 等shell 连自己都起不来所以必须先在目标目录准备完整的rootfs以笔者为例即针对/home/sharkchili/tmp执行如下指令sudocurl-Ohttps://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-minirootfs-3.20.3-x86_64.tar.gzsudotar-xzfalpine-minirootfs-3.20.3-x86_64.tar.gzsudormalpine-minirootfs-*.tar.gz再建三个测试目录让“预制好的 dir1/dir2/dir3”名副其实mkdir -p /home/sharkchili/tmp/{dir1,dir2,dir3}完成安装后我们就可以得到完整的root目录功能落地基于上述的设计我们也就有了明确的思路对应容器配置的核心步骤为Go 程序监听用户输入参数例如传入/bin/sh时就创建一个隔离的子进程执行 shellclone 调用发生在父进程 run 中通过syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS隔离主机名、进程号视图和挂载表子进程在 child 中执行 chroot 将/home/sharkchili/tmp设置为根通过os.Chdir(/)切到新根并挂载/procmount -t proc proc /proc让ps只反映本 namespace最后 exec 替换当前进程镜像启动我们输入的命令最终代码如下所示当我们键入sudo go run main.go run /bin/sh 即希望创建一个隔离的容器执行shell时这段代码逻辑为步入 run 分支复用终端输入输出并通过 SysProcAttr 配置隔离的 namespace通过/proc/self/exe重新执行当前程序把参数换成child /bin/sh于是再次进入 main() 落到 child 分支main 分支步入 child 函数child 分支通过 chroot 切换根目录通过os.Chdir(/)切到新根并挂载/procmount -t proc proc /proc执行 shell 并启动funcmain(){switchos.Args[1]{caserun:run()casechild:child()default:panic(what?)}}funcrun(){fmt.Printf(Running %v as a container\n,os.Args[2:])//重新执行当前go程序,并加上child参数,例如:sudo go run main.go run /bin/sh//子进程收到的就是 child /bin/sh//由此走到main分支执行child逻辑cmd:exec.Command(/proc/self/exe,append([]string{child},os.Args[2:]...)...)//复用父进程终端输入和输出cmd.Stdinos.Stdin cmd.Stdoutos.Stdout cmd.Stderros.Stderr// 创建新的 UTS、PID、Mount namespace分别隔离主机名、进程号视图和挂载表cmd.SysProcAttrsyscall.SysProcAttr{Cloneflags:syscall.CLONE_NEWUTS|syscall.CLONE_NEWPID|syscall.CLONE_NEWNS,}//启动并等待子进程must(cmd.Run())}funcchild(){// 子进程 在新的namespace里运行fmt.Printf(Running %v in the child process as container\n,os.Args[2:])// 修改hostname验证是否与宿主机隔离must(syscall.Sethostname([]byte(sharkchili)))//将容器看到的根目录设置为 /home/sharkchili/tmpmust(syscall.Chroot(/home/sharkchili/tmp))//将工作目录切到根目录must(os.Chdir(/))// 在挂载namespace里重挂/proc,ps/top才能反映本namespace的进程(PID隔离可观测)os.MkdirAll(/proc,0755)// 保险:确保挂载点存在must(syscall.Mount(proc,/proc,proc,0,))// 执行我们输入的命令must(syscall.Exec(os.Args[2],os.Args[2:],os.Environ()))}// 判断是否存在异常,若有则直接panic终止funcmust(errerror){iferr!nil{panic(err)}}测试需要注意的是上述代码涉及Linux函数调用所以mac或者windows环境无法直接运行我们需要直接编译并打包到Linux环境才能验收。所以笔者将程序部署到个人服务器并执行如下指令sudogo run main.go run /bin/sh可以看到程序启动一个容器并执行shell查看hostname输出的确实是笔者设置的sharkchili查看根目录除了之前安装的rootfs以外剩下的都是笔者tmp目录预制的文件夹由此可知chroot隐藏生效实测结论容器内ps只列出容器进程PID 1 的 /bin/sh 与 ps 自身、看不到宿主机进程——说明 PID namespace 隔离生效。Running [/bin/sh] as a containerRunning [/bin/sh] in the child process as container/ # psPID USER TIME COMMAND1 root 0:00 /bin/sh6 root 0:00 psdocker安装和配置有了 AI对于这种固定的安装部署工作其实没有任何压力但本着完整性这里还是基于Ubuntu 24.04代号noble环境给出完整步骤。第一步清理可能冲突的旧包没装过也安全提示找不到包属正常sudoapt-getremove-ydockerdocker-engine docker.io containerd runc第二步装依赖并添加官方 GPG key用阿里云源sudoapt-getupdatesudoapt-getinstall-yca-certificatescurlsudoinstall-m0755-d/etc/apt/keyringssudocurl-fsSLhttps://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg-o/etc/apt/keyrings/docker.ascsudochmodar /etc/apt/keyrings/docker.asc第三步添加 apt 源代号 / 架构已按本机写死noble/amd64echodeb [archamd64 signed-by/etc/apt/keyrings/docker.asc] https://mirrors.aliyun.com/docker-ce/linux/ubuntu noble stable\|sudotee/etc/apt/sources.list.d/docker.list/dev/null第四步安装 Docker含 compose、buildx 插件sudoapt-getupdatesudoapt-getinstall-ydocker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin第五步把当前用户加入 docker 组之后免sudo用 dockersudousermod-aGdocker$USERnewgrpdocker# 立即生效或退出 SSH 重新登录第六步验证四条都成功即完成docker--versiondockercompose versionsudosystemctl statusdocker--no-pagerdockerrun hello-world第七步配镜像加速器必配国内直连registry-1.docker.io会被拒docker run hello-world会报connection refused。公共加速地址时效性强失效就搜“docker 镜像加速”换一个。sudotee/etc/docker/daemon.jsonEOF { registry-mirrors: [https://docker.m.daocloud.io] } EOFsudosystemctl restartdocker第八步开机自启——docker 服务本身不用配apt 装完已默认enabledsystemctl is-enabled docker返回enabled容器要单独配--restart unless-stopped机器重启后容器才自动拉起手动docker stop过的不拉。基于docker部署nginx代理服务完成 docker 基础环境配置安装之后我们就来演示两个比较常见的程序部署——先来说说 nginx。整体按“拉取镜像 → 启动验证 → 观察验收 → 进入容器基础操作”几步走。拉取镜像 pull 并验证从公共镜像仓库如 Docker Hub 或已配好的阿里云镜像源原网易蜂巢仓库已关停拉取 nginx 镜像dockerpull nginx完成后键入如下命令查看是否有nginx镜像dockerimages启动验证用-d后台启动并配端口映射-p 8080:80否则浏览器访问不到默认页dockerrun-d-p8080:80 nginx键入下面这个指令查看 nginx 是否运行成功dockerps可以看到 nginx 已经成功运行了。观察验收nginx 默认端口是80配端口映射-p 8080:80后在浏览器键入 ip 地址能看到下图就说明部署成功进入容器基础操作用docker ps找到容器 iddockerps|grepnginx拿着CONTAINER ID运行docker exec -it 容器ID bash如下所示dockerexec-it5a2438be1163bash可以看到我们就像进入一个新的操作系统一样操作的 nginx 容器。用ps -ef确认 nginx 是否在容器中运行容器内可能没有ps命令先装一下apt-getupdateapt-getinstallprocps也可以用which nginx看 nginx 位置最后exit退出whichnginx停止 nginx用docker ps找到容器 id用stop即可。dockerstop 5a2438be1163补充docker 网络模式nginx 端口映射的原理我们都知道docker的隔离性,网络也是个隔离性的一部分,Linux使用了命名空间来进行资源的隔离,比如pid namespace就是用来隔离进程的,mount namespace是用来隔离文件系统的,network namespace是用来隔离网络的.每一个network namespace都提供了一个独立的网络环境,包括网卡路由iptables规则等等,都是与其他network namespace隔离的.docker容器在默认情况下,一般会分配一个独立的network-namespace,也就是网络类型中的Bridge模式(可理解为虚拟机的 NAT模式)。因为 Bridge 模式用了独立的network namespace容器对宿主机不可直接访问所以需要用docker run -p把宿主机端口与容器端口做映射外部用户才能通过宿主机端口访问到容器。还有一种类型是Host 模式主机模式如果指定使用Host模式容器不会获得独立的network namespace而是和宿主机共用一个此时容器不会虚拟出自己的网卡、配置自己的 IP而是直接用宿主机的 IP 和端口——相当于直接在宿主机上使用网络。还有一种网络类型是None.也就是没有网络,这种情况docker将不会和外界的任何东西进行通讯。结合上文 bridgeNAT模式用实操体会端口映射固定映射用-p随机映射用-P。dockerrun-d-p8081:80 nginx把宿主8081映射到容器80访问宿主 8081 即到容器 nginxdocker也支持随机分配映射端口用-P即可。dockerrun-d-Pnginx关于更多docker常用命令因为有 AI这些指令笔者就不逐一演示了只列出常见命令读者可结合 AI 动手实践理解。目的命令说明卸载软件包yum remove docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin用安装时同款包名卸载彻底清理rm -rf /var/lib/docker删镜像、容器、卷及自定义配置搜索镜像docker search java搜远程仓库镜像NAME/DESCRIPTION/STARS/OFFICIAL下载镜像docker pull java拉取镜像列出镜像docker images看本地镜像REPOSITORY/TAG/IMAGE ID/CREATED/SIZE删除镜像docker rmi java删除本地镜像创建并启动容器docker run …最常用-d后台、-P随机端口、-p指定端口ip:hostPort:containerPort等四格式、--network指定网络bridge/host/none/container:…停止容器docker stop 容器id先docker ps找容器 id进入容器docker exec -it 容器id bash进容器操作退出容器exit退出容器回宿主机删除容器docker rm 容器id删除容器查看日志docker logs 容器id查看容器日志查看性能docker stats查看容器资源占用小结**小结**把 docker 串成一条线——从仓库取镜像应用与环境整体打包成一体用镜像跑出隔离的容器容器之间之所以相互隔离靠 chroot/联合挂载隐藏文件系统、namespace 隔离进程与网络、cgroup 约束资源这三件套配合 docker build/run/pull就能做到一次构建、随处快速部署。AI 时代纯手工一条条敲这些命令已不再是价值所在——固定、模式化的部署操作交给 AI 即可复现真正稀缺的是看懂它为什么这样设计。本文用“100 行代码”这个最小单元亲手把 chroot 隐藏宿主目录、namespace 隔离进程与网络这两个核心机制跑成可验证的实例再由它引申到 cgroup 资源限制。理解边界与风险才谈得上用对。带着先借 AI 摸清原理再用最小案例落地印证最后回到日常应用这条路径去读技术是这个时代更准确的用法。参考docker入门利用docker部署web应用:http://t.csdn.cn/PYAr8只需三步完美卸载Docker:https://blog.csdn.net/wangerrong/article/details/126750198解决docker启动报错Error starting daemon: SELinux is not supported with the overlay2 graph driver on this:https://blog.csdn.net/haoding205/article/details/82492263Docker 是怎么工作的:https://mp.weixin.qq.com/s/TA9oDm6_BqBShgVkAOVx3ALinux Clone 系统调用深入理解进程与线程的创建基石:https://geek-blogs.com/blog/linux-clone/Go语言100行代码实现 Docker:https://zhuanlan.zhihu.com/p/1933280608718656864
网站建设高端定制企业官网