Вычисления высокой доступности: как справиться с невозвратным системным вызовом, не рискуя ложными срабатываниями?

У меня есть процесс, работающий на компьютере с Linux как часть системы высокой доступности. Процесс имеет основной поток, который получает запросы от других компьютеров в сети и отвечает на них. Существует также поток пульса, который периодически отправляет многоадресные пакеты пульса, чтобы другие процессы в сети знали, что этот процесс все еще жив и доступен. они решат, что этот процесс умер, и возьмут на себя его обязанности, чтобы система в целом могла продолжать работать.

Все это работает довольно хорошо, но на днях вся система вышла из строя, и когда я исследовал, почему я обнаружил следующее:

  1. Из-за (что, по-видимому) ошибки в ядре Linux коробки, произошло «упс» ядра, вызванное системным вызовом, который сделал основной поток этого процесса.
  2. Из-за ошибки ядра системный вызов так и не вернулся, в результате чего основной поток процесса навсегда завис.
  3. Поток сердцебиения, OTOH, продолжал работать правильно, а это означало, что другие узлы в сети так и не поняли, что этот узел вышел из строя, и ни один из них не вмешался, чтобы взять на себя его обязанности... и поэтому запрошенные задачи не были выполнены. и работа системы фактически остановилась.

Мой вопрос: есть ли элегантное решение, которое может справиться с таким сбоем? (Очевидно, что нужно исправить ядро ​​Linux, чтобы оно не «упс», но, учитывая сложность ядра Linux, было бы неплохо, если бы мое программное обеспечение могло более изящно обрабатывать будущие другие ошибки ядра).

Одним из решений, которое мне не нравится, было бы помещать генератор сердцебиения в основной поток, а не запускать его как отдельный поток, или как-то иначе привязывать его к основному потоку, чтобы, если основной поток зависнет на неопределенное время, сердцебиения не будут отправлены. Причина, по которой мне не нравится это решение, заключается в том, что основной поток не является потоком реального времени, и, таким образом, это приведет к возможности случайных ложных срабатываний, когда операция с медленным завершением ошибочно принимается за сбой узла. Я хотел бы избежать ложных срабатываний, если я могу.

В идеале должен быть какой-то способ гарантировать, что неудачный системный вызов либо вернет код ошибки, либо, если это невозможно, приведет к сбою моего процесса; любой из них остановит генерацию пакетов пульса и позволит продолжить аварийное переключение. Есть ли способ сделать это, или ненадежное ядро ​​также обрекает мой пользовательский процесс на ненадежность?


person Jeremy Friesner    schedule 05.05.2015    source источник
comment
ввести возможность случайных ложных срабатываний, когда медленная операция была ошибочно принята за сбой узла - я не специалист в области высокодоступных вычислений, поэтому это может быть ошибочным, но похоже, что к этому следует относиться очень аналогично отказу узла. В частности, похоже, что вы хотите, чтобы другие узлы начали обрабатывать его работу.   -  person user2357112 supports Monica    schedule 05.05.2015
comment
Если бы вся работа была логически независимой, я бы согласился, но в этой системе узел играет особую роль, а наличие двух узлов, играющих одну и ту же роль одновременно, может вызвать путаницу, особенно когда исходный узел завершил свою долгую задачу только для того, чтобы найти другой узел, пытающийся занять его место. (Эта ситуация не будет фатальной для системы, но я хотел бы избежать этого, если только не произойдет реальный сбой оборудования)   -  person Jeremy Friesner    schedule 05.05.2015


