Mengoptimalkan Desain Skema untuk Spanner

Teknologi penyimpanan Google mendukung beberapa aplikasi terbesar di dunia. Namun, penskalaan tidak selalu merupakan hasil otomatis dari penggunaan sistem ini. Desainer harus memikirkan dengan cermat cara membuat model data mereka untuk memastikan bahwa aplikasi mereka dapat diskalakan dan berperforma seiring dengan pertumbuhannya di berbagai dimensi.

Spanner adalah database terdistribusi, dan penggunaannya secara efektif mengharuskan pemikiran yang berbeda tentang desain skema dan pola akses dibandingkan dengan database tradisional. Pada dasarnya, sistem terdistribusi memaksa desainer untuk memikirkan data dan lokalitas pemrosesan.

Spanner mendukung kueri dan transaksi SQL dengan kemampuan untuk menyebarkan skala secara horizontal. Desain yang cermat sering kali diperlukan untuk mewujudkan manfaat penuh Spanner. Laporan ini membahas beberapa ide utama yang akan membantu Anda memastikan bahwa aplikasi dapat diskalakan ke berbagai level, dan untuk memaksimalkan performanya. Ada dua alat yang memiliki dampak besar pada skalabilitas: definisi kunci dan interleaving.

Tata letak tabel

Baris dalam tabel Spanner diatur secara leksikografis menurut PRIMARY KEY. Secara konseptual, kunci diurutkan berdasarkan penyambungan kolom sesuai urutan dideklarasikan dalam klausa PRIMARY KEY. Ini menunjukkan semua properti standar lokalitas:

  • Memindai tabel dalam urutan leksikografis merupakan langkah yang efisien.
  • Baris yang memadai akan disimpan di blok disk yang sama, dan akan dibaca serta di-cache bersama.

Spanner mereplikasi data Anda di beberapa zona untuk ketersediaan dan skala, dengan setiap zona menyimpan replika lengkap data Anda. Saat menyediakan node instance Spanner, Anda akan mendapatkan resource komputasi sejumlah tersebut di setiap zona ini. Meskipun setiap replika adalah kumpulan data lengkap, data dalam replika dipartisi di seluruh resource komputasi di zona tersebut.

Data dalam setiap replika Spanner diatur menjadi dua level hierarki fisik: database split, lalu block. Pemisahan menyimpan rentang baris yang berdekatan, dan merupakan unit yang digunakan Spanner untuk mendistribusikan database Anda di seluruh resource komputasi. Seiring waktu, bagian dapat dipecah menjadi bagian yang lebih kecil, digabungkan, atau dipindahkan ke node lain di instance Anda untuk meningkatkan paralelisme dan memungkinkan aplikasi Anda untuk diskalakan. Operasi dengan span split lebih mahal daripada operasi setara yang tidak, karena meningkatnya komunikasi. Hal ini berlaku meskipun pemisahan tersebut ditayangkan oleh node yang sama.

Ada dua jenis tabel di Spanner: tabel root (terkadang disebut tabel tingkat atas), dan tabel yang disisipkan. Tabel yang disisipkan didefinisikan dengan menentukan tabel lain sebagai parent, yang menyebabkan baris dalam tabel yang di-sisipkan akan dikelompokkan dengan baris induk. Tabel root tidak memiliki induk, dan setiap baris dalam tabel root menentukan baris tingkat atas, atau baris root baru. Baris yang disisipi dengan baris root ini disebut baris turunan, dan kumpulan baris root beserta semua turunannya disebut hierarki baris. Baris induk harus ada sebelum Anda dapat menyisipkan baris turunan. Baris induk bisa saja sudah ada dalam database atau dapat disisipkan sebelum penyisipan baris turunan dalam transaksi yang sama.

Spanner akan otomatis membagi partisi jika dianggap perlu karena ukuran atau beban. Untuk mempertahankan lokalitas data, Spanner lebih memilih menambahkan batas terpisah yang sedekat dengan tabel root, sehingga setiap hierarki baris tertentu dapat disimpan dalam satu bagian. Artinya, operasi dalam pohon baris cenderung lebih efisien karena cenderung tidak memerlukan komunikasi dengan pemisahan lain.

