パパエンジニアのポエム

奥さんと娘ちゃんへの愛が止まらない

GCEでRedisのレプリケーション組んでみた

GCE(Container-Optimized OS)を2台使って、Redisのレプリケーションを組んでみる。

Redisにおけるレプリケーションとは

RedisのレプリケーションはMaster・Slave型のレプリケーションモデル。
とある本番Redisサーバー(Master)のデータを、別のRedisサーバー(Slave)に完全にコピーする仕組みのこと。 f:id:yuki-toida:20181114142827p:plain

master/slave 各設定ファイル

master.conf

# ログレベル
loglevel debug

# ログファイル
logfile redis.log

# http://redis-documentasion-japanese.readthedocs.io/ja/latest/topics/persistence.html
save 900 1
save 300 10
save 60 10000

# AOF
appendonly no

# save file name
dbfilename dump.rdb

slave.conf

# スレーブ設定
slaveof redis-master 6379

# ログレベル
loglevel debug

# ログファイル
logfile redis.log

# http://redis-documentasion-japanese.readthedocs.io/ja/latest/topics/persistence.html
save 900 1
save 300 10
save 60 10000

# AOF
appendonly no

# save file name
dbfilename dump.rdb

ドキュメントを見ながら書けばおk。
slave.confに masterサーバーのホストを記述してあげる。

Dockerfile

何はともあれDockerfileを記述する。
ROLE引数でmaster, slaveを指定する形にした。

FROM redis:4-alpine
ARG ROLE
COPY ${ROLE}.conf /usr/local/etc/redis/redis.conf
CMD ["redis-server", "/usr/local/etc/redis/redis.conf"]

Cloud Build でビルドし、Container Registryにプッシュする

masterサーバーのコンテナをビルドし、プッシュする。
以下、master.yaml

steps:
- name: 'gcr.io/cloud-builders/docker'
  args:
  - 'build'
  - '--file=Dockerfile'
  - '--tag=asia.gcr.io/$PROJECT_ID/redis-master'
  - '--build-arg=ROLE=master'
  - '.'
images: ['asia.gcr.io/$PROJECT_ID/redis-master']
gcloud container builds submit --config=master.yaml .

slaveサーバーのコンテナをビルドし、プッシュする。
以下、slave.yaml

steps:
- name: 'gcr.io/cloud-builders/docker'
  args:
  - 'build'
  - '--file=Dockerfile'
  - '--tag=asia.gcr.io/$PROJECT_ID/redis-slave'
  - '--build-arg=ROLE=slave'
  - '.'
images: ['asia.gcr.io/$PROJECT_ID/redis-slave']
gcloud container builds submit --config=slave.yaml .

GCEのターミナルからdocker run

わざわざリモートでdocker runを行うことが正しいことかどうかはかなりあやしいけど、楽なのでやっちゃう。

masterサーバー起動

docker run --name redis-master -p 6379:6379 -v /home/redis/data:/data -d asia.gcr.io/${REGISTRY}/redis-master

slaveサーバー起動

docker run --name redis-slave -p 6379:6379 -v /home/redis/data:/data -d asia.gcr.io/${REGISTRY}/redis-slave

動作確認

各サーバーにリモートでログインして、データを確認するだけ。
masterサーバーで値を書き込んで、slaveサーバーにもコピーされてるか確認する。

docker exec -it redis-master /bin/sh
docker exec -it redis-slave /bin/sh

Golangでgrpc-gateway使ってみた

前回の記事gRPCを使ったので、今回はgrpc-gatewayを使ってみる。

grpc-gateway とは

RESTfulなJSON APIをgRPCに変換するリバースプロキシを生成してくれるprotocのプラグイン

https://camo.githubusercontent.com/e75a8b46b078a3c1df0ed9966a16c24add9ccb83/68747470733a2f2f646f63732e676f6f676c652e636f6d2f64726177696e67732f642f3132687034435071724e5046686174744c5f63496f4a707446766c41716d35774c513067677149356d6b43672f7075623f773d37343926683d333730

