Benchmarking Apache vs Nginx auf Heroku

Ich habe das Buildpack von Heroku für PHP von Apache auf NGINX umgestellt. NGINX – ist der Webserver meiner Wahl weil er gegenüber dem Apache Webserver einen deutlich geringeren Resourcenverbrauch und somit ein besseres Laufzeitverhalten unter hoher Last hat. Es können sehr viel mehr gleichzeitige Requests bei gleichbleibendem RAM-Verbrauch beantwortet werden als das mit Apache der Fall ist. Daher tritt die Notwendigkeit horizontal Skalieren zu müssen deutlich später ein – oder anders gesagt: Mit NGINX bekommen Sie deutlich mehr Leistung aus einem einzelnen Heroku Dyno heraus (ein einzelner Dyno stellt 512MB RAM bereit). NGINX kann mehr als 10,000 simultane Verbindungen mit einem sehr geringen Memory Footprint bedienen: ~2.5 MB pro 10k inaktive HTTP keep-alive connections. NGINX erreicht dies, indem er einen asynchronen eventbasierten Ansatz verfolgt um Requests zu bearbeiten anstatt wie beim Apache-Server Model auf Threads oder Prozesse zu setzen. Weiterführende Infos finden Sie in diesem Vergleichs-Wiki.

Für ein Einsatz “in the Wild” wird es sich also sehr positiv auswirken dass die HTTP keep-alive connections weniger Speicher verbrauchen werden und durch den asynchronen eventbasierten Ansatz Requests schneller beantwortet werden können. Die wahre Power von NGINX wird sich also erst bemerkbar machen, wenn man viele Keep-Alive Connections hat die dann immer wieder neue Requests aufnehmen und RAM gegrenzt ist – genau das Szenario wenn man eine high-Traffic WordPress Installation mit möglichst wenigen Heroku Dynos betreiben möchte.

Nachdem ich das Buildpack mit nginx nun seit einigen Wochen im Einsatz habe sollte ich doch zumindest mal einen sehr einfachen Test mit AB ausprobieren von dem ich mir eigentlich kaum aussagekräftige Resultate erwartet hatte.

Vergleicht wird ein WordPress mit W3Total Cache auf einem Heroku Dyno. Einmal mit Apache und einmal mit NGINX. Die Installierten Plugins sind identisch – das Theme nicht und der Content ist auch nicht identisch – daher hat der Apache etwas weniger Bytes pro Requests zu servieren als der NGINX und somit theoretish sogar einen Vorteil – irgendwie sind es ja immer Äpfel und Birnen die verglichen werden. Dennoch gibt es ein grobes Verständnis für den Unterschied zwischen einem Heroku Dyno der mit
A) Apache und php
Serveriert und einem Dyno der mit
B) NGINX und php-fpm
serviert.

A) Default Buildpack von Heroku für PHP

ab -c 100 -n 500 "http://************.herokuapp.com/*******"

Server Software: Apache/2.2.22
Server Hostname: **************.herokuapp.com
Server Port: 80

Document Path: *************
Document Length: 6573 bytes

Concurrency Level: 100
Time taken for tests: 4.302335 seconds
Complete requests: 500
Failed requests: 0
Write errors: 0
Total transferred: 3547274 bytes
HTML transferred: 3373898 bytes
Requests per second: 116.22 [#/sec] (mean)
Time per request: 860.467 [ms] (mean)
Time per request: 8.605 [ms] (mean, across all concurrent requests)
Transfer rate: 805.14 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 96 97 2.1 98 107
Processing: 207 674 144.9 694 1130
Waiting: 109 574 144.7 594 1033
Total: 303 772 145.1 793 1226

Percentage of the requests served within a certain time (ms)
50% 793
66% 835
75% 852
80% 860
90% 919
95% 986
98% 1013
99% 1096
100% 1226 (longest request)

B) Custom Buildpack mit nginx, php-fpm und new relic 2.x

ab -c 100 -n 500 "http://**********.herokuapp.com/******"

Server Software: nginx
Server Hostname: ***********.herokuapp.com
Server Port: 80

