test
Network
docker
kubernetes
QUIC

kubernetesでQUICのLoad Generatorを作った話

ギリシャからこんにちは!こんばんは!
NTTコミュニケーションズアドベントカレンダー 6日目です。

私は今、神々の王ゼウスが生まれた伝説の島、クレタ島にいます。
というのも、こちらで開催されているACM CoNEXT 2018という国際学会に参加しているからです。
QUICに関する最新成果を発表する Evolution, Performance, and Interoperability of QUIC (EPIQ) ワークショップ も併催されました。
ポスター発表もしてきたので、今日はそれに関連して、kubernetes(k8s)を使ったQUIC負荷テスト の構成方法について、お話します。

この記事でわかること

  • QUICについての基礎知識
  • kubernetesを使った負荷テストの構築手順(kubeadm + kubenet)

QUICとは

QUICは、TCP+TLSに代わるUDP上の新しいトランスポートプロトコルです。
Googleが2013年から開発を開始しました。Webを早くすること("Make the web faster")を主目的としており、

  • HoL(ヘッド・オブ・ライン・ブロッキング)の回避
  • 0-RTTの実現(初回は1-RTT) (RTT: Round Trip Time)
  • 独自の輻輳制御
  • コネクションIDによるセッション管理

といった特徴を持っています。
雑に説明すると、「TCP+TLSって2-RTTもかかるなんて遅くね」「TCPの輻輳制御って、モバイルとクラウド時代に合ってないよね」ということで、TCPの改良は(時間がかかるので)諦めて、UDPのユーザランド上でさっさとやりたかった新機能を実装してしまおう、という流れをGoogleが作った、ということです。
詳しくはこちらをどうぞ。

QUICの旨味

Googleの論文によると、YouTubeの再生において、動画のリバッファ(再生停止してくるくるする奴)の発生率を大幅に削減することができたとのこと。うまー。

gQUICとiQUICの違い

現在は、Googleの仕様と経験に基づいて、IETFで標準化が進められており、google QUIC(gQUIC)とIETF QUIC(iQUIC)という名称で区別されます。

  • iQUICは、順調に行けば来年には標準化されると目されています。
  • 標準化後は、googleもiQUICを利用開始する、と言われています。

gQUICはGoogleのためのもの、iQUICはみんなのためのもの。いいね。

QUICとHTTP/3の違い

HTTP over QUICのことをHTTP/3と呼ぶようになりました
QUICはあくまでトランスポートプロトコルなので、いろんなアプリケーションをQUIC上で動かすことができます。
例えば、DNS over QUIC や WebRTC over QUIC などがありうるでしょう。
QUICすなわちHTTP/3ではないこと、ご注意ください。

QUICの普及度

gQUICは、Googleのサービス間で主に使われています。
つまり、ChromeやAndroid端末でYouTubeの動画を視聴したときには、気づかないうちにQUICを使っていることになります。
こちらの論文によると、2017年8月の統計で、Googleから流れるトラフィックの42.1%がQUICであり、全インターネットの2.6%から9.1% がすでにQUICだという統計がでています。すごいですね。

こちらは、CoNEXT2018の論文からですが、イタリアのISPで、Webトラフィックの中でQUICの割合が10%を超えています。
image.png

他方、iQUICは20以上の独立した実装があり、相互接続試験が盛んに行われていますが、商用での大規模な利用はこれからだと思います。

ISP/キャリアから見たQUIC

我々のようなネットワーク事業者では、利用しているネットワーク機器がトラフィックの負荷に耐えられるかどうか、常日頃試験しています。

通常はIXIAのような負荷発生装置(アプライアンス)を使います。

  • 市販の負荷発生装置には、インターネットを模擬するために、IMIXと呼ばれる様々なパケット長のパケットをいい感じにミックスしたトラフィックを生成する機能などがあります
  • また、高価なものでは、HTTPなどのアプリケーショントラフィックを、ステートフルに生成することもできます
  • けれども、gQUICのトラフィックを生成することはできません

