Benchmark Sonuçları
ÖZET
Eş-Zamanlı 2.000 Thread ile 1Cpu 8 Core sistem üzerinde, 15.000 reqs/sec karşılanmaktadır.
Bu sonuçlar çağrılan servisin yanıt süresine, network gecikmesine, gateway üzerine eklenen politikaların sistem gereksinimlerine göre değişiklik göstereceğinden yaptığımız yük testinin detaylarını aşağıdaki bölümde inceleyebilirsiniz.
DETAY
Apinizer platformunun kolay kullanımı ve hızlı desteği nedeniyle yük testlerinin çalıştırılmasında altyapı olarak DigitalOcean platformu kullanılmıştır.
Yük testi topolojisi aşağıdaki gibidir:
Bu topolojiyi oluşturmak ve yük testlerimizi çalıştırmak için şu adımlar izlendi:
1 "Yük Test Sunucusu" Kurulumu ve JMeter Yapılandırması
Yük Testi Sunucusu özellikleri:
- CPU-Optimized
- Dedicated CPU
- 8 vCPUs (Intel, second generation Xeon Scalable processors, 2.5 GHz)
- 16 GB Ram
- 100 GB Disk
- CentOS 8.3 x64
Aşağıdaki adımlar "root" kullanıcısı ile yapıldı.
- Java kurulumu yapıldı.
# yum install java-1.8.0-openjdk -y
- Java kurulumunun başarılı olduğu aşağıdaki komut ile teyit edildi.
# java -version
openjdk version "1.8.0_275"
OpenJDK Runtime Environment (build 1.8.0_275-b01)
OpenJDK 64-Bit Server VM (build 25.275-b01, mixed mode)
- Dosya indirme komutu kuruldu.
# yum install wget -y
- Jmeter kurulumu için gerekli dosya indirildi.
# wget http://apache.stu.edu.tw//jmeter/binaries/apache-jmeter-5.2.1.tgz
- tar dosyası açıldı ve dosyalar ayıklandı.
# tar -xf apache-jmeter-5.2.1.tgz
- Jmeter için ortam bilgisi ayarlandı.
# vim ~/.bashrc
- .bashrc dosyasına aşağıdaki satırlar eklendi.
export JMETER_HOME=/root/apache-jmeter-5.2.1
export PATH=$JMETER_HOME/bin:$PATH
- source komutu ile Linux'un
.bashrc
dosyasını tekrar yüklemesi sağlandı.
# source ~/.bashrc
- Yük testi için script, Jmeter'in arayüzü ile hazırlandı.
- Thread sayısını ve ne kadar süre ile çalışacağı bilgisi parametrik yapıldı.
Örnek içerik:
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.2">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
<stringProp name="TestPlan.comments"></stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="TestPlan.user_define_classpath"></stringProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<intProp name="LoopController.loops">-1</intProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">${__P(threads,10)}</stringProp>
<stringProp name="ThreadGroup.ramp_time">5</stringProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<stringProp name="ThreadGroup.duration">${__P(seconds,30)}</stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
</ThreadGroup>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP Request" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain"><Change with Your IP></stringProp>
<stringProp name="HTTPSampler.port">30080</stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">/apigateway/<Change with Your Path></stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true">
<boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name="filename"></stringProp>
</ResultCollector>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>
<Change with Your IP> yazan kısımda yükün gönderileceği IP adresinin olması gerekli.
<Change with Your Path> yazan kısımda yükün gönderileceği Http istek adresinin olması gerekli.
threads ve seconds değerleri parametrik olup çalışma zamanında verilecek.
Bu konfigürasyon'un ekran görüntüsü:
- Thread Group:
- HTTP Request:
- Aşağıdaki komutu thread ve mesaj süresini değiştirerek çalıştırıldı, sonuçlar kayıt altına alındı.
# jmeter -Jthreads=1000 -Jseconds=600 -n -t ./conf/configurable.jmx
2 "NGINX Sunucusu" Kurulumu ve NGINX Yapılandırması
NGINX backend servisin simülasyonu için kullanıldı.
Kurulu olduğu sunucunun özellikleri:
- CPU-Optimized
- Dedicated CPU
- 4 vCPUs (Intel, second generation Xeon Scalable processors, 2.5 GHz)
- 8 GB Ram
- 50 GB Disk
- CentOS 8.3 x64
Sunucuya şu adımlarla NGINX kuruldu :
Aşağıdaki adımlar "root" kullanıcısı ile yapıldı.
- EPEL repository kurulumunu yapıldı.
# yum install epel-release
- NGINX kurulumunu yapıldı.
# yum install NGINX
- NGINX'i başlatıldı.
# systemctl start NGINX
- NGINX'in başladığından emin olmak için tarayıcı ile kurulum yapılan makineye erişim denendi.
http://server_domain_name_or_IP/
- Başarılı sonucu görüldükten sonra NGINX'in servis olarak çalışması sağlandı.
# systemctl enable NGINX
- NGINX'in yüksek yük altında çalışabilmesi için konfigürasyon dosyasına aşağıdakiler eklendi/düzenlendi.
worker_processes 4;
worker_connections 8192;
worker_rlimit_nofile 40000;
- NGINX'in body olarak 'OK', statusCode olarak 200 dönmesi için aşağıdaki ayar yapıldı:
location / {
return 200 'OK';
add_header Content-Type text/plain;
}
- NGINX'in konfigürasyonu son durumda şu şekilde oldu:
# vi /etc/nginx/nginx.conf
user nginx;
worker_processes 4;
worker_rlimit_nofile 40000;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 8192;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /usr/share/nginx/html;
include /etc/nginx/default.d/*.conf;
location / {
return 200 'OK';
add_header Content-Type text/plain;
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
}
- NGINX'in yukarıda yapılan ayarlarının yeniden yüklemesini sağlandı:
# NGINX -s reload
- NGINX'in ayarları yüklediğinden emin olmak için tarayıcı ile sunucu adresine erişim denendi ve 'OK' yazısı görüldü.
3 "Apinizer ve Log Server" kurulumu ve Kubernetes, MongoDb, ElasticSearch kurulum ve konfigürasyonu
Kubernetes master ve mongodb sunucu özellikleri:
- CPU-Optimized
- Dedicated CPU
- 4 vCPUs (Intel, second generation Xeon Scalable processors, 2.5 GHz)
- 8 GB Ram
- 50 GB Disk
- CentOS 8.3 x64
Log veri tabanı (Elasticsearch) sunucu özellikleri:
- CPU-Optimized
- Dedicated CPU
- 8 vCPUs (Intel, second generation Xeon Scalable processors, 2.5 GHz)
- 16 GB Ram
- 100 GB Disk
- CentOS 8.3 x64
Kubernetes worker sunucu özellikleri:
- CPU-Optimized
- Dedicated CPU
- 16 vCPUs (Intel, second generation Xeon Scalable processors, 2.5 GHz)
- 32 GB Ram
- 200 GB Disk
- CentOS 8.3 x64
Bu bölümde kubernetes master, worker, mongodb, elasticsearch ve Apinizer kurulumları https://docs.apinizer.com/ adresinde yer alan kurulum adımları takip edilerek yapılmıştır.
4 Yük Testinin Önemli Noktaları
Test ederken dikkat edilecek noktalar:
- Apinizer, tüm istek & yanıt mesajlarını ve metriklerini Elasticsearch log veritabanında asenkron olarak saklar. Testler sırasında bu loglama işlemleri olması gerektiği gibi devam etti.
- Tüm testlerimizde ağ gecikmesini azaltmak ve Apinizer'ın gerçek etkisini görmek için iç IP'ler kullanıldı.
- Kubernetes'in çalışma zamanı sırasında pod'ları yeniden başlatmadığını özellikle gözlemledik. Yeniden başlatma sayısı, aşırı yük/tıkanma veya hatalı durumları yansıttığı için önemli bir parametredir.
- Aşağıdaki 4 konfigürasyon Apinizer üzerinde yer alan API Gateway Ortam Ayarlama ekranı ve Elasticsearch Cluster Ayarlama ekranlarında ayarlandı ve testlerde kullanıldı:
Worker Settings | Routing Connection Pool | Elastic Search Client | ||||||||
Core | Ram (gb) | IO Threads | Min. Thread Count | Max. Thread Count | Http Max Connections | Max Conn. Per Route | Max Conn. Total | IO Thread Count | Max Conn. Per Route | Max Conn. Total |
1 | 1 | 1 | 512 | 1024 | 1024 | 512 | 1024 | 4 | 32 | 64 |
2 | 2 | 2 | 1024 | 2048 | 4096 | 2048 | 4096 | 16 | 64 | 128 |
4 | 4 | 4 | 1024 | 4096 | 8192 | 4096 | 8192 | 32 | 64 | 128 |
8 | 8 | 16 | 1024 | 8192 | 8192 | 4096 | 8192 | 32 | 128 | 256 |
- Jvm parametrelerine şu değerler verildi: -server -XX:MaxRAMPercentage=90
- Yukarıdaki 4 durumu Get ve Post isteği ile test edildi. Post isteği için 5K ve 50K’lık istek gövdeleri kullandık.
- Testlerin her bir alt adımı 10'ar dakika sürdü.
5 Sunucu kaynaklarının izlenmesi
Apinizer kubernetes ortamında çalıştığından harcanan kaynakların izlenmesi için iki yöntem tercih edildi. Bunlar:
- Kurulumu şu sayfada açıklanan Kubernetes dashboard,
- JConsole
Kubernetes Dashboard üzerinden kaynakların izlenmesi nispeten kolaydı, sunucunun CPU ve Ram durumları anlık olarak veriyordu. Fakat bu yöntemin sakıncası kaynakları uzun vadeli izleyemiyor olmak ve detayı göstermiyor olmasıydı:
Bu sebeple JConsole kullanımı daha kullanışlı oldu.
JConsole uygulamasının kubernetes içindeki Pod'un içinde çalışan Java uygulamasına erişebilmesi için bazı ayarlar yapıldı:
- Java uygulamasına Jmx parametrelerinin geçilmesi için Java başlangıç parametreleri şu şekilde ayarlandı:
-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=30180 -Dcom.sun.management.jmxremote.rmi.port=30180 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=<Worker Sunucusunun Dış Erişim IP'si>
- Worker pod'un 30180 portundan açılan JMX servisinin dış dünyaya açılabilmesi için kubernetes'de aşağıdaki servis tanımı yapıldı:
apiVersion: v1
kind: Service
metadata:
name: worker-jmx-service
namespace: prod
labels:
app: worker
spec:
selector:
app: worker
type: NodePort
ports:
- name: http
port: 30180
targetPort: 30180
nodePort: 30180
Ayarlar bitince JConsole uygulaması çalıştırıldı ve worker sunucusunun dış erişim adresi ve 30180 portunu verilerek worker üzerinde çalışan JVM'e erişim sağlandı.
6 Yük testi sonuçları
GET | POST 5Kb | POST 50Kb | |||||
No | Thread Count | Throughput | Avg | Throughput | Avg | Throughput | Avg |
A | 50 | 1133 | 43 | 1002 | 49 | 675 | 73 |
100 | 1100 | 90 | 983 | 101 | 653 | 152 | |
250 | 1025 | 242 | 852 | 292 | 554 | 448 | |
500 | 963 | 516 | - | - | - | - | |
B | 50 | 2232 | 22 | 1868 | 26 | 1437 | 34 |
100 | 2169 | 45 | 1768 | 56 | 1409 | 70 | |
250 | 2089 | 119 | 1456 | 170 | 1223 | 203 | |
500 | 1915 | 259 | 1398 | 355 | 1149 | 432 | |
1000 | 1762 | 564 | 1229 | 809 | 877 | 1134 | |
1500 | 1631 | 915 | 1199 | 1245 | - | - | |
2000 | 1379 | 1441 | - | - | - | - | |
C | 50 | 8090 | 6 | 7353 | 6 | 4679 | 10 |
100 | 7816 | 12 | 7257 | 13 | 4675 | 21 | |
250 | 7011 | 35 | 7138 | 34 | 4020 | 61 | |
500 | 6759 | 73 | 7141 | 69 | 3221 | 154 | |
1000 | 6742 | 147 | 7011 | 141 | 2962 | 335 | |
1500 | 6683 | 223 | 6935 | 215 | - | - | |
2000 | 6692 | 297 | - | - | - | - | |
4000 | 6448 | 617 | - | - | - | - | |
D | 50 | 15420 | 3 | 13396 | 3 | 4683 | 10 |
100 | 15812 | 6 | 13482 | 7 | 4671 | 21 | |
250 | 15614 | 15 | 13587 | 18 | 4382 | 56 | |
500 | 15664 | 31 | 13611 | 36 | 3496 | 142 | |
1000 | 15454 | 64 | 13562 | 73 | 3046 | 326 | |
1500 | 15026 | 99 | 13208 | 112 | 2853 | 522 | |
2000 | 14839 | 133 | 13179 | 150 | 2794 | 710 | |
4000 | 14356 | 276 | 12792 | 309 | - | - | |
8000 | 11603 | 655 | 11115 | 701 | - | - |
Throughput & Concurrent Users
Average Response Time & Concurrent Users
Sonuçlar ile ilgili yorumlar:
Sonuçları incelerken çok sık yapılan hata session sayısı ile anlık istek sayısının karıştırılmasıdır. İstek, belirli bir Http metod ile belirli bir hedef için yapılan HTTP isteğidir. Session başına sıfır veya daha fazla istek olabilir. Örneğin bir web uygulamasında 50K session olması anlık isteğin 50K olacağı anlamına gelmez 50K’nın aynı anda istek yapma olasılığı ise çok düşüktür. Gatewaylerde session tutmak çok nadir görülür, genellikle servislere erişim stateless’dir. Bu yüzden eş zamanlı istek sayısını ve gecikmeyi (latency) ölçmek daha anlamlı hale gelir.
Eşzamanlı kullanıcı sayısı arttığında verim belirli bir sınıra kadar arttar. Sonrasında düşüş yaşanmaya başlar. Aslında doğal olan bu seyir dikey büyümenin bir sınırı olduğunu ifade eder. Kabul edilebilir yanıt sürelerine sahip daha fazla eşzamanlı kullanıcıyı desteklemek için, yatay veya dikey ölçeklendirmeyi beraber düşünmek gerekir. Yatay olarak ölçeklendirme yapılırken, diğer gatewaylerde iki veya daha fazla gateway’in önüne bir yük dengeleyici koymak gerekirken Apinizer’da kubernetes altyapısı kullanıldığından bu işlem çok kolay ve hızlı şekilde yapılandırılabilir.
Mesaj boyutları arttığında işlem gücü artacağından verim azalır. Dolayısıyla yanıt süresi de uzar. Genellikle gerçek yaşam senaryolarında istek boyutları 1Kb ortalamasında olsa da testlerimizde 1Kb Post ile Get isteklerimiz arasında çok küçük bir fark olduğundan 5Kb ve 50Kb Post isteklerini incelemeye değer bulduk. Sonuçlar doğal olarak GET isteklerine göre daha düşük bir değerde seyretse de 10 kat artan veriye göre rakamların sadece 4'te birine düşmesi bizim adımıza sevindirici oldu.
Yük testi boyunca harcanan Ram oranları çok tutarlıydı. İsteklerin boyutu on kat artsa da Ram kullanımında ciddi bir artış gözlenmedi. Bu da Openj9'un doğru bir tercih olduğunu ispatladı.
“D” Durumu, 8000 Thread, Get isteği için VM’den bir kesit:
Poliçe Ekleyelim
Gateway’e eklediğimiz her bir poliçe gateway'de karmaşıklığına ve bağımlılıklarına göre performansı etkiler.
Şimdi Apinizer’a “basic authentication” poliçesi ekleyelim. Bu konfigürasyonu tüm durumlar için değil sadece “D” durumu için test edelim, sonuçta bir fikir vermesi yeterli:
GET | GET with Policy | ||||
No | Thread Count | Throughput | Avg | Throughput | Avg |
D | 50 | 15420 | 3 | 14760 | 3 |
100 | 15812 | 6 | 14843 | 6 | |
250 | 15614 | 15 | 14891 | 16 | |
500 | 15664 | 31 | 14748 | 33 | |
1000 | 15454 | 64 | 14285 | 68 | |
1500 | 15026 | 99 | 14373 | 102 | |
2000 | 14839 | 133 | 14280 | 136 | |
4000 | 14356 | 276 | 13795 | 279 | |
8000 | 11603 | 655 | 11437 | 672 |
Throughput & Concurrent Users
Average Response Time & Concurrent Users
Gördüğümüz gibi performansa etkisi hissedilmeyecek derecede de olsa olmuş. Fakat örneğin “content filtering” gibi işlem gücü yüksek maliyetli bir poliçe eklenseydi ya da “Ldap Authentication” gibi dış bağlantı gerektiren ve araya bir de network latency ekleyen poliçe eklenseydi performans daha da hızlı bir şekilde düşecekti. Burada önemli olan her bir poliçenin ne kadar yük getireceğini bilmek ve tasarımı ona göre seçmek.