2019-11-11

Go Conference 2019 Autumnに行ってきました。

biosugar0

Go Conference 2019 Autumnに行ってきたので興味を惹かれたいくつかの発表をまとめます。

API scenario testing tool with plugin package

メルペイ @zoncoen 資料 

webアプリケーションのE2Eテストを行うツールScenarigoを開発した。

どんなツールか?

  • YAMLでシナリオを書ける
  • シナリオの使いまわしができる
  • Goで拡張できる
  • HTML,gRPCが使える

なぜ作ったか?

Postmanの不満

  • 基本的にGUIアプリケーションを使わないといけない。 いつものVimが使いたい
  • シナリオは複雑なJSON定義。プルリクが来たときに長いJSONつらい。レビューしづらい。
  • 汎用的な処理をいい感じに使い回せない。CDNから取ってきてテキストとしてグローバル変数に入れて実行🤔
  • gRPCを直接使えない

GOAL

  • YAML でシナリオが定義できる
    • JSON より手で読み書きしやすい
    • 直接コードを書くより記述量を少なくできる
  • シナリオの使いまわしができる
    • e.g. ユーザーのログイン
  • Go のコードで拡張ができる
    • e.g. ある特定のルールでのID生成
  • gRPC が使える

Scenarigo

シナリオをYAMLで

title: echo-service
steps:
- title: POST /echo
  protocol: http
  request:
    method: POST
    url: '{{env.ECHO_ADDR}}/echo'
    body:
      message: hello
  expect:
    body:
      message: '{{request.message}}'

CLIから実行

$ ECHO_ADDR=http://localhost:8080 scenarigo run ./testdata/
=== RUN   testdata/scenarios/test.yaml
=== RUN   testdata/scenarios/test.yaml/echo-service
=== PAUSE testdata/scenarios/test.yaml/echo-service
=== CONT  testdata/scenarios/test.yaml/echo-service
=== RUN   testdata/scenarios/test.yaml/echo-service/POST_/echo
--- PASS: testdata/scenarios/test.yaml (0.00s)
    --- PASS: testdata/scenarios/test.yaml/echo-service (0.05s)
        --- PASS: testdata/scenarios/test.yaml/echo-service/POST_/echo (0.05s)

Goのパッケージになってるのでテストのファイルに書いて実行できる。 > これは便利そう

package main

import (
    "testing"

    "github.com/zoncoen/scenarigo"
    "github.com/zoncoen/scenarigo/context"
)

func TestEchoService(t *testing.T) {
    r, err := scenarigo.NewRunner(
        scenarigo.WithScenarios("testdata/scenarios"),
    )
    if err != nil {
        t.Fatalf("failed to create a test runner: %s", err)
    }
    r.Run(context.FromT(t))
}

他のシナリオYAMLファイルをinclude して使い回せる。

title: echo-service
steps:
- title: login
  include: './login.yaml' #login処理の使いまわし
  bind:
    vars:
      userToken: '{{vars.userToken}}' # user tokenを変数に受け取るイメージ
- title: POST /echo
  protocol: http
  request:
    method: POST
    url: '{{env.ECHO_SERVICE_ADDR}}/echo'
    header:
      Authorization: 'Bearer {{vars.userToken}}' # 変数に入れたuser token が使える
    body:
      message: hello
  expect:
    body:
      message: '{{request.message}}'

Goで拡張

title: echo-service
plugins:
  gen: gen.so
  echo: echo.so 
steps:
- title: say
  protocol: grpc < Use gRPC
  request:
    client: '{{plugins.echo.NewClient()}}'
    method: Say
    body:
      id: '{{plugins.gen.UUID()}}' < Call Go function
      message: hello
  expect:
    body:
      message: '{{request.message}}'

UUIDをランダム生成する処理をGoで書いておいてYAMLから呼び出せたりする

Plugin

Go拡張機能のために標準パッケージのPluginパッケージを使った

