mruby」カテゴリーアーカイブ

RubyWorld Conference2018で登壇してぶちかましてきた


11/1から松江市のくにびきメッセで開催されたRubyWorld conference2018でngx_mrubyについてトークしてきた。

イベントの全体感として10/31にレセプションがあり、松江市内の居酒屋で盛大におもてなし頂いた。

島根県がかなり力を入れているようで、いつも僕が主戦場にしているWEB系のカンファレンスとは規模も客層もまた違ったものだった。

初日はブースでダラダラコードかきながら、遊んでた。

レセプションのときにMatzさんを捕まえて、出してたPR見てくださいよ〜って言ったら会期中に治してくださって最高だった。

Matzさんの修正でngx_mrubyでも再びインスタンス変数が利用可能になった。

二日目は自分のトークがあったのだけども、もはや微塵も緊張しなくなってしまっている。

普段と客層が違うことを想定していなかったのでちょっと当日もう少しうまく喋れたな〜と反省。ボリューム的にはちょうどだったのでそこは良かった。

また今回もTwitterでしか見たことなかった方々ともお話出来たのも良かった。

Rubyコミュニティ、実はあまりこれまで関わりがなかったのだけども、国内に開発者が多いことで他の言語系のカンファレンスとは多少毛色が違うなぁという印象も残った。なんというか 詳しい の次元が違う。僕も彼らのようにもっともっとRubyに貢献できるようにやっていかねばなぁと感じられたのでとてもいい時間だった。

また世の中にバーーーンと言えるようなものを生んで、次はRubyKaigiに行きたい。

ngx_mrubyを利用して、TLS 1.2以下のサポート終了予告ページを表示する

昨今、セキュリティ上の問題から、TLS 1.2以前のバージョンを利用するブラウザのサポート終了のニュースを目にすることが増えてきました。

Yahoo!セキュリティセンター | セキュリティ強化のお知らせ
Adobe Sign:TLS 1.0 および 1.1 のサポート終了
Office 365 への TLS 1.2 の実装に対する準備

サポート終了にあたり、WEBサービス提供者としてはユーザーに何らかの方法で、ユーザーの対応の有無を通知したい場合がほとんどだと思います。このエントリでは先日、ngx_mrubyに追加された、 tls_version メソッドを利用して、クライアントのサポートするTLSバージョンに応じて表示するサイトのコンテンツを変更する方法を紹介したいと思います。

今回の例は下記のような構成を想定しています。HTTPS(TLS)通信はNginxで終端し、後段のOriginサーバへHTTPでリバースプロキシする構成です。

この構成の場合に、TLS1.2、1.3であれば、 /tls_ok にリバースプロキシし、 該当しなければ /tls_ng にリバースプロキシするコンフィグ例は下記になります。

mruby_ssl_handshake_handler_code '
  ssl = Nginx::SSL.new
  Userdata.new.ssl_tls_version = ssl.tls_version
';

location / {
  mruby_set_code $proxy_path "
    Userdata.new.ssl_tls_version =~ /TLSv1\.(2|3)/ ? 'ok' : 'ng'
  ";
  proxy_pass  http://<origin_ip>/tls_$proxy_path;
}

コンフィグの内容としてはSSLハンドシェーク中にクライアントのTLSバージョンを取得し、それをSSLハンドシェークの処理のコンテキスト外に値を持ち運ぶために、mruby-userdataのクラスであるUserDataに値を格納しています。

locationディレクティブにおいて、UserDataからTLSバージョンを取り出し、値を評価し、 okng をNginxの変数である $proxy_path に格納しています。そして、 $proxy_path の値を利用して、プロキシ先のパスを /tls_ok/tls_ng に振り分けるという実装です。

なお、 tls_version の戻り値はOpenSSLのSSL_get_version関数の戻り値である、次のいずれかです。

  • SSLv3
  • TLSv1
  • TLSv1.1
  • TLSv1.2
  • TLSv1.3
  • unknown

例えば、下記のような実装でも良いとは思います。


Userdata.new.ssl_tls_version.gsub(/TLSv/, '').to_f >= 1.2 ? 'ok' : 'ng'

実行結果は下記のようになります。

irb(main):007:0> "SSLv3".gsub(/TLSv/, '').to_f
=> 0.0
irb(main):008:0> "TLSv1".gsub(/TLSv/, '').to_f
=> 1.0
irb(main):009:0> "TLSv1.2".gsub(/TLSv/, '').to_f
=> 1.2
irb(main):010:0> "unknown".gsub(/TLSv/, '').to_f
=> 0.0
irb(main):011:0> 

