W dzisiejszym artykule przedstawię na przykładzie, jak stosunkowo niewielkim kosztem można zwiększyć moc infrastruktury i jej przepustowość. Okresy przedświąteczne, ale także akcje takie jak blackfriday, cybermonday czy inne tematyczne promocje stawiają administratorom nie lada wyzwanie. Jeżeli mamy odpowiednio duży budżet, możemy przenieść się do chmury, skorzystać z konteneryzacji czy klastrowania. Jeśli jednak nie mamy doświadczenia z orkiestratorami lub nie mamy w firmie budżetu na takie rzeczy możemy stosunkowo niewielkim kosztem dołożyć nieco mocy i sprawić, by nasza infrastruktura obsłużyła więcej ruchu.

Nasze testowe środowisko oprzemy o standardową i najczęściej spotykaną konfigurację, tj. środowisko LAMP pod spodem jako, że spora część developerów chce mieć możliwość korzystania z .htaccess’a oraz Nginx na froncie jako Load Balancer. Do tego PHP oraz silnik bazy danych MariaDB. Testy wykonamy przy pomocy narzędzia do mierzenia wydajności Locust.

Na potrzeby niniejszego artykułu przygotowałem środowisko złożone z kilku niewielkich serwerów VPS, ale można wykorzystać zawarte tu porady zarówno w odniesieniu do mocniejszych instancji (także chmurowych jak EC2), jak i do serwerów dedykowanych. Wszystko pracuje pod kontrolą systemu Debian 10 Buster.

Wersje pakietów:

  1. Debian 10.6
  2. Apache/2.4.38
  3. PHP/7.3.19-1
  4. MariaDB/10.3.25
  5. Nginx/1.18.0

Sieć lokalna: 10.0.0.0/24

Serwer WWW aplikacji

vhost Apache:

<VirtualHost 10.0.0.2:80>

            ServerName wladcy.hosterion.pl

            ServerAdmin a.zug@hosterion.pl

            DocumentRoot /var/www/html

            DirectoryIndex index.html index.php

            <Directory „/var/www/html/”>

            allow from all

            Options -Indexes

            AllowOverride All

            </Directory>

            ErrorLog ${APACHE_LOG_DIR}/error.log

            CustomLog ${APACHE_LOG_DIR}/access.log combined

</VirtualHost>

<IfModule mod_ssl.c>

            <VirtualHost _default_:443>

            ServerName wladcy.hosterion.pl

                       ServerAdmin a.zug@hosterion.pl

                       DocumentRoot /var/www/html

            DirectoryIndex index.html index.php

                       ErrorLog ${APACHE_LOG_DIR}/error.log

                       CustomLog ${APACHE_LOG_DIR}/access.log combined

                       SSLEngine on

                       SSLCertificateFile /etc/ssl/certs/apache-selfsigned.crt

                       SSLCertificateKeyFile /etc/ssl/private/apache-selfsigned.key

                       <FilesMatch „\.(cgi|shtml|phtml|php)$”>

                                   SSLOptions +StdEnvVars

                       </FilesMatch>

                       <Directory /usr/lib/cgi-bin>

                                   SSLOptions +StdEnvVars

                       </Directory>

            </VirtualHost>

</IfModule>

Na potrzeby artykułu przygotowałem prosty vhost, ale nic nie stoi na przeszkodzie, żeby był to bardziej rozbudowany, posiadający własne reguły. Nie ma to tutaj znaczenia, o ile na drugim i kolejnych node’ach będzie to dokładnie taka sama konfiguracja. Co do zasady każdy z tych node’ów powinien mieć możliwość działania niezależnie (serwowanie aplikacji). Baza danych może znajdować się na każdym z node’ów, ale może znajdować się jeszcze gdzie indziej. Często spotykaną konfiguracją jest typowy LAMP i taki też przyjęliśmy do tego artykułu. Baza danych master będzie na node1, a dwie replikacje będą na node2 i node3. Jeśli aplikacja jest dobrze napisana i ma możliwość rozdzielenia ruchu, na przykład na odczyty i zapisy, to podniesiemy znacznie wydajność przełączając na poszczególne node’y. Przykładowo zapisuj na node1, a odczyty na node2/node3. Jeszcze większą wydajność uzyskamy rozdzielając usługi na inne serwery (na przykład bazy danych).

