持ち歩きやすくて高画質との思いから購入した「RICOH GR DIGITAL II」。
おかげですっかり写真の魅力にハマっている開発部の大芝(おおしば)です。最近ではmixiのコミュニティ機能を用いて社内フォトコンテストを開催するなど、もうその勢いは止まりません。

今回は、mixiの主にフロントエンドを開発しているソーシャルグラフ開発チームについて紹介させていただきたいと思います。

■はじめに
ソーシャルグラフ開発チームには、二つのミッションがあります。

一つ目はユーザー数の増加。
二つ目はソーシャルグラフ(人と人とのつながり)の拡充となります。

mixiに置き換えると「招待登録」や「マイミクシィ」関連部分がそれに当たり、同時に主な担当範囲となります。

というわけで当チームではmixiのなかでも比較的クリティカルな部分を担当することになります。したがって影響範囲も非常に大きく、他の開発チームをはじめサービスプランナーやデザイナーと密にコミュニケーションを取りながら進めていくことになりますので、技術の面だけでなく、サービスそのものに対しての考え方についても要求されます。

■事例紹介
それでは、最近リリースしたモノについて担当メンバーの方から紹介させていただきます。

●マイミクシィ一覧・管理機能のリニューアル

開発部のytkこと長島(ながしま)です。この寒い冬を社内の仲間と共に、夜な夜なボンバーマンWiiの爆弾で温まっています。

今年7月にリリースされたマイミクシィ一覧・管理機能について紹介させていただきます。

既にみなさんお使いいただいているかと思いますが、今回のリリースではマイミクシィ一覧・管理においてデザインや操作性が一新されました。

◇マイミクシィ一覧
list_friend
◇マイミクシィ管理・設定
manage_friend

これは、mixiの根底にあるマイミクシィ関係(ソーシャルグラフ)の閲覧制・操作性を向上させることで、今後拡大していくソーシャルグラフにも耐えられるような設計にしつつ、その心理的負荷を下げるというコンセプトが元になっています。

その他、効果を最大限に発揮させるために、次の機能も併せて実装しています。

・マイミクシィ一覧の並び替え機能(ログイン時間順、ニックネーム順の追加)
・グループ絵文字アイコン
・グループの並び替え
・マイミクシィの個別設定機能の強化

並び替え機能については、SQLでの並び替えができない設計上の制約があったため、事前にライブテストを行った上で、一度全てのマイミクシィ情報を取り出してからmod_perl 側で自前ソートするという工夫をしています。また、ニックネーム順については単純な文字コードによるソートではなく、使い勝手を考えて ひらがな or カタカナ、漢字、英数字、記号の順にソートを行えるよう実装しています。

更に今年10月には第二フェーズとして、グループ別のマイミクシィ一覧ページに、「このグループに日記を書く」機能を実装しました。

この機能は私自身のアイデアから実装に至ったものなのですが、企画者と開発者が一緒になって企画~リリース~検証までを行っていくというプロジェクトの進め方が生んだいい例ではないでしょうか。

今後もソーシャルグラフ拡大のお手伝いができるよう、邁進していく次第であります。

●18歳未満への登録開放

SSDと聞くとスコット・スタイナーと馳浩さんを思い浮かべてしまう、開発部のKCです。最近、私が担当した施策について簡単に紹介させていただきます。

私の所属するソーシャルグラフ開発チームは、mixiの根幹機能の改修や実装に携わる事が多くあります。12月10日から始まった18歳未満のユーザーへの登録開放に伴うプロフィール周りの改修もその一つです。

この施策では、

・ユーザー登録の際、年齢入力を必須化
・15歳から17歳まで登録を開放
・18歳未満ユーザーへのコンテンツの表示制御
・18歳以上ユーザーへの検索制限

という改修を行いました。

様々な状況下におけるテストでは、ケースの洗い出しなど大変苦労しましたが、ケースレビューをきちんと行い、テストそのものを入念に行うことで、何とか無事リリースすることが出来ました。

