ブログ・ア・ラ・クレーム

技術的なメモとかライフログとか。

WSA 研 #1 予稿 "再利用性の高い Test Drive Intrastructure 実行環境に関する取り組み"

第1回WSA研究会 という催し物に出る予定でそれ用の予稿を書いたのですが、折角なのでブログにも掲載しておこうと思います。あとで更新するかもです。

再利用性の高い Test Drive Intrastructure 実行環境に関する取り組み

 Infrastructure as code や CI/CD の文化の広がりやインフラ構成の複雑化に伴い、 Test Driven Infrastructure といったインフラ構成をテスト可能にすることで安定稼働を目指す取り組みがなされている。

 Test Driven Infrastructure の実践方法としては 1) AnsibleChef などといった構成管理ツールに特化したテストツールを使う 2) Serverspec などといった構成管理ツールとは独立したツールを使う方針が考えられるが、前者はツールに非常に依存しかつ独特の DSL を要求される、後者はツール特有の学習コストが要求されるといった課題がある。

 実践方法とは別に、インフラ構成やテストに関する情報を残すことも重要である。これは GoogleSite Reliability Engineering の序論に記されている "実装は一時的なものだが文書化された理論は重要" との記述や、 Serverspec が可読性を重要視 して実装されたことからも汲み取れる。

 本研究ではこれらの背景を加味しつつ、前述の実践方針の 2) のような構成管理ツールとは独立した形で、かつ低い学習コストで Test Driven Infrastructure を目指す。 その実現方法として現在、 Harmonium というツールを試験的に実装している。このツールは現状、 Markdown に埋め込まれたシェルスクリプトを実行するツールである。 Harmonium によるテストコードの実行は bats と似たアプローチであるが、ドキュメントを Markdown で書けてかつ独自の記法を要求しないという差分がある。 MarkdownGitHub などのサービスの WebUI 上でレンダリング可能で運用上親和性も高く、テストコードに対する情報を付与しやすい。またシェルスクリプトを用いることで運用者はシェルの知識をそのまま利用することが可能で、別途 Ruby や Serverspec などのシンタックスを習得する必要がなくなる。 現在 Harmonium は golang で実装しており、ツールはワンバイナリで実行可能となるため、 Serverspec をローカルで実行する際に発生する Ruby や依存ライブラリを導入する手間などが軽減される。

参考

 参考にした文書やソフトウェア。本研究で提案する Harmonium は特に Jupyter Notebook に大きく影響を受けている。 Jupyter Notebook はノートブック形式で実装とは別に Markdown で文書を残し、他者と共有ができる。 Jupyter は Cloud Datalab でベースに用いられるなど、データ分析でよく使われるツールとなっている。

 余談だが Harmonium はブラウザ自動テストのためのツール Selenium やアプリ自動テストのためのツール Appium に触発されて命名した。

RocksDB の mruby binding "mruby-rocksdb" を作った

これは mruby Advent Calendar 2017 17 日目の記事です。

本記事は表題の通りの内容になります。 Advent Calendar のネタがどうしても思い浮かばず、ふと去年の自分の記事を振り返ってきたところ KVS の mrbgem を書いてた ので、コードをある程度使いまわして似たようなネタで行くかと考えた次第です。

RocksDB について

Facebook が公開している KVS です。 LMDB とか LevelDB 、 K2HASH と同じような、組み込みの KVS 型 NoSQL って分類できるものかなと思います。

http://rocksdb.org/

特徴としては、フラッシュストレージに対する書き込みのパフォーマンスに配慮されている点があるかなと思います。 さらなる詳細に関してはおそらく web 上に資料やベンチマーク結果などがシェアされていると思うのでそれらに譲ることにします。

mruby-rocksdb について

概要

RocksDB の mruby binding です。 リポジトリは以下の通り。

github.com

RocksDB が C++11 以降を要求することもあり C で書くのも面倒だな・・・と思ってしまったので C++11 以降前提で実装しています。 とは言っても半分くらいのメソッドは Ruby で実装してしまいました。だらしねぇな。

