はじめに
WordCampTokyo 2015が今週末開催されます。
アドベントカレンダーに参加はしてないのですが、WordCampTokyo 2015で月間1000万PVのサイトの講演があることに触発されて、WordPressで大量のコンテンツのウェブサイトを作った話をエンジニア視点で書いてみます。
あらまし
あるとき自社開発のウェブサービスで「とにかくPVを稼ぐサイト」を作ることになりました。
「PVさえあれば広告費は勝手に付いてくる」ということで「とにかくPVを稼ぐサイト」の作成にとりかかりました。
とはいえ自分はあくまでエンジニアで、集客のできるコンテンツを作っていく自信はありませんでした。
そこでチームで相談して、「海外のサイトをクローリングして機械的に日本語翻訳させるウェブサイト」を作ることにしました。
当時流行っていた言葉を借りて良く言えば「マッシュアップ」、悪く言えば「パクり」サイトの作成となりました。
(とはいえまだクローリングという言葉も浸透していないぐらいでしたし、機械的な翻訳で日本人向けに作成することは一定の意義があったかなと思っています)
クローリングしてウェブサイトを作る、ということでCMSの採用を検討しました。
当時のCMSといえばMTがまだまだメジャーで、自分が利用していたこともありMTで開発をスタートさせました。
しかしながらすぐに性能限界が近づきます。コンテンツを追加するとHTMLの再生成を行うMTでは1万件ぐらいで再生成が終わらなくなってしまいました。
一日に数万件の記事をアップしていたのですぐにこれではダメだ、ということになりました。
そこで検索してヒットしたWordPressを利用する事を決断しました。
当時はWordPressのバージョンが2.3ぐらいだったと記憶しています。
このころはカスタムタクソノミーどころかタグ機能すら無くてカテゴリをカスタマイズしてタグにしていた記憶があります。
そんな中で開発がスタートし、記事をどんどんプログラムでWordPressに追加していきました。
前置きが長くなってしまいましたが実際に利用したテクニックについて述べていきます。
環境など
人員
- 兼任プロジェクトマネージャ1名
- 専任エンジニア 2名 (ここに配置)
デザイン、HTMLコーディングは外注しました。
デザインについては僕たちも素人で、デザイナーさんに対して「お客さんがたくさんきてくれるような構成にして欲しいと」言って、本来であればディレクターのするべきことなど、色々と構成の組み直しをお願いしました。
今になって振り返ると、デザイナーさんにあそこまで作ってもらったのは酷だったなぁと反省しています。
でもそもそもディレクターが居なかったですし、存在も知りませんでした。
サーバ
当時は円高が始まる頃で今ほどスマホタブレット市場も無くパソコン関連の価格が割安でしたので、自作サーバ最盛期でした。
ホスティングやハウジングは利用せず、後に鼻毛鯖と命名されるExpress5800をカスタマイズして使っていました。
鼻毛鯖にnon-ECCメモリを16GB〜32GB、CPUに初代Core i7を積んで一台当たり5万円ぐらいで作成しました。
トータルで10台ぐらい導入しました。
ネットワーク環境
ウェブサーバを設置する回線は普通のISP回線を利用しました。
会社で契約しているISP回線は何度か帯域制限を喰らってしまいました。
社長の個人宅のISP回線が余っているとのことで会社と社長宅をそれぞれをVPSで接続したうえで静的ファイルをrsyncさせ、CDNとして公開して運用していました。
また海外のウェブサイトの翻訳だったので海外に踏み台用のサーバを1000円/月ぐらいでいくつか契約していました。
2015年現在では自宅サーバよりクラウドやVPSのサーバ環境、ネットワーク環境がかなり安くなってきていると感じています。
自宅サーバは円安や需要低減によるコスト髙を感じていますが、例えばバックアップ目的などにはクラウドと併用すると良いと考えています。
大規模サイトに使えそうなテクニック
ここからは実際に使っていたテクニックです。
管理ソフトウェア
WordPressの記事が増えてくると管理できなくなります。
管理画面はそのままでは開けません。特に記事やタグの読み込みがひどくすぐに使わなくなりました。
記事の追加などを行うソフトウェアは自前で書き、細かいデータ変更は手動のSQL発行とテーブルの直接編集を行っていました。
エンジニアはテーブルの直接編集もそれほど苦ではありませんでした。
しかしプロジェクトマネージャはテーブル編集が出来ませんでした。
後に類似のサービスを立ち上げましたが、その際にはそういった非エンジニアのために管理部分はCakePHPで専用の管理画面を設けました。
WordPressフロントエンドを担当してバックエンドは他のフレームワークを使う、というやり方はなかなか良いかなと思います。
MySQL
ご存じの通りWordPressではバックエンドDBにMySQLを採用しています。
日々そこそこ大量のデータが入ってきますので更新系だけでもサーバはめいっぱいになっていまいました。
WordPressのデータなので容量はそれほどではなく、終盤で100GB、他に管理用で100GBぐらいでした。
用途別にDB自体をいくつかに分割して物理サーバごと分けました。
運用開始からしばらくしてSSDが普及し初め、120GBで一万円ちょっとだったのでそれに載りきるようにしていました。
水平分割
MySQLは更新系(UPDATE, INSERT)と参照系(SELECT)を分割しました。
更新系を1台、参照系に最大で3台を割り当てました。
更新系から参照系へは標準のレプリケーションを利用しました。
クローラから引き出されるデータは全て更新系に書き込み、参照系では実際にウェブサーバから読み出されるWordPressのテーブル群を格納していました。
WordPressは標準ではレプリケーションによる読み出しをサポートしていません。自前のDB振り分け機能を実装していました。
この振り分け機能はDB負荷を分散させる上で非常に有用でしたが、同時にWordPress側からの更新は非常に行いづらくなっていました。
例えばコメントを付けようと思ってもWordPressが繋いでいるMySQLは参照系ですから、そのままではコメントを付けることが出来ません。
しかしながらサービス自体が提供者->利用者の一方向性のものでしたからコメント機能は重要視しておらず問題にはなりませんでした。
(とはいえ現在から振り返ってみるとこの辺りのサービスの戦略やディレクションについては非常に甘かったと思います。コメント機能などを拡充してもっと利用者によりよいサービスを提供すべきだったな、と反省しています)
WordPress側からもデータの更新が行えましたが、ウェブサーバが一旦引き受けて更新系のMySQLに繋がったサーバを経由して書き込んでいました。
コメント機能についても同様の手順で実装できたと思います。
水平分割を行うコード
以下のコードをwp-config.phpに読み込ませていました。
// ラウンドロビンで回す秒数
define("_ROUND_ROBIN_TIME_", 180);
// metricは高い方が優先される
// ringをtrueにするとラウンドロビンに参加、falseなら不参加
$dbhosts = array
(0=>array
('host' => 'buren',
'metric' => 0,
'ring' => false,
),
1=>array
('host' => 'polk',
'metric' => 0,
'ring' => true,
//'ring' => false,
),
2=>array
('host' => 'tyler',
'metric' => 0,
//'ring' => true,
'ring' => false,
),
);
// getCurrentHostはラウンドロビン, getRandはランダム
//$key = getCurrentHost($dbhosts);
$key = getRand($dbhosts);
define('DB_HOST', $dbhosts[$key]['host']);
function getCurrentHost($dbhosts){
// 1970/01/01 00:00:00がスタート
$unixtime = time();
foreach($dbhosts as $key => $host){
if($host['ring']){
$keys[] = $key;
}
}
$kekey = ($unixtime/_ROUND_ROBIN_TIME_)%count($keys);
return $keys[$kekey];
}
// ランダムな振り分けを行ってくれる関数
function getRand($dbhosts){
$ttl = 0;
foreach($dbhosts as $dbhost){
$ttl += $dbhost['metric'];
$path[] = $ttl-1;
}
$rand = rand(0, $ttl-1);
foreach($path as $key => $val){
if($rand-1 < $val){
break;
}
}
$key;
//echo "$rand $key\n";
return $key;
}
...
include 'db-sort.php';
...
ウェブサーバ
当時はapache+mod_php一択でした。運用開始後ようやくlighttpdが脚光を浴び始めた時代で、nginxはまだ日の目を見ていませんでした。
apacheのリバースプロキシを導入し、PHPを動作させるサーバを複数台用意していました。
ウェブサーバとMySQLが複数台ずつあったので、セション管理は大変そうです。
大変そう、と書いたのはサービスとしてログインなどのセション管理が無かったからです。この点は非常に助かりました。
静的ファイル化によるキャッシュは効果が高かったです。
うろ覚えですが当時はSuperCacheがあまり有名ではなかったこともあり、cronとwgetと.htaccessを組み合わせて自前でキャッシュしていました。
.htaccessを機械生成していたので数千行に及んでいたような気もしますが、それは夢だったのかもしれません。
サイトマップ
大量コンテンツで苦労した物の一つがサイトマップです。
検索エンジンからの流入に頼っていたのでサイトマップの作成は高優先度でした。
しかしながら件数が膨大で、普通のWordPressのプラグインでは全く以て作成できません。
結局自前でサイトマップを作成していました。
上の図はサイトマップの更新をやめてしまう前の情報で、300万件を送信していますね。
今も使えるか微妙なテクニック
以下は当時導入したテクニックです。今でも利用できるかどうかは疑問が残ります。
memcache
MySQLの中間キャッシングとしてmemcacheを導入していました。複数台のウェブサーバがある環境でのmemcachedが当時流行していて、これがパフォーマンスを上げていました。
実はWordPressでは本家にてmemcachedを利用する仕組みを提供していて(うろ覚えです)、お手軽に導入できました。
現在でも利用できるかは不明ですがもしかすると効果があるかもしれませんね。
本体への手入れ
軽量化のためWordPressの本体も改造していました。
デフォルト状態だと目立って遅かったのがsingleの表示周りにある前後の記事を拾ってくる箇所でした。
この辺りについて必要の無い箇所はぶった切っていきました。
またquery stringを削って軽量化する仕組みも組み込んでいました。
しかしながら当然ですが、本体へ手を入れる、というのは引き返せない麻薬みたいなものです。
一度本体に手を入れてその速度を味わってしまうともう後戻りできません。
後戻り、というかバージョンアップができません。
WordPressは当時でも、現在でも、非常に精力的にバージョンアップを続けています。個人的にはそこがWordPressの魅力だと思っています。
バージョンアップ出来ない、と書きましたが、実際にはバージョンアップできるのですが本体に当ててしまった自作のパッチが足かせにり、頻繁に更新していたWordPress本体にパッチを書き続けることが不可能になりました。
そのため最終的には2.9.2で止まってしまいました。
最近ではフックの数が右肩上がりに増えており、重要な箇所にフックを入れてチューニングを含め様々なことができるようになりました。
しかしながら2015年現在でもフックの数は足りない、と個人的には思っています。
ただフックを増やすだけでは性能的、見通し的にに悪くなってきます。
本家のコードの傾向は追いかけられていませんが、クラスのインタフェース化などによるウォーカーなどを利用したオーバーライドで対応していけるのかなぁ、と考えています。
その他
当時の環境など雑多な記録です
OS
サーバOSはFreeBSDを利用していました。
パッケージ管理にportsというものを使っていました。
makeに時間が掛かるのですがRedhatのrpmで依存地獄にはまったことがトラウマでしばらくFreeBSDを使っていました。
ファイルシステム
データベースのファイルシステムにはFreeBSD/Solarisお得意のZFSを使っていました。
MySQLのレプリケーションが壊れることが多くてそのバックアップに活躍してくれました。
ZFSではHDDのスナップショットを取ることができるのですが、
MySQLダウン -> スナップショット -> MySQLスタート -> スナップショットの転送
とすることでコールドバックアップを手早く行っていました。
このやり方は他に聞いたこと無いですがなかなか良かったなぁと思っています。
リソース監視
サービスの死活監視やリソースの履歴はcactiを利用してました。
リソースグラフがあればサーバの状態を相対的に見る事ができるので非常に便利でした。
MRTG+RRDToolに比べてすごく簡単になった!と喜んでいましたがcactiの運用は結構時間を取られることが多く、使いこなしているとは言えない情況でした。
現在ではmuninがお手軽で凄く良いですね。
現在でもアクセス系のGoogleAnalyticsなどとサーバリソースの対応は必ず取るようにした方が良いです。
サーバが重くて人が帰ってしまったのかどうかは計らなくてはなりません。
バージョン管理
バージョン管理にSubersionを採用しました。それまではCSVを見よう見まねで使っていたのですがうまく使いこなせていませんでした。
この案件でようやくSubversionを使えるようになりました。
よく悪い例として言われますが、バージョン管理がバックアップを兼ねていました。
バージョン管理にはとても助けられました。
現在ならgitですね。
プロジェクト管理ツール、BTS
当初はtracを使っていました。
現在はredmineです。
日本語検索
WordPressは日本語検索が遅いです。
(そういえば現在はどうなっているのか、もしかすると改善しているのかもしれませんね)
サービスを開始してすぐに日本語検索の遅さに困ることになりました。
こちらについては大量のデータを保持しているサービスの都合上、検索を重視していましたので検索が遅いことは致命的でした。
検索が遅いことの原因は日本語であるためにフルテキストインデックスが張れていないことです。
そこで日本語検索専用のシステムを構築しました。
サマリテーブルとよばれる、WordPressのテーブルから日本語検索の対象となるもののみを抜き出したテーブルを作成し、そこをフルテキストインデックスが張れるTritonnパッチドなMySQLサーバに置きました。
http://gihyo.jp/dev/clip/01/groonga/0006
(現在は標準でフルテキストインデックスが張れるようですね、良いな!)
https://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CB0QFjAAahUKEwjNmMrC697IAhUGEpQKHblNBow&url=http%3A%2F%2Fqiita.com%2FArimaRyunosuke%2Fitems%2Fd2b3b94f223cb83c463d&usg=AFQjCNEeg5NsjAnCv_U0w_wJpvlC1r1Stg&sig2=rlhDGIBo-0NYAOf16GZA7w&bvm=bv.105841590,d.dGo
WordPressの日本語検索をサマリテーブル参照にする部分については本体に手を加えることで実装しました。
サマリテーブルについては一時間おきに再生成しました。
再生成だけでも時間がかかるためtempテーブルに作成し、alter tableで入れ替えていました。
サマリテーブルを作ってalter tableで入れ替えるというのは便利でした。
更新系プログラム
更新系プログラムも全てPHPで書いていました。このテクニックは現在ではあまり価値が無いと思っています。
更新系プログラムの実装に最も時間が掛かりました。
エンジニア2名の練度もそこまで高くなく、手探りの状態でスタートしました。
翻訳周りのルール、DBへの書き込みの動作も複雑でした。
PHPが4系から5系に変わりつつある時代で、「クラス化」を勧めれば良いコードになる、という合っているのかどうかわからない理論でクラス化を勧めました。
その結果インスタンスを作成するのに10秒ほど掛かるようになってしまいました。インスタンス化だけで10秒ですからひどいものです。
なんとか苦肉の策で載りきるために、PHPの処理系をデーモン化し、プロセス間通信で載りきることにしました。
ちょうどapacheのプリフォークをモデルにしました。
今ですともっと良いコードが掛けて、プリフォークしていなくともインスタンス化が早いはず、と信じています。
(そう思っているのは当人だけかもしれませんが!)
コミュニティ
開発を始めてしばらくして、大阪で開かれたWordPressユーザーズグループ(?の)懇親会に参加しました。
勉強会などに参加したことがありませんでしたので、僕にとって初めてのコミュニティ活動への参加ということで色々とビックリした覚えがあります。
何より「一人じゃ無い」という感覚は凄く救われました。
その後はWordCampや地方のWordBenchにお邪魔していました。
その経験をもとにして後には自分の住んでいる地域でのWordBenchの立ち上げを行うことができ、こちらでも良い出会いがたくさんありました。
地方での開発は孤独感が強いですが、勉強会やセミナーはありがたいですね。
コミュニティが暖かいことはWordPressの良いところですね。
反省点
最も重要だった更新系プログラムが悪かったことが原因だったと反省しています。
早い段階からフレームワークを導入し、リファクタリングの習慣をつけるべきだったと思います。
経験者、熟練者がチームいなかったことも難しかったと思います。せめて外部顧問のような形ででも指針やレビューをもらっていれば、と思います。
HDDの故障やサーバの突発的な過負荷によってすぐに疲弊しましたが概ね楽しかったです。
監視系にまで手が回らず、監視ルールを作る->プログラムやサーバの構成を変更する->障害でメールが止まらない、ということは良くありました。
キャリアのメールボックスが何度一杯になったか分かりません。
サイトの終わり
公開から二年ほどでアクセス数が500万PV/月ぐらいにまで伸びました。しかしながらソースコードも構成もひどいもので、とても保守できませんでした。
マネタイズも失敗しまいた。エンジニア二人が食べて行くほどお金を稼ぐことができませんでした。
その後は放置する、ということで緩やかにサイトは閉鎖されていきました。
まとめ
初心者に毛が生えたぐらいのレベルでスタートでしたが、全ての期間を通じてエンジニアとしては非常に良い経験になりました。
特にramディスクは汎用性が高い上に効果が上がることが多く、最近もその手法でのりきりました。
静的ファイルのキャッシュはめちゃめちゃ早いです。
キャッシュはしっかりとした戦略をたてることで効果を発揮します。更新が多いサービスなのかどうか、どれぐらいキャッシュさせるべきかといったことを想定してキャッシュを組む必要があります。
当時は手動でキャッシュを作っていましたが現在ならWPSuerCacheがすごく便利ですね。
例えば
「MySQLのサーバが死んだ!半日ぐらいで復旧させなきゃ!」
「ISPから規制喰らったけど負荷分散させなきゃ!」
などとその時々で問題になる事柄に全力で取り組むことが勉強になった思っています。そしてまだまだ足りない部分もあるのでこれからも全力で取り組んで行かねばと思います。
改めまして今週末にはWordCampTokyo2015が開催されます。
みなさんよかったら来てくださいね、と書こうと思ったんですがすでに満席なのですね。
ご来場の方はどうぞ宜しくお願いします。