Storage Performance #
Ketika kita merancang arsitektur aplikasi di Kubernetes, perhatian kita sering kali tercurah sepenuhnya pada optimalisasi CPU dan alokasi memori RAM. Kita menghabiskan banyak waktu menyetel Vertical Pod Autoscaler atau menentukan limit resource kontainer secara presisi. Namun, ada satu komponen penting yang sering kali luput dari perhatian hingga masalah besar melanda lingkungan produksi: performa penyimpanan (storage performance).
Storage performance adalah faktor penentu utama bagi kestabilan aplikasi stateful kita. Database yang lambat, kegagalan penulisan log antrian, latensi API yang membengkak, hingga timeout transaksi berantai sering kali bukan disebabkan oleh kurangnya CPU atau memori, melainkan karena keterbatasan I/O pada subsistem penyimpanan.
Dalam artikel ini, kita akan mengupas tuntas metrik-metrik performa storage yang krusial, profil I/O untuk berbagai workload, integrasi local storage berkinerja tinggi, pengaruh jaringan kluster terhadap performa, serta cara melakukan pengujian benchmark secara mandiri di dalam kluster Kubernetes kita.
Tiga Metrik Utama Storage Performance #
Untuk memahami karakteristik penyimpanan, kita harus mengenali tiga metrik fundamental yang menentukan kinerja disk. Ketiga metrik ini tidak berdiri sendiri; mereka saling memengaruhi dan membentuk profil performa suatu volume penyimpanan.
flowchart TD
Metrics["STORAGE PERFORMANCE METRICS"]
Metrics --> IOPS["IOPS (Jumlah operasi R/W per detik)"]
Metrics --> Throughput["Throughput (Volume data ditransfer per detik)"]
Metrics --> Latency["Latency (Waktu respon satu operasi tulis/baca)"]
1. IOPS (Input/Output Operations Per Second) #
IOPS mengukur jumlah operasi baca atau tulis yang dapat diselesaikan oleh disk dalam satu detik. Metrik ini sangat kritis untuk aplikasi yang sering melakukan transaksi kecil-kecil secara acak (random I/O). Database transaksional (OLTP) seperti PostgreSQL, MySQL, dan sistem pencarian seperti Elasticsearch sangat bergantung pada tingginya IOPS untuk memproses ribuan kueri kecil per detik.
2. Throughput (Bandwidth) #
Throughput mengukur volume data total yang dapat dibaca atau ditulis ke media penyimpanan dalam satuan waktu tertentu (biasanya dalam MegaBytes per detik atau MB/s). Throughput sangat penting untuk workload yang membaca atau menulis file besar secara berurutan (sequential I/O). Contoh workload yang membutuhkan throughput tinggi adalah sistem streaming log (fluentd/Loki), batch processing data skala besar, Kafka message broker, dan backup restore database.
3. Latency (Latensi) #
Latensi adalah waktu yang dibutuhkan oleh subsistem storage untuk menyelesaikan satu permintaan I/O tunggal (diukur dalam milidetik atau ms, atau mikrodetik atau µs). Bagi database transaksional, latensi tulis (write latency) dari operasi fsync (menulis data dari cache memori ke piringan disk fisik demi durabilitas data) adalah metrik paling krusial. Jika latensi tinggi, maka setiap transaksi database akan tertahan, memicu penumpukan koneksi dan kegagalan sistem.
Profil I/O Berdasarkan Workload #
Setiap aplikasi stateful memiliki karakteristik I/O yang unik. Kita tidak bisa menggunakan satu tipe StorageClass untuk semua jenis beban kerja. Berikut adalah panduan profil I/O untuk berbagai jenis aplikasi yang umum kita jalankan di Kubernetes:
1. Database Relasional (PostgreSQL & MySQL) #
- Karakteristik: Random I/O tinggi, ukuran blok kecil (biasanya 8KB hingga 16KB per operasi), sensitif terhadap latensi
fsync. - Kebutuhan Utama: Latensi sangat rendah (sub-millisecond) dan IOPS tinggi yang stabil.
- Rekomendasi StorageClass: SSD premium dengan IOPS yang didefinisikan (misalnya tipe
gp3AWS dengan peningkatan IOPS minimal 3000-6000, atau tipeio2untuk database finansial yang sangat sibuk).
2. Message Brokers (Apache Kafka) #
- Karakteristik: Sequential write I/O sangat tinggi, operasi penulisan file log terdistribusi secara append-only.
- Kebutuhan Utama: Throughput sequential yang besar dan kapasitas disk yang lega. IOPS acak tidak terlalu kritis karena Kafka menulis data ke page cache memori terlebih dahulu sebelum diturunkan ke disk secara sekuensial.
- Rekomendasi StorageClass: Tipe SSD standar (
gp3dengan throughput tinggi) atau HDD teroptimasi throughput (st1AWS) untuk kluster Kafka dengan retensi data jangka panjang.
3. Log Aggregator (Elasticsearch / OpenSearch) #
- Karakteristik: Kombinasi random I/O (saat pencarian indeks) dan sequential I/O tinggi (saat melakukan indexing log baru secara terus-menerus dan proses merging shard).
- Kebutuhan Utama: Throughput tulis yang konsisten dan IOPS menengah-tinggi.
- Rekomendasi StorageClass: SSD dengan parameter throughput yang ditingkatkan (misalnya
gp3dengan parameter throughput disetel ke 250-500 MB/s).
Local Persistent Volume untuk Performa Maksimum #
Untuk workload database transaksional ultra-cepat yang membutuhkan waktu respon latensi paling minimum (di bawah 1 milidetik), penyimpanan berbasis jaringan (network-attached storage seperti AWS EBS atau Google Cloud Persistent Disk) terkadang tidak lagi memadai akibat adanya latensi overhead jaringan.
Solusi terbaik kita untuk kasus ini adalah menggunakan Local Persistent Volume (LPV), yaitu memanfaatkan disk NVMe fisik yang terpasang langsung di dalam server fisik worker node kita.
Mari kita perhatikan perbedaan jalur akses I/O antara Local Storage dan Network Storage melalui diagram berikut:
flowchart TD
subgraph LocalStorageJalur["Jalur Local Storage (Latensi Ultra Rendah)"]
PodLocal["Pod Aplikasi"] --> NodeLocal["Worker Node (Kernel OS)"]
NodeLocal --> PhysicalNVMe["Disk NVMe Fisik Lokal"]
end
subgraph NetworkStorageJalur["Jalur Network Storage (Banyak Hop Jaringan)"]
PodNet["Pod Aplikasi"] --> NodeNet["Worker Node (Kernel OS)"]
NodeNet --> NetworkSwitch["Switch Jaringan Kluster"]
NetworkSwitch --> StorageController["Cloud Storage Controller"]
StorageController --> NetworkDisk["Disk Cloud Fisik (e.g. EBS)"]
end
Implementasi Local Persistent Volume #
Karena volume lokal terikat erat pada satu node fisik tertentu, Kubernetes tidak bisa melakukan penyediaan dinamis (dynamic provisioning) secara otomatis jika disk fisik belum dikonfigurasi. Kita harus mendefinisikan StorageClass lokal dan mendaftarkan objek PV secara manual untuk setiap disk fisik node.
1. Membuat StorageClass Lokal #
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-nvme-sc
provisioner: kubernetes.io/no-provisioner # Wajib: Tidak ada dynamic provisioner eksternal
volumeBindingMode: WaitForFirstConsumer # Wajib: Tunggu scheduler menentukan node Pod
2. Membuat PersistentVolume Lokal Secara Statis #
Kita harus menentukan letak direktori disk NVMe pada host node worker dan membuat ikatan afinitas node (node affinity) agar Kubernetes scheduler tahu di mana disk fisik ini berada secara nyata.
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-worker-node-1
spec:
capacity:
storage: 500Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-nvme-sc
local:
path: /mnt/disks/nvme0n1 # Path mounting disk NVMe fisik di worker node
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-node-1 # Disk ini secara fisik HANYA menempel di worker-node-1
Trade-off Local Storage #
Meskipun memberikan performa jutaan IOPS dan latensi mendekati nol, kita harus memahami konsekuensi penggunaannya:
- Ketiadaan Portabilitas: Jika
worker-node-1mengalami kerusakan motherboard, Pod database kita tidak dapat dipindahkan ke node lain karena data fisik kita terkunci di server yang rusak tersebut. - Tanggung Jawab Replikasi di Level Aplikasi: Kita wajib mengimplementasikan arsitektur replikasi data pada level perangkat lunak aplikasi kita (misalnya membuat arsitektur PostgreSQL Primary-Replica, kluster Cassandra, atau arsitektur replikasi Kafka) untuk mengantisipasi kegagalan node worker.
Pengaruh Bandwidth Jaringan Node Terhadap Storage #
Ketika menggunakan volume jaringan cloud (seperti AWS EBS), performa I/O volume kita tidak hanya dibatasi oleh spesifikasi volume itu sendiri, melainkan dibatasi secara ketat oleh bandwidth jaringan instans VM worker node kita.
Di cloud provider seperti AWS, setiap instans VM (EC2) memiliki batas atas kapasitas jaringan terdedikasi untuk I/O penyimpanan (EBS Bandwidth).
Mari kita perhatikan tabel perbandingan bandwidth instans berikut:
| Tipe Instans AWS EC2 | Dedicated EBS Bandwidth | Maksimal Throughput Storage | Batas Maksimal IOPS gp3 |
|---|---|---|---|
| t3.medium | Up to 2.08 Gbps | ~260 MB/s | 3,000 (Terbatas) |
| m5.large | Up to 2.88 Gbps | ~360 MB/s | 4,000 (Terbatas) |
| m5.xlarge | 4.75 Gbps (Dedicated) | ~590 MB/s | 9,000 (Terbatas) |
| m5.4xlarge | 9.50 Gbps (Dedicated) | ~1,187 MB/s | 16,000 (Maksimal gp3) |
Jika kita membuat volume gp3 dengan spesifikasi tinggi sebesar 12.000 IOPS dan throughput 500 MB/s, lalu memasang volume tersebut pada Pod yang dijadwalkan di worker node bertipe t3.medium, performa database kita akan mengalami pelambatan drastis. VM t3.medium akan memangkas (throttle) I/O kita secara sepihak karena kehabisan alokasi bandwidth jaringan instans.
Cara Menguji Performa Storage Secara Mandiri #
Kita tidak boleh berasumsi mengenai performa penyimpanan kluster hanya dengan membaca brosur spesifikasi cloud provider. Kita harus mengukur performa riil secara empiris menggunakan alat benchmark standar industri: fio (Flexible I/O Tester).
Mari kita buat manifest Pod pengujian sementara yang akan me-mount PVC yang ingin kita uji:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: benchmark-test-pvc
namespace: default
spec:
accessModes:
- ReadWriteOnce
storageClassName: secure-database-sc
resources:
requests:
storage: 20Gi
---
apiVersion: v1
kind: Pod
metadata:
name: storage-benchmark-agent
namespace: default
spec:
restartPolicy: Never
containers:
- name: benchmark-container
image: alpine:latest
command: ["/bin/sh", "-c", "apk add --no-cache fio && sleep infinity"]
volumeMounts:
- name: target-storage
mountPath: /data
volumes:
- name: target-storage
persistentVolumeClaim:
claimName: benchmark-test-pvc
Setelah Pod berstatus Running, masuk ke dalam terminal kontainer tersebut:
kubectl exec -it storage-benchmark-agent -n default -- sh
1. Pengujian Random Read/Write (Simulasi Database OLTP) #
Jalankan perintah fio berikut untuk mensimulasikan beban kueri database baca-tulis acak dengan ukuran blok 8KB:
fio --name=db-simulation \
--directory=/data \
--ioengine=libaio \
--rw=randrw \
--rwmixread=75 \
--bs=8k \
--iodepth=64 \
--size=5G \
--direct=1 \
--runtime=60 \
--time_based \
--group_reporting
--rw=randrw&--rwmixread=75: Simulasi beban kueri acak campuran dengan porsi 75% operasi baca dan 25% operasi tulis.--direct=1: Melewati cache sistem operasi kontainer (page cache) untuk memastikan kita menguji performa disk fisik secara murni.
2. Pengujian Sequential Write Throughput (Simulasi Log / Kafka) #
Jalankan perintah berikut untuk mengukur bandwidth penulisan file besar secara berurutan:
fio --name=sequential-write-test \
--directory=/data \
--ioengine=libaio \
--rw=write \
--bs=1M \
--iodepth=4 \
--size=10G \
--direct=1 \
--runtime=60 \
--time_based \
--group_reporting
Perhatikan hasil output pada bagian IOPS dan BW (Bandwidth/Throughput) untuk mengetahui kapasitas kinerja penyimpanan kluster kita yang sebenarnya.
Anti-Pattern vs Solusi dalam Storage Performance #
Mari kita pelajari beberapa kesalahan konfigurasi performa penyimpanan (anti-pattern) yang sering terjadi di kluster produksi beserta cara memperbaikinya.
Anti-Pattern 1: Menggunakan Tipe Disk Standard HDD untuk Workload Database #
Kita menggunakan default StorageClass bertipe HDD magnetik standar untuk menampung data database PostgreSQL dengan tujuan meminimalkan pengeluaran biaya cloud.
Kode Manifest Salah (StorageClass HDD dengan Latensi Tinggi) #
# JANGAN GUNAKAN INI UNTUK DATABASE PRODUKSI
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: cheap-hdd-sc
provisioner: ebs.csi.aws.com
# Menggunakan tipe volume sc1 (Cold HDD) yang lambat untuk kueri acak
parameters:
type: sc1
Kode Solusi (StorageClass SSD gp3 Teroptimasi) #
# SOLUSI: Konfigurasi GP3 SSD dengan provisioned IOPS dan Throughput
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: optimized-database-gp3-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
parameters:
type: gp3
iops: "5000" # Ditingkatkan dari baseline 3000 ke 5000 IOPS
throughput: "250" # Throughput disetel ke 250 MB/s untuk menunjang query berat
encrypted: "true"
Anti-Pattern 2: Menjadwalkan Pod DB Performa Tinggi pada VM Node yang Kecil #
Kita mengonfigurasi database StatefulSet dengan PVC SSD super cepat (10.000 IOPS), tetapi kita membiarkan Pod database tersebut terjadwal pada node worker bertipe instans VM kecil yang tidak memiliki dukungan dedicated bandwidth penyimpanan cloud.
Kode Manifest Salah (Database Berjalan di Node Kecil Tanpa Kendali Penjadwalan) #
# JANGAN LAKUKAN INI: Database rentan ter-throttling di level node VM
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: high-perf-mysql
namespace: production
spec:
replicas: 1
serviceName: mysql-service
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
# Tidak ada nodeSelector / affinity: Pod bebas mendarat di node VM t3.medium kecil
Kode Solusi (Menerapkan NodeAffinity ke Node Pool Teroptimasi) #
Kita harus mengelompokkan worker node kita ke dalam node pool khusus yang berisi VM berkinerja tinggi (teroptimasi EBS, misalnya tipe instans m5.xlarge ke atas), lalu memberi label pada node tersebut.
# SOLUSI: Mengunci penempatan Pod database pada node pool khusus berkinerja tinggi
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: high-perf-mysql
namespace: production
spec:
replicas: 1
serviceName: mysql-service
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node.kubernetes.io/instance-type-group
operator: In
values:
- storage-optimized-nodes # Hanya jadwalkan Pod di node pool m5.xlarge/m5.2xlarge
containers:
- name: mysql
image: mysql:8.0
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "4"
memory: "8Gi"
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
storageClassName: optimized-database-gp3-sc
resources:
requests:
storage: 100Gi
Ringkasan #
- Identifikasi profil I/O aplikasi: Pilih alokasi penyimpanan berdasarkan prioritas kebutuhan aplikasi. Database transaksional membutuhkan IOPS tinggi dan latensi rendah, sedangkan Kafka/Logs membutuhkan throughput yang besar.
- Gunakan Local Persistent Volume untuk latensi nol: Untuk beban kerja yang sangat sensitif terhadap latensi kueri, NVMe lokal adalah solusi terbaik dengan syarat manajemen replikasi data dikelola di tingkat aplikasi.
- Bandwidth I/O terikat pada ukuran VM node: Jangan memasang disk StorageClass berspesifikasi tinggi pada instans VM node yang kecil untuk menghindari pemotongan (throttling) performa secara sepihak oleh infrastruktur cloud.
- Lakukan benchmark empiris dengan
fio: Selalu lakukan uji coba baca-tulis menggunakan utilitasfiolangsung di dalam Pod kluster untuk mengukur IOPS dan bandwidth riil sebelum merilis sistem ke produksi.- Terapkan Node Affinity untuk penempatan Pod: Pastikan Pod database yang membutuhkan performa tinggi dikunci penempatannya ke node pool yang berisi mesin-mesin VM berspesifikasi tinggi yang teroptimasi untuk operasi penyimpanan.
← Sebelumnya: Anti-Pattern Storage Berikutnya: Kubernetes Network Model →