CubicLouve

Spring_MTの技術ブログ

bundlerを使っていて、'/' is not writable. Bundler will use '/tmp/bundrer/home/unknown/' as your home directory temporarily.` と出たときの対応

daemontools経由等で、デーモンのプロセスを立ち上げるときに、プロセス内でbundle exec hogeなどのコマンドを実行すると、

'/' is not writable.
Bundler will use '/tmp/bundrer/home/unknown/' as your home directory temporarily.

のようなwarningメッセージがでます。

これは、HOME が設定されていない環境におけるfallbackのための修正によって発生します。

1.14.5 以降のbundlerを使うと発生します。

https://github.com/bundler/bundler/blob/master/CHANGELOG.md#1145-2017-02-22

github.com

github.com

github.com

原因

下記のようなコードで再現できます(OSはMacです)。

% cat test.rb
p ENV['HOME']
p Gem.user_home

% HOME=/ bundle exec ruby test.rb
`/` is not writable.
Bundler will use `/var/folders/kw/_3kqzwx93nvbbl0h2wk6nkz8bj8f_z/T/bundler/home/hoge' as your home directory temporarily.
"/"
"/"

エラーがでるところはここです。

bundler/bundler.rb at 3203fdd2ad861af2aedfa233b754a02bfc1c4741 · bundler/bundler · GitHub

まずbundleコマンドでbundler自体の設定を行うのですが、bundlerのconfigurationオプションの設定が書かれているファイルから設定を読み込みます。

bundler/settings.rb at 3203fdd2ad861af2aedfa233b754a02bfc1c4741 · bundler/bundler · GitHub

bundler/settings.rb at 3203fdd2ad861af2aedfa233b754a02bfc1c4741 · bundler/bundler · GitHub

このconfigurationオプションの設定とは、

Bundler: bundle config

ここにある設定群になります。

この内容はbundle configで設定した内容が反映されています。

実際の内容はこんな感じです。

% cat .bundle/config
---
BUNDLE_GEM__TEST: "rspec"
BUNDLE_GEM__MIT: "false"
BUNDLE_GEM__COC: "false"
BUNDLE_BUILD__THERUBYRACER: "--with-v8-dir=/usr/local/opt/v8@3.15"
BUNDLE_BUILD__LIBV8: "--with-system-v8"

bundlerのconfigurationオプションの設定があるファイルはglobalとlocalの2つがあります。

localは実行しているカレントディレクトリ/.bundle/config にファイルが設置されています。

globalの場合が今回の問題で、設置されているパスは、環境変数BUNDLE_CONFIGがあれば、 $BUNDLE_CONFIG/.bundle/configになり、なれば$HOME/.bundle/configに設置されています。

ただし、HOMEBUNDLE_CONFIGが設定されていない場合や、HOMEに書込み権限がなければ実際に書込みが発生する際にエラーになっていました。(bundle installbundle updatebundle configを実行した時)

.bundle配下はbundlerの設定やgemのchecksumなどのキャッシュファイルが格納するために必須です。

そこで、HOMEBUNDLE_CONFIGも設定されていない場合や、.bundle配下が書き込めない場合においてtempfileにディレクトリを作成してそこに格納するようにします。

tempfileに格納する際に、warningメッセージがでます。

このメッセージが出た場合

.bundle/configで設定される内容はほとんどがビルドオプションになります。

Bundler: bundle config

bundle installbundle updateを行う場合にはbundlerのンパイルオプション等に影響があるので、要確認です。

ただし、bundle exec 経由でコマンドを実行する場合においては影響がないことがほとんどです。 (特にデーモン化したプロセスでbundler経由でコマンドを呼び出すと、HOMEが/になっているのでこのwarningがでると思います。)

自分で設定した内容を確認して、問題なければwarningをスルーでも大丈夫です。

デーモンでもどうしてもこのwarningを止めたい

bundler binstubを使ってプロジェクトにbundleされた実行可能ファイルを生成してその実行ファイル経由でコマンドを実行してください。

