GCEでRedisのレプリケーション組んでみた
- Redisにおけるレプリケーションとは
- master/slave 各設定ファイル
- Dockerfile
- Cloud Build でビルドし、Container Registryにプッシュする
- GCEのターミナルからdocker run
- 動作確認
GCE(Container-Optimized OS)を2台使って、Redisのレプリケーションを組んでみる。
Redisにおけるレプリケーションとは
RedisのレプリケーションはMaster・Slave型のレプリケーションモデル。
とある本番Redisサーバー(Master)のデータを、別のRedisサーバー(Slave)に完全にコピーする仕組みのこと。
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-gateway とは
- 登場人物
- .protoから.bp.goと.bp.gw.goを生成する
- リバースプロキシサーバ実装
- echo/main.go サーバ実装
- auth/main.go サーバ実装
- 動作確認
前回の記事でgRPC
を使ったので、今回はgrpc-gateway
を使ってみる。
grpc-gateway とは
RESTfulなJSON APIをgRPCに変換するリバースプロキシを生成してくれるprotocのプラグイン。
RESTfulなJSON APIのバックエンドでgRPCを使いたいという案件に最適。
ドキュメント見ながらミニマムで実装してみる。
Usage | grpc-gateway
登場人物
1つのgateway(リバースプロキシサーバ)
に、2つのservice(gRPCサーバ)
がぶら下がっている構成で組む。
gateway/main.go
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
ここを見ながらリクエストをパイプランするmatcher
とfilter
も実装してみた、特に意味はない。
ポート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サーバ
authサーバ (authorizationヘッダ無)
authサーバ (authorizationヘッダ有)
動いてそうだ、これを実際にプロダクションで使うとなるとモノリスからマイクロサービスになるだろうし、.protoファイル
どこに置くとか、サービス間の通信部分の実装どこに書くとか、色々複雑になってきそうだけど、やりきれたら楽しそうな気配がある。
GitHub - yuki-toida/grpc-gateway-sample
GolangでgRPC使ってみた
ほとんど公式のQuickStartのまんまだけど、一応備忘録
gRPCとは
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
のインターフェースをどの層に依存させるか悩みそう。
素直にユーザーインターフェース層かな、そうするとすごく膨れ上がりそうな気配がする。
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からのフィードバック
これで、まだまだ基本のアルゴリズムっていうのがなかなかハードだなーという印象。
ここまでの間に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からのフィードバック
難しくなってきたー。
挿入ソートは離れた要素を直接交換することなく、取り出した値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からのフィードバック
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からのフィードバック
最後の配列を出力する際に、Cとxの入れ子にするのではなく、fmtパッケージ使うと少し実行時間とメモリの省力化に繋がりました。
ただ[]
の見た目がかなりイケてないのでプロダクションで使うなら util
パッケージに押し込めるのがいいかもですね。
for _, x := range C { fmt.Printf("%v\n", strings.Trim(fmt.Sprint(x), "[]")) }