K8s部署PHP应用实战:从踩坑到稳定

2026-07-16 15 min read 0

真实场景:PHP应用上K8s,第一天就挂了

2024年3月,我把一个日活50万的Laravel电商系统从单机迁移到K8s集群。上线第一天,Pod频繁重启,日志全是数据库连接超时。排查发现:PHP-FPM进程数没调、Session共享没做、Nginx Ingress配置错误。更坑的是,业务高峰期Pod自动扩容后,数据库连接数直接打满,MySQL挂了。

这篇文章记录我从踩坑到稳定的全过程。环境:K8s 1.28.2(kubeadm部署)、Docker 24.0.7、PHP 8.3.3、Laravel 11.0、MySQL 8.0.35、Redis 7.2.4。集群3个Worker节点,每节点4C16G。

方案对比:三种部署方式,我全试了一遍

部署PHP应用到K8s,核心要解决三个问题:PHP-FPM进程管理、Session共享、静态资源处理。我对比了三种方案:

方案适用场景QPS(压测)部署复杂度运维成本
Deployment + Service无状态PHP应用1200
StatefulSet + Headless Service有状态(如Session本地存储)2800
Operator模式(KubePHP)大规模集群,自动扩缩容4500低(自动化后)

最终我选择了方案一+外部Redis做Session共享,因为业务对性能要求没那么极端(峰值QPS 2000),且团队K8s经验不足。下面给出完整实现。

完整代码实现:从Dockerfile到Ingress

1. Dockerfile:PHP-FPM + Nginx 双容器

# Dockerfile - PHP-FPM
FROM php:8.3.3-fpm-alpine3.19 AS php-fpm
RUN apk add --no-cache \
    nginx \
    supervisor \
    libzip-dev \
    oniguruma-dev \
    && docker-php-ext-install pdo_mysql mbstring zip opcache

COPY --from=composer:2.7.1 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . .
RUN composer install --no-dev --optimize-autoloader

# 配置PHP-FPM
COPY php-fpm.conf /usr/local/etc/php-fpm.d/zzz-custom.conf
RUN echo "pm.max_children = 50" >> /usr/local/etc/php-fpm.d/zzz-custom.conf \
    && echo "pm.start_servers = 10" >> /usr/local/etc/php-fpm.d/zzz-custom.conf \
    && echo "pm.min_spare_servers = 5" >> /usr/local/etc/php-fpm.d/zzz-custom.conf \
    && echo "pm.max_spare_servers = 20" >> /usr/local/etc/php-fpm.d/zzz-custom.conf

# Nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
COPY default.conf /etc/nginx/conf.d/default.conf

EXPOSE 80 9000
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

2. Nginx配置:处理PHP-FPM代理

# default.conf
server {
    listen 80;
    server_name _;
    root /var/www/html/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # 静态资源缓存
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 7d;
        add_header Cache-Control "public, immutable";
    }
}

3. K8s Deployment:PHP应用无状态部署

# php-app-deployment.yaml
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:1.0.0
        ports:
        - containerPort: 9000
        env:
        - name: DB_HOST
          value: "mysql-service"
        - name: REDIS_HOST
          value: "redis-service"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          tcpSocket:
            port: 9000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          exec:
            command:
            - php
            - -r
            - "echo 'ready';"
          initialDelaySeconds: 5
          periodSeconds: 5
      - name: nginx
        image: nginx:1.25.4-alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx/conf.d
      volumes:
      - name: nginx-config
        configMap:
          name: nginx-config
---
apiVersion: v1
kind: Service
metadata:
  name: php-app-service
  namespace: production
spec:
  selector:
    app: php-app
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP

4. ConfigMap:Nginx配置注入

# nginx-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
  namespace: production
data:
  default.conf: |
    server {
        listen 80;
        server_name _;
        root /var/www/html/public;
        index index.php;
        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }
        location ~ \.php$ {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
        location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
            expires 7d;
            add_header Cache-Control "public, immutable";
        }
    }

5. Ingress:对外暴露服务

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: php-app-ingress
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
  ingressClassName: nginx
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: php-app-service
            port:
              number: 80

6. 数据库与Redis:StatefulSet部署

# mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: production
spec:
  serviceName: mysql
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0.35
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: root-password
        ports:
        - containerPort: 3306
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:
  - metadata:
      name: mysql-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 20Gi
---
apiVersion: v1
kind: Service
metadata:
  name: mysql-service
  namespace: production
spec:
  selector:
    app: mysql
  ports:
  - port: 3306
    targetPort: 3306
  clusterIP: None

效果数据:压测对比

使用wrk压测工具,测试环境:3个Worker节点,每个节点4C16G。压测命令:wrk -t12 -c400 -d30s http://api.example.com

部署方式平均QPSP99延迟(ms)错误率内存使用(Pod)
单机部署(对比)8503200.5%1.2GB
K8s Deployment(无优化)12002801.2%1.8GB
K8s Deployment(优化后)2800950.1%1.1GB
K8s StatefulSet3100820.05%1.0GB

优化点:PHP-FPM进程数从50降到20(根据CPU核数调整)、开启Opcache、使用Redis Session、Nginx开启gzip和静态缓存。优化后QPS提升133%,P99延迟降低66%。

避坑指南:我踩过的5个坑

坑1:PHP-FPM进程数设置过大导致OOM

一开始我设置pm.max_children=100,结果Pod内存直接飙到2GB,被OOM Killer杀掉。正确做法:根据Pod内存限制计算。公式:max_children = (内存限制 - 其他进程内存) / 每个PHP进程平均内存。我Pod限制512Mi,每个PHP进程约30Mi,所以设20。

坑2:Session存储在本地文件,Pod重启后丢失

默认Laravel Session存文件,Pod重启或扩缩容后用户登录状态全丢。解决方案:改用Redis存储Session。在.env设置:SESSION_DRIVER=redis,并确保Redis服务稳定。

坑3:Nginx Ingress的proxy-body-size限制

上传文件超过1MB就返回413。在Ingress注解加:nginx.ingress.kubernetes.io/proxy-body-size: "50m"。同时Nginx配置也要改client_max_body_size。

坑4:数据库连接池耗尽

Pod扩容到10个后,每个Pod建立30个数据库连接,总共300个连接,MySQL默认max_connections=151直接打满。解决:限制每个Pod的连接数(Laravel配置max_connections=10),同时调大MySQL的max_connections到500。

坑5:镜像拉取策略导致滚动更新失败

默认imagePullPolicy=IfNotPresent,更新镜像时如果本地有缓存就不拉新版本。必须显式设置imagePullPolicy: Always,或者给镜像打不同tag(不要用latest)。

总结

K8s部署PHP应用不是简单的docker run,需要理解PHP-FPM特性、K8s资源模型、网络模型。核心要点:无状态化(Session外置)、资源限制精确计算、健康检查配置合理、数据库连接池管理。这套方案上线后稳定运行6个月,零宕机。

IT搬运工

专注技术分享,坚持原创深度内容。

分类
链接
订阅

RSS 订阅关注最新文章

© 2026 IT搬运工 · 站点地图 · 鄂ICP备19007640号-3