chobi_e

MySQLユーザーのためのMySQLプロトコル入門

さいきんMySQLユーザーのためのほげほげ、みたいなのが巷で流行しているようなので暇つぶしがてらに読んでいるMySQLプロトコルについて書いてみようかと思います。

いやまぁ、こういうプロトコルが読めるからといってすごく役立つということは全くないんですが、お酒の席のネタにできたり、高速、簡単、無料で試せるRDS MySQLからRedshiftへのデータ同期に出てくるようなreplicationをいじったツールとかのメンテが容易にできるかもしれなかったり、俺mysqldだぜ、みたいな事ができたり、なんかよくわからないけどちょっとハッピーになれそうですね!

今日は手始めにMySQLとmysql clientがどういう通信をしているのか見ていき、実際にInitial Handshake Packetをparseしてみるところまでをやってみます。

Max OSXでのセットアップ

普段homebrewを使っているのでmysqlとngrepをインストールしてみます。ngrepはお手軽なtcpdumpだと思っていただければOKです。

brew install ngrep
brew install mysql

それではmysqldを起動してngrepを使ってみましょう

mysql.server start
sudo ngrep -x -q -d lo0 '' 'port 3306'

# 別ターミナルでmysqlコマンドを実行する
mysql -u root -h 127.0.0.1 -P 3306
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.6.20 Homebrew

Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> Ctrl-C -- exit!
Aborted

するとngrepをしていたターミナルで下記のような結果が得られると思います。

interface: lo0 (127.0.0.0/255.0.0.0)
filter: (ip or ip6) and ( port 3306 )

T 127.0.0.1:3306 ->; 127.0.0.1:49482 [AP]
  4a 00 00 00 0a 35 2e 36    2e 32 30 00 01 00 00 00    J....5.6.20.....
  21 4b 3a 7e 30 6f 61 78    00 ff f7 21 02 00 7f 80    !K:~0oax...!....
  15 00 00 00 00 00 00 00    00 00 00 58 30 26 6a 64    ...........X0&jd
  3a 34 33 45 40 53 5e 00    6d 79 73 71 6c 5f 6e 61    :43E@S^.mysql_na
  74 69 76 65 5f 70 61 73    73 77 6f 72 64 00          tive_password.  

T 127.0.0.1:49482 -> 127.0.0.1:3306 [AP]
  a4 00 00 01 85 a6 7f 00    00 00 00 01 08 00 00 00    ................
  00 00 00 00 00 00 00 00    00 00 00 00 00 00 00 00    ................
  00 00 00 00 72 6f 6f 74    00 00 6d 79 73 71 6c 5f    ....root..mysql_
  6e 61 74 69 76 65 5f 70    61 73 73 77 6f 72 64 00    native_password.
  67 03 5f 6f 73 07 6f 73    78 31 30 2e 39 0c 5f 63    g._os.osx10.9._c
  6c 69 65 6e 74 5f 6e 61    6d 65 08 6c 69 62 6d 79    lient_name.libmy
  73 71 6c 04 5f 70 69 64    04 36 36 36 37 0f 5f 63    sql._pid.6667._c
  6c 69 65 6e 74 5f 76 65    72 73 69 6f 6e 06 35 2e    lient_version.5.
  36 2e 32 30 09 5f 70 6c    61 74 66 6f 72 6d 06 78    6.20._platform.x
  38 36 5f 36 34 0c 70 72    6f 67 72 61 6d 5f 6e 61    86_64.program_na
  6d 65 05 6d 79 73 71 6c                               me.mysql        

T 127.0.0.1:3306 -> 127.0.0.1:49482 [AP]
  07 00 00 02 00 00 00 02    00 00 00                   ...........     

T 127.0.0.1:49482 -> 127.0.0.1:3306 [AP]
  21 00 00 00 03 73 65 6c    65 63 74 20 40 40 76 65    !....select @@ve
  72 73 69 6f 6e 5f 63 6f    6d 6d 65 6e 74 20 6c 69    rsion_comment li
  6d 69 74 20 31                                        mit 1           

T 127.0.0.1:3306 -> 127.0.0.1:49482 [AP]
  01 00 00 01 01 27 00 00    02 03 64 65 66 00 00 00    .....'....def...
  11 40 40 76 65 72 73 69    6f 6e 5f 63 6f 6d 6d 65    .@@version_comme
  6e 74 00 0c 08 00 08 00    00 00 fd 00 00 1f 00 00    nt..............
  05 00 00 03 fe 00 00 02    00 09 00 00 04 08 48 6f    ..............Ho
  6d 65 62 72 65 77 05 00    00 05 fe 00 00 02 00       mebrew......... 

T 127.0.0.1:49482 -> 127.0.0.1:3306 [AP]
  01 00 00 00 01                                        .....           

mysqldがlistenしているportに接続するとServerからInitial Handshake Packetが送られてきます。この情報を元にClientはHandshakeResponse41のPacketを生成して返送し、Server側で認証がとおればTextProtocolのやり取りに進むことが出来ます。

通信メッセージの単位 – MySQL Packet

通信を行う際はMySQL Packetというメッセージの単位でやり取りをすることになります。MySQL Packetは4byteのheader + payloadという構成になっており、dev.mysql.comの資料から引用すると:

size name description
int<3> payload_length Length of the payload. The number of bytes in the packet beyond the initial 4 bytes that make up the packet header.
int<1> sequence_id Sequence ID
string payload [len=payload_length] payload of the packet

4byteしかないのでさっくりparseできそうですね。

それでは先ほどngrepで表示されていたpacketをMySQL Packetとして手でparseしてみましょう

