Python gRPC Load Balancing

Cok fazla yuk altina girecek olan bir gRPC server isletiyoruz diyelim. Server'i de Python ile yazdigimizi dusunelim. 

- Ne gini kisitlamalar yasayacagiz?
- Serveri horizontal olarak scale etmek icin ne gini seceneklerimiz var?
- Bir de gRPC server local'de calisip, diger desktop uygulamalarina hizmet veriyor ise isler nasil degisir? 

Gelin bu kullanim alani uzerinden biraz beyin cimnastigi yapalim.


1. Network Load Balancing

Server-side
RPC, HTTP 2.0 uzerinde calisan bir iletisim protokolu oldugu icin, HTTP2.0 destekleyen standart load balancerleri kullanabiliriz. En populer secenekler ngix ve envoy proxy olarak gosterilebilir. 



Yalniz burada temel bir problemimiz var. gRPC sessionlari sticky bir yapidadir. Yani bir client bir servere bir kere baglanir  ve uzun bir sure boyunca bagli kalir. Request ve response'lar bur mevcut baglanti uzerinden gerceklesmis olur. Bu sayede her request/response icin tekrardan baglanti kurmaya (ve dolayisi ile TCP handshake yapmaya) gerek kalmaz. Cunku bu bir overhead'dir ve gRPC'nin performansli olmasinin sebeplerinden birisi de bu sticky session'lar. 

Stick session olunca, bir client bir servere bir kere yapisti mi onu ayirmadan, sunucular uzerindeki yuku tekrardan esit bir sekilde dagirmak pek de mumkun olmayacaktir. Cunku load balancing request seviyesinde yapildiginda tam anlamiyla bir esit yuk dagilimi saglanabilir.

Bu problemi cozmek icin 2 yontem mevcut:

1. Clientlar belirli periyotlar ile disconnect olurlar ve tekrar baglanirlar. Bu sayede o anki durumda en uygun olan sunucuya tekrar baglanmis olurlar.

2. Server, periodik olarak, kendisine belirli bir sure baglanmis olan client'i disconnect eder.  Bu durumda client tekrar baglanmak durumunda kalir ve yuk dagitilmis olur. 

Ancak, bu iki cozum yontemi de, yukarida bahsettigimiz gRPC'nin bir avantaji olan stick session ozelligini ortadan kaldirdigi icin, gRPC performansini kotu yonde etkiler. 


Client-side
Sorumlulugu client tarafina alip (thick client) load balancing yapilabilir. gRPC clientlar birden fazla gRPC server oldugunun farkindadir ve (implementasyona bagli olarak, yuk raporu alabilirler) buna gore uygun bir server'i kendileri secerek baglanirlar. 



2. Application Load Balancing

Eger ki gRPC serverimiz son kullanicinin bilgisayarinda calisacak bir sekilde dagitilacak ise, bu durumda network bazli load balancing yapmak ekstra komplikasyonlar getirebilir. Cunku kendi gRPC serverimiz yaninda bir de bu load balancerlari (ornegin nginx) dagitmak, kurdurmak, yasami boyunca monitor etmek, fail olursa restrat etmek gibi housekeeping islemlerini kendimiz yapmak zorunda kaliriz.


Multi-threading
Aslinda baktigimiz zaman gRPC, multi-threading yaklasimini destekliyor. Bir gRPC server olustururken, kac thread ile calismasi gerektigini belirtebiliyoruz. Bu sayede yuksek sayida paralel requestler geldigi zaman bunlara makul latency'ler ile cevap verebiliyoruz.

server = grpc.server( 
    futures.ThreadPoolExecutor(max_workers=8) 
)

Yani verebilirdik. Cunku basta bahsettigimiz gibi, gRPC serveri Python ile gelistitiyoruz. Ve onceki bir yazimizda bahsettigimiz gibi Python, GIL sebebiyle, multi-threading yaklasimi gercek paralelizm sunmuyor. En fazla bir event-loop gibi davranarak, concurrency sagliyor. Bu da tabi ki CPU gerektiren islemlerde hicbir performans artisi saglamiyor. Eger I/O agirlikli isler yapiyor isek, bir thread bir IO beklerken digerine gecebilir (non blocking IO) ve bu sayede CPU utilizasyonu artabilir. Ama dedigim gibi, CPU-bound islerde bu hicbir fayda saglamiyor. 

 

Multi-processing
Python'da multi-threading'in alternatifi, multi processing. Birden fazla process olusturabilir, gRPC serverimiz bunlar uzerinden sunabiliriz. Bu sekilde paralelde requestlere cevap verebiliriz. 

Tabi ki hayat bu kadar kolay degil. Malese, gRPC de, multi-threaded yaklasiminda oldugu kadar kolay bir sekilde multi-processingi desteklemiyor. Kendiniz elle processler yaratip, her bir process uzerinde bagimsiz bir gRPC server olusturmaniz gerekiyor. 

Burada yine bir load balancing problemi ortaya cikiyor. Sonucta tek bir yerden yonetilmeyen, birbirinden bagimsiz bircok process (ve dolayisi ile server) elimizde var. Ve de clientlar sadece tek bir endpointe request atsin diye arzu ediyoruz. 

Yardima, yeni bir soket opsiyonu olan, SO_REUSEPORT yetisiyor. Bu option ile serverlarimizi olsuturur isek, hepsi ayni portu dinlemeye basliyor. Bu sayede request geldikce, uygun olam process requesti alip isliyor. Yani bir load balancing yapisi elde etmis oluyoruz. 

Hatta google'in resmi multi-processing orneginde bu yapi kullanilmis.


Sonuc

Bircok secenegi gozden gecirdik. Eger gRPC server gercekten sunucuda calisacak ise, network bazli load balancing dusunulebilir. Eger kullanicinin makinesinde calisacak ise, ve de serverin yaptigi islemler IO-bound olup da cok fazla CPU kullanmiyor ise multi-threading ile tek satirda bu isin icinden cikabilirsiniz. Ama CPU-bound islemler icin tekrardan farkli processler olusturma ve bu processleri yonetme karmasina gireceksiniz demektir. 

Bir ihtimal daha var ...
Aslinda tum problemin kokeninde multi-threading'i gerektigi gibi kullanamiyor olusumuz yatiyor. Pek cok alanda diger dillere gore hic geri kalmayan Python, (biraz da kendince hakli sebeplerle) multi-threading noktasinda farkli davranis sergiliyor. 

Bu da demek oluyor ki, GIL olmayan bir dil/platform, ornegin JVM uzerinde calisan bir teknoloji de dusunulebilir. Hatta bu kadar paralel, CPU-Bound ve de dogal olarak scalable bir gRPC server ihtiyacimiz varsa, (sahsen benim) aklima direk Scala ve Akka Actor modeli geliyor. Bu, gelecek yazinin konusu ama gercek paralelizm ve muthis olcekleneiblirlik icin, iyi bir secim oldugunu dusunuyorum. 


Postuma burada son verirkene, herkese hayirli hackler dilerim.

Stay hungry, stay foolish, stay healthy :)

Ayrica Make love, not war!









Yorumlar

Bu blogdaki popüler yayınlar

Python'da Multithreading ve Multiprocessing

Threat Modeling 1

Encoding / Decoding