以下の内容はhttps://www.valinux.co.jp/blog/entry/20260312より取得しました。


新Linuxカーネル解読室 - ネットワークトラフィック制御 ~TBF解説~

「Linuxカーネル2.6解読室」(以降、旧版)出版後、Linuxには多くの機能が追加され、エンタープライズ領域をはじめとする様々な場所で使われるようになりました。
それに伴いコードが肥大かつ複雑化し、多くのエンジニアにとって解読不能なブラックボックスとなっています。
世界中のトップエンジニア達の傑作であるLinuxカーネルにメスを入れ、ブラックボックスをこじ開けて、時に好奇心の赴くままにカーネルの世界を解読する「新Linuxカーネル解読室」プロジェクト。

本記事では、Qdiscとそのアルゴリズムである TBF (Token Bucket Filter) についてカーネルv6.14のコードをベースに解説します。

執筆者:千葉工業大学 先進工学研究科 未来ロボティクス専攻 井上 叡 (インターン生)、
    野口 裕貴

※ 「新Linuxカーネル解読室」連載記事一覧はこちら


1. はじめに

現在、Linuxは様々な用途のサーバーとして広く利用されています。サーバーとしてクライアントに快適なサービスを提供する際には、限られたネットワークリソースを公平かつ効率的に分配する必要があります。

これを怠ると、特定のユーザーや一部のアプリケーションによる大容量データの転送が回線全体を占有してしまい、他のユーザーの通信速度が極端に低下したり、リアルタイム性が求められる音声や映像などの通信が途切れたりするなど、サービス全体の品質に深刻な影響を及ぼします。

このような一部の通信による帯域の独占や、突発的なパケット送出によるネットワークの輻輳(ふくそう)を回避するためには、通信の優先順位を管理する優先制御や送信レートを制限する帯域制御を組み合わせた、適切なトラフィック制御が不可欠です。

Linuxでは tc(8)コマンドを通じてQdisc (Queuing Discipline) の設定を行うことで、トラフィック制御を適用できます。QdiscはLinuxカーネルの機能の一部であり、パケットを送信する際の順序やタイミングを管理する「スケジューラー」として機能します。そのアルゴリズムは30種類近く存在し、用途に応じて最適なものを選択することができます。本記事では、このtc(8)コマンドとQdiscの概要、そしてアルゴリズムの一つである TBF (Token Bucket Filter) について紹介します。

2. tc(8)コマンドとQdisc

初めに、tc(8)コマンドについて紹介します。このコマンドはmanページで紹介されている通り、Linuxカーネルにおけるトラフィック制御を設定するために利用されます。Linuxにおいて、トラフィック制御は以下の4つの要素から構成されます。

  1. ドロップ(DROPPING)

    • 帯域上限を超えたパケットを破棄すること。ingress側とegress側の両方で発生する*1
  2. シェーピング(SHAPING)

    • 帯域上限を超えたパケットをキューにバッファさせることでパケットの送信間隔を制御する。これにより、帯域幅を制限するだけでなく、トラフィックの突発的な急増(バースト)を平滑化し、ネットワークの挙動を安定にさせる。egress側で行うトラフィック制御

      図1. シェーピングの動作 イメージ

  3. ポリシング(POLICING)

    • 帯域上限を超えたパケットをドロップすることで帯域制御を行う。ingress側で行うトラフィック制御

      図2. ポリシングの動作 イメージ

  4. スケジューリング(SCHEDULING)

    • パケット送信時に送信順序の並び替え(優先順位付けとも呼ぶ)を行うこと。大量データ転送の帯域を確保しつつ、リアルタイム性が求められるトラフィックの応答性を向上させることができる

      図3. スケジューリングの動作 イメージ

Linuxのトラフィック制御では、これら4つの要素を 「qdisc」・「class」・「filter」 の3つの機能を使って実現します。

  • qdisc

    • パケットのスケジュール(送出順序)やレート(送出速度)を制御する仕組み。カーネルがパケットを送信する際、Ethernetドライバーへ渡す直前でパケットをQdiscへ送り込み、設定されたアルゴリズムに基づいて制御を行う
  • class

    • 一部のQdiscはclassという階層構造を持つことが可能で、その内部に別のQdiscを保持できる。これらを組み合わせることで、より複雑なトラフィック制御を実現する
  • filter

    • classを持つQdiscにおいて、パケットがどのclassに格納されるか振り分ける仕組み。独立した機能ではなく、Qdiscの動作プロセスの一部として機能する

