ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker与K8s不是二选一:容器与编排的云原生实践指南

Docker与K8s不是二选一:容器与编排的云原生实践指南 先说结论Docker和K8s不是二选一的关系它们更像是云原生时代的一台发动机和一套底盘系统。发动机解决“东西怎么装进标准箱子”底盘系统解决“这一万个箱子怎么调度、怎么容灾、怎么对外提供服务”。这两年我见过太多新手上来就问“有了K8s还要Docker干嘛”每次听到这种问题我都得停下来把话说透这俩压根不在一个层面上搞懂它们的分工与配合你才算真正摸到了云原生的门槛。这篇内容适合几类人看刚接触容器、只会在单机跑docker run的新手已经在用Docker但准备搭K8s集群、还没理清概念的准运维以及想系统化梳理“为什么集群里有这么多控制器、为什么会话过期、为什么网络突然不通”的进阶用户。我会从原理讲到排障把这两年实操里踩过的坑一并摆出来争取你看完就能直接上手。1. 云原生双引擎Docker与K8s到底是什么关系1.1 从传统架构到云原生容器为什么先火起来在讲Docker和K8s之前得先聊一聊背景。早年间的主流IT架构是典型的IOE路线IBM的小型机、Oracle数据库、EMC存储整套系统以稳定著称代价是贵、耦合深、扩容难。后来虚拟化普及大家把物理机切成虚拟机用VMware或OpenStack管理资源灵活性上了一个台阶但虚拟机自带完整的操作系统启动慢、资源占用大一个实例动不动几个GB。容器在这时候出现得非常“顺理成章”。它复用宿主机的操作系统内核只把应用和它依赖的库、配置、运行环境打包在一起启动秒级、镜像几百MB甚至几十MB。Docker就是把这个打包能力做成了标准工具链镜像、仓库、容器、网络、数据卷一套流程下来同一份镜像在开发机、测试机、生产机上跑出来的行为是一致的。这正是“自主可控”的第一层含义——不是指某个厂商说了算而是你手里的东西是开源的、可审计的、能自持的出了问题你能自己排查到底。但Docker自身有个短板它擅长管理单机上的容器却管不了几十台机器上的上千个容器。谁来调度谁能保证某个容器挂了会自动拉起流量怎么分发到这些容器上服务间怎么发现彼此这时候K8s出场了。1.2 K8s解决的是容器之外的“编排”难题K8sKubernetes本质上是一个容器编排平台它不生产容器它只负责“管”。你告诉它“我要跑3个nginx副本”它会自动挑合适的节点启动容器维持这个期望状态一个副本挂了它会再拉一个节点宕机了它会把这节点上的Pod赶到别处副本多了它能帮你滚动更新到新版本过程中不中断服务。这套能力靠的是“声明式API”加控制器模式。你描述目标状态K8s负责收敛到目标状态。和传统“命令式”运维相比最大的区别是你不用再关心“第一步做什么、第二步做什么”只需要说清“最终要长成什么样”。系统自己想办法补差距。所以K8s的真正价值不是“跑容器”而是把容器变成一个可以弹性伸缩、自愈、服务发现、灰度发布的基础设施。这也是为什么云原生应用基本上都长在K8s之上。1.3 两者的边界什么归Docker管什么归K8s管很多人混淆Docker和K8s是因为它们经常出现在同一套流程里。我习惯用下面几个问题来区分谁负责“打包”Docker。Docker把应用Build成镜像推到镜像仓库。谁负责“运行”两者都参与。Docker直接运行容器K8s通过容器运行时现在默认是containerdDocker只是其中一种可选运行时间接运行容器。谁负责“调度”K8s。它会决定Pod落在哪台机器上Docker不会。谁负责“服务发现与负载均衡”K8s的Service对象Docker只有单机层面的端口映射和DNS。谁负责“自愈与扩缩容”K8sDocker没有这种能力。理解了这个边界后面学K8s就不会被Docker惯性带偏。像我以前习惯用docker run -p 8080:80 nginx暴露服务到了K8s里就千万别这么想——Pod的IP是动态的你得用Service把流量固定下来。提示K8s的容器运行时默认是containerd它同样具备OCI标准容器的能力镜像格式和Docker共通。所以你用docker build打出来的镜像在K8s里正常能拉。理解Docker没问题但你也不必在K8s节点上依赖完整的Docker全家桶。2. Docker从入门到排障环境、镜像与持久化2.1 环境安装Docker Desktop启动失败的三个典型坑先说Windows上最常见的Docker Desktop安装问题。很多人装完一启动就报“Docker Desktop failed to start”或者直接看到一段英文提示“Virtualization support not detected”。这种情况十有八九是硬件虚拟化没开。Docker Desktop在Windows上依赖WSL2或Hyper-V而WSL2需要BIOS里开启VT-x/AMD-V。排查顺序是这样的打开“任务管理器→性能→CPU”确认“虚拟化”一栏是“已启用”。如果是“已禁用”就得进BIOS找Intel Virtualization Technology或SVM Mode打开。确认Windows功能里勾选了“虚拟机平台”和“适用于Linux的Windows子系统”。如果之前装了老版本的Docker Toolbox先彻底卸载干净再装Docker Desktop。还有一个经常遇到的报错是Failed to connect to the docker api at npipe:////./pipe/docker-desktop-linux。这个看着吓人其实多半是Docker Desktop的Linux后端没起来。最快的处理方法是右键托盘图标选Restart等一两分钟再执行docker version。如果反复起不来优先检查WSL发行版是否损坏执行wsl --shutdown再重新启动Docker Desktop。Ubuntu上安装Docker就简单得多无非是配apt源、安装docker-ce、把当前用户加入docker组。这里有个细节加入docker组后docker ps有时还是提示permission denied必须退出当前终端重新登录才生效。还有一点很重要尽量不要用某个“一键安装脚本”从第三方站点下载时间久了容易踩供应链的坑。2.2 镜像构建Dockerfile的工程化写法镜像能不能打好直接影响交付效率和安全。我推荐所有项目都用多阶段构建不要图省事用一个大基础镜像从头装到尾。举个常见的例子。假设你要构建一个基于Node的静态站点# 第一阶段编译 FROM node:20-slim AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --registryhttps://registry.npmmirror.com COPY . . RUN npm run build # 第二阶段运行 FROM nginx:stable-alpine COPY --frombuilder /app/dist /usr/share/nginx/html EXPOSE 80 CMD [nginx, -g, daemon off;]这样最终镜像里只保留构建产物不包含node_modules、源码和编译工具链体积可能从1GB降到几十MB。镜像小不光省磁盘还意味着攻击面小扫描起来问题少。Dockerfile还有个容易踩的坑缓存失效。COPY这一行如果放在npm ci之前只要项目里任何文件变了缓存就会失效每次都得重新下依赖。正确写法是先拷贝package.json和锁文件跑完依赖安装再把全部源码拷进去这样源码变动不会导致重新装依赖。我还建议把“容器启动后再手动安装依赖”这种操作戒掉。有些人习惯先docker exec -it进容器再一条条装包容器一重启全没了。依赖管理应该写进Dockerfile重建镜像才是正道。我自己调试依赖问题时也是在Dockerfile里先放一个临时步骤跑通了再固化下来。2.3 容器网络与数据卷别让数据随容器一起消失新手最容易犯的错误是把容器当成一台小虚拟机往里塞各种状态。其实容器的设计哲学是“无状态”优先数据要落盘或者落到外部存储。单机Docker的网络模式主要有三种bridge、host、none。默认bridge模式下容器有自己的IP端口要显式映射到宿主机才能被外部访问。常见的-p 8080:80默认绑定的是tcp如果你想暴露udp比如跑DNS服务得两个协议都写-p 53:53/tcp -p 53:53/udp。数据持久化我一般用named volume不推荐直接bind mount宿主机目录到容器里跑生产除非你有明确的文件权限管理需求。原因是bind mount有时会把宿主机目录的权限inherit给容器导致容器内用户没权限读写。签名volume则更可控。举一个保存MySQL数据的例子docker volume create mysql_data docker run -d \ --name mysql8 \ -e MYSQL_ROOT_PASSWORDyour_password \ -v mysql_data:/var/lib/mysql \ -p 3306:3306 \ mysql:8.0如果你不建volume直接-v /some/path:/var/lib/mysql得先确保宿主机目录存在并且属主是uid 999MySQL官方镜像默认的用户否则MySQL容器起不来或者报权限错误。这就是我为什么更推荐named volume的原因——Docker帮你管理目录属主少折腾。2.4 compose实战一条命令拉起MySQL 8.0与Redis主从单机多容器编排docker compose是首选。它用YAML文件声明服务然后docker compose up -d一把拉起比一堆docker run命令好维护得多。下面是一个比较典型的组合MySQL 8.0 Redis主从。services: mysql: image: mysql:8.0 container_name: mysql8 restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: appdb ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 3s retries: 10 redis-master: image: redis:7 container_name: redis-master restart: unless-stopped command: [redis-server, --appendonly, yes, --requirepass, redispass] ports: - 6379:6379 redis-slave: image: redis:7 container_name: redis-slave restart: unless-stopped depends_on: - redis-master command: [redis-server, --slaveof, redis-master, 6379, --masterauth, redispass, --requirepass, redispass] ports: - 6380:6379 volumes: mysql_data:注意Redis副本的--slaveof参数后面要写服务名redis-master而不是localhost因为compose服务间通过服务名互相解析。另外如果Master设置了requirepassSlave必须配masterauth否则主从同步会一直卡在认证失败。我也踩过这个坑当时只配了requirepass没配masterauth日志里刷MASTER - REPLICA sync started但一直连不上后来才发现少了这一行。MySQL这里为什么用healthcheck因为compose的depends_on只保证容器创建顺序不保证服务真的可用。如果你有个后端应用要连数据库最好等healthcheck通过再启动否则后端会反复重试数据库连接体验很差。这就是“依赖管理”在基础设施层面最常见的体现。3. K8s核心对象与控制器机制从Deployment到Operator3.1 Pod、Deployment、Service最常打交道的三件套进入K8s之后第一要务是忘掉“容器”这个概念改成“Pod”。Pod是K8s最小的调度单元里面可以有一个或多个容器共享网络命名空间和存储卷。不要把Pod想成一台机器它更像是一个“进程组”。Deployment负责管理一组无状态Pod它通过ReplicaSet保证副本数量。你写一份Deployment里面描述用什么镜像、启动几个副本、暴露哪个端口然后就交给它维护。Deployment的滚动更新、回滚、副本扩缩容都是日常高频操作。有一个细节值得记住Deployment管理Pod副本时Pod的名字是随机的类似nginx-7d8b9cbb5c-abcde。如果你用kubectl exec -it nginx-xxx /bin/bash去登录某个Pod没问题但要留心Pod漂移后名字会变。想要固定访问入口就得用Service。Service是K8s提供稳定访问入口的抽象。它有一组Label Selector把后端Pod聚合成一个虚拟IP也就是ClusterIP。集群内部访问Service名就能负载均衡到Pod上。如果要从集群外部访问有两种常规方案NodePort在每台节点上开一个端口比如30080外部访问任意节点的30080即可。LoadBalancer在公有云或负载均衡插件上创建一个外部LB。ExternalIPs给Service指定一个固定的外部IP让它直接在该IP上监听。ExternalIPs这个字段其实挺少人提但它适合一些裸金属或自建机房场景。我当时在裸金属环境试过直接把Service的externalIPs写成宿主机IP流量能命中省去再装一层负载均衡的麻烦。不过生产环境还是建议用MetalLB这类方案毕竟直接写死IP不利于IP变化。3.2 控制器模式K8s维护期望状态的底层逻辑我一直觉得理解控制器模式才算摸到K8s的命门。Etcd里存的是期望状态各控制器通过API Server监听实际状态一旦有差异就开始“调谐”也就是“让实际状态趋近期望状态”。拿Deployment举例。你提交了一份replicas: 3的声明DeploymentController看到当前只有2个Pod就创建一个ReplicaSet并补齐副本。Pod挂了ReplicaSet Controller再创建一个新Pod。这个过程没有一条“if-else”命令链而是持续地对比和修正。Operator则是控制器模式的进阶玩法。它把某个业务领域的运维知识写进代码让程序自动完成部署、备份、升级、扩缩容。最典型的案例是数据库Operator比如CrunchyData的PostgreSQL Operator、社区里的MySQL Operator。过去DBA要手工做的主从切换、数据备份Operator可以定时巡检并自动处理。自己写Operator确实有门槛但理解它的价值很简单当一个应用的状态管理规则极其复杂Deployment这种通用控制器无法表达时你需要一个“懂这个应用”的专用控制器。Prometheus Operator就是典型的例子它不止管一个Pod还能根据ServiceMonitor自动发现监控目标。3.3 常用命令备忘get、describe、logs、exec这样用K8s的命令体系虽然庞杂但日常真正高频的其实就十几个。我把最常用的整理成如下速查表目标命令备注查看Podkubectl get pods -o wide-o wide显示节点和IP查看详情kubectl describe pod name排障第一工具查看Pod完整YAMLkubectl get pod name -o yaml看status.conditions查看日志kubectl logs -f name容器多时加-c container进入容器kubectl exec -it name -- /bin/sh没有/bin/bash试试sh创建资源kubectl apply -f file.yaml推荐删除资源kubectl delete deploy name删Service同理看所有命名空间kubectl get ns不知道对象在哪时先看这个动态观察kubectl get pods -w看滚动更新状态很管用端口转发kubectl port-forward svc/xxx 8080:80本地调试神器用describe查一个Pod卡在ContainerCreating的案例你会看到Events里通常会写拉镜像失败、存储卷挂载失败、或探针探测超时的具体原因。信息量远大于一段日志。我自己排查问题第一步永远是describe接日志最后才进容器。还有点要补充K8s集群里跨命名空间的对象命令里都要加-n namespace。不加的话默认查询default命名空间查不到资源很正常。3.4 三台Master的高可用kube-vip和etcd的取舍生产环境说要“高可用K8s集群”通常指把Master节点做成三台三台之间通过etcd形成共识。为什么是奇数台因为etcd用的是Raft协议选主需要过半票数。三台能容忍一台挂掉四台虽然也能容忍一台但一旦只剩三台和集群有五台、四台在容错能力上没有任何本质区别所以奇数台最合适。三台Master架构里最麻烦的是“谁当API Server入口”。K8s API Server需要有一个稳定IP这个IP不能挂在某一台Master上否则那台一挂整个集群的API就断了。常见方案有两类使用keepalived VIP虚拟IP在两台或三台Master间漂移谁挂了VIP自动切到别的节点。使用kube-vip直接在K8s跑一个DaemonSet用ARP或BGP宣告VIP节点故障时自动迁移VIP。kube-vip更云原生化不过配置时要注意它有时要求节点具备发VIP的权限包括允许非本地地址绑定。用kubeadm装三Master集群再叠加kube-vip是很多团队的标准做法了。如果用KubeKey或Sealos这类工具它们会在安装时自动处理高可用组件省掉很多手工步骤。我自己搭建时出现过比较诡异的情况VIP明明还在但客户端访问API Server时而通时而不通。后来查出来是keepalived的VRRP实例和K8s的某个NetworkPolicy冲突导致健康检查报文被拦了。这种问题是纯手工装K8s才会碰到的工具化部署一般不会踩。4. 高发故障排查实录证书、网络与调度的坑4.1 kubeadm证书过期一年之痒的自动化续签方案K8s证书过期几乎是每个用kubeadm自建集群的人都会遇到的坎。要怪只能怪证书有效期太短默认一年。一天你执行kubectl get nodes突然报Unable to connect to the server: x509: certificate has expired or is not yet valid那一刻的心情我不形容了。解决分两类一类是手动续一类是规划好自动续。手动续期的基本流程是kubeadm certs renew all执行完后kubeconfig文件需要重新生成kubeadm init phase kubeconfig all --config /etc/kubernetes/kubeadm-config.yaml然后重启kube-apiserver、kube-controller-manager、kube-scheduler这几个静态Pod。静态Pod的配置在/etc/kubernetes/manifests里删掉对应的YAML文件或直接重启kubelet都能触发重建。还有一类证书是kubelet证书默认在节点上自动轮换但需要你在初始化时给kubelet开启--rotate-certificates并配置证书轮换器。很多集群卡在“节点状态是NotReady”排查半天后来一看日志是kubelet证书过期了。自动续签的经验建议别手工挨个节点跑把它做进运维脚本里。检查证书有效期用一个命令kubeadm certs check-expiration我是把这个命令加进每周巡检脚本的配上cron或自动化任务到期前一周就把续签命令执行掉重启组件后验证节点状态。不要等集群整个不可用才去救火。4.2 网络不通排查从CNI到Service的逐层定位“容器之间ping不通”“Pod访问不了Service”“外部访问不了NodePort”这类问题占比极高。我更愿意给一个通用排查顺序而不是背命令。先说跨节点Pod通信。K8s网络要求Pod之间互通这个是由CNI插件实现的比如Calico、Flannel、Cilium。如果你装了Flannel发现跨节点Pod不通先查节点间UDP 8472和VXLAN是否被防火墙拦了如果用的是Calico重点查BGP邻居状态和IPIP隧道的MTU是否一致。再说Service访问异常。Service只是一个负载均衡规则实际效果依赖kube-proxy。kube-proxy有iptables和ipvs两种模式。ipvs模式下可以用ipvsadm -Ln看转发规则iptables模式下查iptables -t nat -L KUBE-SERVICES。如果Service后端的Pod都正常但访问ClusterIP超时多数是kube-proxy的规则被外部工具清掉了比如有人用iptables -F清空了系统规则把K8s的链也带崩了。遇到“容器访问不了外网”的问题则要检查Pod内的DNS和网关。K8s默认使用集群DNSCoreDNSCoreDNS的Pod自己如果起不来整个集群的服务名解析都会挂。用kubectl get po -n kube-system -l k8s-appkube-dns查CoreDNS状态然后拿dig测试解析基本能定位问题。externalIPs有时也会引发问题。Service配了externalIP但宿主机上访问不通要先确认这台节点的内核有没有开启端口转发以及service的yaml里externalIPs是否和集群网段冲突。这都属于冷门但真实的坑。4.3 调度与资源配额GPU“预冻结”是怎么发生的有热词提到“GPU配额已不够预冻结折合1.33核时”这类提示我在集群运维里也碰到过多次。它本质上是一套平台侧的配额控制机制你申请的GPU资源不够任务就会被冻结。背后的调度逻辑是K8s的Requests和Limits在起作用。K8s里每个容器都可以声明resources.requests和resources.limits。调度器根据requests找能容纳Pod的节点当节点累计已分配的资源超过总容量Pod就会Pending。GPU属于扩展资源节点上装好nvidia-device-plugin后GPU数量会变成可调度的资源。但GPU资源特殊它一般不能超卖你申请一个GPU卡调度器必须找到有空闲卡节点的节点。遇到配额冻结我的处理思路先看Pod的Events确认是不是Insufficient nvidia.com/gpu。再kubectl top nodes看集群有没有空闲GPU节点。检查GPU节点上的taint是否允许你的Pod调度比如很多GPU节点有nvidia.com/gputrue:NoSchedule的污点你的工作负载必须带对应的容忍。至于“核时”这种概念是平台在配额维度对CPU时间做的计量。一个任务被冻结除了资源不够也可能因为执行时间超过限额。这时候要么优化代码减少运行时长要么调整任务调度优先级如果确实是团队刚需只能找管理员提配额。另外提醒一句Requests不要随便设设太高会给调度器造成“资源充足但找不到节点”的假象设太低业务又容易被打爆。这个参数最好结合历史监控数据来定。这是平台侧少有人讲、但极其影响稳定性的细节。4.4 常见问题速查表含Docker与K8s对比现象可能原因快速处理Docker Desktop提示virtualization not detectedBIOS未开启虚拟化进BIOS开VT-x/AMD-V或启用WSL2docker api连接失败npipeDocker Desktop Linux后端未启动Restart Docker Desktopdocker run后端口不通防火墙未放行或绑定了127.0.0.1检查-p 0.0.0.0:8080:80容器内改了文件重启丢失容器本身不可持久化使用volume或bind mountPod一直Pending资源不足或污点不匹配看Events调整requests或tolerationPod日志有但容器Old探针失败检查liveness/readiness探针路径Node状态NotReadykubelet或网络异常journalctl看kubelet日志Service访问超时kube-proxy规则被清重启或排查iptablesipvs规则K8s API证书过期证书默认1年kubeadm certs renew all 重启组件这张表是我这两年排查问题的浓缩版。每次遇到类似报错先查表再深入能省不少盲试时间。5. 学习路径与个人体会5.1 新手怎么安排学习节奏如果完全从零开始我建议不要一上来就啃《Kubernetes权威指南》这种大部头。书是好书但信息密度太高新人容易劝退。正确顺序是先学会Docker熟悉镜像制作、容器运行、volume、compose能在一台机器上跑起一套MySQLRedisNginx就有了体感。再学K8s核心对象不用追求懂所有Operator先把Pod、Deployment、Service、ConfigMap、Secret这些吃透。接着搭一个真实集群用KubeKey或kubeadm装三节点把高可用、证书、网络插件这些坑都踩一遍。最后再回头看权威指南里那些控制器的源码解析你会发现理解速度比一开始快得多。学习过程中一定要动手写YAML。我见过不少“看了很多文档但连Deployment都写不对”的人问题就出在只看不练。YAML写错不一定爆红字它可能只是悄悄不生效比如metadata.name和selector.matchLabels不一致Deployment创建了但Service Match不到后端Pod流量全丢弃。这种错在文档里很难发现非得自己写一遍才知道坑在哪。5.2 由浅入深的实验清单给你列一份我踩过很多坑后总结出来的实用实验清单做完基本上能应付日常问题实验一Docker启动一个nginx映射端口打开浏览器访问。实验二写一个多阶段构建的Dockerfile打镜像并运行。实验三用docker compose跑MySQL和Redis主从验证数据和主从状态。实验四在K8s里部署一个Deployment配Service验证ClusterIP和NodePort。实验五手动删除Pod观察ReplicaSet自动重建的过程。实验六对一个Deployment做滚动更新和回滚观察Pod版本变化。实验七给集群打taint和label验证Pod调度偏好。实验八部署一个带环境变量和Secret的配置熟悉ConfigMap与Secret。实验九模拟证书过期场景执行kubeadm certs renew all恢复集群访问。实验十写一个小型Operator的Demo可以是CRD加一个简单的控制器。这些实验做完你对云原生双引擎的理解会非常扎实。尤其是实验五和六基本能在几分钟内建立起“声明式系统”的直观认知。5.3 关于学习工具与日常习惯的一些碎碎念如果你是在个人电脑上学习Docker Desktop跑K8s是最省事的方式它内置了单节点K8s想模拟多节点建议用虚拟机或云主机搭。集群管理这块KubeSphere自带Web界面和KubeKey工具对新手上手很友好但生产环境还是别嫌麻烦命令行和审计日志必须熟练因为Web界面只是表象底层还是kubectl在起作用。日常维护我有一个坚持了很久的习惯不管改什么配置先备份原文件再apply再kubectl get确认。线上环境尽量不要“摸着石头过河”改动要有预期、可回滚。最后分享一个个人经验云原生的核心不在一堆工具命令而在思维方式的切换。以前我们倾向于“保证机器不挂”现在我们要做的是“保证应用不挂机器挂了也无所谓”。Docker让你把应用标准化成可移植的物件K8s让你用一套系统的力量去维护成千上万个这样的物件。把这两个引擎配合好你的交付速度和稳定性会有质的改变。
返回列表