キーワード:golang, バックグラウンド実行, detach
Firefoxのアドオンは、現在はWebExtensionsというAPI群に基づいて開発するようになっています。 このAPI群には「別のブラウザなどの任意のローカルアプリケーションを直接起動する機能」は含まれておらず、そのようなことをしたい場合にはNative Messagingという仕組みで間接的に実現する必要があります。 これは要するに、「コマンドラインオプションやGUIではなく標準入力から与えられる情報を使って、任意のアプリケーションを起動するランチャー」を開発するということです。 関係を図にすると以下のようになります。
+---------------------+
| Firefox |
|+-------------------+|
||Firefox上のアドオン||
|+-------------------+|
| ↓↑ |
| <WebExtensions API> |
| (Native Messaging) |
+--------↓↑---------+
<標準入出力>
↓↑
+---------------------+
| ランチャー |
+---------------------+
↓
<システムコール>
↓
+---------------------+
|外部アプリケーション |
+---------------------+
このNative Messagingの仕組み自体は、細部を除けばGoogle Chrome用拡張機能での仕組みとほぼ同じ仕様です。 そのためFirefoxとChromeに両対応した実装が既にいくつか存在しており、中には上記のようなランチャーとして振る舞う物もあります。 例えばOpen InというプロジェクトではNode.jsベースで開発されたランチャーアプリケーションの実装を使っています。
この記事では、これと似たような物をGo言語で実装する時の注意点を解説します。
Go言語では、外部アプリケーションを起動する方法としてexec
パッケージを使うのが一般的です。
このパッケージでは同期実行(外部アプリケーションの終了を待ってから次の処理に進む)のexec.Command().Run()
と非同期実行(外部アプリケーションを起動した後、すぐに次の処理に進む)のexec.Command().Start()
の2つの機能が提供されています。
例えば、FirefoxのWebページ上のコンテキストメニューに「このページをInternet Explorerで開く」のような項目を追加してそこから別のブラウザを起動するというような場合、exec.Command().Run()
でIEを起動するとIEを終了するまでの間ずっとランチャーアプリケーションのプロセスが生き続けることになります。
また、そこから起動されるIEのプロセスはFirefoxから見て孫プロセスという扱いになりますので、うっかりその状態でFirefoxを終了すると、孫プロセスになっているIEまでもがまとめて終了されてしまいます。
ということから、このような場面ではexec.Command().Start()
の方を使えばよいと考えられるのですが、実際には期待した通りの結果になりません。
こちらで起動した場合でもIEのプロセスは孫プロセスになってしまい、ランチャーがexec.Command().Start()
を実行してIEを起動した後でmain()
の最後に到達してプロセスが終了すると(Firefoxがランチャーを終了させると)、やはり孫プロセスのIEまで巻き添えで終了されてしまうのです。
このような現象が発生するのは、Firefox・ランチャー・孫プロセスとして起動されたIEの全てが同じプロセスグループに属しているからです。 言い換えると、ランチャーが起動する孫プロセスについてプロセスグループを分ければ(プロセスをデタッチすれば)、Firefoxやランチャーが終了した後もIEを動作させ続けられると考えられます。 Go言語の場合、一般的にはこれは以下のようにして実現できます。
...
import (
"os/exec"
"syscall"
"log"
)
...
func Launch(path string, args []string) {
command := exec.Command(path, args...)
// Windowsの場合
command.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP}
// Linux, maxOS (POSIX)の場合
// command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
err := command.Start()
if err != nil {
log.Fatal(err)
}
}
システムコールに渡すパラメータを指定する必要があるため、Windowsとそれ以外の環境とでは書き方が変わってきます。
このようにすることで晴れてプロセスグループが分かれてくれて、ランチャー終了後も外部アプリケーションが生き続けるようになってくれる……と思われたのですが、実際にWindows環境のFirefoxで検証してみたところ、残念ながら期待通りの結果は得られませんでした。
上記のようなコードを使って一般的なコマンドラインアプリケーションとして起動する状態にしたランチャーで試す分には、期待通りの振る舞い(ランチャーの終了後も外部アプリケーションのプロセスが残る)を見せました。 しかし、FirefoxのアドオンからNative Messagingの仕組みを経由して起動されるNative Messaging Hostとしてランチャーを動作させると、依然として外部アプリケーションまで終了されてしまうのでした。
これは、Go言語一般の話や、Google Chromeなどと共通の仕組みとしてのNative Messagingではなく、Firefox固有の事情による現象です。
実はWebExtensionsにおけるNative Messagingの説明に記載がありますが、Windowsにおいてこのようなランチャーから外部のプロセスを起動する際は、CREATE_NEW_PROCESS_GROUP
ではなくCREATE_BREAKAWAY_FROM_JOB
という定数で示されるフラグを指定する必要があります。
Go言語の場合はsyscall
モジュールにこの定数の定義が含まれていないため、仕様に基づいて0x01000000
という数値を直接記述することになります。
func Launch(path string, args []string) {
command := exec.Command(path, args...)
// CREATE_BREAKAWAY_FROM_JOB = 0x01000000
command.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x01000000}
err := command.Start()
if err != nil {
log.Fatal(err)
}
}
WebExtensionsの元になっているGoogle ChromeのNative Messagingの仕様では特にこのような指定は必要ないため、ChromeとFirefoxの両方に対応したNative Messaging Hostを開発する際には注意が必要です。
このようにフラグを指定して実行すると、無事期待する通りの結果を得ることができました。
(この情報は本記事の初版に対するフィードバックで教えていただきました。ご指摘ありがとうございます。)
結論から述べると、この問題は以下のようなバッチファイルで解決できました。
@ECHO OFF
start %*
これは、自身にコマンドライン引数として渡された内容をそのままコマンド列として非同期に実行するというバッチファイルです。 ランチャーから外部アプリケーションを起動する際に、直接起動せずこのバッチファイル(※正確にはcmd.exe)を介して起動するという使い方をします。
この時の各ソフトウェア同士の関係を図にすると、以下のようになります。
+---------------------+
| Firefox |
|+-------------------+|
||Firefox上のアドオン||
|+-------------------+|
| ↓↑ |
| <WebExtensions APi> |
| (Native Messaging) |
+--------↓↑---------+
<標準入出力>
↓↑
+---------------------+
| ランチャー |
+---------------------+
↓
<システムコール>
↓
+---------------------+
| バッチファイル |
+---------------------+
↓
<startコマンド>
↓
+---------------------+
|外部アプリケーション |
+---------------------+
単純なのですが、これによってバッチファイルから先のプロセスが別のプロセスグループに分かれるようになり、FirefoxからNative Messaging Hostとしてランチャーを起動した場合でも、Firefoxの終了後も外部アプリケーションのプロセスが残り続けるようになりました。
また、このような動作をさせる場合、ランチャーでバッチファイルを起動する際にはcommand.SysProcAttr
の指定はあってもなくても結果は変わりませんでした。
なお、バッチファイルを使うとなるとcmd.exe(コマンドプロンプト)のウィンドウが一瞬表示されるのではないかという懸念もありましたが、実際には特にそのようなこともなく自然に外部アプリケーションが起動しました。 これも、Firefoxがランチャーを起動する際に与えている何らかの指定の影響ではないかと考えられます。
以上、Go言語で実装したランチャーを経由してFirefoxから外部アプリケーションを起動する際に、Firefoxが終了した後も外部アプリケーションを起動した状態のままとするためには、バッチファイルを使うとよいCREATE_BREAKAWAY_FROM_JOB
フラグの指定が必要であるという注意点をご紹介しました。
環境によってはバッチファイルの実行自体が制限されている場合もあるかもしれません。実際に動作するかどうか、はあらかじめ確認を取っていただくことを強くお勧めします。
FirefoxのWebExtensions APIはGoogle Chromeの拡張機能向けAPIを参考に設計されています。そのためChrome用拡張機能の開発でのノウハウの多くを流用することができ、何か詰まった時は「Chrome用拡張機能ではどうするのが普通なのか?」という観点で調べれば解決策が見つかることが多いです。
しかしながら両者は完全に同一のものではなく、前述のようなFirefoxに固有の注意事項というものもあります。本記事の初版公開時には、オフィシャルのドキュメントにある注意書きを見落としたまま先入観からChrome用拡張機能向けの情報だけを調査していた結果、肝心な情報に全く辿り着けないという結果となっていました。検索も万能ではない(検索語句がずれていると必要な情報に辿り着けない)ために頼りすぎる事・その時得られた結果を過信しすぎる事のリスク、先入観に囚われずにオフィシャルの情報を丁寧に読む事の重要性を改めて実感した次第です。
ZulipにPGroongaサポートを実装した須藤です。PyCon JP 2017に参加するためにZulipの開発者の1人であるGregさんが来ていたので、日本PostgreSQLユーザ会(JPUG)さんに主催してもらってZulipとPGroongaのイベント「Zulip & PGroonga Night」を開催しました。
なお、GregさんのPyConJP 2017でのトーク「Clearer Code at Scale: Static Types at Zulip and Dropbox」(動画)はPyConJP 2017のベストトークに選ばれました。すごい!
Zulip & PGroonga NightではPGroongaの紹介とZulipの全文検索インデックスの更新方法の紹介をしました。
関連リンク:
ZulipではPostgreSQL標準のtextsearchを使って全文検索を実現しています。textsearchは言語特化型のインデックスを作るため、同時に複数の言語をサポートすることができません。また、日本語を含むアジア圏の言語のサポートが不十分なため、日本語を全文検索できないという問題もあります。
そこで、私はZulipでPGroongaを使えるようにしました。PGroongaは言語特化型のインデックスも言語非依存のインデックスも作れます。言語非依存のインデックスを作れば同時に日本語も英語もいい感じに全文検索できます。
私がZulipにPGroongaサポートパッチを送ったのは自分たちが必要だからです。クリアコードではチャットツールとしてZulipを使っています。その前はSkypeを使っていました。お客さんとの連絡でSkypeを使う必要があったため、その流れでなんとなく使っていました。Skypeはあまり活用していませんでした。
クリアコードはフリーソフトウェアを推進したい会社なのでフリーソフトウェアではないSkypeを使っていることをどうにかしたいと考えていました。そこで、いくつかフリーソフトウェアなチャットツールを検討しました。そのうちの1つがZulipでした。Zulipのネックは日本語全文検索できないことでした。ネックがあるので選択肢から外すという考え方もあると思いますが、私たちは、自分たちで日本語全文検索できるようにして使うことにしました。フリーソフトウェアのよいところは自分たちで改良できることだからです。
Zulipは書き込み時のレイテンシーを小さくしておくために工夫をしています。チャットアプリケーションでは書き込みがすぐに終わることはよい使い勝手に直結するからです。
書き込み時はデータをPostgreSQLに書き込むだけで、全文検索インデックスの更新は別途バックグラウンドで実行します。このためにトリガーとNOTIFY
とLISTEN
を使っています。
具体的な実装を簡単に紹介します。興味のある人はZulipのコードを見てください。既存のコードから学習することができることもフリーソフトウェアのよいところです。
zerver_message
がメッセージ(チャットの書き込み)を保存しているテーブルです。この中に全文検索対象のデータを入れるカラムを定義します。メッセージのテキストそのものとは別に定義することがポイントです。search_tsvector
がそのカラムです。(PGroongaを使うときの実装ではなくtextsearchを使うときの実装です。)
CREATE TABLE zerver_message (
rendered_content text,
-- ... ↓Column for full text search
search_tsvector tsvector
);
この全文検索対象のデータを入れるカラムには全文検索用のインデックスを作ります。(この例ではPGroongaのインデックスではなくtextsearchのインデックスを作っています。)
CREATE INDEX zerver_message_search_tsvector
ON zerver_message
USING gin (search_tsvector);
このメッセージ用のテーブルにトリガーを設定します。このトリガーはメッセージが追加・更新されたときに「更新ログ」テーブル(fts_update_log
テーブル)にメッセージのIDを追加します。
-- Execute append_to_fts_update_log() on change
CREATE TRIGGER
zerver_message_update_search_tsvector_async
BEFORE INSERT OR UPDATE OF rendered_content
ON zerver_message
FOR EACH ROW
EXECUTE PROCEDURE append_to_fts_update_log();
メッセージのIDを追加する関数の実装は次のようになります
-- Insert ID to fts_update_log table
CREATE FUNCTION append_to_fts_update_log()
RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO fts_update_log (message_id)
VALUES (NEW.id);
RETURN NEW;
END
$$;
「更新ログ」テーブルの定義は次の通りです。全文検索インデックスを更新するべきメッセージのIDを入れているだけです。
-- Keep ID to be updated
CREATE TABLE fts_update_log (
id SERIAL PRIMARY KEY,
message_id INTEGER NOT NULL
);
これで後から全文検索インデックスを更新するための情報を保存する仕組みができました。通常通りメッセージテーブルを操作するだけで実現できていることがポイントです。こうすることでアプリケーション側をシンプルにしておけます。残りの処理は、後から全文検索インデックスを更新する、です。
この処理のためにNOTIFY
とLISTEN
を使います。NOTIFY
はLISTEN
している接続に通知する仕組みです。LISTEN
している接続はNOTIFY
されるまでブロックします。NOTIFY
とLISTEN
を組み合わせることで、ポーリングしなくてもイベントが発生したことに気づくことができます。
今回のケースでは「更新ログが増えた」というイベントに気づきたいです。このイベントが来たら全文検索インデックスを更新したいからです。
そのために、「更新ログ」テーブルにトリガーを追加します。「更新ログ」テーブルにレコードが追加されたらNOTIFY
するトリガーです。
-- Execute do_notify_fts_update_log() on INSERT
CREATE TRIGGER fts_update_log_notify
AFTER INSERT ON fts_update_log
FOR EACH STATEMENT
EXECUTE PROCEDURE
do_notify_fts_update_log();
NOTIFY
する関数の実装は次の通りです。この関数を実行すると、fts_update_log
というイベントをLISTEN
している接続のブロックが解除されます。
-- NOTIFY to fts_update_log channel!
CREATE FUNCTION do_notify_fts_update_log()
RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
NOTIFY fts_update_log;
RETURN NEW;
END
$$;
全文検索のインデックスを更新するSQLはPythonから発行しています。全文検索のインデックスの更新処理は必要なときだけ(更新ログがあるときだけ)実行したいです。必要がないときも更新処理を実行し続けるとムダにCPUを使ってしまうからです。
必要なときだけ処理を実行するために、LISTEN
でブロックします。ブロックが解除されたら(NOTIFY
されたら)必ず更新ログがあるので、処理を実行します。↓には入っていませんが、処理が終わったら次のNOTIFY
があるまでまたブロックする実装になっています。こうすることで必要なときだけ処理を実行できるためムダにCPUを使わずにすみます。
cursor.execute("LISTEN ftp_update_log") # Wait
cursor.execute("SELECT id, message_id FROM fts_update_log")
ids = []
for (id, message_id) in cursor.fetchall():
cursor.execute("UPDATE zerver_message SET search_tsvector = "
"to_tsvector('zulip.english_us_search', "
"rendered_content) "
"WHERE id = %s", (message_id,))
ids.append(id)
cursor.execute("DELETE FROM fts_update_log WHERE id = ANY(%s)",
(ids,))
このような複数プロセスでの待ち合わせを実現するためにRDBMSとは別の仕組みを使うことも多いでしょう。たとえば、RedisのPub/Subを使えるでしょう。別の仕組みを使うと運用が面倒になります。PostgreSQLにはNOTIFY
/LISTEN
があるので、PostgreSQLを使っていて待ち合わせを実現しなければいけないときはNOTIFY
/LISTEN
を使うことを検討してみてください。
ZulipとPGroongaのイベントでZulipとPGroongaの情報を紹介しました。クリアコードはZulipを使っていて、今ではなくてはならないツールになっています。ぜひみなさんもZulipを使ってみてください。
Zulipは基本的に自分たちで運用しますが、運用を任せる選択肢もあります。Zulipの開発チームがクラウドサービスでの提供を進めているのです。オープンソースコミュニティは無料で使えるそうです。興味のある人はzulipchat.comを確認してください。
PGroongaが気になる人は、11月3日開催のPostgreSQL Conference Japan 2017に来てください。日本PostgreSQLユーザ会(JPUG)が主催のPostgreSQLのカンファレンスです。ここでPGroongaの最新情報を紹介する予定です。
この発表のためにmroonga_query_expand()
を実装した須藤です。db tech showcase Tokyo 2017で「MySQL・PostgreSQLだけで作る高速でリッチな全文検索システム」という話をしました。一昔前の全文検索システムはそこそこ速く全文検索してキーワードをハイライト表示できれば普通でしたが、最近の全文検索システムはそれだけだと機能不足で、オートコンプリートやクエリー展開や関連エントリー表示機能などがあって普通というくらいにユーザーの要求レベルがあがっています。これは、GoogleやAmazonなど便利な検索サービスに慣れたユーザーが増えたためです。そんな時代の変化に対応できる全文検索エンジンがGroongaです。GroongaはMySQL・MariaDB・PostgreSQLから使えるためSQLを使って簡単にイマドキの全文検索システムを実装できます。しかも、運用も楽です。そんな話です。
関連リンク:
まず、どういうときにMroonga・PGroongaを使うアプローチを選べばよいかという指針を示しました。
LIKE
次に、以下の機能をMroonga・PGroongaで実現するにはどういうSQLを使えばよいか説明しました。
最後に、次のステップとして構造化データ(オフィス文書・HTML・PDFなど)の対応方法について少し言及しました。Groongaプロジェクトは構造化データからテキスト・メタデータ・スクリーンショットを取得するツールとしてChupaTextを開発しています。コマンドラインでもHTTP経由でもライブラリーとしても使えます。HTTP経由で使う場合はDocker・Vagrantを使うのが便利です。依存ライブラリーを揃える手間がないからです。
Mroonga・PGroongaを使ってイマドキの全文検索システムを実装する方法を紹介しました。コンサルティングやチューニングや開発支援などを提供するサポートサービスがあります。社外秘のデータでお困りの場合はお問い合わせください。NDAを結んだ上で対応できます。
Mroongaはインサイト・テクノロジーさんが進めているPinkshiftでも活用されています。MySQL・MariaDB・PGroongaで高速全文検索が必要ならMroonga・PGroongaを試してみてください。
結構Rubyの拡張ライブラリーを書いている方だと思っている須藤です。RubyKaigi 2017で拡張ライブラリー関連の話をする予定です。RubyKaigi 2017で私の話をより理解できるようになるために内容を紹介します。
関連リンク:
たくさんRubyの拡張ライブラリーを書いてきた経験を活かして拡張ライブラリーのC APIをもっとよくできないかについて考えています。バインディングについてはRubyKaigi 2016で紹介したGObject Introspectionベースがよいと思っていますが、バインディングではないただの拡張ライブラリーはC++を活用するのがよさそうだと思っています。なぜC++を活用するのがよいと思うかは私が実現したいことに関わっています。
私が実現したいことはC/C++のライブラリーを使ってRubyスクリプトを高速化することです。具体的には、xtensorというC++で実装された多次元配列ライブラリーを使ってRubyスクリプトを高速化したいです。
1つ1つの機能に対してバインディングを用意してRubyレベルで組み合わせるやり方もあります。ただ、場合によっては機能を実行する毎にRubyレベルに戻ってくるオーバーヘッドを無視できないことがあります。あると思っています。まだ実際に遭遇したわけではありませんが。
あまりよい例ではありませんが。。。たとえば、GPU上で演算をする機能があって、その機能を実行する毎にGPU上にデータを転送して演算をして演算結果をまた転送しなおすとしたら、オーバーヘッドは無視できません。まぁ、この場合は、拡張ライブラリーで一連の演算をまとめるよりも、必要な間はずっとGPU上にデータを置いておく機能をRubyレベルに用意する方が汎用的でよさそうです。最近、Apache ArrowにGPU上のデータを管理する機能が入ったので、この場合はApache Arrowと連携する機能を用意するのがよさそうです。
C/C++で書かれたライブラリーを使った拡張ライブラリーを書くにはRubyが提供するC APIを使います。このC APIは悪くないのですが、Cなので書いているときに書きにくいなぁと感じることがあります。
たとえば、メソッドを定義するときに関数定義とメソッドの登録が離れるのが不便だなぁと感じます。次のようにrb_hello()
の定義とrb_define_method()
の呼び出しが離れています。
#include <ruby.h>
static VALUE
rb_hello(VALUE self)
{
return rb_str_new_cstr("Hello");
}
void
Init_hello(void)
{
VALUE rb_cHello = rb_define_class("Hello", rb_cObject);
rb_define_method(rb_cHello, "hello", rb_hello, 0);
}
あとは、例外が発生したときにキレイにリソースを開放するためにrb_rescue()
やrb_ensure()
を使うときが面倒です。
他には、RubyのオブジェクトをCの値に変換する各種APIに統一感がないのも地味に使い勝手が悪いです。たとえば、Rubyのオブジェクトをbool
に変換するにはRTEST()
を使いますし、int
に変換するにはNUM2INT()
を使います。
C++11以降の最近のC++を使うことで今のC APIをもっと便利にできます。
たとえば、C++11にはラムダ式があります。これを活用することで次のようにdefine_method()
で直接メソッドを定義できます。これはExt++というライブラリーを使っています。
#include <ruby.hpp>
extern "C" void
Init_hello(void)
{
rb::Class("Hello").
define_method("hello",
[](VALUE self) { // ←ラムダ式
return rb_str_new_cstr("Hello");
});
}
Rubyでdefine_method
を使うと次のような書き方になりますが、少し似ていますね。
class Hello
define_method(:hello) do
"Hello"
end
end
このようなC++11を活用するやり方のメリットは次の通りです。
auto
:型推論を使うことで必要な型だけ書けばすむようになるrange-based for loop
:従来のfor (int i = 0; i < n; ++i)
だけでなく、Rubyのeach
のように自分でインデックスを回さなくてもfor
を使える簡単に言うと、既存の資産を活用しつつ便利になるよ、という感じです。
一方、デメリットは次の通りです。
setjmp()
/longjmp()
で実装されているのでRubyの例外が発生すると、スコープを抜けたC++のオブジェクトのデストラクターが呼ばれないg++
では使えない例外に関してはライブラリーでカバーする方法があるので、基本的にはC++に起因するデメリットになります。
このようなデメリットはあるものの、適切に使えば十分メリットの方が大きくなると思っています。Ruby本体にC++のAPIがあってもいいのではないかと考えていた時期もあったのですが、RubyKaigi 2017の資料をまとめていたら少し落ち着いてきて、今は、もう少し検討してよさそうなら提案しよう、くらいに思っています。
以前からもっと便利に拡張ライブラリーを書きたいという人たちがいます。私はC++11を活用するアプローチがよいと思っていますが、他のアプローチも紹介します。
大きく分けて3つのアプローチがあります。
最後のアプローチがC++11を活用するアプローチです。
最初の「Rubyを拡張する」アプローチはRubexのアプローチです。Rubyに追加の構文を導入して拡張ライブラリーも書けるようにしようというアプローチです。RubyKaigi 2017で発表があります。
Pythonでは同様のアプローチで成功しているプロダクトがあります。それがCythonです。CythonはPythonでデータ分析をする界隈では広く使われています。(使われているように見えます。)
私はこのアプローチはあまりスジがよくないと感じています。理由は次の通りです。
ただ、Cythonが成功している事実と、ちょっとした拡張機能を書く分にはRubyの知識と少しのRubexの知識だけでよい(Cのことはあまり知らなくてよい)という事実があるので、もしかしたらそんなにスジは悪くないのかもしれません。数年後も開発が継続していたら再度検討してみたいです。
2番目の「C以外の言語を使う」アプローチはHelixのアプローチです。Rustで拡張ライブラリーを書けるようにしようというアプローチです。RubyKaigi 2017で発表があります。
私はC/C++のライブラリーを使いたいのでこのアプローチは私の要件にはマッチしないのですが、高速化のために処理を全部で自分で実装する(あるいはRustのライブラリーを活用して実装する)場合はマッチしそうな気がします。
このアプローチのメリットは、Rustを知っているならCで書くよりもちゃんとしたプログラムをすばやく書けることです。デメリットはRubyのC APIのフル機能を使えない(使うためにはメンテナンスを頑張る必要がある)ことです。たとえば、Ruby 2.4からrb_gc_adjust_memory_usage()
というAPIが導入されましたが、Rustからこの機能を使うためにはバインディングを用意する必要があります。つまり、RubyのC APIの進化にあわせてメンテナンスしていく必要があります。
最後に現時点でC++を活用する方法を紹介します。
1つがRiceを使う方法です。RiceはC++で拡張ライブラリーを書けるようにするライブラリーです。10年以上前から開発されています。C++でPythonの拡張ライブラリーを書けるようにするBoost.Pythonに似ています。
例外の対応やメソッドのメタデータとして引数のデフォルト値を指定できるなど便利な機能が揃っています。ただし、昔から開発されているライブラリーで現在はメンテナンスモードなため、C++11への対応はそれほど活発ではありません。メンテナーは反応してくれるので自分がコードを書いて開発に参加するのはよいアプローチだと思います。
もう1つがExt++を使う方法です。Ext++もC++で拡張ライブラリーを書けるようにするライブラリーです。私が作り始めました。RiceはRubyのCのオブジェクトをすべてラップしてC++で自然に扱えるようにするようなAPIです。つまり、できるだけRubyのC APIを使わずにすむようにしたいようなAPIです。私は、もっとC APIが透けて見えるような薄いAPIの方が使いやすいのではないかという気がしているので、その実験のためにExt++を作り始めました。薄いAPIの方が使いやすいのか、結局Riceくらいやらないと使いやすくないのかはまだわかっていません。Red Data Toolsのプロダクトで使って試し続けるつもりです。
RubyKaigi 2017で拡張ライブラリーを書きやすくするためにC++がいいんじゃない?という話をします。
去年もスポンサーとしてRubyKaigiを応援しましたが、今年もスポンサーとしてRubyKaigiを応援します。去年と違って今年はブースはありません。懇親会などで見かけたら声をかけてください。拡張ライブラリーに興味のある人と使いやすいAPIについて話をしたいです!
あと、RubyKaigi 2017の2日目の午後に通常のセッションと並行して「RubyData Workshop」というワークショップが開かれる予定です。まだRubyKaigi 2017のサイトには情報はありませんが、時期に情報が載るはずです。このワークショップではPyCallとRed Data Toolsの最新情報を手を動かして体験することができます。Rubyでデータ処理したい人はぜひお越しください!
RubyKaigi 2017で拡張ライブラリー関連の話をしてきた須藤です。クリアコードはシルバースポンサーとしてRubyKaigi 2017を応援しました。
関連リンク:
C++11を活用するともっと拡張ライブラリーを書きやすくなるよ、という内容でした。詳細は事前情報を読んでください。
個人的には今後の拡張ライブラリー開発にプラスになるとても実用的な話をしたつもりだったのですが、あまり反響がなかったので、よさを伝えきれなかったのだと思います。残念。
誰も質問してくれなかったので付録の生のC API以外で拡張ライブラリーを書く方法の比較はお蔵入りになりました。聞きたい人はなにかのイベントに呼んでください。
発表の反響はあまりなかったですが、Red Data Toolsの反響はありました。
RubyData Workshop in RubyKaigi 2017の1つとしてSpeeeの@hatappiさんとRed Data Toolsの紹介をしました。(@hatappiさんがメインで説明・進行をして、私はたまに補足するスタイル。)
来週の火曜日(9月26日)の夜にSpeeeさんで開催するRed Data Toolsの開発イベントOSS Gate東京ミートアップ for Red Data Tools in Speeeの参加者が増えました。オンラインで相談する場所はGitterのred-data-tools/jaにあるので、開発イベントに参加できない人も一緒に開発しましょう!
Rubyでデータ処理できるようにしたいみなさん、一緒に開発していきましょう!
自分達は開発できない・開発する時間がないけどお金は出せるという場合は、クリアコードに開発の仕事を依頼するというやり方があるのでお問い合わせください。
Red Data Toolsと同じようにOSS Gateも反響がありました。RubyKaigi 2017 前夜祭で安川さんが紹介してくれたのと、Speeeさん・永和システムマネジメントさん・ドリコムさん・ピクシブさんのブースにチラシを置いてもらったのが大きいです。ありがとうございました!
おかげで広島でもOSS Gateの活動を始められそうです。Gitterのoss-gate/hiroshimaで相談しているので、広島でもOSSの開発に参加する人が増えるとうれしい人は参加してください。
全国のOSS Gateワークショップ開催情報は次の通りです。近隣で開催している場合はぜひビギナー・サポーターとして参加してください。
RubyKaigi 2017の発表内容と成果を紹介しました。
Redmineで高速に全文検索する方法で紹介したプラグインの新しいバージョンをリリースしてさらに高速化しました。
Groongaの類似文書検索の機能を使って類似チケットを表示するようになりました。
例えば、あるソフトウェアのサポートをしているときに似た問い合わせが過去にあれば、その似た問い合わせが類似チケットとして表示されることを期待しています。 現在の実装では、チケットのトラッカー、起票者、担当者、カテゴリなどのメタデータを一切考慮していないので精度はそこそこです。 クリアコード社内で運用しているRedmine*1に入れてみたところ類似チケットの検索には15msくらいしかかかっていないのでRedmineの性能に影響はないと考えてデフォルトでONにしてあります。
元のRedmineの全文検索では、最大で以下の9つのテーブルを検索し、検索時に全件取得してキャッシュしていました。
redmine_full_text_search v0.4.0 までは既存のコードをなるべく活かしていたので、クエリの実行数は同じで全件取得していたのも同じでした。
つまり、上記全てのテーブルから検索すると1回のクエリで100msだとしても 9 * 100ms = 900ms
と約1秒かかってしまうことになります。
v0.5.0 では、検索対象のレコードを1つのテーブルにまとめてGroongaの機能を直接使用して全文検索のために実行するクエリを1つにしました。
また、limit と offset を使用して一度に取得するレコード数を減らし、Redmineのキャッシュを使用するのをやめました。
redmine_full_text_searchのバージョンはv0.4.0とv0.6.2を使用しました。 内部ネットワークからMechanizeで特定のユーザーでログインして表にあるクエリーで検索しました。 以下にMariaDB(Mroonga)の場合とPostgreSQL(PGroonga)の場合についてそれぞれの測定結果を示します。
データは、以下のメーリングリストのアーカイブを使用しました。 メーリングリストごとにプロジェクトを作成しました。 メールからチケットを作成し、In-Reply-Toで返信だと判断できる場合は、見つかったチケットへのコメントとして登録するようにしました。
これで31142チケットと72392コメントのデータベースを作成しました。redmine_full_text_search v0.6.2向けのテーブルには103537件レコードが存在します。 Wikiやドキュメント等チケット以外のデータはありません。
測定結果の単位は全てミリ秒です。
Redmine | v0.4.0 | v0.6.2 | |||||
---|---|---|---|---|---|---|---|
query | 1st | 2nd | 1st | 2nd | 1st | 2nd | |
cgi リーク | duration | 1804.86 | 56.0048 | 1028.55 | 62.8551 | 140.443 | 138.351 |
view_runtime | 27.8059 | 29.7982 | 30.5287 | 26.2161 | 61.5869 | 55.7791 | |
db_runtime | 1758.66 | 5.93903 | 969.221 | 7.09439 | 64.5651 | 63.6323 | |
groonga | duration | 1687.73 | 82.2862 | 1053.66 | 124.609 | 119.136 | 115.352 |
view_runtime | 18.7162 | 19.5571 | 25.1116 | 21.4028 | 42.1369 | 39.2488 | |
db_runtime | 1611.95 | 5.80468 | 923.49 | 12.0304 | 63.7171 | 62.6385 | |
mroonga score | duration | 1712.8 | 38.6372 | 1043.22 | 50.2505 | 134.332 | 115.884 |
view_runtime | 19.4748 | 18.8583 | 20.4314 | 21.4807 | 42.9837 | 41.9256 | |
db_runtime | 1678.16 | 5.84049 | 998.232 | 5.45167 | 64.2949 | 61.54 | |
pgrooga score | duration | 1675.0 | 25.8263 | 907.864 | 38.1889 | 72.0365 | 72.458 |
view_runtime | 10.8129 | 10.5722 | 12.1855 | 13.2947 | 12.3002 | 12.2505 | |
db_runtime | 1653.34 | 5.25172 | 877.303 | 5.14269 | 52.3137 | 52.2138 | |
ruby | duration | 1876.77 | 1240.81 | 2858.95 | 1576.22 | 182.263 | 182.67 |
view_runtime | 17.2178 | 17.4677 | 20.9058 | 21.6165 | 44.5798 | 46.555 | |
db_runtime | 683.701 | 9.14142 | 1324.75 | 19.6118 | 108.922 | 106.341 | |
segv | duration | 1734.57 | 97.6753 | 1221.78 | 125.179 | 207.27 | 128.385 |
view_runtime | 51.7349 | 22.1543 | 33.8267 | 31.1135 | 104.215 | 49.5876 | |
db_runtime | 1610.52 | 6.07189 | 1056.77 | 6.53569 | 87.9873 | 62.8314 | |
segv make | duration | 1815.31 | 68.4352 | 958.573 | 72.8359 | 119.899 | 148.094 |
view_runtime | 22.0608 | 32.7035 | 35.3729 | 27.96 | 42.5002 | 67.0567 | |
db_runtime | 1769.95 | 5.81216 | 880.271 | 6.27881 | 64.3565 | 62.977 | |
メモリーリーク | duration | 1665.0 | 49.0786 | 962.625 | 54.4098 | 151.751 | 119.179 |
view_runtime | 21.1638 | 25.3095 | 25.1169 | 23.1611 | 73.9769 | 47.4756 | |
db_runtime | 1626.61 | 5.58291 | 909.367 | 5.83328 | 65.4596 | 60.8111 | |
メモリリーク | duration | 1668.5 | 49.7472 | 1005.86 | 56.3724 | 111.954 | 111.491 |
view_runtime | 22.01 | 22.3757 | 18.6613 | 20.756 | 41.0274 | 40.3688 | |
db_runtime | 1620.18 | 5.67121 | 947.36 | 5.73328 | 60.8254 | 59.7722 | |
ライトニングトーク カンファレンス 開催 | duration | 1730.69 | 39.2599 | 950.232 | 48.4056 | 122.699 | 117.899 |
view_runtime | 18.879 | 20.1971 | 24.7018 | 21.0985 | 43.9869 | 42.6021 | |
db_runtime | 1697.25 | 5.37024 | 901.403 | 5.60327 | 65.7128 | 63.2021 | |
発表者 募集 | duration | 1540.63 | 41.1723 | 1026.68 | 46.5856 | 116.767 | 114.665 |
view_runtime | 18.8667 | 18.5547 | 20.2438 | 19.4315 | 42.4205 | 42.7707 | |
db_runtime | 1505.5 | 5.52283 | 982.723 | 5.2658 | 62.3322 | 60.5112 | |
勉強会 会議 | duration | 1535.28 | 39.5411 | 948.463 | 47.6093 | 114.435 | 137.902 |
view_runtime | 17.7493 | 19.1658 | 19.8384 | 19.5605 | 41.9521 | 40.7409 | |
db_runtime | 1501.66 | 5.67893 | 904.492 | 5.73054 | 61.676 | 60.6336 |
平均と標準偏差は以下の通りです。
Redmine | v0.4.0 | v0.6.2 | |||||
---|---|---|---|---|---|---|---|
1st | 2nd | 1st | 2nd | 1st | 2nd | ||
平均 | duration | 1703.93 | 152.37 | 1163.87 | 191.96 | 132.75 | 125.19 |
view_runtime | 22.21 | 21.39 | 23.91 | 22.26 | 49.47 | 43.86 | |
db_runtime | 1559.79 | 5.97 | 972.95 | 7.53 | 68.51 | 64.76 | |
標準偏差 | duration | 97.12 | 328.76 | 516.86 | 418.29 | 33.65 | 25.04 |
view_runtime | 9.66 | 5.57 | 6.41 | 4.36 | 21.41 | 12.19 | |
db_runtime | 275.78 | 0.98 | 117.69 | 4.05 | 14.46 | 12.87 |
MariaDBの場合はほぼ想定通りの測定結果でした。 プラグインなしの場合は、DBアクセスに初回1500ms以上かかっていたのが2回目は5ms前後になっていてキャッシュが効いています。 プラグインv0.4.0の場合は、DBアクセスに初回1000ms前後にかかってており、2回目は5ms前後になっていました。 プラグインv0.6.2の場合は、DBアクセスに初回、2回目ともに50-100ms程度かかっており、初回の検索から速いのでキャッシュしなくても十分な速度が出ています。
プラグインなしの場合とv0.6.2の場合を比較すると、初回の検索で約24倍高速になっています。
db_runtimeの標準偏差を見ると、v0.6.2のとき最も値が小さくなっており、安定して高速に動作することがわかります。
Redmine | v0.4.0 | v0.6.2 | |||||
---|---|---|---|---|---|---|---|
query | 1st | 2nd | 1st | 2nd | 1st | 2nd | |
cgi リーク | duration | 5432.23 | 5708.34 | 91.9298 | 91.6952 | 205.405 | 207.645 |
view_runtime | 24.3255 | 26.9705 | 31.3033 | 37.1955 | 39.1355 | 50.9562 | |
db_runtime | 5385.85 | 5660.76 | 29.0279 | 29.6823 | 145.829 | 144.418 | |
groonga | duration | 5060.77 | 5071.71 | 669.003 | 647.418 | 185.603 | 182.977 |
view_runtime | 24.3327 | 20.2376 | 26.1071 | 40.4994 | 30.0942 | 30.011 | |
db_runtime | 4933.86 | 4929.92 | 560.77 | 522.823 | 146.799 | 144.304 | |
mroonga score | duration | 5177.38 | 5167.46 | 69.7063 | 64.5032 | 181.835 | 181.996 |
view_runtime | 20.2529 | 20.5412 | 26.1367 | 23.6614 | 29.0306 | 28.7275 | |
db_runtime | 5139.11 | 5127.81 | 22.2003 | 21.8169 | 144.434 | 143.932 | |
pgrooga score | duration | 5037.87 | 5056.32 | 46.4337 | 43.408 | 153.975 | 159.449 |
view_runtime | 11.5761 | 11.7345 | 15.6785 | 14.6403 | 12.8356 | 12.9991 | |
db_runtime | 5013.34 | 5031.37 | 14.6134 | 14.7022 | 134.023 | 138.259 | |
ruby | duration | 3542.06 | 3402.16 | 19749.8 | 19558.4 | 238.018 | 222.578 |
view_runtime | 18.2419 | 18.4601 | 18.6485 | 26.2439 | 43.0062 | 29.8173 | |
db_runtime | 1511.17 | 1508.25 | 18214.4 | 17921.3 | 184.03 | 182.523 | |
segv | duration | 5270.42 | 5333.98 | 522.433 | 443.14 | 384.031 | 190.565 |
view_runtime | 28.5796 | 26.4264 | 43.9667 | 33.4644 | 90.3281 | 35.4176 | |
db_runtime | 5104.6 | 5174.81 | 351.514 | 309.031 | 283.627 | 144.746 | |
segv make | duration | 5691.51 | 5691.54 | 153.903 | 145.227 | 190.858 | 201.922 |
view_runtime | 27.0135 | 27.8609 | 44.5491 | 40.7997 | 32.3304 | 43.489 | |
db_runtime | 5613.84 | 5617.51 | 66.682 | 64.3612 | 148.758 | 147.865 | |
メモリーリーク | duration | 5789.28 | 5752.14 | 75.0013 | 72.5004 | 195.717 | 200.674 |
view_runtime | 23.1544 | 21.7764 | 20.2103 | 21.5645 | 41.4241 | 45.2702 | |
db_runtime | 5740.95 | 5709.43 | 33.8974 | 31.4955 | 141.669 | 143.453 | |
メモリリーク | duration | 5749.46 | 5792.81 | 105.487 | 102.567 | 196.678 | 187.858 |
view_runtime | 23.1136 | 22.6125 | 21.9238 | 23.339 | 37.9664 | 35.4135 | |
db_runtime | 5696.29 | 5738.85 | 57.3894 | 54.1745 | 143.913 | 142.159 | |
ライトニングトーク カンファレンス 開催 | duration | 5842.91 | 5811.98 | 63.857 | 66.4787 | 187.346 | 185.818 |
view_runtime | 20.0885 | 22.1672 | 22.3308 | 23.4369 | 30.5955 | 29.236 | |
db_runtime | 5804.88 | 5770.79 | 22.9653 | 23.6612 | 146.704 | 146.915 | |
発表者 募集 | duration | 5154.93 | 5152.25 | 74.2416 | 73.2 | 183.645 | 181.342 |
view_runtime | 20.5156 | 19.2945 | 27.7632 | 29.5588 | 31.441 | 30.5509 | |
db_runtime | 5114.81 | 5112.4 | 24.3069 | 23.246 | 143.74 | 141.939 | |
勉強会 会議 | duration | 5028.9 | 5064.46 | 65.7615 | 78.8453 | 180.065 | 182.6 |
view_runtime | 19.6628 | 18.504 | 21.4198 | 33.3649 | 29.5505 | 30.3836 | |
db_runtime | 4989.92 | 5025.62 | 24.977 | 24.7126 | 142.494 | 143.389 |
平均と標準偏差は以下の通りです。
Redmine | v0.4.0 | v0.6.2 | |||||
---|---|---|---|---|---|---|---|
1st | 2nd | 1st | 2nd | 1st | 2nd | ||
平均 | duration | 5231.48 | 5250.43 | 1807.30 | 1782.28 | 206.93 | 190.45 |
view_runtime | 21.74 | 21.38 | 26.67 | 28.98 | 37.31 | 33.52 | |
db_runtime | 5004.05 | 5033.96 | 1618.56 | 1586.75 | 158.84 | 146.99 | |
標準偏差 | duration | 590.01 | 633.07 | 5413.33 | 5362.64 | 56.54 | 15.34 |
view_runtime | 4.26 | 4.25 | 8.84 | 7.84 | 17.67 | 9.38 | |
db_runtime | 1096.48 | 1107.02 | 5006.48 | 4927.30 | 39.38 | 10.96 |
PostgreSQLの場合は、プラグインなしのときは初回、2回目ともにDBアクセスに約5秒かかっているのでキャッシュが効いていないことがわかります。 v0.4.0の場合もキャッシュが効いておらず、初回、2回目ともにDBアクセスに約1.5秒かかっています。プラグインなしと比較して、約3倍高速になりました。 v0.6.2ではRedmineのキャッシュを使用しないようにしたので、キャッシュの影響はありません。初回、2回目ともにDBアクセスに約0.15秒かかっています。 プラグインなしと比較すると、約33倍高速になりました。v0.4.0と比較すると約10倍高速になっています。
db_runtimeの標準偏差を見ると、v0.6.2のとき最も値が小さくなっており、安定して高速に動作することがわかります。
redmine_full_text_search v0.6.2の場合、MariaDBとPostgreSQLのdb_runtimeの平均を比較するとMariaDBの方が約2.3倍高速という結果でした。 これは、MroongaとPGroongaでGroonga上のテーブル定義やインデックス定義に違いがあるためではないかと考えています。
今後は、機能を充実させつつMariaDBとPostgreSQLのどちらを使っても快適に検索できるようにしていきたいです。 やりたいことについてはプロジェクトのissuesも見てください。
*1 チケット数は約4000件
GCP上にインスタンスを立ち上げて、簡単にredmine_full_text_searchを試すことができる環境を構築できるようにしました。
Redmineでもっと高速に全文検索する方法で使用したベンチマーク環境を構築するためのスクリプトです。 今後もredmine_full_text_searchをバージョンアップしたとき、性能の変化を簡単に調べれられるように作りました。
TerraformとAnsibleを入れてコマンドを叩くだけです。
コマンドを実行する前に、APIキーを作成しておく必要があります。
$ git clone https://github.com/okkez/redmine_full_text_search-benchmark.git
$ cd redmine_full_text_search-benchmark
$ terraform init Terraform/
$ terraform apply Terraform/
$ ./generate-hosts.rb
このまま ansible を実行すると、SSHのホスト鍵のチェックが実行されて、対話的な操作が必要になるので対話的な操作が不要になるように準備します。
$ for h in fluentd redmine-mariadb redmine-mroonga redmine-postgresql redmine-pgroonga; do \
gcloud compute --project "YOUR PROJECT" ssh --zone "YOUR ZONE" "fluentd" --command "echo"; done
また、ansible が使うUserKnownHostsFileにgcloudコマンドが生成するファイルを指定します。 ansible/ansible.cfg に以下の内容でファイルを作成しておきます。
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o -o UserKnownHostsFile=~/.ssh/google_compute_known_hosts
注意 DBのダンプファイルはリポジトリに含めていないので、各自DBのダンプファイルを用意してください。
ansible/roles/mariadb/files/benchmark-mariadb.dump.sql.xz
ansible/roles/postgresql/files/benchmark-postgresql.dump.sql.xz
最後に ansible-playbook コマンドを実行します。
$ cd ansible
$ ansible-playbook -i hosts playbook.yml
全部で20分もあれば、環境構築が終わります。
今はベンチマーク用なので内部ネットワークからしかアクセスできないようにしてありますが、httpとhttpsは外部からアクセスできるようにして手元のブラウザからでも簡単に動作を確認できるようにしたいです。 また、ベンチマーク用のデータもどこかにアップロードしたものを簡単に使えるようにしたいです。
測定結果をFluentd経由でPostgreSQLに溜めてあるのですが、これをいい感じに可視化したいです。
*1 DBのダンプファイルはxzで圧縮してもサイズが大きいのでリポジトリに含めていません
データ分析基盤構築入門という書籍を著者の一人@yoshi_kenさんから「Fluentdのプラグインの件でお世話になっているので〜」ということでいだたきました。ありがとうございます!
この書籍を読むとFluentd,Elasticsearch,Kibanaの体系化された知識を得ることができます。 特にこれからデータ分析基盤を作ろうと考えている人や、データ分析基盤を構築しているけど、自己流なので自信がないなぁという人におすすめの書籍です。
400ページくらいあり分厚いのですが、サンプルコード・実行例・設定例がたくさんあり、まずは真似して始めてみる、ということが大変やりやすくなっています。
データ分析基盤を構築する目的はなんなのか、データ分析基盤を構築し、提供する立場ではどういう考え方で行動するべきなのかという心構えから、ビジネスではデータ分析基盤をどのように活用される例があるのかも説明されているので、これから始める人にとってちょうど良い内容になっています。
現在存在する日本語の情報でFluentdについて最もきちんとまとまっている情報です。ドキュメント化されていない情報や見つけにくい情報もまとまっているため、リファレンスとして一冊手元にあるととても便利だと思いました。 またFluentd v0.12とFluentd v0.14の違いについても書かれているため、これから新しくFluentdを使い始める人にとっても役に立つでしょう。
多くの人が最初に使うであろうtailプラグインについては徹底攻略として設定例を含めて約20ページの解説が書かれています。 また、Fluentdノードのデザインパターンとしてよく使用されるであろうノード構成について詳しく解説されています。
第2部の最後に運用Tipsとして、様々な知見が書かれています。これらは実際にFluentdを運用しようとしたときに気になる項目ばかりでした。
Elastic社の中の人が書いているので、とても詳しく書いてあります。 Elasticsearchを使ったことがないので、内容の評価はできませんが始めて使う人から、大規模で運用したいと考えている人まで幅広く対応した内容だと思いました。
Kibana入門のタイトル通りの内容でした。Kibanaも使ったことがなかったのですが、この書籍を読んで使い始めることができそうという気持ちになりました。
付録というよりも第5部と言った方が適切なくらいの分量がありました。 Fluentd/Embulkのプラグインリストは、日本語でまとめられている情報では質と量で一番です。
第1部から第4部までと付録はそれぞれ独立しているので、どこからでも読み始めることができます。 しかし、それぞれのソフトウェアについて独立して書かれているため、FluentdとElasticsearchの連携をするための方法については、あまり詳しく書かれていませんでした。 この書籍が書かれた時期は2017年4月より前だと思うのですが、その時期はfluent-plugin-elasticsearchのメンテンスが滞っていたので詳しく書けなかったのだと思われます。 2017年9月から、クリアコードメンバーがCollaboratorとして開発に参加しているので、FluentdからElasticsearchにデータを送るのにfluent-plugin-elasticsearchを使い続けても安心です。
データ分析基盤構築入門[Fluentd、Elasticsearch、Kibanaによるログ収集と可視化]
技術評論社
¥ 3,218