Qdiscの最も単純な利用法は、classを持たないQdiscを単体で利用することです。複雑な帯域制御が必要ではないならば、これで十分な場合が多いです。

3. Qdiscの概要

Qdiscの主な役割は、Linuxカーネル内部におけるパケットのキューイングです。パケットをキューに格納し、トラフィックを制御した上でEthernetドライバーに渡す仕組みであるため、主にパケット送信時に機能します。

本記事では、UDPによるパケット送信を例に挙げ、送信時におけるQdiscの動作について解説します。

以下にUDPを用いたパケット送信処理の全体像を示します。なお、UDP/IPの送信処理全体を含めるとスコープが広がりすぎるため、詳細な解説は別記事にて詳しく解説する予定です。本記事では、パケットをNICのドライバーに渡す前、すなわち Qdiscにパケットがenqueueされてから、dequeueされるまでの動き に焦点を当てて解説していきます。

図4. UDPによるパケット送信時の流れ

上図からわかる通り、Qdiscは主に、パケットをNICのドライバーへ渡す直前で処理を行います。

UDPでパケットを送信する場合、カーネル内では以下の流れで関数が呼ばれています。 ここでは流れを簡略化していますが、UDPレイヤー、IPレイヤー、neighbor、netdeviceの処理を経て、__dev_xmit_skb()という関数に辿り着きます。*2

  • neighbor: IPアドレスとMACアドレスの対応を管理するサブシステム
  • netdevice: イーサドライバー等のネットワークドライバーを抽象化するためのサブシステム
<UDPレイヤー>
 udp_sendmsg() -> (省略) -> udp_send_skb() 
                                ↓
<IPレイヤー>
ip_send_skb() -> (省略) -> ip_finish_output2()
                                ↓
<neighbor>
neigh_output() -> (省略) -> neigh_hh_output()
                                ↓
<netdevice>
dev_queue_xmit() -> __dev_queue_xmit() -> dev_xmit_skb() 👈ここ!

ここで重要なのは、Qdiscはネットワークデバイス(net_device構造体)に紐付いて管理されているという点です。*3

各NICは自身のQdiscインスタンスを保持しており、この紐付けに基づいて適切なQdiscが呼び出されます。

skbとは、カーネル内でパケットデータを扱うための「ソケットバッファ(sk_buff構造体)」を指します。本記事では、コードの処理を追う箇所では「skb」、ネットワーク制御の概念を説明する箇所では「パケット」と呼び分けることがありますが、いずれも送信待ちのデータそのものを指しています。

では、__dev_xmit_skb()という関数の実装を少しだけ見てみましょう。

static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
                 struct net_device *dev,
                 struct netdev_queue *txq)
{
/* ~~ 省略 ~~  */
    spin_lock(root_lock); // 👈 スピンロックによる排他制御
/* ~~ 省略 ~~  */
    } else {
        WRITE_ONCE(q->owner, smp_processor_id());
        rc = dev_qdisc_enqueue(skb, q, &to_free, txq);   // 👈 enqueue関数
        WRITE_ONCE(q->owner, -1);
        if (qdisc_run_begin(q)) {  // 👈 Qdiscの処理開始(など)
            if (unlikely(contended)) {
                spin_unlock(&q->busylock);
                contended = false;
            }
            __qdisc_run(q);        // 👈 Qdiscのメイン処理。内部でdequeue関数が呼ばれる
            qdisc_run_end(q);      // 👈 Qdiscの処理終了
        }
    }
    spin_unlock(root_lock);
/* ~~ 省略 ~~  */
}

処理の流れとしては以下のようになります。

  1. spin_lock(root_lock):複数のコンテキストによる同時操作を防ぐため、スピンロックで排他制御を行う
  2. dev_qdisc_enqueue() :skbをQdisc内のキューに格納する(enqueue)*4
  3. qdisc_run_begin():Qdiscが他で実行中かを確認し、未実行であれば実行中状態へ遷移させる。この時、処理の実行権を取れなかったコンテキストは、実行権を持つコンテキストに以降の処理を委譲する
  4. __qdisc_run():キューからskbを取り出す「dequeue」と「実際の送信関数」をセットで呼び出し、ドライバーへと送信する

