Optimize Nginx for performance

There are many possible real life cases and not all optimization technics will be suitable for you but I hope it will be a good starting place.

Also you shouldn’t copy paste examples with faith that they will make your server fly 🙂 You have to support your decisions with excessive tests and help of monitoring system (ex. Grafana).

Cache static and dynamic content

Setting caching static and dynamic content strategy may offload your server from additional load from repetitive downloads of same, rarely updated files. This will make your site to load faster for frequent visitors.

Example configuration:

location ~* ^.+\.(?:jpg|png|css|gif|jpeg|js|swf|m4v)$ {
    access_log off; log_not_found off;

    tcp_nodelay off;

    open_file_cache max=500 inactive=120s;
    open_file_cache_valid 45s;
    open_file_cache_min_uses 2;
    open_file_cache_errors off;

    expires max;
}

For additional performance gain, you may:

  • disable logging for static files,
  • disable tcp_nodelay option – it’s useful to send a lot of small files (ideally smaller than single TCP packet – 1,5Kb), but images are rather big files and sending them all together will gain better performance,
  • play with open_file_cache – it will take off some IO load,
  • add long long expires.

Caching dynamic content is harder case. There are articles that are rarely updated and they may lay in cache forever but other pages are pretty dynamic and shouldn’t be cached for long. Even if caching dynamic content sounds scary for you it’s not. So called micro caching (caching for short period of time, like 1s) – is great solution for digg effect or slashdotting.

Let say your page gets ten views per second and you will cache ever site for 1s, then you will be able to server 90% of requests from cache. Leaving precious CPU cycles for other tasks.

Compress data

On your page you should use filetypes that are efficiently compressed like: JPEG, PNG, MP3, etc. But all HTML, CSS, JS may be compressed too on the fly by web server, just enable options like that globally:

gzip on;
gzip_vary on;
gzip_disable "msie6";
gzip_comp_level 1;
gzip_proxied any;
gzip_buffers 16 8k;
gzip_min_length 50;
gzip_types text/plain text/css application/json application/x-javascript application/javascript text/javascript application/atom+xml application/xml application/xml+rss text/xml image/x-icon text/x-js application/xhtml+xml image/svg+xml;

You may also precompress these files stronger during build/deploy process and use gzip_static module to serve them without additional overhead for compression. Ex.:

gzip_static on;

Then use script like this to compress files:

find /var/www -iname *.js -print0 |xargs -0 -I'{}' sh -c 'gzip -c9 "{}" > "{}.gz" && touch -r "{}" "{}.gz"'
find /var/www -iname *.css -print0 |xargs -0 -I'{}' sh -c 'gzip -c9 "{}" > "{}.gz" && touch -r "{}" "{}.gz"'

Files have to had same timestamp like original (not compressed) file to be used by Nginx.

Optimize SSL/TLS

New optimized versions of HTTP protocols like HTTP/2 or SPDY require HTTPS configuration (at least in browsers implementation). Then SSL/TLS high cost of every new HTTPS connection became crucial case for further optimizations.

There are few steps required for improved SSL/TLS performance.

Enable SSL session caching

Use ssl_session_cache directive to cache parameters used when securing each new connection, ex.:

ssl_session_cache builtin:1000 shared:SSL:10m;

Enable SSL session tickets

Tickets store information about specific SSL/TLS connection so connection may be reused without new handshake, ex.:

ssl_session_tickets on;

Configure OCSP stapling for SSL

This will lower handshaking time by caching SSL/TLS certificate informations. This is per site/certificate configuration, ex.:

  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_certificate /etc/ssl/certs/my_site_cert.crt;
  ssl_certificate_key /etc/ssl/private/my_site_key.key;
  ssl_trusted_certificate /etc/ssl/certs/authority_cert.pem;

