Home

タムタムの日記

Hiveで整形されていないログを集計する方法

タムタムです。Hadoop アドベントカレンダーの12/24分 を書かせていただきます。
それと、時々ログ解析飲み会というものをやっているという噂があるのですが、わたしも混ぜてください>< 

さて、まずはじめに・・。 ログが整形されているなんて都市伝説です。

自分が作るアプリは最初からログ設計をして整形して出力しているのですが、世の中そんなものばかりではありません。Hiveで集計するためにはある程度書式が整っていないとスマートに処理できません。
適当なスクリプトで処理するのも手ですが、もともと分散しないと処理できないほどの量なのに、それを分散環境で処理しないとか無いと思います・・。

となると、スクリプトを書いてHadoop Streamingでログを処理すればいいよねーとなるわけです。が、用途はある程度限られてしまいますが実はHiveでも出来ます。

例えば、以下のようなログがあるとします。
どこかで似たようなものを見たことある人も居るかもしれませんが気のせいです(=ω=)

2011-10-14 00:00:14  INFO  - [XXX][MONS] (charaId:123456, tmpId:56789) start attack monster(MonsterAAA) at (85.9,199.2,-590.6)
2011-10-14 00:00:14  INFO  - [XXX][MONS] (charaId:123456, tmpId:56789) start attack monster(MonsterAAA) at (85.9,199.2,-590.6)
2011-10-14 00:00:14  INFO  - [XXX][MONS] (charaId:123456, tmpId:56789) start attack monster(MonsterAAA) at (85.9,199.2,-590.6)
2011-10-14 00:00:18  INFO  - [XXX][ITEM] (charaId:123456, tmpId:56789) item can add *******
2011-10-14 00:00:18  INFO  - [XXX][ITEM] (charaId:123456, tmpId:56789) item add ******
2011-10-14 00:00:18  INFO  - [XXX][XXXX] *************
2011-10-14 00:00:18  INFO  - [XXX][MONS] (charaId:123456, tmpId:56789) kill monster(MonsterAAA) at (85.9,199.2,-590.6)

さて、このファイルにはモンスターの討伐記録のログがあります。 一番下のログです。
このログファイルからモンスター討伐数を集計したい場合、一番下の行には、誰が(charaId)いつ(時間)どこで(ワールド、エリア、XYZ座標)なにをした(MonsterAAAを倒した) が記録されており、まずはこの形式のログだけを抽出してテーブルに入れたいわけです。 (ワールドとエリアはログファイルの単位なので記録はされていないです。)

まずは元のgzログをtmp2テーブルに入れます。ログファイルはHDFS上にあります。
パーティションを適当に切ります。idは適当なIDを入れて作業がかぶらないようにする用です。OracleのPLAN_TABLEみたいなイメージです。別に無くてもいいです(=ω=)

create table tmp2(line string)
  partitioned by (id string, world string, area string);

load data inpath '/test/log/neko/2011-10-14-dungeon01.txt.gz'
overwrite into table tmp2
partition(id='aaa111', world='neko', area='dungeon01');

次にMap用のスクリプトを書きます。 (map2.pl)

#!/usr/bin/perl

use warnings;
use strict;

while (<>) {
  my ($world, $area, $line) = split("\t", $_);
  if ($line =~ /^((\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})).*\(charaId:(\d+).*tmpId:(\d+)\).*kill monster\s*\((\w+)\)\s*at\s*\((.*),(.*),(.*)\)/) {
    my @array = (
      $world,
      $area,
      $1,        # datetime
      $4,        # charaid
      $5,        # tmpId
      $6,        # monster-name
      $7, $8, $9 # x,y,z
    );
    print join("\t", @array), "\n";
  }
}

わざわざ配列に入れてjoinする必要なんてありませんよね。ごめんなさい。コメントを見やすくする関係でこう書きました。
ちょっと確認したい場合は、以下のようにやればスクリプトの確認はできます。

zcat hogefuga.log.gz | awk '{print "world01\tdungeon01\t"$0}' | ./map2.pl
world01  dungeon01  2011-10-14 00:00:18    123456  56789   MonsterAAA  85.9    199.2   -590.6

試しにSELECTしてみます。