また mruby-k2hash と同様、サポートするメソッドは RubyDBM class ライクにしてあります。

使用例

mirb で触ってみるテストです。

$ ./bin/mirb
mirb - Embeddable Interactive Ruby Shell

# open
> db = RocksDB.new("./rocksdb")
 => #<RocksDB:0x101501570>

# store
> db['key1'] = 'value1'
 => "value1"

# fetch
> db['key1']
 => "value1"

> db['key2'] = 'value2'
 => "value2"

# each
> db.each do |k,v|; puts "key: #{k}, value: #{v}"; end
key: key1, value: value1
key: key2, value: value2
 => #<RocksDB:0x101501570>

# delete
> db.delete('key2')
 => nil

# etc ...
> db.to_hash
 => {"key1"=>"value1"}
> db.to_a
 => [["key1", "value1"]]
> db.map do |k,v|; k + ' map to ' + v; end
 => ["key1 map to value1"]

mirb で触ってみるテストでした。

おわりに

次はもっと書いたこと無いタイプの mrbgem 書いてみたいものです。

文フリで配布するSF小説書いた

こちらで配布される SF 小説集に拙作を寄稿しました。 イベント詳細はこちらになります。

モチーフにしたのは認知行動療法とルールベースのフィルタリング処理、そして昨今騒がれている Twitter の凍結騒ぎだったりします。 SF 小説なんて書いたのは人生初で、これらがうまく伝わるか、そもそも読者に伝わるものがあるのかが不安なところですが、もしご興味を持っていただけましたらお手にとってみていただけると幸いです。

なぜ小説を書いたのか

普段得られない体験をしたかったのと、自分が今に至るまでのモチベーションの源泉として何かを作りたいという気持ちが深く根ざしておりそれが(普段ブログに書くような)技術要素以外でも作用するか確認してみたかった次第です。 執筆完了した今の状態では、まぁそれなりに得られるものがあったかも・・・というくらいの温度感。

なにはともあれ

みんな今週 23 日木曜日は東京流通センターへ Go!

uniq を少し早く処理するツール "quniq" を作った

TLDR

重複行を除去する uniq コマンドを早く実行するツール "quniq" を Go で作ってみました。

github.com

自分が測った限りでは、他の重複除去を行なうワンライナーと比べて、処理に要する時間が少なくとも 1/3 程度になることが確認できました。

背景

みなさまご存知の通り、 uniq はソート済みの入力を受け付けて重複行を除いた出力を吐き出すツールです。 また -c オプションで重複件数も出力したりすることが可能です。 特に集計の前処理として余分な行を除外したり、アクセスログ中の特定要素のランキング化に利用されることが多いように感じます。

uniq はソート済みの入力を期待するためよく cat /path/to/file | sort | uniq のように sort した結果をパイプで渡すことが多いと思うのですが、入力のサイズが大きくなってくると sort に時間がかかるようになってきます。 そこで今回はより短い所要時間で重複行を除去したり件数カウントができるツール quniq を実装してみることにしました。

設計と実装

高速化のための方針は単純で、 sort を要求せずかつマルチスレッドで処理するようにするというものです。 これをシンプルに実現し、かつツールの利用を簡単にしたかったため、今回は Go で実装することにしました。

実装の概略図を示したものは以下のとおりです。

f:id:syu_cream:20171104222326p:plain

特徴的な点として、 goroutine を複数立てて各 goroutine で一旦入力を map に格納していることが上げられます。 goroutine ごとに個別の map を操作するためこの点では mutex など同期プリミティブは不要であり、また map の key として各入力行を利用するため重複行除去の効果が期待できます。 その後各 goroutine が処理した map を、最終的な結果を格納する map にマージします。 この map の操作は mutex で排他処理をしておきます。 その他、 入力データを格納する buffer は sync パッケージの Pool で管理し、メモリアロケーションの負荷軽減とコード上における goroutine 間 buffer 所有権の管理を行っております。