この際、3の処理で既にQdisc実行中の場合は、すでに実行中のコンテキストに、後続処理(キューに積んだパケットの処理)を委譲し、そのまま呼び元に戻ります。そのためアプリから見ると、Qdiscの実行権を取得できたかどうかでシステムコールから返ってくる時間にばらつきが発生します。

このように、Qdisc に関連する処理のほとんどは__dev_xmit_skb()内で完結しています。Qdiscは、まさにskbを送信(xmit)する動作にフックする形で実装されていると言ってよいのではないかと思います。本記事では、Qdiscの各種アルゴリズムを解説する記事であり、Qdiscの呼び出し前後にあるロジックについては、また別記事で解説する予定です。

上記関数の掲載部分以外の分岐でも、沢山Qdiscに関連する関数が呼ばれています。Qdiscの関連範囲だけなら比較的簡単に読めるため、興味のある方は一度読んでみるとよいかもしれません。*5

4. TBF 概要

Qdiscのアルゴリズムである TBF (Token Bucket Filter) について紹介します。

TBFは、内部に他のQdiscへの参照を保持できる構造を持っています。パケット送信時のdequeue処理では、まず内部に保持されたQdisc(inner Qdisc)からパケットを取り出します。このように、TBFは内部に別のQdiscを包含できる仕組みを備えているため、階層構造を構築可能なQdiscと言えます。*6

対照的に、CoDelなどのQdiscは階層構造を持ちません。dequeue処理時には内部のキューから直接パケットを取り出すため、他のQdiscを参照・保持する仕組みはなく、単独で機能するQdiscとして動作します。

TBF自体は帯域を調整するシェーピング機能のみを担いますが、inner Qdiscの設定によっては、そこでパケットがドロップされることもあります。 *7

TBFの帯域制御には、Token Bucketという概念が用いられています。この仕組みは文章だけでは直感的に理解しづらいため、図を用いて説明します。

図5. TBF (Token Bucket Filter) におけるパケット送信制御のフローのイメージ

図中には様々な要素があります。それぞれ以下のような意味を持ちます。

  • トークン(token)
    • パケットの送信権。これが一定量(1パケット分のbytes数)溜まるとパケットの送信が許可される
  • バケツ(bucket)
    • トークンをためておく場所。バケツの大きさによって一時的な大量通信(バースト送信)をどれくらい許容するかが決まる
  • キュー(queue)
    • 送信パケットが必ず通るキュー。dequeueに失敗し、パケットが送信されない場合はここに蓄積される

基本的な処理として、時間経過とともにバケツの中へトークンが蓄積されていきます。 送信パケットはキューを通過する際、バケツ内にあるトークンの量に応じて、送信(dequeue)が可能かどうかが判断されます。トークンが十分にあれば、パケットはdequeueされますが、不足している場合はそのままキューに留まります。 このとき、トークンがバケツに供給される速度が、TBFで設定した「定常時の最大送信レート」となります。これにより、パケットの送信レートを一定の帯域内に制限することができます。

制限された帯域幅に対して実際の送信レートが低い期間が続くと、バケツにはトークンが蓄積されていきます。この状態で突発的に大量のデータ送信が必要になった際、蓄積された分のトークンを一度に消費することで連続的なdequeueが行われ、一時的に高速な送信が発生します。これをバースト送信と呼びます。このバースト時の最大送信レートは、デフォルトではネットワークの物理的な上限速度となりますが、TBFのパラメーターを調整することで、この上限値も制御可能です。

ここまで説明してきましたが、まとめるとTBFは以下の機能を持ちます。

  1. 定常時の最大送信レートの制限
  2. バースト送信時の送信パケット量の制限
  3. バースト送信時の最大送信レートの制限

5. TBFのパラメーター

ここまで、TBFの基本的な機能について説明してきました。これまでの説明では理解しやすさを優先してパラメーターの詳細には触れませんでしたが、実際にtc(8)コマンドでTBFを運用する際には、各種パラメーターに適切な値を設定する必要があります。

各パラメーターが具体的に何を表しているのか、先ほどの図と照らし合わせながら説明します。

図6. TBFのパラメーター:rate・burst・limitのイメージ

図に示すとおり、TBFを利用するためには、最低限以下のパラメーターを設定する必要があります。