全体の1割に迫る勢いですから、gQUICトラフィックの負荷を生成して、ネットワーク機器へのインパクトを調べておかないといけないですね。
では、試験環境を作ってみましょう。

kubernetesを使った負荷テスト

k8sを使った負荷テストの強みは以下です。

  • コンテナを作れば、色々なアプリケーションの負荷を発生させられる
  • 指定のReplica数を変更するだけで、負荷を簡単に増減できる
  • 負荷を瞬時に同時に発生させられる (CPS: Connection Per Secを負荷としたい場合など)1

k8sで負荷テストというと、Locustを使ってWebの負荷試験を行った話が一番に出てきます。そんなイメージを持っていただけると、この後の話がわかりやすいと思います。

それ、GKEだけじゃなくて、オンプレのk8sで、好きなアプリケーションでできるようになっちゃいましょう。

目的と制約条件

今回の目的では、以下を実現しようとしています。

  • YouTubeの動画視聴を利用する
  • QUICの大規模トラフィックを生成するために、1000人程度のユーザをエミュレーションする
  • それぞれのユーザは異なるIPアドレスを持つ

実現方法

上記はk8sを使って実現できそうです。以下のようなレシピでどうでしょうか。

  • YouTubeを視聴できる chrome+仮想ディスプレイのコンテナを作る
  • k8sのdeploymentを使ってコンテナの展開を制御する

実際の試験構成はこんな感じです2

image.png

ネットワーク構成(kubenet)

通常、k8sのオーバレイネットワークは、k8s上に立ち上がったサービスに外部からアクセスするために、コンテナの追加・削除と連携して動作します。
しかし、今回は、サービス側ではなくクライアント側を展開するので、いくつかの制約を外すことができます。

  • ルーティングさえされていれば、コンテナの追加・削除と連携する必要はない
  • コンテナのサービスIP/Portに対する名前解決は必要ない
  • コンテナ間の通信は必要ない

そこでkubenetです。
kubenetは、GKEで利用されているプラグインで、ingressと協調して動くことが想定されています。しかし、kubenetは大変シンプルな構成ですので、条件を逆手にとって、オンプレでkubenetで構築してしまえばいいのではないでしょうか!

kubenetの特徴については、こちらのURLにまとまっています。

Kubenet is a very basic, simple network plugin, on Linux only. It does not, of itself, implement more advanced features like cross-node networking or network policy. It is typically used together with a cloud provider that sets up routing rules for communication between nodes, or in single-node environments.

まとめると、以下のようなネットワーク構成になります。

network-pluginとして kubenet を利用する

  • worker にプールされたIPアドレスが各Podに払い出される
  • NATなしで外部に直接接続できる

image.png

kubernetesを使った負荷テストの構築手順(kubeadm+kubenet)3

手順は以下になります。

  1. CRI(Docker)のインストール (master/worker共通)
  2. kubeadmのインストール (master/worker共通)
  3. kubeadm initの実施 (master)
  4. kubelet の設定 (master)
  5. kubeadm joinの実施 (worker)
  6. kubelet の設定 (worker)
  7. ルーティングの設定
  8. コンテナの展開 (master)

コマンドはubuntu 16.04で実施したものです。

1. CRI(Docker)のインストール(master/worker共通)

kubernetes v1.6.0から、CRI準拠のコンテナ・ランタイムを選ぶことができるようになりましたが、ここはDocker一択です。
公式ドキュメント CRI installation を参考に進めていきましょう。

注意点としては、rootユーザで進めていきましょう。

Please proceed with executing the following commands based on your OS as root. You may become the root user by executing sudo -i after SSH-ing to each host.

apt-get install -y docker.ioだと、17.03.2-ce が入ります(2018.12.03調べ)。
18.06 が推奨と書いてあるので、バージョン指定の手順ですすめましょう(Dockerのインストール手順は公式マニュアルを見てください)。

2. kubeadmのインストール(master/worker共通)

