Menyeimbangkan Konsistensi Kuat dan Tertunda dengan Datastore

Memberikan Pengalaman Pengguna yang Konsisten dan Memanfaatkan Model Konsistensi Akhir untuk Menskalakan ke Set Data Besar

Dokumen ini membahas cara mencapai konsistensi yang kuat untuk pengalaman pengguna yang positif, sekaligus menggunakan model konsistensi akhir Datastore untuk menangani data dan pengguna dalam jumlah besar.

Dokumen ini ditujukan untuk arsitek dan engineer software yang ingin mem-build solusi di Datastore. Untuk membantu pembaca yang lebih memahami database relasional daripada sistem non-relasional seperti Datastore, dokumen ini menunjukkan konsep analog dalam database relasional. Dokumen ini mengasumsikan bahwa Anda memiliki pengetahuan dasar tentang Datastore. Cara termudah untuk memulai Datastore adalah di Google App Engine menggunakan salah satu bahasa yang didukung. Jika Anda belum menggunakan App Engine, sebaiknya baca Panduan Memulai dan bagian Menyimpan Data untuk salah satu bahasa tersebut terlebih dahulu. Meskipun Python digunakan untuk contoh fragmen kode, Anda tidak memerlukan keahlian Python untuk mengikuti dokumen ini.

Catatan: Cuplikan kode dalam artikel ini menggunakan Library Klien DB Python untuk Datastore, yang tidak lagi direkomendasikan. Developer yang membuat aplikasi baru sangat dianjurkan untuk menggunakan Library Klien NDB, yang memiliki beberapa manfaat dibandingkan dengan library klien ini, seperti menyimpan entity dalam cache secara otomatis melalui Memcache API. Jika saat ini Anda menggunakan Library Klien DB versi lama, baca Panduan Migrasi DB ke NDB

Daftar Isi

NoSQL dan Konsistensi Akhir
Konsistensi Akhir di Datastore
Kueri Ancestor dan Grup Entitas
Batasan Grup Entitas dan Kueri Ancestor
Alternatif untuk Kueri Ancestor
Meminimalkan Waktu untuk Mencapai Konsistensi Penuh
Kesimpulan
Referensi Tambahan

NoSQL dan Konsistensi Tertunda

Database non-relasional, juga dikenal sebagai database NoSQL, hadir sebagai alternatif untuk database relasional dalam beberapa tahun terakhir. Datastore adalah salah satu database non-relasional yang paling banyak digunakan di industri ini. Pada tahun 2013, Datastore memproses 4,5 triliun transaksi per bulan (postingan blog Google Cloud Platform). API ini menyediakan cara yang disederhanakan bagi developer untuk menyimpan dan mengakses data. Skema fleksibel dipetakan secara alami ke bahasa berorientasi objek dan skrip. Datastore juga menyediakan sejumlah fitur yang tidak dapat disediakan secara optimal oleh database relasional, termasuk performa tinggi dalam skala yang sangat besar dan keandalan tinggi.

Bagi developer yang lebih terbiasa dengan database relasional, mungkin sulit untuk mendesain sistem yang memanfaatkan database non-relasional, karena beberapa karakteristik dan praktik database non-relasional relatif asing bagi mereka. Meskipun model pemrograman Datastore sederhana, penting untuk mengetahui karakteristik ini. Konsistensi tertunda adalah salah satu karakteristik ini dan pemrograman untuk konsistensi tertunda adalah subjek utama dokumen ini.

Apa yang dimaksud dengan Konsistensi Tertunda?

Konsistensi tertunda adalah jaminan teoretis bahwa semua pembacaan entity pada akhirnya akan menampilkan nilai yang terakhir diperbarui, dengan ketentuan bahwa tidak ada update terbaru pada entity. Domain Name System (DNS) Internet adalah contoh terkenal dari sistem dengan model konsistensi tertunda. Server DNS tidak selalu mencerminkan nilai terbaru, tetapi nilai tersebut di-cache dan direplikasi di banyak direktori di Internet. Proses mereplikasi nilai yang diubah ke semua server dan klien DNS cukup memakan waktu. Namun, sistem DNS adalah sistem yang sangat sukses dan telah menjadi salah satu fondasi Internet. DNS ini sangat tersedia dan telah terbukti sangat skalabel, sehingga memungkinkan pencarian nama ke lebih dari seratus juta perangkat di seluruh Internet.

Gambar 1 mengilustrasikan konsep replikasi dengan konsistensi tertunda. Diagram ini menggambarkan bahwa meskipun replika selalu tersedia untuk dibaca, beberapa replika mungkin tidak konsisten dengan operasi tulis terbaru di node asal, pada waktu tertentu. Dalam diagram, Node A adalah node asal dan node B dan C adalah replika.

Gambar 1: Penggambaran Konseptual Replikasi dengan Konsistensi Akhir

