Praktik terbaik desain skema

Arsitektur Spanner yang terdistribusi memungkinkan Anda mendesain skema untuk menghindari hotspot, yang terjadi saat terlalu banyak permintaan dikirim ke server yang sama dan mengenali resource server dan berpotensi menyebabkan latensi tinggi.

Halaman ini menjelaskan praktik terbaik untuk mendesain skema agar tidak membuat hotspot. Salah satu cara untuk menghindari hotspot adalah dengan menyesuaikan desain skema agar Spanner dapat membagi dan mendistribusikan data di beberapa server. Mendistribusikan data di seluruh server membantu database Spanner beroperasi secara efisien, terutama saat melakukan penyisipan data massal.

Pilih kunci utama untuk mencegah hotspot

Seperti yang disebutkan dalam Model data dan skema, Anda harus berhati-hati saat memilih kunci utama dalam desain skema agar tidak membuat hotspot secara tidak sengaja di database Anda. Salah satu penyebab hotspot adalah memiliki kolom yang nilainya berubah secara monoton sebagai bagian penting pertama, karena hal ini menghasilkan semua penyisipan yang terjadi di akhir ruang kunci Anda. Pola ini tidak diinginkan karena Spanner menggunakan rentang kunci untuk membagi data di antara server, yang berarti semua penyisipan Anda diarahkan ke satu server yang akhirnya melakukan semua tugas.

Misalnya, Anda ingin mempertahankan kolom stempel waktu akses terakhir pada baris tabel UserAccessLog. Definisi tabel berikut menggunakan kunci utama berbasis stempel waktu sebagai bagian kunci pertama. Kami tidak merekomendasikan hal ini jika tabel melihat tingkat penyisipan yang tinggi:

GoogleSQL


CREATE TABLE UserAccessLog (
LastAccess TIMESTAMP NOT NULL,
UserId     INT64 NOT NULL,
...
) PRIMARY KEY (LastAccess, UserId);

PostgreSQL


CREATE TABLE UserAccessLog (
LastAccess TIMESTAMPTZ NOT NULL,
UserId bigint NOT NULL,
...
PRIMARY KEY (LastAccess, UserId)
);

Masalah yang ada di sini adalah baris ditulis ke tabel ini sesuai urutan stempel waktu akses terakhir, dan karena stempel waktu akses terakhir selalu meningkat, baris tersebut selalu ditulis ke akhir tabel. Hotspot dibuat karena satu server Spanner menerima semua penulisan, yang membebani satu server tersebut.

Diagram di bawah mengilustrasikan kesalahan ini:

Tabel UserAccessLog diurutkan berdasarkan stempel waktu dengan hotspot yang sesuai

Tabel UserAccessLog di atas menyertakan lima contoh baris data, yang mewakili lima pengguna berbeda yang melakukan semacam tindakan pengguna sekitar milidetik yang terpisah satu sama lain. Diagram juga menganotasi urutan Spanner menyisipkan baris (panah berlabel menunjukkan urutan penulisan untuk setiap baris). Karena penyisipan diurutkan berdasarkan stempel waktu, dan nilai stempel waktu selalu meningkat, Spanner selalu menambahkan penyisipan ke akhir tabel dan mengarahkannya pada pemisahan yang sama. (Seperti yang dibahas dalam Model data dan skema, pemisahan adalah kumpulan baris dari satu atau beberapa tabel terkait yang disimpan oleh Spanner berdasarkan row key.)

Hal ini bermasalah karena Spanner menetapkan pekerjaan ke server yang berbeda dalam unit pemisahan, sehingga server yang ditetapkan ke bagian tertentu ini akan menangani semua permintaan penyisipan. Seiring dengan meningkatnya frekuensi peristiwa akses pengguna, frekuensi permintaan penyisipan ke server terkait juga akan meningkat. Server kemudian menjadi rentan menjadi hotspot, dan terlihat seperti batas merah dan latar belakang di atas. Perhatikan bahwa dalam ilustrasi yang disederhanakan ini, setiap server menangani maksimal satu bagian, tetapi sebenarnya Spanner dapat menetapkan lebih dari satu bagian ke setiap server.