W naszym środowisku serwerem WWW odpowiedzialnym za działanie aplikacji będzie Apache, natomiast rozdzieleniem ruchu zajmie się Nginx.

Apache najlepiej w tej konfiguracji uruchomić na interfejsie lokalnym. Nie ma potrzeby wystawiania ich na świat chyba, że jest to uzasadnione. Dobrze wtedy ze świata ten ruch blokować i pozostawić otwarty tylko na konkretne IP.

Nasze pozostałe nnode’y wyglądają tak samo i mają identyczną konfigurację usług. Zarówno serwer WWW Apache jak i baza danych jest uruchomiona tylko na lokalnym adresie wewnętrznym. Serwery komunikują się po sieci wewnętrznej, a load balancer na froncie dopiero wystawia aplikację na świat. Mając dostęp po sieci lokalnej do serwerów (np. mając serwery dedykowane w sieci lokalnej lub w DC) możemy całkowicie odłączyć interfejs zewnętrzny, co dodatkowo podniesie poziom bezpieczeństwa.

Mamy już zainstalowaną aplikację na serwerach (na wszystkich node’ach jednakową).

Serwer Proxy/Load Balancer

Na froncie użyliśmy Nginxa, który posłuży nam jako Load Balancer i będzie rozdzielał ruch między poszczególne node’y pod spodem po sieci lokalnej.

Na ten adres IP skierujemy naszą domenę. Po wpisaniu w pasku przeglądarki adresu naszej strony zostanie ona skierowana na Proxy.

Konfiguracja Nginxa:

server {

            listen 80;

            server_name wladcy.hosterion.pl;

            error_log /var/log/nginx/wladcy.hosterion.pl.error.log;

            access_log /var/log/nginx/wladcy.hosterion.pl.access.log combined;

            root /var/www/html;

            location / {

            proxy_pass http://wladcy;

            proxy_set_header Host $host;

            proxy_set_header X-Real-IP $remote_addr;

            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            proxy_set_header X-NginX-Proxy true;

            proxy_set_header Upgrade $http_upgrade;

            proxy_set_header Connection „upgrade”;

            proxy_cache_bypass $http_upgrade;

            proxy_http_version 1.1;

            proxy_redirect off;

            proxy_next_upstream error timeout http_500 http_502 http_503 http_504;

            proxy_connect_timeout 999s;

            proxy_read_timeout 999s;

            proxy_send_timeout 999s;

            proxy_buffers 16 64k;

            proxy_buffer_size 128k;

            }

}

server {

            listen 443 ssl;

            server_name wladcy.hosterion.pl;

            ssl_protocols TLSv1.2 TLSv1.3;

            ssl_certificate /etc/letsencrypt/live/wladcy.hosterion.pl/fullchain.pem;

            ssl_certificate_key /etc/letsencrypt/live/wladcy.hosterion.pl/privkey.pem;

            ssl_dhparam /etc/nginx/ssl/dhparam.pem;

            ssl_session_timeout  5m;

            ssl_ciphers „EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4”;

            ssl_prefer_server_ciphers   on;

            ssl_session_cache shared:SSL:10m;

            error_log /var/log/nginx/wladcy.hosterion.pl.error.log;

            access_log /var/log/nginx/wladcy.hosterion.pl.access.log combined;

            root /var/www/html;

            location / {

            proxy_pass https://wladcy_ssl;

            proxy_set_header Host $host;

            proxy_set_header X-Real-IP $remote_addr;

            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            proxy_set_header X-NginX-Proxy true;

            proxy_set_header Upgrade $http_upgrade;

            proxy_set_header Connection „upgrade”;

            proxy_cache_bypass $http_upgrade;

            proxy_http_version 1.1;

            proxy_redirect off;

            proxy_next_upstream error timeout http_500 http_502 http_503 http_504;

            proxy_connect_timeout 999s;

            proxy_read_timeout 999s;

            proxy_send_timeout 999s;

            proxy_buffers 16 64k;

            proxy_buffer_size 128k;

            }

}

