☰
Docker+K8s Web集群从零搭建实战:部署、伸缩与踩坑记录
2026/10/21 6:13:57 网站建设 项目流程

这套组合拳打下来,单机部署的玩具项目瞬间变得有点“生产环境”的样子了。这篇东西,就是我当时从零搭一个“基于Docker-K8s的Web集群”的全过程记录,包括怎么想、怎么搭、怎么踩坑、怎么填坑,每一步都给你捋清楚。看完不说能让你立刻成为专家,至少能让你在搭集群的时候少走几个大弯路。

1. 整体设计与思路拆解

1.1 为什么是 Docker + Kubernetes,而不是别的

先说个现实问题:一个Web应用,只跑在一台机器上,一旦流量上来,或者机器出点毛病,服务就跟着没了。你想多搞几台机器,又会发现手动部署简直是噩梦——每台机器都要装环境、拉代码、配参数,搞一次半个下午就没了,而且配置容易漂移,这台机器能跑,那台机器莫名其妙就起不来。

Docker 解决的是“环境一致性问题”。它把你的应用和它依赖的运行时、库文件、配置全部打包成一个镜像,不管底层的机器是什么发行版、什么内核版本,跑起来的行为基本是一致的。我打个比方,Docker 相当于把一整套炊具、食材、菜谱都装进一个集装箱,运到哪里,拆开就能按同样的流程做饭,味道不会差太多。

Kubernetes 解决的是“调度和运维问题”。它管着一堆 Docker 节点(宿主机),你只需要告诉它“我要跑3个副本、每个副本要多少CPU内存、暴露哪个端口”,它就自己去分配机器、启动容器、监控状态、失败了自动拉起新容器,流量来了还能扩容。它相当于一个饭馆的中央调度系统,哪个厨师累了(节点故障),它立刻调别的厨师顶上;客人多了(流量高峰),它喊后厨多炒几份菜(扩容副本)。

所以这个组合核心思路就八个字:镜像交付,集群运行。

1.2 集群拓扑与核心组件规划

我这次搭的集群规模不大,但五脏俱全。一台作为控制平面节点(Control Plane),两台作为工作节点(Worker),再加一个单独的镜像仓库节点。控制平面节点说白了就是“大脑”,它不跑实际业务流量,只负责管理整个集群状态;两个工作节点是“手脚”,业务容器就落在它们上面。

Kubernetes 的核心组件你得先有个概念:

  • kube-apiserver:集群的入口,所有操作(kubectl 命令)都是过它来处理的。
  • etcd:集群的“记事本”,所有配置和状态都存这里面。
  • kube-scheduler:负责把某个新容器安排到哪个合适的节点上。
  • kube-controller-manager:负责维护集群期望状态,比如副本数少了,它去补。
  • kubelet:跑在每个工作节点上,听 apiserver 的命令,负责启动/停止容器。
  • kube-proxy:负责实现服务的网络代理和负载均衡。

当时我在规划拓扑的时候,特意把仓库节点独立出来了,一开始我图省事把仓库放在控制平面节点上,结果后来发现镜像构建和推送占用的 IO 会影响集群管理组件的响应。所以如果条件允许,仓库独立是对的。

1.3 技术选型的几个关键考量

关于容器运行时:Kubernetes 从 1.24 版本开始就不再用 Docker 作为默认运行时了,听起来很吓人,其实不用慌。你仍然可以用 Docker 来构建镜像、管理镜像,只是 Kubernetes 让 kubelet 通过 containerd 这个接口去拉取和运行容器。所以我的策略是:开发环境用 Docker 命令构建镜像,镜像仓库用支持 OCI 标准的仓库,集群运行时用 containerd。这样兼容性最好,不会有老版本弃用的坑。

关于容器网络接口(CNI):我选了 Calico。Flannel 也行,但在网络策略方面 Calico 更完整。Kubernetes 集群里的 Pod 网络段是虚拟的,需要在节点间打通路由,Calico 用的是 BGP 路由模式,性能不错,还能支持更细粒度的网络限制。网上有人说 Calico 配置复杂,但用它的默认配置其实很省心。

