SIGTERM 平滑退出时过早移除 worker pipe 读事件,导致已提交 Task 的 onFinish 丢失
在 Swoole Server 平滑退出过程中,如果 Event Worker 中仍有请求正在等待 Task Worker 的执行结果,Task Worker 可以正常完成任务并发送 FINISH 消息,但 Event Worker 不会再处理该消息,因此 onFinish 或 Task 回调无法触发,存量请求最终只能等待超时。
该问题可以从 Swoole 源码中的退出流程直接看出。
源码调用链
Event Worker 收到 SIGTERM 后,worker_signal_handler() 会调用:
case SIGTERM:
if (swoole_event_is_available()) {
// Event Worker
sw_server()->stop_async_worker(sw_worker());
} else {
// Task Worker
sw_worker()->shutdown();
}
break;参见:[Swoole 5.1.5 worker.cc](https://github.com/swoole/swoole-src/blob/v5.1.5/src/server/worker.cc#L45-L61)
Event Worker 启动时,会将 worker->pipe_worker 注册到 Reactor,并由 Worker_onPipeReceive 处理这条 pipe 上的消息:
reactor->add(worker->pipe_worker, SW_EVENT_READ);
reactor->set_handler(SW_FD_PIPE, Worker_onPipeReceive);Worker_onPipeReceive() 读取消息后,会调用:
serv->worker_accept_event(&pipe_buffer->info);参见:[Swoole 5.1.5 worker.cc](https://github.com/swoole/swoole-src/blob/v5.1.5/src/server/worker.cc#L493-L495)、[[Worker_onPipeReceive](https://github.com/swoole/swoole-src/blob/v5.1.5/src/server/worker.cc#L550-L562)](https://github.com/swoole/swoole-src/blob/v5.1.5/src/server/worker.cc#L550-L562)
Task Worker 完成任务时,reply_task_result() 会把消息类型设置为:
buf.info.type = SW_SERVER_EVENT_FINISH;然后通过 worker pipe 把结果发回原 Event Worker:
ret = send_to_worker_from_worker(
worker,
&buf,
sizeof(buf.info) + buf.info.len,
SW_PIPE_MASTER
);参见:[Swoole 5.1.5 task_worker.cc](https://github.com/swoole/swoole-src/blob/v5.1.5/src/server/task_worker.cc#L253-L310)
Event Worker 收到这条消息后,正常情况下会进入:
case SW_SERVER_EVENT_FINISH: {
onFinish(this, (EventData *) message_bus.get_buffer());
break;
}参见:[Swoole 5.1.5 worker.cc](https://github.com/swoole/swoole-src/blob/v5.1.5/src/server/worker.cc#L190-L193)
但是,Event Worker 收到 SIGTERM 并进入 stop_async_worker() 后,会执行:
if (worker->pipe_worker && !worker->pipe_worker->removed) {
reactor->remove_read_event(worker->pipe_worker);
}参见:[Swoole 5.1.5 worker.cc](https://github.com/swoole/swoole-src/blob/v5.1.5/src/server/worker.cc#L334-L363)
也就是说,Event Worker 在等待 Reactor 中其他存量事件退出之前,就先移除了接收 worker pipe 消息的读事件。
因此,完整的问题链路是:
- Event Worker 提交一个 Task,并等待返回结果。
- Task Worker 已经开始执行该任务。
- Event Worker 收到
SIGTERM,进入stop_async_worker()。 stop_async_worker()调用remove_read_event(worker->pipe_worker)。- Task Worker 正常执行完成,并发送
SW_SERVER_EVENT_FINISH。 - Event Worker 已经不再监听 worker pipe,无法处理 FINISH 消息。
worker_accept_event()不会收到该消息,onFinish或 Task 回调不再触发。- 正在等待 Task 结果的存量请求最终超时。
max_wait_time 无法解决该问题
增大 max_wait_time 只能延长 Reactor 等待其他事件退出的时间。
但是,worker pipe 的读事件在进入等待阶段之前就已经被移除。即使 Task 在 max_wait_time 内正常完成,Event Worker 也不会再读取对应的 FINISH 消息。
所以该问题并不是 Task 执行时间超过了退出超时,而是退出流程过早停止了 FINISH 消息的接收。
问题边界
这里讨论的不是:
- 退出后新提交的 Task;
- 尚未开始执行的排队 Task;
- Task Worker 被强制终止;
max_wait_time设置过短。
这里讨论的是:
- Task 在退出流程开始前已经成功提交;
- Task Worker 已经开始执行;
- Task Worker 最终正常执行完成;
- 只有返回结果对应的 FINISH 消息没有被 Event Worker 处理。
该问题不依赖 Hyperf。Hyperf 只是依赖 Swoole 的 onFinish 获取 Task 返回值;即使直接使用原生 Swoole 的 Task 回调机制,也同样依赖 SW_SERVER_EVENT_FINISH 的处理。
当前项目使用 Swoole 5.1.5。检查 Swoole 6.2.2 的相关实现后,SW_SERVER_EVENT_FINISH 仍然通过 worker pipe 处理,而 stop_async_worker() 中仍然会移除该 pipe 的读事件,因此最新版本似乎仍存在相同行为。
参见:
- [Swoole 6.2.2 FINISH 处理](https://github.com/swoole/swoole-src/blob/v6.2.2/src/server/worker.cc#L180-L183)
- [Swoole 6.2.2 stop_async_worker](https://github.com/swoole/swoole-src/blob/v6.2.2/src/server/worker.cc#L324-L350)
期望行为
平滑退出时,可以停止接收新请求,但对于退出前已经提交、且仍被存量请求等待的 Task,以及存量请求提的新task,应继续接收并处理其 FINISH 消息。
worker->pipe_worker 的读事件至少应保留到以下条件之一成立:
- 已提交 Task 的返回结果已经处理完成;
- Event Worker 中不再存在依赖 Task 返回值的存量请求;
- 达到
max_wait_time,执行强制退出。
不应在 Event Worker 刚进入 stop_async_worker() 时,就直接停止处理所有 worker pipe 消息。
想确认:
- 在平滑退出开始时立即移除
worker->pipe_worker的读事件,这是预期设计还是一个缺陷? - Event Worker 是否可以停止接收新任务,同时继续处理已提交 Task 对应的
SW_SERVER_EVENT_FINISH? - 是否可以将 worker pipe 读事件的移除延迟到 Reactor 中的存量请求完成之后?
- 如果这是预期行为,官方是否有推荐方式保证退出前已提交 Task 的返回结果能够被正常处理?
Source: swoole/swoole-src