高可用性计算:如何处理不返回的系统调用,而不冒风险?

我有一个在Linux计算机上运行的进程,作为高可用性系统的一部分。 这个过程有一个主线程,接收来自networking上其他计算机的请求并作出响应。 还有一个心跳线程会周期性地发送多播心跳包,让networking上的其他进程知道这个进程仍然活着并且可用 – 如果他们没有心跳任何心跳包,他们会认为这个过程已经死了,并将接pipe它的职责,使整个系统能够继续工作。

这一切都工作得很好,但前一天整个系统失败了,当我调查为什么我find了以下内容:

  1. 由于Linux内核中显然存在一个bug,有一个由系统调用引发的内核“oops”,这个进程的主线程产生了这个内核。
  2. 由于内核为“oops”,系统调用永远不会返回,从而使进程的主线程永久挂起。
  3. 心跳线OTOH继续正常工作,这意味着networking上的其他节点从来没有意识到这个节点已经失败,没有一个人介入接pipe它的职责,所以没有执行所请求的任务系统运行有效停止。

我的问题是,是否有一个优雅的解决scheme可以处理这种失败? (显然有一件事是修复Linux内核,所以它不会“哎呀”,但考虑到Linux内核的复杂性,如果我的软件能够更好地处理未来的其他内核错误,那将是一件好事)。

我不喜欢的一种解决scheme是将心跳发生器放入主线程,而不是将其作为单独的线程运行,或者以其他方式将其连接到主线程,以便在主线程无限期挂起时,心跳不会被发送。 我不喜欢这个解决scheme的原因是因为主线程不是一个实时线程,所以这样做会引入偶然的误报的可能性,一个缓慢完成的操作被误认为是一个节点故障。 如果可以的话,我想避免误报。

理想情况下,有一些方法可以确保一个失败的系统调用返回一个错误代码,或者如果这是不可能的,崩溃我的过程; 其中任何一个都会停止生成心跳包并允许进行故障转移。 有没有办法做到这一点,或者是一个不可靠的内核也毁了我的用户进程不可靠?

    我的第二个建议是使用ptrace来查找当前的指令指针。 你可以有一个父线程来处理你的进程并且每秒中断一次以检查当前的RIP值。 这有点复杂,所以我编写了一个演示程序:(仅适用于x86_64,但应通过更改寄存器名称来解决)。

    #define _GNU_SOURCE #include <unistd.h> #include <sched.h> #include <stdlib.h> #include <stdio.h> #include <sys/syscall.h> #include <sys/ptrace.h> #include <sys/wait.h> #include <sys/types.h> #include <linux/ptrace.h> #include <sys/user.h> #include <time.h> // this number is arbitrary - find a better one. #define STACK_SIZE (1024 * 1024) int main_thread(void *ptr) { // "main" thread is now running under the monitor printf("Hello from main!"); while (1) { int c = getchar(); if (c == EOF) { break; } nanosleep(&(struct timespec) {0, 200 * 1000 * 1000}, NULL); putchar(c); } return 0; } int main(int argc, char *argv[]) { void *vstack = malloc(STACK_SIZE); pid_t v; if (clone(main_thread, vstack + STACK_SIZE, CLONE_PARENT_SETTID | CLONE_FILES | CLONE_FS | CLONE_IO, NULL, &v) == -1) { // you'll want to check these flags perror("failed to spawn child task"); return 3; } printf("Target: %d; %d\n", v, getpid()); long ptv = ptrace(PTRACE_SEIZE, v, NULL, NULL); if (ptv == -1) { perror("failed monitor sieze"); exit(1); } struct user_regs_struct regs; fprintf(stderr, "beginning monitor...\n"); while (1) { sleep(1); long ptv = ptrace(PTRACE_INTERRUPT, v, NULL, NULL); if (ptv == -1) { perror("failed to interrupt main thread"); break; } int status; if (waitpid(v, &status, __WCLONE) == -1) { perror("target wait failed"); break; } if (!WIFSTOPPED(status)) { // this section is messy. do it better. fputs("target wait went wrong", stderr); break; } if ((status >> 8) != (SIGTRAP | PTRACE_EVENT_STOP << 8)) { fputs("target wait went wrong (2)", stderr); break; } ptv = ptrace(PTRACE_GETREGS, v, NULL, &regs); if (ptv == -1) { perror("failed to peek at registers of thread"); break; } fprintf(stderr, "%d -> RIP %x RSP %x\n", time(NULL), regs.rip, regs.rsp); ptv = ptrace(PTRACE_CONT, v, NULL, NULL); if (ptv == -1) { perror("failed to resume main thread"); break; } } return 2; } 

    请注意,这不是生产质量的代码。 你需要做一堆修理工作。

    基于此,您应该能够计算出程序计数器是否正在前进,并可以将其与其他信息(如/proc/PID/status )结合起来,以查找系统调用中是否忙碌。 你也许可以扩展ptrace的使用来检查正在使用的系统调用,这样你就可以检查是否合理的等待。

    这是一个拙劣的解决方案,但我不认为你会发现这个问题的一个非hacky解决方案。 尽管有些</s>,,但我不认为(这是未经测试的)它会特别慢。 我的执行暂停监视的线程每秒一次在很短的时间 – 我想这将在100微秒的范围内。 从理论上讲,效率损失约为0.01%。

    我想你需要一个共享的活动标记。

    让主线程(或者更普通的应用程序,所有的工作线程)用当前时间(或时钟滴答,例如通过计算来自clock_gettime(CLOCK_MONOTONIC, ...)的“当前”纳秒)来更新共享活动标记,如果在合理的时间内没有任何活动更新,心跳线程会定期检查上次更新活动标记的时间,取消自己(并因此停止心跳广播)。

    如果工作量很小,这个方案可以很容易地用状态标志来扩展。 主工作线程设置标志并在开始工作单元时更新活动标记,并在工作完成时清除标记。 如果没有工作正在进行,那么心跳被发送而不检查活动标记。 如果正在完成工作,则如果自更新活动标记以来的时间超过了工作单元所允许的最长处理时间,则停止检测信号。 (在这种情况下,多个工作线程都需要自己的活动标记和标志,并且心跳线程可以设计为在任何一个工作线程卡住时停止,或者仅当所有工作线程卡住时才停止,具体取决于其目的和重要性整个系统)。

    (活动标记值(和工作标记)当然必须由在读取或写入值之前必须获取的互斥体保护)。

    也许心跳线程也可能导致整个过程自杀(例如kill(getpid(), SIGQUIT) ),以便通过在包装脚本中循环调用它,特别是在进程重新启动在内核中会导致问题的条件。

    一种可能的方法是将另一组心跳消息从主线程传输到心跳线程。 如果它在一定时间内停止接收消息,则停止发送它们。 (可以尝试其他恢复,如重新启动进程。)

    为了解决实际上只是长时间睡眠的主线程的问题,当确定主线程必须失败时,有一个心跳线程设置的(正确同步的)标志 – 主线程应该检查这个标志在适当的时间(例如在潜在的等待之后)以确保它没有被报告为死亡。 如果有,它会停止运行,因为它的工作已经被另一个节点占用了。

    主线程还可以在其他时间发送I-am-alive事件到心跳线程,而不是一次循环 – 例如,如果它正在进入长时间运行的操作。 如果没有这个,就没办法区分主线程和主线程之间的区别。