名称 単位 説明
rate bps 定常時の送信における最大送信レート
burst byte トークンを貯めることのできるバケツの容量。バースト送信時に送信されるパケットの最大量でもある
limit byte TBFが内部に持つキューの最大長。これを超えた場合、新たに供給されたパケットはドロップされる
latency ms limitと同じ役割。latencyとして設定すると、設定した最大許容遅延に応じたキューの長さを計算して設定される

latencyを設定すると、内部的に適切なlimitの値が自動計算されて適用されます。そのためlatencylimitパラメーターを同時に設定することはできません。

以下のパラメーターはバースト送信時のレートを設定する際に使用します。設定は任意ですが設定しない場合、バースト送信が発生した際はそのネットワークが出せる最大のレートでパケットが送信されます。

名称 単位 説明
peakrate bps バースト送信が発生した際のピークの送信レート。peakrate > rateの関係を満たす必要がある
mtu/minburst byte トークンがこのパラメーターの値以上溜まっている時にパケットの送信要求があると、パケットはバースト送信される。デフォルトではデバイスのmtuと同じ値が設定される

先ほど、TBFは3つの機能を持つと説明しましたが、それぞれの機能に対応するパラメーターは以下の通りです。

  1. 定常時の最大送信レートの制限 ⇒ rate
  2. バースト送信時の送信パケット量の制限 ⇒ burst
  3. バースト送信時の最大送信レートの制限 ⇒ peakrate, mtu/minburst

6. TBFのアルゴリズム

次はTBFのアルゴリズムについて解説します。TBFはQdiscですので、enqueue関数とdequeue関数が動作の基本となります。

6.1 enqueueアルゴリズム

enqueueのアルゴリズムは単純です。以下の図は、送信要求されたデータ(skb)がパケットとして処理される際のフローイメージです。

図7. TBF enqueue処理のフローのイメージ

送信要求としてQdiscに渡されるskbに対して、以下のフローで処理を行います。

  • skbのサイズ(パケット長)を確認し、これが閾値q->max_sizeを超えるかどうか確認する
    • このq->max_sizeに入る値はpeakrateの設定有無により以下のように変化する
      • 【peakrateの設定がない場合】
        • 処理:max_size = min_t(u64, psched_ns_t2l(&rate, buffer), ~0U);
          • buffer には burst の値が格納されているため、基本的には max_size = burst となる *8
      • 【peakrateの設定がある場合】
        • 処理:max_size = min_t(u64, max_size, psched_ns_t2l(&peak, mtu));
        • 基本的にはburst > mtu/minburstの関係であるため、mtu/minburstの値が閾値として設定される
  • 閾値を超えない場合
    • enqueueする
  • 閾値を超える場合:以下の2つの条件を両方満たす場合、分割してenqueueする。分割処理はtbf_segment()が実施する
    1. skbが GSO(Generic Segmentation Offload)対応 *9であるか
    2. q->max_sizeに収まるように分割可能であるか

    条件を満たさない場合、そのskbはドロップされる

実際の実装はこちら

static int tbf_enqueue(struct sk_buff *skb, struct Qdisc *sch,
                       struct sk_buff **to_free)
{
        struct tbf_sched_data *q = qdisc_priv(sch);
        unsigned int len = qdisc_pkt_len(skb);
        int ret;

        if (qdisc_pkt_len(skb) > q->max_size) {
                if (skb_is_gso(skb) &&
                    skb_gso_validate_mac_len(skb, q->max_size))
                        return tbf_segment(skb, sch, to_free);
                return qdisc_drop(skb, sch, to_free);
        }
        ret = qdisc_enqueue(skb, q->qdisc, to_free);
        if (ret != NET_XMIT_SUCCESS) {
                if (net_xmit_drop_count(ret))
                        qdisc_qstats_drop(sch);
                return ret;
        }

        sch->qstats.backlog += len;
        sch->q.qlen++;
        return NET_XMIT_SUCCESS;
}

6.2 dequeueアルゴリズム

TBFのdequeue処理において、帯域制限の要となるトークンは2種類存在し、それぞれ個別に管理されています。定常時の送信レートを制限するrate用のトークンは toks、バースト送信時の最大レートを制限するpeakrate用のトークンはptoksとして実装内で定義されています。

各トークンの性質は以下の表の通りです。

