
1. 为什么这套组合值得花时间研究容器化部署早就不是新鲜事了但真正让一线运维和开发头疼的往往不是能不能跑起来而是跑起来之后存储怎么办。我见过太多团队用容器跑无状态服务很顺手一旦涉及需要持久化的场景——比如网站静态资源、日志文件、上传目录——就开始各种踩坑。传统做法是挂载云盘或者NAS手动配置PV和PVC写一堆YAML调半天挂载参数最后还可能因为权限问题导致容器起不来。这套方案的核心价值在于用ACK托管容器服务做底座用CNFS做存储编排层把NGINX网站这种典型的有状态服务快速落地。CNFS是容器网络文件系统它把底层存储资源抽象成Kubernetes原生的存储类你不需要关心底层是哪种文件存储只需要声明我要一个能读写的共享文件系统剩下的挂载、扩容、快照都由CNFS接管。对于中小规模网站、内部工具站、静态资源站这类场景这套组合的搭建速度可以压缩到半小时以内。适合谁来参考如果你正在用容器跑Web服务但被存储问题困扰或者你准备把传统虚拟机上的NGINX迁移到容器环境又或者你只是想快速搭一个能持久化的演示站点这套方案都能直接抄作业。不需要你精通Kubernetes底层原理但基本的kubectl操作和YAML阅读能力要有。我实际搭过好几套类似环境从最初手动配NAS挂载到后来用CNFS托管中间踩的坑足够写一本小册子。下面把整个思路、操作细节和避坑经验完整拆开讲。2. 整体架构设计与核心组件选型2.1 为什么是ACK加CNFS而不是自建方案先说说选型逻辑。容器编排层面ACK是托管版Kubernetes控制面由云厂商维护你只需要管节点池和工作负载。相比自建K8s省掉了etcd运维、证书轮换、API Server高可用这些琐事。对于网站这种业务稳定性比什么都重要托管版在这方面的优势很明显。存储层面CNFS解决的是容器存储编排的问题。传统方式是你先买一块云盘或NAS然后手动创建PV指向它再创建PVC绑定最后挂到Pod里。这套流程在单集群小规模下还能忍一旦涉及多命名空间、多环境、动态扩容维护成本直线上升。CNFS的做法是定义一个StorageClass底层关联到具体的文件存储实例PVC创建时自动分配存储空间扩容时改一下PVC的spec就行不需要手动干预底层资源。注意CNFS底层通常对接的是NAS类文件存储不是块存储。这意味着它天然支持多Pod同时读写ReadWriteMany适合网站静态资源、共享上传目录这类场景。如果你需要的是数据库那种单Pod独占的低延迟块存储应该选云盘类型的StorageClass不要混用。2.2 NGINX在容器环境下的存储需求拆解一个典型的NGINX网站存储需求可以分成三块静态资源目录HTML、CSS、JS、图片这些文件需要持久化多个副本要共享同一份内容。日志目录access.log和error.log需要持久化以便排查问题但通常不需要多副本共享。配置文件nginx.conf以及conf.d下的站点配置可以通过ConfigMap注入不一定需要持久化存储。这三块里静态资源目录是最需要CNFS的。如果你用emptyDirPod重启后文件就没了如果用hostPathPod漂移到其他节点就找不到文件了。CNFS提供的共享文件系统可以让多个NGINX副本挂载同一个目录内容实时同步扩容副本时不需要额外拷贝数据。2.3 网络与访问链路设计访问链路这块我建议用LoadBalancer类型的Service对外暴露而不是NodePort。NodePort需要你记住节点IP和端口节点扩容或替换后IP会变维护麻烦。LoadBalancer会分配一个固定的对外地址后端自动关联到所有健康的Pod。如果你有域名可以在LoadBalancer地址上再套一层DNS解析。没有域名的话直接用IP访问也行但要注意HTTP和HTTPS的区别。演示环境用HTTP就够了生产环境建议配证书走HTTPS。整体架构用一句话概括客户端请求打到LoadBalancerLB转发到NGINX Pod的80端口NGINX从CNFS挂载的存储卷里读取静态文件返回。日志写入另一个CNFS卷或本地卷配置通过ConfigMap注入。3. 环境准备与基础资源创建3.1 集群和节点池的最低配置建议搭建之前先确认集群规格。NGINX本身很轻量但CNFS的挂载过程需要节点上有对应的CSI插件运行。建议节点池至少2个节点每个节点2核4G起步。单节点也能跑但失去了高可用的意义节点故障时Pod无法漂移。操作系统选常见的Linux发行版就行注意内核版本不要太老否则CSI插件可能不兼容。节点网络要确保能访问到CNFS底层文件存储的内网地址这个通常在同一个VPC内默认是通的。创建集群时记得勾选安装存储插件或者后续手动安装CNFS对应的CSI驱动。不同云厂商的插件名称不一样但核心功能一致让Pod能挂载网络文件系统。3.2 命名空间与资源配额规划我习惯给每个业务单独建命名空间不要把所有东西都堆在default里。创建一个叫web-demo的命名空间kubectl create namespace web-demo如果集群资源紧张可以给这个命名空间设置ResourceQuota限制CPU和内存总量防止某个Pod异常占用过多资源影响其他业务。演示环境可以跳过这步生产环境建议加上。apiVersion: v1 kind: ResourceQuota metadata: name: web-demo-quota namespace: web-demo spec: hard: requests.cpu: 2 requests.memory: 4Gi limits.cpu: 4 limits.memory: 8Gi persistentvolumeclaims: 5这个配额的意思是整个命名空间最多申请2核CPU和4G内存的requestslimits上限翻倍最多创建5个PVC。数字根据实际情况调整别照搬。3.3 CNFS存储类与PVC的创建要点CNFS的核心是StorageClass。创建之前需要先有一个底层的文件存储实例这个在云控制台上创建记下挂载地址。然后创建StorageClassapiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: cnfs-nas-sc provisioner: nas.csi.example.com parameters: server: xxxxx.nas.example.com:/share archiveOnDelete: false reclaimPolicy: Retain allowVolumeExpansion: true mountOptions: - nolock,tcp,noresvport几个关键参数解释一下。provisioner是CSI驱动的标识不同云厂商不一样填错会导致PVC一直Pending。server是NAS挂载地址格式通常是地址:/共享目录。reclaimPolicy: Retain表示PVC删除后底层数据保留防止误删。allowVolumeExpansion: true允许后续扩容。mountOptions里的nolock对NGINX场景很重要避免文件锁导致的性能问题。提示archiveOnDelete设为false表示删除PVC时不归档数据直接保留在NAS上。如果你希望删除时自动清理可以设为true但生产环境慎用数据无价。创建PVC就简单了apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nginx-web-content namespace: web-demo spec: accessModes: - ReadWriteMany storageClassName: cnfs-nas-sc resources: requests: storage: 20GiaccessModes选ReadWriteMany因为多个NGINX副本要同时挂载。storage填20Gi实际用量取决于你的网站文件大小CNFS通常按实际用量计费不会预占全部空间。4. NGINX网站部署的完整实操流程4.1 ConfigMap配置文件的编写与注入先把NGINX配置准备好。用ConfigMap存nginx.conf和站点配置这样修改配置不需要重新打镜像。apiVersion: v1 kind: ConfigMap metadata: name: nginx-config namespace: web-demo data: nginx.conf: | user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; include /etc/nginx/conf.d/*.conf; } default.conf: | server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ 404; } location /healthz { return 200 ok; add_header Content-Type text/plain; } }这里加了一个/healthz路径用于健康检查后面探针会用到。root指向/usr/share/nginx/html这是NGINX默认的静态文件目录我们会把CNFS卷挂载到这个位置。4.2 Deployment编排文件的关键字段说明Deployment是核心字段比较多我拆开讲。apiVersion: apps/v1 kind: Deployment metadata: name: nginx-web namespace: web-demo spec: replicas: 2 selector: matchLabels: app: nginx-web template: metadata: labels: app: nginx-web spec: containers: - name: nginx image: nginx:1.25-alpine ports: - containerPort: 80 volumeMounts: - name: web-content mountPath: /usr/share/nginx/html - name: nginx-config mountPath: /etc/nginx/nginx.conf subPath: nginx.conf - name: nginx-config mountPath: /etc/nginx/conf.d/default.conf subPath: default.conf - name: nginx-logs mountPath: /var/log/nginx resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi livenessProbe: httpGet: path: /healthz port: 80 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /healthz port: 80 initialDelaySeconds: 5 periodSeconds: 5 volumes: - name: web-content persistentVolumeClaim: claimName: nginx-web-content - name: nginx-config configMap: name: nginx-config - name: nginx-logs emptyDir: {}逐项说明。replicas: 2起两个副本配合反亲和性可以分散到不同节点。image用alpine版本体积小启动快。volumeMounts里web-content挂到html目录这是CNFS卷。nginx-config用subPath分别挂载两个文件避免覆盖整个目录。nginx-logs用emptyDir日志不需要持久化Pod重启后清空如果需要保留日志可以换成CNFS卷。resources的requests和limits要合理设置。NGINX处理静态文件很省资源100m CPU和128Mi内存足够。limits设太高没意义设太低会导致OOM。livenessProbe和readinessProbe都指向/healthz确保Pod真正可用后才接入流量。注意subPath挂载ConfigMap文件时如果ConfigMap更新容器内文件不会自动同步。需要重启Pod才能生效。这是Kubernetes的已知行为不是bug。生产环境可以用Reloader之类的工具监听ConfigMap变化自动重启。4.3 Service与对外暴露的配置方式Service用LoadBalancer类型apiVersion: v1 kind: Service metadata: name: nginx-web-svc namespace: web-demo annotations: service.beta.kubernetes.io/loadbalancer-type: public spec: type: LoadBalancer selector: app: nginx-web ports: - name: http port: 80 targetPort: 80 protocol: TCP创建后等一两分钟用kubectl get svc -n web-demo查看EXTERNAL-IP。拿到地址后浏览器访问应该能看到NGINX默认页面或者你上传的index.html。如果集群不支持LoadBalancer可以临时用NodePortspec: type: NodePort ports: - port: 80 targetPort: 80 nodePort: 30080然后通过节点IP:30080访问。但如前所述生产环境不建议这么做。4.4 静态文件上传与目录权限处理CNFS卷挂载后默认属主可能是root而NGINX容器内以nginx用户运行会导致403 Forbidden。解决办法有两个第一种在容器启动命令里加chmodcommand: [sh, -c, chmod -R 755 /usr/share/nginx/html nginx -g daemon off;]第二种用initContainer先调整权限initContainers: - name: fix-permissions image: busybox command: [sh, -c, chmod -R 755 /usr/share/nginx/html] volumeMounts: - name: web-content mountPath: /usr/share/nginx/html我倾向第二种职责更清晰。上传文件的方式可以起一个临时Pod挂载同一个PVC用kubectl cp拷贝进去kubectl run temp-upload --imagebusybox -n web-demo --rm -it --restartNever \ --overrides{spec:{containers:[{name:temp-upload,image:busybox,command:[sh],volumeMounts:[{name:web-content,mountPath:/data}]}],volumes:[{name:web-content,persistentVolumeClaim:{claimName:nginx-web-content}}]}}进去之后把文件放到/data目录即可。或者用云控制台的文件管理功能直接上传到NAS共享目录更直观。5. 常见问题排查与性能调优经验5.1 PVC一直Pending的排查思路这是最常见的问题。按以下顺序排查排查项检查命令可能原因StorageClass是否存在kubectl get sc名称拼写错误或未创建CSI驱动是否运行kubectl get pods -n kube-system | grep csi驱动未安装或崩溃NAS地址是否可达节点上ping或telnet测试网络不通或地址错误事件详情kubectl describe pvc name -n web-demo看Events里的具体报错我遇到过最坑的一次是StorageClass的provisioner名字写错了PVC卡了半小时describe一看Events里写着provisioner not found。所以养成习惯创建完PVC立刻describe看一眼。5.2 NGINX返回403或404的典型原因403通常是权限问题按4.4节的方法处理。404则是文件路径不对检查root指令指向的目录和实际挂载点是否一致。还有一种情况是index文件不存在NGINX找不到默认首页也会返回403。如果配置了try_files $uri $uri/ 404访问不存在的路径会返回404而不是NGINX默认页。这是预期行为不用慌。5.3 多副本场景下的文件同步与缓存问题CNFS底层是共享文件系统多个Pod挂载同一个卷文件内容是实时同步的。但NGINX自身有文件缓存如果开启了open_file_cache更新文件后可能读到旧内容。演示环境默认没开不用管。生产环境如果开了需要设置合理的缓存过期时间或者更新文件后reload NGINX。另外多个Pod同时写同一个日志文件会交错建议每个Pod写自己的日志文件用Pod名做后缀或者直接输出到stdout由日志系统收集。5.4 资源限制与副本数的平衡取舍副本数不是越多越好。每个副本都会占用节点资源而且CNFS的挂载点数量也有限制。2到3个副本对于中小网站足够了。如果流量大优先考虑提升单副本的资源limits而不是无限加副本。CPU limits建议设为requests的2到4倍给突发流量留余量。内存limits不要设得太紧NGINX处理大文件时内存会上涨设太小容易OOMKilled。提示可以用HorizontalPodAutoscaler根据CPU使用率自动扩缩容但CNFS场景下要注意新Pod挂载存储需要时间扩容不是瞬时的。建议设置scaleDown的稳定窗口长一些避免频繁抖动。6. 个人实操体会与后续扩展方向这套方案我前前后后搭了不下十次每次都有新发现。最开始图省事把所有配置塞一个YAML里后来发现维护困难现在习惯按ConfigMap、PVC、Deployment、Service拆成独立文件用kubectl apply -f按顺序执行。顺序很重要PVC没创建成功之前Deployment会一直Pending。还有一个体会是CNFS的挂载速度受网络影响。如果NAS和节点不在同一个可用区延迟会明显增加NGINX读取静态文件的响应时间也会变长。尽量让存储和计算在同一个区域。后续如果要扩展可以考虑几个方向。一是加Ingress替代LoadBalancer Service用域名和路径做路由更灵活。二是接入CDN加速静态资源NGINX只做回源。三是把日志接入集中式日志系统方便检索和分析。四是配置CNFS的快照策略定期备份网站内容防止误操作。这些扩展不需要一次性全上根据实际需求逐步加就行。先把基础跑通再考虑优化。