关于存储:高可用集群不能把数据放在容器里,容器一删数据就没了。所以我用 NFS 做了个共享存储,把需要持久化的数据统一放到 NFS 上,然后通过 PersistentVolume 和 PersistentVolumeClaim 挂载到 Pod 里。这个在后面的博客系统场景里是必备的,因为博客的图片附件、SQLite 数据库文件这些都不能跟着容器走。

2. 环境准备与核心细节解析

2.1 节点规划与系统基础配置

我这里的节点清单大致如下(IP 只是示例,我本地实验环境用虚拟网段):

节点角色主机名配置IP(示例)
控制平面k8s-master014核8G,SSD192.168.1.10
工作节点1k8s-node014核16G,SSD192.168.1.11
工作节点2k8s-node024核16G,SSD192.168.1.12
镜像仓库k8s-registry2核4G,大容量HDD192.168.1.13

系统我统一用的 Ubuntu 22.04 LTS。为啥选它?因为它的内核版本足够新,跑 Kubernetes 1.28 以上版本不需要单独升级内核,而且 containerd 的兼容性也验证得比较充分。

基础配置里有一个特别容易忽略的坑:swapoff -a。Kubernetes 默认要求关闭 swap,因为如果内存不够用了系统开始用交换分区,节点性能会急剧下降,调度器对内存的预估就会失准。你不仅需要执行 swapoff -a,还必须把 /etc/fstab 里的 swap 行注释掉,不然重启后 swap 又回来了,节点状态会一直 NotReady。

2.2 镜像构建与版本管理

镜像标签策略我用了两段式:应用名:版本号-构建序号,例如blog-api:v1.2.0-20240618。这样做的目的是:光看标签就知道是什么时候构建的,而且版本号可以回溯。我见过有人图省事直接用latest标签,结果生产环境一拉就拉到最新镜像,依赖偷偷变了,线上直接挂掉。所以生产环境永远不要用 latest,这条铁律一定要刻在脑子里。

构建多阶段是必须的。比如我的 Web 后端应用是 Go 写的,第一阶段拉取编译环境,编译出二进制;第二阶段用 scratch 或 alpine 作为运行镜像,只把编译好的二进制复制进去。这样做出来的镜像能从 1GB 瘦身到几十 MB,不仅仓库空间省了,集群节点拉镜像的时间也少了。

2.3 集群网络与存储方案怎么选

Kubernetes 里有一个概念叫 Pod 网络,每个 Pod 都有一个独立的 IP 地址,而且这个 IP 是集群内部可达的。我规划的 Pod 网段是 10.244.0.0/16,Service 网段是 10.96.0.0/12。这两个网段千万别跟你的物理机网段重叠,不然路由直接乱套。

存储方案我刚才说了用 NFS,就是网络文件系统。我把 NFS 服务搭在独立的存储节点上,集群里靠 StorageClass 动态创建 PV。这样做的好处是用 PVC 直接请求存储,不用手动去建 PV。当然,NFS 的性能比不上本地磁盘,但对于博客这种 IO 压力不大的场景绰绰有余。如果你的应用是数据库这种高 IO 的,建议考虑类似 Longhorn 这种分布式块存储方案,或者直接上云上的托管盘。

3. 实操过程与关键环节实现

3.1 业务镜像构建与私有仓库推送

我的业务服务有两个:一个是前端的 Nginx 静态站点,一个是后端的 API 服务。这俩镜像构建方式不太一样。

前端镜像的 Dockerfile 思路:

FROM nginx:1.25-alpine as base COPY dist/ /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80

这里要注意,dist/是前端构建完的产物,所以我在 CI 流程里先跑完npm run build再执行docker build。

后端 API 镜像用多阶段构建:

FROM golang:1.22 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o /app/api . FROM alpine:3.20 RUN adduser -D -u 10001 app COPY --from=build /app/api /usr/local/bin/api USER app EXPOSE 8080 ENTRYPOINT ["api"]

