Auto Load Testing - FastAPI + locust

6 minute read

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ı;


Görsel Kaynağı




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))


Görsel Kaynağı

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.

Repo: silverstone1903/auto-load-testing


Locust’un çalışmasına ve çıktılarına ait ekran görüntüleri 👇🏻