Tasarım Zorluğu: Kubernetes VPA'yı Apache Spark ile Entegre Etmek
7 dk okuma
Bu yazı makine çevirisidir. İngilizce aslını oku
İçindekiler
- Apache Spark ve Kubernetes
- Apache Spark'ı Ölçeklemek
- Kubernetes VPA (Vertical Pod Autoscaler)
- VPA'yı Apache Spark ile Kullanmak
- VPA'yı Apache Spark ile Entegre Etmek
- Entegrasyonu tasarlamak
- Kubernetes mutating admission webhook
- Bir VPA zorluğunu çözmek
- Ek bir zorluk olarak Volcano batch scheduler
- Volcano'yu göz önünde bulundurarak tasarlamak
- Gerçekleştirim ayrıntıları üzerine notlar
- Sonuç
Apache Spark ve Kubernetes
Apache Spark, çeşitli veri kaynaklarından veri okuyabilen ve bu veriler üzerinde istenen hesaplamaları dağıtık biçimde yapabilen bir dağıtık veri işleme motorudur. Farklı fiziksel ya da sanal düğümlerde çalışan program örneklerinden oluşan bir sistemdir. “driver” adı verilen bir ana program örneği ile “executor“ adı verilen işçi program örnekleri vardır.

Executor programlarının farklı düğümlerde çalıştırılması gerektiğinden Apache Spark, driver programının istediği executor örneklerini başlatmaktan sorumlu bir “cluster manager” kullanmak zorundadır. Apache Spark, executor'ların başlatılmasını seçilen cluster manager'a devreder. Kubernetes de Apache Spark için böyle bir cluster manager'dır.
Dolayısıyla Apache Spark, yerleşik Kubernetes cluster manager'ı sayesinde Kubernetes üzerinde çalışabilir. Ayrıca Spark uygulamalarını ve parametrelerini Kubernetes'in diliyle (örneğin custom resource definition'lar) tanımlamanıza olanak vererek Spark'ı Kubernetes üzerinde çalıştırmayı daha da kolaylaştıran spark-operator projesi de var.
Apache Spark'ı Ölçeklemek
Apache Spark, yıllar içinde ihtiyaçlara göre geliştirilmiş, hesaplama optimizasyonlarından I/O optimizasyonlarına uzanan, çalışmayı hızlandıran pek çok optimizasyona sahiptir. Apache Spark'ın kullanışlı özelliklerinden biri dinamik executor tahsisidir. Normalde Spark uygulamaları, driver programı başlamadan önce sabit sayıda executor'a sahip olacak şekilde yapılandırılabilir. Ancak Spark'a executor sayısını iş yüküne göre dinamik olarak seçmesini de söyleyebiliriz. Buna dinamik tahsis denir ve bu, Apache Spark'ın yatay ölçekleme özelliğidir.

Ne var ki Apache Spark'ta şu anda bir dikey ölçekleme özelliği yok. Apache Spark'a, CPU ve bellek sınırlarını uygulamanın ihtiyacına göre artırmasını söyleyemiyoruz. Bununla ne demek istediğimi birazdan açıklayacağım.
Kubernetes VPA (Vertical Pod Autoscaler)

Kubernetes'in “Vertical Pod Autoscaler” adında bir özelliği var. Etkinleştirildiğinde uygulamaların kaynak kullanımını ve başka birkaç etkeni izleyip o uygulama için en uygun kaynak miktarlarını tahmin edebilir. Örneğin 256 MB'tan fazlasını kullanmayan varsayımsal bir uygulamaya 512 MB bellek ayırırsak VPA, maliyetten tasarruf etmek için 512 MB yerine (kabaca) 256 MB kullanmamız gerektiğini tahmin edecektir. Tersi de mümkün. 512 MB ayırdığımız halde uygulamamız sık sık bundan fazlasına ihtiyaç duyuyorsa VPA bize performansı iyileştirmek için daha fazlasını, örneğin 1024 MB ayırmamızı söyleyecektir.
VPA'yı Apache Spark ile Kullanmak
Bunu okuyorsanız muhtemelen benden “Eh, VPA'yı kullanıp Apache Spark'a dikey otomatik ölçekleme özelliği ekliyoruz. Hoşça kalın.” dememi bekliyorsunuzdur. Ne yazık ki iş bu kadar basit değil.

VPA, Apache Spark uygulamaları dahil her türlü iş yüküyle kullanılabilen genel bir özelliktir. Ancak Apache Spark'ın kendi kaynak yapılandırma parametreleri olduğundan, VPA'yı etkinleştirmek Apache Spark için yeterli değildir. Executor pod'larını büyütsek bile, ilgili Apache Spark yapılandırma parametrelerini ayarlamadığımız sürece içindeki executor süreci toplam kapasiteyi kullanmayacaktır. Spark uygulamaları dış dünyadan habersizdir. Nerede çalıştıklarından bağımsız olacak şekilde tasarlanmışlardır.
Dolayısıyla Spark uygulamalarımızın VPA tahminlerinden (diğer adıyla VPA önerilerinden) yararlanmasını sağlamak için ek adımlar atmamız gerekir.
VPA'yı Apache Spark ile Entegre Etmek
Spark uygulamalarının ve VPA'nın bir Kubernetes cluster'ında nasıl etkileştiğini düşünmemiz gerekiyor.
Öncelikle Spark uygulamalarımızla eşleşecek VPA nesneleri oluşturmamız gerekiyor. Bu VPA nesneleri metrikleri toplayacak ve uygulamalarımızı çalıştırmaya devam ettikçe bazı öneriler kullanılabilir hale gelecek. Ardından, en uygun kaynak miktarlarından yararlanabilmeleri için bu önerileri Spark uygulama pod'larımıza uygulamamız (ya da VPA'nın uygulamasına izin vermemiz) gerekiyor.
Bunu tek bir Spark uygulaması için yapmak mümkün olsa da binlerce Spark uygulaması için mümkün değil. Dolayısıyla bu süreci doğru biçimde otomatikleştirmemiz gerekiyor.
Entegrasyonu tasarlamak
Burada ulaşmak istediğimiz hedefe uyabilecek çeşitli tasarım yaklaşımları var. Temel tasarım ilkelerim şunlar olacak:
- VPA'nın kendisinin hiçbir pod'u değiştirmesine izin verme; böylece önerilerin uygulanması üzerinde daha ince taneli bir denetimimiz olur.
- Çalışan Spark uygulamalarını ölçeklemek için durdurma ya da yeniden başlatma; çünkü bu, o çalıştırmanın toplam tamamlanma süresini uzatabilir. Ölçeklemeyi yalnızca pod'ları başlatmadan hemen önce yap.
- Aynı uygulamanın örneklerini belirtebilmeleri için kullanıcıların Spark uygulamalarını kolayca etiketlemesine izin ver; böylece metrikleri toplanabilir ve diğerlerinden ayrılabilir.
- Entegrasyonu, kullanıcı açıkça kullanmak istemedikçe varsayılan olarak devre dışı olan bir opt-in özellik yap.
Bu hedefleri karşılamak için entegrasyonu bir Kubernetes mutating admission webhook'u olarak gerçekleştirdim.
Kubernetes mutating admission webhook
Kubernetes, sistemin neredeyse her parçasının özelleştirilmesine olanak tanıyan son derece esnek ve modüler bir sistemdir. Admission webhook özelliğinden yararlanarak Spark uygulamalarının pod oluşturma isteklerini, pod'lar gerçekten oluşturulmadan hemen önce inceleyebilir ve ihtiyaçlarımıza göre değiştirebiliriz. Böylece pod'lar bizim yaptığımız değişikliklere göre oluşturulur.

