在线游戏场景下的 Kubernetes 网络实践与优化
新闻
作者:夏天
译者:
2018-01-09 09:18

  大家好,我今天分享的题目是“在线游戏场景下的 Kubernetes 的网络实践与优化”。我是来自腾讯互娱的周威,我所在的组是计算资源组,我们这个组主要是负责计算资源的一些分配和调度,主要涉及的是物理机、虚拟机和容器,我这边主要是负责容器这方面。 


TenC 容器平台 


我们这边有一个平台叫 TenC 容器平台,这个平台主要是专注于容器计算的一些资源服务,现在主要有三个方面的功能:

  • 容器主机模式,我们在这个上面会把 Init 的这个进程给拉起来,就是说业务使用起来相当于一个轻量级的虚拟机,这样的话会对他们迁移的话可以保存已有的一些习惯。
  • 微服务的这种架构的,原生集群的这种服务,我今天分享的内容主要是关于这一块。
  • 弹性计算功能,因为我们计算资源组有一些 buffer 池,或者说还有一些老旧设备,我们对它去再利用的话,利用容器快速调度的能力就可以提升我们的资源利用率。



网络架构 


第一部分,我先简略的讲一下我们的这个网络架构,大家对 Docker 和 Kubernetes 这个网络应该比较了解了,所以我就简单提一下吧。


1.Docker 网络方案

Docker 的话,这个网络主要是 4 种模式:

  • Host 模式,就是共享主机的 Namespace,IP 和端口。这种模式使用起来的确是比较方便,但是有一定的缺陷,因为是共享的嘛,如果跑多个容器就有可能会导致端口冲突。
  • Bridge 模式,就是网桥模式,它会分配一个网桥,容器起来的时候,将 veth 加到网桥里面,并给它分配一个 IP。
  • Container 模式,这个模式会在起一个容器之后给它设一个网络,接下来起的容器把它加到这个容器的网络空间里面去,然后它们会共享网络空间。
  • None 模式,说白了就是没有网络啦,只有一个 Loopback 设备。

我们一般用的话都是把 Bridge 模式和 Container 模式,两个组合起来用。对于 Kubernetes 这边,每个 Pod 先起一个 Sandbox 的容器,创建好 Namespace,这个容器是 Bridge 模式的,然后将其他容器用 Container 模式加入。



2.跨主机通信方案

刚才说的是单机的一个架构,因为我们是要跑一个集群,肯定是需要跨母机的网络通信,这就涉及到一个跨主机的网络方案。

这个的话一般主流的有这 3 种方案:

  • NAT 模式,NAT 模式相当于所有的容器在同一个主机里相当于一个子网,通过 NAT 方式出去,一般通过 Iptables 实现,这个方案的主要缺陷是性能损耗比较大。
  • Overlay 或者说 tunnel 的模式,一般来说像什么 ipip,VXLAN,VPN 都是这种模式。简单来说就是把容器的网络封装一个它自己的报头,然后让主机根据外面的报头去传送。
  • 路由方式,路由方式就是通过路由,设置路由的方式来让节点知道另外一个容器所在的位置。

3.Overlay Network

这个一般是有 BGP 和 MacVLAN 的方案,但是这种方式的话对底层网络有一定的要求,而且一般来说在单一的一个数据中心里使用,如果这个机器比较多的话,会导致路由膨胀的问题,所以说我们综合了一下这几个方案,现在用的是第二种的方案,就是 Overlay 这个方案底层用的是 VXLAN,Kubernetes 那边的话,我们用的 flannel 这个网络插件。





这是我们的网络架构图,对一个容器,我们分配了一对虚拟的 veth pair。一个是绑定在容器里面,一个是绑在了网桥上面,还有一个 VXLAN 的 tunnel,同时在上面配置了两条路由规则。一个是本机的网段,就是下面这个。通过 cbr0 这个网桥来通信。另外一个就是集群的路由,如果不是本机的网段就会走这个路由。


4.数据流