Bundler: bundle binstubs

MAC(メッセージ認証コード)とは

MAC(Message Authentication Code メッセージ認証コード)について、整理してみました。

(MACアドレスMACはMedia Access Controlの頭文字なので同じ文字面ですが全然意味が違いますね。)

MACは自分に届いたメッセージが送信者が意図した通り(改竄されてない、なりすましていない)であることを確認できます。

メッセージが改竄されていないことは、メッセージのintegrity(正真性、完全性)という性質です。

メッセージがなりすまされて送信されていない(正しい送信者からのものである)ということを、メッセージの認証という性質です。

MACは正真性を確認し、メッセージの認証を行います。

メッセージ認証コードとは

メッセージ認証コードには、

  • 送信者と受信者が共有する鍵(共有鍵、MAC鍵、ハッシュ鍵ともよばれることもある)
  • 任意のメッセージ

を元に固定ビット長の出力をします。

出力された値は MAC と呼ばれます。

一方向ハッシュ関数でメッセージのハッシュ値を取るのと似ていますが、この場合共有鍵はでてきません。

一方向ハッシュ関数を拡張して認証をできるようにしたのがMAC(メッセージ認証コード)になります。

MAC(メッセージ認証コード)を利用するための手順

MACを使った改竄検知、認証のフローの手順です。

  1. 受信者、送信者の二者で共有鍵を共有する(鍵配送問題が存在します)
  2. 送信者はメッセージと共有鍵の2つからMAC値を計算
  3. 送信者はメッセージとMAC値の両方を受信者に送る
  4. 受信者はメッセージを元にMAC値を計算する
  5. 受信者は送信されたMAC値を自分で計算したMAC値を比較する。

最後に計算したMAC値を比較し、MAC値が等しい場合は、メッセージが間違いなく送信者から送られてきたものします。

MAC

MAC(メッセージ認証コード)はハッシュ関数の拡張か生まれているので、一方向ハッシュ関数を用いて実現できます。

また、ブロック暗号でも実現可能です。

CMAC - Wikipedia

HMAC

MAC(メッセージ認証コード)を、一方向ハッシュ関数(SHA256等)を用いて実現する方法です。

  1. 共通鍵を一方向ハッシュ関数のブロック長より短い場合は、0パディングをする。
  2. 固定文字列ipad(バイト値 0x36 を 64回(ブロック長)繰り返した文字列)とパディングした鍵とのビット列のXORをとる
  3. 2で生成したバイト列をメッセージの先頭につける
  4. 3の結果を一方向ハッシュ関数を適用して、ハッシュ値を得る
  5. 固定文字列opad(バイト値 0x5C を 64 回(ブロック長)繰り返した文字列)と4で得られたビット列とのXORをとる
  6. 5で生成したビット列に4で生成されたハッシュをつける
  7. 6の結果を一方向ハッシュ関数に適用してハッシュ値を得る -> これがMAC値になる

再送攻撃(再生攻撃)への対応

すでに送られて、正しく認証されたMAC値とメッセージを取得しておき、それをなにも加工せずに使いまわして同じメッセージを送り続けることです。

防御策としては、

  1. メッセージにシーケンス番号を含めておく

    • 通信相手ごとに最後のシーケンス番号を記録しておかなければならない
  2. メッセージにタイムスタンプを含めておきMAC値が一致しても古いメッセージが送られて来た場合にエラーにする(脆弱性残る)

    • タイムスタンプの粒度(一秒とかだと一秒の間で攻撃される)の問題がある
    • 送信者と受信者で時計を一致させるか、古いと判定するための時間に幅をもたせておくことをするが、ここも攻撃される可能性がある
  3. 使い捨てのランダムな値(ノンス)をメッセージに含めて送信する。

    • 通信量が少し多くなるが、有効な手法

鍵の推測への対応

共有鍵は暗号学的に安全な疑似乱数生成器を使ってください。