T 127.0.0.1:3306 -> 127.0.0.1:49482 [AP]
  4a 00 00 00 0a 35 2e 36    2e 32 30 00 01 00 00 00    J....5.6.20.....
  21 4b 3a 7e 30 6f 61 78    00 ff f7 21 02 00 7f 80    !K:~0oax...!....
  15 00 00 00 00 00 00 00    00 00 00 58 30 26 6a 64    ...........X0&jd
  3a 34 33 45 40 53 5e 00    6d 79 73 71 6c 5f 6e 61    :43E@S^.mysql_na
  74 69 76 65 5f 70 61 73    73 77 6f 72 64 00          tive_password.

MySQL Packetは3byteのpayload length, 1byteのsequence id、payload length – 4がpayloadとなるので

payload_length: 4a 00 00
sequence_id: 00
payload: 0a 35 2e 36    2e 32 30 00 01 00 00 00	5.6.20.....

となるわけですね。それではpayloadを更にparseしてみましょう

Connection Phase Packet

接続した際に最初にServerから送られてくるMySQL PacketがConnection Phase Packetとなります。再びdev.mysql.comの資料から引用します:

1              [0a] protocol version
string[NUL]    server version
4              connection id
string[8]      auth-plugin-data-part-1
1              [00] filler
2              capability flags (lower 2 bytes)
  if more data in the packet:
1              character set
2              status flags
2              capability flags (upper 2 bytes)
  if capabilities & CLIENT_PLUGIN_AUTH {
1              length of auth-plugin-data
  } else {
1              [00]
  }
