前回は、 DICEの初めての配布に際して、近年のIRC周辺の状況と、DICEに実装するIRC機能セットの選択基準について述べた。ただし観念的なバックグラウンドから具体性のあるデザインを抽出し計画を整形する過程の最中で書き留めたものであり、曖昧な部分が数多く存在した。当時の実装は、着手してから半年経過し最初のベータテストに臨んだばかりの段階で、一応のIRC/opennapサーバ機能は備えたとはいえ不安定で、明らかに改善の余地を残していた。現在では、さらに半年の検証と調整を経て、長期運用と数千の同時接続に実際に耐えることができる初めての正式バージョンのリリース(DICE v. 0.1)に至っている。そこで今回は、DICEの汎用ネットワークサーバとしての側面、バージョン0.1リリースに至るDICEの方向性の変更、また opennap準拠サービス(VirtualDirectory)の実装の詳細など、経験に基づいた視点をより多く加えつつ、現時点でのDICEの特性とその背後の意図を解説してここまでの記録としたい。
まず前回の要約をしておこう。前回は、IRCプロトコルの現状と、Unix/Windowsにおけるサーバの実装を概観した。そこでUnix用あるいはクロスプラットフォームではなくWindows 2000以降のWin32に特化したサーバを作ることを決定し、現在のIRCを補完するユーザ認証機構や十分なセキュリティを備えた管理サービスを、IRCとは別のチャンネルとしてサーバに盛り込むことにした。ロードマップとしては、時間的制約から、最初に単体サーバとしてある程度実用性のあるものを作り、その後にクラスタリングについてデザインと実装を行うことにした。ロードマップの第一段階の最初にWindows 2000以降に特化したスケーラブルなネットワークサーバフレームワークを作り、その上にIRCクライアントとの対話機能を載せ、さらに認証情報やユーザ/チャンネルの永続性を担保するためのRDBMSへのアクセス機能を加えた。さらに、ベータテストへ進む寸前に、フレームワークの再利用性を試すためにopennapサーバとしての機能も実装した。
以上がこれまでにDICEについて書いたことである。クライアントとの接続を保持するステートフルセッションコンテナと、エンティティを格納するRDBMSへのコネクションプールを備えたので、任意のアプリケーションコンポーネントを動的にサポートできればJ2EEなどのアプリケーションサーバ風の構成になるはずである。DICEはWindowsサービスアプリケーション(かつてNTサービスと呼ばれていたもの)として、ATLサービスモ ジュールを利用しC++により実装されている。Windowsでは内部に複数のサービスを持つ1つの実行ファイルを作ることも可能だが、ATLではサポー トされていないので、DICEは一つのWindowsサービスで複数のネットワークプロトコルを提供するという形態を取っている。DICE内で個々のプロトコルを扱うサーバ機能を表象するアプリケーションロジックは、C++テンプレートを静的に特殊化するための具象クラスとして実装されている。DICEの絶対目標の一つにパフォーマンス重視という事項があるため、この目的からすれば当然静的結合の方が有利である。パフォーマンス重視だからこそ特定のプラットフォームに向けて(さらにWin9X/NT4との互換性を切り捨てて)開発したのでありその点に関しては一点の迷いもない。
Windows 2000から追加されたAPIをクリティカルな部分で用いた結果、内部の状態遷移もその挙動に深く依存している。従って、Unixへの移植性は今のところ 無いに等しい。ホットスポットにはインラインアセンブラによる最適化も加えた。アプリケーションの性質上大きなメモリブロックを確実にまとめて処理できる 部位が存在せずさほど効果は期待できないが、MMXレジスタも一部で使用した。SSEはAMDの場合AthlonXPからのサポートなので使用していない。デスクトップアプリケーションと異なり数千の接続を抱えるためポインタの参照すら累積コストの観点から節約すべきという状況では、CPUの種類に応じ てルーチンを切り替えるような構造は好ましくない。MMXならば現在出回っているCPUの全てが備えているので、最大公約数として必要スペックに組み入れることにした。
PC-Unix上のアプリケーションについて、OSが軽いので古いハードウェアを再利用できるということがしばしば言われる。それと対称をなすように、DICEはハイエンド化するWindows PCの能力を存分に引き出すことを意図して作った。2GHzのCPUがハイエンドでもないPCに載る時代である。大きなネットワークを構成する時間も能力もない個人でも、中の上くらいのスペックのデスクトップマシンが一台とブロードバンド回線があれば相当に高性能のサーバが運用できるのではないか。また巷に溢れるWindowsサーバ批判の多くがWindows 2000について論じる物ではなく"NT"を対象にした古い物ばかりだったので、Windows 2000で導入された新技術を学習の傍ら用いながらWindowsのサーバとしての性能の検証に乗り出そうと考えた。それ以前にはLinuxや FreeBSD上でのプログラミングもよく行っていたものの、瑣末な作業に時間を取られるのに疲れある時点で飽きてしまったので、労力の節約のために Windows 2000以降のWindowsでのプログラミングに取り組む動機はあった。思うにCUIがUnix一般で好まれるのはそれが汎用かつ万能のインターフェイ スだからである。つまり、一般的なGUIのようにコンポーネントとそれに関連づけられたアクションの数が限定されているのではなく、CUIではシェルでプ ロンプトに続いて文字列を打ち込めばそこからありとあらゆるアクションを起こすことができる。しかし「できる」という、可能性があるだけなのだ。
またプログラミングについて言えば、Win32のAPIは汚いとか仕様が頻繁に変更されるとかという声はあるが、私は必ずしもそうした非難が的を得ているとは思わない。特に新しいAPIについては明かな指向性が見て取れる。何より私は新しい物好きである。Windows 2000以降に追加されたWin32のAPIは興味深い物を多数含んでいる。ちょうど「闘うプログラマー」(原題: Show-Stopper!)を 読んだ後だったので、WindowsNTという、既存のUnix系OSに対抗するために戦略的に造られた新しいOSに興味を惹かれていた。Linux は、"Just for fun"というLinus Torbaldsの言によれば、娯楽(とMinixでは得られない実用性)のために造られたという。対してWindowsNTは、サーバ市場に向けての戦争の道具として造られた純然たる戦争機械である。それに、P-threadによるプログラミングに対し、Windowsの同期プログラミングは単純明快に見えたことも大きい。しかしながらWindows 2000がデスクトップでの作業でそれなりに安定しているといっても何千ものクライアントが接続してきたときに果たして実用性を保てるものだろうか。そういうわけでDICEは私の好奇心そのものである。
わざわざ強調するまでもないがPCの台頭によってサーバアプリケーションをめぐる状況は変化した。IRCについていえば、かつては地理的に近隣に位置する サーバに回線が弱いクライアントが接続し、上位で太い回線を備えたサーバ同士のリレーにより規模拡大が行われた。資源の絶対的欠乏が与件でありそれを切り抜けるべくなされる効率的資源配分が問題解決の主眼だったのである。現在でも大きなIRCネットワークに属するサーバではクライアントの国別ドメインに よって接続の可否を決定するという原始的な資源配分が行われている。それが近年は周知のように一般ユーザが通常使用には余る太い回線を獲得した。ネットワークゲームのようにping値が重要になるほどレイテンシを抑えなければならない環境を除けば、就中IRCのように単純なコミュニケーション形式においては、大域的な資源配分はほとんど問題とならなくなった。問題があるとすればせいぜいNimdaのようなワームが拡散時に感染ルートを最適化する場合に考慮されるくらいである。
近年Microsoftが強調しているマーケティングのための惹句(buzzword)にexperienceなるものがある。ではIRCがIRCであるために必須のexperienceとは何だろうか。そうした問いの中で、サーバ同士がリンクされデータをリレーするというIRCの'R'である属性は、ユーザのexperienceからは最大限隠蔽されるべきであるという命題が浮かび上がるはずだ。その隠蔽の努力を破るのが、文字通りnetsplitとして知られるIRCサーバ同士の接続断裂状態である。netsplit が起こるとそれまでアトミックであったはずのチャンネルという単位が上位層の分裂により分裂を余儀なくされてしまう。Windows用IRCクライアントとして独占的なシェアを持つmIRCは複数IRCネットワークへの同時接続に最近になって対応した。また、opennapサーバへ接続するクライアント機能を持つWinMXは、複数のopennapネットワークへ同時に接続しコマンドを発行できる。
opennapプロトコルもまた、IRC同様に、代表的な実装(opennap/slavanap)は、複数のサーバをリンクすることにより多数のユーザをホストしネットワーク全体のスケーラビリティを向上させようとしており、またIRCに非常に似通ったチャット機能を提供し、netsplitも存在する。しかし、一つのクライアントが複数のサーバに接続できるということの含意は、もはやクライアントがサーバ資源の浪費をモラルに従い厳しく自制する時代は終わりつつあるということである。そして、上記のIRC特有のexperienceとは何かという問いへの回答を試みるならば、それは第一に「1つのチャンネルで同時に多数の人間が文字で会話できること」(collaboration)である。第二に、「チャンネルのオペレータ権限が守られること」(authorization)である。勿論、複数のサーバに別々に接続したユーザが一つの空間を共有できるというIRCの特性は、インターネットの特性と同じく、あるサーバが攻撃に屈しても他 のサーバによってシステムは生き延びるというリスク分散の観点からは評価できる。しかし個々のユーザにとっては、自分が接続しているサーバがダウンすればその時点で放り出されることには変わりはない。
これに対し、サーバ間での会話メッセージのリレーを行わず、特定のチャンネルが特定のサーバへ1対1にマッピングされる状況で全てのクライアントがあらかじめ予備サーバについての知識を持っていれば、異常時には直ちに予備サーバへ移行してチャンネルでの会話を続行できるだろう。背後でのシームレスな責任移譲が重要である。例えばMicrosoftのDirectPlayのP2Pモードでは、64人程度の小規模ネットワークながら、ホストを動的に移行できる。サーバ設営側にとっては、一つの名の下にIRCネットワークを統括し多くのチャンネルを抱えることは重要な関心事であるかも知れない。ところが一般のユーザにとっては、IRCとはとどのつまりチャンネル本位の世界なのである。ただし一般ユーザとサーバ管理者の中間に位置するチャンネ ルオペレータにとっては、大きなネットワークへ参加することにより同じネットワークの他チャンネルから客が来るかも知れないという期待もあることだろう。しかしその期待は、IRCというプロトコル(とその主要な実装例)の欠陥から生ずる副作用に過ぎない。サーバとサーバの垣根がチャンネル間の距離と等しくなれば、大きなネットワークという概念も崩壊するはずである。サーバへの接続という作業さえクライアントの目から隠蔽され、異なるサーバへ接続することと異なるチャンネルへ接続することの境界が無くなれば、リレーなど行う必要性はない。チャンネルをリストするためのディレクトリとして情報の一貫性/検索可能性を維持するために巨大なIRCネットワークが必要だとするのは、資源の無駄であり本末転倒であると考える。ディレクトリ機能と認証機能とが独立したサービスプロバイダとして外部に存在していればよいのであり、会話データのリレーはこのシステムにとって必ずしも本質的ではない。
このテーマについて自問を繰り返すうちに、純粋にロードバランシングのためにクラスタリングを行うこと(たとえば一つのマシンが扱える TCP/IP同時接続数には限界がある)と、IRCのように、分散化が本質的な機能面をも浸食している状態を許容することとの価値のずれに強く違和感を覚 えるようになった。このずれは、前者のロードバランシングで得られた効果を後者の誤ったネットワーク設計による損失で相殺しているという不幸な状況なのではないか。そうした状況に陥りかねない破綻したシステムにコミットすることに何らかの利益はあるのだろうか。IRCは現在のようにインターネットが家庭にまで届く随分前に考案された物であるにも関わらずその根本的部分について改定が加えられていない。純粋に政治的な問題から、互換性を損なう変更ができな かったのである。IRCプロトコルを拡張してクライアントにほんの少しコードを付け加えるだけで上記のシステムは実現できる。しかし実際には私の手元にクライアントのコード(とユーザベース)はない。前回詳しく書いたが新プロトコル(いわゆるIRC3)への移行は計画ないしプロトコルそのものが具体化する以前の段階で無惨な失敗に終わっている。改良されたIRC2が当座の利用には必要十分だったからである。
こうした状況は二律背反にほかならない。既存の、ある程度分散化を達成しているIRCdの実装をコピーすることなら楽だろう。しかしそれは最初から望んでいない道である。IRCプロトコルそのものの病弊は指摘されて久しく私も十分そのことについて学習した。 そこで私は発想の転換を体験することになった。DICEについては、まず単体のサーバとして限界まで性能を突き詰めれば良いではないか、と。分散化云々については棚上げし、局所的パフォーマンスの最大化に努めればよい。そしてIRCプロトコルの実装は、単に既存の良くできたクライアントアプリケーションからDICEというものへ接続する為のインターフェイスとしてDICEが用意するものの1つに格下げされ、最早DICEの目的ではなくなった。IRCプロトコル は、MVCで言えば、DICEのモデルを表現する多数のビューを構築する形式、ストラテジの一つとして利用され、またそれを通して既存のクライアントアプ リケーション資産をDICEが利用できる。
限られた時間と人的資源の中では問題を切り分けなければいつまで経ってもろくな物が出来ないのは目に見えていた。例えばLinuxカーネルが一人の人物によって短期で実用レベルに至り周辺が急速に成長したのに対し、同じオープンなプロジェクトでも、GNU/Hurdのシステムがいつまで経っても実用に達しないのには理由があるはずである。オープンなプロジェクトですらそうなのだから、DICEは私が自己責任で造ってい るということからしてさらに慎重を期しつつ、かつ大胆に不要部分を切り捨てるべきであろう。当初考えていたbetter IRCという目標と比べると相当急な視野の転回ではあったものの、それまで頭を悩ませていたDICEのアイデンティティに関して、ようやく道が開けたような気がした。こうしたことに思い至ったのはDICEにopennap準拠サービス(VirtualDirectory)を組み込んでからかなり後のことであり、それまでは何となくIRCの実装後に単に可能だからという理由で深く考えずにVirtualDirectoryを実装してみたものの今ひとつ意義を見いだしにくい状況だった。
実装については、DICEは、プロトコルとは関係なく汎用ネットワークサーバとして、Apache/IIS/各種J2EEアプリケーションサーバなどを念頭に置きつつ設計したため、全く問題なく対応できる。 当初はopennapはディレクトリの公開がその本質的機能なのだからという原理的理由に基づく効率至上主義に徹し、チャット機能の実装は行わないつ もりだった。そこから180度方針転換し、IRCクライアントとopennapクライアントとが同時に1つのチャンネルに参加して会話できるようにした。ただ し、IRCクライアントの方がチャットに関しては高機能なので、チャンネル管理/作成などの権限は専らIRCクライアントが担うようにデザインした。ま た、メッセージ変換用ローカルプロクシサーバの利用をユーザに強いるわけにはいかず、あるいはDICEを機能毎のプロセスへ分割するつもりもなかったの で、サーバサイドでIRCメッセージとopennapメッセージの相互変換を行う必要がある。さらに、UNIXの世界から生じた日本語IRCでは一般的に JISコードが用いられ、Windowsでの利用が中心であるopennapクライアントでは日本語にシフトJISコードが用いられるので、日本語圏で使用するためにはその変換までサーバサイドで負担しなければならない。これを実現するための障害は技術的なものではなくサーバを非効率にしたくないという私自身の精神的な抵抗だった。結局私は当初の自分の信念を曲げてこれを実装したが、結果的には良かったと思っている。
最もフラストレーションが溜まったのは、IRCプロトコルが明白な欠陥を多く含む不完全なものであり、既存のクライアントへの対応を心がける限りその不完全性に追従しなければならない点である。Microsoftは同じ状況でIRC-XプロトコルをMS-chatとExchangeのために作り実用化した (後にその経験をMSNメッセンジャー製作に生かしたと思われる)が、一個人である私にそれを行う力は無い。特に、優秀で、多くの人が使っているクライア ントアプリケーション(たとえばmIRC)が先に存在する状況では、クライアントアプリケーションの絶大な影響力を無視してはサーバは成り立たない。サー バ製作者が勝手に何か新しいことを始めるわけにはいかない、相手の顔色を伺いながら事を進めなければならないという面倒な世界なのだ。デスクトップアプリケーションのように単純に発想を形ある物にしてやればいいという訳ではなく、相互運用性を高めることが非常に重要である。折角楽しむために趣味でソフトウェアを書いているのに、同時に自分の無力さに耐えながら制約下で作業しなければならないというのも因果な話である。当然ながら作業続行のためにはきっと 良い物が出来上がるに違いないという信念と楽観主義が必要となる。
その点、サーバとクライアントが一致しているpure P2Pアプリケーションではそこを意識せず全て自己の管理下に含めることができると思われる。作業量は倍加するに違いないが、その自由に羨望を禁じ得ない。DICEに次のステップがあるとすればそこにしか存在しないだろう。ただし現状でも完全にIRCプロトコルのRFCに追従するのではなく、ささやかな反逆として一般的なクライアントが理解できる範囲での様々な改定を加えている。唯一救いがあるのは、IRCプロトコルはバイナリプロトコルではなくASCII文字列をコマンドとして使うためコマンドの拡張が容易な点である。telnetでもできるIRCの、UnixのCUI/テクストファイル重視の文化との接点は、ここにある。XMLもこの流れだろう。DICEではIRCユーザ用のシェルを操作するためのコマンドとしてshellという命令を付け加えており、一般のクライアントからは"/shell"とタイプすることによってこのコマンドをサーバへ送信できる(この点opennapメッセージはバイナリプロトコルなので効率的な反面融通が利かない)。
こうした改定は、mIRCで問題なく動作することを念頭に置いて加えている。こう書くと誤解を生みやすいが、特定クライアントのみが備えている振る舞いを当てにして開発しているという意味では勿論無い。RFCに記述されているログイン時のハンドシェイクの手順、チャットに使用する本質的なコマンドなどは全 てRFCに従って実装されている。また、特定リプライの解釈が曖昧になる(つまりクライアントがチャンネルの状況を誤って把握する可能性を生む)ような変 更は一切加えていない。前回書 いたように、IRCに関して現在出されているRFCは「腐ってもRFC」なので一応は準拠している(ただし本当に腐っている部分は積極的に排除されてい る)。それは、現在のデファクトスタンダードとなっている3大ネットワーク(EFnet/DALnet/Undernet)でそれぞれ使用されているサーバソフトウェア(それぞれIRCnetのIRCdから派生しほとんど全て書き直されている)も同様である。
残念ながら日本はIRCが盛んではなく世界の他の部分から隔離されているので、IRCに関する状況は他より遅れている。mIRCをリ ファレンスとしてDICEにIRC機能を実装したのは、mIRCがRFCの最も重要な部分を正しく実装し、かつ例外に対する耐性を備えているからである。IRCに関するRFCの最重要部分は、私見では、個々のコマンドやリプライコードの列挙にあるのではなく、IRCメッセージの仕様を規定したところにある。つまりそれに従ってパーザを作ればサーバが送ったあらゆるメッセージについて(その意味の解釈についてはともかくとして)サーバが意図した形で受け取れる。RFCがBNFによって規定しているIRCのメッセージは、opcodeと0-15個のoperandから成る、RPCに使えそうな形式である。ところが、このパーザの部分すら正しく実装せずメッセージの種類ごとにいわば「決め打ち」している日本製のクライアントがある。当然決め打ちの方がプログラミングが容易でありかつパフォーマンスも高くなるが、後々のことを考えているとは言い難いやり方である。しかもそのクライアントは開発が停止したも同然 なので、このクライアントの側の不具合に対応するために、DICEが譲歩して補助的な情報を加えてやらなければならなかった。他にも、RFCを提出したIRCnetのサーバである"IRCd"(IRCdとは一般名称ではなく特定ソフトウェアの名称である)についてのみ対応したために、結果的に決め打ちないし場当たり的な実装を行っているクライアントがあった。そして、世界的に見れば、この"IRCd"を使用しているネットワークは今となっては少数であ る。
Winsock Programmer's FAQによるとWindows NT4での接続ソケット維持数の限界は1万から2万程度とされている。スケーラビリティを意識した適切な非同期I/O方式(すなわちI/O completion ports - いわゆる「I/O完了ポート」、この訳語が嫌いなので以降では原語で統一する)と、NT4ではなくNT5を使用しなおかつNT4の時代のものよりハイスペックなマシン上で運用すればさらに制限を緩和できるに違いない。DICEはそこを狙っている。IRCサーバの活動中で最も回線の帯域を消費するのはチャ ンネルリストの送信だが(IRCプロトコルにはデータ圧縮方式は規定されていないので参加者が10万人を超えるネットワークではクライアントの要請に応じて送信されるリストは1本が2-3MBに達する)、それを除けば大した帯域を消費するわけではない。そしてDICEにおいては、IRCというプロトコルに関する限り、1万人を超える参加者を擁するネットワークを建てようという目論見とその実現の見込みがない限り、私に分散化のためのコードを書くメリットは何もない。
さらにopennapの場合、サーバ間リンクは絶対に実装しないだろう。opennapでのファイルのやりとりについては1対1のコ ミュニケーションで必要十分であるためIRCのようにチャンネル内での発言の先後の調整を行う必要がなくリンクの実装はたやすい。ただ恐ろしく非効率である。検索結果をサーバ間で受け渡しすることはまだ受忍できる。しかし個別ユーザのディレクトリの閲覧結果までをサーバ間で受け渡すというのは、胸が痛むほどに非効率である。ピア間で直接接続を樹立してリストを受け渡しするコマンドもopennapプロトコルには規定されているが、ユーザセキュリティ上の問題がある。もちろんサーバ間リンクを行いopennapネットワークを拡張することによって生まれるスケールメリットはある。それは、多数の検索結果が得られることではない。WinMXのように複数のサーバに接続できるクライアントが存在する以上ユーザにとって個別ネットワークにおける検索結果数は大した問題ではない。比較的大きなopennapネットワークがユーザに提示できるメリットとは、とどのつまり、Crowdsのように、そのネットワーク内で個々のユーザのプライバシー(実際にはopennapはピア間の責任を曖昧にしないので、opennapについてはプライバシーという言葉は意図的にCrowdsの保障するプライバシーと異なる意味で誤用していることも注記しておく)が確率的に向上する可能性があるという点に存するのであってそれ以上ではない。
ビジネス上の観点からすれば、スケーラビリティを極限まで高めることもサーバアプリケーションの重要な目的ではある。しかしそれと並び、DICEでは、サービスを受けるユーザの視点から、各ユーザがつつがなくexperienceを享受できる環境を作ることを徹底したつもりである。また、サーバ全体の資源を節約するために公共性に配慮しつつ機能を設計しなければならない。従って、リンクを行わず単体のサーバで必要十分のサービスを提供できるならば、当座の目的は達成される。サーバとしての能力を推し量るためには、クライアントユーザから見た総合的な利便性の定義についても考慮すべきである。
DICEが重んじる価値を端的に挙げるならば、まず第一にデータを送受信するネットワークの端点としてのパフォーマンスである。理由は再三これまで述べたとおりだ。Windows XPがデスクトップに入り始めた近年にあっては、DICEのようなアプリケーションを個人で実稼働させることもたやすく、そこで最大限のパフォーマンスを 引き出すことを目標としている。つまり、大規模なサーバマシン上で動かす場合もさることながら、デスクトップでP2Pのノードとしても十分に働きうるネッ トワーク性能の達成である。
そして第二に、クライアントユーザの利便性が重要である。これも理由は既に大半が述べられているが、副次的な指針としては、IRCサーバのようにある種の閉鎖的な空間を作り出す性質のあるサーバアプリケーションではしばしば管理側の望む過多な機能がユーザの利便性を損なっているという事例を目にしたので、敢えてクライアント側の視点を明確にすることにこだわった。デフォルト設定の変更により様々な状況に対応すると言うよりは、デフォルトを最善の状態に保つことにより他に資源を振り向けられるようにしたつもりである。最善というからには、当然そこには私自身の価値観が色濃く反映されてい る。より具体的には、必要なセキュリティの確保は厳格に行うもののその他の点では出来る限りリベラルな環境が保たれるように心がけた。この方針は特に VirtualDirectoryにおいて顕著である。
これと関連する第三の価値が、シンプルさである。使用頻度の低い機能の付加は実行時のオーバーヘッドを高めるので望ましくない。そして、ある成果を、必要な最小の要素のみによって構築することは、美的観点からも好ましい。デスクトップアプリケーションではないので不要な機能は削ぎ落と し、アップタイムを延ばすためにワーキングセットを小さくすることに努めた。DICEにはサーバが担当しなければ実現できないもの(特に認証や最低限のセ キュリティに関する設備)のみを含め、それ以上の、チャンネルやユーザなどのコンポーネントに特有なデータの保持をユーザが望む場合は、ユーザ自身で自前の資源をボットプロセス/サービスとして用意すべきである。
第四に、管理の容易さを達成しなければならない。これは、第一義的には管理に要する人員の削減によって実現すべきであると考え、それなりの接続数を擁するサーバでも1人か2人で運用できるように考慮した。管理業務に専門知識や多数の人員という資源を要するシステムは、他の部分がどれだけ優れていても、それが管理コストを多く消費するという一点のみを原因として総合的に劣る可能性があるというのが私の持論である(Microsoftが Linuxを攻撃する時も同様の論拠によるに違いない)。また、Windows NT系はアプリケーションサーバとしての側面も強く持っており、GUIのMMCからパフォーマンスモニタリングができるなど管理ツールが充実しているの で、Windowsそのものがユーザに公開している機能はDICEの方で敢えて実装する必要もない。ただし、ユーザが交換するメッセージの内容について、セマンティックな面で検閲や取り締まりを行うことは現在の計算機では不可能なので(せいぜい特定文字列に反応しフィルタすることしかできない)、この面でのチェックを行うならば人員に頼るしかない(個人的には推奨しない)。そして、2人のユーザが1対1 で交換するメッセージや、VirtualDirectoryに対する検索クエリについて、プライバシー保護の観点から、DICEは一切のモニタ手段を提供 しない。
以上4つのDICEのデザイン上の価値判断は先天的に存在しているのでこれに反する価値を追及する場合はDICEを使用するべきではない。
ところで、オペレーティングシステムのカーネルについて評価する場合には、小さなカーネルを中心に置き機能毎に分離されたモジュールが通信しあうマイクロカーネルモデルと、カーネルに多くの機能を含むモノリシックモデルの優劣についての議論が大抵持ち込まれる。一般的見解では前者は、分散化を視野に置いたより洗練されているモデルでシステムの堅牢性を高める上で有効であり、WindowsNTカーネルや、MacOS Xが採用するMachカーネルに用いられている。しかし各サービス間の通信/調整コストがかさむためパフォーマンスを確保しにくい。開発も困難である。そこでGNU/LinuxやBSDはモノリシックカーネルを採用し、実績を上げている。IRCサーバも似た状況で、カーネルコアをIRCサーバに見立てれば、近年のIRCネットワークのほとんどはユーザ認証やチャンネル登録、プロクシサーバスキャンなどを行うサービスを中央のIRCサーバ群とは別のプロセスとして、場合によっては別のマシンで稼働させている。つまり資源を有効に分配しリスクを分散するという観点からのマイクロカーネルモデルが一般的である。
IRCプロトコルについて調べ始めた頃に、RFCの中でIRCサーバとサービスとの通信に言及している部分を見つけ、それが何らかのバイナリプロトコルではなくIRCプロトコルを用いて行われることになっているのに驚いたことがある(IRCメッセージはペイロード部分以外のヘッダ部分は ASCII文字列であり、ペイロードを改行記号で終えるストリームである)。つまり各自動プロセスがIRCサーバへログインし、IRCサーバから授権されたIRCユーザとして特殊業務を一般のIRCユーザ相手に提供している。DICEは、そうした機能毎のプロセス分散は行わず、OSカーネル方式のアナロ ジーを使うならば、モノリシックな構成を採用している。
各機能は、内部ではC++のクラスとしてソースレベルで分割され、OSスレッドレベルで相互に作用し合う。ただしスレッドはプールに格 納されているので、ワーカスレッドを仕事に応じて作る単純な場合と異なり、厳密な機能毎の対応があるわけではない。この場合全てカーネルモードで実行されているようなもので、どこか1点でも欠陥が有ればサーバ全体が陥落してしまう。しかし、一台のマシン上でのパフォーマンスでは優位に立つはずである。しかし一方で、完全にマルチスレッド化されているために、CPUが1個しかないシステムではシングルスレッドの擬似マルチタスク プログラムにパフォーマンスが劣ってしまうかもしれない。その点DICEではCPUの数に応じてI/O completion portsとOSの制御下のWin32 thread poolが適切な数にスレッドの増加を抑えコンテクストスイッチのオーバーヘッドを最小限に留め、スレッド間の同期に費やされるコストを補ってくれる。そ して、2-way、4-wayでスレッドを並列動作させられるマルチプロセッサ環境ならば、絶対的な性能向上が約束されている(ただしメモリなどの資源に つき最大限競合を避けるようプログラミングにおいて留意すべきである)。
また、既に書き始めてから1年経ってしまったので判断しにくいが、なるべく早く造るということにも挑戦したのでオブジェクト指向は必須だった。ただし、最初は分散化を指向してオブジェクト間の結合を緩くしていたのが、モノリシック構造での局所的パフォーマンスの最大化を目指すようになると、非常にタイトに組み合わされるようになり、より低レベルな、生成されるコードを意識したコードへ変遷していった。その点C++の柔軟な構造が非常に助けになった。最初は STLを多量に用いていたのが、似たインターフェイスを持つ自前のクラスへ後から置き換えられていった。単にモノリシックなデザインを考えそのパフォーマンスを第一に考える場合、巨大な「サーバ」というクラスないし空間が一つグローバルにシングルトンとして存在するようなものになりそうだが(C言語で書くと実際そういうものになる)、DICEについてはクラスは概ねIRCを構成するコンポーネント/アクターを 表すドメインとして、例えばセッション/チャンネルクラスとそれぞれのファクトリクラス、ファクトリを管理するサーバクラスといった具合に切り分けられ、可読性を高めると同時に、オブジェクト間の通信コストをせいぜいポインタを1個参照する程度に留めることができる。
パフォーマンスを高めるために、C++の仮想関数によるポリモーフィズムや例外処理機構は特殊な場合(COMコンポーネント使用時な ど)を除き一切使用していない。たとえば、オブジェクト指向でよくあるパターンでは、DICEは複数のプロトコルをサポートするので、サーバ上の全てのユーザに対して同じ内容のメッセージを送りたいといった場合に、異なるプロトコルのセッションを横断して一つのメソッドを適用したいという誘惑から、イン ターフェイスを定義してそこから各特殊クラスを導出し、実行時に全ユーザのコレクションにポリモーフィックなメソッドを実行させるということを考えつくかもしれない。そうしたことはDICEでは行わず、C++テンプレートによる静的なバインディングに留めている。
こうしたトップレベルでの設計方針については、なによりopennapサーバとしての機能をSQL Server/MSDEをディレクトリストアとして用いて一週間で実装できたことから判断して、間違っていなかったと思っている。ただし、その後でopennapサーバ機能の実用性を高めるための改善に大変な苦労を要し、結果的に最初の実装を没にする羽目になったので、この特定問題の解決に用いた手段の選択には失敗したと言わざるを得ない。また、中間層では上記のようにオブジェクト指向的なモデルの当てはめが容易だが、低レベルでは非同期I/Oを使用している関係上オブジェクト指向に馴染まないプログラミングが必要になるため、その部分を可能な限り包み隠して下層に押し込めなければならなかった。
低レベル部分で最も苦労したのが、管理サービスの通信部分のプログラミングだった。DICEの管理サービスは、SSL上の独自プロトコルによって管理用 GUIクライアント(DICEAdminShell)と通信を行う。MicrosoftはSSPIという暗号化通信のための抽象化されたインターフェイスを提供しており、ネットワークなどを通してやりとりするデータをNTLM/Kerberos/SSLにより保護できる。 WindowsCEのWinsockではオプションとしてSSL通信が実装されているが、素のWinsock2にはそうした機能は無いので外部ライブラリを使用しない限りSSPIに頼らなければならない。とはいえ最初からSSLを使うと決めていたわけではなかった。要は、IRCのように平文で通信を行うのではなく、インターネットを越えて安全に通信が行える公開鍵暗号を使った通信チャンネルがあればよい。そこでNTLMとKerberosを検討した。しかし私はそれらがWindowsドメインやActiveDirectory内でのアカウントを要求するという点を資料から読みとれず、完全に実装を済ませてローカルでのテストに成功し、インターネット越しに通信を行い失敗した段階で初めて、それらがアカウント情報を同時に送信する必要があることに気付いたのである。最初にKerberosでの通信機能を完全に実装して失敗したので次にドメイン外からも通信できるというNTLMを使って再度失敗してようやくそのことを理解し、なしくずし的にSSL の使用を決めた。
ただしSSLには欠点がある。今回はクライアント認証は必要ないのでクライアントが証明書を備える必要は無いにせよ、サーバ側で設置の際に証明書をインストールする必要がある。また、SSPI上では、NTLMとKerberosは暗号化メッセージがブロック(厳密には、プログラム作成者がサイズ情報を先頭に付け加えることでブロックとして扱えるようにになる)なので状態遷移が比較的単純で、プログラムもほぼ同一になるが、SSLはストリームであるために、同じSSPI上でも全く異なる方式を要する。そしてDICEについては、あらゆるI/Oについて例外なくI/O completion portsを用いることが決定事項だったので、同期I/Oを使って書かれているSSPIによるSSLのサンプルと大きく異なるものを書き上げなければならない。I/O completion portsを用いて書かれたプログラムは、制御のフローがスレッドをまたがって末尾再帰的に進むものになるため状態遷移が非常に複雑になり、バッファも ローカルスタックに割り付けられず管理が難しい。その上にSSPIという既成インターフェイスによるSSLストリームの解釈の各ステージを乗せていくという作業は苦痛以外の何物でもなかった。
DICEAdminShellは単にこの通信クラスのGUIラッパーであり、こちらの通信部分もI/O completion portsで実装され、DICE内部の管理サービスと対称を成している。最初にサーバ/クライアントの通信クラスのペアを同じ手法を用いて作り、コンソー ルで実験してから、DICEとDICEAdminShellの双方に組み込んだ。DICEAdminShellのGUI部分はMFCで適当に作ったので見栄えは悪いものの数日で済んだが、途中でKerberos/NTLMバージョンを没にせざるをえなかった影響で通信部分は完成に1か月もかかってしまった。実装も極めて複雑になり、DICEAdminShell関係のモジュールは再度手を入れるのが億劫な、DICEの最も醜い部分である。そういうわけで当初のデザインでは管理関係のコマンドの大半はDICEAdminShellに入るはずだったのが、結局はセキュリティの低いIRCのavatarへ相当量が移譲されることになった。
手間を省けるという期待からSSPIというブラックボックスを使用したが完全に逆効果だったので、SSHのようなものを適当な公開鍵暗号の実装を用いて自作するべきだった。もし次があるとすればこの部分はそっくり全体が差し替えられる可能性が大きい。ただ、DICEAdminShellに関しては、SSL以外の他の部分はサーバもクライアントもプロトコルも自作なので、自由ではあった。DICEAdminShellはUnicodeでビルドされ、またDICE内で他のDICEAdminShellユーザとUnicode(UTF-16)でチャットを行える。DICE自身もWindows NT系の標準であるところのUnicodeでビルドされているが、IRCメッセージやopennapメッセージは単なる8ビットバイナリなので、Unicodeらしい文字列プログラミングは出る幕がない。ただし、APIのASCIIバージョンとWIDEバージョンを自動選択にしていると合わない部分が出てくるので明示的にASCIIバージョンを指定するか、またはAPIを使用しないで済むように書き換えを行う必要があった。
VirtualDirectory(以下VD)は、DICEに組み込まれた、opennapプロトコル準拠サーバ機能の実装である。opennap は、Napster社のプロトコルをリバースエンジニアリングすることによって得られたプロトコルの名称で、リバースエンジニアリングを行った人々により、ドキュメントと、同名のC言語によるオープンソース実装が提供されている。opennapの仕組みは、サーバが各クライアントからファイルリストを受 け取り、その集合に対しての検索や閲覧の要求を処理するというものであり、サーバについてはP2Pとは何の関係もない。DICEに組み込んだサービスについてわざわざVDと呼称することにしたのは、ひとえに"nap"という文字列がNapster社との誤った関連を想起させるので好ましくないという判断からである。また、IRCの場合と同じく既存のクライアントに対応せざるを得ないので、メッセージフォーマットとopcodeを流用しつつクライアントが解釈可能な範囲内で効率とセキュリティを重視する方向へ微妙な仕様変更を加えている。例えばNapsterは音楽データの共有に使用されていたためにそれを支援する情報がopennapメッセージの中にも組み込まれているが、そうした情報は全て除去し無駄なデータの転送を避けている。またopennapでは、あるクライアントのIPアドレスやダウンロード/アップロード活動に関するデータを同じサーバ上の第三者がWHOISコマンドによって得ることができるが、こうした部分も排除した。IRCサービスの方も、WHOISコマンドから照会先クライアントの活動を表す情報は除いてある。こうした情報はプライバシー保護/個人情報管理の観点からは不要であり、デフォルトでは隠されるべきであると考える。
IRCに関する機能の実装が一段落しベータテストに進む前に、何か短時間で入れられる面白い機能は無いかということでアイデアを探っていたところ、 opennapのチャット機能がIRCのそれのように高機能ではないにせよIRCをモデルに作られていることを知ったのがopennapへの着目の端緒だった。ステートフルなセッションを多数ホストするサーバという形式も同じである。DICE上に存在しているIRCと重複する機能を省いてopennapサーバの本質的機能であるディレクトリに絞れば、まとまった時間がありさえすればDICEのフレームワークを用いて実装できそうな直感はあった。また、DICEはMS SQL ServerまたはMSDEを認証情報を保持するために使用するので、データベースにそのままディレクトリを対応づければ検索機能はデータベースの機能を そのまま流用できる。C言語で書かれたopennap実装のソースコードも実に淡白で、DALnet他のIRCサーバのように機能拡張を重ねて迷宮のようになっているわけでもなく、プロトコルのドキュメントを見ながら短期間で実装できる確信ができた。
そこでIRCをDICE上に実装したときと同じように、opennapセッションのクラスを定義し、opennapメッセージパーザを書き、メッセージハンドラメソッドをひとつずつ実装していった。そしてユーザが提出するパス情報を格納するためのリレーショナルデータベースのスキーマを定義し、テーブルにアクセスするメソッドを実装した。現在ではIRCとVDはチャンネルを共有しているものの、最初は専らIRCユーザにディレクトリ機能を使わせることを想定していた。IRCにもDCCというP2Pによるファイル転送機能はあるが、他のクライアントへファイルのリストを告知するシステムが 欠けているので、それをVDで補うことができる(IRCではその部分をユーザがクライアント用スクリプトで補完している)。そこで+xモードを管理者のみが有効化できる新たなチャンネルモードとして設け(チャンネル属性が文字列であるのもコマンド文字列がASCIIであるのと同じくIRCプロトコルの数少ない可塑性の一である)、+xチャンネルへの特定ホストからのクライアントの参加を参照カウンタとして保持し、IRC側の+xチャンネル参加者のみがVD を使えるようにする機能も付けた。 IRCクライアントはSOCKSプロクシスキャナによる検査を経てログインしているので、ホスト情報をrloginのホスト情報のように用いてVDへセキュリティを伝播させることができる。(ちなみにこの機能は現在は廃止され、opennapクライアントにチャンネルへ参加したときのみ検索などを許可するという機能に置き換えられている。)こうして、この単純なディレクトリ機能は、不完全な部分はあちこちにあったが1週間で実装が完了し、WinMXなどのクライアントからのアクセスも可能になった。その当時はopennapについて大した関心はなく、VirtualDirectoryは単なる自己満足のために入れたDICEの付録以上のものではなかった。
ところが後日、ベータテストが進むにつれDICEの主要な問題となったのは、IRCではなく、VirtualDirectoryの方だった。ユーザが 数百人を超えると期待に反して全く使い物にならなくなったのである。問題は全く予想していないところにあった。MS SQL Server/MSDEである。RDBMSはRAMを節約するために使う物であり、またミッションクリティカルな所で使えるものであると信じ切っていたので非常にショックだったが、MSDEの売りであるところの自動制御機構がどうしてもコントロールできず、どう設定を変更してもRAMの消費が抑えられなかったのである。また、MSDEがボトルネックになり、接続するクライアントが多いとサーバの動作が極めて鈍くなってしまう。opennapの性質上、サーバの再起動直後は多数のユーザが一度に押し寄せ、数十万件のデータを数秒以内に 処理しデータベースへ格納しなければならず、その際にMSDEがキャッシュとして多量のRAMを予約し、その後も容易には離さない。当然、テーブルはディ スク上に存在するので、I/Oによっても遅くなる。つまりSQLでいえばINSERTが問題なのである。
考えてみれば単純なことで何故気付かなかったのかと今にして思うが、そのことに思い至るまでは、専ら検索の遅さに原因があると思 い込んでいた。 MS SQL Server 2000/MSDE2000には単語ベースの索引を作って全文検索を行う新機能があるが、これは特殊機能なので使わず Transact-SQLのLIKEによって全文検索を行っていたので、検索速度の改善のために色々工夫した。まずはレコードの属性によって複数のテーブ ルへ振り分け、検索語によって絞り込みができるようにした。例えば検索語にアルファベットが4文字含まれているならば、アルファベットを3文字しか含まないレコード群は検索する必要がない。これをもっと複雑なアルゴリズムを用いてレコード群を分解し、日本語のファイル名について考慮しつつなるべく均等に複数テーブルへレコードが割り振られるようにしたところ、2倍程度の性能の向上を見ることができた。しかしそれでも、他のopennapの実装は、レコード をメモリ上に持ち、単語から生成したハッシュ群をメモリ上のインデックスとして検索の対象にしているので、DICEの検索はその何倍も時間がかかる。
SQL Serverのエンジンをバックエンドに持っているということを大きな長所として考え、またその時点ではRAMのサイズ的な限界を取り去るために RDBMSを使うという観念を誤って捉えていた(実際には、数GB以上の、RAMのように揮発性であってはならない記憶域が必要になるというミッションクリティカルな事例でなければ、汎用のRDBMSは特殊にチューニングされたデータ構造に常に効率面で劣る)ので、どうしてもRDBMSを積極的に使いたいというこだわりがあり、またMS SQL Serverエンジンの性能を過信していたので、安易に他のopennap実装例に倣う気はしなかった。それに、素朴な形態素解析とハッシュ化を行い検索精度を犠牲にした場合と比べ、RDBMSの検索機能を使っていれば情報の取りこぼしがない100%の検索精度が得られる。また他の実装が日本語文字列に対する検索を念頭に置かず形態素解析を行っている点からしても、それら日本語環境では検索精度において劣ると考えられる実装を参考にすることには危惧感が あった。
そこでまずはMS SQL Serverに対するアクセス手法についてチューニングを図った。DICEは高速化のためにOLE/DBを用いてSQL Serverへネイティブ接続するが、ADOやMFCのDAOと異なり非同期問い合わせができないので、個別ユーザによる検索やディレクトリ閲覧についてサーバ全体へ影響が及ぶことを防ぐために自前で非同期問い合わせをできるようにした。C++的には、それ以前にModern C++ Designでファンクタクラスについて読んでいたのでそれを参考にWin32の機構をラッピングした非同期実行ファンクタを作って使った。さらに、複数テーブルへのデータ挿入/削除のSQLクエリを対象テーブル毎にまとめて直列化(同じデータの挿入と削除が同時に起こるような事態は避けなければならない)し一気に転送するためのバッファ機構も作った。また、テーブルのロックについても粒度を下げて負荷を下げるようにした。さらにストアドプロシージャも使用した。しかしこうしたデータベースへのアクセス手法の改善による性能向上は微々た る量に留まり、到底満足の行く代物ではなかった。
そこまで来ると、観念して別の面から攻めるしかない。この時点では、まだSQLのINSERTが問題なのではなくLIKEによる単純な全文検索が問題なのだと考えていたので、データベースの列に設定できるインデックスを用いた検索が出来ないかと考えた。インデックスを用いることができるようにクエリを構成すれば、テーブルのフルスキャンを行うよりは高速にレコードが取り出せるはずである。その為には、インデックス列に入れるための、元データを識別するためのデータを、元データから作り出す方法を考え出さなければならない。ここまで至ると、現下の問題は、「効率の良い日本語全文検索システムを作るにはどうすれば良いか」というより一般的な問題に帰着する。特殊な点は、opennapのシステムはセッションを多数同時にホストするサーバであり、多数のレコードの入力と出力の要求を、同時に、かつリアルタイムに処理できなければならないということである。従って、レコードのライブラリへの入力に時間がかかってはならず、またライブラリからの出力にも時間がかかってはならない。一見して分かるほどかなり無理のある要求である。
しかし、それまでのDICEへのIRC機能の組み込みは、単に仕様を実装するだけで、若干の追加機能の考案や仕様の再検討、効率的な実装の追及という取り組みはあったものの、手続き的に手順を踏んで進むわりと平坦な道であったのに対し、VDに関する問題は、アルゴリズムとデータ構造を工夫して望む解を速く叩き出すという、実にプログラミングに馴染む問題だった。IRCについての問題が単にWindowsやネットワークプログラミン グ、セキュリティモデルという、他者が構築した既成ブラックボックスシステムの周りをつつくだけの、悪く言えば「作業」であったのに対し、VDの問題はチャレンジングでやり甲斐がある創造的なものであるように思えた。その時点で、それまでのIRCに対する強い関心は褪せてしまい、VDの方へより真剣に取り組む意義が生まれた。また、この問題に関する既存の解答としてのopennap実装もあり、それがある程度の性能を出しているので、そこへ追いつくことを目標として新しいゲームを楽しむこともできる。IRCの場合、過去の無数の先人たちによる10年の努力の成果として現在の主要な実装があるのを知っているので、競争などという言葉はおいそれと口には出来ないが、高々2年かそこらの歴史しかないopennapに関してはそういうことはない。
データベースについての話題に戻ると、まずデータを識別するための情報をいかに作り出すかという問題がある。つまり、大量にある元のデータからなる空間を ブルートフォースで探索するのではなく、入力時にあらかじめインデックスを作成しておき、そこに元データへのポインタを添付しておくという場合の、イン デックスの作成法である。二分探索は、opennapの場合入力と出力が同時に起こりうることから毎回の自己組織化のコストが極めて高くつくために無理で、また部分文字列検索ができなくなるので採用できない。また、opennapの場合、元データが短いので、その点もポジティブな材料として考慮に入れる必要がある。つまり普通の全文検索システムなら圧縮率の高い手法を用いなければならないところ、幸いその必要はない。全然この分野についてはチェックしたことがなく、既存の日本語全文検索システムとしてはNamazuというものがあり形態素解析を使っているということくらいしか知らなかったので、参考資料を探してサーチエンジンで色々見て回ることになった。形態素解析については、opennapの既存の実装が大体それに近いことを行い、ハッ シュ化と組み合わせているが、単純な形態素解析の方式では日本語その他の非ASCIIのデータに対する検索に難があるため採用できない。また日本語マルチバイト文字列に合わせた拡張を行った形態素解析では日本語以外のマルチバイト文字列に対応できない。あくまで検索精度を100%に保ちつつ高速に検索を行うにはどうすればよ いか。
そこで、N-gramというものを知った。非常に単純な原理で、ある文字列に対して2-5文字程度の固定長の枠をあてがい、それを1バ イト(場合によっては1文字)ずつ終端に向かってずらしながら単語を抽出し、その集合をインデックスに用いるというものである。これならば言語に関係なく 検索に用いることができる。古い時代の文書の傾向をN-gramの生起頻度を元にして求めるなど、言語学の分野でも用いられているようである。難点はインデックスが大きくなりやすいことだが、そこは適切にハッシュ化を行ってやればよい。この点、リコーG-baseの全文検索についてのレポートが参考になった。ただ、ハッシュ化を経たN-gramの効果について半信半疑だったのでテスト用にアルゴリズムを作り日本語ファイル名のサンプルデータ群に対し検索を適用したところ英語の検索語の場合ノイズ発生率は40%ほどでやや大きくなったが検索語が日本語の場合は10%以下に留まり、予想よりかなり良 い成績だった。検索語が長くなればノイズ発生率は限りなく低くなる。
N-gramがどうやら使えそうだということがわかったので、元データから取り出した1つの3-gramとその出現位置とをデータベー スのテーブルの1行に収めるようにスキーマを書き、DICEにデータベースアクセスのためのメソッドを実装した。検索時にはそれらテーブルへの複数クエリの結果をJOINすることによって検索語に含まれる3-gramを検索語内での順序と同じ順序で含むデータが導き出される。ただ、1つの元データから64個の3-gramが取れたとすれば、登録時に64回もINSERTが行われてしまう。再三繰り返すが、この時点では検索の方が性能低下の原因だと誤信していたのである。この、インデックスと実データを両方ともデータベース内のテーブルに入れるというやり方をDICEに実装して一番最初のローカルでのテスト は悲惨なものとなった。一番最初のユーザがログインし、1000ファイルほどサーバへ登録を行おうとした時点で、SQL Serverが1GB以上の仮想メモリを使用し、システム自体がほとんどフリーズする状態になってしまったのである。1秒ほどの間に10万件近い数のレコードの挿入を試みたのだから無理もない。その時点で、SQL ServerないしMSDEを積極的に使用するという概念は消し飛んでしまった。
インデックスのみをメモリ上に置き実データをRDBMSへ格納するという方法では、検索は多少速くなるものの本質的な問題の解決にはならない。とすれば、インデックスも実データもオンメモリで持ち、RAMが足りなくなったときだけRDBMSへ実データをページアウトすべきだろう。つまり、高速なRAMを文字通りバッファとして用いるのである。OSの仮想メモリマネージャが行うように。この単純な事実を頑なに無視し続けていたのは完全に私の経験不足が原因であった。そして、RDBMSを中心に据えていたデザインをさっぱりと捨てることにした。DICEではディレクトリ名を除いたファイル名を128バイト以下に制限し、そこから2-gramを抽出し、256 * 256ビットの空間にマップしたものをハッシュ化して256ビット(32ビット * 8)へ圧縮し、ファイル1つについてのインデックスデータとしている。N-gramの位置データは無くてもさほどノイズが増えないというのを実測して確認 したので切り捨てることにした。実際の検索時には、検索語はせいぜい10文字以内なので検索語が含む2-gramの数は少なく疎な部分が多くなり、256 ビットのうち1つか2つの32ビットDWORD同士の論理積を取れば済む場合が多い。検索語が長い場合は2-gramの数は多いものの絞り込みが精密になりノイズが乗りにくくなるため損失は相殺される。そしてこのインデックスへの検索の結果に基づいて元データを引き出し、これをさらに検索語と照合してノイズを除去し、100%の検索精度を達成している。
当初は、128バイトの元データから2-gramを抽出した場合高々127種類の2-gramしか生起せずインデックス256ビットの うち半分は必ず疎になることを利用してインデックスのビットの立っていない部分をランレングス圧縮する機能を入れていたが、当然検索時の展開が必要で検索が遅くなるため、メモリを節約するよりは速度を優先して没にした。元データについては、まず短いデータで単位が細かく互いに独立しているために辞書圧縮などの手法が使えないこと、それから日本語を含む任意のバイナリデータを含ませるためにニブル化やガンマコーディングなどの手法が適用できないことを理由と して圧縮していない。当然速度面では圧縮しない方が有利である。また、最初は、RAMが足りなくなるとRDBMSへディレクトリをページアウトする機構も 入れていたが、オーバーヘッドを無くすために結局没にしてしまった。つまりディレクトリは全てオンメモリに展開される。
当初堅持していたRDBMS重視の方針を完全に撤回しメモリ利用に走ってしまったので、転向したからには性能を追及しようと考え、100万件のレコードで250MB程度のメモリを占有し、1秒以内に検索が完了するという状態をVDの目標とした。この1秒というのはユーザが検索してすぐに結果が返ってくるという体感の指標として考えたもので、1秒以下ならば人間があまり感知しないだろうと推測して設定した。実際には多数のユーザが同時にクエリを発行するため、個々の検索が消費するサーバ内部での時間は1秒よりはるかに短い時間でなければならない。ただ、SQL Serverエンジンを用いた初期のVD実装の改良版での試験ではサーバへの負荷が軽い場合は3秒程度で結果が返っていたので、ブルートフォースで検索していたことを考えるとSQL Serverもなかなか大した物ではある。いずれにせよ、今回のリリースバージョンでは概ね目標を達成できた。並列実行性も大幅に向上しているので、マルチプロセッサ環境やHyper-Threading環境ならほぼリニアな応答性向上が見込める。
並列実行性を高めるということは、それに馴染むアルゴリズムを考案するということの他に、多数のスレッドの並列実行によるデータの破壊やデッドロックを防ぐという配慮も必要とする。つまり、適切に同期を行うべきポイントを見極めてそれを最小限に留め、かつ必要以上に少なくし過ぎないようにしなければならない。IRCチャンネルのようなコレクションについてユーザ毎のイメージ/ビューの整合性を維持するために同期を行う必要がある(Observerパターン)のは言うに及ばず、クライアントが接続を切断したときのセッション毎のリソースの解放と初期化は予想できないタイミングで起 こり、かつ資源の再利用を行っているために、特別の注意を要する。DICE v. 0.1の1年あまりの歴史のうち、数ヶ月がこの問題から生ずる不可解な現象を除去するためのデバッグで過ぎ去っていった。当初はチャンネルに関する同期の問題を片づければ十分だと思っていたところが、実に多種多様な問題が生起し、それらに全て対処する羽目になった。これに関しては、稿を改めて述べて みたい。
DALnetなどの10万人規模の大規模IRCネットワークに参加しているUnixのIRCサーバならば、他のサーバとのリンクを維持しながら、4,000から多くて10,000程度の同時接続をさばいている。DICEでこれまでに得られた運用データでは、IRCクライアントより遙かに多量(少なく見積もっても30倍以上)の帯域を消費するopennapクライアントを4,000個以上1台のマシン上で問題なくホストできたので、IRCクライアントのみをホストする場合は、カーネルメモリさえ十分に確保できれば同じ設備で10,000程度の同時接続を維持可能であると推測できる。
まず前回の要約をしておこう。前回は、IRCプロトコルの現状と、Unix/Windowsにおけるサーバの実装を概観した。そこでUnix用あるいはクロスプラットフォームではなくWindows 2000以降のWin32に特化したサーバを作ることを決定し、現在のIRCを補完するユーザ認証機構や十分なセキュリティを備えた管理サービスを、IRCとは別のチャンネルとしてサーバに盛り込むことにした。ロードマップとしては、時間的制約から、最初に単体サーバとしてある程度実用性のあるものを作り、その後にクラスタリングについてデザインと実装を行うことにした。ロードマップの第一段階の最初にWindows 2000以降に特化したスケーラブルなネットワークサーバフレームワークを作り、その上にIRCクライアントとの対話機能を載せ、さらに認証情報やユーザ/チャンネルの永続性を担保するためのRDBMSへのアクセス機能を加えた。さらに、ベータテストへ進む寸前に、フレームワークの再利用性を試すためにopennapサーバとしての機能も実装した。
以上がこれまでにDICEについて書いたことである。クライアントとの接続を保持するステートフルセッションコンテナと、エンティティを格納するRDBMSへのコネクションプールを備えたので、任意のアプリケーションコンポーネントを動的にサポートできればJ2EEなどのアプリケーションサーバ風の構成になるはずである。DICEはWindowsサービスアプリケーション(かつてNTサービスと呼ばれていたもの)として、ATLサービスモ ジュールを利用しC++により実装されている。Windowsでは内部に複数のサービスを持つ1つの実行ファイルを作ることも可能だが、ATLではサポー トされていないので、DICEは一つのWindowsサービスで複数のネットワークプロトコルを提供するという形態を取っている。DICE内で個々のプロトコルを扱うサーバ機能を表象するアプリケーションロジックは、C++テンプレートを静的に特殊化するための具象クラスとして実装されている。DICEの絶対目標の一つにパフォーマンス重視という事項があるため、この目的からすれば当然静的結合の方が有利である。パフォーマンス重視だからこそ特定のプラットフォームに向けて(さらにWin9X/NT4との互換性を切り捨てて)開発したのでありその点に関しては一点の迷いもない。
Windows 2000から追加されたAPIをクリティカルな部分で用いた結果、内部の状態遷移もその挙動に深く依存している。従って、Unixへの移植性は今のところ 無いに等しい。ホットスポットにはインラインアセンブラによる最適化も加えた。アプリケーションの性質上大きなメモリブロックを確実にまとめて処理できる 部位が存在せずさほど効果は期待できないが、MMXレジスタも一部で使用した。SSEはAMDの場合AthlonXPからのサポートなので使用していない。デスクトップアプリケーションと異なり数千の接続を抱えるためポインタの参照すら累積コストの観点から節約すべきという状況では、CPUの種類に応じ てルーチンを切り替えるような構造は好ましくない。MMXならば現在出回っているCPUの全てが備えているので、最大公約数として必要スペックに組み入れることにした。
PC-Unix上のアプリケーションについて、OSが軽いので古いハードウェアを再利用できるということがしばしば言われる。それと対称をなすように、DICEはハイエンド化するWindows PCの能力を存分に引き出すことを意図して作った。2GHzのCPUがハイエンドでもないPCに載る時代である。大きなネットワークを構成する時間も能力もない個人でも、中の上くらいのスペックのデスクトップマシンが一台とブロードバンド回線があれば相当に高性能のサーバが運用できるのではないか。また巷に溢れるWindowsサーバ批判の多くがWindows 2000について論じる物ではなく"NT"を対象にした古い物ばかりだったので、Windows 2000で導入された新技術を学習の傍ら用いながらWindowsのサーバとしての性能の検証に乗り出そうと考えた。それ以前にはLinuxや FreeBSD上でのプログラミングもよく行っていたものの、瑣末な作業に時間を取られるのに疲れある時点で飽きてしまったので、労力の節約のために Windows 2000以降のWindowsでのプログラミングに取り組む動機はあった。思うにCUIがUnix一般で好まれるのはそれが汎用かつ万能のインターフェイ スだからである。つまり、一般的なGUIのようにコンポーネントとそれに関連づけられたアクションの数が限定されているのではなく、CUIではシェルでプ ロンプトに続いて文字列を打ち込めばそこからありとあらゆるアクションを起こすことができる。しかし「できる」という、可能性があるだけなのだ。
またプログラミングについて言えば、Win32のAPIは汚いとか仕様が頻繁に変更されるとかという声はあるが、私は必ずしもそうした非難が的を得ているとは思わない。特に新しいAPIについては明かな指向性が見て取れる。何より私は新しい物好きである。Windows 2000以降に追加されたWin32のAPIは興味深い物を多数含んでいる。ちょうど「闘うプログラマー」(原題: Show-Stopper!)を 読んだ後だったので、WindowsNTという、既存のUnix系OSに対抗するために戦略的に造られた新しいOSに興味を惹かれていた。Linux は、"Just for fun"というLinus Torbaldsの言によれば、娯楽(とMinixでは得られない実用性)のために造られたという。対してWindowsNTは、サーバ市場に向けての戦争の道具として造られた純然たる戦争機械である。それに、P-threadによるプログラミングに対し、Windowsの同期プログラミングは単純明快に見えたことも大きい。しかしながらWindows 2000がデスクトップでの作業でそれなりに安定しているといっても何千ものクライアントが接続してきたときに果たして実用性を保てるものだろうか。そういうわけでDICEは私の好奇心そのものである。
わざわざ強調するまでもないがPCの台頭によってサーバアプリケーションをめぐる状況は変化した。IRCについていえば、かつては地理的に近隣に位置する サーバに回線が弱いクライアントが接続し、上位で太い回線を備えたサーバ同士のリレーにより規模拡大が行われた。資源の絶対的欠乏が与件でありそれを切り抜けるべくなされる効率的資源配分が問題解決の主眼だったのである。現在でも大きなIRCネットワークに属するサーバではクライアントの国別ドメインに よって接続の可否を決定するという原始的な資源配分が行われている。それが近年は周知のように一般ユーザが通常使用には余る太い回線を獲得した。ネットワークゲームのようにping値が重要になるほどレイテンシを抑えなければならない環境を除けば、就中IRCのように単純なコミュニケーション形式においては、大域的な資源配分はほとんど問題とならなくなった。問題があるとすればせいぜいNimdaのようなワームが拡散時に感染ルートを最適化する場合に考慮されるくらいである。
近年Microsoftが強調しているマーケティングのための惹句(buzzword)にexperienceなるものがある。ではIRCがIRCであるために必須のexperienceとは何だろうか。そうした問いの中で、サーバ同士がリンクされデータをリレーするというIRCの'R'である属性は、ユーザのexperienceからは最大限隠蔽されるべきであるという命題が浮かび上がるはずだ。その隠蔽の努力を破るのが、文字通りnetsplitとして知られるIRCサーバ同士の接続断裂状態である。netsplit が起こるとそれまでアトミックであったはずのチャンネルという単位が上位層の分裂により分裂を余儀なくされてしまう。Windows用IRCクライアントとして独占的なシェアを持つmIRCは複数IRCネットワークへの同時接続に最近になって対応した。また、opennapサーバへ接続するクライアント機能を持つWinMXは、複数のopennapネットワークへ同時に接続しコマンドを発行できる。
opennapプロトコルもまた、IRC同様に、代表的な実装(opennap/slavanap)は、複数のサーバをリンクすることにより多数のユーザをホストしネットワーク全体のスケーラビリティを向上させようとしており、またIRCに非常に似通ったチャット機能を提供し、netsplitも存在する。しかし、一つのクライアントが複数のサーバに接続できるということの含意は、もはやクライアントがサーバ資源の浪費をモラルに従い厳しく自制する時代は終わりつつあるということである。そして、上記のIRC特有のexperienceとは何かという問いへの回答を試みるならば、それは第一に「1つのチャンネルで同時に多数の人間が文字で会話できること」(collaboration)である。第二に、「チャンネルのオペレータ権限が守られること」(authorization)である。勿論、複数のサーバに別々に接続したユーザが一つの空間を共有できるというIRCの特性は、インターネットの特性と同じく、あるサーバが攻撃に屈しても他 のサーバによってシステムは生き延びるというリスク分散の観点からは評価できる。しかし個々のユーザにとっては、自分が接続しているサーバがダウンすればその時点で放り出されることには変わりはない。
これに対し、サーバ間での会話メッセージのリレーを行わず、特定のチャンネルが特定のサーバへ1対1にマッピングされる状況で全てのクライアントがあらかじめ予備サーバについての知識を持っていれば、異常時には直ちに予備サーバへ移行してチャンネルでの会話を続行できるだろう。背後でのシームレスな責任移譲が重要である。例えばMicrosoftのDirectPlayのP2Pモードでは、64人程度の小規模ネットワークながら、ホストを動的に移行できる。サーバ設営側にとっては、一つの名の下にIRCネットワークを統括し多くのチャンネルを抱えることは重要な関心事であるかも知れない。ところが一般のユーザにとっては、IRCとはとどのつまりチャンネル本位の世界なのである。ただし一般ユーザとサーバ管理者の中間に位置するチャンネ ルオペレータにとっては、大きなネットワークへ参加することにより同じネットワークの他チャンネルから客が来るかも知れないという期待もあることだろう。しかしその期待は、IRCというプロトコル(とその主要な実装例)の欠陥から生ずる副作用に過ぎない。サーバとサーバの垣根がチャンネル間の距離と等しくなれば、大きなネットワークという概念も崩壊するはずである。サーバへの接続という作業さえクライアントの目から隠蔽され、異なるサーバへ接続することと異なるチャンネルへ接続することの境界が無くなれば、リレーなど行う必要性はない。チャンネルをリストするためのディレクトリとして情報の一貫性/検索可能性を維持するために巨大なIRCネットワークが必要だとするのは、資源の無駄であり本末転倒であると考える。ディレクトリ機能と認証機能とが独立したサービスプロバイダとして外部に存在していればよいのであり、会話データのリレーはこのシステムにとって必ずしも本質的ではない。
このテーマについて自問を繰り返すうちに、純粋にロードバランシングのためにクラスタリングを行うこと(たとえば一つのマシンが扱える TCP/IP同時接続数には限界がある)と、IRCのように、分散化が本質的な機能面をも浸食している状態を許容することとの価値のずれに強く違和感を覚 えるようになった。このずれは、前者のロードバランシングで得られた効果を後者の誤ったネットワーク設計による損失で相殺しているという不幸な状況なのではないか。そうした状況に陥りかねない破綻したシステムにコミットすることに何らかの利益はあるのだろうか。IRCは現在のようにインターネットが家庭にまで届く随分前に考案された物であるにも関わらずその根本的部分について改定が加えられていない。純粋に政治的な問題から、互換性を損なう変更ができな かったのである。IRCプロトコルを拡張してクライアントにほんの少しコードを付け加えるだけで上記のシステムは実現できる。しかし実際には私の手元にクライアントのコード(とユーザベース)はない。前回詳しく書いたが新プロトコル(いわゆるIRC3)への移行は計画ないしプロトコルそのものが具体化する以前の段階で無惨な失敗に終わっている。改良されたIRC2が当座の利用には必要十分だったからである。
こうした状況は二律背反にほかならない。既存の、ある程度分散化を達成しているIRCdの実装をコピーすることなら楽だろう。しかしそれは最初から望んでいない道である。IRCプロトコルそのものの病弊は指摘されて久しく私も十分そのことについて学習した。 そこで私は発想の転換を体験することになった。DICEについては、まず単体のサーバとして限界まで性能を突き詰めれば良いではないか、と。分散化云々については棚上げし、局所的パフォーマンスの最大化に努めればよい。そしてIRCプロトコルの実装は、単に既存の良くできたクライアントアプリケーションからDICEというものへ接続する為のインターフェイスとしてDICEが用意するものの1つに格下げされ、最早DICEの目的ではなくなった。IRCプロトコル は、MVCで言えば、DICEのモデルを表現する多数のビューを構築する形式、ストラテジの一つとして利用され、またそれを通して既存のクライアントアプ リケーション資産をDICEが利用できる。
限られた時間と人的資源の中では問題を切り分けなければいつまで経ってもろくな物が出来ないのは目に見えていた。例えばLinuxカーネルが一人の人物によって短期で実用レベルに至り周辺が急速に成長したのに対し、同じオープンなプロジェクトでも、GNU/Hurdのシステムがいつまで経っても実用に達しないのには理由があるはずである。オープンなプロジェクトですらそうなのだから、DICEは私が自己責任で造ってい るということからしてさらに慎重を期しつつ、かつ大胆に不要部分を切り捨てるべきであろう。当初考えていたbetter IRCという目標と比べると相当急な視野の転回ではあったものの、それまで頭を悩ませていたDICEのアイデンティティに関して、ようやく道が開けたような気がした。こうしたことに思い至ったのはDICEにopennap準拠サービス(VirtualDirectory)を組み込んでからかなり後のことであり、それまでは何となくIRCの実装後に単に可能だからという理由で深く考えずにVirtualDirectoryを実装してみたものの今ひとつ意義を見いだしにくい状況だった。
実装については、DICEは、プロトコルとは関係なく汎用ネットワークサーバとして、Apache/IIS/各種J2EEアプリケーションサーバなどを念頭に置きつつ設計したため、全く問題なく対応できる。 当初はopennapはディレクトリの公開がその本質的機能なのだからという原理的理由に基づく効率至上主義に徹し、チャット機能の実装は行わないつ もりだった。そこから180度方針転換し、IRCクライアントとopennapクライアントとが同時に1つのチャンネルに参加して会話できるようにした。ただ し、IRCクライアントの方がチャットに関しては高機能なので、チャンネル管理/作成などの権限は専らIRCクライアントが担うようにデザインした。ま た、メッセージ変換用ローカルプロクシサーバの利用をユーザに強いるわけにはいかず、あるいはDICEを機能毎のプロセスへ分割するつもりもなかったの で、サーバサイドでIRCメッセージとopennapメッセージの相互変換を行う必要がある。さらに、UNIXの世界から生じた日本語IRCでは一般的に JISコードが用いられ、Windowsでの利用が中心であるopennapクライアントでは日本語にシフトJISコードが用いられるので、日本語圏で使用するためにはその変換までサーバサイドで負担しなければならない。これを実現するための障害は技術的なものではなくサーバを非効率にしたくないという私自身の精神的な抵抗だった。結局私は当初の自分の信念を曲げてこれを実装したが、結果的には良かったと思っている。
最もフラストレーションが溜まったのは、IRCプロトコルが明白な欠陥を多く含む不完全なものであり、既存のクライアントへの対応を心がける限りその不完全性に追従しなければならない点である。Microsoftは同じ状況でIRC-XプロトコルをMS-chatとExchangeのために作り実用化した (後にその経験をMSNメッセンジャー製作に生かしたと思われる)が、一個人である私にそれを行う力は無い。特に、優秀で、多くの人が使っているクライア ントアプリケーション(たとえばmIRC)が先に存在する状況では、クライアントアプリケーションの絶大な影響力を無視してはサーバは成り立たない。サー バ製作者が勝手に何か新しいことを始めるわけにはいかない、相手の顔色を伺いながら事を進めなければならないという面倒な世界なのだ。デスクトップアプリケーションのように単純に発想を形ある物にしてやればいいという訳ではなく、相互運用性を高めることが非常に重要である。折角楽しむために趣味でソフトウェアを書いているのに、同時に自分の無力さに耐えながら制約下で作業しなければならないというのも因果な話である。当然ながら作業続行のためにはきっと 良い物が出来上がるに違いないという信念と楽観主義が必要となる。
その点、サーバとクライアントが一致しているpure P2Pアプリケーションではそこを意識せず全て自己の管理下に含めることができると思われる。作業量は倍加するに違いないが、その自由に羨望を禁じ得ない。DICEに次のステップがあるとすればそこにしか存在しないだろう。ただし現状でも完全にIRCプロトコルのRFCに追従するのではなく、ささやかな反逆として一般的なクライアントが理解できる範囲での様々な改定を加えている。唯一救いがあるのは、IRCプロトコルはバイナリプロトコルではなくASCII文字列をコマンドとして使うためコマンドの拡張が容易な点である。telnetでもできるIRCの、UnixのCUI/テクストファイル重視の文化との接点は、ここにある。XMLもこの流れだろう。DICEではIRCユーザ用のシェルを操作するためのコマンドとしてshellという命令を付け加えており、一般のクライアントからは"/shell"とタイプすることによってこのコマンドをサーバへ送信できる(この点opennapメッセージはバイナリプロトコルなので効率的な反面融通が利かない)。
こうした改定は、mIRCで問題なく動作することを念頭に置いて加えている。こう書くと誤解を生みやすいが、特定クライアントのみが備えている振る舞いを当てにして開発しているという意味では勿論無い。RFCに記述されているログイン時のハンドシェイクの手順、チャットに使用する本質的なコマンドなどは全 てRFCに従って実装されている。また、特定リプライの解釈が曖昧になる(つまりクライアントがチャンネルの状況を誤って把握する可能性を生む)ような変 更は一切加えていない。前回書 いたように、IRCに関して現在出されているRFCは「腐ってもRFC」なので一応は準拠している(ただし本当に腐っている部分は積極的に排除されてい る)。それは、現在のデファクトスタンダードとなっている3大ネットワーク(EFnet/DALnet/Undernet)でそれぞれ使用されているサーバソフトウェア(それぞれIRCnetのIRCdから派生しほとんど全て書き直されている)も同様である。
残念ながら日本はIRCが盛んではなく世界の他の部分から隔離されているので、IRCに関する状況は他より遅れている。mIRCをリ ファレンスとしてDICEにIRC機能を実装したのは、mIRCがRFCの最も重要な部分を正しく実装し、かつ例外に対する耐性を備えているからである。IRCに関するRFCの最重要部分は、私見では、個々のコマンドやリプライコードの列挙にあるのではなく、IRCメッセージの仕様を規定したところにある。つまりそれに従ってパーザを作ればサーバが送ったあらゆるメッセージについて(その意味の解釈についてはともかくとして)サーバが意図した形で受け取れる。RFCがBNFによって規定しているIRCのメッセージは、opcodeと0-15個のoperandから成る、RPCに使えそうな形式である。ところが、このパーザの部分すら正しく実装せずメッセージの種類ごとにいわば「決め打ち」している日本製のクライアントがある。当然決め打ちの方がプログラミングが容易でありかつパフォーマンスも高くなるが、後々のことを考えているとは言い難いやり方である。しかもそのクライアントは開発が停止したも同然 なので、このクライアントの側の不具合に対応するために、DICEが譲歩して補助的な情報を加えてやらなければならなかった。他にも、RFCを提出したIRCnetのサーバである"IRCd"(IRCdとは一般名称ではなく特定ソフトウェアの名称である)についてのみ対応したために、結果的に決め打ちないし場当たり的な実装を行っているクライアントがあった。そして、世界的に見れば、この"IRCd"を使用しているネットワークは今となっては少数であ る。
Winsock Programmer's FAQによるとWindows NT4での接続ソケット維持数の限界は1万から2万程度とされている。スケーラビリティを意識した適切な非同期I/O方式(すなわちI/O completion ports - いわゆる「I/O完了ポート」、この訳語が嫌いなので以降では原語で統一する)と、NT4ではなくNT5を使用しなおかつNT4の時代のものよりハイスペックなマシン上で運用すればさらに制限を緩和できるに違いない。DICEはそこを狙っている。IRCサーバの活動中で最も回線の帯域を消費するのはチャ ンネルリストの送信だが(IRCプロトコルにはデータ圧縮方式は規定されていないので参加者が10万人を超えるネットワークではクライアントの要請に応じて送信されるリストは1本が2-3MBに達する)、それを除けば大した帯域を消費するわけではない。そしてDICEにおいては、IRCというプロトコルに関する限り、1万人を超える参加者を擁するネットワークを建てようという目論見とその実現の見込みがない限り、私に分散化のためのコードを書くメリットは何もない。
さらにopennapの場合、サーバ間リンクは絶対に実装しないだろう。opennapでのファイルのやりとりについては1対1のコ ミュニケーションで必要十分であるためIRCのようにチャンネル内での発言の先後の調整を行う必要がなくリンクの実装はたやすい。ただ恐ろしく非効率である。検索結果をサーバ間で受け渡しすることはまだ受忍できる。しかし個別ユーザのディレクトリの閲覧結果までをサーバ間で受け渡すというのは、胸が痛むほどに非効率である。ピア間で直接接続を樹立してリストを受け渡しするコマンドもopennapプロトコルには規定されているが、ユーザセキュリティ上の問題がある。もちろんサーバ間リンクを行いopennapネットワークを拡張することによって生まれるスケールメリットはある。それは、多数の検索結果が得られることではない。WinMXのように複数のサーバに接続できるクライアントが存在する以上ユーザにとって個別ネットワークにおける検索結果数は大した問題ではない。比較的大きなopennapネットワークがユーザに提示できるメリットとは、とどのつまり、Crowdsのように、そのネットワーク内で個々のユーザのプライバシー(実際にはopennapはピア間の責任を曖昧にしないので、opennapについてはプライバシーという言葉は意図的にCrowdsの保障するプライバシーと異なる意味で誤用していることも注記しておく)が確率的に向上する可能性があるという点に存するのであってそれ以上ではない。
ビジネス上の観点からすれば、スケーラビリティを極限まで高めることもサーバアプリケーションの重要な目的ではある。しかしそれと並び、DICEでは、サービスを受けるユーザの視点から、各ユーザがつつがなくexperienceを享受できる環境を作ることを徹底したつもりである。また、サーバ全体の資源を節約するために公共性に配慮しつつ機能を設計しなければならない。従って、リンクを行わず単体のサーバで必要十分のサービスを提供できるならば、当座の目的は達成される。サーバとしての能力を推し量るためには、クライアントユーザから見た総合的な利便性の定義についても考慮すべきである。
DICEが重んじる価値を端的に挙げるならば、まず第一にデータを送受信するネットワークの端点としてのパフォーマンスである。理由は再三これまで述べたとおりだ。Windows XPがデスクトップに入り始めた近年にあっては、DICEのようなアプリケーションを個人で実稼働させることもたやすく、そこで最大限のパフォーマンスを 引き出すことを目標としている。つまり、大規模なサーバマシン上で動かす場合もさることながら、デスクトップでP2Pのノードとしても十分に働きうるネッ トワーク性能の達成である。
そして第二に、クライアントユーザの利便性が重要である。これも理由は既に大半が述べられているが、副次的な指針としては、IRCサーバのようにある種の閉鎖的な空間を作り出す性質のあるサーバアプリケーションではしばしば管理側の望む過多な機能がユーザの利便性を損なっているという事例を目にしたので、敢えてクライアント側の視点を明確にすることにこだわった。デフォルト設定の変更により様々な状況に対応すると言うよりは、デフォルトを最善の状態に保つことにより他に資源を振り向けられるようにしたつもりである。最善というからには、当然そこには私自身の価値観が色濃く反映されてい る。より具体的には、必要なセキュリティの確保は厳格に行うもののその他の点では出来る限りリベラルな環境が保たれるように心がけた。この方針は特に VirtualDirectoryにおいて顕著である。
これと関連する第三の価値が、シンプルさである。使用頻度の低い機能の付加は実行時のオーバーヘッドを高めるので望ましくない。そして、ある成果を、必要な最小の要素のみによって構築することは、美的観点からも好ましい。デスクトップアプリケーションではないので不要な機能は削ぎ落と し、アップタイムを延ばすためにワーキングセットを小さくすることに努めた。DICEにはサーバが担当しなければ実現できないもの(特に認証や最低限のセ キュリティに関する設備)のみを含め、それ以上の、チャンネルやユーザなどのコンポーネントに特有なデータの保持をユーザが望む場合は、ユーザ自身で自前の資源をボットプロセス/サービスとして用意すべきである。
第四に、管理の容易さを達成しなければならない。これは、第一義的には管理に要する人員の削減によって実現すべきであると考え、それなりの接続数を擁するサーバでも1人か2人で運用できるように考慮した。管理業務に専門知識や多数の人員という資源を要するシステムは、他の部分がどれだけ優れていても、それが管理コストを多く消費するという一点のみを原因として総合的に劣る可能性があるというのが私の持論である(Microsoftが Linuxを攻撃する時も同様の論拠によるに違いない)。また、Windows NT系はアプリケーションサーバとしての側面も強く持っており、GUIのMMCからパフォーマンスモニタリングができるなど管理ツールが充実しているの で、Windowsそのものがユーザに公開している機能はDICEの方で敢えて実装する必要もない。ただし、ユーザが交換するメッセージの内容について、セマンティックな面で検閲や取り締まりを行うことは現在の計算機では不可能なので(せいぜい特定文字列に反応しフィルタすることしかできない)、この面でのチェックを行うならば人員に頼るしかない(個人的には推奨しない)。そして、2人のユーザが1対1 で交換するメッセージや、VirtualDirectoryに対する検索クエリについて、プライバシー保護の観点から、DICEは一切のモニタ手段を提供 しない。
以上4つのDICEのデザイン上の価値判断は先天的に存在しているのでこれに反する価値を追及する場合はDICEを使用するべきではない。
ところで、オペレーティングシステムのカーネルについて評価する場合には、小さなカーネルを中心に置き機能毎に分離されたモジュールが通信しあうマイクロカーネルモデルと、カーネルに多くの機能を含むモノリシックモデルの優劣についての議論が大抵持ち込まれる。一般的見解では前者は、分散化を視野に置いたより洗練されているモデルでシステムの堅牢性を高める上で有効であり、WindowsNTカーネルや、MacOS Xが採用するMachカーネルに用いられている。しかし各サービス間の通信/調整コストがかさむためパフォーマンスを確保しにくい。開発も困難である。そこでGNU/LinuxやBSDはモノリシックカーネルを採用し、実績を上げている。IRCサーバも似た状況で、カーネルコアをIRCサーバに見立てれば、近年のIRCネットワークのほとんどはユーザ認証やチャンネル登録、プロクシサーバスキャンなどを行うサービスを中央のIRCサーバ群とは別のプロセスとして、場合によっては別のマシンで稼働させている。つまり資源を有効に分配しリスクを分散するという観点からのマイクロカーネルモデルが一般的である。
IRCプロトコルについて調べ始めた頃に、RFCの中でIRCサーバとサービスとの通信に言及している部分を見つけ、それが何らかのバイナリプロトコルではなくIRCプロトコルを用いて行われることになっているのに驚いたことがある(IRCメッセージはペイロード部分以外のヘッダ部分は ASCII文字列であり、ペイロードを改行記号で終えるストリームである)。つまり各自動プロセスがIRCサーバへログインし、IRCサーバから授権されたIRCユーザとして特殊業務を一般のIRCユーザ相手に提供している。DICEは、そうした機能毎のプロセス分散は行わず、OSカーネル方式のアナロ ジーを使うならば、モノリシックな構成を採用している。
各機能は、内部ではC++のクラスとしてソースレベルで分割され、OSスレッドレベルで相互に作用し合う。ただしスレッドはプールに格 納されているので、ワーカスレッドを仕事に応じて作る単純な場合と異なり、厳密な機能毎の対応があるわけではない。この場合全てカーネルモードで実行されているようなもので、どこか1点でも欠陥が有ればサーバ全体が陥落してしまう。しかし、一台のマシン上でのパフォーマンスでは優位に立つはずである。しかし一方で、完全にマルチスレッド化されているために、CPUが1個しかないシステムではシングルスレッドの擬似マルチタスク プログラムにパフォーマンスが劣ってしまうかもしれない。その点DICEではCPUの数に応じてI/O completion portsとOSの制御下のWin32 thread poolが適切な数にスレッドの増加を抑えコンテクストスイッチのオーバーヘッドを最小限に留め、スレッド間の同期に費やされるコストを補ってくれる。そ して、2-way、4-wayでスレッドを並列動作させられるマルチプロセッサ環境ならば、絶対的な性能向上が約束されている(ただしメモリなどの資源に つき最大限競合を避けるようプログラミングにおいて留意すべきである)。
また、既に書き始めてから1年経ってしまったので判断しにくいが、なるべく早く造るということにも挑戦したのでオブジェクト指向は必須だった。ただし、最初は分散化を指向してオブジェクト間の結合を緩くしていたのが、モノリシック構造での局所的パフォーマンスの最大化を目指すようになると、非常にタイトに組み合わされるようになり、より低レベルな、生成されるコードを意識したコードへ変遷していった。その点C++の柔軟な構造が非常に助けになった。最初は STLを多量に用いていたのが、似たインターフェイスを持つ自前のクラスへ後から置き換えられていった。単にモノリシックなデザインを考えそのパフォーマンスを第一に考える場合、巨大な「サーバ」というクラスないし空間が一つグローバルにシングルトンとして存在するようなものになりそうだが(C言語で書くと実際そういうものになる)、DICEについてはクラスは概ねIRCを構成するコンポーネント/アクターを 表すドメインとして、例えばセッション/チャンネルクラスとそれぞれのファクトリクラス、ファクトリを管理するサーバクラスといった具合に切り分けられ、可読性を高めると同時に、オブジェクト間の通信コストをせいぜいポインタを1個参照する程度に留めることができる。
パフォーマンスを高めるために、C++の仮想関数によるポリモーフィズムや例外処理機構は特殊な場合(COMコンポーネント使用時な ど)を除き一切使用していない。たとえば、オブジェクト指向でよくあるパターンでは、DICEは複数のプロトコルをサポートするので、サーバ上の全てのユーザに対して同じ内容のメッセージを送りたいといった場合に、異なるプロトコルのセッションを横断して一つのメソッドを適用したいという誘惑から、イン ターフェイスを定義してそこから各特殊クラスを導出し、実行時に全ユーザのコレクションにポリモーフィックなメソッドを実行させるということを考えつくかもしれない。そうしたことはDICEでは行わず、C++テンプレートによる静的なバインディングに留めている。
こうしたトップレベルでの設計方針については、なによりopennapサーバとしての機能をSQL Server/MSDEをディレクトリストアとして用いて一週間で実装できたことから判断して、間違っていなかったと思っている。ただし、その後でopennapサーバ機能の実用性を高めるための改善に大変な苦労を要し、結果的に最初の実装を没にする羽目になったので、この特定問題の解決に用いた手段の選択には失敗したと言わざるを得ない。また、中間層では上記のようにオブジェクト指向的なモデルの当てはめが容易だが、低レベルでは非同期I/Oを使用している関係上オブジェクト指向に馴染まないプログラミングが必要になるため、その部分を可能な限り包み隠して下層に押し込めなければならなかった。
低レベル部分で最も苦労したのが、管理サービスの通信部分のプログラミングだった。DICEの管理サービスは、SSL上の独自プロトコルによって管理用 GUIクライアント(DICEAdminShell)と通信を行う。MicrosoftはSSPIという暗号化通信のための抽象化されたインターフェイスを提供しており、ネットワークなどを通してやりとりするデータをNTLM/Kerberos/SSLにより保護できる。 WindowsCEのWinsockではオプションとしてSSL通信が実装されているが、素のWinsock2にはそうした機能は無いので外部ライブラリを使用しない限りSSPIに頼らなければならない。とはいえ最初からSSLを使うと決めていたわけではなかった。要は、IRCのように平文で通信を行うのではなく、インターネットを越えて安全に通信が行える公開鍵暗号を使った通信チャンネルがあればよい。そこでNTLMとKerberosを検討した。しかし私はそれらがWindowsドメインやActiveDirectory内でのアカウントを要求するという点を資料から読みとれず、完全に実装を済ませてローカルでのテストに成功し、インターネット越しに通信を行い失敗した段階で初めて、それらがアカウント情報を同時に送信する必要があることに気付いたのである。最初にKerberosでの通信機能を完全に実装して失敗したので次にドメイン外からも通信できるというNTLMを使って再度失敗してようやくそのことを理解し、なしくずし的にSSL の使用を決めた。
ただしSSLには欠点がある。今回はクライアント認証は必要ないのでクライアントが証明書を備える必要は無いにせよ、サーバ側で設置の際に証明書をインストールする必要がある。また、SSPI上では、NTLMとKerberosは暗号化メッセージがブロック(厳密には、プログラム作成者がサイズ情報を先頭に付け加えることでブロックとして扱えるようにになる)なので状態遷移が比較的単純で、プログラムもほぼ同一になるが、SSLはストリームであるために、同じSSPI上でも全く異なる方式を要する。そしてDICEについては、あらゆるI/Oについて例外なくI/O completion portsを用いることが決定事項だったので、同期I/Oを使って書かれているSSPIによるSSLのサンプルと大きく異なるものを書き上げなければならない。I/O completion portsを用いて書かれたプログラムは、制御のフローがスレッドをまたがって末尾再帰的に進むものになるため状態遷移が非常に複雑になり、バッファも ローカルスタックに割り付けられず管理が難しい。その上にSSPIという既成インターフェイスによるSSLストリームの解釈の各ステージを乗せていくという作業は苦痛以外の何物でもなかった。
DICEAdminShellは単にこの通信クラスのGUIラッパーであり、こちらの通信部分もI/O completion portsで実装され、DICE内部の管理サービスと対称を成している。最初にサーバ/クライアントの通信クラスのペアを同じ手法を用いて作り、コンソー ルで実験してから、DICEとDICEAdminShellの双方に組み込んだ。DICEAdminShellのGUI部分はMFCで適当に作ったので見栄えは悪いものの数日で済んだが、途中でKerberos/NTLMバージョンを没にせざるをえなかった影響で通信部分は完成に1か月もかかってしまった。実装も極めて複雑になり、DICEAdminShell関係のモジュールは再度手を入れるのが億劫な、DICEの最も醜い部分である。そういうわけで当初のデザインでは管理関係のコマンドの大半はDICEAdminShellに入るはずだったのが、結局はセキュリティの低いIRCのavatarへ相当量が移譲されることになった。
手間を省けるという期待からSSPIというブラックボックスを使用したが完全に逆効果だったので、SSHのようなものを適当な公開鍵暗号の実装を用いて自作するべきだった。もし次があるとすればこの部分はそっくり全体が差し替えられる可能性が大きい。ただ、DICEAdminShellに関しては、SSL以外の他の部分はサーバもクライアントもプロトコルも自作なので、自由ではあった。DICEAdminShellはUnicodeでビルドされ、またDICE内で他のDICEAdminShellユーザとUnicode(UTF-16)でチャットを行える。DICE自身もWindows NT系の標準であるところのUnicodeでビルドされているが、IRCメッセージやopennapメッセージは単なる8ビットバイナリなので、Unicodeらしい文字列プログラミングは出る幕がない。ただし、APIのASCIIバージョンとWIDEバージョンを自動選択にしていると合わない部分が出てくるので明示的にASCIIバージョンを指定するか、またはAPIを使用しないで済むように書き換えを行う必要があった。
VirtualDirectory(以下VD)は、DICEに組み込まれた、opennapプロトコル準拠サーバ機能の実装である。opennap は、Napster社のプロトコルをリバースエンジニアリングすることによって得られたプロトコルの名称で、リバースエンジニアリングを行った人々により、ドキュメントと、同名のC言語によるオープンソース実装が提供されている。opennapの仕組みは、サーバが各クライアントからファイルリストを受 け取り、その集合に対しての検索や閲覧の要求を処理するというものであり、サーバについてはP2Pとは何の関係もない。DICEに組み込んだサービスについてわざわざVDと呼称することにしたのは、ひとえに"nap"という文字列がNapster社との誤った関連を想起させるので好ましくないという判断からである。また、IRCの場合と同じく既存のクライアントに対応せざるを得ないので、メッセージフォーマットとopcodeを流用しつつクライアントが解釈可能な範囲内で効率とセキュリティを重視する方向へ微妙な仕様変更を加えている。例えばNapsterは音楽データの共有に使用されていたためにそれを支援する情報がopennapメッセージの中にも組み込まれているが、そうした情報は全て除去し無駄なデータの転送を避けている。またopennapでは、あるクライアントのIPアドレスやダウンロード/アップロード活動に関するデータを同じサーバ上の第三者がWHOISコマンドによって得ることができるが、こうした部分も排除した。IRCサービスの方も、WHOISコマンドから照会先クライアントの活動を表す情報は除いてある。こうした情報はプライバシー保護/個人情報管理の観点からは不要であり、デフォルトでは隠されるべきであると考える。
IRCに関する機能の実装が一段落しベータテストに進む前に、何か短時間で入れられる面白い機能は無いかということでアイデアを探っていたところ、 opennapのチャット機能がIRCのそれのように高機能ではないにせよIRCをモデルに作られていることを知ったのがopennapへの着目の端緒だった。ステートフルなセッションを多数ホストするサーバという形式も同じである。DICE上に存在しているIRCと重複する機能を省いてopennapサーバの本質的機能であるディレクトリに絞れば、まとまった時間がありさえすればDICEのフレームワークを用いて実装できそうな直感はあった。また、DICEはMS SQL ServerまたはMSDEを認証情報を保持するために使用するので、データベースにそのままディレクトリを対応づければ検索機能はデータベースの機能を そのまま流用できる。C言語で書かれたopennap実装のソースコードも実に淡白で、DALnet他のIRCサーバのように機能拡張を重ねて迷宮のようになっているわけでもなく、プロトコルのドキュメントを見ながら短期間で実装できる確信ができた。
そこでIRCをDICE上に実装したときと同じように、opennapセッションのクラスを定義し、opennapメッセージパーザを書き、メッセージハンドラメソッドをひとつずつ実装していった。そしてユーザが提出するパス情報を格納するためのリレーショナルデータベースのスキーマを定義し、テーブルにアクセスするメソッドを実装した。現在ではIRCとVDはチャンネルを共有しているものの、最初は専らIRCユーザにディレクトリ機能を使わせることを想定していた。IRCにもDCCというP2Pによるファイル転送機能はあるが、他のクライアントへファイルのリストを告知するシステムが 欠けているので、それをVDで補うことができる(IRCではその部分をユーザがクライアント用スクリプトで補完している)。そこで+xモードを管理者のみが有効化できる新たなチャンネルモードとして設け(チャンネル属性が文字列であるのもコマンド文字列がASCIIであるのと同じくIRCプロトコルの数少ない可塑性の一である)、+xチャンネルへの特定ホストからのクライアントの参加を参照カウンタとして保持し、IRC側の+xチャンネル参加者のみがVD を使えるようにする機能も付けた。 IRCクライアントはSOCKSプロクシスキャナによる検査を経てログインしているので、ホスト情報をrloginのホスト情報のように用いてVDへセキュリティを伝播させることができる。(ちなみにこの機能は現在は廃止され、opennapクライアントにチャンネルへ参加したときのみ検索などを許可するという機能に置き換えられている。)こうして、この単純なディレクトリ機能は、不完全な部分はあちこちにあったが1週間で実装が完了し、WinMXなどのクライアントからのアクセスも可能になった。その当時はopennapについて大した関心はなく、VirtualDirectoryは単なる自己満足のために入れたDICEの付録以上のものではなかった。
ところが後日、ベータテストが進むにつれDICEの主要な問題となったのは、IRCではなく、VirtualDirectoryの方だった。ユーザが 数百人を超えると期待に反して全く使い物にならなくなったのである。問題は全く予想していないところにあった。MS SQL Server/MSDEである。RDBMSはRAMを節約するために使う物であり、またミッションクリティカルな所で使えるものであると信じ切っていたので非常にショックだったが、MSDEの売りであるところの自動制御機構がどうしてもコントロールできず、どう設定を変更してもRAMの消費が抑えられなかったのである。また、MSDEがボトルネックになり、接続するクライアントが多いとサーバの動作が極めて鈍くなってしまう。opennapの性質上、サーバの再起動直後は多数のユーザが一度に押し寄せ、数十万件のデータを数秒以内に 処理しデータベースへ格納しなければならず、その際にMSDEがキャッシュとして多量のRAMを予約し、その後も容易には離さない。当然、テーブルはディ スク上に存在するので、I/Oによっても遅くなる。つまりSQLでいえばINSERTが問題なのである。
考えてみれば単純なことで何故気付かなかったのかと今にして思うが、そのことに思い至るまでは、専ら検索の遅さに原因があると思 い込んでいた。 MS SQL Server 2000/MSDE2000には単語ベースの索引を作って全文検索を行う新機能があるが、これは特殊機能なので使わず Transact-SQLのLIKEによって全文検索を行っていたので、検索速度の改善のために色々工夫した。まずはレコードの属性によって複数のテーブ ルへ振り分け、検索語によって絞り込みができるようにした。例えば検索語にアルファベットが4文字含まれているならば、アルファベットを3文字しか含まないレコード群は検索する必要がない。これをもっと複雑なアルゴリズムを用いてレコード群を分解し、日本語のファイル名について考慮しつつなるべく均等に複数テーブルへレコードが割り振られるようにしたところ、2倍程度の性能の向上を見ることができた。しかしそれでも、他のopennapの実装は、レコード をメモリ上に持ち、単語から生成したハッシュ群をメモリ上のインデックスとして検索の対象にしているので、DICEの検索はその何倍も時間がかかる。
SQL Serverのエンジンをバックエンドに持っているということを大きな長所として考え、またその時点ではRAMのサイズ的な限界を取り去るために RDBMSを使うという観念を誤って捉えていた(実際には、数GB以上の、RAMのように揮発性であってはならない記憶域が必要になるというミッションクリティカルな事例でなければ、汎用のRDBMSは特殊にチューニングされたデータ構造に常に効率面で劣る)ので、どうしてもRDBMSを積極的に使いたいというこだわりがあり、またMS SQL Serverエンジンの性能を過信していたので、安易に他のopennap実装例に倣う気はしなかった。それに、素朴な形態素解析とハッシュ化を行い検索精度を犠牲にした場合と比べ、RDBMSの検索機能を使っていれば情報の取りこぼしがない100%の検索精度が得られる。また他の実装が日本語文字列に対する検索を念頭に置かず形態素解析を行っている点からしても、それら日本語環境では検索精度において劣ると考えられる実装を参考にすることには危惧感が あった。
そこでまずはMS SQL Serverに対するアクセス手法についてチューニングを図った。DICEは高速化のためにOLE/DBを用いてSQL Serverへネイティブ接続するが、ADOやMFCのDAOと異なり非同期問い合わせができないので、個別ユーザによる検索やディレクトリ閲覧についてサーバ全体へ影響が及ぶことを防ぐために自前で非同期問い合わせをできるようにした。C++的には、それ以前にModern C++ Designでファンクタクラスについて読んでいたのでそれを参考にWin32の機構をラッピングした非同期実行ファンクタを作って使った。さらに、複数テーブルへのデータ挿入/削除のSQLクエリを対象テーブル毎にまとめて直列化(同じデータの挿入と削除が同時に起こるような事態は避けなければならない)し一気に転送するためのバッファ機構も作った。また、テーブルのロックについても粒度を下げて負荷を下げるようにした。さらにストアドプロシージャも使用した。しかしこうしたデータベースへのアクセス手法の改善による性能向上は微々た る量に留まり、到底満足の行く代物ではなかった。
そこまで来ると、観念して別の面から攻めるしかない。この時点では、まだSQLのINSERTが問題なのではなくLIKEによる単純な全文検索が問題なのだと考えていたので、データベースの列に設定できるインデックスを用いた検索が出来ないかと考えた。インデックスを用いることができるようにクエリを構成すれば、テーブルのフルスキャンを行うよりは高速にレコードが取り出せるはずである。その為には、インデックス列に入れるための、元データを識別するためのデータを、元データから作り出す方法を考え出さなければならない。ここまで至ると、現下の問題は、「効率の良い日本語全文検索システムを作るにはどうすれば良いか」というより一般的な問題に帰着する。特殊な点は、opennapのシステムはセッションを多数同時にホストするサーバであり、多数のレコードの入力と出力の要求を、同時に、かつリアルタイムに処理できなければならないということである。従って、レコードのライブラリへの入力に時間がかかってはならず、またライブラリからの出力にも時間がかかってはならない。一見して分かるほどかなり無理のある要求である。
しかし、それまでのDICEへのIRC機能の組み込みは、単に仕様を実装するだけで、若干の追加機能の考案や仕様の再検討、効率的な実装の追及という取り組みはあったものの、手続き的に手順を踏んで進むわりと平坦な道であったのに対し、VDに関する問題は、アルゴリズムとデータ構造を工夫して望む解を速く叩き出すという、実にプログラミングに馴染む問題だった。IRCについての問題が単にWindowsやネットワークプログラミン グ、セキュリティモデルという、他者が構築した既成ブラックボックスシステムの周りをつつくだけの、悪く言えば「作業」であったのに対し、VDの問題はチャレンジングでやり甲斐がある創造的なものであるように思えた。その時点で、それまでのIRCに対する強い関心は褪せてしまい、VDの方へより真剣に取り組む意義が生まれた。また、この問題に関する既存の解答としてのopennap実装もあり、それがある程度の性能を出しているので、そこへ追いつくことを目標として新しいゲームを楽しむこともできる。IRCの場合、過去の無数の先人たちによる10年の努力の成果として現在の主要な実装があるのを知っているので、競争などという言葉はおいそれと口には出来ないが、高々2年かそこらの歴史しかないopennapに関してはそういうことはない。
データベースについての話題に戻ると、まずデータを識別するための情報をいかに作り出すかという問題がある。つまり、大量にある元のデータからなる空間を ブルートフォースで探索するのではなく、入力時にあらかじめインデックスを作成しておき、そこに元データへのポインタを添付しておくという場合の、イン デックスの作成法である。二分探索は、opennapの場合入力と出力が同時に起こりうることから毎回の自己組織化のコストが極めて高くつくために無理で、また部分文字列検索ができなくなるので採用できない。また、opennapの場合、元データが短いので、その点もポジティブな材料として考慮に入れる必要がある。つまり普通の全文検索システムなら圧縮率の高い手法を用いなければならないところ、幸いその必要はない。全然この分野についてはチェックしたことがなく、既存の日本語全文検索システムとしてはNamazuというものがあり形態素解析を使っているということくらいしか知らなかったので、参考資料を探してサーチエンジンで色々見て回ることになった。形態素解析については、opennapの既存の実装が大体それに近いことを行い、ハッ シュ化と組み合わせているが、単純な形態素解析の方式では日本語その他の非ASCIIのデータに対する検索に難があるため採用できない。また日本語マルチバイト文字列に合わせた拡張を行った形態素解析では日本語以外のマルチバイト文字列に対応できない。あくまで検索精度を100%に保ちつつ高速に検索を行うにはどうすればよ いか。
そこで、N-gramというものを知った。非常に単純な原理で、ある文字列に対して2-5文字程度の固定長の枠をあてがい、それを1バ イト(場合によっては1文字)ずつ終端に向かってずらしながら単語を抽出し、その集合をインデックスに用いるというものである。これならば言語に関係なく 検索に用いることができる。古い時代の文書の傾向をN-gramの生起頻度を元にして求めるなど、言語学の分野でも用いられているようである。難点はインデックスが大きくなりやすいことだが、そこは適切にハッシュ化を行ってやればよい。この点、リコーG-baseの全文検索についてのレポートが参考になった。ただ、ハッシュ化を経たN-gramの効果について半信半疑だったのでテスト用にアルゴリズムを作り日本語ファイル名のサンプルデータ群に対し検索を適用したところ英語の検索語の場合ノイズ発生率は40%ほどでやや大きくなったが検索語が日本語の場合は10%以下に留まり、予想よりかなり良 い成績だった。検索語が長くなればノイズ発生率は限りなく低くなる。
N-gramがどうやら使えそうだということがわかったので、元データから取り出した1つの3-gramとその出現位置とをデータベー スのテーブルの1行に収めるようにスキーマを書き、DICEにデータベースアクセスのためのメソッドを実装した。検索時にはそれらテーブルへの複数クエリの結果をJOINすることによって検索語に含まれる3-gramを検索語内での順序と同じ順序で含むデータが導き出される。ただ、1つの元データから64個の3-gramが取れたとすれば、登録時に64回もINSERTが行われてしまう。再三繰り返すが、この時点では検索の方が性能低下の原因だと誤信していたのである。この、インデックスと実データを両方ともデータベース内のテーブルに入れるというやり方をDICEに実装して一番最初のローカルでのテスト は悲惨なものとなった。一番最初のユーザがログインし、1000ファイルほどサーバへ登録を行おうとした時点で、SQL Serverが1GB以上の仮想メモリを使用し、システム自体がほとんどフリーズする状態になってしまったのである。1秒ほどの間に10万件近い数のレコードの挿入を試みたのだから無理もない。その時点で、SQL ServerないしMSDEを積極的に使用するという概念は消し飛んでしまった。
インデックスのみをメモリ上に置き実データをRDBMSへ格納するという方法では、検索は多少速くなるものの本質的な問題の解決にはならない。とすれば、インデックスも実データもオンメモリで持ち、RAMが足りなくなったときだけRDBMSへ実データをページアウトすべきだろう。つまり、高速なRAMを文字通りバッファとして用いるのである。OSの仮想メモリマネージャが行うように。この単純な事実を頑なに無視し続けていたのは完全に私の経験不足が原因であった。そして、RDBMSを中心に据えていたデザインをさっぱりと捨てることにした。DICEではディレクトリ名を除いたファイル名を128バイト以下に制限し、そこから2-gramを抽出し、256 * 256ビットの空間にマップしたものをハッシュ化して256ビット(32ビット * 8)へ圧縮し、ファイル1つについてのインデックスデータとしている。N-gramの位置データは無くてもさほどノイズが増えないというのを実測して確認 したので切り捨てることにした。実際の検索時には、検索語はせいぜい10文字以内なので検索語が含む2-gramの数は少なく疎な部分が多くなり、256 ビットのうち1つか2つの32ビットDWORD同士の論理積を取れば済む場合が多い。検索語が長い場合は2-gramの数は多いものの絞り込みが精密になりノイズが乗りにくくなるため損失は相殺される。そしてこのインデックスへの検索の結果に基づいて元データを引き出し、これをさらに検索語と照合してノイズを除去し、100%の検索精度を達成している。
当初は、128バイトの元データから2-gramを抽出した場合高々127種類の2-gramしか生起せずインデックス256ビットの うち半分は必ず疎になることを利用してインデックスのビットの立っていない部分をランレングス圧縮する機能を入れていたが、当然検索時の展開が必要で検索が遅くなるため、メモリを節約するよりは速度を優先して没にした。元データについては、まず短いデータで単位が細かく互いに独立しているために辞書圧縮などの手法が使えないこと、それから日本語を含む任意のバイナリデータを含ませるためにニブル化やガンマコーディングなどの手法が適用できないことを理由と して圧縮していない。当然速度面では圧縮しない方が有利である。また、最初は、RAMが足りなくなるとRDBMSへディレクトリをページアウトする機構も 入れていたが、オーバーヘッドを無くすために結局没にしてしまった。つまりディレクトリは全てオンメモリに展開される。
当初堅持していたRDBMS重視の方針を完全に撤回しメモリ利用に走ってしまったので、転向したからには性能を追及しようと考え、100万件のレコードで250MB程度のメモリを占有し、1秒以内に検索が完了するという状態をVDの目標とした。この1秒というのはユーザが検索してすぐに結果が返ってくるという体感の指標として考えたもので、1秒以下ならば人間があまり感知しないだろうと推測して設定した。実際には多数のユーザが同時にクエリを発行するため、個々の検索が消費するサーバ内部での時間は1秒よりはるかに短い時間でなければならない。ただ、SQL Serverエンジンを用いた初期のVD実装の改良版での試験ではサーバへの負荷が軽い場合は3秒程度で結果が返っていたので、ブルートフォースで検索していたことを考えるとSQL Serverもなかなか大した物ではある。いずれにせよ、今回のリリースバージョンでは概ね目標を達成できた。並列実行性も大幅に向上しているので、マルチプロセッサ環境やHyper-Threading環境ならほぼリニアな応答性向上が見込める。
並列実行性を高めるということは、それに馴染むアルゴリズムを考案するということの他に、多数のスレッドの並列実行によるデータの破壊やデッドロックを防ぐという配慮も必要とする。つまり、適切に同期を行うべきポイントを見極めてそれを最小限に留め、かつ必要以上に少なくし過ぎないようにしなければならない。IRCチャンネルのようなコレクションについてユーザ毎のイメージ/ビューの整合性を維持するために同期を行う必要がある(Observerパターン)のは言うに及ばず、クライアントが接続を切断したときのセッション毎のリソースの解放と初期化は予想できないタイミングで起 こり、かつ資源の再利用を行っているために、特別の注意を要する。DICE v. 0.1の1年あまりの歴史のうち、数ヶ月がこの問題から生ずる不可解な現象を除去するためのデバッグで過ぎ去っていった。当初はチャンネルに関する同期の問題を片づければ十分だと思っていたところが、実に多種多様な問題が生起し、それらに全て対処する羽目になった。これに関しては、稿を改めて述べて みたい。
DALnetなどの10万人規模の大規模IRCネットワークに参加しているUnixのIRCサーバならば、他のサーバとのリンクを維持しながら、4,000から多くて10,000程度の同時接続をさばいている。DICEでこれまでに得られた運用データでは、IRCクライアントより遙かに多量(少なく見積もっても30倍以上)の帯域を消費するopennapクライアントを4,000個以上1台のマシン上で問題なくホストできたので、IRCクライアントのみをホストする場合は、カーネルメモリさえ十分に確保できれば同じ設備で10,000程度の同時接続を維持可能であると推測できる。
コメント
コメントを投稿