yunomuのブログ

趣味のこと

ディスク拡張したのにディスク容量が増えないんだけど(ファイルシステムの話)

 例えばAmazon EC2のEBSのように、ディスクが仮想化されるようになり、そうするとディスクの容量を拡張するというよくわかんない事がお手軽にできるようになり、「ディスク容量増やしたんだけどOSから見ると増えてない。なんで」みたいな事が起こり、「ディスク容量を増やしてもファイルシステムの容量は増えないんだよ」というのを説明するのも面倒くさい。
 良い時代になりました。

 まだ二次記憶装置はハードディスクが多いだろうし、単に私が知ってるというのと、私の習慣でディスクって言葉を使います。が、たぶんCDもDVDもSSDもだいたい同じことです。

 ディスクにはメモリと同じようにデータ格納位置にアドレスが振ってあって、メモリは1バイトごとにアドレスが振られていますが、ディスクの場合は512バイトごとに振らてています。なぜ512バイトなのかは知りません。この512バイトをセクタとかディスクブロックと呼びます。

 ディスクは大抵アクセスは遅いけど大容量で不揮発なので、消えると困るデータはだいたいディスクに保存する事になるんですが、どうやって保存するのか。
 どうやってもなにも単にディスク上のアドレスを指定して書き込めばいいんです。

 例えばディスクに書き込むdk_write(addr, data)みたいな関数があったとしたら

    dk_write(0, "Yahho-");

みたいにやればディスクの0番地にデータを書き込んで、めでたくあなたのコンピュータは起動しなくなります。まあ起動しなくなるかどうかは構成しだいではありますし、ここでは全然関係ないんですけど、単に0番地にはだいたいOSの起動に必要な情報が書かれている事が多いので。ブートセクタとかMBRってやつ。
 dk_writeの中では、out命令とかでハードディスクに対してアドレスとデータが格納されたメモリ上の位置を送信しているだけです。暇だったらATAデバイスドライバのコードとか見てみるとわかるかもしれませんが、DMAとか書き込みスケジューリングとか色々あってわかりづらいかもしれません。私は面倒なのでATAの仕様を調べました。

 それで、この関数があれば一応データは保存できるようになりますが、0番地にはこのデータ、1番地にはこのデータ、みたいに覚えておくのは大変だし、512バイトを超えるデータはどうするんだという話になります。まあ、512バイトを超える場合は連続した領域に書き込めばいいんですが、それにしてもじゃあどこからどこまでが一つのデータなのかを覚えないといけなくなるし、外部断片化が起きるようになります。

 なんにしてもディスク上のデータを管理するなんらかの機構が必要です。
 そのなんらかの機構というやつがファイルシステムです。

 つまり、ファイルシステムというのは、ディスク上のデータの『どこからどこまでがどのデータか』という情報を管理しておく機構で、そのデータの管理単位のことをファイルといいます。
 そして、ここまででOSの話がほとんど出てきていないことからわかるように、ファイルシステムというものはディスク上のデータ管理の話であって、本来OSとはあまり関係ありません。厳密にはというか、狭義では。
 そんなわけで、LinuxでもNTFS用のドライバがあればWindowsのファイルを読むことはできますし、逆にWindowsでもext4とかのドライバがあればLinuxのファイルを読むことができます。

 ファイルシステムがやっている管理というのは、本でいうところの目次を作っているようなものです。本にはページ1つ1つに番号がついていて、目次には章節のタイトルとページ番号の対応が書いてある。基本的にはそれと同じです。この目次に書いてあるタイトルが、ファイルシステムではファイル名になります。

 具体的に、UFS/FFSやext2のようなやつの話をします。以降はFFSの事だと思ってください。FAT系やNTFSは全然違うみたいですし。
 で、FFSでは、ファイルシステムではディスクをデータ用の領域と管理用の領域とに分けています。

 データ用の領域には、データが格納されています。そのまんまですが、データ、つまりファイルの中身です。中身は中身だ。それ以上の情報は無い。
 あ、一応『ここでデータは終わりです』みたいな情報は入っています。
 ファイルシステム上のデータは、ディスク上の複数のセクタをまとめたブロックで管理されています。なぜまとめるかというと、ディスクは物理的な特性上、連続したセクタを読むのは速いけども飛び飛びの領域を読むのは非常に遅いので、できるだけデータはまとめておきたい。512バイトでは小さすぎる。それで4KBなり8KBなりのサイズでまとめて管理することで、読み書きを高速化しようとしています。
 ただあまり大きいと内部断片化か起きるので、適当に小さい必要もあって、その速さと空間効率のバランスを取った結果が8KBくらいだったらしいです。
 ちなみに2004年頃に発表された資料によると、初期のGoogle File Systemのブロックサイズは4MBだったらしいです。当時はデカいと思ったけど、今はそうでもないような気もするけども、アクセス速度を考えるとやっぱりデカい気もする。どうせストライピングしてるだろうから問題ないのかなぁ。おそらく今も変わらないか、小さくなるか、逆にめちゃくちゃデカくなってるか、いやよくわかんない。
 4KBやその倍数や4MBなのはメモリ管理の都合じゃないかな。メモリ管理的に、4KBより小さいのや4MBより大きいのは、現状ではあまり意味が無い気がします。

 管理用の領域には、ファイルシステムの大きさや種類などの基本的な情報や、inodeが格納されています。
 inodeというのは、目次の1行みたいなものです。ファイルのサイズ、中身のデータの格納場所(ブロック番号のリスト)、ファイルの種類(通常ファイル、ディレクトリ、シンボリックリンクなど)、パーミッションなどが記録されたテーブルです。基本的に1エントリが1ファイルを表しています。ただここにはファイル名は入っていません。
 inodeのエントリは固定長なので、番号でアクセスすることができます。この時の番号をinode番号と呼びます。inode番号は"ls -i"などのコマンドで見ることができます。
 ちなみにinodeはIndex-Nodeの略だという人もいますが、実のところは何の略だかよくわかってないらしいです。どうでもいいですけども。

 で、ディレクトリというのがあります。
 ディレクトリというのは、ファイルの一種です。
 ディレクトリ・ファイルは、簡単に言うとファイル名とinode番号が格納されたテーブルです。ファイル名はディレクトリに保存してあります。
 つまり、"/bin/ls"というファイルを読もうとした時には、以下の様な手順を踏みます。
