Kubernetes’te Kaynak Yönetimi Stratejileri: CPU ve Memory Dengesi

Caner Türkaslan
Turk Telekom Bulut Teknolojileri
7 min readSep 13, 2023

Merhaba!
Bu makalede, Kubernetes üzerinde kaynak kullanımının önemli bir yönü olan “request” ve “limit” değerlerini neden, ne zaman ve nasıl kullanmalıyız sorularına odaklanacağız.

Bu konuyu daha iyi anlamak için Containerların nasıl çalıştığı, Linux Namespaceler, cgroups, cpu.cfs_quota_us, cpu.cfs_period_us, throttling ve OOM gibi kavramları değerlendirip farklı use-caseler ile birlikte değişen best practiceleri nasıl kullanabiliriz örneklerle açıklamaya çalışacağım.

Çayınızı, kahvenizi aldıysanız şimdi uzun ve ayrıntılı bir yolculuğa çıkıyoruz.

CFS, Quota ve Period

1. CFS (Complete Fair Scheduler): CFS, Linux işletim sisteminde kullanılan bir zaman paylaşımı algoritmasıdır ve multitasking ortamlarda CPU kaynaklarını adil bir şekilde paylaştırmayı amaçlar. Her bir çalışan processin CPU zamanını düzenlerken, her process’e mantıksal bir zaman dilimi (time slice) atar. Bu processler arasındaki geçişler, mantıksal zaman dilimlerinin bitişlerine bağlı olarak gerçekleşir.

2. cpu.cfs_period_us (CFS Period): cpu.cfs_period_us, bir cgroup (Control Group) içindeki processlerin CPU kullanımını sınırlamak için kullanılır. Bu değer, belirli bir periyotun uzunluğunu belirtir ve genellikle mikrosaniye cinsinden ifade edilir. Bu periyot boyunca, cgroup içindeki processler CFS tarafından ayrılan kaynakları kullanabilirler.

Örnek:

  • cpu.cfs_period_us değeri 100000 (100 ms) olarak ayarlanmış varsayalım. Bu, bir cgroup içindeki processlerin CPU kaynaklarını 100 ms'lik periyotlar halinde kullanabileceği anlamına gelir.

3. cpu.cfs_quota_us (CFS Quota): cpu.cfs_quota_us, bir cgroup içindeki processlerin belirli bir süre içinde ne kadar CPU kullanabileceğini sınırlamak için kullanılır. Bu değer de mikrosaniye cinsinden ifade edilir. cpu.cfs_quota_us değeri, bir periyot boyunca cgroup içindeki processlerin CPU kullanımını sınırlar.

Örnek:

  • cpu.cfs_quota_us değeri 50000 olarak ayarlanırsa, bir cgroup içindeki processlerin bir periyot boyunca yalnızca 50 ms süresince CPU kullanabileceği anlamına gelir.

Örnek Senaryo: Diyelim ki bir sistemde iki farklı cgroup var: A ve B. Her ikisi de aynı CPU’yu paylaşıyor(CPU Shares) ve her ikisi için de cpu.cfs_period_us 100000 olarak ayarlanmış durumda. Ancak, A cgroup'u için cpu.cfs_quota_us değeri 30000 olarak ayarlanmışken, B cgroup'u için cpu.cfs_quota_us değeri 60000 olarak ayarlanmış.

  • Cgroup A’daki processler, her periyotta 30 ms (30000 mikrosaniye) süreyle CPU kullanabilir.
  • Cgroup B’deki processler, her periyotta 60 ms (60000 mikrosaniye) süreyle CPU kullanabilir.

Bu sayede, sistem kaynaklarını farklı cgroup’lar arasında adil bir şekilde paylaştırabilirsiniz.

Containerlara Derin Bir Bakış: Linux Namespaceler ve Cgroups

Containerlar, aslında Linux namespaceleridir.
Linux namespaceler, bir Linux kernel fonksiyonudur ve aynı Linux namespace’indeki bir process veya process kümelerini içeren, bazı kernel resourceları görebilir ve diğer namespacelerdeki processlerden izole edebilir. Bazı namespace örnekleri; PID, UID, Cgroups ve IPC’dir.