SELECT TRANSFORM(line)
USING '/home/hadoop/map2.pl' AS (
  dt timestamp,
  charaId bigint,
  tmpId bigint,
  monster_name string,
  x double,
  y double,
  z double
)
FROM tmp2 WHERE id = 'aaa111'
LIMIT 10;
2011-10-14 00:00:18     12345678        00000000        MonsterAAA     85.9    199.2   -590.6
2011-10-14 00:00:18     12345678        00000000        MonsterBBB    -41.7    184.7   -261.3
2011-10-14 00:00:28     12345678        00000000        MonsterAAA     85.9    199.2   -590.6
2011-10-14 00:00:42     78901234        11111111        MonsterCCC     85.9    199.2   -590.6
2011-10-14 00:00:53     12345678        00000000        MonsterCCC     85.9    199.2   -590.6
2011-10-14 00:01:07     12345678        00000000        MonsterBBB     63.5    200.5   -550.9
2011-10-14 00:01:11     78901234        11111111        MonsterCCC     85.9    199.2   -590.6
2011-10-14 00:01:20     12345678        00000000        MonsterCCC     85.9    199.2   -590.6
2011-10-14 00:01:30     12345678        00000000        MonsterBBB    -61.7    184.0   -258.4
2011-10-14 00:01:37     12345678        00000000        MonsterBBB     13.5    200.1   -570.1

(結果をダミーのものに書き換えるのめんどくさい・・・。でも本物出すわけにはいかないし・・。)

集計してみます。

SELECT world, area, monster_name, count(*) as cnt FROM (
  SELECT TRANSFORM(world, area, line)
  USING '/home/hadoop/map2.pl' AS (
    world string,
    area string,
    dt timestamp,
    charaId bigint,
    tmpId bigint,
    monster_name string,
    x double,
    y double,
    z double
  )
  FROM tmp2 WHERE id = 'aaa111'
) tbl GROUP BY world, area, monster_name ORDER BY cnt desc;
neko    dungeon01       Monster001      3215
neko    dungeon01       Monster002      1733
neko    dungeon01       Monster003      945
neko    dungeon01       Monster004      736
neko    dungeon01       Monster005      476
neko    dungeon01       Monster006      471
neko    dungeon01       Monster007      455
neko    dungeon01       Monster008      407
neko    dungeon01       Monster009      181
neko    dungeon01       Monster010      168
neko    dungeon01       Monster011      154
neko    dungeon01       Monster012      22
neko    dungeon01       Monster013      16
neko    dungeon01       Monster014      8
neko    dungeon01       Monster015      8

集計できました(・ω・)ノ
Tmpテーブルを経由しないといけないのがアレですが、externalなのでログの複製は行われないし、中間テーブル的な整形されたテーブルの実体も持たなくて良いのです。集計前にテーブルに1回入れる場合は普通に末尾にINSERT構文を書けばできます。まぁこういう方法もありますよと言うことで。

今回はMapしか書いてませんが、Reduceもスクリプトで書くことができます。
https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Transform

 

Hadoopアドベントカレンダー2011 17日目 - Hive 0.8 について -

こんにちは、タムタムです。
久しぶりに記事を書きます。 Hadoopアドベントカレンダー2011の17日目という事で書かせていただきます。

私のような底辺エンジニアでも集合論の知識のみで簡単にデータ集計処理が書けるのがHiveの良いところだと思います。
試しに某ゲームのモンスター討伐のログを、数日分、全ワールド、Zoneエリア、モンスター単位で集計したら100秒くらいで結果が出ました。(その時はレコードが1億程度と少ないという事もありますが)
※ちなみに私はマイニングエンジニアではありません。

というわけでHive0.8に関する事を書きたいと思います。
Hive0.8ではBitmapインデックスやtimestamp型が追加されたというのが大きなトピックらしいです。さっそく新機能を試してみたいと思います。また細かい構文が追加されているようなので、それもおいおい試してみたいと思います。今回はtimestamp型について書きます。
ちなみに、まだリリースされていないのでソースを持ってきてビルドする必要があります。ビルドの方法は次の通りです。

svn co http://svn.apache.org/repos/asf/hive/branches/branch-0.8/ hive-0.8
cd hive-0.8
ant clean package

build/dist配下にパッケージが出来ていますので、build/distをHIVE_HOMEに設定してbin/hiveを実行します。

※(追記)Hive 0.8は12/16にリリースされてましたw

