Architekt systemówArchitekt systemów

Jak zbudować implementację typu **CRDT** (conflict-free replicated data type) dla globalnie rozproszonej platformy edycji współpracy, która obsługuje tryb offline dla 10 milionów współpracujących użytkowników, zapewniając jednocześnie silną ostateczną spójność bez centralnej koordynacji i zapobiegając problemowi utraty aktualizacji podczas długotrwałych scenariuszy offline?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź na pytanie.

Aby zaprojektować system oparty na CRDT-ach na taką skalę, musisz porzucić tradycyjne modele Operational Transformation (OT), które wymagają centralnego organu do serializacji operacji. Te przestarzałe podejścia zasadniczo uniemożliwiają prawdziwe możliwości trybu offline, ponieważ wymagają stałego połączenia z serwerem koordynacyjnym do rozwiązywania konfliktów. Zamiast tego, wdrażaj CRDT oparte na stanie (specyficznie RGA - Replicated Growable Array do danych sekwencyjnych), które wykorzystują matematyczne właściwości komutatywności, łączności i idempotencji, aby zagwarantować zbieżność bez koordynacji lub protokołów konsensusu.

Wprowadź protokoły Delta-State Anti-Entropy, w których klienci wymieniają jedynie różnice między swoimi lokalnymi stanami, a nie pełne zrzuty stanu. Takie podejście znacznie zmniejsza zużycie pasma podczas synchronizacji w porównaniu do naiwnych replikacji opartych na stanie. Musisz wykorzystać Hybrid Logical Clocks (HLC), łącząc fizyczne znaczniki czasowe z logicznymi licznikami, aby ustalić przyczynowość i obsługiwać przesunięcia zegarów w różnych regionach bez ścisłej zależności od NTP. Na koniec wdrażaj Garbage Collection Tombstone z wykorzystaniem przycinania opartego na epokach, aby zapobiec nieograniczonemu wzrostowi pamięci z powodu znaczników usunięcia, jednocześnie utrzymując śledzenie przyczynowości dla opóźnionych lub podzielonych replik.

Sytuacja z życia

Nasz zespół otrzymał zadanie odbudowy silnika współpracy w czasie rzeczywistym dla narzędzia projektowego podobnego do Figma, wspierającego 50 000 zespołów korporacyjnych w różnych strefach czasowych. System dziedziczony wykorzystywał Redis pub/sub z połączeniami WebSocket przez centralny serwer Node.js, który zawalił się podczas konferencji branżowych, gdy 10 000+ użytkowników próbowało edytować offline podczas lotów, a następnie ponownie połączyło się jednocześnie. Ta ilość wywołała nieodwracalną różnicę stanów i trwałą korupcję dokumentów, co skutkowało 48 godzinami przestoju i znacznym ubytkiem klientów.

Najpierw oceniliśmy Centralized OT with Lease Locks, podejście, w którym użytkownicy muszą uzyskać wyłączne zamki na sekcje dokumentu przed edytowaniem offline. To rozwiązanie obiecało silną spójność i znajome semantyki ACID, podobne do tradycyjnych baz danych. Jednak wymagało stałego połączenia w celu odnawiania zamków, co całkowicie naruszało wymaganie trybu offline i stworzyło katastrofalny punkt awarii w serwerze zamków, co sprawiałoby, że cały produkt byłby bezużyteczny podczas podziałów sieci.

Drugie proponowane rozwiązanie to Last-Write-Wins (LWW) z zegarami wektorowymi, korzystające z znaczników czasu AWS DynamoDB do deterministycznego rozwiązywania konfliktów. Choć to podejście wspierało prawdziwą edycję offline i było łatwe do wdrożenia z istniejącą infrastrukturą chmurową, cierpiało na katastrofalną utratę danych podczas równoczesnych edycji. Gdy dwóch projektantów jednocześnie przesuwało ten sam komponent offline, tylko znacznik czasu ostatniej synchronizacji przetrwał, cichaczem niszcząc istotę współpracy przez całkowite porzucenie pracy jednego użytkownika bez ostrzeżenia.

Ostatecznie wybraliśmy CRDT oparte na stanie z użyciem biblioteki Yjs z niestandardową synchronizacją delta-stanu przesyłaną przez protokół QUIC. Ten wybór architektoniczny wyeliminował potrzebę centralnej koordynacji podczas edycji, umożliwił matematyczne gwarancje zbieżności niezależnie od czasu trwania podziału sieci i wspierał synchronizację P2P między użytkownikami w tej samej sieci LAN bez łączności z Internetem. Zaimplementowaliśmy kodowanie delta z użyciem Merkle-tree, aby zmniejszyć obciążenia synchronizacji o 94% w porównaniu do pełnego transferu stanu, jednocześnie utrzymując integralność kryptograficzną historii dokumentu.