现在我们来看一下数据的流向,假如有一个容器 C1,他想发一个包到 C2 这个容器。 

流向大概是这样的:首先他发了一个包,因为走了 veth pair 发到了网桥这边,网桥这里看一下它的目的地址,目的地址是 192.168.24.66。它会匹配这条规则,这个规则是走到了这个网卡。然后就会在上面封一个头,这个是内层的,从 C1 发出来的那个报文,然后会封一个 VXLAN 的头,就是这里。

对于 flannel 这个插件来说,这个集群所有的全网信息,IP 地址还有 MAC 信息都会存到 etcd 里面,这个包过来之后,会触发内核的一个 L3 miss,这时 flannel 就会设一个表项告诉内核,把这个包通过 VXLAN 传到这边去,这个 HOST2,这边拿到这个包的话,tunnel 这里拿到的包已经去掉 VXLAN 的报头了,理论上说其实是网卡先发送到这里,然后通过这里把包解析出来,他拿到了里面的 IP 地址,也是这个地址。然后匹配的路由就是自己子网的路由,就发到这个设备上,就是 C2 这个容器,然后这个报文就收到了。




5.SRIOV

刚刚说的是集群里面的通信,我们分配的是集群内部的一个子网 IP,比如刚刚说的要 192.168,这是一个内网 IP 了,但是有的时候我们还是要分配一个物理 IP,就是跟母机同一个网段的一个 IP。我们用的是 SRIOV,这个东西其实是单根虚拟化的一个缩写,他以前一般是用在虚拟机上面的。




比如说虚拟机起来的时候,然后要给他分一个在同母机的一个网站的一个 IP,这个需要网卡的支持。但是我们结合这个 CNI 的规范,就是容器网络接口,由 CoreOS 提出,我们把这两个结合起来,写一个叫 SRV-CNI 的一个插件,这个插件是开源的。通过这个插件,我们可以对容器分配一个物理 IP,这个 IP 是和母机的网在同一个网段,接下来我会具体的讲一下他的这个应用场景。

几个问题及解决方案 


第二部分就是说我讲一下我们在搭建这套集群,搭建这种网络模式的时候遇到一些问题,还有我们给的一些解决方案。


1.业务对外暴露服务

对于我们的游戏业务来说,我们肯定需要接入外网,就是说需要让外网去访问我们这个服务,对吧?如果访问服务的话,有个解决简单的方案,就是我们给他配一个外网 IP,直接访问。但是这样的话,因为我们这个国内的网络情况十分复杂,如果一些接入问题,还有一些网络连通方面的问题就比较麻烦,所以说我们一般不会这么干。我们会采用公司的一个叫 TGW 的一个服务,就是 Tencent Gateway.




他做了一些接入方面的一些优化,然后我们只需要在后端去把这个 IP 这个配置给它,它就会把流量转发过来。但是这套方案在以前的那种服务器,就是物理的服务器的话,没什么问题,但是到了我们这个容器的方案会有一个问题,就是我们这个容器挂掉的时候,我们需要通知  TGW,他说我这个 IP 挂掉了把这个 T 掉,流量就不要的转过来了。但是我们服务是由 Kubernetes 调度的话,其实这么来通知他让他 T 掉不大方便的,相对来耦合性也比较大。

所以我们做了一个这样一个东西。我们在这个里面加了一层代理 LB,其实对于公有云来说,比如说 GCE 和 AWS,原生 Kubernetes 里面集成它们的 LB 功能的,但是因为我们这个是一个私有云,所以我们需要自己设计了一个 LB。我们这个 LB 其实是基于 haproxy 或者 nginx,两个我们都有用,比如说流量从 TGW 这边过来的话,先通过 LB 转发到这边,转到我们下面这个 Service,他对应的 Pod。