这里有几个细节:CGO_ENABLED=0是为了得到纯静态二进制,这样在 Alpine 这种精简镜像里才能跑;adduser是为了不用 root 用户运行应用,符合最小权限原则;USER app这一步很多人忽略,不设置的话容器里跑的是 root,一旦容器被攻破,攻击者直接就拿到了宿主机的 root 权限。

构建完标签:

docker tag nginx-web:latest registry.example.local:5000/blog-web:v1.0.0-20240618 docker push registry.example.local:5000/blog-web:v1.0.0-20240618

如果仓库设了私有认证,记得先docker login registry.example.local:5000。这里注意registry.example.local是我内网自建的域名解析,你可以根据自己的环境换成 IP 或别的域名,但最好别用 IP 直接访问,证书和后续迁移都会麻烦。

3.2 编写 Deployment 与 Service 资源清单

这一步是核心中的核心。我先写 Deployment,它负责管理 Pod 的期望状态。

apiVersion: apps/v1 kind: Deployment metadata: name: blog-api namespace: production spec: replicas: 3 selector: matchLabels: app: blog-api template: metadata: labels: app: blog-api spec: containers: - name: blog-api image: registry.example.local:5000/blog-api:v1.1.0 ports: - containerPort: 8080 protocol: TCP resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: path: /livez port: 8080 initialDelaySeconds: 15 periodSeconds: 20

几个关键点逐个解释:

resources.requests是给调度器看的最低需求,resources.limits是容器的资源上限。我设置的请求是 100m CPU(百分之一核)、128MB 内存,上限是 500m、512MB。这样做的好处是,三个副本一共最多占用 1.5 核、1.5GB 内存,在节点上留出了充足的余量。

探针的重要性再怎么强调都不为过。readinessProbe 决定流量要不要打到这个 Pod 上,如果后端服务还在启动、还没就绪,探针失败了,流量就不会过来;livenessProbe 决定容器是不是还活着,如果一个 Pod 进入了死锁状态,探针就会把容器杀掉重启。这就是 Kubernetes 自愈能力的一个具体体现。

然后写 Service,它提供了一个稳定的访问入口:

apiVersion: v1 kind: Service metadata: name: blog-api namespace: production spec: type: ClusterIP selector: app: blog-api ports: - port: 80 targetPort: 8080

这里的 selector 会和 Deployment 里的 Pod 标签对得上。Service 是集群内部的 VIP,它会自动把访问 :80 的流量转发到后端某个 Pod 的 8080 端口上,并且自带负载均衡。

3.3 水平伸缩与滚动更新的配置思路

Kubernetes 有个很实用的玩法叫 HPA,HorizontalPodAutoscaler,它能根据 CPU 使用率自动调整副本数。我是这样配置的:

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: blog-api-hpa namespace: production spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: blog-api minReplicas: 2 maxReplicas: 8 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60

意思就是 CPU 平均使用率超过 60% 时,HPA 自动加副本,最多加到 8 个;低于 60% 时缩回去,最少保持 2 个。这里我选了 CPU 作为指标,简单直观。生产环境你还可以根据请求量 QPS 做自定义指标伸缩,但那就需要装 Prometheus Adapter 了,复杂度高不少,初学阶段不推荐。

升级用滚动更新,Kubernetes 默认策略就是 RollingUpdate,这很好。但几个参数可以再调一下:

spec: strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0

maxUnavailable 设为 0 就意味着每次最多只能多起一个新 Pod,旧 Pod 一个都不先停,新 Pod 就绪了再停旧的——零停机发布。代价是发布期间会短暂占用比期望副本数多一个 Pod 的资源。如果期望副本是 3,发布过程中可能同时有 4 个 Pod 在跑。

3.4 验证集群可用性的几个检查点

部署完成后,我习惯按这个顺序来检查:

先看节点状态:

kubectl get nodes

所有节点都是 Ready 才是第一步。

再看 Pod 状态:

