Pola Konsistensi Eventual

Read-your-writes, monotonic read, dan pengenalan vector clock untuk pengurutan.

Daftar isi

Topik ini membahas konsistensi eventual sebagai kontrak yang dapat diukur, bukan sekadar label pada produk. Pembahasan dimulai dari pembacaan usang yang muncul saat replika tertinggal, dilanjutkan dengan jaminan sesi read-your-writes dan monotonic read yang menutup celah paling terasa bagi pengguna, lalu vector clock yang mengurutkan versi data tanpa bergantung pada jam fisik. Semua contoh berupa simulasi Python yang dapat dijalankan ulang, dan satu demo interaktif memperlihatkan pelanggaran jaminan sesi secara langsung.

Konsistensi eventual dan pembacaan usang#

Konsistensi eventual menjanjikan bahwa apabila penulisan berhenti sejenak, seluruh salinan data akan menuju nilai yang sama. Janji itu tidak mengatakan apa pun tentang jendela waktu di antara keduanya. Selama replikasi berjalan, salinan pada replika pembaca bisa tertinggal beberapa event, dan pembacaan yang mengembalikan nilai lama disebut pembacaan usang (stale read). Jeda ini normal pada replikasi asinkron dan biasanya hanya berlangsung milidetik sampai beberapa detik, tetapi jendela itulah yang memunculkan keluhan pengguna yang paling sering didengar tim layanan.

Simulasi berikut memodelkan satu replika utama dan satu replika baca yang tertinggal satu event. Pengguna memperbarui alamat, kemudian beberapa pembacaan dilakukan dari titik yang berbeda. Perhatikan baris kedua dan ketiga pada keluaran: pembacaan dari replika masih mengembalikan alamat lama, sedangkan replika utama sudah menyimpan nilai baru.

python konsistensi-eventual.py
# Simulasi konsistensi eventual dengan satu replika yang tertinggal.
# Replika menerima salinan tulisan hanya ketika replikasi berjalan,
# sehingga pembacaan dari replika bisa bersifat usang (stale read).

utama = {"alamat": "Jl. Melati 12"}
replika = {"alamat": "Jl. Melati 12"}
antrean = []   # tulisan yang belum tersalin ke replika

def tulis(kunci, nilai):
    utama[kunci] = nilai
    antrean.append((kunci, nilai))

def jalankan_replikasi():
    while antrean:
        kunci, nilai = antrean.pop(0)
        replika[kunci] = nilai

def baca(sumber, kunci):
    return sumber[kunci]

def baca_dengan_sesi(penulisnya, kunci):
    # sesi penulis diarahkan ke replika utama
    if penulisnya:
        return baca(utama, kunci)
    return baca(replika, kunci)

print("Kondisi awal di replika :", baca(replika, "alamat"))

tulis("alamat", "Jl. Kenanga 7")

print("Pembacaan dari replika  :", baca(replika, "alamat"))
print("Pembacaan dari utama    :", baca(utama, "alamat"))
print("Sesi penulis, dari utama:", baca_dengan_sesi(True, "alamat"))
print("Sesi pembaca lain       :", baca_dengan_sesi(False, "alamat"))

jalankan_replikasi()
print("Replika setelah tersalin:", baca(replika, "alamat"))
text Keluaran
Kondisi awal di replika : Jl. Melati 12
Pembacaan dari replika  : Jl. Melati 12
Pembacaan dari utama    : Jl. Kenanga 7
Sesi penulis, dari utama: Jl. Kenanga 7
Sesi pembaca lain       : Jl. Melati 12
Replika setelah tersalin: Jl. Kenanga 7

Keluaran itu memperlihatkan dua kejadian sekaligus. Pembacaan kedua melanggar harapan paling dasar seorang pengguna, yaitu melihat tulisannya sendiri. Sementara itu, baris terakhir memperlihatkan bahwa nilai akhirnya konvergen setelah replikasi dijalankan, sesuai janji konsistensi eventual. Perbedaan antara jendela inkonsistensi dan kondisi akhir inilah yang sering tertukar dalam diskusi, padahal keputusan desainnya berbeda: jendela ditangani jaminan sesi, sedangkan kondisi akhir ditangani protokol replikasi.

Jaminan sesi read-your-writes dan monotonic read#

Read-your-writes adalah jaminan bahwa pengguna selalu melihat tulisannya sendiri segera setelah tulisan itu dikonfirmasi. Jaminan ini berlaku dalam lingkup satu sesi (session), yakni rangkaian permintaan dari satu pengguna yang dapat diidentifikasi. Cara implementasinya beragam, misalnya perutean sesi ke replika utama selama jendela lag, atau penyimpanan versi terakhir yang pernah dilihat sesi dan penolakan terhadap replika yang belum mencapai versi tersebut. Pada simulasi sebelumnya, fungsi baca_dengan_sesi menerapkan cara pertama, sehingga baris sesi penulis mengembalikan nilai baru meskipun replika baca masih tertinggal.

Monotonic read adalah jaminan bahwa pembacaan beruntun pada satu sesi tidak bergerak mundur. Tanpa jaminan ini, pembacaan pertama bisa ditangani replika yang mutakhir, lalu pembacaan kedua ditangani replika yang tertinggal, sehingga nilai yang sudah terlihat tampak hilang. Jaminan ini diterapkan dengan menempelkan sesi pada satu replika atau dengan membandingkan stempel versi terakhir sebelum menjawab permintaan. Kedua jaminan itu murah dibandingkan konsistensi linier karena hanya mengikat satu sesi, bukan seluruh pengguna pada saat yang sama.