使い方

普通に go get してご利用ください

$ go get github.com/syucream/quniq

quniq は入力が sort 済であることを期待しないので、直接重複除去したい入力を渡してみてください。

$ cat /path/to/file | qunic -c

オプションとしては uniq における -c, -u, -d, -i っぽいものを提供する他、パフォーマンスに左右する -inbuf-weight, -max-workers を提供します。 最後に上げた 2 件のオプションは実行環境によって適した値が変わってくると思うので、いろいろお試しください。

評価

今回は筆者の手持ちの環境である MacBook Air Early 2014, Core i7 4650U 1.6GHz, Mem 8GB なマシンで測定を行ってみます。 また適当に重複がありそうな巨大なファイルを以下のようにして作成しています。本記事での測定では総計 4GB ほどの入力データを用意しました。

$ cat /dev/urandom | tr -dc '0-9' | fold -w 4 | head -n 100000000 > randlog_0
$ cp randlog_0 randlog_1
...

sort | uniq の結果

まずは定番の sort | uniq した時の所要時間を、 bash の time コマンドで測定してみます。 ちなみにこんな記事を目にしたので LANG=C を一応与えてみています。

bash-3.2$ time cat randlog_* | LANG=C gsort | guniq > /dev/null

real    15m39.761s
user    12m59.955s
sys     2m10.857s

sort には --parallel オプションが指定できるのでこれを明示的に与えてみると...少しだけ早くなるように見えます。

bash-3.2$ time cat randlog_* | LANG=C gsort --parallel 4 | guniq > /dev/null

real    14m24.231s
user    12m32.867s
sys     2m7.031s

awk の結果

"uniq large file" とかでぐぐってると awk で処理するワンライナーが見つかったりします。 これは quniq のように map 的な構造で重複除去を行なう & 入力がソートされてなくても良い手法になります。 time の結果を見ると sort | uniq するより早く完了していますね。

bash-3.2$ time cat randlog_* | awk '!_[$0]++' > /dev/null

real    11m13.350s
user    10m59.538s
sys     0m5.868s

sort -u の結果

重複除去だけなら sort コマンドの -u でも行えます。 一応こちらのパフォーマンスも測定してみましょう。

bash-3.2$ time cat randlog_* | LANG=C gsort -u > /dev/null

real    6m4.100s
user    5m46.810s
sys     0m10.659s

--parallel を与えると?

bash-3.2$ time cat randlog_* | LANG=C gsort -u --parallel 4 > /dev/null

real    5m56.870s
user    5m40.977s
sys     0m10.251s

sort -u はかなり早そうです!しかしこの場合は uniq -c のような件数取得が行えなくなるので uniq のユースケースをカバーしきれるわけではないですが・・・。

quniq の結果

最後に拙作ツール quniq の結果を貼ってみますと、、、 sort -u と比較しても約 6m -> 2m と、所要時間が 1/3 ほどになっていることがわかります。 sort | uniq と比べると約14m -> 2m と 1/7 くらいになっていますね!

bash-3.2$ time cat randlog_* | ./quniq --max-workers 4 > /dev/null

real    1m45.362s
user    4m29.294s
sys     0m10.177s

おわりに

というわけで uniq を少し早く処理するツールを作ってみました。 分散処理基盤に突っ込んで力技で重複除去などしてもいいかも知れませんが、手軽に解決したい時の "苦肉"の策として quniq を使える余地はあるんじゃないかと思います。

Scala + Scio で Apache Beam あるいは Google Cloud Dataflow に入門する

Apache Beam データに対する ETL 処理を、様々なランタイムで同じコードで実行できるようにするものです。 これは Google Cloud Dataflow のモデルを元に OSS 化されたもので、バッチ処理とストリーム処理を(ほぼ)同じコードで実装できたり、 Hadoop や Spark 、 Dataflow など複数の実行環境に対応していたりします。 標準では JavaPython 向けの SDK が用意されていますが、前者はシンタックスが独特であり後者はまだ機能が乏しく、発展途上感が否めない部分があります。