kubectl get pods -n production -o wide

全部 Running 且 READY 1/1 才算通过。如果状态不对,就去看详情和日志:

kubectl describe pod blog-api-xxxxx -n production kubectl logs blog-api-xxxxx -n production

最后验证服务是否能正常访问。集群内部的 Service 在外面是访问不到的,所以我用一个 Nginx Ingress Controller 作为统一的流量入口。我的入口配置大致如下:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: blog-web namespace: production spec: ingressClassName: nginx rules: - host: blog.example.com http: paths: - path: / pathType: Prefix backend: service: name: blog-web port: number: 80

这里 host 我用的是blog.example.com,实际上可以改成你自己的域名。配置完,把域名的 DNS A 记录指向集群的 Ingress 节点公网 IP,就能在浏览器访问了。我访问了http://blog.example.com,首页正常渲染,API 接口也能正常返回 JSON 数据,到这一步集群算是跑通了。

4. 常见问题与排查技巧实录

这一部分是我最想写的,因为理论和实操之间隔着一整条河,河里全是坑。

4.1 镜像拉取慢或者拉取失败

这个问题最常见,尤其是第一次搭集群时。默认情况下,节点会去 Docker Hub 拉公共镜像,但因为众所周知的网络原因,速度很慢,甚至超时。而私有仓库虽然在内网快,但如果没配好证书,节点会拒绝拉取。

我的解决办法有三个层级:

  • 给 containerd 配置镜像加速器,修改/etc/containerd/config.toml里的[plugins."io.containerd.grpc.v1.cri".registry]段,添加加速地址。
  • 如果是内部仓库,直接把仓库地址加到 mirrors 里,或者配置成私有仓库并用config_paths指定认证文件。
  • 最省事的方法:在有 Docker 的机器上提前把镜像 pull 下来,然后docker save成 tar 包,传到节点上再用ctr -n=k8s.io images import导入。这个办法在离线环境特别实用,我应急的时候全靠它。

4.2 Pod 一直 Pending 或 CrashLoopBackOff

Pending 意味着调度器无法为 Pod 找到合适的节点。最常见的原因就是资源不足。假如你的集群总共可用内存是 8GB,而 Deployment 里每个副本请求 512MB,副本数是 20,那肯定装不下了。这时候kubectl describe pod会显示Insufficient memory或者0/2 nodes are available。

我的排查步骤是:

kubectl describe pod <pod-name> kubectl top nodes

前者看事件,后者看资源大盘。如果真是资源不够,要么删掉没用的 Deployment,要么加节点,要么把 requests 调小。

CrashLoopBackOff 意味着容器起来了又崩了,循环往复。这种情况 90% 是应用本身的启动问题,最常见是环境变量缺失、数据库连不上、端口被占用。看日志是唯一路径:

kubectl logs <pod-name> --tail=50

如果容器之前有旧版本,--previous参数可以看上一次崩溃的日志:

kubectl logs <pod-name> --previous

我踩过一次坑:应用启动时检测到一个固定路径下的配置文件,但容器里没这个文件,一直报错崩溃。其实就是没挂载 ConfigMap,补上就好了。这类配置问题最好用 ConfigMap 管理,别把配置写死在镜像里。

4.3 探针配置导致服务频繁重启

这个坑非常隐蔽。我最初给某个后端服务配置了 livenessProbe,路径是/healthz,但那个接口的响应时间偶尔会超过 5 秒。K8s 默认的timeoutSeconds是 1 秒,探针 1 秒内收不到响应就认为失败,连续三次失败就会把容器杀了重启。结果我的服务一直在重启,日志里却看不出任何错误,就是探针误杀。

解决办法是把探针的超时时间调大,同时把健康检查路径实现得轻量一些,不要查数据库,只返回一个固定字符串。还有一个经验是:readinessProbe 的初始延时要根据应用的平均启动时间设定,设太短会导致一启动就被杀掉,设太长会影响发布速度。我一般先跑起来看日志,估算启动时间,然后加 5 秒的余量。

