データ指向アプリケーションデザイン

監訳者の@taroleoさん経由で発売前に頂いたのですが、分量が多く(約600ページ)内容もぎっしりで読むのに時間がかかってしまいました。紙媒体のものを希望してお送り頂いたのですが、あまりの厚さに持ち運びが困難なので電子版にすればよかったと若干後悔しました…。

データ指向アプリケーションデザイン ―信頼性、拡張性、保守性の高い分散システム設計の原理

データ指向アプリケーションデザイン ―信頼性、拡張性、保守性の高い分散システム設計の原理

近年クラウドの発展に伴い、小規模なアプリケーションといえども分散データシステムに関する知識が不可欠になってきました。AWSなどのクラウドプラットフォームでは手軽に分散ストレージや分散データベースを利用することができますし、WebアプリケーションとRDBを使うシンプルな構成のシステムでもクラウドプラットフォームの機能で冗長化が可能になりました。マイクロサービスアーキテクチャによるシステム構築にも分散システムへの理解が必要不可欠です。

このような時代において、分散データシステムに関する基礎知識を一冊で、かつ日本語で効率よくインプットすることができるということは非常に価値があります。600ページという分量もさることながら、各章毎に列挙された参考文献のリストからもこの書籍の密度がわかります。読破するのに時間はかかると思いますが、これだけの情報源に自分で当たることを考えればそれも納得というところでしょう。内容も分散システム固有の話だけでなく、前提知識となるデータモデル、ストレージ、レプリケーション、パーテーショニング、トランザクションといった基礎的なトピックがしっかりと押さえられており、データシステムに関わるソフトウェアエンジニアであれば読んでおいて損はありません。

自分自身、日本語で読んだ方が数十倍は効率が良いので日本語訳の出版には感謝しかありませんが、それでも1周では理解が追いついていない箇所が多いので繰り返し読み込みたいと思います。冒頭でも書いた通り、紙媒体のものはかなり重量があって持ち運びに向いていないのと、リファレンスに使うような本でもないので個人的には電子版がおすすめですw

GitBucket 4.32.0をリリースしました

Scalaで実装されたオープンソースのGitサーバ、GitBucket 4.32.0をリリースしました。

https://github.com/takezoe/gitbucket/releases/tag/4.32.0

Scala 2.13.0およびScalatra 2.7.0へのアップデート

今回のバージョンからGitBucketはScala 2.13.0およびScalatra 2.7.0でビルドされるようになりました。これはGitBucket上で動作する全てのプラグインScala 2.13およびSCalatra 2.7でビルドされたものでなくてはならないということを意味します。すべてのオフィシャルプラグインGitBucket Community pluginsで提供されているプラグインおよび以下のプラグインについてはすでにGitBucket 4.32.0で動作するバージョンがリリースされています。

この他のサードパーティプラグインについてはGitBucket 4.32.0では動作しません。もしそれらのプラグインを利用する必要がある場合はプラグイン側で対応が行われるまでGitBucket 4.32.0へのアップグレードを待ったほうが良いかもしれません。

また、Scala 2.13.0はScala 2.13系の最初のGAリリースであるため未知のバグや非互換などによる不具合が生じる可能性があります。GitBucketをクリティカルな用途に使用している場合はより安定した将来のリリースを待つか、十分にテストを行った上でアップグレードすることをお勧めします。

プラグインのネットワークインストールの廃止

GitBucket 4.32.0ではプラグインレジストリからのプラグインのネットワークインストールが廃止されました。従来のバージョンのGitBucketを使用している場合、引き続きプラグインレジストリからのネットワークインストールを利用できますが、プラグインレジストリは近い将来シャットダウンする予定ですので、その後は従来バージョンでのGitBucketでもプラグインのネットワークインストールは利用できなくなります。

GitBucket organizationでメンテナンスされている以下のプラグインについてはGitBucketに標準でバンドルされるようになり、デフォルトで利用可能な状態になります。

上記以外のプラグインについては手動でインストールを行う必要があります。

ドラフトプルリクエス

プルリクエストを作成する際にドラフトモードを選択できるようになりました。ドラフトモードで作成したプルリクエストはドラフトモードを解除するまでマージすることができません。

f:id:takezoe:20190808233239p:plain

プルリクエストがマージ可能な状態になったらドラフトモードを解除することができます。ドラフトモードを解除するとプルリクエストがマージ可能になります。

f:id:takezoe:20190808233255p:plain

コミットID間の差分比較

http://localhost:8080/user/repo/compare/commitId...commitId のようなURLでアクセスすることで差分比較ビューでコミットID間の差分表示ができるようになりました。

