HTraceによるHBaseのトレーシング

(この記事は、Hadoop Advent Calendar 2013の10日目の記事です)

並列分散処理の解析

HadoopやHBaseのような並列分散環境では、処理が多数のノードをまたがって実行されるために、トレーシングや性能のプロファイリングが難しくなります。各ノードで動作するサーバプロセスのロジックでは、他のノードにRPCで処理を依頼して結果を非同期に待つというパターンが多いため、CPUやネットワークのディスクのI/O帯域といったリソースを使い切る状況にはなりにくく、oprofileのようなプロファイラを利用して測定してみても、ボトルネックの手がかりがなかなか見つかりません。
そのような並列分散処理のトレーシングに利用しているツールとしてGoogleが発表したのがDapperで、例によってそれを参考にして作られたオープンソース実装としてHTraceがあります。HBaseのtrunkにはHTraceを利用してトレーシングを行う機能(HBASE-6449)が入っています。
例えば、HBaseシェルで以下のように実行してみます。

> trace 'start'
> create 'test', 'f'
> trace 'stop'

これによって取得されたトレーシング情報により、以下の画像にあるような情報をみることができます。

「クライアントがHMasterのテーブル作成のためのAPIを呼び出し、HMasterがその処理のためにHRegionServerのAPIを呼び出す」というような複数のノードをまたがる処理の流れと、要した時間を分かりやすく見ることができます。テーブルのcreateに続けて、putを実行した場合のトレーシング情報は、以下の画像のようになります。

> trace 'start'
> put 'test', 'row1', 'f:', 'val1'
> trace 'stop'

このグラフ表示自体はHTraceのもっている機能ではなく、HTraceと同様にDapperを参考にしてTwitter社で開発されたZipkinというツールを利用して表示したものです。HTraceには、取得したトレーシングデータをZipkinの形式に変換して送信するためのモジュールが付属しています。
HDFS-5274では、HDFS層にもHTraceによるトレーシング機能を追加するというパッチが既に開発中で、「HLogのhsyncの部分に妙に時間がかかっている」というような、HBaseの先にあるHDFS層も含めた解析ができるようになるものと期待されます。

トレーシングの実装

HTraceを利用したトレーシングは、以下のような使いやすい特徴を持っているというふれこみです。

  • 追加でトレーシング用のモジュールをインストールする必要がない
  • トレーシングを利用するために設定を変更してサーバを再起動する必要がない
  • トレーシング時の性能への影響が小さい