簡単にk8sクラスタが組めるkubeadmを利用していきます。
公式ドキュメント Installing kubeadmを参考に進めていきましょう。

注意点としては、swap offにしないと、kubeletが起動しません。

Swap disabled. You MUST disable swap in order for the kubelet to work properly.

ついでに、下準備をまとめて実施しておきます。

下準備1. swap off

swap on だと kubeadm コマンドで errorがでる理由はこちらを読んでください。

以下を実施。

# swapoff -a

reboot時にも反映されるように /etc/fstab の設定も変更

# vi /etc/fstab
#UUID=<your-uuid-is-here> none            swap    sw              0       0
swapの指定をコメントアウト

free コマンドを実行して、swap の無効化を確認します。

# free
              total        used        free      shared  buff/cache   available
Mem:        4046324      640624      720176       42460     2685524     2961140
Swap:             0           0           0

下準備2. ip_forward=1

今回の構成では、ブリッジネットワークと外部ネットワークの間でホストがルーティングするので、ホストにip_forwardの設定を入れます。

# vi /etc/sysctl.conf
net.ipv4.ip_forward=1

sysctlコマンドで変更しても即時反映されないことがあるので、実施後再起動して、反映されていることを確認するとよいでしょう。

下準備3. 鍵認証(パスフレーズなし)

master/workerでお互いに、ホスト名でログインできるようにしておきましょう。

kubeadmのインストール

公式マニュアルより、以下の手順です。

apt-get update && apt-get install -y apt-transport-https curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
apt-get update
apt-get install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl

バージョン確認

# kubeadm version
kubeadm version: &version.Info{Major:"1", Minor:"12", GitVersion:"v1.12.3", GitCommit:"435f92c719f279a3a67808c80521ea17d5715c66", GitTreeState:"clean", BuildDate:"2018-11-26T12:54:02Z", GoVersion:"go1.10.4", Compiler:"gc", Platform:"linux/amd64"}

3. kubeadm initの実施(master)

公式ドキュメント Creating a single master cluster with kubeadmを参考に、途中まで進めていきます。

# kubeadm init
[init] using Kubernetes version: v1.12.3
(以下省略。kubeadmがどういう動きをしているのかがわかるのでログメッセージはぜひ読んでください)

ログメッセージの指示にしたがって、この部分をコピペで投入します。

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

workerがjoinするためのtokenももらえます。

  kubeadm join <your-ip>:6443 --token <your-token> --discovery-token-ca-cert-hash sha256:<your-hash>

このtokenを手元のメモに控えておくのを忘れないように。24時間経つと無効になるらしいです。

この状態で以下のコマンドでステータスを確認してみましょう。

# kubectl get node
NAME         STATUS     ROLES    AGE     VERSION
k8s-master   NotReady   master   3m36s   v1.12.3

NotReadyになっています。なぜでしょうか。
kubeadmコマンドの裏でkubeletが実行されているわけですが、デフォルトではネットワークの設定が入っていないためです。noop pluginが使われている、とこちらに書いてあります。

By default if no kubelet network plugin is specified, the noop plugin is used, which sets net/bridge/bridge-nf-call-iptables=1 to ensure simple configurations (like Docker with a bridge) work correctly with the iptables proxy.

ネットワークの設定を入れることで、Readyになります。

4. kubelet の設定(master)

公式ドキュメントでは、ネットワーク設定として、ここからCNI準拠のcalicoなどを入れる手順になっていますが、それは無視して、ここから独自の設定に入っていきます。
netowrk-pluginとして、CNIではなくて、kubenetを使います4

The network must be deployed before any applications. Also, CoreDNS will not start up before a network is installed. kubeadm only supports Container Network Interface (CNI) based networks (and does not support kubenet).

マニュアルには、kubenetはサポートしてないと書かれてますが、中身の動作を理解していればそんなことはないです。心を強く持ちましょう。

では、ネットワークの設定を入れていきましょう。/etc/default/kubeletに、以下のようにkubeletのオプションを入れて、再起動します。

