
Η μέρα που η εφαρμογή μας έκανε DDoS στον εαυτό της
Ένα backlog από εκκρεμή uploads, που απελευθερώθηκε όλο μαζί κατά την εκκίνηση, μετέτρεψε κάθε GeekBye client σε μια μικρή επίθεση denial-of-service ενάντια στους ίδιους μας τους servers. Η διόρθωση — και η σκάλα connection-liveness που μας ανάγκασε να χτίσουμε — είναι ένα από τα πιο χρήσιμα πράγματα που μας δίδαξε το v2.
Κάθε μηχανικός κατανεμημένων συστημάτων κάποια στιγμή συναντά το thundering herd (το κοπάδι που ορμά): μια μάζα από clients που όλοι κάνουν το ίδιο πράγμα την ίδια στιγμή, και ο κοινός πόρος πίσω τους λυγίζει. Συνήθως είναι οι clients κάποιου άλλου. Στο GeekBye v2.0.1 ήταν οι δικοί μας, και ο επιτιθέμενος ήταν η ίδια μας η εφαρμογή.
Πώς ένας notetaker επιτίθεται στον εαυτό του
Το GeekBye μπορεί να ανεβάζει ηχογραφήσεις στο Google Drive σου. Αν μια ηχογράφηση δεν μπορεί να ανέβει αμέσως — ήσουν offline, η εφαρμογή έκλεισε, το Drive κόμπιασε — μπαίνει σε ένα backlog για να ξαναδοκιμάσει αργότερα. Λογικό.
Η αστοχία ήταν στο πότε συνέβαινε αυτό το «αργότερα». Στην επόμενη εκκίνηση, η εφαρμογή προσπαθούσε να αδειάσει ολόκληρο το backlog όλο μαζί. Ένας χρήστης με μια βδομάδα εκκρεμείς ηχογραφήσεις γινόταν μια ριπή από ταυτόχρονα upload requests τη στιγμή που άνοιγε την εφαρμογή. Πολλαπλασίασε επί κάθε χρήστη που ανοίγει την εφαρμογή το πρωί, και ο backend μας καλούνταν να κάνει authenticate, rate-check και να επεξεργαστεί έναν τοίχο κίνησης μέσα στα ίδια λίγα δευτερόλεπτα — από clients που όλοι, τεχνικά, συμπεριφέρονταν σωστά.
Ο backend μας έκανε το σωστό και rate-limit-άρισε τον κατακλυσμό. Και εκεί ξεκίνησε η ζημιά δεύτερης τάξης. Μια απόκριση rate-limit είναι ένα HTTP 429, και το 429 μοιάζει πολύ με άλλες αστοχίες αν δεν προσέχεις:
- Το profile fetch στην εκκίνηση πήρε ένα
429και η εφαρμογή το αντιμετώπισε ως auth failed — κάνοντας για λίγο logout χρήστες από μια απολύτως έγκυρη session. - Η σύνδεση transcription πήρε ένα γενικό
429και έδειξε ένα upgrade prompt τύπου audio-limit-reached — λέγοντας σε πληρωμένους χρήστες ότι χτύπησαν ένα quota που δεν είχαν χτυπήσει.
Έτσι μία ρίζα — ένα αρρύθμιστο backlog — παρήγαγε τρία ορατά συμπτώματα: καταπόνηση server, ψεύτικα logouts και ψεύτικα upsells. Το κλασικό σχήμα ενός self-DoS: το φορτίο είναι κακό, αλλά η παρερμηνεία των συμπτωμάτων του φορτίου είναι αυτό που πραγματικά νιώθουν οι χρήστες.
Η διόρθωση, σε δύο στρώματα
Σταμάτα την ποδοπατημή. Το upload backlog τώρα ρυθμίζεται — τα requests απλώνονται στον χρόνο με backoff, συν ένα cooldown μετά από κάθε 429, ώστε ο client να απομακρύνεται από έναν καταπονημένο server αντί να πιέζει πάνω του πιο σκληρά. Ένα thundering herd γίνεται μια τακτική ουρά.
Σταμάτα την παρερμηνεία. Ένα παροδικό 429 ή 5xx κατά την εκκίνηση δεν σε κάνει πια logout — ο client ξεχωρίζει το «ο server είναι στιγμιαία απασχολημένος» από το «η session σου είναι άκυρη». Και ένα γενικό 429 rate-limit στη διαδρομή του transcription δεν μεταμφιέζεται πια σε σφάλμα audio quota· μόνο μια γνήσια απόκριση quota δείχνει το upgrade prompt. Το μάθημα που έμεινε: ένας κωδικός σφάλματος δεν είναι μια σημασία σφάλματος. Το 429 σημαίνει «κόψε ταχύτητα», όχι «είσαι μη εξουσιοδοτημένος» και όχι «ξέμεινες από quota» — και ο client πρέπει να ξέρει τη διαφορά.
Η σκάλα liveness που ακολούθησε
Το ρύθμισμα του κοπαδιού ανέδειξε ένα πιο ήσυχο πρόβλημα: όταν μια σύνδεση όντως χαλούσε υπό φορτίο, πόσο γρήγορα το προσέχαμε; Πιο αργά απ' όσο θέλαμε. Έτσι το v2.0.4 έχτισε μια σκάλα connection-liveness για μεταγραφή σε πραγματικό χρόνο:
- Heartbeats — το transcription socket δέχεται ping σε σταθερό διάστημα, ώστε μια σιωπηλά νεκρή σύνδεση να εντοπίζεται σε δευτερόλεπτα αντί να περιμένει το επόμενο κομμάτι audio να αποτύχει.
- Ένα reconnect kick όταν επανέρχεται το δίκτυο — όταν το OS αναφέρει ότι το δίκτυο επέστρεψε, η εφαρμογή προληπτικά ξαναστήνει τη σύνδεση αντί να περιμένει να σκοντάψει στην ανακάλυψη.
- Liveness μέσω control ping/pong — ένα round-trip σε επίπεδο εφαρμογής που επιβεβαιώνει όχι απλώς ότι «το socket είναι ανοιχτό» αλλά ότι «η άλλη άκρη όντως απαντά».
- Μια πύλη reconnect που ανήκει στο service — ένα μέρος αποφασίζει αν θα ξανασυνδεθεί, αντί να τρέχουν διάφορα μέρη της εφαρμογής να το κάνουν και να πατά το ένα το άλλο.
Τίποτα από αυτά δεν είναι εντυπωσιακό. Όλα τους είναι ο λόγος που μια v2 session νιώθεις ότι απλώς... μένει συνδεδεμένη.
Συνέβη ξανά αυτή τη βδομάδα — ένα σκαλί πιο κάτω
Να το κομμάτι που το κάνει κάτι παραπάνω από πολεμική ιστορία. Η ίδια κατηγορία αστοχίας ξαναφάνηκε πριν λίγες μέρες, ένα επίπεδο κάτω από εμάς. Ένας μόνο client πυροδότησε 21 εκκινήσεις session μεταγραφής σε λιγότερο από 400 milliseconds — και σκάλωσε στο όριο επιπέδου λογαριασμού του speech provider μας, των 15 concurrent requests. Μια ποδοπατημή ενάντια σε κοινή υποδομή, ακριβώς όπως το upload backlog, απλώς στραμμένη σε ένα dependency αντί για τον δικό μας backend.
Το σχήμα είναι πανομοιότυπο, και η διόρθωση επίσης: ο client που ποδοπατά χρειάζεται ένα single-flight guard ώστε να μην μπορεί να πυροδοτήσει είκοσι εκκινήσεις για μία πρόθεση, και ο κοινός πόρος χρειάζεται ένα cap ανά χρήστη ώστε ένας client να μην μπορεί να καταναλώσει όλο το pool. Έχουμε χτίσει την client-side εκδοχή αυτού και πριν — ο upload pacer είναι αυτό το μοτίβο. Τώρα το εφαρμόζουμε στις εκκινήσεις session. Το thundering herd δεν είναι ένα bug που διορθώνεις μια φορά· είναι ένα σχήμα που μαθαίνεις να αναγνωρίζεις παντού όπου clients συναντούν ένα κοινό όριο.
Τρία πράγματα για να κρατήσεις
- Οι δικοί σου clients είναι ένα load test που δεν προγραμμάτισες. Οτιδήποτε κάνει batch-στην-εκκίνηση, retry-στο-άνοιγμα ή reconnect-στο-ξύπνημα είναι ένα thundering herd που περιμένει αρκετούς χρήστες. Ρύθμισέ του τον ρυθμό πριν τους αποκτήσεις.
- Ξεχώρισε τον κωδικό από τη σημασία. Το
429είναι το πιο παρεξηγημένο status που υπάρχει. Το «κόψε ταχύτητα» δεν είναι «κάνε logout» και δεν είναι «πλήρωσέ μας». Δρομολόγησε κάθε αστοχία σε αυτό που όντως σημαίνει. - Το liveness είναι σκάλα, όχι flag. Το «είναι ζωντανή η σύνδεση;» έχει πολλές ειλικρινείς απαντήσεις σε διαφορετικά στρώματα — socket ανοιχτό, bytes να ρέουν, η άλλη άκρη να απαντά, δίκτυο παρόν. Μια εύρωστη εφαρμογή ελέγχει περισσότερα από ένα.
Αυτή είναι η μη εντυπωσιακή μηχανική κάτω από την ηρεμία του GeekBye v2. Για τα features αξιοπιστίας που ενεργοποίησε, δες γιατί ο AI notetaker σου σταματά σε κακό Wi-Fi και live μεταγραφή όταν το firewall μπλοκάρει τα WebSockets (v2.0.8). Για τις γειτονικές εκδόσεις αυτής της σειράς, γιατί ο AI notetaker σου σταματά την ηχογράφηση στη μέση της σύσκεψης (v2.0.9).