最も重要なプロダクション・コードにLuaを使う : PythonからLuaへの移行

Distelli Agentは今、実行ファイルを1つダウンロードするだけで簡単にインストールできます。この実行ファイルを使うと、適切な管理プロセスを持つエージェントをインストールしたり、新規にリリースしたものをアップロードしたりできるようになります。また、このエージェントはプラットフォームを問わず利用可能で、わずかな容量のCPUやメモリフットプリント(1パーセント未満のCPU使用率、10メガバイト以下のメモリ)で動作します。

私たちは、上記のパフォーマンスをある程度信用した上で、PythonからLuaに移行することにしました。

Pythonで挑む

私がDistelliに入社したのは、2014年の12月のことです。当時、Distelli Agentやコマンドラインツールは、システムで標準となっているバージョンのPython(サポートバージョンは2.4から2.7)を使う数個のターボールとして実装されていました。私はDistelliに入るまで、本格的にPythonを使ったことがなかったので、いくつか学ばなければいけないことがありました。例えば、Python2.7で自らコードを書いて、それをテストした時は、旧バージョンのPythonとの非互換が発生して行き詰まりました。finallyブロックとexceptionブロックを一緒に使おうとしたのですが、こういったシンタックスは2.6以前のバージョンでは非対応だからです。そしてこの頃から、私たちはPython 3.xを使おうとする人々から、サポートチケットを受け取るようになりました。早々に3.xのサポートも開始した方が良いのかもしれません。

さらに当時は、「ネイティブのPythonライブラリを使えればいいのになあ」と思っていました。例えば、ホストコンピュータのイーサネットアドレスの検出機能を追加でサポートする必要があったのですが、これはnetifacesのネイティブライブラリを使う(または、Windowsとの互換性はありませんが、shell上でifconfigコマンドを実行して結果を解析する)と実現できます。

従来のエージェント実装においても、デプロイを個々のステップに分割して、その一部をsupervisordを使って実行していましたが、残念ながら、その影響でコードが遅くなり、扱いづらくなっていたのです。

そういうわけで、私はシステムバージョンのPythonに頼る方針はダメだと悟りました。コードを書き直す時期が来ていたのです。

Luaを評価する

私はかねてから、Luaは魅力的な言語だと思っていました。私は何事においても”軽く速く”を重視するタイプで、ランニングや登山、ロッククライミング、バイクなどが大好きです。これらのレジャーはどれも、装備(もしくは胃の中)を軽量化することが、大きな違いを生みます。そしてLuaも”軽く速く”を目指すプログラミング言語です。私が初めてMike PallのLuaJitプロジェクトに興味を持ったのは、それがコンピュータのベンチマークサイトでトップの座に輝いていた頃です。そのサイトは、Luaがどれほど速く動くのかを示すとともに、完成したプログラムがどのプラットフォーム上でも1メガバイト以下になるということを実証していました(つまりLuaがどれだけ軽量かを示していたのです)。

Luaの好きなところは、無理に適合性を追求せず、最低限必要な機能に絞っている点です。例えばLuaで提供されているメタファイルを使うと、演算子の多重定義やオブジェクト指向のプログラミングができます。このように、すべてが1つにまとまったシンプルな作りになっているのです。さらにコルーチンもサポートされているので、”グリーンスレッド”も実装できます。

標準ライブラリに含まれる関数が200個未満であれば、軽量化は簡単です。しかしXMLパーサやJSONパーサ、HTTP(S)ライブラリなどを持っていない場合は、一体どう作業したらよいのでしょう?

問題を片づける

解決策として、LuaからC言語のライブラリを使ってみましょう。Luaの世界において、C言語は優等生です。LuaとC言語のインターフェースは非常に優れていて、きちんと文書化もされています。よってLuaJitを使うと、さらに一歩踏み込んだことができます。Foreign Function Interfaceを利用して、C言語のコードへのインターフェースをLuaのコードで書けるようになるのです。

Luaの標準ライブラリではPOSIX Cライブラリを使って、移植を可能にしています。これは同時に、ソケットが使えず、シグナルやタイマーもサポートされておらず、OSの呼び出しによってスレッドが必ずブロックされてしまうことも意味しています。POSIX Cライブラリを使うのは、時代に逆行するようなものなのです。

