信创云原生:鲲鹏 K8s 上的 PHP 容器化与弹性伸缩

📅 2026/8/21 14:31:19
信创云原生:鲲鹏 K8s 上的 PHP 容器化与弹性伸缩
第 10 章 信创云原生鲲鹏 K8s 上的 PHP 容器化与弹性伸缩10.1 先讲清楚 ARM64 容器化最容易翻车的一件事大白话你在自己的 x86 笔记本上docker build出来的镜像在鲲鹏服务器上跑不起来报错长这样exec /usr/local/bin/docker-php-entrypoint: exec format error这句话的意思是「这个二进制文件的架构和 CPU 不匹配」。因为镜像里的 PHP 是 x86_64 编译的鲲鹏是 aarch64。解决办法有两条优先选第 1 条直接在鲲鹏机器上构建最省事、最可靠本章走这条在 x86 上用buildx交叉构建 arm64 镜像构建慢 5-10 倍因为走 QEMU 模拟但适合 CI 统一出多架构镜像。先确认你的基础镜像有 arm64 版本# 在任意机器上查某个镜像支持哪些架构dockermanifest inspect php:8.3-fpm-alpine|grep-A2platform# 期望能看到 architecture: arm64# 在鲲鹏机器上拉下来后必须核对dockerrun--rmphp:8.3-fpm-alpineuname-m# 期望 aarch6410.2 Dockerfile鲲鹏 ARM64 达梦 Swoole信创环境的镜像有三个额外要求基础镜像要能拉到内网通常有 Harbor 私仓、要装达梦 ODBC 驱动、要装 Swoole。# 文件名Dockerfile # 说明多阶段构建。第一阶段编译扩展第二阶段只留运行时镜像能小 60%。 # 构建命令在鲲鹏机器上执行 # docker build -t harbor.gov.local/php/hyperf-app:1.0.0 . # ---------- 第一阶段构建 ---------- FROM php:8.3-cli-bookworm AS builder # 换成国内源否则 apt 慢到超时 RUN sed -i sdeb.debian.orgmirrors.aliyun.comg /etc/apt/sources.list.d/debian.sources RUN apt-get update apt-get install -y --no-install-recommends \ build-essential autoconf pkg-config \ libssl-dev libcurl4-openssl-dev libzip-dev \ unixodbc unixodbc-dev \ libonig-dev \ rm -rf /var/lib/apt/lists/* # 装 PHP 官方扩展docker-php-ext-install 是官方镜像自带的脚本 RUN docker-php-ext-install -j$(nproc) \ pdo pdo_odbc bcmath sockets pcntl opcache zip # 装 SwooleARM64 上要显式开 cares见第 3 章 RUN pecl install swoole-5.1.3 \ docker-php-ext-enable swoole # 装 Redis 扩展连 TongRDS 用 RUN pecl install redis-6.0.2 \ docker-php-ext-enable redis # ---------- 第二阶段运行时 ---------- FROM php:8.3-cli-bookworm RUN sed -i sdeb.debian.orgmirrors.aliyun.comg /etc/apt/sources.list.d/debian.sources # 运行时只装动态库不装编译工具 RUN apt-get update apt-get install -y --no-install-recommends \ unixodbc libzip4 curl \ rm -rf /var/lib/apt/lists/* # 从构建阶段拷贝编译好的扩展 COPY --frombuilder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/ COPY --frombuilder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/ # 达梦 ODBC 驱动把宿主机上解压好的客户端目录放进构建上下文再 COPY # 达梦客户端要官网下载注意下 aarch64 版本不是 x86_64 版本 COPY dm8_client/ /opt/dm8/ COPY odbc.ini /etc/odbc.ini COPY odbcinst.ini /etc/odbcinst.ini ENV LD_LIBRARY_PATH/opt/dm8/drivers:$LD_LIBRARY_PATH # PHP 配置 COPY php.ini /usr/local/etc/php/php.ini WORKDIR /app COPY --chownwww-data:www-data . /app # 装依赖--no-dev 去掉开发包体积和攻击面都小 COPY --fromcomposer:2 /usr/bin/composer /usr/bin/composer RUN composer install --no-dev --optimize-autoloader --no-interaction \ rm -rf ~/.composer # 不要用 root 跑业务进程等保测评会查这一条 USER www-data EXPOSE 9501 # 健康检查K8s 的 probe 之外再加一层docker 单机跑也能看状态 HEALTHCHECK --interval15s --timeout3s --start-period20s --retries3 \ CMD curl -fsS http://127.0.0.1:9501/health || exit 1 CMD [php, bin/hyperf.php, start]配套的odbcinst.ini告诉 unixODBC 达梦驱动在哪[DM8 ODBC DRIVER] Description DaMeng ODBC Driver Driver /opt/dm8/drivers/libdodbc.so Setup /opt/dm8/drivers/libdodbc.so FileUsage 1配套的php.ini关键项容器里和裸机不同的地方; 容器里日志要走 stdout/stderrK8s 才能采集到 display_errors Off log_errors On error_log /dev/stderr memory_limit 512M date.timezone Asia/Shanghai [opcache] opcache.enable 1 opcache.enable_cli 1 opcache.memory_consumption 256 opcache.max_accelerated_files 20000 ; 容器镜像不可变代码不会变关掉时间戳检查省一次 stat 系统调用 opcache.validate_timestamps 0 ; ARM64 上按第 3 章的结论关掉 JIT opcache.jit disable构建并推到内网 Harbor# 在鲲鹏机器上构建架构天然匹配dockerbuild-tharbor.gov.local/php/hyperf-app:1.0.0.# 关键验证确认镜像是 arm64别推错了dockerinspect harbor.gov.local/php/hyperf-app:1.0.0--format{{.Architecture}}# 期望输出arm64dockerlogin harbor.gov.localdockerpush harbor.gov.local/php/hyperf-app:1.0.010.3 如果必须在 x86 CI 上出 ARM 镜像buildx 方案# 1. 装 QEMU 模拟器让 x86 内核能执行 arm64 二进制dockerrun--privileged--rmtonistiigi/binfmt--installarm64# 2. 建一个支持多架构的 builderdockerbuildx create--namexc-builder--usedockerbuildx inspect--bootstrap# 3. 同时出 amd64 和 arm64直接推仓库多架构镜像必须 --push不能 --loaddockerbuildx build\--platformlinux/amd64,linux/arm64\-tharbor.gov.local/php/hyperf-app:1.0.0\--push.这样鲲鹏和海光两种服务器可以拉同一个 tagDocker 会自动选对应架构。代价是构建时间长arm64 那一半走模拟pecl 编译 Swoole 可能要 10 分钟以上。10.4 K8s 部署清单Deployment Service 探针先给应用加一个健康检查接口探针要用?php// app/Controller/HealthController.phpnamespaceApp\Controller;useHyperf\HttpServer\Annotation\Controller;useHyperf\HttpServer\Annotation\GetMapping;useHyperf\DbConnection\Db;#[Controller]classHealthController{// 存活探针只证明进程没死不查外部依赖// 这里要是查了数据库数据库一抖 K8s 就会把好好的 Pod 全杀掉重启#[GetMapping(/health)]publicfunctionhealth(){return[statusok];}// 就绪探针查依赖依赖不通就摘流量但不重启#[GetMapping(/ready)]publicfunctionready(){$checks[];try{Db::connection(dm8)-select(SELECT 1 FROM DUAL);$checks[dameng]ok;}catch(\Throwable$e){$checks[dameng]fail;}try{$redis\Hyperf\Context\ApplicationContext::getContainer()-get(\Hyperf\Redis\Redis::class);$redis-ping();$checks[tongrds]ok;}catch(\Throwable$e){$checks[tongrds]fail;}$healthy!in_array(fail,$checks,true);return$this-response-withStatus($healthy?200:503)-json([status$healthy?ready:not_ready,checks$checks]);}}Deployment# 文件名k8s/deployment.yamlapiVersion:apps/v1kind:Deploymentmetadata:name:hyperf-appnamespace:gov-appspec:replicas:3revisionHistoryLimit:5selector:matchLabels:app:hyperf-appstrategy:type:RollingUpdaterollingUpdate:maxSurge:1maxUnavailable:0# 保证升级过程中容量不下降template:metadata:labels:app:hyperf-appannotations:prometheus.io/scrape:trueprometheus.io/port:9501prometheus.io/path:/metricsspec:# 关键把 Pod 钉在 arm64 节点上。混合集群里少了这段会随机调度到海光节点然后崩nodeSelector:kubernetes.io/arch:arm64# 把副本尽量打散到不同物理节点单机宕机不会全挂topologySpreadConstraints:-maxSkew:1topologyKey:kubernetes.io/hostnamewhenUnsatisfiable:ScheduleAnywaylabelSelector:matchLabels:app:hyperf-appsecurityContext:runAsNonRoot:truerunAsUser:33# www-datafsGroup:33containers:-name:appimage:harbor.gov.local/php/hyperf-app:1.0.0imagePullPolicy:IfNotPresentports:-containerPort:9501name:httpenv:-name:APP_ENVvalue:prod-name:DB_HOSTvalue:dm8.gov-db.svc.cluster.local-name:DB_PORTvalue:5236-name:DB_DATABASEvalue:MYAPP-name:DB_USERNAMEvalueFrom:secretKeyRef:name:app-secretskey:db-username-name:DB_PASSWORDvalueFrom:secretKeyRef:name:app-secretskey:db-password-name:REDIS_HOSTvalue:tongrds.gov-db.svc.cluster.local-name:REDIS_PASSWORDvalueFrom:secretKeyRef:name:app-secretskey:redis-passwordresources:requests:cpu:500mmemory:256Milimits:cpu:2000mmemory:1GistartupProbe:# 给启动留足时间避免慢启动被误杀httpGet:path:/healthport:9501failureThreshold:30periodSeconds:2livenessProbe:httpGet:path:/healthport:9501initialDelaySeconds:10periodSeconds:15timeoutSeconds:3failureThreshold:3readinessProbe:httpGet:path:/readyport:9501initialDelaySeconds:5periodSeconds:10timeoutSeconds:3failureThreshold:2lifecycle:preStop:# 先睡 5 秒等 Service 把自己从 endpoints 摘掉再让进程退出# 不加这段滚动更新时会有几百个请求打到正在关闭的 Pod 上exec:command:[sh,-c,sleep 5]securityContext:allowPrivilegeEscalation:falsereadOnlyRootFilesystem:false# Hyperf 要写 runtime/,设 true 需挂 emptyDircapabilities:drop:[ALL]terminationGracePeriodSeconds:30imagePullSecrets:-name:harbor-credSecret 和 Service# 文件名k8s/secret.yaml# 生产环境别把 Secret 提交到 git,用 kubectl create secret 或密钥管理系统注入apiVersion:v1kind:Secretmetadata:name:app-secretsnamespace:gov-apptype:OpaquestringData:db-username:myappdb-password:MyApp2024redis-password:TongRDS2024---apiVersion:v1kind:Servicemetadata:name:hyperf-appnamespace:gov-appspec:type:ClusterIPselector:app:hyperf-appports:-name:httpport:80targetPort:9501用命令行创建 Secret推荐避免明文进版本库kubectl create secret generic app-secrets-ngov-app\--from-literaldb-usernamemyapp\--from-literaldb-passwordMyApp2024\--from-literalredis-passwordTongRDS2024kubectl create secret docker-registry harbor-cred-ngov-app\--docker-serverharbor.gov.local\--docker-usernamedeployer\--docker-passwordpassword部署并验证kubectl create namespace gov-app kubectl apply-fk8s/deployment.yaml-fk8s/secret.yaml kubectl-ngov-app rollout status deploy/hyperf-app kubectl-ngov-app get pods-owide# 排错三连Pod 起不来时按顺序看kubectl-ngov-app describe podpod-name# 看事件镜像拉不到调度失败探针失败kubectl-ngov-app logspod-name--tail100# 看应用日志kubectl-ngov-app logspod-name--previous# 看上一次崩溃前的日志10.5 弹性伸缩HPAHPA 要能读到 Pod 的 CPU 用量前提是集群装了 metrics-serverkubectltopnodes# 有输出说明 metrics-server 正常kubectltoppods-ngov-app基于 CPU 和内存的基础 HPA# 文件名k8s/hpa.yamlapiVersion:autoscaling/v2kind:HorizontalPodAutoscalermetadata:name:hyperf-appnamespace:gov-appspec:scaleTargetRef:apiVersion:apps/v1kind:Deploymentname:hyperf-appminReplicas:3maxReplicas:20metrics:-type:Resourceresource:name:cputarget:type:UtilizationaverageUtilization:60# 超过 request 的 60% 就扩-type:Resourceresource:name:memorytarget:type:UtilizationaverageUtilization:75behavior:scaleUp:# 政务系统常见早高峰突增,允许快速扩容stabilizationWindowSeconds:30policies:-type:Percentvalue:100# 每次最多翻倍periodSeconds:30-type:Podsvalue:4# 或每 30 秒最多加 4 个periodSeconds:30selectPolicy:MaxscaleDown:# 缩容要慢,防止流量抖动导致反复扩缩(抖动会让长连接反复断开)stabilizationWindowSeconds:300policies:-type:Podsvalue:1periodSeconds:60为什么 Swoole 应用的 HPA 要特别小心 CPU 阈值Swoole 是常驻进程空闲时 CPU 接近 0但一旦压上来 CPU 会瞬间冲高。如果阈值设到 80%等 HPA 反应过来默认 15 秒采集周期 稳定窗口请求已经堆积了。建议设 50%-60%用容量换响应速度。对长连接服务第 5 章的 WebSocketCPU 不是好指标——连接数才是。用 Prometheus Adapter 暴露自定义指标# 文件名k8s/hpa-websocket.yamlapiVersion:autoscaling/v2kind:HorizontalPodAutoscalermetadata:name:gov-chatnamespace:gov-appspec:scaleTargetRef:apiVersion:apps/v1kind:Deploymentname:gov-chatminReplicas:2maxReplicas:30metrics:-type:Podspods:metric:name:swoole_connection_num# 第 8 章 /metrics 暴露的指标target:type:AverageValueaverageValue:3000# 每个 Pod 平均超过 3000 连接就扩behavior:scaleDown:# 长连接缩容更要慢:缩一个 Pod 就断 3000 个用户,他们会同时重连打爆剩下的 PodstabilizationWindowSeconds:900policies:-type:Podsvalue:1periodSeconds:300验证伸缩是否生效kubectl apply-fk8s/hpa.yaml kubectl-ngov-app get hpa-w# 压测触发扩容(在集群内起个临时 Pod 压)kubectl-ngov-app run bench--rm-it--restartNever\--imagewilliamyeh/wrk ---t4-c400-d120shttp://hyperf-app/api/users/1# 另一个终端观察副本数变化kubectl-ngov-app get hpa hyperf-app-w# READY 列的副本数应从 3 逐步涨上去10.6 长连接服务在 K8s 上的额外配置WebSocket 走 Ingress 需要显式加超时和会话保持否则连接会被 60 秒切断# 文件名k8s/ingress-ws.yamlapiVersion:networking.k8s.io/v1kind:Ingressmetadata:name:gov-chatnamespace:gov-appannotations:nginx.ingress.kubernetes.io/proxy-read-timeout:3600nginx.ingress.kubernetes.io/proxy-send-timeout:3600# 会话保持:让同一个客户端始终连到同一个 Podnginx.ingress.kubernetes.io/affinity:cookienginx.ingress.kubernetes.io/session-cookie-name:chat-routenginx.ingress.kubernetes.io/session-cookie-max-age:3600spec:ingressClassName:nginxrules:-host:chat.example.gov.cnhttp:paths:-path:/pathType:Prefixbackend:service:name:gov-chatport:number:80Service 侧也要延长会话亲和时间apiVersion:v1kind:Servicemetadata:name:gov-chatnamespace:gov-appspec:type:ClusterIPsessionAffinity:ClientIPsessionAffinityConfig:clientIP:timeoutSeconds:3600selector:app:gov-chatports:-port:80targetPort:828210.7 K8s 上的信创合规要点测评会查的四条# 1. 禁止容器以 root 运行 —— 用 Pod Security Standards 在命名空间级别强制apiVersion:v1kind:Namespacemetadata:name:gov-applabels:pod-security.kubernetes.io/enforce:restrictedpod-security.kubernetes.io/enforce-version:latest---# 2. 网络隔离 —— 默认拒绝只放行必要流量apiVersion:networking.k8s.io/v1kind:NetworkPolicymetadata:name:default-deny-ingressnamespace:gov-appspec:podSelector:{}policyTypes:[Ingress]---apiVersion:networking.k8s.io/v1kind:NetworkPolicymetadata:name:allow-from-ingress-and-dbnamespace:gov-appspec:podSelector:matchLabels:app:hyperf-apppolicyTypes:[Ingress,Egress]ingress:-from:-namespaceSelector:matchLabels:kubernetes.io/metadata.name:ingress-nginxports:-port:9501egress:# 只允许访问数据库命名空间和 DNS-to:-namespaceSelector:matchLabels:kubernetes.io/metadata.name:gov-db-to:-namespaceSelector:{}podSelector:matchLabels:k8s-app:kube-dnsports:-port:53protocol:UDP剩下两条是运维动作不是 YAML镜像来源可控所有镜像必须来自内网 Harbor禁止从 docker.io 直拉。用ImagePolicyWebhook或 Kyverno 策略拦截非法仓库。审计日志开启kube-apiserver 要加--audit-log-path和审计策略文件记录谁在什么时候改了什么资源。这条测评一定会要证据截图。10.8 排错手册容器化阶段的高频问题报错 / 现象真实原因怎么修exec format error镜像架构和节点架构不匹配在鲲鹏上重建镜像或用 buildx 出 arm64加nodeSelectorImagePullBackOffHarbor 凭据没配 / 镜像名写错kubectl describe pod看具体信息检查imagePullSecretsPod 反复重启日志无异常livenessProbe 查了数据库数据库慢就被杀存活探针只返回静态 ok依赖检查挪到 readinessProbe滚动更新时有 502旧 Pod 收到 SIGTERM 时还有流量进来加preStop: sleep 5maxUnavailable: 0SQLSTATE[IM002] data source name not found容器里没有/etc/odbc.ini或驱动路径错确认 COPY 了 odbc.ini/odbcinst.iniLD_LIBRARY_PATH指向/opt/dm8/driversWebSocket 每 60 秒断一次Ingress 默认读超时 60s加proxy-read-timeout: 3600HPA 一直显示unknownmetrics-server 没装或没就绪kubectl top pods验证容器必须设resources.requests副本扩了但 QPS 没涨达梦连接池被打满瓶颈在数据库算总连接数副本数 × max_connections 不能超过达梦的 max_sessions容器内存缓慢上涨被 OOMKilledSwoole 常驻进程有内存泄漏设max_request: 100000让 worker 定期重启用memory_get_usage定位关于最后一条特别提醒容器里的memory limit是硬限制超了直接 SIGKILL日志里看不到任何优雅退出信息只有Reason: OOMKilled。查这个要看kubectl-ngov-app describe podpod-name|grep-A3Last State# 期望能看到 Reason: OOMKilled 和 Exit Code: 137