Wandbox は、オンラインでコンパイルや実行が試せるサービスで、主に私(@melponn) と @kikairoya で作っています。
以前 Wandboxを支える技術 というのを書いていたのですが、そこからかなり変わっているため、改めて今の中身がどうなってるかについて書いていきます。
Wandbox は、Wandbox 本体と、各種コンパイラをビルドするための Wandbox Builder に分かれています。
ソースコードは wandbox と wandbox-builder にあります。
Wandbox
犬小屋と牛舎
Wandboxはバックエンドとフロントエンドで分かれていて、フロントエンド側が 犬小屋(kennel) 、バックエンド側が 牛舎(cattleshed) という名前になっています。
犬小屋は、普通の Web サーバです。C++ で書かれています。
牛舎は、ソースを与えると、安全な環境でコンパイルや実行をして、結果を返してくれるアプリケーションです。C++ で書かれています。
ユーザがコンパイルや実行を行うと、以下のようになります。
- 犬小屋がPOSTでコンパイラの種別やソースコードを受け取る
- その受け取ったコンパイラの種別やソースコードを牛舎に投げる1
- 牛舎の中でコンパイルや実行を行う
- そのコンパイルや実行結果を、犬小屋が受け取る
- クライアント側に返して結果を表示する
とても分かりやすい 図が以前の資料にあったので、貼っておきます。
犬小屋
犬小屋は、主に私(@melponn)が書いた、普通の Web サーバです。
C++ と、C++ 用の Web フレームワーク CppCMS を使って書いています。
以前は Haskell と、Haskell 用の Web フレームワーク Yesod を使って書いていましたが、メンテが辛くなったので C++ に置き換えました。
Haskell をやめた理由
Haskell は真面目なアプリケーション開発には向いてない というのが、Yesod で Web サーバを書いて運用してきた私の感想です。
もうちょい具体的に書くと、
- ドキュメント無さすぎ。
- Haskell では "型は良いドキュメントになる" 的な話をよく聞くけど、その慢心があるせいなのか、ドキュメントがまともに書かれてないことが多い。なので「この場合どうなるんだ?」となって結局ソースコードを読みに行くハメになることがほんとに多かった。
- 言語とその周辺ライブラリの進化が早すぎ(少なくとも当時は)。
- 私はアプリケーションを作りたいだけなのに、言語と周辺ライブラリを 何度も 調査し直すことになって全然実装が進まなかった。Iteratee とか Enumerable とか Conduit とか。
- 新しい機能を使わなければいいだけって思うかもしれないけど、Yesod は当時の新しい言語機能やライブラリを大量に使ってたフレームワークなので、これを新しくするとその辺の機能を使うことが強制される。
- 互換性壊しすぎ。
- Haskell も Yesod も、簡単に互換性を壊すので、1ヶ月後ぐらいにアップデートして 何もせずコンパイルが通ることなんてまず無かった 。
- なので結局 Yesod も Haskell もバージョンを固定してアップデートせずに使うようになった。これでしばらく平和が訪れてたんだけど、ある日 Yesod のビルド方法が変わって、既存のビルド方法すら deprecated になった 時点で心が折れた。もうこいつらには付いていけないと思って C++ に移行することにした。
C++ を使う理由
当時、私が普通に書ける言語は C++, C#, Python あたりでした。
Haskell で書いていたので、それより遅くなるのはできるだけ避けたいというのがあって、Python は除外。
C# では Web サービスをまともに書ける気がしなかったので除外。
Go や Rust あたりも一応考えた気はするのですが、当時の私の中では安定してない言語というイメージだったのでやめました。
Haskell では互換性で泣かされてたので、出来る限り安定した言語を使いたいというのがありました。
ということで、安定の C++ です。
高速だし、ドキュメントは揃ってるし、進化はゆっくりだし、互換性が壊れない。
やCN1(やっぱりC++がナンバーワン)2。
GitHubのログを見る限り、22日間で移行を完了しています。
2014年9月15日に C++への移行を開始 して、2014年10月7日に C++への移行を完了。
数年掛けて書いてきたコードがこれだけ早く移行できて、やっぱり Haskell は難しい言語だったんだなと思った記憶があります。
犬小屋の中身について
犬小屋はただの Web サービスなので、普通にリクエストを受けて、レスポンスを返しているだけです。
レスポンスをリアルタイムに返すために EventSource を使っているところがちょっと特殊かも。
でもこれも CppCMS の Asynchronous Applications あたりを読んでやればすぐ分かるので、そんなに難しくありませんでした。
ということで、Web サービスを作れて、C++ が読める人にとっては普通に読めるコードだと思います。
牛舎
牛舎は、主に @kikairoya が書いた、ソースを与えると、安全な環境 でコンパイルや実行をして、結果を返してくれるアプリケーションです。
当時は Docker なんていう便利なアプリケーションは無かったので、大体同じようなことを内部でやっています。
なお牛舎に関しては、彼曰く
@melponn 自由でオープンなソフトウェアなので誰でも内部を理解し、応用することができます
— 対鉱物用武装 (@kikairoya) 2013年10月18日
なので、以下の説明は私がコードを読んで理解したものになります。3
牛舎が閉じた環境で実行するまでの流れ
- 犬小屋からコンパイラの種別やソースコードを受け取る
- ソースコードを実行用ディレクトリに保存する
-
setrlimit
を使って優先度やCPU時間やメモリ量、ファイルサイズなどの各種リソースを制限する -
clone
を使って、ネットワークやPID、マウント等の名前空間を新しく作る - ループバックアドレスやマウントを新しく作り、
chroot
で実行用ディレクトリをルートディレクトリに変更する - 任意のプロセスを実行する
- 出力やシグナルをリアルタイムで受け取って犬小屋に返す
となっていて、3. でリソースが制限され、4. や 5. でネットワークや読み書きできるディレクトリが制限されているので、牛舎が壊されたりパスワードが盗まれたり踏み台にされたりといった問題が起きないようにしています。
あとは root 以外のユーザでも新しい名前空間を作ったり chroot
するために、事前に牛舎の実行バイナリに対して setcap
コマンドで権限を与えています。
Boost
牛舎では Boost が使われています。
Boost.Asio, Boost.MultiIndex, Boost.Spirit 等が使われていて、コンパイル時間に寄与 しています。
犬小屋と牛舎の外側
Wandbox のページは、以前は http://melpon.org/wandbox だったのですが、今は https://wandbox.org/ になっています。
独自ドメイン化と、HTTPS 対応をしました。
HTTPS っていまいちよく分かってないですが、SSL Server Test で A+ を貰ってる ので、多分安心です。
後は無駄に HTTP/2 にも対応してます。
Wandbox Builder
以前はインフラ周りに Chef を使っていましたが、今は Docker とシェルスクリプトに置き換わっています。
以前から、Chef のレシピはほぼシェルスクリプトを実行するだけのレシピと化していて、これなら単にシェルスクリプト流すのと変わらないと思っていました。
なので Wandbox を複数台で運用することにしたタイミングで Chef から Docker+シェルスクリプトに移行しました。
サーバ構成
以前は1台の中で、犬小屋、牛舎、コンパイラのビルドを行っていました。
しかしコンパイラのビルドと、ユーザが送ってきたコードをコンパイルする処理が重なると、CPUやメモリが厳しいことになっていたので、頑張って3台に垂直分割しました。
それぞれ以下の用途になります。
- 犬小屋サーバ4
- 牛舎サーバ
- ビルド用サーバ
ビルド用サーバでビルドしたコンパイラを rsync
で牛舎サーバに送ることで、牛舎用サーバに入っているコンパイラを更新しています。
コンパイラのビルド
以前の環境では、全てのコンパイラのビルドを1サーバ内で行っていましたが、この方法でビルドするのは厳しいものがあります。
クリーンな環境でコンパイルしていない場合、コンパイルするために必要なパッケージが何だったのかを把握できません。
そのため、新しい環境でビルドスクリプトを実行してみるとビルドに失敗することが本当に多いのです。
ということで ビルド環境とビルドスクリプトはセットになっている必要がある という結論に至りました。
丁度 Docker という、仮想マシンより手軽に仮想環境が手に入る時代になっていたので、それを使うことにしました。
コンパイラのビルドは、そのコンパイラ用に作った Docker 環境内で行います。
成果物だけ、マウントしたディレクトリに出力するようにしています。
これでビルドスクリプトは常に同じ環境で実行されるようになるので、ローカル環境が何であっても安定してビルドできるようになりました。
副次的な効果として、ローカルには Docker と git だけ入れておけば、誰でも簡単に各種コンパイラのビルドが可能になっています。5
また、ビルドに使った Docker 環境は Docker Hub に置いている ので、イメージをビルドする必要もありません。6
なお、コンパイラを追加する方法は ドキュメントに書いてる ので、Wandbox にコンパイラを追加して欲しいと思ったら pull req 下さい。
コンパイラのテスト
コンパイラのビルド時に必要な環境と、実行時に必要な環境は異なります。
ビルドが成功して本番サーバ(牛舎サーバ)にコピーしたはいいけど、本番サーバで実行してみると、ダイナミックライブラリが足りなくてクラッシュする のはよくあることです。
コンパイラが動くかを本番サーバに適用する前に確認するため、本番サーバと同等の環境を Dockerfile で提供しています。
この Docker 環境を使ってテストすることで、本番サーバでエラー無く動かせるようにしています。
実際、この Docker 環境でテストして動いたのに、本番サーバだと動かないということは 一度もありませんでした。
本番サーバは全コンパイラが実行できるようにしないといけないので、必要なパッケージは膨れ上がります。
静的リンクを駆使して、可能な限り実行時に必要なパッケージを減らしているのですが、それでも結構いろいろと入れるハメになっています。
ビルドを書きまくった感想
コンパイラのビルドは本当に大変だというのがよく分かりました。
このあたり を見て貰えれば分かると思いますが、ほんとビルドする環境の数が多い。
それに加えて、バージョン毎の分岐もあるというのが大変です。
例えば Clang のビルドスクリプト では、ビルドするためにバージョン毎に分岐を大量に入れる必要がありました。
また、Clang のコンパイルコマンド にもかなり分岐が入っています。
また、ブートストラップ問題 にもよく引っかかりました。
通常、最初のコンパイルで使用するコンパイラは、apt-get
でインストールしておくのが多分一番簡単です。
通常、古いコンパイラをビルドする場合、そのコンパイラは古いコンパイラ自身でコンパイルされています。
つまり古いコンパイラは、その古いコンパイラでのみコンパイルが通ることを確認しているのです。未来のコンパイラなんて取ってこれる訳が無いので当然です。
そして apt-get
でインストールされるコンパイラは(比較的)新しいコンパイラになります。
つまりどうなるかというと、古いコンパイラを、新しいコンパイラを使ってコンパイルする ことになります。
この場合、古いコンパイラがよっぽど互換性を持っていない限りはビルドに失敗します。
大体の古いコンパイラはこれが原因でビルドに失敗しました。
いくつかはパッチを当てることでビルドさせたりもしたのですが、そのバージョンをビルドすることを諦めた言語も結構あります。
別の安定した言語、例えば C++ あたりでコンパイラを書いてくれていれば、こんな問題に引っかかることも少なかったんじゃないかなと思います。
まとめ
ということで Wandbox は上記の技術を使って書いています。
Wandbox の資産は 大量に用意されたコンパイラ なので、ビルド周りは特に頑張って改善しました。
これらの資産を活用しつつ、今後もサービスを続けていこうと思います。
なお Wandbox はいつでも スポンサーを募集 しているので、melponのやる気を出させたい、OSSにお金で貢献したい、Wandbox に自分の名前を載せたい、などあれば是非スポンサーになってみて下さい。
-
犬小屋と牛舎の間は、TCP の上で自作のプロトコルを定義してやりとりしています。普通に JSON 使うだけだと TCP で受信するべきバイト数が分からないので困る。 ↩
-
元ネタはやっぱりカープがナンバーワン ↩
-
なぜそうしたのか?というのに関しては @kikairoya に直接聞いて下さい ↩
-
Wandboxと関係ないサービスもいくつか入っています。melpon.org の サービス とか、cpprefjp の 変換サーバ とか。 ↩
-
当然 Wandbox 上で動くことだけを目的にしてバイナリを作っているので、ビルドできたからといって他の環境で動くとは限らない ↩
-
Dockerfile もコミットしてるので、必要であればビルドすることも可能。当然 Docker Hub の melpon/wandbox に push することはできませんが。 ↩