A ssl_trusted_certificate file have to point to trusted certificate chain file – root + intermediate certificates (this can be downloaded from your certificate provider site (sometimes you have to merge by yourself those files).

Excessive article in this topic could be found here: https://raymii.org/s/tutorials/OCSP_Stapling_on_nginx.html

Implement HTTP/2 or SPDY

If you have HTTPS configured the only thing you have to do is to add two options on listen directive, ex.:

listen 443 ssl http2; # currently http2 is preferred against spdy;

# on SSL enabled vhost
ssl on;

You may also advertise for HTTP connection that you have newer protocol available, for that on HTTP connections use this header:

add_header Alternate-Protocol 443:npn-spdy/3;

SPDY and HTTP/2 protocols use:

  • headers compression,
  • single, multiplexed connection (carrying pieces of multiple requests and responses at the same time) rather than multiple connection for every piece of web page.

After SPDY or HTTP/2 implementation you no longer need typical HTTP/1.1 optimizations like:

  • domain sharding,
  • resource (JS/CSS) merging,
  • image sprites.

Tune other nginx performance options

Access logs

Disable access logs were you don’t need them, ex.: for static files. You may also use buffer and flush options with access_log directive, ex.:

access_log /var/log/nginx/access.log buffer=1m flush=10s;

With buffer Nginx will hold that much data in memory before writing it to disk. flush tells Nginx how often it should write gathered logs to disk.

Proxy buffering

Turning proxy buffering may impact performance of your reverse proxy.

Normally when buffering is disabled, Nginx will pass response directly to client synchronously.

When buffering is enable it will store response in memory set by proxy_buffer_size option and if response is too big it will be stored in temporary file.

proxy_buffering on;
proxy_buffer_size 16k;

Keepalive for client and upstream connections]

Every new connection costs some time for handshake and will add latency to requests. By using keepalive connections will be reused without this overhead.

For client connections:

keepalive_timeout = 120s;

For upstream connections:

upstream web_backend {
    server 127.0.0.1:80;
    server 10.0.0.2:80;

    keepalive 32;
}

Limit connections to some resources

Some time users/bots overload your service by querying it to fast. You may limit allowed connections to protect your service in such case, ex.:

 limit_conn_zone $binary_remote_addr zone=owncloud:1m;

server {
    # ...
    limit_conn owncloud 10;
    # ...
}

Adjust woker count

Normally Nginx will start with only 1 worker process, you should adjust this variable to at the number of CPU’s, in case of quad core CPU use in main section:

worker_processes 4;

Use socket sharding

In latest kernel and Nginx versions (at least 1.9.1) there is new feature of sockets sharding. This will offload management of new connections to kernel. Each worker will create a socket listener and kernel will assign new connections to them as they become available.

listen 80 reuseport;

Thread pools

Thread pools are solution for mostly long blocking IO operations that may block whole Nginx event queue (ex. when used with big files or slow storage).

location / {
    root /storage;
    aio threads;
}

This will help a lot if you see many Nginx processes in D state, with high IO wait times.

Tune Linux for performance

Backlog queue

If you could see on your system connection that appear to be staling then you have to increase net.core.somaxconn. This system parameter describes the maximum number of backlogged sockets. Default is 128 so setting this to 1024 should be no big deal on any decent machine.

echo "net.core.somaxconn=1024" >> /etc/sysctl.conf
sysctl -p /etc/sysctl.conf

File descriptors

If your system is serving a lot of connections you may get reach system wide open descriptor limit. Nginx uses up to two descriptors for each connection. Then you have to increase sys.fs.fs_max.

echo "sys.fs.fs_max=3191256" >> /etc/sysctl.conf
sysctl -p /etc/sysctl.conf

Ephemeral ports

Nginx used as a proxy creates temporary (ephemeral) ports for each upstream server. On busy proxy servers this will result in many connection in TIME_WAIT state.
Solution for that is to increase range of available ports by setting net.ipv4.ip_local_port_range. You may also benefit from lowering net.ipv4.tcp_fin_timeout setting (connection will be released faster, but be careful with that).

Use reverse-proxy

This with microcaching technic is worth separate article, I will add link here when it will be ready.

