天然パーマです。

Golangを初めて本番投入したぜ!

先日「ボケて」サービス群の中でも「スタンプ」という機能を提供するアプリサーバがGo実装になりました。これが僕にとってほぼ初めての「Golang実戦投入」となります。Goの良さについては去年の10月に僕がやっているPodcast「wada.fm」でも言及していました。ただ、単純に良いからと言って、本番への導入となると既にユーザーを抱えているので、敷居が高いと感じます。そこで、工夫しつつ、じっくりと試して、現在に至りました。後述するようにハマりどころはありましたが、そのGoサーバは安定稼働していて、ユーザーへコンテンツを配信しています。そこで今回は新規にGoを導入する際の一つの事例として、今回、僕がやったボケてのスタンプサーバリプレースの経緯を紹介します。

LL言語への「若干の」不満

まず...

そもそもなんでGolangが必要なの?

ってところ。ボケてのアプリケーションはほぼ全てが以前からPerlで書かれていて、キレイに本番で不具合も無く動いてくれていました。Plack/PSGIが登場してもはや久しく、アプリケーションサーバが安定し、またApp::cpanminus/Cartonといったモジュールインストーラやライブラリ管理も賢くやってくれます。ただ、これはLL全般に言えることなんですが、型が無いんですよね。

型がないってことは、サクッと書くときにはいいけど、堅牢にやりたい時に手間がかかります。 例えばPerlにはData::Validatorというライブラリがあるので、それを使ってメソッドへの引数のタイプチェックなどを追加で実装していました。例えばボケて内のlib/Bokete/Model/Boke.pmには以下のようなコードが記載されています。

 sub create {
    state $rule = Data::Validator->new(
        text => 'Str',
        odai_id => 'Str',
        user_id => 'Str',
        category => { isa => 'Maybe[Str]', default => undef },
        tags => { isa => 'ArrayRef', default => sub { [] } }
    )->with('Method');
    my ($self, $args) = $rule->validate(@_);

実装レベルでコードの中に出てくる「データ」もしくは「機能」それぞれに対して厳密にチェックしておきたい場合に、LLだと外部のライブラリを使う手間が生じたり、それが完璧に行われないケースがあるのです。逆に言えばそれがLLのいいところですが、使いドコロによってはより「strictに」やりたいアプリケーションもあるかと思うのです。

また、Perlは素晴らしく書きやすい言語だと思ってはいますが、他の違う言語も習得しておくと後々武器になって、楽しいんじゃないかなーって感じ、Goに手を出したという経緯になります。

Goの魅力

Golangを触ってみて良いなーって感じたことは前述した通り、wada.fmで話しております。興味のある方は聴いて下さい!

www.wada.fm

ただ簡単に列挙するならば...

  • 静的型付け言語だけどスクリプトっぽくかけてとっつきやすい
  • 構造体にメソッド生やして「クラスのようなもの」を実現するとか「素朴」で良い
  • コードを簡潔にするという方針などが現れた「ツンデレ」なコンパイラ
  • goroutine便利だし使いドコロが結構ある
  • Webサーバで負荷テストした時にCPUリソースをキッチリ使いきってくれて消費メモリも既存アプリと比べて少ない
  • 言語仕様的にもミニマムで「組み合わせでなんとかする」って点がPerlっぽい
  • 優良なライブラリもどんどん出てる

などなど、でしょうかね。とにかく触ってみて、スッとコードを書き始めることが出来たのがよかったです。

Goをスクリプト的に利用する

実は今回紹介するスタンプサーバ以前にボケてではGoを使っていました。ただプロダクトとしていつも動くサーバやバッチ、という形では無く、書捨ての「スクリプトっぽい」コードを利用してたのです。

例えばRDBMSのとあるテーブルの構造を変えて、データ移行したい場合においてです。なんでGolangをそこで使うのか?というとgoroutineがあるからです。Goだとgoというキーワードだけで並行処理が走り、かつデータベース接続もロックせずよしなにやってくれるので、MySQLサーバの負荷の様子を見ながら並行数を調整することで「ある程度の負荷をかけながら」素早くデータ移行が出来て便利でした。以下のようにsync.WaitGroupでそのgoroutineで平行に走らせる処理数を調整しています。

 for ;; {
  var results []Entry
  err = db.Select(&results, db.Where("column1".IsNull()), db.Limit(limit));
  if err != nil {
    panic(err)
  }
  if len(results) == 0 {
    break
  }
  var wg sync.WaitGroup
  for _, e := range(results) {
    wg.Add(1)
    go func(entry Entry) {
      work(entry)
      wg.Done()
    }(e)
  }
  wg.Wait()
}

このように最初はスクリプト的にGoを使ってみて感触を掴んでみました。

どこからGolang化していくか?

さて、Goのパフォーマンス性能を活かせて、かつ、現行で走っているサービスに影響がなければ、Golang化したいところ。ただ、移行のコストとかも考えると全てをGoにするのは大変だし、Perlの方が得意なところもあるのでその辺は見極めなくてはいけません。ボケてではMicroservicesという言葉が流行る以前から、ある程度サービスをHTTPレイヤーで切ったコンポーネント化を進めておりました。そこで、大きな部分を一気に変更する、のではなく、小さな一部分だけを違う実装に変えるということが容易です。そこで、目をつけたのがスタンプサーバです。

ボケてではボケのコンテンツを画像として保存したりLINEで送る機能があります。「スタンプ」と呼んでいるのはまさに「LINEスタンプ」からインスプレーションを受けています。ようはボケの対象となるお題画像とその他ボケのテキストなどをコアロジックのWeb APIサーバから引っ張り、画像生成して、配信する機能が実装されています。試しに新しいGolang実装のサーバから配信されているスタンプ画像を直で貼り付けてみるとこのような画像です。

http://stamp.bokete.jp/1916834.png

このサービスはボケてのアプリの中でも「フロントエンドの1機能」としてPerlでつくられていましたが、ホストも違うし動かすサーバも違う。また、データはWeb APIから取得してくればOK!と、切り離しやすい箇所だったので、

まさにここからGolang化を試してみよう

となりました。

スニペットを集める

と言っても、いきなり画像処理や文字列の改行計算を含んだ「ひとつの」アプリケーションを書き始めるのはニュービーにとって漠然としすぎてて、ムリゲです。なので、スタンプサーバの実装要件をピックアップし、それごとに小さなGoコードの塊=コードスニペットをたくさん書いていきました。スニペットが集まっているレポジトリから選んでみると...

  • ローカルの画像をリサイズして書き出す
  • HTTP上の画像を取得して書き出す
  • ローカルの画像をWebで配信する
  • テキストを描画して画像として書き出す
  • 改行を含んだテキストを処理する
  • 日本語を含んだ文字列改行の禁則処理をする

などです。おそらく一番に書いたコードはこんなものでした。リサイズ処理にgithub.com/disintegration/imagingを使っています。

 package main

import (
    "image"
    "github.com/disintegration/imaging"
    "fmt"
    "os"
)

func main() {
    args := os.Args
    if len(args) < 2 {
        panic("The argument is required.")
    }
    filename := args[len(args)-1]
    fmt.Println("Input filename is:", filename)
    orgImg, err := imaging.Open(filename)
    if err != nil {
        panic(err)
    }
    dstImg := resize(orgImg, 600, 0)
    err = imaging.Save(dstImg, "output.jpg")
    if err != nil {
        panic(err)
    }
    fmt.Println("Resize as output.jpg.")
}

func resize(img image.Image, width int, height int) image.Image {
    dstImg := imaging.Resize(img, width, height, imaging.Lanczos)
    return dstImg.SubImage(dstImg.Rect)
}

このようにスニペットを充実させていく作業をすると

お、これは本番用のアプリも実装出来そうだぞ!

と自信がついてきます。

画像生成サーバの実装と運用

そこでいよいよスタンプサーバの実装です。既に処理部分のスニペットがあるので、いかに個別のファイルにまとめてテストを書いて構造化するか?を考えればすんなりといけました。こんなファイル群でstamp_serverコマンドをつくっています。

 $ pwd
/Users/yusuke/go/src/github.com/bokete/webstamp
$ tree ./
./
├── README.md
├── assets
│   ├── font-heavy.ttf
│   ├── font-medium.ttf
│   ├── stamp_404.png
│   ├── stamp_footer.png
│   ├── stamp_header.png
│   ├── stamp_panel.png
│   └── transparent.png
├── assets.go
├── client.go
├── client_test.go
├── cmd
│   └── stamp_server
│       └── main.go
├── stamp.go
├── stamp_test.go
├── util.go
└── util_test.go 

デプロイはあまりベストプラクティスが共有されてない?っぽいので、少々悩みましたが、今まで通りAnsibleで以下の工程を自動化しています。

  • 要らないと思うけど「とりま」nginxをフロントに置く
  • スーパーデーモンにはdaemontoolsを使用
  • lestrratさん作のgo-server-starterをベースにホットデプロイを実現する
  • 上記ライブラリのlistenerをアプリ側に実装しておく
  • Perlアプリの時と同じくserver_starter(Go実装)を利用してアプリを立ち上げる
  • アプリをアップデートする際はサーバ側のGOROOT内でgit pullさせて次にgo getしてbin以下にコマンドを生成
  • 予め仕込んであるdaemontoolsディレクトリを$ svc -h /service/stamp_serverとしてgraceful restart

現状のコードだとgraceful shutdownにはうまく対応出来てない疑惑がありますが、画像配信サーバーですし、間に短いTTLを仕込んだCDNを挟んでますし、なんとかなります。

ハマりどころ

一番最初、デプロイしてしばらくたつと、

あれ〜、やけに詰まるな〜 Goだとパフォーマンス悪くなるのかな〜?

なんて自体に陥りましたが、これは別段Golangのせいでは無く、僕が書いていた禁則処理のロジックで稀に無限ループをつくってしまうバグが潜んでいただけでした。「あるある〜!」そう言語が悪いんじゃないんです、自分が悪かったのです。

と、まぁそこが大きな躓きポイントなだけであとは十分パフォーマンス出てますし、不具合もなく快適なアプリになりました!

まとめ

ボケてにおいてスタンプサーバをGo実装で書き直して、Go本番童貞を捨てた話を書きました。本番化しないと分からないことも結構あったので、やってよかったと思います。Perlと比べると言語の性質的に煩雑になるケースもありますが、コンパイルされる時点で「不本意なことを弾いてくれる」というのは強みであるし、単にGolangが気に入ったので、他のコンポーネントもジャッジ次第でGoで書いていきたいですね!

Go言語によるWebアプリケーション開発

Go言語によるWebアプリケーション開発

改訂2版 基礎からわかる Go言語

改訂2版 基礎からわかる Go言語