cloverrose's blog

Python, Machine learning, Emacs, CI/CD, Webアプリなど

pywebhdfsにHAとFederationをサポートするPRがマージされた

WebHDFSについて

WebHDFShdfsコマンドではなくREST APIにhttpでアクセスできる便利なもの。

Hoop(httpfs)とwebhdfsの違い - たごもりすメモとかが図もあってわかりやすいと思う。

背景1 (hdfsコマンドへの不満)

MapReduceなどを使って解析を行って、その結果を可視化するとかを行っていると、スクリプトの中で頻繁にHDFSにアクセスすることになった。

そしてShellスクリプトが量産されるわけだが、自分はPythonスクリプトを書きたかった。

最初はsubprocessの中でhdfsコマンドを叩いていたが、レイテンシが結構あるし、lsコマンドの結果をパースしてファイル名を取得する必要があって微妙だった。

そんな話をしたらWebHDFSがあるよと教えてもらった。

背景2 (WebHDFSとの出会い)

WebHDFSに触ってみたら、レイテンシがすごく短いし、JSONが返ってくるので扱いやすい。心が踊った。

テンションが上ってHDFSのWebUIみたいなことができるシェルインタプリタを作った。 できる処理はls, cat, cd, pwdでread/get的なものだけにしていた。

その時自分でWebHDFSのREST APIのうすいラッパーを実装した。

これがわりと必要十分な機能は揃っていたためチームでもこのapi.pyが使われるようになった。

背景3 (自作ラッパーの限界)

自作ラッパーはシェルインタプリタ用に遊びで作っていたので、実業務で利用されるとカバーしているAPIが少なかった。

特に新規ファイル作成とかは、1回NameNodeにリクエストを投げて、返ってきたDataNodeのURIに対して再度ファイル内容とともにリクエストを投げるという形式で、自分で全部実装するのはめんどくさすぎた。

また認証周りもめんどくさくて、APIに渡す引数が増えていきそうだった。

そこでOSSPythonのWebHDFSラッパを探すことにした。

ライブラリ探し

PythonからHDFSを操作する - 偏った言語信者の垂れ流しに辿り着いた。

そこでは2つのライブラリが比較されていた。2013年の比較だが参考になった。

実際、PYPIを見てもWebHDFSライブラリはリリースが2014-01-20と古く、もうメンテナンスされていない気がした。

そこでpywebhdfsを使うことにした。しかしこのライブラリではHAとFederationがサポートされていなかった。

HAとFederationについて

HA(High Availability)とはHadoopのNameNodeが単一障害点だった欠点を解消するために、ActiveとStandbyという2つ以上のNameNodeを起動しておき、ActiveなNameNodeが落ちたらStandbyだったNameNodeが自動でActiveに切り替わるという仕組みで、実際の業務でHadoopを使うなら必須な機能。

Federationは複数のNameNodeがメタ情報(ディレクトリ構造とか)を分担してメモリに保持する仕組み。貧弱なメモリのNameNodeで大規模なクラスタを管理しようとすると全てのメタ情報が載り切らない。この時/data/以下はNameNode1で、/user/以下はNameNode2でそれ以外はNameNode3でという感じで分割できる仕組み。

どちらも自分たちのプロジェクトでは使っている。

現状pywebhdfsは1つのNameNodeのホスト名を渡すので、Activeが落ちた時に、APIはずっとエラーを返すようになってしまい、それを外側で検知して切り替えたりしないといけない。

HAとFederationのサポートの実装

いろいろ考えたが、pywebhdfsのIssueにもHAのサポートが欲しいという声が上がっていたので、PRを投げることにした。

しばらくの土日はいろんなパターンでHA/Federationをサポートする仕組みを実装した。

最終的には、パスにマッチする正規表現とそれに該当するNameNodeのリスト(HAならActiveとStandby)を順序付きの辞書でAPIコンストラクタに渡すことにした。

順序付きの辞書にしたのは、それ以外のパス(.*)とは最後にマッチさせたいからだ。

実装にあたって気をつけたのは、HA/Federationを利用していないユーザーには今までと同じインターフェースを保つこと。

そしてPRを投げた。

support federation and HA by cloverrose · Pull Request #22 · pywebhdfs/pywebhdfs · GitHub

もらったレビューを反映して、今朝マージされた :)

1ヶ月待っていたので感慨深かった。

f:id:cloverrose:20150718113204p:plain

余談

HDFSのシェルインタプリタは自作のapi.pyではなく、自分のパッチが当たったpywebhdfsを使って実装し直して現在も気に入って使っています。

Pythonインタプリタを作るときにはreadlineをラップしたcmdという便利なものがあります。

TABで補完とかが簡単に実装できるし、入力読み取り→実行のループも勝手にやってくれます。初めて知ったけど、今後また何か作るときに使っていきたい。

Pythonのjsonモジュールの便利機能

PythonJSONを読み書きする機会が割りとあったんですが、改めて調べたり、公式ドキュメントをちゃんと読んだら便利な機能を知ったのでメモしておきます。

jsonファイルの辞書を順番通りにloadしたい

設定ファイルとしてJSONを使っている時、普通にloadすると辞書の順番は保たれません。 でも、プログラム内部で設定を書き足してdumpするような場合、人間が書いた部分がごちゃごちゃになるとよろしくありません。

Pythonjsonモジュールでは次のようにすると順番通りに読み込めます。

import json
import collections


decoder = json.JSONDecoder(object_pairs_hook=collections.OrderedDict)
with open('conf.json') as conf_file:
    conf = decoder.decode(conf_file.read())

参考

最初に見つけた回答 Can I get JSON to load into an OrderedDict in Python? - Stack Overflow

公式ドキュメントでは以下のように書いてあります。

object_pairs_hook はオプションで渡す関数で、ペアの順序付きリストのデコード結果に対して呼ばれます。 object_pairs_hook の返り値は dict の代わりに使われます。この機能はキーと値のデコードされる順序に依存する独自のデコーダ (たとえば collections.OrderedDict() は挿入の順序を記憶します) を実装するのに使えます。 object_hook も定義されている場合は、 object_pairs_hook が優先して使用されます。

バージョン 2.7 で変更: object_pairs_hook のサポートを追加しました。

jsonをdumpするときにの行末空白(trailing whitespace)を無くしたい

出力するJSONが人が見る設定ファイルの場合、適切に改行やインデントを設定すると思います。 しかし、indentを設定すると行末空白が付いてしまい、設定ファイルをGitなどで管理するときに気になります。

次のようにseparatorsを指定してあげると行末空白を取り除くことができます。

with open('conf.json', 'w') as conf_file:
    json.dump(conf, conf_file, indent=4, separators=(',', ': '))

参考

最初に見つけた回答 Issue 16333: Trailing whitespace in json dump when using indent - Python tracker

公式ドキュメントでは以下のように書いてあります。

separators がタプル (item_separator, dict_separator) ならば、デフォルト区切り文字 (', ', ': ') の代わりに使われます。 (',', ':') が最もコンパクトな JSON の表現です。

サンプル

順番通りにloadして、新しい設定を追加して、行末空白なしで、元の順番通りに書き出す例

# -*- coding:utf-8 -*-
import json
import collections


decoder = json.JSONDecoder(object_pairs_hook=collections.OrderedDict)
with open('conf.json') as conf_file:
    conf = decoder.decode(conf_file.read())

conf['New.Key'] = 'append last'

with open('conf.json', 'w') as conf_file:
    json.dump(conf, conf_file, indent=4, separators=(',', ': '))

この2つは設定ファイル扱う上で今後は無くてはならない感じになりそうです。勉強になった!