mixi利用制限の緩和第一弾であり、これから行う予定になっている数々の施策によって、より多くの方にmixiをご利用いただけるようになっていくのではないでしょうか。その際、安全で安心感のあるサービスである事、ソーシャルグラフの拡大を手助けできるサービスである事を念頭に置きつつ、日々精進して参りたいと思います。

●マイミクシィ申請フロー改善

2008年の春に新卒で入社した開発部の矢部(やべ)です。毎月10回ほどライブに行くような音楽好きです。

ソーシャルグラフ開発チームに配属されて最初に担当した案件が、このマイミクシィ申請フロー改善でした。具体的な改善内容は以下です。

・申請時のメッセージをメッセージボックスに保存
・マイミクシィ拒否の確認画面を追加
・マイミクシィ承認時に相手のホームに赤字アラートを表示
・承認者一覧の表示
・画面デザインの変更、修正

アカウントがひとつではテスト出来なかったり、申請方法がいくつもあったりと、初めての案件としては難しい部分もありましたが、申請時のメッセージの保存は、機能要望でも挙がっていた部分なので、ユーザーの意見を反映できたという点が嬉しく同時にやりがいを感じました。

また、開発が終了した後には、テスト、コードレビュー、QAという工程を経ることで、mixiがサービスとしての品質を保っていることを知りました。安心してみなさんに使っていただけるサービスを提供するためには、入念なチェックが必要であることを認識し、それと共にmixiの開発者としての責任の大きさを改めて実感しました。

●紹介マイミクシィ画面刷新

続けて恐縮ですが同じく矢部です。

こちらは、つい最近リリースされたばかりのものです。
◇紹介マイミクシィ
紹介マイミクシィ

mixiに友人を招待する際にマイミクシィを紹介する機能を今年6月にリリースしましたが、その紹介者選択画面のデザインを一新し、細かな改善を行いました。これにより他ページとの統一性が生まれ、ユーザビリティも向上したと思います。それほど大きな案件ではありませんが、初めてプロジェクトを任せられた案件だったので、JavaScriptやプロジェクト管理のスキルアップに加え、無事リリースできたことで大きな自信を得ることができました。

■最後に
そんな中、ソーシャルグラフ開発チームでは仲間を大募集中です。例えばこんな人材を求めています。

  • 常にユーザー視点に立ち、全体の利用者を意識した考えができる人
  • マイミクシィ周辺を改善させるための具体的な施策をお持ちで、それを自ら実現させたい人
  • サービスプランナーとコミュニケーションを取りながら、モノづくりをするのが好きな人
  • 技術のみに縛られず、技術はモノを実現させるための道具と考えられる人
  • SNSをはじめとした他サービスの動向に興味を持っており、ウェブサービス(特にソーシャル系)に精通している人

以上の項目にピンと来た方、我こそは!という方はFind Job !から応募してください。
mixiをより一層盛り上げていけるような施策を一緒に作っていきましょう!!

コードは掲載しないmilanoです。
こんにちは。

先週のことになりますが、以前紹介したOpenSocialプラットフォーム(通称:mixiアプリ)をパートナー企業向けにリリースしました。
プラットフォーム開発チームのメンバーたちに++です。

同時に、mixi Developer Centerもプチリニューアルし、mixiアプリの情報を公開し始めました。

mdc_img01.jpg

早速いくつものメディアに取り上げていただいています。

ありがとうございます。

opensocial.jpg
当初はパートナー企業向けですが、そのうち個人の開発者にも公開を考えているようです。個人の方はもうちょいお待ちください。
パートナー企業への申し込みはこちらからどうぞ

今後どんなアプリケーションが登場するか、とても楽しみです。
環境を開放してまだ1週間経っていませんが、すでにいくつかのアプリケーションを登録していただいているようです。