(1) "/"のディレクトリを指すinodeを読む("/"のinode番号だけはファイルシステムの管理領域に書いてある)
(2) (1)で読んだinodeから"/"ディレクトリの内容が格納されたブロックを特定し、読む
(3) (2)の内容から、"bin"のエントリを見て、"bin"のinode番号を特定する
(4) "bin"のinodeを読む
(5) "bin"の内容が格納されたブロックを読む
(6) (5)から"ls"のエントリを見て、"ls"のinode番号を特定する
(7) "ls"のinodeを読む
(8) "ls"の内容が格納されたブロックを読む(終わりだよ〜)

 まあ結構面倒くさいわけです。DNSとだいたい同じです。
 ただ、これでファイル名があればファイルの中身にたどり着くことができるようになりました。
 おおむね、ファイルシステムはこんな感じの仕組みになっています。

 このような仕組みになっているので、以下の様な現象が起きます。

 lnコマンドで作れるハードリンクというものがあります。これはファイルの実体は同じものなんだけど、パスや名前が違うファイルを作ることができる機能です。これは違うファイル名に対応するinode番号を同じものにすることで実現することができます。
 ハードリンクはどっちがリンク先リンク元という区別はなくなり、どちらもまったく同じ立場のファイルになります。
 ただし、inode番号というのは定義上ファイルシステムに固有のものなので、違うファイルシステム上のファイルにハードリンクを張ることはできません。

 mvコマンドによるファイルの移動は、同じファイルシステム内であればinode番号は変わらずに、移動元と移動先のディレクトリの情報を書き換えるだけです。ディスク上のデータの格納位置も変わりません。

 ファイルの削除というのは、ディレクトリ内のファイル名とinode番号のエントリを削除したり、inodeからブロック群への参照を削除したりする処理です。『ファイルを削除してもデータは残る』と言われる所以はこれです。それと、ファイル削除のことをunlinkと言ったりするのもこれが理由です。というか、ファイルシステムのインタフェース的にも削除じゃなくてunlinkです。複数のハードリンクがあると消えませんし。

 ところでパーティション(スライス)というのはディスクを複数に分割する機能で、それぞれに異なるファイルシステムを構築することができます。
 なのでパーティションが違うと違うファイルシステムとみなされて、ハードリンクを張ることはできません。
 また、mvコマンドでディスクやパーティションをまたいでファイルを移動させようとすると、移動ではなくてコピーになります。いや最近はコピーした後に移動元を削除してくれますけども。

 ちなみにシンボリックリンクというのはリンク先のファイル名が書かれたファイルなので、リンク先が他のパーティションだろうが他のホストだろうが一時的にしか存在してなかろうがファイル名でアクセスできる限りなんでもアクセスできますが、ファイル名が変わったりするとアクセスできなくなります。

 で、えー、なんだ。話を戻すと、
 ファイルシステムは管理領域とデータ領域に分かれていて、管理領域の中のinodeは固定長で、番号でアクセスできるようになっている。データ領域も8KBだかのブロックに分かれていて、これも番号が振られている。
 この構造自体がディスクに書き込まれている情報なので、一度作ってしまうと、例えばデータ領域に一切変更を加えずに管理領域の容量を増やしたり減らしたりするのは難しい。管理領域のサイズやデータ領域の構造に少しでも変更を加えると全データが使えなくなる可能性がありますし、普通は管理に必要な領域以外は全部データ領域にするので、変更の余地が無いわけです。
 この、ファイルシステムの構造をディスクに書き込む処理の事を『フォーマット』といいます。ファイルシステムの構造を作るのでフォーマットといいます。フォーマットでファイルシステムの内容が全部消えるのはこのせいです。

 そして、inodeは中盤でちょっと触れたように、1エントリが1ファイルに対応しています。そして、ファイルシステム作成時に管理領域の大きさも決めてしまわないといけないので、inodeのエントリ数もファイルシステム作成時に決めてしまわないといけなくなります。
 つまり、ファイルシステム作成時に、そのファイルシステムで管理できるファイル(ディレクトリ等含む)の最大数を決める必要があります。inode数を多くするとファイル数を増やせますが、管理領域が増えるのでデータ領域が減ります。小さいファイルを多数作るならそれもアリでしょう。

 また、データ領域のブロックサイズも作成時に決める必要があります。
 これもまた、大きいと読み書きは速くなりやすくなりますが、小さいファイルが多いと内部断片化しやすくなります。小さいと効率は良くなりますが、大きいファイルを扱う時に外部断片化が起きやすくなって、アクセスは遅くなりやすくなります。デフラグすりゃいいんですが、大変ですし。
 デフラグというのは、この場合は飛び飛びになったデータを並び替えてできるだけ一箇所にまとめることです。一箇所にまとめた方がディスク上の効率が良い。SSDだと関係ないんですけど。

 そんなわけで、実はファイルシステムを作る時というのは結構色々な事を考えないといけないのです。どれくらいの大きさのファイルをどれくらいの数作るか。
 最近はディスクが安いからかなんなのか、そこら辺はあまり神経質に考える必要がなくなっています。が、mkfsコマンドのオプションを見るとまあ一応色々細かく指定することができるようにはなっています。
 昔はOSをインストールするといったらこんな風にまず用途を考えてファイルシステムのパラメータを色々と調整するところから始めないといけなかったそうです。時代は変わって、特に何も考えなくてもOSをインストールできるようになりました。素敵ですね。

 そう、で、だから、ディスク容量を増やしてもinodeの数も増えないしデータ領域のブロック数も増えないからファイルシステム上の容量は増えないわけです。
 ただ、これらの管理情報を後から拡張する方法がファイルシステムに用意されていれば容量を増やすことはできますし、単に新しく作ったファイルシステムに情報をコピーしても増やすことはできます。どちらが簡単かはそれこそファイルシステムによりますね。
 減らすのが難しいのもこういう構造だからです。

 それと途中で「狭義のファイルシステム」みたいな事を書いたと思いますが、じゃあ広義ってなんだというと、ディスク上の構造ではなく、インタフェースとしてのファイルシステムという概念があります。
 この辺から、OSとかユーザ寄りの話になってきて、これはこれでかなり面白いんですが、別の話なので別な時に。
 キーワードは仮想ファイルシステムとか、まあ単にファイルシステムでもいいです。あとはreadカーネルコールの処理を追いかけてみるとか。

