Post on 16-Apr-2017
@mosa_siru (もさ)• ボンバーマン極めてる人
• 2013-2015 DeNA
• Mobage 3rd party API運用
• ハッカドール新規開発(サーバーサイド)
• 2015- Gunosy
• 広告運用、新規事業開発3
今日の内容• JSON-RPCの紹介
• JSON-PRCって?
• JSON-RPC Batch Requestが便利!
• lua_nginx_moduleでBatch Request実装した話
• 地雷踏んだ話5
JSON-PRC
• とってもシンプルなRPCプロトコル
• リクエストもレスポンスも全部指定のJSON形式でやろうぜ!
• 多数の言語でサーバー・クライアント共にフレームワーク化やライブラリ化されている
7
Example(1)
8
Request
{"jsonrpc": "2.0", "method": "subtract", "params": [5, 2], "id": 1}
{"jsonrpc": "2.0", "result": 3, "id": 1}
Response
Example(2)
9
Request
{"jsonrpc": "2.0", "method": "getUser", "params": { "user_id": 1 }, "id": 1}
{"jsonrpc": "2.0", "result": {"name": "mosa", "email": "mosa@hogefuga.com"}, "id": 1}
Response
JSON-PRC• とってもシンプル!
• 特定のmethodにパラメータつきで1つの単純なリクエストを送って、1つのレスポンスをもらう
• 外部に公開するAPIでもない限り、URI設計や冪等性とか色々気にしないといけないRestFulよりも扱いやすい
• 大抵のネイティブアプリとサーバーAPI間の通信、Internal APIなどはこれで十分
10
Batch Request Example
11
Request[ {"jsonrpc": "2.0", "method": "subtract", "params": [5, 2], "id": 1}, {"jsonrpc": "2.0", "method": "getUser", "params": { "user_id": 1 }, "id": 2} ]
[ {"jsonrpc": "2.0", "result": 3, "id": 1}, {"jsonrpc": "2.0", "result": {"name": "mosa", "email": "mosa@hogefuga.com"}, "id": 2} ]
Response
Batch Request• 複数のリクエストを一気に送って、一気にレスポンスがもらえるプロトコル
• JSON Arrayで送ってJSON Arrayで返る
12
• 仕様上、送ったArrayの順番通りにレスポンスが並ぶことは保証されない=> 識別子として "id"のkeyをclientは見るべき
Batch Requestのメリット• リクエスト数を減らすことができる
• APIの設計がシンプルになる(1つのリソースに対して1つ作っておけば良く、UIを気にしなくて良い)
• クライアント側で、複数のAPIのレスポンスを待ったハンドリングがシンプルにできる
• 「2つのAPIの結果を待たないと描画してはいけない」
• 「どれかがこけた場合は全体をエラーにする」13
Batch Requestのデメリット• 重い処理に引きずられる
• 処理に10秒かかるリクエストA
• 処理に1秒かかるリクエストB
• A,BをBatch Requestにすると、最低10秒かかる
14
実装(1) フレームワークで行う
17
• 既存のフレームワーク実装はだいたいこれ。
• 10個分のbatch requestを投げたら処理に10倍時間がかかることになり、イケてない
• 並列でそれぞれのスレッド(プロセス)が処理するのが理想だが、言語によっては複雑な実装になる
実装(2) Proxy serverをたてる
proxyがJSON Arrayのそれぞれに対し、非同期で並列にバックエンドのJSON-RPCサーバーにリクエストを投げ、全て返ってきたら1つにまとめてレスポンスを生成する
実装(2) Proxy serverをたてる
19
• 管理コンポーネントが増えるので、運用の複雑さが増す
• 異常が起きた場合、どの経路で死んだのか多数のログを漁ることになる
• proxy serverの台数管理も考えないといけない
• single requestも考えると、appへのreverse proxy設定をnginxとproxyで二重管理することになりうる
実装(3) nginxで行う
21
• backendのJSON-RPC APIはsingle requestさえ捌ければ良く、サーバー言語に依らずこの構成が取ることができる
• 管理コンポーネントも増えない
• イベントドリブンなnginxの性質を活かし、ノンブロッキング(=backendの処理を待たない)並列なbatch requestを簡単に実装することができる
• single requestも同じnginxで捌けるので、reverse proxy設定が二重管理にならない
library化しました
23
https://github.com/mosasiru/lua-resty-jsonrpc-batch
簡単に、batch requestが並列にノンブロッキングに実装できます。
Basic Usage解説
25
• /api が裏側のJSON-RPC APIに投げる(single
request用)
• /api/batch がbatch requestを分解し、
/api に subrequest (処理を移譲)
• 内部的には、lua_nginx_moduleのlocaltion.capture_multi を利用しています
Advanced Usage解説
27
• でかいArrayが来ると攻撃になるので、最大サイズを10とする
• subrequestの処理時間をnginx access logに入れるために、nginx変数に値を入れている
• ライブラリに用意されたフックポイントを利用している
• 「jsonrpc "substract" methodは /jsonrpc/method/substract
のパスで受けたい」など、動的なlocationに対応している
• というわけで、色々拡張できるようになっています
subrequest の調査
29
• 事前にかなりの検証をした (CentOS 6.2, ngx_openresty1.7.10.1)
• lua実行中にnginx reloadされるとどうなるか?
• subrequest実行中にnginx reloadされるとどうなるか?
• luaでエラーになるとどうなるか?
• upstreamでエラーになるとどうなるか?
• upstreamがtimeoutするとどうなるか?
• client timeoutするとどうなるか?
subrequest の調査
30
• 検証の結果、以下に気をつければ問題ないことがわかる
• アクセスログにsubrequestの情報を盛り込むべき
• luaエラー時にハンドリングできるようlua pcallを利用するべき
• nginx reloadは問題なく可能!
• subrequest upstreamで刺さる分には影響ない
• luaで刺さるとnginx workerプロセスごと刺さる(がくぶる
• upstreamのレスポンスサイズがproxy buffersを超え、proxy_max_temp_file_size
も超えるかOFFにしていると応答が返らない(しかしnginx workerプロセスは刺さらない)
問題なくリクエストを捌ける、が…
32
• 数万req/min を余裕で1台で捌けるが
• なぜか徐々にリークするメモリー
• なぜか時々出るソケットリークエラー
• そしてソケット開けなくなって死ぬ
[alert] 15248#0: open socket #123 left in connection 456
調査の結果
34
upstreamのレスポンスサイズがproxy buffersを超え、proxy_max_temp_file_size も超えるかOFFにしていると応答が返らない(ただしworkerプロセスはブロックされない)
• とあるAPIにてこれが起きていた。
• このときソケットを閉じ忘れるようだ
• メモリも開放されないようだ
コードとか読むかんじ
35
upstreamのレスポンスサイズがproxy buffersを超え、proxy_max_temp_file_size も超えたとき
• single request: bufferingしないで直接upstream =>clientにwriteしてくれる
• subrequest: 全部bufferingしようとして死ぬ