Goでプラグイン機能を実装するのにどういう方法があるのか?

  1. ソフトウェア本体とプラグインをまとめてビルドする
    • 単純だが、ユーザーにGoの開発環境を要求する
  2. 別のプロセスとしてプラグインを起動し通信して動作させる
    • プロセスが別なので本体が影響を受けにくく、Go以外の言語も使えるが、複雑で通信によるオーバーヘッドがある
  3. 動的ライブラリとして実行時に動的にロードする
    • 本体の再ビルドが必要なく、Goの関数がそのまま使えるが、現状Linux,MacOSでしか使えない

-> 今回は3の手法

pluginの使い方

mainのパッケージでコードを書くだけ。 Exported されてる関数と変数を使う側で参照できる

package main

var Variable = "variable"

func Function() string {
    return "function"
}

ビルド時にオプションを入れてビルド

$ go build -buildmode=plugin -o gen/plugin.so src/plugin.go

参照側ではバイナリファイルを指定してOpenし、関数名などをLookupして型アサーションして使う。

package main

import (
    "fmt"
    "plugin"
)

func main() {
    p, err := plugin.Open("gen/plugin.so")
    if err != nil {
        panic(err)
    }
    symbol, err := p.Lookup("Function")
    if err != nil {
        panic(err)
    }
    f := symbol.(func() string)
    fmt.Println(f())
}

なぜpluginパッケージを使ったか?

  • E2EテストでGo拡張したいときは汎用的な機能をプラグインにすることは少なそう
  • サーバーの仕様に密結合なものを入れたいのが多そう
  • Goのコードとしてそのまま触れて、reflect で型情報をとって動的に処理を変えられる
  • 使ってみたかった

How to develop "Container/Kubernetes Ready" Go web Application?

Abema TV @tomiokasyogo 資料

コンテナ

コンテナはホストOS上の独立した実行環境で、 その実態はコンテナランタイムによってホストOSのリソースを隔離、制限されたプロセス

コンテナ関連のプロダクトにはGo言語が多く使われている。 Docker, k8s, Istio...

Goがコンテナと相性がいい理由

  • 依存関係を1つのバイナリにできる
  • シングルバイナリで動作可能
  • システムコールをかんたんに扱える

など

Dockerを使ってコンテナ化するにはDockerfileを記述する

FROM golang:1.13.1-alpine as builder #Baseになるイメージの指定
RUN apk add --nocache ca-certificates git
ENV PROJECT /github.com/tommy-sho/app/app
WORKDIR /go/src/$PROJECT

ENV GO111MODULE on
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /go/bin/app . #ビルドの実行
ENTRYPOINT ["/go/bin/app"]  #アプリケーションの実行

参考 至高のDockerイメージ生成を求めて -2019年版-

Kubernetes

  • コンテナ環境のオーケストレーションツール
  • コンテナのデプロイ、メンテナンス、スケーリングをマネジメントしてくれる
  • Linux FoundationのサブプロジェクトであるCNCFにホストされている

k8sを使って開発する際の注意点

Graceful Start & Shutdown

Podのシャットダウンサイクル

  1. PreStop hookが設定されている場合、最初に実行される
  2. SIGTERMがコンテナに送られる(複数の場合、順不同)
  3. ServiceやReplicaSetのPodリストから削除
  4. Grace periodを超えた場合、SIGKILLが送られる

    Grace period の初期値は30秒(変更可)

SIGTERMをハンドリングしてサーバーの終了処理を行う
// サーバーの起動
go func(){
    logger.Info("server start serving")
    if err := server.Start(":8080"); err != http.ErrServerClosed {
        logger.Fatal("Server closed with err:",zap.Error(err))
    }
}()

//シグナルを受け取るチャネルを作成
stopChan := make(chan os.Signal,1)
signal.Notify(stopChan,
os.Interrupt,
syscall.SIGTERM,
syscall.SIGINT,
)