Sebaliknya, database relasional tradisional dirancang berdasarkan konsep konsistensi kuat, yang juga disebut sebagai konsistensi langsung. Hal ini berarti data yang dilihat langsung setelah pembaruan akan konsisten untuk semua observer entity. Karakteristik ini menjadi asumsi dasar bagi banyak developer yang menggunakan database relasional. Namun, untuk memiliki konsistensi kuat, developer harus mengorbankan skalabilitas dan performa aplikasi mereka. Sederhananya, data harus dikunci selama periode pembaruan atau proses replikasi untuk memastikan bahwa tidak ada proses lain yang memperbarui data yang sama.

Tampilan konseptual dari topologi deployment dan proses replikasi dengan konsistensi yang kuat ditampilkan pada Gambar 2. Dalam diagram ini, Anda dapat melihat bagaimana replika selalu memiliki nilai yang konsisten dengan node asal, tetapi tidak dapat diakses hingga update selesai.

Gambar 2: Penggambaran Konseptual Replikasi dengan Konsistensi Kuat

Menyeimbangkan Konsistensi Kuat dan Tertunda

Database non-relasional telah menjadi populer baru-baru ini, terutama untuk aplikasi web yang memerlukan skalabilitas dan performa tinggi dengan ketersediaan tinggi. Database non-relasional memungkinkan developer memilih keseimbangan yang optimal antara konsistensi kuat dan konsistensi tertunda untuk setiap aplikasi. Hal ini memungkinkan developer untuk menggabungkan manfaat dari keduanya. Misalnya, informasi seperti “mengetahui siapa saja di daftar teman Anda yang online pada waktu tertentu” atau “mengetahui jumlah pengguna yang telah memberikan +1 pada postingan Anda” adalah kasus penggunaan yang tidak memerlukan konsistensi yang kuat. Skalabilitas dan performa dapat disediakan untuk kasus penggunaan ini dengan memanfaatkan konsistensi akhir. Kasus penggunaan yang memerlukan konsistensi yang kuat mencakup informasi seperti “apakah pengguna menyelesaikan proses penagihan atau tidak” atau “jumlah poin yang diperoleh pemain game selama sesi pertempuran”.

Untuk mengeneralisasi contoh yang baru saja diberikan, kasus penggunaan dengan jumlah entity yang sangat besar sering kali menunjukkan bahwa konsistensi tertunda adalah model terbaik. Jika ada banyak sekali hasil dalam kueri, pengalaman pengguna mungkin tidak terpengaruh oleh penyertaan atau pengecualian entitas tertentu. Di sisi lain, kasus penggunaan dengan sejumlah kecil entitas dan konteks yang sempit menunjukkan bahwa konsistensi yang kuat diperlukan. Pengalaman pengguna akan terpengaruh karena konteks akan membuat pengguna mengetahui entitas mana yang harus disertakan atau dikecualikan.

Karena alasan ini, developer harus memahami karakteristik non-relasional Datastore. Bagian berikut membahas cara menggabungkan model konsistensi akhir dan konsistensi kuat untuk membuat aplikasi yang skalabel, sangat tersedia, dan berperforma tinggi. Dengan demikian, persyaratan konsistensi untuk pengalaman pengguna yang positif akan tetap terpenuhi.

Konsistensi Tertunda di Datastore

API yang benar harus dipilih jika diperlukan tampilan data yang sangat konsisten. Berbagai variasi API kueri Datastore dan model konsistensi yang sesuai ditampilkan dalam Tabel 1.

Datastore API

Membaca nilai entity

Pembacaan indeks

Kueri Global

Konsistensi tertunda

Konsistensi tertunda

Kueri Global Khusus Kunci

T/A

Konsistensi tertunda

Kueri Ancestor

Konsistensi kuat

Konsistensi kuat

Pencarian menurut kunci (get())

Konsistensi kuat

T/A

Tabel 1: Kueri/panggilan get Datastore dan kemungkinan perilaku konsistensi

Kueri datastore tanpa ancestor dikenal sebagai kueri global dan dirancang untuk berfungsi dengan model konsistensi tertunda. Hal ini tidak menjamin konsistensi yang kuat. Kueri global khusus kunci adalah kueri global yang hanya menampilkan kunci entity yang cocok dengan kueri, bukan nilai atribut entity. Kueri ancestor menentukan cakupan kueri berdasarkan entity ancestor. Bagian berikut membahas setiap perilaku konsistensi secara lebih mendetail.

Konsistensi Tertunda saat Membaca Nilai Entity

Dengan pengecualian kueri ancestor, nilai entity yang diperbarui mungkin tidak langsung terlihat saat menjalankan kueri. Untuk memahami dampak konsistensi akhir saat membaca nilai entity, pertimbangkan skenario saat entity, Pemain, memiliki properti, Skor. Misalnya, pertimbangkan bahwa Skor awal memiliki nilai 100. Setelah beberapa saat, nilai Skor diperbarui menjadi 200. Jika kueri global dieksekusi dan menyertakan entity Pemain yang sama dalam hasilnya, nilai properti Skor entity yang ditampilkan mungkin tidak berubah, yaitu 100.