本記事では Java SDK をラップしつつ機能追加をされた、 Spotify 製ライブラリ Scio を使って Apache Beam による ETL 処理の実装に入門してみようと思います。

そもそも標準の SDK はどんな感じ?

公式ページのドキュメント がなかなか分かりやすくまとまっております。 元々 GCP のサービスから端を欲しているお陰か、 GCP の他サービス、 GCS や BigQuery などとの連携が非常に容易です。 特に GCS に関しては呼び出すメソッド名は同じまま、パスの表現でローカルファイルを参照するか GCS を参照するかなど切り替えてくれるのが、なかなか抽象化されているように思えて好ましいです。 しかしながら変換処理になると、 PCollectionDoFnapply() するのをメソッドチェーンで繋いで・・・などなどちょっと独特のコードを書く必要があります。

この部分は Python SDK になると抽象度があがり、 MapFlatMapFilter などよくあるコレクション操作の関数が揃っていて便利です。 しかしながら Python SDK は機能がまだ不足しがちで、 Join 系のちょっとしたメソッドがなかったりストリーム処理に対応していないらしかったりするようです。 また、 Apache Beam を利用したコードも実行に時間がかかるため、型チェックで弾ける問題などは弾いて欲しいという欲求も湧き上がります。

そこで Scio

Scio は Java SDK の PCollection などをラップしてかつ抽象的なコレクション操作関数などを追加したライブラリです。 更に Type safe BigQuery I/O という機構や Cassandra, Elasticsearch などへの I/O などのアドオンも存在します。 詳しい情報については GitHub の Wiki が結構充実しています。

Scio 使用例

今回は例として Scala + Scio で BigQuery のレコードを読んでちょっとした処理をし、 BigQuery とついでに GCS に出力することを試してみます。 今回の例のために実装したコードなどは こちら

BigQuery への入出力に関しては Scio の Type safe BigQuery を使ってみます。 これらは @BigQueryType マクロでクエリやスキーマの定義をしておき、そのクラスのオブジェクトに対して変換処理をする形で実装します。 ちなみにこのマクロの実現のため Macro paradise を使っています。 マクロを使う場合は適宜 Macro paradise 利用のための設定を記載しておきましょう。

今回、マクロとクラスの定義については今回は以下のようにしてみました。 入力データは適当なパブリックデータを、 WHERE 句にはレコード数があまり多くなり過ぎないように適度に絞り込む形にしています。

...
  @BigQueryType.fromQuery(
    """
      |SELECT
      |  name, number
      |FROM
      |  [bigquery-public-data:usa_names.usa_1910_current]
      |WHERE
      |  year = 2015 AND gender = 'M'
    """.stripMargin)
  class USANames

  @BigQueryType.toTable
  case class InitialCount(initial: String, number: Long)
...

そして肝心の変換処理は以下のようになりました。 今回、人物の名前のイニシャルを取り出してその数をカウントするという処理を実装してみました。 Java SDK の時の apply() チェーンみたいなものはなく、非常にシンプルなコレクション操作風に実装できています。

    val transformed = extracted
      .flatMap(_.name match {
        case Some(n: String) => if (n.length >= 1) Seq(n.slice(0, 1)) else Nil
        case _ => Nil
      })
      .countByValue
      .map(kv => InitialCount(kv._1, kv._2))

ビルドツールに sbt を使っている場合は、以下のように runMain で実行できます。 (この時 --runner に DataflowRunner を指定する場合、 GCPAPI が有効になってなければ有効にせよなど色々例外が上がるかもです)

$ GOOGLE_APPLICATION_CREDENTIALS=/path/to/json_key sbt "runMain syucream.BqSample --project=<project> --runner=<runner>"

sbt を使って実行するのもだるいので、 sbt-pack を使って事前にビルドしたものを実行する例も示してみます。 ちなみに sbt-assembly は Scio 的には非推奨 なようです。。。