mixiアプリのデベロッパーの方々からのフィードバックを受け、いろいろなAPIを追加していくことも考えています。
デベロッパーと我々が協力しあって素晴らしいプラットフォームを作り上げて行ければと思っています。
よろしくお願いします。

それと、高橋名人も審査員のmixi OpenIDコンテストもよろしくお願いします。
※応募期限が延びました!

朝晩冷えてきましたね。風邪など引いていませんでしょうか。さて、年末が近づいてくるこの時期に弊社のエンジニアが最も気になるのは、お正月。それも来年1月1日を迎えた瞬間です。

1日1日0時に何があるのでしょう?そう、mixiのサービスで最も日記が書き込まれるタイミングになるのです。個人的に「あけおめことよろアタック」と呼んでいます。今年は日記だけではなく、エコーでもメッセージが飛び交うことでしょう。この時期は携帯電話のキャリアでもさまざまな対策を行っていますが、ミクシィでも年末年始でもユーザの方に快適にサービス提供ができるように努めています

以下は昨年の年末年始の日記投稿数の推移です。青色が12/31から1/1、赤色が1/1から1/2になります

diary_posting.png

1/1の方が全体的に多いですが、特に年が変わる前後の投稿数は倍近くなっていることがわかります。この時に負荷により日記の投稿がしづらい状態になっていたので、もっと多くの日記が書かれていたと思われます。

日記の投稿でエラーが起きた原因は、日記のIDを生成するシステムの過負荷にあります。mixi日記では全体で1つの通し番号が付いていますが、このIDを生成するシステムに負荷が集中し、IDが取得できなくなり日記が書けないという状態が発生してしまっていました。

■ID Generatorの改善

そこで来年に向けて、まずはIDを生成するシステムの改善に手を付けました。
日記のID生成システム、「ID Generator」はMySQLのLAST_INSERT_ID()を利用して実装されており、以下のようなテーブルとスクリプトを用いています。

テーブル

create table idpot (
    id bigint unsigned
);

スクリプト

my $next_id;
my $rv  = $dbh->do('UPDATE id_pot SET id=LAST_INSERT_ID(id+1)');
if ($rv == 1) {
    $next_id = $dbh->{'mysql_insertid'};
}

この実装で通常のサービスで問題がでることはありませんが、正月明けのようなアクセスが特に集中する時には、この部分でMySQLがdead lockを引き起こしてしまいます。実は、この問題は既に去年の11月に当ブログで紹介させて頂いてます。dead lockはMySQLに接続するクライアント数が多くなると発生します。

mybenchを利用したテストでは、接続数が250を超えるとエラーが発生しテストが正常に終了しません。

clients count/client total_count fastest slowest average q/sec
100 10 1000 0.0001 0.085 0.019 19.33
150 10 1500 0.0001 0.13 0.047 71.54
200 10 2000 0.0005 0.17 0.10 206.96
250 10 2020 0.158 0.07 182.51

(MySQL 5.0でテスト。クライアント数250では正確な数値は取得できず)

2008年でもこの問題について把握はしていましたが、クライアント数についての見積もりがあまく、dead lockが予測よりも速く発生してしまったため、日記が書けない事態が発生しました。そこで今年はID Generator自体に手を加えました。

ID GeneratorはMySQLへの接続本数を減らすことで改善を目指しました。具体的にはアプリケーションサーバから直接MySQLに接続するのではなく、専用のAPIサーバを置いて間接的に接続するようにしました。APIのサーバはmod_perlのhandlerとして構築をし、ApacheのMaxClientも1台あたり10程度としました。アプリケーションサーバからの接続はtcpのbacklogに積まれますので、MaxClientが少なくても処理が詰まったりしない限りは問題ありません。もしAPIからIDが取得できない場合は、アプリケーションサーバから直接MySQLを参照します。

id_generate_server_blog.png

APIの性能は「ab」を利用してテストしたところ、1台あたり2000req/s以上の性能がでています。簡単なDBのシミュレーションでも秒間数百の処理ができることも確認しています。

