CLOVER🍀

That was when it all began.

HAProxyでHTTPロードバランシング

少し前に、nginxでHTTPロードバランシングをやってみましたが、今度はHAProxyでやってみます。

nginxのHTTPロードバランシングを試す - CLOVER

HAProxyの設定をするには、このあたりを参考にしました。

Proxies

マイクロサービス時代のHAProxy | Yakst

第1章 ロードバランサーの概要 - Red Hat Customer Portal

HAProxyを使い始めてみる

HAProxy を使ってみたメモ - ngyukiの日記

HAProxy のバックエンド Web サーバーヘルスチェックでコンテンツの中身をチェックする

HAProxyをDNS名で指定したバックエンドのIPアドレスが変わったらリロードする

使うHAProxyは、1.6.3です。

では、書いていきましょう。

バックエンドサーバーを書く

まずは、ロードバランサの背後にいるバックエンドのサーバーを用意します。nginxの時も、簡単にSpring Boot CLIで用意しました。以下の3つのサーバー名で、サーバーを用意します。

  • server1 (172.17.0.2)
  • server2 (172.17.0.3)
  • server3 (172.17.0.4)

server.groovy

import javax.servlet.http.HttpServletRequest

@RestController
class HelloController {
    def logger = org.slf4j.LoggerFactory.getLogger(getClass())

    @GetMapping('hello')
    def hello(HttpServletRequest request) {
        logger.info(java.time.LocalDateTime.now().toString() + ": access " + request.requestURI)
        'Hello ' + InetAddress.localHost.hostName + "!!" + System.lineSeparator()
    }
}

アクセスしたら「Hello [ホスト名]!!」を返却します。

サーバーを起動。

$ spring run server.groovy

ロードバランサを設定する

それでは、この3つのサーバーの前段にHAProxyをバランサーとして配置します。

なお、HAProxyは

  • 172.17.0.5

で稼働しているものとします。

haproxy.cfgの設定を、デフォルトの設定から以下のように追記
/etc/haproxy/haproxy.cfg

frontend app-http
    bind *:8080
    default_backend app-servers

backend app-servers
    balance roundrobin
    mode http

    option forwardfor

    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.2:8080
    server server2 172.17.0.3:8080
    server server3 172.17.0.4:8080

HAProxyはfrontendでクライアント側からのリクエストの受け付けの設定、backendで実際のサーバーへの設定を行うようです。

今回は、8080ポートでリッスン、バックエンドには「app-servers」という名前のものを指定。「app-servers」はこのあと定義します。

frontend app-http
    bind *:8080
    default_backend app-servers

バックエンドの設定。バランシングアルゴリズムラウンドロビン、serverで背後にいるサーバーを指定します。あとはX-Forwarded-Forの設定を入れています。

backend app-servers
    balance roundrobin
    mode http

    option forwardfor

    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.2:8080
    server server2 172.17.0.3:8080
    server server3 172.17.0.4:8080


で、HAProxyを再起動。

確認。

$ curl http://172.17.0.5:8080/hello
Hello server1!!
$ curl http://172.17.0.5:8080/hello
Hello server2!!
$ curl http://172.17.0.5:8080/hello
Hello server3!!
$ curl http://172.17.0.5:8080/hello
Hello server1!!
$ curl http://172.17.0.5:8080/hello
Hello server2!!
$ curl http://172.17.0.5:8080/hello
Hello server3!!

1台(server3)をダウンさせてアクセスしてみます。

$ curl http://172.17.0.5:8080/hello
Hello server1!!
$ curl http://172.17.0.5:8080/hello
Hello server2!!
$ curl http://172.17.0.5:8080/hello
<html><body><h1>503 Service Unavailable</h1>
No server is available to handle this request.
</body></html>

あら…。
※このあと繰り返しても、同じです。

ヘルスチェックを設定する

で、こういうのだと困るので、ヘルスチェックを設定してみます。server3は、起動状態に戻します。

ヘルスチェックを行うには、serverのオプションに「check」を入れます。

    server server1 172.17.0.2:8080 check
    server server2 172.17.0.3:8080 check
    server server3 172.17.0.4:8080 check

