Sistem Chat

Pengiriman, pengurutan pesan, presence, dan penyimpanan pada sistem chat.

Daftar isi

Sistem chat mengantar pesan antarpengguna dalam hitungan ratusan milidetik dan menampilkannya dengan urutan yang sama pada semua perangkat. Topik ini merancang sistem tersebut dengan kerangka studi kasus yang sama, yaitu kebutuhan dan estimasi, pengiriman melalui gateway koneksi, pengurutan dengan sequence number, presence, penyimpanan, dan mode kegagalan. Satu simulasi Python memperlihatkan penerima menyusun ulang pesan yang tiba tidak berurutan, dan satu demo interaktif mengulang perilaku yang sama secara langsung.

Kebutuhan dan estimasi#

Secara fungsional, sistem mengantar pesan pribadi dan pesan kanal grup, menyimpan seluruh riwayat agar dapat dibaca ulang, dan menampilkan pesan dengan urutan yang sama di setiap perangkat. Klien yang mengirim ulang karena jaringan yang buruk tidak boleh menciptakan pesan dobel, dan pengguna melihat status kehadiran kontaknya. Secara nonfungsional, jeda dari menekan kirim sampai pesan tampil diharapkan di bawah 500 milidetik, sehingga jalur pengantarannya tidak boleh melewati queue yang dalam.

Estimasinya memakai 1.000.000 pengguna serentak yang masing-masing mengirim 2 pesan per menit, sehingga lajunya sekitar 33.000 pesan per detik. Isi pesannya kecil, biasanya beberapa ratus byte, sehingga tekanan desainnya bukan ukuran data, melainkan jumlah kejadian kecil per detik dan jumlah koneksi yang tetap terbuka. Angka 33.000 per detik ini juga menjelaskan mengapa pengiriman pesan tidak ditempatkan pada jalur permintaan-respons biasa yang membuka koneksi baru untuk setiap pesan.

Antarmuka#

Perangkat klien dan layanan chat berkomunikasi melalui tiga jalur, dan hanya jalur pengambilan selisih yang berbentuk permintaan biasa. Pengiriman dan penerimaan pesan berlangsung di atas koneksi dua arah yang tahan lama, sedangkan penyelarasan riwayat setiap perangkat cukup dengan menyebut kanal dan posisi terakhirnya.

text Contoh: antarmuka sistem chat
kirim pesan     : klien mengirim isi pesan beserta idempotency key
                  melalui koneksi dua arahnya
langganan kanal : klien berlangganan kanal, pesan baru dengan nomor
                  urut berikutnya diteruskan otomatis oleh server
ambil selisih   : klien menyebut kanal dan offset terakhirnya,
                  jawabannya pesan mulai offset itu sesuai urutan

Antarmuka ini sengaja tidak menyediakan operasi pengurutan dari sisi klien, karena urutan adalah tanggung jawab server kanal sebagaimana dibahas pada bagian berikutnya. Penyebutan offset pada pengambilan selisih juga membuat seluruh perangkat milik satu pengguna menerima jawaban yang berbeda, yaitu selisih yang sesuai dengan posisi masing-masing, tanpa operasi tambahan. Bentuk seminimal ini menetapkan apa yang tidak perlu ada pada antarmuka, misalnya parameter pengurutan dan penanda telah dibaca, karena keduanya lahir dari sequence number dan offset yang sudah melekat pada pesan.

Desain tingkat tinggi#

Desainnya tersusun atas empat komponen, yaitu gateway koneksi yang memegang sambungan dua arah, layanan pesan yang menentukan urutan, penyimpanan pesan per kanal, dan layanan presence.

Klien AKlien BGatewaykoneksiLayananpesanPenyimpanan pesanper kanal, bernomor urutLayanan presenceheartbeat
Klien tersambung ke gateway koneksi, sedangkan layanan pesan menyimpan pesan per kanal dan bertukar status dengan layanan presence

