Skip to content

Instantly share code, notes, and snippets.

@suma
Last active May 9, 2016 22:59
Show Gist options
  • Save suma/5298923 to your computer and use it in GitHub Desktop.
Save suma/5298923 to your computer and use it in GitHub Desktop.
MessagePack RPCクライアントの振る舞い及びエラー処理

MessagePack RPCクライアントの振る舞い及びエラー処理

概要

MessagePack-RPCのクライアントのエラー処理を正しく、またその後に何をすべきか(可能か)まとめる。 「このエラー起きたとき、どう処理すべきか?」ということを説明する。

説明しないこと:RPCサーバの作り方、各言語のライブラリのプロトコル実装やエラー定義の差。

結論

  • エラー内容を正確に取得したければC++クライアントを使う
  • Ruby, Python, Javaの実装では、1度RPCに成功した後、同じオブジェクトで次にRPCを実行して発生したトランスポート層のエラーは必ずtimeout errorとなってしまう(トランスポート層で何が起こったのか、接続し直したのか、何が原因となっているのか例外からは判別できない)
  • 回避策として、RPCを呼び終わったらセッションを閉じて、再度接続し直すようにRPCを実行する。そうすることで、接続タイムアウトなのか、他のエラーなのか原因か判別できるようになる

対象

以下のライブラリと、TCP/IPを対象とする。

  • Ruby (msgpack-rpc version 0.5.1)
  • Python (msgpack-rpc-python version 0.3.2)
  • Java (org.msgpack.rpc version 0.7.0)
  • C++ (jubatus-msgpack-rpc version ?)

ちなみに全部IPv4, v6の両対応のはず。ホスト名を指定するとRuby, Python, C++だとsockaddrで自動解決した最初の値、JavaはInetAddress.getByNameによるIPアドレスとなる。

sockaddrの順番ってPOSIXで定義さてないように思うが、LinuxやMacの実装ではv6アドレスがあればそちらが先頭に返る模様。 see: http://man7.org/linux/man-pages/man2/bind.2.html

調査、整理した概要

  • TCPの接続はどの段階で実行されるか

  • コンストラクタ(オブジェクト作成時) → 無し

  • RPC呼び出しを実行時(まだTCPセッションが確立されていない・接続されていない場合に限る)

  • 接続失敗したとき、どうすれば再接続できるか

  • どれも再度RPCを実行すれば、そのときに接続を試行するよう実装されている

  • ステップタイムアウト(一般的な用語?)

  • MessagePack-RPCのクライアントは、RPCでデータの受信が発生していない連続した待機時間をステップと実装で呼んでいる。このステップの時間が設定されたステップタイムアウトを超えるとクライアント側で通信を切断し、タイムアウトエラー(トランスポート層のエラーに分類)を発生させる。

  • 1バイトでも受信があると、ステップは0にクリアされる。

  • TCPセッションを切断する方法はあるか、またその後に同じオブジェクト(インスタンス)からTCP再接続できるか

  • Ruby → Closeを呼ぶ、再接続できる

  • Python → closeを呼ぶ、再接続できる

  • Java → closeを呼ぶ、その後は再接続できない

  • C++ → jubatus-msgpack-rpcではcloseを呼ぶ、またその後に再接続できる。msgpack-rpc本家のclientではデストラクタでのみ能動的に切断できる。

  • どの言語もオブジェクトが消されるとき切断されるはず

  • トランスポート層のエラーにはどういった種類があるか

  • 接続タイムアウト(接続リトライのエラーを返す実装もある→Python)

  • 通信タイムアウト(TCP接続してセッション確立は完了しており、その後のRPC通信中)

  • 実装によっては他のエラーもある: Ruby,

  • トランスポート層(TCP)ではないエラー

  • 呼び出した名前のRPCがサーバに存在しない

  • サーバに呼び出された名前のRPCは存在するが、与えられた引数の型や数が異なっている

  • サーバ側で何らかのエラーが発生した:→ どんなエラーが返ってくるか・エラー定義に対応できるかは各言語のライブラリの実装依存となる

  • アプリケーションのエラーが返ることもある

クライアントの普通の使い方

  1. クライアントのオブジェクト(インスタンス)を作る
  2. 必要ならタイムアウトを設定する
  3. RPCを呼ぶ
  4. 例外(エラー)が発生すれば、エラー処理
    • トランスポート層のエラー? 再試行してもいいし、諦めてもいい
    • アプリケーション層?(RPCの名前がない、引数の型が違うのならサーバかクライアントのミスである)
  5. 停止(TCP切断) or オブジェクトを使い回して3,4を繰り返す

通信の接続及びリトライの調査、タイムアウト

Ruby

  • 指定可能なタイムアウト等:ステップタイムアウト、再接続の試行回数上限
  • 接続失敗したときは再接続を繰り返し試す
  • → 上回数を超えるとエラー: ConnectionTimeoutError::CODE, ["connection timed out"]

Python

  • 指定可能なタイムアウト等:ステップタイムアウト、再接続の試行回数上限
  • 接続失敗したときは再接続を繰り返し試す
  • reconnect_limitを超えるとエラー self._session.on_connect_failed(TransportError("Retry connection over the limit"))

Java

  • 指定可能なタイムアウト等:接続タイムアウト、再接続の試行回数上限、ステップタイムアウト

  • 接続失敗したときは再接続を繰り返し試す

  • 回数に制限有りStreamClientConfigのgetReconnectionLimit

  • Session.transportConnectFailedが呼び出される

  • TODO: このとき例外が呼ばれるかはよくわからないので要調査

  • 一度closeしたら、再接続はできない