f:id:takezoe:20190808233313p:plain

プルリクエストのデフォルトプライオリティ

デフォルトとして設定されたプライオリティがイシューだけでなくプルリクエストにも反映されるようになりました。

f:id:takezoe:20190808233328p:plain

イシュー、プルリクエストタイトル編集時のフォーカス

イシューやプルリクエストのタイトル編集ボタンをクリックした際にタイトルのテキストフィールドにフォーカスするようになり、ボタンクリック後に即座にタイトルを編集することができるようになりました。

f:id:takezoe:20190808233343g:plain

今回のバージョンではこの他にも様々な改善やバグフィックスを行っています。詳細についてはIssueの一覧をご覧ください。

ThinkPad USBキーボード(前モデル)のトラックポイントスクロールをMacで使えるようにする

以下の記事でも書いた通り、以前はThinkPad USBキーボードの前モデル(7列キーボード)を愛用していたのですが、MacをHigh Sierraにバージョンアップしてからというもの、どういうわけかトラックポイントのセンターボタンでのスクロールが効かなくなってしまい長らく使用を断念していました。

takezoe.hatenablog.com

ところがふと思い立ってKarabiner Elementsについてググっていたところ以下のような記事を発見しました。

applech2.com

もしやと思いあれこれ試してみたところ、見事センターボタンによるスクロールを復活させることに成功したのでその設定をメモしておこうと思います。

まずはKarabiner ElementsのDevicesタブでThinkPadキーボードのマウスデバイス側のイベント変更を許可しておきます(ThinkPadキーボードは同じ名前でキーボードデバイスとマウスデバイスの2つが表示されているはずです)。

f:id:takezoe:20190806002656p:plain

上記の記事にあるように https://pqrs.org/osx/karabiner/complex_modifications/ から「Change mouse motion to scroll (rev 1)」をKarabiner Elementsにインポートし、「Change button4 + mouse motion to scroll wheel (rev 1)」を有効にします。

f:id:takezoe:20190806002726p:plain

最後にSimple ModificationsタブでThinkPadキーボードのマウスデバイス側で「button3」を「button4」にマッピングします。

f:id:takezoe:20190806002739p:plain

以上でKarabiner Elementsの設定は完了です。あとはMac側の設定でスクロール方向やマウスカーソルの移動速度、スクロール速度などを調節すれば普通に使えるようになると思います。

完全に使用を諦めてずっと積みキーボードと化していたのですがこれで再び活用できそうです。このモデルは日本語配列のものしか持っていないのですが、使えるとなるとUS配列のものも欲しくなりますね…。しかし現在では入手困難でたまに出回ってもプレミア価格となっているようです…。

TransmogrifAIを使ってPredictionIO用のAutoMLテンプレートを作ってみた

Apache PredictionIOは、SalesforceによってApache Software Foundationに寄贈されたオープンソース機械学習プラットフォームです。

PredictionIOは機械学習ワークフローの全プロセスをカバーし、エンジンテンプレートという雛形をベースにすることで機械学習を使用した予測Web API簡単に作成することができます。 すぐに使える様々なエンジンテンプレートが用意されており、その中から目的に合ったものを選ぶことができます。 ただし、エンジンテンプレートはあくまでテンプレートなので、使用するデータやアルゴリズムにあわせてScala / Javaコードを書いてテンプレートをカスタマイズする必要があります。

一方で、TransmogrifAIは、Salesforceによって開発されたApache Spark上で動作するAutoMLライブラリで、コーディングなしで最良のモデルを見つけることを自動化することができます。そこで、TransmogrifAIを使用してPredictionIO用のコーディング不要の機械学習テンプレートを作成できるのではないかと思い、作ってみました。

github.com

タイタニックのサンプルを実行してみる

テンプレートの使い方を見るためにタイタニックのサンプルを動かしてみましょう。その前にPredictionIO自体のセットアップは済ませておく必要があります。

まずはpioコマンドでアプリケーションを作成します。

$ pio app new MyAutoMLApp1
[INFO] [App$] Initialized Event Store for this app ID: 4.
[INFO] [Pio$] Created a new app:
[INFO] [Pio$]       Name: MyAutoMLApp1
[INFO] [Pio$]         ID: 1
[INFO] [Pio$] Access Key: xxxxxxxxxxxxxxxx

アクセスキーを環境変数に設定します。

$ export ACCESS_KEY=xxxxxxxxxxxxxxxx

イベントサーバーを起動します。

$ pio eventserver &

