From d8649e2765a0495a2a98973ea922764784740080 Mon Sep 17 00:00:00 2001 From: fontvielle Date: Thu, 22 Oct 2026 16:12:03 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B2=D1=8F=D0=B7=D1=8C=20=D1=81=20?= =?UTF-8?q?=D0=9D=D0=A0=D0=94:=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=20M2M=20=D0=BD=D0=B5=20=D0=BF=D0=BE=D0=B2=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=20=D0=BA=D0=B0=D0=B6=D0=B4=D0=BE=D0=BC=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=B5=20bj;=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=BB=D1=87=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=9D=D0=A0=D0=94?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=20=E2=80=94=20=D0=B6=D1=91=D0=BB=D1=82=D1=8B?= =?UTF-8?q?=D0=B9,=20=D1=88=D0=B0=D0=BF=D0=BA=D1=83=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D1=81=D0=B8=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 22.10.2026 на 9 за день ушло больше 100 запросов #M2MHR (проверка + каждый перезапуск), с 14:01 НРД перестал отвечать — шапка уходила в красный при работающем обмене. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn --- internal/lkgateway/nsdhealth.go | 54 ++++++++++++++++++++++++++-- internal/lkgateway/nsdhealth_test.go | 4 ++- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/internal/lkgateway/nsdhealth.go b/internal/lkgateway/nsdhealth.go index 4889a6f..b7d446e 100644 --- a/internal/lkgateway/nsdhealth.go +++ b/internal/lkgateway/nsdhealth.go @@ -39,6 +39,44 @@ const ( var nsdHistoryFile = "/var/lib/bj/.bj/nsd-health.json" +// nsdProbeFile — время последней проверки M2M и ответа на неё: переживает перезапуск bj. Без него каждый +// перезапуск (установка обновления) слал запрос справочника через минуту после старта; 22.10.2026 на 9 +// набралось больше 100 запросов за день, и с 14:01 НРД перестал на них отвечать. +var nsdProbeFile = "/var/lib/bj/.bj/nsd-probe.json" + +type nsdProbeSaved struct { + Sent time.Time `json:"sent"` + Reply time.Time `json:"reply"` +} + +func nsdProbeSave() { + nsdH.Lock() + v := nsdProbeSaved{Sent: nsdH.ProbeSentAt, Reply: nsdH.ProbeReplyAt} + nsdH.Unlock() + if raw, err := json.Marshal(v); err == nil { + _ = os.WriteFile(nsdProbeFile, raw, 0o600) + } +} + +// nsdProbeLoad — восстановить время проверки после перезапуска; возвращает, когда слать следующую. +func nsdProbeLoad(now time.Time) time.Duration { + var v nsdProbeSaved + if raw, err := os.ReadFile(nsdProbeFile); err == nil && json.Unmarshal(raw, &v) == nil { + nsdH.Lock() + if nsdH.ProbeSentAt.IsZero() { + nsdH.ProbeSentAt = v.Sent + } + if v.Reply.After(nsdH.ProbeReplyAt) { + nsdH.ProbeReplyAt = v.Reply + } + nsdH.Unlock() + if left := nsdProbeEvery - now.Sub(v.Sent); left > time.Minute { + return left + } + } + return time.Minute +} + // Состояния участков по возрастанию тяжести. const ( segOK = "ok" @@ -105,11 +143,12 @@ type nsdSample struct { // nsdProbeReply — пришёл справочник: НРД ответил (по нашей проверке или сам). func nsdProbeReply(at time.Time) { nsdH.Lock() - defer nsdH.Unlock() if !nsdH.ProbeSentAt.IsZero() && nsdH.ProbeReplyAt.Before(nsdH.ProbeSentAt) && at.After(nsdH.ProbeSentAt) { nsdH.ProbeLatency = at.Sub(nsdH.ProbeSentAt) } nsdH.ProbeReplyAt = at + nsdH.Unlock() + nsdProbeSave() } // nsdDelivery — итог передачи пакета в НРД (из клиента ИШ). @@ -200,7 +239,12 @@ func nsdHealthNow(rc *RuntimeConfig, now time.Time) NSDHealth { case nsdH.ProbeErr != "": m.State, m.Text, m.Detail = segErr, "запрос проверки не доставлен", nsdH.ProbeErr case pending && now.Sub(nsdH.ProbeSentAt) >= nsdProbeErrAfter: - m.State, m.Text = segErr, "НРД не ответил на проверку "+humanDur(now.Sub(nsdH.ProbeSentAt)) + // запрос доставлен, но ответа нет: НРД отвечает на запрос справочника не всегда (ограничивает их + // число) — это не обрыв связи, поэтому жёлтый; обрыв видно по участкам «Шлюз» и «НРД» + m.State, m.Text = segWarn, "НРД не ответил на проверку "+humanDur(now.Sub(nsdH.ProbeSentAt)) + if !nsdH.ProbeReplyAt.IsZero() { + m.Text += " (прошлый ответ " + agoText(nsdH.ProbeReplyAt, now) + ")" + } case pending && now.Sub(nsdH.ProbeSentAt) >= nsdProbeWarnAfter: m.State, m.Text = segWarn, "ждём ответ НРД на проверку "+humanDur(now.Sub(nsdH.ProbeSentAt)) case pending && nsdH.ProbeReplyAt.IsZero(): @@ -239,6 +283,9 @@ func nsdHealthNow(rc *RuntimeConfig, now time.Time) NSDHealth { // серым работающий обмен. Серый — только когда по известным участкам связь не подтверждена. h.State = segOK for _, s := range h.Segs { + if s.Key == "m2m" && ns.State == segOK && s.State != segErr { + continue // НРД отвечает шлюзу — задержка ответа на справочник не красит шапку (видна в панели) + } if s.State != segUnknown && segRank(s.State) > segRank(h.State) { h.State = s.State } @@ -284,6 +331,7 @@ func (s *Server) sendNSDProbe(ctx context.Context) { nsdH.Lock() nsdH.ProbeSentAt, nsdH.ProbeErr = time.Now(), "" nsdH.Unlock() + nsdProbeSave() cctx, cancel := context.WithTimeout(ctx, 40*time.Second) defer cancel() if _, err := sendHandbookRequest(cctx, s.igwClient, s.igwChannel); err != nil { @@ -298,7 +346,7 @@ func (s *Server) sendNSDProbe(ctx context.Context) { func (s *Server) nsdHealthLoop(ctx context.Context) { healthRegister("nsd-health", 3*time.Minute) nsdHistoryLoad() - probeT := time.NewTimer(time.Minute) + probeT := time.NewTimer(nsdProbeLoad(time.Now())) // после перезапуска — не раньше часа с прошлой проверки defer probeT.Stop() minute := time.NewTicker(time.Minute) defer minute.Stop() diff --git a/internal/lkgateway/nsdhealth_test.go b/internal/lkgateway/nsdhealth_test.go index fa2b97d..fe61fc1 100644 --- a/internal/lkgateway/nsdhealth_test.go +++ b/internal/lkgateway/nsdhealth_test.go @@ -32,6 +32,7 @@ func nsdTestReset(t *testing.T) { sendFails.loaded = false oldHist := nsdHistoryFile nsdHistoryFile = filepath.Join(t.TempDir(), "h.json") + nsdProbeFile = filepath.Join(t.TempDir(), "p.json") t.Cleanup(func() { sendFailFile, sendFails.loaded, nsdHistoryFile = old, false, oldHist igwWatch.mu.Lock() @@ -98,7 +99,8 @@ func TestNSDHealthStates(t *testing.T) { nsdH.ProbeSentAt = now.Add(-12 * time.Minute) nsdH.ProbeReplyAt = now.Add(-25 * time.Minute) nsdH.Unlock() - if h = nsdHealthNow(rc, now); segState(h, "m2m") != segErr { + // молчание на запрос справочника при живом НРД — жёлтый участок, шапку не красит + if h = nsdHealthNow(rc, now); segState(h, "m2m") != segWarn || h.State != segOK { t.Fatalf("НРД молчит на проверку 12 мин: %+v", h) } }