PHP 怎么存活探针

wen PHP项目 1

**
《PHP的“存活探针”实战指南:从心跳检测到故障自愈,运维必懂的核心机制》

PHP 怎么存活探针


目录导读

  1. 什么是PHP存活探针?——不只是“ping”那么简单
  2. 为什么传统探针会误判?——PHP-FPM与CLI模式的本质差异
  3. 三大主流探针实现方案(HTTP轮询 / 进程信号 / 自定义心跳表)
  4. 进阶:结合队列与Redis的“业务级存活”检测
  5. 常见问题问答(FAQ)——解决你的部署焦虑
  6. 探针代码示例与排错清单

什么是PHP存活探针?——不只是“ping”那么简单

很多开发者以为“存活探针”就是定期向服务器发一个HTTP请求,收到200就认为服务活着,但PHP环境远比这复杂——PHP-FPM进程池中的worker是否卡死?opcache是否失效?数据库连接池是否耗尽? 这些都不会体现在一个简单的HTTP状态码里。

真正的存活探针(Liveness Probe)需要回答三个问题:

  • 网络层:TCP端口是否可连接?
  • 应用层:PHP能否正常解析并执行一个最小脚本?
  • 依赖层:Redis/MySQL等外部服务是否仍可交互?

笔者曾在生产环境遇到“HTTP 200但页面全部空白”的情况——PHP-FPM的worker集体僵死,但Nginx仍返回缓存头,传统探针完全失效,只有业务级探针才能捕获。


为什么传统探针会误判?——PHP-FPM与CLI模式的本质差异

关键误区:很多人用curl http://localhost/health.php来探测,但该请求可能被Nginx缓存,或者PHP-FPM的pm.max_children已满,而探针请求被排队等待——此时HTTP依然能返回200,但实际业务请求已超时。

更隐蔽的问题是CLI模式探针

php /var/www/healthcheck.php

这种探针只验证了PHP-CLI二进制能运行,却完全忽略了PHP-FPM池的健康状态,两者复用同一份php.ini,但进程模型完全不同(CLI每次启动独立进程,FPM常驻内存复用Zend VM)。

正确做法:探针必须直接请求FPM暴露的/status端点,并解析active processesmax children reached等指标。


三大主流探针实现方案

方案A:HTTP业务探针(适合微服务/K8s)

// health.php
$checks = [
    'php_version' => PHP_VERSION,
    'db' => function() { try { PDO::connect(...); } catch (Exception $e) { return false; } }
];
if (in_array(false, $checks, true)) {
    http_response_code(503);
    exit('Unhealthy');
}
echo 'OK';

注意:必须禁用OPcache对health.php的缓存,否则探针本身永远“健康”。

方案B:PHP-FPM的ping.path直接暴露

php-fpm.conf中开启:

ping.path = /ping
ping.response = pong

然后探针直接请求http://127.0.0.1:9000/ping,这是最轻量的方法,能直接反映FPM master进程的存活状态,但不检测业务依赖。

方案C:共享内存表(适合高并发集群)

shmopredisSETNX记录每个worker的“最后心跳时间”:

// worker启动时
$redis->set('worker_' . getmypid(), time(), ['EX' => 30]);

探针扫描所有worker_*键,若一半以上过期,则判定服务濒死,这种方式能发现“假死”状态——进程还在,但无法处理新请求。


进阶:结合队列与Redis的“业务级存活”检测

对于处理异步任务(如订单通知)的PHP应用,单纯的“进程活着”毫无意义。业务级探针应模拟一个完整事务:

  1. 写入测试键SET probe_test_{时间戳} "1" EX 10
  2. 消费测试:从队列中取出该任务并执行(或确认存在)
  3. 删除测试键:验证清理逻辑无阻塞

若整个流程超时(gt;5秒),则说明消息队列积压或Redis响应变慢,这种探针需要双端口部署/internal/health-live(轻量)与/internal/health-ready(重量),前者给K8s的livenessProbe,后者给readinessProbe

坑点警示:千万不能让业务探针写入生产数据库,否则测试数据会污染统计报表,应当使用独立的test_前缀库或Redis DB编号。


常见问题问答(FAQ)——解决你的部署焦虑

Q1:为什么我的探针总是显示存活,但网站实际已经打不开?
A:检查pm.max_children是否达到上限,如果所有worker都在占用中,探针请求虽然会排队成功,但业务请求全部超时,请在你的探针脚本里加入对/statusmax_children_reached计数器的监控。

Q2:在Docker/K8s中,探针应该探测宿主机的端口还是容器内的?
A:务必探测容器内的localhost,如果探测宿主机,可能会经过负载均衡或Nginx转发,掩盖了业务容器的真实状态。

Q3:PHP的gc_collect_cycles()会影响探针结果吗?
A:会,某些旧代码的循环引用导致内存泄漏,触发垃圾回收时进程会短暂卡顿,建议在探针脚本里添加gc_disable(),否则探针可能在你GC时超时误报。

Q4:探针脚本本身应该加密吗?
A:绝对不要加密或混淆,探针文件一旦异常,你必须能快速查看其原始代码,只需确保该文件不允许外部请求(通过Nginx配置仅允许内网IP访问)。


探针代码示例与排错清单

最终推荐的全栈探针(伪代码)

$status = ['live' => true, 'ready' => true];
// 1. 基础Liveness: 检查进程内存
if (memory_get_usage() > 200 * 1024 * 1024) $status['live'] = false;
// 2. Readiness: 测试DB连接(使用连接池复用)
if (!checkDb()) $status['ready'] = false;
// 3. 业务深度探测:清空旧的测试队列
$queue->cleanOldTests();
// 4. 输出JSON并设置HTTP状态码
if (!$status['live']) http_response_code(500);
elseif (!$status['ready']) http_response_code(503);
else http_response_code(200);
header('Content-Type: application/json');
echo json_encode($status);

排错清单(快速定位)

  • 探针返回503但日志无错误 → 检查session.save_path是否可写(PHP挂起常见原因)
  • 探针请求超时 → strace -p PID 看是否阻塞在select()fsockopen()
  • 内存溢出 → 排查memory_limit设置,但注意探针不应受此限制(用ini_set单独放大)
  • 时区问题:探针时间条件判断错误导致误报(例如time()与数据库时间差8小时)

最后提醒:任何探针都无法100%覆盖所有故障形态。日志监控(ELK)+ 链路追踪(SkyWalking) 才是终极解决手段,但一个设计良好的存活探针,能帮你将故障恢复时间从小时级降到分钟级——这就是它的价值所在。

抱歉,评论功能暂时关闭!