一旦这个设备这个 Pod 增加一个,弹性伸缩,或者说已经挂掉了,然后调度了也一样吧,增加了一个 Pod。我们这个 LB 会 Watch K8S 的 API 接口,然后我们就感知到这个变化,我们就把这个加到 haproxy 或者 nginx 的配置里面,Reload 一下,其实对于 LB 这层的话,它变动不是很多。因为他也不会经常去发布它,除非物理 IP 挂掉了,也不会经常去调度它,它的变化就很少。对于 TGW 这层基本上不用变动。而 LB 这层的配置由我们自己感知,自己来做。这样就和 TGW 互相解耦。这个就是我们对外暴露的一个解决方案。


2.Iptables 性能堪忧

上面这个问题就是我们原来用的是 Iptables,就是 Kubernetes 的 Service 所依赖的 Kube-proxy 使用的是 Iptables 来转发流量。但是对于游戏来说游戏的模块是比较多的。看一下,这就是我从一个游戏里面拿出来的一个列表,基本上是差不多有四十个模块。




而对于游戏的话,他分区也分的比较多,大家玩游戏的时候经常能看见网通区,电信区这类的。就算他不分这些区的话,还得根据登陆来分 QQ 区,微信区,所以区还是分的比较多的,这样的话就是每个区域四十几个 Service,再多几个业务在同一个集群里面,就会导致 Service 非常多,我们统计了一下,每个 Service 差不多需要 8 条 Iptables 规则来转发流量,这样在我们的集群里就可能会产生 1w 多条 Iptables 规则。

我们测试了一下,因为 Iptables 是一个链式的存储,如果加一下规则的话,需要把所有的规则遍历一下,就会导致加一条规则需要 5 分钟。这个 5 分钟对我们来说是完全不能接受的,这个体验很差的,比如说你生成一个模块,5 分钟才能用。

基于这个问题的话,我们当时也想了几个方案:

第一个方案最先想到的,既然 Service 的太多了,那就拆一下吧,拆成好几个集群,每个集群上的 Service 肯定就少一点。但这样的话集群拆多了的话,肯定我们这个管理成本就上升了,也不好管这么多集群。所以我们当时就觉得这个方案不怎么好。

第二个就是我们 Headless 的 Service,Headless 大家都知道吧,就是没有 IP 的 Service。如果用这种 Service 的话,所有请求获得后端 IP 地址是通过 DNS 来查到的。就是解析一次 DNS,DNS 那边随机给我返回一个 IP。但这个也有一个问题,就是这个游戏模块,或者第三方库,实现的 DNS 解析这个过程其实是没有那么靠谱的。比如说有的库解析一次,然后存起来一小时,就不再解析了,甚至有的地方做得更彻底,程序起来了,把它解析一下,存起来,程序不重启就不管了,总的来说 DNS 解析有的时候没那么可控,觉得这个方案也没那么好使。

然后找了第三个方案。IPVS 的方案,原来的 Iptables 是基于 List,链表的方式,它查的比较慢,但是IPVS 是基于 Hash 的存储,它查的就很快了。我们也试了一下,一样的规则数量,1w 条,增加一条,只需要 2 分钟,这个就非常快了。在调度或者生产的时候这个地方就不存在瓶颈了。但是这个 IPVS 用的话还有点小问题,这个我们下一节再说。


3.访问外部模块权限扩大

接下来还有一个问题,我们集群里面跑的容器,需要访问一些外部的周边系统,一般来说在我们公司的话,这些周边系统都会做一些权限控制和一些白名单的限制。或者说简单一点,就像 MySQL 的那样,它也有基于 IP 的权限验证。如果直接通过 NAT 方式去访问周边服务的话,这个时候外部的服务,看到的就是母机的 IP。

如果我们加白名单的话,就需要把母机的 IP 加到白名单里面,但是因为调度的原因,我们并不知道这个容器下一次会跑到哪个母机上面,那这样的话,我们需要把所有 IP 都加到白名单里。这样做其实是有很大的安全风险,这里也需要一个好的解决方案。




