Wyobraźmy sobie, że mamy już zbudowaną własną aplikację, stronę internetową, która staje się coraz bardziej popularna. Zdobywamy nowych użytkowników, którzy łączą się do naszego serwera. Serwer staje się coraz bardziej obciążony i staje się przez to wolniejszy. Wolniej odpowiada, strona zaczyna wczytywać się coraz wolniej, aż w końcu ładuje się na tyle wolno, że piszą do nas wkurzeni klienci … i tak dalej.


Wiele firm mierzy się właśnie z takim problemem, nie tylko dlatego, że sieć Internet, zakupy, blogi i inne usługi online stają się coraz bardziej popularne, ale ostatnie wydarzenia epidemiczne w Polsce i na świecie niejako wymusiły przerzucenie większej niż dotychczas ilości usług do świata cyfrowego. Ludzie zaczęli robić więcej zakupów przez Internet, a mając więcej czasu podczas lockdown’u generowali coraz więcej ruchu na stronach.


Istnieje przynajmniej kilka rozwiązań, aby poprawić osiągi naszej strony i tym samym sprawić, że obsłużymy jeszcze więcej ruchu bez problemów. Możemy tu wyliczyć między innymi:

  1. poprawienie i optymalizacja kodu aplikacji pod kątem zbyt długich zapytań do bazy lub przejścia na nowsze wersje bibliotek itp.
  2. migracja do chmury i skalowanie środowiska, ale wiadomo, że wiąże się to z dużymi kosztami. Dodatkowo dobrzy administratorzy od chmury są po prostu drodzy, więc nie każdy może sobie na to pozwolić.
  3. migracja na większy serwer – ale przecież pojedyncze nody mają swoje ograniczenia więc problem na pewno powróci prędzej czy później wraz z kolejnym wzrostem połączeń i klientów
  4. load balansing – czyli dokładanie nodów w razie potrzeby i rozkładanie ruchu między nimi i to właśnie o load balansingu będzie dzisiejszy artykuł.


Nginx to nie tylko doskonały i zoptymalizowany pod kątem wydajności serwer www to także inne funkcje, takie jak na przykład balansowanie ruchu pomiędzy nodami i rozdzielanie go.

Node 1, Node 2, Proxy


Mamy naszą aplikację na jakimś serwerze, na którym może być serwer www, baza danych itp. – dokładnie tak, jak w tradycyjnej konfiguracji w większości przypadków. Serwer ten z powodu obciążenia, nie daje już sobie rady, stawiamy więc replikację aplikacji (kopię) i chcemy, aby od tej pory ruch był rozkładany pomiędzy serwery Node1 i Node2. Nasza aplikacja to typowa napisana w PHP i serwowana przy pomocy serwera www Apache2.


Node 1


Instalujemy Apache2 i php7.3:

apt-get update

apt-get install php7.3

apt-get install php-amqp php-amqplib php-apcu php-gettext php-imagick php-memcache php-pear php-redis php-tideways php-xdebug php-xml php7.3 php7.3-bcmath php7.3-cgi php7.3-cli php7.3-common php7.3-curl php7.3-dev php7.3-gd php7.3-imap php7.3-intl php7.3-json php7.3-mbstring php7.3-mongodb php7.3-mysql php7.3-odbc php7.3-opcache php7.3-pgsql php7.3-pspell php7.3-readline php7.3-soap           php7.3-sqlite3 php7.3-tidy php7.3-xmlrpc php7.3-xsl php7.3-zip

apt-get install apache2


Stack:

  • aplikacja napisana w PHP
  • serwer www Apache2
  • baza danych
  • interfejs sieci zewnętrznej – 95.217.133.74
  • interfejs sieci wewnętrznej – 192.168.0.2




Konfiguracja Apache2:


Apache2 konfigurujemy w /etc/apache2/ports.conf do działania tak, aby nasłuchiwał wyłącznie na interfejsie i adresie IP lokalnej karty sieciowej. Dzięki temu Apache2 nie jest dostępny na nodzie z zewnątrz.






Jak widać na powyższym przykładzie bindujemy serwer www Apache2 na adres IP wewnętrznej sieci, a nie zewnętrznej lub 0.0.0.0 (wszystkie).


Node 2


Instalujemy Apache2 i php7.3:

apt-get update

apt-get install php7.3