Namun, jika ada hotspot di baris turunan, Spanner akan mencoba menambahkan batas terpisah ke tabel yang disisipkan untuk mengisolasi baris hotspot tersebut, beserta semua baris turunan di bawahnya.

Memilih tabel mana yang harus menjadi root adalah keputusan penting dalam mendesain aplikasi Anda untuk diskalakan. Root biasanya mencakup hal-hal seperti Pengguna, Akun, Project, dan sejenisnya, dan tabel turunannya menyimpan sebagian besar data lain tentang entity yang dimaksud.

Rekomendasi:

  • Gunakan awalan kunci umum untuk baris terkait dalam tabel yang sama untuk meningkatkan lokalitas.
  • Menyisipkan data terkait ke tabel lain kapan pun diperlukan.

Kompromi lokalitas

Jika data sering ditulis atau dibaca bersama, data dapat memanfaatkan latensi dan throughput untuk mengelompokkannya dengan memilih kunci utama secara hati-hati dan menggunakan interleaving. Ini karena ada biaya tetap untuk berkomunikasi dengan server atau blok disk apa pun, jadi mengapa tidak mendapatkan hasil sebanyak mungkin selama di sana? Selain itu, semakin banyak server yang berkomunikasi dengan Anda, semakin tinggi kemungkinan Anda mendapati server yang sibuk sementara, sehingga meningkatkan latensi tail. Terakhir, transaksi dengan span split, meskipun otomatis dan transparan di Spanner, memiliki biaya CPU dan latensi yang sedikit lebih tinggi karena sifat commit dua fase yang terdistribusi.

Di sisi lain, jika data terkait tetapi tidak sering diakses bersama, pertimbangkan untuk keluar dari cara Anda untuk memisahkannya. Cara ini paling diuntungkan jika data yang jarang diakses berukuran besar. Misalnya, banyak database menyimpan data biner besar secara out-of-band dari data baris utama, dengan hanya referensi ke data besar yang disisipkan.

Perlu diperhatikan bahwa beberapa level operasi commit dua fase dan operasi data non-lokal tidak dapat dihindari dalam database terdistribusi. Jangan terlalu khawatir untuk mendapatkan cerita lokalitas yang sempurna untuk setiap operasi. Berfokuslah untuk mendapatkan lokalitas yang diinginkan untuk entity root terpenting dan pola akses paling umum, serta biarkan operasi terdistribusi yang lebih jarang atau kurang sensitif performa terjadi saat diperlukan. Pembacaan terdistribusi dan commit dua fase digunakan untuk membantu menyederhanakan skema dan memudahkan pekerjaan programmer: dalam semua kasus penggunaan yang paling penting bagi performa, lebih baik membiarkan keduanya.

Rekomendasi:

  • Atur data Anda ke dalam hierarki sehingga data yang dibaca atau ditulis bersama cenderung berada di dekat Anda.
  • Sebaiknya simpan kolom besar dalam tabel yang tidak disisipkan jika lebih jarang diakses.

Opsi indeks

Indeks sekunder memungkinkan Anda menemukan baris berdasarkan nilai selain kunci utama dengan cepat. Spanner mendukung indeks non-interleftd dan intersticking. Indeks non-berselang-seling adalah indeks default dan jenis yang paling terkait dengan apa yang didukung dalam RDBMS tradisional. Eksperimen tidak menempatkan batasan apa pun atas kolom yang diindeks, dan meskipun kuat, opsi ini tidak selalu menjadi pilihan terbaik. Indeks sisipan harus ditentukan di atas kolom yang memiliki awalan yang sama dengan tabel induk, dan memungkinkan kontrol lokalitas yang lebih besar.

