update.6月からDMM.comラボでミドルウェアを作るエンジニアインターンをしている@kawasin73です。DMM.comラボではluaで実装されたKVS(キーバリューストア)を利用しています。するKVSで、社内でluaの皮を被ったC言語で実装されたものが運用されています。ラボの方が行い、実装は僕がすることになりました。channel等)については僕が学びながらそれについて都度相談するというスタイルで行なっています。その開発記を連載しています。第2回の今回は、TCPサーバーを実装する時にパフォーマンス上気をつけるべきことと、開発の手順をまとめていきます。僕が実装を始めるにあたり、メンターの方からパフォーマンスに気を使うようにアドバイスをいただきました。ハイパフォーマンスなKVSを実装することはより早くレスポンスを返せるだけでなく、CPUリソースをフルに使うことができ、サーバーの台数を減らし、コストを下げることができる。データベースはWebAppサーバーなどと違い、スケールアウトが難しくスケールアップで対応する必要があり、ダウンタイムが発生する可能性がある。パフォーマンスを上げることでスケールアップの必要なタイミングを遅らせることができ、運用の負担を減らすことができる。いったメリットがあります。これらのメリットを享受するためには、最初からパフォーマンスに気を使った実装を行うことが必要です。教えていただいたことと、そのキーワードをもとに調べたことをまとめていきます。コンテキストスイッチ現代のメジャーなOSは、1つのCPUの上で複数のプロセスやスレッドの処理を少しずつ進めていくことで、マルチタスクを実現しています。プロセスやスレッドの処理が切り替わって別の処理が始まることを「コンテキストスイッチ」と呼びます。ハイパフォーマンスなサーバーを実装するときには、このコンテキストスイッチをなるべく起こさないことが重要になります。コンテキストスイッチが起こり、他のプロセスにCPUを奪われるとその時間分サーバーは動くことができなくなります。コンテキストスイッチを起こさず、ギリギリまでCPUを保持してサーバーのためにCPUを動かし続けることが重要です。コンテキストスイッチ自体にもオーバーヘッドがあり、パフォーマンス劣化に繋がります。コンテキストスイッチは、基本的にシステムコールを呼んだときに起こります。(一部コンテキストスイッチを起こさないシステムコールもあります)そのため、システムコールを不必要に呼ばないことが大切です。グリーンスレッドスレッドには大きく分けて2つの種類があります。ネイティブスレッドグリーンスレッドです。ネイティブスレッドはOSが提供するスレッドで、グリーンスレッドはOSではなく言語や仮想マシンが提供するスレッドです。ネイティブスレッドは、同時に数千や数万のスレッドを立ち上げることは難しいですが、グリーンスレッドは軽量でメモリの限りスレッドを立ち上げることができます。GO言語の特徴の一つであるgoroutineは、グリーンスレッドです。これは、処理を途中で中断し、その後再開できるものです。グリーンスレッドとコルーチンの違いは、スケジューラがあるかどうかです。グリーンスレッドにはスケジューラがあり、コルーチンにはスケジューラがありません。それぞれ代表的なものには以下のような言語があります。ノンブロッキングI/OブロッキングI/OとノンブロッキングI/Oについては、この記事がわかりやすいです。ノンブロッキングI/Oと非同期I/Oの違いを理解するブロッキングI/Oファイルへの書き込み読み込みや、ネットワークからのデータの読み取り書き込みなどは、比較的時間がかかる処理です。ここでは、ネットワークからの読み取りを例にとります。ネットワークの読み取りはカーネル側で行いますが、その読み取りが完了するまでユーザー側が待つのがブロッキングI/Oです。読み取りが完了するまでユーザー側は停止し処理をブロックしてしまいます。ノンブロッキングI/Oブロックしてしまうと、サーバー処理のパフォーマンスは劣化してしまいます。そこで、カーネル側で読み取りの準備が完了していないときに、完了するまで待つのではなく、即座にエラー(EAGAIN)を返してもらうのが、ノンブロッキングI/Oです。Eventを使います。Eventを使うと、カーネル側で読み取りの準備が完了するとユーザー側に通知が飛んできて、ユーザー側で読み込みを再開します。これにより、カーネル側で読み込みの準備をしている間に別のスレッドの処理を進めて、CPUを効率的に使うことができます。(I/Oの多重化)Go言語ではどうなるの?goroutineの嬉しいところは、ノンブロッキングな処理も同期的に書くことができることです。あるgoroutineが処理を待っているときには、別のgoroutineがスケジューリングされて動くため、CPUを効率的に使うことができます。#golangが参考になります。Eventに登録して(fd.pd.waitRead())、I/Oの多重化を行なっています。Readができるようになると、goroutineは中断されたところから再開されるため、同期的に(ブロッキングI/Oの時と同じように)コードが表現されています。今回実装するKVSは、独自の可変長バイナリプロトコルによって生成されたバイト列をTCPの上でやりとりします。Parserを実装する必要があります。Parserをストリームパーサーとして実装します。サーバーでTCPから一度にReadするバイト列は、リクエストの一部だけしかなかったり、1つ以上のリクエストが連続して含まれていたりします。一度にパーサーに渡すバイト列で1つのリクエストの途中までしかわからないことがあります。6つの部分に分かれています。1回のReadで得られたバイト列で、途中のKEYLENまでパースできたが、KEYの途中からのバイト列が足りないとします。KEYLEN分のパース処理を2回行うことになり、CPUリソースの無駄になります。情報とパースできなかったKEYの一部のバイト列を保持して、次にReadされたバイト列を受け取ってから途中からパースしていきます。ストリームパーサーには以下のメリットがあります。今回開発するKVSは外側から、以下のようなモジュールに分かれています。インメモリーKVS今回は、これらのレイヤーを外側から開発していこうということになりました。それにより以下のようなメリットがあります。常に動くものが得られ、アジャイルな開発ができる。ベンチを都度取りながら開発を進めることができ、パフォーマンスを気にした開発をしやすい。レイヤーごとに実装することで、開発のマイルストーンを小さな単位で置きやすい。進捗の進み具合がわかりやすい。レイヤーごとに実装を完了させてインターフェースを確定させていくので、手戻りが発生しにくい。サーバーやTCPは普遍的なものであり、普遍的なものから実装することで初めて触れるGo言語の特性を学びながら実装を進めることができる。全体的な流れはこのような順番にしました。(Streamでなくて大丈夫。実装Test最終的なパフォーマンスチェックこの中では、以下のタイミングでそれぞれベンチマークをとり、パフォーマンスチェックをします。echoサーバーのパフォーマンスはその言語における最大値になるので、なるべく高いパフォーマンスを出せるようにチューニングする必要があります。そこからParserなど実装を進めていくにつれ、全体的なパフォーマンスは落ちていきます。最後にハイパフォーマンスなTCPサーバーを作るために必要な知識と、全体的な開発手順を解説しました。Shutdownに対応したTCPのechoサーバーを実装する部分について解説していきます。Kawa@kawasin73ソフトウェアエンジニアです。東京大学4年Go/Ruby/Swift/Python/Javascript/Java/Chttps://kawasin73.hatenablog.com/FollowDMM.com総合エンタテイメントサイト「DMM.com」を運営。会員数は2,900万人を突破。動画配信、FX、英会話、ゲーム、太陽光発電、3Dプリンタなど40以上のサービスを展開。沖縄での水族館事業参入、ベルギーでのサッカークラブ経営など、様々な事業を手掛ける。VENTURES」による出資や、M&Aなどを積極的に展開している。upLoginユーザーは見つかりませんでした。