Blog

Meldingen die één keer afgaan, niet één keer per server

Een van onze operationele meldingen — "lage credits" voor de gepubliceerde app van een builder — moet ons hooguit één keer per dag bereiken. Op een middag kwam hij vijf keer binnen. De oorzaak: elke kopie van onze API onthield in zijn eigen geheugen welke meldingen het al had verstuurd, dus twee servers stuurden hem dubbel, en elke deploy wiste dat geheugen en liet ze hem opnieuw versturen. Hetzelfde patroon zorgde ervoor dat een achtergrondtaak zijn werk dubbel deed. Beide coördineren nu via een gedeelde opslag, en beide kiezen de veilige kant als die opslag niet beschikbaar is.

Het symptoom

Mythex stuurt meldingen naar het team wanneer iets aandacht nodig heeft: een service ligt eruit, een taak staat stil, of het creditsaldo van een builder raakt op terwijl zijn app gepubliceerd is. Elke melding heeft een deduplicatievenster — een periode waarin dezelfde melding maar één keer wordt verstuurd. Voor de melding over lage credits is dat venster 24 uur.

Vijf kopieën van dezelfde melding op één middag betekende dat het venster niet hield. Erger nog, ze kwamen in paren binnen, ongeveer 11 minuten uit elkaar.

Waarom het gebeurde

Het meldingenpakket van ons hield de deduplicatie in het geheugen bij: een tabel "meldingssleutel → wanneer die voor het laatst is verstuurd", binnen het draaiende proces. Dat werkt wanneer er één proces is dat nooit herstart. Productie heeft geen van beide:

  • Twee API-replica's. Productie draait met twee kopieën van de API. Elk had zijn eigen tabel, dus elk stuurde de melding één keer. Dat zijn de paren.
  • Deploys. Elke deploy vervangt beide replica's door nieuwe processen met lege tabellen. Na een deploy ging de melding opnieuw uit, alsof hij nooit was verstuurd.

De ongeveer 11 minuten tussen de twee meldingen in elk paar kwamen van de taak die deze melding aanmaakt. Die draait op beide replica's en neemt een lock, dus de replica's winnen de lock om beurten, en elk maakte de melding aan wanneer het aan de beurt was.

In dezelfde code zat een tweede, stillere bug. Om te voorkomen dat de tabel eindeloos groeit, ruimde hij zichzelf op zodra er 500 sleutels in stonden — hij verwijderde elk item ouder dan 15 minuten, ongeacht het venster. Onder belasting kon een deduplicatie van 24 uur een kwartier duren.

De oplossing

Eén gedeeld record per melding

Voordat de API een melding verstuurt, claimt hij nu de deduplicatiesleutel van die melding in Redis — een gedeelde in-memory opslag waar beide replica's mee praten en die niet opnieuw start wanneer de API dat wel doet. De claim is één bewerking: "zet deze sleutel alleen als hij nog niet bestaat, en laat hem verlopen na het venster". Welke replica de sleutel het eerst claimt, verstuurt de melding; de andere vindt de sleutel bezet en blijft stil. Een nieuw proces na een deploy vindt de sleutel nog steeds staan.

De sleutel en het venster komen uit dezelfde regel die het meldingenpakket gebruikt, die nu door beide wordt gedeeld, zodat de twee lagen het niet oneens kunnen zijn over wat "dezelfde melding" is.

Bij falen de veilige kant kiezen

De belangrijkste beslissing was wat we doen als Redis niet bereikbaar is. We kozen ervoor de melding toch te versturen. Als Redis uitligt of niet binnen 1,5 seconde antwoordt, gaat de melding eruit. Een dubbele melding is ruis. Een verloren melding betekent dat iemand niet wordt verwittigd.

De zwaarste meldingen, P0, worden helemaal nooit door deduplicatie tegengehouden.

Elk item behoudt zijn eigen venster

In de eigen in-memory deduplicatie van het meldingenpakket slaat elk item nu op wanneer zijn venster eindigt, en het opruimen verwijdert alleen items waarvan het venster voorbij is. Een sleutel van 24 uur blijft 24 uur, hoeveel andere meldingen er ook binnenkomen.

Dezelfde bug, ergens anders

Hetzelfde patroon had ook een stuk achtergrondwerk verdubbeld: een reconciliatietaak die onze administratie van de status van elke werkruimte gelijk houdt met die van onze sandboxprovider. Elke ronde somt elke sandbox in het account op — duizenden in productie, ongeveer vijf seconden — plus elk draaiend of gepauzeerd project. Beide replica's draaiden hem elke twee minuten.

De schrijfacties zijn idempotent — twee keer uitvoeren geeft hetzelfde resultaat als één keer — dus er brak niets. Het was alleen twee keer zoveel werk. Nu wordt elk venster van twee minuten in Redis geclaimd, op dezelfde manier als onze gebruiksmeters dat al deden, en draait alleen de replica die het venster claimt de ronde. Als Redis niet bereikbaar is, draait de ronde toch: het werk twee keer doen is onschadelijk, het niet doen niet.

Hoe we het hebben getest

De tests bootsen de echte opzet na. Ze laden de meldingenmodule twee keer, als twee aparte "replica's", wijzen beide naar één gedeelde opslag, verstuurren vanuit elk dezelfde melding, en laden daarna een derde, vers exemplaar om "na een deploy" na te bootsen en de melding opnieuw aan te maken. Precies één melding wordt verstuurd. Andere tests controleren dat de melding van een andere builder nog steeds uitgaat, dat een P0 vanuit beide replica's wordt verstuurd, en dat de melding nog steeds wordt verstuurd wanneer de opslag uitligt of langer dan de time-out hangt. De opschoonfix heeft een eigen test: een sleutel van 24 uur overleeft 600 andere meldingen die 20 minuten later binnenkomen.

Voor de reconciliatietaak levert twee replica's die tegelijk tikken, plus een derde tik in hetzelfde venster, precies één lijst op.

Wat we ervan geleerd hebben

  • Toestand in het geheugen hoort bij één proces. Alles wat tussen servers waar moet zijn — "één keer verstuurd", "één keer uitgevoerd" — moet op een plek staan die ze delen.
  • Deploys zijn herstarts. Toestand die alleen overleeft tot de volgende herstart, overleeft geen gewone werkdag.
  • Beslis welke kant je op faalt. Voor meldingen betekent een uitgevallen gedeelde opslag meer meldingen, niet minder.
  • Zoek naar het patroon, niet alleen naar het geval. De melding was het luide geval; de dubbel draaiende reconciliatietaak was dezelfde bug, die stilletjes werk kostte.

Van binnen Mythex is niets hiervan te zien. Voor meer over hoe we het platform stabiel houden, zie hoe we deploys naar nul 502-fouten brachten en het ontbrekende swapbestand. Als je nieuw bent met het idee om meer dan één kopie van een service te draaien, dan behandelt onze gids over wat een server is de basis.

Mythex · Pricing · Bouw alles met AI · Website- en app-templates · Mythex vergeleken · Woordenlijst voor apps bouwen met AI · Gratis tools voor apps bouwen met AI · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Ελληνικά Español Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)