■最新情報DBへの書き込みを非同期に

ID Generatorを改善することで現在顕在化しているボトルネックの解消はできましたが、今年はもう一つ、次に問題になりそうなDBの改善にも取り組みました。mixiの日記DBは弊社でLevel2分散と読んでいるユーザパーティショニングを行っています。日記のDBはノードと呼ばれる日記を保存するDBと、マネージャーと呼ぶ、ユーザの日記がどのノードにあるかを管理するDBの2種類があります。ユーザの日記を表示する時にはマネージャーのDBに接続をし、ユーザIDから、どのノードに日記が保存されているのを問い合わせ、その結果得られたノードDBに再度接続をするようになっています。

diary_partitioning1.png

ただし、この状態ではホームなどのマイミク最新日記を取得する場合に、全部のノードに1つ1つ接続をしてマイミクの最新の日記を検索しなればならないので、ノードとは別に最新日記のIDとタイトルだけを入れる最新情報DBを用意してあります。ちょうど1週間のデータを保存していますので、Weekly DBと呼んでいます。日記が投稿された際には、各ノードとWeekly DBの2カ所にデータを保存しています。

Weekly DBは負荷的な問題は現状ありませんが、1月1日のような日記が集中して書かれる瞬間では、書き込みが多くのアプリケーションサーバから一斉に行われるため、ボトルネックになることが予測されます。

diary_weekly.png

そこでエコーでも用いたQ4MというMySQLのStorage Engineとして開発されているJob Queueのシステムを日記投稿にも投入しました。

diary_weekly_queue.png

書き込みを一旦Q4Mでキューイングし、表側の非同期にWeekly DBへと書き込んで行きます。日記の投稿が集中した場合、Weekly DBではDisk IOを分散することが難しいですが、Q4Mを用いる事でIOを時間軸で分散することができるのでWeekly DBへの負荷が集中し、サービスに影響が出てしまうのを防ぐ事ができます。また、Q4M自体の負荷が高まった場合は、単純に台数を増やす事で解決ができます。

Q4Mを導入した事でもう一つ耐障害性が高まった部分があります。Weekly DBにハードウェア障害が起きた場合にも最新日記をQ4M上に貯めておき、DBが復旧した時点であらためてDBに挿入していくことができます。これによってマイミクの最新日記をQ4M導入前よりも確実に届けることができます。

■まとめ

2009年1月1日にむけて2つの改善を行いました。1つはIDを生成するDB、もう一つは最新情報DBへの保存の非同期化です。2009年”は”ユーザの皆様の「あけおめことよろ」をすべて正常に受け付けることができると思います。来年1月1日に良い結果がでれば、またエンジニアブログで紹介したいと思います。

どうぶつの森にハマって、たぬきち商店が早終いする関係で退勤時間もめっさ早くなったmikioです。今回は、Tokyo TyrantのキャッシュとLua拡張を使って超お手軽にリアルタイム検索システムを作る方法について述べます。

ユースケース

高い頻度で更新されるWeb上のテキストをリアルタイムに検索したいと思ったことはありませんか? mixi日記や各種のブログサービスやRSSリーダなどで扱う大量のコンテンツを安価かつ簡単に検索したいと思ったことはありませんか? 私は結構あります。要件を箇条書きすると以下のような感じでしょうか。

  1. 最新データの合計100万件くらいを検索できればよく、古いデータは自動的に消えてほしい。
  2. ただし、更新はリアルタイムにして、書いた瞬間に検索結果に反映されてほしい。
  3. サーバ1台で更新1000qpsおよび検索100qpsは処理したい。
  4. 再現率よりも精度とリアルタイム性を重視したい。

例として「mixiエコー」を考えてみます(あるいはWassr/Twitter/はてなブックマークなどでも同様です)。100文字以下くらいの短いテキストが高い頻度で更新され、主に最新のデータに注目が集まるコンテンツです。これをリアルタイムに検索可能にしたいのです。