パッケージ管理システムのこと

 自分で作ったプログラムを実行することは、ほとんどの場合で簡単にできます。だいたい動作確認しながら作るので当たり前です。
 でも自分で作ったプログラムを人に渡して使ってもらおうとすると、これが結構面倒くさい。
 ここでプログラムというのは、実行ファイルというかOSのロードモジュールの事を指すとします。

 まず、渡した相手のコンピュータのCPUが違うと動かない。PowerPCやARM用のプログラムはIntelアーキテクチャのプロセッサでは動かない。
 動的リンクライブラリを使っている場合は、動的リンクライブラリのパスが違うと動かない。
 そもそも依存しているライブラリがインストールされていないと動かない。
 設定ファイルを使う場合はパスやファイル名が違うと動かない。
 プログラムファイルの形式が、OSのローダが対応しているものでないと動かない。ELFなのかMach-OなのかPortable Executableなのか。
 OSの機能を呼び出すなら、呼び出し方が合っていないといけないし、呼び出す機能が存在しないといけない。
 デバイスを使っている場合は、デバイスの指定方法やアクセス方法が違うと動かない。まあこの辺はだいたいOSに任せるけども。
 などなど、結構面倒くさい。

 実のところこれらの面倒くさいあれこれは、プログラムをソースコードのまま渡すことである程度解決します。
 通常、コンパイラは自分の環境で動くプログラムを出力するので、CPUが違って動かないとか、プログラムの形式が違って動かないとかいう事は起きません。
 動的リンクライブラリにしても、パスはコンパイル時(正確にはリンク時)にプログラムに埋め込んでくれるので、インストールしてさえあればおおむね大丈夫です。
 自分の環境で動かないという事があれば、なんだったらソースコードを直してもいい。ヘッダファイルに環境依存情報(ファイルパスとかメモリ量とか)が書いてあって、そこをちょっと直すと動くという事は、よくあるというかあったというかあったらしいというか想像に難くない。

 そういう環境依存でのコンパイル段階でのトラブルを避けるために、ヘッダファイルやマクロを使って色んな環境でコンパイルできるように工夫します。例えば、

#if VERSION <= 5
#include "env5.h"
#endif

などとやると、VERSIONマクロが5以下の時だけenv5.hというヘッダファイルが読み込まれます。
 こういう風に、OSが違ったら、バージョンが違ったら、ライブラリがインストールされてなかったら、int型のバイト数が違ったら……と、様々な状況を想定してコードを書きます。
 場合によっては、環境に依存したヘッダファイルを生成するためのプログラムを作って、コンパイル前に実行したりします。OSやコンパイラコンパイルではたぶん必ずやっています。

 そうなってくると、コンパイル時に色んな事を考えないといけなくなります。どういう手順でコンパイルすればいいのか、マクロ変数の値をなんにしたらいいか、ヘッダファイルはどれを使えばいいのか、そもそもどのファイルをコンパイルしてリンクすればいいのか。
 いろいろあって面倒なので、たいていそういう情報はMakefileに書いてあります。というかこれくらい色々あると開発中でも面倒なので、そういうコンパイル手順のアレコレはだいたいMakefileに書いてあります。なので、

% gcc -o program src1.c src2.c src3.c -Iinclude -L/usr/local/lib -DARCH=I386

みたいにやっていたのが、

% make

とかで一発で実行ファイルができあがる。便利!

 ところが、まだまだそううまくいかない。
 Makefileには、プログラムのコンパイルの手順が書いてあるんですが、中身は単にコマンドの羅列なので、コマンドが無かったり名前が違ったりすると動きません。ファイルを移動するmvコマンドがmoveだったり、whichが無かったり、ライブラリが変な場所にインストールされていたりするとコンパイルできなかったりします。
 さらに、仮にプログラムがコンパイルできたとしても、動的リンクライブラリが無かったりパスを間違えていたりすると実行できません。

 これを解決するのがautoconfだったりconfigureスクリプトです。
 よくプログラムをソースからインストールする時に