項目 toks ptoks
役割 定常時の最大送信レート制限 バースト送信時の最大送信レートの制限
最大値(容量) burstに相当する時間 [ns] mtu/minburstに相当する時間 [ns]
消費量の計算 skbのサイズ(パケット長) / rate [ns] skbのサイズ (パケット長)/ peakrate [ns]
供給について 前回処理からの経過時間に応じて供給 [ns]
ptoksと同量)
前回処理からの経過時間に応じて供給 [ns]
toksと同量)
備考 常に使用される peakrate設定時のみ有効
(未設定時は計算されない)

実装上では、toksptoksなどのトークン量はデータサイズ(bytes)ではなく、時間(ナノ秒単位)で表現されています。 これは、後述するタイマー(hrtimer)にセットする際、あらかじめ時間単位で計算しておく方が処理効率が良いためです。

図8. TBF dequeue処理のフローのイメージ

dequeueの具体的な処理は、以下のステップで進みます。また以下の説明では、peakrateが設定されている前提での処理の流れとなりますが、peakrateが設定されていない場合、以下で説明するすべての処理にpeakrateに関連する処理がないものとして動きます。

  1. 送信待ちskbの確認:送信待ちキューの先頭にあるskbのサイズ(パケット長)を確認する

  2. 新規供給量の計算:前回の処理からの経過時間を計算し、新しく供給する時間(ナノ秒)を算出する。この際、長期間放置された場合の異常値を防ぐため、バケツの最大容量で一度上限をカットする

  3. トークンの補充:各バケツの前回残量に、手順2の新規供給量を足し合わせる。足し合わせた後の総量も、バケツの最大容量を超えないように再度上限をカットする

  4. 送信に必要なトークンの消費:送信に必要となるトークン量を、それぞれのバケツから差し引く*10

    • toks = toks - (skbのサイズ(パケット長) / rate)
    • ptoks = ptoks - (skbのサイズ(パケット長) / peakrate)
  5. 送信可否の判定:計算後のトークン残量(toks および ptoks)が共に 0 以上であるかを確認する

    • 【トークン十分】(条件を満たす場合):skbをキューから取り出し(dequeue)、呼び出し元へ返す。返されたskbはその後、ドライバーに渡される
    • 【トークン不足】(条件を満たさない場合):送信可能になる時間に割込みが入るようにタイマー(hrtimer)をセットする。指定時間後にsoftirqコンテキストでnet_tx_actionを実行し、その延長で再度dequeue処理を実行する
  6. 処理の終了:送信可能なskbがない、またはトークン不足で送信を待機する場合は、NULL を返して処理を終了

つまり、TBFにおける送信レートの制限は、実質的に手順5のタイマー(hrtimer)を用いて意図的な送信遅延を発生させることによって実現されています。

実際の実装はこちら

static struct sk_buff *tbf_dequeue(struct Qdisc *sch)
{
    struct tbf_sched_data *q = Qdisc_priv(sch);
    struct sk_buff *skb;

    skb = q->Qdisc->ops->peek(q->Qdisc);

    if (skb) {
        s64 now;
        s64 toks;
        s64 ptoks = 0;
        unsigned int len = Qdisc_pkt_len(skb);

        now = ktime_get_ns();
        toks = min_t(s64, now - q->t_c, q->buffer);

        if (tbf_peak_present(q)) {
            ptoks = toks + q->ptokens;
            if (ptoks > q->mtu)
                ptoks = q->mtu;
            ptoks -= (s64) psched_l2t_ns(&q->peak, len);
        }
        toks += q->tokens;
        if (toks > q->buffer)
            toks = q->buffer;
        toks -= (s64) psched_l2t_ns(&q->rate, len);

        if ((toks|ptoks) >= 0) {
            skb = Qdisc_dequeue_peeked(q->Qdisc);
            if (unlikely(!skb))
                return NULL;

            q->t_c = now;
            q->tokens = toks;
            q->ptokens = ptoks;
            Qdisc_qstats_backlog_dec(sch, skb);
            sch->q.qlen--;
            Qdisc_bstats_update(sch, skb);
            return skb;
        }

        Qdisc_watchdog_schedule_ns(&q->watchdog,
                       now + max_t(long, -toks, -ptoks));

        /* Maybe we have a shorter packet in the queue,
          which can be sent now. It sounds cool,
          but, however, this is wrong in principle.
          We MUST NOT reorder packets under these circumstances.

          Really, if we split the flow into independent
          subflows, it would be a very good solution.
          This is the main idea of all FQ algorithms
          (cf. CSZ, HPFQ, HFSC)
        */

        Qdisc_qstats_overlimit(sch);
    }
    return NULL;
}