Source:
http://www.fromdual.com/huge-amount-of-time-wait-connections
https://www.nginx.com/blog/10-tips-for-10x-application-performance/
https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/
https://www.nginx.com/blog/thread-pools-boost-performance-9x/
https://tweaked.io/guide/kernel/
https://t37.net/nginx-optimization-understanding-sendfile-tcp_nodelay-and-tcp_nopush.html

fail2ban – block wp-login.php brute force attacks

Lately I had a lot of brute force attacks on my WordPress blog. I used basic auth to /wp-admin part in nginx configuration to block this and as a better solution I wan't to block source IPs at all on firewall.

To do this, place this filter code in /etc/fail2ban/filter.d/wp-login.conf:

# WordPress brute force wp-login.php filter:
#
# Block IPs trying to authenticate in WordPress blog
#
# Matches e.g.
# 178.218.54.109 - - [31/Dec/2015:10:39:34 +0100] "POST /wp-login.php HTTP/1.1" 401 188 "-" "Mozilla/5.0 (Windows NT 6.0; rv:34.0) Gecko/20100101 Firefox/34.0"
#
[Definition]
failregex = ^<HOST> .* "POST /wp-login.php
ignoreregex =

Then edit your /etc/fail2ban/jail.local and add:

[wp-login]
enabled   = true
port      = http,https
filter    = wp-login
logpath   = /var/log/nginx/access.log
maxretry  = 3

Now restart fail2ban:

service fail2ban restart

All done 🙂

Nginx – enabling SPDY with freeware certificate

I was thinking about allowing access to my website using SPDY protocol for better performance and security (and for fun of course 🙂 ). But SPDY have one disadvantage – you need SSL certificate signed by known authority that will verfiy in common browsers. So you can’t use self signed certificates because everyone will see a warning entering your site. Certs are quite expensive so I started searching for free one and to my surprise I found such!

I found these two sites where you can generate freeware certificates for your website:

I wouldn’t trust these certification authorities enough to use it for: access my mail or other private data. But I’m fine with using it for my public websites (like my blog) to gain speed from SPDY.

Configuring cert

Fetch the Root CA and Class 1 Intermediate Server CA certificates:

wget http://www.startssl.com/certs/ca.pem
wget http://www.startssl.com/certs/sub.class1.server.ca.pem

Create a unified certificate from your certificate and the CA certificates:

cat ssl.crt sub.class1.server.ca.pem ca.pem > /etc/nginx/conf/ssl-unified.crt

Enable SPDY

Configure your nginx server to use the new key and certificate (in the global settings or a server section):

ssl on;
ssl_certificate /etc/nginx/conf/ssl-unified.crt;
ssl_certificate_key /etc/nginx/conf/ssl.key;

Then enable SPDY like that:

server {
listen your_ip:80;
listen your_id:443 default_server ssl spdy;

# other stuff
}

Advertise SPDY protocol

Now advertise SPDY with Alternate-Protocol header – add this clause in main location:

add_header Alternate-Protocol "443:npn-spdy/2";

Have fun with SPDY on your site 🙂

Preparing video files for streaming on website in MP4 and WEBM format

Some time ago I prepared a PC that was responsible for batch encoding of movies to formats suitable for web players (such as. Video.js, JW Player, Flowplayer, etc.)

I used HandBrake for conversion to MP4 format (becase this soft was the fastest one) and ffmpeg (aka avconv in new version) for two pass encoding to WEBM.

Below are commands used by me for that conversion:

  • MP4
    HandBrakeCLI -e x264  -q 20.0 -a 1 -E faac -B 64 -6 mono -R 44.1 -D 0.0 -f mp4 --strict-anamorphic -m -x ref=1:weightp=1:subq=2:rc-lookahead=10:trellis=0:8x8dct=0 -O -i "input_file.avi" -o "output_file.mp4"
  • WEBM
    avconv -y -i "input_file.avi" -codec:v libvpx -b:v 600k -qmin 10 -qmax 42 -maxrate 500k -bufsize 1000k -threads 4 -an -pass 1 -f webm /dev/null
    avconv -y -i "input_file.avi" -codec:v libvpx -b:v 600k -qmin 10 -qmax 42 -maxrate 500k -bufsize 1000k -threads 4 -codec:a libvorbis -b:a 96k -pass 2 -f webm "output_file.webm"

