Menyeimbangkan Konsistensi yang Kuat dan Berakhir dengan Datastore

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

Dokumen ini membahas cara mencapai konsistensi kuat untuk menciptakan pengalaman pengguna yang positif, sembari menggunakan model konsistensi tertunda dari Datastore untuk menangani data dan pengguna dalam jumlah besar.

Dokumen ini ditujukan untuk arsitek dan engineer software yang ingin membangun solusi di Datastore. Untuk membantu pembaca yang lebih memahami database relasional daripada sistem non-relasional seperti Datastore, dokumen ini menunjukkan konsep yang serupa 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 belum menggunakan App Engine, sebaiknya baca Panduan Memulai dan Menyimpan Data terlebih dahulu untuk salah satu bahasa tersebut. Meskipun Python digunakan untuk contoh fragmen kode, namun keahlian Python tidak diperlukan untuk mengikuti dokumen ini.

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

Daftar Isi

NoSQL dan Eventual Consistency
Eventual Consistency in Datastore
Ancestor Query dan Entity Group
Batasan Entity Group dan Ancestor Query
Alternatif untuk Kueri Ancestor
Meminimalkan Waktu untuk Mencapai Konsistensi Penuh
ReferensiKesimpulan{/1

NoSQL dan Konsistensi Akhir

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. Pada tahun 2013, Datastore memproses 4,5 triliun transaksi per bulan (postingan blog Google Cloud Platform). Layanan ini memberikan cara yang disederhanakan bagi developer untuk menyimpan dan mengakses data. Skema fleksibel memetakan secara alami ke bahasa berorientasi objek dan skrip. Datastore juga menyediakan sejumlah fitur yang tidak secara optimal disediakan 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 mungkin relatif asing bagi mereka. Meskipun model pemrograman Datastore sederhana, penting untuk mengetahui karakteristik ini. Konsistensi tertunda adalah salah satu karakteristik dan pemrograman untuk konsistensi tertunda merupakan subjek utama dokumen ini.

Apa itu Konsistensi Akhir?

Konsistensi akhir adalah jaminan teoretis bahwa, jika tidak ada pembaruan baru pada entity yang dibuat, semua operasi baca entity pada akhirnya akan menampilkan nilai yang terakhir diperbarui. Internet Domain Name System (DNS) adalah contoh terkenal dari sistem dengan model konsistensi tertunda. Server DNS tidak selalu mencerminkan nilai terbaru, tetapi nilai tersebut disimpan dalam 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 yang telah menjadi salah satu fondasi Internet. Layanan ini sangat tersedia dan telah terbukti sangat skalabel, yang memungkinkan pencarian nama ke lebih dari seratus juta perangkat di seluruh Internet.

Gambar 1 mengilustrasikan konsep replikasi dengan konsistensi tertunda. Diagram 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, sedangkan 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.

Gambaran konseptual topologi deployment dan proses replikasi dengan konsistensi kuat ditunjukkan dalam 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 yang Kuat dan Konsistensi pada Akhir

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

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

Oleh karena itu, penting bagi developer untuk memahami karakteristik non-relasional Datastore. Bagian berikut membahas bagaimana model konsistensi akhir dan konsistensi kuat dapat digabungkan untuk membangun aplikasi yang skalabel, sangat tersedia, dan berperforma tinggi. Dengan demikian, persyaratan konsistensi untuk pengalaman pengguna yang positif akan tetap terpenuhi.

Konsistensi Akhir di Datastore

API yang benar harus dipilih jika tampilan data yang sangat konsisten diperlukan. Berbagai varietas API kueri Datastore dan model konsistensinya yang terkait ditampilkan pada 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

Cari menurut kunci (get())

Konsistensi kuat

T/A

Tabel 1: Kueri Datastore/mendapatkan panggilan 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 keys-only adalah keys-only yang hanya menampilkan kunci entity yang cocok dengan kueri, bukan nilai atribut entity. Kueri ancestor mencakup kueri berdasarkan entity ancestor. Bagian berikut membahas setiap perilaku konsistensi secara lebih mendetail.

Konsistensi Akhir saat Membaca Nilai Entity

Dengan pengecualian kueri ancestor, nilai entity yang diperbarui mungkin tidak langsung terlihat saat mengeksekusi kueri. Untuk memahami dampak konsistensi tertunda saat membaca nilai entity, pertimbangkan skenario di mana entitas, Player, memiliki properti, Score. Misalnya, anggaplah Skor awal memiliki nilai 100. Setelah beberapa waktu, nilai Skor diperbarui menjadi 200. Jika kueri global dijalankan dan menyertakan entitas Pemain yang sama dalam hasilnya, ada kemungkinan nilai Skor properti entitas yang ditampilkan mungkin tidak berubah, yakni 100.

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

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

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

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

Konsistensi Akhir dalam Membaca Indeks

Indeks mungkin belum diperbarui saat kueri global dijalankan. Ini berarti bahwa, meskipun Anda dapat membaca nilai properti terbaru entitas, "daftar entitas" yang disertakan dalam hasil kueri mungkin difilter berdasarkan nilai indeks lama.

Untuk memahami dampak konsistensi tertunda terhadap pembacaan indeks, bayangkan skenario di mana entity baru, Pemutar, dimasukkan ke dalam 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 kemudian akan mengharapkan entitas Pemain, yang baru saja dimasukkan, muncul di hasil kueri. Mungkin secara tidak terduga, sebagai gantinya, Anda mungkin menemukan bahwa Entitas pemain tidak muncul dalam hasil. Situasi ini dapat terjadi jika tabel indeks untuk properti Skor tidak diperbarui dengan nilai yang baru dimasukkan pada saat eksekusi kueri.

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

Akibat proses dua fase ini, ada penundaan waktu sebelum pembaruan terbaru pada entity terlihat dalam kueri global. Sama seperti nilai entitas, jeda waktu biasanya kecil, tetapi bisa lebih lama (bahkan menit atau lebih dalam situasi tertentu).

Hal yang sama juga dapat terjadi setelah pembaruan. Misalnya, Anda memperbarui entitas yang sudah ada, yaitu Pemain, dengan nilai properti Skor baru 0, dan menjalankan kueri yang sama segera setelahnya. Anda mungkin memperkirakan entitas tersebut tidak akan muncul di hasil kueri karena nilai Skor baru 0 akan mengecualikannya. Namun, karena perilaku update indeks asinkron yang sama, entity masih dapat disertakan dalam hasil.

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

Konsistensi Kuat dalam Membaca Nilai dan Indeks Entity

Di Datastore, hanya ada dua API yang menyediakan tampilan yang sangat konsisten untuk membaca nilai dan indeks entity: (1) pencarian berdasarkan metode 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 update yang tertunda pada salah satu replika dan tabel indeks, lalu menjalankan kueri pencarian atau ancestor. Dengan demikian, nilai entity terbaru, berdasarkan tabel indeks yang diupdate, akan selalu ditampilkan dengan nilai berdasarkan update terbaru.

Pencarian berdasarkan panggilan kunci, berbeda dengan kueri, hanya menampilkan satu entitas atau serangkaian entitas yang ditentukan oleh kunci atau serangkaian kunci. Ini berarti bahwa kueri ancestor adalah satu-satunya cara di Datastore untuk memenuhi persyaratan konsistensi yang kuat beserta persyaratan pemfilteran. Namun, kueri ancestor tidak berfungsi tanpa menentukan grup entity.

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 tertunda. Di Datastore, grup entity adalah unit dengan konsistensi, transaksional, dan lokalitas yang kuat. Dengan memanfaatkan grup entity, developer dapat menentukan cakupan konsistensi 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 root entity dan turunan atau penerusnya.[1] Untuk membuat entity group, developer menentukan jalur ancestor, yang pada dasarnya adalah serangkaian kunci induk yang mengawali kunci turunan. Konsep entity group diilustrasikan dalam Gambar 3. Dalam hal ini, entitas root dengan kunci "ateam" memiliki dua turunan dengan kunci "ateam/098745" dan "ateam/098746".

Gambar 3: Tampilan Skema Konsep Grup Entity

Di dalam grup entity, karakteristik berikut dijamin:

  • Konsistensi Kuat
    • Kueri ancestor pada grup entity akan menampilkan hasil yang sangat konsisten. Dengan cara ini, hal itu mencerminkan nilai entity terbaru yang difilter oleh status indeks terbaru.
  • Transaksialitas
    • Dengan mendemarkasi transaksi secara terprogram, grup entity memberikan karakteristik ACID (atomicity, konsistensi, isolasi, dan ketahanan) dalam transaksi.
  • Lokalitas
    • Entity dalam grup entity akan disimpan di tempat yang dekat secara fisik pada server Datastore, karena semua entity diurutkan dan disimpan berdasarkan urutan leksikografis kunci. Hal ini memungkinkan kueri ancestor memindai grup entity dengan cepat menggunakan I/O minimal.

Kueri ancestor adalah bentuk kueri khusus yang hanya dijalankan terhadap grup entity yang telah ditentukan. Hal ini dijalankan dengan konsistensi kuat. Di balik layar, Datastore memastikan bahwa semua replikasi dan update indeks yang tertunda akan diterapkan sebelum menjalankan kueri.

Contoh Kueri Ancestor

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

# 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, pada umumnya, kueri tidak akan menampilkan entity yang ditambahkan dalam pernyataan di atasnya. Karena kueri mengikuti baris setelah penyisipan dilakukan, indeks tidak akan diperbarui saat kueri dijalankan. Namun, ada juga masalah dengan validitas kasus penggunaan ini: apakah benar-benar perlu menampilkan daftar semua orang dalam satu halaman tanpa konteks? Bagaimana jika ada satu juta orang? Halaman akan memerlukan waktu terlalu lama untuk dikembalikan.

Sifat kasus penggunaan menunjukkan bahwa kita harus menyediakan beberapa konteks untuk mempersempit kueri. Dalam contoh ini, konteks yang akan kita gunakan adalah organisasi. Jika kita melakukannya, maka kita dapat menggunakan organisasi sebagai grup entitas dan mengeksekusi kueri ancestor, yang memecahkan masalah konsistensi. Hal ini ditunjukkan dengan kode Python di bawah ini.

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 dimasukkan. Contoh tersebut dapat diperluas untuk melihat perincian seseorang dengan membuat kueri nama orang tersebut dengan ancestor sebagai bagian dari kueri. Atau, hal ini dapat dilakukan dengan menyimpan kunci entity, kemudian menggunakannya untuk melihat perincian dengan pencarian berdasarkan kunci.

Mempertahankan Konsistensi Antara Memcache dan Datastore

Grup entity juga dapat digunakan sebagai unit untuk menjaga 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 apa pun di seluruh entity group dengan membaca dari satu set metadata, bukan memindai semua entity individual dalam grup.

Keterbatasan Grup Entitas dan Kueri Ancestor

Pendekatan penggunaan grup entity dan kueri ancestor bukanlah solusi aman. Ada dua tantangan dalam praktik yang menyulitkan untuk menerapkan teknik ini secara umum, seperti yang tercantum di bawah ini.

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

Batas Penulisan

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

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

Ketetapan Hubungan Grup Entitas

Tantangan kedua adalah ketetapan hubungan grup entitas. Hubungan grup entity dibentuk secara statis berdasarkan penamaan kunci. Atribut ini tidak dapat diubah setelah membuat entitas. Satu-satunya opsi yang tersedia untuk mengubah hubungan adalah menghapus entity dalam grup entity dan membuatnya kembali. Tantangan ini mencegah kami menggunakan grup entitas untuk menentukan cakupan ad-hoc demi konsistensi atau transaksialitas secara dinamis. Sebaliknya, cakupan konsistensi dan transaksialitas terkait erat dengan grup entitas statis yang ditentukan pada waktu desain.

Misalnya, perhatikan skenario saat Anda ingin menerapkan transfer bank di antara dua rekening bank. Skenario bisnis ini memerlukan konsistensi dan transaksialitas yang kuat. Namun, kedua akun tersebut tidak dapat dikelompokkan ke dalam satu grup entity pada saat-saat terakhir atau didasarkan pada induk global. Grup entity tersebut akan menciptakan bottleneck untuk seluruh sistem yang akan menghambat permintaan transfer bank lainnya dijalankan. Jadi grup {i>entity<i} tidak dapat digunakan dengan cara ini.

Ada cara alternatif untuk menerapkan transfer bank dengan cara yang sangat skalabel dan tersedia. Daripada menempatkan semua akun dalam satu grup entitas, Anda dapat membuat grup entitas untuk setiap akun. Dengan melakukannya, Anda dapat menggunakan transaksi untuk memastikan ACID diperbarui di 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 batasan transaksi, lihat Transaksi dan grup entitas.

Alternatif untuk Kueri Ancestor

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

Kueri Global khusus kunci yang Diikuti oleh Pencarian berdasarkan Kunci

Kueri global khusus kunci adalah jenis kueri global khusus yang hanya menampilkan kunci tanpa nilai properti entity. Karena nilai yang ditampilkan hanya 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 mengakibatkan entitas tidak diambil sama sekali. Hasil kueri dapat berpotensi dihasilkan berdasarkan pemfilteran nilai indeks lama. Singkatnya, developer dapat menggunakan kueri global khusus kunci yang diikuti dengan pencarian berdasarkan kunci hanya jika persyaratan aplikasi memungkinkan nilai indeks yang belum konsisten pada saat kueri.

Menggunakan Memcache

Layanan Memcache tidak stabil, tetapi sangat konsisten. Jadi, dengan menggabungkan pencarian Memcache dan kueri Datastore, Anda dapat membangun sistem yang sering kali akan meminimalkan masalah konsistensi.

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

  • Untuk permintaan penyisipan atau update, 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 saat daftar tersebut tidak ada di Memcache.

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

Ada beberapa praktik terbaik saat menggunakan Memcache sebagai lapisan caching 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 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 yang singkat untuk akhir masa berlaku setiap entri guna meminimalkan kemungkinan inkonsistensi dalam kasus pengecualian Memcache.
  • Gunakan fitur bandingkan dan tetapkan saat memperbarui entri untuk kontrol serentak. Hal ini akan membantu memastikan bahwa pembaruan simultan pada entri yang sama tidak akan mengganggu satu sama lain.

Migrasi Bertahap ke Grup Entitas

Saran yang dibuat di bagian sebelumnya hanya mengurangi kemungkinan perilaku yang tidak konsisten. Desain aplikasi yang baik berdasarkan grup entity dan kueri ancestor jika diperlukan konsistensi kuat. Namun, memigrasikan aplikasi yang sudah ada mungkin tidak memungkinkan, termasuk mengubah model data dan logika aplikasi yang ada dari kueri global ke kueri ancestor. Salah satu cara untuk mencapai hal ini adalah dengan melakukan proses transisi bertahap, seperti berikut:

  1. Identifikasi dan prioritaskan fungsi dalam aplikasi yang memerlukan konsistensi kuat.
  2. Menulis logika baru untuk fungsi insert() atau update() menggunakan grup entity selain (bukan mengganti) logika yang sudah ada. Dengan cara ini, setiap penyisipan atau pembaruan baru pada grup entitas baru dan entitas lama dapat ditangani oleh fungsi yang sesuai.
  3. Ubah logika yang ada untuk fungsi baca atau kueri, dan kueri ancestor akan dijalankan terlebih dahulu jika ada grup entity baru untuk permintaan tersebut. 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 entity, yang meminimalkan risiko masalah yang disebabkan oleh konsistensi tertunda. Dalam praktiknya, pendekatan ini bergantung pada persyaratan dan kasus penggunaan tertentu untuk penerapannya pada sistem aktual.

Penggantian ke Mode Terdegradasi

Saat ini, sulit mendeteksi situasi secara terprogram ketika konsistensi aplikasi menurun. Namun, jika Anda kebetulan menentukan dengan cara lain bahwa konsistensi aplikasi menurun, mungkin aplikasi dapat mengimplementasikan mode terdegradasi yang dapat diaktifkan atau dinonaktifkan untuk menonaktifkan beberapa area logika aplikasi yang memerlukan konsistensi kuat. Misalnya, pesan pemeliharaan untuk layar tertentu tersebut dapat ditampilkan, bukan menampilkan hasil kueri yang tidak konsisten di layar laporan penagihan. Dengan cara ini, layanan lain dalam aplikasi dapat terus ditayangkan, dan kemudian, mengurangi dampaknya terhadap pengalaman pengguna.

Meminimalkan Waktu untuk Mencapai Konsistensi Penuh

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

  • Penomoran berurutan dalam kunci entitas
  • Terlalu banyak indeks

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

AntiPola #1: Penomoran Berurutan 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 entitas apa pun 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 dibahas 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 pada server tertentu, yang dapat menghasilkan 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 tersebar (lihat dokumentasi referensi). Kebijakan default ini menghasilkan urutan ID acak dengan panjang hingga 16 digit yang kira-kira didistribusikan secara seragam. Dengan menggunakan kebijakan ini, traffic aplikasi besar akan didistribusikan dengan lebih baik di antara serangkaian server Datastore, sehingga waktu konsistensi menjadi lebih singkat. Kebijakan default direkomendasikan kecuali aplikasi Anda secara khusus memerlukan kompatibilitas dengan kebijakan lama.

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

Untuk memahami distribusi akses yang tidak merata melalui keyspace, perhatikan contoh tempat entity 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 memusatkan akses pada entity Person yang baru saja dibuat. Dalam hal ini, semua kunci yang sering diakses akan memiliki ID yang lebih tinggi. Beban tersebut kemudian dapat dikonsentrasikan pada server Datastore tertentu.

Atau, untuk memahami distribusi merata melalui keyspace, pertimbangkan untuk menggunakan 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 keyspace dan di beberapa server. Hal ini mengasumsikan bahwa ada jumlah entitas Person yang cukup besar.

Pola Anti #2: Terlalu Banyak Indeks

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

Dalam sebagian besar kasus, indeks kustom ditambahkan untuk mendukung persyaratan seperti tugas dukungan pelanggan, pemecahan masalah, atau analisis data. BigQuery adalah mesin kueri yang skalabel secara masif yang mampu mengeksekusi kueri ad-hoc pada set data besar tanpa indeks bawaan. Layanan ini 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 kebutuhan bisnis yang berbeda. 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 penerapan alternatif untuk indeks kustom, rekomendasi lainnya adalah menentukan properti yang tidak diindeks secara eksplisit (lihat Jenis properti dan nilai). Secara default, Datastore akan membuat tabel indeks yang berbeda untuk setiap properti yang dapat diindeks dari suatu jenis entity. Jika Anda memiliki 100 properti dalam satu jenis, akan ada 100 tabel indeks untuk jenis tersebut, dan 100 pembaruan tambahan pada setiap pembaruan pada suatu 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 konsistensi, pengoptimalan indeks ini dapat menghasilkan penurunan biaya penyimpanan Datastore yang cukup besar dalam aplikasi besar yang banyak menggunakan indeks.

Kesimpulan

Konsistensi tertunda merupakan elemen penting dari database non-relasional yang memungkinkan developer menemukan keseimbangan optimal antara skalabilitas, performa, dan konsistensi. Penting untuk memahami cara menangani keseimbangan antara konsistensi tertunda dan kuat untuk mendesain model data yang optimal bagi aplikasi Anda. Di Datastore, penggunaan grup entity dan kueri ancestor adalah cara terbaik untuk menjamin konsistensi yang kuat atas cakupan entity. Jika aplikasi Anda tidak dapat menggabungkan grup entity karena pembatasan 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 kurangi pengindeksan untuk mengurangi waktu yang diperlukan demi konsistensi. Anda juga perlu menggabungkan Datastore dengan BigQuery guna memenuhi kebutuhan bisnis untuk kueri yang kompleks dan untuk mengurangi penggunaan indeks Datastore sejauh mungkin.

Referensi Tambahan

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




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

[2] Batas yang didukung adalah satu update per detik per entity group di luar transaksi, atau satu transaksi per detik per entity group. Jika menggabungkan beberapa update menjadi satu transaksi, Anda akan memiliki batas ukuran transaksi maksimum sebesar 10 MB dan tingkat operasi tulis maksimum server Datastore.