BBR推奨のパケットスケジューラーのキューイングアルゴリズムによるソケットバッファ枯渇問題

# tl;dr
トラフィックが多いL4ロードバランサーなどのサーバーでは、BBRを有効にし、キューイングアルゴリズムをfqにするとソケットバッファ(以下バッファと呼ぶ)が枯渇するので、fqにするのはお勧めしないというお話です。

# 前提

この記事ではBBRとはなんぞや、keepalivedはなんぞやということは説明しておりません。
前提として、BBRの概要、keepalivedの概要がわかっていることで話をします。
以下が今回検証した環境です。

OS: Ubuntu20.04
keepalived: 2.0.19

# はじめに
TCPの輻輳制御のアルゴリズムであるBBRを有効にしたら、TCPの速度がはやくなるのではないかという話になり、BBRを有効にしてkeepalivedを動かしたら以下のエラーが出力されてサーバとの通信ができなくなりました。


```yaml
Keepalived_vrrp[6098]: Error 105 (No buffer space available) sending gratuitous ARP on eth0 for XXXX
```

BBRを有効にするためにはカーネルパラメータは以下のパラメータを設定。

```yaml

net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
```

ここで、 `net.core.default_qdisc` をfqにしているのですが、こちらの設定でなにやらバッファを使い果たしていそうでした。

# net.core.default_qdiscとは何者か

パケットスケジューラーのキューイングアルゴリズムを決めるカーネルパラメータになっています。
このパラメータで指定したアルゴリズムでバッファからパケットをどのように取り出して、
パケットをNICから出力するかが決まります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575547/1e3670f4-f35f-e4c6-8a04-8beb34ab60eb.png)


キューイングアルゴリズムはいろいろありますが、今回は以下の3つで検証しました。

- pfifo_fast
- fq
- fq_codel

選定理由としては、pfifo_fastはsystemdのバージョン217までのデフォルトのアルゴリズムで、
それ以後のデフォルトのアルゴリズムがfq_codelのためです。
fqは後述しますが、BBRの推奨のアルゴリズムになっております。

参考資料: https://wiki.archlinux.org/index.php/Advanced_traffic_control

## キューイングアルゴリズムの概要
### pfifo_fast

FIFO(First In First Out)のアルゴリズムでバッファから取り出されてパケットを出力します。
バッファに最初に入れたものが最初に出力される仕組みです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575547/0fbd6cc0-0c86-784e-9926-123ccd14afa9.png)

### fq(Fair Queuing)

公平キューイング(Fair Queuing)はバッファを分割し、トラフィックに対して公平に帯域を割り振るアルゴリズムになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575547/b18ea90a-ac5b-361b-51c7-573085e93005.png)

### fq_codel(Fair Queuing + Codel)
公平キューイング(Fair Queuing)だが、pacingによってパケット間の空き時間を作るのではなく、遅延によって一定の速度でデータを通信させます。

# BBRのキューイングアルゴリズムについて
IETFの資料(参考資料: https://www.ietf.org/proceedings/97/slides/slides-97-iccrg-bbr-congestion-control-02.pdf) ではBBRを使ううえで、FQは必須ではないが、BBRはpacingが必要であると書いています。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575547/33e921e8-19d0-f802-7de1-69f4522e5522.png)

※pacingとはパケットとパケットの間に意図的に空き時間を作り，一定の速度でデータを通信するというもので、 ネットワーク上で最もボトルネックになる部分の速度に合わせてデータを通信すれば，他のパケットが混在しない限りはパケットの廃棄は発生せず，最も効率よくデータが転送できます

また、現状の実装だと、BBRはpacingが使える必要があり、
それを実装しているキューイングアルゴリズムがfqであるともIETFの資料に書いてます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575547/bcd90482-c287-d0d6-f0bb-468ccc7f51f0.png)


そのため、BBRはキューイングアルゴリズムとして、FQは必須ではないが、
pacingを実装しているキューイングアルゴリズムが現状(2020/08/13現在)fqのみなので、実質fqはBBRに必要だと考えます。



ちなみにキューイングアルゴリズムがfq以外でもBBRを設定することは可能ですが、
BBRはpacingを利用できることを前提としているので、ネットワークに余分な負荷をかける可能性があります。

# 検証
BBRを有効にしたkeepalivedでバッファを枯渇させないための検証をおこないました。

## BBR+fq以外のアルゴリズム
以下のように `net.core.default_qdisc` をfq以外にすることで、
バッファが枯渇せずに正常に動きました。

