ブログのしゅーくりーむ

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

文フリで配布する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 に置き換わるらしいですね。こんな問題にはもうハマらなくて済むのか、あるいは別のハマりどころに結局苦しめられるのか・・・

mruby を使ってワンバイナリで Serverspec のテストを実行可能にするツール "sssspec" を作った

ながすぎる;よんでない

リポジトリは以下にあります。

以下のように spec を書いて…

def __main__(argv)
  describe file('mrblib') do
    it { should be_readable.by('yourgroup').by_user('you') }
  end
end

sssspec をビルドして…

$ rake

生成されたバイナリひとつで spec を実行することができます

$ ./sssspec
.
Total: 1
   OK: 1
   KO: 0
Crash: 0
 Time: 0.16 seconds

背景

Serverspec はプロセスの起動やパッケージのインストール状態、パッケージの存在確認などサーバの状態をテストするのに有用なツールです。

しかしながらこれをローカル実行するには当然 Ruby 処理系や関連 gem をインストールしておく必要があります。Serverspec の為だけに Ruby を導入する必要が出て来る場合、これはちょっとした障壁となると思われます。 また Serverspec は ssh 経由でリモート実行もできますが、構成上、自動化したい際はデプロイツールの都合、社内ルールの問題で利用できない可能性があります(と、ぼくは思ってるんですが、実際のところそういった制約はどの程度あるのだろう)

Serverspec の導入障壁を減らしたいと考えた僕は、 mruby-cli を利用してワンバイナリで実行可能にした Itamae であるところの MItamae を参考になんとかすることにしました。 そうだ、 mruby 、やろう。

構成など

必要なものたち

mruby で Serverspec を実行するために最低限必要なものは、以下のものと思われます。

  • Serverspec 本体の mrbgem
  • Specinfra の mrbgem
  • RSpec の mrbgem
  • mruby で spec を実行可能にする mruby-cli ベースのコード

これらのうち、 “Specinfra の mrbgem” については @k0kubun さんによって作られた mruby-specinfra が利用できそうです。 というわけで残り 3 コンポーネントを自前で用意することにしました。

mruby-rspec

mruby-rspec は mruby で RSpec を実行可能にする mrbgem です。

経緯の話をすると、当初は Ruby 向けの rspec-core などを mruby 向けに作り直そうと試みたのですが、 RSpec の既存資産がなかなか大きく、 mruby 向けに改修していく作業の途中で心が折れてしまいました。 現在は、限られた機能のみ mruby 向けに移植している(途中だったらしい) sagmor/mruby-spec をベースに mrbgem を実装しています。

mruby-rspec は未だかなり機能が少ないため、本記事の “sssspec” をまともに使い倒して行こうとすると、もう少し機能追加が必要なものと思われます。

mruby-serverspec

mruby-serverspec は本家 Serverspec の matcher, type をほぼ使いまわしで構成した mrbgems です。 ssh 経由でのリモート実行など一部のロジックは削っております。

この辺りは本家 Serverspec のコードを引っ張り出して無理やり mrbgems 化して、動かない箇所を片っ端から修正していくという大変泥臭い作業をして構成しました。

sssspec

sssspec は mruby-cli ベースのテンプレートを修正し、かつこれまで紹介した mrbgems を組み合わせて実現した、ワンバイナリで spec 実行可能なツールです。 実装は 9 割くらい mruby-cli のテンプレそのままで、あり解説すべき点は特に無いので省きます。 ちなみに名前は rrrspec を意識し、 “Single Shot ServerSpec” の略としています。

今後の展望

ひとまず動くところまで作りたかったので、 sssspec と周辺 mrbgems はかなり大雑把な作りになっています。まずは全体的にもう少しリファクタリング等していきたいです。

また現状 mruby-rspec の機能が少ないせいで実用に耐えない気がしています。個人的には少なくとも、 shared_examples が無いと辛いなと思っております。従ってそのあたりの RSpec 便利機能を mruby-rspec にも実装していきたいと思っております。

本記事と関係のない余談

ルビィの話書いてて思い出したんですが、 秋からラブライブ!サンシャイン!! の二期が放映開始されますね! ちなみに僕は渡辺曜ちゃんが好きです。

技術書典2でmrubyに関する薄い本をだします

表題のとおりです。

4/9(日) アキバ・スクエアで開催される 技術書典2 にて mruby に関する薄い本を出します! う-10 サークル名「なるはやでいい感じにやるマン*1です。 https://techbookfest.org/event/tbf02/circle/5686683802533888

主なターゲット読者を Rubyist として、 mruby の言語処理系周り (mruby-compiler とか RiteVM とか) や CRuby との差異 (khash とか GC とか) をいい感じに書いていければと思ってます。 もしご参加の際はぜひよろしくお願いいたします!

*1:なお原稿の進捗具合は、なるはやともいい感じとも言えない模様