爆速でGo!

GoGo!

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)

}

Feature Flagを用いたA/B TestingツールをGCPで設計する

先日、AbemaTVが開催する「AbemaTV x A/B Testing」にてFeature Flagを用いたA/B Testingツールのインフラアーキテクチャ設計を行いました。
その時の学びをまとめていきます。

要件

Feature Flagを用いてA/Bテストできれば良いのですが、以下は守る必要がありました。

  • Uiの表示速度に影響しない
  • データに対して柔軟に素早く集計・分析できるようにする
  • スケーラブル

アーキテクチャ構成図

f:id:NakaWatch:20180312110231p:plain

僕達の最終的なアウトプットはこのようになりました。
色々書いてありますが、大事なのは以下の2つです。

  • 構成図下部のclientアプリがフォアグラウンドに切り替わったタイミングでfeature flagを受け取る。
  • test対象のclientは定期的にaction logをlogging service(構成図右下)にpostする。

Feature Flag取得時のキャッシュ戦略

f:id:NakaWatch:20180312113110p:plain

上図の通り、とりあえずUIを表示し非同期で取得し次回起動時に反映するという方式をとりました。
こうすることで、「UIの表示速度に影響しない」という要件を満たします。
しかしデメリットもあります。
それはFeature Flag取得までにラグがあることです。
すぐにFlagを切り替えたい時に、二回のアプリ起動が必要になります。
そのためアプローチとして以下の措置をとりました。

  1. プッシュ通知でアップデートフラグをローカルに立てる
  2. クライアントが次回アクティブになった時に強制的に反映(ロードが生じる)

わざわざローカルにフラグを立てるのは、プッシュ通知時に一斉にアップデートするというモデルはスケーラブルではないからです。

スケーラブル戦略

最も瞬間的にアクセスが集中する部分はflag取得前のtestユーザーチェックだと考えました。
(↓緑で囲われてる部分)

f:id:NakaWatch:20180314093944p:plain

そこで、MySQLの前段にRedisを置くことを決断しました。
set型の集合にuser_idをkeyにして、testユーザーチェック等の処理に必要な情報を置いておきます。
こうすることで、O(1)でのreadが可能になります。
また、Redis Clusterを使用することで複数ノードに自動的に分散することができます。 永続化とクラスタリングにはデータ更新時に整合性の問題がありますが、今回はtest終了時まで更新はないので問題ありません。

各micro serviceの仕事

lab receive service(構成図左下)

  • Labからセグメント情報もらう
  • ユーザーがアンケート等で直接入力した値と比較し、いい感じに挿入

logging service(右下)

  • clientから生のaction logを受け取る
  • redisからuserが参加中のtest情報を受け取り、BigQueryに挿入

aggregate service(右上)

  • バッチでBigQueryに対してクエリを実行
  • 管理画面からの呼び出しによって人為的にクエリ実行

cache service(左上)

  • テスト実行者が設定した情報をもとに、前段に置く値を決定する

フィードバック

構成図右部のlog集計部分をLambda Architectureに従って設計するべきだとのフィードバックを受けました。

f:id:NakaWatch:20180314094938p:plain

Lambda Architecture

Lambda Architectureとは、以下3つのレイヤーから構成される設計指針です。

バッチレイヤー:ログをバッチで集計するので正確だけど、リアルタイムではない
サービスレイヤー:バッチレイヤーの集計結果を提供する
スピードレイヤー:データはリアルタイムだが、データは少し不正確の可能性ある

スピードレイヤーの集計で得た最新の値とバッチレイヤーの集計によって得た確定した値をマージすることで、素早く推定することが出来るというモデルらしいです。
まだあまり理解していないので、詳しくは以下書籍を読んでみます。

www.oreilly.co.jp

まとめ

無知は罪だということを痛感しました。引き続きインプットの質を高めていこうと思います。

GoでDDD設計する際のrepositoryをどう定義するか

GoDDDでrepositoryを設計する際に色々考えたのでメモ

アーキテクチャ

この記事では、レイヤードアーキテクチャを使用します。
しかし正式なレイヤードでなく、以下のようにinfra層がdomain層に依存する形で設計します。

f:id:NakaWatch:20180210164321p:plain

ディレクトリ構成