こうした要求は、キャッシュ風に振る舞う転置インデックスを用いることで実現することができます。ということで、Tokyo Tyrant(TT)のキャッシュとLua拡張機能を用いた転置インデックスを実装してみました。キャッシュはTokyo Cabinet(TC)のオンメモリハッシュデータベースとして表現します。

転置インデックスを表現するキャッシュ

転置インデックスとは、レコードのテキスト内のトークンをキーとして、その出現位置情報のリスト(posting list)を関連づけるkey/valueデータベースです。これを作るには、レコードのテキストをトークンに分割するとともに、各トークンに対してレコードIDをposting listの末尾に連結していくという操作を繰り返します。擬似コードで書くと以下のようになります。

FOR_EACH record (all_records){
  all_tokens = tokenize(record.text)
  FOR_EACH token (all_tokens){
    concatenate_to_posting_list(index, token, record.id)
  }
}

今回は、上記のindexとしてTCのオンメモリハッシュデータベースを用います。concatenate_to_posting_list操作はLua拡張の組み込み関数_putcatを使うだけでOKです。TTでオンメモリハッシュデータベースを用いる場合、キャッシュの最大サイズを指定すると、それに溢れた場合はLRU(least recent used)のレコードから削除してメモリ使用量を一定に保つことができます。そうすると「古いデータは自動的に消えてほしい」が一部達成できるわけですが、それだけでは充分ではありません。頻出するトークンのposting listは連結を続けると肥大化していくので、ある一定の長さで前の方を切ってあげないといけません。そのため、一定の頻度でposting listを最適化する操作を実行して乗りきることにします。

lru_rotate.png

Lua拡張での実装

Luaの関数として、以下の操作を実装します。トークンの分割はクライアント側で行い、空白区切りのリストとして渡すものとします。日本語のテキストを扱う場合はmecabやchasenなどの形態素解析器を使ってトークンを切り出すことになるでしょう。

  • put : レコードIDとトークンのリストを受け取り、各トークンのデータをposting listに連結する。
  • search : トークンのリストを受け取り、その全てを含むレコードのIDのリストを返す。

putを最も単純化すると、以下のような実装になります。たったこれだけで最低限の転置インデックスって作れちゃうものなんです。posting listはレコードIDをBERエンコードしたデータを連結して表現しています。

DELIMS = " \t\r\n"   -- delimiters of tokenizing

function put(id, text)
   local tokens = _tokenize(text, DELIMS)
   local idsel = _pack("w", id)
   for i = 1, #tokens do
      _putcat(tokens[i], idsel)
   end
   return "ok"
end

function _tokenize(text, delims)
   local tokens = {}
   for token in string.gmatch(text, "[^" .. delims .. "]+") do
      if #token > 0 then
         table.insert(tokens, token)
      end
   end
   return tokens
end

とはいえ、今回は定期的にposting listを最適化するという操作が必要なので、実際にはもうちょっと複雑になります。また、最低限のエラーチェックも入れるようにします。以下では、10回に1回の確率でoptが真になり、その場合はposting listの後方500個を残して前方を捨てる処理を行います。それらの処理をアトミックに行うため、トークンをロック/アンロックする処理を前後に入れています。

DELIMS = " \t\r\n"   -- delimiters of tokenizing
OPTFREQ = 0.1        -- frequency of optimization
LIMNUM = 500         -- limit number of kept occurrence

function put(id, text)
   id = tonumber(id)
   if not id or id < 1 then
      return nil
   end
   if not text then
      return nil
   end
   local opt = math.random() < OPTFREQ
   local tokens = _tokenize(text, DELIMS)
   local idsel = _pack("w", id)
   for i = 1, #tokens do
      token = tokens[i]
      if not _lock(token) then
         break
      end
      if opt then
         local ids = {}
         local idsel = _get(token)
         if idsel then
            ids = _unpack("w*", idsel)
         end
         local nids = {}
         local top = #ids - LIMNUM + 2
         if top < 1 then
            top = 1
         end
         for j = top, #ids do
            table.insert(nids, ids[j])
         end
         table.insert(nids, id)
         idsel = _pack("w*", nids)
         _put(token, idsel)
      else
         _putcat(token, idsel)
      end
      _unlock(token)
   end
   return "ok"