後者のほうが今後1.4が出た場合も何もしなくていいので便利かもしれません。

追記(2018/08/07)

ngx_mrubyを利用していない場合でも $ssl_protocol に同等の値が入るので、ngx_mrubyを未導入の場合はこちらの値を利用すると良いでしょう。

まとめ

今後、業界内で更に加速するTLS 1.2以下のサポート終了にあたり、 tls_version のAPIを利用すればミドウェアのレイヤで簡単にクライアントを振り分けることが可能です。ngx_mrubyを利用すればこれだけではなく、下記の資料のように多くの問題をプラガブルに解決できる仕組みが揃っているので、ぜひこの機会に導入を考えてみてはいかがでしょうか。


mrubyの文字列結合のパフォーマンスを改善する

先日、来る下記のイベントの資料でmrubyの文字列結合におけるメモリパフォーマンスについて記述し、それを社内で共有したところ、それをみた @matsumotory がmrubyにおける文字列結合は + での結合より、破壊的ではあるが <<のほうがパフォーマンスが良いということに気づいた。

valgrindで測定すると下記のような具合である。

# new.rb
a = "aaa"
b = "bbb"
100000.times do |n|
  a += b
end
$ sudo valgrind ./mruby new.rb
...
==2374== HEAP SUMMARY:
==2374==     in use at exit: 0 bytes in 0 blocks
==2374==   total heap usage: 102,786 allocs, 102,786 frees, 15,002,227,528 bytes allocated
# concat.rb
a = "aaa"
b = "bbb"
100000.times do |n|
  a << b
end
$ sudo valgrind ./mruby concat.rb
==2380== HEAP SUMMARY:
==2380==     in use at exit: 0 bytes in 0 blocks
==2380==   total heap usage: 2,780 allocs, 2,780 frees, 1,152,039 bytes allocated

メモリのアロケーションだけで比較すると13000倍くらいの差があることがわかります。これはなぜこのような挙動になるかというと、 + の実装であるmrb_str_plus<< の実装であるmrb_str_catの違いにある。mrb_str_plusは c = "a" + "b" の場合、まず c のメモリをmrubyの 文字列2文字分確保してから、 "a""b" をその領域にコピーする。対してmrb_str_catは "a" << "b"の場合、"a"のメモリ領域を mrb_realloc を利用して拡張し、そこに "b"をコピーするような挙動をする。

ようするに前者における c の領域分のメモリ確保の差が先のvagrindで13000倍程度あることになる。この挙動についてはCRubyにおいても同じなので、破壊的な変更が許容出来るのであれば積極的に << を活用したほうがパフォーマンスは上がるだろうと思う。

厳密にはGCが走った時点でcの領域は開放されるので、実使用メモリとは乖離があるが、アロケーションされるという事実は変わらない。

さて、本題なのだが、先の @matsumotory とのディスカッションの中で、mrb_str_catがreallocするのであれば、予めそのサイズがわかっていれば文字列のサイズを予め設定することで、realloc処理すらなくして、爆速に出来るのでは?という話になった。

mrubyにおいては、文字列や配列の長さとは別に capa というmrb_int型の変数でメモリ容量が管理されている。ここの必要数が予めわかっているのであれば設定しておけば良いので、capaのsetter,getterを開発した。

具体的な用途に関しては例えばHTTPレスポンスのように予めヘッダーでレスポンスボディのサイズがわかっているような場合に、capaをそのサイズで設定しておくことで、realloc処理を無くすことが出来る。

まずは必要なcapacityを調べる。処理速度で比較したいので、先程よりループ数を100倍程度にします。

a = "aaa"
b = "bbb"
10000000.times do |n| 
    a << b
end
puts a.capacity
# => 48234496

次に速度を計測します。

# time mruby/bin/mruby sample.rb
real    0m1.396s
user    0m1.376s
sys     0m0.012s

さて、この値をベースに、このようにcapacityを設定したコードで計測。

c = 48234496
a = "aaa"
b = "bbb"
a.capacity = c

10000000.times do |n|
  a << b
end
puts a.capacity
# time mruby/bin/mruby sample.rb
real    0m1.365s
user    0m1.324s
sys     0m0.028s

「あれっ・・・早くなってない・・・」(0.03秒)

本来ならここで爆速バンザーーーイとなって意気揚々とまとめでも書くのですが、なぜでしょう。

ltraceを利用してreallocの数を比較したところ、さほど差がないことがわかりました。