Namespaceler hakkında bilinmesi gereken bir diğer şey, bu namespacelerin iç içe geçebilir olmalarıdır, yani namespace diğer namespacelerin içinde olabilir. Child namespaceler, parent namespacelerden izole edilir, ancak parent namespaceler, child namespaceler içinde olan her şeyi görebilir.

Containerı başlattığınızda, namespacelerin bir kümesini oluşturur ve uygulamanızı bu namespacelerin içinde çalıştırır. Bu nedenle bir containerın içinde, uygulamanızın PID’sini genellikle 1 olarak (veya çalıştırdığınız şeye bağlı olarak düşük bir sayı) görmüşsünüzdür, ancak containerın dışında (parent PID namespace’inde), uygulamanızın PID’si çok daha büyük bir sayıdır. Bu, aynı processtir. Ancak containerın içindeki PID, namespace alanındaki daha yüksek PID ile eşlenir ve diğer namespacelerden (diğer containerlar) izole edilir.

Namespaceler, processleri birbirinden izole etmemize olanak tanır demiştik, peki kaynak tüketimleri nasıl olacak? Tüm containerlarımız izole bir şekilde çalıştıklarını düşünüyorsa, diğerlerini etkilemeyecek şekilde çok fazla kaynak tüketebilirler. Bu duruma “noisy neighbors” denir.

Peki, noisy neighbors ile nasıl başa çıkabiliriz? İlk yaklaşım, her processin tüketebileceği kaynakları sınırlamaktır ve bu noktada Linux kernel bize “Control groups (Cgroups)” olarak bilinen başka bir özelliği sunar. Cgroups, her processin tüketebileceği kaynakları sınırlamak, hesaplamak ve izole etmek için yapılandırılır. Bu özelliği kullanarak Kubernetes, containerların kaynak kullanımını sınırlayabilmektedir.

Kubernetes 1.25 sürümü ile gelen Cgroups v2 aşağıdaki gibi çeşitli iyileştirmeler sunuyor.

  • API’da tek bir birleşik hiyerarşi tasarımı
  • Containerlara daha güvenli sub-tree yapısı ve yeteneği
  • Birden çok kaynak üzerinde geliştirilmiş resource allocation yönetimi ve izolasyonu
  • Farklı memory allocation tiplerine (network memory, kernel memory, vb.) yönelik hesaplamalar
  • Page cache write backs işlemleri gibi anında gerçekleşmeyen kaynak değişikliklerinin hesaplanması(disk I/O, in-memory cache)

Kubernetes de Memory Quality of Service(QoS) özelliği de Cgroups v2 temeline dayanıyor. Daha fazla bilgi için;

Kubernetes CPU Limit Kullanımı ve CPU Throttling

Çoğu zaman Kubernetes’te CPU limitlerini kullanmamız gerektiğini düşünüyoruz ancak bu yanlış bir yaklaşım. Bazı durumlar Kubernetes CPU limitleri yarardan çok zarar verir. Aslında bunlar Kubernetes CPU Throttling yaşanmasının bir numaralı nedenidir.
CPU throttling’i kaynakların kısıtlanması şeklinde açıklayabiliriz . Bu durum uygulama latency’sinin(gecikmesinin) ve response time’ın (cevap süresinin) artmasına sebep olur.

Yukarıda anlattığım konu başlıklarıyla beraber neden Kubernetes resource’unda CPU limit vermememiz gerektiğini ve CPU throttling’in hangi durumlarda uygulamanın response time’ına zarar verdiğini ele alalım.

Sıkıştırılabilir(Compressible) ve Sıkıştırılamaz(Incompressible) Kaynaklar

Sıkıştırılabilir kaynak, kaynağın kullanımı maksimuma ulaştığında, bu kaynağı kullanan processlerin, kaynak serbest kalana kadar beklemesidir. Başka bir deyişle processlerin throttling durumu yaşaması.

