srvdというデーモンを書いた

一身上の都合によりsrvdというデーモンを書いた。

github.com

これは何?

DNSSRVレコードをバックエンドにしたconfdみたいなものです。

SRVレコードの値に合わせてミドルウェアの設定ファイルを書き換えて、設定ファイルが変更されたらミドルウェアをリロードする、みたいな。

ことの発端

某所ではMySQLのスレーブへのロードバランサーとして、Railsサーバに同居しているHAProxyを使ってるんですよ。 中央集権的ロードバランサーに比べて、スループットがよいとか、大量のコネクションが一カ所に集中しないとか性能的にはいいんですが、いかんせん設定ファイルをRailsサーバにばらまくのがめんどくさい。設定ファイルをばらまいた後は大量のHAProxyのリロードとRailsのリロード。

それをなんとかしたいと思ったので、いろいろ試してみたんですよ。

四苦八苦していたら同僚がHAProxyのserver-template(とそのデメリット)を教えてくれたので、さらに検証。

www.haproxy.com

HAProxy 1.8だと

server-template db 10 _mysql._tcp.example.com:3306 check port 3306 resolvers dns

みたいな設定を書くと、SRVレコードの値に合わせてバックエンドを設定してくれるんですよね。

一件、なかなかよいのですが

  • バックエンドのサーバ数が固定
    • 例えば、server-template db 10と書くとSRVレコードの返す値が2個でもdb1〜db10までのバックエンドが作られて、db1・db2以外は「ヘルスチェック失敗」というステータスになる
  • 減ったサーバはメンテ状態になって残る
    • SRVレコードがdb-001・db-002と返していたのが、db-002だけになると、db-001のバックエンドは「メンテ状態」のステータスで残り続ける

バックエンドが大きく変わるような場合だと、指定したサーバ数からあふれたり、残り続けたバックエンドがどうなるのかがよく分からない…なんとなーく、よくない匂いを感じる…

それで、consul-template使うか、いやconsulのクラスタを管理したいわけじゃないんだよな、もっとシンプルにやりたいんだよ、そういえばconfdってSRVレコード対応してたっけ…と調べてみると

confd/dns-srv-records.md at master · kelseyhightower/confd · GitHub

etcdやconsulのノードを見つけるためにSRVレコードは使えるけど、バックエンドとしては使えない。 たしかに、DNSはKVSじゃないしね…

confdのバックエンドに追加する修正を投げようかと思ったけれど、なんとなくポリシーが違いそうだし、confdのソースをざっと眺めた感じ、これくらいなら実装できるか、と思って作った次第です。

使い方

srvdの設定ファイルがこんな感じ。

src = "/etc/haproxy/haproxy.cfg.tmpl"
dest = "/etc/haproxy/haproxy.cfg"
domain = "_mysql._tcp.example.com"
reload_cmd = "/bin/systemctl reload haproxy.service"
check_cmd = "/usr/sbin/haproxy -c -V -f {{ .src }}"
interval = 1
timeout = 3
#resolv_conf = "/etc/resolv.conf"
cooldown = 60
#status_port = 8080

haproxy.cfgのテンプレートはこんな感じ。

backend nodes
  mode tcp
  # see https://godoc.org/github.com/miekg/dns#SRV
  {{ range .srvs }}
  server {{ .Target }} {{ .Target }}:{{ .Port }} check
  {{ end }}

SRVレコードを

10 10 3306 db-001.example.com.

と設定して、srvdを起動すると、以下のようなhaproxy.cfgが作成される。

backend nodes
  mode tcp
  # see https://godoc.org/github.com/miekg/dns#SRV

  server db-001.example.com. db-001.example.com.:3306 check

RaisのUnitファイルはこんな感じ。

[Unit]
Description=Rails
After=network.target
ReloadPropagatedFrom=haproxy.service