Nginx configuration for MP4

I used configuration similar to that below for MP4 pseudostreaming and to protect direct urls to videos from linking on other sites (links will expire after sometime). There is also example usage of limit_rate clause that will slow down downloading of a file (it’s still two times bigger than video streaming speed so should be enough).

location ~ \.m(p4|4v)$ {
  ## This must match the URI part related to the MD5 hash and expiration time.
  secure_link $arg_ticket,$arg_e;

  ## The MD5 hash is built from our secret token, the URI($path in PHP) and our expiration time.
  secure_link_md5 somerandomtext$uri$arg_e;

  ## If the hash is incorrect then $secure_link is a null string.
  if ($secure_link = "") {
    return 403;
  }

  ## The current local time is greater than the specified expiration time.
  if ($secure_link = "0") {
    return 403;
  }

  ## If everything is ok $secure_link is 1.
  mp4;
  mp4_buffer_size     10m;
  mp4_max_buffer_size 1024m;

  limit_rate          1024k;
  limit_rate_after    5m;
}

Source:
http://nginx.org/en/docs/http/ngx_http_mp4_module.html
http://wiki.nginx.org/HttpSecureLinkModule

Nginx – przydatne rewrite’y i różne sztuczki

Polubiłem Nginx’a i wykorzystuję go na coraz więcej sposobów. Kilka rzeczy udało mi się całkiem fajnie w nim skonfigurować i postanowiłem zebrać te przykłady by następnym razem gdy postanowię do nich sięgnąć nie musieć wertować konfigów po serwerach 🙂

Słowo wstępu

Niektóre rewrite’y kończą się znakiem ? – czemu?
Otóż Nginx próbuje automatycznie dodawać parametry na końcu przepisanego adresu. Jeśli jednak wykorzystamy zmienną $request_uri to ona sama w sobie zawiera już parametry zapytania (czyli to co w URI znajduje się po znaku ?) i właśnie dodanie pytajnika tuż za tą zmienną powoduje że argumenty nie są dublowane.
Ma to też zastosowanie gdy chcemy by rewrite kierował np. na główną stronę beż żadnych dodatkowych argumentów (zostaną one obcięte).
Więcej na ten temat można znaleźć w dokumentacji Nginx.

Inna warta wspomnienia uwaga dotyczy drobnej optymalizacji, o której warto pamiętać na etapie tworzenia rewrite’ów (można znaleźć masę kiepskich przykładów w sieci): na początku najlepiej jest stworzyć coś co działa (i przy małym ruchu może to być wystarczające) a później optymalizować – moje przykłady starałem się zoptymalizować według zalecanych praktyk.
Dlatego zamiast pisać:

rewrite ^(.*)$ $scheme://www.domain.com$1 permanent;

lepiej napisać:

rewrite ^ $scheme://www.domain.com$request_uri? permanent;

(nie wykorzystujemy przechowywania wartości dopasowania – mniejsze zużycie pamięci i lżejsza interpretacja REGEXP’a).
A jeszcze lepiej napisać:

return 301 $scheme://www.domain.com$request_uri;

(w ogóle nie wykorzystujemy REGEXP’ów praktycznie zerowy narzut na przetwarzanie) – dzięki za uwagę: lukasamd.

Przekierowanie starej domeny na nową

server {
    listen 80;
    server_name old-domain.com www.old-domain.com;
    return 301 $scheme://www.new-domain.com$request_uri;
    # rewrite ^ $scheme://www.new-domain.com$request_uri? permanent;
    # or
    # rewrite ^ $scheme://www.new-domain.com? permanent;
}