6.3 dequeue処理の具体的なフロー

前述したdequeueの解説だけでは、処理のイメージが沸きづらいと思いますので、具体的に数値を用いて、処理の流れを追っていきます。 以下では、peakrateの設定がない場合とある場合の2つのケースでそれぞれ処理を追っていきます。

※ 以下の数値は解説用の簡略化された値です。実際には計算高速化のため、内部で値の切り捨てや変換が行われます。また、以下に登場する toks、ptoks の単位はすべて [ns](ナノ秒)です。

ケース1:peakrateの設定 なし

単純なトークンバケットアルゴリズムです。バケツにトークンがある限り連続送信(バースト送信)し、枯渇すると送信レートの制限がかかります。

1. 前提パラメーター

項目 設定値 備考
rate 16kbps (2kbytes/s) 定常時の最大送信レート制限
burst 10kbytes バースト許容量
送信パケットのサイズ 15kbytes 5kbytes × 3個に分割処理されると仮定
初期トークン量(toks 5,000,000,000 [ns] 最大値(burst)から開始

2. 処理の流れ

ここで、送信要求されたパケットはenqueue内での分割アルゴリズムによって5kbytesの3つのパケットに分割されているとします。

  • (1). パケット1

    • toks = 5,000,000,000 - 2,500,000,000 = 2,500,000,000
    • toks >= 0 なので、即時送信される(バースト送信)
  • (2). パケット2

    • toks = 2,500,000,000 - 2,500,000,000 = 0
    • toks >= 0なので、即時送信される(バースト送信)
  • (3). パケット3

    • toks = 0 - 2,500,000,000 = -2,500,000,000
    • toks < 0なので送信されない(待機)
      • now + 2,500,000,000 が hrtimerに設定される(2.5秒経過し、トークンが回復)
  • (4). パケット3

    • toks = 2,500,000,000 - 2,500,000,000 = 0
    • toks >= 0なので送信される

バースト分(パケット1, 2)は一気に送信され、バースト分を超えた(トークン枯渇後)パケット3はrate通り(5kbytes / 2.5s = 2kbytes/s)に送信レートが制限されます。

図9. peakrateなし 処理の流れ

ケース2:peakrateの設定 あり

2つのバケツ(toks, ptoks)を用いて、定常時送信・バースト送信の異なる2つの状態の送信レートを制御します。

1. 前提パラメーター

項目 設定値 備考
peakrate 32kbps (4kbytes/s) バースト送信時のレート
rate 16kbps (2kbytes/s) 定常時の帯域制御
burst 10kbytes
mtu/minburst 1500bytes
パケットサイズ 30kbytes 1500bytes × 20個に分割処理されると仮定
初期トークン量(toks 5,000,000,000 [ns] 最大値(burst)から開始
初期トークン量(ptoks 375,000,000 [ns] 最大値(mtu/minburst)から開始

2. 処理の流れ

ここで、送信要求されたパケットはenqueue内での分割アルゴリズムによって、1500bytesの20個のパケットに分割されているとします。

  • (1). パケット1

    • toks = 5,000,000,000 - 750,000,000 = 4,250,000,000
    • ptoks = 375,000,000 - 375,000,000 = 0
    • (toks|ptoks) >= 0なので、即時送信される(バースト送信)
  • (2). パケット2

    • toks = 4,250,000,000 - 750,000,000 = 3,500,000,000
    • ptoks = 0 - 375,000,000 = -375,000,000
    • (toks|ptoks) < 0 なので送信されない(ptoksの回復待機)
    • max_t(long, -toks, -ptoks) = 375,000,000
      • now + 375,000,000にタイマーが起動するように設定される
        • 設定されたタイマーは、0.375秒後に起動されて、1500bytesのパケットが送信される。これにより、1500 / 0.375 = 4kbytes/sとなり、peakrate通りに制限される
  • (3). パケット2

    • toks = 3,875,000,000 - 750,000,000 = 3,125,000,000
    • ptoks = 375,000,000 - 375,000,000 = 0
    • (toks|ptoks) >= 0 なので、送信される(バースト送信(peakrate制御))
  • (4). パケット3 ~ 13

    • (2)~(3)処理を繰り返す
    • この時、ptoksは 375,000,000回復してそこから375,000,000引くことが繰り返される。しかし、toksは375,000,000の回復量に対して750,000,000引かれるので、パケットを1つ送る毎にtoksの合計が375,000,000ずつ減っていく事になる
  • (5). パケット14

    • toks = 125,000,000 - 750,000,000 = -625,000,000
    • ptoks = 0 - 375,000,000 = -375,000,000
    • (toks|ptoks) < 0 なので送信されない(toks, ptoksの回復待機)
    • max_t(long, -toks, -ptoks) = 625,000,000となり、now + 625,000,000にタイマーが起動するように設定される
  • (6). パケット14

    • (5)で設定されたタイマーが625,000,000、つまり0.625秒後に起動される
      ※ この時toksとptoksの供給量は等しいが、ptoksの最大容量が上記の供給量よりも小さいためクリップされる
      • 1500bytes / 0.625s = 2.4kbytes/s で送信される
        • わずかに超過しているものの、rateの制限に沿った挙動へと変化したことがわかる
        • この次以降のパケットはtoks, ptoksともに0スタートになるため、よりrateの制限に近い値の制限が掛かる

図10. peakrateあり 処理の流れ

peakrate を設定した際、観測されるバースト送信量が burst の設定値を上回るように見えることがあります。

上記の例でも、burst を 10kbytes に設定した環境において、1500bytes のパケットが13個(計 19.5kbytes)連続して送信されています。しかし、これは burst の設定超過やバグ(誤動作)ではありません。burst はあくまで基本となるトークン(toks)の最大容量を定めるパラメーターです。一方で、peakrate を設定すると、それとは完全に独立して mtu/minburst に基づく別のトークン(ptoks)によるパケット送出制御が並行して機能します。

つまり、設定した burst の値だけで実際の連続送信量が決まるわけではないため、peakrate を利用する際は「2つのトークンによる独立した制御仕様」を留意した上でパラメーターを設定する必要があります。

7. おわりに

本記事では、Linuxのトラフィック制御を行う際に使用するコマンドtc(8)やQdisc。そしてQdiscのアルゴリズムであるTBFについて紹介しました。今回紹介したTBFは数あるQdiscのアルゴリズムの中で比較的試しやすく、また理解もしやすいものとなっています。ぜひ一度触ってみてください。次回は現代のQdiscのデフォルトで設定される「FQ-CoDel」を理解するための基礎である「CoDel」について解説していく予定です。





ex. TBF パラメーター設定 Tips

TBFを使用する際、パラメーター同士の相互作用により意図しない挙動が発生することがあります。 最後に付録として、以下に重要なTipsと推奨設定をまとめます。

ex1. peakrate 設定時の注意点:バースト量の誤差

peakrate を設定すると、バースト送信時のtoksの減少量がpeakrate未設定時に比べて低下するため、burstで設定した値を超過してバースト送信してしまいます。(具体的な処理は「dequeue処理の具体的なフローのケース2」を参照)

peakrateを設定すると、バースト時の送信速度を制限できますが、引き換えに総バースト量(データ量)が設定値burstを超過してしまいます。

peakrateの設定 メリット デメリット
あり バースト送信時の送信レートを制限できる 総送信量はburst設定値を超過してしまう
なし burstの設定値通りの量でバースト送信は止まる バースト送信時は送信レートがラインレートになってしまう

したがって、バースト送信量をなるべく正確に制限したい場合、peakrateは設定しないことを推奨します。

ex2. TBFでの「定常送信」と「バースト送信」の境界線

TBFによるパケット送信が「バースト送信」と判定する閾値は、設定により異なります。

ケース1:peakrate設定あり

前回の送信からの経過時間が (mtu/minburst) / rate 秒以内であれば「定常」、それ以上間隔が空くとトークンが回復し「バースト」として扱われます。

トークン残量が極めて少ない(連続して送信している)状態は「定常的な送信」とみなされますが、少しでも送信間隔が空くと瞬時にトークンが回復し、「バースト送信」の判定に切り替わります。

  • peakraterateの切り替わりの閾値
    • toks < mtu/minburst
  • 許容される最大のパケット送信間隔
    • (mtu/minburst) / rate
【例:mtu/minburst = 1500, rate = 8Mbpsの場合】

・ toksが0になってからmtu/minburstまで回復するまでの時間:約1.5ms
  ・ もし 2ms間隔 でパケットを送り続けた場合 ⇒ 1.5msより間隔が広いためトークンが満タンになり、毎回 バースト送信(peakrate制限) 扱いとなる。

ケース2:rateのみ設定

単純に現在のトークン残量で送信可能かどうかで判断されます。*11

  • 条件:toks < (packet_size / rate)
    • 満たす場合:hrtimerによる待機が発生して、rateによるレート制限が掛かる
    • 満たさない場合:即時送信(ただし、溜まっていたトークンを消費)

ex3. ユースケース別の推奨設定

ケース1:バーストを極力排除し、常に一定レートで送信したい

  • 推奨設定:
    • peakrate:設定しない
    • burst:デバイスのMTUより「少しだけ」大きい値 (例:MTU 1500なら 1600程度)

重要:burst の値は必ず デバイスのMTU ≦ burst に設定してください。MTUより小さいとパケットが送信できなくなります

この設定のメリット:

  1. バーストの抑制:最大でも1パケット分(約1500bytes)しかバーストしないため、実質的に常にrate近似で動作する
  2. 精度の向上:パケットがTBFへenqueueされる際、小さいburstサイズに合わせて分割されるため、細かい粒度で送信間隔が調整される

ケース2:「定常的な送信」時の送信レートを一定にしたい

  • 推奨設定:
    • 以下のどれかを設定する
      • peakrateを設定する
      • burstをMTU程度に小さくする

上記の設定を行うと、パケットはenqueue時、必ず以下の小さい方のサイズに分割されます。

packet_size = min(mtu/minburst, burst)

これにより、パケットが細分化され、より滑らかで一定の送信レートを実現できます。


*1:ポートやインターフェース等から見たトラフィックの方向を示す用語です。ingressは流入、egressは送出を指します。ingress/egressはあくまでも「向き」を示す概念であり、これらに制御機能等の意味は有しません。トラフィック制御を行うtcでは、制御対象トラフィックを特定するために、これらのingress/egressを意識することがあります。

*2:TCPを使った場合でも最終的にUDPと同じ__dev_xmit_skb()という関数に辿り着きます。

*3:正確には、QdiscはNICの送信キューに紐付きます。そのため、複数の送信キューを持つNICの場合は複数のQdiscが紐付きます。

*4:ここで引数として渡される txq は、送信キューを指します。マルチキュー対応のNICなど、送信キューを複数持つデバイスの場合、前処理である netdev_core_pick_tx() 関数によって適切に選択された送信キューがこの txq に指定されます。

*5:Qdiscの各種アルゴリズムは、linux/net/sched/配下にsch_というprefixが付いたファイル名で実装されています。各種アルゴリズムは必ずenqueueとdequeue関数を持っていて、QdiscのAPIは主にこの2種類の関数を通じてやり取りする非常に分かりやすい構造になっています。

*6:TBFに関しては参照するドキュメントによって「クラスフル / クラスレス」の記載が異なります。しかし筆者の見解としては、TBFのmanに記載されている説明や実装を踏まえると、実態としてクラスフルのQdiscであると考えています。

*7:TBFのデフォルトのinner qdisc(bfifo)では、キュー溢れ時にドロップが発生します。しかし、inner qdiscにCoDel等をアタッチし、ECN(Explicit Congestion Notification)設定を有効にしている場合は、パケットを破棄せずECNマークによる混雑通知を行うため、必ずしもドロップされるとは限りません。

*8:buffer には burst の値(bytes)を時間(ナノ秒)に変換したものが格納されています。このパケット長を時間(ナノ秒)に変換する処理は psched_l2t_ns() が行っています。

*9:GSO(Generic Segmentation Offload)は簡潔に言えば、送信側で行うデータの分割処理を効率化する機能です。こちらについては別記事にて解説予定です。

*10:実際の処理では割り算ではなく、掛け算とビットシフト用いて固定小数点演算をしています。

*11:peakrate なしの場合、タイムウィンドウ(観測期間)の取り方によって評価が変わります。「長い目で見ればレート制限内だが、瞬間的にはバーストしている」状態になりやすいため、用途に応じたタイムウィンドウの設計が重要です。




以上の内容はhttps://www.valinux.co.jp/blog/entry/20260312より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14