37
nginx luaJSON-RPC batch requestを実装して地雷踏んだ話 2016/02/08 @mosa_siru 1

lua_nginx_module JSON-RPC 2.0 Batch Request

Embed Size (px)

Citation preview

nginx luaでJSON-RPC batch

requestを実装して地雷踏んだ話2016/02/08 @mosa_siru

1

@mosa_siru

2

@mosa_siru (もさ)• ボンバーマン極めてる人

• 2013-2015 DeNA

• Mobage 3rd party API運用

• ハッカドール新規開発(サーバーサイド)

• 2015- Gunosy

• 広告運用、新規事業開発3

今日の内容

4

今日の内容• JSON-RPCの紹介

• JSON-PRCって?

• JSON-RPC Batch Requestが便利!

• lua_nginx_moduleでBatch Request実装した話

• 地雷踏んだ話5

JSON-RPCって?

6

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": "[email protected]"}, "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": "[email protected]"}, "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

Batch Requestはどう実装すべきか

15

実装(1) フレームワークで行う

フレームワーク側で、for loop でそれぞれ順番に処理し、全て終わったらまとめてレスポンスを生成する

16

実装(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で行う

nginxのレイヤーでbatch requestはバラしてしまう

(2)のproxyの役割をluaが担当20

実装(3) nginxで行う

21

• backendのJSON-RPC APIはsingle requestさえ捌ければ良く、サーバー言語に依らずこの構成が取ることができる

• 管理コンポーネントも増えない

• イベントドリブンなnginxの性質を活かし、ノンブロッキング(=backendの処理を待たない)並列なbatch requestを簡単に実装することができる

• single requestも同じnginxで捌けるので、reverse proxy設定が二重管理にならない

というわけで nginx + luaで実装した話

22

library化しました

23

https://github.com/mosasiru/lua-resty-jsonrpc-batch

簡単に、batch requestが並列にノンブロッキングに実装できます。

24

Basic Usage解説

25

• /api が裏側のJSON-RPC APIに投げる(single

request用)

• /api/batch がbatch requestを分解し、

/api に subrequest (処理を移譲)

• 内部的には、lua_nginx_moduleのlocaltion.capture_multi を利用しています

26

Advanced Usage解説

27

• でかいArrayが来ると攻撃になるので、最大サイズを10とする

• subrequestの処理時間をnginx access logに入れるために、nginx変数に値を入れている

• ライブラリに用意されたフックポイントを利用している

• 「jsonrpc "substract" methodは /jsonrpc/method/substract

のパスで受けたい」など、動的なlocationに対応している

• というわけで、色々拡張できるようになっています

運用可能なの?

28

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プロセスは刺さらない)

導入してみた

31

問題なくリクエストを捌ける、が…

32

• 数万req/min を余裕で1台で捌けるが

• なぜか徐々にリークするメモリー

• なぜか時々出るソケットリークエラー

• そしてソケット開けなくなって死ぬ

[alert] 15248#0: open socket #123 left in connection 456

とりあえず 定期reloadする運用…

33

調査の結果

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しようとして死ぬ

subrequest利用時にはproxy_buffersを

適切に上げる運用にしましょう

36

おわり

37