Wykorzystanie return w tej sytuacji jest nieco bardziej optymalne gdyż nie angażuje w ogóle silnika REGEXP a w tej sytuacji jest wystarczające.
Pierwsza linia z rewrite i $request_uri spowoduje przepisywanie też parametrów wywołań do nowej lokalizacji co jest jak najbardziej sensowne gdy pomimo domeny nie zmieniła się zbytnio struktura strony.
Jeśli strona jednak się zmieniła to możemy zdecydować o przekierowaniu bez parametrów – po prostu na główną stronę – i to robi druga linia.
W obu przypadkach parametr permanent nakazuje użycie kodu przekierowania HTTP 301 (Moved Permanently), co ułatwi zorientowanie się crawlerom że ta zmiana jest już na stałe.

Dodanie WWW na początku domeny

server {
    listen 80;
    server_name domaim.com;
    return 301 $scheme://www.domain.com$request_uri;
    #rewrite ^ $scheme://www.domain.com$request_uri? permanent;
    # or
    #rewrite ^(.*)$ $scheme://www.domain.com$1 permanent;
}

Przykład zakomentowany jest według dokumentacji mniej optymalny ale również zadziała. Reszta jest prosta i samoopisująca się 🙂
A to jeszcze bardziej ogólna wersja dla wielu domen:

server {
    listen 195.117.254.80:80;
    server_name domain.pl domain.eu domain.com;

    return 301 $scheme://www.$http_host$request_uri;
    #rewrite ^ $scheme://www.$http_host$request_uri? permanent;
}

Ta wersja wykorzystuje zmienną $http_host do przekierowania na domenę z zapytania (zmienna ta zawiera też numer portu jeśli jest niestandardowy np. 8080, w przeciwieństwie do zmiennej $host, która zawiera tylko domenę).

Usunięcie WWW z początku domeny

server {
    listen 80;
    server_name www.domain.com;
    return 301 $scheme://domain.com$request_uri;
    #rewrite ^ $scheme://domain.com$request_uri? permanent;
}

Czasami może się przydać jeszcze inny kawałek, gdy strona działa na wielu domenach i chcemy przekierować wszystkie:

server {
    server_name www.domain.com _ ;
    # server_name www.domain1.com www.domain2.com www.domain3.eu www.domain.etc.com;

    if ($host ~* www\.(.*)) {
        set $pure_host $1;
        return 301 $scheme://$pure_host$request_uri;
        #rewrite ^ $scheme://$pure_host$request_uri? permanent;
        #rewrite ^(.*)$ $scheme://$pure_host$1 permanent;
    }
}

Choć to podejście nie jest zalecane (pomimo zwięzłości). Lepiej zdefiniować dwa bloki server z domenami www.* i domenami bez www na początku. Ale z drugiej strony to cholernie wygodne… 😉

Przekierowanie “pozostałych” zapytań na domyślną domenę

server {
    listen 80 default;
    server_name _;
    rewrite ^ $scheme://www.domena.com;
    #rewrite ^ $scheme://www.domena.com/search/$host;
}

To bardzo przydatny przykład – czyli domyślny vhost, który “przyjmie” wszystkie zapytania do domen nie zdefiniowanych w konfiguracji i przekieruje na naszą “główną stronę”.
Zakomentowany przykład jest nieco bardziej przekombinowany bo próbuje wykorzystać wyszukiwarkę na naszej stronie do wyszukania “czegoś” pomocnego – z tym przykładem należy uważać bo jeśli do serwera trafi dużo błędnych zapytań to może zostać przeciążony “bzdetnymi” wyszukiwaniami.

Przekierowanie pewnych podstron po zmianie struktury strony

server {
    listen 80;
    server_name www.domain.com;

    location / {
        try_files $uri $uri/ @rewrites;
    }

    location @rewrites {
        rewrite /tag/something  $scheme://new.domain.com permanent;
        rewrite /category/hobby /category/painting permanent;
        # etc ...

        rewrite ^ /index.php last;
    }
}

