Go言語から、ZookeeperでのHA動作を試してみる

アプリ冗長には、[Apache Zookeeper](https://zookeeper.apache.org)が活用されているそうですが、その動作イメージが理解できていなかったので、調査してみました。

# ◼︎ Zookeeperを制御するには...
Go言語から、Zookeeperを制御するにあたり、[Native Go Zookeeper Client Library](https://github.com/samuel/go-zookeeper)のオープンソース活用を前提にしております。


# ◼︎ Zookeeper動作環境をつくる

- Zookeeperをインストール（例えば、Mac OS Xにインストールする場合には...）

```
$ brew install zookeeper
```

- 必要に応じて、Zookeeperを起動する

```
 $ zkServer start
JMX enabled by default
Using config: /usr/local/etc/zookeeper/zoo.cfg
Starting zookeeper ... STARTED
```

- Native Go Zookeeper Client Libraryをインストールする

```
$ go get github.com/samuel/go-zookeeper/zk
```

# ◼︎ サンプルアプリを配置する

- storedデータ確認用サンプルアプリ

Zookeeperに保管されるkey-valueストアの値を確認することができます。
HA動作を試すことを目的としているので、<b>"/zookeeper-for-myApp"</b>のみに限定しております。

```go:sample_zookeeper_cli.go
package main

import (
    "github.com/samuel/go-zookeeper/zk"
    "os"
    "strings"
    "time"
    "fmt"
)

var RootPath string = "/zookeeper-for-myApp"
var zksStr string = os.Getenv("ZOOKEEPER_SERVERS")

func Connect(zks []string) *zk.Conn {
    conn, _, err := zk.Connect(zks, time.Second)
    if err != nil {
        panic(err)
    }
    return conn
}

func main() {
    zks := strings.Split(zksStr, ",")
    conn := Connect(zks)
    defer conn.Close()

    data, _, err := conn.Get(RootPath)
    fmt.Printf("%s: %s\n", RootPath, string(data))

    children, _, err := conn.Children(RootPath)
    if err != nil {
        panic(err)
    }
        for _, name := range children {
            data, _, err := conn.Get(RootPath+"/"+name)
            if err != nil {
                panic(err)
            }
        fmt.Printf("%s: %s\n", name, string(data))
    }
}
```

- 冗長確認用サンプルアプリ

ユーザアプリがHA構成（Active-StandBy）で動作します。

```go:sample_zookeeper_myApp.go 
package main

import (
    "github.com/samuel/go-zookeeper/zk"
    "os"
    "os/signal"
    "strings"
    "strconv"
    "time"
    "log"
)

var RootPath string = "/zookeeper-for-myApp"
var zksStr string = os.Getenv("ZOOKEEPER_SERVERS")

func CreateRootPath(zks []string) *zk.Conn {
    flags := int32(0)
    acl := zk.WorldACL(zk.PermAll)
    conn, _, err := zk.Connect(zks, time.Second)
    conn.Create(RootPath, []byte{0}, flags, acl)
    if err != nil {
        panic(err)
    }
    defer conn.Close()
    return conn
}


func JoinRootPath(zks []string) {
    acl := zk.WorldACL(zk.PermAll)
    conn, _, err := zk.Connect(zks, time.Second)
    if err != nil {
        panic(err)
    }
    lockPrefix := "/lock-"

    path, err := conn.CreateProtectedEphemeralSequential(RootPath+lockPrefix,
                                                         []byte{0}, acl)
    myData := getParse(path)
    ticker := time.NewTicker(time.Second)
    for {
        var isLeader bool = true
        children, _, err := conn.Children(RootPath)
        if err != nil {
            isLeader = false
        }
        if children == nil {
            isLeader = false
        }
        for _, c := range children {
            otherData := getParse(c)
            if myData > otherData {
                isLeader = false
                break
            }
        }

        if isLeader == true {
            log.Println("### I am Leader !! ###")
        }
        <-ticker.C
    }
}

func getParse(path string) int {
    splits := strings.Split(path, "-")
    seq := splits[len(splits)-1]
    data, _ := strconv.Atoi(seq)
    return data
}

func main() {
    zks := strings.Split(zksStr, ",")
    CreateRootPath(zks)
    quit_channel := make(chan os.Signal, 1)
    signal.Notify(quit_channel, os.Interrupt)
    go JoinRootPath(zks)
    <-quit_channel
}
```

# ◼︎ さっそく、動かしてみる
Zookeeper自体も冗長構成をとることも可能ですが、ここでは、シンプルにStandAlone構成としました。

![zookeeper.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/103595/6ee3b2ef-826a-edad-71d7-a9047f514296.jpeg)


(1) 環境変数の設定
Zookeeperにアクセスする場合には、ZOOKEEPER_SERVERS環境変数を有効にしておく必要があります。ZookeeperをインストールしたホストのIPアドレスを指定します。

```
$ export ZOOKEEPER_SERVERS=192.168.100.201:2181
```
という感じで、環境変数を有効にします。


(2) sample_zookeeper_myApp.goの起動
まずは、192.168.100.201側から起動します。

```text:192.168.100.201側
$ go run sample_zookeeper_myApp.go 
2016/01/23 08:49:33 ### I am Leader !! ###
2016/01/23 08:49:34 ### I am Leader !! ###
2016/01/23 08:49:35 ### I am Leader !! ###
2016/01/23 08:49:36 ### I am Leader !! ###

.. snip
```
このタイミングで、Zookeeperでのstoredデータを確認しておきます。

```
$ go run sample_zookeeper_cli.go 
/zookeeper-for-myApp: 
_c_d7f13bfb7352e81a199896a8da87623d-lock-0000000018: 
```

すると、"_c_d7f13bfb7352e81a199896a8da87623d-lock-0000000018"を保管されました。ここでは、<b>"0000000018"</b>という数値に注目しておいてください。

続いて、192.168.100.202側も、起動します。何も表示されません。StandByモードで動作しているためです。

```text:192.168.100.202側
$ go run sample_zookeeper_myApp.go
```
再び、Zookeeperでのstoredデータを確認します。

```
$ go run sample_zookeeper_cli.go 
/zookeeper-for-myApp: 
_c_d7f13bfb7352e81a199896a8da87623d-lock-0000000018: 
_c_2937f250274512907861a5d5ffde2481-lock-0000000019: 
```

今度は、"_c_2937f250274512907861a5d5ffde2481-lock-0000000019"が追加されました。

先ほどの"0000000018"よりも、値が大きい、<b>"0000000019"</b>という数値に注目してください。HA構成で動作するユーザアプリが、「Activeモードで動作するのか？」、「StandByモードで動作するのか？」の判定は、このstoredデータの末尾の数値の大小比較で決定されます。
ちなみに、判定ロジックの挙動については、以下の記事が参考になると思います。
[Chengwei's Words: How Zookeeper Leader Election Works](http://www.chengweiyang.cn/2014/07/05/how-zookeeper-leader-election-works/)

(3) Active側アプリの強制停止
192.168.100.201側のアプリを強制停止してみます。

```text:192.168.100.201側
$ go run sample_zookeeper_myApp.go 

.. snip

2016/01/23 08:49:58 ### I am Leader !! ###
2016/01/23 08:49:59 ### I am Leader !! ###
2016/01/23 08:50:00 ### I am Leader !! ###
2016/01/23 08:50:01 ### I am Leader !! ###
2016/01/23 08:50:02 ### I am Leader !! ###
2016/01/23 08:50:03 ### I am Leader !! ###
2016/01/23 08:50:04 ### I am Leader !! ###
^C
```

すると、先ほどまで、StandByモードで動作していた、192.168.100.202側では、Active動作を開始するようになりました。（なお、HA動作によるActiveノード切り替えに約６秒程度の時間を要している様子も併せて、確認できると思います。）

```text:192.168.100.202側
$ go run sample_zookeeper_myApp.go
2016/01/23 08:50:10 ### I am Leader !! ###
2016/01/23 08:50:11 ### I am Leader !! ###
2016/01/23 08:50:12 ### I am Leader !! ###
2016/01/23 08:50:13 ### I am Leader !! ###
2016/01/23 08:50:14 ### I am Leader !! ###
2016/01/23 08:50:15 ### I am Leader !! ###
2016/01/23 08:50:16 ### I am Leader !! ###
2016/01/23 08:50:17 ### I am Leader !! ###
2016/01/23 08:50:18 ### I am Leader !! ###
2016/01/23 08:50:19 ### I am Leader !! ###
2016/01/23 08:50:20 ### I am Leader !! ###
2016/01/23 08:50:21 ### I am Leader !! ###
```
再び、Zookeeperでのstoredデータを確認します。

```
$ go run sample_zookeeper_cli.go 
/zookeeper-for-myApp: 
_c_2937f250274512907861a5d5ffde2481-lock-0000000019: 
```

今度は、"_c_2937f250274512907861a5d5ffde2481-lock-0000000019"しか保管されておりません。

192.168.100.201側のアプリを強制停止したことにより、"_c_d7f13bfb7352e81a199896a8da87623d-lock-0000000018"が削除されたことがわかります。

以上より、サンプルアプリによるHA構成(Active-StandBy)が、正しく動作することが確認できました。

# ◼︎ 終わりに、
Zookeeperは、所謂、分散Key-Valueストアなのですね。
Zookeeperを活用したHA構成を実現するには、storedデータを随時ルックアップして、HA冗長の切り替え判定ロジックを実装する必要があります。ただし、[Native Go Zookeeper Client Library](https://github.com/samuel/go-zookeeper)のオープンソース活用すれば、そんなに稼働をかけずに、オリジナルな判定ロジックを実装したHA構成を実現することができるようになります。

# ◼︎ 参照元
- [Apache ZooKeeperはどうしてこんなに使われているのか](http://qiita.com/sakamotomsh/items/63500e184ad583bcb9e3)
- [Mark McGranaghan: Getting Started with Zookeeper and Go](https://mmcgrana.github.io/2014/05/getting-started-with-zookeeper-and-go.html)
- [Chengwei's Words: How Zookeeper Leader Election Works](http://www.chengweiyang.cn/2014/07/05/how-zookeeper-leader-election-works/)