4. 実装

では、説明はここまでにしておいて、 Linux で帯域管理を実装することにしましょう。

4.1. 警告

DSL モデムへ送出するデータの、実際の速度を制限するのは、 思ったほど簡単ではありません。ほとんどの DSL モデムは実際はイーサネットブリッジにすぎず、 Linux 機 と ISP のゲートウェイとの間で、データを互いにブリッジしあっているのです。 ほとんどの DSL モデムは、データ送出のため、リンク層に ATM を使っています。 ATM は常に 53 バイト長のデータをセル単位で送出しています。 そのうち 5 バイトはヘッダー情報で、残りの 48 バイトはデータに利用できます。 送出するのが 1 バイトでも、ATM のセルは常に 53 バイト長なので、 全体で 53 バイトの帯域を消費してしまいます。 つまり、データが 0 バイト + TCP ヘッダー が 20 バイト + IP ヘッダーが 20 バイト + イーサネットヘッダーが 18 バイト という典型的な TCP の ACK パケットを送出している場合にあたります。 現実には、送出しているイーサネットパケットが (TCP ヘッダーと IP ヘッダーしかない) 40 バイトの負荷しかない場合でも、 イーサネットパケットの最低負荷は 46 バイトのデータになり、残りの 6 バイトには ヌルが詰まります。 つまり、イーサネットパケットの実際の長さにヘッダー長を加えると、18 + 46 = 64 バイトになるということです。 ATM で 64 バイトを送出するためには、ATM セルを二つ送出し、106 バイトの帯域を消費しなければなりません。 これは、どの TCP ACK パケットについても、42 バイトの帯域を浪費しているという意味です。 DSL モデムが使用しているカプセル化に、Linux が責任を持つなら良いのですが、 Linux が責任を持っているのは、TCP ヘッダーと IP ヘッダー、それに 14 バイトの MACアドレスだけです (4 バイトの CRC はハードウェアレベルで処理するので、Linux ではこの 4 バイトは含めていません)。 Linux では、46 バイトという最小イーサネットパケットを勘定に入れていないし、 ATM の固定サイズセルも考慮していないのです。

こういったことから、 (使用している様々な種類のカプセル化を考慮できるパケットスケジューラが見つかるまでは) 外向きの帯域は、 実際の帯域容量よりも若干低めに制限しなければならないことがわかります。 お使いの帯域をうまく制限できる値を捜し当てたと思っても、 大きなファイルをダウンロードすると、遅延は 3 秒以上に跳ね上がり始めることがよくあります。 これはたいてい、小さな ACK パケットが消費する帯域を、Linux が計算違いしているからです。

この問題の解決策を、筆者は何カ月かの間ずっと検討してきましが、ほぼメドが立ちました。 さらにテストを行なうために、もうすぐ一般にリリースします。 この方法では、Linux の 速度制限パケットに対する QoS の代わりに、 ユーザー空間にあるキューを使います。 基本的には Linux のユーザー空間にあるキューを使って、 HTB キューを実装しました。 (これまでのところ)この方法を使うと、 外向きのトラフィックをかなりうまく調整できているので、 大量のバルクデータの(同時に数個の)ダウンロードや (gnutella を用いて、同時に数個の)アップロードの最中でも、 約 15ms という、トラフィックの無い状態のわずかな遅延を越えたのは、 400ms が最大でした。 この QoS の方法に関する詳細に、 メーリングリストに参加して更新情報を受け取るか、 あるいはこの HOWTO の更新状況をチェックして下さい。

4.2. スクリプト:myshaper

次のリストは、筆者が自分の Linux ルーターで帯域管理をするのに使っているスクリプトです。 このスクリプトでは、本文書でカバーした考え方のいくつかを使っています。 外向きのトラフィックは、種類に応じて七つのキューのどれかに配置されます。 内向きのトラフィックは、速度が超過している場合に、 TCP パケットを最初に落とすようになっている(優先順位が一番低い)、 二つのキューに配置されます。 このスクリプトで指定してある速度は、 筆者の構成ではうまくいくようですが、 読者の皆さんの結果は違うかもしれません。