# build by using sbt-pack
$ GOOGLE_APPLICATION_CREDENTIALS=/path/to/json_key sbt pack

# run
$ GOOGLE_APPLICATION_CREDENTIALS=/path/to/json_key ./target/pack/bin/bq-sample --project=<project> --runner=<runner>"

runner に Dataflow を指定すると、 GCP の Dataflow の画面上でパイプラインを見ることができます。 今回の例の場合ですと処理のフローとしては BigQuery の入力が 1 つ、いくつかの変換処理を経て GCS と BigQuery の 2 出力を持つ、という形になります。 これがそれらしく図示されていて、どうやら途中でワーカが追加されオートスケールされていることが分かります。

f:id:syu_cream:20171020005438p:plain

おわりに

今回触った範囲ですと、 Scala + Scio で Apache Beam 、 Dataflow がかなり楽に動かせるように見受けられます。 もうちょっと複雑なパイプラインを書くとどうなるかはわかりませんが、今後に期待を寄せたいところであります。

AWS Lambda と CloudWatch を使ってローカルで動かしていたcronの処理をサーバレスにする

普通に生きていると「あっこのバッチ処理定期的に動かしたいな」と思うことがあると思われます。 自分もご多分に漏れずそうだったのですが、私物 PC で cron で回すのも煩わしい(外出中はスリープさせたいので)し、 VPS などでインスタンス借りるのも目的に対してやり過ぎ感があります。 というわけで今回は AWS Lambda + CloudWatch を使って置き換えてみました

前提として

最初は Google Cloud Functions でやろうと思っていたのですが、ただ cron の代替としてちょっとした処理を動かしたい為に結構な労力を払わされそうに見受けられました。

https://developers-jp.googleblog.com/2017/04/how-to-schedule-cron-jobs-with-cloud.html

AWS であれば自分にとって必要最小限のサービスで構成できそうなので乗り換えた次第です。 要件によっては GCP で構成しても良さそうかな・・・と思います。

手順

以下の順序で cron の処理の置き換えをしました。

  • バッチ処理を Node.js で再実装

    • 元々 10 数分で書いたシェルスクリプトだったのですが Lambda でサポートしている言語で再実装しておきました
    • Lambda で実行可能なコードであれば他の言語でも別に良い
  • CloudWatch にイベント登録

    • 以下画面からルールの追加画面に遷移し・・・

f:id:syu_cream:20171009231543p:plain

  • スケジュールをお好みの形で設定

f:id:syu_cream:20171009231655p:plain

  • 後は Lambda で関数が定期的に呼び出されていることを確認

    • 自分が書いた CloudWatch の設定では 1h に一回呼び出されることを想定したものであり、以下のような呼び出し回数グラフが得られました

f:id:syu_cream:20171009231901p:plain

料金

以上で実現したバッチ処理の置き換えですが、料金はどのくらいになるのでしょうか。 自分が書いた Lambda の関数呼び出しは大体 7s 前後で終わっているようで、これを 1h に一回呼び出す状態で運用しています。 この結果、 AWS 請求画面でレポートされる請求に関する情報は以下のとおりになりました。

f:id:syu_cream:20171009232144p:plain

無料枠に収まっているというか全然使いこなせていない感があります。

Mac で ngx_mruby 込みで nginx をビルドする際にいろいろハマってた

ちょっと前に解決した話なんですが備忘録的に書いておきます。 (2017/10/03 記述に漏れがあったので修正しました)

前提

最近の openssl の Formula は link を --force を付けたとしても禁止しています。理解はできるけどサクッとビルドしたい時にやや面倒・・・

$ brew link openssl --force
Warning: Refusing to link: openssl
Linking keg-only openssl means you may end up linking against the insecure,
deprecated system OpenSSL while using the headers from Homebrew's openssl.
Instead, pass the full include/library paths to your compiler e.g.:
  -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib

Homebrew の Formula が入らないよ問題