//シグナルを受け取るまでブロック
<-stopChan
// shutdown処理
Podの削除がPodのServiceからの削除より早いと、該当Podへのリクエストが失敗する

1.PreStop hookが設定されている場合、最初に実行される 2.SIGTERMがコンテナに送られる(複数の場合、順不同)

ここまでの処理と、

3.ServiceやReplicaSetのPodリストから削除

の処理は非同期なため、Podの削除がPodのServiceからの削除より早いと、該当Podへのリクエストが失敗する可能性がある。 上記の問題を解決するために、  preStopHook を利用する。

  • preStop hook でsleepさせる
  • sleepしている間に、シャットダウン時に来ているリクエストとPodリストからの削除を完了させる
containers:
    - name: app
      image: app:v1
      ports:
          - containerPort: 8080
      lifecycle:
          preStop:
              exec:
                  command: ["sleep","10"]
health check

まずはトラフィックが受けいれられる状態かをチェックするReadinessProbeをおすすめ

  • DBへの疎通確認やキャッシュのリロードもhealth check のエンドポイントで確認
  • すぐにサーバが応答できる常態かをチェックできる
コンテナを使うと各環境(dev,prod) で同じアプリケーションを動作させられる

通常DB接続先などが異なるので、設定はコードと分離する必要がある -> 環境変数として格納する

ただし、アプリケーション内部の設定は含まない (serverのkeep aliveなど)

環境変数を扱うライブラリ

json tagのように env:"HOME"アノテーションしておくと、 env.Parse(&cfg)で環境変数を構造体へマッピングしてくれる

ロギング
  • コンテナはステートレスなものなので、ログはデータではなくイベントストリームとして標準出力に出す
  • Fluentdなどのログコレクターがそれらのログを回収する
  • ログはJSONなどに構造化しておくと検索しやすくオススメ
  • 重要度レベルをつける(INFO,DENGERなど)

ログのオススメライブラリuber-go/zap

モダンな開発環境で注意すべき点のまとめ

herokuの開発者、設立者のAdam Wigginsによって提唱 Twelve-Factor App

  • 依存関係の明示化
  • 環境ごとの設定の切り替えに環境変数を使う
  • ログのイベントストリーム化
  • ビルド、リリースの分離

など12の原則が述べられている

さらに、クラウドネイティブアプリ向けに追記された Beyond The Twelve-Factor App もある

Goで超高速な経路探索エンジンを作る

DeNA @avvmoto 資料

Goでダイクストラ法を高速に実装する

ダイクストラ法

  1. 起点の最小距離を0,ほかのノードの値を未定義(∞) に設定
  2. 未確定ノードのうち、最小値のコストのノードを見つけ(ここを高速化)、確定ノードとする。
  3. 2で確定ノードとなったノードjから伸びているノードを(グラフライブラリー高速化)、未確定ノードに入れる。もし必要があれば、暫定的なコストを更新する。
  4. すべてのノードが確定ノードになっていなければ、2に戻る。

高速化に必要なGoのベストプラクティス

Slice1

lengthやcapacityを指定してsliceを作成する

s1 := make([]int,length) // capacity = length
s2 := make([]int,length,capacity)

事前に必要な長さの目安をcapacity に明示しよう (appendを使えば、それ以上に要素を追加してもOK)

Slice2

メモリアロケートの回数は少なくしよう

s1 := make([]*BigStruct,0,1e5)

構造体のポインタへのスライスだと、ポインタしかアロケートされていない。

s1 := make([]*BigStruct,0,1e5)
for i := 0; i < 1e5; i++{
    s1 = append(s1,&BigStruct{}) //  構造体は、都度allocateされている
}

構造体のスライスとして、一回でまるっとアロケートしておこう

s2 := make([]BigStruct,0,1e5)

slice3

lengthを延長しアロケート済みの領域を使おう

