Praktik terbaik desain skema

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 UserAccessLog yang diurutkan berdasarkan stempel waktu dengan hotspot yang sesuai

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 SingerId dan AlbumId, 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:

Tabel UserAccessLogs diurutkan berdasarkan UserId dengan throughput penulisan yang seimbang

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:

Tabel UserAccessLogs diurutkan berdasarkan ShardId dengan throughput tulis yang seimbang

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 LIMIT untuk 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