Ответы (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, &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, чтобы проверить, какие системные вызовы используются, чтобы вы могли проверить, разумно ли ждать.

Это хакерское решение, но я не думаю, что вы найдете нехакерское решение этой проблемы. Несмотря на хакерство, я не думаю (это не проверено), что это будет особенно медленно; моя реализация приостанавливает отслеживаемый поток один раз в секунду на очень короткое время, которое, как я полагаю, будет в диапазоне сотен микросекунд. Теоретически это около 0,01% потери эффективности.

person Cel Skeggs    schedule 07.05.2015

Я думаю, вам нужен общий маркер активности.

Пусть основной поток (или, в более общем случае, все рабочие потоки) обновляет общий маркер активности текущим временем (или тактом часов, например, путем вычисления «текущей» наносекунды из clock_gettime(CLOCK_MONOTONIC, ...)), а поток сердцебиения периодически проверяет, когда этот маркер активности обновлялся в последний раз, отменяя себя (и, таким образом, останавливая широковещательную передачу пульса), если в течение разумного времени не было никакого обновления активности.

Эту схему можно легко расширить с помощью флага состояния, если рабочая нагрузка очень спорадическая. Основной рабочий поток устанавливает флаг и обновляет маркер активности, когда он начинает единицу работы, и очищает флаг, когда работа завершена. Если работа не выполняется, пульсация отправляется без проверки маркера активности. Если работа выполняется, пульсация останавливается, если время, прошедшее с момента обновления маркера активности, превышает максимальное время обработки, разрешенное для единицы работы. (Каждый из нескольких рабочих потоков в этом случае нуждается в собственном маркере активности и флаге, а поток пульса можно спроектировать таким образом, чтобы он останавливался, когда какой-либо один рабочий поток застревает или только когда застревают все рабочие потоки, в зависимости от их целей и важности для системы. общая система).

(Значение маркера активности (и рабочий флаг), конечно, должно быть защищено мьютексом, который должен быть получен перед чтением или записью значения.)

Возможно, поток сердцебиения также может привести к самоубийству всего процесса (например, kill(getpid(), SIGQUIT)), чтобы его можно было перезапустить, вызывая его в цикле в сценарии-оболочке, особенно если перезапуск процесса очищает условие в ядре, которое может вызвать проблема в первую очередь.

person Greg A. Woods    schedule 06.05.2015
comment
Проблема в том, что я не могу дать надежную верхнюю границу максимального времени обработки, допустимого для единицы работы. - person Jeremy Friesner; 07.05.2015
comment
Вы не можете??? Я думаю, вам, возможно, придется пересмотреть всю свою концепцию того, что значит знать, когда ваш основной поток жив. Если единица работы может занять неограниченное количество времени, вы никогда не можете быть уверены, что она ответит на новые рабочие запросы! - person Greg A. Woods; 07.05.2015
comment
Это нормально, если это займет много времени, если это то, что задумал пользователь. Что не нормально, так это то, что выполнение потока останавливается (например, из-за ошибки ядра) и вообще никогда не завершается (примечание: увы, это не то же самое, что сбой). Также не очень хорошо введение исправления, которое приводит к сбою системы (с точки зрения пользователя), если задача пользователя занимает больше времени, чем произвольно заданное количество времени для завершения. - person Jeremy Friesner; 07.05.2015
comment
Вы отклоняетесь от сценария, который описали в своем вопросе! Ваш рабочий поток недоступен во время работы, если выполнение некоторой единицы работы может занять неограниченное количество времени. О чем вы на самом деле спрашиваете? На каком основании вы определили интервал сердцебиения??? - person Greg A. Woods; 07.05.2015
comment
Это нормально, если последующие запросы должны ждать, пока не завершится первый запрос, потому что из-за особенностей системы в любой момент времени может обрабатываться только один запрос. Чего я пытаюсь избежать, так это сценария, описанного в моем вопросе, когда сбой ядра приводит к постоянной остановке обработки, но этот сбой не вызывает сбоя, и, таким образом, сбой не обнаруживается, и не происходит аварийное переключение на резервное оборудование. . Я хотел бы сделать это без добавления тайм-аутов, если это возможно, поскольку добавление тайм-аута означает, что есть риск сделать тайм-аут слишком коротким или слишком длинным. - person Jeremy Friesner; 07.05.2015
comment
Что ж, любой небесконечный тайм-аут короче, чем бесконечное зависание..... Как/должен ли пользователь обнаружить зависание сейчас? Что это за застрявший системный вызов? Сколько и какие системные вызовы выполняет основной поток при обработке одной единицы работы? Сколько времени обычно должен занимать системный вызов, который застрял? (Кстати, когда я сказал единицу работы, я не обязательно имел в виду всю работу для данного запроса.) - person Greg A. Woods; 07.05.2015
comment
Возможно ли вообще динамически определять верхнюю границу времени на основе выполняемой задачи? - person Cel Skeggs; 07.05.2015
comment
иногда я мог бы измерить его эмпирически, но это дает мне только время для этой конкретной задачи, под этой конкретной нагрузкой, на этой конкретной машине, в это конкретное время. Это может быть достаточно хорошо, если я затем умножу измерения на 5 или что-то в этом роде, но это не придаст мне большой уверенности, поскольку это не задача в реальном времени, и задачи, вероятно, в любом случае будут другими в будущем. - person Jeremy Friesner; 07.05.2015
comment
Грег, пользователь, теперь не может легко обнаружить зависание; все, что они могут сделать, это заметить, что поток не отвечает и использует 0% ЦП, и если они прочитают журнал ядра, они увидят сообщение oops в то же время, когда поток перестал функционировать. Неясно, какой системный вызов является виновником, но я думаю, что это вызов дискового ввода-вывода, поскольку трассировка стека «oops» связана с файловой системой. - person Jeremy Friesner; 07.05.2015
comment
Приоритет № 1 -- SIGQUIT процесса, получение дампа ядра и выяснение, где находится каждый поток !!! (компилируйте с -g, а в идеале с -O0 сначала!) # 2 подумайте о том, может ли внутренний цикл обработки в рабочем потоке щелкнуть таймер активности. - person Greg A. Woods; 07.05.2015

Одним из возможных способов может быть другой набор сообщений пульса от основного потока к потоку пульса. Если он перестает получать сообщения в течение определенного периода времени, он также прекращает их отправку. (И можно попробовать другое восстановление, например, перезапустить процесс.)

Чтобы решить проблему с основным потоком, фактически просто находящимся в длительном сне, установите (правильно синхронизированный) флаг, который устанавливает поток пульса, когда он решил, что основной поток, должно быть, потерпел неудачу - и основной поток должен проверить этот флаг в подходящее время (например, после потенциального ожидания), чтобы убедиться, что он не был зарегистрирован как мертвый. Если это так, он перестает работать, потому что его работа уже была занята другим узлом.

Основной поток также может отправлять события I-am-alive потоку пульса в другое время, а не только один раз в цикле, например, если он переходит к длительной операции. Без этого невозможно определить разницу между неисправным основным потоком и спящим основным потоком.

person Cel Skeggs    schedule 06.05.2015
comment
Проблема, которую я вижу с этим подходом, заключается в том, что немного непрактично вставлять вызовы SendIAmAlive() во все возможные подпрограммы, которые может когда-либо вызывать программа (некоторые из которых находятся в стороннем коде и т. д.). - person Jeremy Friesner; 06.05.2015
comment
Это не должно быть везде - только там, где это важно. Я бы предположил, что если ваш основной процесс обрабатывает сетевые запросы, каждый раз в цикле будет очевидное место, где он ожидает другого запроса. Это было бы очевидным местом для вызова NotifyIAmAlive(). Единственный раз, когда он должен идти в другое место, это если у вас есть действительно длинные операции в другом месте цикла. - person Cel Skeggs; 06.05.2015
comment
Дело в том, что не всегда очевидно, где будут действительно длинные операции, и они не обязательно будут в коде, который я могу модифицировать. Я не хочу рисковать, если узнаю, что я пропустил момент, когда мне звонит клиент и сообщает, что моя сверхнадежная система указывает на аппаратный сбой всякий раз, когда они пытаются выполнить операцию X. - person Jeremy Friesner; 06.05.2015
comment
Хм... так что я полагаю, что вам действительно нужен способ определить разницу между аварийным системным вызовом и действительно долго выполняющимся вызовом процедуры. К сожалению, я не думаю, что механизм для этого существует (по крайней мере, на уровне системных вызовов), а это означает, что все, что я думаю, вы можете сделать, это найти способ угадать природу паузы. - person Cel Skeggs; 07.05.2015
comment
Может быть, мне нужен способ для потока сердцебиения наблюдать за регистром счетчика программ основного потока, чтобы увидеть, перемещается ли он когда-либо... хм, звучит сложно :) - person Jeremy Friesner; 07.05.2015
comment
Вы могли бы использовать ptrace? Но это может быть медленно. - person Cel Skeggs; 07.05.2015