【发布时间】:2016-03-03 20:22:57
【问题描述】:
上下文是Redis issue。我们有一个wait3() 调用,它等待 AOF 重写子进程在磁盘上创建新的 AOF 版本。当孩子完成后,通过wait3() 通知父母,以便用新的 AOF 替换旧的 AOF。
但是,在上述问题的上下文中,用户通知了我们一个错误。我对 Redis 3.0 的实现做了一些修改,以便清楚地记录 wait3() 何时返回 -1 而不是因为这种意外情况而崩溃。所以这显然是发生了什么:
-
wait3()在我们有待处理的孩子等待时调用。 -
SIGCHLD应设置为SIG_DFL,Redis 中根本没有设置此信号的代码,因此这是默认行为。 - 当第一次 AOF 重写发生时,
wait3()成功按预期工作。 - 从第二次 AOF 重写开始(创建第二个孩子),
wait3()开始返回 -1。
AFAIK 在当前代码中我们调用wait3() 是不可能的,而没有挂起的子代,因为在创建 AOF 子代时,我们将 server.aof_child_pid 设置为 pid 的值,我们只重置它在成功的wait3() 呼叫之后。
所以wait3() 应该没有理由以-1 和ECHILD 失败,但确实如此,所以可能不是出于某种意外原因创建了僵尸孩子。
假设 1:Linux 在某些奇怪的情况下可能会丢弃僵尸孩子,例如因为内存压力?看起来不合理,因为僵尸只附加了元数据,但谁知道呢。
请注意,我们将wait3() 称为WNOHANG。鉴于SIGCHLD 默认设置为SIG_DFL,唯一会导致失败并返回-1 和ECHLD 的条件应该是没有僵尸可用于报告信息。
假设2:其他可能发生但无法解释的事情是,在第一个孩子死后,SIGCHLD 处理程序设置为SIG_IGN,导致@987654342 @ 返回 -1 和 ECHLD。
假设3:有没有办法从外部去除僵尸孩子?也许这个用户有某种脚本可以在后台删除僵尸进程,这样wait3() 的信息就不再可用?据我所知,如果父母不等待它(使用waitpid 或处理信号)并且如果SIGCHLD 未被忽略,则应该永远不可能删除僵尸,但也许存在是一些 Linux 特有的方式。
假设4:实际上Redis代码中存在一些bug,导致我们第一次成功wait3()child没有正确重置状态,后来我们一次次调用wait3()但不再有僵尸,所以它返回 -1。分析代码看起来不可能,但也许我错了。
另一件重要的事情:我们过去从未观察到这一点。显然只发生在这个特定的 Linux 系统中。
更新:Yossi Gottlieb 提出 SIGCHLD 出于某种原因被 Redis 进程中的另一个线程接收(不正常发生,仅在此系统上)。我们已经在 bio.c 线程中屏蔽了 SIGALRM,也许我们也可以尝试从 I/O 线程中屏蔽 SIGCHLD。
附录:Redis 代码部分选择
wait3() 被调用的地方:
/* Check if a background saving or AOF rewrite in progress terminated. */
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
int statloc;
pid_t pid;
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
int exitcode = WEXITSTATUS(statloc);
int bysignal = 0;
if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);
if (pid == -1) {
redisLog(LOG_WARNING,"wait3() returned an error: %s. "
"rdb_child_pid = %d, aof_child_pid = %d",
strerror(errno),
(int) server.rdb_child_pid,
(int) server.aof_child_pid);
} else if (pid == server.rdb_child_pid) {
backgroundSaveDoneHandler(exitcode,bysignal);
} else if (pid == server.aof_child_pid) {
backgroundRewriteDoneHandler(exitcode,bysignal);
} else {
redisLog(REDIS_WARNING,
"Warning, detected child with unmatched pid: %ld",
(long)pid);
}
updateDictResizePolicy();
}
} else {
backgroundRewriteDoneHandler的部分选择:
void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
if (!bysignal && exitcode == 0) {
int newfd, oldfd;
char tmpfile[256];
long long now = ustime();
mstime_t latency;
redisLog(REDIS_NOTICE,
"Background AOF rewrite terminated with success");
... more code to handle the rewrite, never calls return ...
} else if (!bysignal && exitcode != 0) {
server.aof_lastbgrewrite_status = REDIS_ERR;
redisLog(REDIS_WARNING,
"Background AOF rewrite terminated with error");
} else {
server.aof_lastbgrewrite_status = REDIS_ERR;
redisLog(REDIS_WARNING,
"Background AOF rewrite terminated by signal %d", bysignal);
}
cleanup:
aofClosePipes();
aofRewriteBufferReset();
aofRemoveTempFile(server.aof_child_pid);
server.aof_child_pid = -1;
server.aof_rewrite_time_last = time(NULL)-server.aof_rewrite_time_start;
server.aof_rewrite_time_start = -1;
/* Schedule a new rewrite if we are waiting for it to switch the AOF ON. */
if (server.aof_state == REDIS_AOF_WAIT_REWRITE)
server.aof_rewrite_scheduled = 1;
}
如您所见,所有代码路径都必须执行将server.aof_child_pid 重置为-1 的cleanup 代码。
问题期间 Redis 记录的错误
21353:C 29 Nov 04:00:29.957 * AOF 重写:写时复制使用了 8 MB 内存
27848:M 11 月 29 日 04:00:30.133 ^@ wait3() 返回错误:没有子进程。 rdb_child_pid = -1, aof_child_pid = 21353
如您所见,aof_child_pid 不是 -1。
【问题讨论】:
-
对我来说,这听起来好像你正在测试快,早,孩子根本还没有结束。
-
也许您可能想详细说明如何确保这一点:“wait3() 在我们有待处理的孩子等待时调用。”确实如此,因为显然不是。我不得不承认,我不知道 Redis 代码,但是您会使用哪些其他机制来同步进程的实时时间,但使用对
wait*()的调用?我会说你正面临一场比赛。 -
还有更多的可移植代码(并且可能会减少您观察到的此类问题),您希望将
signal()替换为sigaction()。 -
@antirez 旧的 unix 信号确实将信号处理程序重置为默认值 (
SIG_DFL)在第一次处理信号之后。所以假设 2 有可能发生。只需将signal()调用替换为sigaction()(不会重置为 SIG_DFL)即可查看是否为真。 -
Redis 在 sentinelCollectTerminatedScripts() 中有另一个 wait3() 调用,我们可以确定这不会占用
rdb_child_pid /server.aof_child_pid在这种情况下标识的进程吗?
标签: c linux system-calls zombie-process waitpid