# capacity設定なし
$ ltrace -e realloc mruby/bin/mruby sample.rb 2>&1 |  wc -l
2787

# capacity設定あり
$ ltrace -e realloc mruby/bin/mruby sample.rb 2>&1 |  wc -l
2767

これは何故かと言うと、mrb_str_catがreallocする時に、 MRB_INT_MAX / 2 を超えない限りは、原則既存のcapaの2倍の値でreallocするから、そもそもあまりreallocが起きないことにあります。

ふむふむ良く出来てると思いましたが、もう少しサイズが大きい文字列を扱うときにはもっと効いてくると思うので、capacity設定使っていきましょう。

けんちゃんくんさんの乱とはなんだったのか

mod_mruby ngx_mruby Advent Calendar 2017の記事を書きます。内容は僕が12月の初旬に格闘していたくんさんレンタルについてです。

なお、無事、収束しているのでではなくとして扱います。変と乱の違いは下記を参照ください。

nginxのtracとしてはこのあたりが近いと思います。

乱の詳細

ペパボのEC事業部のケンちゃんくんさんがロリポップマネージドクラウド上でサービスを開始したくんさんレンタルのサイト描画が正常に行われない場合があるというのが初期症状でした。

ブラウザのデバッガーを利用するとこのように、jsや画像資産が正常にダウンロード出来ていないことが確認できました。

jsや画像系で起きていたのでHTTP/2のサーバプッシュを疑ったのですが、特にそういった設定は入っていなかったため、次に、ストリームの不具合を疑い、nginxのhttp2_max_concurrent_streamsに1を設定してみました。

すると問題なく表示がされたので、被疑がHTTP/2に移ります。この段階でHTTP/2を利用しなければ問題ないことは確定ではあったので、本番サーバでは一時的にHTTP/2を無効にしました。

次に状況を再現し、デバッグするためにdocker-composeを利用して、諸々の環境を準備し、手元で実行したところ全く症状が起きないことがわかりました。

本番環境と手元の環境の差異を考えてみると、クライアントとサーバのNW距離と、それに比例するNWレイテンシが考えられます。

その仮説に基づき、下記のパラメーターのチューニングを行いました。(一部)

  • net.ipv4.tcp_tw_reuse
  • net.ipv4.tcp_rmem
  • net.ipv4.tcp_wmem
  • net.core.rmem_max
  • net.core.wmem_max

しかし、スループットに多少の変化はあったものの、不安定な状況は改善しませんでした。次に被疑に考えたのはngx_mrubyのフックポイントです。この症状が起きる前にmruby_output_header_filterを利用して、レスポンスの直前にある処理を行うというフックを追加していました。このフックを利用することでレスポンスの処理がブロックされることになるので、そこが一つ何か問題を起こしているのではないかと考えました。

そこで試しに、フックポイントをmruby_output_header_filterからmruby_output_body_filterへ変更したところ、問題なくコンテンツが表示されるようになったため、この時点でmruby_output_header_filterが原因だとこの時点で断定しました。

後に、問題はこれだけでは解決せず、最終的にはmruby_log_handlerのフックを利用することになりました。

これらのことからHTTP/2とngx_mrubyを利用した場合に、output_filterで何か問題がありそうだと思って、先程まで環境を粛々と作っておったのですが、手元の環境では再現すらせず、完全に詰んでいました・・・

おそらく、HTTP/2 + ngx_mruby + NWレイテンシなのかなぁ・・・とぼやっとは思っているものの、今日中に環境作るのがあれだったのでひとまず記事を書いてみました。今日はいよいよクリスマス・イブですね!明日はペパボのアドカレの担当なので皆様また明日お会いしましょう?

builderscon Tokyo 2017で圧倒的な敗北をもらって来た

8/4〜8/6にかけてbuilderscon Tokyo 2017にスピーカーとして参加させていただきました。当日話した資料については下記の通りです。

今回はmrubyの言語そのものの魅力と、僕たちが実装し、運用している内容について紹介させていただきました。

発表後、いくつかの質問や、Twitterでも質問をいただきすごく嬉しかったです。

一方で、今回の登壇は自分の現実をいい意味でフィードバックもらえる機会だったなと感じています。

具体的には当日全く集客ができませんでした。

会場としてはメインホールをいただいており、この上ない状況だったのですが、自身の実力がたりず、会場の10分の1も埋められていなかったのではないかなと思います。

