Perencanaan kapasitas dan retensi

Penetapan ukuran dari laju ingest, serta biaya kardinalitas dan data mentah.

Daftar isi

Perencanaan kapasitas basis data time-series berbeda dari basis data operasional karena laju ingest yang konstan, retensi yang panjang, serta biaya kardinalitas yang tumbuh secara kombinatorial. Topik ini menjelaskan tiga variabel yang menentukan ukuran basis data, yaitu laju ingest, retensi waktu, serta kardinalitas deret. Setelah membaca topik ini, ukuran disk, anggaran kardinalitas, dan kebijakan retensi tahunan dapat dihitung sebelum data mengalir, sehingga keputusan tentang downsampling dan kompresi diambil berdasarkan angka, bukan dugaan.

Tiga variabel penentu ukuran#

Ukuran basis data time-series pada akhirnya ditentukan oleh tiga variabel yang saling mengalikan. Variabel pertama adalah laju ingest, yaitu jumlah titik yang ditulis per satuan waktu, biasanya diukur dalam titik per detik. Variabel kedua adalah retensi, yaitu durasi data dipertahankan sebelum dihapus, biasanya diukur dalam hari atau bulan. Variabel ketiga adalah kardinalitas, yaitu jumlah deret unik yang dihasilkan oleh kombinasi tag. Hasil kali ketiganya menentukan jumlah titik yang disimpan pada satu waktu, sedangkan ukuran per titik menentukan ukuran disk akhir.

Ketiga variabel tidak bebas satu sama lain. Peningkatan kardinalitas tanpa peningkatan jumlah node mempercepat laju ingest, sebab setiap node mempublikasikan lebih banyak deret. Sebaliknya, peningkatan retensi tanpa peningkatan laju ingest hanya menambah ukuran disk, bukan beban tulis. Pemahaman tentang interaksi ini penting, sebab keputusan untuk menambah tag baru atau memperpanjang retensi harus diiringi perhitungan ulang ketiga variabel, bukan satu saja.

Laju ingest sebagai basis#

Laju ingest adalah variabel yang paling mudah dihitung dan paling sering dijadikan titik awal perencanaan. Pada pemantauan node dengan interval scrap 15 detik, setiap deret menghasilkan empat titik per menit, atau 5.760 titik per hari. Pada tiga node dengan 100 deret per node, laju ingest harian adalah 1.728.000 titik, atau sekitar 20 titik per detik. Angka ini menjadi dasar perkalian selanjutnya untuk retensi dan ukuran per titik.

Pemilihan ukuran per titik bergantung pada basis data dan apakah kompresi aktif. Pada TimescaleDB, baris mentah dengan tanda waktu, satu tag teks, dan satu field float menggunakan sekitar 30 hingga 40 byte, sedangkan setelah kompresi kolumnar turun menjadi sekitar 10 hingga 15 byte per baris. Pada InfluxDB 3.x, penyimpanan kolumnar Parquet menggunakan sekitar 16 byte per titik untuk data terkompresi. Angka-angka ini hanya perkiraan kasar dari dokumentasi resmi, sehingga pengukuran pada beban kerja nyata tetap diperlukan sebelum menetapkan anggaran produksi.

Biaya kardinalitas#

Kardinalitas adalah variabel yang paling sering diabaikan dan paling sering menyebabkan kejutan kapasitas. Kardinalitas tinggi terjadi ketika sebuah tag bernilai unik untuk setiap titik atau setiap sesi, misalnya user_id pada sistem dengan ribuan pengguna aktif. Setiap nilai tag baru menghasilkan deret baru, sehingga kombinasi 1.000 user_id × 100 node × 5 metrik menghasilkan 500.000 deret, sebuah jumlah yang jauh melebihi skenario pemantauan biasa.

Biaya kardinalitas tinggi tidak terbatas pada ukuran disk. Setiap deret baru memerlukan entri pada indeks terbalik, sehingga memori yang digunakan oleh basis data tumbuh seiring dengan kardinalitas, bukan dengan jumlah titik. Pada InfluxDB, kardinalitas tinggi membebani series file dan memperlambat pemindaian. Pada TimescaleDB, kardinalitas tinggi pada tag yang dipartisi oleh space partitioning menyebabkan banyak chunk kecil, sehingga planner menanggung biaya pemindaian metadata yang tinggi. Diagram berikut memperlihatkan bagaimana penambahan satu tag user_id melompatkan ukuran disk dari skenario normal menjadi ordo yang berbeda.

