こちらも気付いた点をまとめて更新していくことにします。早くも自分が後悔甘く見ていた部分が見えたのでさらさらさせていただきます。
lighttpd開発の動機: なぜシングルスレッドか
lighttpdのHistoryから設計思想を確認。
For me it was a proof-of-concept of the c10k problem written by Dan Kegel. How to handle 10000 connections in parallel on one server. I already had seen apaches killing systems because they ran out of memory into swap with only 100 parallel connections.
大目標がThe C10K problemという、10,000セッション同時接続という難題に挑戦しようという壮大な計画だったとか。なんとも凄い!
目標10,000接続となると、単純にマルチスレッド化してしまってはメモリもかさみ、かつ非同期コントロールにも難が出るため、メモリを抑えるシングルスレッドでの表現の中で速度が出るように工夫を施したと考えるのが正しそうですね。
さて、これをマルチスレッドで表現するとしたら、単純にconnectionごとにスレッドを立てていたらメモリが追い付かない。
The C10K problemでは、1 threadで2MByte消費するなら、512スレッドで1GByteのメモリ使用と算段を立てていました。
If each thread gets a 2MB stack (not an uncommon default value), you run out of virtual memory at (2^30 / 2^21) = 512 threads on a 32 bit machine with 1GB user-accessible VM (like, say, Linux as normally shipped on x86).
実際私の64ビット環境で試してみたところ、1 threadに8MByteのメモリ消費をしていました。
手元で動作させている素のlighttpdが112MByte消費なので、メモリ使用量の観点で見ると20スレッド程度が許容範囲でしょうか。設定変更は可能なようにしますが、いずれにせよスレッド数の上限を決め、スレッドに数コネクションを振り分ける仕組みにするのが適切ですね。可能であればより空きが見えるといいけど。
幸いThe C10K problemに設計アイディアがあるので、参考に色々検討することにします。
(色々サイトを見ていると、IIJの方が既に完璧なものを作られている気がしましたが、敢えて自力でトライしていこうと思います。)
必須機能: Thread poolデザインパターン
上記のように、単純にマルチスレッド化をするとメモリを食いつぶすだけ。そんな時に便利なのがThread poolデザインパターンです。
Wikipediaより。
多数のスレッドを作成して、それらに多数のタスクを処理させる。典型的な状況ではスレッド数よりもかなり多くのタスクが存在し、各スレッドは、あるタスクの処理が終わると次の処理待ちタスクの実行に取りかかる。一般に、Producer-consumerパターンを使って実現される。
ざっくりいうと複数のスレッドの作成して(プールして)おいて、空きスレッドを有効活用して効率を上げようというもの。Java等には既に存在するらしいです。ただ昔Cで誰かが自作したthread poolを仕事で利用したことがあるんですが、利用の仕方がうまくなかったためデータ同期に混乱して大変なことになった記憶があります。
じゃあどういうケースだとうれしいのか。コマンド形式は実行して終わりなのでプールのメリットがあまりなさそう。
プールして面白いのは勝手にイベントトリガーで処理してくれるようなものかな。
C言語、特にLinuxでのイベントといえばファイルディスクリプタ。ということで、ファイルディスクリプタ契機のイベントスレッドをプールし、それらのスレッドにFDをaddしてイベントを拾えるようにしようかなと思います。
早い話がepool_wait/selectをやってるスレッドを沢山ラップしてadd先を振り分けてあげるだけ。これなら簡潔だしFDを利用した安全なデータ同期も出来るって考えです。The C10K problemの記載が多分そんな感じでしょう。(適当
やっぱりLinuxC言語のイベントといえばFDですね!
lighttpdの強さ
今見ている感じの強みを上げます。(今後更新予定)
-
排他制御のない作り
- シングルスレッドなのでmutex等がいらない為、マルチスレッドでもやたらと同期を取るものよりは当然早くなります。
-
イベント委譲の意識の高さ
- 目標10,000接続があるからかもしれませんが、1コネクションで状態が変わったらすぐ次のコネクションへと、他の処理への移行が早く出来るように考えられている気がします。多分出来るだけ早く新しいコネクションが捌けるように。
-
必要な処理は集中して捌けるような構成
- 単純に2の仕組みを実装するだけだと、逆に1コネクションのある処理はさっさと進めたいという時もなかなか時間が回らず間が開くケースも考えられます。その点をメインループの前後でコネクション処理側が追加出来るイベントハンドラーを用意し(joblist/handle_trigger/fdevent等)、fdevent処理やhandle_triggerといった、メインループ内の前後で実行される処理により必要な処理をメイン処理の後にやりようでもう一度自身に処理が回るチャンスを与えています。結果素早く次に移行できる状態遷移はさっさと実行できるようになっています。
-
ビット演算等、純粋な速度向上実装の活用
- 文字列操作よりもデータ比較の方が速い!ということで文字列処理はさっさと得意な形に移行してる感があります。
-
毎リクエスト/レスポンスで使用する型はメモリ上に展開してしまう。
- リクエスト、レスポンスで使用するようのtmpバッファやタイムスタンプをServer構造体内に持っておき、毎リクエスト、レスポンス時のデータ作成/削除時間を減らしている。細かい工夫だが効果は期待出来る
4について。例えばresponseヘッダーパースでは、時間のかかる文字列のパースは1度だけにして以降はparsed_response という変数を利用しています。
こういった細かな工夫の積み重ねがありそうです。
case 4:
if (0 == strncasecmp(key, "Date", key_len)) {
con->parsed_response |= HTTP_DATE;
}
break;
こういった細かい配慮はどの言語でも大事だったりしますよね。スピード勝負の場合は出来るだけシンプルに、少ない手番で実現される処理、クラスメソッドの方が基本速いはず。
メモリは潤沢で意識しなくてもいい時代でも、無駄なものはないか、使いまわせるものはないか、意識するのは大事かもしれませんね。
マルチスレッド化で何を意識すべきか?
-
極力排他制御のない作り
- スレッド内で閉じたデータ構造設計が理想
- 外部との通信が必要なもののみ排他を、出来ればsocket通信のようなセマフォではない通信による制御に頼る(検知速度向上の為)
-
イベント委譲の意識
- 出来るだけ難しいことはしない、シンプルに作る
-
必要な処理は集中して捌けるような構成
- 既存の仕組みで活用できるものをこちらもメインループとの同期方法次第。
-
純粋な速度向上実装の活用
- そのまま本家を参考に流用しましょう!
マルチスレッドが速度で劣る面は排他制御が絡んだ場合。極力排他がかからないよう、上手にFDを利用してconnectionシーケンスを崩さない仕組みを心がけます。
後はメモリ管理をどうするかですね。
lighttpd公式サイト History
The C10K problem
ちなみに
こんな詳しくlighttpdの中身を解析したサイトがありました。純粋にlighttpdに興味のある方はこちらを参照ください。
私は一通り作り終えるまでは意地でも見ません笑
https://fisproject.jp/2013/09/%E3%80%90libev%E3%80%91lighttpd%E3%81%AE%E8%A7%A3%E8%AA%AC%E3%80%90socket%E3%80%91/