こればっかりは当日にはどうすることもできない話ではあるのですが、つまるところ、その前段で、日常的にいかにアウトプットして、世界にインパクトを与え続けられているということが実践できてないのだなと感じました。

これまで積んで来たもので、それなりのステージに立てるようにはなって来たものの、まだまだ広く認められるというレベルには全く至っていないので、胡座をかくことなく、コードでも文章でもアウトプットし続けなければならいと身が引き締まりました。

僕はベストスピーカーの土俵にすら立てなかったので、また来年、強くなって帰ってこようと思います。

ここまでがスピーカーとしての話で、参加者としては、海外からのゲストスピーカーを始め、非常に幅広いテーマでこのカンファレンスでしか聞けない!!!という話しが多くあり非常に楽しく過ごすことができました。

個人的な話にはなるのですが、ペパボの縁がありつつも、会えそうで会えなかったmizzyさんとお会いし、さらには晩御飯をご馳走になったり、現在進行形で執筆を進めさせていただいている技術評論社の@inaoさんにダイレクトに原稿の催促をいただくなど、東京のカンファレンスだからこそ味わえる体験をさせていただきました。

カンファレンスに出て行き、アウトプットし、フィードバックを得るという意味では最高の体験になったなと思います。

参加者としてもまた来年足を運びたいなと思える、最高のイベントでした。

最後にまとめると、僕はやっぱり今回は悔しいです。しかし、その悔しさの原因がわかり、やることもはっきりしているので、さらに成長した姿を随時お届けできたらなと思います。

当日の僕のセッションに足を運んでくださった皆様、本当にありがとうございます。今日より最高な話を次も持って行くので、ぜひご期待ください。

mrubyでOpenSSLを利用し、SSL証明書を作成管理する

同僚である@matsumotoryのFastCertificateの実装について、ngx_mrubyからシェルスクリプトで構築されたコマンドであるdehydratedを利用して実装されているのですが、この場合、mrubyから証明書取得のステータスが管理しづらかったり、どうせならmrubyで全てコントロールしたいよねという話から、mruby-acme-clientを書きました。

中身の実装としてはCRubyのunixcharles/acme-clientとほぼ同じものなのですが、mrubyにはまだOpenSSLのバインディング実装がなかったことから、この機会に必要なものはCRubyの実装をCloneしました。

中にはmrubyに未実装なCRubyのメソッドなどがruby/opensslで利用されていたりして、苦労しましたがCRubyそのものの実装を読んだりしながら進めました。副次的な効果として、OpenSSLがどのようなバインディングで利用されているか、CRubyの細かい実装を読むことができたり、深く潜るきっかけになったなと思います。

証明書は下記のようなコードでLetsEncryptを利用して取得が可能です。

private_key = OpenSSL::PKey::RSA.new(4096)
endpoint = "http://127.0.0.1:4000/"
client = Acme::Client.new(
  private_key,
  endpoint,
  { request: { open_timeout: 5, timeout: 5 } }
)

# クライアントの登録
registration = client.register('mailto:contact@example.org')
registration.agree_terms

domains = %w(eample.org www.example.org)

# ドメインの保持を証明する認証
domains.each do |n|
  authorization = client.authorize(n)

  challenge = authorization.http01
  challenge.put_content '/var/www/html'

  challenge = client.fetch_authorization(authorization.uri).http01
  challenge.request_verification # => true
  puts challenge.authorization.verify_status # => 'pending'

  sleep(1)
  puts challenge.authorization.verify_status # => 'valid'
end

# 証明書の作成
csr = Acme::Client::CertificateRequest.new(domains)
certificate = client.new_certificate(csr)
{
  'privkey.pem' => certificate.request.private_key.to_pem,
  "cert.pem" => certificate.to_pem,
  "chain.pem" => certificate.chain_to_pem,
  "fullchain.pem" => certificate.fullchain_to_pem
}.each do |k,v|
  File.open(k, 'w'){|fp|
    fp.puts v
  }
end

開発中においてはletsencrypt/boulderを利用し、docker-composeでローカルにLetsEncrypt側のAPIを起動(127.0.0.1:4000)して開発を進めました。これを使うと、デバッグが非常にはかどりました。内容としてはdocker-composeの出来がよく、起動ごとに毎回Golangで書かれたAPIをコンパイルして起動してくれるので、簡単にAPI側にデバッグログを追加できたりして、どこの部分が失敗してるのかを追いやすい仕組みになっています。