俗话说的好,所有的软件问题都可以通过加一个中间层来解决。然后我们也是这么解决的,就是中间加一层,加一个 Service。对于每个容器来说,它如果访问这个 DB,我们给它创建一个 Service,这个 Service 把所有的请求拿过来,然后把它转到这个 DB 去。这个时候对于这个容器来讲,它访问的就是这个 Service,对于这个 DB 来讲,它看到的就是这几个 IP,它不会看到母机 IP。这个时候我们就给这三个 IP 授权,其实我刚刚说的那个 SRV,分配一个物理 IP,其实这个地方也用到了,就是这种 IP。而且在这个地方,我们其实对 Kubernetes 稍微做了一点改动。

Kubernetes 的 Service 的这个名字是有限制的,就是 xx,或者 aaa-bbb 什么的,不能带点的名字。但是有的时候会有一个问题,就是如果用域名来访问的话,比如说我们这个Service设置一个名字叫 aaa,那么它的域名就是 aaa,再访问的时候,如果这个不是一个 DB,比如说这个 Web 服务,它用的是 https 的,这会导致域名和证书不匹配了。这个不匹配的话,虽然我们可以忽略这个这书,或者忽略这个错误,但是这个的话也是会引起安全风险。所以说我们就改进一下,把这个规则放开了,让它可以设加点的这个 Service 名字。

举个例子,如果这个系统需要访问百度,然后你就可以把这个 Service 命名为 www.baidu.com,那么在集群内部去访问的时候可以直接访问 www.baidu.com,这时解析出来的 IP 就是 Service 的 VIP。这边有个 Trick,因为 Kubernetes 的 Service 其实是一个很长的域名,后面有一个 namespace.svc.cluster.local 的后缀。但是你要短域名的话,你可以不用加这个后缀。直接用它,虽然它其实是一个很长的域名,但是解集的时候会被自动加上去。这样的话,接入我们平台的时候,它的那个程序和配置完全不用改,域名都不用改,直接放进来就可以跑,这样接入成本就很低了。这就是权限扩大的一个问题。


踩的一些坑 


好,上面三个就是我们的一些的问题的解决方案。下面就是第三个部分,遇到一些坑,我们的做法。


1.IPVS 功能尚不成熟

第一个是 Kubernetes 的 IPVS 功能,我们当时测 IPVS 其实是比较早的一个时候,大概是今年夏天的时候,那个时候 IPVS 这个功能还在开发,我们那时候跑的是 1.7 的版本,我们就把 IPVS 的 Patch 给 Backport 过来进行测试,等到 1.8 发布的话我们就迁移到了 1.8 这个版本。


但是 1.8 的这个版本的 IPVS 功能其实是 Alpha 特性。我们测的时候也遇到了一些问题,我们合入了主线的一些修复,还有自己提交的一些 Patch,这些都 Merge 到主线了,会在 1.9 版本发布,IPVS 也变成了 Beta 版。1.9 应该是今天才发布,所以说如果大家也是像我们一样用 IPVS 这个功能的话,我还是建议大家第一天就用这个版本。




2.跨 Namespace 转发问题

有一个就是我刚刚说的那个代理。这个代理的话,它是一个 4 层转发,就比如说你去访问那个 mysql,不可能说你要访问 mysql,就写个 mysql 代理,转一下协议。访问 mongodb 就写个 mogodb 的代理。


所以这里用的是 4 层转发,当然 haproxy 也可以,但是我们基于其他原因考虑,用的是 Iptables 或者 IPVS。如果用 IPVS 的话,我们遇到这样一个问题,我们设想是这么来转发的,在一个母机上面有个容器,我们通过访问代理的这个 Service 转到了代理这个容器上面,由代理容器访问外部服务,数据流向应该是这样的,这个图应该比较直观了。