今まで日付データをhiveで扱うにはstringとして保持してWHERE句にUDFで日付に戻して計算をするとか、bigintでunixtimeとして保持したりしていました。timestamp型についてまずはどんな操作ができるのか調べてみます。

hive> show functions;

の結果を0.7と0.8で比較し、0.8で追加されたUDFを列挙しました。 (descriptionはコマンド打っても出てこなかったものがあるので書くのをやめましたw)

UDF Description
ewah_bitmap
ewah_bitmap_and
ewah_bitmap_empty
ewah_bitmap_or
from_utc_timestamp
map_keys
map_values
named_struct
timestamp
to_utc_timestamp

またJIRAのチケットによると、datetimeに関する様々なUDFがtimestampをサポートしたようです。

例えば以下のような操作ができます。
サンプルデータを1件用意します。書式はyyyy-MM-dd HH:mm:ss か yyyy-MM-dd HH:mm:ss.fffffffffのみです。

2011-12-25 09:00:00
create table ts1(t1 timestamp);
load data local inpath '/mnt/iscsi/test1/data4.txt' overwrite into table ts1;

select cast(t1 as float), cast(t1 as double) from ts1 limit 1;
1.3247712E9     1.3247712E9

select cast(t1 as bigint), cast(t1 as int), cast(t1 as boolean), cast(t1 as tinyint), cast(t1 as smallint) from ts1 limit 1;
1324771200      1324771200      true    -128    26496

select cast(t1 as string) from ts1 limit 1;
2011-12-25 09:00:00

select cast('2011-12-25 9:00:00.000000000' as timestamp) from ts1 limit1;
2011-12-25 09:00:00

select
  unix_timestamp(t1),
  year(t1),
  month(t1),
  day(t1),
  dayofmonth(t1),
  weekofyear(t1),
  hour(t1),
  minute(t1),
  second(t1),
  to_date(t1)
from ts1;
1324771200      2011    12      25      25      51      9       0       0       2011-12-25

select date_add(t1, 5), date_sub(t1, 5) from ts1 limit 1;
2011-12-30      2011-12-20

select datediff(t1, t1), datediff(t1, '2011-12-10'), datediff('2011-12-20 9:00:00', t1) from ts1 limit 1;
0       15      -5

新しいUDFも試してみましょう。

select from_utc_timestamp(t1, 'JST') from ts1 limit 1;
2011-12-25 18:00:00

select from_utc_timestamp(t1, 'UTC') from ts1 limit 1;
2011-12-25 09:00:00

select from_utc_timestamp(t1, 'PST') from ts1 limit 1;
2011-12-25 01:00:00

select to_utc_timestamp(t1, 'JST') from ts1 limit 1;
2011-12-25 00:00:00

select to_utc_timestamp(t1, 'UTC') from ts1 limit 1;
2011-12-25 09:00:00

select to_utc_timestamp(t1, 'PST') from ts1 limit 1;
2011-12-25 17:00:00

うまく動いているように見えます。が、以下のクエリを発行すると正しい結果が得られません。
SELECT句に複数記述するのはNGなのかな?

select from_utc_timestamp(t1, 'JST'), from_utc_timestamp(t1, 'UTC'), from_utc_timestamp(t1, 'PST') from ts1 limit 1;
2011-12-25 10:00:00     2011-12-25 10:00:00     2011-12-25 10:00:00

select to_utc_timestamp(t1, 'JST'), to_utc_timestamp(t1, 'UTC'), to_utc_timestamp(t1, 'PST') from ts1 limit 1;
2011-12-25 08:00:00     2011-12-25 08:00:00     2011-12-25 08:00:00

ちなみに、2011-12-25 09:00:00はどのTimeZoneの9時なのか?という事が気になると思います。チケットを見ると、「hive.time.default.timezone」こういう設定が一瞬だけあったようですが、「Removed references to a "default" timezone. All times are treated as UTC」という事で無かったことになっています。実際は上の結果を見る限り(castとか)TimeZone.getDefault()が使われているように見えます。(すいませんソース追っていません)。なのでOracleVMの場合はVMプロパティのuser.timezoneで指定できますが、システムプロパティなので他に影響を及ぼしそうで怖いです。

Javaのコードで確認したら似たような動作したので、たぶんDefaultのTimeZoneを使っていると思います。grepしたらUTCとかいっぱい出てきましたけどw
(ていうか、Hiveのソース読めよって話ですよね)

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("JST"));
System.out.println(sdf.parse("2011-12-25 09:00:00").getTime());

sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
System.out.println(sdf.parse("2011-12-25 09:00:00").getTime());
1324771200000
1324803600000

個人的には、時間に関する型をサポートするならSimpleDateFormatの書式をtableの設定で出来ると嬉しいんですけどね。

というわけで、timestamp型に関する事を書いてみました。
もしもネタが足りなければ列指向のRCFileとSequenceFileの性能の違いやRCFileをJavaののコードで作る方法あたりを書きたいと思います。

間違いを見つけたらどんどん指摘してくれると嬉しいです(・ω・)ノ

ATOKのカナロック解除方法

ATOKを使っていると突如カナロックがかかってしまう現象があります。わたしの環境ではメモリ不足になると(仮想メモリを使うようになると)発生するみたいです。

で、これの解除方法ですが、CTRL+SHIFT+CAPS LOCKで直りました。わたしのノートだけかもしれませんが(´・ω・)

 

zfsのキャッシュサイズ(c_max)調整

ZFSはデフォルトで搭載メモリの3/4までキャッシュするような実装になっています。メモリ4GB積んでいると3GBまでキャッシュするようになっているわけですね。ファイルサーバとしてしか使わないのであれば、それでも良いのですが、他にもメモリを使いたいのでちょっと調整してみます。

まずは、現在の値を確認します。

root@tamsvr00:~# kstat | grep c_max
        c_max                           3206670336

そして変更します。/etc/systemに以下の記述を追加します。

set zfs:zfs_arc_max = 0x40000000

これで1GBになります。再起動必須です。

再起動後、もっかい確認すると設定が変更されています。

root@tamsvr00:~# kstat | grep c_max
        c_max                           1073741824

「zfs_arc_max」で検索をすると、それっぽい記事が(というかGoogleの一番上にZFSのTuningに関するWikiが)ヒットします。

どこかの勉強会で、メモリ云々でソースを書き換えたとか言ってた人が居たので、この設定で変更できますよっと。(というかソース書き換えたって事はソース見ているって事だから設定値読んでいるって事も分かっているはず。という事は、これの事ではないのかな・・?)

 

ちなみにこのネタは会社の人に教えて貰ったものだったのでした。

ZFS on Solaris

久しぶりの更新です。
非常にバタバタしている・・というか、月300時間労働超えました(´・ω・)ウエェ・・。

クローラーを動かしたり、色々と実験をしているのでファイル置き場が無くなってその度にサーバを止めてHDDを追加していくのは大変です。というわけで、今回はそこそこ容量の大きなファイルサーバを追加しました。

構成は以下になります。

  • OpenSolaris (Solaris10はライセンスが・・・)
  • AtomD510
  • メモリ4GB
  • HDD IDE 80GB (システム用)
  • HDD SATA 2TB * 4基 (データ用)
  • GigabitLAN * 1

最初はUSBにインストールして使おうとしましたが、USBへの書き込みが発生するとシステム全体が止まって使い物にならなかったのでHDDにインストールする事にしました。

DVDドライブはないので、LiveUSBでブート。それからHDDにインストール。(Liveで起動するとインストールするというアイコンがあるので、それで)。最初はLiveUSBブート → USBにインストールでしたが、↑の理由により断念。

ZFSは非常に簡単。今回はrootで以下のコマンドを叩くだけ。

    zpool create data raidz c4t0d0 c4t1d0 c7d0 c8d0

これだけです。 これだけで/dataにマウントまでしてくれます。マウントを変えたい場合は「zfs set mountpoint」で変更するだけ。NFSで共有したい場合は、

    zfs set sharenfs=on data

これだけです。

raidzはRAID5と同等です。(いやー本当はraidz2にしたいのですがHDDが足りなくて・・・)。raid5とか会社で運用しているところがあればプギャー・・ですね。

root@tamsvr00:~# zpool list
NAME    SIZE   USED  AVAIL    CAP  HEALTH  ALTROOT
data   7.25T  1.31M  7.25T     0%  ONLINE  -
rpool    76G  6.62G  69.4G     8%  ONLINE  -
root@tamsvr00:~# zpool status
  pool: data
 state: ONLINE
 scrub: none requested
