grpc-streams-checker: мониторинг gRPC-стримов

Проблема

Для API в стиле запрос/ответ мониторинг доступности давно решён: отправить запрос, проверить статус-код — и готово. Серверные стриминговые RPC (server-side streaming RPCs) ломают эту модель. Стрим — это канал, который сервер держит открытым и по которому со временем отправляет сообщения. Поэтому сокет может быть поднят, хэндшейк может завершиться успешно, health-эндпоинт может быть зелёным — а данные при этом не поступают вообще.

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

client ──▶ LB ──▶ Envoy (grpc-web / HTTP/2) ──▶ backend service ──▶ upstream feed

Каждый узел — потенциальное место, где стрим может тихо умереть, сохраняя при этом полную видимость здоровья:

  • idle-timeout на балансировщике или прокси незаметно убивает долгоживущие соединения;

  • трансляция Envoy / grpc-web может буферизовать или дропать фреймы, пока TCP-сессия остаётся активной;

  • бэкенд держит стрим открытым, но апстрим-фид замолчал — серверу нечего отправлять;

  • стрим генерирует фреймы, но после неудачного деплоя они содержат мусор — пустые payload’ы, отсутствующие поля.

Ни один из этих сценариев не виден HTTP 200-пробе или gRPC health check. Единственная проверка, которая отвечает на вопрос «действительно ли этот стрим отдаёт валидные данные?» — это клиент, который открывает стрим, читает несколько фреймов и проверяет их содержимое. Именно этим и занимается данный чекер.

Что делает одна проверка

Для каждого стрима из конфигурации при каждом запуске чекер:

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

  2. открывает серверный стриминговый RPC как настоящий клиент (TLS или plaintext, с опциональными auth-метаданными);

  3. собирает minFrames фреймов (по умолчанию 1; установите 3–5, чтобы проверить устойчивый поток, а не единичный первый пакет) в рамках временного бюджета, замеряя время до первого фрейма и максимальный разрыв между фреймами;

  4. валидирует payload каждого фрейма по заданным правилам — обязательные поля, точное совпадение, регулярные выражения;

  5. отменяет стрим и записывает результат в метрики Prometheus.

Один неудачный стрим не прерывает весь запуск; каждый режим отказа получает собственную метку результата: ok · timeout · insufficient_frames · validation_failed · error (с gRPC-статус-кодом).

Как это выглядит в продакшене

Дашборд Grafana на основе метрик чекера — наблюдение за шестью продакшен-стриминговыми методами в течение недели: процент успешных проверок по методам, количество запросов и топ-10 по длительности стрима:

Дашборд Grafana на основе метрик grpc-streams-checker

Быстрый старт (60 секунд, без инфраструктуры)

git clone https://github.com/youngpabl0/grpc-streams-checker.git
cd grpc-streams-checker && npm ci

# terminal 1 — demo gRPC server with a PriceStream
node examples/demo-server.js

# terminal 2 — run the checker against it
cp streams.example.json streams.json
node src/index.js --once
#  → {"stream":"price_stream","result":"ok","frames":3,"firstFrameMs":223,...}

Сломайте сервер и понаблюдайте, как чекер это замечает:

node examples/demo-server.js --silent    # stream opens, no frames  → result: timeout
node examples/demo-server.js --broken    # frames with empty fields → result: validation_failed
node examples/demo-server.js --slow=2000 # sluggish producer        → max_inter_frame_ms grows

Флаг --once завершает процесс с ненулевым кодом, если хотя бы один стрим завершился с ошибкой — удобно использовать как CI-шлюз или cron-пробу.

Конфигурация

