Running with Ruby

Tag: cluster

Multiple UNIX sockets bindings for Puma cluster

If you want to bind Puma to a unix socket, you can do this either by providing a -b options:

$ puma -b unix:///var/run/puma.sock

or using a puma.rb config file and setting the bind options.

# puma.rb config file
# ...config...
bind 'unix://var/run/puma.sock'
# ...config

Unfortunately if you try to bind multiple Pumas to one socket, you might end up with issue similar to this one:

2015/02/16 12:12:22 [error] 2476#0:
 *16083152 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 64.251.13.85, server: app,
  request: "POST /notifier_api/v2/notices HTTP/1.1", upstream: "http://unix:///var/run/puma.sock:/notifier_api/v2/notices", host: "app"
2015/02/16 12:12:23 [error] 2476#0:
 *16083176 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 64.251.10.203, server: app,
  request: "POST /notifier_api/v2/notices HTTP/1.1", upstream: "http://unix:///var/run/puma.sock:/notifier_api/v2/notices", host: "app"
2015/02/16 12:12:23 [error] 2474#0:
 *16083180 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 64.251.13.85, server: app,
  request: "POST /notifier_api/v2/notices HTTP/1.1", upstream: "http://unix:///var/run/puma.sock:/notifier_api/v2/notices", host: "app"
2015/02/16 12:12:43 [error] 2476#0:
 *16083282 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 64.251.13.85, server: app,
  request: "POST /notifier_api/v2/notices HTTP/1.1", upstream: "http://unix:///var/run/puma.sock:/notifier_api/v2/notices", host: "app"
2015/02/16 12:12:43 [error] 2476#0:
 *16083292 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 64.251.13.85, server: app,
  request: "POST /notifier_api/v2/notices HTTP/1.1", upstream: "http://unix:///var/run/puma.sock:/notifier_api/v2/notices", host: "app"
2015/02/16 12:12:43 [error] 2476#0:
 *16083301 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 64.251.13.85, server: app,
  request: "POST /notifier_api/v2/notices HTTP/1.1", upstream: "http://unix:///var/run/puma.sock:/notifier_api/v2/notices", host: "app"
2015/02/16 12:13:33 [error] 2474#0:
 *16083390 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 64.251.13.85, server: app,
  request: "POST /notifier_api/v2/notices HTTP/1.1", upstream: "http://unix:///var/run/puma.sock:/notifier_api/v2/notices", host: "app"
2015/02/16 12:13:33 [error] 2474#0:
 *16083392 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 64.251.13.85, server: app,
  request: "POST /notifier_api/v2/notices HTTP/1.1", upstream: "http://unix:///var/run/puma.sock:/notifier_api/v2/notices", host: "app"
2015/02/16 12:13:33 [error] 2474#0:
 *16083397 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 64.251.13.85, server: app,
  request: "POST /notifier_api/v2/notices HTTP/1.1", upstream: "http://unix:///var/run/puma.sock:/notifier_api/v2/notices", host: "app"
2015/02/16 12:13:33 [error] 2474#0:
 *16083398 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 64.251.13.85, server: app,
  request: "POST /notifier_api/v2/notices HTTP/1.1", upstream: "http://unix:///var/run/puma.sock:/notifier_api/v2/notices", host: "app"
2015/02/16 12:13:43 [error] 2477#0:
 *16083428 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 64.251.13.85, server: app,
  request: "POST /notifier_api/v2/notices HTTP/1.1", upstream: "http://unix:///var/run/puma.sock:/notifier_api/v2/notices", host: "app"

So, what is the reason? Your one and only socket might just not be enough. You can call bind multiple times, to create multiple sockets that your clustered Puma will use:

# ...config...
bind 'unix://var/run/puma1.sock'
bind 'unix://var/run/puma2.sock'
bind 'unix://var/run/puma3.sock'
bind 'unix://var/run/puma4.sock'
bind 'unix://var/run/puma5.sock'
# ...config...

Btw great docs for Puma – it’s nowhere mentioned and the easiest place to understand how it works, is in the source code, that looks like that:

 # Bind the server to +url+. tcp:// and unix:// are the only accepted
# protocols.
#
def bind(url)
@options[:binds] << url
end

Podstawy loadbalancingu – nginx + thin (backend, frontend)

Wstęp

Ostatnimi czasy przyszło mi pracować nad dość zasobożerną aplikacją. Nie wdając się w szczegóły, podzielona jest ona na frontend i backend. Frontend (jak sama nazwa wskazuje) odpowiada za generowanie tego co widzi użytkownik. Backend służy zaś do komunikacji poszczególnych fragmentów systemu oraz za przetwarzanie i gromadzenie danych. Aktualnie w czasie pracy – średnio obciążony system generuje około 10-40 żądań na sekundę na backendzie. Przód zasadniczo nie jest aż tak obciążony. Warto jednak mieć na uwadze to, że aplikacja ta jest w fazie beta i ilość użytkowników jest mocno ograniczona. W związku z tym skalowalność jak najbardziej mile widziana :)

Load balancing