#!/bin/bash
#
# myshaper - DSL/ケーブル モデム用外向きトラフィックシェーパ & 優先順位付け
#            元は ADSL/Cable wondershaper (www.lartc.org)
#
# Dan Singletary 作 (8/7/02)
#
# 注意!! - 本スクリプトは、以下のサイトから利用できる
#            HTB キューと IMQ の適切なパッチがカーネルに
#            あたっていることが前提です。
#            (将来のカーネルでは、パッチは不要になるかもしれません)
#
#       http://luxik.cdi.cz/~devik/qos/htb/
#       http://luxik.cdi.cz/~patrick/imq/
#
# myshaper 用設定オプション
#  DEV    - DSL/ケーブル モデムと接続するデバイスを ethX に設定する
#  RATEUP - DSL/ケーブル モデムの外向き帯域より、この値を若干低めに
#           設定する。
#           筆者のラインは 1500/128 で、RATEUP=90 にすると、
#           上り 128kbps ではうまく動作します。
#           でも皆さんのやり方は違うかもしれません。
#  RATEDN - DSL/ケーブル モデムの内向きの帯域よりも、若干低めに
#           設定します。
#
#
#  imq を使って内向きのトラフィックをスリムにする理論
#
#    インターネット上の他のホストから送出されるデータの速度を、
#  直接制限することは不可能です。内向きのトラフィックの速度を
#  削るには、TCP の輻輳回避アルゴリズムに頼るしかありません。
#  このため、「できそうなのは TCP コネクションで内向きのトラ
#  フィックをスリムにしてみることだけです。」つまり tcp 以外の
#  トラフィックはどれでも優先順位の高いクラスに配置するという
#  ことです。というのは、tcp 以外のパケットを落としてし
#  まうと、結果としてはたぶん再送が行なわれて、単に不要に帯域を消
#  費してしまうだけとなってしまうからです。
#    HTB キューがオーバーフローする際、tcp パケットを落として、
#  内向きの TCP のトラフィックを削ってみます。このキューは、実際に
#  内向き側のデバイスが出せる速度よりも若干低めの所定の速度 (RATEDN) で、
#  パケットを通すだけです。この速度を越える TCP パケットを落と
#  すことで、ISP 側のキューがオーバーフローしているせいで、この
#  パケットが落ちているように、見せかけようとしています。
#  このようにする利点は、ISP 側のキューが実際には満杯にはなって
#  いないけれども、一杯になったように見せかければパケットが落ち、それに
#  反応して TCP が転送速度を落とすので、ISP 側のキューがけっして飽和
#  しないことです。
#    優先順位に基づくキューイング方式を使うと、より優先順位が
#  高いバケツ(クラス)に配置する、ある種のパケット(ssh、telnet、等々)
#  は「落とさない」という選択が明確にできるという利点があります。
#  なぜかといえば、パケットはどのクラスからも、最低限の速度で
#  公平にデキューされるという条件になっており、常に優先順位がもっとも低い
#  クラスから取り出されるからです。(このスクリプトでは、各
#  バケツは最低でも、帯域の七分の一という公平な割り当てで配送
#  します)。
#
#  重要な点を繰り返します。
#   *接続中に tcp パケットを落とすと、輻輳回避アルゴリズムによって、
#     受信側の速度が低下することになります。
#   *TCP 以外のパケットを落としても、何も得るものはありません。事実
#     そのパケットが重要なものだったら、どのみちおそらく再送されるで
#     しょう。ですからこういったパケットはけっして落とさないようにす
#     る必要があります。こうすれば、TCP コネクションが飽和しても
#     TCP のように再送の仕組みがないプロトコルに対して、悪影響
#     を及ぼすことはありません。
#   *内向きの速度全体を、実際にデバイス(ADSL モデム/ケーブルモデム)
#     が出せる速度よりも低くするように、内向きの TCP コネクショ
#     ンをスピードダウンすると、結果として ISP 側のキュー(DSLAM、ケー
#     ブル接続、等々)にはほとんどパケットが溜らなくなる「はずです」。
#     ISP 側のキューには、1500Kbps で4秒分のデータ、つまり 6 メガビッ
#     トのデータが溜っているのがわかりました。ですからパケットがキュー
#     に溜らなければ、遅延は低下することになります。
#
#  注意(テスト前に持ち上がった疑問):
#    *このやり方で内向きのトラフィックを制限すると、TCP のバルク転送の性能
#      が悪くなってしまうのではないか?
#      - 当座の答えは、そんなことはない、です。(64 バイト未満の小さい)
#        ACK パケットに優先順位をつければ、パケットの再送で持っていた帯
#        域を消費しなくても、スループットが最大になります。
#   