Po sześciu miesiącach ruchu produkcyjnego system skutecznie obsłużył 72-godzinną awarię Cloudflare, która dotknęła cały region, gdzie użytkownicy kontynuowali edytowanie offline i bezproblemowo połączyli się po ponownym połączeniu bez utraty danych. Czas ładowania dokumentów poprawił się z 4,2 sekundy do 180 milisekund dzięki eliminacji obrotów serwera dla rozwiązywania konfliktów. Koszty infrastruktury spadły o 60% dzięki eliminacji nakładów koordynacyjnych i możliwości wykorzystania buforowania w brzegach zamiast mocnych instancji obliczeniowych w centralnych serwerach.

Czego często brakuje kandydatom

Jak CRDT radzą sobie z nieograniczonym wzrostem tombstonów, gdy użytkownicy usuwają treści, i co wyzwala ich bezpieczne usunięcie?

Większość kandydatów zakłada, że usunięcia mogą być natychmiast usuwane z pamięci, ale CRDT wymagają tombstonów do śledzenia przyczynowości i zapobiegania odtwarzaniu usuniętych danych podczas scalania z opóźnionymi replikami. Rozwiązanie implementuje wykrywanie Causal Stability za pomocą porównania zegarów wektorowych; gdy węzeł zauważy, że wszystkie inne repliki potwierdziły usunięcie do określonego znacznika czasu, tombstone staje się stabilny i kwalifikowany do usunięcia. Musisz wdrożyć Garbage Collection opartą na epokach, gdzie tombstony są oznaczane do usunięcia po konfigurowalnym czasie życia i fizycznie usuwane tylko wtedy, gdy cięcie przyczynowe wykaże, że żadna opóźniona replika ich nie potrzebuje do zbieżności. Bez tego mechanizmu pojedyncze urządzenie offline sprzed sześciu miesięcy mogłoby wskrzesić dawno usunięte dane po ponownym połączeniu, naruszając oczekiwania użytkowników co do trwałego usunięcia i zgodności z przepisami dotyczącymi prywatności.

Jaka jest fundamentalna różnica między CRDT opartymi na stanie i operacjami dotyczących wymagań sieciowych i dlaczego wybrałbyś jedno nad drugim w warunkach ograniczonej przepustowości sieci mobilnej?

Op-based CRDT wymagają dostarczenia dokładnego raz i gwarancji przyczynowego nadawania od warstwy transportowej, takiej jak Apache Kafka lub RabbitMQ, co czyni je nieodpowiednimi dla zawodnych sieci mobilnych, gdzie wiadomości mogą być zgubione lub zdublowane bez ostrzeżenia. CRDT oparte na stanie tolerują duplikację wiadomości i dowolne opóźnienia, ale tradycyjnie wymagały przesyłania całego stanu dokumentu, co jest nieprzystępne dla dużych plików projektów w sieciach komórkowych. Zaawansowane rozwiązanie wykorzystuje Delta-state CRDT, które przesyłają tylko mutacje od ostatniej udanej synchronizacji, łącząc odporność sieciową opartą na stanie z efektywnością podejść op-owych. W kontekście mobilnym wdrażasz Exponential Backoff Delta Sync z Bloom Filters, aby unikać ponownego wysyłania już widzianych aktualizacji, redukując użycie danych mobilnych o 99% w porównaniu z synchronizacją całego stanu, zachowując jednocześnie zdolności offline-first.

Jak zapobiegać "anomalii wplecenia" w CRDT sekwencyjnych, gdy dwóch użytkowników jednocześnie wstawia tekst w tej samej pozycji kursora, zapewniając, że ich edycje pojawiają się jako ciągłe bloki, a nie dowolnie wplecione znaki?

Standardowe LWW lub proste CRDT oparte na licznikach powodują problem ''helo'', w którym równoczesne wstawienia "hi" i "bye" w tej samej pozycji stają się niewyraźnym "hbyeio". Rozwiązanie wymaga zastosowania Replicated Growable Array (RGA) lub algorytmów Woot, które przypisują globalnie unikalne identyfikatory (GUID) do każdego znaku w zależności od identyfikatora węzła i znacznika czasu, z deterministycznymi zasadami łamania remisu, które ustalają całkowity porządek. Podczas wstawiania dołączasz nowy element do konkretnego identyfikatora poprzednika, a nie do indeksu numerycznego, tworząc strukturę listy powiązanej, w której jednoczesne wstawienia tworzą niezależne gałęzie, które scalają się deterministycznie bez wplecenia. Musisz też wdrożyć optymalizacje Run-Length Encoding, aby zapobiec dominacji nadmiaru GUID w rozmiarze dokumentu, osiągając zwykle mniej niż 20% nadmiaru metadanych dla dokumentów tekstowych, jednocześnie zachowując intuicyjną semantykę scalania, która zachowuje zamiar równoczesnych edycji.