できないこと

  • 三者に対する証明 送信者と受信者の区別が第三者には判断できない。

デジタル署名によって解決できます。

  • 否認防止 送信者と受信者の区別が第三者には判断できない。

これもデジタル署名によって解決できます。

認証付き暗号(Authenticated Encryption with Associated Data)

認証付き暗号 - Wikipedia

Authenticated Encryption with Associated Dataを略してAEADとも呼ばれます。

MAC(メッセージ認証コード)と対称暗号を組み合わせて、機密性(暗号)、認証・正真性(MAC(メッセージ認証コード))を満たす仕組みです。

AEAD(認証付き暗号)では不適切に細工された暗号文を識別して(MACの認証と正真性)復号を拒否することができるため、選択暗号文攻撃に対して安全になります。

AEADは現在のところTLSで利用できる最良の暗号化利用モードです。

SSL/TLSの暗号化利用モードにおいて、AEAD(認証付き暗号)の一種GCMが利用可能です。

Galois/Counter Mode - Wikipedia

ただし下記記事にあるように、素のままのOpenSSLには問題ないのですが、自前で実装されている場合は注意が必要です。

d.hatena.ne.jp

国際標準暗号GCMの新たな安全性評価結果(2012年08月17日):プレスリリース | NEC

参照

HMAC: Keyed-Hashing for Message Authentication

qiita.com

http://www.gtc.co.jp/glossary/cia.html

この記事は、クリエイティブ・コモンズ・表示・継承ライセンス3.0のもとで公表されたウィキペディアの項目「メッセージ認証符号」を素材として二次利用しています。

stackprofの原理

stackprofがどうやってプロファイルを行っているかを追ってみます。

プロファイルするコードは下記を使います。

stackprof/sample.rb at master · tmm1/stackprof · GitHub

自分用のメモなので、間違い等があるのはご容赦ください。。(随時ブラッシュアップしていければ)

プロファイルを取る仕組み

StackProd::Middleware経由でのプロファイリングの流れは下記の通り。

  1. リクエストを受ける
  2. StackProf.startが呼ばれプロファイリング開始
  3. リクエストを処理する
  4. StackProf.stopを呼びプロファイルングを終了
  5. save_every で指定した回数にリクエスト回数が達した場合、プロファイル結果をファイルに書込む

さらに今回はStackProf.startからStackProf.stopの流れを詳細におっていきます。

準備

gdbでおっていくので、マクロ等も展開できるようにデバッグ情報を付けてrubyをビルドします。

$ wget https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.1.tar.gz
$ tar zxf ruby-2.4.1.tar.gz
$ cd ruby-2.4.1 
# -O0で最適化を無効にし、-g3でデバッグ情報を付ける
./configure optflags="-O0" debugflags="-g3" --prefix="${HOME}/.rbenv/versions/2.4.1-O0_g3"
make
make install

サンプリング

StackProf.startが呼ばれると、stackprof_startが呼ばれます。 https://github.com/tmm1/stackprof/blob/master/ext/stackprof/stackprof.c#L61

start時に、modecpu または wallで設定すると、setitimer(2)intervalで指定した時間(マイクロ秒)のインターバルタイマーを設定し、タイマーが満了したときに送られるシグナルに対してsigaction(2)でサンプリングのシグナルハンドラーを設定します。

stackprofで指定するintervalはサンプリングする間隔(これは時間)で、save_everyはサンプリングした結果をファイル等に保存する間隔(これはリクエスト回数)です。

save_everyはHTTPサーバー向けのオプション(StackProf:: Middlewareでのみ指定可能)で、save_everyで指定した数リクエストがきたら、サンプリング結果を保存します。

save_everyが大きすぎると、サンプリング結果が肥大化し、保存するときに時間がかかってしまいますが、小さいとこまめに保存されるためファイル数が膨大になってしまうので運用しながら要調整です。

mode:objectの場合は、TracePointを使って、オブジェクトの確保のたびにサンプリングを行います。

