Этот пост разбирает один из наших ongoing-кейсов: DePIN саб-оператор, чей флот мы держим уже больше полугода. По договорённости с клиентом мы не называем ни протокол, ни имя оператора, но можем описать архитектуру, регламент работы и один инцидент, который оказался поучительным. Если коротко: 200 нод в 8 регионах, 99.94% аптайм в rolling 30 days, и 28-часовое расследование DNS-инцидента, который тихо съедал около 1.8% reward'а на участке EU-флота.
Setup
Клиент - DePIN sub-operator, держит storage / edge-compute нагрузку (по классу нагрузки сопоставимо с Filecoin или io.net, но конкретный протокол мы оставляем за скобками). На момент онбординга у них было около 130 нод, развёрнутых руками одним инженером, который выгорел и ушёл. К моменту, когда мы взяли флот, нод стало 200: 3 EU, 2 NA, 2 APAC, 1 LATAM регион. Микс по классам провайдеров: 4 коло-площадки и 1 bare-metal-on-demand вендор для всплесков ёмкости.
Reward-токен сети settle'ится каждые 6h epoch on-chain. Ноды отправляют proof-of-storage submissions на reward server сети; сервер агрегирует и пишет on-chain в конце эпохи. У нас внутри стоит свой expected-reward calculator: он смотрит на наблюдаемые submissions, применяет правила протокола и предсказывает, сколько reward'а должно прийти. Дальше reconciliation: сравниваем on-chain receipt с предсказанием. Drift >0.5% за эпоху это Sev-2 alert. Drift >0.2% устойчиво несколько эпох подряд это Sev-3.
Scope нашей работы: day-2 ops, региональный rebalancing (когда протокол меняет веса по регионам, мы перебрасываем capacity), reward reconciliation, и incident response с обещанным MTTA <15 минут и root cause в окне 12-24 часа.
Первые месяцы
Скучные. Это, в общем-то, то, как должно выглядеть нормальное операционное обслуживание. Флот стабилен, drift по reconciliation плавает в диапазоне 0.18-0.4% от ожидаемого. Sev-3 alert на reconciliation не срабатывал ни разу за первые четыре месяца. Несколько штатных событий: один коло-провайдер в Сингапуре анонсировал maintenance window, мы перевели 7 нод на временную ёмкость у bare-metal вендора и вернули обратно через 36 часов. Один диск в Амстердаме выдал SMART-предупреждение, заменили без даунтайма для нагрузки. Один протокольный апгрейд, прокатили по канарейке (3 ноды, потом 25 нод, потом весь флот) за 18 часов.
Если бы кейс закончился на этом, писать было бы не о чем. Интересное началось на пятом месяце.
Month 5, четверг 14:32 UTC: drift Sev-2
Reconciliation alert: drift скакнул с 0.32% (предыдущая эпоха) до 1.84% за одну 6-часовую эпоху. Pager на дежурного. Ack за 6 минут, в пределах SLO.
Первичный triage показал, что drift не случайный, а структурный: примерно 17 нод в EU-регионах «гостят». Они исправно отправляют proof-of-storage submissions, видны в наших локальных метриках, но не регистрируются на reward server'е. Reward server считает их офлайн, и за эпоху это съело около 1.6 процентных пункта от ожидаемого reward'а. Не катастрофа, но и не шум: clearly что-то сломалось, и сломалось одинаково на группе нод.
Часы 0-6: первые гипотезы
Прошли по очевидному:
- Health нод: процессы живые, диски здоровые, CPU и память в норме, локальные логи показывают успешные submissions.
- Upstream сеть от каждой ноды до периметра: packet loss в норме, RTT в норме, ничего не дропается.
- TLS до reward-server endpoint: сертификат валидный, SNI правильный, handshake завершается.
- On-chain settlement: никаких reorg'ов в нужном окне, блоки приходят, миссед-блоков нет.
- NTP по региону: разлёт между нодами <10 ms, в пределах допуска протокола.
К часу 6 все очевидные слои выглядели чистыми. Ноды думают, что они отправили submissions, ноды видят, что endpoint отвечает 200, а reward server при этом не видит submissions от 17 конкретных нод. Где-то по дороге submissions исчезают.
Часы 6-12: подняли network observability глубже
Запустили mtr с каждой из «гостящих» нод до reward endpoint'а и сравнили с маршрутами от 5 нод того же региона, которые работали нормально. Маршруты чистые на всех 22. Никаких аномалий по AS-хопам, никакой просадки на конкретном пиринге.
Но при сравнении что мы заметили: 12 из 17 «гостящих» нод резолвили rewards-eu.<network>.io в другой IP, чем 5 «здоровых» нод того же региона. И reward server, естественно, accept'ил submissions с одного набора IP-источников, а не с другого. Submissions уходили, но в никуда: попадали на старый IP, который технически отвечал, но логически уже не был активным reward-сервером этого региона.
Возник вопрос: чей DNS врёт.
Час 14: правильный resolver-инструмент
Здесь начинается интересное. На «гостящей» ноде запустили resolvectl statistics. Cache hit rate на reward endpoint около 100%, что в принципе нормально для часто запрашиваемого имени. Дальше resolvectl query rewards-eu.<network>.io --cache=no: вернулся другой IP, чем тот, который был в кеше. То есть кеш systemd-resolved отдавал стейл-запись.
Посмотрели возраст записи в кеше: больше 50 минут. При том, что reward server публикует A-запись с TTL 60 секунд. По-хорошему ноде следовало бы перерезолвить минимум 50 раз за это время, но она не делала ничего.
Причина: systemd-resolved имеет внутренний cache-max-time (значение по умолчанию там в районе 2 часов на момент инцидента), и эта настройка молча оверрайдит TTL входящих ответов. То есть TTL=60s из ответа применяется как «не меньше», а не как «и не больше». Если record хранится в кеше, он висит там столько, сколько хочет резолвер, а не столько, сколько разрешил origin.
Backstory, которую мы вытащили позже из ответа клиента и публичной документации сети: reward server использует DNS-based regional failover с TTL 60s. У них нормальный blue-green деплой, 4-5 раз в неделю в EU-регионе они переключают активный IP. В предыдущие месяцы наши ноды тоже хитили старые IP до получаса после каждого cutover. Но большую часть времени старый IP был ещё «жив» в смысле, что accept'ил соединение и форвардил submissions на новый бэкенд. На этой неделе они выкатили cutover на свежий хост за более строгой ACL: старый IP отвечал на TCP, отвечал 200 на health-чек, но submissions с неавторизованного источника тихо drop'ал.
Это объясняло всё. Submissions уходят, endpoint отвечает 200, reward server этих submissions не видит.
Часы 14-28: фикс по Ansible
Решение делилось на три шага.
Час 14-16, точечный фикс. Написали Ansible-роль, которая заменяет systemd-resolved на unbound на 17 EU-нодах. Минимальный конфиг unbound с max-cache-ttl: 0 для зоны reward-сервера и нормальный TTL для всего остального. Прокатили на 17 нод, проверили на каждой, что dig +trace возвращает актуальный IP и что reward server начал видеть submissions.
Час 16-22, прокатка по флоту. Та же роль батчами по 25 нод раскатилась на оставшиеся 183 ноды в 8 регионах. Между батчами 30-минутное окно наблюдения. Альтернативу с dnsmasq обсудили, отказались: unbound у нас уже стоит на нескольких bare-metal нодах под другой проект, оператировать два резолвера на разных частях флота смысла не было.
Час 22-28, синтетический чек и runbook. Добавили синтетический probe: каждая нода раз в эпоху делает dig +trace до reward endpoint своего региона и репортит резолвенный IP в наш центральный inventory. Если хотя бы одна нода отстаёт более чем на одну эпоху от активного IP, который видит большинство флота, срабатывает Sev-3. Параллельно обновили конфиг unbound: для зоны reward-сервера max-cache-ttl: 0, для всего остального стандартные значения. Никаких лишних запросов наружу, провисает только то, где это критично.
Час 28: drift вернулся к baseline
Reconciliation drift в эпохе сразу после прокатки фикса упал на 1.6 процентных пункта. К концу следующих 24 часов вернулся в нормальный коридор 0.18-0.35%. Клиент получил постмортем на 4 странице в течение 48 часов после закрытия инцидента, и компенсацию по reward'у мы рассчитали и применили в следующем биллинговом цикле.
Совокупный ущерб по reward'у: около 1.8% от ожидаемого за один уикенд эпох, в денежном выражении это четырёхзначная сумма в USDC. Не катастрофа в годовом масштабе, но достаточно, чтобы оправдать всю интеграцию reconciliation-калькулятора и Sev-2 алерта, который её ловит.
Что попало в раннбук
Четыре изменения в provisioning- и observability-регламенте:
- Любая DePIN-нагрузка с DNS-based failover на upstream-зависимостях провижионится с явной caching policy резолвера. Дефолтный
systemd-resolvedдля таких нод запрещён. - Выбор резолвера (
systemd-resolvedvsunboundvsdnsmasq) теперь явный пункт в provisioning-чеклисте, не предполагается по умолчанию. - Reward-drift Sev-2 alert триггерит DNS-cache audit раньше остальных проверок. Если за первые 30 минут расследования группа дрифтящих нод коррелирует географически, первая команда дежурного это
resolvectl statisticsиdig +traceна нескольких затронутых нодах. - Синтетический DNS-query чек на критичных endpoint'ах теперь стандартная observability для любого DePIN-флота с DNS-based failover. Один
dig +traceна ноду на эпоху, репорт в inventory, alert на расхождение.
Урок
Самый занятный слой стека (on-chain экономика, settlement, консенсус) вёл себя ровно как написано в документации. Скучный слой (libc, systemd-resolved, дефолтная политика кеша) тихо ел 1.5% reward'а за уикенд эпох, пока reconciliation Sev-2 не сработал.
Сигнал был доступен в resolvectl statistics буквально с первого дня операций: cache hit rate под 100% на endpoint, который должен инвалидироваться раз в минуту. Никто не смотрел, потому что аптайм нод выглядел зелёным, и потому что наш собственный Sev-3 порог по reconciliation был выставлен слишком мягко (0.5% за эпоху как Sev-2, без отдельного Sev-3 на устойчивый drift в 0.3-0.5%). После инцидента порог Sev-3 опустили до 0.25% устойчивых трёх эпох подряд.
Здесь нет «волшебного» урока. Все компоненты, которые ломались, общеизвестны и хорошо задокументированы. Урок в том, что в DePIN-операциях слой, на котором ломается дороже всего, обычно не тот, на который смотрит большинство дашбордов. Reconciliation между ожидаемым и фактическим reward'ом это не nice-to-have. Это единственный способ заметить, что протокол вам платит меньше, чем должен, и заметить это в окне часов, а не недель.
Команда XIMTRX