はじめに
前回の記事では P4Runtime を用いてテーブルエントリ登録やトラヒックカウンタ値の取得を行うコントロールプレーン機能を実装しました. 今回は取得したトラヒックカウンタ値に基づき,指定のトラヒック量を超過した場合に一定時間トラヒック制限を適用する機能を実装します.特に,カウンタ値の取得は送信元 MAC アドレス毎に行い,またデータプレーンにおけるトラヒック制限は Meter (RFC2698) を用いて実装します.
本記事で説明する内容のソースコードはこちらで公開していますので併せて参照ください.P4 ってそもそも何?という方はこちらの記事を,P4Runtime の基礎から説明が必要な方はこちらの記事をそれぞれ参照ください.
本記事のサマリ
本記事には P4/P4Runtime で送信元 MAC アドレス毎のトラヒック制限機能を実装するための要素として下記の内容が記載されています.
- Meter (RFC2698) の動作原理
- direct meter / direct counter の概念,および P4 言語による実装
- direct meter / direct counter の P4Runtime による制御方法,および Go 言語による実装
注意
本記事で説明する P4 プログラムは v1model をアーキティクチャとして使用しています.細かい実装については使用するデバイス(アーキティクチャ)毎に若干異なる点に注意してください.P4 におけるアーキティクチャの概念についてはこちらの記事やこちらの記事で説明していますので併せて参照ください.
Meter(RFC2698)の動作原理
Meter は Two Rate Three Color Marker (trTCM) とも呼ばれ,2種類の閾値に対し,トラヒック流量に応じた3種類の "色付け" を行う機構であり RFC2698 で規定されています.一般に meter はパケットの明示的なドロップやマーキングにより流量制御を行う場合に使用されますが,meter 自体はあくまで閾値に対する流量の超過を "色" で出力するのみであり,その結果をどう利用するかはデータプレーン側の実装に委ねられます.
Meter には committed
と peak
の2種類の information rate / burst size が規定されており,通過するトラヒックの流量(information rate = 帯域 / パケットサイズ = burst size)がそれぞれを超過した場合に対応する "色" を出力します.出力する "色" とその条件は下記となります.
-
GREEN
⇒committed
およびpeak
いずれも超過していない場合 -
YELLOW
⇒committed
を超過しているがpeak
は超過していない場合 -
RED
⇒peak
を超過している場合
これらの4種類の閾値はコントロールプレーンから設定し,データプレーンは任意のタイミングで(v1model では read
という関数,PSA では execute
という関数を使用して)対応する "色" を取得します.なお,各閾値はそれぞれ下記のように省略して記載される場合が多いようです(P4Runtime でも各閾値は下記のような名前の変数としてそれぞれ扱われます).
-
CIR
: committed information rate -
CBS
: committed burst size -
PIR
: peak information rate -
PBS
: peak burst size
meter の動作は committed
および peak
に対応する2つのトークンバケット(以下ではそれぞれ P
と C
と表記)を用いてモデル化されます.P
および C
のサイズはそれぞれ PBS
と CBS
(対応するバーストサイズ)です.初期状態では P
および C
はそれぞれ最大量(PBS
/ CBS
)で満たされていますが,トラヒックが通過する毎に通過した量だけトークンバケットの中身が減少していきます.なお P
および C
の中身は毎秒 PIR
/ CIR
ずつ増加し,最大で PBS
/ CBS
まで中身が回復します.従って,トークンバケットの中身が空になると下記のいずれかを意味します.
- burst size 以上の大きさのパケットが通過
- 単位時間あたりに information rate 以上のトラヒックが通過
"色" の出力の仕方は color-blind
mode と color-aware
mode の2パターンありますが,基本はいずれも同じでトークンバケットが空かどうかで出力する "色" を決定します.すなわち,それぞれ下記の場合に各色を出力します.
-
GREEN
⇒P
およびC
がいずれも空でない場合 -
YELLOW
⇒P
は空でないが,C
が空である場合 -
RED
⇒P
が空である場合
color-blind
mode の場合はトークンバケットが空かどうかのみで "色" の判定を行いますが,color-aware
mode の場合はトークンバケットが空かどうか,に加えて入力パケットに対する事前の "色付け" を許容し,パケットが事前に色付けされている場合はそちらの色を優先して出力します.
今回は上記で説明した meter を利用してトラヒック制限機能を実装します.特に送信元 MAC アドレス毎に meter を用意し,トラヒック制限を有効化している場合のみ read
で色を出力 & 色が RED
の場合にパケットドロップをデータプレーン側で実行することで,コントロールプレーンから設定した PIR
および PBS
にトラヒックを制限します.以下では,P4 および P4Runtime を用いてこれらの処理をどのように記述するか,について説明します.
direct meter / direct counter の P4 実装
送信元 MAC アドレス毎に「トラヒック制限」および「トラヒックカウント」を行うために,テーブルエントリ単位でこれらの機能が実行される direct meter および direct counter を使用します.direct meter / direct counter はそれぞれ P4 で下記のように定義されます(limitter
および meter_cnt
という direct meter / direct counter をそれぞれ定義しています).
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
direct_counter(CounterType.bytes) meter_cnt;
direct_meter<bit<2>>(MeterType.bytes) limitter;
<中略>
}
direct_meter<T>...
の T
には bit<W>
を指定します.これは direct meter を read
した際に出力される color のビット幅を表し 2 <= W
である必要があります.今回の実装では color を格納するための変数を 2bit 幅で定義しているので direct_meter<bit<2>>...
として direct meter を定義しています.
direct_meter / direct counter の測定単位は MeterType
および CounterType
で指定しますが,v1model ではそれぞれ下記のいずれかから選択可能です.
enum CounterType {
packets,
bytes,
packets_and_bytes
}
enum MeterType {
packets,
bytes
}
direct meter / direct counter はテーブルエントリ単位で実行されるため,P4 プログラムでは table に紐付ける形で記述します.特に今回は送信元 MAC アドレス毎に direct meter / direct counter を実行するため,送信元 MAC アドレスを key とする table (check_limit
) に対して下記のように direct meter / direct counter を紐付けます.
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
<中略>
table check_limit {
key = { hdr.ethernet.srcAddr: exact; }
actions = {
limit_traffic;
NoAction;
}
size = 1024;
default_action = NoAction;
counters = meter_cnt;
meters = limitter;
}
<中略>
}
table の設定項目に couters
と meters
があり,それぞれに対して先に定義した direct counter / direct meter を設定することで,当該 table の各エントリにこれらの counter と meter が紐つく形となります.
direct counter については前回の記事で実装した counter と若干異なり,明示的に count
はせず,テーブルエントリに hit した際に自動的にトラヒック量がカウントされます.したがって,データプレーン側では基本的に何もせず,コントロールプレーンは所望のテーブルエントリを指定してそのエントリに紐つくカウンタ値を取得します.一方で meter についてはトラヒックの color
を取得し,得られた color
に応じてどのような処理を行うかをデータプレーン側で記述します.今回の実装 では meter で取得した color
が RED
の場合(PIR
あるいは PBS
を超過している場合)に当該パケットをドロップする実装としています.こちらは limit_traffic
という action で実装しており,下記のように記述されます.
struct metadata {
bit<2> color;
bool drop_flag;
}
<中略>
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
<中略>
action limit_traffic() {
limitter.read(meta.color);
if (meta.color == V1MODEL_METER_COLOR_RED) {
meta.drop_flag = true;
}
}
<中略>
}
v1model では meter が出力する color
を下記のように定義しており read
で取得した color
を下記と比較することで meter が出力した color
を判別します.なお,meter が下記 color
を出力する閾値についてはコントロールプレーンから実装しますが,こちらの詳細は本記事の後半で説明します.
#define V1MODEL_METER_COLOR_GREEN 0
#define V1MODEL_METER_COLOR_YELLOW 1
#define V1MODEL_METER_COLOR_RED 2
上記の各要素を組み合わせることで送信元 MAC アドレス毎のトラヒック制限機能( = 入力トラヒックが指定レート/サイズを超過した場合にパケットをドロップする処理)が実装可能です.入力パケットに対して,まずトラヒック制限要否を判断する table (check_limit
) を適用し,対応するテーブルエントリ(送信元 MAC アドレス)の action が traffic_limit
かつ meter の出力が RED
の場合に drop_flag
を true にして, drop()
を実行し当該パケットをドロップします.こちらは P4 で下記のように記述されます.
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
<中略>
action drop() {
mark_to_drop(standard_metadata);
}
<中略>
apply {
meta.drop_flag = false;
check_limit.apply();
if (meta.drop_flag == true) {
drop();
} else {
/* L2 Switching */
}
}
}
ckeck_limit
table のエントリが監視対象の MAC アドレスに対応し,テーブルエントリの登録 / 削除によって監視対象の MAC アドレスを追加・削除します.また,各エントリ(監視対象の MAC アドレス)の action は初期登録時は NoAction()
とすることでトラヒック制限機能を無効化しています.コントロールプレーンから各エントリのトラヒックカウンタ値を定期的に取得して,閾値を超えた場合に対応するエントリの action を NoAction
から limit_traffic
に切り替えることでトラヒック制限機能を有効化しています. これらのコントロールプレーン実装の詳細についてはこちらを参照ください.
以上は direct meter / direct counter を用いて送信元 MAC アドレス毎のトラヒック制限機能を実現するためのデータプレーン側の実装となります.以下ではコントロールプレーンから direct meter / direct counter を制御する P4Runtime 実装(特に Go 言語による実装)について要点を説明します.
direct meter / direct counter 制御の Go 実装
P4 で実装したトラヒック制限機能やトラヒックカウンタは P4Runtime を用いてコントロールプレーンから制御されます.P4Runtime では direct meter や direct counter はテーブルエントリと同様 Entity
として扱われます.従って,各 Entity
を制御 or 各 Entity
から情報を取得する場合はこちらの記事やこちらの記事で説明したように Entity
の Write()
や Read()
という形で実装されます.
direct meter や direct counter の Entity
はそれぞれ p4runtime.pb.go で下記のように定義されています.
/* direct meter の Entity(と関連する変数)*/
type Entity_DirectMeterEntry struct {
DirectMeterEntry *DirectMeterEntry
}
type DirectMeterEntry struct {
// The associated table entry. This field is required.
// table_entry.action is ignored. Other fields specify the match.
TableEntry *TableEntry
Config *MeterConfig
}
type MeterConfig struct {
// Committed information rate (units per sec)
Cir int64
// Committed burst size
Cburst int64
// Peak information rate (units per sec)
Pir int64
// Peak burst size
Pburst int64
}
/* direct counter の Entity(と関連する変数)*/
type Entity_DirectCounterEntry struct {
DirectCounterEntry *DirectCounterEntry
}
type DirectCounterEntry struct {
// The associated table entry. This field is required.
// table_entry.action is ignored. Other fields specify the match.
TableEntry *TableEntry
Data *CounterData
}
type CounterData struct {
ByteCount int64
PacketCount int64
}
direct meter については下記のパラメータを設定した Entity
を Update
に格納し Write()
でデータプレーン側に書き込みます.
- committed information rate(
Cir
) - committed burst size(
Cburst
) - peak information rate(
Pir
) - peak burst size(
Pburst
)
先に説明した通り Cir
/ Cburst
は YELLOW
に対応するレート / サイズ,Pir
/ Pburst
は RED
に対応するレート / サイズです.このとき UpdateType
は MODIFY
のみ許容されている点に注意が必要です.基本的に "direct" な meter や counter はテーブルエントリと表裏一体なので,これらの INSERT
や DELETE
は元のテーブルエントリに付随して実行されるようです.direct meter の Entity
を生成し Write()
で書き込む処理は Go で下記のように実装されます.
import(
v1 "github.com/p4lang/p4runtime/go/p4/v1"
)
func main(){
/* direct meter の Entity 生成 */
directmeterentry := &v1.Entity_DirectMeterEntry{
DirectMeterEntry: &v1.DirectMeterEntry{
TableEntry: /* 設定したい table entry */,
Config: &v1.MeterConfig{
Cir: /* committed information rate (in int64) */,
Cburst: /* committed burst size (in int64) */,
Pir: /* peak information rate (in int64) */,
Pburst: /* peak burst size (in int64) */,
},
},
}
/* Update 生成 */
update := &v1.Update{
Type: v1.Update_MODIFY,
Entity: &v1.Entity{
Entity: directmeterentry,
},
}
updates := []*v1.Update{update}
/* Write() でデータプレーンに書き込み */
atomicityType := /* Atomicity を指定 */
var atomicity v1.WriteRequest_Atomicity
switch atomicityType {
case "CONTINUE_ON_ERROR":
atomicity = v1.WriteRequest_CONTINUE_ON_ERROR
case "ROLLBACK_ON_ERROR": // OPTIONAL
atomicity = v1.WriteRequest_ROLLBACK_ON_ERROR
case "DATAPLANE_ATOMIC": // OPTIONAL
atomicity = v1.WriteRequest_DATAPLANE_ATOMIC
default:
atomicity = v1.WriteRequest_CONTINUE_ON_ERROR
}
write_request := v1.WriteRequest{
DeviceId: /* device id */,
ElectionId: /* election id */,
Updates: updates,
Atomicity: atomicity,
}
write_response, err := client.Write(context.TODO(), &write_request)
if err != nil {
/* Error 処理 */
}
}
なお上記の client
は P4RuntimeClient
のインスタンスであり,事前に StreamChannel の確立や Primary 選出,および ForwardingPipelineConfig の設定等が行われているものとしています.詳細は前回の記事を参照ください.
direct counter からトラヒックカウンタ値を取得する場合も,direct meter と同様に Entity_DirectCounterEntry
にカウンタ値を取得したいテーブルエントリを設定して Read()
でデータプレーン側から値を取得します.direct counter の Entity
を生成し Read()
でカウンタ値を取得する部分は Go で下記のように実装されます.
import(
v1 "github.com/p4lang/p4runtime/go/p4/v1"
)
func main(){
/* direct counter の Entity 生成 */
directcounterentry := &v1.Entity_DirectCounterEntry{
DirectCounterEntry: &v1.DirectCounterEntry{
TableEntry: /* カウンタ値を取得したい table entry */,
},
}
/* ReadClient の生成 */
read_request := v1.ReadRequest{
DeviceId: /* device id */,
Entities: []*v1.Entity{&v1.Entity{ Entity: directcounterentry}},
}
read_client, err := client.Read(context.TODO(), &read_request)
if err != nil {
/* Error 処理 */
}
/* カウンタ値(DirectCounterEntry)の取得 */
read_response, err := read_client.Recv()
if err != nil {
/* Error 処理 */
}
entities := read_response.GetEntities()
if entity == nil {
/* Error 処理 */
}
}
entities
にカウンタ値をもつ DirectCounterEntry
が格納されているため,こちらからカウンタ値を取得します.なお direct counter のカウンタ値を指定の値に変更したい(特に今回の実装ではカウンタ値を 0 にクリアする)場合は Data
に指定の値を格納し UpdateType
を MODIFY
としてデータプレーンに書き込むことで実装出来ます.例えば,あるテーブルエントリの direct counter をゼロクリアする場合は下記のように実装されます(Write()
でデータプレーンに書き込む部分は direct meter と同様のため省略しています).
import(
v1 "github.com/p4lang/p4runtime/go/p4/v1"
)
func main(){
/* direct counter の Entity 生成 */
var zero_int64 int64 = 0
directcounterentry := &v1.Entity_DirectCounterEntry{
DirectCounterEntry: &v1.DirectCounterEntry{
TableEntry: /* カウンタ値を変更したい table entry */,
Data: &v1.CounterData{
ByteCount: zero_int64,
PacketCount: zero_int64,
},
},
}
/* Update を生成 */
update := &v1.Update{
Type: v1.Update_MODIFY,
Entity: &v1.Entity{
Entity: directcounterentry,
},
}
updates := []*v1.Update{update}
/* Write() でデータプレーンに書き込み */
}
おわりに
本記事では送信元 MAC アドレス毎のトラヒック制限機能を P4/P4Runtime で実装するための基礎として下記を説明しました.
- Meter (RFC2698) の動作原理
- direct meter / direct counter の概念,およびその P4 実装
- direct meter / direct counter の P4Runtime による制御方法,およびその Go 実装