config:

        NAME        STATE     READ WRITE CKSUM
        data        ONLINE       0     0     0
          raidz1-0  ONLINE       0     0     0
            c0t0d0  ONLINE       0     0     0
            c0t1d0  ONLINE       0     0     0
            c7d0    ONLINE       0     0     0
            c8d0    ONLINE       0     0     0

errors: No known data errors

  pool: rpool
 state: ONLINE
 scrub: none requested
config:

        NAME        STATE     READ WRITE CKSUM
        rpool       ONLINE       0     0     0
          c1d0s0    ONLINE       0     0     0
root@tamsvr00:~# df -h
Filesystem            Size  Used Avail Use% Mounted on
rpool/ROOT/opensolaris
                       71G  4.3G   67G   7% /
swap                  4.8G  324K  4.8G   1% /etc/svc/volatile
/usr/lib/libc/libc_hwcap1.so.1
                       71G  4.3G   67G   7% /lib/libc.so.1
swap                  4.8G  8.0K  4.8G   1% /tmp
swap                  4.8G   48K  4.8G   1% /var/run
data                  5.4T  927K  5.4T   1% /data
rpool/export           67G   23K   67G   1% /export
rpool/export/home      67G   23K   67G   1% /export/home
rpool/export/home/tamtam
                       67G   45M   67G   1% /export/home/tamtam
rpool                  67G   81K   67G   1% /rpool

こんな感じでパリティ有で5.4TBの領域が出来ました。
これを各サーバで共有するのと、WindowsからはSambaでファイル置き場として共有します。CPUがあまり気味なのでMercurialやMailサーバくらい置こうかな。

レモン牛乳

栃木っこにはおなじみのレモン牛乳。
実は都内でも普通に売っています。

私はこれを飲むと運動会を思い出すのです。運動会では、このレモン牛乳(200ミリリットルのやつ)とコーヒー牛乳と普通の牛乳が売店で売り出すのです。

レモン牛乳

都内では500ミリリットルのやつが189円でした。こんなに高かったっけ・・。
というか、このヨーグルトってなんですか。こんな派生製品知らないよ!

(追記)
私がいつも購入する場所は、スーパーオオゼキの戸越銀座店です。日曜日にはいつも置いてあります。
(これ書くと私の生息範囲がわかっちゃうのと、私の購入分が減るのが嫌なんですが・・・w)

(さらに追記)
ヨーグルト食べました。
なんでもかんでも商品化すれば良いってもんじゃねーぞっ! という感想です。
「わぁー黄色い。まるでヨーグルトの糖尿病やぁ~!」

デザイン変更案

遅くなりましたが、あけましておめでとうございます。
そろそろブログのデザインを変更したいなと思いまして、ちょっとワンポイントとなるイラストを探してました。

ポップな感じにするならこんな素材かなぁ。

繊細な感じにするならこんな素材?

うーん。迷う。
コンセプトアート的な素材集ってないかなぁ・・。もうプロの人に依頼しようかな・・・w

ちなみに、いずれの画像も用途制限無しの素材なのです。画力の無い自分には嬉しい限りです。
素材集のお値段は1980円です。

DVD-ROM付き 萌えキャラフリーイラスト集

Postgresをインストールしてみた

MySQLはJDBCからアクセスする時に全件取得するのでバッチ処理が書きづらいという理由でPostgreSQLをインストールしてみました。(フェッチサイズをInteger.MIN_VALUEに設定すれば1件づつの取得になりますが、この間に他のトランザクション走らせることができないので。)(あれ・・・コネクションを2本使えばいいのでは・・?)

インストールしたバージョンは8.4.2です。

ビルドします。

./configure --prefix=/lunar
make
make check
make install

DB初期化します。

initdb --encoding=utf-8 -U totoro --pgdata=/lunar/data/postgres

起動します。

pg_ctl -w -o "-i" -D /lunar/data/postgres -l /lunar/var/postgres.log start

スキーマを作ります。

createdb -E utf-8 -O totoro -W

 外部から接続できるように/lunar/data/postgres/pg_hba.confに設定を追加します。 allとか本当は良くないけど(・ε・)キニシナイ!!

host    all         all         192.168.0.0/24        md5

パスワードを設定しなおしました。

ALTER USER totoro with password 'xxxxxxxx';

他のホストから接続してみる。

