我有一个在Linux计算机上运行的进程,作为高可用性系统的一部分。该进程有一个主线程,它接收来自网络上其他计算机的请求并响应它们。还有一个心跳线程定期发送多播心跳包,让网络上的其他进程知道这个进程仍然存在并且可用 - 如果他们暂时没有心跳任何心跳包,其中一人会认为这个过程已经死亡并将接管其职责,以便整个系统能够继续发挥作用。
这一切都运行良好,但有一天整个系统都失败了,当我调查为什么我发现了以下内容:
我的问题是,是否有一个优雅的解决方案可以处理这种失败? (显然要做的一件事就是修复Linux内核,以便它不会出现问题,但考虑到Linux内核的复杂性,如果我的软件可以处理未来的其他内核错误会更好优雅地)。
我不喜欢的一个解决方案是将心跳生成器放入主线程,而不是将其作为单独的线程运行,或者以其他方式将其绑定到主线程,以便在主线程中使用无限期地挂断,心跳不会被送到。我不喜欢这个解决方案的原因是因为主线程不是实时线程,所以这样做会引入偶然误报的可能性,其中一个缓慢完成的操作被误认为是一个节点失败。如果可以,我想避免误报。
理想情况下,有一些方法可以确保失败的系统调用返回错误代码,或者如果不可能,则会崩溃我的进程;其中任何一个都会停止生成心跳包并允许故障转移继续进行。有没有办法做到这一点,或者一个不可靠的内核是否会使我的用户进程失去不可靠性?
答案 0 :(得分:1)
我认为您需要一个共享活动标记。
让主线程(或者在更一般的应用程序中,所有工作线程)用当前时间(或时钟滴答)更新共享活动标记,例如通过从{{1计算"当前"纳秒如果在合理的时间内没有任何活动更新,则让心跳线程定期检查上次更新此活动标记的时间,取消自身(从而停止心跳广播)。
如果工作负载非常零星,则可以使用状态标志轻松扩展此方案。主工作线程设置标志并在开始工作单元时更新活动标记,并在工作完成时清除标记。如果没有工作,则发送心跳而不检查活动标记。如果正在进行工作,那么如果更新活动标记后的时间超过了工作单元允许的最长处理时间,则心跳停止。 (在这种情况下,多个工作线程都需要自己的活动标记和标记,并且心跳线程可以设计为在任何一个工作线程卡住时停止,或者仅在所有工作线程卡住时停止,具体取决于它们的目的和对整体系统)。
(活动标记值(和工作标志)当然必须受到在读取或写入值之前必须获取的互斥锁的保护。)
也许心跳线程也会导致整个进程自杀(例如clock_gettime(CLOCK_MONOTONIC, ...)
),以便可以通过在包装器脚本的循环中调用它来重新启动它,特别是如果进程重新启动清除了内核中的条件首先会引起问题。
答案 1 :(得分:1)
我的第二个建议是使用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, ®s);
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解决方案,但我不认为你会发现这个问题的非hacky解决方案。尽管有黑客,但我并不认为(这是未经测试的)它会特别慢;我的实现暂停监视线程每秒一次非常短的时间 - 我猜这将在100微秒的范围内。从理论上讲,效率损失约为0.01%。
答案 2 :(得分:0)
一种可能的方法是从主线程到心跳线程有另一组心跳消息。如果它在一段时间内停止接收消息,它也会停止发送消息。 (并且可以尝试其他恢复,例如重新启动过程。)
要解决主线程实际上处于长时间休眠状态的问题,请确定主线程必须已经失败时心跳线程设置的(正确同步)标志 - 并且主线程应该检查这个标志在适当的时候(例如在潜在的等待之后)确保它没有被报告为死亡。如果有,它会停止运行,因为它的作业已经被另一个节点占用了。
主线程还可以在循环周期之外的其他时间将I-am-alive事件发送到心跳线程 - 例如,如果它进入长时间运行的操作。如果没有这个,就无法区分失败的主线程和睡眠主线程之间的区别。