Yük testi, zirve gününün trafiğini o gün gelmeden simüle edilmiş kullanıcılarla yeniden oluşturmak ve API'nin nerede yavaşladığını ya da hata vermeye başladığını görmek demektir. Locust kullanıcı davranışını Python koduyla tanımlar; böylece ASP.NET Core ile yazılmış bir API'ye karşı giriş yapma, okuma ve gönderme gibi gerçekçi bir akış çalıştırabilirsiniz. Bu rehberdeki seçenekler Locust 2.46 dokümantasyonuna göre kontrol edildi; güncel Locust sürümleri Python 3.11 veya sonrasını gerektiriyor.
Bilinen bir zirveden önce neden test edilmeli?
Bazı zirveler önceden görülebilir: sınav günü, kayıtların açılması, bir kampanya. Gozine2'de 45.000 ile 70.000 eşzamanlı kullanıcıya ulaşan sınav zirvelerinden önce darboğazları bulmak için Locust yük testleri kullandım, ardından SQL Server sorgularını, önbellekleme yollarını ve en yoğun API'leri iyileştirdim. Sınavdan bir hafta önce bulunan bir sorun, düzeltmek ve yeniden test etmek için zaman bırakır; sınav günü böyle bir zaman yoktur.
Herhangi bir kod yazmadan önce iki şeyi netleştirin:
- Hedef yük. Eşzamanlı kullanıcı sayısını ve her endpoint'in payını gerçek veriden türetin: kayıt sayılarından ya da önceki zirvenin loglarından.
- Geçme kriterleri. Örneğin "hedef yükte her endpoint'in 95. yüzdeliği X milisaniyenin, hata oranı da %Y'nin altında kalır". Kriter olmadan her sonuç iyi ya da kötü diye okunabilir.
Test ortamındaki veri hacmi de production'a yakın olmalıdır; on bin satırda hızlı olan bir sorgu on milyon satırda hızlı olmayabilir.
Gerçekçi bir locustfile yazmak
Locustfile sıradan bir Python modülüdür. HttpUser sınıfından türeyen her sınıf bir kullanıcı tipidir ve Locust simüle edilen her kullanıcı için ondan bir instance oluşturur. Aşağıdaki örnekteki kullanıcı, giriş yapan, soruları getiren, cevap gönderen ve ara sıra sonuçlarına bakan bir öğrencidir. Endpoint'ler ve sayılar örnektir:
import csv
import itertools
import os
import random
from locust import HttpUser, between, task
from locust.exception import StopUser
with open("test_users.csv", newline="", encoding="utf-8") as f:
ACCOUNTS = itertools.cycle(list(csv.DictReader(f)))
EXAM_IDS = [1201, 1202, 1203]
class Student(HttpUser):
wait_time = between(3, 12)
def on_start(self):
account = next(ACCOUNTS)
with self.client.post("/api/auth/login", name="login", catch_response=True,
json={"userName": account["user"],
"password": os.environ["LOADTEST_PASSWORD"]}) as resp:
if resp.status_code != 200:
resp.failure(f"login: HTTP {resp.status_code}")
raise StopUser()
token = resp.json()["accessToken"]
self.client.headers["Authorization"] = f"Bearer {token}"
self.exam_id = random.choice(EXAM_IDS)
@task(6)
def next_question(self):
self.client.get(f"/api/exams/{self.exam_id}/questions/next",
name="/api/exams/[id]/questions/next")
@task(3)
def submit_answer(self):
with self.client.post(f"/api/exams/{self.exam_id}/answers",
name="/api/exams/[id]/answers", catch_response=True,
json={"choice": random.randint(1, 4)}) as resp:
if resp.status_code == 200 and resp.json().get("saved") is not True:
resp.failure("200 but not saved")
@task(1)
def my_results(self):
self.client.get("/api/me/results")
Görev ağırlıkları ve bekleme süresi
@task ile işaretlenen metotlar görevlerdir (task). Her görevden sonra kullanıcı bir sonrakini rastgele seçer ve parantez içindeki sayı onun ağırlığıdır: burada soru getirme, sonuçları görüntülemeye göre altı kat, cevap göndermeye göre iki kat daha sık seçilir.
between(3, 12), her kullanıcının her görevden sonra, soruyu okuyan bir öğrenci gibi 3 ile 12 saniye arasında beklemesi anlamına gelir. Sabit bir hız için constant_throughput ve constant_pacing de var. Bu fonksiyonlar isteklere değil görevlere uygulanır ve yalnızca hıza üst sınır koyar: yanıtlar yavaşlarsa ek kullanıcı oluşturmazlar.
on_start ile giriş yapmak
Locust, her kullanıcı başladığında on_start metodunu bir kez çağırır; giriş yapıp token almanın yeri burasıdır. Her kullanıcının client nesnesi, requests.Session sınıfından türeyen bir HttpSession'dır; bu yüzden self.client.headers üzerinde ayarlanan bir header, o kullanıcının sonraki tüm istekleriyle gönderilir. Herkes aynı hesapla giriş yapmasın diye test hesapları bir dosyadan okunur; parolaları da koddan değil, bir ortam değişkeninden gelir. Giriş başarısız olursa giriş isteğinin kendisi başarısız olarak kaydedilir ve StopUser o kullanıcıyı durdurur; böylece istatistikler 401 hatalarıyla dolmaz. Locust bunu bir traceback ile loglar ve yerine yeni bir kullanıcı başlatmaz; dolayısıyla aktif kullanıcı sayısı düşer.
name ile gruplama, catch_response ile başarısızlık kuralları
Locust istatistikleri istek adına göre toplar ve bu ad varsayılan olarak URL'dir; yol bir ID içeriyorsa her ID kendi satırını alır. name argümanı tüm bu istekleri /api/exams/[id]/answers gibi tek bir etiket altında gruplar.
Varsayılan olarak durum kodu 400'ün altındaki her yanıt başarılı, 4xx ve 5xx ise başarısız sayılır. Ama bir API işi yapmadan da 200 döndürebilir. catch_response=True ve bir with bloğuyla kararı siz verirsiniz: resp.failure() isteği başarısız olarak kaydeder, resp.success() ise bir hata kodunu bile başarılı sayar. Başarısızlıklar mesaj metnine göre gruplanır, bu yüzden net mesajlar yazın. Bloğun içinde başka bir exception fırlatılırsa, örneğin yanıt gövdesi JSON olmadığı için, o istek istek istatistiklerine değil yalnızca exception listesine kaydedilir; bu yüzden JSON hatasını yakalayıp resp.failure() ile kaydedin.
Web arayüzü olmadan çalıştırmak ve raporlar
Web arayüzü bir senaryoyu denemek için kullanışlıdır, ama tekrarlanabilir çalıştırmalarda sonuçların karşılaştırılabilmesi için sabit parametrelerle --headless kullanın:
locust -f exam_day.py --headless -H https://staging.example.com \
-u 3000 -r 50 -t 30m --csv exam_day --html exam_day.html
-uzirvedeki eşzamanlı kullanıcı sayısı,-rise saniyede eklenen kullanıcı sayısıdır; burada 3000 kullanıcıya ulaşmak bir dakika sürer. Herkesin aynı anda başladığı bir sınav için bu dik artış senaryonun parçasıdır.-tçalışma süresidir ve ramp-up'ın sonundan değil, testin başından itibaren sayılır.--csv exam_dayseçeneğiexam_day_stats.csv(endpoint başına genel istatistikler),exam_day_failures.csv,exam_day_exceptions.csvveexam_day_stats_history.csvdosyalarını yazar. Geçmiş dosyasına çalışma boyunca yeni satırlar eklenir: varsayılan olarak yalnızca toplam (aggregated) değer için,--csv-full-historyile her endpoint için.--html, grafikler ve tablolar içeren bir HTML raporu yazar; bir ticket'a eklemek ya da iki çalıştırmayı karşılaştırmak için kullanışlıdır.
Tek bir istek bile başarısız olursa Locust 1 koduyla çıkar (--exit-code-on-error ile değiştirilebilir); bu da testi CI/CD'de çalıştırmayı kolaylaştırır. --reset-stats, tüm kullanıcılar başlatıldıktan sonra istatistikleri bir kez sıfırlar; böylece ramp-up dönemi nihai sonuca karışmaz (dağıtık bir çalıştırmada bunu hem master'a hem de worker'lara verin). Bir sınav senaryosunda o dönemi geçmiş dosyasında ayrıca inceleyin.
Tek makine yetmediğinde: dağıtık çalıştırma
GIL nedeniyle her Python süreci fiilen tek bir CPU çekirdeği kullanır. Locust sürecinin CPU kullanımı %90'ı aşarsa bir CPU usage above 90% uyarısı loglar; bu noktadan sonra sayılar API'nin davranışından çok yük üretecinin sınırları hakkında bilgi verir.
Dağıtık modda bir master süreç testi koordine eder ve istatistikleri toplar, worker süreçler ise kullanıcıları çalıştırır. Tek bir makinede locust --processes 4 bir master ve dört worker başlatır, --processes -1 ise mantıksal çekirdek başına bir worker başlatır; bu seçenek fork() çağrısına dayanır ve Windows'ta çalışmaz. Birden çok makine için ilk komut master'da, ikincisi yük üreten iki makinenin her birinde çalışır:
locust -f exam_day.py --master --headless --expect-workers 8 \
-H https://staging.example.com -u 12000 -r 200 -t 30m --csv exam_day
locust -f - --worker --master-host 10.0.0.10 --processes 4
--expect-workersmakineleri değil worker süreçlerini sayar: dörder süreçli iki makine 8 eder. Master hepsi bağlanana kadar bekler.-f -locustfile'ı master'dan alır, ama yalnızca tek bir dosyayı;test_users.csvgibi veri dosyalarının her makinede bulunması gerekir. Her süreç hesap listesini baştan okur; mükerrer hesaplar sorun oluyorsa hesapları yalnızca makinelere değil, worker süreçlere de bölüştürün.- Worker'lar varsayılan olarak master'a
5557portundan bağlanır. - Makine eklemeden önce
FastHttpUsersınıfını deneyin: Locust dokümantasyonuna göre çok daha az CPU zamanı kullanır. Yanıtları hızlandırmaz, yalnızca yük üretecini hafifletir. API'si benzerdir ama aynı değildir; örneğinself.client.headersyoktur, bu yüzden token header'ının her istekle gönderilmesi gerekir.
Sonuçları okumak: RPS, medyan, yüzdelikler ve başarısızlıklar
Locust tablosu her endpoint için istek ve başarısızlık sayısını, ortalama, minimum, maksimum ve medyan değerlerini ve saniyedeki istek sayısını (RPS) gösterir; sonunda da bir yüzdelikler tablosu ekler. RPS gerçek throughput'tur; kullanıcı eklediğiniz hâlde RPS artmıyorsa ya sistem ya da yük üretecinin kendisi tavana ulaşmıştır. Medyan, tipik bir isteğin yaşadığı süredir; 95. ve 99. yüzdelikler ise yavaş kuyruğu (tail) gösterir: isteklerin %5'i ve %1'i bu değerlerden yavaştır. Yanlarında başarısızlık oranı olmadan bu sayıların hiçbiri pek bir şey ifade etmez.
Ortalama yanıltıcıdır, çünkü birkaç çok yavaş istek dağılımın şeklini göstermeden onu yukarı çeker. Örneğin 100 isteğin 90'ı 20 milisaniye, 10'u 2 saniye sürerse ortalama 218 milisaniyedir; hiçbir isteğin gerçekte yaşamadığı bir sayı. Medyan 20 milisaniye, 95. yüzdelik iki saniyedir; asıl sorun yalnızca yüzdeliklerde görünür.
İki ince nokta var. Locust yüzdelikleri hesaplamak için 100 milisaniyenin üzerindeki yanıt sürelerini kabaca iki anlamlı basamağa yuvarlar (147 ms, 150 olarak kaydedilir); dolayısıyla yüzdelikleri yaklaşıktır. Ayrıca Locust'un yük modeli kapalıdır: bir kullanıcı, yanıt gelmeden sonraki isteğini göndermez; bu yüzden sunucu yavaşladığında gönderim hızı da düşer ve yavaş dönem istatistiklerde daha az örnekle temsil edilir. Bu etkiye coordinated omission denir. Gerçek kullanıcılarınız birbirinden bağımsız geliyor ve birbirlerinin yanıtını beklemiyorsa, Locust'un yüzdelikleri o kullanıcıların yaşadığından daha iyimser görünebilir.
Locust dokümantasyonu doygunluk (saturation) desenini şöyle anlatır: kullanıcılar eklenmeye devam eder, ama RPS artmayı bırakır ve yanıt süreleri hızla tırmanır. Sonuçları raporlarken koşulları sayıların yanına yazın. İzlenme süresi olay ingest servisinin (.NET 10 üzerinde bir minimal API) 12 çekirdekli bir iş istasyonunda, 10.000 olay ve 50 eşzamanlı kullanıcıyla yapılan bir Locust benchmark'ı saniyede yaklaşık 2.450 istek, 13 ms medyan ve 21 ms 95. yüzdelik gösterdi; servisin bellek kullanımı en fazla yaklaşık 108 MB'a çıktı. Bu tür sayılar, aynı kodun iki sürümünü aynı makinede karşılaştırmak için anlamlıdır; bir production sunucusunun kapasitesini tahmin etmek için değil.
Darboğazı bulmak: sunucu tarafına da bakın
Locust size hangi endpoint'in yavaşladığını söyler, nedenini değil. Benim yöntemim kısa: doygunluğa ulaşana kadar yükü adım adım artırın ve o noktada hangi kaynağın tükendiğine bakın. API sürecinin CPU'su %100'e yakınsa darboğaz muhtemelen uygulamanın kendi işlemesindedir; CPU düşük ve yanıt süreleri yüksekse istekler bir şeyi bekliyordur. Sonra yalnızca tek bir şeyi değiştirin, aynı senaryoyu aynı veriyle yeniden çalıştırın ve iki çalıştırmanın CSV dosyalarını yan yana koyun.
.NET tarafında dotnet-counters, bir sürecin canlı sayaçlarını hiçbir kod değişikliği olmadan gösterir. İlk komut süreç ID'sini bulur (burada 4312):
dotnet-counters ps dotnet-counters monitor -p 4312 --counters System.Runtime,Microsoft.AspNetCore.Hosting,Microsoft.Data.SqlClient.EventSource
- Thread pool. .NET 9 ve sonrasında
dotnet.thread_pool.thread.countvedotnet.thread_pool.queue.lengthsayaçlarını izleyin (.NET 8'de adları ThreadPool Thread Count ve ThreadPool Queue Length). Microsoft'un dokümantasyonuna göre thread pool starvation'ın işareti, CPU %100'ün epey altında kalırken thread sayısının yavaş ve sürekli artmasıdır: saniyede yaklaşık bir ya da iki thread. En yaygın neden.Resultya da.Wait()gibi sync-over-async koddur. - Bağlantı havuzu. SqlClient sayaçları (Microsoft.Data.SqlClient kütüphanesinin 3.0 sürümünden beri mevcut) havuzdaki aktif ve boş bağlantıları gösterir (
number-of-active-connectionsvenumber-of-free-connections). Her havuz için varsayılanMax Pool Size100'dür; tüm bağlantılar kullanımdayken bağlantı açma isteğiConnect Timeoutsüresi boyunca (varsayılan 15 saniye) bekler ve sonra başarısız olur. Kanıt değil, pratik bir kural:Connect Timeoutayarını değiştirmediyseniz ve bir grup yanıt yaklaşık 15 saniye sürdüyse havuzun tükendiğinden şüphelenin ve uygulama logunda timeout mesajını arayın. Sınırı yükseltmeden önce bağlantı sızıntılarını, yavaş sorguları ve bloklanmış transaction'ları kontrol edin. - SQL Server. Test sırasında
sys.dm_exec_requests, her isteğin neyi beklediğini (wait_type) ve onu hangi oturumun blokladığını (blocking_session_id) gösterir. Query Store açıksa SQL Server 2017 ve sonrası her sorgunun beklemelerini kategoriye göre kaydeder.sys.dm_os_wait_statsson yeniden başlatmadan itibaren birikir; bu yüzden testten önce ve sonra bir anlık görüntü alın.LCK_M_*bir kilidi beklemek,PAGEIOLATCH_*diskten okumayı beklemek demektir;ASYNC_NETWORK_IOise SQL Server'ın uygulamanın sonuçları tüketmesini beklediği anlamına gelir; bu durumda darboğaz bazen uygulamanın kendisindedir. Daha fazla ayrıntı SQL Server performansı rehberinde. - Redis.
SLOWLOG GETyavaş komutları listeler (ağ hariç, yalnızca sunucudaki çalışma süresi). StackExchange.Redis timeout mesajı thread pool istatistiklerini de içerir (Busy ve Min değerlerini gösteren WORKER bölümü); Busy, Min'den yüksekse yeni thread'ler gecikmeyle eklenir ve timeout'un kök nedeni Redis'te değil uygulamada olabilir.
Sık yapılan hatalar
- Bir kapasite testinde Locust'u API makinesinde çalıştırmak. Yük üreteci ile API CPU için yarışır; gerçek ağ yolu ve load balancer da devre dışı kalır. Aynı kodun iki sürümünü karşılaştırmak için bu sorun değildir; yeter ki bunu böyle raporlayın ve buradan production kapasitesi hakkında sonuç çıkarmayın.
- Soğuk başlangıç. Bir deploy'un hemen ardından birçok metot henüz JIT ile derlenmemiştir, önbellekler boştur ve havuzlar yeni büyümeye başlamaktadır. Kararlı durum davranışını ölçüyorsanız önce kısa, düşük yüklü bir çalıştırma yapın; sınavın başlangıcı gibi akının kendisini ölçüyorsanız soğuk başlangıç senaryonun parçasıdır.
- Gerçekçi olmayan veri. Herkes tek bir hesapla giriş yapar ya da aynı sınavı açarsa, ya her şey önbellekten gelir ya da herkes aynı satırdaki bir kilit için sıraya girer. ID'lerin dağılımı ve veri hacmi production'a yakın olmalıdır.
4xxve5xxyanıtlarını görmezden gelmek. Bir hata yanıtı genellikle doğru bir yanıttan daha ucuzdur; bu yüzden kullanıcılar hiçbir şey alamazken RPS yüksek, yanıt süreleri düşük görünür. Yaygın durumlar: uzun bir test sırasında token'ın süresi dolduğunda401; varsayılan olarak503döndüren ve IP'ye göre bölümlenmişse birkaç IP'den gelen tüm Locust kullanıcılarını tek bir kotaya sokan ASP.NET Core rate limiter'ı; ve izlenme süresi servisinin, bellek içi channel'ı dolduğunda istekleri reddetmesi gibi kasıtlı bir503. Bu son durum içincatch_responseile ayrı bir başarısızlık mesajı belirleyin.- Bekleme süresi olmaması.
wait_timeolmadan sonraki görev hemen çalışır ve "3000 kullanıcı" artık 3000 gerçek öğrenciyle karşılaştırılamaz.
Sınav zirvesinde yüksek erişilebilirlik vaka çalışması, zirve öncesi testlerin arka planını anlatıyor; yukarıdaki benchmark'ın ayrıntıları da izlenme süresi notunda. Bir sistemi trafik zirvesinden önce hazırlamak istiyorsanız Benimle çalışın sayfası nasıl yardımcı olabileceğimi anlatıyor.
Sık sorulan sorular
Locust nedir?
Locust, kullanıcı davranışını Python koduyla tanımlayan açık kaynaklı bir yük testi aracıdır. Simüle edilen her kullanıcı kendi senaryosunu çalıştırır ve Locust her endpoint için istek sayısını, başarısız istekleri, RPS'i ve yanıt süresi yüzdeliklerini raporlar.
.NET ile yazılmış bir API'yi test etmek için Locust kullanabilir miyim?
Evet. Locust, API ile dışarıdan, HTTP üzerinden konuşur; bu yüzden sunucu tarafındaki dil ve framework onun için fark etmez. Yalnızca senaryonun kendisi Python ile yazılır.
Locust'ta spawn rate ne anlama gelir?
Locust'un hedef sayıya ulaşana kadar saniyede eklediği kullanıcı sayısıdır. Son yükü değiştirmez, yalnızca oraya ne kadar hızlı ulaşacağınızı belirler; herkesin aynı anda başladığı bir olayda dik bir artış senaryonun parçasıdır.
Neden ortalama yanıt süresi yerine 95. yüzdeliğe bakmalı?
Birkaç çok yavaş istek ortalamayı yukarı çeker ve dağılımın şeklini gizler. 95. yüzdelik, isteklerin en yavaş %5'inin aştığı değeri gösterir; kullanıcıların hissettiği de bu yavaş kuyruktur.
Locust ne zaman dağıtık çalıştırılmalı?
Locust, CPU kullanımının %90'ın üzerinde olduğu uyarısını verdiğinde ya da tek bir süreç ihtiyacınız olan yükü üretemediğinde. Her Locust süreci fiilen tek bir CPU çekirdeği kullanır; bu yüzden olağan yaklaşım çekirdek başına bir worker çalıştırmak ve gerektiğinde makine eklemektir.
Mahdi Molavi