Saat Spanner menambahkan lebih banyak baris ke tabel, pembagian akan bertambah, dan saat mencapai sekitar 8 GB, Spanner akan membuat pemisahan lain, seperti yang dijelaskan dalam Pemisahan berbasis beban. Spanner akan menambahkan baris baru berikutnya ke bagian baru ini, dan server yang ditetapkan ke pemisahan akan menjadi hotspot potensial baru.

Saat hotspot terjadi, Anda mungkin mengamati bahwa penyisipan Anda berjalan lambat dan pekerjaan lain pada server yang sama mungkin melambat. Mengubah urutan kolom LastAccess ke urutan menaik tidak menyelesaikan masalah ini karena semua operasi tulis disisipkan di bagian atas tabel, yang masih mengirim semua penyisipan ke satu server.

Praktik terbaik desain skema #1: Jangan memilih kolom yang nilainya meningkat atau menurun secara monoton sebagai bagian penting pertama untuk tabel dengan kecepatan tulis yang tinggi.

Menggunakan ID Unik Universal (UUID)

Anda dapat menggunakan ID Unik Universal (UUID) seperti yang ditentukan oleh RFC 4122 sebagai kunci utama. Sebaiknya gunakan UUID Versi 4 karena menggunakan nilai acak dalam urutan bit. Kami tidak merekomendasikan UUID Versi 1 karena UUID Versi 1 menyimpan stempel waktu dalam bit urutan tinggi.

Ada beberapa cara untuk menyimpan UUID sebagai kunci utama:

  • Di kolom STRING(36).
  • Dalam sepasang INT64 kolom.
  • Di kolom BYTES(16).

Untuk kolom STRING(36), Anda dapat menggunakan fungsi GENERATE_UUID() Spanner (GoogleSQL atau PostgreSQL) sebagai nilai default kolom agar Spanner dapat otomatis menghasilkan nilai UUID.

Ada beberapa kelemahan menggunakan UUID:

  • Ukurannya sedikit besar, menggunakan 16 byte atau lebih. Opsi lain untuk kunci utama tidak menggunakan penyimpanan sebesar ini.
  • Koneksi tidak membawa informasi tentang data tersebut. Misalnya, kunci utama SingerId dan AlbumId memiliki arti yang melekat, sedangkan UUID tidak.
  • Anda kehilangan lokalitas di antara record terkait. Itulah sebabnya menggunakan UUID menghilangkan hotspot.

Nilai berurutan bit-reverse

Anda harus memastikan bahwa kunci utama numerik (INT64 di GoogleSQL atau bigint di PostgreSQL) tidak meningkat atau menurun secara berurutan. Kunci utama berurutan dapat menyebabkan hotspotting dalam skala besar. Salah satu cara untuk menghindari masalah ini adalah dengan membalik nilai berurutan secara bit, memastikan untuk mendistribusikan nilai kunci utama secara merata di seluruh ruang kunci.

Spanner mendukung urutan bit-reversed, yang menghasilkan nilai bit-reversed integer yang unik. Anda dapat menggunakan urutan dalam komponen pertama (atau satu-satunya) di kunci utama untuk menghindari masalah hotspot. Untuk informasi selengkapnya, lihat Urutan terbalik bit.

Tukar urutan kunci

Salah satu cara untuk menyebarkan penulisan ke ruang kunci secara lebih seragam adalah dengan menukar urutan kunci sehingga kolom yang berisi nilai monoton bukan menjadi bagian kunci pertama:

GoogleSQL

CREATE TABLE UserAccessLog (
LastAccess TIMESTAMP NOT NULL,
UserId     INT64 NOT NULL,
...
) PRIMARY KEY (UserId, LastAccess);

PostgreSQL

CREATE TABLE UserAccessLog (
LastAccess TIMESTAMPTZ NOT NULL,
UserId bigint NOT NULL,
...
PRIMARY KEY (UserId, LastAccess)
);

Dalam skema yang telah diubah ini, penyisipan kini diurutkan terlebih dahulu berdasarkan UserId, bukan berdasarkan stempel waktu akses terakhir yang kronologis. Skema ini menyebarkan penulisan ke berbagai bagian, karena kecil kemungkinan satu pengguna menghasilkan ribuan peristiwa per detik.

Diagram di bawah mengilustrasikan lima baris dari tabel UserAccessLog yang disusun Spanner dengan UserId, bukan stempel waktu akses:

Tabel UserAccessLog diurutkan berdasarkan UserId dengan throughput operasi tulis yang seimbang