# 注意:次の設定は、筆者の環境ではうまく機能しています:
# 1.5M/128K ADSL (Pacific Bell Internet (SBC Global Services) 経由)

DEV=eth0
RATEUP=90
RATEDN=700  # この値が 1500 (1.5Mbps) という容量よりかなり低いことに注意して下さい。
            # このため、TCP ウィンドウを操作するといった、もっと優れた
            # ものが実装されて使えるようになるまで、わざわざ内向きのトラフィック
            # を制限する必要はないかもしれません。

#
# 設定オプションの終り
#

if [ "$1" = "status" ]
then
        echo "[qdisc]"
        tc -s qdisc show dev $DEV
        tc -s qdisc show dev imq0
        echo "[class]"
        tc -s class show dev $DEV
        tc -s class show dev imq0
        echo "[filter]"
        tc -s filter show dev $DEV
        tc -s filter show dev imq0
        echo "[iptables]"
        iptables -t mangle -L MYSHAPER-OUT -v -x 2> /dev/null
        iptables -t mangle -L MYSHAPER-IN -v -x 2> /dev/null
        exit
fi

# すべてを既知の状態にリセットする(クリアする)
tc qdisc del dev $DEV root    2> /dev/null > /dev/null
tc qdisc del dev imq0 root 2> /dev/null > /dev/null
iptables -t mangle -D POSTROUTING -o $DEV -j MYSHAPER-OUT 2> /dev/null > /dev/null
iptables -t mangle -F MYSHAPER-OUT 2> /dev/null > /dev/null
iptables -t mangle -X MYSHAPER-OUT 2> /dev/null > /dev/null
iptables -t mangle -D PREROUTING -i $DEV -j MYSHAPER-IN 2> /dev/null > /dev/null
iptables -t mangle -F MYSHAPER-IN 2> /dev/null > /dev/null
iptables -t mangle -X MYSHAPER-IN 2> /dev/null > /dev/null
ip link set imq0 down 2> /dev/null > /dev/null
rmmod imq 2> /dev/null > /dev/null

if [ "$1" = "stop" ] 
then 
        echo "Shaping removed on $DEV."
        exit
fi

###########################################################
#
# 外向きのスリム化(帯域全体を RATEUP に制限する)

# 優先順位が低いパケットでは、遅延が約 2 秒になるように、
# キューサイズを設定する。
ip link set dev $DEV qlen 30

# 外向きデバイスの mtu を変更する。mtu を小さくすると遅延は低下するが、
# IP と TCP のプロトコルオーバーヘッドのため、スループットは若干低下
# することになる。
ip link set dev $DEV mtu 1000

# HTB の root qdisc を追加
tc qdisc add dev $DEV root handle 1: htb default 26

# 主要な速度制限クラスを追加
tc class add dev $DEV parent 1: classid 1:1 htb rate ${RATEUP}kbit

# リーフクラスを追加 - 「最低でも」帯域を「公平に分ける」ことを各クラスに認める。
#                      こうすれば、どのクラスも他のクラスのせいでキューが空になる
#                      ことはけっしてない。他のどのクラスも未使用なら、使える帯域
#                      はすべて使っても良い。
tc class add dev $DEV parent 1:1 classid 1:20 htb rate $[$RATEUP/7]kbit ceil ${RATEUP}kbit prio 0
tc class add dev $DEV parent 1:1 classid 1:21 htb rate $[$RATEUP/7]kbit ceil ${RATEUP}kbit prio 1
tc class add dev $DEV parent 1:1 classid 1:22 htb rate $[$RATEUP/7]kbit ceil ${RATEUP}kbit prio 2
tc class add dev $DEV parent 1:1 classid 1:23 htb rate $[$RATEUP/7]kbit ceil ${RATEUP}kbit prio 3
tc class add dev $DEV parent 1:1 classid 1:24 htb rate $[$RATEUP/7]kbit ceil ${RATEUP}kbit prio 4
tc class add dev $DEV parent 1:1 classid 1:25 htb rate $[$RATEUP/7]kbit ceil ${RATEUP}kbit prio 5
tc class add dev $DEV parent 1:1 classid 1:26 htb rate $[$RATEUP/7]kbit ceil ${RATEUP}kbit prio 6