end

searchは以下のような実装になります。トークンが複数指定された場合はそれらのAND検索を行うことになるのですが、Luaのテーブルを使ってハッシュジョインすることで論理積を実装しています。なお、第2引数に戻り値の最大件数を指定するとともに、戻り値の第1行目には全体のヒット数を返すようにしています。

DELIMS = " \t\r\n"   -- delimiters of tokenizing
DEFMAX = 10          -- default maximum number of search

function search(phrase, max)
   if not phrase then
      return nil
   end
   max = tonumber(max)
   if not max or max < 0 then
      max = DEFMAX
   end
   local tokens = _tokenize(phrase, DELIMS)
   local hits = {}
   local tnum = #tokens
   for i = 1, tnum do
      local idsel = _get(tokens[i])
      if idsel then
         local ids = _unpack("w*", idsel)
         local uniq = {}
         for j = 1, #ids do
            local id = ids[j]
            if not uniq[id] then
               local old = hits[id]
               if old then
                  hits[id] = old + 1
               else
                  hits[id] = 1
               end
               uniq[id] = true
            end
         end
      end
   end
   local result = {}
   for id, num in pairs(hits) do
      if num == tnum then
         table.insert(result, id)
      end
   end
   table.sort(result)
   local rtxt = #result .. "\n"
   local bot = #result - max
   if bot < 1 then
      bot = 1
   end
   for i = #result, bot, -1 do
      if max < 1 then
         break
      end
      rtxt = rtxt .. result[i] .. "\n"
      max = max - 1
   end
   return rtxt
end

上記のソースコード全体をTTの最新版(1.1.8)にusherette.luaというファイルとして同梱しておきましたので、いろいろ改造してみて遊んでみてください。

実際のサービスでは必ずしも各エントリに単純な数値のプライマリIDが与えられているわけではないので、そういった場合はインデックス用の内部IDを振り直した上で、内部IDと外部IDの関連づけもデータベースに入れて管理することになるでしょう。

実際に動かしてみる

で、オンメモリだとどんだけ早いのかというのが問題なわけです。TTのPerlクライアントライブラリを使ってテストしてみましょう。まずは、以下のようなクライアントを書きます。平均20トークンのレコード20万件を登録するスクリプトです。register.plなどとして保存してください。

use TokyoTyrant;

my $rdb = TokyoTyrant::RDB->new();
$rdb->open("localhost", 1978);

for(my $i = 1; $i <= 200000; $i++){
    my $text = "";
    my $wnum = int(rand(19)+1);
    for(my $j = 0; $j < $wnum; $j++){
        $text .= sprintf("\t%d", int(rand($i/10)));
    }
    $rdb->ext("put", $i, $text);
}

$rdb->close();

次に、サーバを起動します(TTは–enable-luaをつけてビルドしておいてください)。データベースの設定は、オンメモリデータベースを、最大サイズ100MB、バケット数10万にします。これは、メモリ使用量200MBくらい、異なり語数10万語くらいを想定した設定です。

$ ttserver -ext usherette.lua '*#capsiz=100m#bnum=100000'

でもって、別端末でクライアントを動かしましょう。5個のクライアントを同時に動かして、合計100万レコードを登録します。

$ perl register.pl &
perl register.pl &
perl register.pl &
perl register.pl &
time perl register.pl

私の環境では100秒かかりました。ということはだいたい10000qps程度の登録性能が出るようです。これならどんなに大繁盛しているサイトで使ってもボトルネックにはならないでしょう。ちなみに検索は以下のようなスクリプトでテストできます。

use TokyoTyrant;

my $rdb = TokyoTyrant::RDB->new();
$rdb->open("localhost", 1978);

