CLOVER🍀

That was when it all began.

HAProxyのmaxconnパラメーターを調べる

これは、なにをしたくて書いたもの?

HAProxyにmaxconnというパラメーターがあるのですが、これを指定した時の振る舞い…同時接続数という意味で…がよくわからないので、
ちょっと確認してみることにしました。

環境

環境はUbuntu Linux 18.04 LTSで、HAProxyのバージョンは以下です。

$ haproxy -v
HA-Proxy version 1.8.8-1ubuntu0.4 2019/01/24
Copyright 2000-2018 Willy Tarreau <willy@haproxy.org>

maxconnについて

パラメーター自体の説明は、こちら。

default、frontend、listenに指定できます。

maxconn

またserver単位にも指定できます。

server / maxconn

文字通り、同時接続数を設定できるパラメーターです。

このエントリでは、frontendとserverに着目して見ていきます(defaultとlistenは、frontendと合わせて理解できると思うので)。

バックエンドのサーバー

HAProxyの背後に置く、簡単なバックエンドのサーバープログラムを用意しましょう。Javaで書くことにします。

Javaのバージョン。

$ java -version
openjdk version "11.0.3" 2019-04-16
OpenJDK Runtime Environment (build 11.0.3+7-Ubuntu-1ubuntu218.04.1)
OpenJDK 64-Bit Server VM (build 11.0.3+7-Ubuntu-1ubuntu218.04.1, mixed mode, sharing)

用意したプログラム。ポートを指定して起動できるようにしています。
SimpleHttpServer.java

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import com.sun.net.httpserver.HttpServer;

public class SimpleHttpServer {
    public static void main(String... args) throws IOException {
    int port;

    if (args.length > 0) {
        port = Integer.parseInt(args[0]);
    } else {
        port = 8080;
    }

    int threadPoolSize = 100;

        HttpServer server = HttpServer.create(new InetSocketAddress(port), 128);
        server.createContext("/", exchange -> {
            LocalDateTime now = LocalDateTime.now();

            String formattedNow = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);

            System.out.printf("[%s] %s %s, waiting...%n", formattedNow, exchange.getRequestMethod(), exchange.getRequestURI());

            try {
                TimeUnit.SECONDS.sleep(1L);
            } catch (InterruptedException e) {
                // ignore
            }

            byte[] body = String.format("[%s] OK!!%n", formattedNow).getBytes(StandardCharsets.UTF_8);
            exchange.sendResponseHeaders(200, body.length);
            try (OutputStream os = exchange.getResponseBody()) {
                os.write(body);
            }
        });

        server.setExecutor(Executors.newFixedThreadPool(threadPoolSize));

        System.out.printf("[%s] server startup.%n", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));

        server.start();
    }
}

backlogは128、スレッド数は100、アクセス時には1秒間のwaitを行います。

このプログラムを「172.17.0.1」のサーバーで動作させるものとし、以下のように3つのプロセスを起動します。

$ java SimpleHttpServer.java 8000

$ java SimpleHttpServer.java 9000

$ java SimpleHttpServer.java 10000

HAProxyの設定

HAProxyの設定は、まずは以下から始めたいと思います。
/etc/haproxy/haproxy.cfg

global
    log /dev/log    local0
    log /dev/log    local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

defaults
    log global
    mode    http
    option  httplog
    option  dontlognull
        timeout connect 50000
        timeout client  50000
        timeout server  50000

frontend http
    bind *:80
    default_backend servers

backend servers
    balance roundrobin
    mode http
    http-request set-header X-Forwarded-Port %[dst_port]
    http-request add-header X-Forwarded-Proto https if { ssl_fc }

    server server1 172.17.0.1:8000
    server server2 172.17.0.1:9000
    server server3 172.17.0.1:10000

frontend stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 10s

statsを用意しています。

このHAProxyは、172.17.0.2で動作しているものとします。

ここで、まずは「http://172.17.0.2:8404/stats」にアクセスしてみましょう。

f:id:Kazuhira:20190728003828p:plain

ここで、Sessionsに着目すると、frontdendが2000、backendが200になっています。2000は、maxconnのデフォルト値みたいですね。

HAProxyでserverのmaxconnを設定してみましょう。3にしてみます。

    server server1 172.17.0.1:8000 maxconn 3
    server server2 172.17.0.1:9000 maxconn 3
    server server3 172.17.0.1:10000 maxconn 3

backendの、各ServerのLimitに値が反映されました。maxconnは、Limitとしてstats上は見えるみたいですね。

f:id:Kazuhira:20190728004109p:plain