nginx-full Formula はナイスなことにオプションで ngx_mruby を加えてビルドすることができるようになっています。 http://hb.matsumoto-r.jp/entry/2016/02/10/224532

しかし最近以下の点でハマるようになっていました。

  • Homebrew がデフォルトで sandbox-exec してビルドするようになり、パーミッションの都合 --with-mruby-module ではビルドが通らなくなる
    • 一応 --no-sandbox すれば sandbox-exec しなくなるので回避できる
  • ngx_mruby のリンク途中でコケる

前者の問題は本質的には mruby-nginx-module の Formula の構成の問題です。 Homebrew で sandbox-exec してビルドする際、ビルド対象の Formula のディレクトリや /tmp などに書き込み権限を絞ってしまうため、現状の "nginx-full Formula から mruby-nginx-module Formula のビルドを行なう" ような操作が通らなくなります。 これは根が深い問題なので、今回はひとまず後者の問題を回避できるようにしました。

https://github.com/Homebrew/homebrew-nginx/pull/324

これで以下のようにすれば引き続き Homebrew で ngx_mruby をらくらくインストールすることができます。

# 残念ながら --no-sandbox は今のところ必要。しかたないね
$ brew install nginx-full --with-mruby-module --no-sandbox

ngx_mruby をソースからビルドする時困るよ問題

前述の通り openssl のインクルードパスなどを、 ngx_mruby の場合は nginx や mruby のビルド時にも渡るようにしてやらなければならないので一工夫がいります。 幸いにも ngx_mruby の configure, Makefile ではいくつか nginx, mruby のビルド時に任意のオプションを渡すための記述がされているのでそれらを駆使するのが良さそうです。

(2017/10/03 追記・修正) nginx の ビルド時には難ありです。

openssl のソースコードを手元に用意して、 ngx_mruby の --with-ngx-src-root にパスを渡すのがひとつのビルド方法に思われます。

./configure --with-ngx-src-root=/path/to/ngx_mruby/build/nginx_src --with-ngx_config_opt="--with-debug --with-http_stub_status_module --with-http_ssl_module --with-openssl=/path/to/openssl"
...

自分は面倒に感じてしまったので、 nginx の configure に以下のようなパッチを当てて、 Homebrew でインストールした openssl を参照するようにしてしまいました。。。

113a114,129
>         if [ $ngx_found = no ]; then
>
>             # Homebrew
>
>             ngx_feature="OpenSSL library in /usr/local/opt/"
>             ngx_feature_path="/usr/local/opt/openssl/include"
>
>             if [ $NGX_RPATH = YES ]; then
>                 ngx_feature_libs="-R/usr/local/opt/openssl/lib -L/usr/local/opt/openssl/lib -lssl -lcrypto $NGX_LIBDL"
>             else
>                 ngx_feature_libs="-L/usr/local/opt/openssl/lib -lssl -lcrypto $NGX_LIBDL"
>             fi
>
>             . auto/feature
>         fi
>

これを利用すれば以下の通りにビルドできます

$ ./configure --with-ngx-src-root=/path/to/ngx_mruby/build/nginx_src --with-ngx_config_opt="--with-debug --with-http_stub_status_module --with-http_ssl_module
...

$ NGX_MRUBY_CFLAGS="-I/usr/local/opt/openssl/include" NGX_MRUBY_LDFLAGS="-L/usr/local/opt/openssl/lib -lcrypto" make
...
checking for OpenSSL library ... not found
checking for OpenSSL library in /usr/local/ ... not found
checking for OpenSSL library in /usr/pkg/ ... not found
checking for OpenSSL library in /opt/local/ ... not found
checking for OpenSSL library in /usr/local/opt/ ... found
...

かなり遠回り感が出てしまいますが、自分の環境ではこれで無事ビルドできました

ところで

High Sierra では OpenSSL が LibreSSL に置き換わるらしいですね。こんな問題にはもうハマらなくて済むのか、あるいは別のハマりどころに結局苦しめられるのか・・・