本稿のサンプルコードはGitHubにもアップしています。併せてご参照ください。
はじめに
HBaseは強い一貫性を持ち、アトミックなインクリメント処理が可能であることは前回までに説明してきました。今回はその特性を利用して、大規模なアクセスカウンタを作成していきます。
HBaseを大規模なアクセス解析サービスとして利用しているものの中では、Facebookのページインサイトが有名です。
今回紹介する簡易アクセス解析サービスは、Facebookのページインサイトと比べると簡易なものですが、HBaseにおけるさまざまなテクニックが含まれていますので、それらを紹介していきたいと思います。
対象読者
- HBaseを使ってみたいけど、どう使ったらよいか分からない方
- MySQLなどのRDB以外のデータベースを使ってみたい方
要件定義
それでは、さっそく要件定義をしていきましょう。以下のような機能を持つ簡易アクセス解析サービスを作っていくことにします。
- URLごとにアクセスをリアルタイムにカウントできる
- アクセスは、アワリー(毎時)、デイリー(毎日)、トータルで取得できる
- アクセスを取得する際に、取得する時間の範囲を指定できる
- 指定したドメインを共通に持つのすべてのアクセスを取得できる
論理設計
簡易アクセス解析サービスのER図は、以下のようになります。主キーとなるURLを「ドメイン」と「パス」に分けています。
物理設計
次に物理設計に入っていきます。物理設計は、「クエリ設計」をした後に、HBase上にどうマッピングするかを設計する「スキーマ設計」を行います。
クエリ設計
まず、クエリから考えていきましょう。要件定義からクエリを考えていくと、以下のようなメソッドを実装すれば良いと思います。
// アクセスをカウントする void count(String domain, String path, int amount) throws IOException; // アワリー(毎時)のアクセスを取得する List<Access> getHourlyCount(String domain, String path, Calendar startHour, Calendar endHour) throws IOException; // デイリー(毎日)のアクセスを取得する List<Access> getDailyCount(String domain, String path, Calendar startDay, Calendar endDay) throws IOException; // トータルのアクセスを取得する long getTotalCount(String domain, String path) throws IOException;
getHourlyCount、getDailyCount、getTotalCountはそれぞれアワリー、デイリー、トータルのアクセス数を取得するものです。ただし、クエリパターンとしてはそれらを同時に取ることはない設計になっています。
また、getHourlyCount、getDailyCount、getTotalCountの引数のpathはnullを許容し、その場合は指定したドメインを共通に持つすべてのアクセス数を取得できるようにします。
getHourlyCountやgetDailyCountの引数であるstartXXX、endXXXは、取得するアクセスの時間の範囲を表しています。getHourlyCountの引数は年月日時を指定し、getDailyCountは年月日を指定するようにします。
Accessクラスは以下のようになっています。
「時間」に関しては、引数同様にgetHourlyCountで取得したものは年月日時まで格納されており、getDailyCountで取得したものは年月日まで格納されます。
public class Access { // 時間 private Date date; // ドメイン private String domain; // パス private String path; // アクセス数 private long count; // ... setterやgetterは省略 }
スキーマ設計
それでは、実際にどのようにHBaseに格納していくのかを考えていきましょう。共通のドメインを持つすべてのアクセス数を同時に取得できる必要があります。
そこで、リバースドメインというテクニックを紹介します。このテクニックは、読んで字のごとくドメインを反対にします。
例えば、"blog.ameba.jp"というドメインを反対にして"jp.ameba.blog"というようにします。このようにすることで、jp.amebaをプレフィックスとしてScanすると、"jp.ameba.blog"や"jp.ameba.pigg"といったような共通のドメインを持つアクセス数を取得できるようになります。
今回は、このリバースドメインとpathを連結したものをRowKeyとします。クエリ設計から、アワリー、デイリー、トータルのアクセス数は同時に取得されません。なので、ColumnFamilyを別にして、これらを保存することにします。
これまでに説明した通り、ColumnFamilyごとに別の保存単位になるため、クエリパターンがまったく異なるデータを分けることで、ディスクI/Oの切り分けが可能です。
アワリー、デイリー、トータルのColumnFamilyをそれぞれ、"h"、"d"、"t"とします。
これは前回も説明しましたが、ColumnFamilyやColumnは各エントリに保存されるため、短いほうが効率的になるためです。
Columnには、アワリーの場合は年月日時を、デイリーの場合は年月日を入れます。トータルの場合はColumnを使用しないので空にしてしまいます。
また、Timestampは前回同様、データを追加・更新するときの時間を使うことにします。これらをまとめると、以下のようなスキーマになります。
RowKey | ColumnFamily | Column | Timestamp | Value |
---|---|---|---|---|
(reverse domain)-path | "h" | yyyyMMddHH | timestamp | counter |
(reverse domain)-path | "d" | yyyyMMdd | timestamp | counter |
(reverse domain)-path | "t" | "" | timestamp | counter |