- Meskipun batas CPU diterapkan pada kontainer, runtime Go pada dasarnya tidak menyadarinya, sehingga dapat membuat thread berdasarkan seluruh inti host dan meningkatkan latensi
- GC Go sebagian besar berjalan secara bersamaan dengan aplikasi, tetapi memerlukan bagian stop-the-world(STW) yang menghentikan semua goroutine pada Sweep Termination dan Mark Termination
- Linux CFS membagi dan mengalokasikan waktu CPU per detik berdasarkan jumlah inti, dan
--cpus=4 berarti memberi kontainer jatah waktu CPU setara 4 detik setiap detik
- Jika kontainer dibatasi 4 core di host 16 core, Go dapat menempatkan goroutine pada 16 thread OS, sehingga setelah kuota CPU habis STW bisa menjadi lebih panjang
- Jika
GOMAXPROCS disesuaikan dengan batas CPU kontainer, pada contoh ini siklus GC turun dari di bawah 2.5ms menjadi di bawah 1ms, dan STW berkurang hingga sekitar 26μs
Ketidaksesuaian antara batas CPU kontainer dan runtime Go
- Saat menjalankan aplikasi Go di dalam kontainer, batas CPU berfungsi mencegah konsumsi seluruh CPU host
- Masalahnya, runtime Go secara default tidak mengenali batas CPU milik kontainer
- Karena ketidaksesuaian ini, runtime menganggap ia bisa memakai CPU lebih banyak daripada quota sebenarnya, yang berujung pada latensi tinggi
Titik terjadinya STW pada GC Go
- Garbage collector Go berjalan secara bersamaan dengan aplikasi hampir sepanjang waktu
- Namun, ada dua bagian dalam proses GC yang mengharuskan semua goroutine dihentikan
- Tahap penghentian sebelum Mark Phase untuk menerapkan write barrier disebut Sweep Termination
- Tahap penghentian setelah Mark Phase untuk menghapus write barrier disebut Mark Termination
- Bagian STW biasanya berada di kisaran beberapa puluh mikrodetik
- Aplikasi contoh adalah aplikasi web sederhana yang banyak melakukan alokasi memori, dan source code tersedia di go-cfs-blog
- Kontainer dijalankan dengan batas 4 CPU
docker run --cpus=4 -p 8080:8080 $(ko build -L main.go)
- Trace dapat dikumpulkan dengan paket
runtime/trace dan dianalisis dengan go tool trace
- Pada eksekusi ini, siklus GC kurang dari 2.5ms, tetapi hampir 10% di antaranya adalah bagian STW
- Untuk aplikasi yang sensitif terhadap latensi, proporsi sebesar ini pun bisa menjadi masalah
Cara kerja batas CPU Docker dan Linux CFS
- Batas CPU
--cpus di Docker adalah hard limit
--cpu-shares juga bisa diatur, tetapi hanya dipaksakan saat host mengalami keterbatasan CPU
- Jika host masih longgar, kontainer bisa memakai lebih banyak daripada inti CPU yang dialokasikan
- Saat host masuk kondisi terbatas, aplikasi akan benar-benar dibatasi
- Linux Completely Fair Scheduler(CFS) diperkenalkan pada Linux 2.6.23 dan menjadi scheduler default hingga sebelum Linux 6.6
- CFS adalah proportional share scheduler, yang menetapkan weight proses secara proporsional terhadap jumlah inti CPU yang dapat digunakan
- Weight proses yang dapat memakai 4 inti CPU adalah 4
- Weight proses yang dapat memakai 2 inti CPU adalah 2
- CFS membagi dan mengalokasikan waktu CPU
- Sistem 4 core dapat mengalokasikan waktu CPU setara 4 detik setiap detik
- Mengalokasikan sejumlah inti CPU ke kontainer pada dasarnya sama dengan meminta waktu sebanyak
n CPU kepada scheduler Linux
--cpus=4 berarti kontainer menerima waktu CPU setara 4 detik setiap detik
Mengapa STW menjadi lebih panjang
- Runtime Go membuat satu thread OS untuk setiap inti CPU saat startup
- Pada mesin 16 core, ia dapat membuat 16 thread OS terlepas dari batas CPU CGroup
- Runtime menjadwalkan goroutine di atas thread-thread OS tersebut
- Meskipun batas CPU kontainer hanya 4 core, Go tetap dapat menempatkan goroutine pada seluruh 16 thread OS
- Dalam kondisi ini, runtime akan berharap bisa menggunakan waktu CPU setara 16 detik setiap detik
- Waktu STW yang panjang terjadi karena semua goroutine harus dihentikan, termasuk yang berada di thread yang sedang menunggu dijalankan kembali oleh scheduler Linux
- Setelah kontainer sudah menghabiskan quota CPU-nya, thread-thread tersebut tidak akan dijadwalkan
Menyesuaikan GOMAXPROCS dengan quota CPU
- Go dapat membatasi jumlah thread CPU yang digunakan runtime melalui variabel lingkungan
GOMAXPROCS
- Pada kontainer dengan quota CPU 4, tetapkan juga
GOMAXPROCS=4
docker run --cpus=4 -e GOMAXPROCS=4 -p 8080:8080 $(ko build -L main.go)
- Pada aplikasi dan beban yang sama, setelah
GOMAXPROCS disamakan dengan quota CPU, waktu GC menjadi lebih singkat
- Dalam trace, siklus GC turun menjadi di bawah 1ms, dan bagian STW menjadi 26μs
- Dibandingkan waktu STW saat tidak ada batas
GOMAXPROCS, ini sekitar 1/10-nya
GOMAXPROCS harus diatur sesuai jumlah inti CPU yang bisa dipakai kontainer
- Jika mengalokasikan fractional CPU, bulatkan ke bawah
- Jika mengalokasikan kurang dari 1 CPU, bulatkan ke atas
- Rumusnya adalah
GOMAXPROCS=max(1, floor(CPUs))
- automaxprocs dari Uber adalah library open source yang menghitung nilai ini secara otomatis dari cgroups kontainer
- Sebuah GitHub Issue telah dibuka untuk menambahkan dukungan default di runtime Go
Hal yang perlu dicek pada layanan Go yang dikontainerkan
- Menetapkan batas CPU saja tidak cukup; GOMAXPROCS juga harus disesuaikan agar runtime Go mencerminkan batas tersebut
- Jika sulit menghitungnya secara manual, nilai berbasis cgroups bisa diatur otomatis dengan library seperti automaxprocs
- Layanan Go yang sensitif terhadap latensi perlu memeriksa waktu STW di GC trace untuk memastikan quota CPU dan konfigurasi runtime tidak saling bertentangan
1 komentar
Komentar Hacker News
Masalah yang umum terlihat di berbagai bahasa adalah aplikasi mendeteksi jumlah core mesin dengan melihat /proc/cpuinfo
Namun di dalam kontainer Docker atau teknologi kontainer lain, file ini terlihat sama persis seperti host kontainer, dan mencantumkan semua core, berapa pun jumlah yang sebenarnya dialokasikan ke kontainer
Untuk beberapa waktu saya sempat berpikir Docker mungkin bisa membuat /proc/cpuinfo palsu yang hanya mencantumkan “Docker CPU” yang dialokasikan ke tugas, tetapi setelah dipikir lagi sepertinya itu tidak akan berhasil karena beberapa alasan
Yang dibatasi adalah berapa lama kontainer bisa menggunakan core-core tersebut
Ada juga pengecualian, dan dokumentasinya ada di sini: https://kubernetes.io/docs/tasks/administer-cluster/cpu-mana...
nproc, dan pernah melihatnya dipakai di kontainer lain sepertibundle install -j $(nproc)Ini menghormati alokasi CPU, jadi menyediakan fungsi yang dicari
Saya tidak tahu apakah aplikasi sembarang memakai nproc ketika memungkinkan
“Mencetak jumlah unit pemrosesan yang tersedia untuk proses saat ini, yang bisa lebih sedikit daripada jumlah prosesor online. Jika informasi ini tidak dapat diperoleh, mencetak jumlah prosesor yang terpasang”
https://www.gnu.org/software/coreutils/manual/html_node/npro...
https://www.flamingspork.com/blog/2020/11/25/why-you-should-...
Go melihat jumlah CPU mask pada saat startup, lalu tidak melihatnya lagi setelah itu
Di Kubernetes, ini menjadi masalah karena CPU yang terlihat saat proses berjalan bisa berubah
lxcfs adalah filesystem FUSE yang meniru /proc dengan menyimpulkan nilai cgroup, sehingga aplikasi dan library tidak perlu peduli apakah mereka berjalan di dalam kontainer
Misalnya, /proc/uptime seharusnya mencerminkan waktu aktif kontainer, bukan host, dan /proc/cpuinfo mencerminkan batas yang lebih rendah di antara kombinasi cpu.max dan cpuset.cpus sebagai jumlah CPU
Inferensi jumlah CPU juga bisa dilakukan dengan system call
sched_getaffinity, dan cara ini tidak bergantung pada /proc/cpuinfoJadi, bergantung pada library yang dipakai, Anda bisa berada dalam situasi yang menyulitkan
Penjelasan ini agak keliru secara halus
Dari sudut pandang Docker, ekstensi CFS cgroup memiliki beberapa kenop yang bisa disetel:
cfs_quota_us,cfs_period_us(nilai default umum adalah 100ms, bukan 1 detik), dan sharesJika shares disetel, penjadwalan proporsional berbasis bobot diterapkan, tetapi itu hanya berarti saat ada kontensi
Dua nilai pertama memberlakukan quota yang ketat
Daripada memakai flag
--cpuDocker, lebih baik memakai--cpu-sharesuntuk menghindari pemaksaan quota yang umumnya tidak bergunaMenurut dokumentasi Linux,
cpu.sharesadalah bobot tiap grup pada hierarki yang sama, dancpu.cfs_period_usadalah periode scheduler untuk menentukan bandwidth, dengan default 100000us atau 100mscpu.cfs_quota_usadalah waktu maksimum grup saat ini dapat berjalan selama setiapcfs_period_us, dan nilai ini adalah waktu akumulatif di seluruh CPU sistem, jadi agar bisa memakai 2 CPU sepenuhnya, nilainya harus disetel menjadi dua kalicfs_period_us--cpuDocker, sebagai gantinya…” terlalu kuat tanpa petunjuk tambahanSama sekali tidak bisa dianggap “umumnya tidak berguna”
shares dan quota ditujukan untuk use case yang berbeda, jadi pahami use case Anda sendiri dan pilih yang sesuai
--cpu, aplikasi dapat mendeteksinyaMungkin karena memakai cpuset
Jika memakai quota, aplikasi tidak bisa mendeteksinya, sehingga besar kemungkinan akan membuat thread jauh lebih banyak daripada yang diperlukan
Saya akan coba memperjelas bagian ini
Saya pikir gejalanya muncul seperti ini, tetapi saya perlu membuat ungkapannya lebih jelas
Aplikasi harus berperilaku dengan benar
Jika memakai CPU reservations alih-alih CPU limits, penyesuaian seperti ini tidak diperlukan: https://home.robusta.dev/blog/stop-using-cpu-limits
CPU reservations juga pada dasarnya adalah limits, tetapi dideklarasikan sebagai batas implisit sekaligus jaminan
Jadi biarkan runtime Go memakai semua CPU yang tersedia, dan jika terjadi perebutan CPU, biarkan scheduler Linux membatasinya sesuai reservations yang dideklarasikan
Melainkan karena tidak ingin terbiasa dengan kondisi bisa memakai CPU berlebih yang tidak dijamin
Saat node semakin terisi pod lain, pod yang baru saja berjalan baik bisa tiba-tiba melambat
Dengan limits, kita bisa menyimulasikan perilaku yang sama dan bersiap dengan perencanaan kapasitas yang tepat
Itu bukan satu-satunya cara, tetapi cara yang paling sederhana
Saya ingin tahu lebih banyak soal diskusi ini, tetapi artikel yang ditautkan sepertinya hanya membahas anggapan orang bahwa limit diperlukan untuk menjamin CPU bagi semua pod
Artikel ini sendiri tidak salah dan secara umum lebih dekat ke content marketing, tetapi kesimpulannya terlalu luas dan mengabaikan beberapa alasan bagus untuk menetapkan limits
Ada juga tulisan dari tempat yang sama yang memang keliru: https://home.robusta.dev/blog/containers-dont-use-chroot
Ada workload yang menghabiskan semua kapasitas burst hanya demi keuntungan kecil, dan ada kalanya kapasitas burst HTTP server harus diprioritaskan dibanding cronjob yang akan selesai dalam waktu tertentu
Pernah juga ada kasus developer tidak memperbarui requests meski kebutuhan aplikasi meningkat, lalu terjadi insiden ketika waktu CPU luang tiba-tiba berkurang
Secara teori itu adalah resource minimum yang dijamin, tetapi jika container-container yang sibuk berjalan bersama di host yang sama, tail latency dan latency rata-rata bisa meningkat secara tidak wajar
Latency pada instans EC2 4 core saat utilisasi CPU 50% dan 90% cukup berbeda
Pada reservations pun mirip: meski tiap container mendapatkan reservation-nya, proses sibuk lain di host yang sama membuat utilisasi CPU relatif menjadi sangat tinggi
OOMKiller bisa mengambilnya
Jika CPU dan memory limits sama-sama tidak ada, kita tidak bisa mendapatkan kelas Guaranteed QoS, jadi pada titik tertentu pod bisa saja dieviction
Saya beberapa kali kena masalah CFS scheduler saat memakai container dan cgroup
Penasaran scheduler baru itu apa
Ada yang di sini sudah memakainya di cluster produksi?
Sudah hampir 20 tahun kita membuang-buang core: https://people.ece.ubc.ca/sasha/papers/eurosys16-final29.pdf
Masalahnya container memasang batas resource, tetapi Go, sebagai proses di dalam container, tidak memeriksa fitur sistem operasi yang dipakai untuk batas itu saat menghitung jumlah paralelisme yang tersedia
Selain GOMAXPROCS, rilis Go terbaru juga punya GOMEMLIMIT
Dengan https://github.com/KimMachineGun/automemlimit, batas ini bisa disetel otomatis mirip seperti https://github.com/uber-go/automaxprocs
Tahun lalu di tempat kerja sebelumnya, saat menjadi platform engineer dan mengelola cluster Kubernetes on-premises serta infrastruktur pipeline CI/CD, saya menemukan ini
Saya melihat ketidaksesuaian antara CPU fisik dan CPU yang dialokasikan menimbulkan masalah, terutama CPU throttling, tetapi sulit menemukan solusi yang bisa diskalakan dan berdampak pada semua deployment Go di cluster
Meminta semua developer dari ratusan proyek menambahkan dependency autoprocs bukan pilihan
Alternatif seperti menyamakan semua CPU request/limit ke bilangan bulat lalu memasukkan nilai itu sebagai environment variable GOMAXPROCS di manifest Kubernetes juga merepotkan dan tidak realistis
Pada akhirnya kami menerapkan variabel GOMAXPROCS hanya pada sebagian aplikasi yang banyak memakai multi-threading dan mendapatkan perbaikan, tetapi kami masih belum menemukan solusi yang bisa diterapkan ke semua deployment dalam arsitektur microservices, ketika kebutuhan CPU tiap proyek sangat bervariasi
Membatasi GOMAXPROCS bisa menimbulkan masalah latency serius ketika traffic menumpuk pada proses dan queueing-nya sederhana
Terlepas dari gagasan tentang berapa banyak waktu yang rata-rata akan dipakai proses, dalam praktiknya yang terbaik adalah menyetel GOMAXPROCS ke nilai yang disediakan hardware
Dari sudut pandang orang yang tidak akrab dengan Docker atau Go, saya penasaran apakah perilaku ini memang disengaja
Bisakah tim Go membuatnya mengenali CGroups limit?
Apakah runtime lain juga berperilaku serupa?
Atau yang dimaksud runtime seperti containerd?
Waktu itu di Scala
Ada juga teknik GC yang membuat jeda menjadi lebih singkat
Misalnya, dengan menjalankan pekerjaan yang seharusnya dilakukan saat jeda secara bersamaan, lalu mengulanginya lagi di titik aman
Harapannya, berkat pekerjaan konkuren itu, pekerjaan di titik aman berubah menjadi pengecekan sederhana bahwa “tidak ada yang perlu dikerjakan”
Jika pekerjaan digandakan, throughput GC bisa memburuk
Tulisan ini membahas container, tetapi masalahnya tampaknya muncul kapan pun Go hanya bisa mengakses waktu CPU yang lebih sedikit daripada yang diharapkan
Bukankah hal yang sama terjadi jika menjalankan Go di sistem yang juga memiliki proses lain yang memakai CPU?
Bahkan mungkin cukup dengan menjalankan dua program Go secara bersamaan?