ここで、frontendのmaxconnを5にしてみましょう。

frontend http
    bind *:80
    maxconn 5
    default_backend servers

すると、frontendのSessionsにおけるLimitの値が5になります。

f:id:Kazuhira:20190728004240p:plain

maxconnを確認するには、SessionsのLimitを見よ、と。

ちなみにですね、frontendのmaxconnを指定すると、backlogも同じ値になります。

$ ss -tnl
State                   Recv-Q                   Send-Q                                        Local Address:Port                                       Peer Address:Port                   
LISTEN                  0                        5                                                   0.0.0.0:80                                              0.0.0.0:*                      
LISTEN                  0                        128                                                 0.0.0.0:8404                                            0.0.0.0:*

serverのmaxconnの指定では、こうはなりません。

これは、maxconnの値がlistenシステムコールのbacklogとして使われるからですね。

https://github.com/haproxy/haproxy/blob/v1.8.0/src/listener.c#L196

maxconnとは異なる値でbacklogを指定したい場合は、設定ファイルでbacklogを明示的に指定します。

backlog

serverのみのmaxconnを指定する

frontendのmaxconnは削除して、1度serverのみでmaxconnを指定してみましょう。

    server server1 172.17.0.1:8000 maxconn 3
    server server2 172.17.0.1:9000 maxconn 3
    server server3 172.17.0.1:10000 maxconn 3

各サーバーのmaxconnは3ずつ、backendとして受け入れられるのは9までですね。

Linux上のbacklogは、デフォルトの128のままです。

$ ss -tnl
State                   Recv-Q                   Send-Q                                        Local Address:Port                                       Peer Address:Port                   
LISTEN                  0                        128                                                 0.0.0.0:80                                              0.0.0.0:*                      
LISTEN                  0                        128                                                 0.0.0.0:8404                                            0.0.0.0:*  

ここで、Apache Benchを使って、concurrenty 50、リクエスト数500でアクセスしてみます。

$ ab -c 50 -n 500 http://172.17.0.2/

この時にstatsを見てみましょう。

f:id:Kazuhira:20190728005250p:plain

SessionsのCurとMaxが上がります。Curは現在のserverへの接続数、Maxはこれまでの最大値を表しているようです。

ここで、Queueに着目すると41になっています。

Apache Benchからの同時接続数は50、実際に受け入れられる同時接続数は9でしたね。41がキューに入っているようなので、計算が
合ってそうですね。

なので、backendのserverへ振る先がなければ、そのリクエストはキューに入ります、と。

このキューの長さを指定するのがmaxqueueのようなのですが…?

maxqueue

キューに保持できる期間を指定するのがtimeout queueのようですね。

timeout queue

frontendのみmaxconnを指定する

では、serverのmaxconnは1度削除して

    server server1 172.17.0.1:8000
    server server2 172.17.0.1:9000
    server server3 172.17.0.1:10000

frontendのみ指定してみましょう。

frontend http
    bind *:80
    maxconn 9
    default_backend servers

再び、Apache Benchをかけてみます。

$ ab -c 50 -n 500 http://172.17.0.2/

今回は、Queueに値が入りません。それでも、serverへのSessionsの値は絞られているので同時接続数としては制限できているようです。

f:id:Kazuhira:20190728010015p:plain

キューの代わりとしては、Linux側のbacklogに接続が溜まっていきます。

$ ss -tnl
State                   Recv-Q                   Send-Q                                        Local Address:Port                                       Peer Address:Port                   
LISTEN                  10                       9                                                   0.0.0.0:80                                              0.0.0.0:*                      
LISTEN                  0                        128                                                 0.0.0.0:8404                                            0.0.0.0:*

このため、backlogに入りうる接続数を越えるとApache Bench側でresetを受け取るようになったりします。

Benchmarking 172.17.0.2 (be patient)
Completed 100 requests
apr_socket_recv: Connection reset by peer (104)
Total of 126 requests completed

backlogだけ増やしてみましょう。

frontend http
    bind *:80
    backlog 50
    maxconn 9
    default_backend servers

やっぱり、backlogに溜まっていくだけですね。

$ ss -tnl
State                   Recv-Q                   Send-Q                                        Local Address:Port                                       Peer Address:Port                   
LISTEN                  41                       50                                                  0.0.0.0:80                                              0.0.0.0:*                      
LISTEN                  0                        128                                                 0.0.0.0:8404                                            0.0.0.0:*

backlogに受け入れられる分だけ、キャパシティとしては上がりますが。

serverとfrontendの両方指定する