% ./configure
% make
% make install

みたいな事をすると思いますが、この"./configure"というやつ。これを実行すると、コンパイルに必要なライブラリやコマンドやヘッダファイルがインストールされているかをチェックして、環境に適したMakefileを生成してくれます。
 configureが無い場合はまたそれを生成するツールなんかもあったりします。ここらへんは、だいたいREADMEとかMakefileの頭のコメントとか適当に実行した時のエラーメッセージに書いてあるし、慣れればファイル構成を見ただけでどうすればいいかわかるようになります。

 これでもうソースコードで公開されているソフトウェアの大半はコンパイルして使う事ができるようになる気がします。が、そもそもそのソースコードはどこで配布されてるのか。まあ今だったらググれば大抵公式サイトが出てくるのでそこから取ってくればよろしい。でも公式サイトがわからなかったり、検索エンジンが無い時代だったりすると、探すのが大変だったり、人に教えるのも大変だったり。
 バージョンアップされた時も、更新した情報をどこからか入手して、ソースコードを入手して、コンパイルする必要があって、結構面倒です。

 そこで、公開されている便利なプログラムの一覧を管理するシステムができます。とりあえず私が知ってるのはFreeBSDportsなのでportsの話をします。
 portsは、プログラムのリストを持っていて、例えばemacsなら/usr/ports/editor/emacs/とか、curlなら/usr/ports/net/curl/という風に、カテゴリごとに分けたディレクトリに情報が保存されています。いや正確なパスなどは忘れましたが。これで、例えば

% cd /usr/ports/editor/emacs
% make && make install

みたいにやるとめでたくemacsがインストールされます。

 例えばこの場合の/usr/ports/editor/emacsにあるMakefileが何をやっているかというと、emacsソースコードを配布しているサイトからソースコードと、場合によっては修正パッチが入ったファイルをダウンロードしてきて、解凍してパッチを当ててコンパイルしている。ソースコードがどこで配布されているかとか、パッチはどれかとか、そういうのは全部このMakefileやそれが呼び出すシェルスクリプトなんかに書いてあって、自動的に処理してくれる。といってもまあそういうのもパッケージの管理者が書いていてくれているんですが。

 で、バージョンアップの時はどうやっているのかというと、まあバージョンアップがあった事を知るのはやっぱりこまめな情報収集が必要だったりするんですけども、このportsのディレクトリ構成自体がCVSとかを使ってどこかのマスターデータと同期するようになっていて、誰かがソフトウェアを更新するとパッケージ管理者がportsのマスターデータを修正して、利用者はデータ同期してその情報を持ってきて、あとは最初と同じように"make && make install"で更新するというか上書きする。
 非常にシンプルで想像しやすい。

 ただ、portsは基本的にはソースコードからインストールする構造になっているので、インストールに結構時間がかかる。
 もちろん最初の方で書いたように、ソースコードからコンパイルした方が安全ではあるものの、例えば同じOSを使っていてハードウェアもOSで吸収できる程度の違いしか無いような環境だとか、実行ファイルやOSに互換性がある場合なんかは、あるコンピュータ上でコンパイルした実行ファイルがそのまま他のコンピュータにコピーしても動いたりします。
 というかWindowsなんかはそうだし、最近のOSはだいたいそうですね。
 そうなると、わざわざソースコードをダウンロードしてコンパイルするよりも実行ファイルをダウンロードした方が圧倒的に速いのでそうしようとなる。コンパイルは、どこかの似たような環境の誰かがやってくれていればいい。
 ただし、依存する他のプログラムやライブラリなんかが存在しないとやっぱり動かないので、その辺はきちんと把握しておく必要がある。
 ということで、バイナリパッケージというのが生まれた、のだと思います。

 例えばRed Hat LinuxRPMなんかは、パッケージをrpm形式のファイルで管理していて、rpmファイルの中には実行ファイルやライブラリや設定ファイルが入っている。それと、それらのファイルをどこに置けばいいのかとか、あとは依存する外部ライブラリの名前なんかの情報も書かれています。
 rpmファイルをインストールすると、中の情報にしたがって適切にプログラムやファイルを配置してくれて、RPMが持っているデータベースに情報が登録されます。これで現在どんなパッケージがインストールされているかを管理することができるようになっていて、例えば依存するパッケージがインストールされていなかった場合にはエラーを出してインストールが失敗します。
 RPMの場合はこんな感じで、自分でRPMファイルを探してきて、依存するパッケージも自分で探してインストールする必要がありますが、パッケージ管理の基本としてはそんな感じです。

 もちろんそれだと面倒くさいので、Yumみたいなのが登場します。
 Yumは、パッケージ名を指定するとRPMファイルをダウンロードするだけでなく、依存するパッケージがある場合はそれらもインストールしてくれるとても便利なものです。もちろんyumも中央で管理されているリポジトリがあって、それが更新されることでパッケージのバージョンアップにも追随できます。これまでの問題を概ね全部解決できるような素敵なシステムです。
 Debian使ってたからdpkg/APTで説明すればよかった……。まあAPTも同じような感じです。
 YumやAPTのみならず、RubyのgemとかJavaMavenとかHaskellのcabalとかもおおむね同じ仕組になっています。いやcabalはソースからビルドだからportsに近いし、gemもコンパイル無いし、MavenはバイナリだけどJavaだし、仕組みは似てるけど違うか。

 そしてこれより先の事は知らん。

 というのが、私が認識しているパッケージ管理システムの話です。実際のところは、APTあたりからこの世界に入ってきたのでよくわかりません。
 いや、そういえば数ヶ月前にこの辺の事を教えてと言われていたので。
 だからどうってことは無いんですけども、歴史というか、流れをある程度知っていると次の新しいものを理解しやすかったりするし、なんだったら作れる可能性もできるし、面白いんじゃないですかね。

 で、そもそも質問ってなんでしたっけ。