Gateway koneksi mengelola puluhan ribu sambungan sekaligus tanpa memahami isi pesan, dan pemetaan dari pengguna ke gateway tempatnya tersambung disimpan pada penyimpanan terdistribusi karena pengguna berpindah gateway sewaktu-waktu. Jumlah gateway ditambah secara horizontal ketika jumlah koneksi naik, dan klien yang kehilangan gateway-nya cukup menyambung ulang ke gateway lain karena tidak ada keadaan penting yang melekat pada satu mesin. Layanan presence berdiri terpisah agar lonjakan heartbeat tidak bercampur dengan jalur pesan, sedangkan penyimpanan pesan berdiri sendiri agar riwayat kanal tetap utuh meskipun layanan di depannya diperbarui.

Pengiriman pesan#

Koneksi tahan lama dibangun dengan WebSocket, yaitu protokol yang membuka jalur dua arah di atas satu sambungan TCP, sehingga pesan dapat tiba tanpa menunggu permintaan berikutnya. Alur pesan bergerak dari pengirim ke gateway, lalu ke layanan pesan yang menyimpan pesan ke penyimpanan, dan diteruskan ke gateway penerima untuk dikirim ke perangkatnya. Penerima yang sedang tidak tersambung tidak mengubah alur ini, karena pesan tetap tersimpan dan diserahkan ketika koneksi kembali.

Ketidakpastian jaringan ditangani pada titik pengiriman. Klien yang tidak yakin apakah pengiriman sebelumnya berhasil mengulang pesan yang sama dengan idempotency key yang sama, yaitu pengenal unik yang ditetapkan klien untuk satu pesan, sehingga layanan mengenali ulangannya dan menyimpan pesan itu sekali saja. Tanpa pengenal ini, setiap pengiriman ulang berpotensi menjadi pesan kedua yang tampak sah. Pengenal itu juga membuat pengiriman aman diulang pada tingkat jaringan apa pun, karena keputusan menahan duplikat diambil di satu tempat, yaitu layanan pesan, alih-alih diserahkan kepada klien yang kondisinya tidak dapat dipercaya.

Pengurutan dengan sequence number#

Urutan pesan tidak dapat ditentukan dari waktu klien, karena jam antarperangkat bergeser dan selisihnya tidak terkendali. Penomoran diberikan di sisi kanal, yaitu server kanal menetapkan sequence number berurutan tanpa celah untuk setiap pesan yang masuk, satu pemberi nomor per kanal. Waktu tetap dicatat untuk ditampilkan, tetapi urutan tampil mengikuti nomornya, dan mekanisme ini menyederhanakan persoalan pengurutan peristiwa pada sistem terdistribusi menjadi penomoran tunggal per kanal.

Penerima hanya menyampaikan pesan dengan nomor yang diharapkan berikutnya. Pesan yang mendahului giliran ditampung pada buffer, sedangkan celah yang menetap memicu permintaan pengiriman ulang mulai offset, yaitu posisi pesan terakhir yang sudah diterima. Simulasi berikut mengirim delapan pesan ke satu kanal dengan jaringan yang mengubah urutan tiba dan menghilangkan satu pesan.

python simulasi-pengurutan.py
"""Simulasi pengurutan pesan dengan sequence number per kanal.

Pesan bernomor 1 sampai 8 dikirim ke kanal, tetapi tiba di penerima
dalam urutan berbeda dan pesan 6 tidak terkirim sama sekali. Penerima
hanya menyampaikan pesan dengan nomor berurutan, menampung pesan yang
mendahului giliran, dan meminta pengiriman ulang saat ada celah.
"""

URUTAN_TIBA = [1, 3, 2, 5, 7, 8, 4]  # pesan 6 hilang di jaringan

ISI_PESAN = {
    1: "Materi minggu ini sudah terbit.",
    2: "Terima kasih, sedang dibaca.",
    3: "Ada yang bisa menjelaskan bagian cache?",
    4: "Bagian cache ada di subbab tiga.",
    5: "Sudah paham, terima kasih.",
    6: "Catatan diskusi sudah diarsipkan.",
    7: "Baik, sampai jumpa minggu depan.",
    8: "Sampai jumpa.",
}


def kirim(berikutnya, buffer_pesan, nomor):
    """Serahkan satu pesan tiba, kembalikan daftar pesan yang terkirim."""
    buffer_pesan[nomor] = ISI_PESAN[nomor]
    terkirim = []
    while berikutnya[0] in buffer_pesan:
        n = berikutnya[0]
        terkirim.append(n)
        del buffer_pesan[n]
        berikutnya[0] += 1
    return terkirim


