Ö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:


image2021-6-30_10-7-57.png


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:


Aşağıdaki adımlar "root" kullanıcısı ile yapıldı.


  • Java kurulumu yapıldı.
# yum install java-1.8.0-openjdk -y
BASH
  • 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)
BASH
  • Dosya indirme komutu kuruldu.
# yum install wget -y
BASH
  • Jmeter kurulumu için gerekli dosya indirildi.
# wget http://apache.stu.edu.tw//jmeter/binaries/apache-jmeter-5.2.1.tgz
BASH
  • tar dosyası açıldı ve dosyalar ayıklandı.
# tar -xf apache-jmeter-5.2.1.tgz
BASH
  • Jmeter için ortam bilgisi ayarlandı.
# vim ~/.bashrc
BASH
  • .bashrc dosyasına aşağıdaki satırlar eklendi.
export JMETER_HOME=/root/apache-jmeter-5.2.1
export PATH=$JMETER_HOME/bin:$PATH
BASH
  • source komutu ile Linux'un .bashrc dosyasını tekrar yüklemesi sağlandı.
# source ~/.bashrc
BASH
  • 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>


XML

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


2 "NGINX Sunucusu" Kurulumu ve NGINX Yapılandırması


NGINX backend servisin simülasyonu için kullanıldı.

Kurulu olduğu sunucunun özellikleri:

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
BASH
  • NGINX kurulumunu yapıldı.
# yum install NGINX
BASH
  • NGINX'i başlatıldı.
# systemctl start NGINX
BASH
  • 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/
BASH
  • Başarılı sonucu görüldükten sonra NGINX'in servis olarak çalışması sağlandı.
# systemctl enable NGINX
BASH
  • 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;
BASH
  • 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;
}
BASH
  • 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 {
        }
    }

}
BASH
  • NGINX'in yukarıda yapılan ayarlarının yeniden yüklemesini sağlandı:
# NGINX -s reload
BASH
  • 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:

Log veri tabanı (Elasticsearch) sunucu özellikleri:

Kubernetes worker sunucu özellikleri:


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 SettingsRouting Connection Pool Elastic Search Client 
CoreRam (gb)IO ThreadsMin. Thread CountMax. Thread CountHttp Max ConnectionsMax Conn. Per RouteMax Conn. TotalIO Thread CountMax Conn. Per RouteMax Conn. Total
11151210241024512102443264
222102420484096204840961664128
444102440968192409681923264128
88161024819281924096819232128256
  • 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:

  1. Kurulumu şu sayfada açıklanan Kubernetes dashboard,
  2. 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>
BASH


  • 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
YML


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ı




GETPOST 5KbPOST 50Kb
NoThread CountThroughputAvgThroughputAvgThroughputAvg
A5011334310024967573
100110090983101653152
2501025242852292554448
500963516----
B50223222186826143734
100216945176856140970
250208911914561701223203
500191525913983551149432
1000176256412298098771134
1500163191511991245--
200013791441----
C508090673536467910
100781612725713467521
250701135713834402061
5006759737141693221154
1000674214770111412962335
150066832236935215--
20006692297----
40006448617----
D50154203133963468310
100158126134827467121
25015614151358718438256
500156643113611363496142
1000154546413562733046326
15001502699132081122853522
200014839133131791502794710
40001435627612792309--
80001160365511115701--


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:



GETGET with Policy
NoThread CountThroughputAvgThroughputAvg
D50154203147603
100158126148436
25015614151489116
50015664311474833
100015454641428568
1500150269914373102
20001483913314280136
40001435627613795279
80001160365511437672


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.