検証値1

```yaml

net.core.default_qdisc = pfifo_fast
net.ipv4.tcp_congestion_control = bbr
```

検証値2

```yaml

net.core.default_qdisc = fq_codel
net.ipv4.tcp_congestion_control = bbr
```

いままでは、fqのpacingがネットワーク上で最もボトルネックになる部分の速度に合わせてデータを通信するので、未送信のパケットがバッファに積み上がってましたが、fq以外にするとpacingされないのでパケットが積み上がらずバッファが枯渇しないとためと想定されます。

## TCP_NOTSENT_LOWATを設定する
BBR+fqで設定しているものを探したところ、CloudFlareの記事で以下の設定を設定していました。

```
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_notsent_lowat = 16384
```

参考資料: https://blog.cloudflare.com/http-2-prioritization-with-nginx/

`net.ipv4.tcp_notsent_lowat` の設定値が不明でしたが、調べたところ以下の意味合いがあることがわかりました。

> このTCP_NOTSENT_LOWATの値を設定すると未送信のデータサイズを明示的に制限することができます。そのため、低速回線環境下でもカーネルに送信データを飲み込まれることなく、SPDYサーバプロセスが優先度に応じたデータ送信を制御できることになります。
これにより、バッファに積み上がる未送信データサイズを制限することができ、それにより様々なネットワーク環境下で最適な通信速度を実現できます。

参考資料: https://jovi0608.hatenablog.com/entry/20140207/1391732796

自分たちの環境の場合、低速ネットワーク環境下でpacingを行ったため、 バッファからパケットを取り出して通信するよりもバッファにパケットが積み上がるのが早く、そのためバッファが枯渇していたと考えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575547/1561a2b1-0adc-71a2-81eb-ab2a0bfe9aba.png)


現在使っている `TCP_NOTSENT_LOWAT` は4GBと大きく、低速環境に向かないため、

```
net.ipv4.tcp_notsent_lowat = 4294967295
```

Cloudflareの設定値と同じ16KBと小さくして検証を行いました。

```
net.ipv4.tcp_notsent_lowat = 16384
```

その結果、keepalivedでBBRを有効で、キューイングアルゴリズムをfqにしてもバッファサイズが枯渇することはなくなりました。

### TCP_NOTSENT_LOWATの適切な値は？
適切な値は参考資料とおりだと、カーネルをセルフビルドするしかなさそうです。。

>W. Chanがブログで書いているようクライアントのネットワーク環境に依存するんですが、いったいどうやって決めたらいいでしょうか。 ちょうど先日チューリッヒで IETF httpbis WG の HTTP/2.0に関する中間会議があり >W. Chan と直接会う機会がありましたので聞いてみました。
>私：  「ブログに書いてあるTCP_NOTSENT_LOWATの値って簡単に決められないよね。」
>Chan: 「そう、ネットワーク環境によって違うからちゃんとモニターして決めないと。」
私：  「Googleではどうしているの？」
Chan: 「データ送信キューの遅延をモニターして値を決めてるよ。」
私：  「えっ！ どうやって？ ユーザーランドからはそんなのわかんないよ。」
Chan: 「そりゃカーネルに手を入れて測定しているさ。」
さすがGoogle・・・

参考資料: https://jovi0608.hatenablog.com/entry/20140207/1391732796

# 結論
`TCP_NOTSENT_LOWAT` の最適値をだすためにはカーネルのセルフビルドをさせる可能性があるため、
keepalivedでの `TCP_NOTSENT_LOWAT` 設定はデフォルト値にしました。
また fq_codelのwikiには以下のような記載がありました。

> net.core.default_qdisc = fq_codel - best general purpose qdisc
> net.core.default_qdisc = fq - for fat servers, fq_codel for routers.

参考資料: https://www.bufferbloat.net/projects/codel/wiki/

keepalivedなどのL4ロードバランサーはトラフィックが多いので、
fq_codelがオススメとのこと。

そのため、キューイングアルゴリズムはfqを見送り、BBRも有効にさせませんでした。

# 蛇足
今回の件で、キューイングアルゴリズムによりパケットがどうなっているのかを監視するために<a href="https://github.com/livesense-inc/mackerel-plugin-qdisc">mackerelのplugin</a>を作りました。これにより、キューがどのくらい積まれているのか、どのくらい確立済みソケットがあるのかを監視できます。もし、このプラグインのqlenやbacklog、tx_dropsが積み上がっているとしたらなんらかのネットワークチューニングが必要だと考えます。