爆速でGo!

GoGo!

gRPCは何故誕生したのか

Click here for English version

gRPCという言葉はすっかり有名になり、日本でも「社内でgRPCを導入した」「gRPCサーバーを構築する方法」といったような記事が溢れかえるようになりました。
これにより、gRPCの導入ハードルは下がり、比較的簡単にプロダクションレディなgRPCサーバーを作ることが出来るようになりました。
しかしそもそもどういった背景でgRPCが誕生したのでしょうか。

背景

gRPC Blogには、以下のように記載されています。

GoogleはstubbyというRPCインフラストラクチャ持っており、10年以上Googleのデータセンター内のマイクロサービスを接続してきた。
統一されたクロスプラットフォームのRPCインフラストラクチャを持つことで、全体の効率、セキュリティ、信頼性の改善の展開を可能にし、その期間の信じられないほどの成長を支えた。

stubbyには多くの素晴らしい機能があるが、社内のインフラに密接に結びつきすぎて一般公開には適していなかった。
SPDY, HTTP/2, QUICの登場により Stubbyをもっと汎用的に作り直して、モバイル、IoT、クラウド等のユースケースに適用させるべき時期が来たことが明らかになった。

また、Google Cloud Platform Blogには、以下のように記載されています。

高度にスケーラブルで疎結合のシステムを構築することは、常に困難だった。 モバイル機器やIoT機器の普及、データ量の急増、顧客の期待の高まりに伴い、インターネット規模で効率的かつ確実にシステムを開発して運用することが重要である。
この種の環境では、開発者は複数の言語、フレームワーク、テクノロジー、複数の第一および第三者サービスを扱うことがある。 これにより、サービス契約を定義して実施したり、認証や承認、ヘルスチェック、ロードバランシング、ロギングと監視、トレースなどの横断的な機能を全体にわたって一貫性を持たせることが難しくなる。 今日のクラウドネイティブの世界では、新しいサービスをすばやく追加する必要があり、各サービスの期待は柔軟で弾力性があり、可用性が高く、構成可能であることが特に課題となる。
過去15年間、Googleはこれらの問題を内部的にStubbyというRPCフレームワークで解決した。このRPCフレームワークは、数十億回のリクエストをインターネット規模で処理できるコアRPCレイヤーで構成されている。 今この技術は、gRPCと呼ばれるオープンソースプロジェクトの一環として、誰でも利用可能である。 これは、私たちがGoogleで楽しむのと同じスケーラビリティ、パフォーマンス、機能をコミュニティ全体に提供することを目的としている。

以上を踏まえ、誕生の経緯を雑にまとめるとこんな感じでしょうか

  • スケーラブルで疎結合なシステムを作るのは難しいけど、モバイルとか普及している現代はやらないといけない
  • GoogleではクロスプラットフォームのRPCフレームワーク使って対応していた
  • でもGoogle内部のインフラにめっちゃ依存してた
  • HTTP/2とか出てきて、Googleのインフラに依存せずとも標準規格で同等以上のものを作れるようになった
  • 標準規格ベースで作り変えて一般公開しよう!

参考

gRPCについて理解を深めたい場合は、以下から眺めていくと徐々に分かっていくと思います

間違い等ありましたら、コメントやtwitter等でご指摘頂けると嬉しいです。

ISUCON8予選は学生枠6位で本戦出場が決まりました

この記事はこちらに移動しました。

isucon.net

ISUCON8の予選に、メルカリで一緒だった@zaq1tomo@inatonixzin-gonicというチーム名で出場しました。
結果は学生枠6位だったので、やったこととかまとめます。

事前準備

  • 二度のisucon合宿を行った(どちらも環境構築で頓挫)
  • 何回か集まってisucon7の過去問練習
  • ishoconという株式会社scoutyさん主催のコンテストで練習した
  • hidakkathonという株式会社会社サイバーエージェントさん主催のコンテストで練習した
  • 前日に当日と同じ要領でhidakkathonの問題をみんなでやった

とまあ初出場ということもあり結構ちゃんと準備して、何も言い訳できない状況だったのでとりあえず通過できてよかったです

やったこと

チューニング対象は、イベントの座席予約アプリでした。
言語は、3人の技術スタック的にGo一択でした。

github.com

@nakabonne: インフラ担当
@zaq1tomo: アプリ担当
@inatonix: アプリ担当

序盤

@nakabonne

リバースプロキシは完全にnginxのつもりでいたので、h2oアクセスログをalpがパースできる形式で出力するのに結構時間かかってしまって、大迷惑かけてしまいました。
本戦では爆速でやりたいです。
ちなみに今回はnginxに差し替えてる方が結構いたので、インフラ担当としてその辺のトレードオフも見積もれるようになりたいです。