14 TiB9 TiB5 TiB1 TiB0300 deret3 node10.000 deret50 node150.000 deret500 node500.000 deretuser_id tag14 TiBdisk setahun, TimescaleDB (kompresi aktif)
Lompatan ukuran disk saat tag user_id menambah kardinalitas dari 300 deret menjadi 500.000 deret

Perhitungan kapasitas tahunan#

Perhitungan kapasitas tahunan menggabungkan ketiga variabel menjadi satu angka disk. Skrip berikut menghitung tiga skenario kapasitas, yaitu skala kecil, menengah, dan besar, lalu menambahkan skenario kardinalitas tinggi untuk memperlihatkan lompatan ukuran. Skrip tidak menjalankan SQL atau perintah basis data, melainkan menghitung secara mekanis dengan asumsi ukuran per titik dari dokumentasi resmi.

python perencanaan_kapasitas.py
"""Perencanaan kapasitas basis data time-series.

Skrip ini TIDAK menjalankan SQL atau perintah basis data. Tujuannya
memperlihatkan bagaimana laju ingest, retensi, dan kardinalitas
berinteraksi dalam menentukan kebutuhan disk. Skrip menghitung
tiga skenario: skala kecil, skala menengah, dan kardinalitas tinggi,
sehingga pembaca dapat melihat kapan setiap variabel menjadi beban.
"""
from __future__ import annotations

# Asumsi umum untuk skenario pemantauan node.
INTERVAL_SCRAP_SEC = 15
HARI_RETENSI = 365

# Ukuran per baris pada TimescaleDB (perkiraan).
# Timestamptz 8 byte + tag (teks rata-rata) + float 8 byte + overhead 24 byte.
TSDB_BYTE_MENTAH = 36
TSDB_BYTE_KOMPRES = 12  # setelah kompresi kolumnar
TSDB_OVERHEAD = 0.20    # indeks, chunk, TOAST
HARI_MENTAH = 7         # chunk mentah dipertahankan sebelum dikompres

# Pada InfluxDB 3.x, baris disimpan dalam format kolumnar Parquet.
# Estimasi 16 byte per titik untuk data terkompresi (referensi dokumentasi).
INFLUX_BYTE_PER_TITIK = 16
INFLUX_OVERHEAD = 0.10  # indeks dan metadata