Admission webhook'u gerçekleştirip Kubernetes cluster'ımıza deploy ettiğimizde Kubernetes API'si, pod oluşturma isteğiyle ne yapılacağını webhook uç noktamıza soracaktır. İsteği reddedebilir ya da değiştirebilir ve Kubernetes API'sine bir yanıt döndürebiliriz. O da söylediğimiz değişiklikleri uygular.
Webhook uç noktam bir pod oluşturma isteği yakaladığında, henüz oluşturulmamışsa ilgili VPA nesnesini oluşturacak. Ayrıca kullanılabilir önerileri de denetleyecek. Varsa bu öneriler, kaynak istekleri/sınırları ile SPARK_EXECUTOR_MEMORY ve SPARK_EXECUTOR_CORES gibi Spark'a özgü ortam değişkenlerini değiştirerek pod'a uygulanabilir. Gerekirse başka ölçütlere göre seçici kararlar vermek de mümkün. Elimizdeki pod oluşturma isteğini değiştirmekte tamamen özgürüz.
Bir VPA zorluğunu çözmek
VPA, doğrudan belirli controller nesnelerini (Deployment gibi) hedeflemenize izin verir ama pod'ları hedefleyemezsiniz. Dolayısıyla Spark uygulama grubumuzla eşleşecek keyfi bir label selector tanımlayamıyoruz. Bir controller nesnesine ihtiyacımız var. Bu da entegrasyon için bir zorluk oluşturuyor.
Neyse ki VPA, controller nesnesinin pod'ların gerçek sahibi olup olmadığını denetlemiyor; pod'ları eşleştirmek için onun label selector'ını kullanıyor. Geçici çözüm olarak hiç örneği olmayan sahte deployment nesneleri oluşturup bu deployment nesnelerini VPA hedefi olarak ayarlayarak istediğimiz herhangi bir pod'u eşleştirebiliriz.
Webhook, bir pod oluşturma isteği incelendiğinde sahte deployment nesnesinin oluşturulmasını da üstleniyor. Sahte deployment'ları VPA nesneleri ve Spark pod'larıyla eşleyebildiğimizden emin olmak için burada dikkatli olmamız gerekiyor.
Ek bir zorluk olarak Volcano batch scheduler
Kubernetes'in elbette kendi varsayılan scheduler'ı var. Pod'ları kullanılabilir düğümlere basitçe yerleştirebilir/atayabilir. Bunu yaparken karmaşık bir mantık izlemez ve pod'ları birbirinden bağımsız ele alır.
Ancak Spark gibi dağıtık işleme motorları için durum böyle değil. Bunlar birlikte çalışması gereken birden fazla parçadan oluşur. Driver programını çalıştırabilir ama executor'ları çalıştıramazsak hesaplama hiç başlamaz; bu da zaman ve kaynak israfıdır. Oysa varsayılan Kubernetes scheduler'ı bu tür sorunları çözmek için tasarlanmamıştır.

