Arsitektur terdistribusi Spanner memungkinkan Anda mendesain skema untuk menghindari hotspot - situasi saat terlalu banyak permintaan dikirim ke server yang sama yang memenuhi resource server dan berpotensi menyebabkan latensi yang 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 Anda beroperasi secara efisien, terutama saat melakukan penyisipan data massal.
Memilih kunci utama untuk mencegah hotspot
Seperti yang disebutkan dalam Skema dan model data, Anda harus berhati-hati saat memilih kunci utama dalam desain skema agar tidak secara tidak sengaja membuat hotspot di database. Salah satu penyebab hotspot adalah memiliki kolom yang nilainya berubah secara monoton sebagai bagian kunci pertama, karena hal ini menyebabkan semua penyisipan 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 pekerjaan.
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. Sebaiknya jangan lakukan 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 useraccesslog ( lastaccess timestamptz NOT NULL, userid text, ... PRIMARY KEY (lastaccess, userid) );
Masalahnya di sini adalah baris ditulis ke tabel ini berdasarkan urutan stempel waktu akses terakhir, dan karena stempel waktu akses terakhir selalu bertambah, baris tersebut selalu ditulis ke akhir tabel. Hotspot dibuat karena satu server Spanner menerima semua operasi tulis, yang membebani satu server tersebut.
Diagram berikut mengilustrasikan masalah ini:
Tabel UserAccessLog
sebelumnya menyertakan lima contoh baris data, yang
mewakili lima pengguna berbeda yang melakukan tindakan pengguna dengan selisih waktu
milidetik satu sama lain. Diagram ini juga menganotasi 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 bagian 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 tugas 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 sederhana ini, setiap server menangani maksimal satu bagian, tetapi Spanner dapat menetapkan lebih dari satu bagian ke setiap server.
Saat Spanner menambahkan lebih banyak baris ke tabel, bagian akan bertambah, dan saat mencapai sekitar 8 GB, Spanner akan membuat bagian lain, seperti yang dijelaskan dalam Pembagian berbasis beban. Spanner menambahkan baris baru berikutnya ke bagian baru ini, dan server yang ditetapkan ke bagian tersebut menjadi hotspot potensial baru.
Saat hotspot terjadi, Anda mungkin mengamati bahwa penyisipan lambat dan pekerjaan lain
di server yang sama mungkin melambat. Mengubah urutan kolom LastAccess
menjadi urutan menaik tidak akan menyelesaikan masalah ini karena semua operasi tulis
akan 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 kunci pertama untuk tabel kecepatan penulisan yang tinggi.
Menggunakan ID Unik Universal (UUID)
Anda dapat menggunakan ID Unik Universal (UUID) sebagaimana didefinisikan oleh RFC 4122 sebagai kunci utama. Sebaiknya gunakan UUID Versi 4, karena menggunakan nilai acak dalam urutan bit. Sebaiknya jangan gunakan UUID Versi 1 karena UUID tersebut menyimpan stempel waktu dalam bit urutan tinggi.
Ada beberapa cara untuk menyimpan UUID sebagai kunci utama:
- Di kolom
STRING(36)
. - Dalam sepasang kolom
INT64
. - Di kolom
BYTES(16)
.
Untuk kolom STRING(36)
, Anda dapat menggunakan fungsi GENERATE_UUID()
Spanner (GoogleSQL atau
PostgreSQL) sebagai nilai default kolom agar
Spanner otomatis menghasilkan nilai UUID.
Misalnya, untuk tabel berikut:
GoogleSQL
CREATE TABLE UserAccessLogs (
LogEntryId STRING(36) NOT NULL,
LastAccess TIMESTAMP NOT NULL,
UserId STRING(1024),
...
) PRIMARY KEY (LogEntryId, LastAccess, UserId);
PostgreSQL
CREATE TABLE useraccesslog (
logentryid VARCHAR(36) NOT NULL,
lastaccess timestamptz NOT NULL,
userid text,
...
PRIMARY KEY (lastaccess, userid)
);
Anda dapat menyisipkan GENERATE_UUID()
untuk menghasilkan nilai LogEntryId
.
GENERATE_UUID()
menghasilkan nilai STRING
, sehingga kolom LogEntryId
harus menggunakan jenis STRING
untuk GoogleSQL, atau jenis text
untuk PostgreSQL.
GoogleSQL
INSERT INTO
UserAccessLog (LogEntryId, LastAccess, UserId)
VALUES
(GENERATE_UUID(), '2016-01-25 10:10:10.555555-05:00', 'TomSmith');
PostgreSQL
INSERT INTO
useraccesslog (logentryid, lastaccess, userid)
VALUES
(spanner.generate_uuid(),'2016-01-25 10:10:10.555555-05:00', 'TomSmith');
Ada beberapa kelemahan menggunakan UUID:
- Ukurannya sedikit besar, menggunakan 16 byte atau lebih. Opsi lain untuk kunci utama tidak menggunakan penyimpanan sebanyak ini.
- ID ini tidak membawa informasi tentang data. Misalnya, kunci utama
SingerId
danAlbumId
memiliki makna yang melekat, sedangkan UUID tidak. - Anda kehilangan lokalitas antara data terkait, itulah sebabnya penggunaan UUID menghilangkan hotspot.
Lakukan bit-reverse pada nilai yang berurutan
Anda harus memastikan bahwa kunci utama numerik (INT64
di GoogleSQL atau
bigint
di PostgreSQL) tidak bertambah atau menurun secara berurutan. Kunci utama berurutan dapat menyebabkan hotspot dalam skala besar. Salah satu cara untuk
menghindari masalah ini adalah dengan membalikkan bit nilai berurutan, dengan memastikan untuk
mendistribusikan nilai kunci utama secara merata di seluruh ruang kunci.
Spanner mendukung urutan bit-reverse, yang menghasilkan nilai bit-reverse bilangan bulat unik. Anda dapat menggunakan urutan di komponen pertama (atau satu-satunya) dalam kunci utama untuk menghindari masalah hotspot. Untuk informasi selengkapnya, lihat Urutan bit terbalik.
Menukar urutan tombol
Salah satu cara untuk menyebarkan operasi tulis di ruang kunci secara lebih merata adalah dengan menukar urutan kunci sehingga kolom yang berisi nilai monoton bukan bagian kunci pertama:
GoogleSQL
CREATE TABLE UserAccessLog ( UserId INT64 NOT NULL, LastAccess TIMESTAMP NOT NULL, ... ) PRIMARY KEY (UserId, LastAccess);
PostgreSQL
CREATE TABLE useraccesslog ( userid bigint NOT NULL, lastaccess TIMESTAMPTZ NOT NULL, ... PRIMARY KEY (UserId, LastAccess) );
Dalam skema yang diubah ini, penyisipan kini diurutkan terlebih dahulu menurut UserId
, bukan
berdasarkan stempel waktu akses terakhir kronologis. Skema ini menyebarkan operasi tulis di antara
pemisahan yang berbeda karena satu pengguna tidak mungkin menghasilkan ribuan
peristiwa per detik.
Gambar berikut menunjukkan lima baris dari tabel UserAccessLog
yang
diurutkan Spanner dengan UserId
, bukan stempel waktu akses:
Di sini, Spanner mengelompokkan data UserAccessLog
menjadi tiga bagian,
dengan setiap bagian berisi sekitar seribu baris nilai UserId
yang diurutkan. Ini adalah estimasi yang wajar tentang cara data pengguna dapat dibagi,
dengan asumsi setiap baris berisi sekitar 1 MB data pengguna dan diberikan ukuran pemisahan maksimum
sekitar 8 GB. Meskipun peristiwa pengguna terjadi dengan selang waktu sekitar satu milidetik, setiap peristiwa diangkat oleh pengguna yang berbeda, sehingga urutan penyisipan kemungkinan besar tidak akan membuat hotspot dibandingkan dengan menggunakan stempel waktu untuk pengurutan.
Lihat juga praktik terbaik terkait untuk mengurutkan kunci berbasis stempel waktu.
Meng-hash kunci unik dan menyebarkan operasi tulis di seluruh shard logis
Teknik umum lainnya untuk menyebarkan beban di beberapa server adalah dengan membuat kolom yang berisi hash kunci unik yang 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 tersebar lebih merata di seluruh ruang kunci.
Anda dapat menggunakan nilai hash untuk membuat shard atau partisi logis di database. Dalam database yang di-shard secara fisik, baris tersebar di beberapa
server database. Dalam database yang di-shard secara logis, data dalam tabel menentukan
shard. Misalnya, untuk menyebarkan operasi tulis ke tabel UserAccessLog
di seluruh shard logika
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 dari hash. Contoh:
GoogleSQL
ShardId = hash(LastAccess and UserId) % N
Pilihan fungsi hash dan kombinasi kolom Anda menentukan cara baris tersebar di seluruh ruang kunci. Spanner kemudian akan membuat pemisahan di seluruh baris untuk mengoptimalkan performa.
Diagram berikut menggambarkan cara menggunakan hash untuk membuat tiga shard logika dapat menyebarkan throughput operasi tulis secara lebih merata di seluruh server:
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 kebetulan berada dalam pemisahan yang berbeda. Penyisipan
didistribusikan secara merata di antara bagian, yang menyeimbangkan throughput operasi tulis ke
tiga server yang menangani bagian.
Spanner juga memungkinkan Anda membuat fungsi hash di kolom yang dihasilkan.
Untuk melakukannya di GoogleSQL, gunakan fungsi FARM_FINGERPRINT selama waktu operasi tulis, seperti 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 Anda tersebar di seluruh rentang kunci. Anda tidak memerlukan hash kriptografis, meskipun hash kriptografis 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 akan lebih cepat jika ada lebih sedikit nilai hash yang akan dipindai.
- Jumlah node.
Menggunakan 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 ingin membaca histori terbaru, Anda menggunakan tabel interleaved
untuk histori, dan Anda membaca baris induk. Dalam hal ini,
dengan kolom stempel waktu
DESC
, entri histori terbaru disimpan di samping 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 sejauh mana Anda akan kembali. Misalnya, Anda dapat
menggunakan kueri SQL dengan
LIMIT
untuk mendapatkan peristiwa N terbaru, atau Anda dapat merencanakan untuk membatalkan pembacaan setelah membaca sejumlah baris tertentu. Dalam kasus ini, Anda ingin memulai dengan entri terbaru dan membaca secara berurutan entri yang lebih lama 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 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, bagian atas adalah yang terbaru, atau bagian atas adalah yang terlama.
Menggunakan indeks sisipan pada kolom yang nilainya meningkat atau menurun secara monoton
Serupa dengan contoh kunci utama sebelumnya yang harus Anda hindari, sebaiknya jangan membuat indeks non-sisipan pada kolom yang nilainya meningkat atau menurun secara monoton, meskipun bukan kolom kunci utama.
Misalnya, Anda menentukan tabel berikut, dengan 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 menghasilkan jebakan 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.
Namun, Anda dapat membuat indeks yang diselingi seperti ini, karena baris indeks yang diselingi diselingi di baris induk yang sesuai, dan tidak mungkin satu baris induk menghasilkan ribuan peristiwa per detik.
Praktik terbaik desain skema #3: Jangan buat indeks non-sisipan pada kolom dengan kecepatan penulisan tinggi yang nilainya meningkat atau menurun secara monoton. Daripada menggunakan indeks yang diselingi, gunakan teknik seperti yang akan Anda gunakan untuk desain kunci utama tabel dasar saat mendesain kolom indeks—misalnya, tambahkan `shardId`.
Langkah selanjutnya
- Lihat contoh desain skema.
- Pelajari cara memuat data secara massal.