HAProxyを再起動。

確認。

$ curl http://172.17.0.5:8080/hello
Hello server1!!
$ curl http://172.17.0.5:8080/hello
Hello server2!!
$ curl http://172.17.0.5:8080/hello
<html><body><h1>503 Service Unavailable</h1>
No server is available to handle this request.
</body></html>

$ curl http://172.17.0.5:8080/hello
Hello server1!!
$ curl http://172.17.0.5:8080/hello
Hello server2!!
$ curl http://172.17.0.5:8080/hello
Hello server1!!

1回失敗してますが、そのあとはOKそうですね。

もう少し、細かくヘルスチェックの設定をしてみましょう。

ヘルスチェック対象のURLや間隔を指定してみます。

ヘルスチェック用のURLを、バックエンドサーバーに追加します。「/health-check」でアクセスするものとしましょう。
server.groovy

import javax.servlet.http.HttpServletRequest

@RestController
class HelloController {
    def logger = org.slf4j.LoggerFactory.getLogger(getClass())

    @GetMapping('hello')
    def hello(HttpServletRequest request) {
        logger.info(java.time.LocalDateTime.now().toString() + ": access " + request.requestURI)
        'Hello ' + InetAddress.localHost.hostName + "!!" + System.lineSeparator()
    }

    @GetMapping('health-check')
    def healthCheck(HttpServletRequest request) {
        logger.info(java.time.LocalDateTime.now().toString() + ": access " + request.requestURI)
        'OK ' + InetAddress.localHost.hostName + "!!" + System.lineSeparator()
    }
}

HAProxy側の設定は、このようにします。

backend app-servers
    balance roundrobin
    mode http

    option forwardfor
    option httpchk GET /health-check

    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.2:8080 inter 3000 check
    server server2 172.17.0.3:8080 inter 3000 check
    server server3 172.17.0.4:8080 inter 3000 check

「option httpchk」でヘルスチェック先の設定をして

    option httpchk GET /health-check

バックエンドサーバーには、間隔3秒でヘルスチェックを行う、と。

    server server1 172.17.0.2:8080 inter 3000 check
    server server2 172.17.0.3:8080 inter 3000 check
    server server3 172.17.0.4:8080 inter 3000 check

この状態でHAProxyを起動すると、バックエンドサーバーには3秒に1度アクセスが来ることが確認できます。

2016-11-26 06:33:34.921  INFO 147 --- [nio-8080-exec-1] HelloController                          : 2016-11-26T06:33:34.905: access /health-check
2016-11-26 06:33:37.958  INFO 147 --- [nio-8080-exec-2] HelloController                          : 2016-11-26T06:33:37.958: access /health-check
2016-11-26 06:33:40.991  INFO 147 --- [nio-8080-exec-3] HelloController                          : 2016-11-26T06:33:40.991: access /health-check

結果は、最初にヘルスチェックを入れた時とそう変わらないので割愛。

ボディの内容のチェックもできるみたい?

HAProxy のバックエンド Web サーバーヘルスチェックでコンテンツの中身をチェックする

セッションパーシステンスする

最後に、セッションパーシステンスしてみましょう。

セッションを使って確認するので、サーバー側のコードを以下のように変更します。けっこう適当ですが、ご愛嬌。

server.groovy

import javax.servlet.http.HttpServletRequest

@RestController
class HelloController {
    def logger = org.slf4j.LoggerFactory.getLogger(getClass())

    @GetMapping('hello')
    def hello(HttpServletRequest request) {
        logger.info(java.time.LocalDateTime.now().toString() + ": access " + request.requestURI)

        def session = request.session
        def now = session.getAttribute('now')
        if (!now) {
            now = java.time.LocalDateTime.now().toString()
            session.setAttribute('now', now)
        }

        '[' + now + '] Hello ' + InetAddress.localHost.hostName + "!!" + System.lineSeparator()
    }

    @GetMapping('health-check')
    def healthCheck(HttpServletRequest request) {
        logger.info(java.time.LocalDateTime.now().toString() + ": access " + request.requestURI)
        'OK ' + InetAddress.localHost.hostName + "!!" + System.lineSeparator()
    }
}