for(my $i = 1; $i <= 200000; $i++){
    my $text = "";
    my $wnum = int(rand(2)+1);
    for(my $j = 0; $j < $wnum; $j++){
        $text .= sprintf("\t%d", int(rand($i/10)));
    }
    $rdb->ext("search", $text);
}

$rdb->close();

5クライアント同時実行で269秒かかりましたので、3700qps程度の検索性能が出るようです。Luaのテーブルのオーバーヘッドが思ったより大きいみたいでちょっとがっかりですが、まあ検索クエリが3700qpsを越えるサイトなんて知らないので問題ないでしょう。

まとめ

Tokyo Tyrantのサーバ1台を用意するだけで、任意のタグ方式(もしくは分かち書き方式)のリアルタイムな検索システムを、mixiのような大規模なサイトのQPSにも耐えるように稼働させることができます。あるいは、あるエントリが書かれた瞬間に、同じ語彙を含むエントリを提示する、はてなのおとなり日記みたいな機能をリアルタイムに実現することができます。

オンメモリだとサーバが落ちたらインデックスが消えてしまうわけですが、それに関してはレプリケーション機能で冗長性を確保することで対処できるでしょう。巨大なインデックスを更新するのにはある程度時間がかかりますが、その更新間の遅延を埋め合わせるためにオンメモリの簡易検索システムを併用するというのもよいアイデアだと思います。再現率ではタグ方式の転置インデックスに優るN-gram方式の転置インデックスやsuffix arrayを使っても更新遅延が大きくなりがちなので、それらと組み合わせるのも妙案かもしれません。

リアルタイム検索の手法は今後も模索していく所存ですので、質問や要望などあれば、Tokyo Cabinetコミュニティにてお寄せいただけると幸いです。

こんにちは、某Perl界隈のIRCチャンネルでPythonがマイブーム的なKY誤爆をしてしまったtmaesakaです。

先日、以前から興味のあったGoogle App EngineMemcache APIについて少し調べ、こちらに英文で報告したのですが、今日は日本語で要約したまとめを紹介します。

まず軽く前置きですがGoogle App Engine (GAE)とは、Googleが提供しているウェブアプリケーションをGoogleのインフラ上でスケーリングや冗長化など、ある程度のノウハウや資金を要求される面倒な事を気にせずに運営できるプラットフォームです。つまり、典型的なPaaSの例であり、サービスの運営コストをelastic(伸縮)にします。昨今バズワード化しつつあるクラウドコンピューティングの一種でもあります。

GAEのインフラはGoogleより提供されているAPIセットを用いて利用します。その中にはon memory cacheがMemcache APIという形で提供されています。インターフェイスはmemcachedと同様Key/Valueベースのもので、アプリケーションのトータルパフォーマンス向上に役立つAPIです。

このAPIはmemcachedを連想させられるネーミングですが、実際にGAEのドキュメントを読むと、こう記述されています:

The Memcache API has similar features to and is compatible with memcached by Danga Interactive.

つまりmemcached互換で、なおかつ似ていると書かれていているだけです。普通はこの辺で納得するのでしょうが、私はこういった文を見ると調べたくなる性格なので、ちょっと深入りしてみました。

実際にMemcache APIを使うのは簡単で、’memcache’ モジュールをGAEパッケージからインポートします:

from google.appengine.api import memcache

あとはアプリケーションから必要に応じて、各種APIメソッドをコールするだけです。

プロトコル違反なKeyでセットしてみる

memcachedのASCIIプロトコルでは、Keyの長さは250バイトまでという制限があります。それ以上の長さのKeyを送信するとmemcachedはERRORを返します。では、GAEはどうでしょう?

遊び気分で300バイトのKeyで適当な値をSetしようとする以下のコードを走らせてみたところ:

from google.appengine.api import memcache
 
memcache.flush_all()
test_key = 'x' * 300
 