# vi /etc/default/kubelet
KUBELET_EXTRA_ARGS=--network-plugin=kubenet --non-masquerade-cidr=0.0.0.0/0 --pod-cidr=192.0.2.0/24

# systemctl daemon-reload
# service kubelet restart
  • kubenetを使う場合は、--network-plugin=cniではなく、--network-plugin=kubenetを指定することになります。
  • --non-masquerade-cidr=0.0.0.0/0で、コンテナ発の通信がNAT対象外になります
  • --pod-cidr=192.0.2.0/24で、Podに払い出されるIPアドレスのプールを記述します。

するとあら不思議、ステータスがReadyになりました。

# kubectl get nodes
NAME         STATUS   ROLES    AGE   VERSION
k8s-master   Ready    master   28m   v1.12.3

masterノードにおけるpod-cidrに関する補足

masterノードには試験用のコンテナをデプロイしないので、pod-cidrを使わないということもあるでしょう。
残念ながらその場合でも、masterノードでのpod-cidrの指定は必須になります。

--pod-cidr のオプションを入れなかった場合、kubeletは以下のエラーを吐きます。

journalctl -f -u kubelet
Dec 03 16:20:54 k8s-master kubelet[25338]: E1203 16:20:54.861566   25338 pod_workers.go:186] Error syncing pod 7e0b2d4e-f6c7-11e8-9941-000c29b3ec12 ("coredns-576cbf47c7-n8459_kube-system(7e0b2d4e-f6c7-11e8-9941-000c29b3ec12)"), skipping: network is not ready: [runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: Kubenet does not have netConfig. This is most likely due to lack of PodCIDR]

kubeletのオプション指定に関する補足

kubeletのオプションの一覧は、kubelet -hで見ることができます。
今回使ったオプションは廃止予定となっており、YAML形式のconfigで与える方式に移行することを勧められます。

# kubelet -h | grep pod-cidr
      --pod-cidr string
The CIDR to use for pod IP addresses, only used in standalone mode.  In cluster mode, this is obtained from the master. For IPv6, the maximum number of IP's allocated is 65536 (DEPRECATED: This parameter should be set via the config file specified by the Kubelet's --config flag. See https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/ for more information.)

こちら--configで読み込ませるYAMLファイルのオプションの指定の仕方が書いてあります(試していないです)。

5. kubeadm joinの実施(worker)

worker側で、clusterにjoinするために、先ほどメモしたtokenを使います。

# kubeadm join <your-ip>:6443 --token <your-token> --discovery-token-ca-cert-hash sha256:<your-hash>

やはりNotReadyで立ち上がってきます。

master:~# kubectl get nodes
NAME          STATUS     ROLES    AGE   VERSION
k8s-master    Ready      master   36m   v1.12.3
k8s-worker1   NotReady   <none>   13s   v1.12.3

kubeadmによるクラスタでは、workerのROLESは<none> で登録されるのでこれで正しい状態です。

6. kubelet の設定(worker)

同様に、worker側でもkubeletの設定を投入して、kubeletの再起動をしましょう。

# vi /etc/default/kubelet
KUBELET_EXTRA_ARGS=--network-plugin=kubenet --non-masquerade-cidr=0.0.0.0/0 --pod-cidr=203.0.113.0/24 --cluster-dns=8.8.8.8

# systemctl daemon-reload
# service kubelet restart
  • --cluster-dns=8.8.8.8で、外部のDNSを指定しました。ユーザを模擬したコンテナを作るという目的の元では、コンテナ間の通信は不要なので、外部のDNSを指定できるのは、むしろ都合がよかったりします。

このようにReadyになりました。

master:~# kubectl get nodes
NAME          STATUS   ROLES    AGE    VERSION
k8s-master    Ready    master   45m    v1.12.3
k8s-worker1   Ready    <none>   9m9s   v1.12.3

7. ルーティングの設定

