HbaseとPigで遊んでみる。その2
http://d.hatena.ne.jp/goth_wrist_cut/20110529/1306695210
前回の続きでする。
前回までのあらすじ。
- EasyMode上等
- OpenJDKではなく、OracleJDK
- conf-pseudo便利
- .outではなく.logをつかうべし
- ディレクトリは勝手に作らせるが吉
- Webインターフェースが素敵
- 円周率はおよそ3.6。
今回はHBaseの導入と起動、そんでshellで遊ぶ。
実際にTwitterのTLをいれよう、とか思ったのだけど、思った以上に長くなったのでここまで。
次回はTwitterからデータを入れてみます。
起動
インストールは前回でまとめてやってしまったので特にやることは無し。
設定に関しても疑似分散モードなら無設定で動きます。
完全分散モードの場合には、完全分散で動かすよ!という指定やらデータの保存先(HDFSのnamenodeとパス)の指定が必要になります*1。
また、Zookeeperと呼ばれる多数決で意志決定する、要はエヴァのMagiみたいなのが必要となるのですが、疑似分散モードではHBaseのmasterサーバがZookeeperを持っていてよしなにしてくれます。
ただ、このHBase masterの持っているZookeeperは他のZookeeperと干渉するので、Zookeeperをインストールし場合には明示的に止めておく必要があります。
$ sudo /sbin/chkconfig --list hadoop-zookeeper hadoop-zookeeper 0:off 1:off 2:on 3:on 4:on 5:on 6:off $ sudo /sbin/chkconfig hadoop-zookeeper off $ sudo /sbin/chkconfig --list hadoop-zookeeper hadoop-zookeeper 0:off 1:off 2:off 3:off 4:off 5:off 6:off $ sudo /etc/init.d/hadoop-zookeeper stop JMX enabled by default Using config: /etc/zookeeper/zoo.cfg Stopping zookeeper ... STOPPED
今回は疑似分散でテストしているのでしばらく黙っていてもらいます。
Zookeeperを止める代わりにHBase側のZookeeperのポート番号を変えてもよし。
/etc/hbase/conf/hbase-site.xmlのconfigurationにプロパティを追加します(デフォルトポートは2181番)。
<configuration> <property> <name>hbase.zookeeper.property.clientPort</name> <value>2182</value> </property> </configuration>
こっちの方がお行儀がいいのかな?
まぁ、でも使いもしないZookeeperが動いてても、というのはあるので、疑似分散でテストしている時には自動起動を切っておいても、という感じ。
というか、インストールしなければよかった、という話か……。ついうっかり。
とりあえず、Zookeeperの問題が片付いたのでHBaseを起動してみましょう。
他のZookeeperが起動していないのを確認したらhbase-masterとhbase-regionserverを起動します。
$ sudo /etc/init.d/hadoop-hbase-master start starting master, logging to /var/log/hbase/hbase-hbase-master-vCentOS.out $ sudo /etc/init.d/hadoop-hbase-regionserver start starting regionserver, logging to /var/log/hbase/hbase-hbase-regionserver-vCentOS.out
例のごとく死活管理はWebインターフェースがおすすめ。
「Hadoop徹底入門(初版第1刷)」には60011と書いてますが、60010がWebインターフェースのポートです。
regionserverは下の方に並んでます*2。
Region Servers Address Start Code Load vCentOS:60030 1306776614039 requests=0, regions=5, usedHeap=28, maxHeap=997 Total: servers: 1 requests=0, regions=5
なんかデータ入ってますが、テストしてたときの残りです。気にせんといてください。
HBaseについて
HBaseについてちょっとだけ。
HBaseの説明はすんごいわかりやすいサイトがごまんとあるのでさっくりと。
HBaseはいわゆるNoSQLのデータベースで、HDFSを使ってデータを保存してる。
HDFSは基本的に大きいデータをシーケンシャルに、というものなので、データベースみたいにこまい変更がちょくちょく、というのには向いてない。
そこで、HBaseではデータベースに対する「操作を」ひたすら保存していく、という形でこまい変更を処理しているらしい。
HBaseがひとつのセルに複数ヴァージョン値をもつ、っていうのはたぶんこの辺が理由なんだと思う。ソース読んだ訳じゃないからわからんけど。
この実際にデータが入るセルへのアクセスは行(Row)と列(Column)で行う*3。
RowはSQLでいう主キーみたいな感じ。HBaseだと基本的にRowキーでしか検索ができない。
もうちょっというと、Rowキーでデータがソートされていて、完全一致ないし前方一致したものを範囲で拾ってこれる。
中に入ってるデータで検索したい、ってときにはMapReduceしてね、という感じ。
Columnは主キー以外のいわゆるカラム。カラムファミリ(ColumnFamily)と修飾子(Qualifier)をコロン(:)でくっつけたもの。
Qualifierのほうは完全に自由で、いわゆるスキーマレス。空でもok。
で、ColumnFamilyはというと、いわばカラムのグループみたいな感じ?
同じColumnFamilyに属するColumnは同じファイルに保存されるらしい。
なので、常に一緒に使うデータは同じColumnFamilyに、別々にアクセスするデータは別ColumnFamilyに、というのが基本的な運用らしい。
ColumnFamilyはテーブル作成時に指定する必要があるので、この辺はちゃんと設計せんとまずそう。
まぁ、最悪metaとdataみたいな大雑把な分け方でいいと思うのだけど。
HBaseの提供するデータ操作は基本的には3種類、putとgetとscan。
putは読んで字のごとくデータの書き込みをするんだけど、書き込みだけでなく削除もput。だから編集とか更新と呼ぶのが正解かも。
そんなわけで、かどうかは知らないけどThriftだとmutateになっている。
1つのRowに対して複数のColumnへの編集をAtomicに行える。
複数のRowに対するAtomicな操作は提供されていないらしい。ふぅむ。
getとscanがデータの読み込み。
getはRowとColumnと、あと場合によってはTimestampとで、ほしいデータにアクセスできる。
要はふつーのデータ読み込み。
scanはRowキーのprefixを使った範囲アクセス。
例えばRowキーがhogeから始まるやつを順番に見ていく、というような操作。
データがRowキーでソートされているHBaseならでは、なのかな?
以上!
という非常にシンプルな操作系。
SQLと比べるとあまりにアレで、何ができるの?という気分になるけど、要はMapReduceでかけ、ってことなのかな?
あるいはデータの入れ方を工夫してscanをうまく使うとかかなぁ。
PigとかからHBaseを触れるとしちめんどくさいMapReduceをJavaで書く、という苦行からエスケープできるんでしょうけど、日本語情報がほとんどない!というね。
TODO: 調べる。
HBase Shell
そんなわけで実際にHBaseにデータをいれてみる。
HBaseで遊ぶならなんといってもhbase shell。
お手軽簡単に、HBaseとキャッキャウフフできちゃいます。
前回も少しふれたけど、hbase shellはjRubyのirbなので、Rubyのコードやらなんやらが動いちゃいます。
とりあえずはテーブルの作成と削除から。
$ hbase shell HBase Shell; enter 'help<RETURN>' for list of supported commands. Type "exit<RETURN>" to leave the HBase Shell Version 0.90.1-cdh3u0, r, Fri Mar 25 16:10:51 PDT 2011 hbase(main):001:0> create 'test_table', 'cf1', 'cf2', {NAME => 'cf3', VERSIONS => 1} 0 row(s) in 6.8170 seconds hbase(main):002:0> list TABLE test_table 1 row(s) in 0.1100 seconds hbase(main):003:0> describe 'test_table' DESCRIPTION ENABLED {NAME => 'test_table', FAMILIES => [{NAME => 'cf1', BLOOMFILTER => 'NONE', REPLICATION_SC true OPE => '0', COMPRESSION => 'NONE', VERSIONS => '3', TTL => '2147483647', BLOCKSIZE => '65 536', IN_MEMORY => 'false', BLOCKCACHE => 'true'}, {NAME => 'cf2', BLOOMFILTER => 'NONE', REPLICATION_SCOPE => '0', COMPRESSION => 'NONE', VERSIONS => '3', TTL => '2147483647', B LOCKSIZE => '65536', IN_MEMORY => 'false', BLOCKCACHE => 'true'}, {NAME => 'cf3', BLOOMFI LTER => 'NONE', REPLICATION_SCOPE => '0', COMPRESSION => 'NONE', VERSIONS => '1', TTL => '2147483647', BLOCKSIZE => '65536', IN_MEMORY => 'false', BLOCKCACHE => 'true'}]} 1 row(s) in 0.0870 seconds hbase(main):004:0> disable 'test_table' 0 row(s) in 2.6040 seconds hbase(main):005:0> describe 'test_table' DESCRIPTION ENABLED {NAME => 'test_table', FAMILIES => [{NAME => 'cf1', BLOOMFILTER => 'NONE', REPLICATION_SC false OPE => '0', COMPRESSION => 'NONE', VERSIONS => '3', TTL => '2147483647', BLOCKSIZE => '65 536', IN_MEMORY => 'false', BLOCKCACHE => 'true'}, {NAME => 'cf2', BLOOMFILTER => 'NONE', REPLICATION_SCOPE => '0', COMPRESSION => 'NONE', VERSIONS => '3', TTL => '2147483647', B LOCKSIZE => '65536', IN_MEMORY => 'false', BLOCKCACHE => 'true'}, {NAME => 'cf3', BLOOMFI LTER => 'NONE', REPLICATION_SCOPE => '0', COMPRESSION => 'NONE', VERSIONS => '1', TTL => '2147483647', BLOCKSIZE => '65536', IN_MEMORY => 'false', BLOCKCACHE => 'true'}]} 1 row(s) in 0.0870 seconds hbase(main):006:0> drop 'test_table' 0 row(s) in 1.5820 seconds hbase(main):007:0> describe 'test_table' ERROR: Failed to find table named test_table Here is some help for this command: Describe the named table. For example: hbase> describe 't1'
テーブルの作成はcreate。
第二引数以降はColumnFamilyの指定。連想配列を使うとColumnFamilyのプロパティも指定できる。
要はRubyなので['cf1', 'cf2']とか配列渡してみたり、(0..10).map{|i| "cf#{i}"}と連番で作ってみたりも。
createでこういう事をするのはあまりないケースかもだけど、適当なサンプルデータ作ったりするのには便利。
テーブル一覧がlist、詳細情報を見るのにはdescribe。
describeの表示がびみょーに見づらくてwirbleとか使えたらなぁ、と思ったけど、よくよく考えると使えてもカラーリングされないじゃん。返値じゃないんだし。
ぐぬぬ。
テーブルの削除はdrop、なんだけど、先にdisableで無効化する必要あり。
disableしたテーブルはenableで再び有効化する事ができる。
データのput/get
$ hbase shell HBase Shell; enter 'help<RETURN>' for list of supported commands. Type "exit<RETURN>" to leave the HBase Shell Version 0.90.1-cdh3u0, r, Fri Mar 25 16:10:51 PDT 2011 hbase(main):001:0> create 'test_table', 'cf1', 'cf2', {NAME => 'cf3', VERSIONS => 1} 0 row(s) in 3.0080 seconds hbase(main):002:0> put 'test_table', 'row1', 'cf1:col1', 'Hello' 0 row(s) in 0.1080 seconds hbase(main):003:0> put 'test_table', 'row1', 'cf1:col2', 'World' 0 row(s) in 0.0680 seconds hbase(main):004:0> get 'test_table', 'row1' COLUMN CELL cf1:col1 timestamp=1306824912123, value=Hello cf1:col2 timestamp=1306824920915, value=World 2 row(s) in 0.1790 seconds hbase(main):005:0> put 'test_table', 'row1', 'cf1:col2', 'HBase!' 0 row(s) in 0.1780 seconds hbase(main):006:0> get 'test_table', 'row1' COLUMN CELL cf1:col1 timestamp=1306824912123, value=Hello cf1:col2 timestamp=1306824944683, value=HBase! 2 row(s) in 0.1490 seconds hbase(main):007:0> get 'test_table', 'row1', {COLUMN => 'cf1', VERSIONS => 2} COLUMN CELL cf1:col1 timestamp=1306824912123, value=Hello cf1:col2 timestamp=1306824944683, value=HBase! cf1:col2 timestamp=1306824920915, value=World 3 row(s) in 0.2040 seconds hbase(main):008:0> put 'test_table', 'row1', 'cf2:', 'old data' 0 row(s) in 0.4210 seconds hbase(main):009:0> put 'test_table', 'row1', 'cf3:', 'old data' 0 row(s) in 0.0510 seconds hbase(main):010:0> put 'test_table', 'row1', 'cf2:', 'new!' 0 row(s) in 0.0290 seconds hbase(main):011:0> put 'test_table', 'row1', 'cf3:', 'new!' 0 row(s) in 0.0430 seconds hbase(main):012:0> get 'test_table', 'row1', {COLUMN => ['cf2', 'cf3']} COLUMN CELL cf2: timestamp=1306825154941, value=new! cf3: timestamp=1306825157611, value=new! 2 row(s) in 0.0740 seconds hbase(main):013:0> get 'test_table', 'row1', {COLUMN => ['cf2', 'cf3'], VERSIONS => 2} COLUMN CELL cf2: timestamp=1306825154941, value=new! cf2: timestamp=1306825097674, value=old data cf3: timestamp=1306825157611, value=new! 3 row(s) in 0.1650 seconds hbase(main):014:0> delete 'test_table', 'row1', 'cf2:' 0 row(s) in 0.0650 seconds hbase(main):015:0> delete 'test_table', 'row1', 'cf3:' 0 row(s) in 0.0560 seconds hbase(main):016:0> get 'test_table', 'row1', {COLUMN => ['cf2', 'cf3']} COLUMN CELL 0 row(s) in 0.0540 seconds hbase(main):017:0> get 'test_table', 'row1', {COLUMN => ['cf2', 'cf3'], VERSIONS => 2} COLUMN CELL 0 row(s) in 0.0470 seconds
hbase shellはputとdeleteを同時にできないっぽいなぁ。
VERSIONSを設定するとそれより古い情報が見られなくなる。
けどまぁ、普通は最新のを使うだけで困らなそうだし、VERSIONSは常に1でもよいかも。
各コマンド*4の引数は結構バリエーションがあるのでhelpで叩くなり、引数無しで叩くなりすると幸せになれるかも。
あと基本的にカラムはColumnFamilyだけを指定するとそのColumnFamilyに属するカラムがまとめて取得できる、のだけど、APIによっては挙動違うかも。
少なくともThriftのmutate(put)では、ColumnFamilyまるごと消そうとしてColumnFamilyだけを指定してみたけど消えなかった記憶が……。
この辺もあとでまとめておきたいね。
最後にscan。
最後ぐらいすこし実用っぽそうな雰囲気のしそうな感じのexampleを。
$ hbase shell HBase Shell; enter 'help<RETURN>' for list of supported commands. Type "exit<RETURN>" to leave the HBase Shell Version 0.90.1-cdh3u0, r, Fri Mar 25 16:10:51 PDT 2011 hbase(main):001:0> create 'inits', {NAME => 'data', COMPRESSION => 'GZ', VERSIONS => 1}, 'meta' 0 row(s) in 2.1140 seconds hbase(main):002:0> Dir.glob("/etc/init.d/*").each{|f| base=File::basename f; stat=File::stat f; put 'inits', base, 'data:', (File::read f); put 'inits', base, 'meta:name', f; put 'inits', base, 'meta:size', (stat.size); put 'inits', base, 'meta:mode', (stat.mode.to_s(8)); put 'inits', base, 'meta:ctime', (stat.ctime.to_s); put 'inits', base, 'meta:mtime', (stat.mtime.to_s); } 0 row(s) in 0.0620 seconds 0 row(s) in 0.0220 seconds 〜〜〜略〜〜〜 0 row(s) in 0.0200 seconds 0 row(s) in 0.0230 seconds => ["/etc/init.d/krb524", "/etc/init.d/anacron", "/etc/init.d/irqbalance", "/etc/init.d/single", "/etc/init.d/hadoop-0.20-jobtracker", "/etc/init.d/iptables", "/etc/init.d/sendmail", "/etc/init.d/hadoop-zookeeper", "/etc/init.d/oddjobd", "/etc/init.d/acpid", "/etc/init.d/hidd", "/etc/init.d/hadoop-hbase-master", "/etc/init.d/hadoop-hbase-regionserver", "/etc/init.d/hadoop-0.20-tasktracker", "/etc/init.d/atd", "/etc/init.d/sshd", "/etc/init.d/netplugd", "/etc/init.d/avahi-daemon", "/etc/init.d/netconsole", "/etc/init.d/svnserve", "/etc/init.d/firstboot", "/etc/init.d/irda", "/etc/init.d/nfslock", "/etc/init.d/haldaemon", "/etc/init.d/avahi-dnsconfd", "/etc/init.d/dund", "/etc/init.d/readahead_later", "/etc/init.d/yum-updatesd", "/etc/init.d/syslog", "/etc/init.d/rdisc", "/etc/init.d/hadoop-hbase-thrift", "/etc/init.d/iscsi", "/etc/init.d/restorecond", "/etc/init.d/network", "/etc/init.d/netfs", "/etc/init.d/functions", "/etc/init.d/ypbind", "/etc/init.d/smartd", "/etc/init.d/xfs", "/etc/init.d/isdn", "/etc/init.d/killall", "/etc/init.d/readahead_early", "/etc/init.d/pcscd", "/etc/init.d/kudzu", "/etc/init.d/iscsid", "/etc/init.d/multipathd", "/etc/init.d/bluetooth", "/etc/init.d/ip6tables", "/etc/init.d/capi", "/etc/init.d/auditd", "/etc/init.d/rpcidmapd", "/etc/init.d/mdmpd", "/etc/init.d/autofs", "/etc/init.d/NetworkManager", "/etc/init.d/wpa_supplicant", "/etc/init.d/gpm", "/etc/init.d/mcstrans", "/etc/init.d/tcsd", "/etc/init.d/ntpd", "/etc/init.d/rawdevices", "/etc/init.d/cpuspeed", "/etc/init.d/crond", "/etc/init.d/pand", "/etc/init.d/hadoop-0.20-namenode", "/etc/init.d/lvm2-monitor", "/etc/init.d/microcode_ctl", "/etc/init.d/nfs", "/etc/init.d/saslauthd", "/etc/init.d/dnsmasq", "/etc/init.d/hadoop-0.20-datanode", "/etc/init.d/mdmonitor", "/etc/init.d/conman", "/etc/init.d/halt", "/etc/init.d/messagebus", "/etc/init.d/nscd", "/etc/init.d/rpcsvcgssd", "/etc/init.d/psacct", "/etc/init.d/portmap", "/etc/init.d/hadoop-0.20-secondarynamenode", "/etc/init.d/rpcgssd", "/etc/init.d/jexec"] hbase(main):003:0> scan 'inits', {COLUMN => 'meta:name'} ROW COLUMN+CELL NetworkManager column=meta:name, timestamp=1306828398781, value=/etc/init.d/NetworkManager acpid column=meta:name, timestamp=1306828391435, value=/etc/init.d/acpid anacron column=meta:name, timestamp=1306828389790, value=/etc/init.d/anacron atd column=meta:name, timestamp=1306828392440, value=/etc/init.d/atd 〜〜〜略〜〜〜 wpa_supplicant column=meta:name, timestamp=1306828398901, value=/etc/init.d/wpa_supplicant xfs column=meta:name, timestamp=1306828396047, value=/etc/init.d/xfs ypbind column=meta:name, timestamp=1306828395777, value=/etc/init.d/ypbind yum-updatesd column=meta:name, timestamp=1306828394506, value=/etc/init.d/yum-updatesd 81 row(s) in 0.8350 seconds hbase(main):004:0> scan 'inits', {STARTROW => 'hadoop-0.20', COLUMN => 'meta:', LIMIT => 5} ROW COLUMN+CELL hadoop-0.20-datanode column=meta:ctime, timestamp=1306828401092, value=Tue May 31 09:33:28 JST 2011 hadoop-0.20-datanode column=meta:mode, timestamp=1306828401072, value=100222 hadoop-0.20-datanode column=meta:mtime, timestamp=1306828401112, value=Sat Mar 26 09:08:27 JST 2011 hadoop-0.20-datanode column=meta:name, timestamp=1306828401024, value=/etc/init.d/hadoop-0.20-datanode hadoop-0.20-datanode column=meta:size, timestamp=1306828401045, value=2919 hadoop-0.20-jobtracker column=meta:ctime, timestamp=1306828390763, value=Tue May 31 09:33:28 JST 2011 〜〜〜略〜〜〜 hadoop-0.20-tasktracker column=meta:mtime, timestamp=1306828392393, value=Sat Mar 26 09:08:27 JST 2011 hadoop-0.20-tasktracker column=meta:name, timestamp=1306828392263, value=/etc/init.d/hadoop-0.20-tasktracker hadoop-0.20-tasktracker column=meta:size, timestamp=1306828392284, value=2958 5 row(s) in 0.2560 seconds hbase(main):005:0> scan 'inits', {STARTROW => 'hadoop', ENDROW => 'hadooq', COLUMN => 'meta:size'} ROW COLUMN+CELL hadoop-0.20-datanode column=meta:size, timestamp=1306828401045, value=2919 hadoop-0.20-jobtracker column=meta:size, timestamp=1306828390658, value=2945 hadoop-0.20-namenode column=meta:size, timestamp=1306828400192, value=2919 hadoop-0.20-secondarynamenode column=meta:size, timestamp=1306828402297, value=3036 hadoop-0.20-tasktracker column=meta:size, timestamp=1306828392284, value=2958 hadoop-hbase-master column=meta:size, timestamp=1306828392010, value=4248 hadoop-hbase-regionserver column=meta:size, timestamp=1306828392146, value=4272 hadoop-hbase-thrift column=meta:size, timestamp=1306828394973, value=4248 hadoop-zookeeper column=meta:size, timestamp=1306828391137, value=4622 9 row(s) in 0.1820 seconds
途中やたら横長なRubyコードがありますが、改行するとこんな感じ。
Dir.glob("/etc/init.d/*").each{|f| base=File::basename f; stat=File::stat f; put 'inits', base, 'data:', (File::read f); put 'inits', base, 'meta:name', f; put 'inits', base, 'meta:size', (stat.size); put 'inits', base, 'meta:mode', (stat.mode.to_s(8)); put 'inits', base, 'meta:ctime', (stat.ctime.to_s); put 'inits', base, 'meta:mtime', (stat.mtime.to_s); }
なんてことはなくて、単に/etc/init.d/以下のファイルの内容と情報をひたすらputしているだけ。
ファイル数が思った以上に多かったのと、あとputを各6回叩いているのでやたら出力が出ます。
うーん。本当なら一回/ファイルのputで済むんだけどなぁ。hbase shellで複数カラムいじる方法がわからん。
まぁ、hbase shellはお手軽に、ってことでシンプルに作ってあるのかも。
で、本題のscanとしては、基本的には順番に見ていくだけ。
Thriftの方にはwith prefixという、prefixに前方一致するものだけをscanするAPIがあるんだけど、なぜかhbase shellではSTAETROW/ENDROWしかない。
PREFIX => 'hadooop'と指定できればいいのに。
あとLIMITはいつもつける癖をつけといた方がいいかも。
この状態でなにも考えずscan叩くと、延々とスクロールする画面を眺めるはめに。
Ctrl-Cでhbase shellごと落としてもいいのだけど、irb上で関数とかいろいろ定義した後だとすごくやるきが萎える。
基本的にRowキーを工夫して、一番「よくやる」動作をscan一発でできるようにしておくと楽なのかも。
今回は適当にbasename使ったけど、pathにしておけば、あるディレクトリの中のファイルを全部scan、とかできそう。
hbase shellのだいたいの用法はこんな感じかなぁ。
なんども書いているのだけど要はirbなので、ちょっとした処理やら簡単なバッチ処理なら書けます。