mode 説明
:wall setitimer(2)ITIMER_REAL(実時間(時計時間 wall-clock-time)でタイマーが減少)を使います。タイマーが満了になるとSIGALRMシグナルが送られます。intervalはマイクロ秒になります。
:cpu setitimer(2)ITIMER_PROF(CPU時間でタイマーが減少)を使います。タイマーが満了になるとSIGPROFシグナルが送られます。intervalはマイクロ秒になります。
:object Rubyの新しいオブジェクト生成の毎にサンプリングします。RubyVM時間とでも言えるかな。

シグナルハンドラーはRubyのC-APIrb_postponed_job_register_oneを使ってサンプリングするjobを登録します。

サンプリングするときには、RubyのC-APIrb_profile_framesを使って、call stackにアクセスして、スタックフレームを取得します。

ruby/vm_backtrace.c at 249790802db62ff22c79830d4054c449fa3c243b · ruby/ruby · GitHub

サンプリング結果

各サンプルは複数個のスタックフレームで構成され、スタックフレームはMyClass#methodblock in MySingleton.methodように見えます。

サンプル内のこれらのフレームごとに、stackprofは下記のメタデータを収集します。

(aggregateオプションを有効にした場合のみです。デフォルトでは有効になっています。)

主に個々の部分でメタデータの収集を行っています。

https://github.com/tmm1/stackprof/blob/master/ext/stackprof/stackprof.c#L387

metadata 説明
samples スタックフレームで一番最初に呼ばれている回数。つまり、サンプリングしたときにまさに呼び出されていた回数。まずこの数字を確認する。
total_samples スタックフレームの中にふくまれてる回数。
lines スタックフレームの呼び出されている行とその回数。
edges このスタックフレームを呼び出したメソッドとその回数

結果

  • print_method(/pow|newobj|math/)

stackprof print_method(/pow|newobj|math/)

  • print_text

stackprof result.print_text

stackprof print_debug

サンプリングしていたときにスタックフレームは下記のようになっています。

  • A#powの場合

pow <- initializeなので、 powsamplestotal_samplesのカウントに1足され、initializeのtotal_samplesに1足されます。

$ gdb -nw -silen -x ./.gdbinit --args `rbenv which ruby` sample.rb

(gdb) break stackprof.c:387
(gdb) run
(gdb) rb_p rb_profile_frame_method_name(_stackprof.frames_buffer[0])
"pow"
(gdb) rb_p rb_profile_frame_method_name(_stackprof.frames_buffer[1])
"initialize"
(gdb) rb_p rb_profile_frame_method_name(_stackprof.frames_buffer[2])
  • A#mathの場合 math <- math <- initializeなので、matchsamplesには1、total_samplesのカウントに2足され、initializeのtotal_samplesに1足されます。

そのため、mathtotal_samplessamplesより約2倍ほど多くなります。

mathは中にブロックがあり、それがスタックに積まれています。

(gdb) rb_p rb_profile_frame_method_name(_stackprof.frames_buffer[0])
"math"
(gdb) rb_p rb_profile_frame_method_name(_stackprof.frames_buffer[1])
"math"
(gdb) rb_p rb_profile_frame_method_name(_stackprof.frames_buffer[2])
"initialize"

:wall:cpuの使い分け

ネットワークアクセスをする場合をサンプリングしてみます。

  • :wall

stackprof with wall

  • :cpu

stackprof with cpu

ネットワークI/Oの待ち時間やディスクI/Oの待ち時間はCPU時間には反映されないので、:cpuの場合は、ネットワークレイテンシが反映されないです。

これを踏まえると、まずは:wallモードでサンプリングしボトルネックがどこにあるか明らかにしましょう。

I/O待ち多く(特にデータベース外部サービスの外部要因でどうにもならない場合)、ノイズにしかならないので、:cpuモードでもサンプリングするのがよいでしょう。

参照

speakerdeck.com

関数一覧 (Ruby 2.7.0 リファレンスマニュアル)