Perilaku ini disebabkan oleh replikasi antar-server Datastore. Replikasi dikelola oleh Bigtable dan Megastore, teknologi yang mendasari Datastore (lihat Referensi Tambahan untuk mengetahui detail Bigtable dan Megastore selengkapnya). Replikasi dijalankan dengan algoritma Paxos, yang menunggu secara sinkron hingga sebagian besar replika mengonfirmasi permintaan pembaruan. Replika diperbarui dengan data dari permintaan setelah jangka waktu tertentu. Jangka waktu ini biasanya singkat, tetapi tidak ada jaminan tentang panjangnya yang sebenarnya. Kueri dapat membaca data yang sudah tidak berlaku jika dijalankan sebelum update selesai.

Dalam banyak kasus, update akan menjangkau semua replika dengan sangat cepat. Namun, ada beberapa faktor yang, jika digabungkan, dapat memperpanjang waktu untuk mencapai konsistensi. Faktor ini mencakup insiden di seluruh pusat data yang melibatkan pengalihan sejumlah besar server antar-pusat data. Mengingat variasi faktor-faktor ini, kami tidak dapat memberikan persyaratan waktu yang pasti untuk membangun konsistensi penuh.

Waktu yang diperlukan untuk kueri menampilkan nilai terbaru biasanya sangat singkat. Namun, dalam situasi tertentu saat latensi replikasi meningkat, waktunya bisa jauh lebih lama. Aplikasi yang menggunakan kueri global Datastore harus dirancang dengan cermat untuk menangani kasus ini dengan baik.

Konsistensi akhir saat membaca nilai entity dapat dihindari dengan menggunakan kueri khusus kunci, kueri ancestor, atau pencarian menurut kunci (metode get()). Kita akan membahas berbagai jenis kueri ini secara lebih mendalam di bawah.

Konsistensi Tertunda saat Membaca Indeks

Indeks mungkin belum diperbarui saat kueri global dieksekusi. Artinya, meskipun Anda mungkin dapat membaca nilai properti terbaru dari entity, “daftar entity” yang disertakan dalam hasil kueri dapat difilter berdasarkan nilai indeks lama.

Untuk memahami dampak konsistensi akhir pada pembacaan indeks, bayangkan skenario saat entitas baru, Pemain, disisipkan ke Datastore. Entitas memiliki properti, Skor, yang memiliki nilai awal 300. Segera setelah penyisipan, Anda menjalankan kueri khusus kunci untuk mengambil semua entity dengan nilai Skor lebih besar dari 0. Anda akan mengharapkan entitas Pemain, yang baru saja disisipkan, muncul di hasil kueri. Mungkin tanpa diduga, Anda mungkin mendapati bahwa entitas Pemain tidak muncul dalam hasil. Situasi ini dapat terjadi jika tabel indeks untuk properti Skor tidak diperbarui dengan nilai yang baru disisipkan pada saat eksekusi kueri.

Perlu diingat bahwa semua kueri di Datastore dijalankan terhadap tabel indeks, tetapi pembaruan pada tabel indeks bersifat asinkron. Setiap pembaruan entitas pada dasarnya terdiri dari dua fase. Pada fase pertama, fase commit, operasi tulis ke log transaksi dilakukan. Pada fase kedua, data ditulis dan indeks diperbarui. Jika fase commit berhasil, fase tulis dijamin akan berhasil, meskipun mungkin tidak langsung terjadi. Jika Anda membuat kueri entity sebelum indeks diperbarui, Anda mungkin akan melihat data yang belum konsisten.

Sebagai akibat dari proses dua fase ini, ada penundaan waktu sebelum pembaruan terbaru pada entitas terlihat dalam kueri global. Sama seperti konsistensi nilai entitas pada akhirnya, penundaan waktu biasanya kecil, tetapi mungkin lebih lama (bahkan beberapa menit atau lebih dalam keadaan luar biasa).

Hal yang sama juga dapat terjadi setelah update. Misalnya, Anda memperbarui entity yang ada, Pemain, dengan nilai properti Skor baru 0, dan langsung mengeksekusi kueri yang sama setelahnya. Anda akan mengharapkan entitas tidak muncul dalam hasil kueri karena nilai Skor baru sebesar 0 akan mengecualikannya. Namun, karena perilaku pembaruan indeks asinkron yang sama, entity masih dapat disertakan dalam hasil.

Konsistensi akhir saat membaca indeks hanya dapat dihindari dengan menggunakan kueri ancestor atau pencarian menurut metode kunci. Kueri khusus kunci tidak dapat menghindari perilaku ini.

Konsistensi Kuat saat Membaca Nilai dan Indeks Entity

Di Datastore, hanya ada dua API yang memberikan tampilan yang sangat konsisten untuk membaca nilai dan indeks entity: (1) metode pencarian berdasarkan kunci dan (2) kueri ancestor. Jika logika aplikasi memerlukan konsistensi yang kuat, developer harus menggunakan salah satu metode ini untuk membaca entity dari Datastore.

Datastore dirancang khusus untuk memberikan konsistensi yang kuat pada API ini. Saat memanggil salah satunya, Datastore akan menghapus semua pembaruan yang tertunda di salah satu replika dan tabel indeks, lalu menjalankan kueri lookup atau ancestor. Dengan demikian, nilai entity terbaru, berdasarkan tabel indeks yang diperbarui, akan selalu ditampilkan dengan nilai berdasarkan update terbaru.

