资讯动态

PHP应用基于Kubernetes HPA的自动伸缩改造实战指南

发布时间:2026/10/15 5:07:54 来源:尧图企业网站定制
最近在折腾一套PHP业务系统的伸缩改造。背景特别典型日常请求量很平稳一到活动窗口或者批量数据任务跑起来并发能瞬间冲到平日的几十倍。之前一直是运维值班盯监控流量一上来就手动加机器流量下去再手动减总担心加慢了把服务拖垮。把整套自动伸缩配置全流程走完之后最大的感受是这件事做起来没有想象中复杂但有几个前置条件没处理好后面的坑会一个接一个。这篇就把从方案选型、指标设计、镜像改造到HPA落地的完整过程梳理出来正在给PHP应用做弹性扩容、考虑容器化改造的团队可以参考。1. 自动伸缩方案怎么选先想清楚再动手1.1 PHP应用为什么适合做自动伸缩PHP-FPM的进程模型决定了它天然适合水平伸缩。每个请求进来FPM会分配一个worker进程处理处理完请求后进程保留并继续接收下一个请求。这个模型本身没有跨进程的复杂状态依赖只要把session、缓存、文件这些外部依赖抽离到公共组件上一个实例复制成十个实例不会产生太大冲突。很多团队以为PHP应用要上水平扩展必须把代码彻底重写。其实大多数业务只需要做好几处改造就能支持多实例比如将session从文件改成Redis将日志输出到stdout而不是本地磁盘将用户上传的文件改存对象存储数据库连接串统一走中心配置。这些改造都不需要动核心业务逻辑。自动伸缩的价值在于流量不是恒定的。用饭店打比方FPM worker就是服务员高峰期客人多、服务员少服务质量就下降如果饭店能随时增加服务员低峰期又让一部分服务员休息经营成本自然更可控。自动伸缩要解决的正是“服务员数量”的问题。1.2 三种常见落地方式对比水平伸缩的落地路径不止一种我实际接触过的方案大致分三类。第一类是使用云厂商的弹性伸缩组。把PHP服务打包成自定义镜像加入负载均衡由平台根据CPU、内存、带宽等指标决定实例数量。这种方式离传统运维最近但镜像维护成本高每改一次代码就要重新打镜像而且伸缩策略通常面向整机实例粒度比较粗。第二类是自己在集群层面写调度脚本配合Docker和监控告警。比如写脚本监控容器CPU超过阈值就调用Docker接口启动新容器流量下降再销毁容器。这种方式做小规模实验没问题但脚本里的边界条件很容易漏比如启动中的容器还没就绪就被分配流量、节点资源不足、缩容时连接还没排空等推进到生产环境风险偏高。第三类是Kubernetes原生HPA。它直接面向Deployment工作负载根据资源指标或者自定义指标自动调整Pod副本数扩缩容策略、稳定窗口、并发策略都有现成配置项和Prometheus等监控体系也打通得很好。现在的实际项目我基本都推荐这一种。落地方式扩缩粒度扩容速度指标来源适合场景云厂商弹性伸缩组整机实例秒级到分钟级CPU/内存/带宽等传统部署、镜像规范自建脚本 Docker容器秒级自定义脚本小规模实验Kubernetes HPA容器/Pod秒级到分钟级Resource/Custom metrics容器化改造后的业务HPA最大的优势是原生支持Pod副本粒度的伸缩配合Ingress的会话保持和健康检查可以实现非常平滑的上下线。更重要的是HPA整个控制循环是闭环的默认每15秒通过metrics接口拉取一次指标定时重新计算期望副本数再调用Deployment的伸缩接口把副本数调整到期望值整个过程不需要人工干预。1.3 整体架构设计给PHP服务做自动伸缩并不是只加一个HPA就完事。我建议先把整个调用链梳理清楚。比较典型的架构可以概括成一句话负载均衡器 - Nginx网关 - PHP-FPM容器池 - Redis缓存与Session共享 - 中心数据库。前端负载均衡负责把流量分发到多个Nginx入口Nginx层处理静态资源同时把PHP请求通过FastCGI转发给PHP-FPMPHP-FPM以Pod副本池的形式存在每个Pod都是一个无状态的PHP运行环境Redis保存Session、热点数据和任务队列MySQL作为唯一持久化数据源。这个架构里有几点必须在动手前确认。第一PHP代码不能依赖本地文件系统做持久化第二业务缓存不能只放在单机内存里否则流量分发到不同Pod会出现数据不一致第三定时任务不能挂在某个Pod上否则Pod重启或缩容后任务就丢了需要拆成独立Job或单独的调度服务。把这些隔离点都处理干净后续HPA的配置才有意义。否则就算扩缩容成功用户数据也会出问题到时候排查起来比手动扩容还痛苦。2. 伸缩指标选哪些别只盯着CPU2.1 常见指标与适用场景HPA支持多种指标类型最常用的是资源指标也就是CPU和内存。CPU利用率的计算方式是“Pod当前CPU使用量除以requests里声明的CPU值”这也解释了为什么很多人配置HPA后发现预期不符——如果Pod没有声明resources.requestsHPA根本拿不到利用率数据。CPU指标适合大多数PHP应用。流量上来并发请求增加CPU使用率通常也会涨。不过CPU指标有滞后性PHP-FPM进程在请求处理完后会有短暂空闲短时间内的CPU尖峰在15秒采样窗口内未必能被准确捕捉所以设置目标利用率时建议预留一定缓冲。内存指标在PHP场景里要谨慎使用。PHP-FPM worker的内存占用通常比较稳定正常业务下不会随流量增长而立刻上升更多的是缓慢累积只有出现内存泄漏或者慢查询堆积时才明显。如果只依赖内存扩容很容易出现“流量涨了十分钟Pod还没扩上去”的情况。内存指标更适合作为辅助指标和CPU一起做多指标判断。比资源指标更贴近业务的是QPS或者并发连接数。这类指标可以从php-fpm-exporter或者网关层采集再通过Prometheus Adapter暴露给HPA。如果业务对响应时间敏感并且你清楚单个worker能承载多少并发基于QPS的伸缩策略会更准确。还有一种常用指标是Redis队列长度适合大量异步任务处理的场景比如批量发消息、批量导出数据队列长了就应该扩容消费者实例。2.2 指标采集链路怎么搭如果是Kubernetes环境CPU和内存指标直接依赖metrics-server。安装metrics-server之后kubectl top nodes和kubectl top pods就能看到实时用量HPA也能直接从metrics API拿到数据。如果需要QPS、队列长度这类自定义指标我通常会在集群里部署Prometheus同时使用php-fpm-exporter采集FPM状态再用Prometheus Adapter把Prometheus里的指标映射成Kubernetes自定义指标。核心链路是PHP-FPM暴露status页面 - php-fpm-exporter抓取并转成Prometheus格式 - Prometheus周期性采集 - Adapter查询Prometheus并将数据映射为custom.metrics.k8s.io接口 - HPA从这个接口读取指标。这条链路每一环都是独立的。只看CPU和内存不部署Prometheus也能跑只要涉及自定义指标就必须把Prometheus和Adapter配好。实际运维时建议先把资源指标跑通确认HPA工作正常之后再逐步加入自定义指标避免一开始就让排查面铺太大。2.3 关键阈值怎么定从并发量反推配置HPA时最常被问的问题就是“averageUtilization到底填多少”。我的习惯是先由业务并发倒推。先估算单worker的QPS假设一个PHP-FPM worker处理一个请求平均耗时200毫秒那它1秒内最多处理5个请求。如果目标业务需要支撑1000 QPS理论上就需要200个worker同时处理。再按每个Pod配置的进程数来算如果pm.max_children设置为50则至少需要4个Pod才能承载这1000 QPS。用公式表达就是单worker QPS 1 / 单请求平均处理耗时单位秒需要的worker总数 目标QPS / 单worker QPS最小Pod副本数 需要的worker总数 / 单Pod的max_children接下来看CPU目标利用率。假设单个Pod的requests.cpu设置为500m也就是半个核Pod实际执行业务时CPU最多能跑满1个核。如果HPA目标利用率设为60%那么当Pod用了300m CPU时HPA就认为达到了目标线开始触发扩容。这里的60%意味着给每个Pod预留40%缓冲用来应对瞬时脉冲不至于一有波动就疯狂扩。不过这只是初值。真实流量一定是波动的压测之后还要根据实际曲线调整。我一般会在上线后观察一周看HPA的扩缩次数和Pod的CPU使用分布再回来微调目标值以及minReplicas。3. 实操从Docker镜像到HPA的完整落地流程3.1 基础镜像与PHP-FPM配置自动伸缩的前提是镜像可以随时被拉起并且拉起后能快速提供服务。Dockerfile我习惯用轻量基础镜像比如php:8.2-fpm-alpine可以显著减小镜像体积加快扩容时拉取镜像的耗时。一个基础示例FROM php:8.2-fpm-alpine RUN docker-php-ext-install pdo_mysql mysqli opcache \ docker-php-ext-enable opcache # 按业务需要安装 redis 扩展等 RUN apk add --no-cache libzip-dev \ docker-php-ext-install zip EXPOSE 9000 CMD [php-fpm]镜像里的关键点有两个。一是扩展要提前编进镜像不能等容器启动时再临时安装否则扩容时会因为安装扩展拖慢启动时间二是基础镜像尽量固定版本不要用latest方便复现和回溯。PHP-FPM的进程池配置直接决定单个容器的吞吐能力。我一般会在www.conf里这样设置pm dynamic pm.max_children 30 pm.start_servers 5 pm.min_spare_servers 3 pm.max_spare_servers 10 pm.max_requests 1000pm.max_children是最核心的参数限制单个Pod同时处理的请求数。设置多少不能拍脑袋要结合容器内存限制和每个worker的内存占用。如果每个worker平均占用30MB内存容器内存限制为1GB那么max_children参考值可以取30并为PHP自身和管理进程预留一部分内存。pm.max_requests表示每个worker处理1000个请求后自动重启目的是避免长期运行的worker累积内存问题这个值在容器场景里建议保留。3.2 无状态改造Session、日志与文件真正让自动伸缩能跑起来的关键是让业务代码做到无状态。这一步不做后面所有配置都是白搭。Session必须从文件存储迁移到Redis。PHP配置里指定session.save_handler redis session.save_path tcp://redis-host:6379这样用户请求无论被负载均衡分到哪个Pod都能读取同一个Session不会出现“登录后请求一跳转就掉线”的现象。业务日志要尽量输出到标准输出而不是写在Pod本地磁盘。容器场景中本地磁盘的日志会随Pod销毁而消失排查问题时很难追溯。更合理的做法是应用把日志打到stdout由日志采集组件统一收集。用户上传的文件、生成的临时文件也不能写本地磁盘。水平伸缩环境下Pod随时可能被重建缩容后本地文件就没了。需要把上传文件统一放到对象存储或者至少放到独立持久卷但持久卷在多Pod共享时存在锁和一致性问题不如对象存储干净。3.3 在Kubernetes中部署PHP服务容器和无状态改造做完后就要接入Kubernetes。Deployment的配置有几点直接关系到HPA能否正常工作。apiVersion: apps/v1 kind: Deployment metadata: name: php-fpm labels: app: php-fpm spec: replicas: 2 selector: matchLabels: app: php-fpm template: metadata: labels: app: php-fpm spec: containers: - name: php-fpm image: registry.example.com/php-fpm:latest imagePullPolicy: IfNotPresent ports: - containerPort: 9000 resources: requests: cpu: 500m memory: 512Mi limits: cpu: 1 memory: 1024Mi readinessProbe: tcpSocket: port: 9000 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: tcpSocket: port: 9000 initialDelaySeconds: 15 periodSeconds: 20 lifecycle: preStop: exec: command: [sh, -c, sleep 10] terminationGracePeriodSeconds: 60resources.requests在这里是最重要的配置。HPA计算CPU利用率时分母用的是requests.cpu而不是limits.cpu。如果没有设置requestsHPA会显示Unknown无法正常工作。readinessProbe和livenessProbe的作用是告诉Kubernetes“这个PHP-FPM进程是否还可以接收请求”。这里用tcpSocket探测9000端口是一种简洁的检查方式。生产环境更精确的做法是通过Nginx转发一个健康检查路由给PHP-FPM或者借助php-fpm-exporter暴露status页面探活。如果探活失败新Pod不会被加入负载均衡旧Pod也不会被摘流自动伸缩反而会把问题放大。preStop和terminationGracePeriodSeconds是为了优雅退出。缩容时Pod会被标记为Terminating随后执行preStop里的命令sleep 10秒给正在处理的PHP请求留出完成时间。这个配置对长请求业务尤其重要。部署完成后执行kubectl apply -f php-fpm-deployment.yaml kubectl get pods -l appphp-fpm确认两个Pod都处于Running且Ready状态再继续配HPA。3.4 创建HPA并验证扩容缩容HPA的完整配置我这边放一个autoscaling/v2版本的示例apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: php-fpm-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-fpm minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleDown: stabilizationWindowSeconds: 600 policies: - type: Percent value: 20 periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 60minReplicas是最低副本数通常根据业务基线来定我的习惯是至少保持2个副本避免单点maxReplicas要结合集群资源上限来确定不是越大越好。averageUtilization填60是为了让Pod保留40%的扩容缓冲memory填80属于兜底策略防止内存被打满。behavior里的scaleDown稳定窗口设置为600秒意思是缩容决策至少要观察10分钟防止流量稍微抖动就大规模缩容scaleUp不设稳定窗口保证流量一涨就能立刻扩容。缩容策略20%的意思是每60秒最多缩容20%的副本这样数据库和Redis不会被断崖式连接释放打垮。应用配置kubectl apply -f php-fpm-hpa.yaml kubectl get hpa php-fpm-hpa状态正常时TARGETS列会显示CPU和内存的实时百分比比如“35%/60%”。如果显示 优先检查metrics-server是否安装、Pod是否设置了requests。接下来做压测验证。我用wrk简单压一下wrk -t4 -c200 -d120s http://app.example.com/benchmark压测期间另开一个终端观察副本数量变化kubectl get pods -l appphp-fpm -w kubectl describe hpa php-fpm-hpa正常情况下压测开始后一两分钟内Pod数量就会从2涨上去。HPA的事件里会显示扩容原因比如“CPU 90% target 60%”一眼就能看清触发逻辑。压测结束后Pod不会立刻缩回而是在稳定窗口结束后逐渐减少这是正常行为不用觉得奇怪。4. 常见问题与排查技巧实录4.1 指标显示UnknownHPA不扩容这是新手上路遇得最多的问题。HPA显示 或 无非两种原因一是metrics-server没有安装二是Deployment里面根本没写resources.requests。排查命令很简单kubectl top nodes kubectl top pods如果这两条命令报错说明metrics-server有问题如果top命令正常但HPA仍然取不到指标大概率就是某个资源没有设置requests。记住一个原则HPA使用的利用率以requests为基准任何被HPA管理的Deployment都必须先声明requests。还有一种情况是Pod一直处于ContainerCreating状态metrics自然不存在。这种情况往往和镜像拉取慢、节点资源不足有关。节点扩容耗时属于集群问题不在HPA范畴但实际效果会让HPA显得“没反应”。4.2 扩容后服务反而变慢甚至出现雪崩一个典型场景压测时Pod扩到了8个整体QPS没上去反而每个Pod的CPU都飙得很高。排查下来往往是新Pod还没有完成PHP-FPM预热就被负载均衡分到大量流量。PHP解释器第一次执行脚本时比较慢OpCache又是空的新Pod的处理能力远低于稳定运行的Pod流量一股脑灌进去自然变慢。处理上我通常做几件事在readinessProbe里加长initialDelaySeconds给Pod留出初始化时间为PHP-FPM预留一定数量的start_servers让进程池提前加热如果Nginx和PHP-FPM分别部署还要考虑Nginx sidecar的预热。这个问题的本质是“扩容速度”和“服务就绪速度”不匹配自动伸缩只是把副本数拉起来了应用本身是否ready要看探活和预热配置。4.3 缩容抖动引发数据库连接风暴流量下降时如果HPA把Pod副本数快速减少数据库连接会突然断裂重连。PHP-FPM连接MySQL通常不设置短超时Pod被强杀后数据库侧会保留一堆半开连接等待TCP超时才释放数据库连接数一下子就打满出现连锁反应。解决思路是把缩容放慢。HPA的behavior.scaleDown.stabilizationWindowSeconds设置为600秒缩容策略按百分比限制能明显缓解。另外MySQL连接层加Proxy或者使用连接池组件也是生产环境常用的手段。我踩过几次坑之后现在对缩容比扩容更谨慎。扩容慢一点最多是硬撑一下缩容太快是真的会出事故。4.4 Session丢失、上传文件失效这类问题通常不会立刻暴露而是隔一段时间有人反馈“用户偶尔掉登录”“上传的头像突然显示不出来”。排查后会发现问题出在状态放到了Pod本地session.save_handler还是默认的files多Pod时请求被负载到不同PodSession自然对不上上传文件写到了本地磁盘Pod被缩容重建后文件就没了。解决办法就是前面说的无状态改造。Session指向Redis文件改存对象存储或者集中存储。这个改造一定要在配置HPA之前完成否则自动伸缩越敏捷状态丢失的问题就越频繁。5. 从自动伸缩到成本优化后续还能怎么扩展5.1 实际收益对比自动伸缩带来的最大收益不是“自动”两个字而是资源使用率的变化。在我接触过的一个模拟项目里原先固定部署3台4核8G机器日常流量下CPU使用率只有15%左右活动高峰期3台又不够用。改成2到8个Pod的自动伸缩后波谷时只保留2个Pod波峰时最多到6个Pod整体资源利用率从15%提升到接近60%长期使用下来成本下降幅度很可观。这里的核心是“按需付费”的思维。固定资源永远要做容量规划而容量规划永远面临双重风险规划少了扛不住流量规划多了浪费成本。自动伸缩把这个问题移交给指标和策略去动态平衡。5.2 扩展方向自定义指标、定时伸缩、事件驱动HPA跑熟之后可以按业务形态继续升级。第一基于QPS或并发连接数的自定义指标。如果CPU和内存指标在突发流量下触发不够及时可以采用Prometheus Adapter把php-fpm-exporter的QPS指标接入HPA。例如定义指标查询统计最近2分钟每个Pod的请求速率并设置合理的平均值作为伸缩基准。第二定时伸缩。如果业务有明确的波峰波谷时段比如每天上午和晚间各有高峰可以用Keda的Cron触发器或者云上定时伸缩服务在高峰前提前扩容、低峰前提前缩容。这种方式比纯指标响应更快因为指标伸缩总要等流量上来才开始动作。第三事件驱动伸缩。比如消费者进程处理Redis消息队列消息积压超过阈值才需要扩容直接用HPA盯CPU可能反应不够精准。Keda这类事件驱动伸缩器可以根据队列长度、消息处理延迟等业务指标直接调整副本数把伸缩逻辑和业务逻辑真正绑在一起。集群层面还可以继续接入Cluster Autoscaler让底层节点也随Pod需求自动增减这样从“应用层自动伸缩”延伸到“基础设施层自动伸缩”整个资源的弹性才算闭环。最后说一点个人体会。自动伸缩配置本身并不复杂真正花时间的是想清楚业务容量模型一个worker能扛多少QPS、一个Pod能承载多少并发、业务对缩容延迟的容忍度是多少。建议先把监控做好观察几天真实流量曲线再拍板目标值配置之后每周回顾一次HPA的扩缩记录。照搬一份YAML贴上去大概率会遇到“扩了不该扩的、缩了不该缩的”这类问题。先把基础流程跑通再逐步细化策略是这件事最稳妥的路径。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