CPU sıkıştırılabilir bir kaynaktır; yani CPU kullanımı %100 ise CPU gerektiren bir processin CPU kaynağı serbest kalana kadar beklemesi gerekir.

Öte yandan, bir kaynağın sıkıştırılamaz olması, processlerin onu bekleyemeyeceği anlamına gelir; ya çalıştırılamazlar ya da başka bir processin kill edilip yeni process için kaynakları serbest bırakması gerekir.(Memory, OOM, Eviction)

Memory sıkıştırılamaz bir kaynaktır; yani memory yetersizse ve yeni veya mevcut bir process için memory allocate etmek istiyorsanız memory kullanan bir processi sonlandırmanız gerekir, aksi takdirde process crash olacaktır.

Kubernetesin yönettiği tek sıkıştırılabilir kaynak CPU’dur. Kubernetes’in yönettiği diğer kaynakların (memory, HugePages, ephemeral storage ve PID’ler) tümü sıkıştırılamaz.

CPU Throttling

Örnek:
İşlem başına 200 ms işlem süresine sahip, bir CPU üzerinde çalışan single-thread bir uygulamayı düşünelim. Aşağıdaki görselde isteği tamamlayan bir uygulama gösterilmektedir.

Single Core Throttling

Şimdi CPU limiti 0.5 CPU(limits.cpu=500m) olan bir uygulamayı düşünün. Uygulama her 100 ms periyot için(cfs_period_us) yalnızca 50ms runtime alabilecektir.

Bu, isteğin tamamlanmasının 200ms yerine toplam 350ms süreceği anlamına gelir böylece uygulama CPU throttling yaşayacaktır.

Kubernetes’te default, 100.000 olarak yapılandırılmıştır.(cpu.cfs_period_us 100000=100 ms)
Kubernetes sürümüne, dağıtımına, kullanılan işletim sistemine ve container runtime’na göre(Openshift, rancher, vanilla kubernetes, cri-o, docker, runc) yapılandırma değiştirilebilir. (container runtime, kubelet, kernel level vb.)

Multi-thread (Java thread-pool gibi) veya multi-process taskler için, aynı anda 4 CPU’ya sahip bir worker üzerinde çalışan uygulamayı düşünelim.
Periyodumuz yine 100ms ve 500m cpu limit verilmiş olsun.

Her 100 ms’de 12,5 ms tüketebiliriz ve periyotun geri kalan süresinde 87,5ms throttling yaşarız.(kısıtlanır.)

4 Core Throttling

Worker node ne kadar çok CPU’ya sahip olursa ve containerımız ne kadar çok cpu kullanırsa, throttling o kadar kötü olur; dolayısıyla aynı örneği alıp 8 CPU’lu bir node’da çalıştırırsak, 100ms periyotta container’a ayrılan kotanın yalnızca 6,25ms’de tüketecektir ve 93,75ms throttling yaşanacaktır!

16 veya 40 CPU olan bir node üzerinde de örnek vermek isterdim ama asıl noktayı anladığımızı düşünüyorum. 🙃

Best Practiceler ve Öneriler

Bir başka best practice yanlış anlaşılması;

CPU request’inizi veya limitinizi 1 virtual core veya altına ayarlamaktır. Bu yalnızca single-thread containerlar için geçerlidir.
Paralel işler için birden fazla container veya pod kullanmak, aynı iş için çalışan processleri bir pod’da çoğaltmaktan daha iyidir.

Ancak Java ve GoLang gibi diller dizayn gereği concurrent yapıdadır. Bu nedenle concurrency ile birlikte multi-thread kullanırken, uygulamanız gerektiriyorsa CPU requestiniz olarak kesinlikle 1'den fazla vCPU ayarlamak gerekecektir.

CPU resource’u için öneriler

CPU Requestleri, container’ın kullanmasını beklediğiniz değere göre ayarlayın. Beklenen CPU kullanımından daha az ayarlanmamalıdır.

Concurrency önemlidir, bu nedenle CPU requestlerini 1*(number of concurrent threads/processes) daha yüksek bir değere ayarlanmamalıdır.