Pencarian berdasarkan panggilan kunci, berbeda dengan kueri, hanya menampilkan satu entity atau kumpulan entity yang ditentukan oleh kunci atau kumpulan kunci. Artinya, kueri ancestor adalah satu-satunya cara di Datastore untuk memenuhi persyaratan konsistensi yang kuat bersama dengan persyaratan pemfilteran. Namun, kueri ancestor tidak berfungsi tanpa menentukan entity group.

Kueri Ancestor dan Grup Entitas

Seperti yang telah dibahas di awal dokumen ini, salah satu manfaat Datastore adalah developer dapat menemukan keseimbangan yang optimal antara konsistensi yang kuat dan konsistensi akhir. Di Datastore, grup entity adalah unit dengan konsistensi, transaksionalitas, dan lokalitas yang kuat. Dengan memanfaatkan grup entity, developer dapat menentukan cakupan konsistensi yang kuat di antara entity dalam aplikasi. Dengan cara ini, aplikasi dapat mempertahankan konsistensi di dalam grup entity sekaligus mencapai skalabilitas, ketersediaan, dan performa yang tinggi sebagai sistem yang lengkap.

Grup entity adalah hierarki yang dibentuk oleh entity root dan turunan atau penerusnya.[1] Untuk membuat grup entity, developer menentukan jalur ancestor, yang pada dasarnya adalah serangkaian kunci induk yang diawali dengan kunci turunan. Konsep grup entitas diilustrasikan dalam Gambar 3. Dalam hal ini, entity root dengan kunci “ateam” memiliki dua turunan dengan kunci “ateam/098745” dan “ateam/098746”.

Gambar 3: Tampilan Skema Konsep Grup Entitas

Di dalam grup entity, karakteristik berikut dijamin:

  • Konsistensi Kuat
    • Kueri ancestor pada grup entity akan menampilkan hasil yang sangat konsisten. Dengan cara ini, nilai entitas terbaru akan tercermin dan difilter oleh status indeks terbaru.
  • Transaksi
    • Dengan menandai transaksi secara terprogram, grup entity menyediakan karakteristik ACID (atomicity, consistency, isolation, dan durability) dalam transaksi.
  • Lokalitas
    • Entity dalam entity group akan disimpan di tempat yang secara fisik berdekatan di server Datastore, karena semua entity diurutkan dan disimpan berdasarkan urutan leksikografis kunci. Hal ini memungkinkan kueri ancestor memindai grup entity dengan cepat dengan I/O minimal.

Kueri ancestor adalah bentuk khusus kueri yang hanya dieksekusi terhadap entity group yang ditentukan. Fungsi ini dijalankan dengan konsistensi yang kuat. Di balik layar, Datastore memastikan bahwa semua replika yang tertunda dan pembaruan indeks diterapkan sebelum menjalankan kueri.

Contoh Kueri Ancestor

Bagian ini menjelaskan cara menggunakan grup entity dan kueri ancestor dalam praktik. Pada contoh berikut, kita mempertimbangkan masalah pengelolaan data untuk orang. Misalkan kita memiliki kode yang menambahkan entitas dari jenis tertentu yang segera diikuti dengan kueri pada jenis tersebut. Konsep ini ditunjukkan oleh contoh kode Python di bawah.

# Define the Person entity
class Person(db.Model):
    given_name = db.StringProperty()
    surname = db.StringProperty()
    organization = db.StringProperty()
# Add a person and retrieve the list of all people
class MainPage(webapp2.RequestHandler):
    def post(self):
        person = Person(given_name='GI', surname='Joe', organization='ATeam')
        person.put()
        q = db.GqlQuery("SELECT * FROM Person")
        people = []
        for p in q.run():
            people.append({'given_name': p.given_name,
                        'surname': p.surname,
                        'organization': p.organization})

Masalah dengan kode ini adalah, dalam sebagian besar kasus, kueri tidak akan menampilkan entity yang ditambahkan dalam pernyataan di atasnya. Karena kueri mengikuti baris yang langsung mengikuti penyisipan, indeks tidak akan diperbarui saat kueri dieksekusi. Namun, ada juga masalah dengan validitas kasus penggunaan ini: apakah benar-benar perlu menampilkan daftar semua orang di satu halaman tanpa konteks? Bagaimana jika ada satu juta orang? Halaman akan memerlukan waktu terlalu lama untuk ditampilkan.

Sifat kasus penggunaan menunjukkan bahwa kita harus memberikan beberapa konteks untuk mempersempit kueri. Dalam contoh ini, konteks yang akan kita gunakan adalah organisasi. Jika melakukannya, kita dapat menggunakan organisasi sebagai entity group dan menjalankan kueri ancestor, yang akan menyelesaikan masalah konsistensi. Hal ini ditunjukkan dengan kode Python di bawah.

class Organization(db.Model):
    name = db.StringProperty()
