サーバ１台構成システムの為に go-redis-setlock を改造した話

# サーバ１台構成システムの為に go-redis-setlock を改造した話

サーバ１台構成でも blue green deploy したい皆様こんにちは。

## システム構成

- サーバ１台
- docker-compose 環境

|dockerコンテナ|
|:---:|
|Load Balancer (nginx)|
|app blue|
|app green|
|batch blue|
|batch green|
|db|
|redis|

## web

サーバ１台構成の場合、web だと、

|前側コンテナ|後ろ側コンテナ|
|---:|---|
|Load Balancer (nginx)|
|+|app blue|
|+|app green|

のような構成で blue green deploy します。
Load Balancer(nginx) は単に切り替えスイッチですね。

### deploy の流れ (web)

deploy 時にサービス瞬断するのはサーバ１台構成の場合でもダメなので、
(ハード故障時は仕方ない。SPOF てんこもりなので。)

app blue がアクティブ、app green がスタンバイの時

1. app green に次バージョンの app を deploy する
2. app green で動作確認する
3. Load Balancer (nginx) で app green をアクティブにする
4. Load Balancer (nginx) で app blue を非アクティブにする(強制切断しない)
5. app blue への接続が無くなるのを待つ(web 画面なので１０秒程度)
6. 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 は同時に動いては困るスクリプトが多いので、複数コンテナ間の排他制御が必要です。

前提１：batch スクリプト中に、先に起動したものだけが動く排他制御の仕組みがある
前提２：batch job 毎に、動く・動かないを切り替える仕組みがある(supervisordとか)

batch blue がアクティブ、batch green がスタンバイの時

1. batch green を空で起動する(docker-compose start [batch green])
2. batch green に次バージョンの batch を deploy する
3. batch green を動かす（batch blue と並行動作）
4. batch blue で新しい job が起動しないようにする(実行中 job の強制終了はしない)
5. batch blue で実行中の各 job が終了し次第、jobを停止していく(supervisorctl stop [job1])
6. batch blue を止める(docker-compose stop [batch blue])

の流れで、（gracefulに）切り替えます。

|blue/greenコンテナ|最初|1|2|3|4|5|6|
|---|---|---|---|---|---|---|---|
|batch blue|◎|◎|◎|◎|○|○|×|
|batch green|×|△|△|◎|◎|◎|◎|

◎：アクティブ
○：アクティブ→スタンバイの移行期間
△：スタンバイ
×：停止

### 排他制御の実現方法

サーバ１台構成なので、flock でも実現可能ではありますが、
それだと将来サーバ複数台構成になった時に改造必須です。
最初からサーバ複数台構成でも大丈夫な実装にしたいですね。
redis を使って排他制御しましょう。

#### redis でどう排他制御を実現するか

- 同時に実行しようとしたスクリプトは早いもの勝ち
- 遅れて実行したスクリプトは、前回処理日時を取得して、それが直近だった場合は他コンテナで実施済みと見なして終了する

の２つが実現できればいいですね。

|第１層|第２層|第３層|用途|
|---|---|---|---|
|/job/job1/ p1.sh|||redisで排他ロック取得|
||/job/job1/ p2.sh||前回実施日時を取得して、直近で実施されていない事を確認|
|||/job/job1/ main.php|本体処理|

redis で排他制御するにはこんなのがあります。

[go-redis-setlock (fujiwara版)](http://fujiwara.github.io/go-redis-setlock/)

#### go-redis-setlock (fujiwara版) を使ってみた

サーバ１台構成の為、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)
```

サーバ１台構成でのみ必要とされる、ニッチすぎる機能なので、
プルリクせずにハードフォークで済ますことにしました。

[go-redis-setlock (改造版)](https://github.com/sreinfrasystemjp/go-redis-setlock)


#### 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
```

#### 第１層、第２層、第３層の各スクリプト

ENV
```
REDIS_SOCK="/var/run/redis/redis.sock"
RKEY_JOB1="batch_rkey_job1"
RKEY_JOB1_DT="batch_rkey_job1_dt"
```

第１層 : /job/job1/p1.sh

```
#!/bin/bash

# ロックが取得できたら実行
# ロックが取得出来なかったら即終了
go-redis-setlock -redis unix:${REDIS_SOCK} \
-n -x ${RKEY_JOB1} /job/job1/p2.sh
```

第２層 : /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})

# チェック日時＝現在日時−１時間
HHMMSS=10000
DT_CHECK=$((${DT} - ${HHMMSS}))

# チェック日時 <= 前回実施日時 の時、他コンテナで実施済みと見なして終了
if [ ${DT_LOWER} -le ${DT_EXEC} ] ; then
  echo "skip"
  exit 0
fi

# 本体処理
/job/job1/main.php
```

第３層 : /job/job1/main.php

```
本体処理
```