アプリ冗長には、Apache Zookeeperが活用されているそうですが、その動作イメージが理解できていなかったので、調査してみました。
◼︎ Zookeeperを制御するには...
Go言語から、Zookeeperを制御するにあたり、Native Go Zookeeper Client Libraryのオープンソース活用を前提にしております。
◼︎ 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動作を試すことを目的としているので、"/zookeeper-for-myApp"のみに限定しております。
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)で動作します。
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構成としました。
(1) 環境変数の設定
Zookeeperにアクセスする場合には、ZOOKEEPER_SERVERS環境変数を有効にしておく必要があります。ZookeeperをインストールしたホストのIPアドレスを指定します。
$ export ZOOKEEPER_SERVERS=192.168.100.201:2181
という感じで、環境変数を有効にします。
(2) sample_zookeeper_myApp.goの起動
まずは、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"を保管されました。ここでは、"0000000018"という数値に注目しておいてください。
続いて、192.168.100.202側も、起動します。何も表示されません。StandByモードで動作しているためです。
$ 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"よりも、値が大きい、"0000000019"という数値に注目してください。HA構成で動作するユーザアプリが、「Activeモードで動作するのか?」、「StandByモードで動作するのか?」の判定は、このstoredデータの末尾の数値の大小比較で決定されます。
ちなみに、判定ロジックの挙動については、以下の記事が参考になると思います。
Chengwei's Words: How Zookeeper Leader Election Works
(3) Active側アプリの強制停止
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ノード切り替えに約6秒程度の時間を要している様子も併せて、確認できると思います。)
$ 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のオープンソース活用すれば、そんなに稼働をかけずに、オリジナルな判定ロジックを実装したHA構成を実現することができるようになります。