# qdisc を リーフクラスに接続 - 各優先順位のクラスに SFQ する。SFQ しておけば、各クラス内で
#                               コネクションを(ほとんど)公平に扱うことが保証される。
tc qdisc add dev $DEV parent 1:20 handle 20: sfq perturb 10
tc qdisc add dev $DEV parent 1:21 handle 21: sfq perturb 10
tc qdisc add dev $DEV parent 1:22 handle 22: sfq perturb 10
tc qdisc add dev $DEV parent 1:23 handle 23: sfq perturb 10
tc qdisc add dev $DEV parent 1:24 handle 24: sfq perturb 10
tc qdisc add dev $DEV parent 1:25 handle 25: sfq perturb 10
tc qdisc add dev $DEV parent 1:26 handle 26: sfq perturb 10

# fwmark で、フィルタがクラスに振り分ける - ここでパケットにセットした fwmark に従って、直接
#                                           優先順位の付いたクラスに振り分ける(fwmark はあとで、
#                                           iptables を使ってセットする)。先に、ディフォルトの
#                                           優先順位のクラスを 1 から 26 までとしたので、マーキングが無い
#                                           パケット(あるいは見慣れない ID のパケット)は、
#                                           ディフォルトで一番優先順位の低いクラスに入る。
tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 20 fw flowid 1:20
tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 21 fw flowid 1:21
tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 22 fw flowid 1:22
tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 23 fw flowid 1:23
tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 24 fw flowid 1:24
tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 25 fw flowid 1:25
tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 26 fw flowid 1:26

# 一連の MYSHAPER-OUT を iptables の mangle テーブルに追加 - これでテーブルを設定し、パケットの
#                                                            フィルターとマーキングに使う。
iptables -t mangle -N MYSHAPER-OUT
iptables -t mangle -I POSTROUTING -o $DEV -j MYSHAPER-OUT

# fwmark エントリを追加して、トラフィックの種類ごとに分類 - 必要なクラスに従って、fwmark を 20
#                                                           から 26 に設定。20 が最高の優先順位。
iptables -t mangle -A MYSHAPER-OUT -p tcp --sport 0:1024 -j MARK --set-mark 23 # 低位ポートのトラフィック用ディフォルト
iptables -t mangle -A MYSHAPER-OUT -p tcp --dport 0:1024 -j MARK --set-mark 23 # "" 
iptables -t mangle -A MYSHAPER-OUT -p tcp --dport 20 -j MARK --set-mark 26     # ftp のデータポート。優先順位は低い
iptables -t mangle -A MYSHAPER-OUT -p tcp --dport 5190 -j MARK --set-mark 23   # aol のインスタントマネージャ
iptables -t mangle -A MYSHAPER-OUT -p icmp -j MARK --set-mark 20               # ICMP (ping) - 優先順位は高い、友人たちに印象づけよう
iptables -t mangle -A MYSHAPER-OUT -p udp -j MARK --set-mark 21                # DNS 名前解決(パケットは小さい)
iptables -t mangle -A MYSHAPER-OUT -p tcp --dport ssh -j MARK --set-mark 22    # secure shell
iptables -t mangle -A MYSHAPER-OUT -p tcp --sport ssh -j MARK --set-mark 22    # secure shell
iptables -t mangle -A MYSHAPER-OUT -p tcp --dport telnet -j MARK --set-mark 22 # telnet (ew...)
iptables -t mangle -A MYSHAPER-OUT -p tcp --sport telnet -j MARK --set-mark 22 # telnet (ew...)
iptables -t mangle -A MYSHAPER-OUT -p ipv6-crypt -j MARK --set-mark 24         # IPSec - 負荷がどんなものかは知らないんだけど ...
iptables -t mangle -A MYSHAPER-OUT -p tcp --sport http -j MARK --set-mark 25   # ローカルのウェブサーバー
iptables -t mangle -A MYSHAPER-OUT -p tcp -m length --length :64 -j MARK --set-mark 21 # こまごましたパケット(たぶん ACK だけ)
iptables -t mangle -A MYSHAPER-OUT -m mark --mark 0 -j MARK --set-mark 26      # 冗長化 - マーキング無しパケットは、なんでも 26(優先順位は低い)