Volcano, bu tür sorunları çözmek için tasarlanmış bir batch scheduler'dır. Başlangıçta Huawei bünyesinde geliştirilmiş ve açık kaynak yapılmıştır. Şu anda kuluçka aşamasında bir CNCF projesi. Volcano “gang scheduling” destekler; yani driver ve executor pod'larını birlikte yerleştirir ya da bunun için yeterli kaynak yoksa hiçbirini yerleştirmez.
Volcano hem düz Spark on Kubernetes iş yüküyle hem de spark-operator iş yüküyle kullanılabilir. Birkaç parametre ayarlamamız yeterli.
Volcano'yu Spark on Kubernetes (ya da spark-operator) ile etkinleştirmek kolay, ama bu entegrasyonumuza başka bir zorluk getiriyor. Pod oluşturma isteklerini incelemeye ve pod'ları VPA önerilerine göre değiştirmeye devam edebiliriz. Ancak Volcano, pod'ları düğümlere yerleştirirken/atarken toplam kaynak gereksinimlerini dikkate aldığından bu, Volcano etkin iş akışıyla tutarsızlıklara yol açar. Pod kaynaklarını değiştirirsek yerleştirilemeyen pod'larla karşılaşabilir ve Volcano'nun iç işleyişini bozabiliriz.
Volcano'yu göz önünde bulundurarak tasarlamak
Volcano, her Spark uygulaması için “PodGroup“ nesneleri (Volcano'nun kendi tanımladığı bir CRD) oluşturur ve ardından yerleştirme kararlarını PodGroup'larda tanımlı parametrelere göre verir. Dolayısıyla PodGroup'ları ihtiyaçlarımıza göre değiştirebilirsek Volcano kullanırken herhangi bir tutarsızlık ya da sorun olmaması gerekir.
Webhook'umuza Volcano PodGroup oluşturma isteklerini dinleyecek başka bir uç nokta ekleyebiliriz. Pod oluşturma isteklerini işlerken yaptıklarımızın aynısını burada da yapabiliriz; tek fark, bu kez PodGroup üzerindeki toplam kaynak miktarlarına karşılık gelen parametreleri değiştirmemiz gerekmesi.
Gerçekleştirim ayrıntıları üzerine notlar
Go, Kubernetes'in ve admission webhook gibi eklentilerinin standart dilidir. Kubernetes ile entegrasyonu zahmetsizdir; performans kazanımları ve başka avantajları da vardır.
İlk PoC'yi, ekip yoğun Spark çalışmaları nedeniyle Scala'ya aşina olduğundan Scala ile gerçekleştirdim. Yine de sırf serileştirme sorunlarıyla epey zaman kaybettim. Sonra Go'ya geçtim ve Go'yu bu proje için öğrenmiş olmama, yani bunun ilk Go projem olmasına rağmen proje sorunsuzca büyüdü.
Geliştirmenin olabildiğince yerel olması gerektiğine inanıyorum. Geri bildirim döngüsü daha kısa ve ortam denemeler için daha güvenli olduğundan bu yaklaşım verimliliği artırıyor. Bu yüzden yerel bir Kubernetes test ortamı kurmak için K3d kullandım. Performansı nedeniyle K3d'yi Minikube'e tercih ettim ve tavsiye ederim.
Elle testi kolaylaştırmak ve tekrarlanabilir kılmak için birkaç betik ve uzun bir Makefile hazırladım. Ardından Makefile'ımda tanımlı çeşitli hedefleri birleştiren e2e testler oluşturdum. Böylece birden fazla gerçek dünya senaryosunu uçtan uca bir ortamda (örneğin Volcano'yu, spark-operator'ı vb. gerçekten deploy ederek) test edebiliyorum.
Sonuç
Kubernetes VPA'yı Apache Spark ile entegre etmek, başta göründüğünden daha karmaşık çıktı. VPA harika bir özellik sunsa da onu Spark'ın ihtiyaçlarına uyarlamak, çözümün pratik ve güvenilir olması için dikkatli bir tasarım ve otomasyon gerektiriyor.
Bu proje bana Go dili, Kubernetes'in iç işleyişi ve testi, VPA, Volcano, Spark-operator, otomasyon ve ekosistemleri anlama konularında değerli bilgi ve deneyim kazandırdı. Ayrıca doğru araçları seçmenin ve büyük resmi göz önünde bulundurarak tasarlamanın önemini bir kez daha gösterdi.
Uygulayarak deneyim kazanmanın ve öğrenmenin gücüne inanıyorum. Bu, süreci keyifli, etkili ve kalıcı kılıyor. Bu projede çalışma fırsatı bulduğum için minnettarım.
Bu entegrasyonu iyileştirmek ve genişletmek için hâlâ yer var ve bu alanda yeni zorluklarla yüzleşmek için sabırsızlanıyorum.
Okuduğunuz için teşekkürler. Bir sonraki yazıda görüşmek üzere!