ギリシャからこんにちは!こんばんは!
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%を超えています。
他方、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
ネットワーク構成(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なしで外部に直接接続できる
kubernetesを使った負荷テストの構築手順(kubeadm+kubenet)3
手順は以下になります。
- CRI(Docker)のインストール (master/worker共通)
- kubeadmのインストール (master/worker共通)
- kubeadm initの実施 (master)
- kubelet の設定 (master)
- kubeadm joinの実施 (worker)
- kubelet の設定 (worker)
- ルーティングの設定
- コンテナの展開 (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がでる理由はこちらを読んでください。
- https://qiita.com/tkusumi/items/0962220a0700cb1f6eb3
- http://dr-asa.hatenablog.com/entry/2017/12/19/095008
以下を実施。
# 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を指定してみます。
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を入れて、以下のように展開します。
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
replica=1000
こんな感じで、ユーザ数を増やしてQUICトラフィックの負荷をスケールさせることができました!
1000コンテナであっても、1分半以内にすべてのコンテナが通信を開始しています。
最後に
k8sで負荷テストするのは、思ったよりも旨味がありました。
おかしくなったサーバを、drainで利用停止にしたり、workerの数を増やしたりと、負荷テストにおいて面倒な部分をk8sが吸収してくれます。
サーバサイドではないk8sの使い方の一例として、本記事にはヒントになりそうな色々な要素をなるべく取り入れました。
長くなってしまってすみません。最後まで読んでいただきありがとうございました。
-
負荷発生装置(アプライアンス)だと、負荷発生の準備に20-30分くらいかかることがあります。ネットワークの負荷試験は大変時間がかかるのです。 ↩
-
中の人と話せるパスがない場合は、公開サービスに対してこのような試験をするのは控えるべきです。 ↩
-
今年3月に行った試験では kubernetes v1.9.1を利用していましたが、今回の記事作成にあたって、最新のkubernetes v1.12.3 で構築できることを確かめました。本記事の手順は、v1.9.1とv1.12.3で動作確認できていることになります。 ↩
-
kubeletが、kubernetesクラスタの各ノードで働くエージェントで、kubenetが、network pluginの一種です。名前がややこしい。ちなみに、利用可能なCNIのバイナリはkubeadm installの際に自動的に
/opt/cni/bin
に設置されています。kubenetは内情としては、CNI bridgeの機能を使っています。 ↩ -
YouTubeは ABS(Adaptive Bitrate Streaming)といって、ネットワーク品質に応じて、再生品質が再生中に動的に変わります。そのような再生品質(ビットレート)の変化イベントも取得することができます。 ↩