Arsitektur terdistribusi Spanner memungkinkan Anda mendesain skema untuk menghindari hotspot - situasi ketika terlalu banyak permintaan dikirim ke server yang sama sehingga membebani resource server dan berpotensi menyebabkan latensi tinggi.
Halaman ini menjelaskan praktik terbaik untuk mendesain skema guna menghindari pembuatan hotspot. Salah satu cara untuk menghindari hotspot adalah menyesuaikan desain skema agar Spanner dapat membagi dan mendistribusikan data ke beberapa server. Mendistribusikan data ke seluruh server membantu database Spanner Anda beroperasi secara efisien, terutama saat melakukan penyisipan data massal.
Spanner otomatis mendeteksi peluang untuk menerapkan praktik terbaik desain skema. Jika rekomendasi tersedia untuk database, Anda dapat melihatnya di halaman Spanner Studio untuk database tersebut. Untuk mengetahui informasi selengkapnya, lihat Melihat rekomendasi praktik terbaik desain skema.
Memilih kunci utama untuk mencegah hotspot
Untuk menghindari pembuatan hotspot di database, dengan cermat pilih kunci utama selama desain skema.
Penyebab umum hotspot adalah penggunaan kunci yang meningkat atau menurun secara monoton, seperti stempel waktu. Kunci monoton menyebabkan semua entri baru ditulis ke rentang ruang kunci yang sama. Karena Spanner menggunakan rentang kunci untuk mendistribusikan data ke seluruh server, kunci monoton mengarahkan semua traffic penyisipan ke satu server, sehingga membuat bottleneck.
Misalnya, Anda ingin mempertahankan kolom stempel waktu akses terakhir pada baris tabel UserAccessLogs. Definisi tabel berikut menggunakan kunci utama berbasis stempel waktu sebagai bagian kunci pertama. Sebaiknya jangan gunakan cara ini jika tabel memiliki tingkat penyisipan yang tinggi:
GoogleSQL
CREATE TABLE UserAccessLogs ( LastAccess TIMESTAMP NOT NULL, UserId STRING(1024), ... ) PRIMARY KEY (LastAccess, UserId);
PostgreSQL
CREATE TABLE useraccesslogs ( lastaccess timestamptz NOT NULL, userid text, ... PRIMARY KEY (lastaccess, userid) );
Masalahnya di sini adalah baris ditulis ke tabel ini dalam urutan stempel waktu akses terakhir, dan karena stempel waktu akses terakhir selalu meningkat, stempel waktu tersebut selalu ditulis ke akhir tabel. Hotspot dibuat karena satu server Spanner menerima semua penulisan, yang membebani satu server tersebut.
Diagram berikut menggambarkan potensi masalah ini:
Tabel UserAccessLogs sebelumnya menyertakan lima contoh baris data, yang mewakili lima pengguna berbeda yang melakukan semacam tindakan pengguna dengan selisih sekitar satu milidetik. Diagram ini juga memberi anotasi urutan penyisipan baris oleh Spanner (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 ke pemisahan yang sama. (Seperti yang
dibahas dalam Skema dan model
data, pemisahan adalah kumpulan
baris dari satu atau beberapa tabel terkait yang disimpan Spanner
dalam urutan kunci baris.)
Hal ini bermasalah karena Spanner menetapkan pekerjaan ke server yang berbeda dalam unit pemisahan, sehingga server yang ditetapkan ke pemisahan tertentu ini akhirnya menangani semua permintaan penyisipan. Seiring meningkatnya frekuensi peristiwa akses pengguna, frekuensi permintaan penyisipan ke server yang sesuai juga meningkat. Server kemudian menjadi rentan menjadi hotspot, dan terlihat seperti batas dan latar belakang merah yang ditampilkan pada gambar sebelumnya. Dalam ilustrasi yang disederhanakan ini, setiap server menangani paling banyak satu pemisahan, tetapi Spanner dapat menetapkan lebih dari satu pemisahan ke setiap server.
Saat Spanner menambahkan lebih banyak baris ke tabel, pemisahan akan bertambah, lalu Spanner membuat pemisahan baru sesuai kebutuhan. Untuk mempelajari lebih lanjut cara pembuatan pemisahan, lihat Pemisahan berbasis beban. Spanner menambahkan baris baru berikutnya ke pemisahan baru ini, dan server yang ditetapkan ke pemisahan tersebut menjadi potensi hotspot baru.
Saat hotspot terjadi, Anda mungkin mengamati bahwa penyisipan Anda lambat dan pekerjaan lain di server yang sama mungkin melambat. Mengubah urutan kolom LastAccess menjadi urutan menaik tidak menyelesaikan masalah ini karena semua penulisan akan disisipkan di bagian atas tabel, yang masih mengirim semua penyisipan ke satu server.
Praktik terbaik desain skema #1: Jangan pilih kolom yang nilainya meningkat atau menurun secara monoton sebagai bagian kunci pertama untuk tabel kecepatan penulisan yang tinggi.
Menggunakan ID unik universal (UUID)
Anda dapat menggunakan ID unik universal (UUID) seperti yang ditentukan oleh
RFC 9562 sebagai
kunci utama. Sebaiknya gunakan
UUID Versi 4
, karena menggunakan nilai acak dalam urutan bit. Sebaiknya jangan gunakan UUID Versi 1 karena menyimpan stempel waktu dalam bit urutan tinggi. Anda dapat menyimpan nilai UUID Versi 4 di kolom UUID di Spanner.
Pertimbangkan hal berikut sebelum memutuskan untuk menggunakan UUID:
- UUID berfungsi secara independen dari konten rekaman. Tidak seperti kunci semantik seperti
SingerIddanAlbumId, UUID adalah ID unik yang tidak terkait dengan data itu sendiri. - UUID tidak mempertahankan lokalitas antara rekaman terkait, itulah sebabnya penggunaan UUID menghilangkan hotspot.
Untuk kolom UUID, Anda dapat menggunakan fungsi GoogleSQL
NEW_UUID()
Spanner atau fungsi PostgreSQL
gen_random_uuid() untuk membuat nilai UUID.
Misalnya, untuk tabel berikut:
GoogleSQL
CREATE TABLE UserAccessLogs (
LogEntryId UUID DEFAULT (NEW_UUID()),
LastAccess TIMESTAMP NOT NULL,
UserId STRING(1024)
) PRIMARY KEY (LogEntryId);
PostgreSQL
CREATE TABLE useraccesslogs (
logentryid uuid PRIMARY KEY DEFAULT gen_random_uuid(),
lastaccess timestamptz NOT NULL,
userid text);
Anda dapat menggunakan fungsi UUID yang dihasilkan untuk membuat nilai LogEntryId baru.
GoogleSQL
INSERT INTO UserAccessLogs (LastAccess, UserId)
VALUES ('2016-01-25 10:10:10.555555-05:00', 'TomSmith');
PostgreSQL
INSERT INTO UserAccessLogs (LastAccess, UserId)
VALUES ('2016-01-25 10:10:10.555555-05:00', 'TomSmith');
Untuk kolom UUID, Anda dapat menggunakan fungsi GoogleSQL
NEW_UUID()
Spanner atau fungsi PostgreSQL
gen_random_uuid()
sebagai nilai default kolom sehingga
Spanner otomatis menghasilkan nilai UUID.
Misalnya, untuk tabel berikut:
GoogleSQL
CREATE TABLE UserAccessLogs (
LogEntryId UUID NOT NULL,
LastAccess TIMESTAMP NOT NULL,
UserId STRING(1024),
...
) PRIMARY KEY (LogEntryId);
PostgreSQL
CREATE TABLE useraccesslogs (
logentryid uuid PRIMARY KEY NOT NULL,
lastaccess timestamptz NOT NULL,
userid text);
Anda dapat menyisipkan NEW_UUID() GoogleSQL atau gen_random_uuid() PostgreSQL untuk menghasilkan nilai LogEntryId.
Fungsi ini menghasilkan nilai UUID, sehingga kolom LogEntryId harus menggunakan jenis UUID untuk GoogleSQL atau PostgreSQL.
GoogleSQL
INSERT INTO
UserAccessLogs (LogEntryId, LastAccess, UserId)
VALUES
(NEW_UUID(), '2016-01-25 10:10:10.555555-05:00', 'TomSmith');
PostgreSQL
INSERT INTO
useraccesslogs (logentryid, lastaccess, userid)
VALUES
(gen_random_uuid(),'2016-01-25 10:10:10.555555-05:00', 'TomSmith');
Anda juga dapat menyisipkan nilai UUID yang telah Anda buat di tempat lain, seperti aplikasi backend. Hal ini karena UUID bersifat unik, terlepas dari tempat pembuatannya.
GoogleSQL
INSERT INTO
UserAccessLogs (LogEntryId, LastAccess, UserId)
VALUES
('4192bff0-e1e0-43ce-a4db-912808c32493', '2016-01-25 10:10:10.555555-05:00', 'TomSmith');
PostgreSQL
INSERT INTO
useraccesslogs (logentryid, lastaccess, userid)
VALUES
('4192bff0-e1e0-43ce-a4db-912808c32493','2016-01-25 10:10:10.555555-05:00', 'TomSmith');
Lakukan bit-reverse pada nilai yang berurutan
Anda harus memverifikasi bahwa kunci utama numerik (INT64 di GoogleSQL atau bigint di PostgreSQL) tidak meningkat atau menurun secara berurutan. Kunci utama berurutan dapat menyebabkan hotspot dalam skala besar. Salah satu cara untuk menghindari masalah ini adalah dengan melakukan bit-reverse pada nilai berurutan, dan memastikan untuk mendistribusikan nilai kunci utama secara merata di seluruh ruang kunci.
Spanner mendukung urutan bit-terbalik, yang menghasilkan nilai bit-terbalik bilangan bulat unik. Anda dapat menggunakan urutan di komponen pertama (atau satu-satunya) dalam kunci utama untuk menghindari masalah hotspot. Untuk mengetahui informasi selengkapnya, lihat Urutan bit-terbalik.
Menukar urutan kunci
Salah satu cara untuk menyebarkan penulisan di ruang kunci secara lebih seragam adalah dengan menukar urutan kunci sehingga kolom yang berisi nilai monoton bukan merupakan bagian kunci pertama:
GoogleSQL
CREATE TABLE UserAccessLogs ( UserId INT64 NOT NULL, LastAccess TIMESTAMP NOT NULL, ... ) PRIMARY KEY (UserId, LastAccess);
PostgreSQL
CREATE TABLE useraccesslogs ( userid bigint NOT NULL, lastaccess TIMESTAMPTZ NOT NULL, ... PRIMARY KEY (UserId, LastAccess) );
Dalam skema yang diubah ini, penyisipan kini pertama-tama diurutkan berdasarkan UserId, bukan berdasarkan stempel waktu akses terakhir kronologis. Skema ini menyebarkan penulisan di antara pemisahan yang berbeda karena kemungkinan satu pengguna menghasilkan ribuan peristiwa per detik sangat kecil.
Gambar berikut menunjukkan lima baris dari tabel UserAccessLogs yang diurutkan Spanner dengan UserId, bukan stempel waktu akses:

Di sini, Spanner dapat membagi data UserAccessLogs menjadi tiga pemisahan, dengan setiap pemisahan berisi sekitar seribu baris nilai UserId yang diurutkan. Meskipun peristiwa pengguna terjadi dengan selisih sekitar satu milidetik, setiap peristiwa dimunculkan oleh pengguna yang berbeda, sehingga urutan penyisipan jauh lebih kecil kemungkinannya untuk membuat hotspot dibandingkan dengan menggunakan stempel waktu untuk pengurutan. Untuk
mempelajari lebih lanjut cara pembuatan pemisahan, lihat
Pemisahan berbasis beban
Lihat juga praktik terbaik terkait untuk mengurutkan kunci berbasis stempel waktu.
Melakukan hash pada kunci unik dan menyebarkan penulisan di seluruh shard logis
Teknik umum lainnya untuk menyebarkan beban di beberapa server adalah membuat kolom yang berisi hash kunci unik sebenarnya, lalu menggunakan kolom hash (atau kolom hash dan kolom kunci unik secara bersamaan) sebagai kunci utama. Pola ini membantu menghindari hotspot, karena baris baru disebarkan secara lebih merata di seluruh ruang kunci.
Anda dapat menggunakan nilai hash untuk membuat shard logis, atau partisi, di database. Dalam database yang di-shard secara fisik, baris disebarkan di beberapa server database. Dalam database yang di-shard secara logis, data dalam tabel menentukan shard. Misalnya, untuk menyebarkan penulisan ke tabel UserAccessLogs di seluruh N shard logis, Anda dapat menambahkan kolom kunci ShardId ke tabel:
GoogleSQL
CREATE TABLE UserAccessLogs ( ShardId INT64 NOT NULL, LastAccess TIMESTAMP NOT NULL, UserId INT64 NOT NULL, ... ) PRIMARY KEY (ShardId, LastAccess, UserId);
PostgreSQL
CREATE TABLE useraccesslogs ( shardid bigint NOT NULL, lastaccess TIMESTAMPTZ NOT NULL, userid bigint NOT NULL, ... PRIMARY KEY (shardid, lastaccess, userid) );
Untuk menghitung ShardId, lakukan hash pada kombinasi kolom kunci utama, lalu hitung modulo N dari hash. Contoh:
GoogleSQL
ShardId = hash(LastAccess and UserId) % N
Pilihan fungsi hash dan kombinasi kolom Anda menentukan cara baris disebarkan di seluruh ruang kunci. Spanner kemudian akan membuat pemisahan di seluruh baris untuk mengoptimalkan performa.
Diagram berikut menggambarkan cara penggunaan hash untuk membuat tiga shard logis dapat menyebarkan throughput penulisan secara lebih merata di seluruh server:

Di sini, tabel UserAccessLogs diurutkan berdasarkan ShardId, yang dihitung sebagai fungsi hash kolom kunci. Lima baris UserAccessLogs dikelompokkan menjadi tiga shard logis, yang masing-masing secara kebetulan berada dalam pemisahan yang berbeda. Penyisipan disebarkan secara merata di antara pemisahan, yang menyeimbangkan throughput penulisan ke tiga server yang menangani pemisahan.
Spanner juga memungkinkan Anda membuat fungsi hash di kolom yang dihasilkan.
Untuk melakukannya di GoogleSQL, gunakan fungsi FARM_FINGERPRINT selama waktu penulisan, seperti yang ditunjukkan dalam contoh berikut:
GoogleSQL
CREATE TABLE UserAccessLogs (
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 Anda disebarkan di seluruh rentang kunci. Anda tidak memerlukan hash kriptografi, meskipun hash kriptografi mungkin merupakan pilihan yang baik. Saat memilih fungsi hash, Anda harus mempertimbangkan faktor-faktor berikut:
- Penghindaran hotspot. Fungsi yang menghasilkan lebih banyak nilai hash cenderung mengurangi hotspot.
- Efisiensi baca. Pembacaan di semua nilai hash lebih cepat jika ada lebih sedikit nilai hash yang akan dipindai.
- Jumlah node.
Saat menggunakan ShardId untuk mencegah hotspot, gunakan panduan berikut untuk memilih nilai N, jumlah shard logis:
Korelasikan N dengan jumlah node: tetapkan N agar sama dengan jumlah node yang Anda harapkan dimiliki instance Anda. Misalnya, jika Anda mengharapkan instance Anda dapat di-scale up hingga 10 node, nilai N=10 adalah titik awal yang efektif. Hal ini membantu Spanner mendistribusikan beban penulisan secara merata di seluruh node.
N adalah nilai statis: mengubah N setelah penyiapan awal memerlukan pembaruan skema dan berpotensi memerlukan pengisian ulang data. Oleh karena itu, Anda harus memilih nilai untuk N yang dapat mengakomodasi kebutuhan penskalaan Anda.
Hindari nilai N yang terlalu besar: meskipun Anda mungkin tergoda untuk memilih nilai N yang sangat besar untuk mempersiapkan pertumbuhan, hal ini umumnya tidak diperlukan. Shard yang lebih banyak daripada server fisik tidak akan meningkatkan performa secara signifikan dibandingkan dengan biaya Spanner tambahan. Menyelaraskan N dengan jumlah node adalah strategi yang efektif untuk mendistribusikan workload.
Gunakan urutan menurun untuk kunci berbasis stempel waktu
Jika Anda memiliki tabel untuk histori yang menggunakan stempel waktu sebagai kunci, pertimbangkan untuk menggunakan urutan menurun untuk kolom kunci jika salah satu hal berikut berlaku:
- Jika Anda ingin membaca histori terbaru, Anda menggunakan tabel bersisipan
untuk histori, dan Anda 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 tengah untuk melewati histori yang lebih lama. - Jika Anda membaca entri berurutan dalam urutan kronologis terbalik, dan
Anda tidak tahu persis seberapa jauh Anda akan kembali. Misalnya, Anda dapat menggunakan kueri SQL dengan
LIMITuntuk mendapatkan N peristiwa terbaru, atau Anda dapat berencana untuk membatalkan pembacaan setelah membaca sejumlah baris tertentu. Dalam kasus ini, Anda ingin memulai dengan entri terbaru dan membaca entri yang lebih lama secara berurutan hingga kondisi Anda terpenuhi, yang dilakukan Spanner secara lebih efisien untuk kunci stempel waktu yang disimpan Spanner dalam urutan menurun.
Tambahkan kata kunci DESC untuk membuat kunci stempel waktu menurun. Contoh:
GoogleSQL
CREATE TABLE UserAccessLogs ( 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, yang teratas adalah yang terbaru, atau yang teratas adalah yang terlama.
Kapan harus menggunakan indeks bersisipan
Untuk panduan memilih antara indeks bersisipan dan global, lihat Memilih antara indeks bersisipan dan global.
Mirip dengan contoh kunci utama sebelumnya yang harus Anda hindari, sebaiknya jangan membuat indeks non-bersisipan pada kolom yang nilainya meningkat atau menurun secara monoton, meskipun kolom tersebut bukan kolom kunci utama.
Misalnya, Anda menentukan tabel berikut, yang kolom LastAccess adalah 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) );
Mungkin akan lebih mudah untuk menentukan indeks pada kolom LastAccess untuk mengkueri 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 akan menghasilkan potensi masalah yang sama seperti yang dijelaskan dalam praktik terbaik sebelumnya, karena Spanner menerapkan indeks sebagai tabel di balik layar, dan tabel indeks yang dihasilkan menggunakan kolom yang nilainya meningkat secara monoton sebagai bagian kunci pertamanya.
Anda dapat membuat indeks bersisipan tempat baris akses terakhir disisipkan di bawah baris pengguna yang sesuai. Hal ini karena kemungkinan satu baris induk menghasilkan ribuan peristiwa per detik sangat kecil.
GoogleSQL
CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(UserId, LastAccess), INTERLEAVE IN Users;
PostgreSQL
CREATE INDEX usersbylastaccess ON users(userid, lastaccess) WHERE lastaccess IS NOT NULL, INTERLEAVE IN Users;
Praktik terbaik desain skema #3: Jangan buat indeks non-bersisipan pada kolom dengan kecepatan penulisan tinggi yang nilainya meningkat atau menurun secara monoton. Gunakan indeks bersisipan, atau gunakan teknik seperti yang akan Anda gunakan untuk desain kunci utama tabel dasar saat mendesain kolom indeks—misalnya, tambahkan `shardId`.
Langkah berikutnya
- Lihat contoh desain skema.
- Pelajari cara memuat data secara massal.