Netpulse_SasS/agent/README.md
zotac c8075ee2a6 Етап 2: Go-агент — планувальник, буфер, сесія, модулі ICMP і SNMP
Модулі вкомпільовані (без .so): один бінарник на Alpine, Windows і роутер.
Креденшели беруться на момент виконання, бо мають TTL. Розклад вирівняний
по сітці інтервалу, тому переживає рестарт. Буфер обмежений і за кількістю,
і за пам'яттю; при переповненні викидає найстаріше, зміни статусу — останніми.

Перевірено на Debian 13 / Go 1.25: go vet чисто, go test -race усі пакети ok.
Релізний бінарник 12 МБ, базовий RSS 11.6 МБ при бюджеті 30 МБ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 03:46:46 +03:00

9.7 KiB
Raw Blame History

NetPulse Agent

Легкий зонд збору телеметрії. Єдиний статичний бінарник, усі з'єднання вихідні — у мережі клієнта не треба відкривати жодного порту.

go build -trimpath -ldflags "-s -w" -o netpulse-agent ./cmd/netpulse-agent
./netpulse-agent -server monitor.example.com:443 -agent-id <uuid> -cert agent.pem -key agent.key

Будова

cmd/netpulse-agent/       точка входу, збирання компонентів
internal/
  config/                 прапорці + NETPULSE_*, mTLS
  module/                 контракт модуля, реєстр, маршрутизація check_type → модуль
  telemetry/
    interner.go           series_ref: серія реєструється раз за сесію
    buffer.go             обмежений буфер із бюджетом пам'яті
  scheduler/              min-heap за часом запуску, семафор паралельності
  session/                gRPC-клієнт, рукостискання, реконект, ack
  modules/
    icmp/                 ping, RTT, jitter, втрати
    snmp/                 лічильники інтерфейсів (v2c/v3), довільні OID

Рішення, які варто розуміти

Модулі вкомпільовані, а не завантажуються динамічно. Плагінність на агенті навмисно простіша, ніж на сервері: жодних .so. Причина — той самий бінарник має працювати на Alpine, Windows і роутері з musl, а бюджет RSS не дозволяє тягнути рантайм плагінів. «Активація» означає, що сервер дозволив модуль цьому зонду (ControlDown.ModuleControl).

Креденшели беруться на момент виконання, а не з плану. У них TTL. Якби вони копіювались у задачу, після ротації паролів агент довбав би пристрої простроченими даними, доки не приїде новий план — і заблокував би обліковий запис на половині комутаторів. Прострочені не віддаються взагалі: явна помилка «немає креденшелів» краща за блокування.

Розклад вирівняний по сітці інтервалу, а не «зараз + інтервал». Після рестарту агента задача повертається у свій слот. schedule_offset рахує сервер детерміновано від check_id, тому 5000 чеків з інтервалом 60 с розведені й переживають перезапуск без збою фази.

Буфер викидає найстаріші дані, а не найновіші. Коли зв'язок відновиться, оператору потрібен передусім поточний стан мережі. Зміни статусу пристрою викидаються останніми: без них мапа показуватиме пристрій живим, поки він лежить. Кількість викинутого їде в AgentHealth.dropped_samples, щоб діра була видимою.

Швидкості інтерфейсів рахує агент. Лише він знає фактичний інтервал між двома опитуваннями; серверний розрахунок за номінальним інтервалом помиляється рівно на мережеву затримку й джитер планувальника. При перевороті лічильника виставляється counter_reset, і швидкості не заповнюються — краще діра в графіку, ніж стрибок на терабіт і хибний алерт.

Один писар у контрольний стрім. gRPC не допускає паралельних Send, а слати треба і heartbeat, і статуси задач. Тому окрема горутина-писар і канал. Читання підтверджень телеметрії — на іншій горутині: Send + Recv на різних горутинах це єдине, що gRPC дозволяє робити паралельно на одному стрімі.

