Google の Seesaw v2 で遊んでみた

↓ のニュースを見て面白そうだったので遊んでみました。
[GoogleがロードバランサーSeesawをオープンソース化（Go言語で書かれている）](http://jp.techcrunch.com/2016/01/30/20160129google-open-sources-its-seesaw-load-balancer/)

まだドキュメントが心もとないので、ソースを斜め読みしながら設定しました。
https://github.com/google/seesaw

試した環境は CentOS 7.2 on vSphere ESXi です。せっかくなのでメモとして残しておきます。

# 構成

既存のテスト環境に入れているので、アドレスは適当です。

```none:構成
* Seesaw は node01 にインストール
* node01 のインタフェースは IPアドレスを振った方が Node用で、無い方が LB の VIP用
* Requirement には Node のインタフェースは2つで、いずれも同じ L2 セグメントにするように書かれている
* VIP は 192.168.37.200
* client, server ともに同じセグメントで DSR 構成

                    +--------------+
                    |              |
                    |    node01    |
 192.168.37.0/24    |              |
                    +--------------+
                   .76|          |  <== (プロセスが起動するとここに .200 が来る)
                eno16777728  eno33554960
                      |          |
+--------------------------------------------------+
        |                  |            |
    .172|               .71|            |.72
    +--------+      +-----------+   +-----------+
    | client |      | backend01 |   | backend02 |
    +--------+      +-----------+   +-----------+

```

ソースの最新コミットは df04a7468ba6a2cacdcc771c05833b18cf3b07ff でした。
ここではエニーキャストには触れません。

# インストール

README は Debian/Ubuntu での例でしたが、個人的に慣れている CentOS でいきます。

```console:seesawのビルドからインストールまで
## テストなので・・・
# setenforce 0

## 必要なものを準備
# yum install -y git golang protobuf-compiler libnl3-devel

## パスは適当、root で作業しても気にしない
# mkdir go
# export GOPATH=/root/go
# go get -u golang.org/x/crypto/ssh
# go get -u github.com/dlintw/goconf
# go get -u github.com/golang/glog
# go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
# go get -u github.com/miekg/dns

## ビルドしてみる
# export PATH=$PATH:${GOPATH}/bin
# go get -u github.com/google/seesaw
# cd /root/go/src/github.com/google/seesaw/
# make test
# make install


## README のやり方だと Upstart 用の設定があるのでそこだけ飛ばす
# SEESAW_BIN="/usr/local/seesaw"
# SEESAW_ETC="/etc/seesaw"
# SEESAW_LOG="/var/log/seesaw"
# install -d "${SEESAW_BIN}" "${SEESAW_ETC}" "${SEESAW_LOG}"
# install "${GOPATH}/bin/seesaw_cli" /usr/bin/seesaw
# for component in {ecu,engine,ha,healthcheck,ncc,watchdog}; do
  install "${GOPATH}/bin/seesaw_${component}" "${SEESAW_BIN}"
done
# install "etc/seesaw/watchdog.cfg" "${SEESAW_ETC}"
# /sbin/setcap cap_net_raw+ep "${SEESAW_BIN}/seesaw_ha"
# /sbin/setcap cap_net_raw+ep "${SEESAW_BIN}/seesaw_healthcheck"
```

## 設定ファイル

よく分かっていないながらも、それっぽい設定ファイルを書きます。
config_server に何も無いと seesaw-config.example.com を参照してしまうようなので、別の設定を書いて hosts で解決できるようにして止めておきます。

```ini:/etc/seesaw/seesaw.cfg
[cluster]
anycast_enabled = false
name = test
node_ipv4 = 192.168.37.76
## peer_ipv4 は、ここでは存在しない 2台めの Seesaw ノード用
#peer_ipv4 = 192.168.37.77
## vip_ipv4 はここの構成だと意味がなさそうだが、入れておかないと動かない
vip_ipv4 = 192.168.37.80

[config_server]
## example では URL になっているけど、違うかも
## とりあえず仮で埋めておく
primary = https://seesaw-config.localdomain/

[interface]
node = eno16777728
lb = eno33554960
```

```none:/etc/hosts
127.0.0.1       seesaw-config.localdomain
```

セグメントを分けるのが面倒だったので DSR 構成で試します。
バックエンドは適切に設定してあるものとします。

```protobuf:/etc/seesaw/cluster.pb
seesaw_vip: <
  fqdn: "seesaw-vip.localdomain."
  ipv4: "192.168.37.80/24"
  status: TESTING
>
node: <
  fqdn: "node01.localdomain."
  ipv4: "192.168.37.76/24"
  status: TESTING
>
#node: <
#  fqdn: "node02.localdomain."
#  ipv4: "192.168.37.77/24"
#  status: TESTING
#>
vserver: <
  name: "test-vserver"
  entry_address: <
    fqdn: "lb-test.localdomain."
    ipv4: "192.168.37.200/24"
    status: TESTING
  >
  rp: "admin@localdomain"
  vserver_entry: <
    protocol: TCP
    port: 80
    scheduler: RR
    healthcheck: <
      type: HTTP
      port: 80
      mode: DSR
      proxy: false
      tls_verify: false
    >
  >
  backend: <
    host: <
      fqdn: "backend01.localdomain."
      ipv4: "192.168.37.71/24"
      status: TESTING
    >
    weight: 1
  >
  backend: <
    host: <
      fqdn: "backend02.localdomain."
      ipv4: "192.168.37.72/24"
      status: TESTING
    >
    weight: 1
  >
>
```

# Seesaw を実行

`/usr/local/seesaw/seesaw_watchdog` を実行して、動かしてみましょう。
フォアグラウンドでプロセスが残ります。ログを確認してみます。

```none:/var/log/seesaw/seesaw_ncc.log
F0131 18:57:34.972241    5389 ipvs.go:40] IPVS initialisation failed: failed to resolve family name
```

どうやら正しく動いていません。
自分で ip_vs モジュールをロードしておく必要があるようです。

```console:ip_vsモジュール設定
# echo ip_vs > /etc/modules-load.d/ipvs.conf
# systemctl restart systemd-modules-load.service
```

あらためて起動。

```none:/var/log/seesaw/seesaw_engine.log
F0131 19:38:11.393617   20543 core.go:274] Failed to initialise LB interface: Failed to initialise sysctls: open /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established: no such file or directory
```

別のログファイルにエラーが出ています。
さらにモジュールをロードします。

```cnosole:nf_conntrack_ipv4モジュール設定
# echo nf_conntrack_ipv4 > /etc/modules-load.d/nf_conntrack.conf
# systemctl restart systemd-modules-load.service
```

3度目のトライ。

```none:/var/log/seesaw/seesaw_engine.log
F0131 20:03:08.723735    1775 core.go:274] Failed to initialise LB interface: Failed to get dummy interface: no such network interface
```

まだ怒られますね。
どうやら DummyInterface が無いのが問題なようです。ソース中のデフォルトインタフェース名は dummy0 のようで、設定ファイルから変更できるか分からないので、名前を合わせて作ってみます。

```console:dummyインタフェース作成
# ip link add dummy0 type dummy
```

そして起動。

```none:/var/log/seesaw/seesaw_engine.log
F0201 08:55:13.497926   18909 core.go:607] Failed to bring LB interface up: Failed to get IPv4 default route: Default route not found
```

今度は、以下のあたりで問題になっています。

```go:ncc/ip.go（部分的に抜粋）
 40         routeDefaultIPv4Regexp = regexp.MustCompile(
 41                 `^default via (\d+\.\d+\.\d+\.\d+) dev ([a-z]+[0-9]) `)

264         if dr := routeDefaultIPv4Regexp.FindStringSubmatch(out); dr != nil {
265                 return net.ParseIP(dr[1]).To4(), nil
266         }
267         return nil, fmt.Errorf("Default route not found")
```

私の環境だと eno16777728 のようになっているため、NIC の名前が合っていません。仕方ないのでソースに手を加えてビルドし直します。
デフォルトルートが Seesaw ノードに設定されていなくてもエラーになるようなので、未設定の場合は設定しておきます。

```diff:ncc/ip.go
--- ncc/ip.go.org       2016-01-31 07:17:00.039679149 +0900
+++ ncc/ip.go   2016-02-01 11:44:38.874337074 +0900
@@ -38,7 +38,7 @@
        ifaceNameRegexp        = regexp.MustCompile(`^\w+\d+(\.\d+)?$`)
        ipv6AddrRegexp         = regexp.MustCompile(`^\s*inet6 ([a-f0-9:/]+) .*$`)
        routeDefaultIPv4Regexp = regexp.MustCompile(
-               `^default via (\d+\.\d+\.\d+\.\d+) dev ([a-z]+[0-9]) `)
+               `^default via (\d+\.\d+\.\d+\.\d+) dev ([a-z]+[0-9]+) `)
 )

 // validateInterface validates the name of a network interface.
```

影響範囲が分からないのでバイナリは全て install し直します。

```console:rebuild
# cd $GOPATH/src/github.com/google/seesaw
# make install

## 環境変数は前に設定したものが残っている想定
# install "${GOPATH}/bin/seesaw_cli" /usr/bin/seesaw
# for component in {ecu,engine,ha,healthcheck,ncc,watchdog}; do
  install "${GOPATH}/bin/seesaw_${component}" "${SEESAW_BIN}"
done
# install "etc/seesaw/watchdog.cfg" "${SEESAW_ETC}"
# /sbin/setcap cap_net_raw+ep "${SEESAW_BIN}/seesaw_ha"
# /sbin/setcap cap_net_raw+ep "${SEESAW_BIN}/seesaw_healthcheck"
```

改めて `/usr/local/seesaw/seesaw_watchdog` を起動すると、ついに動きました。

## 動作確認

```console:動作確認
## backend で指定したサーバにヘルスチェックが来ています
# tail -f /var/log/nginx/access.log    ## backend01
192.168.37.76 - - [01/Feb/2016:12:48:35 +0900] "GET / HTTP/1.1" 200 3700 "-" "Go 1.1 package http" "-"
# tail -f /var/log/nginx/access.log    ## backend02
192.168.37.76 - - [01/Feb/2016:12:48:36 +0900] "GET / HTTP/1.1" 200 3700 "-" "Go 1.1 package http" "-"

## client (192.168.37.172) から GET してみると、round robin されました
# curl http://192.168.37.200/hogehoge > /dev/null    ## from client
# curl http://192.168.37.200/hogehoge > /dev/null    ## from client
# grep hogehoge /var/log/nginx/access.log    ## backend01
192.168.37.172 - - [01/Feb/2016:13:42:59 +0900] "GET /hogehoge HTTP/1.1" 404 3650 "-" "curl/7.29.0" "-"
# grep hogehoge /var/log/nginx/access.log    ## backend02
192.168.37.172 - - [01/Feb/2016:13:43:03 +0900] "GET /hogehoge HTTP/1.1" 404 3650 "-" "curl/7.29.0" "-"


## seesaw の cli から状態を確認できます
# seesaw -c "show nodes"
Nodes
[1] node01.localdomain. enabled
# seesaw -c "show backends"
Backends
[   1] backend01.localdomain.
[   2] backend02.localdomain.
# seesaw -c "show vservers"
Vserver
  Name:                test-vserver
  Hostname:            lb-test.localdomain.
  Status:              enabled (override state default; config state enabled)
  IPv4 Address:        192.168.37.200/24
  IPv6 Address:        <not configured>

  Services:

    IPv4 TCP/80    (DSR, rr scheduler)
        State:       enabled, healthy, active
        Watermarks:  Low 0.00, High 0.00, Currently 1.00

## LVS なので、ipvsadm コマンドでも状態を見ることもできます
# yum install -y ipvsadm
# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  192.168.37.200:80 rr
  -> 192.168.37.71:80             Route   1      0          0
  -> 192.168.37.72:80             Route   1      0          0
FWM  65536 rr
  -> 192.168.37.71:0              Route   1      0          6
FWM  65537 rr
  -> 192.168.37.72:0              Route   1      0          10
```

ただ、まだ設定は改善が必要そうです。
`lb=` に設定したインタフェースは MAC アドレスが変更されており、仮想MACアドレスを意識したものと思われますが今回の構成では `node=` に設定した NIC の MAC アドレスで ARP 応答されています。
遊び時間が足りませんでしたが、いずれ調べたいです。


```console:
## Seesaw ノード
# ip a show dev eno33554960
3: eno33554960: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:00:5e:00:01:3c brd ff:ff:ff:ff:ff:ff
    inet 192.168.37.80/24 brd 192.168.37.255 scope global eno33554960
       valid_lft forever preferred_lft forever
    inet 192.168.37.200/24 brd 192.168.37.255 scope global secondary eno33554960
       valid_lft forever preferred_lft forever
    inet6 fe80::200:5eff:fe00:13c/64 scope link
       valid_lft forever preferred_lft forever

## client からの見え方
# ip n show to 192.168.37.200
192.168.37.200 dev enp3s0.37 lladdr 00:0c:29:94:66:fc STALE
```

真面目に使うときにはユニットファイルを書くことになると思います。
Upstart 向け設定では `limit nofile 8192 8192` が書かれているので、ユニットファイルを作る際は `LimitNOFILE` を入れておくと良さそうです。

Config Server も試したかったのですが、理解が足りていないのでいつか・・・