@inatonix, @zaq1tomo

方針決め

リソース

f:id:NakaWatch:20180917102529p:plain

弱者の戦い方として、無理に複数台構成はしないと最初から決めていたため、ゴリゴリインメモリキャッシュを使おうと話していました。
実行時にCPU使用率が100%に張り付くので、なるべくredis等の別プロセスは立ち上げたくないと感じたので、キャッシュするならGoプロセス管轄内のメモリに乗せようとなりました。
ただ、mariadbが大分CPU食っているのでdbだけ別インスタンスに移したい気持ちはありました。

スロークエリ

f:id:NakaWatch:20180917102542p:plain

reservationsテーブルから全座席ごとにselectしてるクエリが37万回呼ばれていて、これを解消しない限り次に進まないことがすぐに分かりました。
今回はボトルネックが明白だったため、学生でも迷わずに進める良問でした。

アクセスログ

f:id:NakaWatch:20180917103204p:plain

一番上の /api/users/:id で、先程の重いクエリが流れていたため、平均9秒かかっていました。その他レスポンスタイム上位のエンドポイントはほとんど同じクエリがボトルになっていました。

中盤

@nakabonne
  • 二人の修正が競合しないようにひたすらデプロイ
  • reservationテーブルのuser_idとかにindex張ってみる → 意味なし
  • h2oでロードバランシングしてみる → 出来なかった

reservationsテーブルにはevent_idcanceled_at IS NULLにマルチカラムインデックス張るのが正解だったっぽいです。
単体テーブルへのクエリならまだいいのですが、内部結合となると実行計画が全く理解できず死亡しました。

@inatonix, @zaq1tomo
  • それぞれ別のアプローチでgetEventsクエリのN+1解消に取り組む
    • @inatonix: Goプロセス内インメモリキャッシュ
    • @zaq1tomo: クエリ変えてクエリ数減らす

僕のアクセスログ解析が遅れたせいもあり、全くスコアが上がらず完全にお通夜モードになっていた15時頃(だったかな)に、@zaq1timoのN+1修正が火を吹き1000点→10000点と10倍になりました。
/api/users/:idの平均レスポンスタイムも9秒→1秒まで解消していました。

終盤

@zaq1tomoが合計料金をSUMするクエリをチューニングしてくれてちょっとスコアが上がり、諸々のログを吐かないようにしたら13000点を超えるようになりました。
ただ、ランダムにfailしたりしてハラハラしていました。
競技30分前に再起動し最終的に14000点を超えたところでコードフリーズし、お片付けを始めました。

感想

1392名参加している大会で、障害等を何も起こさなかった運営の方々がかっこよすぎました。
今回は各チームごとにベンチマークを用意してくれた(構成的にするしかなかった)Conohaさんにも感謝の念が溢れます。
本戦勝つぞー

Go × Clean Architectureのサンプル実装

Click here for English version

*追記:Student Goで発表しました。

speakerdeck.com

クリーンアーキテクチャとは

以下を実現することで、関心の分離をするアーキテクチャパターンです。

詳しくは様々な記事で説明されているので、今エントリでは割愛し実装パターンに絞って紹介します。

サンプルアプリケーション

↓サンプルコード

github.com

仕様は、/users にPOSTすることでユーザー登録するだけのapiです。
基本はmanuelkiessling/go-cleanarchitecturehirotakan/go-cleanarchitecture-sampleを参考にしていますがこれらはDBアクセスに直接sqlドライバーを使用しているため、今回はORMを使用した実装パターンを作ってみました。ORMライブラリはgormを使用しています。

app設計概要

f:id:NakaWatch:20180711163231j:plain

何層に分けるかは自由なのですが、閉鎖性共通の原則(CCP)に則って分けます。つまり、同じ理由で変更するコンポーネントはまとめます。
今回は原文に従って4層に分けました。
この時、外側から内側に向かって単一方向に依存することを徹底します。

ディレクトリ構成

       ├── adapter
       │   ├── controllers
       │   ├── gateway
       │   └── interfaces
       ├── domain
       ├── external
       │   └── mysql
       ├── main.go
       └── usecase
           └── interfaces

ディレクトリは、それぞれ以下の層の役割をしています。

ディレクト
external frameworks & drivers
adapter interface adapters
usecase app business rules
domain enterprise business rules

依存関係の徹底(重要)

実装の説明に入る前に、大事なルールを一つ説明します。
前述したとおり、依存関係は外側から内側へ単一方向へ保つ必要があります。
しかしながらどんなプログラムにも入力と出力があり、内側で処理した結果を外側へ出力するということが頻繁に起こります。
つまり、内側から外側へ依存したい場面が必ず現れます。その矛盾を依存関係逆転の原則を用いて解決することが、クリーンアーキテクチャをクリーンに保つための鍵となります。