.
├── app
├── domain
│   └── user
│       ├── user.go
│       └── user_repository.go
├── infra
│   └── mongo
│       └── user_repository.go
└── ui

repository定義

今回は以下のUserエンティティを永続化することを想定して勧めていきます。

package user

type User struct {
    ID   uint
    Name string
}

repositoryはinfra層とdomain層の2層に定義します。

domain層

domainディレクトリ配下に、
以下のようなRepositoryをインターフェースとして定義しておき、このRepositoryを実装すればUserエンティティが返ってくることを保証しておきます。

domain/user/user_repository.go

type UserRepository interface {
    Find(id int64) (User, error)
    FindAll(limit int) ([]User, error)
}

infra層

infraディレクトリ配下に、
domain層のRepositoryインターフェースを実装したRepositoryを定義します。

infra/mongo/user_repository.go

package mongo

type UserRepository struct {
    Context context.Context
}

func (r *UserRepository) Find(id int64) (user.User, error) {
    return r.find(r.Context, id)
}
func (r *UserRepository) FindAll(limit int) ([]user.User, error){
    return r.findAll(r.Context, limit)
}

repositoryの使用

package main

import (
    "fmt"

    "../domain/user"
    "../infra/mongo"
)

func main() {
        // Userエンティティが取得できることを保証
    var repo user.UserRepository
    repo = getUserRepoFromInfra()
    user, _ := repo.Find(1)
    fmt.Printf("type is %T", user) // => type is user.User
}

func getUserRepoFromInfra() user.UserRepository {
    return &mongo.UserRepository{}
}

窓口の抽象化

以上のように、domain層とinfra層の窓口を抽象化しておくことでインフラの差し替えを容易にする事が出来ます。

2018年のテーマは「持ちつ持たれつ」

2017年は4皮くらい剥けた年でした。 まずはそんな2017年のテーマを振り返ります。

2017年は決める年

nakawatch.hatenablog.com

去年正月に書いたとおり、2017年は決める年でした。

人生哲学を決める

決まりました。
どんどん見える世界が変わっているので確定ではありませんが、毛穴から手が出そうなくらいなりたい姿があります。
29歳までにカリスマのソフトウェアエンジニアになる
まつもとゆきひろさん、宮川達彦さん、中島聡さん、小飼弾さん、伊藤直也さん、笹田耕一さん、ひげぽんさん、、、、
29までに彼らのようなカリスマになって各分野のカリスマ達と対等に情報交換をし、今見えない世界を見たい。
それから30代の生き方を決めたい。
大きなOSSにコミットして、各地で講演会して、技術書書いて、WEB+DBに連載載せる29歳になりたい。
中学の時は、グラブ職人に憧れていました。知る人ぞ知る存在になりたい気持ちが昔から強かったのです。
この夢に全身が納得していますが、最初は上手くいきませんでした。

f:id:NakaWatch:20180102205204p:plain

このように、人生ピラミッドを作りまくってた時もありました。
外に説明するには非常に筋の通った綺麗なものにはなりましたが、自分は納得していませんでした。
なんでも筋を通せばいいってもんじゃないですね。

決めるスピードと精度を高める

「意思決定」が苦手だったので、この一年はそこを意識しました。
インターン先の上司との面談を繰り返して、自分は意思決定が苦手なのではなく、捨てることが苦手だということが分かりました。
何を捨てるか、どんな情報が集まれば決定出来るかを意識した結果後悔ない意思決定を素早く出来るようになりました。

逐一何時までに何をするかを決める

何時までに何をするか、小さな意思決定を毎日した一年でした。
12月にはサイバーエージェントにて、約2週間でマッチングアプリのサーバーサイド開発を完了させました。
時間密度を濃くすることを意識した成果がはっきりと見えました。

2018年は持ちつ持たれつ