死んでもTwitterで遊ぼう

 先生が「死ぬまでTwitterで遊ぼう」みたいな事を言ったら、学生に「いや別に死んでもTwitterできますよプログラマなんだから」みたいな事を言われた、という話を去年京都で飲んだ時に聞いた。
 そういえば私は昔、人間を作りたくて色々勉強してた事を思い出した。いや今も作りたいんですが。

 で、とりあえずそれっぽく見えるものだけでも作ろうと思って、作ったのがこれ。botです。

 仕組みは単に、元ネタの文章を形態素解析して、前の語に続く語を出現頻度に応じてランダムに選んでいるだけ。文はmecabで解析して、作った辞書はredisに突っ込んでる。

 ついでにサーバ引っ越しとかなんとかもいっしょにやってたので20時間くらいかかったというか、久しぶりに気がついたら朝になっているというやつをやりました。
 なぜかテンションが上がりすぎて、1週間くらいかけて遊ぼうと思ってたのに1日で終わってしまった。まあまだ改良はいくらでもできるんですけど。
 なんかこういう、気がついたら朝になってたみたいなのは5〜6年ぶりな気がしますけども、楽しいは楽しいんですが食事も睡眠も忘れるというのはあまり健康には良くないんじゃないかという気もしますね。楽しかったけど。

 このbotの元データとしては私のTwitterのpostを使っていて、1万ツイート1MBくらいで、辞書の構築には1分半くらいかかる。
 さすがに元データが少ないので、元の発言とほとんど変わらないものが出てくる事がそれなりにあって微妙なんですが、そこら辺はなんか突然変異みたいな仕組みでも入れてやってみようかなぁと思っているところです。
 でも試しに私が書いた10MBくらいのテキストを食わせてみたところ、かなり意味わかんない言葉を生成できるようになったので、辞書のデータ量を増やした上で言葉を覚えさせる方向に改良していった方がいいような気がします。
 ただ10MBのテキストからの辞書構築には3時間かかったのでちょっと面倒ですね。いや構築途中でもしゃべれるのでいいんですが。アルゴリズム見直すのも面倒くさいし。

 そのうち記憶とか知覚とかも実装したいというか、2歳児くらいにはならないもんかなと思っているんですが、どうしたものだろうか。外界からの刺激を受け取るなり与えるなりさせないといけないんだけど、どう処理しようかなぁ。
 などと考えているうちにGWの残り2日が終わりそうです。

 @yunomu11がbotに延々話しかけるだけのアカウントになっていてもそっとしておいてください。

Android SDKのSQLiteDatabase#execSQLのbindArgsの型

AndroidSDKのSQLiteDatabase#execSQLのマニュアル
[http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#execSQL(java.lang.String, java.lang.Object[])]
よくあるsqlとbindArgs(Object配列)を渡す構造なんですが、このbindArgsのマニュアルを見ると

Parameters

sql the SQL statement to be executed. Multiple statements separated by semicolons are not supported.
bindArgs only byte[], String, Long and Double are supported in bindArgs.

byte[], String, Long and Doubleのどれかしかサポートしてないよと書いてあります。

すると「え、でもInteger入れたいんだけどどうなのよ」ってなるわけじゃないですか。
で、中身を見てみると

SQLiteDatabase.java

    public void execSQL(String sql, Object[] bindArgs) throws SQLException {
        if (bindArgs == null) {
            throw new IllegalArgumentException("Empty bindArgs");
        }
        executeSql(sql, bindArgs);
    }

ここはnullを弾いてるだけ。

SQLiteDatabase.java

    private int executeSql(String sql, Object[] bindArgs) throws SQLException {
        acquireReference();
        try {
            if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) {
                boolean disableWal = false;
                synchronized (mLock) {
                    if (!mHasAttachedDbsLocked) {
                        mHasAttachedDbsLocked = true;
                        disableWal = true;
                    }
                }
                if (disableWal) {
                    disableWriteAheadLogging();
                }
            }

            SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs);
            try {
                return statement.executeUpdateDelete();
            } finally {
                statement.close();
            }
        } finally {
            releaseReference();
        }
    }

ここはSQLiteStatementのコンストラクタに渡して、その後にSQLiteStatement#executeUpdateDeleteを呼んでる。

SQLiteStatement.java

    SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) {
        super(db, sql, bindArgs, null);
    }

コンストラクタでは親クラスのコンストラクタに渡しているだけ。おそらく単に親のフィールドに格納していて、getBindArgs()で取ってきたりしているんだろう。というかしてた。ちょっと気を使った書き方になってたのでライブラリ作る人は見てみてもいいかも。

SQLiteStatement.java

    public int executeUpdateDelete() {
        acquireReference();
        try {
            return getSession().executeForChangedRowCount(
                    getSql(), getBindArgs(), getConnectionFlags(), null);
        } catch (SQLiteDatabaseCorruptException ex) {
            onCorruption();
            throw ex;
        } finally {
            releaseReference();
        }
    }

