CPU, seperti yang kita bedah di CPU Limit dan Reservation, ditegakkan dengan cara yang sopan: pelanggar hanya ditunda sampai jendela penjadwalan berikutnya. Memori tidak diberi kesopanan itu — halaman yang sudah dialokasikan tidak bisa dikembalikan sedikit demi sedikit, sehingga langit-langitnya ditegakkan dengan satu senjata: OOM killer yang menjatuhkan SIGKILL tanpa negosiasi. Kita sudah menyaksikan hukuman itu berlangsung di chapter CPU dan Memory Limit lewat container lapar yang tumbang di plafon 100 MB; tim toko bahkan sudah merasakan versi dunia nyatanya, ketika backend tumbang OOM saat laporan rekap dan sejak itu menjalani hidup dengan pagar 256 MiB di compose.yaml. Yang belum kita bedah adalah ruang di antara hidup dan mati itu: apa sebenarnya yang dikendalikan --memory-swap, bagaimana kernel memutuskan siapa yang dibunuh, dan bagaimana menggeser nasib korban bila pembunuhan tak terhindarkan. Chapter ini membuka ketiganya, lalu menutupnya dengan eksperimen yang memperlihatkan container yang sama mati di 128 MB pada satu konfigurasi dan bertahan sampai 256 MB pada konfigurasi lain.
Recap Langit-langit dan Jaminan Lunak #
Dua flag berikut sudah kita kuasai; tabel ini cukup menjadi pengingat sebelum kita naik level:
| Flag | Peran | Contoh |
|---|---|---|
--memory | langit-langit keras — pelanggar dibunuh OOM killer | --memory 256m |
--memory-reservation | jaminan lunak saat memori host tertekan | --memory-reservation 128m |
Keduanya dibuktikan di chapter CPU dan Memory Limit: --memory 256m terbaca sebagai 268435456 byte di docker inspect, dan reservasi di bawahnya menjaga porsi minimum tanpa pernah membunuh siapa pun. Satu istilah yang perlu ditegaskan sebelum lanjut: Docker menyebut reservasi ini soft limit di sebagian dokumentasinya — pada chapter ini kita konsisten memakai kata reservasi, karena itulah perilakunya yang sebenarnya.
Semantik Penuh –memory-swap #
--memory-swap adalah flag yang paling sering disalahpahami di keluarga memori, dan kesalahpahamannya lahir dari namanya: flag ini TIDAK mengatur besar swap, melainkan total gabungan RAM ditambah swap yang boleh dipakai container. Jatah swap murninya adalah nilai total dikurangi --memory. Dengan -m 128m sebagai patokan, seluruh wujud nilainya tampak seperti ini:
Nilai --memory-swap (dengan -m 128m) | RAM maks | Swap tambahan maks | Total maks | Perilaku saat tabrakan |
|---|---|---|---|---|
| tidak disetel | 128 MB | 128 MB | 256 MB | swap otomatis dipakai setelah RAM penuh |
128m (sama dengan --memory) | 128 MB | 0 | 128 MB | tanpa swap — OOM datang tepat di 128 MB |
-1 | 128 MB | sebatas sisa swap host | tak dibatasi | swap dipakai selama host masih punya |
256m | 128 MB | 128 MB | 256 MB | sama seperti tidak disetel, tetapi eksplisit |
Dua aturan main menyertai tabel itu. Pertama, daemon menolak nilai --memory-swap yang lebih kecil dari --memory — total yang lebih rendah dari RAM-nya sendiri memang mustahil; nilai yang sama persis boleh, dan itulah bentuk “tanpa swap”. Kedua, seluruh tabel tunduk pada kenyataan host: bila swap host mati atau habis, jatah swap di atas kertas tidak menolak apa pun — kebijakan swap host perlu diperiksa dengan swapon --show sebelum mengandalkan angka, peringatan yang sudah kita tanam di chapter CPU dan Memory Limit.
Eksperimen Mati di 128 atau Bertahan sampai 256 #
Sekarang kita pentaskan dua kematian yang berbeda umur. Korban tetap menggunakan image polinux/stress yang sudah ada di mesin latihan sejak chapter dasar: satu pekerja memori berusaha mengalokasikan dan menyentuh 300 MB. Konfigurasi pertama memasang total 128 MB — RAM penuh, tanpa swap:
$ docker run --name rapat -m 128m --memory-swap 128m polinux/stress --vm 1 --vm-bytes 300M --vm-hang 0Output yang Dihasilkan (dipotong):
stress: info: [1] dispatching hogs: 1 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL [1] (416) <-- worker 6 got signal 9
stress: WARN [1] (417) <-- reaping child worker 6
...$ docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' rapatOutput yang Dihasilkan:
true 137Drama ini berlangsung dalam hitungan detik dan berakhir persis di plafon. Jejak di log kernel menegaskannya:
$ sudo dmesg | grep -i 'out of memory' | tail -1Output yang Dihasilkan:
[ 5124.117893] Memory cgroup out of memory: Killed process 9187 (stress) total-vm:315392kB, anon-rss:125952kB, shmem-rss:0kB, UID:0 pgtables:264kB oom_score_adj:0Konfigurasi kedua membiarkan --memory-swap tidak disetel — total otomatis menjadi dua kali memori, 256 MB:
$ docker run --name longgar -m 128m polinux/stress --vm 1 --vm-bytes 300M --vm-hang 0Output yang Dihasilkan (dipotong; angka waktu ilustratif):
stress: info: [1] dispatching hogs: 1 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL [1] (421) <-- worker 6 got signal 9
stress: WARN [1] (422) <-- reaping child worker 6
...$ docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' longgarOutput yang Dihasilkan:
true 137Mari kita telaah apa yang membedakan dua kematian itu:
rapatmati cepat di sekitar 128 MB —anon-rss:125952kB(sekitar 123 MB) menunjukkan korban tumbang sedikit di bawah plafon; kernel memotong sebelum langit-langit benar-benar terlewati, dan pola FAIL-WARN berulang beberapa kali sebelum indukstressikut tumbang, persis pola containerlapardi chapter dasar.longgarmati lebih lambat di sekitar 256 MB — pekerja melewati 128 MB RAM dengan mulus karena kernel mulai menukar halaman-halamannya ke disk; aplikasi terus hidup, tetapi setiap sentuhan halaman yang sudah pindah ke disk berubah menjadi operasi I/O, sehingga drama yang sama datang belasan detik kemudian alih-alih seketika.- Keduanya berakhir
true 137— OOM killer tidak peduli apakah halaman korban ada di RAM atau di swap; begitu total gabungan tersentuh, vonisnya sama. - Bertahan bukan berarti sehat — bila pekerja hanya meminta 250 MB (di bawah total 256 MB), container
longgarakan terus hidup, tetapi lajunya merosot drastis karena halaman bolak-balik disk — kondisi swap thrashing. Latensi permintaan naik jauh sebelum kematian datang, dan bagi toko-backend pilihan kebijakannya nyata: mati cepat lalu bangkit bersih lewat restart policy, atau hidup lumpuh sambil menyeret seluruh pengguna.
Sebelum melupakan rapat dan longgar, satu kalimat untuk hati nurani: swap adalah disk, dan disk adalah tempat terlambat di komputer — jatah swap sebesar apa pun tidak mengubah kenyataan itu, ia hanya memindahkan kematian sedikit lebih jauh. Bekas eksperimen kita bereskan:
$ docker rm -f rapat longgarOutput yang Dihasilkan:
rapat
longgarSwappiness sebagai Knob yang Jarang Disentuh #
Ketiga flag di atas mengatur berapa banyak; satu knob lagi mengatur seberapa cepat: --memory-swappiness menerima nilai 0 sampai 100 yang mengendalikan seberapa agresif kernel menukar halaman container ke swap sebelum benar-benar terdesak. Angka rendah berarti kernel bertahan di RAM selama mungkin dan baru menukar saat tekanan sungguhan; angka tinggi berarti kernel menukar dengan semangat penuh sejak awal. Nilai bawaannya mengikuti host (umumnya 60), dan knob ini jarang perlu disetel karena default tersebut sudah berimbang — kesalahan umumnya justru memasang 0 dengan harapan “tanpa swap”, padahal 0 tetap mengizinkan penukaran darurat. Satu catatan zaman: padanan knob per-cgroup ini tidak hadir pada cgroups v2, sehingga di host modern nilainya bisa saja tidak berpengaruh sama sekali.
OOM Killer dari Dalam #
Sekarang kita buka mesin pembunuhnya. Limit yang kita pasang lewat --memory ditulis daemon ke berkas cgroup container — memory.max untuk plafon RAM dan memory.swap.max untuk jatah swap, pada host cgroups v2. Setiap permintaan halaman memori dari proses container melewati gerbang ini: bila penghitungan menunjukkan plafon tersentuh dan kernel tidak berhasil merebut halaman yang bisa direklamasi, kernel memicu peristiwa memory cgroup OOM di dalam cgroup container itu. Penilaian lalu berlangsung lokal: kernel menghitung skor keburukan untuk setiap proses yang hidup di cgroup tersebut dan menjatuhkan SIGKILL pada skor terburuk — korban terkurung rapi, tetangga tidak tersentuh, dan inilah pembeda utamanya terhadap OOM level host yang kita bahas di chapter Resource Limit.
Jejak peristiwanya sendiri sudah kita kenal sebagai rantai tiga lapis dari chapter Membaca Container Exit: baris Memory cgroup out of memory: Killed process ... di dmesg, field State.OOMKilled bernilai true, dan exit code 137 alias 128 + 9 di kolom status. Tiga sumber berbeda yang saling mengunci — dan sekarang kita tahu persis mesin apa yang menulis ketiganya.
Menonaktifkan Algojo dengan –oom-kill-disable #
Docker menyediakan flag untuk memohon pengecualian dari algojo: --oom-kill-disable meminta kernel tidak membunuh proses container saat cgroup-nya kehabisan memori. Tanpa --memory, flag ini nyaris tak berarti karena tidak ada cgroup OOM lokal untuk ditekan; dengan --memory, konsekuensinya justru lebih buruk dari kematian: alokasi memori mulai gagal, aplikasi menerima galat kehabisan memori dari sistem, dan biasanya jatuh dengan caranya sendiri — keluar dengan exception, atau menggantung tanpa bisa melayani siapa pun.
Menggeser Nasib dengan –oom-score-adj #
Bila pembunuhan tidak bisa dicegah, nasib korban setidaknya bisa digeser. Flag --oom-score-adj menerima nilai -1000 sampai 1000; semakin rendah nilainya, semakin kecil peluang proses container menjadi korban saat OOM — penilaian skor kernel memperhitungkan anggota setiap proses ini, dan nilai -1000 menjadikan proses kebal pembunuhan OOM. Kegunaan praktisnya pada host padat: service paling kritis — misalnya toko-db — diberi nilai rendah semacam -500, sehingga bila suatu saat OOM level host terjadi, kernel lebih memilih korban dari proses lain yang skornya lebih buruk. Perlu diingat bahwa flag ini hanya memindahkan nasib, bukan menambah memori: bila cgroup container sendiri yang OOM, penyesuaian skor di dalamnya tetap berujung satu korban dari container yang sama.
Satu catatan sejarah menutup bagian mesin ini: flag --kernel-memory, yang dulu membatasi memori kernel per container, telah usang dan diabaikan pada kernel modern — cukup kenal namanya agar tidak terkejut membacanya di skrip lama warisan tim.
Tabel Keputusan Limit Reservasi dan Swap #
Ketiga knob memori kini punya wajah masing-masing, dan resep pemadannya mengikuti sifat aplikasi:
| Situasi container | Resep yang cocok |
|---|---|
| cache stateless yang boleh mati kapan saja | --memory ketat, --memory-swap sama dengan --memory — tanpa swap, mati cepat, bangkit bersih |
| backend atau database yang sakit bila mati | --memory + --memory-swap longgar (atau -1 pada host dengan swap sehat) + --memory-reservation di bawahnya |
| sandbox development di laptop | --memory saja sudah cukup; sisanya berisik untuk kebutuhan sekecil itu |
| service paling kritis di host ramai | resep backend ditambah --oom-score-adj rendah untuk menggeser nasib |
Benang merahnya: swap layak dipakai sebagai ruang napas untuk beban yang benar-benar sementara, bukan sebagai penampung kebocoran memori permanen — kebocoran yang ditampung swap hanya menunda kematian sambil menambahi penderitaan latensi.
Jejak di docker inspect #
Seluruh knob memori meninggalkan jejak angka byte yang bisa dibaca ulang. Kita pasang satu container dengan kombinasi lengkap:
$ docker run -d --name terukur -m 128m --memory-reservation 96m --memory-swap 256m alpine:3.20 sleep 600Output yang Dihasilkan:
7b2e9d4ac1f8e3a6b5d0c9f2e7a4b1d8c3f6e9b2a5d0c7f4e1b8a3d6c9f2e5b7$ docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.MemorySwap}}' terukurOutput yang Dihasilkan:
134217728 100663296 268435456Mari kita telaah tiga angka raksasa itu:
134217728— plafon RAM 128 MB dalam byte polos; 128 dikali 1024 dua kali.100663296— reservasi 96 MB; jaminan lunak yang berdiri di bawah plafon.268435456— total RAM ditambah swap 256 MB; dengan plafon RAM 128 MB, jatah swap murninya 128 MB — aritmetika--memory-swapdikurangi--memoryyang kita dapatkan di tabel semantik.
Kita pamitkan aktor terakhir hari ini:
$ docker rm -f terukurOutput yang Dihasilkan:
terukurMemori kini terpetakan tuntas dari plafon sampai algojo: langit-langit keras, reservasi lunak, ruang napas swap, sampai cara menggeser korban. Namun ada satu kelas kecelakaan yang tidak dihitung dalam byte maupun detik: container yang mati bukan karena kehabisan ruang, melainkan karena kehabisan slot — setiap proses dan thread di dalamnya menempati satu kursi di tabel proses kernel, dan satu baris kode yang salah cukup untuk melahirkan jutaan kursi dalam hitungan detik. Chapter berikutnya, PIDs Limit dan Ulimit, memasang pagar pada jumlah itu: --pids-limit yang membuat fork bomb tumbang di dalam kandangnya, serta --ulimit yang mewariskan batas klasik Unix ke setiap proses container.
Referensi #
- Runtime options with Memory, CPUs, and GPUs
— semantik resmi
--memory,--memory-swap,--memory-swappiness, dan flag OOM. - docker container run — daftar flag memori lengkap beserta bentuk nilainya.
- docker container inspect
— pembacaan
HostConfig.Memory,MemoryReservation, danMemorySwaplewat template.