ページの先頭です
2015年2月12日
Glasgow Haskell Compiler(GHC)は、関数型言語Haskellの主要コンパイラです。GHCは(並列性に加えて)並行性を主要な目的として長年開発されてきました。そのため、GHCには、
など、マルチコアで簡潔に並行性を実現するための部品が揃っています。そこで、Haskellで Web サーバなどの並行プログラムを書き、GHC でコンパイルすれば、マルチコア環境でスケールするのを期待したくなります。
残念ながら GHC 7.6.3 までは、入出力を司るIOマネージャの実装にボトルネックがあり、マルチコア環境でスケールしませんでした。エール大学のAndreas Voellmy氏と筆者は、IOマネージャの改良に取り組み、その成果が GHC 7.8.1 に取り込まれました。
7.8.1 以降の GHC で、これまでの並行プログラムをコンパイルすると、マルチコア環境でスケールするようになります。並行プログラムへの変更は不要です。
なお、GHC の最新のバージョンは 7.8.4 で、2月末には 7.10.1 がリリースされる予定です。一般ユーザが使う最新の Haskell Platform 2014.2.0.0 には、GHC 7.8.3 が入っています。
GHC で並行プログラムをコンパイルするときは、必ず -threaded オプションを付けてください。以下、"Server.hs" という Haskell のコードをコンパイルする例です。
% ghc -o server -threaded Server.hs
これで、"server" という実行バイナリができます。この実行バイナリには、GHC のランタイムがリンクされています。GHC のランタイムは、ユーザが指定したコマンドライン引数のうち、"+RTS" と "-RTS" で囲まれた部分を横取りします。ランタイムオプション "-N" で数を指定すると、その実行バイナリは、その分のコアを使用して実行されます。
以下は、コアを4つ使って "server" を実行させる例です。
% server +RTS -N4 -RTS
ハイパースレッドを利用している場合は、実コアではなく、ハイパースレッド化されたコアが対象となります。
数を省略すると、存在するコアすべてを使用します。
% server +RTS -N -RTS
なお、実行バイナリに指定するオプションの最後に、ランタイムオプションを書く場合は、最後の -RTS は省略可能です。
% server +RTS -N
IOマネージャがどれくらい改良されたかを示すために、GHC 7.6.3 と GHC 7.8.4 でコンパイルされたサーバプログラムの性能を比較してみました。計測のために用いた環境は、以下の通りです。
計測ソフトウェアはweighttpを用いて、以下のようなパラメータを指定し、スループット(req/s)を計測しました。ダウンロードする "index.html"の大きさは 612バイトです。
% weighttp -n 100000 -c 1000 -k -t 16 http://IPアドレス/
サーバプログラムとしては、筆者が開発しているwittyを使用ました。wittyは、GHCのランタイムやライブラリに潜むボトルネックを客観的に示すためのプログラムです。コマンドラインオプションを指定することで、筆者が知っているボトルネックを回避するようになります。今回は以下のようなオプションを指定しました。詳しいことは説明しませんが、これで IO マネージャ自体の性能を測定できます。
% witty -a -m -r 8080 +RTS -A32m
"-A32m" は、ガベージコレクタが使うメモリの大きさを 32Mバイトに指定します。経験的に、マルチコアで動くサーバでは、これぐらいの数値がメモリ使用量に対する性能が適切なようです。
witty は、GHC 7.6.3 と GHC 7.8.4 の両方でコンパイルした2つの実行バイナリを利用するのに加えて、2つの使い方をしました。
1つは、ランタイムオプション "-N" でコア数を指定します。これが一般的な使い方です。もう1つは、witty の "-n" オプションを指定します。witty は、このオプションで指定された数だけ(nginx のように)preforkします。
今回はコア数として、1、2、4、8、16 を指定しました。その結果を以下のグラフに示します。
まず、一般的な使い方である紫と赤の線を比較してください。GHC 7.6.3 がまったくスケールしないのに対して、GHC 7.8.4がスケールしているのが分かると思います。これが、我々の研究成果です。
緑の線は、prefork を用いれば GHC 7.6.3 でもマルチコアでスケールするサーバを実装できることを示しています。ただ、prefork を使わなくても、GHC 7.8.4 でコンパイルして、一般的な使い方をすればよいことも分かると思います。
青と赤の線の差は、改良された IO マネージャに未だに残るオーバーヘッドを示しています。解決できるか分かりませんが、筆者の課題リストに積んであります(ガベージコレクタのスケーラビリティに難があるのかもしれません)。
次は、実用的なWebサーバの性能を計測してみましょう。今回は、3つのWebサーバを計測してみました。
1つ目は、WAI(Web Application Interface)の HTTP エンジンであるWarpです。WAIアプリとして、受け取ったHTTPリクエストを無視し、"Hello, world!"という文字列をHTTPレスポンスとして返すプログラムを書き、Warpにリンクしました。このWAIアプリのコードを以下に示します。
{-# LANGUAGE OverloadedStrings #-} import Network.HTTP.Types (status200) import Network.Wai (responseLBS) import Network.Wai.Handler.Warp (run) import Control.Concurrent main :: IO () main = runInUnboundThread $ run 8080 app where app _ respond = do respond $ responseLBS status200 [("Content-Type", "text/plain")] "Hello, world!"
このWAIアプリは、分岐もなく、メモリにしかアクセスしませんから、Warp自体の性能を示すと考えられます。なお、Warp は Web アプリケーションフレームワークである Yesod や Scotty で利用されています。ちなみに筆者は、Warp の開発チームで性能向上を担当しています。
次のWebサーバは、筆者が開発している Web サーバ Mightyです。Mighty は、WAIアプリの1つとして実装しているので、Warp にリンクされます。Mighty には、筆者のサイトを運用するのに必要な機能、例えば静的コンテンツの提供、CGI、リバースプロキシなどが実装されています。
最後のWebサーバは、nginxです。
公平な比較になるよう、Mighty と nginx をできるだけ同じように振る舞うよう設定しました。例えば、両者ともログを取らず、またファイル記述子を再利用するようにしました。
計測環境と計測方法は前述の通りです。Warp と Mighty は GHC 7.8.4 でコンパイルし、ランタイムオプション -N でコア数を指定しました。nginx は、"worker_processes" で prefork するワーカの数を指定しました。
以下に、計測結果のグラフを示します。
これは、ある環境で、ある計測ツールのあるパラメータを使用するとこうなっただけであり、このグラフのみでとやかく言うつもりはありません。nginxは、Mightyよりもはるかに高機能な Web サーバなので、比較自体に意味があるのかも分かりません。
とにかく、Haskellでも高性能なサーバが(簡潔に)書けること、7.8.1 以降の GHC を使えば、そのサーバはマルチコア環境でスケールすることを感じ取っていただければ幸いです。
もし、IOマネージャの実装に興味がある人がいるようでしたら、また別の機会に説明したいと思います。
執筆者プロフィール
山本 和彦(やまもと かずひこ)
株式会社IIJイノベーションインスティテュート(IIJ-II)技術研究所 主幹研究員
1998年IIJ入社。開発した代表的なオープンソフトに Mew、Firemacs、Mighty がある。「プログラミング Haskell」や「Haskellによる並列・並行プログラミング」の翻訳者。職場では Haskell、家庭では3人の子供と格闘する日々を送っている。
関連リンク
ページの終わりです