{
  "addr": "localhost:50051",        // default target for all streams
  "dc": "dev",                      // datacenter label on every metric
  "tls": false,                     // TLS on by default; false = plaintext
  "intervalSeconds": 30,            // how often to re-check (daemon mode)
  "firstFrameTimeoutMs": 5000,      // budget for collecting minFrames
  "minFrames": 3,                   // frames to demand per check (sustained flow)
  "protoIncludeDirs": ["protos"],   // resolve `import` statements inside protos

  "auth": {                         // optional; ${VARS} expand from the environment
    "token": "${GRPC_TOKEN}",       // → authorization: Bearer <token>
    "metadata": { "x-tenant": "${TENANT_ID}" }
  },

  "streams": [
    {
      "name": "price_stream",              // metric label + --only key
      "proto": "protos/price.proto",       // path to the .proto file
      "package": "example.v1",
      "service": "PriceService",
      "method": "PriceStream",             // a server-side streaming RPC
      "request": { "symbol": "BTCUSD" },   // request message for the call
      "minFrames": 3,                      // per-stream override
      "timeoutMs": 10000,                  // per-stream override
      "addr": "other-host:443",            // per-stream override
      "tls": true,                         // per-stream override
      "expect": {                          // payload validation, per frame
        "fields": ["symbol", "price", "ts"],      // must exist & be non-empty
        "match": { "symbol": "BTCUSD" }           // exact match…
        // "match": { "symbol": "/^BTC/" }        // …or /regex/
      }
    }
  ]
}

Переменные окружения для переопределения настроек: CONFIG, ADDR, DC, PORT (по умолчанию 4777), TIMEOUT_MS, INTERVAL_SECONDS, MIN_FRAMES, LOG_LEVEL, LOG_PRETTY=true.

Для более сложной аутентификации — JWT, AWS SigV4, mTLS — расширьте единственную точку расширения в src/auth.js.

Подключение .proto-файлов

Чекер загружает исходные .proto-файлы во время выполнения — компилировать стабы не нужно. Три распространённых способа разместить их рядом с чекером:

  1. Скопировать напрямую — поместите файлы в директорию protos/ в вашем деплойменте и укажите "proto": "protos/your_service.proto". Если ваши .proto-файлы импортируют общие определения, перечислите корневые директории в "protoIncludeDirs", чтобы импорты разрешались корректно.

  2. Git submodule / subtree — если в вашей организации есть монорепозиторий с .proto-файлами (что встречается часто), зафиксируйте его версию: git submodule add <proto-repo-url> protos. Тогда чекер всегда тестирует именно ту версию контракта, которую вы зафиксировали — обновите сабмодуль, чтобы протестировать новый контракт.

  3. Встроить в образ — в своём Dockerfile добавьте слой COPY protos/ /app/protos/, или монтируйте директорию во время выполнения: -v $PWD/protos:/app/protos:ro.

Встроенные типы Google (well-known types, google/protobuf/.proto) входят в состав protobufjs и разрешаются автоматически; protoIncludeDirs нужен только для *ваших общих импортов.

Метрики

Доступны по адресу :4777/metrics (/healthz — для пробы живости). Метки на каждой метрике стрима: stream, method (RPC-метод — используется в дашбордах с разбивкой по методам), addr, dc.

Процент успешных проверок по методу — запрос, лежащий в основе панелей дашборда выше:

sum by (method) (rate(grpc_stream_check_total{result="ok"}[5m]))
/
sum by (method) (rate(grpc_stream_check_total[5m]))
Метрика Тип Значение

grpc_stream_up

gauge

1 = последняя проверка прошла успешно (фреймы получены и payload валиден)

grpc_stream_check_total{result}

counter

проверки по результату: ok/timeout/insufficient_frames/validation_failed/error

grpc_stream_error_total{code}

counter

ошибки по gRPC-статус-коду (UNAVAILABLE, DEADLINE_EXCEEDED, …)

grpc_stream_first_frame_ms

gauge

время до первого фрейма в последней проверке

grpc_stream_first_frame_duration_seconds

histogram

то же, для вычисления перцентилей во времени

grpc_stream_max_inter_frame_ms

gauge

максимальный разрыв между фреймами в рамках одной проверки — индикатор устойчивости потока

grpc_stream_frames_last_check

