サーバ1台構成システムの為に go-redis-setlock を改造した話
サーバ1台構成でも blue green deploy したい皆様こんにちは。
システム構成
- サーバ1台
- docker-compose 環境
dockerコンテナ |
---|
Load Balancer (nginx) |
app blue |
app green |
batch blue |
batch green |
db |
redis |
web
サーバ1台構成の場合、web だと、
前側コンテナ | 後ろ側コンテナ |
---|---|
Load Balancer (nginx) | |
+ | app blue |
+ | app green |
のような構成で blue green deploy します。
Load Balancer(nginx) は単に切り替えスイッチですね。
deploy の流れ (web)
deploy 時にサービス瞬断するのはサーバ1台構成の場合でもダメなので、
(ハード故障時は仕方ない。SPOF てんこもりなので。)
app blue がアクティブ、app green がスタンバイの時
- app green に次バージョンの app を deploy する
- app green で動作確認する
- Load Balancer (nginx) で app green をアクティブにする
- Load Balancer (nginx) で app blue を非アクティブにする(強制切断しない)
- app blue への接続が無くなるのを待つ(web 画面なので10秒程度)
- Load Balancer (nginx) でapp blue に接続出来ないようにする
の流れで、(gracefulに)切り替えます。
後ろ側コンテナ | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
app blue | ◎ | ◎ | ◎ | ○ | ○ | △ |
app green | △ | △ | ◎ | ◎ | ◎ | ◎ |
◎:アクティブ
○:アクティブ→スタンバイの移行期間
△:スタンバイ
×:停止
batch
cron 等で動く batch も、web 同様の瞬断無し deploy であって欲しいですね。
共通コンテナ | blue/greenコンテナ |
---|---|
redis(排他制御用) | |
batch blue | |
batch green |
deploy の流れ (batch)
batch は同時に動いては困るスクリプトが多いので、複数コンテナ間の排他制御が必要です。
前提1:batch スクリプト中に、先に起動したものだけが動く排他制御の仕組みがある
前提2:batch job 毎に、動く・動かないを切り替える仕組みがある(supervisordとか)
batch blue がアクティブ、batch green がスタンバイの時
- batch green を空で起動する(docker-compose start [batch green])
- batch green に次バージョンの batch を deploy する
- batch green を動かす(batch blue と並行動作)
- batch blue で新しい job が起動しないようにする(実行中 job の強制終了はしない)
- batch blue で実行中の各 job が終了し次第、jobを停止していく(supervisorctl stop [job1])
- batch blue を止める(docker-compose stop [batch blue])
の流れで、(gracefulに)切り替えます。
blue/greenコンテナ | 最初 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
batch blue | ◎ | ◎ | ◎ | ◎ | ○ | ○ | × |
batch green | × | △ | △ | ◎ | ◎ | ◎ | ◎ |
◎:アクティブ
○:アクティブ→スタンバイの移行期間
△:スタンバイ
×:停止
排他制御の実現方法
サーバ1台構成なので、flock でも実現可能ではありますが、
それだと将来サーバ複数台構成になった時に改造必須です。
最初からサーバ複数台構成でも大丈夫な実装にしたいですね。
redis を使って排他制御しましょう。
redis でどう排他制御を実現するか
- 同時に実行しようとしたスクリプトは早いもの勝ち
- 遅れて実行したスクリプトは、前回処理日時を取得して、それが直近だった場合は他コンテナで実施済みと見なして終了する
の2つが実現できればいいですね。
第1層 | 第2層 | 第3層 | 用途 |
---|---|---|---|
/job/job1/ p1.sh | redisで排他ロック取得 | ||
/job/job1/ p2.sh | 前回実施日時を取得して、直近で実施されていない事を確認 | ||
/job/job1/ main.php | 本体処理 |
redis で排他制御するにはこんなのがあります。
go-redis-setlock (fujiwara版) を使ってみた
サーバ1台構成の為、redis は /var/run/redis/redis.sock の listen のみです。
実行
REDIS_SOCK=/var/run/redis/redis.sock
go-redis-setlock -redis ${REDIS_SOCK} -n -x rkey_lock sleep 3
結果 (ERROR)
Redis server seems down: dial tcp: address /var/run/redis/redis.sock: missing port in address
ソースコードの redis 接続部分
c, err = redis.DialTimeout("tcp", opt.Redis, time.Duration(timeout)*time.Second)
はい。tcp 接続限定ですね。
go-redis-setlock を改造した
redis は unix domain socket 接続限定のままにしたかったので(サーバリソース節約志向)、
go-redis-setlock を改造しました。
- c, err = redis.DialTimeout("tcp", opt.Redis, time.Duration(timeout)*time.Second)
+ prot := "tcp"
+ server := opt.Redis
+ if ("unix:" == server[:5]) {
+ prot = "unix"
+ server = server[5:]
+ }
+ c, err = redis.DialTimeout(prot, server, time.Duration(timeout)*time.Second)
サーバ1台構成でのみ必要とされる、ニッチすぎる機能なので、
プルリクせずにハードフォークで済ますことにしました。
go-redis-setlock (改造版) を使ってみた
実行
REDIS_SOCK=/var/run/redis/redis.sock
go-redis-setlock -redis unix:${REDIS_SOCK} -n -x rkey_lock sleep 3
結果 (OK)
3秒sleep
第1層、第2層、第3層の各スクリプト
ENV
REDIS_SOCK="/var/run/redis/redis.sock"
RKEY_JOB1="batch_rkey_job1"
RKEY_JOB1_DT="batch_rkey_job1_dt"
第1層 : /job/job1/p1.sh
#!/bin/bash
# ロックが取得できたら実行
# ロックが取得出来なかったら即終了
go-redis-setlock -redis unix:${REDIS_SOCK} \
-n -x ${RKEY_JOB1} /job/job1/p2.sh
第2層 : /job/job1/p2.sh
#!/bin/bash
# 現在日時
DT=$(date +%Y%m%d%H%M%S)
# 前回実施日時
JOB1_DT=$(redis-cli -s ${REDIS_SOCK} GET ${RKEY_JOB1_DT})
# チェック日時=現在日時−1時間
HHMMSS=10000
DT_CHECK=$((${DT} - ${HHMMSS}))
# チェック日時 <= 前回実施日時 の時、他コンテナで実施済みと見なして終了
if [ ${DT_LOWER} -le ${DT_EXEC} ] ; then
echo "skip"
exit 0
fi
# 本体処理
/job/job1/main.php
第3層 : /job/job1/main.php
本体処理