Brokers και μισθώσεις
Τα backends RabbitMQ και NATS δίνουν μία εγγύηση: ένα instance κερδίζει κάθε εκτέλεση. Η σελίδα αυτή εξηγεί γιατί σταματούν εκεί, και ποιες δυνατότητες κάθε broker θα μπορούσαν να προσθέσουν εφεδρικούς runners, προστασία επικάλυψης (overlap) και ανάκτηση (catch-up).
Γιατί λειτουργούν μόνο για claim σήμερα
Ενότητα με τίτλο «Γιατί λειτουργούν μόνο για claim σήμερα»Οι εφεδρικοί runners, η προστασία επικάλυψης και η ανάκτηση εξαρτώνται όλα από μια μίσθωση (lease): μια εγγραφή ανά εκτέλεση που λέει ποιος την εκτελεί και μέχρι πότε. Τα άλλα instances πρέπει να μπορούν να διαβάσουν αυτή την εγγραφή και να την αντικαταστήσουν σε ένα ατομικό βήμα.
| Backend | Πώς εκτελεί το «ανάγνωση της εκτέλεσης, απόφαση, εγγραφή» ως ένα βήμα |
|---|---|
Redis lease |
Ένα Lua script |
| PostgreSQL | Ένα transaction στη γραμμή της εκτέλεσης |
| SQLite | Ένα transaction BEGIN IMMEDIATE |
Τα backends των brokers χρησιμοποιούν έναν μηχανισμό που απαντά σε ένα μόνο ερώτημα: «ήμουν πρώτος;»
| Backend | Ο μηχανισμός | Τι αφήνει πίσω του |
|---|---|---|
| RabbitMQ | Μια ουρά που κρατά ένα μήνυμα και απορρίπτει τα υπόλοιπα, με publisher confirms | Ένα μήνυμα που κανείς δεν καταναλώνει |
| NATS | Το de-duplication του JetStream βάσει Nats-Msg-Id |
Ένα message ID που απομνημονεύεται για το duplicate window |
Και οι δύο επιλέγουν ακριβώς έναν νικητή. Κανένα από τα δύο αποτελέσματα δεν μπορεί να ανανεωθεί, να λήξει νωρίτερα ή να αναληφθεί από άλλον, οπότε τίποτα δεν καταγράφει ότι ένας runner σταμάτησε:
- Χωρίς εφεδρικούς runners. Αν ο runner καταρρεύσει, κανένα instance δεν μπορεί να το αντιληφθεί.
- Χωρίς προστασία επικάλυψης. Δεν υπάρχει ενεργή μίσθωση που να δείχνει ότι η προηγούμενη εκτέλεση βρίσκεται ακόμη σε εξέλιξη.
- Χωρίς ανάκτηση. Ένα claim λέει ότι μια εκτέλεση διεκδικήθηκε, όχι ότι ολοκληρώθηκε.
Τα ίδια όρια ισχύουν για τις στρατηγικές stream-id, idmp και script του Redis. Προέρχονται από τον μηχανισμό του claim, όχι από τους brokers ως τέτοιους.
Τι θα μπορούσε να καλύψει το κενό
Ενότητα με τίτλο «Τι θα μπορούσε να καλύψει το κενό»RabbitMQ
Ενότητα με τίτλο «RabbitMQ»Το dead-lettering από μόνο του δεν εντοπίζει έναν runner που κατέρρευσε. Η δυνατότητα που το κάνει είναι η χειροκίνητη επιβεβαίωση (manual ack) με επαναπαράδοση (redelivery): ένας consumer κρατά το μήνυμα της εκτέλεσης ανεπιβεβαίωτο όσο εκτελείται η εργασία, και αν η σύνδεσή του διακοπεί, ο broker δίνει το μήνυμα σε άλλον consumer.
| Τι λείπει | Δυνατότητα του RabbitMQ | Περιορισμός |
|---|---|---|
| Εφεδρικοί runners | Ένα ανεπιβεβαίωτο μήνυμα παραδίδεται ξανά όταν διακοπεί η σύνδεση του runner | Ο εντοπισμός ακολουθεί το heartbeat του AMQP, όχι το leaseMs |
maxAttempts |
x-delivery-limit σε quorum queue, και μετά dead-letter ως «failed» |
Απαιτεί quorum queues |
| Παρωχημένες εκτελέσεις | TTL ανά μήνυμα· τα ληγμένα μηνύματα γίνονται dead-letter ως «skipped» | Κανένας σοβαρός: αποφασίζει το ρολόι του broker |
Overlap skip |
Single active consumer με prefetch 1 σε μία ουρά ανά εργασία | Η επόμενη εκτέλεση περιμένει αντί να παραλειφθεί, και ένα instance εκτελεί όλες τις εκτελέσεις της εργασίας |
| Ανάκτηση | Ένα durable μήνυμα περιμένει το πρώτο instance που θα επανέλθει | Μόνο αν κάποιο instance ήταν ενεργό για να δημοσιεύσει την εκτέλεση |
Τρία προβλήματα παραμένουν:
- Ο broker θα μοίραζε τις εκτελέσεις. Σήμερα κάθε instance αποφασίζει με το δικό του χρονόμετρο πότε θα κάνει claim. Με consumers, το RabbitMQ προωθεί μια εκτέλεση σε ένα instance, κάτι που είναι διαφορετικό μοντέλο τόσο από αυτό της μίσθωσης όσο και από αυτό του claim.
- Η ουρά-πύλη (gate queue) πρέπει να παραμείνει. Κάτι πρέπει να εξακολουθεί να εμποδίζει ένα καθυστερημένο instance από το να δημοσιεύσει την ίδια εκτέλεση δύο φορές. Μια κατάρρευση ανάμεσα στη νίκη στην πύλη και στη δημοσίευση του μηνύματος της εκτέλεσης χάνει την εκτέλεση.
- Οι μακροχρόνιες εργασίες προσκρούουν στο timeout της επιβεβαίωσης. Το RabbitMQ κλείνει ένα channel που κρατά μια παράδοση ανεπιβεβαίωτη για πολύ ώρα, 30 λεπτά από προεπιλογή.
Επομένως το RabbitMQ θα μπορούσε να αποκτήσει εφεδρικούς runners και επαναλήψεις, αλλά το overlap skip μόνο κατά προσέγγιση.
Το NATS δεν χρειάζεται τη λύση με τις ουρές. Το key-value store του JetStream διαθέτει το create, που πετυχαίνει μόνο αν το κλειδί δεν υπάρχει, και το update με αναμενόμενο revision, που πετυχαίνει μόνο αν κανείς δεν έγραψε ενδιάμεσα. Αυτό είναι compare-and-swap, και αρκεί για μια πραγματική μίσθωση.
| Ενέργεια της μίσθωσης | Ενέργεια του key-value |
|---|---|
| Claim μιας εκτέλεσης | create, ή update πάνω στο revision που διαβάστηκε. Μια σύγκρουση σημαίνει ότι άλλο instance ήταν πρώτο |
| Heartbeat | update πάνω στο revision που έγραψε τελευταία ο runner |
| Ανάληψη (takeover) | update πάνω στο revision της ληγμένης μίσθωσης |
| Απόρριψη ενός παλιού runner | Το revision του είναι ξεπερασμένο, οπότε το heartbeat και το «finished» του αποτυγχάνουν |
Overlap skip |
Ένα κλειδί ανά εργασία κρατά την κατάσταση των εκτελέσεών της, οπότε ο έλεγχος και η εγγραφή είναι ένα βήμα |
Με αυτό, το NATS θα μπορούσε να γίνει backend μίσθωσης με τις ίδιες εγγυήσεις με το Redis, την PostgreSQL και το SQLite.
Ένα σημείο μένει ανοιχτό: η λήξη της μίσθωσης στο ρολόι του store. Ένα compare-and-swap στο NATS δεν μπορεί να διαβάσει την ώρα του server. Η μίσθωση θα στηριζόταν στο TTL του κλειδιού, ή στο server timestamp του τελευταίου revision.
Τι να χρησιμοποιήσετε σήμερα
Ενότητα με τίτλο «Τι να χρησιμοποιήσετε σήμερα»| Χρειάζεστε | Χρησιμοποιήστε |
|---|---|
| Έναν νικητή ανά εκτέλεση, και η περιστασιακή απώλεια μιας εκτέλεσης είναι αποδεκτή | RabbitMQ ή NATS, όπως είναι |
| Εφεδρικούς runners, προστασία επικάλυψης, ανάκτηση ή παράδοση (hand-over) | Redis lease, PostgreSQL ή SQLite |
Σε ένα backend μόνο για claim, γράψτε κάθε εκτέλεση ώστε να επεξεργάζεται ό,τι εκκρεμεί. Μια χαμένη εκτέλεση κοστίζει τότε μια καθυστέρηση, όχι χαμένη δουλειά.
Οι επιλογές που μόνο οι μισθώσεις μπορούν να τηρήσουν δεν είναι διαθέσιμες σε backend μόνο για claim: overlap, maxAttempts και catchUpMs στις εργασίες, leaseMs και heartbeatMs στον scheduler. Κώδικας με τύπους που τις ορίζει δεν μεταγλωττίζεται, και ένας decorator ή JavaScript που τις ορίζει παίρνει σφάλμα όταν δημιουργείται η εργασία ή ο scheduler. Αυτό ισχύει και για Redis και Valkey με στρατηγική άλλη από lease.