これは、なにをしたくて書いたもの?
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に指定できます。
またserver単位にも指定できます。
文字通り、同時接続数を設定できるパラメーターです。
このエントリでは、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」にアクセスしてみましょう。
ここで、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上は見えるみたいですね。
ここで、frontendのmaxconnを5にしてみましょう。
frontend http bind *:80 maxconn 5 default_backend servers
すると、frontendのSessionsにおけるLimitの値が5になります。
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を明示的に指定します。
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を見てみましょう。
SessionsのCurとMaxが上がります。Curは現在のserverへの接続数、Maxはこれまでの最大値を表しているようです。
ここで、Queueに着目すると41になっています。
Apache Benchからの同時接続数は50、実際に受け入れられる同時接続数は9でしたね。41がキューに入っているようなので、計算が
合ってそうですね。
なので、backendのserverへ振る先がなければ、そのリクエストはキューに入ります、と。
このキューの長さを指定するのがmaxqueueのようなのですが…?
キューに保持できる期間を指定するのが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の値は絞られているので同時接続数としては制限できているようです。
キューの代わりとしては、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側のキューには入りません。
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に入っていることが確認できます。
あとは、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 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.
これは、バックエンドのサーバーへの接続タイムアウトですね。
キューの長さを指定する
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個のリクエストがキューにいるみたいですし。
このキューの部分は、もっとちゃんと見た方がいいかもしれません。
まとめ
HAProxyのfrontendとserverのmaxconnの設定と、いろいろパターンを変えつつ確認してみました。
ちょっとキュー自体の理解についてはまだ不足な感じがしますが、frontendとserverのmaxconnの差はわかった気がします。
接続は受け入れるものの、指定の同時接続数を超えた場合はキューにためていくのがserverのmaxconn、そもそもserverのキューまで
流しもしないのがfronendのmaxconnですね。
組み合わせると、serverのキューに入れる量をコントロールできそうではありますが、もうちょっとキューを調べないとハッキリしない
ところがある気がします。