upstream wladcy {

   ip_hash;

  server 10.0.0.2 max_fails=0 fail_timeout=10s weight=1;

  server 10.0.0.3 max_fails=0 fail_timeout=10s weight=1;

  server 10.0.0.4 max_fails=0 fail_timeout=10s weight=1;

}

upstream wladcy_ssl {

   ip_hash;

  server 10.0.0.2:443 max_fails=0 fail_timeout=10s weight=1;

  server 10.0.0.3:443 max_fails=0 fail_timeout=10s weight=1;

  server 10.0.0.4:443 max_fails=0 fail_timeout=10s weight=1;

}

vhost dla statystyk nginxa > stub_status.conf

server {

            listen 127.0.0.1:80;

            server_name 127.0.0.1;

            location /nginx_status {

                       stub_status on;

                       allow 127.0.0.1;

                       deny all;

            }

}

Na potrzeby artykułu przygotowałem prosty konfig dla Nginx’a, dzięki któremu mamy zarówno komunikację po SSL jak i bez, mamy przekazywanie nagłówków, podniesione wartości timeout oraz buforów.

Aby jeszcze dokładniej poznać specyfikę zachowania się środowiska, wykorzystamy darmowy Amplify od Nginx’a.

Test

Włączamy podgląd na wszystkie 3 node’y. Na razie nic się nie dzieje.

Sprawdzamy amplify:

Ustawiamy początkową wartość 200 użytkowników na test obciążenia.

Na pierwszym node z serwerem Apache pojawia się ruch:

Pozostałe serwery odpoczywają. Będziemy sukcesywnie podnosić ilość jednoczesnych użytkowników, aby sprawdzić jak będzie zachowywało się środowisko i kiedy osiągniemy limit i dostawimy kolejnego node’a.

Pierwsze błędy środowisko generuje nam przy około 1000 użytkownikach na stronie.

Dokładamy drugiego node’a.

W tej chwili ruch rozkłada się pomiędzy dwoma serwerami:

Po dodaniu kolejnego serwera i zresetowaniu statystyk nie tylko podwoiliśmy moc naszego środowiska i jesteśmy w stanie obsłużyć sporo większy ruch, ale także spadł nam response time (czas odpowiedzi), więc użytkownicy przy tak dużym ruchu otrzymują wyniki bez opóźnień. Przy 2000 użytkowników na stronie środowisko nie pokazuje błędów.

Po dodaniu 3000 użytkowników środowisko z dwoma serwerami node zaczęło się blokować, dlatego dołożyłem kolejny serwerek > node3.

Po zresetowaniu statystyk wygląda na to, że obsługujemy ten ruch bez problemu.

Tak można skalować w nieskończoność, dokładając kolejne node’y. Idea i zasada jest dokładnie taka sama. Dostawienie kolejnego serwera z aplikacją, bazą danych itd.

To oczywiście nie jest w pełni automatyczne skalowanie, nie jest to środowisko orkiestrowane czy konteneryzowane, ale w sytuacji, kiedy chcemy stosunkowo niskim kosztem dołożyć trochę mocy możemy wykorzystać powyższy mechanizm.

Sprawdźmy jeszcze co pokazuje Amplify:

Zewnętrzny monitoring Amplify pokazuje, mniej więcej zgodne wartości z tymi, które ustawiliśmy w aplikacji do testowania Locust.

To oczywiście nie wszystko co można zrobić. Nie zawsze dostawienie node’a pomoże. Istnieje jeszcze szereg czynników wpływających na to, czy nasza aplikacja będzie potrafiła w ten sposób pracować. Jeżeli aplikacja potrafi rozdzielić zapisy i odczyty do bazy danych to dodatkowo można zwiększyć przepustowość środowiska rozdzielając ruch na bazy danych.

W kolejnych artykułach pokażę jeszcze inne możliwości optymalizacji zarówno PHP, jak i bazy danych dzięki, czemu można jeszcze znacząco podnieść wydajność środowiska.