冒頭で申したとおり、2017年は4皮くらい剥けた年でした。
それも、多くの人の助けがあったおかげです。
しかし、自分はヒトに頼るのが苦手な一匹狼体質です。(誕生時に姓名判断したところ、唯一心配だったのがこれだったらしい)
困っても一人で解決しようとする。ヒトにもあまり干渉しない。それが自分のアイデンティティであるとも思っていました。
しかしやっぱり一人の力には限界がある。そして寂しい
2017年の終盤は、1人では乗り越えられない壁がたくさん現れました。そこではちきれそうになった自分を救ってくれたのは、いつもヒトでした。
助けてもらうと、助け返したくなります。
助け合うと、二人の間に暖かい信頼関係が生まれることに気づきました。
この「暖かさ」を感じてる時が一番幸せです。
この形容し難い「暖かさ」を大切にしたい。それは必然的にヒトを大切にするということです。
今年は色々大きな勝負があると思います。ヒトを頼ってヒトに頼ってもらう一年にします。

サブテーマは「自信」

自信がある分野に関わってると楽しいですね。
自信がある時は、余裕があるので楽しいです。
逆に自分が生きづらいと感じる時は大体自信がない時です。
この生きづらさを潰すために、「自信」にフォーカスしていきたいと思います。
結果を出し、評価されないと自信を持てないタイプなので、勝負の結果にこだわっていきます。
勝負が苦手だった自分が去年一年で相当勝負強くなったので、引き続き1位にこだわっていきたいと思います。

持ちつ持たれつ壁を超えていって、ヒトとして自信をつける一年にします。

GAE/Goで位置情報マッチングAPIの設計を考える