Im starsza strona tym więcej zbiera się linków, których po prostu nie można usunąć, a które z racji wprowadzonych zmian nie mają prawa bytu w nowym układzie. Warto je przekierować w nowe miejsca, lub najbardziej odpowiadające/bliskie tym starym. Problemem może się wkrótce stać duża lista przekierowań, która zaciemni konfigurację.
Powyższy sposób w dość optymalny sposób porządkuje takie przekierowania – najpierw sprawdza czy przypadkiem nie próbujemy pobrać istniejących plików, jeśli nie to wrzuca nas nas na listę przekierowań, a jeśli i tu nic nie znajdzie to zapytanie przekazywane jest do głównego skryptu strony.

Przekierowanie w zależności od wartości parametru w URI

if ($args ~ producent=toyota){
    rewrite ^ $scheme://toyota.domena.com$request_uri? permanent;
}

To rzadko stosowane przekierowanie a w dodatku mało czytelnie i ponoć mało wydajne… Ale potrafi być bardzo przydatne gdy chcemy przepisać adres w zależności od wartości parametru np. gdy pewna podstrona doczeka się rozbudowy w zupełnie nowym serwisie lub gdy chcemy ładnie przekierować adresy ze starej strony na nową.

Blokowanie dostępu do ukrytych plików

location ~ /\. { access_log off; log_not_found off; deny all; }

Przyznaję – to nie rewrite… Ale ta linijka jest równie przydatna – pozwala zablokować możliwość pobierania ukrytych plików (np. .htaccess’ów po konfiguracji z Apachego).

Wyłączenie logowania dla robots.txt i favicon.ico

location = /favicon.ico { try_files /favicon.ico =204; access_log off; log_not_found off; }
location = /robots.txt  { try_files /robots.txt =204; access_log off; log_not_found off; }

To też nie rewrite – ale bardzo fajnie obsługuje sytuację gdy mamy i gdy nie mamy powyższych dwóch pliczków. Po pierwsze wyłącza logowanie i serwuje je gdy są dostępne. Gdy nie istnieją to serwuje puste pliki (kod 204) dzięki czemu nie przeszkadzają nam 404-ki 🙂

Blokowanie dostępu do obrazków dla nieznanych referererów

location ~* ^.+\.(?:jpg|png|css|gif|jpeg|js|swf)$ {
    # definiujemy poprawnych refererow
    valid_referers none blocked *.domain.com domain.com;
    if ($invalid_referer)  {
        return 444;
    }
    expires max;
    break;
}

Zabezpieczenie warte tyle co nic bo banalne do ominięcia – ale jeśli zdarzy się że ktoś postanowi wykorzystać grafikę z naszej stronki np. w aukcji na allegro czy własnym sklepie to tą prostą sztuczką możemy go przyciąć i przeważnie jest to wystarczające.
Muszę też zaznaczyć szczególne znaczenie wartości kodu błędu 444 w Nginx’ie – powoduje on zerwanie połączenia bez wysyłania jakiejkolwiek odpowiedzi. Jeśli nie chcemy być tak okrutni to możemy użyć innego kodu, np.: 403 albo 402 🙂

Przekierowanie ciekawskich w “ciemną dupę”

location ~* ^/(wp-)?admin(istrator)?/?  {
    rewrite ^ http://whatismyipaddress.com/ip/$remote_addr redirect;
}

Ten prosty redirect odwodzi wielu amatorów zbyt głębokiego penetrowania naszej strony… A pozostałych na pewno rozbawi 🙂

Inne przykłady konfiguracji na mojej stronie:
Nginx – hide server version and name in Server header and error pages
Nginx – kompresowanie plików dla gzip_static
Nginx – konfiguracja pod WordPress’a
Nginx – ustawienie domyślnego vhosta
Nginx – mój domyślny config

Źródła:
http://www.engineyard.com/blog/2011/useful-rewrites-for-nginx/
http://wiki.nginx.org/HttpRewriteModule