Di sini Spanner membagi data UserAccessLog menjadi tiga bagian, dengan setiap bagian berisi sekitar seribu baris nilai UserId yang diurutkan. Ini adalah perkiraan yang wajar tentang cara pembagian data pengguna, dengan asumsi setiap baris berisi sekitar 1 MB data pengguna dan diberi ukuran pemisahan maksimum sekitar 8 GB. Meskipun peristiwa pengguna terjadi dalam selang waktu sekitar satu milidetik, setiap peristiwa dipicu oleh pengguna yang berbeda, sehingga urutan penyisipan jauh lebih kecil kemungkinannya untuk membuat hotspot dibandingkan dengan menggunakan stempel waktu untuk mengurutkan.

Lihat juga praktik terbaik terkait untuk menyusun kunci berbasis stempel waktu.

Melakukan hashing pada kunci unik dan menyebarkan operasi tulis ke seluruh shard logis

Teknik umum lainnya untuk menyebarkan beban ke beberapa server adalah dengan membuat kolom yang berisi hash dari kunci unik aktual, lalu menggunakan kolom hash tersebut (atau kolom hash dan kolom kunci unik secara bersamaan) sebagai kunci utama. Pola ini membantu menghindari hotspot, karena baris baru tersebar lebih merata di seluruh ruang kunci.

Anda dapat menggunakan nilai hash untuk membuat shard atau partisi logis di database. Dalam database dengan sharding fisik, baris tersebar di beberapa server database. Dalam database yang di-sharding secara logis, data dalam tabel menentukan shard. Misalnya, untuk menyebarkan penulisan ke tabel UserAccessLog di seluruh shard logis N, Anda dapat menambahkan kolom kunci ShardId ke tabel:

GoogleSQL

CREATE TABLE UserAccessLog (
ShardId     INT64 NOT NULL,
LastAccess  TIMESTAMP NOT NULL,
UserId      INT64 NOT NULL,
...
) PRIMARY KEY (ShardId, LastAccess, UserId);

PostgreSQL

CREATE TABLE UserAccessLog (
ShardId bigint NOT NULL,
LastAccess TIMESTAMPTZ NOT NULL,
UserId bigint NOT NULL,
...
PRIMARY KEY (ShardId, LastAccess, UserId)
);

Untuk menghitung ShardId, lakukan hashing pada kombinasi kolom kunci utama, lalu hitung modulo N hash. Contoh:

ShardId = hash(LastAccess and UserId) % N

Pilihan fungsi hash dan kombinasi kolom Anda menentukan penyebaran baris di seluruh ruang kunci. Spanner kemudian akan membuat pemisahan di seluruh baris untuk mengoptimalkan performa.

Diagram di bawah mengilustrasikan bagaimana penggunaan hash untuk membuat tiga shard logis dapat mendistribusikan throughput tulis secara lebih merata di seluruh server:

Tabel UserAccessLog diurutkan berdasarkan ShardId dengan throughput operasi tulis yang seimbang

Di sini, tabel UserAccessLog diurutkan berdasarkan ShardId, yang dihitung sebagai fungsi hash kolom kunci. Lima baris UserAccessLog dibagi menjadi tiga shard logis, yang masing-masing secara kebetulan berada dalam bagian yang berbeda. Penyisipan disebar secara merata di antara bagian, yang menyeimbangkan throughput tulis ke tiga server yang menangani pemisahan.

Spanner juga memungkinkan Anda membuat fungsi hash di kolom yang dihasilkan.

Untuk melakukannya di Google SQL, gunakan fungsi FARM_FINGERPRINT selama waktu tulis, seperti yang ditunjukkan dalam contoh berikut:

GoogleSQL

CREATE TABLE UserAccessLog (
ShardId INT64 NOT NULL
AS (MOD(FARM_FINGERPRINT(CAST(LastAccess AS STRING)), 2048)) STORED,
LastAccess TIMESTAMP NOT NULL,
UserId    INT64 NOT NULL,
) PRIMARY KEY (ShardId, LastAccess, UserId);

Pilihan fungsi hash Anda menentukan seberapa baik penyisipan penyisipan Anda ke seluruh rentang kunci. Anda tidak memerlukan hash kriptografi, meskipun hash kriptografis mungkin merupakan pilihan yang baik. Saat memilih fungsi hash, Anda harus mempertimbangkan faktor-faktor berikut:

  • Menghindari hotspot. Fungsi yang menghasilkan lebih banyak nilai hash cenderung mengurangi hotspot.
  • Efisiensi pembacaan. Pembacaan di semua nilai hash akan lebih cepat jika hanya ada lebih sedikit nilai hash yang dapat dipindai.
  • Jumlah node.