今のままだと、こんな感じにアクセスしても都度違うサーバーに振り分けられます。当然、セッションも維持されません。

$ curl -b cookie.txt -c cookie.txt http://172.17.0.5:8080/hello
[2016-11-26T06:52:16.530] Hello server1!!

で、HAProxy側の設定は、このように変更。

backend app-servers
    balance roundrobin
    appsession JSESSIONID len 32 timeout 2h request-learn

参考)
demo/HAProxy-の各種設定方法.md at master · worksap-ate/demo · GitHub

としたら、うまくいきませんでした。HAProxyがappsessionを理解できないらしく、コケてしまいます…。

$ sudo service haproxy start
 * Starting haproxy haproxy                                                                                                                                                                             [ALERT] 330/065527 (648) : parsing [/etc/haproxy/haproxy.cfg:42] : 'appsession' is not supported anymore, please check the documentation.
[ALERT] 330/065527 (648) : Error(s) found in configuration file : /etc/haproxy/haproxy.cfg
[ALERT] 330/065527 (648) : Fatal errors found in configuration.
                                                                                                                                                                                                 [fail]

困りました。ドキュメントにもappsessionは書いてあるのに…。

で、ちょっと調べたらこんなエントリを見つけまして。
Load Balancing, Affinity, Persistence, Sticky Sessions: What You Need to Know - HAProxy Technologies

これを見ると、appsessionではなくてcookieを使え、と。

http://cbonte.github.io/haproxy-dconv/1.6/configuration.html#4-cookie:cookie

HAProxyの設定は、このように変更。

backend app-servers
    balance roundrobin
    cookie JSESSIONID prefix nocache
    mode http

    option forwardfor
    option httpchk GET /health-check

    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.2:8080 inter 3000 check cookie server1
    server server2 172.17.0.3:8080 inter 3000 check cookie server2
    server server3 172.17.0.4:8080 inter 3000 check cookie server3

Cookie名は、「JSESSIONID」として、prefixを指定します。

    cookie JSESSIONID prefix nocache

prefixでは、Cookieの先頭に指定した接頭辞が入ることになるので、バックエンドサーバーにもその設定を入れます。

    server server1 172.17.0.2:8080 inter 3000 check cookie server1
    server server2 172.17.0.3:8080 inter 3000 check cookie server2
    server server3 172.17.0.4:8080 inter 3000 check cookie server3

cookieのあとに続いているのが、接頭辞になります。

試してみましょう。

スティックされるようになりました。

$ curl -b cookie.txt -c cookie.txt http://172.17.0.5:8080/hello
[2016-11-26T07:07:06.833] Hello server1!!
$ curl -b cookie.txt -c cookie.txt http://172.17.0.5:8080/hello
[2016-11-26T07:07:06.833] Hello server1!!
$ curl -b cookie.txt -c cookie.txt http://172.17.0.5:8080/hello
[2016-11-26T07:07:06.833] Hello server1!!

この時、Cookieの値を見ると、Cookieの値に指定したprefixが入っています。

$ cat cookie.txt 
# Netscape HTTP Cookie File
# http://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_172.17.0.5	FALSE	/	FALSE	0	JSESSIONID	server1~D338ED4E06048EEAABE43E26BA1EB2E4

振り分け直すと、こんな感じに。

#HttpOnly_172.17.0.5	FALSE	/	FALSE	0	JSESSIONID	server2~1CA5FDBD2807B239AAFFA1E555C0B9D0

とりあえず、やりたいことはできた感じです。

まとめ

というわけで、HAProxyを使ってHTTPロードバランシングを試してみました。

最後にセッションパーシステンスもやってみましたけど、appsessionを1.6のHAProxyが使えないのがバグなのか、それとも機能として落とされたのかがよくわかりませんでした…。

'appsession' is not supported anymore ... · Issue #105 · docker/dockercloud-haproxy · GitHub

[SOLVED] HAProxy 1.6 Ubuntu 16.04 fails to start with 1.5 config file [Archive] - Ubuntu Forums