本文目录导读:

在PHP项目中实现主备切换与故障转移,通常涉及数据库、缓存、服务节点等不同层面的高可用设计,以下是针对不同场景的详细方案和最佳实践。
数据库主备切换
1 读写分离 + 自动故障转移
使用 MySQL + ProxySQL / MaxScale / HAProxy 实现透明切换。
// 配置示例(使用 PDO + 读写分离)
$config = [
'write' => [
'host' => '192.168.1.10', // 主库
'port' => 3306,
],
'read' => [
['host' => '192.168.1.11', 'port' => 3306],
['host' => '192.168.1.12', 'port' => 3306], // 备库
],
'user' => 'app_user',
'password' => 'secret',
'database' => 'myapp',
];
// 连接池管理(简化版)
class DBConnectionManager {
private static $master = null;
private static $slaves = [];
public static function getWriteConnection() {
if (self::$master === null) {
self::$master = self::connect($config['write']);
}
return self::$master;
}
public static function getReadConnection() {
// 随机选择从库
$slave = $config['read'][array_rand($config['read'])];
$key = $slave['host'];
if (!isset(self::$slaves[$key])) {
self::$slaves[$key] = self::connect($slave);
}
return self::$slaves[$key];
}
}
2 自动故障检测与切换
使用 Keepalived + MySQL MHA / Orchestrator 实现VIP漂移。
# keepalived配置示例
vrrp_instance VI_1 {
state MASTER # 备库改为 BACKUP
interface eth0
virtual_router_id 51
priority 100 # 备库设为 90
advert_int 1
authentication {
auth_type PASS
auth_pass 1234
}
virtual_ipaddress {
192.168.1.100/24 # VIP地址
}
track_script {
chk_mysql
}
}
PHP 应用只需连接 VIP(192.168.1.100),切换过程对应用透明。
应用层主备切换
1 PHP-FPM 多实例 + Nginx 负载均衡
# nginx upstream 配置
upstream php_backend {
server 10.0.0.1:9000 weight=5; # 主节点
server 10.0.0.2:9000 weight=5; # 备节点
server 10.0.0.3:9000 backup; # 备用节点(故障时启用)
# 健康检查(需要 nginx-plus 或第三方模块)
health_check interval=5s fails=3 passes=2;
}
server {
location ~ \.php$ {
fastcgi_pass php_backend;
include fastcgi_params;
}
}
2 应用级故障转移(代码层面)
// 多节点调用示例
class FailoverClient {
private $servers = [
'http://primary.app.local',
'http://secondary.app.local',
];
public function call($endpoint, $data) {
foreach ($this->servers as $server) {
try {
$response = $this->doRequest($server . $endpoint, $data);
if ($response['status'] === 200) {
return $response['body'];
}
} catch (Exception $e) {
// 记录日志,尝试下一个节点
error_log("Failover: {$server} failed - " . $e->getMessage());
continue;
}
}
throw new Exception("All servers unavailable");
}
}
缓存主备切换(Redis)
1 Redis Sentinel 自动故障转移
// 使用 Predis 或 phpredis 连接 Sentinel
$sentinel = new Predis\Client([
'tcp://192.168.1.10:26379',
'tcp://192.168.1.11:26379',
'tcp://192.168.1.12:26379',
]);
// 获取当前主节点
$master = $sentinel->getMaster('mymaster');
echo "Current master: {$master[0]}:{$master[1]}\n";
// 连接主节点写入
$redis = new Predis\Client("tcp://{$master[0]}:{$master[1]}");
$redis->set('key', 'value');
// 连接从节点读取(可选)
$slaves = $sentinel->getSlaves('mymaster');
$slave = $slaves[array_rand($slaves)];
$redisSlave = new Predis\Client("tcp://{$slave[0]}:{$slave[1]}");
$value = $redisSlave->get('key');
2 Redis Cluster 自动分片
// 连接 Redis 集群(自动处理节点切换)
$client = new Predis\Client([
'tcp://192.168.1.10:7000',
'tcp://192.168.1.11:7000',
'tcp://192.168.1.12:7000',
], ['cluster' => 'redis']);
全面的故障转移方案
1 健康检查 + 降级策略
class ServiceMonitor {
private $healthChecks = [];
private $serviceStatus = [];
public function registerCheck($name, callable $check) {
$this->healthChecks[$name] = $check;
$this->serviceStatus[$name] = true;
}
public function runChecks() {
foreach ($this->healthChecks as $name => $check) {
try {
$result = $check();
$this->serviceStatus[$name] = $result;
} catch (Exception $e) {
$this->serviceStatus[$name] = false;
// 触发告警
$this->alert("Service {$name} failed");
}
}
}
public function isServiceAvailable($name) {
return $this->serviceStatus[$name] ?? false;
}
}
// 使用示例
$monitor = new ServiceMonitor();
$monitor->registerCheck('database', function() {
$db = DB::connection();
return $db->select('SELECT 1')->count() > 0;
});
// 降级逻辑
if (!$monitor->isServiceAvailable('database')) {
// 切换到本地缓存或静态数据
return Cache::getFallback('users');
}
2 断路器模式(Circuit Breaker)
使用 PHP Circuit Breaker 库(如 guzzlehttp/circuit-breaker):
use GuzzleHttp\Client;
use GuzzleHttp\Subscriber\CircuitBreaker\CircuitBreaker;
$client = new Client([
'base_uri' => 'http://api.example.com',
'timeout' => 5.0,
]);
$circuitBreaker = new CircuitBreaker($client, [
'failures' => 3,
'success_threshold' => 2,
'timeout' => 30000, // 30秒后重试
]);
try {
$response = $circuitBreaker->get('/users');
} catch (Exception $e) {
// 断路器打开,返回降级数据
return ['error' => 'Service unavailable', 'data' => $cachedData];
}
部署架构建议
+-------------+
| 负载均衡器 |
| (VIP/HAProxy)|
+------+------+
|
+----------------+----------------+
| | |
+-------+------+ +-------+------+ +-------+------+
| Nginx + PHP | | Nginx + PHP | | Nginx + PHP |
| (主节点) | | (备节点) | | (备节点) |
+-------+------+ +-------+------+ +-------+------+
| | |
+----------------+----------------+
|
+----------+----------+
| MySQL |
| (主-主 同步或主从) |
+-------------------+
| Redis |
| (Sentinel/集群) |
+-------------------+
最佳实践总结
| 场景 | 推荐方案 | 切换时间 | 复杂度 |
|---|---|---|---|
| MySQL 主备 | ProxySQL + Orchestrator | < 10秒 | 中 |
| Redis 故障 | Redis Sentinel | < 30秒 | 低 |
| PHP 应用节点 | Nginx + 健康检查 | < 5秒 | 低 |
| 全流程自动切换 | Kubernetes + 服务网格 | 秒级 | 高 |
关键注意事项:
- 会话保持:PHP Session 需使用 Redis/Cookie 共享,避免切换后登录失效
- 缓存预热:主备切换后,新主库可能需要重建缓存
- 数据一致性:主从延迟可能导致读写冲突,建议强制读主库场景
- 监控告警:对接 Prometheus + Grafana 实时监控健康状态
- 人工干预:保留手动切换接口,防止自动化误判
通过以上方案,可实现 PHP 项目 99.9% 以上的可用性,具体选择需根据业务重要性、预算和技术栈灵活调整。