Высокая доступность вычислений: как справиться с невозвратным системным вызовом, не рискуя получить ложные срабатывания?
У меня есть процесс, который выполняется на компьютере Linux как часть системы высокой доступности. У процесса есть основной поток, который получает запросы от других компьютеров в сети и отвечает на них. Существует также поток пульса, который периодически отправляет многоадресные пакеты пульса, чтобы другие процессы в сети знали, что этот процесс еще жив и доступен - если они какое-то время не генерируют какие-либо пакеты пульса от него, один из они предполагают, что этот процесс умер, и возьмут на себя свои обязанности, чтобы система в целом могла продолжать работать.
Все это работает довольно хорошо, но на днях вся система вышла из строя, и когда я выяснил, почему я обнаружил следующее:
- Из-за (что очевидно) ошибки в ядре Linux на ядре, было "упс" ядра, вызванное системным вызовом, который сделал основной поток этого процесса.
- Из-за того, что ядро "упс", системный вызов никогда не возвращался, в результате чего основной поток процесса постоянно зависал.
- Поток сердцебиения, OTOH, продолжает работать правильно, что означало, что другие узлы в сети никогда не осознавали, что этот узел вышел из строя, и никто из них не вмешался, чтобы взять на себя его обязанности... и поэтому запрошенные задачи не были выполнены и работа системы фактически прекратилась.
У меня вопрос, есть ли элегантное решение, которое может справиться с такой неудачей? (Очевидно, одна вещь, которую нужно сделать, это исправить ядро Linux, чтобы оно не "упало", но, учитывая сложность ядра Linux, было бы неплохо, если бы мое программное обеспечение могло обрабатывать и другие ошибки ядра более изящно).
Одно из решений, которое мне не нравится, состоит в том, чтобы поместить генератор сердцебиения в основной поток, а не запускать его как отдельный поток, или каким-либо другим способом привязать его к основному потоку, чтобы, если основной поток зависал на неопределенное время, сердцебиение не будет отправлено. Причина, по которой мне не нравится это решение, состоит в том, что основной поток не является потоком в реальном времени, и таким образом это может привести к случайным ложным срабатываниям, когда медленная или полная операция была принята за ошибку узла. Я бы хотел избежать ложных срабатываний, если смогу.
В идеале должен быть какой-то способ убедиться, что сбойный системный вызов либо вернет код ошибки, либо, если это невозможно, завершит мой процесс; любой из них остановит генерацию пакетов пульса и позволит продолжить отработку отказа. Есть ли способ сделать это, или ненадежное ядро обрекает мой пользовательский процесс на ненадежность?
3 ответа
Мое второе предложение заключается в использовании 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, чтобы проверить, какие системные вызовы используются, чтобы вы могли проверить, стоит ли ожидать этого.
Это хакерское решение, но я не думаю, что вы найдете нехакерское решение этой проблемы. Несмотря на хакерство, я не думаю (это не проверено), что это будет особенно медленно; моя реализация приостанавливает отслеживаемый поток один раз в секунду на очень короткое время - что, я думаю, будет в диапазоне сотен микросекунд. Это примерно 0,01% потери эффективности, теоретически.
Я думаю, вам нужен общий маркер активности.
Попросите основной поток (или в более общем приложении, все рабочие потоки) обновить маркер общей активности с текущим временем (или тактом, например, вычисляя "текущую" наносекунду из clock_gettime(CLOCK_MONOTONIC, ...)
), и пусть поток пульса периодически проверяет, когда этот маркер активности последний раз обновлялся, отменяя саму себя (и, таким образом, останавливая трансляцию пульса), если в течение разумного времени не было обновления активности.
Эта схема может быть легко расширена с помощью государственного флага, если рабочая нагрузка очень спорадическая. Основной рабочий поток устанавливает флаг и обновляет маркер активности, когда он начинает единицу работы, и очищает флаг, когда работа завершена. Если работа не выполняется, пульс отправляется без проверки маркера активности. Если работа выполняется, то пульс останавливается, если время, прошедшее с момента обновления маркера активности, превышает максимально допустимое время обработки для единицы работы. (В этом случае нескольким рабочим потокам требуется собственный маркер активности и флаг, и поток пульса можно спроектировать так, чтобы он останавливался, когда застревал один рабочий поток, или только когда все рабочие потоки зависали, в зависимости от их целей и важности для общая система).
(Значение маркера активности (и рабочий флаг), конечно, должно быть защищено мьютексом, который должен быть получен перед чтением или записью значения.)
Возможно, сердцебиение может также привести к самоубийству всего процесса (например, kill(getpid(), SIGQUIT)
), чтобы его можно было перезапустить, вызвав его в цикле в скрипте-обертке, особенно если перезапуск процесса очищает условие в ядре, которое в первую очередь может вызвать проблему.
Один из возможных способов - иметь другой набор сообщений пульса из основного потока в поток пульса. Если он прекращает получать сообщения в течение определенного периода времени, он также прекращает отправлять их. (И может попробовать другое восстановление, такое как перезапуск процесса.)
Чтобы решить вопрос о том, что основной поток на самом деле просто находится в длительном сне, необходимо иметь (правильно синхронизированную) метку, которую поток пульса устанавливает, когда он решил, что основной поток должен был выйти из строя - и основной поток должен проверить этот флаг в соответствующее время (например, после потенциального ожидания), чтобы убедиться, что оно не было зарегистрировано как мертвое. Если это так, он прекращает работу, потому что его работа уже была бы занята другим узлом.
Основной поток также может отправлять события I-am-alive в поток пульса в другое время, чем один раз в цикле - например, если он входит в длительную операцию. Без этого невозможно определить разницу между неисправным основным потоком и спящим основным потоком.