Spanner menyimpan data indeks dengan cara yang sama seperti tabel, dengan satu baris per entri indeks. Banyak pertimbangan desain untuk tabel juga berlaku pada indeks. Indeks yang tidak disisipkan menyimpan data dalam tabel root. Karena tabel root dapat dibagi di antara baris root mana pun, hal ini memastikan bahwa indeks yang tidak disisipkan dapat diskalakan ke ukuran arbitrer dan, mengabaikan hot spot, ke hampir semua beban kerja. Sayangnya, hal ini juga berarti bahwa entri indeks biasanya tidak berada dalam bagian yang sama dengan data utama. Hal ini menimbulkan pekerjaan tambahan dan latensi untuk setiap proses penulisan, dan menambahkan pemisahan tambahan untuk berkonsultasi pada waktu baca.

Sebaliknya, indeks yang berselang-seling menyimpan data dalam tabel yang disisipkan. Setelan ini cocok saat Anda melakukan penelusuran dalam domain entitas tunggal. Indeks yang bertautan memaksa data dan entri indeks untuk tetap berada di hierarki baris yang sama, sehingga penggabungan di antara keduanya jauh lebih efisien. Contoh penggunaan indeks berselang-seling:

  • Mengakses foto Anda berdasarkan berbagai urutan seperti tanggal pengambilan, tanggal terakhir diubah, judul, album, dll.
  • Menemukan semua postingan Anda yang memiliki kumpulan tag tertentu.
  • Menemukan pesanan belanja saya sebelumnya yang berisi item tertentu.

Rekomendasi:

  • Gunakan indeks yang tidak berselang-seling jika Anda perlu menemukan baris dari mana saja dalam database.
  • Pilih indeks sisipan setiap kali penelusuran Anda dicakupkan ke satu entity.

Klausa indeks STORING

Indeks sekunder memungkinkan Anda menemukan baris berdasarkan atribut selain kunci utama. Jika semua data yang diminta ada dalam indeks itu sendiri, data tersebut dapat dilihat sendiri tanpa membaca kumpulan data utama. Tindakan ini dapat menghemat resource secara signifikan karena tidak perlu penggabungan.

Sayangnya, jumlah kunci indeks dibatasi hingga 16 dan 8 KiB dalam ukuran agregat, sehingga membatasi apa yang dapat dimasukkan ke dalamnya. Untuk mengompensasi batasan ini, Spanner memiliki kemampuan untuk menyimpan data tambahan dalam indeks apa pun, melalui klausa STORING. STORING kolom dalam indeks menyebabkan nilainya diduplikasi, dengan salinan yang disimpan dalam indeks. Anda dapat menganggap indeks dengan STORING sebagai tampilan terwujud tabel tunggal sederhana (saat ini tampilan tidak didukung secara native di Spanner).

Penerapan STORING yang berguna adalah sebagai bagian dari indeks NULL_FILTERED. Hal ini memungkinkan Anda menentukan tampilan terwujud dari subset tabel sparse yang dapat Anda pindai secara efisien. Misalnya, Anda dapat membuat indeks seperti itu pada kolom is_unread kotak surat agar dapat menayangkan tampilan pesan yang belum dibaca dalam pemindaian tabel tunggal, tetapi tanpa membayar salinan lengkap setiap kotak surat.

Rekomendasi:

  • Gunakan STORING dengan hati-hati untuk mengorbankan performa waktu baca dengan ukuran penyimpanan dan performa waktu tulis.
  • Gunakan NULL_FILTERED untuk mengontrol biaya penyimpanan indeks sparse.

Antipola

Antipola: pengurutan stempel waktu

Banyak desainer skema cenderung menentukan tabel root yang diurutkan berdasarkan stempel waktu, dan diperbarui pada setiap penulisan. Sayangnya, ini adalah salah satu hal yang paling tidak skalabel yang dapat Anda lakukan. Alasannya adalah desain ini menghasilkan hot spot besar pada akhir tabel yang tidak dapat dimitigasi dengan mudah. Saat kecepatan tulis meningkat, begitu pula RPC ke satu bagian, begitu juga peristiwa pertentangan kunci dan masalah lainnya. Sering kali masalah semacam ini tidak muncul dalam pengujian beban kecil, dan muncul setelah aplikasi berada dalam produksi selama beberapa waktu. Saat itu, sudah terlambat!