gauge

количество фреймов, полученных в последней проверке

grpc_stream_frames_total

counter

общее количество полученных фреймов за всё время

grpc_stream_validation_fail_total{rule}

counter

ошибки валидации payload: missing_field/mismatch/empty_frame

grpc_stream_last_success_timestamp_seconds

gauge

момент последней полностью успешной проверки стрима

grpc_stream_last_run_timestamp_seconds

gauge

момент последнего завершённого запуска чекера

Алертинг

Готовые правила Prometheus находятся в alerts/grpc-streams-checker.rules.yml — восемь алертов с уровнями серьёзности, окнами for: и описаниями в стиле runbook. Совместимы как с обычным rule_files: в Prometheus, так и с PrometheusRule для prometheus-operator. Ключевые алерты:

Алерт Условие Серьёзность

GrpcStreamDown

grpc_stream_up == 0 в течение 2м — валидные данные не поступают

critical

GrpcStreamFirstFrameSlow

первый фрейм приходит дольше 2с на протяжении 5м — деградация до наступления темноты

warning

GrpcStreamStuttering

разрыв между фреймами > 5с — продюсер не успевает

warning

GrpcStreamPayloadInvalid

ошибки валидации — данные испортились после деплоя

critical

GrpcStreamErrors

повторяющиеся gRPC-ошибки по статус-коду

warning

GrpcStreamNoRecentSuccess

ни один стрим не прошёл проверку за последние 5м

critical

GrpcStreamsCheckerDead

сам чекер остановился — тишина не означает здоровье

critical

Docker

docker build -t grpc-streams-checker .
docker run -p 4777:4777 \
  -v "$PWD/streams.json:/app/streams.json:ro" \
  -v "$PWD/protos:/app/protos:ro" \
  -e GRPC_TOKEN \
  grpc-streams-checker

Kubernetes

Готовые манифесты находятся в deploy/kubernetes/:

  • checker.yaml — ConfigMap’ы (streams.json + proto-файлы), Deployment с пробами liveness/readiness, консервативными ресурсами (запросы 25m/96Mi, лимиты 200m/256Mi — между запусками чекер простаивает), усиленным securityContext (не-root, read-only rootfs, без capabilities) и Service, открывающий /metrics.

  • servicemonitor.yaml — ServiceMonitor и обёртка PrometheusRule для prometheus-operator; группы правил берутся из alerts/grpc-streams-checker.rules.yml.

kubectl apply -f deploy/kubernetes/checker.yaml
# prometheus-operator users:
kubectl apply -f deploy/kubernetes/servicemonitor.yaml

Токены аутентификации должны храниться в Secret, экспортируемом как переменная окружения (GRPC_TOKEN) — конфигурационный файл ссылается на неё как ${GRPC_TOKEN}, поэтому никакие учётные данные никогда не попадают в ConfigMap.

Паттерн обобщается

Тот же подход — «открой, потребуй фреймы, проверь payload» — работает для любого долгоживущего транспорта с push-семантикой: WebSocket, SSE, консьюмеры очередей сообщений. Ценность состоит в том, чтобы превратить вопрос «данные действительно текут и они валидны?» в размеченные метрики, по которым вы сами себя будите, а не узнаёте об этом от пользователей.

На чём построен

Этот инструмент — тонкий, опinionated слой над отличными open-source проектами:

  • @grpc/grpc-js — чистый JS-клиент gRPC, выполняющий каждую проверку.

  • @grpc/proto-loader — загрузка .proto-файлов во время выполнения (через protobufjs); именно он устраняет необходимость в кодогенерации.

  • prom-client — метрики Prometheus для Node.

  • pino — быстрое структурированное JSON-логирование.

  • express — обслуживает /metrics и /healthz.

Лицензия

Apache License 2.0 — свободное использование, модификация и распространение; сохраняйте уведомление об авторских правах и файл NOTICE. Copyright 2026 Daniil Romashov.

© 2026 meganuke