class Person(db.Model):
    given_name = db.StringProperty()
    surname = db.StringProperty()
class MainPage(webapp2.RequestHandler):
    def post(self):
        org = Organization.get_or_insert('ateam', name='ATeam')
        person = Person(parent=org)
        person.given_name='GI'
        person.surname='Joe'
        person.put()
        q = db.GqlQuery("SELECT * FROM Person WHERE ANCESTOR IS :1 ", org)
        people = []
        for p in q.run():
            people.append({'given_name': p.given_name,
                        'surname': p.surname})

Kali ini, dengan organisasi ancestor yang ditentukan dalam GqlQuery, kueri akan menampilkan entity yang baru saja disisipkan. Contoh ini dapat diperluas untuk melihat perincian setiap orang dengan membuat kueri nama orang tersebut dengan ancestor sebagai bagian dari kueri. Atau, hal ini juga dapat dilakukan dengan menyimpan kunci entity, lalu menggunakannya untuk melihat perincian dengan pencarian berdasarkan kunci.

Mempertahankan Konsistensi Antara Memcache dan Datastore

Entity group juga dapat digunakan sebagai unit untuk mempertahankan konsistensi antara entri Memcache dan entity Datastore. Misalnya, pertimbangkan skenario saat Anda menghitung jumlah Orang di setiap tim dan menyimpannya di Memcache. Untuk memastikan data yang di-cache konsisten dengan nilai terbaru di Datastore, Anda dapat menggunakan metadata grup entity. Metadata menampilkan nomor versi terbaru dari grup entity yang ditentukan. Anda dapat membandingkan nomor versi dengan nomor yang disimpan di Memcache. Dengan metode ini, Anda dapat mendeteksi perubahan pada entity mana pun di seluruh grup entity dengan membaca dari satu kumpulan metadata, bukan memindai setiap entity dalam grup.

Batasan Kueri Ancestor dan Grup Entitas

Pendekatan penggunaan grup entity dan kueri ancestor bukanlah solusi yang sempurna. Ada dua tantangan dalam praktik yang mempersulit penerapan teknik ini secara umum, seperti yang tercantum di bawah.

  1. Ada batas satu update per detik untuk setiap entity group.
  2. Hubungan grup entitas tidak dapat diubah setelah pembuatan entitas.

Batas Tulis

Tantangan penting adalah sistem harus dirancang untuk memuat jumlah pembaruan (atau transaksi) di setiap grup entity. Batas yang didukung adalah satu pembaruan per detik per entity group.[2] Jika jumlah pembaruan harus melebihi batas tersebut, grup entity mungkin menjadi bottleneck performa.

Pada contoh di atas, setiap organisasi mungkin perlu memperbarui data setiap orang di organisasi. Pertimbangkan skenario dengan 1.000 orang di “ateam” dan setiap orang mungkin memiliki satu pembaruan per detik di salah satu properti. Akibatnya, mungkin ada hingga 1.000 pembaruan per detik di grup entity, yang tidak akan dapat dicapai karena batas pembaruan. Hal ini menunjukkan bahwa penting untuk memilih desain grup entitas yang sesuai dan mempertimbangkan persyaratan performa. Ini adalah salah satu tantangan dalam menemukan keseimbangan optimal antara konsistensi tertunda dan konsistensi kuat.

Ketetapan Hubungan Grup Entitas

Tantangan kedua adalah sifat hubungan grup entitas yang tidak dapat diubah. Hubungan grup entity dibentuk secara statis berdasarkan penamaan kunci. Nama ini tidak dapat diubah setelah membuat entitas. Satu-satunya opsi yang tersedia untuk mengubah hubungan adalah menghapus entitas dalam grup entitas dan membuatnya lagi. Tantangan ini mencegah kita menggunakan grup entity untuk menentukan cakupan ad-hoc secara dinamis untuk konsistensi atau transaksionalitas. Sebagai gantinya, cakupan konsistensi dan transaksionalitas terkait erat dengan grup entitas statis yang ditentukan pada waktu desain.

Misalnya, pertimbangkan skenario saat Anda ingin menerapkan transfer bank antara dua rekening bank. Skenario bisnis ini memerlukan konsistensi dan transaksi yang kuat. Namun, kedua akun tersebut tidak dapat dikelompokkan ke dalam satu entity group pada menit terakhir atau didasarkan pada induk global. Grup entitas tersebut akan menyebabkan bottleneck untuk seluruh sistem yang akan menghambat permintaan transfer dana lainnya untuk dieksekusi. Jadi, grup entitas tidak dapat digunakan dengan cara ini.

Ada cara alternatif untuk menerapkan transfer kawat dengan cara yang sangat skalabel dan tersedia. Daripada menempatkan semua akun dalam satu grup entitas, Anda dapat membuat grup entitas untuk setiap akun. Dengan demikian, Anda dapat menggunakan transaksi untuk memastikan pembaruan ACID ke kedua rekening bank. Transaksi adalah fitur Datastore yang memungkinkan Anda membuat kumpulan operasi dengan karakteristik ACID untuk maksimal dua puluh lima grup entity. Perhatikan bahwa dalam transaksi, Anda harus menggunakan kueri yang sangat konsisten seperti pencarian berdasarkan kueri kunci dan ancestor. Untuk mengetahui informasi selengkapnya tentang pembatasan transaksi, lihat Transaksi dan grup entity.