例:構造体のスライスに要素を1つ追加する

    s := make([]BigStruct,0,c)
    for i := 0; i < E; i++{
        if len(s) + 1 < cap(s){
            s = s[:len(s) + 1 ] // sliceの延長
            InitBigStruct(&s[len(s)])
        } else if cap(s) < len(s) +1{
            s = append(s,NewBigStruct()) // capacityが足りなくなったら、appendで規定配列を拡張
        }
    }

map

  • goのmapは内部的に Bucketの配列となる
  • 1つのBucketには、8つのキー・値ペアを保存
  • Bucketの個数は常に2の累乗
  • マップのキーはハッシュ値に変換され、ハッシュ値の下位ビットがBucketの選択に用いられる

  • Bucketの構成
    • ハッシュ値の上位ビットの配列
    • キー・値ペアを格納するバイトの配列
  • 下位ビットが衝突して、同じBucketに9個以上入れる場合は? *  Overflow Bucketが作成され、各Bucketから参照される
  • mapにkey/valueの追加を繰り返すと、 Overflow Bucketが増えてゆき、性能が下がる
  • 閾値を超えるとmapの拡張が走る
    • 既存Bucketの2倍のBucketの配列がアロケートされる
    • key/valueが追加、または削除のタイミングで、古いBucket配列から新しいBucket配列へ退避してゆく

ベストプラクティス

  • 事前にmapの目安がわかるなら、 capacity hintを指定してmapを作ることで mapの拡張が最小限に抑えられる
make(map[string]int,100)
  • スライス同様、初期のcapacityを超えて要素を追加することができる
  • 指定したほうが追加が早い

身も蓋もないことを言うとMapよりSliceのほうが早いので、速度を求めるなら極力Mapを避けて実装する。

mapにもcapacityがあるのは知らなかった。

Welcome to Linter

DeNA @theoden9014 資料

Linterとは?

コンパイラで検出できないエラーや構文を静的解析によって検査するツールのこと

Linterを使う利点

  • 一定のコード品質を機械的に担保
  • コードレビューする際の初期品質が向上

GoにおけるLinter

  • golang.org/x/tools/go/analysys パッケージによってフレームワーク化されている
  • analysys.Analyzerが1モジュールとなっている
  • go tool vet も複数のanalysys.Analyzerを利用して実装している
  • Analyzerの中で依存関係をもたせ、得られた結果を他のAnalyzerで利用することが可能になっている

カスタムLinterの開発手順

  1. analysys.Analyzerを実装
  2.  alanysistest を使ってanalysys.Analyzerをテストする
  3. コマンド化
var Analyzer = &analysys.Analyzer{
    Name: "sample",                                   // Analyzerの名前
    Doc: "this is sample"                             // 説明
    Requires: []*analysys.Analyzer{inspect.Analyzer}, // 依存するAnalyzerのリスト
    Run: run,                                         // 実際の解析処理をここに記述する(package単位で実行される)
    FactTypes: nil,                                   // このAnalyzer内で複数パッケージをまたいだデータ共有等を行いたい場合に利用する
    ResultType: nil,                                  // このAnalyzerが解析した結果を他のAnalyzerにも提供したい場合はここで型を宣言する
}

我々にはカスタムする需要はまだなさそう。

プロダクトにLinterを導入する

  • テストと同様に、開発フローの一部として継続的に回す必要がある
    • CIに組み込む
  • 開発初期段階から導入しておくべき
  • 開発途中で導入する際はルールを少なくして、徐々に増やしていくと良い
  1. CIを導入する
  2. 既存のLinterを導入する
  3. CIに組み込む
  4. ドメイン固有のルールが必要になったらLinterの開発を検討する

既存のLinter

  • go vet
    • コンパイラで検出できないエラーを検知
  • golint
    • コーディングスタイルの検査
  • golangci-lint
    • 様々なlinterのアグリゲーター

golangci-lint

一般的なルールはこれを使うといい。

これは入れてチェックしてみたい。

最新の記事