「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。2017-02-16LinuxカーネルのTCPスタックとシステムコールの組み合わせによる手法よりも高速にポートのListenチェックを行うプログラミングまずは前回の記事で盛大な誤認をしていたことを訂正しなければなりません。hb.matsumoto-r.jp前回の記事では、高速にリモートホストのポートチェックを3パケットで実現する実装を行うために、RAWソケットとユーザランドの簡易TCPスタックを実装してパケットを放出しましたが、カーネルのTCPスタックによって自動的にRSTパケットが返されるため、RSTパケットが返されるよりもはやくrecvfromしないとSYN+ACKを受け取れないと述べました。実際に改めて試すと、RSTパケットがカーネルによって放出されていても、ユーザランドTCPスタックでSYN+ACKパケットを受け取ることができました。色々な過程の中で以下の2つの要因が重なり、誤認していました。C言語のif文の書き方のバグrecvfromの2回実行してはじめてSYN+ACKが得られるまず一つ目のC言語の書き方のバグですが、以下のように書いておりました。*/これだと、演算の()抜けの問題によりrecvfromの値が常に0になってしまいます。あああああああ、という初歩的なミスです。二つ目は、sendto後にrecvfromを実行した時に、一回目の実行では最初のこちらから放出したSYNパケットが、二回目の実行でSYN+ACKパケットが得られます。これも、一回目だけ実行していたことによって、SYN+ACKが取れないということ、recvfromの値が0であったことから、カーネルのTCPスタックに邪魔されているのだと誤認してしまいました。その流れでスレッドなどを使って取り組んでいましたが、実はスレッドなど使わなくても、RSTパケットがカーネルから放出されても、RAWパケットにはデータが渡ってくることがわかりました。うおおお、これで勝てる!!!!!ということで、LinuxのカーネルのTCPスタックを使うよりも高速かつ少ないパケット数でポートチェックできるのでは?という前回の方針で実装していきたいと思います。今後のmrubyのmrbgem化や、ミドルウェアに対するリクエストの処理過程で実現したいといった目的から、できればマルチスレッドやマルチプロセスな実装にはしたくないので、シングルスレッド前提で考えてみます。上記の前提に従って、リモートホストのポートのListenチェックを高速に行う実装は以下の3つの戦略が考えられます。connect()システムコールとカーネルTCPスタックで実現connect()システムコールとSO_LINGERとカーネルTCPスタックで実現RAWソケットとユーザランドTCPスタックを実装して実現connect()システムコールとカーネルTCPスタックで実現これは、TCPの処理はカーネルのTCPスタックにおまかせして、connect()システムコールとclose()で実現する最も簡単な方法です。パケットとしては、3way-handshakeでセッション確立後、FINパケットで接続を終了する処理になります。この場合は通常全部で6パケットでチェック可能になります。実装的には以下のようになるでしょう。次の戦略としては、同様にカーネルのTCPスタックに処理はおまかせしつつ、セッション終了時にFINパケットで終了するのではなく、SO_LINGERオプションをソケットに設定してからclose()することにより、FINパケットの代わりにRSTパケットを送って接続を終了します。この場合、3way-handshakeとRSTパケットにより、合計で4パケットでのチェックが可能になります。実装は以下の通りです。}}RAWソケットとユーザランドTCPスタックを実装して実現そして、前回記事であきらめた戦略ですが、これも壮大な誤認を解決した(していただいた)ことにより実現可能そうです。RAWパケットを作って、自身でTCP/IPのヘッダを定義した上で、戻りのパケットを同様にヘッダ解析することによって、ユーザランド側に新ためて実装した簡易TCPスタックによってポートチェックを行います。sendto後にrecvfromを複数回行う必要があるため、recvfromをループ内で実行します。}}さてベンチマークだ!!!ということで、ついに自分が当初考えていた高速なポートチェックの実装およびベンチマーク比較が行えそうです。一度は前回記事のように諦めてしまいましたが、なんとかここまでたどり着けて、ブログ書いてよかったなぁという気持ちしかありません。いうわけで、以下のように、ポートチェックのループを複数回回して、その時の処理性能を比較してみましょう。0;}では、上記のコード全てをコンパイルして、性能評価を行いました。環境はCorei7の2.8GHz、メモリ16GBのMacBookProに、Vagrantでubuntu16.04環境をコア2個+メモリ4GBで構築しました。通常のconnectパターンでは、ソケットのTIME_WAITが溜まってconnectがEADDRNOTAVAILを返し、アドレスがアサインできないエラーが多発して処理が遅くなりすぎるので、TIME_WAITを使いまわせるように以下のチューニングも同時に行いました。1では、比較してみましょう!以下実行結果です。kernelやったーーーーーーーー!connecnt+SO_LINGER+カーネルTCPスタックパターンよりも速くポートチェックができたーーーーーーーー!チェック回数を変えた上でのベンチマークは、大体以下のような表になりました。上記のようなベンチマークを何度も行いましたが、概ねconnectのみの実装以外は安定して同様の数値が得られました。遅すぎて計測不能SO_LINGERを使わないconnectについては、FINパケットを放出するのでカーネルのFINパケットに関わる設定の問題などでTIME_WAITが沢山たまって、再connectが度々発生したり、上記のようなチューニングをしてもパケット数が多くなることなどからどうしても性能が出ませんでした。connectのみの実装はカーネルのチューニング次第でもう少し改善できそうではありますが、やはり他の手法と比べると格段に遅かったです。RAWパケットパターンとconnect+SO_LINGERパターンは、カーネルのチューニング有る無しに関わらず安定して性能が出ていました。このあたりも普通に使う分にはメリットになるでしょう。まとめ以上のように、色々試行錯誤しつつも、インターネット上の皆様に様々な助言を頂きながら、ようやく実現したい実装のPoCが完成しました。皆様ありがとうございました。ベンチマーク比較の結果、RAWソケットと簡易ユーザランドTCPスタックでの実装が一番はやいことがわかりました。この実装をベースに、mrubyのFastポートチェック的なmrbgemを作り、mrubyを組み込んだソフトウェアからリモートホストのポートのListenチェックを高速かつ少ないパケット数で簡単に実現できそうです。いうことで、リモートホストのポートチェックにもこだわると色々と面白いやり方がありますね、という過程のご紹介でした。このエントリによって、カーネルチューニングによるconnectのみのチェックの性能向上案や実装の更なる改善などありましたらぜひぜひアンサーエントリを頂けると幸いです。以下に今回のコードを全て貼り付けておきますので自由にお使いください。2016の23日目の記事です。Serverを作ったPmilterというサーバソフトウェアを作りました。はてなブログPro研究者と技術者をやっています。詳しくはリンクの「研究・開発業績ページ」を御覧ください。さくらインターネットに入社して1年が経ちました非厳密計算で確率的に解釈するコンピューティングへの流れラストオーサーとしての国際会議投稿やジャーナル執筆のサポートについてviを:wqや:q!、あるいはZZで終了するのとではどちらが効率的か自分の研究開発に自信を持つためには月別アーカイブリンク研究・開発業績ページ旧ブログ検索はてなブログをはじめよう!matsumoto_rさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。