Sohbet uygulaması, en çok durumu gizleyen sistem tasarımı sorusudur. Her kullanıcı belirli bir sunucuya canlı bir bağlantı tutar, her mesaj tam olarak bir kez ve sırasıyla ulaşmalıdır ve aynı mesajın bir kişiye de on bin kişiye de gitmesi gerekebilir. "İstemci, sunucu, veritabanı" gösteren bir diyagram bunların hiçbirini yanıtlamaz.
Bu yazı bir sohbet backend'ini adım adım, her seferinde bir karar ele alarak inceliyor: istemciler nasıl bağlanır, bir mesaj sohbetteki yerini nasıl alır, onu görmesi gereken herkese nasıl ulaşır ve nasıl saklanır. Başlangıç noktası olarak kullanabileceğiniz bir mimari diyagramla bitiyor.
Gereksinimler
- Metin ve medya içeren birebir ve grup sohbetleri.
- Mesajlar sohbetin her üyesine aynı sırayla ulaşır.
- İstemci bir gönderimi yeniden denediğinde kopya oluşmaz.
- Çevrimiçi kullanıcılar mesajları bir saniye içinde alır; çevrimdışı kullanıcılar bir push bildirimi alır ve yeniden bağlandıklarında eksiklerini tamamlar.
- İletildi ve okundu bilgileri ile bir çevrimiçi göstergesi.
Bağlantılar: neden WebSocket, neden ağ geçitleri
Yeni mesajları yoklamak istekleri boşa harcar ve gecikme ekler. Bir WebSocket bağlantısı açık kalır, böylece sunucu bir mesajı oluştuğu anda iletebilir. Her cihaz bir sohbet ağ geçidine tek bir bağlantı tutar.
Ağ geçitleri sistemin durum tutan kısmıdır. Bir ağ geçidi yalnızca kendisine hangi kullanıcıların bağlı olduğunu bilir. Onları ince tutun: bağlantıyı bir kez doğrulayın, mesajları alıp durumsuz bir servise verin ve giden mesajları doğru sokete itin. Yeni bir ağ geçidi sürümü dağıttığınızda bağlantılar kopar ve istemciler yeniden bağlanır; bu yüzden istemciler backoff ve jitter ile yeniden bağlanmalı ve gördükleri son mesajdan devam etmelidir.
Önüne bir katman 4 yük dengeleyici ya da WebSocket yükseltmelerini anlayan bir dengeleyici koyun ve uzun ömürlü bağlantıların yükü dengesizleştireceğini bekleyin. Yeni bağlantılar en az yüklü ağ geçidini tercih etmelidir.
Mesaj göndermek: idempotency ve sıralama
İstemci göndermeden önce bir client_msg_id (bir UUID) üretir. Onay gelmeden bağlantı koparsa istemci aynı ID ile yeniden gönderir ve mesaj servisi kopyayı atar. Bu olmadan, kararsız mobil ağlar çift mesaj üretir.
Sıralama global değil, sohbet başınadır. Mesaj servisi her mesaja kendi sohbetindeki bir sonraki sıra numarasını verir ve mesajı saklar. İstemciler kendi saatlerine göre değil, her zaman bu numaraya göre sıralar. Numarayı atamanın iki pratik yolu var:
- Mesajı saklayan aynı yazmada artırılan, sohbet başına bir sayaç.
- "Sıra"nın "sunucunun gördüğü sıra" anlamına gelmesini kabul edebiliyorsanız, sunucunun ürettiği Snowflake tarzı bir ID gibi zamana göre sıralanabilir bir ID.
Sıra numarası eksikleri tamamlamayı da ucuzlatır. Yeniden bağlanan bir istemci "42 numaralı sohbette 1871'e kadar bende var" der ve sonrasındaki her şeyi alır.
Fan-out: çevrimiçi, çevrimdışı ve büyük gruplar
Mesaj saklandıktan sonra servis bir olay yayınlar. Gerisini fan-out işçileri devralır:
- Sohbetin üyelerini bulur.
- Redis gibi bir varlık ve oturum kaydında hangi üyelerin çevrimiçi olduğuna ve bağlantılarını hangi ağ geçidinin tuttuğuna bakar.
- Mesajı bu ağ geçitlerine gönderir; ağ geçitleri de onu soketlere iter.
- Çevrimdışı üyelere APNs ve FCM üzerinden push bildirimi gönderir.
Mesajı bir kez yazıp okumaları yaymak, depolamayı üye sayısına değil mesaj sayısına doğrusal tutar. Büyük gruplar dikkat ister: 100.000 üyeli bir kanal mesaj başına 100.000 push bildirimi tetiklememelidir. Yalnızca bunu seçen üyeleri bilgilendirin ya da toplayıp özetleyin.
Varlık
Varlık, TTL'li bir kalp atışıdır. Ağ geçidi yaklaşık 30 saniyede bir "kullanıcı 7, ağ geçidi 3'e bağlı" bilgisini yeniler; anahtarın süresi dolunca kullanıcı çevrimdışıdır. Her varlık değişikliğini her kişiye gerçek zamanlı yayınlamayın. Kullanıcıların gecikmesini ne kadar az fark ettiğine bakılırsa, sohbetin en pahalı özelliklerinden biridir. Varlığı bir sohbet açıldığında getirin ve periyodik olarak yenileyin. Süresi dolan anahtarlar önbellek girdileriyle aynı şekilde çalışır; önbellek stratejileri yazısı TTL'leri ve hepsinin süresi aynı anda dolduğunda ne olduğunu anlatır.
İletim bilgileri ve uçtan uca şifreleme
İletildi ve okundu bilgileri de mesajdır. Bir cihaz 1871 numaralı mesajı aldığında küçük bir "1871'e kadar iletildi" olayı gönderir; kullanıcı sohbeti açtığında "1871'e kadar okundu" gönderir. Mesaj başına bir bilgi yerine üye ve sohbet başına en yüksek numarayı saklayın; böylece kalabalık gruplarda maliyet sabit kalır.
Uçtan uca şifreleme, sunucunun görebildiklerini değiştirir. Signal'inki gibi bir protokolle sunucu şifreli metni saklar ve iletir; bu yüzden arama, moderasyon ve bağlantı önizlemeleri istemcilere taşınır. Erken karar verin, çünkü hem depolamayı hem fan-out'u değiştirir.
Depolama
Mesajlar bir kez yazılır, çoğunlukla yakın zamanlı olanlar okunur ve neredeyse her zaman sohbete göre okunur. Bu, sohbete göre bölümlenmiş ve sıra numarasına göre sıralanmış bir depoyu işaret eder. Örneğin Discord, yoğun bir kanalın devasa tek bir bölüme dönüşmemesi için mesajları kanal artı sabit bir zaman kovasına göre bölümler; trilyonlarca mesajı nasıl sakladığını anlattığı yazı bu modelin Cassandra'dan ScyllaDB'ye taşınmasını anlatır.
Medyayı mesaj deposunun dışında tutun. İstemciler görselleri ve videoları önceden imzalı bir URL ile doğrudan nesne depolamaya yükler, mesaj da bir referans taşır. Mesaj servisi baytlarla hiç uğraşmaz.
Mimari diyagram
Diyagram içeriği
- Users: mobile and web
- Load balancer
- Chat gateway
- Message service
- Message store
- Message events
- Fan-out workers
- Presence and sessions
- APNs and FCM
- Media bucket
- wss
- send
- append, next seq
- event
- who is online, which gateway
- deliver
- offline
- heartbeat
- pre-signed upload
Nasıl okunur:
- Sol: kullanıcılar yük dengeleyici üzerinden WebSocket (wss) ile bir sohbet ağ geçidine bağlanır. Medya yüklemeleri doğrudan kovaya gider.
- Orta: ağ geçidi her gönderimi mesaj servisine verir; servis kopyaları
client_msg_idile atar, bir sonraki sıra numarasını verir ve mesajı depoya ekler. - Sağ: servis bir olay yayınlar. Fan-out işçileri varlığı ve oturumları kontrol eder, doğru ağ geçidi üzerinden iletir ve çevrimdışı üyelere push bildirimi gönderir.
- Ağ geçidinden varlığa giden kalp atışı noktalıdır, çünkü mesaj başına değil bir zamanlayıcıyla çalışır.
- Markdown bloğu mesaj zarfını gösterir; böylece herkes alanlarda anlaşır.
Neler etiketlenmeli
- Hangi bileşenlerin durum tuttuğu. Ağ geçitleri bağlantıları tutar; mesaj servisi hiçbir şey tutmaz. Her birini nasıl dağıtıp ölçekleyeceğinizi bu belirler.
- Sıralama kuralı. "Sohbet başına sıra, sunucu tarafından atanır" diyagramda yer almalı.
- Kopya ayıklama anahtarı. Gönderim yolunda
client_msg_idadını yazın. - Çevrimiçi ve çevrimdışı yollar. İkisini de çizin; hataların çoğu ikincisinde yaşar.
Olay güdümlü mimari yazısı olay ve tüketici tarafını daha ayrıntılı ele alır; sekans diyagramı örneği de gönderim ve onay alışverişini adım adım belgelemek için iyi bir yoldur.
Archboard'da oluşturun
- Google ya da GitHub ile giriş yapın ve bir diyagram oluşturun.
- Kullanıcılar için C4 Modeli paketinden bir Kişi ve Bulut paketinden bir Bulut yük dengeleyici ekleyin.
- C4 Modeli paketinden üç Konteyner düğümü ekleyin: sohbet ağ geçidi, mesaj servisi ve fan-out işçileri. Her birine durum tutup tutmadığını not edin.
- Mesaj deposu için C4 Modeli paketinden bir Veritabanı, varlık ve olaylar için Veri paketinden Redis ve Kafka, medya için Bulut paketinden bir Nesne depolama düğümü ve APNs ile FCM için bir Harici sistem ekleyin.
- Bunları bağlayın, kalp atışını ve medya yüklemesini kesikli ya da noktalı kenarlarla çizin ve mesaj zarfını içeren, Temel paketinden bir Markdown bloğu ekleyin.
Örnek 11 diyagram düğümü kullanıyor ve Free planın sınırları içinde kalıyor.
SSS
Bir sohbet uygulaması için WebSocket gerekli mi? Gerçek zamanlı iletim için evet; ya da gönderimler için HTTP ile birlikte server-sent events gibi benzer bir push kanalı. Long polling küçük uygulamalarda çalışır ama daha fazla istek harcar ve gecikme ekler.
Mesajları sırada nasıl tutarım? Sunucunun mesajı saklarken sohbet başına bir sıra numarası atamasına izin verin ve istemcilerin buna göre sıralamasını sağlayın. İstemci saatleri birbirini tutmaz ve sıralama için güvenilemez.
Kopya mesajlardan nasıl kaçınırım? İstemci göndermeden önce bir mesaj ID'si üretir ve yeniden denemelerde aynısını kullanır. Sunucu ID'yi saklar ve aynı ID'ye sahip ikinci mesajı yok sayar.
Sohbet mesajlarını hangi veritabanı saklamalı? Sohbete göre hızlı okuyan ve sıraya göre sıralayan bir veritabanı; örneğin sohbete ve zaman kovasına göre bölümlenmiş bir geniş sütunlu depo. Daha küçük uygulamalar için sohbet ve sıra üzerinde indeksli Postgres iyi çalışır.
Mesajlar çevrimdışı kullanıcılara nasıl iletilir? Diğer mesajlar gibi saklanırlar. Fan-out adımı bir push bildirimi gönderir; uygulama yeniden bağlandığında elindeki son sıra numarasından sonraki her şeyi ister.