Airframe: Lightweight Building Blocks for Scala

Taro L. Saito
Airframe
Published in
12 min readJan 18, 2018
Monterey, California

日本語ではまだ情報を用意していなかったので、Airframeについて紹介していきます。

AirframeはScalaでアプリケーションを作る際に便利な「道具」をオープンソースにしたものです。2016年から開始して少しずつ現在の形に整理し、簡単なプログラムを作るときはもちろん、現在ではTreasure Data社内でより複雑なScalaアプリケーションを構築する際に欠かせない構成要素(building block)となっています。

Airframeを開発したきっかけは、Google Guiceなど既存のDependency Injection(DI)ライブラリがScalaで使いにくいという理由からでした。Dependency injectionとはオブジェクトの構築をコード中に手書きで行うのではなくDIフレームワークに任せることで、プログラマの手間を省き、モジュールの切り替えを容易にするための仕組みです。しかし、Google Guiceを使ったとしてもオブジェクトのライフサイクル管理は別途実装する必要があり、また、Scalaの構文に@Injectアノテーションを埋め込むのが非常に煩わしくありました。そこで、以下の記事にまとめたように既存ライブラリのpros/consを検討した結果、1から作ってしまっても良いだろうという結論に至りました。

ScalaのためのDI AirframeのDIは、bind-design-buildの3ステップで実行できる非常に簡単なものですが、Google Guiceでサポートしているbindingの機能を全て網羅しています。また、Sessionを通してオブジェクトのライフサイクルを管理できるため、Session終了後にスレッドプール、データベースコネクション、http serverのシャットダウンを行う処理なども手軽に記述できるのが特徴です。Google Guiceユーザーは通常、Guiceを拡張して同等の処理を実装する必要がありますが(例えば、Facebook Prestoでは、airlift-bootstrapというライブラリを作成してGuiceを拡張しています)Airframeでは全ての機能が標準で備わっているのでこれ1つ済むのが強力です。

Scalaの新しいデザインパターン Airframeを使っていくうちに、アプリケーションのコンフィグ、リソース管理、サービスのmix-in、部分的なDIによるファクトリーの構築など、Scalaの新しいデザインパターンともいうべき知見も生まれてきました。必要最小限のコンポーネントの組み合わせでテストを書けるようになるだけではなく、冗長なオブジェクトの受け渡し処理の記述をAirframeに任せられるようになり、コードを対象ロジックのみに絞って簡潔に記述できるようになりました。

Mono-Repoへ Airframeの利用が浸透していくに連れ、以前から使っていたロギング、コンフィグ、JMXモニター、コマンドラインオプションパーサーなど、アプリケーションを作るときに必須のモジュールもAirframe内にmono-repo(単一レポジトリ)として統合していくのが都合が良いという発見もありました。各モジュールで細かなversionの違いを制御するよりは、Airframeと同時に使うライブラリは、Airframeとともにテストしリリースするほうが安全で信頼性が増し、なおかつ、一括で処理できるためリリースの手間も省けるからです。

リリースの自動化 現在、Airframeのコードはmasterコミットごとにsnapshot releaseをし、git tagをつけるとScala 2.12, 2.11, Scala.js用にクロスビルドされたライブラリがMaven Centralに自動でdeployされる仕組みになっています。以前は、sbt-releaseプラグインを使っていたのですが、各Scala version毎のテスト x モジュール分の処理が直列に実行されるためリリースだけでも2時間以上かかってしまっていました。現在はCIでテスト済みのmaster branchのコミットに対してgit tagをつけるだけなのでリリース作業はTravis CI上で10分以内に終了します。この自動リリース作業では、以前作成したsbt-sonatype プラグイン(現在2000以上のScalaプロジェクトが利用している)も使われています。かれこれ10年以上オープンソース活動をしていますが、このAirframeのリリース手順が現在もっとも簡便で高速なベストプラクティスになっています。Travis CI上でGPG署名などを行う方法に関しては以下のブログが参考になりました。

標準ビルディングブロック Mono-repo化したAirframeには、Treasure Dataで普段利用しているライブラリを逐次追加しています。全てについて触れるのは難しいので、ここでは「なぜ」それらのライブラリが必要だったかのかを紹介していきます。

airframe-log: slf4jなど広く使われているロギングライブラリはclasspathにあるlogger実装(slf4j-nop, logbackなど)でログの出力先を切り替える実装になっています。しかし、dependency hell(主にHadoop周り)により複数のlogger実装が含まれてしまい制御が困難、XMLによる設定(logback独自の仕様)が覚えられない、debugログを増やすと性能に影響を与える、毎回Logger.getLoggerと書かなくてはいけないなど、非常に手間のかかる仕事でした。そこでScala Macroを用いてロガーの挿入の自動化、debugログを表示しない場合の性能負荷をなくし、カラフルかつ、ソースコードの行番号まで表示できる強力なロガーが誕生しました。実際、目でみるロガーとしては非常に便利です。

airframe-config: アプリケーションの設定をする際、デプロイ先の環境固有の設定は複数環境分まとめてYAMLファイルに記述してDockerイメージに焼いてしまいたいが、パスワードなどのcredentialは外部から渡したい状況があり、そのフローを整理し、最終的にScalaのcaseクラスとしてconfigを読む必要がありました。設定のデフォルト値はcaseクラスのデフォルト値で設定(Scala reflectionで読む)し、最中的にデフォルト値 or YAML or 外部パラメータの組み合わせでアプリケーションを設定することが容易になりました。複数環境にデプロイする際も、環境の指定+外部パラメータで制御し、Dockerイメージは1つ用意するだけで済んでいます。