4.4 流量接入与域名配置的坑

第一次配置 Ingress 时,我遇到过一个超绝的怪问题:Ingress 配好了,但从外部访问域名始终 404。折腾了半天才发现,Ingress Controller 所在的命名空间不对,我的 Ingress 资源创建在了 production 空间,但 Ingress Controller 根本不会监听那个空间的资源,或者监听的是别的注解格式。

还有一次是 HTTPS 证书配置的问题,证书对应的域名和实际访问的域名不一致,浏览器直接不安全提示。所以如果用了 HTTPS,记得检查证书明细里的 SAN 字段是不是包含了你的域名。

另外提醒一个点:Service 的 targetPort 要选对。Ingress -> Service -> Pod 的链路里,任何一个端口写错,外部访问就是 502 或连接超时。排查这类问题,可以进到 Ingress Controller 的 Pod 里手动 curl 一下后端的 Service IP 和端口,看通不通。

4.5 其他实战问题速查

症状可能原因排查命令解决方案
节点 NotReady网络插件没装好,或 swap 没关kubectl describe node重新安装 Calico;确认 swapoff -a
DNS 无法解析服务名CoreDNS 副本挂了或拒绝请求kubectl get pods -n kube-system重启 CoreDNS;检查上游 DNS 配置
kubectl 命令报权限错误kubeconfig 没有对应集群权限kubectl config view重新拉取 admin kubeconfig
服务间调用超时安全组/防火墙拦截了 Pod 网段流量iptables -L -n放行 Pod CIDR 网段
PVC 一直 PendingStorageClass 不存在或无法连接 NFSkubectl describe pvc确认 StorageClass 名称正确,NFS 可访问
端口无法从外部访问Service 类型选错kubectl get svc外部访问换 LoadBalancer 或 NodePort
升级后流量全部 502Service selector 与新版 Pod 标签不匹配kubectl get endpoints修正 Service 的 selector

这张表是我反复排查后整理出来的,基本覆盖了集群初期的大多数猫腻。

5. 写在最后的经验杂谈

这套集群我陆陆续续跑了几个月,中间经历过节点宕机、流量突增、应用发布,总体来说是稳的。但我也必须说句实话,Kubernetes 本身是一个工具,不是一个银弹。如果你的业务只是一个小站点,每天几百次访问,一台机器一个 Docker 就足够应付了,硬上 K8s 只会增加运维复杂度,而不是给你省事。

但如果你面对的是一个正在快速增长的 Web 服务,那这套架构的弹性和自愈能力就能体现出来。比如我之前遇到某次推广活动,流量短时间暴涨,我原本只配了 3 个副本,监控里看到 CPU 持续超过阈值,HPA 自动把它扩到了 7 个,整个过程我什么都没干,就看着面板上的副本数自己在涨,那种感觉是真的爽。

最后再分享一个我个人的工作习惯:每次对集群做改动前,先把当前所有 YAML 文件做一次版本化备份,用 Git 管理。我在项目根目录下建了一个deploy/目录,把 Deployment、Service、Ingress、ConfigMap 之类的东西全部按环境分目录存放。这样一旦改出了问题,一条命令就能回滚到上一个稳定版本。不少人嫌 Git 管理 YAML 麻烦,但真的出过事故之后,你会发现这个习惯救过你的命。

还有一个建议,趁早把监控装起来。不用上太复杂的全家桶,先用 Prometheus 加 Grafana 把你集群的 CPU、内存、网络、Pod 状态这些基础指标拉出来就行。我看到过很多集群出故障都是半夜,如果没有监控,你连哪里破了都不知道。监控日志和告警,是你睡个安稳觉的前提。

这套东西后续可扩展的方向还很多,比如 CI/CD 流水线接入(提交代码自动构建镜像自动发布)、日志采集用 EFK 方案、服务网格 Istio 的流量治理。不过这些都是后话了,先把上面的基础打牢,集群先稳定跑起来,再一步一步加东西也不迟。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询