Confronto tra contenitori servlet incorporati in Spring Boot

1. Introduzione

La crescente popolarità delle applicazioni cloud native e dei microservizi genera una maggiore domanda di contenitori servlet incorporati. Spring Boot consente agli sviluppatori di creare facilmente applicazioni o servizi utilizzando i 3 contenitori più maturi disponibili: Tomcat, Undertow e Jetty.

In questo tutorial, dimostreremo un modo per confrontare rapidamente le implementazioni del contenitore utilizzando le metriche ottenute all'avvio e sotto un certo carico.

2. Dipendenze

La nostra configurazione per ogni implementazione di container disponibile richiederà sempre che dichiariamo una dipendenza da spring-boot-starter-web nel nostro pom.xml .

In generale, vogliamo specificare il nostro genitore come spring-boot-starter-parent e quindi includere gli antipasti che vogliamo:

 org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE     org.springframework.boot spring-boot-starter   org.springframework.boot spring-boot-starter-web   

2.1. Tomcat

Non sono necessarie ulteriori dipendenze quando si utilizza Tomcat perché è incluso per impostazione predefinita quando si utilizza spring-boot-starter-web .

2.2. Molo

Per poter utilizzare Jetty, dobbiamo prima escludere spring-boot-starter-tomcat da spring-boot-starter-web .

Quindi, dichiariamo semplicemente una dipendenza da spring-boot-starter-jetty :

 org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-tomcat     org.springframework.boot spring-boot-starter-jetty  

2.3. Risacca

L'impostazione per Undertow è identica a Jetty, tranne per il fatto che usiamo spring-boot-starter-undertow come nostra dipendenza:

 org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-tomcat     org.springframework.boot spring-boot-starter-undertow 

2.4. Attuatore

Useremo l'attuatore di Spring Boot come un modo conveniente per stressare il sistema e interrogare le metriche.

Dai un'occhiata a questo articolo per i dettagli su Actuator. Aggiungiamo semplicemente una dipendenza nel nostro pom per renderlo disponibile:

 org.springframework.boot spring-boot-starter-actuator 

2.5. Panca Apache

Apache Bench è un'utilità di test di carico open source fornita in bundle con il server Web Apache.

Gli utenti Windows possono scaricare Apache da uno dei fornitori di terze parti collegati qui. Se Apache è già installato sulla macchina Windows, dovresti riuscire a trovare ab.exe nella tua directory apache / bin .

Se sei su una macchina Linux, ab può essere installato usando apt-get con:

$ apt-get install apache2-utils

3. Metriche di avvio

3.1. Collezione

Per raccogliere le nostre metriche di avvio, registreremo un gestore di eventi da attivare su ApplicationReadyEvent di Spring Boot .

Estrarremo programmaticamente le metriche che ci interessano lavorando direttamente con MeterRegistry utilizzato dal componente Actuator:

@Component public class StartupEventHandler { // logger, constructor private String[] METRICS = { "jvm.memory.used", "jvm.classes.loaded", "jvm.threads.live"}; private String METRIC_MSG_FORMAT = "Startup Metric >> {}={}"; private MeterRegistry meterRegistry; @EventListener public void getAndLogStartupMetrics( ApplicationReadyEvent event) { Arrays.asList(METRICS) .forEach(this::getAndLogActuatorMetric); } private void processMetric(String metric) { Meter meter = meterRegistry.find(metric).meter(); Map stats = getSamples(meter); logger.info(METRIC_MSG_FORMAT, metric, stats.get(Statistic.VALUE).longValue()); } // other methods }

Evitiamo la necessità di interrogare manualmente gli endpoint REST dell'attuatore o di eseguire una console JMX autonoma registrando metriche interessanti all'avvio all'interno del nostro gestore di eventi.

3.2. Selezione

Ci sono un gran numero di metriche che Actuator fornisce fuori dagli schemi. Abbiamo selezionato 3 metriche che aiutano a ottenere una panoramica di alto livello delle caratteristiche chiave di runtime una volta che il server è attivo:

  • jvm.memory.used : la memoria totale utilizzata dalla JVM dall'avvio
  • jvm.classes.loaded – the total number of classes loaded
  • jvm.threads.live – the total number of active threads. In our test, this value can be viewed as the thread count “at rest”

4. Runtime Metrics

4.1. Collection

In addition to providing startup metrics, we'll use the /metrics endpoint exposed by the Actuator as the target URL when we run Apache Bench in order to put the application under load.

In order to test a real application under load, we might instead use endpoints provided by our application.

Once the server has started, we'll get a command prompt and execute ab:

ab -n 10000 -c 10 //localhost:8080/actuator/metrics

In the command above, we've specified a total of 10,000 requests using 10 concurrent threads.

4.2. Selection

Apache Bench is able to very quickly give us some useful information including connection times and the percentage of requests that are served within a certain time.

For our purposes, we focused on requests-per-second and time-per-request (mean).

5. Results

On startup, we found that the memory footprint of Tomcat, Jetty, and Undertow was comparable with Undertow requiring slightly more memory than the other two and Jetty requiring the smallest amount.

For our benchmark, we found that the performance of Tomcat, Jetty, and Undertow was comparable but that Undertow was clearly the fastest and Jetty only slightly less fast.

Metric Tomcat Jetty Undertow
jvm.memory.used (MB) 168 155 164
jvm.classes.loaded 9869 9784 9787
jvm.threads.live 25 17 19
Requests per second 1542 1627 1650
Average time per request (ms) 6.483 6.148 6.059

Note that the metrics are, naturally, representative of the bare-bones project; the metrics of your own application will most certainly be different.

6. Benchmark Discussion

Developing appropriate benchmark tests to perform thorough comparisons of server implementations can get complicated. In order to extract the most relevant information, it's critical to have a clear understanding of what's important for the use case in question.

It's important to note that the benchmark measurements collected in this example were taken using a very specific workload consisting of HTTP GET requests to an Actuator endpoint.

It's expected that different workloads would likely result in different relative measurements across container implementations. If more robust or precise measurements were required, it would be a very good idea to set up a test plan that more closely matched the production use case.

In addition, a more sophisticated benchmarking solution such as JMeter or Gatling would likely yield more valuable insights.

7. Choosing a Container

Selecting the right container implementation should likely be based on many factors that can't be neatly summarized with a handful of metrics alone. Comfort level, features, available configuration options, and policy are often equally important, if not more so.

8. Conclusion

In questo articolo, abbiamo esaminato le implementazioni dei contenitori servlet incorporati Tomcat, Jetty e Undertow. Abbiamo esaminato le caratteristiche di runtime di ogni contenitore all'avvio con le configurazioni predefinite esaminando le metriche esposte dal componente Actuator.

Abbiamo eseguito un carico di lavoro artificioso sul sistema in esecuzione e poi abbiamo misurato le prestazioni utilizzando Apache Bench.

Infine, abbiamo discusso i meriti di questa strategia e menzionato alcune cose da tenere a mente quando si confrontano i benchmark di implementazione. Come sempre, tutto il codice sorgente può essere trovato su GitHub.