if __name__ == "__main__":
    berikutnya = [1]
    buffer_pesan = {}
    for nomor in URUTAN_TIBA:
        terkirim = kirim(berikutnya, buffer_pesan, nomor)
        if terkirim:
            daftar = ", ".join(f"#{n}" for n in terkirim)
            print(f"tiba #{nomor} -> terkirim {daftar} sesuai urutan")
        else:
            print(f"tiba #{nomor} -> menunggu #{berikutnya[0]}, "
                  f"dibuffer ({', '.join('#' + str(n) for n in sorted(buffer_pesan))})")

    print(f"pesan #{berikutnya[0]} tidak kunjung tiba, penerima meminta "
          f"pengiriman ulang mulai offset {berikutnya[0]}")
    print()
    print("pengiriman ulang #6 tiba:")
    terkirim = kirim(berikutnya, buffer_pesan, 6)
    daftar = ", ".join(f"#{n}" for n in terkirim)
    print(f"  terkirim {daftar} sesuai urutan")
    print(f"semua 8 pesan tersampaikan, buffer akhir: {buffer_pesan or 'kosong'}")
text Keluaran
tiba #1 -> terkirim #1 sesuai urutan
tiba #3 -> menunggu #2, dibuffer (#3)
tiba #2 -> terkirim #2, #3 sesuai urutan
tiba #5 -> menunggu #4, dibuffer (#5)
tiba #7 -> menunggu #4, dibuffer (#5, #7)
tiba #8 -> menunggu #4, dibuffer (#5, #7, #8)
tiba #4 -> terkirim #4, #5 sesuai urutan
pesan #6 tidak kunjung tiba, penerima meminta pengiriman ulang mulai offset 6

pengiriman ulang #6 tiba:
  terkirim #6, #7, #8 sesuai urutan
semua 8 pesan tersampaikan, buffer akhir: kosong

Keluarannya memperlihatkan tiga perilaku sekaligus. Pesan 3 yang tiba lebih dulu ditampung dan diserahkan bersama pesan 2 begitu nomor itu tiba, sehingga urutan akhirnya tetap benar. Pesan 7 dan 8 menunggu dalam buffer sampai celah 4 tertutup, yang memperlihatkan konsekuensinya, yaitu pesan setelah celah ikut tertunda meskipun sudah tiba. Pesan 6 yang tidak pernah tiba tidak akan pulih dengan sendirinya, sehingga penerima mengenali celah yang menetap dan meminta pengiriman ulang mulai offset 6, lalu seluruh sisa pesan tersampaikan. Demo berikut mengulang skenario yang sama secara interaktif.

Presence#

Presence adalah status kehadiran pengguna, yaitu keterangan aktif atau tidaknya sebuah koneksi, dan menjadi bagian dari pengalaman chat meskipun bukan bagian dari kebenaran pesan. Klien mengirim heartbeat berkala selama koneksi hidup, server menganggap pengguna keluar apabila heartbeat berhenti melewati selang tertentu, dan perubahan statusnya disebar ke kontak yang menampilkannya. Selang penentuannya dibuat longgar, karena jaringan seluler sering berpindah-pindah dan status yang berkedip lebih buruk daripada status yang sedikit basi.

Presence tidak boleh dijadikan gerbang pengiriman. Pesan tetap dikirim dan disimpan meskipun penerima tampak tidak aktif, karena status bisa salah atau terlambat diperbarui, sedangkan pesan yang hilang tidak dapat dikembalikan. Aplikasi chat umumnya menampilkan keterangan terakhir aktif alih-alih mengklaim kehadiran yang presisi, dan status yang disebar pun cukup tiga keadaan, yaitu aktif, tidak aktif, dan terakhir aktif, karena perincian yang lebih halus menuntut akurasi yang memang tidak tersedia pada sinyalnya.

Penyimpanan dan resinkronisasi#