C++ (msgpack-rpc/jubatus-msgpack-rpc)

  • 指定可能なタイムアウト等:接続タイムアウト、再接続の試行回数上限、ステップタイムアウト
  • 接続失敗したときは再接続を繰り返し試す
  • 試行回数上限を超えたら connect_error

FAQ

RPCが成功し、クライアントからは切断せずサーバがセッションを切断してからRPCを呼ぶとどうなるか

Ruby, Pythonのクライアントはセッションの切断を検出できない。また、RPCのリクエストを送った後、サーバの反応が遅いためにクライアント自身でタイムアウトしたのか、TCPセッションの切断と区別することができない。

トランスポート層のエラーが起きたときも同じ結果、もしくは同様のエラーを返す可能性がある。

  • Ruby: MessagePack::RPC::TimeoutError: request timed out
  • Python: msgpackrpc.error.TimeoutError: Request timed out
  • C++: rpc_error: connection closed
  • Java: org.msgpack.rpc.error.RemoteError: timedout (ログにjava.net.ConnectException: Connection refusedなど出力)

エラー出たらどうするか?(トランスポート層編)

  • 接続タイムアウトだったとき

  • 解決策:再接続を試みるか、諦める

  • よくある原因:クライアントがホスト名解決でIPv6のアドレスを取得してIPv6で接続しようとするが、サーバはIPv4でlistenしているケース(逆も発生しうる)。IPアドレスをv4で直接指定するとよい。

  • RPC通信中にタイムアウトもしくは通信エラーが出たとき

  • トランスポート層のエラー(タイムアウトの旨)が発生するので、諦めるか再度RPCを実行する

  • このエラーが返ったときクライアント側では、RPCがサーバで実行完了したか、実行中か、実行に失敗したかを知る手段をRPCの仕組みでは提供してくれない。

  • 回避策:1回だけリクエストを実行し、かつその呼び出し結果を得たいのであれば、上記のエラーに耐える仕組みをRPCを用いて構築する必要がある。RPCの意味論はexactly-onceを提供しない・できない。課題:RPCのリクエストや実行結果をクライアントが取得完了するまで保持するキューの仕組み。また、RPC実行前のキューや、クライアントに取得されてない実行結果に永続化を入れるか入れないか、それらにタイムアウトは存在するか、などの要件定義も必要。実現できないなら、RPCの採用をやめてジョブキューなどを用いて実現可能か検討したほうがよいのではないか。本ドキュメントの目的から外れているのでこれ以上は記述しない。

  • RPCライブラリの実装によっては定義してあるエラー

  • 解決策:再度要求を試みるか、諦める

  • どう処理するかはアプリ及びライブラリ依存である

エラー出たらどうするか?(トランスポート以外)

  • 呼び出した名前のRPCがサーバに存在しない

  • 確認:バグもしくはサーバ・クライアント間のバージョンの不一致によるものではないか

  • サーバに呼び出された名前のRPCは存在するが、与えられた引数の型や数が異なっている

  • 確認:バグもしくはサーバ・クライアント間のバージョンの不一致によるものではないか

  • RPCライブラリの実装で共通のもの

  • 解決策:再度要求を試みるか、諦める

  • どう処理するかはアプリ及びライブラリ依存である

  • RPCサーバのアプリケーション側の実行エラーが返る

  • 解決策:再度要求を試みるか、諦める

  • アプリケーション依存の処理をする

TCPセッション確立後(RPC成功後)、サーバから切断したいときはどうする

  • 専用のメソッドあれば呼ぶ
  • ある: Ruby(Close), Python(close), Java(close), C++(close: jubatus-msgpack-rpc)
  • クライアントのオブジェクト(インスタンス)を削除する
  • Ruby, Python, Java, C++(本家msgpack-rpc及びjubatus-msgpack-rpc)

タイムアウト・試行回数上限を指定・変更したい

  • コンストラクタでタイムアウトを指定する

  • Ruby: ステップタイムアウト。試行回数上限を指定するには第一引数にTCPTransportオブジェクト)reconnect_limitアクセサ有り)、第二引数にaddress(Address)を指定する

  • Python: 第一引数にステップタイムアウト、第三引数に試行回数を指定する

  • C++: 第一引数にtcp_builder(connect_timeout, reconnect_limitで指定できる)、第二引数へaddressを指定する。ステップタイムアウトは別途

  • Java: StreamClientConfig(setConnectTimeout, setReconnectionLimit)で指定する

  • オブジェクトのメンバ関数やプロパティから変更する

  • Ruby: timeoutプロパティでステップタイムアウトの取得・変更

  • Python: サポートしていない

  • Java: setRequestTimeoutでステップタイムアウトの変更

  • C++: set_clientでステップタイムアウトの変更

各言語実装ごとのエラー

主要な接続タイムアウト(接続失敗も含む)と通信タイムアウトについて紹介する。

接続タイムアウト・接続失敗

  • Ruby: MessagePack::RPC::ConnectionTimeoutError
  • MessagePack::RPC::ConnectionRefusedError
  • Ptyon: msgpackrpc.error.TransportError: Retry connection over the limit
  • C++: msgpack::rpc::connect_error: connect failed (connect_errorはtimeout_errorを継承)
  • Java: org.msgpack.rpc.error.RemoteError: timedout
  • Javaでは接続タイムアウトと通信タイムアウトの区別ができない

通信タイムアウト

  • Python: msgpackrpc.error.TimeoutError: Request timed out
  • Ruby: MessagePack::RPC::TimeoutError: request timed out
  • C++: msgpack::rpc::timeout_error: request timed out
  • Java: org.msgpack.rpc.error.RemoteError: timedout
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment