CubicLouve

Spring_MTの技術ブログです。https://github.com/SpringMT (http://spring-mt.tumblr.com/ からの移転)

Macでクラッシュレポートを見る

MacC++書いててSEGVとかを起こしたときのクラッシュレポートの置き場所をいつも忘れるのでメモ

/Users/:user/Library/Logs/DiagnosticReports 
/Library/Logs/DiagnosticReports 

この配下にある.crashを見る

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-teim)でタイマーが減少)を使います。タイマーが満了になると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.4.0)

macro RTEST (Ruby 2.4.0)

Man page of SIGACTION

Man page of GETITIMER

Man page of SIGSETOPS

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

rb_postponed_job_register_oneの説明 d.hatena.ne.jp 「postponed_job」の検索結果 - PB memo

rb_profile_framesの説明 ruby-trunk-changes r43156 - r43183 - PB memo

Ruby 2.1: Profiling Ruby · computer talk by @tmm1

st_updateの説明 ruby-trunk-changes r34141 - r34150 - PB memo

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

ログについてのまとめ

ログとは

時系列順になっているデータのこと。

全てが過去に発生する。

データログ - Wikipedia

ログ取得の目的

  • 記録・履歴(点)
    • 事象単体が意味を持つ場合に、時刻を含めてそのときの状況のデータを記録する
      • エラーログ
      • クラッシュログ
  • 変化量を捉える(線)
    • 時系列順に連続して観察したい場合に、時刻を含めてその時の状況のデータを記録し、その後に連続性を確認する
    • 連続性を確認するために、グラフなどで可視化する必要がある場合が多い

ログの設計

ログは出そうと思えばいくらでもだせる。

(それこそメソッド呼び出し単位で)

全部を出すと必要なデータが埋もれてしまう。

なので、ログの目的を明確化し、必要なログだけを出すようにする。

name Who 誰が Whom 誰に対して Where どこに 最終的な出力場所 What 何を When いつ 出力するタイミング Why なぜ HOW to どのように 出力方法 How many どれくらいの量 How much コスト感
アクセスログ Web サーバー 開発者 Hadoop アクセスログ アクセスするたびに ユーザー調査のため fluentd 100 logs / sec
アプリログ
分析ログ

ログの内容

ログの内容の基本的なもの。

情報は出力しすぎるとコストになるので出力する情報が必要か検討する。

名前 説明 詳細
When いつ 出力日時 yyyy-MM-ddTHH:mm:ss.ffffff、単位、出力時刻のタイムゾーン等を決めておく
Where どこで 出力された場所 アプリ名 ホスト、サーバーのIPアドレス プロセスID ログを出力したコードのパスや行数
Who 誰が クライアントのIPアドレス、ユーザーID、ユーザーエージェント、MySQLIPアドレス
What 何を ログレベル メッセージ 理由とか 実行コマンド

ログのフォーマット

ログの管理

ローテーションと保存期間

一つのファイルにログを書き続けると、一ファイルあたりの容量が増えていくのであとで加工しにくい(ログの肥大化)。

一定の容量や期間でログをまとめて別ファイルにして管理するようにする。

また一定期間を過ぎたログを削除もしくバックアップサーバー等に転送することで、サーバーのディスク容量があふれるのを防ぐ

logrotateなどがその役割を担ってくれている。

ログの活用

流量の監視

ログの送信方法

大量に送る場合の負荷

参照

dev.classmethod.jp

techblog.yahoo.co.jp

8.2.1 ログの運用

www.ipa.go.jp

"ArgumentError: key must be %d bytes" のエラーから暗号化を勉強し直す

Rubyの2.4以上から、共通鍵暗号を扱うOpenSSL::Cipherにおいて、暗号化鍵と初期化ベクトル(IV)を設定する際に、指定した暗号化方式の鍵長のビット数を超えて指定した場合、ArgumentErrorを返すようになりました。

irb(main):001:0> require 'openssl'
=> true
irb(main):002:0> c = OpenSSL::Cipher.new('aes-256-cbc')
=> #<OpenSSL::Cipher:0x007fc3c389da30>
irb(main):003:0> c.encrypt
=> #<OpenSSL::Cipher:0x007fc3c389da30>
irb(main):004:0> c.key = "1" * 33
ArgumentError: key must be 32 bytes
    from (irb):4:in `key='
    from (irb):4
    from /Users/hoge/.rbenv/versions/2.4.1/bin/irb:11:in `<main>'
irb(main):018:0> c.iv = "1" * 17
ArgumentError: iv must be 16 bytes
    from (irb):18:in `iv='
    from (irb):18
    from /Users/hoge/.rbenv/versions/2.4.1/bin/irb:11:in `<main>'