依存関係逆転の原則(DIP)とは

簡単に言うと、内側は外側に依存するのではなく、抽象に依存するべきであるという原則です。
よくわからないと思うので、実際にコードを見てみましょう。

func (i *UserInteractor) Add(u domain.User) (int, error) {
    return i.UserRepository.Store(u)
}

これは、内から2番目の層、app business rules層 の実装です。
この層の外側から渡されたユーザデータを、外側にあるDBに保存しようとしています。
ここでやってしまいがちなのが、そのまま外側の層にアクセスしてしまうことです。
しかし外側に直接アクセスしてしまうと依存関係が内→外に向いてしまうので避けたいところです。
そこで依存関係逆転の原則を使います。Goではこれをinterfaceを定義することで実現します。

type UserRepository interface {
    Store(domain.User) (int, error)
    FindByName(string) ([]domain.User, error)
    FindAll() ([]domain.User, error)
}

同じ層にrepositoryインターフェースを定義し、このインターフェースに依存するようにします。
そして具象は外側で定義しておき、実行時に外側から渡してあげることで外→内の依存関係を保つことができます。
これが依存関係逆転の原則です。
ここを見ると分かりやすいかと思います。
サンプルコードでは、抽象に依存するためのinterfaceを各層のinterfacesディレクトリにまとめてあります。

各層の実装

内側の2層はプロジェクトによって結構変わってくると思うので、ここでは外側の2層に絞って説明していきます。

Frameworks & Drivers

DBアクセス

以下のようなDB接続等の外部との仲介実装はこの層で完結するようにします。

var db *gorm.DB

func Connect() *gorm.DB {
    var err error

    db, err = gorm.Open("mysql", "root:@tcp(db:3306)/hoge")

    if err != nil {
        panic(err)
    }
    db.Table("users").CreateTable(&gateway.User{})
    return db
}

func CloseConn() {
    db.Close()
}

ルーティング

http通信処理もこの層で完結します。今回はWAFにginを使用していますが、この層が独立しているので差し替えが容易になっています。
また、DBのコネクションやlogger等の具象型もここで内側の層に渡すようにしています。
こうすることで、前述した依存関係逆転の原則を実現しています。

var Router *gin.Engine

func init() {
    router := gin.Default()

    logger := &Logger{}

    conn := mysql.Connect()

    userController := controllers.NewUserController(conn, logger)

    router.POST("/users", func(c *gin.Context) { userController.Create(c) })

    Router = router
}

Interface Adapters

ORMのマッパーもここで定義します。
ここでは、DB用に最適化された型をドメインロジック用に最適化された型に変換することで、インピーダンス・ミスマッチを解決することに徹しています。
adapter層は抽象に依存しているとはいえ、この層で定義するinterfaceは外側のライブラリにある程度依存してしまいます。
原則では、

「抽象」は実装の詳細に依存してはならない

とされていますが、この層は変換が目的なので、ある程度どちらのことも知っているのが自然かと思います。
ここに関してもっと良い実装パターンをご存知の方がいらしたらご教授願いたいです。

type (
    UserRepository struct {
        Conn *gorm.DB
    }

    User struct {
        gorm.Model
        Name  string `gorm:"size:20;not null"`
        Email string `gorm:"size:100;not null"`
        Age   int    `gorm:"type:smallint"`
    }
)

func (r *UserRepository) Store(u domain.User) (id int, err error) {
    user := &User{
        Name:  u.Name,
        Email: u.Email,
    }

    if err = r.Conn.Create(user).Error; err != nil {
        return
    }

    return int(user.ID), nil
}

まとめ

見ての通り、user登録するだけのapiでもこんな大きなプロジェクトになってしまいます。
抽象化することで関心を分離することができますが、そこまでして分離する必要があるかはよく考える必要がありそうです。
基本的に抽象化するとコードは複雑化し、直感的ではなくなるので、アプリケーションの規模が小さい場合は効力を発揮しない場合が多いと思います。
何か間違いがあればご指摘頂けると大変助かります。

参考

csvをGoの構造体にマッピングする

Goでcsvを扱う際は、標準パッケージのendording/csvで対応できますが、いちいちスライスを扱うのは少しつらいものがあります。
大抵の場合構造体にマッピングした方が扱いやすいため、その方法を紹介していこうと思います。
csvマッピングライブラリはjszwec/csvutilgocarina/gocsvがありますが、今回は前者を使っていきます。

github.com

mapperの定義

User struct {
    ID              int    `csv:"id"`
    Name            string `csv:"name"`
}

以下のuser.csvを読み込みます。

