A Day In The Life

とあるプログラマの備忘録

Kubernetes上にRedis Sentinelを構築する

GCP環境でRedisを使う

GCP には Cloud Memorystore という Redis 互換のマネージドサービスがあるのですが、対応している Redis のバージョンが古いので自前で GKE 上に Redis を立てることにしました。どうせ立てるなら master/slave 構成のほうが良いだろうということで Redis Sentinel にしました。

環境

  • GKE 1.13.6
  • Redis 5

Redis Sentinelって何?

Redis Sentinel は master/slave 構成の Redis の監視を行ってくれるサーバソフトウェアです。自動フェイルオーバー機能を持っています。

Redisの設定

Redis の Config ファイルとコンテナ起動時に必要なスクリプトを作成します。

RedisのConfigファイルを作成する

Redis の master/slave 用の Config ファイルと Sentinel 用の Config ファイルを作成します。 はじめにマスター用の Config ファイル master.conf を作成します。

bind 0.0.0.0
port 6379

dir /redis-data

次にスレーブ用の Config ファイル slave.conf を作成します。

bind 0.0.0.0
port 6379

dir .

slaveof redis-0.redis 6379

最後に Sentinel 用の Config ファイル sentinel.conf を作成します。

bind 0.0.0.0
port 26379

sentinel monitor master redis-0.redis 6379 2
sentinel parallel-syncs master 1
sentinel down-after-milliseconds master 10000
sentinel failover-timeout master 20000

redis-0.redis をマスターに設定し sentinel でマスターを監視するように config に指定しています。 sentinel.confの設定内容については以下の記事が参考になります。

コンテナ起動時に実行するスクリプトを作成します

Redis コンテナ起動時に実行するスクリプト init.sh を作成します。どのコンテナをマスターにするか決定するためのものです。

#!/bin/bash
if [[ ${HOSTNAME} == 'redis-0' ]]; then
  redis-server /redis-config/master.conf
else
  redis-server /redis-config/slave.conf
fi

Sentinel 用のスクリプト sentinel.sh を作成します。マスターが立ち上がるのを待ってからredis-sentinelコマンドを実行します。

#!/bin/bash
while ! ping -c 1 redis-0.redis; do
    echo 'Waiting for server'
    sleep 1
done

redis-sentinel /redis-config/sentinel.conf

Redis 関連のファイルは以上で作成完了です。ファイル構成は以下のようになります。