apt-get install php-amqp php-amqplib php-apcu php-gettext php-imagick php-memcache php-pear php-redis php-tideways php-xdebug php-xml php7.3 php7.3-bcmath php7.3-cgi php7.3-cli php7.3-common php7.3-curl php7.3-dev php7.3-gd php7.3-imap php7.3-intl php7.3-json php7.3-mbstring php7.3-mongodb php7.3-mysql php7.3-odbc php7.3-opcache php7.3-pgsql php7.3-pspell php7.3-readline php7.3-soap           php7.3-sqlite3 php7.3-tidy php7.3-xmlrpc php7.3-xsl php7.3-zip

apt-get install apache2


Stack:

  • kopia aplikacji napisanej w PHP
  • serwer www Apache2
  • baza danych (np replikacja w konfiguracji master-slave lub multimaster)
  • interfejs sieci zewnętrznej – 95.217.208.76
  • interfejs sieci wewnętrznej – 192.168.0.3




Konfiguracja Apache2:


Apache2 konfigurujemy w /etc/apache2/ports.conf do działania tak, aby nasłuchiwał wyłącznie na interfejsie i adresie IP lokalnej karty sieciowej. Dzięki temu Apache2 nie jest dostępny na nodzie z zewnątrz.






Proxy:


Instalujemy proxy Nginx:

apt-get install nginx-full


Stack:

  • serwer proxy Nginx
  • interfejs sieci zewnętrznej – 95.217.208.84
  • interfejs sieci wewnętrznej – 192.168.0.4




Konfiguracja Nginx:



nano /etc/nginx/sites-enabled/default

server {

               listen 80 default_server;

               root /var/www/html;

               index index.php index.html index.htm index.nginx-debian.html;

               server_name _;

               error_log /var/log/nginx/mojastrona.pl.error.log;

               access_log /var/log/nginx/mojastrona.pl.access.log combined;

               location / {

               proxy_pass http://strona;

               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 90s;

               proxy_read_timeout 120s;

               proxy_send_timeout 90s;

               proxy_buffers 16 64k;

               proxy_buffer_size 128k;

               }

}

upstream strona {

                ip_hash;

                server 192.168.0.2:80 max_fails=0 fail_timeout=10s weight=1;

                server 192.168.0.3:80 max_fails=0 fail_timeout=10s weight=1;

}

Testujemy konfigurację load balansera


Po odpowiednim skonfigurowaniu serwerów wchodzimy na stronę WWW – wchodzimy na adres IP, domenę serwera proxy. W naszym przypadku dla niniejszego poradnika pokażę ideę i zasadę działania. Niniejsza konfiguracja jest zupełnie podstawowa bez certyfikatu SSL i domeny, ale dodanie SSL’a nie zmienia idei i zasady działania, należy dołożyć wyłącznie blok SSL’a i sposób działania się nie zmienia.


Wchodzimy na stronę:




Wyświetla nam się nasza aplikacja (w tym przypadku podstawowe php.info, ale nie ma znaczenia tak na prawdę co jest w node’ach. Idea jest dokładnie taka sama).


Sprawdzamy logi:


Proxy nginx:


ADRES_IP_KLIENTA – – [21/Jun/2020:19:05:23 +0200] „GET / HTTP/1.1” 200 29329 „-” „Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36”


Node1:


192.168.0.4 – – [21/Jun/2020:19:05:23 +0200] „GET / HTTP/1.1” 200 29527 „-” „Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36”


Node2:


Nie ma nic, bo połączenie trafiło do Node1. Inny klient, trafi na Node2, a nie np. na Node1 i wtedy w logach Node2 pojawi się wpis.

Podsumowanie


W powyższej konfiguracji mamy podstawowy load balanser, który rutuje nam ruch w zależności od obciążenia hostów losowo. Klient, który odwiedził naszą stronę i trafił na konkretnego Node’a na niego będzie skierowany przez cały czas podczas tej sesji.


To oczywiście tylko zarys, a nie wyczerpanie tematu w całości. Istnieje jeszcze wiele opcji konfiguracyjnych jak SSL, jak sesje, timeouty, zabezpieczenia itd, ale co do zasady idea się nie zmienia. Jest to najprostrzy sposób na to, aby stosunkowo niewielkim kosztem utworzyć sobie klaster dla naszej aplikacji lub środowiska i balansować ruch w zależności od obciążenia na konkretne Node’y.


Istnieje jeszcze wiele protipów, które możemy zastosować w tej konfiguracji, aby na przykład Node’y pokazywały w swoich logach adres IP klienta, a nie adres IP naszego serwera proxy i tym podobne.