executeUpdateDeleteメソッドでは、getBindArgsで親クラスから取ってきた値をSQLiteSession#executeForChangedRowCountに渡している。

SQLiteSession.java

    public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags,
            CancellationSignal cancellationSignal) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
            return 0;
        }

        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
        try {
            return mConnection.executeForChangedRowCount(sql, bindArgs,
                    cancellationSignal); // might throw
        } finally {
            releaseConnection(); // might throw
        }
    }

ここでは、2箇所でbindArgsが使われているけれど、1つ目のexecuteSpecialの中では使われていなかったので、2つ目のSQLiteConnection#executeForChangedRowCountに渡されている方が本命。

SQLiteConnection.java

    public int executeForChangedRowCount(String sql, Object[] bindArgs,
            CancellationSignal cancellationSignal) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        int changedRows = 0;
        final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount",
                sql, bindArgs);
        try {
            final PreparedStatement statement = acquirePreparedStatement(sql);
            try {
                throwIfStatementForbidden(statement);
                bindArguments(statement, bindArgs);
                applyBlockGuardPolicy(statement);
                attachCancellationSignal(cancellationSignal);
                try {
                    changedRows = nativeExecuteForChangedRowCount(
                            mConnectionPtr, statement.mStatementPtr);
                    return changedRows;
                } finally {
                    detachCancellationSignal(cancellationSignal);
                }
            } finally {
                releasePreparedStatement(statement);
            }
        } catch (RuntimeException ex) {
            mRecentOperations.failOperation(cookie, ex);
            throw ex;
        } finally {
            if (mRecentOperations.endOperationDeferLog(cookie)) {
                mRecentOperations.logOperation(cookie, "changedRows=" + changedRows);
            }
        }
    }

真ん中あたりのbindArgumentsで使われている。これ以降には出てこないので、たぶんここでstatementの中に入っちゃってるんだろう。nativeExecuteForChangedRowCountまで見れば処理の本体が見られるだろうけど、今回の興味はここではない。
ところでこのtry節のネストっぷりは、なるほどなぁと思った。次からこうしよう。

SQLiteConnection.java

    private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
        final int count = bindArgs != null ? bindArgs.length : 0;
        if (count != statement.mNumParameters) {
            throw new SQLiteBindOrColumnIndexOutOfRangeException(
                    "Expected " + statement.mNumParameters + " bind arguments but "
                    + bindArgs.length + " were provided.");
        }
        if (count == 0) {
            return;
        }

        final int statementPtr = statement.mStatementPtr;
        for (int i = 0; i < count; i++) {
            final Object arg = bindArgs[i];
            switch (DatabaseUtils.getTypeOfObject(arg)) {
                case Cursor.FIELD_TYPE_NULL:
                    nativeBindNull(mConnectionPtr, statementPtr, i + 1);
                    break;
                case Cursor.FIELD_TYPE_INTEGER:
                    nativeBindLong(mConnectionPtr, statementPtr, i + 1,
                            ((Number)arg).longValue());
                    break;
                case Cursor.FIELD_TYPE_FLOAT:
                    nativeBindDouble(mConnectionPtr, statementPtr, i + 1,
                            ((Number)arg).doubleValue());
                    break;
                case Cursor.FIELD_TYPE_BLOB:
                    nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg);
                    break;
                case Cursor.FIELD_TYPE_STRING:
                default:
                    if (arg instanceof Boolean) {
                        // Provide compatibility with legacy applications which may pass
                        // Boolean values in bind args.
                        nativeBindLong(mConnectionPtr, statementPtr, i + 1,
                                ((Boolean)arg).booleanValue() ? 1 : 0);
                    } else {
                        nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
                    }
                    break;
            }
        }
    }

ここではfor文の中でなんぞ判定して、数値ならNumberにキャストしてからlongValueなりdoubleValueを呼び出してる。これならIntegerでもいけるっぽい。
ところでこのDatabaseUtils.getTypeOfObjectって何だ。

DatabaseUtils.java

    public static int getTypeOfObject(Object obj) {
        if (obj == null) {
            return Cursor.FIELD_TYPE_NULL;
        } else if (obj instanceof byte[]) {
            return Cursor.FIELD_TYPE_BLOB;
        } else if (obj instanceof Float || obj instanceof Double) {
            return Cursor.FIELD_TYPE_FLOAT;
        } else if (obj instanceof Long || obj instanceof Integer
                || obj instanceof Short || obj instanceof Byte) {
            return Cursor.FIELD_TYPE_INTEGER;
        } else {
            return Cursor.FIELD_TYPE_STRING;
        }
    }

このように、実際にはここでinstanceofを使って型を判別しております。見たところIntegerでもShortでもFloatでも良さそうですね。まあFloatをintValueで変換とかはしてくれなさそうですが、おおむね問題は起きないようにしてあるっぽいです。

で、マッチしなかったらStringとみなされて、さっきのbindArgumentsに戻ってみると、そこでさらにBooleanかどうかの判定が行われて、trueなら1、falseなら0、BooleanじゃなければObject#toStringが呼ばれる、と。
これ、このswitch文は必要なのかな。DatabaseUtils.getTypeOfObjectは消して、このswitch文と同化してしまえばいい気がするけどどうなんだろう。getTypeOfObjectがどこかの使い回しだったりするのかな。まあするんだろうなUtilsだし。