psql -h 192.168.1.202 -U totoro postgres

手間はMySQLよりも1つ多いくらい。
やばい。。Postgresの設定パラメータの意味がわからない(´・ω・)
メモリ設定とかDISKのフラッシュ設定とかログ設定とか運用設定とか。

調べるか(´・ω・)

 

ClassLoaderを使って実行時にクラスを更新する方法(リロード処理)

JavaのClassLoaderの話をちょっとだけ。意外にClassLoaderを知らない人が居るようです。

ざっくりと説明をしますと、
クラスローダーにはBootstrapクラスローダーやシステムクラスローダー、コンテキストクラスローダーがあります。(あと拡張クラスローダーですかね)。そんでもって、クラスローダーは親子関係を持っています。委譲モデルと言って、あるクラスローダーにクラスをロードする依頼があると、親に依頼を委譲します。最初に発見されたクラスが実際にロードされるわけです。

1. Bootstrap (rt.jar)
2. 親クラスローダーA
3. 子クラスローダーB

こんなクラスローダーがあるとしてまだロードされていないクラスXをロードしようとした場合は次のように動作します。

  1. B.loadClass(X)
  2. Aに委譲
  3. Bootstrapに委譲
  4. BoostrapにXは存在しない
  5. AにXは存在しない
  6. BにXが存在するのでロードする。

ちなみにClassLoader#findClassをオーバーライドして自前の処理で上書きしてしまえば、この委譲モデルを破壊する事ができます。まぁそんな事は非推奨ですけど(´・ω・)

JVMプロセス内においては異なるClassLoaderで読み込んだクラスは例え同じバイナリでも異なるクラスとして扱われます。ClassLoaderAによってロードされたクラスXと、ClassLoaderBによってロードされたクラスXは別物です。
※クラスAとBが親子関係を持っていて、AがXをロードしている時にBでロードしても委譲モデルでAが既にロードしてあるXを使うので、こういう場合は同じ定義を指します。
TomcatのWebApps1とWebApps2の関係を想像すると理解しやすいと思います。

この特性を生かすとリロード処理が実現できるわけです。

ClassLoaderの細かい話はDeveloperWorksの記事が丁寧なのでそちらを参照した方が良いです。今回はTomcatにも実装されているリロード処理のサンプルを作ってみたいと思います。TomcatのドキュメントにTomcatのClassLoaderの戦略が書いてあるのでそれを参考にすると分かりやすいと思います。

例えばRPCサーバやスケジューラーを考えます。これらを設計する時には大きく二つに分けることができ、RPCサーバではサーバ本体と各命令、スケジューラーでも本体と各タスクという感じです。Tomcatの場合はServletエンジンとビジネスロジックを書いたServlet。(ビジネスロジックはServletに書くものではありませんけどね。)
RPCの命令、スケジューラーのタスク、ServletコンテナのServletは抽象化する事ができ、インターフェースを切ることが多いです。(HttpServletとかね。)

擬似コードだとこんな感じですね。

class MyServer {
  void run() {
    ITask task = getTask();
    task.execute();
  }
}

このMyServerとITaskを本体側のClassLoaderに、ITaskを実装したクラスを別のClassLoaderに読み込ませます。
例えば本体はCLASSPATHを指定してシステムクラスローダーに読み込ませます。(TomcatはBoostrap用のクラスを挟んでいますけどね。)
タスクは独自のクラスローダーをインスタンス化(このオブジェクトをc1とする)して(URLClassLoaderを直接でも良い)、そのClassLoaderを使ってクラス定義を読み込ませます。C言語だとdlopenみたいなイメージです。

異なるクラスローダーから読み込んだクラスは別のクラスになるので、リロードしたい場合はClassLoaderをもう1回インスタンス化(c2)してあげればいいのです。

文章だけで説明してもアレなので簡単なサンプルを書いてみました。
Thread#setContextClassLoaderを使うともっと簡単に書けると思います。

