スティルハウスの書庫の書庫

はてなダイアリーで書いてた「スティルハウスの書庫」を移転してきました。

Task Queue君とmemcache君、疑って正直すまんかった

ここ2日くらいデバッグではまりました。もともとこんな処理してるコードです:

  • a. クライアントが大量データダウンロードのリクエストを投げ、ポーリングをはじめる
  • b. リクエストに基づいてTask Queueに数個〜数10個のタスクが積まれる
  • c. 個々のタスクの結果はmemcache上に集約される
  • d. 集約されつつあるデータをクライアントがダウンロードする

これ通常はうまく動いてたのですが、対象データの規模が非常に大きいと時折データの抜けが発生するという現象がクラウド上で起きてました。その原因を調査してましたが、やっぱり真っ先に疑われるのは、Task Queueによる非同期な並列処理の部分と、memcacheに集約する部分です。

タスクほんとに全部終わってる?

このアプリでは、上記のb.とc.の部分が確実に実行されていることを確認するために、

  • 個々のタスクにmemcacheにトークンを登録する
  • 各タスクは処理が完了したらトークンを削除する

って感じの仕組みでタスクの確実な完了を確認してます。それでも、やっぱり非同期に並列に実行させると実際になにがどうなってるのかすごく不安になります。かくしてTask Queueは松本サリン事件の河野さん並のひどい扱いを受けるわけです。

非同期処理とか並列処理ってどうテストするの?

そもそも、Task Queueで組んだa.とかb.の処理って、単体テストをどう書いたらよいかわからず、手動テストです。twitterでは@bluerabbit777jpさんが同じ悩みをつぶやいてたので私もいっしょに悩んでたら、@marblejenkaさんが以下のページを教えてくれました:

Unit Testing Asynchronous Code

        synchronized(lock) {
          lock.notify();
        }

つまりwait/notifyでスレッド間同期して、マルチスレッドの非同期なコードをテストするって趣旨です。ローカル環境のテストはこれで行けそうな感じですが、クラウド上のTQはマルチスレッドじゃなくて分散処理なのでこの手法は使えません。(追記:MemcacheService#incrementでグローバルなwait/notifyを実装するって手もあるかな。30秒制限あるけど。。)

あと@marblejenkaさんからの別のアイディア:

@kazunori_279 @bluerabbit777jp taskqueueだとgoogleパッケージに自作queueを置くと拡張できるので、テスト用queueみたいなのを作ってaddしたtask一つ一つが終わるのを待ってassert、的なことができるかもです。

これもローカルでは有効ですね。ApiProxyとかごにょごにょしてTQのスタブを作って、上記のwait/notifyと組み合わせたりしてローカルのテストは書けそうです。

しかしクラウド上での確実な単体テストってどうやればよいのかわかりません〜。

memcacheもusual suspects

memcache、こいつも実にあやしい。なんといっても、データなんていつでも破棄されうるよなんて公言してるのだから。あくまでキャッシュなのです。今回のアプリでは、

  • 個々のタスクにランダムなトークンを割り振る
  • タスクはそのトークンをキーに結果をmemcacheに書き込む
  • クライアントのポーリング時に、トークンをキーにしてmemcacheから値を取得する
    • もし結果がnull(memcache上に残っていない)だったらタスクやりなおし

というロジックになっており、万が一memcache上のデータの破棄が発生した場合でも、データの抜けのような不整合は発生しない仕組みにしています。またこれまでのところ、memcache上の値がすぐに消えるという現象は経験したことがありません(もちろんそれに頼ることはできないです。いつかは黒鳥が出ますからね)。が、やっぱりあやしすぎる。

また、memcache上に処理結果を集約する手段として自作のスピンロックを使っています。こんなクリティカルなコードを自分で書いたら、まずそこばっかり疑いますよね。

真犯人はd.でした

時間をかけてb.やc.を中心にさまざまなカウンタやログを仕込んでクラウド上でのデバッグを繰り返したところ、彼らはまったくのシロであることが立証されました。結局、クライアントにダウンロードする部分d.のコードのポカミスに過ぎませんでした。

ということで、Task Queueとmemcacheを駆使して非同期処理とか並列処理とかロックとか書くと衝撃的にパフォーマンスが向上します(最初にTQを動かしたときは正直ちょっと引きました)が、いざバグが出たときに「その部分が原因ではない」ことを示すためのburden of proofが重すぎる気がします。やっぱりMapReduceみたいに高レベルな「お手軽並列処理」(スケルトン並列プログラミング)等の手法で、今回のような低レベル実装を避ける手立てがないと、実開発への投入には敷居が高いなぁと思いました。というわけで、

@asami224さんのつぶやき:

simplemodelerで解決しようとしている問題:非同期処理の自動生成。同期処理に対応する非同期処理を必ず作らなくてはならないがルーチンワークであり手組みは辛い。 @myen

これに期待しましょう!