id, name
1, nakabonne
2, ryo nakao

Unmarshal

var users []User
// バイト列を読み込む
b, _ := ioutil.ReadFile("user.csv")
// ユーザー定義型スライスにマッピング
_ := csvutil.Unmarshal(b, &users)

Marshal

// ユーザー定義型スライスの作成
users := []User{
    {ID: 1, Name: "nakabonne"},
    {ID: 2, Name: "ryo nakao"},
}
// バイト列に変換
b, err := csvutil.Marshal(users)

【Go】エディタ操作用簡易ライブラリを作った

はじめに

しっかりエディタでファイルオープンするとなると意外に煩雑になることに気づきました。 そこでedindというライブラリに切り出したので、今回はその紹介をしたいと思います。

github.com

使い方

クイックスタート

Factoryを生成し、エンドユーザーのシェルで実行可能なエディタを検出します。

import "github.com/nakabonne/edind"

// Factory生成
f := edind.NewEditorFactory()

// 実行可能なエディターを検出
editor, _ := f.DetectEditor()

// pathを渡してファイルを開く
_ = editor.Open("sample.txt")

選択肢追加

エディタ検出時の選択肢を追加したい場合、以下のように AddChoices メソッドを使用します。

f := edind.NewEditorFactory()
f.AddChoices(
        []string{"vi"},
        []string{"oni", "-w"},
)

5分でElasticsearch+Kibanaの環境を作る

動機

とりあえずローカルで一刻も早く動かしたい場合にサクッと構築する手順をまとめます。

手順

1, Dockerのインストール
2, dokcer-compose.ymlを作成し、以下をコピペする

version: "3.0"
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:5.1.1
    environment:
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - xpack.graph.enabled=false
      - xpack.monitoring.enabled=false
      - xpack.security.enabled=false
      - xpack.watcher.enabled=false
    ports:
      - "9200:9200"
      - "9300:9300"
    volumes:
      - es-data:/usr/share/elasticsearch/data

  kibana:
    image: docker.elastic.co/kibana/kibana:5.1.1
    ports:
      - "5601:5601"
    links:
      - elasticsearch:elasticsearch

volumes:
  es-data:
    driver: local

3, docker-compose.ymlが存在するディレクトリでdocker-compose upする(お使いのdocker-composeバージョンによってはyamlファイル内のversionを変更する必要があります)
4, http://localhost:5601にブラウザでアクセスし、kibanaの画面にアクセス

補足

xpackというパッケージの機能を全てオフにすることで、認証なしでリクエストを送ることが出来るようになります。
なので、サクッとローカルで試したい場合はこれらを外すことでスムーズに行なえます。
xpackとは何でしょうか。公式ガイドには以下のように書かれています。

X-Packは、セキュリティ、アラート、モニタリング、レポート、グラフの機能をインストールしやすい1つのパッケージにまとめたElastic Stackの拡張機能です。

GAE/Goからelasticsearchへ接続する

クライアントライブラリの選定

以上の2つが候補としてあげられます。どちらかというとGoらしく書ける印象だった2つ目のolivere/elasticを今回は使用します。

クライアントの生成

olivere/elasticは使用しているelasticsearchのバージョンごとにライブラリのブランチも切られています。
今回はelasticsearchバージョン5を使用しているので、gopkg.in/olivere/elastic.v5をimportして使用します。

import (
        elastic "gopkg.in/olivere/elastic.v5"
       "google.golang.org/appengine/urlfetch"
)

const ESURL = "https://35.174.182"

func main(){
    c := context.Background
    httpCli := urlfetch.Client(c) // URL Fetch Serviceの使用
    cli, err := elastic.NewClient(elastic.SetURL(ESURL), elastic.SetHttpClient(httpCli)) // clientの生成
    if err != nil {
        panic(err)
    }
}

NewClient(options ...ClientOptionFunc) (*Client, error)を呼び出すことによってクライアントを生成します。
ClientOptionFunc型の値を引数に与えることによってクライアントの設定を行います。

URL Fetch Service

GAE/Goから外部にリクエストを送る場合は、URL Fetch Serviceを使用します。
そのため、NewClient時にHTTPクライアントをurlfetch.Clientに差し替える必要があります。

操作

先程生成したクライアントを使用してelasticsearchのデータを操作していきます。
詳しくはwikiを参照して下さい。

func main(){
    cli.Start()
    defer cli.Stop()

    // ドキュメントの追加
    _, err := cli.Index().
        Index("index").
        Type("type").
        Id("id").
        BodyJson(struct{}).
        Do(c)

    // ドキュメントの削除
    _, err := cli.Delete().
        Index("index").
        Type("type").
        Id("id").
        Do(c)

}