macro RTEST (Ruby 2.7.0 リファレンスマニュアル)

https://linuxjm.osdn.jp/html/LDP_man-pages/man2/sigaction.2.html

https://linuxjm.osdn.jp/html/LDP_man-pages/man2/setitimer.2.html

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/sigdelset.3.html

real time/user CPU time/system CPU&nbsp;timeの違いをメモsiguniang.wordpress.com

rb_postponed_job_register_oneの説明 d.hatena.ne.jp ruby trunk changes

rb_profile_framesの説明 2013-10-07

Ruby 2.1: Profiling Ruby · computer talk by @tmm1

st_updateの説明 2011-12-28

BazelでC++をビルドする

以前Bazelの入門の記事をかきましたが、今回はC++をビルドする環境を作成してみます。 spring-mt.hatenablog.com

下記のチュートリアル通りにやっていきます。 Build Tutorial - C++ - Bazel

まずはexampleをcloneしてやっていくのですが、前回やった残りのプロジェクトもあるのでサクッと自分でやっていきます。

【1】単一パッケージ、単一ターゲットでビルド

workspaceのセットアップを行います。

workspaceにはソースファイルとビルド成果物が含まれます。

Bazelのowrkspaceとして、トップディレクトリにWORKSPACEファイルを置きます。

今回は依存関係がないので、空ファイルで置いておきます。

プロジェクトをビルドするために、全てのインプットと依存関係を同一workspaceに含める必要があります。

異なるワークスペースに存在するファイルは、リンクしていなければ互いに独立しています。

BUILDファイルはソースコードなどビルドに必要なファイルやビルド方法、出力されるファイルなどの設定を記述するファイルです。

BUILDファイルを含むワークスペース内のディレクトリはパッケージという単位になります。

BUILDファイルにはBazelのさまざまな種類の設定を記述しますが、一番重要な設定はビルドルールです。

ビルドルールは、実行可能なバイナリやライブラリなど、どのような成果物を作成するかを記述します。

BUILDファイル内のビルドルールのそれぞれの実体はターゲットと呼ばれ、ソース・ファイルと依存関係の特定のセットを指します。ターゲットは他のターゲット郡を指すこともできます。

最初に下記構成にしてみました。

.
├── WORKSPACE
└── main
    ├── BUILD
    └── hello-world.cc

まずは単純なBUILDファイルを見ていきます。

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
)

nameで指定したパラメータがターゲット名になります。(nameの指定は必須です。)

hello-worldターゲットはcc_binaryルールをインスタンス化します。 このルールでは依存関係のないhello-world.ccソースファイルから自己完結型の実行可能バイナリをビルドします。

ではビルドしてみます。

ビルドコマンドは

bazel build //main:hello-world

//main: はworkspaseのルートディレクトリからのBUILDファイルが配置されてる相対パスを示しています。hello-worldBUILDファイル内で指定したnameです。

% bazel build //main:hello-world
.....................
INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 12.816s, Critical Path: 3.27s
% bazel build //main:hello-world
INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 0.165s, Critical Path: 0.00s

(二回目はキャッシュが使われて高速にビルドできています)

ビルドが完了すると、workspaceのルート直下のbazel-binに成果物ができています。

 % bazel-bin/main/hello-world
Hello world
Fri Aug  4 01:11:51 2017

依存性の確認

BUILDファイルにはビルドの依存関係がすべて明示的に記述されています。

Bazelはこれらのステートメントを使用してプロジェクトの依存関係グラフを作成し、正確なインクリメンタルビルドを可能にします。

前述のプロジェクトの依存性のグラフは下記コマンドで生成できます。

% bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' --output graph
digraph mygraph {
  node [shape=box];
"//main:hello-world"
"//main:hello-world" -> "//main:hello-world.cc"
"//main:hello-world.cc"
}

グラフ構造はGraphViz で可視化できます。

【2】単一のパッケージ、複数のターゲット