最後にtoStringしてるってことは、enumにtoStringを実装しておいてぶっ込むとかもできるし、細かく楽ができそうですね。

SSHがつながらない

前回と同じネットワーク構成で。
f:id:yunomu:20140311120402p:plain
AからVPN経由でCへのSSH接続を試みる。

% ssh 192.168.2.11
ssh_exchange_identification: read: Connection reset by peer

つながらない。

Aからインターネット経由でC(eth0)へのSSH接続は普通にできる。

% ssh server_public_ip
Last login: Fri Mar 14 14:36:19 2014 from 192.168.1.2
[yunomu@host ~] $

だいたいこういう感じ。

/var/log/secureで関係ありそうなのは以下の1行だけ。

Mar 14 13:41:41 host sshd[30405]: Did not receive identification string from 192.168.1.2

他の/var/log/messagesとかは何も出てなかった。

TCPWrapperやxinetdは使っていなくて、生sshdで動いている。
なのでhosts.allowやhosts.denyは空で問題ないはず。

死んだsshdのプロセスがウヨウヨいたりして新規接続の確立ができてないのかなと思ったけど特にそういう気配も無し。

というかそもそも/var/log/secureの通り一応sshdが動くところまでは行ってるわけで。sshdが動いた上で、クライアントからのデータが何も来ないからしばらくしてコネクションが切られているっぽい。

さらに面倒くさいことに、2日前まではインターネット経由でもVPN経由でもつながっていたし、この記事を書き始めてから試したらつながった。
つながったじゃないか! なんなんだこれ!

というわけで、またよくわからないうちに事件が解決してしまった。いや解決してないんだけど。

ネットワークがつながらない

基本がわかっていないとなにもかもわからなくなる。

出来事

外で借りてるサーバ(C)を使ってこんな感じのネットワークを作った。
f:id:yunomu:20140311120402p:plain

  • AはWindows 7
  • Aの隣に何台かノート(D)やら携帯(E)やらがある
  • BはUT-VPNクライアントが動く程度のLinux(CentOS 5系だったと思う)
  • CはVPSLinux(これもCentOS 5系のはず)
  • BとCはUT-VPNで接続していて、仮想HUBはCにある
  • ルータはなんか数千円の安いやつ
  • 白い四角はルーティングテーブル

この状態で、CにはDBやらWebサーバやらいろいろ置いてあって、AからVPN経由で触って遊べるようにしたつもり。

で、しばらくなんの問題もなく遊んでいたんですが、なんかAのネットワークとCの通信がうまくいったりいかなかったりする。
症状はこんな感じ。

  • AとEからはCのサービス(HTTP)が見られる
  • DからはCのサービス(HTTP)が見られない
    • Bのeth0ではHTTPのrequest/responseパケットをキャプチャできる
  • A, D, EからC(192.168.2.11)にpingを飛ばすと全て応答する
  • CからAやDにpingを飛ばすとどちらも応答しない
    • Bのeth0ではicmpパケットのecho/replyをキャプチャできる

何がうまくいかないのかさっぱりわからん。なんでDのTCPだけ通信できないんだ。

よく見てみると、DからCにTCPで通信する時、行きはルータ->B->Cと経由しているけど、帰りはBからルータを経由せずにDに帰るようになっている。
ということでBのルーティングテーブルを変更して192.168.1.0/24を192.168.1.1に向けてみた。
するとDはCとTCPでも通信できるようになったが、今度はAとEがCと通信できなくなった。

まあそういういい加減な対処は無かったことにして。

なんとなく、ルータを経由したりしなかったりするのが良くないんじゃないかという気がするので、さしあたりDに192.168.2.0/24->192.168.1.10のルーティングを追加して、まあなんとか使える感じにしてみた。
一晩寝ると、CからAへのpingは通るようになっていたけども、CからDへは通らず。

なんか、何年も前に何か問題が起きたわけじゃないけども、こういう感じの問題に対処するために何かした記憶がおぼろげにあるような気がしないでもないけども、全然思い出せない。

よくわからない

問題は、何が悪いかよくわかっていないこと。割とつながってるやつもいるし、ネットワーク的には間違っていないようにも見える。
つながったりつながらなかったりするってことは、ARPテーブルがどうのこうのいう感じかなぁとも思ったものの、いじってみても特に変化はなさそう。

こういう時に、これがIPとして正しい動きなのか、TCPとして正しい動きなのか、Ethernetとして正しい動きなのか、それともそれぞれの機器の実装の問題なのか、そのあたりの切り分けができない。
自信を持って「これはIP的にはちゃんとできてるハズなんだけどねぇ~」みたいな事が言えない。
本当に教科書的な意味で、基本がよくわかっていない。

私のネットワークの知識はなんかルータいじってる過程でいつの間にか身についたものなので、プロトコルに関しては実のところよく知らなかったりするのです。誰だよ私がネットワークに詳しいなんて言ったボンクラは。(私です)

そろそろTCP/IPの本を一冊くらい読んでまともに勉強してみようかなぁと思いました。

それはそれとして件の問題は片付いていないんですが。

haskell-relational-recordとMySQLの識別子の大文字小文字

haskell-relational-record(hrr)とMySQLを使ってプログラムを書いてみようと思った。

書き方自体はこのあたり。
https://github.com/khibino/haskell-relational-record
https://github.com/krdlab/haskell-relational-record-driver-mysql

