Java
AWS
mecab
JNI
lambda

AWS Lambda で JNI を利用する(MeCab を例として)

More than 1 year has passed since last update.

AWS Lambda では Java コードを実行できるが、JNI 経由でネイティブコードを呼べるだろうか?試してみよう。

例として使うのは日本語形態素解析エンジンの MeCab である。Java から MeCab を呼び出す Lambda 関数を作成することを目指す。


問題となる点

問題点 1: MeCab をどこにインストールするか。

Lambda の実行環境は普通の Amazon Linux と考えてよい。Lambda だからといって禁止されていることはあまりないが、新たにパッケージをインストールすることはできない。ではどうするかというと、ビルドした MeCab バイナリを Lambda にアップロードする .zip ファイルに同梱するのだ。

問題点 2: インストールした MeCab をどうやってリンクするか。

MeCab は動的ライブラリ (.so) としてインストールされる。これをメインとなる言語から利用するには実行時にリンクできなくてはならない。

最初に思いつくのは、環境変数 LD_LIBRARY_PATH を .zip を展開したツリー中の lib ディレクトリに設定して別プロセスを起動するという方法だ(Python での例だが「AWS Lambda で MeCab を動かす | Developers.IO」という記事の方法)。しかしこの方法はプロセス起動のオーバーヘッドが大きいため、できれば避けたい。

AWS Lambda で MeCab を動かす(改) - Shogo's Blog」では Python の動的リンクモジュール ctype を使ってフルパスでライブラリファイルを指定してリンクしている。この方法を採用するとオーバーヘッドは無くなるが、依存関係のある複数の動的ライブラリを .zip に同梱した場合にややこしいことになるだろう(たとえば Cabocha + MeCab)。依存元のライブラリを動的リンクしたときに、依存先のライブラリを探すパスが決定できないからである(最初に述べた LD_LIBRARY_PATH を設定する方法なら可能だ)。

LD_LIBRARY_PATH を設定する方法はお手軽だ。オーバーヘッドを考えなければこの方法で十分である。Developers.IO の記事と同じように MeCab をインストールして実行対象の .jar ファイルを作成して .zip に同梱する。そして LD_LIBRARY_PATH=XXX java -jar handler.jar のようなプロセスを Python から生成すれば良い(Lambda の Python 環境でも Java ランタイムはインストールされていて java コマンドが利用可能)。

ここではプロセス作成のオーバーヘッドを避けるために、Java のライブラリ動的リンク機能である System.loadSystem.loadLibrary を使う方法を模索しよう。


Lambda Java 環境の調査

前記の記事では、lib ディレクトリの決定のためにカレントディレクトリの絶対パスを使用している。Lambda の Python 環境では .zip ファイルが /var/task に展開されて、/var/task がカレントディレクトリの状態で .py ファイルが実行される。Java 環境でも同様だろうか?

System.getProperty("user.dir");  // => "/"

new java.io.File(".").getAbsoluteFile().getParent(); // => "/"

驚いたことに、Java の場合は Python の場合とは異なり .zip が展開されるディレクトリではなくルートディレクトリにカレントディレクトリが設定されて実行されることが分かった。

Python と同様の方法が使えないことが分かったので環境変数でもリストアップしてみようか。

System.getenv().toString();  // =>

// CLASSPATH=/var/runtime:/var/runtime/lib/LambdaSandboxJavaAPI-1.1.jar
// LAMBDA_TASK_ROOT=/var/task
// LD_LIBRARY_PATH=/lib64:/usr/lib64:/var/runtime:/var/task:/var/task/lib

なんと LD_LIBRARY_PATH が設定されていて、しかも .zip が展開される先の /var/task/lib が含まれている!つまり Python の例のようにフルパスで動的ライブラリをロードする(これは Java では System.load を使う)必要はなく、システムの lib ディレクトリにライブラリファイルが存在するかのように名前だけで動的リンクできる(System.loadLibrary を使う)。Cabocha + MeCab の場合のように存関係のある複数の動的ライブラリを同梱しても問題が起きない。


問題となる点(再)

というわけで /var/task/lib に動的ライブラリが配置されるようにすれば良いことが分かったので、以下の問題点が解決される。

問題点 3: MeCab をどのように configure するか。

.zip のルートとなるディレクトリを --prefix= で指定すれば良い。このとき、例えば開発環境で --prefix=/home/ec2-user/mecab-lambda と指定して ~/mecab-lambda ディレクトリにコードをビルドしたとしよう。このディレクトリの内容を .zip ファイルにまとめて実行時には /var/task に展開されるわけである。

この場合問題となるのは、MeCab コード中で prefix として指定された絶対パスを利用してファイルアクセスなどを行うと、実行環境では失敗してしまう可能性があることである。

