Auto Load Testing - FastAPI + locust
Published:
Yük Testi Nedir?
Yük testi web uygulamanızın aynı anda kaç kullanıcı kaldırabildiğini, kaç istekten/kullanıcıdan sonra kilitlendiğini ya da belirli bir yük altında ne kadar süre sonra cevap veremez hale geldiği sorularına cevap bulmanıza yarayan bir testtir. Yazılım geliştirme döngüsünde (software devlopment cycle) kodları test ederken kullanılan birim (unit), entegrasyon (integration) ya da sistem (system) testleri genellikle tek bir kullanıcı ile yapılan testlerdir ve sistemin çalışacağı canlı ortamdaki gerçek davranışı hakkında tam bir öngörü sağlamamaktadır. Sistemin limitleri hakkında daha gerçekçi sonuçları görebilmek için yük testlerinden faydalanılır. Yük testleri genellikle olağan dışı trafik oluşturacak durumlar simüle edilerek kurgulanır. Kampanya zamanları (Black Friday), süreli satış (limitli bilet satışı) veya push bildirimi sonrası gibi örnek senaryolar olabilir. Yük testi sistemin performansı, uygulamanın limiti ve sistemde (ya da sistem tasarımında) ortaya çıkan darboğazların (bottleneck) belirlenmesine yardımcı olur.
Yük testinde göz önüne alınacak metrikler;
- CPU/Ram kullanımı
- Tepki (response) süresi
- Ortalama yükleme (load) süresi
- Başarılı/Başarısız (fails) istek sayısı
- Network (bandwidth/throughput) kullanımı
Bununla birlikte elde edilen sonuçların analiz edilip gerekli geliştirmelerin yapılması gerekir (ya da beklenir). Örneğin test sonucunda veri tabanına giden bir sorgunun çok fazla vakit aldığını ve son kullanıcının bu durumdan etkilendiği fark edilirse burada sorgunun optimize edilmesi ya da ilgili veri taşınabilir (çok büyük olmaması) bir veri ise daha hızlı çalışacağı (mem-cached - redis) bir veri tabanına alınmasıyla problem giderilebilir. Tüm bu durumlar ML-Serving API için de geçerli olduğundan modelleri canlıya almadan önce yük testi yapmakta fayda var. Tabii öncesinde yukarıda da bahsettiğim unit, integration gibi testlerin yapılması gerekiyor. ML sistemlerinin test edilmesi hakkında Made with ML‘e göz atabilirsiniz.
Son olarak ise yük testi için bir hayli alternatif olmakla birlike sanırım en bilinenleri; JMeter, locust ve k6 olarak söylenebilir. Python desteğinden (ve bildiğim/kullandığım tek yük testi aracı olmasından) dolayı locust’u kullanacağım.
ML uygulamaları için yük testi hakkında bazı blog yazıları;
- Why Load Testing is Essential to Take Your ML App to Production
- Load Testing a Machine Learning Model API
- Performance Testing an ML-Serving API with Locust!
- Performance testing FastAPI ML APIs with Locust
Uygulama
Uygulama için daha önce Reco Model Monitoring - FastAPI + Prometheus + Grafana yazımda oluşturduğum API’den faydalanacağım. Yazıdaki API için veri hazırlığını docker içerisinde yaparken (feather
formatını kullanıyordum, daha hızlı okuma/yazma) bu çalışmada veriyi Github’a da yüklediğim için boyutunu küçültmek amacıyla parquet
formatını tercih ettim ve veri hazırlığı aşamasını da kaldırmış oldum. Bunun yanı sıra monitoring ihtiyacı olmadığı için kodda yer alan prometheus-exporter kısımlarını da çıkardım. Her zaman olduğu gibi bu uygulamayı da ayağa kaldırmak için dockerise ettim ve yine docker-compose‘dan yararlanacağız.
Locust
locust Python ile geliştirilmiş açık kaynak bir yük testi aracı. Ölçeklenebilir (scalable) ve dağıtık (distributed) bir şekilde çalışabiliyor. Bu çalışmada cli üzerinden kullanmış olsam da basit bir arayüze de sahip. Dokümanına buradan ulaşabilirsiniz.
Locust çıktılarının cli ekranında okunabilir olması için hardcoded biçimde user ve item listesi tanımladım ve bunları da 10 ile sınırladım. User ve Item için endpoint’lere istek atarken bu liste içerisinden rastgele bir biçimde id’leri çekiyor. Burada bir diğer seçenek ise (docker’a dosya kopyalamak ve pandas kurmak istemediğim için) ham veriyi ekleyip onun içerisinden rastgele seçim yaptırmak olabilir.
locustfile.py
içerisindeki kodları aşağıda görebilirsiniz. API’de bulunan healthcheck (/
), user (/predict/user/{user_id}
) ve item (/predict/item/{item_id}
) endpoint’leri için test senaryolarımızı belirliyoruz. class
içerisinde HttpUser
olarak tanımladığımız HealthCheck
senaryosunda her bir task (kullanıcı) arasında 10-15 saniye aralığında beklemesi gerektiğini belirtiyoruz. Buradaki amaç API hala ayakta mı değil mi kontrolü olduğu için sürekli istek atılmasına gerek yok, bu nedenle de wait time
ekliyorum. Diğer endpoint’lerde ise durum farklı, bu nedenle Reco
altında kullanıyoruz çünkü eş zamanlı (concurrent) olarak istek atmamız gerekiyor. Burada wait time
yerine gelen ise @task(n)
yapısı. Burada tanımladığımız n
ise bir sonraki task’e geçmeden önce ilgili task’in kaç defa yapılacağı. Yani HealthCheck
için bir kez istek atarken test_user
ve test_item
için 5’er defa istek attıktan sonra bir diğer task’e geçiyor. Bir diğer deyişle de Reco
‘ya gelecek 10 isteğin 5 tanesi test_user
‘a gittikten sonra kalan 5 tane de test_item
‘a gidecek, yani @task(n)
ile trafiği ağırlıklandırmak da mümkün.
import time
import json
from locust import HttpUser, task, between
import random
users = ['17850', '13047', '12583', '13748', '15100',
'15291', '14688', '17809', '15311', '16098']
items = ['85123A', '71053', '84406B', '84029G', '84029E',
'22752', '21730', '22633', '22632', '84879']
headers = {'Content-Type': 'application/json',
'Accept': 'application/json'}
class HealthCheck(HttpUser):
wait_time = between(10, 15)
@task
def test_hc(self):
self.client.get("/", headers=headers)
class Reco(HttpUser):
@task(5)
def test_user(self):
user = int(random.choice(users))
self.client.get(
"/predict/user/{0}".format(user))
@task(5)
def test_item(self):
item = random.choice(items)
self.client.get(
"/predict/item/{0}".format(item))
Uygulamayı Ayağa Kaldırma
Uygulamayı ayağa kaldırmak için docker-compose ile build almak yeterli. locust kullanırken test öncesinde arayüzden kullanıcı sayısı ve saniyede kaç kullanıcı ekleneceği (hatch rate) değerleri giriliyor. Eğer siz de benim gibi arayüz kullanamayacaksanız burada iki seçeneğiniz var; ya kod içerisinde host
, duration
, users
gibi değişkenleri tanımlayabilirsiniz ya da cli ile bu argümanları girebilirsiniz. Argümanları docker-compose.yml içerisinde aşağıdaki şekilde kullandım.
--headless
argümanı arayüzü başlatmasına gerek olmadığını belirtiyor. Arayüz ile belirtilebilecek olan değerler olan -u
simüle edilecek kullanıcı sayısını, -r
saniyede kaç kullanıcı simüle edileceğini -host
ise hedef adresi belirtiyor. Son olarak --run-time
ise uygulama için koyduğumuz zaman limiti, bu sürenin sonunda locust durduruluyor. Bu limit nedeniyle docker’daki locust uygulaması durdurulduğu için compose uygulaması turuncu (app exited) renge dönüyor ki meali uygulamada sorun olduğu. Bu nedenle compose ile ayağa kaldırırken --exit-code-from locust
argümanını ekledim. Böylece locust ile yük testi bitince locust ve fastapi uygulamaları durduruluyor.
command: -f /mnt/locust/locustfile.py --headless -u 50 -r 2 --run-time 10s --host http://fastapi:8000
Burada not düşmem gereken bir diğer konu ise bu örnek için locust ve API aynı makine içerisinde çalışıyor. Aynı makineden yük testi yapıldığı durumda API ve locust aynı kaynakları paylaşıyor. Bu nedenle daha gerçekçi sonuçlar almak adına uygulama ve locust’un farklı makinelerde kurulu olması gerekmekte.
Uygulamayı ayağa kaldırmak için ihtiyacımız olan 2 şey var;
Docker ve docker-compose kurulu ise aşağıdaki adımlar ile uygulamayı ayağa kaldırabilirsiniz.
git clone https://github.com/silverstone1903/auto-load-testing
docker-compose up --build --exit-code-from locust
Docker-compose ile uygulama sorunsuz bir şekilde ayağa kalktıysa aşağıdaki adresten API arayüzüne erişebilirsiniz.
- FastAPI: http://localhost:8000
Repo: silverstone1903/auto-load-testing
Locust’un çalışmasına ve çıktılarına ait ekran görüntüleri 👇🏻