1. 项目缘起与整体架构设计
PHP 项目上 Kubernetes,这件事放在五六年前,很多团队会觉得没必要——一个 LNMP 就能跑起来的东西,何必套一层容器编排。但这两年情况变了:业务要求快速迭代、多环境一致性、灰度发布、弹性伸缩,传统“一台机器一个项目”的部署方式越来越撑不住。我所在的团队维护着一套中等规模的 PHP 后端服务,包含 API 网关、业务逻辑层、异步队列消费者和几个定时任务脚本,早期用 Jenkins 直接 rsync 到几台固定服务器上,每次发布都像拆盲盒——环境差异、扩展困难、回滚靠手动备份,出问题排查起来非常痛苦。
后来我们决定把整套 PHP 应用容器化,用 Kubernetes 做编排,Jenkins 做 CI/CD 流水线。这篇文章就是把这套方案从设计到落地的完整过程拆开讲清楚,包括为什么这么选、每一步怎么做、踩过哪些坑。如果你也在维护 PHP 项目,正考虑往容器化和自动化部署方向走,或者已经上了 K8s 但流水线还跑得不顺畅,这篇内容应该能给你一些直接能用的参考。
核心关键词先摆出来:PHP、Kubernetes、Jenkins、CI/CD、Ingress。整条链路的目标很明确——代码提交后自动触发构建、测试、打包镜像、推送到镜像仓库、更新 K8s 部署,并且支持一键回滚。听起来是标准操作,但 PHP 项目有一些特殊性,比如 Composer 依赖管理、环境变量注入、文件存储、队列进程与 Web 进程的分离,这些在流水线设计时都要单独考虑。
1.1 为什么选择 Kubernetes 而不是 Docker Compose
很多人会问,PHP 项目用 Docker Compose 不就够了吗?小规模确实够。但我们有几个硬需求是 Compose 满足不了的:第一,需要根据 CPU 和内存使用率自动扩缩容,流量高峰时 Web 进程要能自动增加;第二,需要滚动更新,发布过程中不能中断服务;第三,多环境(开发、测试、预发、生产)需要一致的编排定义,Compose 在不同环境下的配置管理比较吃力;第四,需要服务发现和负载均衡,内部服务之间调用不能写死 IP。
Kubernetes 的 Deployment、Service、Ingress、HPA 这些资源对象刚好覆盖了这些需求。Deployment 负责滚动更新和副本管理,Service 做内部服务发现,Ingress 统一入口和域名路由,HPA 做自动扩缩容。而且 K8s 的声明式 API 让整个部署状态可版本化、可审计,这对后期运维非常重要。
1.2 Jenkins 在流水线中的角色定位
Jenkins 在这里承担的是“构建编排器”的角色。虽然现在 GitLab CI、GitHub Actions、Argo CD 等工具都很流行,但 Jenkins 的优势在于插件生态成熟、对自定义构建步骤支持灵活、可以很方便地对接企业内部的各种系统。我们用它来做代码拉取、Composer 依赖安装、单元测试、镜像构建与推送、K8s 部署更新这一整套流程。
Jenkins 的 Pipeline 用 Groovy 脚本描述,可以做到“流水线即代码”,跟项目代码一起版本管理。这样每次修改构建逻辑都有记录,也方便回滚。我们用的是声明式 Pipeline 语法,结构清晰,维护成本低。
1.3 整体架构与数据流向
整个架构可以这样理解:开发者在 GitLab 上提交代码,Webhook 触发 Jenkins 流水线。Jenkins 首先拉取代码,在构建容器里执行 Composer 安装和测试,然后构建 Docker 镜像并推送到 Harbor 镜像仓库。接着 Jenkins 调用 kubectl 更新 Kubernetes 的 Deployment 镜像版本,K8s 执行滚动更新,新的 Pod 启动后通过 Readiness Probe 检查,确认健康后接入 Service,Ingress 将外部流量路由到新的 Pod。
整个过程中,配置信息通过 ConfigMap 注入,敏感信息通过 Secret 管理,持久化数据挂载 PVC,日志通过 Sidecar 或 DaemonSet 收集。这套架构跑通之后,发布从原来的半小时缩短到三分钟以内,回滚只需要在 Jenkins 上点一下“回滚到上一版本”。
2. PHP 项目容器化的关键细节
PHP 项目做容器化,跟 Java 或 Go 项目不太一样。Java 打成一个 fat jar 就能跑,Go 编译成静态二进制更简单。PHP 需要 PHP-FPM 或 Apache 作为运行时,代码是解释执行的,依赖通过 Composer 管理,这些特点决定了镜像构建和运行方式有自己的讲究。
2.1 基础镜像选择与分层构建策略
基础镜像我们试过几种:官方的php:8.2-fpm-alpine、php:8.2-fpm(基于 Debian)、还有自己从 Ubuntu 基础镜像手动装 PHP。最终选择了php:8.2-fpm-alpine,原因是 Alpine 体积小,基础镜像只有几十 MB,构建出来的最终镜像也小,推送到镜像仓库和拉取到 K8s 节点都快很多。
但 Alpine 有个坑:它用的是 musl libc 而不是 glibc,某些 PHP 扩展或依赖 glibc 的二进制工具可能不兼容。我们遇到过一次,某个图像处理库在 Alpine 下编译失败,后来换成php:8.2-fpm(Debian 基础)才解决。所以如果你的项目依赖比较重,建议先用 Debian 基础镜像跑通,再考虑是否切换到 Alpine。
分层构建方面,Dockerfile 的写法很关键。我们把 Composer 安装和代码复制分开,利用 Docker 层缓存加速构建:
FROM php:8.2-fpm-alpine RUN apk add --no-cache \ libzip-dev \ icu-dev \ oniguruma-dev \ && docker-php-ext-install pdo_mysql mbstring zip intl opcache COPY --from=composer:2 /usr/bin/composer /usr/bin/composer WORKDIR /var/www/html COPY composer.json composer.lock ./ RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist COPY . . RUN composer dump-autoload --optimize --no-dev RUN chown -R www-data:www-data storage bootstrap/cache这样写的好处是:只要composer.json和composer.lock没变,Composer 安装这一层就会命中缓存,不用每次重新下载依赖。代码复制放在后面,代码改动时只重新构建最后几层,构建速度能快好几倍。
2.2 环境变量与配置管理
PHP 项目通常用.env文件管理配置,但容器化之后不应该把.env打进镜像,因为不同环境配置不同,而且敏感信息不能写在代码仓库里。我们的做法是:镜像里只保留.env.example,实际运行时通过 K8s 的 ConfigMap 和 Secret 注入环境变量。
ConfigMap 存非敏感配置,比如APP_ENV、APP_DEBUG、LOG_LEVEL、DB_HOST、REDIS_HOST这些。Secret 存敏感信息,比如数据库密码、API 密钥、JWT Secret。然后在 Deployment 里通过envFrom引用:
envFrom: - configMapRef: name: php-app-config - secretRef: name: php-app-secret这里有个细节要注意:PHP-FPM 默认不会把环境变量传递给 PHP 脚本,需要在 FPM 池配置里设置clear_env = no。我们一开始没注意这个,结果容器里getenv()拿不到任何环境变量,排查了半天。后来在 Dockerfile 里加了一行:
RUN echo "clear_env = no" >> /usr/local/etc/php-fpm.d/www.conf另外,Laravel 这类框架有配置缓存机制,php artisan config:cache会把配置缓存到文件里。如果配置来自环境变量,必须在容器启动时重新生成缓存,否则改环境变量不生效。我们在 Entrypoint 脚本里加了判断,启动时自动执行php artisan config:cache和php artisan route:cache。
2.3 存储与日志处理方案
PHP 应用通常需要写日志、存缓存、上传文件。容器里的文件系统是临时的,Pod 重启后数据就没了。所以需要区分哪些数据要持久化,哪些可以丢。
日志我们直接输出到 stdout/stderr,由 K8s 的日志收集组件统一采集。这样就不需要在容器里管理日志文件轮转,也方便在 Kibana 或 Grafana 里统一查看。Laravel 项目可以在config/logging.php里把默认 channel 改成stderr。
上传文件和用户生成的内容需要持久化,我们挂载了 PVC。但 PVC 有个限制:ReadWriteMany 类型的存储不是所有 K8s 集群都支持。如果多个 Pod 需要同时读写同一份文件,要么用支持 RWX 的存储类(比如 NFS、CephFS),要么把文件存储改成对象存储(比如 S3 兼容的 MinIO),应用层直接调 SDK 上传。我们后来选了后者,因为对象存储扩展性更好,也不受 K8s 存储类限制。
缓存方面,我们用了 Redis,不依赖本地文件。Session 也存 Redis,这样多个 Pod 之间可以共享会话,用户请求打到哪个 Pod 都不影响。
2.4 PHP-FPM 与 Nginx 的容器编排方式
传统部署里 Nginx 和 PHP-FPM 通常装在同一台机器上,Nginx 通过 FastCGI 把 PHP 请求转发给 FPM。容器化之后有两种做法:一种是把 Nginx 和 FPM 打进同一个镜像,用 Supervisor 管理两个进程;另一种是分成两个容器,放在同一个 Pod 里,通过 localhost 通信。
我们选了第二种,原因是职责分离更清晰,Nginx 容器和 FPM 容器可以独立更新,日志也分开收集。同一个 Pod 内容器共享网络命名空间,Nginx 配置里fastcgi_pass 127.0.0.1:9000就能直接连到 FPM。
Nginx 配置通过 ConfigMap 挂载,这样改配置不需要重新构建镜像。但要注意,ConfigMap 更新后 Pod 里的文件不会自动更新,需要重启 Pod 或者用 Reloader 这类工具监听 ConfigMap 变化自动滚动更新。
3. Jenkins CI/CD 流水线搭建实录
Jenkins 流水线是整个方案的核心枢纽,它把代码提交、构建、测试、镜像推送、K8s 部署串成一条自动化链路。这一章我把流水线的每个阶段拆开讲,包括 Jenkins 本身的部署方式、Pipeline 脚本怎么写、以及各个阶段的关键配置。
3.1 Jenkins 在 Kubernetes 中的部署方式
Jenkins 本身我们也跑在 K8s 上,用 Helm Chart 安装。这样 Jenkins 的配置、插件、Job 定义都可以通过代码管理,迁移和备份也方便。Helm 安装命令大概是这样:
helm repo add jenkins https://charts.jenkins.io helm repo update helm install jenkins jenkins/jenkins \ --namespace jenkins \ --create-namespace \ --set controller.serviceType=ClusterIP \ --set persistence.enabled=true \ --set persistence.size=50Gi安装完成后通过 Ingress 暴露 Jenkins Web UI,域名比如jenkins.example.com。Ingress 配置里要注意设置proxy-body-size,否则上传大文件或构建产物时可能被 Nginx 拦截。
Jenkins 的 Agent 我们用 Kubernetes Plugin 动态创建。每次流水线运行时,Jenkins 自动在 K8s 集群里创建一个 Pod 作为构建环境,构建完成后 Pod 自动销毁。这样做的好处是构建环境隔离,不同项目可以用不同的构建镜像,互不影响。Agent Pod 的模板里可以指定构建镜像,比如我们用了一个预装了 PHP、Composer、Docker CLI、kubectl 的镜像作为构建环境。
3.2 声明式 Pipeline 脚本结构拆解
我们的 Jenkinsfile 放在项目根目录,跟代码一起版本管理。整体结构分为几个 Stage:Checkout、Install Dependencies、Test、Build Image、Push Image、Deploy、Verify。
pipeline { agent { kubernetes { yaml ''' apiVersion: v1 kind: Pod spec: containers: - name: builder image: registry.example.com/build/php-builder:8.2 command: ["cat"] tty: true - name: docker image: docker:24-dind securityContext: privileged: true ''' } } environment { REGISTRY = 'registry.example.com' IMAGE_NAME = "php-app" IMAGE_TAG = "${env.BUILD_NUMBER}-${env.GIT_COMMIT.take(7)}" } stages { stage('Checkout') { steps { checkout scm } } stage('Install Dependencies') { steps { container('builder') { sh 'composer install --no-dev --prefer-dist --optimize-autoloader' } } } stage('Test') { steps { container('builder') { sh 'vendor/bin/phpunit --coverage-text' } } } stage('Build Image') { steps { container('docker') { sh """ docker build -t ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} . """ } } } stage('Push Image') { steps { container('docker') { sh """ docker push ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} """ } } } stage('Deploy') { steps { container('builder') { sh """ kubectl set image deployment/php-app \ php-fpm=${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \ -n production """ } } } stage('Verify') { steps { container('builder') { sh 'kubectl rollout status deployment/php-app -n production --timeout=120s' } } } } }这个脚本里几个关键点:agent部分定义了构建 Pod 的模板,包含 builder 和 docker 两个容器;environment定义了镜像仓库地址和镜像标签规则,我们用构建号和 Git commit 短哈希组合,保证每次构建的镜像标签唯一且可追溯;Deploy阶段用kubectl set image更新 Deployment 的镜像,触发滚动更新;Verify阶段用kubectl rollout status等待更新完成,如果超时或失败,流水线会标记为失败。
3.3 镜像构建与推送的优化实践
镜像构建阶段有几个优化点值得说。第一,用 Docker BuildKit 加速构建,开启方式是在构建命令前加DOCKER_BUILDKIT=1,或者在 Jenkins Agent 的 Docker 配置里默认开启。BuildKit 支持并行构建和更好的缓存管理,构建速度能提升不少。
第二,镜像标签策略。我们除了用BUILD_NUMBER-GIT_COMMIT这种唯一标签,还会额外打一个latest标签指向最新版本。但 K8s Deployment 里不要用latest,因为latest标签不变时 K8s 不会触发滚动更新。必须用唯一标签,这样每次kubectl set image都能检测到变化。
第三,镜像推送前做安全扫描。我们用 Trivy 扫描镜像里的漏洞,如果发现高危漏洞就中断流水线。扫描命令集成在 Push 之前:
trivy image --exit-code 1 --severity HIGH,CRITICAL ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}第四,镜像仓库用 Harbor,支持镜像复制、漏洞扫描、RBAC 权限控制。Harbor 的 Webhook 还可以在镜像推送后触发其他动作,比如通知或自动部署。
3.4 部署策略与回滚机制
K8s 的 Deployment 默认滚动更新策略是RollingUpdate,可以通过maxSurge和maxUnavailable控制更新速度。我们设置maxSurge: 1、maxUnavailable: 0,意思是更新时先创建一个新 Pod,等新 Pod 就绪后再删一个旧 Pod,保证服务不中断。
strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0回滚方面,K8s 保留了 Deployment 的修订历史,默认保留 10 个版本。回滚命令很简单:
kubectl rollout undo deployment/php-app -n production但手动敲命令不够方便,我们在 Jenkins 里做了一个参数化构建,可以选择“部署”或“回滚”。回滚时指定要回滚到的修订版本号,Jenkins 执行kubectl rollout undo --to-revision=N。这样开发和运维人员不需要记命令,在 Jenkins 界面上点几下就能完成回滚。
还有一个细节:kubectl rollout status默认超时时间比较短,如果镜像拉取慢或 Pod 启动慢,可能还没就绪就超时了。我们把它设成 120 秒,并且配合progressDeadlineSeconds一起用。如果超过这个时间还没完成,流水线失败并触发告警。
4. Kubernetes 资源编排与 Ingress 配置
K8s 这边的资源定义是整个部署的“最终形态”,所有配置都通过 YAML 文件声明。这一章我把 Deployment、Service、Ingress、HPA、ConfigMap、Secret 这些资源的关键配置逐一说明,并解释每个参数背后的考量。
4.1 Deployment 配置要点与健康检查
Deployment 是 PHP 应用的核心资源定义。除了基本的镜像、副本数、端口配置,健康检查是最关键的部分。K8s 支持三种探针:Liveness Probe、Readiness Probe、Startup Probe。
Liveness Probe 判断容器是否存活,如果失败会重启容器。Readiness Probe 判断容器是否准备好接收流量,如果失败会从 Service 的 Endpoints 里移除。Startup Probe 用于启动慢的应用,在 Startup Probe 成功之前,Liveness 和 Readiness 都不会执行。
PHP-FPM 容器我们配置了 TCP 探针检查 9000 端口,Nginx 容器配置了 HTTP 探针检查/health路径。/health这个接口在 PHP 里实现,返回应用状态,包括数据库连接、Redis 连接是否正常。
apiVersion: apps/v1 kind: Deployment metadata: name: php-app namespace: production spec: replicas: 3 selector: matchLabels: app: php-app template: metadata: labels: app: php-app spec: containers: - name: php-fpm image: registry.example.com/php-app:latest ports: - containerPort: 9000 resources: requests: cpu: 250m memory: 256Mi limits: cpu: 1000m memory: 512Mi livenessProbe: tcpSocket: port: 9000 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: exec: command: - php - /var/www/html/artisan - health:check initialDelaySeconds: 5 periodSeconds: 5 envFrom: - configMapRef: name: php-app-config - secretRef: name: php-app-secret volumeMounts: - name: nginx-config mountPath: /etc/nginx/conf.d - name: nginx image: nginx:1.25-alpine ports: - containerPort: 80 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi readinessProbe: httpGet: path: /health port: 80 initialDelaySeconds: 5 periodSeconds: 5 volumes: - name: nginx-config configMap: name: nginx-config资源限制这块,requests是调度时预留的资源,limits是容器能使用的上限。PHP-FPM 的pm.max_children要根据内存限制来算。假设每个 PHP 进程平均占 30MB 内存,容器内存限制 512MB,那么pm.max_children最多设 15 左右,留一些余量给系统进程。设太大容易 OOM,设太小并发处理能力不足。
4.2 Service 与 Ingress 的流量接入配置
Service 给 Pod 提供一个稳定的访问入口。我们创建了一个 ClusterIP 类型的 Service,只暴露 Nginx 的 80 端口,PHP-FPM 的 9000 端口不需要对外暴露,只在 Pod 内部访问。
apiVersion: v1 kind: Service metadata: name: php-app-service namespace: production spec: selector: app: php-app ports: - name: http port: 80 targetPort: 80 type: ClusterIPIngress 负责把外部流量路由到 Service。我们用 Nginx Ingress Controller,配置了域名、TLS 证书、路径规则。TLS 证书通过 cert-manager 自动申请和续期,不用手动管理。
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: php-app-ingress namespace: production annotations: nginx.ingress.kubernetes.io/proxy-body-size: "50m" nginx.ingress.kubernetes.io/proxy-read-timeout: "300" nginx.ingress.kubernetes.io/proxy-send-timeout: "300" cert-manager.io/cluster-issuer: letsencrypt-prod spec: ingressClassName: nginx tls: - hosts: - api.example.com secretName: php-app-tls rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: php-app-service port: number: 80几个注解值得说明:proxy-body-size控制上传文件大小限制,默认是 1MB,PHP 项目经常需要上传文件,设成 50MB 比较合理;proxy-read-timeout和proxy-send-timeout控制超时时间,PHP 处理耗时请求时默认 60 秒可能不够,设成 300 秒;cert-manager.io/cluster-issuer指定证书签发者,cert-manager 会自动创建和续期 TLS 证书。
4.3 HPA 自动扩缩容配置
HPA 根据 CPU 或内存使用率自动调整 Pod 副本数。我们配置了基于 CPU 的扩缩容,目标使用率 70%,最少 3 个副本,最多 20 个副本。
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: php-app-hpa namespace: production spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-app minReplicas: 3 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70HPA 要生效,Deployment 里必须配置resources.requests.cpu,否则 HPA 无法计算使用率。另外,HPA 扩缩容有冷却时间,扩容默认 3 分钟,缩容默认 5 分钟,避免频繁抖动。如果业务流量波动大,可以调整behavior字段控制扩缩容速度。
4.4 ConfigMap 与 Secret 的更新策略
ConfigMap 和 Secret 更新后,挂载到 Pod 里的文件不会自动更新,环境变量也不会自动刷新。我们的做法是:配置变更时,更新 ConfigMap 后手动触发一次滚动更新,让新 Pod 加载新配置。
kubectl rollout restart deployment/php-app -n production也可以用 Reloader 这类工具自动监听 ConfigMap 变化并触发滚动更新。但自动更新有风险,如果配置写错了,所有 Pod 同时重启可能全部启动失败。我们更倾向于手动触发,至少在预发环境验证过再更新生产。
Secret 的管理要更谨慎。我们不允许在代码仓库里明文存 Secret,而是用 Sealed Secrets 或 External Secrets Operator 从外部密钥管理系统同步。这样即使代码仓库泄露,敏感信息也不会暴露。
5. 常见问题排查与实战避坑指南
这套方案跑了一年多,踩过的坑不少。这一章我把典型问题和解决方法整理出来,希望能帮你少走弯路。
5.1 镜像构建与推送常见故障
问题一:Composer 安装超时。构建时composer install卡住或超时,通常是因为网络问题。解决方法是在构建镜像里配置 Composer 镜像源,或者用企业内部的 Composer 代理。我们在composer.json里配置了config.repositories指向内部 Packagist 镜像,构建速度稳定很多。
问题二:镜像推送失败,提示 no space left on device。Jenkins Agent 的 Docker 数据目录满了。解决方法是定期清理无用镜像和构建缓存,或者在 Agent 模板里挂载独立的 Docker 数据卷。我们加了一个构建后步骤,自动执行docker system prune -f --filter "until=24h"。
问题三:镜像标签冲突。多个流水线并发构建时,如果都用latest标签,可能互相覆盖。解决方法是每次构建用唯一标签,latest只作为辅助标签,部署时用唯一标签。
5.2 K8s 部署与运行时的典型异常
问题一:Pod 一直处于 Pending 状态。通常是资源不足或调度约束不满足。用kubectl describe pod查看事件,如果是Insufficient cpu或Insufficient memory,说明节点资源不够,需要扩容节点或降低requests。如果是nodeSelector或affinity不满足,检查节点标签是否正确。
问题二:Pod 启动后反复重启。用kubectl logs查看容器日志,如果是 PHP-FPM 启动失败,检查配置文件是否正确挂载。我们遇到过一次,ConfigMap 里的 Nginx 配置有语法错误,Nginx 启动失败导致 Pod 重启。解决方法是本地用nginx -t验证配置后再更新 ConfigMap。
问题三:Readiness Probe 失败,Pod 无法接入 Service。检查探针配置的路径和端口是否正确,以及应用是否真的准备好了。PHP 应用启动时可能需要连接数据库、加载配置,如果这些操作耗时较长,initialDelaySeconds要设大一些,或者用 Startup Probe 代替。
问题四:滚动更新卡住,新 Pod 一直不就绪。用kubectl rollout status查看状态,用kubectl describe deployment查看事件。常见原因是镜像拉取失败、资源不足、探针配置不当。我们遇到过一次,新镜像的 PHP 版本升级了,但某个扩展没装,导致 FPM 启动失败。解决方法是构建镜像后先在本地跑一遍,确认能正常启动再推送。
5.3 Jenkins 流水线调试技巧
技巧一:用sh步骤时加set -x。这样命令执行时会打印出来,方便定位哪一步出错。但注意不要在生产环境的敏感操作里加,避免泄露密钥。
技巧二:用post块处理成功和失败。流水线成功时发通知,失败时也发通知并保留现场。我们配置了失败时自动收集 Pod 日志和事件,附加到构建记录里,方便排查。
技巧三:用when条件控制阶段执行。比如只有main分支才部署到生产,其他分支只构建和测试。这样避免误操作。
stage('Deploy to Production') { when { branch 'main' } steps { // 部署逻辑 } }技巧四:用timeout包裹可能卡住的步骤。比如镜像构建、部署等待,设置超时时间,避免流水线无限期挂起。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Pod Pending | 资源不足、调度约束 | kubectl describe pod | 扩容节点或调整 requests |
| Pod 反复重启 | 配置错误、启动失败 | kubectl logs | 检查配置文件和启动命令 |
| Readiness 失败 | 探针配置不当 | kubectl describe pod | 调整探针参数或应用启动逻辑 |
| 滚动更新卡住 | 镜像拉取失败、资源不足 | kubectl rollout status | 检查镜像地址和资源配额 |
| Ingress 502 | Service 无可用 Endpoints | kubectl get endpoints | 检查 Pod 就绪状态和 Service selector |
| HPA 不生效 | 缺少 resources.requests | kubectl describe hpa | 配置 CPU requests |
| ConfigMap 不更新 | 挂载文件不自动刷新 | 检查 Pod 内文件 | 手动触发滚动更新 |
| 镜像推送慢 | 网络带宽不足 | 查看推送日志 | 用内网镜像仓库或压缩镜像 |
6. 个人实操体会与后续扩展方向
这套方案从最初设计到稳定运行,前后折腾了大概两个月。中间经历过几次生产事故,也积累了一些文档里不会写的经验。
第一个体会是:不要一次性把所有东西都上齐。我们一开始就想把 HPA、Istio、Prometheus 监控全部集成进去,结果复杂度太高,出了问题排查困难。后来退回到最小可用方案,先跑通 Deployment + Service + Ingress + Jenkins 流水线,稳定之后再逐步加 HPA、监控、告警。这样每一步都有基线,出问题容易定位。
第二个体会是:PHP 项目的容器化改造,最难的不是 K8s 配置,而是应用本身的适配。比如环境变量读取、日志输出方式、文件存储路径、Session 管理,这些都需要改代码。如果项目历史包袱重,建议先在新项目上试点,跑通后再逐步迁移老项目。
第三个体会是:回滚机制一定要提前验证。我们第一次回滚是在生产出问题时,结果发现回滚命令执行了但 Pod 没起来,因为旧版本的镜像被清理了。后来我们规定镜像仓库保留至少 30 天的历史版本,并且每次发布后自动验证回滚流程。
后续扩展方向,我们正在做的是把 Jenkins 流水线迁移到 GitOps 模式,用 Argo CD 做持续部署。Jenkins 只负责构建和推送镜像,Argo CD 监听镜像仓库变化自动同步到 K8s 集群。这样部署状态更可控,也更容易做多集群管理。另外还在探索用 OpenTelemetry 做全链路追踪,把 PHP 应用的性能监控和 K8s 事件关联起来,提升故障排查效率。
如果你也在做类似的事情,我的建议是先把 CI 流水线跑通,再搞 CD;先把单环境跑稳,再搞多环境;先把手动回滚练熟,再搞自动回滚。每一步都验证过再往下走,比一口气全上要靠谱得多。