上流のルータから、各ホストのpod-cidr向けにルーティングしてあげましょう。スタティック経路で十分です。

ip route 203.0.113.0/24 <worker-IPaddress>

worker間(コンテナ間)でルーティングさせたい場合は、各ホストにip route addで経路追加していけばOKです。

8. コンテナの展開

確認用に nginxのimageを使ってみます。deployment-example.yml というファイルを作成してreplicasを指定してみます。

deployment-example.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: deployment-example
spec:
  replicas: 10
  template:
    metadata:
      labels:
        app: deployment-example
    spec:
      containers:
      - name: nginx
        image: nginx:1.10

createする!

# kubectl create -f deployment-example.yml

確認してみましょう。-o wideオプションをつけると、Podに割り当てられたIPアドレスも出てきます。このように、pod-cidrからIPアドレスが払い出されて、複数コンテナがほぼ同時に立ち上がることがわかります。

master:~# kubectl get pods -o wide | sort -r -k6
NAME                                  READY   STATUS    RESTARTS   AGE   IP                NODE          NOMINATED NODE
deployment-example-6dfbcfd7c8-bt2vj   1/1     Running   0          77s   203.0.113.1    k8s-worker1   <none>
deployment-example-6dfbcfd7c8-vfmbs   1/1     Running   0          77s   203.0.113.2    k8s-worker1   <none>
deployment-example-6dfbcfd7c8-w8nt7   1/1     Running   0          77s   203.0.113.3    k8s-worker1   <none>
deployment-example-6dfbcfd7c8-x7qgs   1/1     Running   0          77s   203.0.113.4    k8s-worker1   <none>
deployment-example-6dfbcfd7c8-hk5zm   1/1     Running   0          77s   203.0.113.5    k8s-worker1   <none>
deployment-example-6dfbcfd7c8-xqv7j   1/1     Running   0          77s   203.0.113.6    k8s-worker1   <none>
deployment-example-6dfbcfd7c8-zqmqc   1/1     Running   0          77s   203.0.113.7    k8s-worker1   <none>
deployment-example-6dfbcfd7c8-mv29l   1/1     Running   0          77s   203.0.113.8    k8s-worker1   <none>
deployment-example-6dfbcfd7c8-2mrtg   1/1     Running   0          77s   203.0.113.9    k8s-worker1   <none>
deployment-example-6dfbcfd7c8-52pms   1/1     Running   0          77s   203.0.113.10   k8s-worker1   <none>

ちゃんとコンテナで名前解決(cluster-dnsで指定したDNSが使われます)できて外部に通信できていることを確かめます。また、これはNATされていません。

kubectl exec -it deployment-example-6dfbcfd7c8-bt2vj ping google.co.jp
PING google.co.jp (216.58.197.131): 56 data bytes
64 bytes from 216.58.197.131: icmp_seq=0 ttl=56 time=0.830 ms
64 bytes from 216.58.197.131: icmp_seq=1 ttl=56 time=0.950 ms
64 bytes from 216.58.197.131: icmp_seq=2 ttl=56 time=1.093 ms
^C--- google.co.jp ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.830/0.958/1.093/0.108 ms

これで、試験環境の準備は整いました。

YouTube視聴コンテナ

YouTube視聴用のコンテナを用意する際のエッセンスを紹介しましょう。

Headless ChromeではQUICは使えない

こちらにバグレポートがありますが、Headlessモード(ディスプレイ無しのChrome)ではQUICが使えません。仕方がないので、各コンテナに仮想ディスプレイ(xvfb)を入れることにしました。

YouTube iFrame API

YouTube iFrame APIという素晴らしいAPIがあります。このAPIを通して、動画の再生品質や、リバッファ(再生停止)が発生した回数や長さなどの情報を得ることができます5
<iframe>でYouTubeの動画を埋め込んだWebページを用意して、ブラウザから情報が取れるようにしました。動画を埋め込むことで、不要な広告の再生をやめさせることができるという副次的な効果もありました。