Jika aplikasi Anda benar-benar harus menyertakan log yang diurutkan berdasarkan stempel waktu, pertimbangkan apakah Anda dapat membuat log tersebut menjadi lokal dengan menyisipkannya ke salah satu tabel root lainnya. Hal ini bermanfaat karena mendistribusikan {i>hot spot<i} ke banyak akar. Namun, Anda tetap harus berhati-hati karena setiap root yang berbeda memiliki kecepatan tulis yang cukup rendah.

Jika Anda memerlukan tabel yang diurutkan berdasarkan stempel waktu global (silang root), dan perlu mendukung kecepatan operasi tulis yang lebih tinggi daripada kemampuan satu node, gunakan sharding level aplikasi. Sharding tabel berarti mempartisi tabel menjadi beberapa jumlah N dari pembagian yang kurang lebih sama yang disebut shard. Tindakan ini biasanya dilakukan dengan memberi awalan pada kunci utama asli dengan kolom ShardId tambahan yang berisi nilai bilangan bulat di antara [0, N). ShardId untuk penulisan tertentu biasanya dipilih secara acak, atau dengan melakukan hashing pada sebagian kunci dasar. Hashing sering kali lebih disukai karena dapat digunakan untuk memastikan semua catatan dari jenis tertentu masuk ke shard yang sama, sehingga meningkatkan performa pengambilan. Apa pun itu, tujuannya adalah untuk memastikan bahwa seiring waktu, operasi tulis didistribusikan ke semua shard secara merata. Pendekatan ini terkadang berarti operasi baca harus memindai semua shard untuk merekonstruksi total urutan penulisan asli.

Ilustrasi shard untuk paralelisme dan baris dalam urutan waktu per shard

Rekomendasi:

  • Hindari tabel dan indeks yang diurutkan dengan stempel waktu kecepatan tulis tinggi dengan biaya apa pun.
  • Gunakan beberapa teknik untuk menyebarkan hot spot, baik dengan menyisipkan di tabel lain atau sharding.

Antipola: urutan

Developer aplikasi suka menggunakan urutan database (atau penambahan otomatis) untuk menghasilkan kunci utama. Sayangnya, kebiasaan dari hari-hari RDBMS ini (yang disebut kunci surrogate) hampir sama berbahayanya dengan anti-pola pengurutan stempel waktu yang dijelaskan di atas. Alasannya adalah urutan database cenderung memunculkan nilai dengan cara kuasi-monoton, dari waktu ke waktu, hingga menghasilkan nilai yang dikelompokkan berdekatan satu sama lain. Hal ini biasanya menghasilkan hot spot saat digunakan sebagai kunci utama, terutama untuk baris root.

Berlawanan dengan kebijakan konvensional RDBMS, sebaiknya gunakan atribut dunia nyata untuk kunci utama jika diperlukan. Hal ini terutama terjadi jika atribut tidak akan pernah berubah.

Jika Anda ingin membuat kunci utama unik numerik, usahakan agar bit urutan tinggi dari angka berikutnya didistribusikan kira-kira secara merata di seluruh ruang angka. Salah satu triknya adalah menghasilkan angka urut dengan cara konvensional, lalu membalik bit untuk mendapatkan nilai akhir. Atau, Anda dapat mencari generator UUID, tetapi berhati-hatilah: tidak semua fungsi UUID dibuat secara setara, dan beberapa menyimpan stempel waktu dalam bit urutan tinggi, yang secara efektif menghilangkan manfaatnya. Pastikan generator UUID Anda secara acak memilih bit urutan tinggi.

Rekomendasi:

  • Hindari penggunaan nilai urutan yang bertambah sebagai kunci utama. Sebagai gantinya, balikkan nilai urutan, atau gunakan UUID yang dipilih dengan cermat.
  • Gunakan nilai dunia nyata untuk kunci utama, bukan kunci surrogate.