イベントデータをイベントサーバにインポートします。テンプレートのルートディレクトリで以下のコマンドを実行します。

$ python ./data/import_titanic.py --file ./data/titanic.csv --access_key $ACCESS_KEY

学習を行います。結構時間がかかります。

$ pio train

学習が終了したらWeb API起動します。デフォルトでは8080ポートで起動します。

$ pio deploy

実際にリクエストを投げて動作を確認します。

$ curl -H "Content-Type: application/json" -d '{ "pClass": "2", "name": "Wheadon, Mr. Edward H", "sex": "male", "age": 66, "sibSp": 0, "parCh": 0, "ticket": "C.A 24579", "fare", 10.5, "cabin": "", "embarked": "S" }' http://localhost:8000/queries.json
{"survived":0.0}

$ curl -H "Content-Type: application/json" -d '{ "pClass": "2", "name": "Nicola-Yarred, Miss. Jamila", "sex": "female", "age": 14, "sibSp": 1, "parCh": 0, "ticket": "2651", "fare", 11.2417, "cabin": "", "embarked": "C" }' http://localhost:8000/queries.json
{"survived":1.0}

データにあわせてカスタマイズする

カスタマイズするにはコードを変更する必要はなく、テンプレートに含まれているengine.jsonという設定ファイルを修正するだけです。

"algorithms": [
  {
    "name": "algo",
    "params": {
      "target" : "survived",
      "schema" : [
        {
          "field": "survived",
          "type": "double",
          "nullable": false
        },
        {
          "field": "pClass",
          "type": "string",
          "nullable": true
        },
        ...
      ]
    }
  }
]

データにあわせてschemaを定義し、予測対象のフィールド名をtargetに指定します。現時点ではtargetで指定するフィールドはdouble型である必要があります。

まとめ

Apache PredictionIOとTransmogrifAIはどちらもApache Sparkで動作し、Salesforce社によってオープンソース化された機械学習プロダクトです。組み合わせて利用することで機械学習のワークフロー全体を自動化することができます。もちろんこのAutoMLテンプレートには改善点も多々ありますが、組み合わせ的にはなかなか良いコンビネーションなのではないかと思います。

なお、このテンプレートの作成中、TransmogrifAIの主要開発者の1人であるMatthew TovbinさんからTwitter上でいくつかの有用なアドバイスをいただきました。また、TransmogrifAIのドキュメントでこの記事の英語版を紹介していただきました。ありがとうございました。

Databricks社がOSS化したSpark用ストレージレイヤ「Delta Lake」について

先日開催されたSpark + AI Summit 2019にあわせてDatabricks社からSpark用のストレージレイヤ「Delta Lake」のOSS化が発表されました。

databricks.com

GitHubリポジトリはこちら。

github.com

Delta LakeはSparkのライブラリとして実装されており、分散ストレージ上で以下のような機能を提供します。

実際に動かしてみる

Delta Lakeの動作にはSpark 2.4.2以降が必要です。ローカルファイルシステムでも動作するのでspark-shellで動きを確認してみました。

$ bin/spark-shell --packages io.delta:delta-core_2.12:0.1.0

まずはテーブルへの書き込みと読み込みをやってみます。

scala> val df1 = Seq((9, "Lacazette", "France"), (14, "Aubameyang", "Gabon"), (17, "Iwobi", "Nigeria"), (23, "Welbeck", "England")).toDF("number", "name", "country")
df: org.apache.spark.sql.DataFrame = [number: int, name: string ... 1 more field]

scala> df1.write.format("delta").save("/tmp/arsenal")

scala> val df2 = spark.read.format("delta").load("/tmp/arsenal")
df: org.apache.spark.sql.DataFrame = [number: int, name: string ... 1 more field]

scala> df2.show()
+------+----------+-------+
|number|      name|country|
+------+----------+-------+
|    14|Aubameyang|  Gabon|
|     9| Lacazette| France|
|    23|   Welbeck|England|
|    17|     Iwobi|Nigeria|
+------+----------+-------+

追記してみます。

scala> val df3 = Seq((10, "Ozil", "Germany"), (11, "Torreira", "Uruguay"), (15, "Maitland-Niles", "England"), (34, "Xhaka", "Swizerland")).toDF("number", "name", "country")
df3: org.apache.spark.sql.DataFrame = [number: int, name: string ... 1 more field]

scala> df3.write.format("delta").mode("append").save("/tmp/arsenal")

scala> val df4 = spark.read.format("delta").load("/tmp/arsenal")
df4: org.apache.spark.sql.DataFrame = [number: int, name: string ... 1 more field]