Demo berikut memodelkan situasi tersebut. Klik tulis alamat baru, langsung klik baca tanpa mencentang sesi untuk melihat pelanggaran read-your-writes, lalu centang sesi dan ulangi pembacaannya. Tombol jalankan replikasi menutup jendela lag sehingga pembacaan dari replika kini sejalan.

Vector clock untuk mengurutkan versi#

Masalah berikutnya muncul saat dua replika masing-masing menerima tulisan selama koneksi antara keduanya terputus. Kedua versi sama-sama sah, dan sistem perlu tahu apakah salah satunya lebih baru atau keduanya saling tidak mengetahui. Membandingkan jam fisik tidak dapat diandalkan karena clock skew, sehingga diperlukan struktur yang merekam sebab-akibat secara eksplisit. Vector clock menjawab kebutuhan itu dengan satu penghitung per replika: setiap tulisan lokal menaikkan penghitung replikanya, dan saat replika bertukar data, kedua jam digabungkan dengan mengambil nilai maksimum per komponen.

Perbandingan dua jam menghasilkan tiga kemungkinan. Versi a berada sesudah b apabila semua komponen a paling tidak sama besar dengan b dan satu komponen benar-benar lebih besar. Versi a berada sebelum b apabila syaratnya berlaku sebaliknya. Apabila masing-masing punya komponen yang lebih besar, keduanya bersamaan, dan itulah tanda konflik yang harus diselesaikan secara eksplisit, misalnya lewat penggabungan tingkat aplikasi.

python vector-clock.py
# Simulasi vector clock untuk membandingkan dua versi data.
# Perbandingan menghasilkan "a sebelum b", "a sesudah b",
# atau "a bersamaan b" ketika keduanya saling tidak mengetahui.

def bandingkan(a, b):
    a_lebih_baru = any(a[k] > b.get(k, 0) for k in a)
    b_lebih_baru = any(b[k] > a.get(k, 0) for k in b)
    if a_lebih_baru and not b_lebih_baru:
        return "a sesudah b"
    if b_lebih_baru and not a_lebih_baru:
        return "a sebelum b"
    if not a_lebih_baru and not b_lebih_baru:
        return "a sama dengan b"
    return "a bersamaan b (konflik)"

def gabungkan(a, b):
    hasil = dict(a)
    for k, v in b.items():
        hasil[k] = max(hasil.get(k, 0), v)
    return hasil

# kondisi awal: kedua replika memegang versi yang sama
versi_a = {"A": 1, "B": 1}
versi_b = {"A": 1, "B": 1}
print("Versi awal:", versi_a)

# selama koneksi terputus, kedua replika menulis versinya sendiri
versi_a["A"] += 1
versi_b["B"] += 1
print("Replika A menulis ->", versi_a)
print("Replika B menulis ->", versi_b)
print("Perbandingan      :", bandingkan(versi_a, versi_b))

# ketika terhubung kembali, kedua versi digabungkan
versi_hasil = gabungkan(versi_a, versi_b)
print("Gabungan          ->", versi_hasil)
print("A terhadap gabungan:", bandingkan(versi_a, versi_hasil))
text Keluaran
Versi awal: {'A': 1, 'B': 1}
Replika A menulis -> {'A': 2, 'B': 1}
Replika B menulis -> {'A': 1, 'B': 2}
Perbandingan      : a bersamaan b (konflik)
Gabungan          -> {'A': 2, 'B': 2}
A terhadap gabungan: a sebelum b

Keluaran menunjukkan alur lengkapnya. Kedua tulisan selama koneksi terputus menghasilkan versi yang bersamaan, sehingga dibutuhkan keputusan konflik. Penggabungan mengambil maksimum tiap komponen dan menghasilkan versi yang mendominasi keduanya, sehingga perbandingan terakhir menyatakan a berada sebelum gabungan. Ukuran jam bertumbuh sejumlah replika, sehingga pada klaster besar vector clock biasanya dipangkas dengan interval waktu.

Kesalahan umum#

Kesalahan pertama adalah menganggap konsistensi eventual berarti semua salinan sama seketika. Penyebabnya adalah menyamakan jendela lag dengan janji konvergensi. Perbaikannya adalah mengukur lag replikasi dan menerapkan jaminan sesi pada operasi yang terasa oleh pengguna, karena kedua hal itu berada pada lapisan yang berbeda.

Kesalahan kedua adalah memilih versi terbaru berdasarkan jam fisik antarnode. Penyebabnya adalah clock skew yang membuat stempel waktu tidak sebanding, seperti dibahas pada topik Waktu dan Pengurutan. Perbaikannya adalah menggunakan vector clock atau hybrid logical clock untuk pengurutan lintas replika.

Kesalahan ketiga adalah memperlakukan hasil perbandingan vector clock sebagai urutan total. Dua versi yang bersamaan memang tidak memiliki urutan, sehingga memaksakan urutan berarti menghilangkan informasi konflik. Perbaikannya adalah menangani kasus bersamaan dengan strategi yang eksplisit, misalnya penggabungan atau keputusan tingkat aplikasi.

Ringkasan#

  • Konsistensi eventual menjanjikan konvergensi setelah tulisan berhenti, tetapi tidak menjanjikan apa pun tentang jendela lag, sehingga pembacaan usang adalah kondisi kerja yang normal.
  • Read-your-writes dan monotonic read adalah jaminan per sesi yang murah dan menutup keluhan pengguna yang paling sering muncul.
  • Vector clock membandingkan dua versi menjadi sebelum, sesudah, atau bersamaan, dan penggabungannya mengambil maksimum per komponen.

Referensi#