Saving Money by Switching from PHP to D – The D Blog を 許可を得て 翻訳しました。
誤訳等あれば気軽に Pull requestを投げてください。
2nightは イタリアのナイトライフとレストランにフォーカスしたオンラインマガジン として2000年に生まれました。 外出した時にできるクールなことを世に広めるという使命を維持し、 ブランドイベントやマスメディアではない非伝統的な手段によるマーケティングイベントの作成に特化するため、 長年に渡って成熟した経験的マーケティングエージェンシーにむけて発展をしてきました。
我々が2nightでDを使い始めたのは2012年にAndroid/iOSアプリから利用されるウェブサービスを開発したときでした。 それ以来問題なく動いていますが、小さな実験でしかありませんでした。 2019年、たくさんの実験のはてに、我々は大きな一歩を踏み出すと決めました。 我々はウェブサイト全体をPHPからDへ切り替えます。 ウェブサイトの新しい見た目を考えておりちょうどいいタイミングでした。 そして我々はインフラストラクチャ全体を書き換えるこの機会を掴んだのです。
開発
仕事は我々が考えていたよりも簡単になりました。 我々のMongoデータベースに対する小さなDバックエンドを数百行で書きました。 the NGINX server とのインターフェースのための Simple Common Gateway Interface (SCGI) ライブラリや、DOMのためのライブラリを作りました。 ボンヤリしたHTMLテンプレート言語を使うかわりにHTML DOMを使うことで開発はおおいに加速しました。 この方法ではHTMLやJavaScriptに関わる人間はDやテンプレート言語について学ぶ必要なしに、 プレインなHTMLやCSSファイルをデプロイできます。 一方で、バックエンドに関わる人間はHTMLタグについて考えなくて済み、要素にはID、クラス等で簡単にアクセスできます。 HTMLタグがページ中を動き回ったとしても問題なく動作します。 フロントエンドのHTML+CSS+JavaScriptとバックエンドのDは完全に独立しています。
この方法でコードを書くのは非常に簡単です。 たとえばブログページをビルドしたいとしましょう。 まずは以下のようなシンプルなHTMLファイルを作ります。
<!DOCTYPE html>
<html lang="en">
<head><title>Test page</title></head>
<body>
<!-- Main post -->
<h1>Post title</h1>
<h2>The optional subheading</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Proin a velit tempus, eleifend ex non, aliquam ipsum.
Nullam molestie enim leo, viverra finibus diam faucibus a.
Ut dapibus a orci in eleifend.
</p>
<!-- Two more posts -->
<div id="others">
<h3>Other posts</h3>
<div>
<h4>Post#2</h4>
<p>
Morbi tempus pretium metus, et aliquet dolor.
Duis venenatis convallis nisi, auctor elementum augue rutrum in.
Quisque euismod vestibulum velit id pharetra.
Morbi hendrerit faucibus sem, ac tristique libero...
</p>
</div>
<div>
<h4>Post #3</h4>
<p>Sed sit amet vehicula nisl. Nulla in mi est.
Vivamus mollis purus eu magna ullamcorper, eget posuere metus sodales.
Vestibulum ipsum ligula, vehicula sit amet libero at, elementum vestibulum mi.
</p>
</div>
</div>
</body>
</html>
これはHTMLを知る誰もが編集できる合法なHTML5ファイルです。 このテンプレートをデータベースからの実データで埋めなければなりません。 話を簡単にするために、この例ではデータベースは配列として表現されます。
// A blog post
struct SimplePost
{
string heading;
string subheading;
string text;
string uri;
}
SimplePost[] posts = [
SimplePost("D is awesome!", "This is a real subheading", "Original content was replaced", "http://dlang.org"),
SimplePost("Example post #1", "Example subheading #1", "Random text #1"),
SimplePost("Example post #2", "Example subheading #2", "Random text #2"),
SimplePost("Example post #3", "Example subheading #3", "This will never be shown")
];
最初に、HTMLテンプレートをそのまま読み、我々のHTML5ライブラリでパースします。
auto page = readText("html/test.html");
// Parse the source
auto dom = parser.parse(page);
そしてメイン記事のコンテンツを配列の最初の要素から得たデータに置き換えます。 正しいHTML要素を選択するためにタグ名を使います。
// Take the first element from our data source
auto mainPost = posts.front;
// Update rendered data of main post
dom.byTagName("h1").front.innerText = mainPost.heading;
dom.byTagName("p").front.innerText = mainPost.text;
dom.byTagName("a").front["href"]= mainPost.uri;
記事にサブタイトルがあるかをチェックしたいです。 存在しない場合は関連するタグを削除します。
// If we have a subtitle we show it. If not, we remove the node from our page
if (mainPost.subheading.empty) dom.byTagName("h2").front.detach();
else dom.byTagName("h2").front.innerText = mainPost.subheading;
同じことをテンプレート言語で行おうと思うと、こんなふうにHTMLを散らかさないといけないかもしれません。
<!-- We don't like this! -->
<? if(!post.subheading.isEmpty) ?>
<h2><?= post.subheading ?></h2>
<? endif ?>
これではロジックがビューに混ざってしまいHTMLファイル全体が混乱してしまいます。
HTMLフロントエンドで作業する人皆がpost
とは何なのか、このオブジェクトの背後にあるロジック、
そしてテンプレート言語そのものについて知っていることを想定しています。
最後に、多くのHTML編集者がカスタム構文にかき回されることになります。
さらに言うとこれはまだ単純な例でしかありません!
例に戻って、ページの最後のパートを埋めるにはDOMからコンテナを取得しなければなりません。 DOM上をIDで検索することが必要です。
auto container = dom.byId("others").front;
コンテナ内の最初の要素をテンプレートとして使っています。 なので最初の要素をクローンして、コンテナ自身は空にします。
// Use the first children as template
auto containerItems = container.byCssSelector(`div[id="others"] > div`);
auto otherPostTemplate = containerItems.front.clone();
// Remove all existing children from container
containerItems.each!(item => item.detach);
最後にデータソース内の各投稿を子要素としてコンテナに追加します。
// Take 2 more posts from list. We drop the first, it's the main one.
foreach(post; posts.drop(1).take(2))
{
// Clone our html template
auto newOtherPost = otherPostTemplate.clone();
// Update it with our data
newOtherPost.byTagName("h4").front.innerText = post.heading;
newOtherPost.byTagName("p").front.innerText = post.text;
// Add it to html container
container.appendChild(newOtherPost);
}
まとめると以下のようになります。
import std;
import arrogant;
// Init
auto parser = Arrogant();
// A blog post
struct SimplePost
{
string heading;
string subheading;
string text;
string uri;
}
/*
Of course real data should come from a db query.
We're using an array for simplicity
*/
SimplePost[] posts = [
SimplePost("D is awesome!", "This is a real subheading", "Original content was replaced", "http://dlang.org"),
SimplePost("Example post #1", "Example subheading #1", "Random text #1"),
SimplePost("Example post #2", "Example subheading #2", "Random text #2"),
SimplePost("Example post #3", "Example subheading #3", "This will never be shown")
];
void main()
{
// Our template from disk
auto page = readText("html/test.html");
// Parse the source
auto dom = parser.parse(page);
// Take the first element from our data source
auto mainPost = posts.front;
// Update rendered data of main post
dom.byTagName("h1").front.innerText = mainPost.heading;
dom.byTagName("p").front.innerText = mainPost.text;
dom.byTagName("a").front["href"] = mainPost.uri;
// If we have a subtitle we show it. If not, we remove the node from our page
if (mainPost.subheading.empty) dom.byTagName("h2").front.detach();
else dom.byTagName("h2").front.innerText = mainPost.subheading;
// -----
// Other articles
// -----
// Get the container
auto container = dom.byId("others").front;
// Use the first children as template
auto containerItems = container.byCssSelector(`div[id="others"] > div`);
auto otherPostTemplate = containerItems.front.clone();
containerItems.each!(item => item.detach);
// Take 2 more posts from list. We drop the first, it's the main one.
foreach(post; posts.drop(1).take(2))
{
// Clone our html template
auto newOtherPost = otherPostTemplate.clone();
// Update it with our data
newOtherPost.byTagName("h4").front.innerText = post.heading;
newOtherPost.byTagName("p").front.innerText = post.text;
// Add it to html container
container.appendChild(newOtherPost);
}
writeln(dom.document);
}
このプログラムは以下のような合法なHTML5ページを出力します。
<!DOCTYPE html>
<html lang="en">
<head><title>Test page</title></head>
<body>
<h1>D is awesome!</h1>
<h2>This is a real subheading</h2>
<p>Original content was replaced</p>
<a href="http://dlang.org">More...</a>
<h3>Other posts</h3>
<div id="others">
<div>
<h4>Example post #1</h4>
<p>Random text #1</p>
</div>
<div>
<h4>Example post #2</h4>
<p>Random text #2</p>
</div>
</div>
</body>
</html>
もちろん、他の方法、他の言語でも同様の結果を得ることは可能です。 我々のライブラリはModestと言うプレインCライブラリのラッパでしかありません。 違うのはDのパワフルで理解しやすい構文により読み書きしやすいことです。 上記のコードはある程度プログラミング経験のある人なら誰でも簡単に理解できます。 私はDを全く知らなかった同僚からプルリクエストを受け取りました。
これは全体像のほんの一部であり、我々は様々な目的のための様々なライブラリを使っています。
パフォーマンス
明らかに、パフォーマンスの面では大成功でした。 ウェブサイトはまるでローカルマシンで実行されているようで、大幅な速度の向上とレイテンシの低下が全面的に得られました。 切り替えの後、クラウドサービスのロードがあまりに低くてウェブサイトがダウンしたかと思いました! PHPからDへの切り替えにより各Amazon AWSマシンのインスタンスサイズは半分になりました。 それでもマシンにはまだ余裕があります。 データベースクラウドにも大きな影響が見られました。 使われる計算力は元の4分の1になっています。 すべては即時かつ大幅なコスト削減をもたらし、これまでと比べてかかるコストは半分以下になりました。
One more thing…
にもかかわらずローンチから数日後、我々は発生するコストに気づきました。 我々はウェブサイトに表示する画像のホストやカットのためにサードパーティサービスに依存しています。 正しく写真をクロップするには何が被写体化を認識してフレームに収まるようにしなければならないため、これは難しいタスクです。 旧ウェブサイトでは画像の形を固定しており、サードパーティサービスは特殊なケースに使用していました。 新しい2night.itではマスタ画像に対して様々なカットをする必要があり、コストは15倍になりました! 幸運なことに、OpenCV APIのDバインディングが利用できることに気づきました。 これを被写体を残したまま写真をカットするスマートなアルゴリズムの開発に利用しています。 そして再び、サービスのパフォーマンスはマシンを増やす必要がないほど素晴らしいものになりました。 1週間で写真に対するコストは月数千ユーロからほぼゼロに落ちました。