はじめに
前回の記事では P4 で記述した簡易 L2 Switch に VLAN (802.1q) 対応,およびトラヒックカウンタ実装を行いました. 本記事では,前回実装した簡易 L2 Switch を P4Runtime で制御し,MACテーブルのエントリ登録,およびカウンタ値の取得を行う P4Runtime client を実装して, P4Runtime を用いたコントロールプレーン開発の基礎について説明します.なお,言語は Go 言語を用いて実装します.
Go を用いた P4Runtime client については既に実装・公開されているものもあり,例えば ebiken さんの github や antonin さんの github が大変参考になります.本記事と併せて参照すれば相乗効果が高いと思います.
本記事のサマリ
本記事には P4Runtime を用いてコントロールプレーンを開発するために必要な要素として,下記の内容が記載されています.
- P4Runtime の概要(コントロールプレーン実装の基礎)
- テーブルエントリの登録方法
- マルチキャストグループの登録方法
- トラヒックカウンタ(Counter)値の取得方法
P4Runtime を用いたコントロールプレーン実装の基礎を「準備編(本記事)」で, P4Runtime の Go 実装を用いたテーブルエントリ登録やトラヒックカウンタ値の取得を「実装編」でそれぞれ説明します.そもそも P4 って何?という方は前回の記事を参照頂ければと思います.
おことわり
本記事は 2020.12 時点の P4Runtime Specification を参照した説明となっています. P4Runtime は標準化作業中の内容であるため,最新版の P4Runtime Specification を公式ページで参照するようお願いいたします.
P4Runtime によるコントロールプレーン実装の基礎
準備編では,そもそも P4Runtime とは何か, を起点とし P4Runtime を用いたコントールプレーン開発の要素として,大きく下記3つについて概説します.
- StreamChannel と Primary 選出
- コントロールプレーンとデータプレーンの接続処理を行います.
- ForwardingPipelineConfig の設定
- P4 で記述した自作データプレーンを target(P4対応デバイス)に設定します.
- Entity の Write/Read
- Entity(データプレーンの制御可能なオブジェクト)の読み書きを行います.
P4Runtime とは
P4Runtime とは P4.org にて仕様化が進められている P4 のコントロールプレーン向けの制御インターフェースです. protocol buffers を用いて記述されており p4lang の github で公開されています. 複数の proto ファイルが存在しますがメインとなるのは p4runtime.proto で,特に P4Runtime の client(コントロールプレーン)は下記のように定義されています(コメントを一部省略しています).
service P4Runtime {
// Update one or more P4 entities on the target.
rpc Write(WriteRequest) returns (WriteResponse) {
}
// Read one or more P4 entities from the target.
rpc Read(ReadRequest) returns (stream ReadResponse) {
}
// Sets the P4 forwarding-pipeline config.
rpc SetForwardingPipelineConfig(SetForwardingPipelineConfigRequest)
returns (SetForwardingPipelineConfigResponse) {
}
// Gets the current P4 forwarding-pipeline config.
rpc GetForwardingPipelineConfig(GetForwardingPipelineConfigRequest)
returns (GetForwardingPipelineConfigResponse) {
}
// Represents the bidirectional stream between the controller and the
// switch (initiated by the controller) ...
rpc StreamChannel(stream StreamMessageRequest)
returns (stream StreamMessageResponse) {
}
rpc Capabilities(CapabilitiesRequest) returns (CapabilitiesResponse) {
}
}
P4Runtime を用いたコントロールプレーンは,上記で定義される RPC (Remote Procedure Call) をベースに gRPC client として実装されます. それぞれの RPC の役割はざっくりと下記のようになります.
-
StreamChannel
RPC- コントロールプレーンとデータプレーン間で双方向のセッションを確立します.
-
SetForwardingPipelineConfig
RPC- データプレーンに対し forwarding-pipeline config を設定します.
-
GetForwardingPipelineConfig
RPC- データプレーンから forwarding-pipeline config を取得します.
-
Write
RPC- P4 Entity (P4Runtime で制御可能なオブジェクト) の update を行います.
-
Read
RPC- P4 Entity (P4Runtime で制御可能なオブジェクト) の read を行います.
Capabilities については本記事では割愛しますが, p4runtime.proto で定義される CapabilitiesRequest/Response
(下記)より推測される限り, データプレーン側が対応している P4Runtime のバージョンを確認する RPC のようです.
message CapabilitiesRequest {
}
message CapabilitiesResponse {
// The full semantic version string (e.g. "1.1.0-rc.1") corresponding to the
// version of the P4Runtime API currently implemented by the server.
string p4runtime_api_version = 1;
}
P4Runtime に対応した target には gRPC server が用意されており,コントロールプレーン(gRPC client)と target(上の gRPC server) が上記の RPC で通信し,テーブルエントリの登録やトラヒックカウンタ値の取得等を行います.
StreamChannel と Primary 選出
P4Runtime でデータプレーンを制御するためには,まずコントロールプレーンとデータプレーン間にセッションを確立する必要があります. StreamChannel
RPC はこのセッション確立を行うための RPC で specification には下記のように記載されています.
To support multiple controllers, P4Runtime uses the streaming channel (available via StreamChannel RPC) for session management. The workflow is described as follows:
- Each controller instance (e.g. a controller process) can participate in one or more roles. For each (device_id, role_id), the controller receives an election_id. This election_id can be the same for different roles and/or devices, as long as the tuple (device_id, role_id, election_id) is unique. For each (device_id, role_id) that the controller wishes to control, it establishes a StreamChannel with the P4Runtime server responsible for that device, and sends a MasterArbitrationUpdate message containing that tuple of (device_id, role_id, election_id) values. The P4Runtime server selects a primary independently for each (device_id, role_id) pair. <以下略>
詳細は前後の文脈を参照頂きたいですが,ざっくりと下記のような意味です.
P4Runtime は複数のコントロールプレーンの接続を許容するために device_id
role_id
election_id
の 3 つのパラメータを用いてコントロールプレーンプロセスを識別します. 特に device_id
と role_id
の組み合わせ毎に election_id
が最大となるコントロールプレーンを Primary に選出します. Primary となったコントロールプレーンは role_id
に紐付く "役割" の範囲内でデータプレーンの排他的な制御が可能となります.
なお role_id
に紐付く "役割" をどのように規定するかは P4Runtime の対象外となります.また role_id
を指定しない場合は "full pipeline access" を表す default role が設定され,その role_id
は 0 となります.
コントロールプレーンは StreamChannel
を介して MasterArbitrationUpdate
message を送信し device_id
role_id
election_id
をデータプレーンに通知します.データプレーンは device_id
role_id
に対応するその時点での Master の election_id
と MasterArbitrationUpdate
の election_id
を比較して Primary の選出を行います.
MasterArbitrationUpdate
は実際には StreamMessageRequest
に含まれる形で StreamChannel
RPC を介してデータプレーンに送信されます. 各 message は p4runtime.proto にてそれぞれ下記のように定義されています(一部コメントを省略しています).
message StreamMessageRequest {
oneof update {
MasterArbitrationUpdate arbitration = 1;
PacketOut packet = 2;
DigestListAck digest_ack = 3;
.google.protobuf.Any other = 4;
}
}
...
message MasterArbitrationUpdate {
uint64 device_id = 1;
Role role = 2;
Uint128 election_id = 3;
.google.rpc.Status status = 4;
}
StreamMessageRequest
に含まれる PacketOut
や DigestListAck
は本記事では割愛しますが,コントロールプレーンとの PacketI/O や Digest 利用時に用いるようです. こちらは ebiken さんの github で実装があるため興味のある方はこちらを参照ください.
このように P4Runtime でデータプレーンを制御するためには,まず P4Runtime client(コントロールプレーン)がデータプレーンとの間で StreamChannel
を確立し Primary となる必要があります.
ForwardingPipelineConfig の設定
コントロールプレーンとデータプレーンの間で StreamChannel
を確立し Primary に選出されたら,次はデータプレーンそのものの設定を行う必要があります. データプレーンに関する設定は ForwardingPipelineConfig
として p4runtime.proto で下記のように定義されています.
message ForwardingPipelineConfig {
config.v1.P4Info p4info = 1;
// Target-specific P4 configuration.
bytes p4_device_config = 2;
// Metadata (cookie) opaque to the target. A control plane may use this field
// to uniquely identify this config. ...
message Cookie {
uint64 cookie = 1;
}
Cookie cookie = 3;
}
p4info
はデータプレーンの各オブジェクト(table や action 等)に関する情報を管理する補助情報で p4info.proto で定義されています.
p4info
および p4_device_config
はいずれも P4 プログラムのコンパイル時に生成されます. コンパイラによって生成方法にばらつきがあるかと思いますが,例えば p4.org による P4 コンパイラの reference 実装 では下記のようにコンパイルを行うことで p4info.txt
と my-p4-program.json
が生成されます(target
や arch
は環境に合わせて変更してください).
p4c --target bmv2 --arch v1model --p4runtime-files p4info.txt my-p4-program.p4
コンパイラによって生成された p4info
と p4_device_config
を格納した ForwardingPipelineConfig
よりSetForwardingPipelineConfig
RPC でデータプレーンの設定を行います. 実際には下記で定義される SetForwardingPipelineConfigRequest
message に ForwardingPipelineConfig
を格納してデータプレーンに送信します(コメントを一部省略しています).
message SetForwardingPipelineConfigRequest {
enum Action {
UNSPECIFIED = 0;
// Verify that the target can realize the given config. ...
VERIFY = 1;
// Save the config if the target can realize it. ...
VERIFY_AND_SAVE = 2;
// Verify, save and realize the given config. ...
VERIFY_AND_COMMIT = 3;
// Realize the last saved, but not yet committed, config. ...
COMMIT = 4;
// Verify, save and realize the given config, while preserving the
// forwarding state in the target. ...
RECONCILE_AND_COMMIT = 5;
}
uint64 device_id = 1;
uint64 role_id = 2;
Uint128 election_id = 3;
Action action = 4;
ForwardingPipelineConfig config = 5;
}
コントロールプレーンの識別のために device_id
role_id
election_id
も同時に格納していますが,これらに加えて下記いずれかの action
を選択します.
- VERIFY
-
ForwardingPipelineConfig
が適用可能かの確認のみ.
-
- VERIFY_AND_SAVE
-
ForwardingPipelineConfig
が適用可能かを確認した後に保存(適用はしない).
-
- VERIFY_AND_COMMIT
-
ForwardingPipelineConfig
が適用可能かを確認した後に適用.
-
- COMMIT
- 最後に保存した
ForwardingPipelineConfig
を適用.
- 最後に保存した
- RECONCILE_AND_COMMIT
- データプレーンの状態を保護しつつ
ForwardingPipelineConfig
の確認と適用.
- データプレーンの状態を保護しつつ
ForwardingPipelineConfig
を Commit まで実施することで,自作データプレーンを target にデプロイすることが出来ます.
Entity の Write/Read
StreamChannel
を確立し ForwardingPipelineConfig
の設定まで完了したら,Entity の Write/Read を通したデータプレーンの制御・データプレーンからの情報取得が可能となります. Entity とは P4Runtime で制御可能なオブジェクトの総称で p4runtime.proto では下記のように定義されています.
message Entity {
oneof entity {
ExternEntry extern_entry = 1;
TableEntry table_entry = 2;
ActionProfileMember action_profile_member = 3;
ActionProfileGroup action_profile_group = 4;
MeterEntry meter_entry = 5;
DirectMeterEntry direct_meter_entry = 6;
CounterEntry counter_entry = 7;
DirectCounterEntry direct_counter_entry = 8;
PacketReplicationEngineEntry packet_replication_engine_entry = 9;
ValueSetEntry value_set_entry = 10;
RegisterEntry register_entry = 11;
DigestEntry digest_entry = 12;
}
}
上記の proto ファイルより,テーブルエントリ(TableEntry
)やトラヒックカウンタ(CounterEntry
)も Entity として定義されていることが分かります.これらの Entity に対し Write
RPC や Read
RPC を通した制御あるいは情報取得を行うことで,コントロールプレーンによるデータプレーンの制御やデータプレーンからの情報取得が可能となります. なお,上記の Entity のうち本記事で取り上げるものは TableEntry
PacketReplicationEngineEntry
CounterEntry
の3つになります.これらの Entity の詳細は「実装編(執筆予定)」で Go 言語による実装も加味しつつ掘り下げて説明します.
Entity の Write
Entity への書き込み(テーブルエントリの登録等)は Write
RPC を用いて行います. コントロールプレーン側では p4runtime.proto で定義される下記の WriteRequest
message に必要な情報を入力しWrite
RPC でデータプレーン側に通知します(下記の記載はコメントを一部省略しています).
message WriteRequest {
uint64 device_id = 1;
uint64 role_id = 2;
Uint128 election_id = 3;
// The write batch, comprising a list of Update operations. ...
repeated Update updates = 4;
enum Atomicity {
// Required. This is the default behavior. The batch is processed in a
// non-atomic manner from a data plane point of view. ...
CONTINUE_ON_ERROR = 0;
// Optional. Operations within the batch are committed to data plane until
// an error is encountered. At this point, the operations must be rolled
// back such that both software and data plane state is consistent with the
// state before the batch was attempted. ...
ROLLBACK_ON_ERROR = 1;
// Optional. Every data plane packet is guaranteed to be processed according
// to table contents before the batch began, or after the batch completed
// and the operations were programmed to the hardware. ...
DATAPLANE_ATOMIC = 2;
}
Atomicity atomicity = 5;
}
上記より WriteRequest
message では,コントロールプレーンを識別するための device_id
role_id
election_id
に加えて Update
message および atomicity
を指定することが分かります.
atomicity
は Entity の制御にエラーが発生した際の挙動を指定するパラメータで,それぞれ下記のように規定されているようです.
- CONTINUE_ON_ERROR
- エラーが発生しても処理を続行する(特に復元処理は行わない).
- ROLLBACK_ON_ERROR
- エラー発生時は処理を無効化し,データプレーンを
Write
RPC 以前の状態に戻す.
- エラー発生時は処理を無効化し,データプレーンを
- DATAPLANE_ATOMIC
- 処理適用中のデータプレーンは
Write
RPC の直前の状態であることを保証する.
- 処理適用中のデータプレーンは
Update
message は p4runtime.proto で下記のように定義されています.
message Update {
enum Type {
UNSPECIFIED = 0;
INSERT = 1;
MODIFY = 2;
DELETE = 3;
}
Type type = 1;
Entity entity = 2;
}
Write
RPC で Entity を制御する際は WriteRequest
message に制御したい Entity の Update
message を格納し(複数指定可),かつこれらの Update
message に UpdateType
と Entity
の情報をそれぞれ格納する形になります.なお UpdateType
としては下記の3パターンがあります.
- INSERT
- 新規 Entity を登録する.
- MODIFY
- 既存 Entity を修正する.
- DELETE
- 既存 Entity を削除する.
このように Entity を制御する場合は WriteRequest
message に上記で説明した各種情報を入力し Write
RPC で server(データプレーン)側に通知することで,テーブルエントリ登録などの処理を行うことが出来ます.
Entity の Read
Entity に関する情報の取得は Read
RPC を用いて行います.コントロールプレーン側では ReadRequest
message を Read
RPC でデータプレーンに通知し,その返信として ReadResponse
message を受け取ります.それぞれ p4runtime.proto で下記のように定義されています.
message ReadRequest {
uint64 device_id = 1;
repeated Entity entities = 2;
}
message ReadResponse {
repeated Entity entities = 1;
}
情報を取得したい Entity を ReadRequest
message に入れてデータプレーンに通知すると,その値が格納された Entity が返ってくる,という非常にシンプルな仕組みになっています.
ReadRequest
message では device_id
のみを指定しています. この点について specification では下記のような記載があります.
Since ReadRequests do not mutate any state on the switch, they do not require an election_id, and they do not require the presence of an open StreamChannel between the server and client.
Read
RPC はデータプレーンの状態を変更するものではないため,Entity の情報を読み込むだけならば Primary である必要はなく,Entity を含むデバイスがどれかが分かれば十分のようです.
おわりに
本記事では P4Runtime を用いてコントロールプレーンを実装する際の基礎として,下記について整理しました.
- P4Runtime の概要
- StreamChannel と Primary 選出
- ForwardingPipelineConfig の設定
- Entity の Write/Read
「実装編」では,これらの各処理を Go 言語を用いて実装し,前回の記事で実装した簡易 L2 Switch のテーブルエントリ登録とトラヒックカウンタ値取得を行います.
改訂記録
- P4Runtime の仕様更新 (2020.12.12)
- 2020.12.1 付けで P4Runtime の仕様が v1.2.0 → v1.3.0 に更新されたことに伴い,下記修正しました.
- P4Runtime 仕様のリンク更新
- master の表記をそれぞれ primary に変更.
- 2020.12.1 付けで P4Runtime の仕様が v1.2.0 → v1.3.0 に更新されたことに伴い,下記修正しました.