Pesan ditulis sekali dan dibaca berkali-kali, yaitu oleh setiap anggota kanal dan oleh setiap perangkatnya. Penyimpanannya tersusun per kanal dengan urutan sequence number, dan setiap perangkat menyimpan offset terakhirnya per kanal, sehingga sinkronisasi kembali hanya mengambil selisih mulai offset itu. Perangkat baru yang masuk kanal tidak mengunduh seluruh riwayat dari awal, melainkan mulai dari posisi yang dipilihnya, dan dua perangkat milik pengguna yang sama saling lepas karena masing-masing memegang offsetnya sendiri.

Penyimpanan yang sama juga menjadi sumber pemulihan. Pesan yang sudah tersimpan sebelum diteruskan ke penerima dapat dikirim ulang tanpa risiko kehilangan, dan permintaan pengiriman ulang dari penerima cukup dilayani dengan membaca dari offset yang diminta. Dua kebutuhan baca yang berbeda, yaitu riwayat lama dan selisih terbaru, dilayani dari penyusunan yang sama karena keduanya berupa rentang nomor urut. Dengan begitu, penomoran, penyimpanan, dan resinkronisasi memakai satu bilangan yang sama, yaitu sequence number kanal.

Mode kegagalan#

  • Gateway koneksi mati. Seluruh koneksi yang dipegangnya terputus sekaligus, tetapi pesan tidak hilang karena sudah tersimpan sebelum diteruskan. Klien menyambung ulang ke gateway lain, lalu meminta selisih mulai offset terakhirnya.
  • Layanan pesan mati. Pesan tertahan pada queue di depannya dan dikerjakan setelah pulih, sehingga keterlambatan singkat lebih dapat diterima daripada pesan yang hilang.
  • Pemberi sequence number kanal mati. Kanal yang bersangkutan berhenti sesaat, dan nomor terakhir yang tersimpan membuat penomoran berlanjut tanpa dobel setelah pemulihannya.
  • Penyimpanan pesan mati. Pesan baru tidak dapat disimpan, sehingga layanan pesan menolak dengan pesan error yang jelas dan klien menunggu, alih-alih menganggap pesan terkirim. Ketersediaan penyimpanan inilah dasar janji bahwa pesan tidak hilang.
  • Koneksi tidak stabil. Pesan tiba tidak berurutan dan ditampung oleh buffer penerima, sedangkan celah yang menetap memicu permintaan pengiriman ulang, sebagaimana simulasi pada bagian pengurutan.

Kesalahan umum#

  • Mengurutkan pesan dengan waktu klien. Jam antarperangkat bergeser, sehingga urutan tampil berbeda antarpenerima. Perbaikannya adalah sequence number yang diberikan server kanal.
  • Menolak mengirim ke pengguna yang tampak tidak aktif. Presence bisa basi, dan pesan yang tidak dikirim tidak akan pernah tiba meskipun pengguna kembali. Pesan selalu disimpan, dan status kehadiran hanya urusan tampilan.
  • Mengirim ulang tanpa idempotency key. Ketidakpastian jaringan memicu pengiriman ganda, dan pengguna melihat pesannya tampil dua kali. Pengenal unik dari klien membiarkan layanan menahan ulangannya.
  • Memuat seluruh riwayat untuk sinkronisasi. Perangkat yang kembali daring menunggu lama dan penyimpanan dibebani berlebih, padahal offset terakhir cukup untuk mengambil selisihnya. Batas jumlah pesan yang diambil sekali juga dipasang agar permintaan pertama tidak menumpuk.

Ringkasan#

  • Koneksi tahan lama dengan WebSocket dan gateway koneksi memindahkan pesan tanpa menunggu permintaan baru, sedangkan idempotency key menahan pesan dobel akibat kirim ulang.
  • Server kanal memberi sequence number per kanal, penerima menampung pesan yang mendahului giliran, dan celah yang menetap memicu pengiriman ulang mulai offset terakhir.
  • Presence dibangun dari heartbeat dengan selang longgar dan hanya untuk tampilan, bukan gerbang pengiriman, karena statusnya bisa basi.
  • Penyimpanan per kanal dengan urutan sequence number menjadikan offset perangkat sebagai kursor sinkronisasi sekaligus dasar pemulihan.

Referensi#