# 外向きのスリム化終了
#
####################################################

echo "Outbound shaping added to $DEV.  Rate: ${RATEUP}Kbit/sec."

# 上りのトラフィックをスリムにしたいだけのときは、以下の行のコメントをはずすこと。
# exit

####################################################
#
# 内向きのスリム化(帯域全体を RATEDN に制限する)

# imq モジュールがロードされたことを確認

modprobe imq numdevs=1

ip link set imq0 up

# qdisc 追加 - ディフォルトの低位優先順位クラス 1 から 21

tc qdisc add dev imq0 handle 1: root htb default 21

# 主要な速度制限クラスを追加
tc class add dev imq0 parent 1: classid 1:1 htb rate ${RATEDN}kbit

# リーフクラス追加 - TCP は 21 に、TCP 以外は 20に、それぞれ振り分け。
#
tc class add dev imq0 parent 1:1 classid 1:20 htb rate $[$RATEDN/2]kbit ceil ${RATEDN}kbit prio 0
tc class add dev imq0 parent 1:1 classid 1:21 htb rate $[$RATEDN/2]kbit ceil ${RATEDN}kbit prio 1

# qdisc をリーフクラスに接続 - 各優先順位のクラスに SFQ する。SFQ しておけば、各クラス内で
#                               コネクションを(ほとんど)公平に扱うことが保証される。
tc qdisc add dev imq0 parent 1:20 handle 20: sfq perturb 10
tc qdisc add dev imq0 parent 1:21 handle 21: red limit 1000000 min 5000 max 100000 avpkt 1000 burst 50

# fwmark で、フィルタがクラスに振り分ける - ここでパケットにセットした fwmark に従って、直接
#                                           優先順位の付いたクラスに振り分ける(fwmark はあとで、
#                                           iptables を使ってセットする)。先に、ディフォルトの
#                                           優先順位のクラスを 1 から 26 までとしたので、マーキングが無い
#                                           パケット(あるいは見慣れない ID のパケット)は、
#                                           ディフォルトで一番優先順位の低いクラスに入る。

tc filter add dev imq0 parent 1:0 prio 0 protocol ip handle 20 fw flowid 1:20
tc filter add dev imq0 parent 1:0 prio 0 protocol ip handle 21 fw flowid 1:21

# 一連の MYSHAPER-IN を iptables の mangle テーブルに追加 - これでテーブルを設定し、パケットの
#                                                            フィルターとマーキングに使う。
iptables -t mangle -N MYSHAPER-IN
iptables -t mangle -I PREROUTING -i $DEV -j MYSHAPER-IN

# fwmark エントリを追加して、トラフィックの種類ごとに分類 - 必要なクラスに従って、fwmark を 20
#                                                           から 26 に設定。20 が最高の優先順位。

iptables -t mangle -A MYSHAPER-IN -p ! tcp -j MARK --set-mark 20              # tcp 以外のパケットの優先順位を最高にする
iptables -t mangle -A MYSHAPER-IN -p tcp -m length --length :64 -j MARK --set-mark 20 # 短い TCP パケットは、たぶん ACK
iptables -t mangle -A MYSHAPER-IN -p tcp --dport ssh -j MARK --set-mark 20    # secure shell
iptables -t mangle -A MYSHAPER-IN -p tcp --sport ssh -j MARK --set-mark 20    # secure shell
iptables -t mangle -A MYSHAPER-IN -p tcp --dport telnet -j MARK --set-mark 20 # telnet (ew...)
iptables -t mangle -A MYSHAPER-IN -p tcp --sport telnet -j MARK --set-mark 20 # telnet (ew...)
iptables -t mangle -A MYSHAPER-IN -m mark --mark 0 -j MARK --set-mark 21              # 冗長化 - マーキング無しパケットは、なんでも 26(優先順位は低い)

# 最後に、これらのパケットが先に設定した imq0 を通るよう指示する。
iptables -t mangle -A MYSHAPER-IN -j IMQ

# 内向きのスリム化終了
#
####################################################

echo "Inbound shaping added to $DEV.  Rate: ${RATEDN}Kbit/sec."