元々haskell-relational-recordはPostgreSQLのために作られているので(訂正:IBM-DB2 + ODBCで動かしていたのが最初だそうです)、MySQLから使おうとするとちょっと詰まるのかもしれない。
まずdefineTableFromDBが動かなかったんですけど、それはちょっと原因がよくわからないので保留。

そもそもの問題が適当に作られたテーブルの形式だったりするわけなんですが、おおむねこういう感じになっている。

mysql> connect myapp
Connection id:    126780
Current database: myapp
mysql> desc USER;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| NAME         | varchar(32)  | NO   | PRI | NULL    |       |
| MAIL_ADDRESS | varchar(128) | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

MySQLではスキーマとデータベースの区別が無いというか、同じ物を指すっぽいので、データベース名もスキーマ名も同じ"myapp"になっている。そして、テーブル名とフィールド名を大文字のスネークケース。どこの常識だこれはという感じ。まあこれはそういうものなので仕方ないものとする。

これを元にテーブル定義を作るとこうなる。defineTableFromDBが動かないので自分で定義する。

{-# LANGUAGE TemplateHaskell, MultiParamTypeClasses, FlexibleInstances #-}

module User where

import Database.HDBC.Query.TH (defineTableDefault')
import Database.Record.TH (derivingShow)
import Language.Haskell.TH
import DataSource

defineTableDefault' "myapp" "USER"
    [ ("NAME", conT ''String)
    , ("MAIL_ADDRESS", conT ''String)
    ] [derivingShow]

でき上がるテーブルとクエリの定義はこうなる。

*Main> :i USER
data USER
  = USER {nAME :: !String,
          mAILADDRESS :: !String}
     -- Defined at User.hs:10:1
instance Show USER -- Defined at User.hs:10:1
instance TableDerivable USER -- Defined at User.hs:10:1
instance ProductConstructor (String -> String -> USER)
  -- Defined at User.hs:10:1
*Main> uSER
SELECT NAME, MAIL_ADDRESS FROM MYAPP.user

とてもきもちわるい。その上この時点で既に破綻している。

  • データ型名が全部大文字で気持悪い
  • フィールド名の'_'が消えて先頭だけが小文字になっていて気持悪い
  • クエリの関数名が同様に"uSER"て何よ
  • クエリの中身のデータベース名が大文字に、テーブル名が小文字になっている

この、最後のやつが困る。他はまあ気持悪いだけなのでいいのだけど。
どう困るかというと、このクエリを発行するとそんなテーブル無いよと言われる。MySQLはテーブル名の大文字と小文字を区別することがある。

MySQLのドキュメントにはこういう事が書いてある。

5.2.2. 識別子の大文字小文字の区別
MySQL において、データベースはデータディレクトリ内のディレクトリに対応しています。データベース内の各テーブルも、データベースディレクトリ内の少なくとも 1 つ(記憶エンジンによってはそれ以上)のファイルに対応しています。トリガーもファイルに対応しています。そのため、ベースとなるオペレーティングシステムで大文字と小文字が区別される場合、データベース名とテーブル名でも大文字と小文字が区別されます。
MySQL :: MySQL 5.1 リファレンスマニュアル (オンラインヘルプ) :: 5.2.2 識別子の大文字小文字の区別

つまりMySQLのデータがWindowsファイルシステム上に置かれている場合はデータベース名やテーブル名の大文字小文字は区別しないけれど、UnixLinuxの多くのファイルシステム上に置かれていた場合は区別しますという事。なんじゃそりゃという感じだけども、これも仕方ない。

これをなんとかしてhrrを使うためには、ソースをいじる必要がある。
問題はテーブル定義と同時に作成されるクエリ関数なわけなので、それを定義している場所を探すと、tableSQLという関数が見つかる。場所はここ。
haskell-relational-record/relational-query/src/Database/Relational/Query/TH.hs at master · khibino/haskell-relational-record · GitHub

tableSQL :: String -> String -> String
tableSQL schema table = map toUpper schema ++ '.' : map toLower table

とてもわかりやすいですね。
これの"map toUpper"と"map toLower"を消してあげればいいのです。PostgreSQLでは基本的にテーブル名は小文字なのでこうなってるんだと思います。スキーマ名は忘れたけど大文字なんだっけ?

ということで、hrrはまだhackageに登録されていないので皆様各々githubからcloneして使っておられると思いますので、この辺りをいじる心理的障壁は比較的少ないんじゃないかと思います。やっちゃいましょう。branch作ってcommitしておくと本家が更新した時に追随が楽だと思います。いや検証してPull Request送れという話はもっともなんですが。

ともあれこれで一応クエリは利用可能になります。めでたしめでたし。

*Main> uSER
SELECT NAME, MAIL_ADDRESS FROM myapp.USER

ところで実はMySQLはデータベース名とテーブル名についてはなんか面倒な仕様があるんですが、フィールド名の大文字小文字は区別しないので、テーブル定義のところはフィールド名を小文字のスネークケースにすると少しだけ幸せになります。

defineTableDefault' "myapp" "USER"
    [ ("name", conT ''String)
    , ("mail_address", conT ''String)
    ] [derivingShow]

ごらんよ、焼け石に水って感じだけども。これでも普通に使えます。

*Main> :i USER
data USER
  = USER {name :: !String,
          mailAddress :: !String}
     -- Defined at User.hs:10:1
instance Show USER -- Defined at User.hs:10:1
instance TableDerivable USER -- Defined at User.hs:10:1
instance ProductConstructor (String -> String -> USER)
  -- Defined at User.hs:10:1
*Main> uSER
SELECT name, mail_address FROM myapp.USER