string[10]     reserved (all [00])
  if capabilities & CLIENT_SECURE_CONNECTION {
string[$len]   auth-plugin-data-part-2 ($len=MAX(13, length of auth-plugin-data - 8))
  if capabilities & CLIENT_PLUGIN_AUTH {
string[NUL]    auth-plugin name
  }

Connection Phaseのデータ構造は可変長のデータがあまり出てこないので簡単ですね。ngrepで見ていた時はこんな感じのpayloadだったので

            0a 35 2e 36    2e 32 30 00 01 00 00 00
21 4b 3a 7e 30 6f 61 78    00 ff f7 21 02 00 7f 80
15 00 00 00 00 00 00 00    00 00 00 58 30 26 6a 64
3a 34 33 45 40 53 5e 00    6d 79 73 71 6c 5f 6e 61
74 69 76 65 5f 70 61 73    73 77 6f 72 64 00

手で分割していくとこのようになります。

protocol version: 0a
server version: 35 2e 36    2e 32 30 00
connection id: 01 00 00 00
auth-plugin-data-part-1: 21 4b 3a 7e 30 6f 61 78
filter: 00
capability flags: ff f7
character set: 21
status flag: 02 00
capability flags: 7f 80
auth-plugin-data: 15
reserved: 00 00 00 00 00 00 00    00 00 00
auth-plugin-data-part-2: 58 30 26 6a 64 3a 34 33 45 40 53 5e 00
auth-plugin-name: 6d 79 73 71 6c 5f 6e 61 74 69 76 65 5f 70 61 73    73 77 6f 72 64 00

なんとなくそれっぽいデータになりましたね。とはいえ手で分割してても時間かかりますし、面白く無いのでInitial PacketをGoでparseしてみるプログラムを書いてみました。

これを元にnet.DialでつなげてみたりするとInitial Packetだけはparseできたり、無駄に頑張ればオレオレMySQL Clientが作れるので参考に遊んでみてもらえれば、と思います。

http://play.golang.org/p/Gvb4fxZETE

もし間違っていたら突っ込みいただけるとありがたいです。

そのほか

  • homebrewのmysqlはmysql.server stopで止められます

http://dev.mysql.com/doc/internals/en/client-server-protocol.html

  • Character Set

http://dev.mysql.com/doc/internals/en/character-set.html#packet-Protocol::CharacterSet

sashima

GREEのUserAgent比率を公開します(2014/10)

開発企画室の佐島です。

今月もGREEを利用して頂いているクライアントのUA比率を公開します。

グラフは以下のデータを元に作成しています。

  • 本データはGREEのSNSサービスのブラウザに基くデータです。
  • 本データの内容の正確性・信頼性については保証いたしかねます。
  • データはスマートフォンに限ったブラウザ比率を元に集計しています。
yuya.yaguchi

よくわかるLinux帯域制限

矢口です。

みなさんはLinuxのtcという機能をご存知でしょうか。送信するパケットの帯域制御を行うことができる大変強力な機能で、グリーでもいくつかの用途で使用されています。

具体的な事例の一つはRedisです。Redisではreplicationを新規に開始する際やfailoverが発生しmasterが切り替わった際(特に2.6系)にストアされている全データが転送されます。しかし帯域制限をかける機能がないため、ネットワーク帯域を圧迫してしまう危険性があります。また通常のクライアントとの通信でも大量のクエリにより予想以上の帯域を使用してしまう可能性があります。このような場合にtcを用いることでRedisの使用する帯域をコントロールできます。

このように有用なtcですが残念なことに日本語/英語ともにわかりやすい解説や詳細な情報は多くありません。

私も社内において使われていたtcの設定に問題が見つかり、修正が必要になった際に初めて触れたのですが情報が少なく苦労しました。

この文章はその数少ないドキュメント(参考文献の項目に記載してあります)やカーネルのコードを読みながら理解した内容について社内向けにまとめたものです。

Abstract

LinuxにはQoSやTraffic Control(tc)と呼ばれるIPパケットの送信制御機構があります。tcは様々な目的に利用することができますが、最も一般的な利用方法は特定のソフトウェア/セッションの帯域を制限したい場合でしょう。

大きなトラフィックを発生させるソフトウェアの多くはbwlimitといったパラメータを持ちトラフィックを制限できるようになっていますが、中にはそのような機能を保たないものもあります。例えばRedisは新しいReplicationを張る際にストアされたデータすべてをネットワークをいっぱいに使って送信しようとします。

tcを利用することでこのような通信の帯域使用を一定値に制限し、同じサーバー/スイッチ/ルーターを使用する他の通信に影響を及ぼすことを防ぐことができます。

この文章ではtcについて帯域制限に利用する機能を中心に原理・実装を解説し、実用的な設定方法を説明していきたいと思います。

より具体的な設定の指針等については別ドキュメントにまとめましたのでそちらをご確認ください。

Linux TC (帯域制御、帯域保証) 設定ガイドライン

Traffic Controlのコンセプト

Traffic Controlは送信したいパケットをschedulerと呼ばれるものに通すことで行われます。schedulerはキューイング規則(Queueing discipline: qdisc)とも呼ばれ、パケットを一時的に格納し、順番を並べ替えたり、遅延させたり、破棄したりすることができます。以下qdiscと呼びます。

qdiscは極めて高度で複雑な構造となっていますが、外部からのインターフェースはとてもシンプルなものとなっています。外部から見たqdiscの操作は主に2つしかありません。enqueueとdequeueです。

qdiscがない場合のパケット送信

まずqdiscが存在しないと仮定した場合を考えてみましょう。パケット送信は以下の図のようにプロトコル層がdevice driverにパケットを渡す処理となっています。

qdiscはこのプロトコル層とdevice driverの間に入ります。

enqueue

enqueueは送信される予定のパケットをqdiscに格納しようとする処理です。主にTCPなどのプロトコル層から呼び出されます。enqueueは失敗することもあり、その場合はそのパケットはdropします。どのような場合にenqueueが成功しどのような場合に失敗するかはqdisc内部で判断され、外部には結果のみが伝わります。

dequeue

dequeueはqdiscに格納されたパケットを取り出し、デバイスドライバに渡す処理です。dequeueもenqueueと同様に失敗する場合があります。失敗は単にqdiscが空の場合にも発生しますし、何らかの理由で格納されているパケットをqdiscが送信したくない場合にも発生します。どのような場合に成功しどのような場合に失敗するかはqdisc内部で判断され、外部には結果のみが伝わります。

qdiscの一覧と目的、仕組み

この章では主なqdiscの一覧とその目的と仕組みについて説明します。

classless qdisc

classlessなシンプルなqdiscについて説明します。

PFIFO

PFIFO(Packet First-In First-Out)は最初に入ったパケットが最初に出ていくシンプルなqdiscです。

P(Packet)というプレフィックスがついているのは格納できるパケットの限界をパケット数で規定しているからです。限界をByteで規定できるBFIFOというものもあります。

PFIFO_FAST

PFIFO_FASTはPFIFOにパケットの優先度(Priority: prio)を考慮する機能を追加したものです。内部に複数のqueueを持っており、enqueueの際にIPヘッダのTOSを読み優先度で分類してqueueに格納します。dequeueの際は優先度の高いqueueから先に取り出されます。

このPFIFO_FASTはLinuxのデフォルトqdiscとなっています。

また、格納できるpacket数は指定がない場合tx_queue_lenとなります。この値はifconfigコマンドのtxqueuelenの項目で確認/変更することができます。

SFQ

SFQ(Stochastic Fair Queuing)はセッションごとに公平にパケット送信機会を得ることができるqdiscです。

PFIFOで多数のパケットを送信するセッションで帯域やqdiscがいっぱいとなっている場合、あまりパケットを送信しないセッションがパケットを送信しようとしても、enqueueできずdropしてしまったり、既に多数のpacketがqdiscに格納されていてdequeueするまでに長い時間がかかることがあります。このような場合にPFIFOの代わりにSFQを使用することでこのようなセッションがパケットをdropせずに迅速に送信できる可能性を高めることができます。

ただしSFQを使用することでlatencyが大きくなってしまうという欠点が存在します。

SFQでは多数のqueueとハッシュテーブルを用意し、パケットを所属するセッションに応じたqueueに格納します。dequeueでは各queueを順番にまわり(Round Robin)パケットを取り出します。また1つのqueueから一定数のパケットを取り出すと強制的に次のqueueに取り出す対象を移します。

同一セッションかどうかを判定するのにはハッシュ値のkeyにはdestination address, source address, protocol番号が使用されます。プロトコルがTCP, UDPなどの場合はsource port, destination portも使用されます。

TBF

TBF(Token Bucket Filter)はbitrateを制御するためのqdiscです。

Token Bucketという概念を用いることでbitrate制限を実現しています。

Token Bucketは時間経過によりtokenが追加されるバケツでパケットをdequeueする際にdequeueしたパケットのbyte数分tokenを消費します。tokenをすべて消費しバケツが空になった状態ではdequeueが失敗します。

指定したbitrateより少ないbitrateの分しか送信しようとしているパケットがないときにはバケツにはtokenが貯まります。貯まっているtokenの分のパケットについては連続してdequeueを行うことができ、これを burst と呼びます。

パラメータとしてburst(バケツの大きさ)とbitrate(時間あたりのバケツへのtokenの供給量)を指定することでパケット送信の特性をコントロールできます。

なぜこのようなややこしい仕組みとなっているかは後の章「bitrateとtimer解像度」で説明します。

※なおバケツというのはあくまでメタファーであり、実装上はtokensといった名前のint型の変数です。そのためburstを大きくして例えば10MBにしても何かのバッファとして10MBメモリを多く消費するといったことはありません。

(このあたりは詳しく調べていないのですが、パケット送信において消費するメモリを決定するのは/proc/sys/net/core/wmem_default, /proc/sys/net/core/wmem_max, /proc/sysy/net/ipv4/tcp_wmem, 各qdiscのlimitなどでしょう)

classful qdisc

classful qdiscとはclass(階層を持った分類)構造を持ったqdiscです。ここでのclassにはオブジェクト指向などにおけるclassの意味合いはないことに注意してください。主なclassful qdiscにHTBがあります。

classful qdiscの仕組み

classful qdiscは内部に複数のqdiscを持ち、enqueueされようとしているパケットはいずれかの内部qdiscに格納されます。classful qdiscにおいては内部でのenqueue処理が2つに分かれます。classifyと内部qdiscへのenqueueです。

classify、class構造、内部qdiscについてそれぞれ説明していきます。

classify

※筆者はclassについては詳しくないため、下記の記述は正確でない可能性があります。

classifyはfilterからどの内部qdiscに格納するべきかを決定する処理です。

下記のコマンドを例にしてみましょう。

このコマンドのうち3-5行目は実はclassifyには関係がありません。

1:10といった数字が難解に思えるでしょう。この数字はclassidと呼ばれ、majorid:minoridという書式になっています。

今回のコマンドにはわずかなclassしか登場しませんが内部的にはもっとたくさんのclassidが存在します。たとえばmajorid 1の中には1:0, 1:1, 1:2, …, 1:100, 1:1000, …といったclassidが存在します。classifyではこの中で最も小さな番号から分類に利用しようと試みます。今回の例では次にようになります。

  1. 1:0を確認する
  2. 1:0はfilterである。IPパケットがTCP、UDPなどであった場合にsource portが6379であるか確認し、そうであれば1:10に分類し、classifyを完了する。
  3. 2に該当しなかった場合、他にfilterがないため、defaultで設定された1:20に分類し、classifyを完了する。

class構造

先ほどのコマンド例ではclassifyのルールだけでなくclass構造も定義していました。今回の例だと下図のような構造になります。

classifyではパケットがこのtreeのような構造のいずれかの葉(leaf)に分類されるようなルールを設定する必要があります。

では、このclass構造はどのように利用されるのでしょうか。実はそれはclassful qdiscの実装に任されています。

内部qdisc

内部qdiscはclassfulなqdiscの内部で使用されるqdiscです。一般的にはclassless qdiscであるPFIFOやSFQなどが使用されます。

class構造の葉(leaf)となる要素にはかならずqdiscが1つ付属します。

以下の例ではclassful qdiscであるAのleafとなる1:10と1:20の内部qdiscとしての処理をBとCに行わせるようhandleしています。

ここではBとCに新たなmajorid 100:と200:を与えていますが、PFIFOはclassless qdiscであるため大きな意味は持ちません。他のmajoridとかぶらないものを指定すれば問題ありません。

なお、内部qdiscは指定を省略した場合にも暗黙的に作成されます。HTBの場合PFIFOが作成されます。

HTB

HTB(Hierarchical Token Bucket)は階層構造とTBF機能を持ったclassful qdiscです。TBFと比較すると以下のような機能が追加されています。

  • トラフィックを分類してトラフィックごとに帯域制限を行うことができる
  • 分類されたトラフィックが確実に使用できる帯域を確保する帯域保証ができる
  • classのtree構造を持つことができ、その中で帯域を譲りあうことができる。

HTBでは各classが使用できる帯域を指定することができます。あらためて今まで利用してきた例を見てみましょう。

rateceil が帯域についての指定です。この2つの概念について説明するのはとても難しいのですが、一般的な使用方法においてのことについてのみですと比較的簡単なものとなります。

またburstとcburstはそれぞれrateとceilに対応するtoken bucketのsizeとなります。後述する方法で計算・設定してください。

HTBにも同様の機能があった、ceilから説明していきます。

ceil

ceilはそのclassが使用することができる最大の帯域、つまり帯域制限の値です。上記の例ですと1:1は1000Mbits/sに、1:10は200Mbits/sに、1:20は1000Mbits/sに制限されています。

ceilを指定するとき、子のceilのうち最大のものが親のceilと同一かそれ以下になるようにしてください。さもないと、挙動の説明が大変難しいものとなります。ただし合計値の計算は厳密である必要はなく、概ね合っていれば実用上の問題はありません。

ceilが明示的に指定されない場合、rateと同一の値となります。

rate

rateはclass構造の最上位とそれ以外で意味合いが異なります。

最上位では単にbitrateの上限を意味します。これはceilと同じです。そのため最上位classではrate=ceilとすることを推奨します。

それ以外の箇所ではrateはそのclassに保証されている帯域です。上記の例ですと1:1には1000Mbits/sが、1:10には200Mbits/sが、1:20には800Mbits/sが使用できることが保証されています。

rateを指定するとき、子のrateの合計が親のrateと同一かそれ以下になるようにしてください。さもないと、挙動の説明が大変難しいものとなります。ただし合計値の計算は厳密である必要はなく、概ね合っていれば実用上の問題はありません。

今回の例ですと

200Mbits/s+800Mbits/s=1000Mbit/s

で親のrateと子のrateの合計が同一のものとなっています。

注意点として、実際には1000Mbits/sよりも速度がでない回線を使用する場合、1:10と1:20が2対8の割合で回線を分け合える訳「ではない」です。番号が小さい1:10のパケットが優先的に送信され、quantum(後述)で指定したByte数送信すると次の1:20に切り替わります。そのため、送信するパケットのByte数が大きいときは1対1に近い比率となります。

rateは親のrateが保証されているという前提で子に分け与えられています。指定したrateの中で最も親となるものの子供の合計、この例ですと1:10と1:20の合計の1000Mbit/sはこの設定を行う者自信がなんらかの方法で保証する必要があります。

帯域(rate)の分けあい

ある程度の帯域を保証したいが、保証した帯域を常に使用しつづける訳ではないというトラフィックは往々にして存在します。HTBではそのようにして確保したものの使用していない帯域を、別のトラフィックが必要している場合はそちらに分けあたえることができます。これはHTBのドキュメントや実装ではborrowやlendという表現を用いていますが、借りたものを返したりするわけではないので、分けあいや分け与えという表現が適切でしょう。

具体的にはパケットをdequeueする際、以下のようなロジックで分けあいが発生します。

  1. mainoridの小さいleaf classからrate token bucketを確認し、tokenが残っていればそれを使用する。親、その親(いれば)、…、rootのrate token bucketからも同じ量のtokenを減らす。
  2. leaf classのrate token bucketが空の場合、別のleaf classのパケット送信に処理を移す。
  3. すべてのleaf classでパケットdequeueができないが、いずれかのleafのqdiscにパケットが存在する場合、leafの1つ上の階層の親のclass達を調べ始める。
  4. 親をたどっていきtokenが残っているclassが存在しないか調べる。みつかった場合にはその親とそこよりも上のclassからtokenを減らす。子のtokenは減らさない。

以下のような構成からdequeueするシナリオを考えてみましょう。1tokenは1byteと結びつけられることにします。

最初にleaf classからdequeueが試みられます。番号が若い1:10からdequeueが行われます。

次のdequeueでは1:10からdequeueできるpacketは存在しないため、1:20からdequeueが行われます。

これで1:20のtokenは0になりました。次のdequeueで再び1:20からdequeueを行おうと試みるのですがtokenが足りません。

そこでleafの1つ上の階層(1:1)からのdequeueが試みられます。1:1にはまだtokenがありますね。

1:20に与えられたtokenは使い果たしていたものの1:1にtokenが残っていたため無事にdequeueが行えました。これが分けあいです。

なお、当然ですが1:10などがtokenを使い切っていた場合にはその親の1:1のtokenも減ってしまうため、分けあいはできないことになります。

ちなみに、最後の状態でBにパケットがenqueueされ、dequeueしようとするとどうなるのでしょうか。

こうなります。

なんと1:1のtokensが-100になってしまいました。このようにtoken bucketはマイナスの値をとることもできます。

quantum

quantumとはdequeue対象のclassを強制的に切り替える機能のしきい値[Byte]です。デフォルトでは以下の擬似コードのように算出されます。r2qはRate To Quantumの略でrate[Byte/Sec]からquantumへ変換する係数で、デフォルト値は10です。

quantumは小さすぎると内部的なclassのlookup処理が増えパフォーマンスに影響し、大きすぎると特定のclassのみがdequeueし続けることとなります。通常はr2q=10のままで問題ないでしょう。

r2qからの計算でquantumが1000未満や200000を超える時、kernelで以下のような警告がでます。無視しても構いませんがquantumを明示的に指定することで警告を避けることができます。

bitrateとtoken bucketとtimer解像度

token bucketの説明でbucket size/buffer/burstなどの名前で呼ばれていたパラメータについて説明します。なぜこのようなものが必要なのでしょうか。一定量までは速度制限をかけたくない場合にburstという概念は便利そうですが、むしろ一瞬でもbitrateを超えることは避けたいと思うことがほとんどなのではないでしょうか。

token bucketの実装

これまでの説明ではtoken bucketはこのような仕組みだと説明しました

しかし実際には上図のようにtokenがpacketを送信できる量たまった瞬間にdequeueされるわけではありません。「Traffic Controlのコンセプト」で書いたように、外部からdequeue要求が来たタイミングでしかdequeueすることができません。詳しくは次の「bitrateとdequeue機会」で説明しますが、dequeue機会は制御できないタイミングで発生し、その間隔は10ms以上になることもあります。

この10msという数字はLinuxのHigh Resolution Timer(hrtimer)でのイベント発生の最小間隔です。token bucketの実装にはこのhrtimerが使用されています。そしてこのtimerの解像度はkernelのビルドオプションによって異なりますが10ms(1000HZ)や4ms(250HZ)であることが多いです。

手元のUbuntuでは以下のコマンドで確認することができ250HZ=4msでした。RedHat系では1000HZ=1msであることが多いそうです。

このようなtimerの事情を反映させると以下のような図になります。

ここでbucket sizeを考えてみましょう。bucket sizeを2としてみましょう

dequeueできる量が減ってしまいました。このようにbucketが小さすぎるとdequeue間隔よりも小さな間隔で溢れてしまうのです。制限したbitrateを超えてしまうのも問題ですが、制限したbitrateに達しないのもまた問題ですね。

burst, cburstの理論値の算出

このようにbitrateが小さすぎる現象を避けるためのbucket size(buffer, burst)の最小値の計算は以下のようになります。

burst[Bytes] = bitrate[bits/s] / 8 * timer_resolution[sec]

希望するbitrateが100Mbits/s、タイマ解像度が10msの場合次のようになります。

100Mbits/s / 8 * 10ms

= 100 * 10^6 / 8 * 10 * 10^-3

= 100 / 8 * 10 * 10^3

= 125kBytes

なおHTBにおいてburst, cburst値を明示しない場合、自動計算が行われるのですが、その値はrateが1000Mbits/sの場合に1600Bytesと言った極めて低く使い物にならない値です。これらの値はかならず明示的に指定するようにしてください。

dequeue機会の発生する頻度

前章でtimerの解像度がdequeue機会の発生する頻度と関係するという話をしましたが、残念ながらdequeue機会はtimerの解像度と完全に一致するわけではありません。HTBを使用している場合における、dequeue機会はこれだけあります。

  • プロトコル層からpacketをqdiscにenqueueすることに成功したとき
  • device driverからのパケット送信完了の報告(ハードウェア割り込み)が発生したとき
  • HTBが設定したtimerのcallbackが発生したとき(注: 頻度はtimerの最小間隔の10msなどであるとは限らない、もっとおおきな間隔のtimerが設定される場合もある)
  • その他いろいろ

timer解像度はburst値を設計する上で、重要な指針となりますが実際のdequeue機会はそれよりも短い間隔でも長い間隔でも発生するのです。

そのため、実運用でのburst値はかならず実際にbitrateを測定しながら調整を行う必要があります。私が調整を行った時は前章での理論値の4倍〜10倍程度で実測値が安定しました。

実運用での注意点

パケット送受信は多くのパラメータや機能が関わる極めて複雑な機構です。安定した運用を実現するためにはこのドキュメントに挙げた項目以外にも関係する設定やパラメータを洗い出し、適切な値を設定した上で、念入りに検証を行う必要があります。よく調整される値としては以下のようなものがあります。

  • /proc/sys/net/core
  • rmem_default, rmem_max, wmem_default, wmem_max
  • /proc/sys/net/ipv4
  • tcp_mem, tcp_rmem, tcp_wmem

また、場合によっては誤った設定でも一時的にパフォーマンスがでてしまうケースがあることにも注意が必要です。

例えばHTBにおいてburst値が低すぎる場合でも内部qdiscがPFIFOでlimitが1000などに設定されていると、その分だけはenqueueを続けられ、前章のdequeue機会の「プロトコル層からpacketをqdiscにenqueueすることに成功したとき」を得られるため、PFIFOのlimitに達するまではそれなりのbitrateが出てしまい、途中から失速するという不可解な挙動を生み出します。この挙動は内部qdiscを明示的に指定せず、txqueuelen(=PFIFO limit)がデフォルト値として1000などに設定されていた場合も発生し、より原因の特定を困難にします。

また同様に内部qdiscにSFQを使用した場合も、巨大なトラフィックを発生させているセッション以外のセッションでのパケット送信はenqueueが成功する仕組みになっているため、「プロトコル層からpacketをqdiscにenqueueすることに成功したとき」のdequeue機会を得られ、不安定ながらタイマ解像度による制約により出ないと考えられる高いbitrateとなることがあります。

用語集

device: Linuxでネットワークインターフェースとして認識されているものです、eth0といった名前になっていることが多いEthernetポートなど物理的な実態があるものも、lo(local loopback)のようにソフトウェア的に実装されているものもあります。

device driver: deviceのコア部分です。送信においてはqdiscを抜けたパケットを実際に送信したり、送信が完了した際にそれを通知する割り込みを発生させたりします。

その他

本ドキュメントではtcの設定にiproute2のtcコマンドを用いてますがtcng(Traffic Control Next Generation)というツールが存在し、より人間的な文法でtcの設定を行えるようです。

私は試していませんが、これからtcを導入する方はこちらの利用を検討してもいいかもしれません。

まとめ

このドキュメントではHTBを中心にPFIFO, PFIFO_FAST, SFQなど帯域制限・帯域保証の設定に関係することのあるtcのqdiscやその設定項目について説明しました。このドキュメントと別に記載したガイドラインをあわせることで安定したtc運用の一助となれば幸いです。

参考文献

https://github.com/torvalds/linux

もっとも参考になりとても正しいです。qdiscの実装はnet/schedあたりに置かれています。

Traffic Control HOWTO

比較的新しく読みやすいです。(2006年)

Linux Advanced Routing & Traffic Control HOWTO

古いが一部こちらにしか乗っていない情報も存在します。

Linux Advanced Routing & Traffic Control HOWTO

上記ドキュメントの日本語訳

Differentiated Service on Linux HOWTO

yuya.yaguchi

Linux TC (帯域制御、帯域保証) 設定ガイドライン

Abstract

このドキュメントはLinuxにおいて帯域制限のためにtcを用いる際のガイドラインです。

tcは様々な用途に活用できるものですが、プロダクションにおいて特定のserver daemonのトラフィックを制限するというシナリオで活用することを目的としています。

tcのより詳しい詳細については別にドキュメントを書きましたのでそちらを参照してください。

よくわかるLinux帯域制限

Root qdiscの選定

帯域制限を行いたい場合のqdiscは主に以下のようになるでしょう。

  • TBF
  • PRIO + 内部qdiscとしてTBF
  • HTB

それぞれ用途に合わせて適切なものがあるのですが、機能としてはHTBが前者2つの上位互換となるので、迷った場合にはHTBを使えば問題ありません。ということで以後HTBの設定について解説します。

class構造,トラフィックのclassify, filterの設定

すべてのトラフィックを一括で制限したい場合

HTBを使う場合は最低限のclass構造を設定します。filterはdefaultの設定のみ行います。

トラフィックを分類して特定のトラフィックを制限したい場合

特定のトラフィックのみを制限したい場合にはそのトラフィックを特定するためのfilterと言われる分類ルールを記述する必要があります。

filterには以下のような情報が活用できます。

  • 送信元(source)のIP
  • 送信元(source)のポート番号
  • 送信先(destination)のIP
  • 送信先(destination)のポート番号
  • プロトコル(tcp, udp, icmp, …)
  • device(eth0, …)
  • TOSフィールド
  • その他

残念ながら特定のprocessからの通信を抽出するようなfilterは(少なくとも簡単には)つくることができません。

以下のような2分割を行うclass構造に対応するfilterを考えます。

source / destination IP によるfilter

例として以下のケースを考えます。

tcを設定するコンピュータAのprivate IP: 192.168.1.5

主な通信先となるコンピュータBのprivate IP: 192.168.1.10

subnet mask: 255.255.255.0 (= /24)

filterを設置するclassは1:0、

filterにヒットした場合の分類先は1:20とします。

送信元がAの場合にヒットさせたいときは以下のようにします。(注:tcは送信するパケットの制御のみを行うため、大抵の場合このfilterには意味がありません)

送信先がBの場合にヒットさせたいときは以下のようにします。

送信先がLAN内のIPである場合にヒットさせたいときは以下のようにします。

source / destination port によるfilter

送信元portが5555の場合にヒットさせたいときは以下のようにします。

送信先portが6666の場合にヒットさせたいときは以下のようにします。

複合的なfilterの例

ANDの結合は一行に複数のルールを並べることで実現できます。

送信先IPが192.168.1.10で送信先portが6666である場合にヒットさせたいときは以下のようにします。

ORの結合は一つのclassに複数のルールを設置することで実現できます。

送信元portが5555 or 5556 or 5557の場合にヒットさせたいときは以下のようにします。

今回上げた以上に複雑なAND/ORを組み合わせたルールについては自分は検証したことがありません。

検証

classにどの程度のpacketが流れているかは以下のコマンドで確認できます。

実際にトラフィックを流しながらfilterの設定が正しいか確認しましょう。具体的な確認方法については「トラフィックの確認」で解説しています。

その他

このドキュメントでは一部のみを取り上げています。完全な情報については以下のURLを参照してください。

Linux Advanced Routing & Traffic Control HOWTO Chapter 9. Queueing Disciplines for Bandwidth Management

ただしこのfilterの設定、class構造の設定は極めて難解です。iproute2のtcコマンドの代わりに、こういった設定が簡単に行えるとされているTCNGを利用してみてもいいかもしれません。

rate, ceilの設定

rate, ceilは分類したclassにどの程度のトラフィックを割り当てるかをbitrateで指定する仕組みです。

一般的にトラフィックを制御するためにコントロールするものにはbitrateとパケット数があると思いますが、残念ながらパケット数を制御する方法はtcでは提供されていません。

表記について

tcコマンドにおいては単位の表記に注意が必要です。文脈によるが以下の様に解釈されます。

  • bps: Byte Per Second
  • bit: Bit Per Second
  • b: Byte
  • bit: Bit

bpsがByte Per Secondとなることは大きな誤解を生む原因となります。。この単位は使用しないことが推奨されます。このドキュメントでも以降Per Secondとなる単位についてはすべてbitまたはbits/sと表記します。

なおK, M, G等の単位については問題なく使用できます。ただしそれぞれ10^3, 10^6, 10^8として扱われ、2^10, 2^20, 2^30ではないことには注意が必要です。

rate

rateはclass構造の最上位とそれ以外で意味合いが異なります。

最上位では単にbitrateの上限を意味します。これはceilと同じです。そのため最上位classでは必ずrate=ceilとするようにしてください。

それ以外ではclassに対して「ここまでのbitrateが出る」ことを保証する(=帯域保証する)数値です。

class構造の親のrateが子のrateの合計と同一かそれ以下になるようにしてください。

例として以下のようなclass図で1Gbits/sの回線に接続している場合を考えます。

1:10は確実に300Mbits/sが与えられて欲しい(=帯域保証したい)トラフィック、1:20はその他のトラフィックで特に帯域保証は必要ないものとします。

1:1には実際の回線の上限のトラフィックを設定します。

1:10には回線について「確実に出るであろうbitrate」を設定します。ここでは1Gbits/sなのですが、確実にその速度が出るかはわかりません。他のマシンが帯域を使いすぎている場合も考えられます。ここでは、おおまかな計算として8掛けしてrate: 800Mbits/sとします。

1:100は帯域保証が必要なトラフィックのclassです。rate: 300Mbits/sとします。

1:200はその他のトラフィックのclassです。帯域保証は必要ないため、rate: 1Mbits/sとします。(0は設定できないため)

ここで、1:200には帯域保証が不要ではなく、1:100に保証されている帯域以外については1:200が「確実に」使用したい(=帯域保証したい)場合には、800 – 300 = 500でrate: 500Mbits/sとします。

ceil

ceilはclassに対して「これ以上のbitrateが出ないようにする」(=帯域制限する)数値です。

class構造の親のceilを子のceilの最大のものが超えないようにしてください。

引き続きrateの時に利用した例を使って説明します。

1:10については300Mbits/sまで出て欲しいが逆に300Mbit/s以上は出ないで欲しい。

1:20については回線を目一杯使用しても構わない。

という場合

1:1は回線の最大値である1Gbits/sとします。ceil: 1000Mbits/s(注:それ以上の値でも構いません)

1:10は制限したい数値にします。ceil: 300Mbits/s

1:20も回線の最大値である1Gbits/sとします。ceil: 1000Mbits/s

burst, cburstの設定

burstはrateに、cburstはceilにそれぞれ対応するbufferのようなものの大きさです。詳細な理解については別ドキュメントを参照してください。

ここでは設定方法のみを説明したいと思います。

qdiscからのパケットの送信は常に行われているわけではなく0-100ms程度のランダムな間隔で行われています。また、qdiscへのパケットの到着も一定ではなくパケットを送信するapplication, server processの都合により時間ごとに偏りがでます。burstを大きくすることでこのような偏りに対応して、1sec, 10secなどの単位で設定したbitrateを「出し続ける」ことができます。

burstの設定は最小の値を計算して、それを測定しながら安定して設定したbitrateが出るまで大きくしていく作業となります。

quantum, r2qの設定

quantum, r2qの設定は通常必要ないです。デフォルトのr2q=10で問題ありません。

ただし設定によりkernelのwarningが出ることがあります。その場合も無視してしまっても大丈夫なのですが、r2q経由ではなく明示的quantumを指定することで警告を避けることができます。詳細は別ドキュメントを参照してください。

計算

burstの「最小でもこのくらいにしておかなくてはならない」という値については計算で得ることができます。

burst[Bytes] = bitrate[bits/s] / 8 * 10[msec]

となります。なぜ10msecなのか等については別ドキュメントを参照してください。

検証

最小の値を計算したら、実際に検証しながら数値を大きくしていきます。大抵の場合最小の値の3〜20倍程度の大きさとなるでしょう。

例としてrate, ceilのいずれかを100Mbits/sとしたい場合には以下のようにしていきます。

Step1. 最小の値を計算する。

= 100 * 10^6 / 8 * 10 * 10^-3

= 100 / 8 * 10 * 10^3

= 125kBytes

Step2. 実際に利用するHTBフィルタの該当classにrate=ceil=検証中のbitrate, burst=cburst=検証中の値となるように設定する

他の設定がボトルネックになったり、逆に他の設定のせいで制限が外れてしまうことを防ぐようにします。

次のようなフィルタで100Mbitの箇所について検証する場合、こうなります。

そして1:10にトラフィックを流しながらburst値を大きくしていきます。できれば実際のアプリケーションによるトラフィックが望ましいです。

以下は例です。

burst[Bytes] bitrate[Mbit/s]
125kB 50MB
250kB 89MB
500kB 98MB
1000kB 101MB
1500kB 101MB

この例では1000KB以上が必要であることがわかりました。

なお今回は1Gbit/sの回線において100Mbit/sの制限値であったため検証を行えましたが、実環境では他への影響を考慮して、限界値に近い800Mbit/s等の検証を行うのは危険な場合があります。その場合は他の測定値を参考にしながら、十分大きめの値を設定してください。

例えば今回の例では最終的には次のような設定としても構いません。

内部qdiscの設定

HTBのclass構造で葉(末端)となるclassには必ず内部qdiscが付属します。特に指定しない場合も暗黙的に付きますが、tcのshowコマンドでデバッグが行いにくくなることや、どのようなパラメータが設定されているか不明となることなどデメリットが多くあります。そのため、内部qdiscは必ず明示的に指定してください。

内部qdiscの種類

内部qdiscには以下のようなものがあります。

  • PFIFO
    • HTBの内部qdiscには一般的にこれを用いる
  • PFIFO_FAST
    • PFIFOでTOSを考慮して優先度の高いIPパケットが先に送信されるようにしたもの
    • ただしHTB内ではclass番号が小さいものが先に処理されるルールがあり、こちらが優先されるため、使うメリットが弱くなっている。具体的には1:10と1:20があった場合には、「1:10の優先パケット→1:10の通常パケット→1:20の優先パケット→1:20の通常パケット」の順に処理される
  • SFQ
    • 複数のセッションに公平に送信機会を与える仕組みを持つ
    • 制限目一杯のパケットを送信することが多いclass内で、すべてのセッションのlatencyを公平にしたい場合には役立つ
    • 一方通常の通信でも処理のオーバーヘッドによりlatencyが増えてしまうため、必要性がない場合は使用しない方が良い。

設定方法

PFIFOを設定する場合は以下のようにします。

limitはqdiscにいくつのpacketを貯めておけるかというパラメータです。通常は1000や10000などにしておけば問題ありません。

limitが小さいとbitrateがでなかったりpacketのdropが発生する原因となります。

limitが大きいと帯域を目一杯使用している場合にlatencyが悪化する原因となる場合があります。

qdiscでどの程度のdropが発生していて、どの程度のpacketが貯まっているかは以下のコマンドで確認できます。

実際にトラフィックを流しながら設定を確認しましょう。具体的な確認方法については「トラフィックの確認」で解説しています。

トラフィックの確認

実際に流れているトラフィックを確認するにはtc -sコマンドを利用します。具体例を挙げながら解説していきます。

tc -s classではclassにおけるトラフィックを確認できます。tc -s qdiscでは各qdisc (HTB, HTBの内部qdisc)におけるトラフィックを確認できます。

これらにおいて、主に確認すべき点は各classのSent byte, dropped, overlimitsです。

Sent XXX bytesはそのclass, qdiscが送信したパケットの累計バイト数を表しています。流したトラフィックが想定したclassを通っているかがわかります。

droppedはenqueueできなかったパケット数です。latencyより安定したtroughputを重視する場合は内部qdiscのlimitを増やすことでdropを抑えることができます。

overlimitsはdequeueしようとした際にrateやceilなど何らかの制限に引っかかりdequeueできなかった回数です。速度制限が有効かどうかの目安となります。

設定例

ここでは完全な設定例をいくつか示します。

すべてのトラフィックをまとめて帯域制限する

すべての項目を500Mbit/sに制限する例です。

この例では帯域保証は必要ないため、rateはceilと同一の値としてしまっています。

rateを0にしても同様の動作となります。

先頭のqdisc delは既に設定が存在した場合にそれを削除するものです。安全のため、かならず記述しておくことをおすすめします。

特定のトラフィックにのみ帯域制限をかける

192.168.1.10宛のトラフィックのみを100Mbit/sに制限する例です。帯域保証はありません。

DBにおいて定期的にバックアップを行う際の帯域制限などのようなイメージでしょうか。

特定のトラフィックにのみ帯域制限をかけながら帯域保証も行う

source portが5555, 5556のトラフィックを200Mbit/sに制限する例です。帯域保証も同200Mbit/sとします。

一応その他のトラフィックについてもある程度(200Mbit/s)の帯域保証を行うようにします。

イメージとしてRedisのreplicationを想定しています。レプリケーションが切れたり遅延したりしないように帯域保証が必要で、かつ新規レプリケーションの際には膨大なトラフィックが流れるため、帯域制限も必要となります。

1:20のceilを1000Mbitにしているため、1:10のトラフィックが少ない場合は1:20が1:10に保証された分も使用することができるようになっています。

なお、上記の例では回線全体で保証できるbitrateが明示されていません。やや冗長な表現となりますが、以下のように修正することで回線全体の保証を明示できます。(回線全体での保証を800Mbits/sとする)

チェックリスト

作成したtcの設定をプロダクションに適用する前に、必ず以下の項目を確認してください。

  • htbの各classについてrate, ceil, burst, cburstが明示的に設定されている
  • 最上位classではrate==ceilとなっている
  • htbの各末端classについて内部qdiscとその設定が明示的に設定されている
  • 帯域の制限(XXXbits/s以上出ないこと)、帯域の保証(XXXbits/sまで出ること)について実環境で検証されている

注意

このガイドラインは策定されたばかりで十分にプロダクションでの実績をもつものではありません。個々のケースにおいては無条件に利用するのではなく、あくまで参考程度にお考えください。