if not memcache.set(test_key, 'some_val'):
    print 'Failed to set'
    quit()
 
print "Looks like we're good = " + memcache.get(test_key)

以下のエラーがローカルのApp Serverから返ってきました:

Keys may not be more than 250 bytes in length, received 300 bytes

あらあら、上記のエラーだけを見ると明らかにmemcachedっぽい動作ですが、memcachedに合わせているだけかもしれません。Keyの長さ制限はドキュメントに記述されていないので、このエラーに遭遇したら驚くデベロッパーもいるかもしれませんね(レアなケースですが)。

さて、次はもっとマジメに独特な情報で比較しましょう。

メモリ消費量で比較してみる

memcachedではサーバインスタンスがどれだけのデータ(総バイト数)をキャッシュしているかをstatsコマンドで容易に取得する事が可能です。同様に得られる情報は制限されるものの、Memcache APIでも同じ事が可能です:

from google.appengine.api import memcache
 
stats = memcache.get_stats()
if stats: print stats['bytes']

ここでトリビアですが、memcachedから取得できるキャッシュサイズは純粋に全てのkeyとvalueを合計した値ではなく、各レコードのオーバヘッド(item構造体のサイズ)を加算した値です。

この値を実際にGoogle上にデプロイしてあるアプリが返す結果と比較してみました:

1 x 128 byte value with a 5 byte key
Memcache API: 133 bytes
memcached-1.2.6: 184 bytes

64 x 128 byte values with 5 byte keys
Memcache API: 8512 bytes
memcached-1.2.6: 11776 bytes

128 x 128 byte values with 5 byte keys
Memcache API: 17024 bytes
memcached-1.2.6: 23552 bytes

なんとGAEは一切オーバヘッドを報告しません。これでMemcache APIのバックエンドに様々な可能性が広がりましたね。例えばネットワーク越しに分散されているGoogle Sparse Hashかもしれないし、アプリケーションのキャッシュマネージメントの事情でstats情報を独立した仕組みで保持しているのかもしれません。次のセクションで分散やマネージメントに関する推理を紹介します。

運用を考えてみる

特定のアプリケーションに関するキャッシュ情報の取得」は言葉にすると簡単に聞こえるものの、実際は簡単ではありません。まず考えなくてはならない事は、アプリケーションがどの様にキャッシュスペースを与えられているのかという点です。

例えばアプリケーションに対して必要に応じた数の専用インスタンスを用意するか、「専用」という概念を捨て、Keyに対しApplication Identifierをappend/prependして、他アプリケーションとキャッシュプールを共有するといったモデルです。どちらのモデルを採用するにしても、キャッシュのstatsという概念はインスタンス毎に存在するものなので、「このアプリケーションがxxx」という情報の管理と保持には独立した仕組みが必要と考えられます。つまりアプリケーション毎にindexが必要だという事です、例えばInverted Indexあたりのデータストラクチャ(appid -> stats_postings)で保持するなど。

上記の様な工夫を行わなければ、statsリクエストに対して毎回、ルックアップと演算がオンザフライで発生する事になり、低効率なシステムとなります。したがって下の層でmemcachedを使っていたとしても、統計管理が独立していれば、オーバヘッド抜きの純粋な値が保持される可能性もあります。GAEが保持と管理が楽な情報だけをstatsインターフェイスで提供している説明にもなります。

  • hits
  • misses
  • byte_hits
  • items
  • bytes
  • oldest_item_age

さいごに

Google App Engineで遊んでいて感動した事は、ドキュメンテーション(英語のやつ)がとてつもなく解り易い事です。あまり賢くなくて挫折屋な私でも、すぐに自立して簡単なコードが書けるレベルまでいけたほどです。

プログラミング言語面では現状、Pythonしかサポートされていませんが、もし今後いろいろな言語、特にRubyあたり(私は書けませんが)が対応されたら実に凄いプロダクトになるんじゃないかな〜、と思いました。Perl対応も完了したらかなり盛り上がるかも。