Optimasi concurrency pada chapter sebelumnya ditutup dengan satu pengamatan penting: dua aplikasi dengan throughput yang sama bisa terasa sangat berbeda bagi penggunanya. Perbedaannya bersumber dari dua metrik yang sering tercampur — latency, waktu yang dirasakan satu operasi dari dikirim sampai selesai, dan throughput, jumlah operasi yang diselesaikan sistem per satuan waktu. Keduanya memakai satuan berbeda, menjawab pertanyaan berbeda, dan kerap bergerak berlawanan ketika kita menyetel sistem. Chapter ini memisahkan keduanya dengan matematika yang bisa dihitung ulang: hukum Little sebagai jembatan antar-metrik, persentil sebagai cara membaca latensi, dan batching sebagai contoh trade-off klasik.
Dua Metrik Dua Pertanyaan #
| Metrik | Pertanyaan yang dijawab | Satuan lazim | Tempat terbaca |
|---|---|---|---|
| Latency | berapa lama satu operasi selesai | milidetik (sebagai distribusi) | benchmark AverageTime, log akses |
| Throughput | berapa banyak operasi per satuan waktu | req/s, item/s, op/s | benchmark Throughput, load test |
Latency adalah pengalaman satu pengguna pada satu momen; throughput adalah kapasitas agregat sistem. Menyetel yang satu hampir selalu menggeser yang lain: memperbesar antrean batch menaikkan throughput namun menambah waktu tunggu tiap item. Keputusan yang benar bergantung pada siapa konsumen sistemnya, maka kedua metrik harus dipilih sadar, bukan diukur campur.
Hukum Little sebagai Jembatan #
Hukum Little menyatakan L = λ × W: jumlah request yang sedang berada di dalam sistem (in-flight) sama dengan laju kedatangan dikali waktu rata-rata tiap request berada di sistem. Untuk perencanaan kapasitas, bentuk λ = L / W lebih sering dipakai — throughput maksimal sebuah sistem stabil ditentukan konkurensi dan latensinya:
/**
* Kalkulator latency vs throughput atas model tetap: hukum Little
* (L = lambda x W), persentil dari sampel latensi beku, dan trade-off
* batching dengan biaya tetap. Seluruh angka adalah hitungan murni.
*/
public class HukumLittle {
public static void main(String[] args) {
// ---- 1. Hukum Little: L (in-flight) = lambda x W ----
System.out.println("== 1. Hukum Little: L (in-flight) = lambda x W ==");
System.out.printf("%6s %8s %16s%n", "L", "W (ms)", "lambda (req/s)");
for (int[] baris : new int[][] {{8, 50}, {8, 200}, {16, 50}, {16, 200}}) {
double lambda = 1000.0 * baris[0] / baris[1];
System.out.printf("%6d %8d %16.0f%n", baris[0], baris[1], lambda);
}
double butuh = 100.0 * 100 / 1000; // lambda 100 req/s, W 100 ms
System.out.println("kebutuhan terbalik: target 100 req/s pada W = 100 ms butuh L = "
+ (int) Math.ceil(butuh));
System.out.println();
// ---- 2. Persentil dari 20 sampel latensi tetap ----
int[] latensi = {12, 12, 13, 13, 13, 14, 14, 14, 15, 15,
16, 16, 17, 18, 19, 21, 24, 30, 48, 96};
int total = 0;
for (int x : latensi) {
total += x;
}
System.out.println("== 2. Distribusi latensi: 20 sampel tetap (ms) ==");
System.out.printf("mean = %.1f%n", (double) total / latensi.length);
System.out.println("p50 = " + persentil(latensi, 50));
System.out.println("p90 = " + persentil(latensi, 90));
System.out.println("p99 = " + persentil(latensi, 99)
+ " (pada 20 sampel, p99 = nilai maksimum)");
int duaTermahal = latensi[latensi.length - 1] + latensi[latensi.length - 2];
System.out.printf("10%% sampel termahal menyumbang %.1f%% total waktu tunggu%n",
100.0 * duaTermahal / total);
System.out.println();
// ---- 3. Trade-off batching: biaya tetap 2 ms + 1 ms per item ----
System.out.println("== 3. Batching: biaya tetap 2 ms + 1 ms per item ==");
System.out.printf("%8s %12s %9s%n", "ukuran", "item/s", "ms");
for (int batch : new int[] {1, 10, 25, 50}) {
double waktuMs = 2.0 + batch;
double throughput = 1000.0 * batch / waktuMs;
System.out.printf("%8d %12.0f %9.0f%n", batch, throughput, waktuMs);
}
System.out.println("batch 10 menangkap " + Math.round(100.0 * 10 / 12)
+ "% dari throughput maksimal teoretis (1000 item/s)");
System.out.println();
System.out.println("Kesimpulan: batching menaikkan throughput dan latensi");
System.out.println("bersamaan; SLA yang kita pegang menentukan pilihan.");
}
// Persentil nearest-rank: nilai pada posisi ceil(p/100 x n) setelah terurut
static int persentil(int[] data, double p) {
int posisi = (int) Math.ceil(p / 100.0 * data.length);
return data[posisi - 1];
}
}Output yang Dihasilkan:
== 1. Hukum Little: L (in-flight) = lambda x W ==
L W (ms) lambda (req/s)
8 50 160
8 200 40
16 50 320
16 200 80
kebutuhan terbalik: target 100 req/s pada W = 100 ms butuh L = 10
== 2. Distribusi latensi: 20 sampel tetap (ms) ==
mean = 22.0
p50 = 15
p90 = 30
p99 = 96 (pada 20 sampel, p99 = nilai maksimum)
10% sampel termahal menyumbang 32.7% total waktu tunggu
== 3. Batching: biaya tetap 2 ms + 1 ms per item ==
ukuran item/s ms
1 333 3
10 833 12
25 926 27
50 962 52
batch 10 menangkap 83% dari throughput maksimal teoretis (1000 item/s)
Kesimpulan: batching menaikkan throughput dan latensi
bersamaan; SLA yang kita pegang menentukan pilihan.Mari kita telaah bagian-bagian kuncinya:
- Baris 10-15 (tabel Little): dua tuas terlihat jelas. Menggandakan
Ldari 8 ke 16 menggandakan throughput dari 160 menjadi 320 req/s padaW50 ms; memperbaikiWdari 200 menjadi 50 ms memberikan efek penggandaan yang sama tanpa menambah konkurensi. - Baris 16-18 (kebutuhan terbalik): untuk target 100 req/s pada latensi 100 ms, sistem membutuhkan
L = 10— sepuluh request yang benar-benar berjalan serentak. Angka ini menentukan ukuran connection pool sebelum load test pertama. - Baris 22-36 (distribusi latensi): dua puluh sampel tetap diringkas menjadi mean, p50, p90, dan p99 — rata-rata 22 ms tampak sehat sampai p99 menunjukkan 96 ms.
- Baris 55-58 (
persentil): metode nearest-rank mengambil nilai pada posisiceil(p/100 × n)dari data terurut — definisi sederhana yang konsisten dengan persentil yang dilaporkan alat ukur sungguhan.
Latensi Adalah Distribusi #
Tidak ada “latensi aplikasi 22 ms” — yang ada adalah distribusi, dan mean hanyalah satu ringkasannya. Output bagian kedua menunjukkan jaraknya: mean 22 ms, p50 15 ms, p90 30 ms, p99 96 ms. Pengguna tidak pernah mengalami rata-rata; setiap pengguna mengalami tepat satu sampel, dan pada layanan dengan 100 request per detik, p99 terjadi kira-kira sekali setiap detik. Ukuran tambahan pada output membuatnya kuantitatif: dua sampel termahal (10% dari data) menyumbang 32,7% dari seluruh waktu tunggu — memperbaiki ekor distribusi memberi dampak terbesar pada total waktu tunggu.
Trade-off Klasik Batching #
Bagian ketiga program memodelkan penulisan data dengan biaya tetap 2 ms per operasi plus 1 ms per item. Batch 10 item menaikkan throughput dari 333 menjadi 833 item/s dengan latensi naik dari 3 menjadi 12 ms; batch 50 hanya menambah throughput tipis (962) tetapi melipatgandakan latensi menjadi 52 ms. Batch 10 sudah menangkap 83% throughput maksimal teoretis — titik jenuh amortisasi biaya tetap datang cepat, dan sisanya dibayar dengan latensi. Untuk job malam ETL yang diukur item per detik, batching besar masuk akal; untuk API yang dihadapi pengguna interaktif, latensi per request biasanya lebih berharga.
Kedua metrik kini terpisah dengan rumus yang bisa dihitung ulang. Langkah terakhir section ini memastikan angka-angka itu tidak menguap diam-diam seiring kode berevolusi — pokok bahasan chapter penutup: performance regression testing.
Referensi #
- Oracle: Garbage Collection Tuning Guide — Introduction
- Oracle: Garbage Collection Tuning Guide — Ergonomics
- OpenJDK: JMH — Java Microbenchmark Harness
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
HukumLittle.java
Unduh berkas/**
* Kalkulator latency vs throughput atas model tetap: hukum Little
* (L = lambda x W), persentil dari sampel latensi beku, dan trade-off
* batching dengan biaya tetap. Seluruh angka adalah hitungan murni.
*/
public class HukumLittle {
public static void main(String[] args) {
// ---- 1. Hukum Little: L (in-flight) = lambda x W ----
System.out.println("== 1. Hukum Little: L (in-flight) = lambda x W ==");
System.out.printf("%6s %8s %16s%n", "L", "W (ms)", "lambda (req/s)");
for (int[] baris : new int[][] {{8, 50}, {8, 200}, {16, 50}, {16, 200}}) {
double lambda = 1000.0 * baris[0] / baris[1];
System.out.printf("%6d %8d %16.0f%n", baris[0], baris[1], lambda);
}
double butuh = 100.0 * 100 / 1000; // lambda 100 req/s, W 100 ms
System.out.println("kebutuhan terbalik: target 100 req/s pada W = 100 ms butuh L = "
+ (int) Math.ceil(butuh));
System.out.println();
// ---- 2. Persentil dari 20 sampel latensi tetap ----
int[] latensi = {12, 12, 13, 13, 13, 14, 14, 14, 15, 15,
16, 16, 17, 18, 19, 21, 24, 30, 48, 96};
int total = 0;
for (int x : latensi) {
total += x;
}
System.out.println("== 2. Distribusi latensi: 20 sampel tetap (ms) ==");
System.out.printf("mean = %.1f%n", (double) total / latensi.length);
System.out.println("p50 = " + persentil(latensi, 50));
System.out.println("p90 = " + persentil(latensi, 90));
System.out.println("p99 = " + persentil(latensi, 99)
+ " (pada 20 sampel, p99 = nilai maksimum)");
int duaTermahal = latensi[latensi.length - 1] + latensi[latensi.length - 2];
System.out.printf("10%% sampel termahal menyumbang %.1f%% total waktu tunggu%n",
100.0 * duaTermahal / total);
System.out.println();
// ---- 3. Trade-off batching: biaya tetap 2 ms + 1 ms per item ----
System.out.println("== 3. Batching: biaya tetap 2 ms + 1 ms per item ==");
System.out.printf("%8s %12s %9s%n", "ukuran", "item/s", "ms");
for (int batch : new int[] {1, 10, 25, 50}) {
double waktuMs = 2.0 + batch;
double throughput = 1000.0 * batch / waktuMs;
System.out.printf("%8d %12.0f %9.0f%n", batch, throughput, waktuMs);
}
System.out.println("batch 10 menangkap " + Math.round(100.0 * 10 / 12)
+ "% dari throughput maksimal teoretis (1000 item/s)");
System.out.println();
System.out.println("Kesimpulan: batching menaikkan throughput dan latensi");
System.out.println("bersamaan; SLA yang kita pegang menentukan pilihan.");
}
// Persentil nearest-rank: nilai pada posisi ceil(p/100 x n) setelah terurut
static int persentil(int[] data, double p) {
int posisi = (int) Math.ceil(p / 100.0 * data.length);
return data[posisi - 1];
}
}