GOMEMLIMIT виставляється в коді (48 МБ). Зонд часто живе на роутері або в контейнері з 64 МБ. Хай збирач працює агресивніше, ніж OOM killer вб'є процес і осліпить моніторинг саме тоді, коли він потрібен.

Параметри

Прапорець Змінна Призначення
-server NETPULSE_SERVER адреса сервера host:port
-agent-id NETPULSE_AGENT_ID ідентифікатор зонда з Enroll
-cert / -key / -ca NETPULSE_CERT / _KEY / _CA mTLS
-insecure NETPULSE_INSECURE=1 без TLS, лише локальний стенд
-modules NETPULSE_MODULES активні до першої відповіді сервера
-max-concurrency NETPULSE_MAX_CONCURRENCY скільки чеків паралельно
-buffer-items / -buffer-bytes NETPULSE_BUFFER_* ліміти буфера

Перевірка сертифіката сервера не вимикається прапорцем: зонд ходить через інтернет, і «тимчасово без перевірки» — це той самий канал, яким їдуть паролі від усіх комутаторів клієнта.

Параметри чеків

icmp.ping

{"count": 3, "packet_size": 56, "interval_ms": 100}

snmp.if — перелік інтерфейсів приходить від сервера, бо він уже є в inv.interfaces. Агент не ходить по ifTable, щоб з'ясувати, що існує: виявлення нових інтерфейсів — робота модуля topology, і вона окрема саме тому, що впирається в ліміт тарифу.

{"use_hc_counters": true,
 "interfaces": [{"if_index": 1, "interface_id": "<uuid>", "speed_bps": 1000000000}]}

snmp.get

{"oids": [{"oid": "1.3.6.1.4.1.9.9.109.1.1.1.1.8.1",
           "metric_key": "cpu.util", "unit": "pct", "scale": 1}]}

Стан перевірки

Стенд Debian 13, Go 1.25.13. go vet чисто, тести з -race:

Пакет Результат
internal/scheduler ok
internal/telemetry ok
internal/session ok
internal/modules/icmp ok

Заміряно на релізному бінарнику (CGO_ENABLED=0, -trimpath -s -w):

  • розмір — 12 МБ
  • базовий RSS у циклі реконекту — 11.6 МБ (бюджет 30 МБ)

Що саме доводять тести:

Тест Що перевіряє
TestNextRunSurvivesRestart слот задачі не з'їжджає після перезапуску агента
TestOffsetsSpreadLoad 60 різних offset дають 60 різних моментів запуску
TestSchedulerSkipsWhenPreviousStillRunning повільний чек не множить сам себе; пропуск явний
TestSchedulerRejectsUnknownModule задача на неактивний модуль відхиляється зі слідом
TestInternerLabelOrderDoesNotMatter недетермінований порядок map не плодить серій
TestBufferDropsOldestOnOverflow при переповненні лишається найновіше, викинуте пораховане
TestBufferPrioritisesStatusChanges зміна стану пролазить у батч поперед метрик
TestBufferRequeuePreservesData розрив між Send і Ack не втрачає ні семпли, ні дескриптори
TestSessionFullCycle Hello → план у зустрічному напрямку → виконання з креденшелами → телеметрія на сервері
TestSessionReconnectsAfterDrop агент повертається сам після розриву
TestPingLoopback реальний ICMP-сокет, не мок

Обмеження перевірки: TestPingLoopback пропускається під звичайним користувачем в unprivileged LXC — ядро не дає ані unprivileged-, ані raw-сокета, і sysctl net.ipv4.ping_group_range там недоступний. Під root на тому ж стенді тест проходить. У проді агенту треба CAP_NET_RAW або дозволений ping_group_range.

Модуль snmp не перевірявся проти живого пристрою — на стенді немає SNMP-агента. Він компілюється й проходить vet, але логіка перевороту лічильників і розрахунку util_pct чекає на перевірку з реальним обладнанням.