Alternatif untuk Kueri Ancestor

Jika Anda sudah memiliki aplikasi yang ada dengan sejumlah besar entity yang disimpan di Datastore, mungkin sulit untuk menggabungkan entity group setelahnya dalam latihan pemfaktoran ulang. Anda harus menghapus semua entitas dan menambahkannya dalam hubungan grup entitas. Jadi, dalam pemodelan data untuk Datastore, penting untuk membuat keputusan tentang desain grup entity pada fase awal desain aplikasi. Jika tidak, Anda mungkin dibatasi dalam pemfaktoran ulang ke alternatif lain untuk mencapai tingkat konsistensi tertentu, seperti kueri khusus kunci yang diikuti dengan pencarian berdasarkan kunci, atau dengan menggunakan Memcache.

Kueri Global Khusus Kunci yang Diikuti dengan Pencarian menurut Kunci

Kueri global khusus kunci adalah jenis khusus kueri global yang hanya menampilkan kunci tanpa nilai properti entity. Karena nilai yang ditampilkan hanya berupa kunci, kueri tidak melibatkan nilai entity dengan kemungkinan masalah konsistensi. Kombinasi kueri global khusus kunci dengan metode pencarian akan membaca nilai entity terbaru. Namun, perlu diperhatikan bahwa kueri global khusus kunci tidak dapat mengecualikan kemungkinan indeks belum konsisten pada saat kueri, yang dapat menyebabkan entity tidak diambil sama sekali. Hasil kueri berpotensi dihasilkan berdasarkan pemfilteran nilai indeks lama. Singkatnya, developer dapat menggunakan kueri global khusus kunci, diikuti dengan pencarian menurut kunci hanya jika persyaratan aplikasi memungkinkan nilai indeks belum konsisten pada saat kueri.

Menggunakan Memcache

Layanan Memcache tidak stabil, tetapi sangat konsisten. Jadi, dengan menggabungkan pencarian Memcache dan kueri Datastore, Anda dapat membuat sistem yang akan meminimalkan masalah konsistensi sebagian besar waktu.

Misalnya, pertimbangkan skenario aplikasi game yang mempertahankan daftar entity Pemain, masing-masing dengan skor lebih besar dari nol.

  • Untuk permintaan penyisipan atau pembaruan, terapkan ke daftar entity Pemain di Memcache serta Datastore.
  • Untuk permintaan kueri, baca daftar entity Pemain dari Memcache dan jalankan kueri khusus kunci di Datastore jika daftar tidak ada di Memcache.

Daftar yang ditampilkan akan konsisten setiap kali daftar yang di-cache ada di Memcache. Jika entri telah dihapus, atau layanan Memcache tidak tersedia untuk sementara, sistem mungkin perlu membaca nilai dari kueri Datastore yang mungkin menampilkan hasil yang tidak konsisten. Teknik ini dapat diterapkan ke aplikasi apa pun yang dapat menoleransi sedikit inkonsistensi.

Ada beberapa praktik terbaik saat menggunakan Memcache sebagai lapisan penyimpanan dalam cache untuk Datastore:

  • Menangkap pengecualian dan error Memcache untuk mempertahankan konsistensi antara nilai Memcache dan nilai Datastore. Jika Anda menerima pengecualian saat memperbarui entri di Memcache, pastikan untuk membatalkan validasi entri lama di Memcache. Jika tidak, mungkin ada nilai yang berbeda untuk entity (nilai lama di Memcache dan nilai baru di Datastore).
  • Tetapkan periode habis masa berlaku pada entri Memcache. Sebaiknya tetapkan jangka waktu singkat untuk masa berlaku setiap entri guna meminimalkan kemungkinan inkonsistensi jika terjadi pengecualian Memcache.
  • Gunakan fitur compare-and-set saat memperbarui entri untuk kontrol konkurensi. Hal ini akan membantu memastikan bahwa update serentak pada entri yang sama tidak akan saling mengganggu.

Migrasi Bertahap ke Grup Entitas

Saran yang dibuat di bagian sebelumnya hanya mengurangi kemungkinan perilaku yang tidak konsisten. Sebaiknya desain aplikasi berdasarkan grup entity dan kueri ancestor jika konsistensi yang kuat diperlukan. Namun, migrasi aplikasi yang ada mungkin tidak memungkinkan, yang dapat mencakup perubahan model data dan logika aplikasi yang ada dari kueri global menjadi kueri ancestor. Salah satu cara untuk mencapainya adalah dengan memiliki proses transisi bertahap, seperti berikut:

  1. Identifikasi dan prioritaskan fungsi dalam aplikasi yang memerlukan konsistensi yang kuat.
  2. Tulis logika baru untuk fungsi insert() atau update() menggunakan grup entity selain (bukan mengganti) logika yang ada. Dengan cara ini, penyisipan atau pembaruan baru pada grup entity baru dan entity lama dapat ditangani oleh fungsi yang sesuai.
  3. Ubah logika yang ada untuk kueri ancestor fungsi baca atau kueri dieksekusi terlebih dahulu jika ada grup entity baru untuk permintaan. Jalankan kueri global lama sebagai logika penggantian jika grup entity tidak ada.