サンプルの内容は定期的にタスクを実行するだけのものです。Reload処理はJMX経由で行うようにしました。
Eclipse上で作る時はタスククラスは別プロジェクトで作るのが良いです。同じプロジェクトに入れてしまうとシステムクラスローダーによってタスクもロードされてしまうので。色々適当です。Sleepの代わりにSemaphoreを使っている理由はsleepの場合Future#cancelで割り込みが発生しなかったからです。Semaphoreだと割り込みが入ってすぐに終了できます。
tasklist.propertiesに登録したいタスクのクラス名を書く必要があります。まぁこんな事をしなくてもLunarClassLoaderに手を加えて特定のインターフェースを実装しているクラスを検索(列挙)すれば良いだけです。今回はそこまで余力がありませんでした。 タスク用のクラスパスはソースに直書きしています。自分の環境に合わせて修正してください。VMの引数に-Dhogehogeを指定してシステムプロパティとして読み込むのが一般的な方法かと思います。

package at.orz.lunar;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import javax.management.MBeanServer;
import javax.management.ObjectName;

public class LunarBootstrap {

    public static void main(String[] args) throws Exception {

        URL[] urls = new URL[]{
            new File("C:/usr/local/eclipse3.5/workspace/lunar-subsystem/lunar-task.jar").toURI().toURL(),
            new File("C:/usr/local/eclipse3.5/workspace/lunar-subsystem/bin").toURI().toURL()
        };

        final ExecutorService service = Executors.newCachedThreadPool();
        final LunarServer lunarServer = LunarServer.newInstance(urls, Thread.currentThread().getContextClassLoader());
        final Future<?> future = service.submit(lunarServer);
        service.shutdown(); // 新規追加STOP

        MBeanServer mbServer = ManagementFactory.getPlatformMBeanServer();
        mbServer.registerMBean(new ServerOperationMXBean() {
            @Override
            public void stop() {
                lunarServer.stop();
                future.cancel(true);
                if (!service.isShutdown()) {
                    service.shutdown();
                }
            }
            @Override
            public void reload() {
                lunarServer.reload();
            }
        },  new ObjectName("at.orz.lunar.LunarBootstrap:name=LunarServer"));

    }

}

シングルインスタンスモデルにするか、プロトタイプモデルにするか迷ったのでclsMapとかでClass定義を保持しているわけです。今回はシングルインスタンスモデルにしたので実はclsMapは必要ありません。

package at.orz.lunar;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Properties;

public class LunarClassLoader extends URLClassLoader {

    private HashMap<String, Class<? extends LunarTask>> clsMap = new HashMap<String, Class<? extends LunarTask>>();
    private HashMap<String, LunarTask> insMap = new HashMap<String, LunarTask>();