先日、サイバーエージェントさんの学生版ガレスタというインターンに参加しました。
6週間で企画からサービス完成までを4人(ビジネス1人, デザイナー1人, エンジニア2人)で行うというものでした。
今記事ではこのインターンで得た技術的なことを、つらつら書いていきたいと思います(´・ω・`)

要件

位置情報を用いたマッチングモバイルアプリのAPI

技術選定

言語

Go

  • チャット実装時のマルチスレッド処理が簡潔
  • 薄くAPI作れるから短期間開発○
  • 丁度いい型安全
  • 開発効率を上げるエコシステムが整ってる
  • モバイルAPIの実例多い
  • インターン後仲間集める時に、若く優秀なエンジニアを集めやすい

http通信

net/http

  • 薄い
  • 慣れてる
  • 標準なのでバージョン管理安心

ORマッパー

gorm

  • Githubスター数一番多い
  • RORのActive Recordライクに書ける
  • 情報多い

RDB

MySQL

  • 慣れてる

KVS

Redis

  • 3.2から追加された"Geo"系コマンドを使って位置情報管理したい

インフラ

Google App Engine

  • サーバーサイドは一人なのでとにかく開発に集中したい
  • PaaSの中で最も妥当な柔軟性、料金と感じた

アプリケーション設計

$GOPATH
├── app
│   ├── app.yaml
│   ├── config.go
│   ├── handler.go # Application層
│   └── main.go # UI層
└── src
    ├── domain # Domain層
    │   ├── config.go
    │   ├── detail
    │   ├── feed
    │   ├── location
    │   └── registration
    ├── infra # Infrastructure層
    │   ├── cache
    │   ├── config
    │   ├── objstorage
    │   └── orm
    └── middleware

レイヤードアーキテクチャを用いた軽量DDDを採用しました。

DDDを完璧に実現しようとすると、分析、設計、実装というフローを踏まないといけないのですが、今回は短時間且つ仕様も変わっていくため、分析はせず軽量なDDDを実現しました。

DDDについてはまとまった知識がなかったので、今回エリック・エヴァンスのDDD本を初めて読みました。

www.shoeisha.co.jp

GAEは、app.yamlが置いてあるディレクトリ以下を全てビルドしてしまうので、Domain層以下は別packageとして管理しています。
集約毎にディレクトリを分け、その中でentity、value object、repositoryを定義しています。
infra配下ではDBに依存するコードをまとめ、出入りするデータをrepositoryにinterfaceとして定義しておきます。
このようにすることで、ミドルウェアの差し替えを容易にしています。

GAE/GoのDDD設計はまだまだデファクトスタンダードが定まっていないようなので、これからも熟考していこうと思います。

インフラ設計

全体アーキテクチャ

f:id:NakaWatch:20171226160338p:plain

ほとんどのデータはCloudSQLにMySQLを乗せて管理しました。
CloudSQLはフェイルオーバー時に数分のダウンタイムが発生してしまうというデメリットがありますが、短期開発ということで立ち上げ時の設定が楽であるという最大のメリットを汲み採用しました。
CloudStorageでは画像中心に、サイズの大きいオブジェクトを管理しています。
特筆すべきはredisです。

redisによる位置情報管理

位置情報機能の要件は"近い順にアクティブユーザーを返す"でした。
そこで最初に考えたアーキテクチャはこんなものでした

f:id:NakaWatch:20171226162856p:plain

列志向のBigtableに位置情報を溜めていき、アクティブユーザーを返す度にBigQueryを回すというものです。
ありえませんね。
ユーザー毎にクエリを回すというこのモデルではパフォーマンスにもお財布にも優しくないですね。
サイバーエージェントの社員さんに聞いたところ、redis3.2から位置情報を扱うコマンドが追加されたらしく、使ってみることにしました。
>>Aさんから半径100km以内のユーザーを近い順に頂戴!<<
わがままですね。
でもこんなわがままを、ジェントルマンなredisは1コマンドで叶えてくれます。
GEORADIUSBYMEMBERという夢のコマンドをredisにささやきます。

// GEOADDで追加
> GEOADD location 13.583333 37.316667 "Aさん" 13.361389 38.115556 "Bさん"
(integer) 2

// GEORADIUSBYMEMBERで取得
> GEORADIUSBYMEMBER location "Aさん" 100 km
1) "Aさん"
2) "Bさん"

GEOADDでsorted set型の集合にぶっこみ、ソートします。
GEORADIUSBYMEMBERを使用すればO(N+log(M))で検索することが可能です。
*N=指定範囲内の要素数, M=インデックス内の項目数

これで当初よりシンプル&ハイパフォーマンス&低料金で要件を実現することが出来ました。
社員さんありがとうございます〜〜

テーブル設計

ゼロイチでDB設計したことなかったので、しっかり以下3つ読みました。

www.oreilly.co.jp

www.shoeisha.co.jp

www.shoeisha.co.jp

プライマリーキーをとりあえず"id"というサロゲートキーにしてしまうアンチパターン「idリクアイアド」を避け、
なるべく自然キーを用いた設計を心がけました。

API設計

ゼロイチでAPI設計したことなかったので、しっかりこれを読みました。

www.oreilly.co.jp

リソースのページネーションを実現する際、最初は相対位置を利用していましたが、絶対位置を利用することでパフォーマンスと整合性の改善をしました。

まとめ

何が言いたいのかまとまっていませんが、とりあえず多くの技術的学びがあったということです。
痛感したのは、どんなに短期開発でも時間かけるべき部分には大いに時間をかけるべきだということです。
どこを丁寧に行うか適切に判断することが出来れば、スピードと品質は必ずしもトレードオフにならないと感じました。
このアプリは近いうちにリリースするので、その時にまた報告致します。

Goで軽量なスクレイピングライブラリを作ってみた

リポジトリ

github.com

はじめに

GoでWebクローラーを開発する際、皆さんどうされてますか??
フルスクラッチで作るのは少し面倒だし、asciimoo/collyのようなフルスタックなのはいらない、、という時に丁度いいライブラリがなかったので作りました!
本当に最低限の機能のみ搭載しました。

機能

主な3つの機能を紹介します。

オーガニック検索

キーワードを入れて自然検索結果画面に表示されるページURLを返します。

import "github.com/ryonakao/netsurfer"
urls, err := netsurfer.OrganicSearch("キーワード", 1)

順位調査

指定キーワードで検索した時、指定ページがオーガニックで何位に掲載されるかを返します。

import (
    "net/url"
    "github.com/ryonakao/netsurfer"
)
u, _ := url.Parse("https://qiita.com/ryonakao")
rank, _ := netsurfer.GetRank(u, "ryonakao", 2)

HTML取得

指定ページの静的ページを返します。

import "github.com/ryonakao/netsurfer"
html, err := netsurfer.GetHTML("https://qiita.com/ryonakao")

実装一部紹介

SERPsのurlスライスを返す以下の関数を御覧ください。

gist.github.com

ユーザーにdepthを指定してもらうことで、一回のスクレイピングで全SERPsを取得しています。

ryonakao/netsurderPuerkitoBio/goqueryに依存しています。

まとめ

ライブラリ開発めちゃんこ楽しい♪
どんどん作っていこうと思います!!