Μετάβαση στο περιεχόμενο

Brokers και μισθώσεις

Τα backends RabbitMQ και NATS δίνουν μία εγγύηση: ένα instance κερδίζει κάθε εκτέλεση. Η σελίδα αυτή εξηγεί γιατί σταματούν εκεί, και ποιες δυνατότητες κάθε broker θα μπορούσαν να προσθέσουν εφεδρικούς runners, προστασία επικάλυψης (overlap) και ανάκτηση (catch-up).

Οι εφεδρικοί 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 ως τέτοιους.

Το 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.