大きなプロジェクトを複数のターゲットとパッケージに分割することで、変更した部分の再ビルドだけの高速なインクリメンタルビルドができ、分割されたプロジェクトを一度にビルドするとことでビルド時間の短縮ができます。

【1】プロジェクトを2つのターゲットに分けます。

diff --git a/CppExamples/main/BUILD b/CppExamples/main/BUILD
index 20c6f47..43b22a4 100644
--- a/CppExamples/main/BUILD
+++ b/CppExamples/main/BUILD
@@ -1,4 +1,13 @@
+cc_library(
+    name = "hello-greet",
+    srcs = ["hello-greet.cc"],
+    hdrs = ["hello-greet.h"],
+)
+
 cc_binary(
     name = "hello-world",
     srcs = ["hello-world.cc"],
+    deps = [
+        ":hello-greet",
+    ],
 )

hello-worldのリンクしたいライブラリのラベルをdepsに指定します。

こうすると、hello-worldをビルドする前に、hello-greetのビルドが実行されます。

% bazel build //main:hello-world
INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 5.813s, Critical Path: 1.16s

% bazel-bin/main/hello-world
Hello world
Sun Aug  6 15:23:47 2017

hello-greet.ccが修正されたら、hello-greet.ccのみコンパイルが行われます。

【3】複数のパッケージ、複数のターゲット

BUILDファイルを含む新しいディレクトリを作成します。 (BUILDファイルを含むワークスペース内のディレクトリはパッケージという単位)

cc_library(
    name = "hello-time",
    srcs = ["hello-time.cc"],
    hdrs = ["hello-time.h"],
    visibility = ["//main:__pkg__"],
)

srcsでソースファイルを、hdrsでヘッダーファイルを指定します。

lib/BUILDのなかで指定した// lib:hello-timeターゲットをmain/ BUILDから明示的に見えるようにするためにvisibility//main:__pkg__を指定しています。

デフォルトでは、ターゲットは同じBUILDファイル内の他のターゲットに対してのみだけに公開されています。

Bazelは、ターゲットの可視性を利用して、ライブラリの実装の詳細がパブリックAPIに漏れることを防いでいます。

Common Definitions - Bazel

ビルドをしてみます。

% bazel build //main:hello-world
INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 0.365s, Critical Path: 0.00s
% bazel-bin/main/hello-world
Hello world
Sun Aug  6 22:01:01 2017

ラベルの使い方

BUILDファイルやコマンドライン上では、ラベルからターゲットを参照します。

シンタックスは下記の通り。

//path/to/package:target-name

ターゲットがruleのターゲットの場合、path/to/packageはBUILDファイルを含むディレクトリへのパスで、target-nameはBUILDファイルのターゲット(name)の名前です。

ターゲットがファイルである場合、path/to/packageはパッケージのrootからのパスであり、target-nameはターゲットファイル名です。

同じパッケージ内のターゲットを参照するときは、パッケージパスを省略して//:target-nameとだけで使えます。

同じBUILDファイル内のターゲットを参照する場合は、//のworkspaceのルート識別子をスキップして、:target-nameだけを使用することができます。

Q4MのConditional Subscriptionのベンチマーク

Q4MのConditional Subscriptionを使う前にベンチマークを取ってみました。

Q4M - Tutorial

マシン

Model Name: MacBook Pro
Model Identifier: MacBookPro13,2
Processor Name: Intel Core i5
Processor Speed: 2.9 GHz
Number of Processors: 1
Total Number of Cores: 2
L2 Cache (per Core): 256 KB
L3 Cache: 4 MB
Memory: 16 GB

テーブルスキーマ

CREATE TABLE `q4m_test` (
  `id` varchar(255) COLLATE utf8mb4_bin NOT NULL,
  `payload` text COLLATE utf8mb4_bin NOT NULL,
  `enqueued_at` int(10) unsigned NOT NULL
) ENGINE=QUEUE;  

enqueued_atをConditional Subscriptionで使います。

enqueued_atにはキューを入れた時間を入れています。