Strategi ini memungkinkan migrasi bertahap dari model data yang ada ke model data baru berdasarkan grup entitas yang meminimalkan risiko masalah yang disebabkan oleh konsistensi akhir. Dalam praktiknya, pendekatan ini bergantung pada kasus penggunaan dan persyaratan tertentu untuk penerapannya ke sistem yang sebenarnya.

Penggantian ke Mode Terdegradasi

Saat ini, sulit untuk mendeteksi situasi secara terprogram saat aplikasi mengalami penurunan konsistensi. Namun, jika Anda kebetulan menentukan melalui cara lain bahwa aplikasi telah mengalami penurunan konsistensi, Anda mungkin dapat menerapkan mode degradasi yang dapat diaktifkan atau dinonaktifkan untuk menonaktifkan beberapa area logika aplikasi yang memerlukan konsistensi yang kuat. Misalnya, daripada menampilkan hasil kueri yang tidak konsisten di layar laporan penagihan, pesan pemeliharaan untuk layar tertentu dapat ditampilkan. Dengan cara ini, layanan lain dalam aplikasi dapat terus ditayangkan, dan pada akhirnya, mengurangi dampak terhadap pengalaman pengguna.

Meminimalkan Waktu untuk Mencapai Konsistensi Penuh

Dalam aplikasi besar dengan jutaan pengguna atau terabyte entitas Datastore, penggunaan Datastore yang tidak tepat dapat menyebabkan konsistensi yang menurun. Praktik tersebut mencakup:

  • Penomoran berurutan dalam kunci entity
  • Terlalu banyak indeks

Praktik ini tidak memengaruhi aplikasi kecil. Namun, setelah aplikasi berkembang menjadi sangat besar, praktik ini meningkatkan kemungkinan waktu yang lebih lama diperlukan untuk konsistensi. Jadi, sebaiknya hindari hal tersebut pada tahap awal desain aplikasi.

Anti-Pola #1: Pemberian Nomor Urut Kunci Entitas

Sebelum rilis App Engine SDK 1.8.1, Datastore menggunakan urutan ID bilangan bulat kecil dengan pola yang umumnya berurutan sebagai nama kunci default yang dibuat secara otomatis. Dalam beberapa dokumen, hal ini disebut sebagai “kebijakan lama” untuk membuat entity yang tidak memiliki nama kunci yang ditentukan aplikasi. Kebijakan lama ini menghasilkan nama kunci entity dengan penomoran berurutan, misalnya 1000, 1001, 1002. Namun, seperti yang telah kita bahas sebelumnya, Datastore menyimpan entity berdasarkan urutan leksikografis nama kunci, sehingga entity tersebut kemungkinan besar akan disimpan di server Datastore yang sama. Jika aplikasi menarik traffic yang sangat besar, penomoran berurutan ini dapat menyebabkan konsentrasi operasi di server tertentu, yang dapat menyebabkan latensi yang lebih lama untuk konsistensi.

Di App Engine SDK 1.8.1, Datastore memperkenalkan metode penomoran ID baru dengan kebijakan default yang menggunakan ID yang tersebar (lihat dokumentasi referensi). Kebijakan default ini menghasilkan urutan acak ID hingga 16 digit yang kira-kira didistribusikan secara seragam. Dengan menggunakan kebijakan ini, kemungkinan traffic aplikasi besar akan didistribusikan dengan lebih baik di antara sekumpulan server Datastore dengan waktu yang lebih singkat untuk konsistensi. Kebijakan default direkomendasikan kecuali jika aplikasi Anda secara khusus memerlukan kompatibilitas dengan kebijakan lama.

Jika Anda menetapkan nama kunci secara eksplisit pada entity, skema penamaan harus dirancang untuk mengakses entity secara merata di seluruh ruang nama kunci. Dengan kata lain, jangan konsentrasikan akses dalam rentang tertentu karena diurutkan berdasarkan urutan leksikografis nama kunci. Jika tidak, masalah yang sama seperti penomoran berurutan dapat terjadi.

Untuk memahami distribusi akses yang tidak merata di ruang kunci, pertimbangkan contoh saat entitas dibuat dengan nama kunci berurutan seperti yang ditunjukkan dalam kode berikut:

p1 = Person(key_name='0001')
p2 = Person(key_name='0002')
p3 = Person(key_name='0003')
...

Pola akses aplikasi dapat membuat “hot spot” pada rentang nama kunci tertentu, seperti memiliki akses terkonsentrasi pada entity Orang yang baru dibuat. Dalam hal ini, kunci yang sering diakses akan memiliki ID yang lebih tinggi. Kemudian, beban dapat dikonsentrasikan pada server Datastore tertentu.