どのようにしてこれを実現しているのか、HBaseに追加されたトレーシング用のコードを簡単に確認してみます。
クライアント側のコードには、RPCを実行する際にサーバに渡すデータに、トレーシング用のデータを埋め込むためのコードが入っています。このコードは、クライアントアプリケーションがトレーシングを実行するためのAPIを呼び出した結果として、「トレーシング区間」にいる場合のみ実行されるような条件分岐の中にあります。
o.a.h.hbase.ipc.RpcClient#writeRequest::

    protected void writeRequest(Call call) {
      if (shouldCloseConnection.get()) return;
      try {
        RequestHeader.Builder builder = RequestHeader.newBuilder();
        builder.setCallId(call.id);
        if (Trace.isTracing()) {
          Span s = Trace.currentSpan();
          builder.setTraceInfo(RPCTInfo.newBuilder().
            setParentId(s.getSpanId()).setTraceId(s.getTraceId()));
        }

RPCで渡されるトレーシング情報は、呼び出しの親子関係を判断するためのIDだけという、小さなものです。
Tracing.proto::

message RPCTInfo {
  optional int64 trace_id = 1;
  optional int64 parent_id = 2;
}

これがリクエストヘッダのoptionalなフィールドとして定義されています。
RPC.proto::

message RequestHeader {
  // Monotonically increasing call_id to keep track of RPC requests and their response
  optional uint32 call_id = 1;
  optional RPCTInfo trace_info = 2;
  optional string method_name = 3;
  // If true, then a pb Message param follows.
  optional bool request_param = 4;
  ...

RPCで呼ばれた処理をサーバ側で実行するための共通部品にも、リクエスト中にトレーシング情報が存在すれば、トレーシング用のロジックを初期化するコードがあります。これによってサーバサイドでの処理の実行時刻についての情報が取得されます。また、サーバ側のロジックがさらにRPCで別ノードの処理を呼び出す場合にも、上記のトレーシング用呼び出し元情報(RPCTinfo)がリクエストに付加されて下流に伝わっていくことになります。
o.a.h.hbase.ipc.CallRunner#run::

      try {
        ...
        if (call.tinfo != null) {
          traceScope = Trace.startSpan(call.toTraceString(), call.tinfo);
        }
        ...
      } finally {
        if (traceScope != null) {
          traceScope.close();
        }

HBaseの各サーバプロセス(HMasterやHRegionServer)内では、トレーシング情報を保存するためのReceiverと呼ばれるモジュールが動作しています。このReceiverが、各ノードのトレーシング情報をファイルに書き出すなり、Zipkinに送信するなりします。トレーシング情報は、プロセス内のキュー経由でReceiverに渡されて非同期に処理されるので、性能に影響を与えにくくなっています。
o.a.h.hbase.regionserver.HRegionServer::

    private SpanReceiverHost spanReceiverHost;
    ...
    spanReceiverHost = SpanReceiverHost.getInstance(getConfiguration());

さらに、トレーシングを実行する頻度や契機を制御するための、Samplerというモジュールをトレーシングの際に利用することができます。

    public static TraceScope startSpan(String description, Sampler<TraceInfo> s, TraceInfo tinfo) {
       Span span = null;
       if (isTracing() || s.next(tinfo)) {
         span = new MilliSpan(description, tinfo.traceId, tinfo.spanId,
             random.nextLong(), Tracer.getProcessId());
       }
       return continueSpan(span);
     } 

まとめると、小さなトレーシング情報をRPCで受け渡されるデータのオプショナルなフィールドとして定義し、トレーシング情報が付いているリクエストが来た時だけトレーシングのための条件分岐に入るという作りが、使いやすさとオーバーヘッドの小ささを実現するポイントとなっているようです。

HTraceを使ってみる

ここでは参考として、実際にHTraceとZipkinを動かしてトレーシング情報を見てみるための手順を紹介します。以下の手順は、Amazon EC2RHELインスタンス(RHEL-6.4_GA-x86_64-10-Hourly2 (ami-5769f956))を利用して試した際のものです。
HTraceが追加されているのは0.96系以降のバージョンなので、trunk版のソースコードをビルドして実行します。まず、ビルドに必要なツール類をインストールします。(ここでは手っ取り早くRHELに付属のOpenJDK 7を利用していますが、実際の運用にはまだOracle JDKのほうが無難だと思います。)

$ sudo yum install git gcc gcc-c++ java-1.7.0-openjdk-devel

$ tar zxf apache-maven-3.0.5-bin.tar.gz
$ export PATH=/opt/apache-maven-3.0.5/bin:$PATH

$ wget http://protobuf.googlecode.com/files/protobuf-2.5.0.tar.bz2
$ tar jxf protobuf-2.5.0.tar.bz2 
$ cd protobuf-2.5.0
$ ./configure && make
$ sudo make install

次に、HBaseのソースコードをチェックアウトして、以下のdiffのように一部のファイルを修正します。

$ git clone https://github.com/apache/hbase
$ cd hbase
$ vi conf/hbase-site.xml 
$ vi pom.xml 
$ vi hbase-server/pom.xml 
$ git diff
diff --git a/conf/hbase-env.sh b/conf/hbase-env.sh
index 91aca76..2597970 100644
--- a/conf/hbase-env.sh
+++ b/conf/hbase-env.sh
@@ -26,7 +26,7 @@
 # into the startup scripts (bin/hbase, etc.)
 
 # The java implementation to use.  Java 1.6 required.
-# export JAVA_HOME=/usr/java/jdk1.6.0/
+export JAVA_HOME=/usr/lib/jvm/java-1.7.0-openjdk.x86_64
 
 # Extra Java CLASSPATH elements.  Optional.
 # export HBASE_CLASSPATH=
diff --git a/conf/hbase-site.xml b/conf/hbase-site.xml
index c516ac7..51b98aa 100644
--- a/conf/hbase-site.xml
+++ b/conf/hbase-site.xml
@@ -21,4 +21,8 @@
  */
 -->
 <configuration>
+  <property>
+    <name>hbase.trace.spanreceiver.classes</name>
+    <value>org.cloudera.htrace.impl.ZipkinSpanReceiver</value>
+  </property> 
 </configuration>
diff --git a/hbase-server/pom.xml b/hbase-server/pom.xml
index f5723b2..555488b 100644
--- a/hbase-server/pom.xml
+++ b/hbase-server/pom.xml
@@ -447,6 +447,10 @@
       <groupId>org.cloudera.htrace</groupId>
       <artifactId>htrace-core</artifactId>
    </dependency>
+    <dependency>
+      <groupId>org.cloudera.htrace</groupId>
+      <artifactId>htrace-zipkin</artifactId>
+    </dependency>
   </dependencies>
   <profiles>
     <!-- Skip the tests in this module -->
diff --git a/pom.xml b/pom.xml
index 77c97b7..f785c4c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1353,6 +1353,11 @@
         <artifactId>htrace-core</artifactId>
         <version>${htrace.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.cloudera.htrace</groupId>
+        <artifactId>htrace-zipkin</artifactId>
+        <version>${htrace.version}</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
   <!-- Dependencies needed by subprojects -->

書き換えたら、HBaseをビルドします。

$ mvn package -DskipTests

ビルドが終わったら、そのままソースツリーの中からサーバを実行することができます。デフォルトの設定の場合、ZooKeeperサーバもHMasterもHRegionServerも全部、1つのプロセス中で実行するローカルモードで起動します。

$ bin/hbase master start

つぎに、トレーシング情報を表示するために利用するZipkinをダウンロードします。

$ git clone https://github.com/twitter/zipkin
$ cd zipkin

Zipkinはソースツリーを取得してコマンドを実行すると、その場でまずビルドが行われ、サーバプロセスが起動してきます。とりあえずターミナルを3つ開いて、以下のコマンドを実行します。

(ターミナル1)$ bin/collector
(ターミナル2)$ bin/query
(ターミナル3)$ bin/web

準備が整ったら、HBase Shellを起動し、トレーシングを実行してみます。

$ bin/hbase master start
> trace 'start'
> create 'test', 'f'
> trace 'stop'

ZipkinのWeb UIはデフォルト設定では8080番ポートで待ち受けているため、Webブラウザでそこにアクセスして、トレーシング情報を確認します。

ローカルモードで確認できたら、HBaseを疑似分散モードで起動するように設定変更して試してみるとよいでしょう。上の方で貼付けた画像は、疑似分散モードで試した際のものです。