但是我们测试的时候发现,这样做流量不通,过不去。我们抓包的时候发现就流量从这里过来了,这边也收到了,但是没往这转,在这里断掉了。我们就很奇怪,这是怎么回事,经过挺久的分析查找,翻了下内核代码。看这里,IPVS 这里有个标志位,IPVS 转过的数据包,它再来的时候就不会在转发了,这个可能是内核防止形成环路,或者重复转发的一个考虑。但是在我们这个场景上面,因为我们这边转过去的话,在容器里面我们还要去转发一次。它如果不转的话,这个流量肯定出不去了。


我们认为这样做,在传统的同一个 Namesapce 的情况下,这样转发是没有问题的,但是如果跨了 Namespace 的话的,也不转发的话是不合理的,因为不同 Namespace 之间的转发不应该受到影响。于是我们就给内核提交了一个 Patch,社区接受了我们的 Patch 把这个 bug 修复了。


根据这个邮件,大家可以看一下,这应该是 11 月 4 号合入主线的,大概在上月底至本月初合到了所有 Stable 分支,所以说如果大家也想这样用的话,就用新一点版本的内核,或者把这个 Patch 自己合过来吧。




3.网卡丢包问题

最后一个问题就是网卡丢包问题。我们测试的时候发现流量大了,或者说服务性能跑起来之后网卡就丢包。


我们登上母机看了一下,发现有一个内核进程,这个进程大家应该认识吧,就是软中断的处理进程,它跑的 CPU 非常高,这个这后面这个标号零,就是代表它是运行在在 CPU#0 的。这个中断比较高,我们看了一发现是网卡中断占用比较高。因为我们这边是开了 SRIOV 的机器上,对于我们的网卡来说,有 16 个收发队列,但是如果我们使用 SRIOV 的话,会给每个 VF 分配一个队列,我们一般会开启 12 个 VF,这样 PF 只剩下 4 个队列可用。


进一步分析发现,有几个业务进程也跑在 CPU#0 上面,我们其实是把 VF 的中断绑定到了前 4 个 CPU 上面了,这样的话业务进程也分配到 CPU#0 的话,占用了 CPU 就导致网卡中断处理不过来,然后就发生丢包。对于这个问题来说,其实最彻底的解决方案是使用 GRO 来减少 CPU 使用率,但是 GRO 这个特性需要网卡和内核支持,我们的环境没法开启。


我们就使用了另一个方案,把前 4 个核全部预留出来,全力的跑网卡中断程序,让业务进程不会占用这 4 个 CPU,这样就解决了网卡丢包问题。这就是我们这解决的一个方案。




Q&A


Q1: 我想先问一下,就是你刚才提到的那个游戏规模是怎么样的? 还有一个是 flannel 性能,刚才提到的 Iptables 问题是不是 flannel 造成的?还有一个就是你们弹性伸缩的话,为什么没有使用 Kubernetes 的方案?

A:第一个问题,我们的游戏规模,这个问题比较敏感,其实我刚那个图,就是服务的那个列表。就是从一个实际运行的游戏上面截取出来的,第二个问题,Iptables 的性能其实就是它自己的实现的问题,和 flannel 是没有关系的,因为你可以回去试一下,在一个机器上配置 1w 条 Iptables 规则的话,再加一条,就会需要很久时间,或者删一条,也是一样的。flannel 的话开启 UDP 的 RSS,性能会好很多,最后那个问题,其实我们就是用的 Kubernetes 的伸缩方案,这个 LB 只是用来对接 TGW,它只是感知 Pod 变化,它自己并不参与伸缩的过程。

Q2: 老师你好,我是搞网络的,我就说你们 MTU 设置多大?还有针对移动网络有那些优化?

A:对于更外层一点的接入层,其实是 TGW 做的,他们专门做这些优化的团队。是比较专业的对国内这个网络环境做优化的,我们更专注在集群网络在这里,这些要是由我们团队来做的话就太复杂了。MTU 的话因为我们用的一般就是 1500,因为公司网络的限制,巨型帧的标准不统一还有一些兼容性考虑,我们没有使用。其实开启了 GSO 的话已经可以可以显著减少 CPU 的消耗了。


END

554 comCount 0