swoole方案 微服务无缝发布 (平滑上下线) 机制
·
微服务平滑上下线机制
大白话先说清楚
没有平滑下线:
K8s要杀Pod → 直接kill进程
→ 正在处理的100个请求全部中断
→ 用户看到500错误
有了平滑下线:
K8s要杀Pod → 先通知Swoole
→ 从Etcd摘掉节点(新请求不来了)
→ 等老请求全部处理完
→ 再销毁进程
→ 用户无感知
preStop钩子是什么:
K8s杀Pod之前会先执行preStop命令
给你一段时间做善后工作
就像"你要被开除了,先把手头的活交接完"
整体时序:
K8s发升级指令
│
├─ 1. 执行preStop钩子(发信号给Swoole)
│
├─ 2. Swoole收到信号 → 从Etcd摘除节点
│ └─ 新请求不会再路由过来
│
├─ 3. 等待已有请求处理完(最多等30秒)
│
├─ 4. 所有请求处理完 → 进程退出
│
└─ 5. K8s销毁Pod
K8s配置
# deployment.yaml
spec:
template:
spec:
containers:
- name: order-service
lifecycle:
preStop:
exec:
# Pod停止前执行:发SIGTERM给PHP进程
command: ["/bin/sh", "-c", "kill -SIGTERM 1 && sleep 35"]
# K8s等待终止的最长时间(要比业务等待时间长)
terminationGracePeriodSeconds: 60
代码
<?php
// server.php
// 记录当前正在处理的请求数
$GLOBALS['requestCount'] = 0;
$GLOBALS['stopping'] = false;
// ─── Etcd操作
function etcdDelete(string $key): void
{
go(function () use ($key) {
$client = new Swoole\Coroutine\Http\Client('etcd服务IP', 2379);
$client->post('/v3/kv/deleterange', json_encode([
'key' => base64_encode($key),
]));
$client->close();
echo "已从Etcd摘除节点:{$key}\n";
});
}
function etcdPut(string $key, string $value, string $leaseId): void
{
go(function () use ($key, $value, $leaseId) {
$client = new Swoole\Coroutine\Http\Client('etcd服务IP', 2379);
$client->post('/v3/kv/put', json_encode([
'key' => base64_encode($key),
'value' => base64_encode($value),
'lease' => $leaseId,
]));
$client->close();
});
}
function etcdGrantLease(int $ttl): string
{
$client = new Swoole\Coroutine\Http\Client('etcd服务IP', 2379);
$client->post('/v3/lease/grant', json_encode(['TTL' => $ttl, 'ID' => 0]));
$result = json_decode($client->body, true);
$client->close();
return $result['ID'] ?? '';
}
// ─── 服务启动
$server = new Swoole\Http\Server('0.0.0.0', 9514);
$server->set([
'worker_num' => 4,
'max_wait_time' => 30, // worker退出最多等30秒
'reload_async' => true, // 异步重启,不强杀
]);
$server->on('workerStart', function ($server, $workerId) {
$ip = '192.168.1.10';
$port = 9514;
$nodeKey = "/services/order-service/{$ip}:{$port}";
$nodeVal = json_encode(['ip' => $ip, 'port' => $port, 'worker' => $workerId]);
// 注册到Etcd
$leaseId = etcdGrantLease(30);
etcdPut($nodeKey, $nodeVal, $leaseId);
echo "Worker{$workerId} 注册到Etcd\n";
// 续约心跳
$timerId = Swoole\Timer::tick(10000, function () use ($leaseId, $workerId) {
// 正在停止就不续约了(让租约自然过期也行,但主动删更快)
if ($GLOBALS['stopping']) return;
go(function () use ($leaseId) {
$client = new Swoole\Coroutine\Http\Client('etcd服务IP', 2379);
$client->post('/v3/lease/keepalive', json_encode(['ID' => $leaseId]));
$client->close();
});
});
// ─── 收到SIGTERM(K8s preStop发来的)
Swoole\Process::signal(SIGTERM, function () use (
$server, $nodeKey, $leaseId, $timerId, $workerId
) {
echo "Worker{$workerId} 收到SIGTERM,开始平滑下线\n";
$GLOBALS['stopping'] = true;
// Step1:停止续约,清除心跳定时器
Swoole\Timer::clear($timerId);
// Step2:从Etcd摘除节点(新请求不再路由过来)
etcdDelete($nodeKey);
// Step3:等待已有请求处理完
go(function () use ($server, $workerId) {
$waited = 0;
while ($GLOBALS['requestCount'] > 0 && $waited < 30) {
echo "Worker{$workerId} 还有 {$GLOBALS['requestCount']} 个请求处理中,等待...\n";
Swoole\Coroutine::sleep(1);
$waited++;
}
if ($GLOBALS['requestCount'] > 0) {
echo "Worker{$workerId} 等待超时,强制退出\n";
} else {
echo "Worker{$workerId} 请求全部处理完,安全退出\n";
}
// Step4:退出进程
$server->stop();
});
});
});
// ─── 请求进来,计数+1
$server->on('request', function ($req, $resp) {
// 正在停止,拒绝新请求(理论上Etcd摘除后不会再来,双重保险)
if ($GLOBALS['stopping']) {
$resp->status(503);
$resp->end(json_encode(['msg' => '服务升级中,请重试']));
return;
}
$GLOBALS['requestCount']++;
// 模拟业务处理(假设要花2秒)
Swoole\Coroutine::sleep(2);
$resp->end(json_encode(['code' => 0, 'msg' => '处理完成']));
// 请求处理完,计数-1
$GLOBALS['requestCount']--;
});
// ─── Worker退出前最后确认
$server->on('workerStop', function ($server, $workerId) {
echo "Worker{$workerId} 已安全退出\n";
});
$server->start();
完整时序图
K8s触发滚动更新
│
▼
执行preStop: kill -SIGTERM 1
│
▼
Swoole收到SIGTERM
├─ stopping = true(拒绝新请求)
├─ 停止心跳续约
├─ 删除Etcd节点 ←── 负载均衡立刻摘掉这个节点
│ 新请求路由到其他Pod
│
▼
等待 requestCount == 0
│
├─ 每秒检查一次
├─ 最多等30秒
│
▼
requestCount == 0
│
▼
server->stop() → workerStop → 进程退出
│
▼
K8s销毁Pod(用户全程无感知)
新Pod上线流程(上线也要平滑)
// 新Pod启动时:先注册Etcd,再接受流量
$server->on('workerStart', function ($server, $workerId) {
// 先做好准备:预热缓存、建好连接池
warmUpCache();
initConnectionPool();
// 准备好了再注册到Etcd(注册了才有流量进来)
$leaseId = etcdGrantLease(30);
etcdPut($nodeKey, $nodeVal, $leaseId);
echo "准备就绪,开始接收流量\n";
});
核心三句话
1. SIGTERM信号 → K8s preStop触发,Swoole收到后开始善后
2. 摘除Etcd节点 → 新请求不来了,只处理存量请求
3. requestCount → 计数器归零才退出,一个请求都不丢
更多推荐
所有评论(0)