libuvを覗いてみましょう。libuvは完全に非同期式のマルチプラットフォームライブラリです。もともとは、node.jsをサポートするために書かれたものでしたが、今では多くの言語の動作をサポートするのに使用されています。Tim Caswell(またの名をcreationix)は、「luv」と呼ばれるこのライブラリへのバインディングを書きました。次に、彼は軽量化した「luvi」と呼ばれる”ベースとなる”実行ファイルを作成しました。これは、Luaインターフェースを有するさまざまなCのライブラリから構成されています。このすべてを静的にライブラリにリンクすることができます。このライブラリは、5メガバイト未満で、libuv, zlib, openssl, pcreなどのライブラリ、そしてもちろん、上で述べたライブラリへのバインディングを有するLuaランタイムも組み込まれています。

Luaコードを実行するためにluviを使うことはできますが、直接バイナリにコードをバンドリングするためのビルトインサポートも付属しています。これを使用すればとても簡単にLuaコードをリリースすることができます。2014年11月に、このプロジェクトの存在に気が付き、その経過を追い始めました。

PythonからLuaへの移行

2015年2月、”ライブ”ログをエージェントに追加するための開発に取り掛かりました。その時点で、2つの基本となる実装のアイデアがありました。1つは、「実際に使用されている/されていないを問わず、サーバにいつでもログをストリームする。そこへ、私たちのウェブインターフェースが中間のデータストアに問い合わせをするようにする」という案でした。もう一つのアイデアは「エージェントが直接リクエストに応じるようにする」というもので、中間のデータストアの維持費の削減が見込まれます。私たちは後者の解決策を採用しました。

現在のエージェントはデプロイメントに対して約10秒ごとにポーリングを行っています。しかし、ライブログをサポートするためには、より早く反応できる何かが必要になります。これはluviを導入する良いきっかけでした。そこで私は、バックエンドのサーバに接続し続けようとし、ウェブインターフェースが直接エージェントにリクエストを行うような、ちょっとしたプログラムを書きました。このプロジェクトは成功を収め、luviでの構築に自信が付きました。しかし、利用者側で3つのプロセス(python supervisord, python agent, luvi)を並行して常に実行させ続けることには、納得していませんでした。エージェントによるフットプリントは最小限にしたいと誰もが考えました。

2015年3月、Windowsをサポートして欲しいという顧客と出会いました。これはエージェントコードをLuaに移植する絶好の機会(口実かも)だと思いました。そして5月1日、手直ししたエージェントの最初のバージョンが使用できるようになりました。それ以後も、進化と改善を続けています。

その後、半年かけてPython CLIとエージェントのコードベースをすべてLuaに移植しました。バージョン3.59にも素早く適用することができ、今ではそれを使って、ソフトウェアを開発したり、重要なデータを暗号化したりすることができます。

まとめ

これは正しい決断だったのでしょうか? 全般的に見れば、正しかったと思います。例として、エージェントをインストールするのは、以前よりはるかに簡単になりました。実行ファイルを1つダウンロードするだけで良いのです。

エージェントの実行時のフットプリントは最小限に抑えられています。通常、メモリの使用量は10メガバイト以下、CPUの使用率は1パーセント未満となっています。エージェントのバージョンを2.xから3.xに切り替えた時には、デプロイメントは驚くほど速くなりました。

エージェントのメンテナンスも簡単です。ネイティブライブラリを使っているので、luviを単に更新するだけでよいのです。例えば今は、libyamlやlibsqlite3をluviからのフォークにバンドルしています。Cのコードをビルドする時間と複雑さは、luviのコードベースの随時アップデート時、あるいは、新しいネイティブライブラリが追加された時に限定されています。一度ベースとなるluviを構築すれば、新しいバンドルはあっという間に作成できます。つまり、最終的な実行ファイルにluaのコードを含めて、コンパイルし圧縮するだけです。

そしてようやく、Distelli Agentの移植ができました。このおかげで、SmartOSに対してもエージェントのビルドをスムーズに進めることができました。