def sampel_per_hari(deret: int) -> int:
    return deret * (60 // INTERVAL_SCRAP_SEC) * 60 * 24


def ukuran_tsdb(deret: int, hari: int = HARI_RETENSI) -> int:
    sps = sampel_per_hari(deret)
    mentah = min(hari, HARI_MENTAH) * sps * TSDB_BYTE_MENTAH
    kompres = max(0, hari - HARI_MENTAH) * sps * TSDB_BYTE_KOMPRES
    return int((mentah + kompres) * (1 + TSDB_OVERHEAD))


def ukuran_influx(deret: int, hari: int = HARI_RETENSI) -> int:
    sps = sampel_per_hari(deret)
    return int(sps * hari * INFLUX_BYTE_PER_TITIK * (1 + INFLUX_OVERHEAD))


def fmt_gib(byte: int) -> str:
    return f"{byte / (1024 ** 3):.2f} GiB"


def fmt_tib(byte: int) -> str:
    return f"{byte / (1024 ** 4):.2f} TiB"


def skenario(nama: str, deret: int) -> dict:
    sps = sampel_per_hari(deret)
    laju_per_detik = sps / 86400
    tsdb = ukuran_tsdb(deret)
    influx = ukuran_influx(deret)
    return {
        "nama": nama,
        "deret": deret,
        "sampel_per_hari": sps,
        "laju_per_detik": laju_per_detik,
        "tsdb_tahun": tsdb,
        "influx_tahun": influx,
    }


def main() -> None:
    print("=== Asumsi umum ===")
    print(f"interval scrap: {INTERVAL_SCRAP_SEC} detik")
    print(f"retensi: {HARI_RETENSI} hari ({HARI_RETENSI // 365} tahun)")
    print(f"chunk mentah TimescaleDB: {HARI_MENTAH} hari sebelum dikompres")
    print(f"ukuran mentah TimescaleDB: ~{TSDB_BYTE_MENTAH} byte/baris")
    print(f"ukuran terkompres TimescaleDB: ~{TSDB_BYTE_KOMPRES} byte/baris")
    print(f"ukuran InfluxDB 3.x terkompres: ~{INFLUX_BYTE_PER_TITIK} byte/titik")
    print()

    print("=== Skenario kapasitas ===")
    skenarios = [
        skenario("kecil: 3 node x 100 deret", 3 * 100),
        skenario("menengah: 50 node x 200 deret", 50 * 200),
        skenario("besar: 500 node x 300 deret", 500 * 300),
    ]
    for s in skenarios:
        print(f"\n{s['nama']}")
        print(f"  total deret: {s['deret']:,}")
        print(f"  sampel/hari: {s['sampel_per_hari']:,}")
        print(f"  laju ingest: {s['laju_per_detik']:.0f} titik/detik")
        print(f"  TimescaleDB setahun: {fmt_gib(s['tsdb_tahun'])}")
        print(f"  InfluxDB 3.x setahun: {fmt_gib(s['influx_tahun'])}")

    print()
    print("=== Biaya kardinalitas ===")
    # Kardinalitas = jumlah deret unik = kombinasi tag.
    # Misal: 1000 user_id, 100 node, 5 metrik -> 500.000 deret
    print("kasus: 1000 user_id x 100 node x 5 metrik = 500.000 deret")
    deret_kard = 1_000 * 100 * 5
    sps_kard = sampel_per_hari(deret_kard)
    print(f"  deret: {deret_kard:,}")
    print(f"  sampel/hari: {sps_kard:,}")
    print(f"  laju ingest: {sps_kard / 86400:.0f} titik/detik")
    print(f"  TimescaleDB setahun: {fmt_tib(ukuran_tsdb(deret_kard))}")
    print(f"  InfluxDB 3.x setahun: {fmt_tib(ukuran_influx(deret_kard))}")
    print()

    print("=== Tradeoff retensi data mentah vs downsampling ===")
    # Bandingkan: 365 hari data mentah vs 30 hari mentah + 335 hari cagg harian
    deret = 50 * 200  # menengah
    sps = sampel_per_hari(deret)
    mentah_365 = ukuran_tsdb(deret, hari=365)
    mentah_30 = ukuran_tsdb(deret, hari=30)
    # cagg harian: 1 baris per deret per hari, ~50 byte per baris (5 agregat)
    cagg_baris = deret * (365 - 30)
    cagg_ukuran = int(cagg_baris * 50 * (1 + TSDB_OVERHEAD))
    total_hibrid = mentah_30 + cagg_ukuran
    print(f"skenario menengah ({deret:,} deret):")
    print(f"  365 hari mentah: {fmt_gib(mentah_365)}")
    print(f"  30 hari mentah + 335 hari cagg: {fmt_gib(total_hibrid)}")
    print(f"  penghematan: {mentah_365 / total_hibrid:.1f}x lebih kecil")


if __name__ == "__main__":
    main()
text Keluaran
=== Asumsi umum ===
interval scrap: 15 detik
retensi: 365 hari (1 tahun)
chunk mentah TimescaleDB: 7 hari sebelum dikompres
ukuran mentah TimescaleDB: ~36 byte/baris
ukuran terkompres TimescaleDB: ~12 byte/baris
ukuran InfluxDB 3.x terkompres: ~16 byte/titik

=== Skenario kapasitas ===

kecil: 3 node x 100 deret
  total deret: 300
  sampel/hari: 1,728,000
  laju ingest: 20 titik/detik
  TimescaleDB setahun: 8.78 GiB
  InfluxDB 3.x setahun: 10.34 GiB

menengah: 50 node x 200 deret
  total deret: 10,000
  sampel/hari: 57,600,000
  laju ingest: 667 titik/detik
  TimescaleDB setahun: 292.77 GiB
  InfluxDB 3.x setahun: 344.61 GiB

besar: 500 node x 300 deret
  total deret: 150,000
  sampel/hari: 864,000,000
  laju ingest: 10000 titik/detik
  TimescaleDB setahun: 4391.53 GiB
  InfluxDB 3.x setahun: 5169.15 GiB

=== Biaya kardinalitas ===
kasus: 1000 user_id x 100 node x 5 metrik = 500.000 deret
  deret: 500,000
  sampel/hari: 2,880,000,000
  laju ingest: 33333 titik/detik
  TimescaleDB setahun: 14.30 TiB
  InfluxDB 3.x setahun: 16.83 TiB

=== Tradeoff retensi data mentah vs downsampling ===
skenario menengah (10,000 deret):
  365 hari mentah: 292.77 GiB
  30 hari mentah + 335 hari cagg: 34.18 GiB
  penghematan: 8.6x lebih kecil

Keluaran di atas memperlihatkan dua hal. Pertama, skenario besar dengan 150.000 deret menggunakan sekitar 4,4 TiB setahun, sebuah angka yang masih dapat dikelola dengan satu server berdisk besar. Kedua, penambahan satu tag user_id melompatkan ukuran menjadi 14,3 TiB, lebih dari tiga kali lipat dari skenario besar, meskipun jumlah node dan metrik tetap. Lompatan ini menunjukkan bahwa kardinalitas, bukan jumlah node, adalah variabel yang paling berbahaya bagi kapasitas.

Tradeoff data mentah versus downsampling#

Perhitungan pada skenario menengah memperlihatkan bahwa mempertahankan 365 hari data mentah menggunakan 292,77 GiB, sedangkan kombinasi 30 hari mentah dengan 335 hari continuous aggregate harian hanya menggunakan 34,18 GiB, rasio 8,6 kali lebih kecil. Penghematan ini bukan gratis, sebab continuous aggregate harian menyembunyikan puncak per menit. Keputusan tentang berapa lama data mentah dipertahankan bergantung pada kebutuhan investigasi insiden, sebab insiden yang terjadi 60 hari lalu tidak dapat diperiksa pada resolusi per menit bila data mentah sudah dihapus.

Pola praktis yang sering dijumpai pada produksi adalah tiga tingkat retensi. Tingkat pertama adalah data mentah selama 7 hingga 30 hari untuk investigasi insiden terkini. Tingkat kedua adalah continuous aggregate per jam selama 90 hari untuk dashboard mingguan dan bulanan. Tingkat ketiga adalah continuous aggregate per hari selama satu tahun atau lebih untuk audit dan kapasitas historis. Pemisahan ini memungkinkan dashboard tetap responsif tanpa membebani disk dengan data mentah yang jarang dibaca.

Kesalahan umum#

  • Menambahkan user_id atau session_id sebagai tag tanpa menghitung perkalian kardinalitas. Akibatnya, ukuran disk melompat ordo magnitudo dan kinerja basis data menurun karena indeks terbalik membengkak.
  • Memperpanjang retensi data mentah dari 30 hari menjadi 365 hari tanpa mengaktifkan kompresi. Akibatnya, disk dipenuhi baris mentah yang sebenarnya jarang dibaca.
  • Menghitung kapasitas hanya dari laju ingest tanpa memperhitungkan overhead indeks, chunk, dan TOAST. Akibatnya, ukuran aktual pada disk melebihi proyeksi sebesar 20 hingga 50 persen.

Ringkasan#

  • Tiga variabel penentu ukuran basis data time-series adalah laju ingest, retensi waktu, dan kardinalitas deret, yang hasil kalinya menentukan jumlah titik yang disimpan.
  • Biaya kardinalitas tidak terbatas pada disk, sebab setiap deret unik memerlukan entri pada indeks terbalik, sehingga memori tumbuh seiring kardinalitas, bukan jumlah titik.
  • Kombinasi retensi data mentah pendek dengan continuous aggregate panjang mengecilkan ukuran disk sekitar 8,6 kali pada skenario menengah, dengan biaya kehilangan resolusi menitan pada data lama.
  • Anggaran kardinalitas eksplisit sebelum menambah tag adalah praktik yang mencegah kejutan kapasitas pada skala produksi.

Referensi#