Trendyol Platform Team - Caching Service to Service Communications on Kubernetes & Istio
Service discovery ile haberleşen microserviceler arası caching sorununu nasıl çözdük?
Selamlar, bu yazıda Trendyol Platform ekibi olarak geliştirdiğimiz bir uygulama olan Sidecache projesinden, geliştirme sürecinde edindiğimiz tecrübelerden ve bu projeye olan ihtiyaçtan bahsedeceğim.
Genel itibari ile anlatacaklarım aşağıdaki konu başlıkları etrafında olacaktır.
- Neden böyle bir projeye ihtiyaç duyduk?
- Bu proje ile neyi amaçladık?
- Sidecar Pattern nedir?
- Istio ile cache requestlerini yönlendirmeyi nasıl çözdük?
- Cache backendi olarak ne kullandık?
- Golang ile cache proxy geliştirmesini nasıl yaptık?
- Geliştirme sonrası metrikler
Neden Bu Projeye İhtiyaç Duyduk?
Platform ekibi olarak geliştirdiğimiz yeni service discovery altyapısı ile microserviceler arası haberleşmede harici load balancerları aradan çıkarttık.
Servisler arası load balancerların aradan çıkmasının avantajları olduğu gibi kaybettiğimiz bir kabiliyetimiz de oldu, bu da caching idi. Bu noktada yüksek yük altında çalışan bazı servislerimizde responseları cachelemek gibi bir ihtiyacımız doğdu.
Bunun üzerine bizlerde bu sorunu nasıl çözebileceğimiz üzerine düşündük. Öncelikle aklımıza gelen çözümlerden bir tanesi istio&envoy proxy bu ihtiyaç için bir çözüm sağlıyor mu bunu araştırmak oldu. Bu araştırmalar sonucu öğrendik ki henüz istio üzerinde built-in böyle bir özellik bulunmuyor lakin şöyle bir pr mevcut: https://github.com/envoyproxy/envoy/pull/10536
Buradan sonra alternatif çözümler üretmeye başladık. En kötü senaryoyu düşünerek uygulama içerisinden bizim belirlediğimiz bir cache endpointine gidecek middleware yapısı oluşturalım dedik ve bunun üzerine java uygulamaları için gökhan karadaş şöyle bir geliştirme yaptı: https://github.com/previousdeveloper/java-distributed-data-cache
Bu noktadan sonra cache işlemlerini yönetecek projenin konumlandırılmasını düşünmeye başladık. İşte bu noktada devreye sidecar pattern konusu giriyor.
Nedir bu Sidecar Pattern?
Kubernetes üzerinde çalıştırdığımız podlar içerisinde containerlar barındırır. Genellikle pod başına bir tane container çalıştırırız fakat kubernetes bize birden fazla container çalıştırma imkanı sağlar.
Caching işlemini her ekip kendi içerisinde çözmeye kalksaydı, birbirinden farklı dil ve frameworkler için cache yapısı geliştirilecek, aynı iş için birden fazla takım efor sarf edecek, mevcut uygulama kodlarına geliştirme yapmak gerekecekti. Onun yerine bir platform uygulaması geliştirip, herkesin kullanılabileceği hale getirmek bizim için daha mantıklı bir çözümdü. Bu sebeple cache uygulamasını bir sidecar olarak hayal ettik.
Sidecar, ambassador ve adapter adında üç farklı temel pod tasarım deseni bulunuyor.
Bu konunun detaylarını multi-container pod design konu başlığı altında bulabilirsiniz.
Biz bir proxy pattern uygulamaya karar verdik ve uygulamamızı bu doğrultuda geliştirmeye başladık. En temel seviyede yazacağımız sidecar, gelen requestleri karşılayıp aynı networkte bulunan uygulama containerına iletecek. Böylelikle tek bir pod içerisinde birden fazla container çalıştırıp aradaki haberleşmeyi localhost üzerinden sağlamış olacağız.
Caching Patterns
Caching hepimizin bildiği üzere servislerimizde database ile olan haberleşmeyi en aza indirmek ve daha fazla throughput sağlayabilmek amacıyla implement ettiği bir geliştirme.
Şimdi cache işlemi için var olan bazı tasarımlara bir göz atalım.
Embedded Cache: Cache işlemlerinin uygulama içerisinden yönetildiği yöntem.
Client-Server Cache: Cache isteklerinin uygulama içerisinden external bir cache servera iletildiği yöntem.
Sidecar Cache: Kubernetes spesifik olan bu yöntem embedded ve client-server yönteminin birleşimi gibi düşünebiliriz. Requesti uygulama karşılar ve cache işlemleri için sidecara istek atar.
Reverse Proxy Cache: Cache işlemlerinin reverse proxy (nginx, haproxy etc.) üzerinde yönetildiği yöntemdir.
Reverse Proxy Sidecar Cache: Bu yöntem ise bizim implement ettiğimiz yöntemdir. Burada requesti cache sidecarı karşılar ve cache bulunmuyorsa uygulama containerına yönlendirir.
Bu konuyu daha detaylı incelemek istenler için:
Biz yöntem olarak reverse proxy sidecar cache uygulamayı tercih ettik. Gelen GET requestlerini geliştirdiğimiz cache sidecara yönlendireceğiz ve böylelikle daha önce cachelemiş olduğumuz response’u client tarafına dönecebileceğiz.
Henüz cachelenmemiş requestleri ise ana uygulamamızın çalıştığı containera ileteceğiz (proxying). Pod içerisinde bulunan containerlar aynı network interface’inde çalıştıkları için localhost üzerinden minimum latency ile aradaki iletişim oldukça hızlı bir şekilde gerçekleşecek (external bir sisteme atılan isteğe oranla). Bu noktadan sonra geriye sadece gelen requestleri nasıl yönlendireceğimiz ve uygulamayı nasıl geliştireceğimiz kalıyor.
Istio ile Request Yönlendirme
İhtiyaçlarımızdan biriside gelen requesti bazı ön tanımlı kurallara göre cache containerına veyahut ana uygulamamızın bulunduğu containera yönlendirmekti. Service mesh için hali hazırda istio kullanıyoruz ve bu request yönlendirmelerini istio ile nasıl yapabileceğimizi araştırdık.
Bu noktada mevcut bazı yöntemleri inceleyelim.
Lua Filter
LuaFilter istio üzerinde kullanabileceğimiz hem gelen requesti hemde dönen responseları intercept edebileceğimiz bir özellik. Bu intercept işlemini istio proxy sidecar üzerinden yönetiyor.
Örnek olması açısından gelen her requeste 400 dönen ve response’a custom header ekleyen bir LuaFilter yazalım:
Bu örnek ile LuaFilter kullanarak gelen giden requestleri intercept edebildiğimizi görüyoruz. Bu noktada istersek gelen requesti öncelikle farklı bir hedefe yönlendirebilir veya doğrudan clienta dönecek şekilde akış devam ettirilebilirdi.
LuaFilter ile ilgili detaylara aşığdan erişebilirsiniz.
Burada dikkat edilmesi gereken nokta LuaFilter ile yapacağınız işlemlerin blocking olmaması.
Do not perform blocking operations from scripts. It is critical for performance that Envoy APIs are used for all IO.
EnvoyFilter
Bir diğer yöntem ise lua script tanımlaması yapmak yerine direkt olarak envoy proxy üzerinde konfigürasyon yapmak olacak. LuaFilter dan biraz daha karmaşık olan doğrudan envoy konfigürasyonları ile yapılan bu işlemde requestleri farklı adrese yönlendirmeyi sağlayabiliyoruz.
Örneğin birer adet nginx ve httbin deploymentlarımız olsun. Diyelim ki nginx:8080 e gelen requestleri httpbin:8080 e yönlendirmek istiyoruz. Aşağıdaki EnvoyFilter konfigürasyonu ile gelen requestleri başka bir container&poda yönlendirebiliyoruz. Örnek olması açısından reponse’a custom bir header da ekliyoruz.
Aşağıda bulunan EnvoyFilter’ı inceleyelim:
Bu konfigürasyonu clustera uyguladıktan sonra herhangi bir pod içerisinden:
curl nginx:8080/headers -v
şeklinde bir istek attığımız zaman default nginx response’u yerine httpbin response’u geldiğini göreceğiz (örneğimizin çalışması için deploymentlarımızı expose ederek service objelerini oluşturmayı unutmayalım).
VirtualService
Şimdi son olarak VirtualService kavramını inceleyelim.
Istio kullanırken service mesh içerisinde routing tanımlamalarını VirtualService custom resourceları ile yapıyoruz. VirtualService üzerinde match ruleları tanımlayabiliyoruz ve bu bize bazı esneklikler sağlıyor. Örneğin xxx/yyy pathine gelen GET requestlerini 9191 portuna diğer requestleri ise 8084 e yönlendir diyebiliyoruz. Üstelik EnvoyFilter gibi daha karmaşık konfigürasyon ihtiyaçları ortadan kalkıyor. Aynı zamanda fault injection, retrying, timeout management gibi süreçleri de VirtualServiceler ile yönetebiliyoruz.
VirtualService matcherlar ile ilgili detaylı bilgi için:
Bahsettiğim kullanım avantajlarından ötürü request yönlendirme konfigürasyonlarını VirtualService tanımlamaları ile yapmaya karar verdik.
Şimdi örnek olarak bir VirtualService tanımı yazalım ve gelen GET requestlerini cache sidecarımıza diğer requestleri ise uygulama containerına yönlendirelim.
Örnek VirtualService tanımı:
İki farklı route tanımı bulunuyor. Bunlardan birisi ana uygulama portu(8080) diğeri ise cache sidecar portu(9191). Buradaki tanıma göre, tüm get requestleri cache sidecar containerı üzerinden geçecek.
SideCache Projesi
Tüm bu cache işlemlerini yönetecek uygulamayı ise go dilini kullanarak geliştirdik. Bir reverse proxy görevi gören bu uygulama storage olarak Couchbase kullanarak responseları cacheleyip gelen requestleri cache üzerinden dönüyor. Eğer daha önce cachelenmemiş bir request gelirse bu requesti ana uygulamaya proxy ediyor. Hangi response değerinin cacheleyeceği ise api tarafından dönen custom bir header ile belirleniyor. Bu header değerinde cache TTL süresi bulunuyor.
Proxy işlemini go dili içerisinde bulunan httputil paketindeki ReverseProxy yapısını kullanarak implement ettik.
Geliştirdiğimiz projeyi github üzerinden inceleyebilirsiniz, sizlerde kubernetes üzerinde çalışan microservice’leriniz için sidecache projesini kullanabilirsiniz.
Admission Webhook ile Dynamic Sidecar Injection
Platform ekibi olarak geliştirdiğimiz uygulamaları, kullanacak ekiplerin en az değişiklik ile hayatlarına dahil edebilmesini amaçlayarak geliştirmelerimizi bu yönde yapıyoruz.
Sidecache projesini kullanmak isteyen ekiplerin uygulamaları için deployment dosyalarında container tanımlamalarını manuel olarak yapmaları yerine bu süreci otomatize etmek için kubernetes üzerinde bir mutating admission webhook oluşturduk. Böylelikle sidecache containerını projesine eklenmek isteyen ekipler deploymentlarına sadece bir annotation ekleyerek deployment esnasında containerın inject edilmesini sağlayabiliyor.
Admission webhooklar ile kubernetes resource’larını dinleyip üzerlerinde validation&mutation işlemleri yapabiliyoruz. Biz deploymentların create ve update isteklerini dinleyip, eğer istediğimiz annotation bulunuyorsa sidecar cache container’ımızı deployment’a inject ediyoruz.
Batuhan Apaydın tarafından yazılan bu yazıda Admission Webhook yapısı hakkında detaylı bilgiyi alabilirsiniz.
Metrikler
Sidecache inject edilmiş projelerde bazı metrikleri sizlerle paylaşmak istiyorum.
Aşağıda bulunan metriklerin birincisinde, sidecache eklendiği andan itibaren api responselarında oluşan değişiklikleri görebiliyoruz. Throughput azalıyor olması ve search ratelerin düşmesi artık responseların doğrudan cache üzerinden geldiğini gösteriyor. Bu sayede uygulamalar daha az kaynak tüketip daha fazla istek karşılayabilir hale geliyor.
İkinci metrikte ise gelen requestlerin ortalama %26 sının cacheden döndüğünü görüyoruz (cachelenecek response’u api kendisi belirlediği için bu değer değişkenlik gösterebilir).
Yazıyı burada tamamlarken herkese keyifli okumalar diliyorum. Umarım bu süreçte edindiğimiz tecrübelerimiz sizler için de faydalı olmuştur.
Bir sonraki yazıda görüşmek üzere.
May the source be with you.
#joinus #cometotrendyol