Atau, untuk memahami distribusi yang merata di seluruh ruang kunci, sebaiknya gunakan string acak panjang untuk nama kunci. Hal ini diilustrasikan dalam contoh berikut:

p1 = Person(key_name='t9P776g5kAecChuKW4JKCnh44uRvBDhU')
p2 = Person(key_name='hCdVjL2jCzLqRnPdNNcPCAN8Rinug9kq')
p3 = Person(key_name='PaV9fsXCdra7zCMkt7UX3THvFmu6xsUd')
...

Sekarang, entity Person yang baru dibuat akan tersebar di seluruh ruang kunci dan di beberapa server. Hal ini mengasumsikan bahwa ada cukup banyak entity Orang.

Anti Pola #2: Terlalu Banyak Indeks

Di Datastore, satu pembaruan pada entitas akan menyebabkan pembaruan pada semua indeks yang ditentukan untuk jenis entitas tersebut. Jika aplikasi menggunakan banyak indeks kustom, satu update dapat melibatkan puluhan, ratusan, atau bahkan ribuan update pada tabel indeks. Dalam aplikasi besar, penggunaan indeks kustom yang berlebihan dapat menyebabkan peningkatan beban pada server dan dapat meningkatkan latensi untuk mencapai konsistensi.

Umumnya, indeks kustom ditambahkan untuk mendukung persyaratan seperti dukungan pelanggan, pemecahan masalah, atau tugas analisis data. BigQuery adalah mesin kueri yang sangat skalabel dan dapat menjalankan kueri ad hoc pada set data besar tanpa indeks bawaan. BigQuery lebih cocok untuk kasus penggunaan seperti dukungan pelanggan, pemecahan masalah, atau analisis data yang memerlukan kueri kompleks daripada Datastore.

Salah satu praktiknya adalah menggabungkan Datastore dan BigQuery untuk memenuhi berbagai persyaratan bisnis. Gunakan Datastore untuk pemrosesan transaksi online (OLTP) yang diperlukan untuk logika aplikasi inti dan gunakan BigQuery untuk pemrosesan analisis online (OLAP) untuk operasi backend. Anda mungkin perlu menerapkan alur ekspor data berkelanjutan dari Datastore ke BigQuery untuk memindahkan data yang diperlukan untuk kueri tersebut.

Selain implementasi alternatif untuk indeks kustom, rekomendasi lainnya adalah menentukan properti yang tidak diindeks secara eksplisit (lihat Properti dan jenis nilai). Secara default, Datastore akan membuat tabel indeks yang berbeda untuk setiap properti yang dapat diindeks dari jenis entity. Jika Anda memiliki 100 properti pada jenis, akan ada 100 tabel indeks untuk jenis tersebut, dan 100 pembaruan tambahan pada setiap pembaruan ke entitas. Oleh karena itu, praktik terbaiknya adalah menetapkan properti yang tidak diindeks jika memungkinkan, jika tidak diperlukan untuk kondisi kueri.

Selain mengurangi kemungkinan peningkatan waktu untuk konsistensi, pengoptimalan indeks ini dapat menghasilkan pengurangan biaya penyimpanan Datastore yang cukup besar dalam aplikasi besar yang banyak menggunakan indeks.

Kesimpulan

Konsistensi tertunda adalah elemen penting dari database non-relasional yang memungkinkan developer menemukan keseimbangan yang optimal antara skalabilitas, performa, dan konsistensi. Penting untuk memahami cara menangani keseimbangan antara konsistensi yang kuat dan konsistensi yang akhirnya untuk mendesain model data yang optimal bagi aplikasi Anda. Di Datastore, penggunaan entity group dan kueri ancestor adalah cara terbaik untuk menjamin konsistensi yang kuat di seluruh cakupan entity. Jika aplikasi Anda tidak dapat menggabungkan grup entity karena batasan yang dijelaskan sebelumnya, Anda dapat mempertimbangkan opsi lain seperti menggunakan kueri khusus kunci atau Memcache. Untuk aplikasi besar, terapkan praktik terbaik seperti penggunaan ID yang tersebar dan pengurangan pengindeksan untuk mengurangi waktu yang diperlukan untuk konsistensi. Anda mungkin juga perlu menggabungkan Datastore dengan BigQuery untuk memenuhi persyaratan bisnis terkait kueri yang kompleks dan mengurangi penggunaan indeks Datastore sebanyak mungkin.

Referensi Lainnya

Referensi berikut memberikan informasi selengkapnya tentang topik yang dibahas dalam dokumen ini:




[1] Grup entity bahkan dapat dibentuk dengan hanya menentukan satu kunci root entity atau parent entity, tanpa menyimpan entity sebenarnya untuk root entity atau parent entity, karena fungsi grup entity semuanya diterapkan berdasarkan hubungan antar-kunci.

[2] Batas yang didukung adalah satu pembaruan per detik per entity group di luar transaksi, atau satu transaksi per detik per entity group. Jika menggabungkan beberapa pembaruan ke dalam satu transaksi, Anda dibatasi pada ukuran transaksi maksimum 10 MB dan kecepatan tulis maksimum server Datastore.