[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/hello
ExecStart=/usr/local/bin/bundle exec puma
ExecReload=/bin/kill -s USR2 $MAINPID

[Install]
WantedBy=multi-user.target

RailslocalhostのHAProxyを参照するように設定。

default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: scott
  password: tiger
  host: 127.0.0.1

コントローラで接続先が分かるようにして

  def index
    render plain: Item.connection.execute("show variables like 'hostname'").first.last + "\n"
    ActiveRecord::Base.clear_all_connections!
  end

curlで叩き続けると

$ while true; do curl localhost:3000; sleep 1; done
db-001
db-001
db-001

db-001に接続していることが分かる。


そうしたらSRVレコードにdb-001を追加。

10 10 3306 db-001.example.com.
10 10 3306 db-002.example.com.

変更して30秒〜1分ぐらい待つと、haproxy.cfgが書き換わってHAProxyとRailsがリロード。

Aug 03 02:35:01 app-101 srvd[20298]: 2018/08/03 02:35:01 The configuration has been changed. Update /etc/haproxy/haproxy.cfg
Aug 03 02:35:01 app-101 srvd[20298]: 2018/08/03 02:35:01 Run '/usr/sbin/haproxy -c -V -f {{ .src }}' for checking
Aug 03 02:35:01 app-101 srvd[20298]: 2018/08/03 02:35:01 /usr/sbin/haproxy: stdout: Configuration file is valid
Aug 03 02:35:01 app-101 srvd[20298]: 2018/08/03 02:35:01 Run '/bin/systemctl reload haproxy.service' for reloading
Aug 03 02:35:01 app-101 systemd[1]: Reloading HAProxy Load Balancer.
Aug 03 02:35:01 app-101 systemd[1]: Reloading Rails.
Aug 03 02:35:01 app-101 bundle[22391]: * Restarting...
Aug 03 02:35:01 app-101 haproxy[30792]: Configuration file is valid
Aug 03 02:35:01 app-101 haproxy-systemd-wrapper[21157]: haproxy-systemd-wrapper: re-executing
Aug 03 02:35:01 app-101 systemd[1]: Reloaded HAProxy Load Balancer.
Aug 03 02:35:01 app-101 haproxy-systemd-wrapper[21157]: haproxy-systemd-wrapper: executing /usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -Ds -sf 30587
Aug 03 02:35:01 app-101 systemd[1]: Reloaded Rails.
Aug 03 02:35:01 app-101 haproxy[30802]: Proxy localnodes started.
Aug 03 02:35:01 app-101 haproxy[30802]: Proxy localnodes started.
Aug 03 02:35:01 app-101 haproxy[30802]: Proxy nodes started.
Aug 03 02:35:01 app-101 haproxy[30802]: Proxy nodes started.
Aug 03 02:35:01 app-101 bundle[22391]: Puma starting in single mode...
Aug 03 02:35:01 app-101 bundle[22391]: * Version 3.12.0 (ruby 2.3.1-p112), codename: Llamas in Pajamas
Aug 03 02:35:01 app-101 bundle[22391]: * Min threads: 5, max threads: 5
Aug 03 02:35:01 app-101 bundle[22391]: * Environment: development
Aug 03 02:35:02 app-101 bundle[22391]: * Inherited tcp://0.0.0.0:3000
Aug 03 02:35:02 app-101 bundle[22391]: Use Ctrl-C to stop

新しいDBに接続するようになる。

backend nodes
  mode tcp
  # see https://godoc.org/github.com/miekg/dns#SRV

  server db-001.example.com. db-001.example.com.:3306 check

  server db-002.example.com. db-002.example.com.:3306 check
$ while true; do curl localhost:3000; sleep 1; done
...
db-001
db-001
db-001
db-002
db-001
db-002
db-001
db-002

その他

まあまあ使えるかな、と思いつつまだ実践に投入できていないのでなんともかんとも。 Dockerコンテナ内でのHAProxyのリロードも考えねば。