airframe-opts: コマンドラインパーサー。個人や社内で長らく使っていたものをAirframeに組み込みました。functionやそのパラメータに対するannotationでコマンドラインの指定ができ、ユーザーの入力したコマンドライン引数が関数のパラメータに自動でマッピングされるので手軽にコマンドラインプログラムを定義することができます。sbt-packと組み合わせるとportableなScalaアプリケーションパッケージが作成でき、dockerコンテナを起動する際のコマンドライン処理にも使っています。

airframe-metrics: 実行時間、time range、データサイズなど、人間にとって入力しやすい、理解しやすい形で扱うニーズが多々ありました。そこで10m (10分), -7d (過去7日間)、10GBなどの文字列をそのままデータとして扱えるためのライブラリを作りました。モニタリング用途や、時系列データの処理の際に重宝しています。

airframe-surface: Scalaの他のJVM言語にはない強力な特徴がscalasigというクラスファイルに埋め込まれた詳細な型情報です。これがあると、関数やクラスの引数名、型、Genericsの型まで完全な形で実行時に知ることができます。airframe-surfaceはこの情報を実行時に取り出すためのライブラリで、airframe-optsが関数のパラメータ名に値を代入して呼び出せるのも、AirframeのDIが様々な型のbindingに対応できるのも全てsurfaceの機能です。現在の形にいたるまで紆余曲折があったライブラリです。当初(実は2012年頃からコードがある)はscalasigを直接parseしていたために複雑なコードになっていましたが、Scala reflection, Scala macroを上手に使えるようになり現在は比較的シンプルな実装になりました。Scalaの内部で型がどのように扱われているかを理解するのに当時非常に苦労しました。

airframe-codec: プログラムで作成したデータを保存するのにどんなデータフォーマットを使いますか?JSON, XML, CSV, それともParquet, ORC? Treasure DataではMessagePackを使います。基本的にプログラム中で扱うモデルクラスと、外部からやってくるデータフォーマットの型(例えば、CSVはタブ区切りのstring型データの羅列でしかない)は完全には一致しません。stringをintegerとしてparseして扱ったり、floatをintに丸めたりなどの変換が必須です。MessagePackではデータ型を自己で記述する形式のデータフォーマットであるため変換先の型が分かっていればデータ型のマッピングが可能です。さらに、MessagPackから目的のデータ型への変換ルール集を用意すれば、MessagePackを鎹(かすがい)として、CSV -> MessagePack -> Object -> MessagePack -> JSONなどというオブジェクトのserialization/deserilzationを挟んだデータの自動変換が可能になっています。Objectへのマッピングルール生成には上記のsurfaceも活躍しています。

データ変換のコンセプト的にはembulkが近いですが、airframe-codecは主に「プログラム中」から様々なデータを扱い、自在に保存できる様にするためのライブラリです。まだAPIはstableではありませんが、このアプローチは実際使ってみるとプログラムの実行状態のスナップショットを取るのに役立ったり、型の変更にも強いなど、非常に便利でした。今後、オブジェクト変換時のエラー処理、RPC用途、Scala.js経由でブラウザから使うことも想定して、より安全にデータ変換を可能にするためのPure-Scala MessagePackの実装も進行中です。

他にもairframe-jmx (JMX経由でプログラムの状態を監視)、airframe-jdbc (再利用可能なJDBCコネクションプール実装)などもあります。

Airframeの今後 AirframeのDIとしての機能はプロダクションでも利用されて十分安定しており、今後大きな変更はないと思われます。社内ではApache SparkとAirframe DIを組み合わせて利用している例もあります。より細粒度のSession(http request scope, thread-local scope)などを管理する際に若干拡張があると思われますがAPIは現在の形のままのはずです。その他、airframe-log, airframe-configなどもコードや使い方そのものは安定しています。今後大きく変わる可能性があるのがairframe-codecで、データにエラーがあったときの処理(退避するか無視するか)、高速化、event-drivenでのMessagePackの処理、データの変換レイヤーとしての使いやすさ、などいくつか課題が残っています。当面はairframe-codecのAPIの安定化、pure-scala MessagePackの実装が主な開発になります。その他、distributed tracingへの応用、fluentdとの連結、それ以外にもまだまだ野心的なアイデアもありますが、それはまたの機会に。

コミュニティ 現在のところ、Airframeはオープンソースではあるものの、個人や社内でのユースケースが主なターゲットで、PRなどによるGitHub上でのコミュニティ活動は盛んではありません。airframe-codecなどのAPIが安定するまではそれで良いという面もあります。Airframeを使ってみて、こう拡張したら良い、この機能があると便利というアイデアは歓迎しますし、PRベースでの改良案になるとより具体的で助かります。リリース作業などは先に述べたように自動化されているので、オープンソースコミュニティ運営のための準備は比較的できている方だと思います。当面の課題は、ここにあげた様なライブラリで解決できる問題が外部にどれだけあるかを把握し、それに加えてどのようなニーズがあるかを共有することでしょうか。GitHub、あるいはGitterチャンネルなどで御意見お待ちしております。

--

--

Taro L. Saito
Airframe

Ph.D., researcher and software engineer, pursuing database technologies for everyone http://xerial.org/leo