実際に MeCab では設定ファイルのパスを構成するために prefix が利用され、設定ファイルの内容にも prefix が埋め込まれている。上記の Python の例では MeCab をコード中から呼ぶ際のパラメーターとして設定ファイルや辞書のパスを実行時に生成して渡すことでこの問題を回避している。

しかしそこまでしなくても、展開先ディレクトリは /var/task であると決め打ちした方がよいのではないだろうか。というのは Java の場合、Python でのようにカレントディレクトリで展開先ディレクトリのパスを取得することはできず、環境変数 LAMBDA_TASK_ROOT を参照するしか無く1、この時点で undocumented な仕様に依存しているからである。どうせ依存するなら /var/task 決め打ちでいいのではなかろうか。

これらの知見を元に以下では実際に動作する Lambda 関数を作ってみよう。


開発環境

Lambda 上で動作するネイティブライブラリをビルドするには指定された Amazon Linux のイメージを利用する必要がある。具体的なイメージ ID は「Lambda 実行環境と利用できるライブラリ 」に記述されているのでそのイメージを用いて EC2 インスタンスを作成する。

まず開発用ツールを導入する。

$ sudo yum groupinstall 'Development Tools' -y

$ sudo yum install java-1.8.0-openjdk-devel -y
$ sudo yum erase java-1.7.0-openjdk -y

MeCab パッケージのソースを用意する。

$ mkdir ~/make

$ cd ~/make
$ curl -L 'http://mecab.googlecode.com/files/mecab-0.996.tar.gz' | tar zxf -
$ curl -L 'http://mecab.googlecode.com/files/mecab-ipadic-2.7.0-20070801.tar.gz' | tar zxf -
$ curl -L 'http://pkgs.fedoraproject.org/repo/pkgs/mecab-java/mecab-java-0.996.tar.gz/e50066ae2458a47b5fdc7e119ccd9fdd/mecab-java-0.996.tar.gz' | tar zxf -


Lambda 関数の作成と実行

以下のコードを Lambda 関数にしよう。

public class MeCabLambda {

static {
System.loadLibrary("MeCab");
}
public String handler(String text) {
return new org.chasen.mecab.Tagger ("").parse(text);
}
}

このコードを実行する Lambda 用の .zip ファイルを作成してみよう。

$ sudo mkdir /var/task

$ sudo chown $USER.$USER /var/task
$ cd /var/task
$ vi MeCabLambda.java # 中身は↑

/var/task ディレクトリに MeCab をインストールする。

$ cd ~/make/mecab-0.996

$ ./configure --prefix=/var/task --with-charset=utf8
$ make clean && make install

MeCab 辞書をインストールする。

$ cd ~/make/mecab-ipadic-2.7.0-20070801

$ ./configure --prefix=/var/task --with-mecab-config=/var/task/bin/mecab-config --with-charset=utf-8
$ make clean && make install

Java バインディングをインストールする。

$ cd ~/make/mecab-java-0.996

$ vi Makefile # INCLUDE= の行を書き換える
INCLUDE=/usr/lib/jvm/java/include
$ make clean
$ PATH=/var/task/bin:$PATH make
$ cp libMeCab.so /var/task/lib
$ cp MeCab.jar /var/task/lib

MeCabLambda をビルド。

$ cd /var/task

$ javac -cp lib/MeCab.jar MeCabLambda.java

アップロード用 .zip の作成。

$ zip -r ~/MeCabLambda.zip .

以下の手順で Lambda 関数を作成する。


  • Lambda のコンソールへ行く。

  • [Create a Lambda function] をクリックする。

  • [Select blueprint] で [Skip] をクリックする。

  • [Name] に "MeCabLambda" を入力する。

  • [Runtime] から [Java 8] を選択する。

  • 手元のローカルマシンや S3 を経由して ~/MeCabLambda.zip をアップロードする。

  • [Handler] に "MeCabLambda::handler" を入力する。

  • [Role] に Basic execution role を指定する。

  • [Next] をクリックする。

  • [Create function] をクリックする。

以下の手順で Lambda 関数をテスト実行する。


  • [Test] をクリックする。

  • 表示されている JSON を "サンプルテキスト" で置き換える(要クオーテーション)。

  • [Save and test] をクリックする。

実行は成功し結果が表示される。


課題


  • Lambda の undocumented な挙動に依存している。


    • 環境変数 LD_LIBRARY_PATH の設定に依存。

    • .zip ファイルが展開されるパスを決め打ち。



なお、後で調べてみると Python 実行環境でも LD_LIBRARY_PATH は同様に設定されていることが分かったので、本記事と同様にすれば MeCab を呼ぶ Python コードではライブラリのリンクのことなどは何も気にしなくてもよいことになる。シンプルじゃない?





  1. MeCabLambda.class.getResource("MeCabLambda.class") を使えばできそう...