続いて、serverとfrontedのmaxconnを両方とも指定してみましょう。

server。

    server server1 172.17.0.1:8000 maxconn 3
    server server2 172.17.0.1:9000 maxconn 3
    server server3 172.17.0.1:10000 maxconn 3

frontend。

frontend http
    bind *:80
    maxconn 5
    default_backend servers

frontendの方が、値を小さくしています。

Apache Benchを実行。

ab -c 50 -n 500 http://172.17.0.2/

こうすると、frontendで止まってしまうので、server側のキューには入りません。

f:id:Kazuhira:20190728010826p:plain

backlog側にいます、と。

$ ss -tnl
State                   Recv-Q                   Send-Q                                        Local Address:Port                                       Peer Address:Port                   
LISTEN                  6                        5                                                   0.0.0.0:80                                              0.0.0.0:*                      
LISTEN                  0                        128                                                 0.0.0.0:8404                                            0.0.0.0:*

frontendのmaxconnを、serverの合計よりも大きくしてみます。

frontend http
    bind *:80
    maxconn 12
    default_backend servers

Apache Benchを実行すると、frontendとserverの合計数の差である3だけ、Queueに入っていることが確認できます。

f:id:Kazuhira:20190728011106p:plain

あとは、backlogに溜まっています。

$ ss -tnl
State                   Recv-Q                   Send-Q                                        Local Address:Port                                       Peer Address:Port                   
LISTEN                  13                       12                                                  0.0.0.0:80                                              0.0.0.0:*                      
LISTEN                  0                        128                                                 0.0.0.0:8404                                            0.0.0.0:*

つまり、serverのキューに入れたければ、serverのmaxconnの合計よりもfrontendのmaxconnの合計を大きくしないといけない、ということですね。

frontendのmaxconnは、backendのserverにリクエストを流す視点での同時接続数制御な感じがしますね。

timeout queueを指定する

ここで、frontendのmaxconnは再び削除します。

frontend http
    bind *:80
    default_backend servers

serverは、maxconnを指定したままです。

    server server1 172.17.0.1:8000 maxconn 3
    server server2 172.17.0.1:9000 maxconn 3
    server server3 172.17.0.1:10000 maxconn 3

timeout queueを、短い値で指定してみましょう。

defaults
        log     global
        mode    http
        option  httplog
        option  dontlognull
        timeout queue 500
        timeout connect 50000
        timeout client  50000
        timeout server  50000

こうすると、キューに入ったリクエストが500 msecで諦め、クライアントに503を返すようになります。

When a server's maxconn is reached, connections are left pending in a queue
which may be server-specific or global to the backend. In order not to wait
indefinitely, a timeout is applied to requests pending in the queue. If the
timeout is reached, it is considered that the request will almost never be
served, so it is dropped and a 503 error is returned to the client.

timeout queue

ちなみに、timeout queueを指定しない場合は、timeout connectの値が使われるそうです。

The "timeout queue" statement allows to fix the maximum time for a request to
be left pending in a queue. If unspecified, the same value as the backend's
connection timeout ("timeout connect") is used, for backwards compatibility
with older versions with no "timeout queue" parameter.

timeout connect

これは、バックエンドのサーバーへの接続タイムアウトですね。

キューの長さを指定する

serverのキューですが、デフォルトでは無制限の長さらしく、server単位にキューの長さを指定できるとか。

ちょっと試してみましょう。

    server server1 172.17.0.1:8000 maxconn 3 maxqueue 5
    server server2 172.17.0.1:9000 maxconn 3 maxqueue 5
    server server3 172.17.0.1:10000 maxconn 3 maxqueue 5

Apache Benchをかけつつstatsを見たところ、確かにQueueのLimitは設定されるものの、server単位のQueueのCurとかになにか変化が
現れるわけではないですね。backend全体としては、41個のリクエストがキューにいるみたいですし。

f:id:Kazuhira:20190728013750p:plain

このキューの部分は、もっとちゃんと見た方がいいかもしれません。

まとめ

HAProxyのfrontendとserverのmaxconnの設定と、いろいろパターンを変えつつ確認してみました。

ちょっとキュー自体の理解についてはまだ不足な感じがしますが、frontendとserverのmaxconnの差はわかった気がします。

接続は受け入れるものの、指定の同時接続数を超えた場合はキューにためていくのがserverのmaxconn、そもそもserverのキューまで
流しもしないのがfronendのmaxconnですね。

組み合わせると、serverのキューに入れる量をコントロールできそうではありますが、もうちょっとキューを調べないとハッキリしない
ところがある気がします。