2.3までは短い場合のみエラーになっており、長い場合は切り捨てて使っていました。

https://github.com/ruby/ruby/blob/v2_4_0/doc/ChangeLog-2.4.0#L4332

github.com

class OpenSSL::Cipher (Ruby 2.4.0)

切り捨ててしまうのは、そもそもOpenSSLの仕様です。

なぜ切り捨てるようにできるのか?というのを暗号化の復習も兼ねて調べてみました。

暗号化

暗号化とは、意味の分かる情報(平文)を、意味の分からない情報(暗号文)に変換することを言います。

また、意味の分からない情報を意味の分かる情報に戻すことを復号(化)と言います。

暗号 - Wikipedia

暗号化する際に使う変換のアルゴリズムとかの歴史を追っていくと終わらないので、こちらを是非読んで貰えれば。

暗号化周りをわかりやすく読みやすく書いている本なので一読することをおすすめします。

AESとは

今回は暗号化方式の一つのAESを取り上げます。 AESはブロック暗号という暗号アルゴリズムに当たります。

ブロック暗号は、特定の固定長のビット数のまとまり(ブロック)に分けて、ブロックごとに暗号化していく暗号アルゴリズムのことです。

ブロックのビット数はブロック長と呼ばれます。

AESは128ビット(16バイト)のブロック長のみです。

暗号化する対象は、この固定長のまとまりであるブロック一つより大きいことがほとんどなので、 ブロック長ごとにブロック暗号アルゴリズムを繰り返し使って全体を暗号化する事になります。

この繰り返しの方法をブロック暗号のモードと呼びます。(複数の方法があります)

また、AESは暗号化と復号に同一の(共通の)鍵を使う対称鍵暗号化方式(共通鍵暗号方式)です。

AESの暗号化について

AESでは暗号化の鍵のビット長(鍵長)は128ビット、192ビット、256ビットが選択可能です。

この鍵を使って、ブロック長の128ビットと同じ長さのサブ鍵を作成して、それを利用して暗号化を行います。

なので、ブロック長と同じ長さである必要はありません。

鍵長がながければ長いほど、暗号強度は上がります。

AESの暗号化・復号ロジックの詳細は

の3章や

www.atmarkit.co.jp

などを参照ください。

ブロック暗号のモード CBC(Cipher Block Chaining)について

一つ前の暗号ブロックと平文ブロックのXOR(排他的論理和)を取ってから暗号化を行うモードです。

初期化ベクトル(IV)

最初の平文を暗号化するときには一つ前のの暗号化ブロックがないので、1ブロック分のビット列を用意する必要があります。 このビット列を初期化ベクトル(IV)といいます。

CBCモードの暗号化、復号化のフロー

CBCモードのフローは下記の通りです。

CFB encryption 作者 Gwenda (PNG version), WhiteTimberwolf (SVG version) (PNG version) [Public domain], ウィキメディア・コモンズ経由で

CFB decryption

暗号利用モード - Wikipedia

長い鍵と初期化ベクトル(IV)は切り捨てても問題ない?

そもそもAESの場合だと、鍵長を長くしても扱えませんし、IVを長くしてもブロック長を超えて与えても使えないから切り捨てるしかないですね。

でも、意図せずそういう設定にしてしまった可能性があるので、Rubyではあえてエラーにしているということかなと思います。

鍵のとIVを生成

ビット列として十分のランダムであることが求められます。

例えば鍵長が128ビットだと、2の128乗で340282366920938463463374607431768211456通り組み合わせがあります。

ただし、a-z A-Z 0-9 だけ鍵を作ろうとすると、62 の 16乗で、47672401706823533450263330816通りの組み合わせに減ってしまいます。

そこで、Rubyであれば、 module SecureRandom (Ruby 2.4.0) を使って、安全な鍵を生成するのがよいかと思います。

irb(main):001:0> require 'securerandom'
=> true
irb(main):002:0> SecureRandom.base64(16)
=> "NkIG8GcEL9fYTH+BXKYjQg=="

参考

理解してるつもりの SSL/TLS でも、もっと理解したら面白かった話 · けんごのお屋敷

thinkit.co.jp

The AES-CBC Cipher Algorithm and Its Use with IPsec

http://www.risk.tsukuba.ac.jp/pdf/group-work2005/2005group-4-resume.pdf

鍵 (暗号) - Wikipedia

openssl/evp_enc.c at 64846096b18340b9a39ddd29a7a0e23c56f22959 · openssl/openssl · GitHub

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