Conditional Subscriptionにはintegral values(整数値)なので、unixtimestamp形式で保存しています。

ベンチの条件

予め6000件のデータを作成しました。 payloadには1000バイトの文字列を入れています。

【1】6000件のデータのうち、最後の1000件のキューを取り出して処理するパフォーマンス

こんなスクリプトで計測しました。

require 'mysql2'
require 'benchmark'

client = Mysql2::Client.new(host: "localhost", username: "root", database: 'test')

1000.times do
  result = Benchmark.realtime do
    result = client.query("SELECT queue_wait('q4m_test:enqueued_at>=1501686146', 5)")                                                                   
    client.query('select queue_end()')
  end
  puts "#{result}"
end

結果

f:id:Spring_MT:20170803002413p:plain

縦軸が秒になります。 横軸が時間軸になります。

特に劣化する様子はありません。

【2】5000件のデータを処理するパフォーマンス

上で5000件になったデータを全て順に処理していくようにして処理速度を計測してみました。

require 'mysql2'
require 'benchmark'

client = Mysql2::Client.new(host: "localhost", username: "root", database: 'test')

1000.times do
  result = Benchmark.realtime do
    result = client.query("SELECT queue_wait('q4m_test:enqueued_at<1501686146', 5)")                                                                   
    client.query('select queue_end()')
  end
  puts "#{result}"
end

結果

f:id:Spring_MT:20170803002854p:plain

縦軸が秒になります。 横軸が時間軸になります。

1とくらべてもそこまで大きな変化はありませんでした。

キューを入れる処理

キューを入れるときも処理速度を計測してみました。

require 'mysql2'
require 'benchmark'
require 'securerandom'

client = Mysql2::Client.new(host: "localhost", username: "root", database: 'test')
payload = 'a' * 1000

stmt = client.prepare('INSERT INTO q4m_test VALUES (?, ?, ?)')
                                                                                                                                                        
4000.times do
  result = Benchmark.realtime do
    stmt.execute(SecureRandom.uuid, payload, Time.now.to_i + 1000)
  end
  puts "#{result}"
end

結果

f:id:Spring_MT:20170803004509p:plain

○でくくったパフォーマンスが劣化した部分はおそらく、compactionが走った影響かと思われます。

参考にした記事

lestrrat.ldblog.jp

Bazel入門

Bazelとは

Googleが内部で利用していたビルドツールのオープンソース版として提供されているものです。

Bazel - a fast, scalable, multi-language and extensible build system" - Bazel

FAQ - Bazel

ビルドやテストの高速化を目指して作られています。

C++Javaだけでなく、iOSAndroidアプリのビルドにも対応しています。

今後のロードマップにはAndroid Studioとの統合も入っています。

Bazelを導入する理由

  • Gradleよりも構造的な設計になっていて、各アクションが何をするかが正確に理解しやすい構成になっている(らしい)。
  • すでに、ローカルでのキャッシュ、並列実行、依存性解析の最適化などが行われており高速に動作する。 さらに、分散キャッシュの仕組みも今絶賛入ろうとしていることろのなので、ビルドの高速化という意味では期待ができそう。

Bazelの公式ページも参照ください。

FAQ - Bazel

Bazelのinstall

Macであればhomebrew経由でインストールできます。

Installing Bazel on macOS - Bazel

homebrewでinstallすると最後に補完スクリプトのパスが表示されるのでそれを利用してzshの補完の設定をします。

Bash completion has been installed to:
  /usr/local/etc/bash_completion.d

zsh completions have been installed to:
  /usr/local/share/zsh/site-functions

Installing Bazel - Bazel

fpath[1,0]=~/.zsh/completion/
mkdir -p ~/.zsh/completion/
cp scripts/zsh_completion/_bazel ~/.zsh/completion

rm -f ~/.zcompdump; compinit

# This way the completion script does not have to parse Bazel's options
# repeatedly.  The directory in cache-path must be created manually.
zstyle ':completion:*' use-cache on
zstyle ':completion:*' cache-path ~/.zsh/cache