Menggunakan urutan menurun untuk kunci berbasis stempel waktu

Jika Anda memiliki tabel untuk histori yang menggunakan stempel waktu sebagai kunci, pertimbangkan menggunakan urutan menurun untuk kolom kunci jika salah satu hal berikut berlaku:

  • Jika ingin membaca histori terbaru, Anda harus menggunakan tabel sisipan untuk histori dan membaca baris induk. Dalam hal ini, dengan kolom stempel waktu DESC, entri histori terbaru disimpan berdekatan dengan baris induk. Jika tidak, membaca baris induk dan histori terbarunya akan memerlukan pencarian di bagian tengah untuk melewati histori yang lebih lama.
  • Jika Anda membaca entri berurutan dalam urutan kronologis terbalik, dan tidak tahu persis seberapa jauh Anda akan berjalan. Misalnya, Anda mungkin menggunakan kueri SQL dengan LIMIT untuk mendapatkan peristiwa N terbaru, atau Anda mungkin berencana untuk membatalkan pembacaan setelah membaca sejumlah baris tertentu. Dalam hal ini, Anda ingin memulai dengan entri terbaru dan membaca entri lama secara berurutan hingga kondisi terpenuhi, yang dilakukan Spanner dengan lebih efisien untuk kunci stempel waktu yang disimpan Spanner dalam urutan menurun.

Tambahkan kata kunci DESC agar kunci stempel waktu menurun. Contoh:

GoogleSQL

CREATE TABLE UserAccessLog (
UserId     INT64 NOT NULL,
LastAccess TIMESTAMP NOT NULL,
...
) PRIMARY KEY (UserId, LastAccess DESC);

Praktik terbaik desain skema #2: Urutan menurun atau urutan menaik bergantung pada kueri pengguna, misalnya, teratas adalah yang terbaru, atau teratas adalah yang paling lama.

Menggunakan indeks sisipan pada kolom yang nilainya meningkat atau menurun secara monoton

Serupa dengan contoh kunci utama sebelumnya yang harus Anda hindari, sebaiknya buat indeks yang tidak disisipkan pada kolom yang nilainya meningkat atau menurun secara monoton, meskipun indeks tersebut bukan kolom kunci utama.

Misalnya, Anda menentukan tabel berikut, dengan LastAccess sebagai kolom non-kunci utama:

GoogleSQL

CREATE TABLE Users (
UserId     INT64 NOT NULL,
LastAccess TIMESTAMP,
...
) PRIMARY KEY (UserId);

PostgreSQL

CREATE TABLE Users (
UserId     bigint NOT NULL,
LastAccess TIMESTAMPTZ,
...
PRIMARY KEY (UserId)
);

Sepertinya mudah untuk menentukan indeks pada kolom LastAccess guna membuat kueri database dengan cepat untuk akses pengguna "sejak waktu X", seperti ini:

GoogleSQL

CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(LastAccess);

PostgreSQL

CREATE INDEX UsersByLastAccess ON Users(LastAccess)
WHERE LastAccess IS NOT NULL;

Namun, hal ini menyebabkan kesalahan yang sama seperti yang dijelaskan dalam praktik terbaik sebelumnya, karena Spanner mengimplementasikan indeks sebagai tabel di balik layar, dan tabel indeks yang dihasilkan menggunakan kolom yang nilainya meningkat secara monoton sebagai bagian penting pertamanya.

Namun, Anda dapat membuat indeks sisipan seperti ini, karena baris indeks yang disisipkan disisipkan dalam baris induk yang sesuai, dan satu baris induk tidak mungkin menghasilkan ribuan peristiwa per detik.

Praktik terbaik desain skema #3: Jangan membuat indeks yang tidak disisipkan pada kolom kecepatan operasi tulis tinggi yang nilainya meningkat atau menurun secara monoton. Daripada menggunakan indeks sisipan, gunakan teknik seperti yang Anda gunakan untuk desain kunci utama tabel dasar saat mendesain kolom indeks, misalnya, tambahkan `shardId`.

Langkah selanjutnya