    public LunarClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
        init();
    }

    @SuppressWarnings("unchecked")
    private void init() {

        try {

            Enumeration<URL> en = getResources("tasklist.properties");
            while (en.hasMoreElements()) {
                URL url =  en.nextElement();

                Properties prop = new Properties();
                InputStream is = url.openStream();
                prop.load(is);
                is.close();

                for (String clsName: prop.stringPropertyNames()) {
                    try {
                        Class<?> c = loadClass(clsName);
                        if (LunarTask.class.isAssignableFrom(c)) {
                            Class<? extends LunarTask> cls = (Class<? extends LunarTask>) c;
                            clsMap.put(clsName, cls);
                            System.out.printf("register class:%s%n", clsName);

                            LunarTask task = cls.newInstance();
                            task.init();
                            insMap.put(clsName, task);
                            System.out.printf("init class:%s%n", clsName);
                        }
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }

            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    void destroy() {
        for (LunarTask task : insMap.values()) {
            try {
                task.destroy();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        insMap.clear();
        clsMap.clear();
    }

    public LunarTask getTask(String className) {
        return insMap.get(className);
    }

    public Iterable<LunarTask> taskIterator() {
        return new HashMap<String, LunarTask>(insMap).values();
    }

}
package at.orz.lunar;

import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class LunarServer implements Runnable, ServerOperationMXBean {

    private LunarClassLoader loader;
    private volatile boolean running;
    private LunarServer() {

    }

    @Override
    public void run() {

        ExecutorService service = Executors.newFixedThreadPool(3);

        running = true;
        while (running) {
            for (final LunarTask task : loader.taskIterator()) {
                service.submit(new Runnable() {
                    @Override
                    public void run() {
                        task.execute();
                    }
                });
            }
            try {
                Semaphore sema = new Semaphore(0);
                sema.tryAcquire(10, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        service.shutdown();

    }

    @Override
    public void reload() {
        LunarClassLoader curLoader = loader;
        LunarClassLoader newLoader = new LunarClassLoader(curLoader.getURLs(), curLoader.getParent());
        this.loader = newLoader;
        curLoader.destroy();
    }

    @Override
    public void stop() {
        running = false;
    }

    public static LunarServer newInstance(URL[] urls, ClassLoader parent) {
        LunarServer server = new LunarServer();
        server.loader = new LunarClassLoader(urls, parent);
        return server;
    }

}
package at.orz.lunar;

public interface LunarTask {
    public void init();
    public void destroy();
    public void execute();
}
package at.orz.lunar;

public interface ServerOperationMXBean {
    public void stop();
    public void reload();
}

ここからタスクです。Eclipse上では別プロジェクトにした方が良いです。今回は2つのクラスを登録します。

package at.orz.lunar.task;

import at.orz.lunar.LunarTask;

public class SampleTask1 implements LunarTask {
    @Override
    public void destroy() {
        System.out.println("SampleTask1.destroy()");
    }
    @Override
    public void execute() {
        System.out.println("☆SampleTask1.execute()");
    }
    @Override
    public void init() {
        System.out.println("SampleTask1.init()");
    }
}
package at.orz.lunar.task;

import at.orz.lunar.LunarTask;

public class SampleTask2 implements LunarTask {
    @Override
    public void destroy() {
        System.out.println("SampleTask2.destroy()");
    }
    @Override
    public void execute() {
        System.out.println("★SampleTask2.execute()");
    }
    @Override
    public void init() {
        System.out.println("SampleTask2.init()");
    }
}

クラスパスのルートにtasklist.propertiesを作成します。

at.orz.lunar.task.SampleTask1
at.orz.lunar.task.SampleTask2

実行した後にSampleTask2の内容を変更(★を★★★★★に変更)した後にjconsoleからreloadを呼び出した結果が↓です。

register class:at.orz.lunar.task.SampleTask2
SampleTask2.init()
init class:at.orz.lunar.task.SampleTask2
register class:at.orz.lunar.task.SampleTask1
SampleTask1.init()
init class:at.orz.lunar.task.SampleTask1
★SampleTask2.execute()
☆SampleTask1.execute()
★SampleTask2.execute()
☆SampleTask1.execute()
☆SampleTask1.execute()
★SampleTask2.execute()
☆SampleTask1.execute()
★SampleTask2.execute()
register class:at.orz.lunar.task.SampleTask2
SampleTask2.init()
init class:at.orz.lunar.task.SampleTask2
register class:at.orz.lunar.task.SampleTask1
SampleTask1.init()
init class:at.orz.lunar.task.SampleTask1
SampleTask1.destroy()
SampleTask2.destroy()
☆SampleTask1.execute()
★★★★★SampleTask2.execute()
☆SampleTask1.execute()
★★★★★SampleTask2.execute()

リロード処理の所にログ仕込むの忘れたのでリロードが発端になっているかは確認できませんが、リロード処理は呼ばれています(; ・`д・´)
そしてリ新しいクラスの内容が読み込まれているのも確認できます。

※途中から書くのが面倒になったのがソースからわかっちゃうと思いますが、サンプルなので気にしない(・ε・)

ただし、このような処理はちょっとミスするとPermGenが解放されずにOutOfMemoryErrorを引き起こす原因になりやすいので気をつけましょー。(IBMのVM使えばいいじゃんとかそういう野暮な事は言わないの。)

 

Solr1.4のbackupのバグ

Solr1.4ではとあるURLにアクセスする事でバックアップ(スナップショット)を取れるようになっています。

http://wiki.apache.org/solr/SolrReplication

↑のSolrのWikiにもあるようにReplicationの機能をONにする必要がありますが、スレーブの設定をしなくても動作するのでマスタのみでもきちんと動作します。
以下のURLにアクセスするとデータディレクトリと同列に「snapshot.日時」というディレクトリを作成します。

http://master_host:port/solr/replication?command=backup

この時、日付はyyyyMMddHHmmssという書式が指定されているはずなのですが、実はHHがhhになっており、午後の18時に実行しても18にならずに06になってしまいます。

SnapShooterというクラスのDATE_FMTの指定を見ると思いっきり小文字で指定されています。 

むむー。バグだよね。これは。

まぁだいたいが早朝にバックアップをすると思うから気がつかないんだと思うけど。。

Home

Search
Feeds

Return to page top