Bazelでビルド

まずは簡単なチュートリアルを試してみます。

Getting Started - Bazel

WORKSPACEファイル

Bazelは、ビルドしたいソースコードとビルド成果物を含むworkspaceというディレクトリでビルドを行います。

必要に応じて、1つのworkspaceを複数のプロジェクトで共有することができます。

workspaceはどこでもよいのですが、トップディレクトリにWORKSPACEファイルを必ず置く必要があります。

このWORKSPACEファイルに外部の依存性の定義を行います。

依存がなければ空ファイルでも大丈夫です。

touch WORKSPACE

BUILDファイル

Bazelは、プロジェクト内でのビルドターゲットを知るために、BUILDファイルを使います。

BUILDファイルはPythonに似たBazelのビルド用言語で記述します。

BUILDファイルにいろいろなルールを書いていきます。

各ルールは、入力、出力、および入力からの出力を計算する方法を指定します。

genruleについて

シェルコマンドを呼び出すgenruleは、Makefileを使っている人たちにおなじみのルールです。

genruleの詳細は下記ページにまとまっています。

General Rules - Bazel

今回は単純にechoコマンドで標準出力に出した内容をリダイレクトしてファイルに書き込むルールを作ってみます。

genruleでは、 cmdで指定したコマンドが実行されます。

genrule(
  name = "hello",
  outs = ["hello_world.txt"],
  cmd = "echo Hello World > $@",
)

今回、cmdの中で使われている$@はBazelの中で使える変数で、outsが一つの場合において、outsの内容が展開されます。

利用可能な変数の一覧は Make variables にまとまっています。

ターゲットは、ルールの中で設定しているnameによって指定されるlabelになります。

また、Bazelの成果物はソースツリーを汚染しないようにするために、ソースツリーとは別のbazel-genfilesディレクトリに格納されます。

1回目

% bazel build :hello
....................
INFO: Found 1 target...
Target //:hello up-to-date:
  bazel-genfiles/hello_world.txt
INFO: Elapsed time: 7.322s, Critical Path: 0.01s

2回目

% bazel build :hello
INFO: Found 1 target...
Target //:hello up-to-date:
  bazel-genfiles/hello_world.txt
INFO: Elapsed time: 0.155s, Critical Path: 0.00s

% cat bazel-genfiles/hello_world.txt
Hello World

BUILDファイルにoutsを複数して$@を利用するとビルドエラーになります。

genrule(
  name = "hello",
  outs = ["hello_world.txt", "hello_world2.txt"],
  cmd = "echo Hello World > $@",
)
% bazel build :hello
ERROR: /Users/hoge/BazelTest/SampleEcho/BUILD:4:9: in cmd attribute of genrule rule //:hello: variable '$@' : more than one output file.
ERROR: Analysis of target '//:hello' failed; build aborted.
INFO: Elapsed time: 0.161s

ルールは、他のルールの出力を入力として利用できます。

生成したsrcはlabelから参照できます。

$<srcのファイルの変数です。

genrule(
  name = "hello",
  outs = ["hello_world.txt"],
  cmd = "echo Hello World > $@",
)

genrule(
  name = "double",
  srcs = [":hello"],
  outs = ["double_hello.txt"],
  cmd = "cat $< $< > $@",                                                                                                                               
)
% bazel build :double
INFO: Found 1 target...
Target //:double up-to-date:
  bazel-genfiles/double_hello.txt
INFO: Elapsed time: 0.396s, Critical Path: 0.04s
% cat bazel-genfiles/double_hello.txt
Hello World
Hello World

genruleですが、通常利用するためのものではないです。

それぞれの言語に特化したルールがあるので、それを使ったほうがよいです。

Build Encyclopedia - Bazel

参考にした記事

knowledge.sakura.ad.jp

blog.matsuokah.jp

Common C++ Build Use Cases - Bazel

github.com

github.com