|-- master.conf
|-- slave.conf
|-- sentinel.conf
|-- init.sh
`-- sentinel.sh

ConfigMapを作成する

RedisのConfigファイルとコンテナ起動時に使うスクリプトをまとめて redis-config という名前の ConfigMap を作成します。

$ kubectl create configmap \
> --from-file=slave.conf=./slave.conf \
> --from-file=master.conf=./master.conf \
> --from-file=sentinel.conf=./sentinel.conf \
> --from-file=init.sh=./init.sh \
> --from-file=sentinel.sh=./sentinel.sh \
> redis-config

Redisの Serviceの作成

k8s の構成ファイルを作成します。Redis にアクセスするためのサービスと Redis コンテナを定義するステーフルセット(StatefulSet)を使って構築します。はじめに Redis にアクセスするための service.yaml を作成します。

apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  ports:
  - port: 6379
    name: peer
  clusterIP: None
  selector:
    app: redis

次に Redis コンテナを定義する redis.yaml を作成します。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  selector:
    matchLabels:
      app: redis
  replicas: 3
  serviceName: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      initContainers:
      - image: busybox:latest
        name: redis-init
        command: [sh, -c, cp -L /tmp-config/* /redis-config]
        volumeMounts:
        - mountPath: /tmp-config
          name: tmp
        - mountPath: /redis-config
          name: config
      containers:
      - command: [sh, -c, source /redis-config/init.sh]
        image: redis:5-alpine
        name: redis
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - mountPath: /redis-config
          name: tmp
        - mountPath: /config
          name: config
        - mountPath: /redis-data
          name: data
      - command: [sh, -c, source /redis-config/sentinel.sh]
        image: redis:5-alpine
        name: sentinel
        volumeMounts:
        - mountPath: /redis-config
          name: config
      volumes:
      - configMap:
          name: redis-config
        name: tmp
      - emptyDir:
        name: config
      - emptyDir:
        name: data

デプロイメント(Deployment)ではなくステーフルセットを使うとポッドに ID が採番されポッド名-インデックスの形式でポッドにアクセスできます。replicas に3を指定しているので redis-0, redis-1, redis-2 という名前のポッド(Pod)が生成されます。

k8s 1.13以降はConfigMapが読み取り専用になったので注意が必要です

Redis Sentinel は Config ファイルにアクセスして内容を更新します。ConfigMap のままでは正常に動作しません。なのでボリュームに ConfigMap を読み込んでから書き込み可能な emptyDir なボリュームにファイルをコピーする必要があります。

# ファイルコピー部分の抜粋
initContainers:
- image: busybox:latest
  name: redis-init
  command: [sh, -c, cp -L /tmp-config/* /redis-config]
  volumeMounts:
  - mountPath: /tmp-config #読み取り専用
    name: tmp
  - mountPath: /redis-config #書き込み可能
    name: config

ConfigMap はボリューム作成時にシンボリックリンクとして作成されリンク先が読み取り専用に設定されます。なのでdefaultModeで書き込み権限を付与しても書き込みできるようにはなりません。

Redisのデプロイ

サービスをデプロイします。

$ kubectl apply -f service.yaml

ステートフルセットをデプロイします。

$ kubectl apply -f redis.yaml

確認

redis-2 にアクセスして IP アドレスが返ってくるか確認します。

$ kubectl exec redis-2 -c redis \
-- redis-cli -p 26379 sentinel get-master-addr-by-name master

10.0.9.6
6379

参考書籍

こちらの「入門 Kubernetes」の14章を参考にしました。

参考

ConfigMap の読み取り専用を回避する方法

sentinel.conf の設定

go-mode + eglotを使ってEmacsのGolang開発環境を整える

普段 Golang でサーバコードを書くときはもっぱら Emacs を使っています。Go Modules に移行してから gocode が動作しなくなったので最近はやりの LSP(Language Server Protocol) を導入することにしました。Go の Language Server は gopls 、Emacs のLSPクライアントは eglot を使います

動作確認バージョン

Emacsは25以上、Golangは1.11以上なら問題なく動作するはずです。

gopls のインストール

Go の Language Server は3種類あります。一番下の gopls が Golang の公式 Language Server です。

一長一短あるようですが公式が安心そうなので gopls をインストールします。

$ go get -u golang.org/x/tools/cmd/gopls 

goimportsのインストール

Go の import を整理してくれるコマンドもついでにインストールしておきます。

$ go get golang.org/x/tools/cmd/goimports

init.el の設定

Emacs の LSP クライアントは lsp-mode が有名ですが、動作がめちゃくちゃ重かったのと余計な表示がいっぱい出てきたのでシンプルな eglot を使うことにしました。入力補完は company-mode を使います。あと GOPATH や GOROOT など環境変数設定のために exec-path-from-shell を使います。

;; パッケージ管理サーバ
(require 'package)
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t)
(add-to-list 'package-archives '("marmalade" . "http://marmalade-repo.org/packages/"))
(package-initialize)
;; パッケージ情報の更新
;(package-refresh-contents)

;; インストールするパッケージのリスト
(defvar my/favorite-packages
  '(
    go-mode
    eglot
    company
    use-package
    exec-path-from-shell
    ))
;; my/favorite-packagesからインストールしていないパッケージをインストール
(dolist (package my/favorite-packages)
  (unless (package-installed-p package)
    (package-install package)))

;; シェルに設定されている環境変数を引き継ぐ
(exec-path-from-shell-initialize)

;; eglot
;; M-.で定義ジャンプ、M-,でジャンプ先からもどる
;; eglot はデフォルトの Language Server として go-langserver を使うので golsp に変更する
;; 事前に go get -u golang.org/x/tools/cmd/gopls しておくこと
(use-package eglot
  :config
  (define-key eglot-mode-map (kbd "M-.") 'xref-find-definitions)
  (define-key eglot-mode-map (kbd "M-,") 'pop-tag-mark)
  (add-to-list 'eglot-server-programs '(go-mode . ("gopls")))
  (add-hook 'go-mode-hook 'eglot-ensure))

;; company-mode
(use-package company
  :config
  (global-company-mode)
  (setq company-idle-delay 0)
  (setq company-minimum-prefix-length 1)
  (setq completion-ignore-case t)
  (setq company-dabbrev-downcase nil)
  (setq company-selection-wrap-around t))

;; go-mode
;; global-whitespace-modeを使うときはindent-tabs-modeをnilにすること、companyが誤作動する
;; 事前に go get golang.org/x/tools/cmd/goimports しておくこと
(let ((envs '("GOROOT" "GOPATH")))
  (exec-path-from-shell-copy-envs envs))
(use-package go-mode
  :commands go-mode
  :defer t
  :init
  (add-hook 'go-mode-hook (lambda()
                            (setq indent-tabs-mode nil)
                            (setq c-basic-offset 4)
                            (setq tab-width 4)
                            ))
  :config
  (setq gofmt-command "goimports")
  (add-hook 'before-save-hook 'gofmt-before-save))

emacs-lispのコードはこちらに置いてます。

参考

Python を使って地域メッシュコードから緯度経度ポリゴンを算出する

日本の地域メッシュコードから緯度経度ポリゴンを算出する Python3.7 のプログラムを作成しました。 前回の記事の逆バージョンです。

プログラム

3次メッシュコード(基準地域メッシュコード)から緯度経度ポリゴンを算出します。精度を出すためにミリ秒で計算しています。

# ミリ秒
MILLISECOND = 3600000

class Grid(object):
    def __init__(self, code, parent=None, divide=0):
        size = len(code) // 2
        # コードの上位桁
        self.upper = int(code[:size])
        # コードの下位桁
        self.lower = int(code[size:])
        self.height_ms = MILLISECOND * (40 / 60)
        self.width_ms = MILLISECOND
        self.parent = parent
        if parent:
            self.height_ms = parent.height_ms / divide
            self.width_ms = parent.width_ms / divide

    def origin(self):
        if self.parent:
            p = self.parent.origin()
            lat_ms = p[0] + (self.height_ms * self.upper)
            lon_ms = p[1] + (self.width_ms * self.lower)
        else:
            # 親が存在しない=1次メッシュ
            lat_ms = self.upper * MILLISECOND / 1.5
            lon_ms = (self.lower + 100) * MILLISECOND
        return lat_ms, lon_ms


class Code2Polygon(object):
    def __init__(self, code):
        first_code = code[0:4]
        second_code = code[4:6]
        third_code = code[6:8]
        # 1次メッシュ
        self.__first = Grid(first_code)
        # 2次メッシュ(1次メッシュを8分割)
        self.__second = Grid(second_code, self.__first, 8)
        # 3次メッシュ(2次メッシュを10分割)
        self.__third = Grid(third_code, self.__second, 10)

    def __to_polygon(self, s, w, n, e):
        return [s, w], [s, e], [n, e], [n, w], [s, w]

    def __to_bounds(self, origin, height_ms, width_ms):
        south = origin[0]
        west = origin[1]
        north = south + height_ms
        east = west + width_ms
        return south / MILLISECOND, west / MILLISECOND, north / MILLISECOND, east / MILLISECOND

    def third_bounds(self):
        origin = self.__third.origin()
        return self.__to_bounds(origin, self.__third.height_ms, self.__third.width_ms)

    def third(self):
        t = self.third_bounds()
        return self.__to_polygon(t[0], t[1], t[2], t[3])

3次メッシュのみ算出してます。

使い方

Code2Polygon クラスのコンストラクタに地域メッシュコードを渡してポリゴンを取得します。

def test_code2polygon():
    # 渋谷付近のメッシュコード
    c2p = Code2Polygon('53393595')
    print(c2p.third())

上記プログラムの出力結果は以下のようになります。

([35.65833333333333, 139.6875], [35.65833333333333, 139.7], [35.666666666666664, 139.7], [35.666666666666664, 139.6875], [35.65833333333333, 139.6875])

参考記事

Python を使って緯度経度から地域メッシュコードを算出する

緯度経度(latitude, longitude)から日本の地域メッシュコードを算出する Python3.7 のプログラムを作成しました。

プログラム

緯度経度から1-3次メッシュコード(基準地域メッシュコード)を算出します。精度を出すためにミリ秒で計算しています。

import math

# 計算の単位(ミリ秒)
MILLISECOND = 3600000

class FirstGrid(object):
    def __init__(self, lat, lon):
        '''
        1次メッシュコード計算
        '''
        self.lat = lat
        self.lon = lon
        # メッシュの高さ
        self.height_ms = MILLISECOND * (40 / 60)
        # メッシュの幅
        self.width_ms = MILLISECOND
        # メッシュコードの上位桁
        self.upper = int(math.floor(lat * 15 / 10))
        # メッシュコードの下位桁
        self.lower = int(math.floor(lon - 100))
        # 南端(緯度)
        self.south_ms = self.upper * MILLISECOND / 1.5
        # 西端(経度)
        self.west_ms = (self.lower + 100) * MILLISECOND

    def code(self):
        ''' メッシュコード '''
        return f'{self.upper}{self.lower}'

    def origin(self):
        ''' メッシュの南西端(緯度経度) '''
        return self.south_ms / MILLISECOND, self.west_ms / MILLISECOND

class Grid(FirstGrid):
    def __init__(self, parent, divide):
        '''
        2,3次メッシュコード計算
        :param parent 親メッシュ
        :param divide 分割単位
        '''
        self.lat = parent.lat
        self.lon = parent.lon
        lat_ms = parent.lat * MILLISECOND
        lon_ms = parent.lon * MILLISECOND
        # 親メッシュの高さと幅から当該メッシュの高さと幅を算出する
        self.height_ms = parent.height_ms / divide
        self.width_ms = parent.width_ms / divide
        h = self.height_ms
        w = self.width_ms
        # 上位桁
        self.upper = int(math.floor((lat_ms - parent.south_ms) / h))
        # 下位桁
        self.lower = int(math.floor((lon_ms - parent.west_ms) / w))
        # 南端
        self.south_ms = self.upper * h + parent.south_ms
        # 西端
        self.west_ms = self.lower * w + parent.west_ms

    def code(self):
        return f'{self.upper}{self.lower}'

    def origin(self):
        return self.south_ms / MILLISECOND, self.west_ms / MILLISECOND

class LatLon2Code(object):
    def __init__(self, lat, lon):
        f = FirstGrid(lat, lon)
        # 2次メッシュは1次メッシュを8分割する
        s = Grid(f, 8)
        self.__first = f
        self.__second = s
        # 3次メッシュは2次メッシュを10分割する
        self.__third = Grid(s, 10)

    def first(self):
        return self.__first.code()

    def second(self):
        return f'{self.__first.code()}{self.__second.code()}'

    def third(self):
        return f'{self.__first.code()}{self.__second.code()}{self.__third.code()}'

地域メッシュは英語だと Grid Square と表現するらしいのでクラス名は Grid にしています。1次メッシュの計算が特殊で2次3次は同じ計算をするので、1次と2次3次でクラスを分けました。

使い方

LatLon2Code クラスのコンストラクタに緯度経度を渡してコードを取得します。

def test_latlon2code():
    # 渋谷
    ll2c = LatLon2Code(35.6640352, 139.6982122)
    assert(ll2c.first() == '5339')
    assert(ll2c.second() == '533935')
    assert(ll2c.third() == '53393595')

参考記事

PostGISでgeometry型からgeography型に変換するときは座標系に注意する

SRID=4612(JGD2000測地系+地理座標系)のgeometry型フィールドだとgeography型に変換できる

postgres=> SELECT ST_SetSRID(geom, 4612)::geography FROM map WHERE id=1;
                     st_setsrid
----------------------------------------------------
 0101000020041200006B405577CB756140404749F6A1BF4140

SRID=3857(WGS84測地系球面+メルカトル図法)のgeometry型フィールドをgeography型に変換しようとするとエラーになる

postgres=> SELECT ST_SetSRID(geom, 3857)::geography FROM map WHERE id=1;
ERROR:  Only lon/lat coordinate systems are supported in geography.

EPSG: 3857は投影座標系で単位がメートルなのでダイレクトに変換できないみたいです。

参考

Golang書くときのちょっとしたテクニック

MarshalJSONを使ってJSONに表示用のフィールドを追加する

無限ループしないように元の構造体を拡張する

// UTCな時間をJsonに変換するタイミングでJSTに変換する例
import "time"

type Hoge struct {
    ID uint
    CreatedAt time.Time
}

func (h Hoge) MarshalJSON() ([]byte, error) {
    type Alias Hoge
    return json.Marshal(&struct {
        Alias
        CreatedAtJST time.Time
    }{
        Alias:     (Alias)(h),
        CreatedAtJST: h.CreatedAt.In(time.LoadLocation("Asia/Tokyo")),
    }
}

参考

Enum

数値系Enum

ゼロ値をUnknownにするのがGolang

type DeviceType uint
const (
    Unknown DeviceType = iota
    Android
    IOS
)

文字列系Enum

type Status string
const (
    Success Status = "success"
    Failure Status = "failure" 
)

関数にオプショナルな引数を設定したいとき

FunctionalOptionPatternを使って実装する

https://blog.web-apps.tech/go-functional-option-pattern/

リクエストボディの読み出しが1回しかできないときの対処方法

リクエストボディを ioutil.ReadAll 関数で読み出すと2回目以降は中身空っぽになります。それの回避方法です。

http - How to read response body twice in Golang middleware? - Stack Overflow

Gotestでいい感じのスタブを作る方法

gomockインストールするのはちょっとなぁって時に良いです。

Golangにおけるinterfaceをつかったテストで mock を書く技法 - haya14busa

今時のサーバエンジニアのためのオススメ本

2017年の11月から本格的にサーバサイドのお仕事をするようになりました。それで今までに読んだオススメの本をまとめました。

使いやすいAPIを開発するために

RESRfulなAPIを開発するための指針など。他社(Twitter, GitHub, Yahoo.com...etc)のAPIを比較しながら説明

サーバ側のアーキテクチャ構成

マイクロサービス

運用しつつアーキテクチャを進化させていく

運用的な話

SREの基礎やSRE全般の話題