Performans istediğiniz uygulamalarda, latency ve response time önemliyse kesinlikle CPU limit ayarlanmamalıdır.

Kubernetes Memory Kullanımı ve OOM

Bu durumu iki farklı şekilde ele alabiliriz.

1. memory.limit > memory.request

Eğer uygulamanın kullanacağından daha düşük bir memoy allocate ediyorsak örneğin request: 2GB ve bunu limit: 4GB ile sınırlandırıyorsak,
OOM olarak bilinen Out of Memory hatası ile karşılaşma olasılığımız yüksektir.

Container’a 2GB memory allocate et ama 4GB’a kadar kullanabilirsin dediğimizde aslında node üzerinde sınırlı olan bir kaynağı ne kadar kullanabileceğini bilmeden izin vermiş oluruz.
Üstelik diğer containerların kullanabileceği kaynağı da etkilemiş olacağız.(Memory overcommitment)

Node üzerinde yeteri kadar memory kalmadığında ve container’ımız istediğinden (request) daha fazla memory kullanmaya başladığında(limit) node üzerinde çalışan kubelet, kubernetes control-plane’e containerın OOMKilled hatası nedeniyle sonlandırıldığını belirten bir bildirim gönderir.(137) Control-plane daha sonra containerı yeniden başlatır veya farklı bir node üzerinde reschedule eder.

2. memory.limit == memory.request

Yukarıdaki örnekte olduğu gibi OOM hatasından kaçınmak istiyorsak uygulamanın çalışacağı memory sınırlarında (limit) bir kaynak istemeliyiz (request).

Bu değerler genel anlamda nodeların memory miktarına, node sayısına, uygulama sayısına bağlı olabilir.

Scaling ve Scheduling

Kısaca scaling ve scheduling konularında da öneriler sunmak istiyorum.
Ayrıntılı ve uygulamalı şekilde örnekleri artık bir sonraki yazımda anlatacağım. 😊

Pod priority, HPA(Horizantal Pod Autoscaler-Yatay Ölçekleme) gibi mekanizmaları kullanılarak doğru zamanda önceliklendirmelerle latency yaşamadan uygulamalarımızı ölçeklendirebiliriz.
Podlarımızı belirli nodelarda çalıştırmak için taint-toleration/pod-based,node-based affinity gibi özellikleri kullanarak scheduling tanımlarımızı genişletebiliriz.

Pod seviyesinden çıkıp node tarafına geldiğimizde ise infrastructure’ı otomatik provizyonlanan nodelar ile (cluster autoscaling-karpenter) daha esnek yapıda ve istediğimiz kaynaklarla uygulamalarımızı daha güvenilir çalıştırabiliriz.

Scaling profilleri ve scheduling kurallarını tanımlayarak, önceden belirlenmiş koşullara göre, nodelar gibi cluster kaynaklarını otomatik olarak ölçeklendirebiliriz.

Örneğin trafiğin yoğun olduğu zamanlarda yeterli kaynakların mevcut olmasını sağlamak için Karpenter’ı belirli zamanlardan önce node sayısını artıracak şekilde yapılandırabiliriz.

Bu proaktif ölçeklendirme yaklaşımı, sistemi yavaşlatmadan artan yükün sorunsuz şekilde yönetilmesine olanak tanıyacaktır.

Sonuç olarak, Kubernetes’te kaynak yönetim süreçlerini daha sağlıklı yapılandırmak adına bu başlıklar çerçevesinde hareket etmek uygulamalarımızın daha stabil, kontrol edilebilir optimum kaynak kullanımlarıyla, kesintisiz ve daha az gecikme ile çalışması noktasında önemlidir.

Bu makalenin yazılmasında ve kontrolünde destekleri olan Zeynep Rumeysa Yorulmaz’a teşekkür ederim.👾

Bir sonraki yazımda Scaling ve Scheduling konularına değinmeyi düşünüyorum. Şimdilik görüşmek üzere, esen kalın.

--

--