Document Path: *******
Document Length: 25102 bytes

Concurrency Level: 100
Time taken for tests: 2.592448 seconds
Complete requests: 500
Failed requests: 3
(Connect: 0, Length: 3, Exceptions: 0)
Write errors: 0
Total transferred: 13395072 bytes
HTML transferred: 13252730 bytes
Requests per second: 192.87 [#/sec] (mean)
Time per request: 518.490 [ms] (mean)
Time per request: 5.185 [ms] (mean, across all concurrent requests)
Transfer rate: 5045.81 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 97 101 1.9 101 109
Processing: 305 358 64.0 332 824
Waiting: 108 140 47.9 124 553
Total: 403 459 64.2 435 926

Percentage of the requests served within a certain time (ms)
50% 435
66% 448
75% 464
80% 487
90% 538
95% 579
98% 662
99% 756
100% 926 (longest request)

Die ausgewiesenen Length Fehler können ignoriert werden – “ab” geht wohl davon aus dass alle Results die selbe Länge haben müssten was bei dynamischen Antworten nicht der Fall ist. Wichtig ist, dass keine echten “Non-2xx responses” auftreten.

Ich habe also im Mittel 116.22 [#/sec] via apache zu 192.87 [#/sec] via nginx.
Nginx kann ich auf einem Dyno bis zu ca. 300 [#/sec] hochtreiben bevor er Fehler in Form von “Non-2xx responses” erzeugt. Natürlich kommen die Antworten aus dem Cache und greifen nicht auf die Datenbank zurück. Aber die Requests wurden auf eine tatsächlich im Einsatz befindliche WordPress App mit W3Total Cache abgefeuert. Wenn man nun schaut wie weit man mit nur einem optimiertem WordPress mit nur einem Dyno kommt, so ist das doch schon sehr beeindruckend. Wer mehr Requests/Sekunde bedienen muss, kann weitere Dynos dazu schalten.

Kombiniert mit CloudFlare was auch schon viele Requests vom Original-Server fernhält weil die vom CloudFlare CDN bedient werden kann und zusätzlich Kombiniert mit CloudFront für statische Media-Dateien, kann man so mit einer WordPress-Installation auch allerhöchstem Performance Bedarf gerecht werden.

Hier als “Bonus” der gleiche Test – diesmal ist CloudFlare vorgeschaltet:


ab -c 100 -n 500 "http://www.********.de/*********"

Server Software: cloudflare-nginx
Server Hostname: www.*******.de
Server Port: 80

Document Path: *******
Document Length: 21871 bytes

Concurrency Level: 100
Time taken for tests: 1.344726 seconds
Complete requests: 500
Failed requests: 12
(Connect: 0, Length: 12, Exceptions: 0)
Write errors: 0
Total transferred: 11158552 bytes
HTML transferred: 10955146 bytes
Requests per second: 371.82 [#/sec] (mean)
Time per request: 268.945 [ms] (mean)
Time per request: 2.689 [ms] (mean, across all concurrent requests)
Transfer rate: 8103.51 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 5 7 1.5 7 11
Processing: 132 218 128.4 154 1059
Waiting: 117 157 63.6 132 597
Total: 140 226 127.9 162 1066

Percentage of the requests served within a certain time (ms)
50% 162
66% 196
75% 237
80% 318
90% 414
95% 442
98% 611
99% 815
100% 1066 (longest request)

Ein solcher “Hammering-Test” auf immer die selbe URL ergibt natürlich keine Werte die einem realen Einsatz-Scenario entsprechen. Dennoch ist es sinnvoll auf diese Weise verschiedene Konfigurationen durchzuspielen und ein Optimum zu finden um das maximum aus einem Dyno und dem Zusammenspiel zwischen Heroku und CloudFlare und allen weiteren Beteiligten Komponenten herauszuholen.

Und weil meine kleine Testreihe nur als Quick-and-Dirty-Test zu verstehen ist und sicherlich viele systematische Fehler enthält, möchte ich noch auf diesen fanstastischen Blog Beitrag verweisen, der Nginx Vs Apache im AWS-Umfeld vergleicht.