RESTfulなJSON APIのバックエンドでgRPCを使いたいという案件に最適。
ドキュメント見ながらミニマムで実装してみる。
Usage | grpc-gateway

登場人物

1つのgateway(リバースプロキシサーバ) に、2つのservice(gRPCサーバ)がぶら下がっている構成で組む。

gateway/main.go

  • ポート8080
  • RESTfulなJSON APIで受けて、gRPCで各サービスと通信を行うリバースプロキシサーバ

echo/main.go

  • ポート9090
  • gRPCで受けたパラメータをそのまま返すサーバ

auth/main.go

  • ポート9091
  • Authorizationヘッダが入ってないとエラーをレスポンス
  • gRPCで受けたパラメータをそのまま返すサーバ

.protoから.bp.go.bp.gw.goを生成する

.protoから、gRPCのインターフェースを定義した.bp.goを生成するコマンド。

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --go_out=plugins=grpc:. \
  proto/*.proto

.protoから、リバースプロキシとして動作するためのインターフェースを定義した.bp.gw.go生成

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --grpc-gateway_out=logtostderr=true:. \
  proto/*.proto

ちなみに、swaggerも吐き出せるので便利。

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --swagger_out=logtostderr=true:. \
  proto/*.proto

リバースプロキシサーバ実装

echoとauthの2つのgRPCサーバへのプロキシサーバを

grpc-ecosystem.github.io
ここを見ながらリクエストをパイプランするmatcherfilterも実装してみた、特に意味はない。

ポート9090、9091へプロキシ

package main

import (
    "flag"
    "fmt"
    "net/http"

    "github.com/golang/glog"
    "github.com/golang/protobuf/proto"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    "golang.org/x/net/context"
    "google.golang.org/grpc"

    gw "github.com/yuki-toida/grpc-gateway-sample/proto"
)

func main() {
    flag.Parse()
    defer glog.Flush()

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    mux := runtime.NewServeMux(runtime.WithIncomingHeaderMatcher(matcher), runtime.WithForwardResponseOption(filter))
    opts := []grpc.DialOption{grpc.WithInsecure()}

    if err := gw.RegisterEchoServiceHandlerFromEndpoint(ctx, mux, "localhost:9090", opts); err != nil {
        panic(err)
    }

    if err := gw.RegisterAuthServiceHandlerFromEndpoint(ctx, mux, "localhost:9091", opts); err != nil {
        panic(err)
    }

    if err := http.ListenAndServe(":8080", mux); err != nil {
        glog.Fatal(err)
    }

}

func matcher(headerName string) (string, bool) {
    ok := headerName != "Ignore"
    return headerName, ok
}

func filter(ctx context.Context, w http.ResponseWriter, resp proto.Message) error {
    w.Header().Set("X-Filter", "FilterValue")
    return nil
}

echo/main.go サーバ実装

何の変哲もないgRPCサーバ、ポート9090でリッスン

package main

import (
    "context"
    "net"

    pb "github.com/yuki-toida/grpc-gateway-sample/proto"
    "google.golang.org/grpc"
)

func main() {
    listener, err := net.Listen("tcp", ":9090")
    if err != nil {
        panic(err)
    }

    server := grpc.NewServer()
    pb.RegisterEchoServiceServer(server, &EchoServiceServer{})
    server.Serve(listener)
}

type EchoServiceServer struct{}

func (s *EchoServiceServer) Post(c context.Context, m *pb.Message) (*pb.Message, error) {
    return m, nil
}

func (s *EchoServiceServer) Get(c context.Context, p *pb.Param) (*pb.Param, error) {
    return p, nil
}

auth/main.go サーバ実装

grpc.UnaryInterceptor(authentication)でHTTPヘッダーにAuthorizationの有無を判定している
ポート9091でリッスン

package main

import (
    "context"
    "fmt"
    "net"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"

    pb "github.com/yuki-toida/grpc-gateway-sample/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/metadata"
)

func main() {
    listener, err := net.Listen("tcp", ":9091")
    if err != nil {
        panic(err)
    }

    opts := []grpc.ServerOption{grpc.UnaryInterceptor(authentication)}
    server := grpc.NewServer(opts...)

    pb.RegisterAuthServiceServer(server, &AuthServiceServer{})
    server.Serve(listener)
}

func authentication(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Error(codes.Unauthenticated, "not found metadata")
    }
    values := md["authorization"]
    if len(values) == 0 {
        return nil, status.Error(codes.Unauthenticated, "not found metadata")
    }
    return handler(ctx, req)
}

type AuthServiceServer struct{}

func (s *AuthServiceServer) Get(c context.Context, p *pb.Param) (*pb.Param, error) {
    return p, nil
}

動作確認

  • echoサーバ f:id:yuki-toida:20181113150712p:plain

  • authサーバ (authorizationヘッダ無) f:id:yuki-toida:20181113150845p:plain

  • authサーバ (authorizationヘッダ有) f:id:yuki-toida:20181113151100p:plain

動いてそうだ、これを実際にプロダクションで使うとなるとモノリスからマイクロサービスになるだろうし、.protoファイルどこに置くとか、サービス間の通信部分の実装どこに書くとか、色々複雑になってきそうだけど、やりきれたら楽しそうな気配がある。
GitHub - yuki-toida/grpc-gateway-sample

GolangでgRPC使ってみた

ほとんど公式のQuickStartのまんまだけど、一応備忘録

gRPCとは

https://grpc.io/img/landing-2.svg

gRPC は、Protocol Buffers を使ってデータをシリアライズし、高速な通信を実現できるRPCフレームワークGoogle謹製)。

セットアップ

  • Install gRPC
go get -u google.golang.org/grpc
  • Install protobuf for Mac
brew install protobuf
  • Install protocol buffers lib for Golang
go get -u github.com/golang/protobuf/protoc-gen-go

.protoファイルを実装する

ドキュメントを参考にインターフェースを定義する。

何も考えずにミニマムでメソッドを作ってみた。

syntax = "proto3";

package proto;

service Test {
  rpc Get (Request) returns (Response);
}

message Request {
  string Message = 1;
}

message Response {
  string Message = 1;
}

.protoファイルから.goファイルを生成する

protocコマンドを使って.protoファイルから.goファイルを生成する。

protoc -I proto/ proto/*.proto --go_out=plugins=grpc:proto

goのインターフェースが定義されたtest.pb.goファイルが作成されているはず。

サーバーサイドプログラミング

何も考えずにtest.pb.goファイルに定義されているインターフェースを実装し、

ポート50051でリッスンする。

package main

import (
    "context"
    "net"

    pb "github.com/yuki-toida/grpc-sample/proto"
    "google.golang.org/grpc"
)

func main() {
    listener, err := net.Listen("tcp", ":50051")
    if err != nil {
        panic(err)
    }
    server := grpc.NewServer()
    pb.RegisterTestServer(server, &Server{})
    server.Serve(listener)
}

type Server struct{}

func (s *Server) Get(c context.Context, r *pb.Request) (*pb.Response, error) {
    return &pb.Response{Message: r.Message}, nil
}

クライアントサイドプログラミング

ポート50051でリッスンされているサーバーに接続するクライアントを実装する。

この例では、コマンドラインパラメータを受け取りそのまま標準出力される。

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    pb "github.com/yuki-toida/grpc-sample/proto"
    "google.golang.org/grpc"
)

func main() {
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    message := "Hello"
    if 1 < len(os.Args) {
        message = os.Args[1]
    }

    client := pb.NewTestClient(conn)
    res, err := client.Get(ctx, &pb.Request{Message: message})
    if err != nil {
        panic(err)
    }
    fmt.Println(res)
}

大規模開発におけるDDD時、gRPCのインターフェースをどの層に依存させるか悩みそう。

素直にユーザーインターフェース層かな、そうするとすごく膨れ上がりそうな気配がする。

GitHub - yuki-toida/grpc-sample

Golangでマージソートの問題を解いてみた

前回同様、AOJの問題を解いてみます。 yuki-toida.hatenablog.com

マージソートの問題を解いてみた

問題

https://onlinejudge.u-aizu.ac.jp/courses/lesson/1/ALDS1/5/ALDS1_5_B

自分が書いたコードはこちら

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

var cnt int

// マージソート
func main() {
    sc := bufio.NewScanner(os.Stdin)
    sc.Split(bufio.ScanWords)
    sc.Scan()
    n, _ := strconv.Atoi(sc.Text())
    A := make([]int, n)
    for i := 0; i < n; i++ {
        sc.Scan()
        A[i], _ = strconv.Atoi(sc.Text())
    }

    sorted := mergeSort(A)
    fmt.Println(strings.Trim(fmt.Sprint(sorted), "[]"))
    fmt.Println(cnt)
}

// Runs mergeSort algorithm on a slice single
func mergeSort(slice []int) []int {
    if len(slice) <= 1 {
        return slice
    }
    mid := len(slice) / 2
    left := mergeSort(slice[:mid])
    right := mergeSort(slice[mid:])
    return merge(left, right)
}

// Merges left and right slice into newly created slice
func merge(left, right []int) []int {
    size, k, j := len(left)+len(right), 0, 0
    tmp := make([]int, size)

    for i := 0; i < size; i++ {
        if len(left) <= j {
            tmp[i] = right[k]
            k++
        } else if len(right) <= k {
            tmp[i] = left[j]
            j++
        } else if right[k] < left[j] {
            tmp[i] = right[k]
            k++
        } else {
            tmp[i] = left[j]
            j++
        }
        cnt++
    }
    return tmp
}

AOJからのフィードバック

f:id:yuki-toida:20180911185829p:plain

これで、まだまだ基本のアルゴリズムっていうのがなかなかハードだなーという印象。

ここまでの間にGolangの基本的な構文や実装方法は結構理解できてきたからもういいかなー。

Golangで挿入ソートの問題を解いてみた

前回同様、AOJの問題を解いてみます。 yuki-toida.hatenablog.com

挿入ソートの問題を解いてみた

問題

https://onlinejudge.u-aizu.ac.jp/courses/lesson/1/ALDS1/1/ALDS1_1_A

自分が書いたコードはこちら

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

// 挿入ソート
func main() {
    sc := bufio.NewScanner(os.Stdin)
    sc.Split(bufio.ScanWords)
    n := nextInt(sc)
    ary := make([]int, n)
    for i := 0; i < n; i++ {
        ary[i] = nextInt(sc)
    }
    print(ary)
    insertionSort(ary, n)
}

func nextInt(sc *bufio.Scanner) int {
    sc.Scan()
    num, _ := strconv.Atoi(sc.Text())
    return num
}

func print(ary []int) {
    fmt.Printf("%v\n", strings.Trim(fmt.Sprint(ary), "[]"))
}

func insertionSort(ary []int, n int) {
    for i := 1; i < n; i++ {
        v := ary[i]
        j := i - 1
        for 0 <= j && v < ary[j] {
            ary[j+1] = ary[j]
            j--
        }
        ary[j+1] = v
        print(ary)
    }
}

AOJからのフィードバック

f:id:yuki-toida:20180911183414p:plain

難しくなってきたー。

挿入ソートは離れた要素を直接交換することなく、取り出した値vより大きい要素のみを後方に移動するので、安定ソートアルゴリズムです。

挿入ソートの計算量を考える

最悪の場合、各iループがi回発生するので(N2 - N) / 2 となるので、O(N2) となりそうです。

ただデータの並びが、昇順に並んでいると移動の必要が無くN回のみなので、O(N)で済みそうですね。

Golangで文字列変換の問題を解く

前回同様、AOJの問題を解いてみます。 yuki-toida.hatenablog.com

文字列変換の問題をやってみた

問題

https://onlinejudge.u-aizu.ac.jp/courses/lesson/2/ITP1/9/ITP1_9_D

自分が書いたコードはこちら

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

// 文字列変換
func main() {
    sc := bufio.NewScanner(os.Stdin)
    sc.Scan()
    str := sc.Text()
    sc.Scan()
    q, _ := strconv.Atoi(sc.Text())
    for i := 0; i < q; i++ {
        sc.Scan()
        ary := strings.Split(sc.Text(), " ")
        a, _ := strconv.Atoi(ary[1])
        b, _ := strconv.Atoi(ary[2])
        switch ary[0] {
        case "replace":
            str = str[:a] + ary[3] + str[b+1:]
        case "reverse":
            str = str[:a] + reverse(str[a:b+1]) + str[b+1:]
        case "print":
            fmt.Printf("%v\n", str[a:b+1])
        }
    }
}

func reverse(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}

AOJからのフィードバック

f:id:yuki-toida:20180911175351p:plain

Golangのスライスの部分取得が便利だったので抜粋。

str[:]       // 0 から len(str)-1 まで
str[a:]     // a から len(str)-1 まで
str[a:b]   // a から b-1 まで
str[:a]     // 0 から aまで

これはとても分かりやすく良いですね、使っていこう。

Golangでプロコンの問題を解く 行列編

前回同様、AOJの問題を解いてみます。 yuki-toida.hatenablog.com

行列の問題をやってみた

問題

https://onlinejudge.u-aizu.ac.jp/courses/lesson/2/ITP1/7/ITP1_7_D

自分が書いたコードはこちら

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

// 行列の積
func main() {
    sc := bufio.NewScanner(os.Stdin)
    sc.Scan()
    nml := strings.Split(sc.Text(), " ")
    n, _ := strconv.Atoi(nml[0])
    m, _ := strconv.Atoi(nml[1])
    l, _ := strconv.Atoi(nml[2])
    A := make([][]string, n)
    B := make([][]string, m)
    for i := 0; i < n; i++ {
        sc.Scan()
        A[i] = make([]string, m)
        for j, v := range strings.Split(sc.Text(), " ") {
            A[i][j] = v
        }
    }
    for i := 0; i < m; i++ {
        sc.Scan()
        B[i] = make([]string, l)
        for j, v := range strings.Split(sc.Text(), " ") {
            B[i][j] = v
        }
    }

    C := make([][]int64, n)
    for i := 0; i < n; i++ {
        C[i] = make([]int64, l)
        for j := 0; j < l; j++ {
            for k := 0; k < m; k++ {
                v1 := parseInt64(A[i][k])
                v2 := parseInt64(B[k][j])
                C[i][j] += v1 * v2
            }
        }
    }

    for _, x := range C {
        fmt.Printf("%v\n", strings.Trim(fmt.Sprint(x), "[]"))
    }
}

func parseInt64(s string) int64 {
    v, _ := strconv.ParseInt(s, 10, 64)
    return v
}

AOJからのフィードバック

f:id:yuki-toida:20180911172847p:plain

最後の配列を出力する際に、Cとxの入れ子にするのではなく、fmtパッケージ使うと少し実行時間とメモリの省力化に繋がりました。

ただ[]の見た目がかなりイケてないのでプロダクションで使うなら util パッケージに押し込めるのがいいかもですね。

   for _, x := range C {
        fmt.Printf("%v\n", strings.Trim(fmt.Sprint(x), "[]"))
    }