Kube-router 实战


使用 Kube-router 作为 Kubernetes 负载均衡器

Kube-router 实战

使用 Kube-router 作为 Kubernetes 负载均衡器

Fri Apr 20, 2018

3800 Words|Read in about 8 Min
Tags: kubernetes   lvs  

Kube-router 是一个挺有想法的项目,兼备了 calicokube-proxy 的功能,是基于 Kubernetes 网络设计的一个集负载均衡器、防火墙和容器网络的综合方案。

1. 体系架构


Kube-router 是围绕 观察者控制器 的概念而建立的。

观察者 使用 Kubernetes watch API 来获取与创建,更新和删除 Kubernetes 对象有关的事件的通知。 每个观察者获取与特定 API 对象相关的通知。 在从 API 服务器接收事件时,观察者广播事件。

控制器 注册以获取观察者的事件更新,并处理事件。

Kube-router 由3个核心控制器和多个观察者组成,如下图所示。

流程分析

Kube-router 启动之后,首先创建 wathcer:

func (kr *KubeRouter) Run() error {
	...
	err = kr.startApiWatchers()

startApiWatchers 中,会启动 endpoint、namespace、pod、node、networkpolicy、service 这六个 wather。

这六个 wathcer 将监听的变化发送到 Broadcaster

func NewBroadcaster() *Broadcaster {
	return &Broadcaster{}
}

func (b *Broadcaster) Add(listener Listener) {
	b.listenerLock.Lock()
	defer b.listenerLock.Unlock()
	b.listeners = append(b.listeners, listener)
}

func (b *Broadcaster) Notify(instance interface{}) {
	b.listenerLock.RLock()
	listeners := b.listeners
	b.listenerLock.RUnlock()
	for _, listener := range listeners {
		go listener.OnUpdate(instance)
	}
}

之后创建三个 controller:NetworkPolicyControllerNetworkRoutingControllerNetworkServicesControllers。 每个 controller 会监听所关心的资源的变化。

func NewNetworkServicesController(clientset *kubernetes.Clientset,\
	config *options.KubeRouterConfig) (*NetworkServicesController, error) {
	...
	nsc := NetworkServicesController{}
	...
	watchers.EndpointsWatcher.RegisterHandler(&nsc)
	watchers.ServiceWatcher.RegisterHandler(&nsc)
	...

每个 controller 遵循以下结构。

func Run() {
    for {
        Sync() // control loop that runs for ever and perfom sync at periodic interval
    }
}

func OnUpdate() {
    Sync() // on receiving update of a watched API object (namespace, node, pod, network policy etc)
}

Sync() {
    //re-concile any state changes
}

Cleanup() {
    // cleanup any changes (to iptables, ipvs, network etc) done to the system
}

2. 主要功能


基于 IPVS/LVS 的负载均衡器 | --run-service-proxy

Kube-router 采用 Linux 内核的 IPVS 模块为 K8s 提供 Service 的代理。

Kube-router 的负载均衡器功能,会在物理机上创建一个虚拟的 kube-dummy-if 网卡,然后利用 k8s 的 watch APi 实时更新 svcep 的信息。svc 的 cluster_ip 会绑定在 kube-dummy-if 网卡上,作为 lvs 的 virtual server 的地址。realserver 的 ip 则通过 ep 获取到容器的IP地址。

基于 Kubernetes 网络服务代理的 Kube-router IPVS 演示

asciicast

特征:

  • 轮询负载均衡
  • 基于客户端IP的会话保持
  • 如果服务控制器与网络路由控制器(带有 –-run-router 标志的 kube-router)一起使用,源IP将被保留
  • –-masquerade-all 参数明确标记伪装(SNAT)

更多详情可以参考:

容器网络 | --run-router

Kube-router 利用 BGP 协议和 Go 的 GoBGP 库和为容器网络提供直连的方案。因为用了原生的 Kubernetes API 去构建容器网络,意味着在使用 kube-router 时,不需要在你的集群里面引入其他依赖。

同样的,kube-router 在引入容器 CNI 时也没有其它的依赖,官方的 bridge 插件就能满足 kube-rouetr 的需求。

更多关于 BGP 协议在 Kubernetes 中的使用可以参考:

网络策略管理 | --run-firewall

网络策略控制器负责从 Kubernetes API 服务器读取命名空间、网络策略和 pod 信息,并相应地使用 ipset 配置 iptables 以向 pod 提供入口过滤,保证防火墙的规则对系统性能有较低的影响。

Kube-router 支持 networking.k8s.io/NetworkPolicy 接口或网络策略 V1/GA semantics 以及网络策略的 beta 语义。

更多关于 kube-router 防火墙的功能可以参考:

3. 使用 kube-router 替代 kube-proxy


下面进入实战阶段,本方案只使用 kube-router 的 service-proxy 功能,网络插件仍然使用 calico(估计只有我能想到这么奇葩的组合了 ✌️)

前提

  • 已有一个 k8s 集群
  • kube-router 能够连接 apiserver
  • 如果您选择以 daemonset 运行 kube-router,那么 kube-apiserver 和 kubelet 必须以 –allow-privileged=true 选项运行

集群环境

角色 IP 地址 主机名
k8s master 192.168.123.250 node1
k8s node 192.168.123.248 node2
k8s node 192.168.123.249 node3

安装步骤

如果你正在使用 kube-proxy,需要先停止 kube-proxy 服务,并且删除相关 iptables 规则。

$ systemctl stop kube-proxy
$ kube-proxy --cleanup-iptables

接下来以 daemonset 运行 kube-router,这里我们使用 DR 模式。

$ kubectl --namespace=kube-system create configmap kube-proxy  --from-file=kubeconfig.conf=/root/.kube/config
$ wget https://raw.githubusercontent.com/cloudnativelabs/kube-router/master/daemonset/kubeadm-kuberouter-all-features-dsr.yaml

# 将 kubeadm-kuberouter-all-features-dsr.yaml 里的 --run-router 参数和 --run-firewall 参数的值改为 false
$ kubectl create -f kubeadm-kuberouter-all-features-dsr.yaml

在每台机器上查看 lvs 条目

$ ipvsadm -Ln

IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.254.0.1:443 rr persistent 10800
  -> 192.168.123.250:6443         Masq    1      0          0
  
$ ipvsadm -S -n

-A -t 10.254.0.1:443 -s rr -p 10800
-a -t 10.254.0.1:443 -r 192.168.123.250:6443 -m -w 1

可以看出,kube-router 使用的是 lvs 的 nat 模式。

创建一个应用测试 kube-router

$ kubectl run whats-my-ip --image=cloudnativelabs/whats-my-ip --replicas=3

# 暴露服务
$ kubectl expose deploy whats-my-ip --target-port=8080 --port=8080

查看创建好的服务

$ kubectl get pods -owide

NAME                           READY     STATUS    RESTARTS   AGE       IP               NODE
whats-my-ip-845d4ff4f6-d2ptz   1/1       Running   0          23h       172.20.135.8     192.168.123.249
whats-my-ip-845d4ff4f6-jxzzn   1/1       Running   0          23h       172.20.166.130   192.168.123.250
whats-my-ip-845d4ff4f6-szhhd   1/1       Running   0          34s       172.20.104.9     192.168.123.248

$ kubectl get svc

NAME          TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
kubernetes    ClusterIP   10.254.0.1       <none>        443/TCP    45d
whats-my-ip   ClusterIP   10.254.108.117   <none>        8080/TCP   16s

查看 lvs 规则条目

$ ipvsadm -Ln

IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.254.0.1:443 rr persistent 10800
  -> 192.168.123.250:6443         Masq    1      0          0
TCP  10.254.175.147:8080 rr
  -> 172.20.104.9:8080            Masq    1      0          0
  -> 172.20.135.8:8080            Masq    1      0          0
  -> 172.20.166.130:8080          Masq    1      0          0

可以发现本机的 Cluster IP 代理后端真实 Pod IP,使用 rr 算法。

通过 ip a 可以看到,每添加一个服务,node 节点上面的 kube-dummy-if 网卡就会增加一个虚IP。

session affinity

Service 默认的策略是,通过 round-robin 算法来选择 backend Pod。 要实现基于客户端 IP 的会话亲和性,可以通过设置 service.spec.sessionAffinity 的值为 ClientIP (默认值为 “None”)。

$ kubectl delete svc whats-my-ip
$ kubectl expose deploy whats-my-ip --target-port=8080 --port=8080 --session-affinity=ClientIP

$ ipvsadm -Ln

IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.254.0.1:443 rr persistent 10800
  -> 192.168.123.250:6443         Masq    1      0          0
TCP  10.254.226.105:8080 rr persistent 10800
  -> 172.20.135.8:8080            Masq    1      0          0
  -> 172.20.166.130:8080          Masq    1      0          0
  -> 172.20.104.9:8080            Masq    1      0          0
  
$ ipvsadm -S -n

-A -t 10.254.0.1:443 -s rr -p 10800
-a -t 10.254.0.1:443 -r 192.168.123.250:6443 -m -w 1
-A -t 10.254.226.105:8080 -s rr -p 10800
-a -t 10.254.226.105:8080 -r 172.20.135.8:8080 -m -w 1
-a -t 10.254.226.105:8080 -r 172.20.166.130:8080 -m -w 1
-a -t 10.254.226.105:8080 -r 172.20.104.9:8080 -m -w 1

可以看到 lvs 的规则条目里多了个 persistent,即 lvs 的持久连接,关于 lvs 持久连接的具体内容可以参考我的另一篇博文 LVS负载均衡之持久性连接介绍

可以通过设置 service.spec.sessionAffinityConfig.clientIP.timeoutSeconds 的值来修改 lvs 的 persistence_timeout 超时时间。

$ kubectl get svc whats-my-ip -o yaml

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: 2018-04-20T08:16:38Z
  labels:
    run: whats-my-ip
  name: whats-my-ip
  namespace: default
  resourceVersion: "6323769"
  selfLink: /api/v1/namespaces/default/services/whats-my-ip
  uid: 26315fdf-4473-11e8-8388-005056a1bc83
spec:
  clusterIP: 10.254.226.105
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    run: whats-my-ip
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800
  type: ClusterIP
status:
  loadBalancer: {}

NodePort

$ kubectl delete svc whats-my-ip
$ kubectl expose deploy whats-my-ip --target-port=8080 --port=8080 --type=NodePort

$ ipvsadm -Ln

IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  192.168.123.249:34507 rr
  -> 172.20.135.8:8080            Masq    1      0          0
  -> 172.20.166.130:8080          Masq    1      0          0
  -> 172.20.104.9:8080            Masq    1      0          0
TCP  10.254.0.1:443 rr persistent 10800
  -> 192.168.123.250:6443         Masq    1      0          0
TCP  10.254.175.147:8080 rr
  -> 172.20.135.8:8080            Masq    1      0          0
  -> 172.20.166.130:8080          Masq    1      0          0
  -> 172.20.104.9:8080            Masq    1      0          0

可以看到不仅有虚拟IP条目,还多了对应主机的 lvs 条目。

更改算法

  • 最少连接数
$ kubectl annotate service my-service "kube-router.io/service.scheduler=lc"
  • 轮询
$ kubectl annotate service my-service "kube-router.io/service.scheduler=rr"
  • 源地址哈希
$ kubectl annotate service my-service "kube-router.io/service.scheduler=sh"
  • 目的地址哈希
$ kubectl annotate service my-service "kube-router.io/service.scheduler=dh"

4. 问题解决


接下来需要面对一些非常棘手的问题,我尽可能将问题描述清楚。

问题1:在集群内某个节点主机上通过 SVC IP+Port 访问某个应用时,如果 lvs 转到后端的 pod 在本主机上,那么可以访问,如果该 pod 不在本主机上,那么无法访问。

可以通过抓包来看一下,现在 service whats-my-ip 后端有三个 pod,分别运行在 node1node2node3 上。

$ kubectl get pods -owide

NAME                           READY     STATUS    RESTARTS   AGE       IP               NODE
whats-my-ip-845d4ff4f6-d2ptz   1/1       Running   0          23h       172.20.135.8     192.168.123.249
whats-my-ip-845d4ff4f6-jxzzn   1/1       Running   0          23h       172.20.166.130   192.168.123.250
whats-my-ip-845d4ff4f6-szhhd   1/1       Running   0          34s       172.20.104.9     192.168.123.248

node3 上访问 whats-my-ip 服务:

$ ip a show|grep 10.254.175.147

    inet 10.254.175.147/32 brd 10.254.175.147 scope link kube-dummy-if

$ ipvsadm -Ln -t 10.254.175.147:8080

Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.254.175.147:8080 rr
  -> 172.20.104.9:8080            Masq    1      0          0
  -> 172.20.135.8:8080            Masq    1      0          0
  -> 172.20.166.130:8080          Masq    1      0          0

# 第一次访问,不通
$ curl 10.254.175.147:8080

# 第二次访问
$ curl 10.254.175.147:8080

HOSTNAME:whats-my-ip-845d4ff4f6-d2ptz IP:172.20.135.8

# 第三次访问,不通
$ curl 10.254.175.147:8080

同时在 node1 上抓包:

$ tcpdump -i ens160 host 172.20.166.130 -nn

tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens160, link-type EN10MB (Ethernet), capture size 262144 bytes
03:27:26.337553 IP 10.254.175.147.42036 > 172.20.166.130.8080: Flags [S], seq 405854371, win 43690, options [mss 65495,sackOK,TS val 359417229 ecr 0,nop,wscale 7], length 0
03:27:27.340131 IP 10.254.175.147.42036 > 172.20.166.130.8080: Flags [S], seq 405854371, win 43690, options [mss 65495,sackOK,TS val 359418232 ecr 0,nop,wscale 7], length 0

可以看到 node1 将数据包丢弃了,因为源IP是 10.254.175.147,系统认为这是 node1 自己本身。

根本原因可以查看 node3 的路由表:

$ ip route show table local|grep 10.254.175.147

local 10.254.175.147 dev kube-dummy-if proto kernel scope host src 10.254.175.147
broadcast 10.254.175.147 dev kube-dummy-if proto kernel scope link src 10.254.175.147

src 的值用来告诉该 host 使用 10.254.175.147 作为 source address,可以通过修改路由表来解决这个问题:

$ ip route replace local 10.254.175.147 dev kube-dummy-if proto kernel scope host src 192.168.123.249 table local

再次在 node1 上抓包可以发现源IP已经变成了 192.168.123.249

$ tcpdump -i ens160 host 172.20.166.130 -nn

tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens160, link-type EN10MB (Ethernet), capture size 262144 bytes
03:39:42.824412 IP 192.168.123.249.52684 > 172.20.166.130.8080: Flags [S], seq 3520353543, win 43690, options [mss 65495,sackOK,TS val 360153716 ecr 0,nop,wscale 7], length 0
03:39:42.824542 IP 172.20.166.130.8080 > 192.168.123.249.52684: Flags [S.], seq 4057001749, ack 3520353544, win 28960, options [mss 1460,sackOK,TS val 360143668 ecr 360153716,nop,wscale 7], length 0
03:39:42.824706 IP 192.168.123.249.52684 > 172.20.166.130.8080: Flags [.], ack 1, win 342, options [nop,nop,TS val 360153716 ecr 360143668], length 0
03:39:42.825066 IP 192.168.123.249.52684 > 172.20.166.130.8080: Flags [P.], seq 1:84, ack 1, win 342, options [nop,nop,TS val 360153716 ecr 360143668], length 83: HTTP: GET / HTTP/1.1
03:39:42.825112 IP 172.20.166.130.8080 > 192.168.123.249.52684: Flags [.], ack 84, win 227, options [nop,nop,TS val 360143669 ecr 360153716], length 0
03:39:42.825589 IP 172.20.166.130.8080 > 192.168.123.249.52684: Flags [P.], seq 1:174, ack 84, win 227, options [nop,nop,TS val 360143669 ecr 360153716], length 173: HTTP: HTTP/1.1 200 OK
03:39:42.825735 IP 192.168.123.249.52684 > 172.20.166.130.8080: Flags [.], ack 174, win 350, options [nop,nop,TS val 360153717 ecr 360143669], length 0
03:39:42.825787 IP 192.168.123.249.52684 > 172.20.166.130.8080: Flags [F.], seq 84, ack 174, win 350, options [nop,nop,TS val 360153717 ecr 360143669], length 0
03:39:42.825882 IP 172.20.166.130.8080 > 192.168.123.249.52684: Flags [F.], seq 174, ack 85, win 227, options [nop,nop,TS val 360143669 ecr 360153717], length 0
03:39:42.826002 IP 192.168.123.249.52684 > 172.20.166.130.8080: Flags [.], ack 175, win 350, options [nop,nop,TS val 360153718 ecr 360143669], length 0


问题2:在集群内某个节点主机上通过 SVC IP+Port 访问 service kubernetes 时,如果该节点是 master 节点(即 kube-apiserver 运行在该节点上),那么可以访问,如果该节点不是 master 节点,那么无法访问。

原因和问题1类似,可以通过修改路由表解决:

# 例如在 node3 节点上
$ ip route replace local 10.254.0.1 dev kube-dummy-if proto kernel scope host src 192.168.123.249 table local


问题3:在某个 pod 内访问该 pod 本身的 ClusterIP:Port,如果 lvs 转到后端的 IP 是该 pod 的 IP,那么无法访问,如果不是则可以访问。

kube-proxy 的 iptables 模式也有同样的问题,这个问题可以忽略。

总结

问题1和问题2修改路由表可以通过批量 shell 脚本来解决:

#!/bin/sh

default_if=$(ip route|grep default|awk '{print $5}')
localip=$(ip a show ${default_if}|egrep -v inet6|grep inet|awk '{print $2}'|awk -F"/" '{print $1}')
svc_ip=$(ip route show table local|egrep -v broadcast|grep kube-dummy-if|awk '{print $2}')

for ip in $svc_ip; do
ip route replace local $ip dev kube-dummy-if proto kernel scope host src $localip table local;
done

如果想要在创建 service 时自动修改路由表,最好还是将该 fix 整合进 kube-router 的源码中。

5. 参考


「真诚赞赏,手留余香」

杨传胜

真诚赞赏,手留余香

使用微信扫描二维码完成支付

See Also