パパエンジニアのポエム

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

Cloud Storage FUSEを使ってGCSをGCEにマウントする

Cloud Storage FUSE とは

GCSを、Linux または OS X システム上でファイル システムとしてマウントするためのGoogle謹製OSS

Cloud Storage FUSE  |  Cloud Storage  |  Google Cloud

インストール

gcsfuse/installing.md at master · GoogleCloudPlatform/gcsfuse · GitHub

export GCSFUSE_REPO=gcsfuse-`lsb_release -c -s`
echo "deb http://packages.cloud.google.com/apt $GCSFUSE_REPO main" | sudo tee /etc/apt/sources.list.d/gcsfuse.list
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
sudo apt-get update
sudo apt-get install gcsfuse

マウントする

GCSのバケット名を bucket-vod とするとし、マウント元を/home/bucket-vodとすると、

sudo mkdir /home/bucket-vod
sudo gcsfuse -o nonempty bucket-vod /home/bucket-vod

これで、/home/bucket-vodbucket-vod にマウントされる。

GCE起動時にgcsfuseを実行する

GCEのカスタムメタデータstartup-script に同じコマンドをを仕込む。
これで、サーバー再起動等行っても正常にマウントされる。

sudo gcsfuse -o nonempty bucket-vod /home/bucket-vod

GCE で Wowza Streaming Server をつくる② GPUアクセラレーション編

前回の続き。
この記事ではGPUをGCEで使えるようにする設定を行う。

NVIDIA Driver をダウンロードする

Nvidiaの公式サイトから下記画像の沿ってドロップダウンを選択しダウンロードする。

  • Tesla
  • K-Series
  • Tesla K80
  • Linux 64-bit

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

GCEでNVIDIA Driver を使うためのセットアップ

Set up NVIDIA NVENC accelerated encoding on Debian

この辺を参考にしつつ、ウルトラググりながら依存ライブラリをインストールする。

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install build-essential
sudo apt-get install linux-headers-$(uname -r)
sudo apt-get install dkms

blacklist.confblacklist nouveauを追加する。 vimで編集します。

sudo vim /etc/modprobe.d/blacklist.conf

ここで忘れずにGCE再起動。

NVIDIA Driver をGCEにアップロードしインストールする

GCEインスタンス名をwowzaとし、ドライバをNVIDIA-Linux-x86_64-396.26.runとすると、
下記SCPコマンドでアップロード可能。

gcloud compute scp ./NVIDIA-Linux-x86_64-396.26.run wowza:~/

GCEターミナル内でドライバを実行権限をつけてインストールする。

chmod +x NVIDIA-Linux-x86_64-396.26.run
sudo ./NVIDIA-Linux-x86_64-396.26.run

動作確認

nvidia-smi コマンドでGPUの使用状況が取得できれば正常にインストール完了。
一筋縄ではいかなかったので、ググラビリティが試されるかも。

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

GCE で Wowza Streaming Server をつくる① GCE起動編

Wowza Streaming Engine(WSE)のライセンス等は既に取得済みとして進める。
この記事では、WSEのOSイメージを作成し、GCEのイメージとして登録し、
ファイアーウォールのルールを設定し疎通できるようにし、GPUアクセラレーション前提でGCEを作成する。

WSEのOSイメージを作成する

GCPでホストされているWSEのバージョン4.7.6を使用する。
OSイメージ名はwowza-476とする。

PROJECT_ID={個別入力}
IMAGE=wowza-476
IMAGE_URI="http://storage.googleapis.com/wowzamediasystems/wse/WowzaStreamingEngine-4.7.6-byol-20180809.image.tar.gz"

gcloud compute images create $IMAGE --project=$PROJECT_ID --source-uri=$IMAGE_URI

これでGCEのOSイメージとしてwowza-476が使えるようになる。

ファイアーウォールのルールを作成する

WSEのRTMPプロトコルとWSE Manager(管理画面)のAPIアクセスを有効にするために、ファイアウォールルールを作成する。
tcp:554,tcp:1935,tcp:8084-8089を通すために追加する。

PROJECT_ID={個別入力}
RULE=wowza
TCP_STREAMING="tcp:554,tcp:1935,tcp:8084-8089"

gcloud compute firewall-rules create $RULE --allow=$TCP_STREAMING --project=$PROJECT_ID

GCE作成

GPUアクセラレーションを行いたいので、GPUタイプのマシンを選択する。
東京リージョンでは使えないので、台湾リージョンのものを使う。

  • リージョン
    • asia-east1
  • マシンタイプ
    • n1-standard-8
  • マシンタイプGPU
  • カスタムイメージ
    • wowza-476
  • カスタムメタデータ
    • WZA_serverLicenseKey : {ライセンススキー}
    • WZA_managerUsername : {管理画面ログイン名}
    • WZA_managerPassword : {管理画面ログインパス}
  • ネットワークタグ
    • wowza

これで、GCEでWowzaStreamingEngine サーバーを構築完了。

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