shun159 さんと twitter していて、流れで「コントローラが OFPT_ERROR のメッセージを送るとマルチパートメッセージが壊れるかも」という話をしたのですが、ここにいたるまでの説明をしないと飛躍が激しいのは明らかなので、理由をまとめておこうと思います。きっと他の人が見ても役に立つはず…立つはず…
まず「実は xid には内部的に 2 種類ある」というところに行こうと思います。
さて openflow 仕様全体を通して、次のことが記載されています。
- プロトコルはメッセージ単位で行われる。
- メッセージは非同期で送受信される。
- ofp_header という共通ヘッダを使うという点で対称になっている。
xid
ofp_header に xid というフィールドがあります。本文に明示的に説明文はありませんが、コメントに書いてある通り transaction id であることは明らかで、request-reply 型のやり取りを成立させるために使われます。例えば次のようなフローに役に立ちます。
Controller Switch
| |
| request X |
|------------------> |
| |
| request Y |
|------------------> |
| |
| reply Y' |
| <------------------|
| |
| reply X' |
| <------------------|
| |
X, Y の応答は順不同で構いません。xid をみれば、どちらが X に対応する応答か判別できるからです。性能を上げるためには、このような非同期通信はできるようになっていないと辛いので、xid の仕組みは妥当です。
barrier
非同期であるのはいいのですが、順番に実行しないといけない何かがあった場合に、それを保証する仕組みが必要になります。ここで OFPT_BARRIER_REQUEST, OFPT_BARRIER_REPLY という特殊メッセージが導入されます。BARRIER は、非同期性を一部制限します。openflow の接続は TCP のストリーム型なので、request は順序通り送信先に送られます。応答を返す際に BARRIER に対してだけは順序を保証することになっています。つまり典型的には次のような形になります。
Controller Switch
| |
|- X --------------> |
|- Y --------------> |
|- B --------------> |
|- Z --------------> |
| |
| <------------- Y' -|
| <------------- X' -|
| <------------- B' -|
| <------------- Z' -|
| |
X', Y' は B' よりも後に返されることはないし、Z' は B' よりも先に返されることはありません。
またいっぽうで request に対する reply を必ず返さないといけないかというと、実はできればそれも省略したいところです。通常の状態ではエラーが発生しないべきなので、その状態に適した方式が良いでしょう。ここで BARRIER を使うとたいへん効果的です。次のような通信を見てみましょう。
Controller Switch
| |
|- X --------------> |
|- Y --------------> |
|- Z --------------> |
|- B --------------> |
| |
| <------------- Y' -|
| <------------- B' -|
| |
BARRIER 応答をもってして、X, Z が正常終了したことを知る、ということです。非同期であるがゆえに request をバルクで送ることができて、応答確認もまとめて行えます。
xid 再び
さてコントローラとスイッチは共通のフォーマットで、ある意味対称に通信しているわけですが、このような request-reply 型の通信を相互にできるか、ということを考えてみます。結論から述べると、これはそのままでは無理です。ofp_header には xid のフィールドが一つしかありません!xid に設定されている transaction id が、request のものなのか reply のものなのか区別がつきません。
openflow では ofp_header の type と xid 組み合わせて解釈することにします。type によって xid がどちらが送信したものかを判別します。例えば受信したメッセージが OFPT_ECHO_REPLY であった場合は、自分が以前に対応する OFPT_ECHO_REQUEST を送っているはずだ、ということです。Controller と Switch は互いに好きな xid を採番して送信していても、type と xid を組み合わて判別できていれば原則的には混乱は起こりません。
つまり openflow プロトコル通信路上で流れている xid にだけ注目すると、コントローラ側で生成した xid とスイッチ側で生成した xid が混ざっているという事です。
これで「実は xid には内部的に 2 種類ある」に、たどり着けました。コントローラ側で管理しているものか、スイッチ側で管理しているものか、の 2 種類です。
例外があるので明示的に書かれてはいないけれども、原則的にはコントローラ側の xid は reqest-reply ベースのもので、スイッチ側の xid は notification のシーケンスナンバーがベースだと捉えると理解しやすいです。余談ですが UDP を通信路として使う場合、メッセージタイプが限定されるのも、この辺が理由になります。
OFPT_MULTIPART_REQUEST
ofp_header のメッセージ長は uint16 の最大長以下に制限されますが、可変長のもっと長い情報をトランザクションとしてやり取りしたい要求は出てきます。ここでマルチパートという特殊メッセージが導入されます。OFPT_MULTIPART_REQUEST も OFPT_MULTIPART_REPLY も継続フラグが付いている限り、1 トランザクションに連結すべきメッセージとして溜め込みます。ここでも xid を目印にして連結を行います。
一般的な request-reply のやり取りは、送信 1 メッセージ受信 1 メッセージなので、どちらの側でも、受け取ったメッセージの処理を処理し終わったら、その場で xid をコピーして送り返せばよいだけで、あまり混乱はありません。複数メッセージをまとめて 1 transaction として扱わなければならないマルチパートが入ってくると、想定しうる異常状態が増えます。例えば OFPMPF_REQ_MORE が 0 のメッセージを受け取らないまま BARRIER を受け取ってしまった場合など。この場合は、トランザクション自体を失敗したとして応答せざるを得ないでしょう。
さらに考えてみます。マルチパートの受信が完了していない状態で、同一 xid のマルチパートで「ない」メッセージを受信した場合、どうするのが妥当でしょうか。これは厄介です。単に無視してしまえばよいようにも思えますが、OFPT_ERROR だったらどうでしょうか(Q1)。そしてもし、この OFPT_ERROR の xid が自分が送信していない xid であればなおさらどうでしょうか(Q2)。もっと困るのは OFPT_BARRIER_REQUEST です(Q3)。マルチパートに対してエラーを返さないといけないような気もしますが、OFPT_ERROR と OFPT_BARRIER_REPLY とを続けて同じ xid で送ってよいものでしょうか?
Q1, Q2, Q3 が起こりうる事態か、ということになりますが、これは解釈が難しいところですが、Q1 は起こり得ると考えて構わないと思います。仕様では OFPT_ERROR はスイッチがコントローラに送る、とありますが、逆をやってはいけないという記述が無いことと、OFPT_ERROR が symmetric message に分類されているからです。拡張性をかんがみると experimenter メッセージをスイッチが送って、その応答が error である、というのは十分考えられます:
Controller Switch
| |
| <-------- EXP xid=1 -|
| |
|- MP_REQ xid=1 -----> |
|- MP_REQ xid=1 -----> |
| |
|- ERROR xid=1 ------> |
| |
|- MP_REQ xid=1 fin -> |
| |
| <----- MP_RES xid=1 -|
| |
マルチパートで処理中の xid と同じだけれども、この場合の OFPT_ERROR メッセージの xid は Switch 側が要求した xid に対応する正当な xid となります。
実装上の安全性
仕様から読み出される原理的な背景はこのぐらいで十分だと思います。次に実装上の安全性について考えてみます。
マルチパートを連結する処理ですが、xid のみ注目していて、その xid でマルチパートでないメッセージを受け取った場合にパニックする、というのはありそうです。これを引き起こしそうな動作は避けたほうが良さそうです。
いろいろまとめると、こんな感じになるのではないでしょうか:
- OFPT_ERROR は通常はスイッチ→コントローラの方向のみに使うべき。(実際のところ、スイッチもエラーを報告されてもどうしようもない事が多い。)
- スイッチ→コントローラの request 型の OFPT_EXPERIMENTER は避けたほうが良い。
- 避けられない場合は、OFPT_ERROR でなく、OFPT_EXPERIMENTER で返したほうが良い。
- コントローラ→スイッチの OFPT_EXPERIMENTER は問題ない。OFPT_ERROR をスイッチ→コントローラで返せばよい。
- コントローラが OFPT_MULTIPART_REQUEST を生成している最中にエラーを起こしてしまって、今まで送ったメッセージを無かったことにしたい場合は、OFPT_BARRIER_REQUEST を送ればよい。そして単に対応する OFPT_ERROR を受け取ればよい。
- スイッチは未完成なマルチパートがあるときに OFPT_BARRIER_REQUEST を受け取ったら xid が同じ場合も異なる場合も OFPT_ERROR と OFPT_BARRIER_REPLY の両方を返したほうが矛盾が少ない。
- マルチパートを連結する実装は、メッセージタイプやマルチパートメッセージのタイプも参照して行う。マルチパート連結できないメッセージで、かつ、身に覚えのない xid のメッセージを不幸にして受け取ってしまった場合は、プロトコル上は単に無視するとよい。