scala> df4.show()
+------+--------------+----------+
|number|          name|   country|
+------+--------------+----------+
|    15|Maitland-Niles|   England|
|    34|         Xhaka|Swizerland|
|    14|    Aubameyang|     Gabon|
|    11|      Torreira|   Uruguay|
|     9|     Lacazette|    France|
|    23|       Welbeck|   England|
|    17|         Iwobi|   Nigeria|
|    10|          Ozil|   Germany|
+------+--------------+----------+

タイムトラベルで最初のバージョンのテーブルを参照してみます。ここではversionAsOfというオプションでバージョンを指定していますが、timestampAsOfというオプションで日付を指定することもできるようです。

scala> val df5 = spark.read.format("delta").option("versionAsOf", 0).load("/tmp/arsenal")
df5: org.apache.spark.sql.DataFrame = [number: int, name: string ... 1 more field]

scala> df5.show()
+------+----------+-------+
|number|      name|country|
+------+----------+-------+
|    14|Aubameyang|  Gabon|
|     9| Lacazette| France|
|    23|   Welbeck|England|
|    17|     Iwobi|Nigeria|
+------+----------+-------+

スキーマバリデーションを試してみます。数値のフィールドに文字列を入れようとするとこんな感じでエラーになります。

scala> val df6 = Seq(("8", "Ramsey", "Wales")).toDF("number", "name", "country")
df6: org.apache.spark.sql.DataFrame = [number: string, name: string ... 1 more field]

scala> df6.write.format("delta").mode("append").save("/tmp/arsenal")
org.apache.spark.sql.AnalysisException: Failed to merge fields 'number' and 'number'. Failed to merge incompatible data types IntegerType and StringType;;
  at org.apache.spark.sql.delta.schema.SchemaUtils$.$anonfun$mergeSchemas$1(SchemaUtils.scala:526)
  ...

また、存在しないカラムを追加しようとすると以下のようなエラーになります。エラーメッセージにあるようにmergeSchemaオプションを付けるとスキーマを変更できます。

scala> val df6 = Seq((8, "Ramsey", "Wales", "Juventus")).toDF("number", "name", "country", "next_club")
df6: org.apache.spark.sql.DataFrame = [number: int, name: string ... 2 more fields]

scala> df6.write.format("delta").mode("append").save("/tmp/arsenal")
org.apache.spark.sql.AnalysisException: A schema mismatch detected when writing to the Delta table.
To enable schema migration, please set:
'.option("mergeSchema", "true")'.

Table schema:
root
-- number: integer (nullable = true)
-- name: string (nullable = true)
-- country: string (nullable = true)


Data schema:
root
-- number: integer (nullable = true)
-- name: string (nullable = true)
-- country: string (nullable = true)
-- next_club: string (nullable = true)


If Table ACLs are enabled, these options will be ignored. Please use the ALTER TABLE
command for changing the schema.
...

ファイルレイアウトや内部動作について

ファイルはテーブル毎に以下のようなレイアウトで書き込まれます。

f:id:takezoe:20190503131318p:plain

データはコミット毎に追加分がParquetで書き込まれ、_delta_logディレクトリに差分情報がJSON形式で保存されます。読み込む際は差分ログをリプレイして必要なParquetファイルを読み込みます。この仕組みによってACIDトランザクションJSONファイルの書き込みによってコミット)とタイムトラベル(JSONの差分ログをリプレイすることで任意の時点のデータを復元)が可能になっています。なお、トランザクションには楽観的排他制御が使用されており、別トランザクションで先に書き込みが行われた場合はトランザクションが失敗するようになっています。

このように差分を積み重ねていく方式のため、一度作成したParquetファイルを更新する必要がなく高速に書き込みを行うことができます。また、スキーマなどのメタデータJSONの差分ログ内に書き込まれるので、中央集約型のメタデータストアがボトルネックになることはないという設計のようです。

ただ、これだけだと履歴が増えるとParquetファイルも増えるので当然読み込みが遅くなります。そのため、_delta_logディレクトリ内にチェックポイント毎にスナップショットのParquetファイルが作成され、スナップショットが存在する場合は実際には直近のスナップショットに対してそれ以降の差分を適用するという動作になります。スナップショットはデフォルトでは10コミット毎に作成されますが、これはテーブルの性質によって調整する必要があると思います。

まとめ

Delta LakeはSparkに特化していることもあり、意外とコンパクトな実装になっています。スナップショットの作成などもSparkの機能が活用されており、実装としては思っていたよりもシンプルなものでした。今後レコードの更新・削除やデータのバリデーションなどもサポート予定のようです。

スナップショットをどんどん作っていくのでストレージの消費が大きくなりそうなのと、Spark(の特定バージョン)への依存性といったあたりが懸念材料でしょうか。ファイルレイアウトや仕組み自体はSparkに特化したものではないので他の分散コンピューティングエンジン向けのドライバを書くこともできそうです。

まだざっとコードを読みつつ手元で軽く動かしてみた程度なので見落としている部分もあるかもしれませんが、ひとまずこんなところです。ソースを読み込みつつもう少し研究してみたいと思います。

Scala用のLanguage Server「Metals」をAtomで使ってみる

MetalsはScalametaのサブプロジェクトとして開発されているScala用のLanguage Serverで、Visual Studio CodeAtomVimEmacsなどのテキストエディタScalaプログラミングを行う際にIDEのような機能を利用できるようにするものです。

scalameta.org

コード補完などの機能が実装されたMetals 0.5.0がリリースされたとのことなので早速試してみました。エディタとしては、フル機能は使用できないようですが普段使っているAtomを使ってみました。

セットアップは簡単で、Atomのパッケージマネージャで「ide-scala」パッケージをインストールするだけです(環境変数JAVA_HOMEが設定されている必要があります)。他にもいくつか必要なパッケージがありますが自動的にインストールされます。Visual Studio Codeでも同様に簡単にインストールできるようです。

パッケージの設定画面で使用するMetalsのバージョンを指定できます。

f:id:takezoe:20190414003701p:plain

パッケージをインストール後、build.sbtがあるディレクトリを開くとプロジェクトをインポートするかどうかを確認するダイアログが表示されます。Metalsは直接sbtを実行するのではなくBloopを使っているようです。Bloopについては以前ブログを書きました。

takezoe.hatenablog.com

インポートされない場合はコマンドパレットから「Metals: Import Build」というコマンドを実行すると強制的に再インポートを行うことができます。そこそこ時間がかかりますが、インポートが終わるとMetalsが提供する機能を利用できるようになります。

コンパイルエラーはエディタ上に表示されますし、CTRL + クリックでコードジャンプも可能です。

f:id:takezoe:20190414003719p:plain

ScaladocやJavadoc、メソッド定義などの情報をホバー表示することもできます。

f:id:takezoe:20190414003736p:plain

コード補完もちゃんと効きます。Visual Studio Codeだとパラメータヒントも表示されるようです。ただ、IntelliJのように同時にimport文を付け足してくれたりするような便利補完機能はないです。import文を再編成する機能もまだありません。

f:id:takezoe:20190414003809p:plain

なお、プロジェクトには.metals.bloopというディレクトリが作成されるので、.gitignoreに追加しておくとよいでしょう。

Bloopを使っているのでsbtプラグインに依存するプロジェクトだと問題があったり、機能面でもIntelliJの代わりに使うにはまだまだ厳しい感じがありますが、Ensimeの開発が止まってしまったのでテキストエディタを使ってScalaを書きたいという方には期待のプロジェクトでしょう。個人的にはEmacsでも試してみたいところです。

Java逆引きレシピ 第2版が発売されます

5年ほど前に翔泳社さんで共著で執筆させていただいた逆引きJavaレシピの改訂版が発売になります。

Java逆引きレシピ 第2版 (PROGRAMMER’S RECiPE)

Java逆引きレシピ 第2版 (PROGRAMMER’S RECiPE)

第一版の内容をベースにしつつ、大きなトピックとしてはJava 11、JUnit 5に対応しています。Java 11については執筆期間などの問題や、本格的な採用はこれからということを考えると情報がまとまっていた方がキャッチアップにも役立つのではということで、最後の章にまとめる形にしました。

今回の執筆にあたりJava 9以降の変更を改めて振り返ってみて、ここ数年でJavaも大きく変化しているということを実感しました。これまで言語として安定しているという点は良くも悪くもJavaの大きな特徴の1つでもあったわけですが、今後は開発言語としてJavaを採用する場合でもきちんと新しいバージョンをキャッチアップしていくことができるのか?ということを考えないといけないのかもしれません。

作業自体は昨年の夏頃から始めていたのですが、Java 11のリリースを待つ必要があり、だいぶ時間がかかってしまいました。また、改訂作業なので楽かと思いきや、第一版の原稿に校正作業で入れた変更を反映したり、正誤表の内容を反映したりといった事前作業だけで数ヶ月かかってしまったり、個人的にも転職を挟む形になってしまうなど、しんどい部分も多かったですがなんとか予定通り出すことができてようやく一区切りという感じです。