FoundationDB menyimpan seluruh data dalam bentuk ordered key-value store global. Setiap key diurutkan secara leksikografis menggunakan byte order standar. Karakteristik ini membuat operasi range query sangat efisien jika struktur byte dirancang dengan benar, namun rentan menimbulkan read/write bottleneck jika distribusi key terkonsentrasi pada satu storage server (hot-spotting).

1. Desain Key-Space untuk Range Query

Prinsip utama optimasi range query FoundationDB adalah memanfaatkan pengurutan leksikografis secara deterministik. Lapisan abstraksi tuple bawaan FoundationDB mengonversi tipe data primitif ke representasi byte yang mempertahankan urutan tipe asli (misalnya integer, UUID, string).

Untuk data berbasis multi-tenant atau hierarkis, tempatkan entitas pembatas lingkup di byte paling depan (prefix) guna membatasi jangkauan range scan:

// Format skema key data:
// (prefix_tabel, tenant_id, timestamp, entity_id) -> JSON / Protobuf Payload
(1, "tenant_abc", 1711929600, "ord_901") = { ... }
(1, "tenant_abc", 1711929660, "ord_902") = { ... }

Dengan struktur ini, query seluruh pesanan milik tenant_abc dalam rentang waktu tertentu dapat dieksekusi menggunakan batas awal dan batas akhir tuple tanpa menyentuh data tenant lain.

2. Mitigasi Hot-Spotting Monotonic Key

Pola kunci yang naik secara monoton (misalnya auto-increment ID atau raw timestamp di prefix utama) mengarahkan seluruh operasi write ke shard terakhir di storage server. FoundationDB membagi shard berdasarkan ukuran data (umumnya setiap ~100MB), bukan hash otomatis.

Strategi Prefix Hashing / Bucket Splitting

Untuk mendistribusikan beban penulisan ke berbagai storage server secara merata, gunakan salt bucket terhitung sebelum timestamp:

// Format: (prefix, bucket_id, timestamp, entity_id)
bucket_id = hash(entity_id) % TOTAL_BUCKETS

Kelemahan: Range query temporal tidak lagi dapat dibaca dengan satu kali scan linier. Pembacaan harus menggunakan teknik scatter-gather secara paralel ke semua bucket yang ada.

3. Pola Secondary Index dan Pagination Leksikografis

Secondary index di FoundationDB dibuat manual sebagai pasangan key terpisah di dalam transaksi yang sama guna menjamin konsistensi ACID.

// Primary Key:
("data", "user_123") = { "status": "ACTIVE", "name": "Alice" }

// Secondary Index:
("idx_status", "ACTIVE", "user_123") = ""

Untuk membaca data berurutan (pagination), hindari penggunaan offset numerik. Gunakan key terakhir yang dibaca sebagai continuation token.

4. Batasan Teknis FoundationDB

  • Batas Transaksi 5 Detik: Transaksi FoundationDB otomatis dibatalkan (transaction_too_old) jika berlangsung lebih dari 5 detik. Range query bervolume besar harus dipecah menjadi batch-batch kecil.
  • Ukuran Data: Ukuran key maksimal 10.000 byte (disarankan < 1 KB). Ukuran value maksimal 100.000 byte. Data besar harus dipecah menjadi chunk terpisah.
  • Batas Transaksi Write: Total write dalam satu transaksi tidak boleh melebihi 10 MB.

5. Implementasi Operasional (Python)

Contoh berikut mendemonstrasikan implementasi indexing sekunder, range query dengan batching, dan pencegahan batas waktu 5 detik:

import fdb
import fdb.tuple

fdb.api_version(710)
db = fdb.open()

# Namespace prefix
DATA_PREFIX = 1
INDEX_PREFIX = 2

@fdb.transactional
def insert_record(tr, entity_id: str, status: str, payload: bytes):
    data_key = fdb.tuple.pack((DATA_PREFIX, entity_id))
    index_key = fdb.tuple.pack((INDEX_PREFIX, status, entity_id))
    
    # Simpan data utama dan index secara atomik
    tr[data_key] = payload
    tr[index_key] = b""

def query_by_status(db, status: str, batch_size: int = 100):
    start_key = fdb.tuple.pack((INDEX_PREFIX, status))
    stop_key = fdb.tuple.range((INDEX_PREFIX, status)).stop
    
    more = True
    while more:
        # Jalankan pembacaan dalam batas transaksi independen
        records, last_key = _read_batch(db, start_key, stop_key, batch_size)
        for key, _ in records:
            unpacked = fdb.tuple.unpack(key)
            entity_id = unpacked[2]
            yield entity_id
            
        if len(records) < batch_size:
            more = False
        else:
            # Lanjutkan pagination berbasis leksikografis (exclusive)
            start_key = fdb.KeySelector.first_greater_than(last_key)

@fdb.transactional
def _read_batch(tr, start, stop, limit):
    # Mengambil range tuple dengan batas jumlah record
    r = tr.get_range(start, stop, limit=limit)
    items = list(r)
    last_key = items[-1].key if items else None
    return items, last_key

6. Best Practice Operasional

  • Gunakan StreamingMode yang sesuai: gunakan fdb.StreamingMode.want_all untuk fetch data penuh, atau fdb.StreamingMode.iterator untuk streaming bertahap.
  • Jangan menaruh indeks dinamis pada key yang sering berubah (write-heavy update) tanpa mempertimbangkan overhead konsensus.
  • Jika pagination memerlukan filter multi-kolom, urutkan elemen tuple dari kardinalitas terendah ke tertinggi.