Bij Slash verwerken we jaarlijks miljarden dollars aan uitgaven. We zijn de afgelopen 12 maanden met meer dan 1000% gegroeid en lossen voortdurend nieuwe problemen op steeds grotere schaal op. Naarmate de hoeveelheid data toenam en ons systeem complexer werd, bleken onze aannames over datarelaties vaak niet meer te kloppen. We kwamen problemen tegen die verschillende oorzaken hadden: slechte backfills, bugs in de productiecode en kleine fouten tijdens datamigraties. Na verloop van tijd werd het steeds moeilijker om de juistheid van onze data te controleren, wat ons ertoe bracht om wat we nu Health Checks noemen te ontwikkelen: een systeem dat is ontworpen om de juistheid van data in de productie continu te controleren.
Gezondheidscontroles zijn een hulpmiddel om invarianten in onze codebase te verifiëren. Wanneer we systemen bouwen, ontwerpen we deze altijd met een reeks impliciete en expliciete invarianten. Dit zijn regels/aannames waarvan we verwachten dat ze altijd gelden, zodat we daarop complexere systemen kunnen bouwen.
Een triviaal voorbeeld van een invariant: in deKaarttabel in onze database, waarin elke rij een creditcard vertegenwoordigt, is er eenstatuskolom waarin een van de waarden 'gesloten' kan zijn en eengeslotenRedenkolom die NULLABLE is. Een invariabele is dat destatuskolom is "gesloten" **als en alleen als** degeslotenRedenveld is NIET NULL.
Het bovenstaande voorbeeld is niet per se een voorwaarde waarvoor we een gezondheidscontrole zouden schrijven, maar het is een voorbeeld van een invariant waarvan we ervoor zorgen dat deze geldt wanneer we met dit deel van de codebase werken.
Ons oorspronkelijke ontwerp voor de gezondheidscontrole
Onze eerste versie van gezondheidscontroles zag er ongeveer zo uit:
We hebben gezondheidscontroles in eerste instantie ontworpen om te worden gedefinieerd door SQL-query's. We voerden een SQL-query uit op een tabel om eventuele "potentiële fouten" op te sporen. Maar omdat het uitvoeren van SQL-query's op grote tabellen duur is, voerden we deze controles uit op een uiteindelijk consistente leesreplica (in ons geval was dit Snowflake). Voor elk resultaat dat door de query werd geretourneerd, voerden we vervolgens een controle uit op onze hoofdproductiedatabase om er zeker van te zijn dat het geen vals-positief resultaat was. Dit ontwerp had een aantal problemen:
- We moesten twee query's onderhouden. De eerste query zou worden uitgevoerd op de leesreplica voor de hele tabel. Beheerders zouden ook expliciet moeten nadenken over en omgaan met paginering voor de query. De tweede query zou worden uitgevoerd op de productie voor elk afzonderlijk resultaat dat de eerste query zou retourneren.
- Queries op de hele tabel zouden steeds complexer en onleesbaarder worden en moeilijk te onderhouden, omdat we een heleboel bedrijfslogica in één enkele SQL-query zouden proberen te coderen.
Tweede iteratie van gezondheidscontroles
We hebben besloten:
- Gezondheidscontroles moeten altijd worden uitgevoerd op basis van onze primaire productiedatabase. Anders lopen we het risico dat we niet alle problemen opmerken die zich daadwerkelijk voordoen.
- Het uitvoeren van "volledige tabelscans" in een SQL-query is een groot taboe, maar het uitvoeren van een gecontroleerde "volledige tabelscan" door elke rij te doorlopen en een gezondheidscontrole uit te voeren, is eigenlijk prima — zolang de belasting constant en voorspelbaar is en geen pieken vertoont, gaat het meestal goed.
- Het uitvoeren van een iteratie in de tijd op tabellen in productie (en dat abstraheren voor de ontwikkelaar) vereenvoudigt onze DX enorm:
- We hoeven niet langer twee complexe SQL-query's met paginering te onderhouden. We hoeven alleen nog maar de applicatielogica te definiëren voor het controleren van de status van een enkel item.
- Elke gezondheidscontrole wordt gedefinieerd voor een volledige tabel (in de toekomst kunnen we ervoor kiezen om gezondheidscontroles uit te breiden, zodat deze specifiek kunnen worden herhaald voor een indexdefinitie).
Een belangrijke les die we als team in de loop der tijd hebben geleerd, is dat voorspelbare systemen die een constante belasting uitoefenen ideaal zijn. De grootste boosdoener voor productieproblemen zijn doorgaans plotselinge, onvoorspelbare veranderingen, zoals een grote piek in de DB-belasting of een plotselinge verandering in een interne queryplanner van een database.
Het contra-intuïtieve aspect van het constant belasten van systemen, vooral met betrekking tot databases en wachtrijen, is dat het verspillend kan lijken. Ik was altijd van mening dat het beter was om systemen standaard niet te belasten en alleen een paar keer per dag te werken wanneer dat nodig was. Dit kan echter leiden tot pieken in de werklast, waardoor ons systeem soms onverwacht slechter gaat presteren. In werkelijkheid hebben we gemerkt dat het meestal beter is om gedurende een lange periode een kleine constante belasting op de database te hebben, zelfs als die constante belasting 24/7 ongeveer 1-5% van het totale CPU-gebruik uitmaakt. Wanneer de belasting voorspelbaar is, is het gemakkelijk om het gebruik over de hele linie te monitoren. Deze voorspelbaarheid stelt ons in staat om met vertrouwen horizontaal te schalen en vooruit te plannen voor mogelijke toekomstige prestatieproblemen.
Het team van AWS heeft hier een interessant artikel over geschreven: Betrouwbaarheid, constant werk en een goede kop koffie
De tweede versie van de gezondheidscontroles ziet er nu als volgt uit:
Met dit ontwerp wordt het heel eenvoudig om nieuwe gezondheidscontroles toe te voegen aan een enkele entiteit, aangezien elke gezondheidscontrole wordt gedefinieerd als een asynchrone enkele functie. We zouden deze gezondheidscontroles meerdere keren per dag over hele tabellen kunnen laten lopen zonder dat we een afweging hoeven te maken tussen frequentie, aangezien de belasting constant is. Voor de bovenstaande gezondheidscontrole voert elke entiteit een snelle zoekopdracht uit in de tabel `LimitRule`. Deze controles worden continu uitgevoerd en leggen op elk moment een minimale maar constante belasting op de database.
Hoe dit systeem onder de motorkap werkt, kan grofweg worden vereenvoudigd tot het volgende ontwerp:
- We voeren onze controles uit bovenop Temporal, zodat we de uiteindelijke uitvoering kunnen garanderen. We gebruiken het product 'schedules' van Temporal om ervoor te zorgen dat onze cron elke minuut wordt uitgevoerd.
- Elke minuut bekijken we alle controles die NU moeten worden gepland en voeren we die controles uit in een takenwachtrij. We beperken onze takenwachtrij zodanig dat er nooit meer dan een bepaald aantal RPS naar onze database wordt verzonden.
- Elke activiteit die wordt uitgevoerd, voert een soort 'constant' werk uit. Geen O(N) of andere tijdcomplexiteit die toeneemt in verhouding tot het totale aantal entiteiten in de database. Dit zorgt ervoor dat activiteiten snel worden uitgevoerd, gemakkelijk te voorspellen zijn en de takenwachtrij zelf niet blokkeren.
Wat nu?
Gezondheidscontroles vormen tegenwoordig een belangrijke basis in onze testpijplijn. Ze vormen de basis voor onze doorlopende productieverificatietests. Naarmate we zijn gegroeid, zijn er steeds meer algemene tests nodig geweest om ons product stabiel en functioneel te houden. Het heeft ons verschillende pogingen gekost om dit goed te krijgen. We wilden al drie jaar een vorm van ledger-testen/gegevensverificatie in de productie implementeren. Pas anderhalf jaar geleden hebben we eindelijk iets ontwikkeld. Over het algemeen hebben we een aantal belangrijke lessen geleerd:
- Verzend iets, wat dan ook, en herhaal dit in de loop van de tijd. We hadden de tweede iteratie van onze gezondheidscontroles niet kunnen bereiken zonder de eerste te hebben verzonden.
- Houd het simpel — we willen het voor engineers zo gemakkelijk mogelijk maken om gezondheidscontroles te bouwen en te onderhouden. Complexe bedrijfslogica die in meerdere SQL-query's is ingebed, is nooit leuk om mee te werken.
- Voer onze controles rechtstreeks uit op de productie — dat is onze bron van waarheid, en als we dingen tegen iets anders afzetten, wordt het inherent complexer en gevoeliger voor valse positieven/negatieven.
Vandaag de dag zijn gezondheidscontroles de eerste stap die we hebben gezet om onze systemen beter te testen en te verifiëren buiten de meer traditionele vormen zoals testen en observatie. Naarmate we blijven groeien, zullen we steeds nieuwe manieren bedenken om ons product stabiel te houden.