Aby utrzymać wysoką wydajność i responsywność tejże aplikacji, zdecydowałem się na wykorzystanie load balancingu. Czym jest load balancing? Za wikipedią:

Równoważenie obciążenia (ang. load balancing) – technika rozpraszania obciążenia pomiędzy wiele procesorów, komputerów, dysków, połączeń sieciowych lub innych zasobów.

Przykładowy system load balancing mógłby zapewniać bezawaryjną i optymalną pracę na N-aplikacjach znajdujących się na bliźniaczych M-serwerach (zawierających równoważne aplikacje). Podczas gdy wielu użytkowników wysyła wiele żądań, trafiają one najpierw do SLB, który analizuje obciążenie na poszczególnych M-serwerach. Następnie dokonuje optymalnego wyboru – odsyłając użytkownika do jednego z serwerów do konkretnej aplikacji – według żądania.

W moim wypadku zasobem jest instancja Thina. Rozkład obciążeń ma u mnie “zrównoleglić” przetrwarzanie tak, aby dane napływające przez API nie musiały “czekać” aż konkretna instancja Thina skończy przetwarzać poprzednie informacje. Dodatkowo chciałem odseparować jak najbardziej wpływ backendu na frontend. Wszelkie “zasobożerne” obliczenia zamykają się w mocniejszej z maszyn, zaś na świat wystawiona jest ta “lżejsza”. Dodatkowo cała komunikacja między-serwerowa leci po vlanie.

nginx

Jako load balancer wybrałem nginx’a ponieważ jest lekki, szybki i wykrywa jak któryś z thinów jest niedostępny (np. padnie). Jego konfiguracja aby działał jako proces do równoważenia obciążeń nie stanowi większego problemu. Całość konfiguracji wykonujemy w przestrzeni http:


http{
# Konfiguracja
}

Zacznijmy od zdefiniowania sobie tego gdzie znajdują się nasze instancje thinów. Load balancer stoi na maszynie pierwszej, tam gdzie frontend. Dlatego też odwołujemy się do serwerów leżących na nim poprzez localhost:

    upstream frontends {
        server localhost:3001;
        server localhost:3002;
        server localhost:3003;
    }

    upstream backends {
        server serwer2:3001;
        server serwer2:3002;
        server serwer2:3003;
        server serwer2:3004;
        server serwer2:3005;
        server serwer2:3006;
    }

Oczywiście zamiast nazwy zdefinowanej w hostach (server2), możemy wskazywać bezpośrednio na IP i port, np: 192.168.1.10:3001. Porty zostały wybrane ze względów sentymentalnych i nie mają wpływu na działanie nginxa (no chyba że wpiszemy adres pod którym nic nie ma).

Dalej ustawiamy (bądź nie) kilka opcji związanych z timeoutami i gzipem:

    include       mime.types;
    default_type  application/octet-stream;

    keepalive_timeout 165;
    proxy_read_timeout 400;
    sendfile on;
    tcp_nopush on;
    gzip on;
    gzip_min_length 1000;
    gzip_proxied any;

    proxy_next_upstream error;

Ich dokładny opis można znaleźć np. w dokumentacji do nginxa.

Dalej musimy ustalić które żądania (skąd przychodzące) mają być realizowane przez którą z maszyn (frontend czy backend). Zajmijmy się najpierw backendem:

    server {
        listen 192.168.1.3:81;
        server_name server2;

        client_max_body_size 5M;

        location / {
            proxy_pass_header $server;
            proxy_set_header Host $http_host;
            proxy_redirect off;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Scheme $scheme;
            proxy_pass http://backends;
        }

    }

Ustawiamy nasłuch (192.168.1.3:81), kilka innych parametrów i najważniejsze czyli proxy_pass który przekierowywać będzie żądania na zdefiniowany przez nas upstream. To samo robimy dla frontendu:

    server {
        listen 192.168.1.2:80;
        server_name jakasnazwa;

        client_max_body_size 5M;

        location / {
            proxy_pass_header $server;
            proxy_set_header Host $http_host;
            proxy_redirect off;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Scheme $scheme;
            proxy_pass http://frontends;
        }
        
        error_page 502 /502.html;  
        location = /502.html {  
            root  /home/sciezka/error_templates;  
        }  

	error_page 500 /500.html;  
        location = /500.html {  
            root  /home/sciezka/error_templates;
	}
        error_page 404 /404.html;  
        location = /404.html {  
            root  /home/sciezka/error_templates;  
        }

    }

Jedyną różnicą jest ustawienie stron błędów tak aby były ustawiane przez nas. Na backendzie jest to zbędne, jednak tutaj w przypadku downtime’u miło byłoby móc powiadomić użytkowników wyświetlając stosowną stronę z komunikatem.

Na sam koniec ustawiamy nasłuch samego nginxa na świat:

    # Nginx status
    server {
      listen 80;
      server_name localhost;
      location /nginx_status {
        stub_status on;
        access_log off;
        allow 127.0.0.1;
        deny all;
      }   
    }

I możemy działać :)

Copyright © 2018 Running with Ruby

Theme by Anders NorenUp ↑