mrubyにこの実装ができたことで、今後FactCertificateのようなことはもちろんのこと、例えば内部用のシステムだけど、LetsEncryptの証明書を使いたい、だけど認証のためだけにSGやiptablesを開けるのはなぁ・・・・といった漠然とした不安に対してもngx_mrubyでコントロールしつつ証明書の作成、更新を行うことが可能になります。

最後に

OpenSSLの実装がすごい量で大変であることを聞きながら、同僚にやると言ってしまった手前、何とかモチベーションを保ちつつも進めることができましたが、今回僕が実装しただけでも結構な量で本当にコンピューター言語のメンテナって大変なんだなぁと思いました。僕らが何気なく使っているライブラリも日々誰かの時間で成り立っていることを改めて感じたので、今よりもっと言語コミュニティに貢献していかねばという強い気持ちを持った。

スレッドセーフなmruby-signal-threadを書いた

最近仕事でmrubyを触り始めて、だいぶC言語に触れ合う時間が増えてきたので、@matsumotoryのススメもあって、mgemを書きました。

なぜ作ったのか

mrubyには既にmruby-signalというCRubyのクローン実装のmgemも存在するのですが、マルチスレッドでの動作を考えた場合に、グローバル変数にmrb_stateを持つことから、意図しない動作となるケースがあるため再実装しました。シングルプロセス、シングルスレッドで使うにはmruby-signalで十分なので、今回はコントリビュートではなく別実装とした意図があります。

使い方

シンプルにこのような実装にしました。mruby-threadにシグナルを指定可能にしたようなイメージが近いと思います。

SignalThread.trap(:HUP) do
  puts "foo"
end

puts "wait..."
loop { sleep 1 } 
$ mruby/bin/mruby example/signal_thread.rb &
wait...
$ kill -HUP $(pidof mruby)
foo

工夫

実装するにあたり工夫が必要だったのは、下記のコードです。

static mrb_value mrb_signal_thread_wait(mrb_state *mrb, mrb_value self)
{
  int sig, s;
  mrb_value *argv;
  mrb_int argc;
  sigset_t set, mask;
  mrb_value command, block;

  mrb_get_args(mrb, "*&", &argv, &argc, &block);

  if (!mrb_nil_p(block) && MRB_PROC_CFUNC_P(mrb_proc_ptr(block))) {
    mrb_raise(mrb, E_RUNTIME_ERROR, "require defined block");
  }

  sig = trap_signm(mrb, argv[0]);

  // 全てのシグナルをセットする
  sigfillset(&mask);
  // 処理したいシグナルだけを削除する
  sigdelset(&mask, sig);
  if (pthread_sigmask(SIG_BLOCK, &mask, NULL) != 0) {
    mrb_raise(mrb, E_RUNTIME_ERROR, "set mask error");
  }

  sigemptyset(&set);
  sigaddset(&set, sig);

  for (;;) {
    sigwait(&set, &sig);
    mrb_yield_argv(mrb, block, 0, NULL);
  }
}

今回の実装はSignalThread#trapが実行されるごとに、mruby-threadを利用して、そのシグナルを処理する専用のスレッドを起動し、スレッドでsigwaitするような実装としています。しかし、その場合、複数のスレッドを起動した場合に、先に起動したスレッドに後続のスレッドが処理するはずのシグナルが配送されてしまう事が起こりえます。

  1. HUPを処理するスレッド1を起動
  2. USR1を処理するスレッド2を起動
  3. USR1シグナルを送る

この場合に、スレッド1にUSR1が配送されてしまうケースに該当します。スレッド1のsigwaitはHUPのみを待ち受けているのですが、そこにUSR1が配送されても意図したハンドラ処理を行うことが出来ません。この辺のシグナルの流れは同僚の@harasouが書いたLinux シグナルの基礎が詳しいです。

この問題を避けるために、SignalThread#trapから起動するスレッド内で実行されるSignalThread#waitでは、引数に指定されたシグナル以外は全てpthread_sigmaskでマスクして、sigwaitすることで、意図しないシグナルの配送を防ぐ実装にしています。

最後に

これまでmgemコントリビュート中心に関わっていた、mrubyにおいて初めて自分でmgemを実装したわけですが、mruby開発の面白さとして、Rubyの実装をC言語で書いて、さらには今回のようにmrblibで既存のgemと組み合わせて一つのバイナリとするような、これまでとは違った融合的な楽しさがあるなと感じています。開発者としてほんとにチャンスが多い言語だと思うので、これからさらに食い気味に関わっていきたいと思います。
そして来年は100万円狙っていく。

100万円の漢