取得可能な情報の例はこちらです(独自に加工しています)。

-[ RECORD 1 ]---------------+---------------------------------------------------
initdomtreetime             | 10
frequency_of_quality_change | 3
unloadeventtime             | 0
buffered_fraction           | 6.263613008118926
qualities_changed           | [large, hd1440, hd2160]
redirecttime                | 0
readystart                  | 1
domreadytime                | 1425
videoid                     | IZJ48x7LsHg
durations_of_qualities      | [366, 113, 292042]
quic_flag                   | true
lookupdomaintime            | 26
quality                     | hd2160
requesttime                 | 12
loadtime                    | 1473
available_qualities         | [hd2160, hd1440, hd1080, hd720, large, medium, small, tiny, auto]
connecttime                 | 0
loadeventtime               | 0
stall_1                     | {duration=684, seek_time=5.000114440917969}
stall_2                     | {duration=348, seek_time=221367.10892752075}
appcachetime                | 0
timestamp                   | 2018-03-07 23:36:05

DockerFile

DockerFileは直接お見せすることはできませんが、

  • chrome
  • xvfb
  • selenium

が入ったコンテナイメージを用意して、init スクリプトで「chromeを立ち上げてYouTubeの視聴をseleniumで制御する」ようにすれば、ユーザの挙動の模擬をすることができます。

deployment

レポジトリ(自前で用意)にdocker imageを入れて、以下のように展開します。

quic-deployment.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: quic-test-deployment
spec:
  replicas: 1000
  template:
    metadata:
      labels:
        app: quic-test
    spec:
      containers:
      - name: quic
        image: <your-repository>:5000/quic-image:latest
        volumeMounts:
          - mountPath: /dev/shm
            name: dshm
      imagePullSecrets:
      - name: regsecret
      volumes:
      - name: dshm
        emptyDir:
          medium: Memory

これでkubectl create -f quic-deployment.ymlでcreateすれば、replicasの数を変更するだけで、指定の数のコンテナが全てのworkerで均等にdeployされます。Cool!

結果

QUIC→HTTP2→QUIC→HTTP2→...と、順番に試験しています。QUICはUDP、HTTP2はTCPです(色は合わせてません)。

replica=10

image.png

replica=1000

image.png

こんな感じで、ユーザ数を増やしてQUICトラフィックの負荷をスケールさせることができました!
1000コンテナであっても、1分半以内にすべてのコンテナが通信を開始しています。

最後に

k8sで負荷テストするのは、思ったよりも旨味がありました。
おかしくなったサーバを、drainで利用停止にしたり、workerの数を増やしたりと、負荷テストにおいて面倒な部分をk8sが吸収してくれます。
サーバサイドではないk8sの使い方の一例として、本記事にはヒントになりそうな色々な要素をなるべく取り入れました。
長くなってしまってすみません。最後まで読んでいただきありがとうございました。


  1. 負荷発生装置(アプライアンス)だと、負荷発生の準備に20-30分くらいかかることがあります。ネットワークの負荷試験は大変時間がかかるのです。 

  2. 中の人と話せるパスがない場合は、公開サービスに対してこのような試験をするのは控えるべきです。 

  3. 今年3月に行った試験では kubernetes v1.9.1を利用していましたが、今回の記事作成にあたって、最新のkubernetes v1.12.3 で構築できることを確かめました。本記事の手順は、v1.9.1とv1.12.3で動作確認できていることになります。 

  4. kubeletが、kubernetesクラスタの各ノードで働くエージェントで、kubenetが、network pluginの一種です。名前がややこしい。ちなみに、利用可能なCNIのバイナリはkubeadm installの際に自動的に/opt/cni/binに設置されています。kubenetは内情としては、CNI bridgeの機能を使っています。 

  5. YouTubeは ABS(Adaptive Bitrate Streaming)といって、ネットワーク品質に応じて、再生品質が再生中に動的に変わります。そのような再生品質(ビットレート)の変化イベントも取得することができます。