Автономные боты и проверка на истории: инженерия надёжности
Собственная внутренняя разработка в NDA-домене «финтех»: десяток автономных сервисов, которые принимают решения в реальном времени по потоку внешних событий. Кейс не про доходность — про инженерию: сбор данных с прослеживаемым источником, фоновые службы, которые не умирают тихо, проверка на реальной истории вместо веры в красивую кривую, и риск-контур, который выключает систему раньше человека.
Автономная система ломается тихо
У фонового процесса нет пользователя, который пожалуется. Он продолжает крутиться, писать логи и честно рапортовать «жив» — при том что данных он уже не получает, а решения принимает по устаревшему состоянию. Между поломкой и её обнаружением проходят часы, и всё это время система выглядит здоровой.
Три реальных сценария из этого проекта. Первый: внешний API поменял одно слово в названии
инструмента — парсер перестал распознавать часть потока, поломку заметили спустя
10 часов. Второй: сокет открыт,
исключений нет, события идут — но события одного конкретного типа не приходят уже
20+ минут; общий сторожевой процесс, следивший за «последним событием вообще», молчал, потому что остальные
типы продолжали идти. Третий: сторожевой процесс часами писал
restarts=0 failures=0,
пока два процесса были мертвы — pgrep -f
без якоря матчил командную строку соседних шеллов, включая собственную.
И четвёртый класс, самый дорогой: система не падает, а тихо врёт. Офлайн-симулятор определял исход сделки по неверному моменту фиксации — совпадение с реальным правилом расчёта оказалось 51%, то есть подбрасывание монеты. Всё, что было построено поверх этого симулятора, месяцами выглядело как аналитика, а было шумом. Пока никто не проверил само правило расчёта, отличить одно от другого было нечем.
Пять контуров, каждый проверяется отдельно
Система принятия решений в реальном времени — это не «алгоритм». Это пять независимых контуров: сбор данных, живучесть демонов, наблюдаемость, офлайн-проверка гипотез и риск-контур. Ломается любой из них по отдельности, поэтому каждый должен быть проверяем без остальных.
Основной поток событий плюс догоняющий опрос раз в 5 с. Каждая запись в базе помечена источником, временем и тем, кто её загрузил
Свой запускатор на Python с двойным ветвлением вместо запуска из оболочки, systemd с автоперезапуском и общим журналом
Сторожевой процесс отдельно на каждый тип события, проверка на изменение формата данных у внешнего сервиса, сводка в мессенджер
Прогон по историческим записям: реальные движения цены, отсев повторов, разбивка по режимам рынка
Многослойная проверка по расписанию, файл-метка полной остановки как аварийный выключатель, возврат в работу только через скрипт
Bash не отцепляет демон на macOS — это проверено, а не мнение
Перезапуск процесса из скрипта выглядит тривиально ровно до первого зависания. Пять вариантов
фонового запуска были проверены эмпирически, и все пять оставляли родительский шелл висеть в
__wait4
на якобы отцепленном потомке — один процесс провисел так 1 ч 32 мин:
nohup … & disown— классика, которая не работаетset -m+ disown, запуск в подоболочке, вложенные варианты форка — тот же результат- в macOS нет
setsid(1), поэтому «правильного» шелл-способа просто не существует
Решение — один общий запускатор на Python:
fork → setsid → fork → execvp,
внук гарантированно получает PPID=1.
Все перезапуски в проекте идут только через него; для задач под macOS дополнительно выставлен
AbandonProcessGroup,
иначе система прибирает потомков вместе с завершившимся скриптом.
Живучесть меряется по каждому типу события отдельно
Базовый сторожевой процесс следит за временем последнего события любого типа и останавливает сервис, если тишина длится дольше 360 секунд — порог подобран эмпирически, на 180 с ловились ложные срабатывания на штатных паузах цикла. Служба управления поднимает сервис обратно, поэтому сторожевой процесс может позволить себе просто выйти с особым кодом возврата.
Но это лечит только полную смерть соединения. Реальный инцидент был тоньше: перестал приходить один тип события, остальные текли как обычно — и общий счётчик тишины не сработал ни разу. Отсюда правило, которое теперь применяется ко всем сервисам: liveness — это не «процесс жив», а «когда я последний раз получил то, ради чего я живу». У каждого критичного типа события свой счётчик, в отчёт о состоянии выводится «сколько минут назад» по каждому потоку, плюс превентивная переподписка по таймеру.
Проверка «процесс жив» должна быть честной
Шаблоны pgrep -f
обязаны быть заякорены на конец аргументов
(…/book_recorder\.py$).
Без якоря проверка матчит любой соседний шелл, у которого искомая строка оказалась в командной
строке — включая сам вызывающий скрипт. Мониторинг, который сам себя считает живым процессом,
хуже отсутствия мониторинга: он выдаёт зелёный статус на мёртвую систему.
Сначала SQL и происхождение каждой строки
Правило для всего нового кода: данные пишутся в базу, а файлы JSONL допустимы только как скользящий
буфер с ретеншеном ≤48 ч и ежедневной ротацией. У каждого датасета — строка в
data_provenance
(источник, статус данных, заметки аудита, можно ли удалять), у каждой записи —
ingested_at
и ссылка на исходный датасет. Повод был предельно бытовой: аудит нашёл десятки гигабайт
дубликатов, которые никто не мог удалить, потому что никто не мог доказать их происхождение.
- живые писатели открывают соединение на flush и сразу закрывают — блокировка держится миллисекунды, а не всё время жизни процесса
- база с непрерывной записью — single-writer; аналитические подключения только
read_only=True - новый живой писатель получает отдельный файл базы, а не делит его с существующим
Записи о решениях не удаляются никогда
Постмортем, из которого выросло отдельное правило: около пяти недель истории решений исчезли под давлением на диск — штатная ротация архивов снесла файлы, которые невозможно восстановить. Ответ — хранилище, куда можно только дописывать: все записи о входах и закрытиях собираются из живых прогонов и из архивов, дедуп по хешу строки (операция идемпотентна, можно гонять хоть каждые 15 минут), синхронизация на офсайт-копию. Главное правило вшито в код чистки: харвест выполняется до любого удаления, и если он не удался — удаление отменяется целиком. Диск дешевле данных, всегда.
Событийная реакция вместо опроса по таймеру
Опрос раз в 30 секунд — нормальное решение, когда события редкие. Здесь окна принятия решения живут секунды, поэтому реакция идёт на входящее событие, а опрос остаётся догоняющей проверкой с интервалом 5 с. Полный путь измерен по компонентам, чтобы спорить о задержках можно было цифрами, а не ощущениями:
- подключение и подписка — ~40 мс; запрос детали — ~26 мс; чтение внешнего состояния — ~35 мс; отправка — ~41 мс
- криптографическая подпись — 0,24 мс после перехода с реализации на чистом Python на встроенную в систему; до этого она была на три порядка дороже и съедала весь выигрыш
- фоновые кэши и параллельные запросы вместо последовательных ожиданий — ещё десятки миллисекунд
- быстрый путь целиком — 250–350 мс; ниже без смены архитектуры не опуститься, дальше упор в сеть
Ничего экзотического — всё держится на дисциплине
Приём потока событий, циклы принятия решений, сторожевые задачи рядом с рабочим кодом
Реакция на событие — основной путь, опрос раз в 5 с — только чтобы догнать пропущенное
Основное хранилище — база; у каждой записи есть источник; пишет один процесс, аналитика читает только на чтение
10 сервисов на одной машине, перезапуск без ручных действий, журналы в одном месте
Фоновые задачи на macOS, которые гарантированно не умирают вместе с терминалом
Синхронизация хранилища записей, определение исходов сделок, регулярные сверки
Только там, где нужен сырой поток; всё остальное — в базе
Ошибка закрывается тестом, который бы её поймал, — иначе она вернётся
Состояние всех сервисов и «сколько минут назад» по каждому потоку событий
Правила с датой доказательства, инцидентом и условиями отмены
Система не стала непадающей — она стала честной
порог по каждому типу событий и проверка на изменение формата данных
на одном хосте, Restart=always + journald, перезапуск без человека
совпадение с реальным правилом расчёта после исправления — на всей проверенной выборке, она пока небольшая
Журнал инвариантов вместо устной памяти
Каждое найденное правило записывается как пронумерованный инвариант: дата, когда правило доказано, инцидент, который его породил, инструкция «как применять» и — отдельным пунктом — условия, при которых правило перестанет действовать. Правило не появляется из головы: оно появляется после инцидента и содержит способ его воспроизвести. За счёт этого одна и та же ошибка не переоткрывается через полгода на новом сервисе.
Проверка на истории, которая не льстит
Проверять исправление на живом запуске — значит ждать дни и всё равно не иметь статистики. Вместо этого исправление прогоняется по историческим записям: у каждой закрытой записи сохранены реальные экстремумы движения, поэтому контрфактический результат считается по тому, как всё двигалось на самом деле, а не по пересимуляции. Методика зафиксирована как набор обязательных правил:
- сначала метрики без допущений — распределения и доли достижения уровней; если движения нет вообще, никакая настройка выходов не поможет
- дедуп реплик: один и тот же сигнал размножается по конфигурациям примерно в 12 раз — без дедупа выборка фиктивно раздувается
- сегментация по режиму рынка и по типу сигнала; смешанное число прячет, что система выигрывает в одном режиме и проигрывает в другом
- границы для неоднозначных случаев: если порядок событий внутри бара неизвестен, считаются оптимистичная и пессимистичная оценки; вывод принимается, только если он одинаков в обеих
- контроль случайным входом: та же логика выхода на случайных входах должна проигрывать — иначе «эффект» создан не сигналом, а механикой выхода
- n ≥ 30 в каждой ячейке, out-of-sample и walk-forward, поправка на множественную проверку гипотез
Практический эффект неприятный и полезный одновременно: большинство идей, которые выглядели работающими, на этой методике отваливаются. Из восьми проверенных механических стратегий выжили две. Это и есть результат — знать, что остальные шесть не работают, до того как за них заплачено.
Риск-контур выключает раньше человека
Многослойный монитор ходит по расписанию и проверяет несколько независимых условий остановки. Останов оформляется файлом-маркером, который видят все процессы, — это простой и надёжный kill-switch без сетевой зависимости. Дальше действует дисциплина: маркеры остановки не редактируются руками, возврат в работу — только через штатный скрипт, который проверяет причину останова. И отдельное правило для новых фильтров: сначала audit-режим, где фильтр только логирует своё решение, и лишь после накопления данных он получает право блокировать.
Общий итог, если убрать детали: цифрам, которые выдаёт система, стало можно верить, а отказы стали видимыми за минуты, а не за часы. Для автономной системы это и есть главное свойство — всё остальное строится поверх.
Где ещё ложится та же методология
Это кейс не про трейдинг. Это типовая задача «автономный процесс принимает решения по потоку внешних событий, и никто не жалуется, когда он ломается». Контур переносится почти без изменений:
- → Интеграции с внешними API, где формат меняется без предупреждения — canary на дрейф схемы вместо разбора инцидента постфактум
- → Фоновые обработчики и очереди, где «процесс жив» ≠ «работа идёт»: liveness по типам событий, а не по PID
- → Машинное обучение и аналитика, где расчёты на исторических данных расходятся с рабочей системой — сначала проверяется правило расчёта целевого показателя, потом сама модель
- → Автономные AI-агенты, которые тратят деньги или совершают действия — им нужны тот же аварийный выключатель, режим наблюдения и лимиты
- → Наборы данных под аудит — записанное происхождение каждой строки единственный способ через полгода ответить, откуда взялась цифра в отчёте
- → Телеметрия, IoT, логистика — тот же класс: поток событий, узкие окна реакции, отказы без единого сообщения об ошибке
- Запускатор с двойным ветвлением и готовые описания служб для systemd и launchd — фоновая задача действительно отцепляется от терминала
- Сторожевой процесс на каждый тип событий, проверка на изменение формата данных у внешнего сервиса и отчёт о состоянии со временем последнего события
- Слой хранения на базе SQL: у каждой записи есть источник, соединение открывается только на запись, чтение отделено от записи
- Хранилище только на дозапись: данные собираются до любой чистки, при сбое сбора чистка отменяется, копия хранится отдельно
- Методика офлайн-проверки: дедуп, сегментация, границы для неоднозначных случаев, контроль случайным входом
- Формат журнала инвариантов: дата доказательства, инцидент, как применять, когда правило умрёт
Если процесс работает без надзора, вопрос не «упадёт ли», а узнаете ли вы об этом
Начинать имеет смысл с двух измерений: через сколько минут вы узнаете о тихом отказе и считает ли офлайн-оценка то же самое, что прод. Обычно этих двух проверок хватает, чтобы найти первые две-три дыры — до того, как они найдутся сами.
Похожие кейсы
Избирательная маршрутизация трафика через собственный сервер
Статические маршруты по AS-диапазонам вместо неработающего механизма авто-маршрутов, точечные DNS-привязки…
Конвертер выписок Федерального Казначейства в 1С
XML-протокол V3 + V4 → формат 1CClientBankExchange, Windows-1251.
Аудит процессов: 3 точки автоматизации, окупаемость 4,2× за 12 мес
Аудит за 2 недели: наблюдение за работой 6 ролей, замер времени по 20 главным операциям. Нашли: синхронизация…
Аудит за 5 000 ₽ — с конкретным отчётом и сметой
Расскажу что внедрить в вашем бизнесе в первую очередь, какая будет окупаемость, и нужен ли вообще AI для вашей задачи (иногда — нет).
Или просто напишите свой вопрос — отвечу в течение 2 часов