Ingress是从Kubernetes集群外部访问集群内部服务的入口,但Ingress控制器本身不具备将自身暴露到公网的能力,需要借助LoadBalancer类型的Service,如果你的Kubernetes 集群是运行在AWS、阿里云等公有云上时,通过指定service的类型很容易实现。

1
2
3
4
5
    internet
        |
   [ Ingress ]
   --|-----|--
   [ Services ]

创建一个nginx-ingress LoadBalancer Service的配置大概如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: v1
kind: Service
metadata:
  annotations:
    service.beta.kubernetes.io/alicloud-loadbalancer-address-type: intranet
  labels:
    app: nginx-ingress-lb
  name: nginx-ingress-lb
  namespace: kube-system
spec:
  clusterIP: 10.5.38.199
  externalTrafficPolicy: Local
  healthCheckNodePort: 32075
  ports:
  - name: http
    nodePort: 30247
    port: 80
    protocol: TCP
    targetPort: 80
  - name: https
    nodePort: 30136
    port: 443
    protocol: TCP
    targetPort: 443
  selector:
    app: ingress-nginx
  sessionAffinity: None
  type: LoadBalancer

上边配置中的参数除了下边两个参数外,其他的都很好理解。

  • externalTrafficPolicy
  • healthCheckNodePort

本文主要来分析一下这两个参数的作用。

kubernetes 在创建LoadBalancer类型的Service时,默认会将集群中除了UnschedulableMasterNodeReady外的其它节点都加入负载均衡器,成为负载均衡的后端RealServer,具体节点的处理逻辑在getNodeConditionPredicate这个函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func getNodeConditionPredicate() corelisters.NodeConditionPredicate {
	return func(node *v1.Node) bool {
		// We add the master to the node list, but its unschedulable.  So we use this to filter
		// the master.
		if node.Spec.Unschedulable {
			return false
		}

		// As of 1.6, we will taint the master, but not necessarily mark it unschedulable.
		// Recognize nodes labeled as master, and filter them also, as we were doing previously.
		if _, hasMasterRoleLabel := node.Labels[LabelNodeRoleMaster]; hasMasterRoleLabel {
			return false
		}

		// ServiceNodeExclusion:启用从云提供商创建的负载均衡器中排除节点。如果节点标记有 alpha.service-controller.kubernetes.io/exclude-balancer 键(启用 LegacyNodeRoleBehavior 时)或 node.kubernetes.io/exclude-from-external-load-balancers,则可以排除节点。
		if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.ServiceNodeExclusion) {
			if _, hasExcludeBalancerLabel := node.Labels[LabelNodeRoleExcludeBalancer]; hasExcludeBalancerLabel {
				return false
			}
		}

		// If we have no info, don't accept
		if len(node.Status.Conditions) == 0 {
			return false
		}
		for _, cond := range node.Status.Conditions {
			// We consider the node for load balancing only when its NodeReady condition status
			// is ConditionTrue
			if cond.Type == v1.NodeReady && cond.Status != v1.ConditionTrue {
				klog.V(4).Infof("Ignoring node %v with %v condition status %v", node.Name, cond.Type, cond.Status)
				return false
			}
		}
		return true
	}
}

如果kube-proxy是运行在ipvs模式下,外部的请求要进到集群里边会经过以下几个步骤:

  1. 客户端请求流量到达负载均衡器
  2. 负载均衡器将流量均衡转发到节点上的Node Port
  3. 节点上的Node Port再通过Ipvs将流量均衡转发到endpoint(Pod)上

serviceendpoint不在本机时,ipvs会将流量转发(DNAT)到其它节点上的endpoint(Pod)上,为了保证回去的包(Node->SLB)的源IP不变会再用iptables一次SNAT,具体过程下面会分析。在这种情况下流量虽然在整个集群内得到了更好的均衡,但是也由于做了SNAT后导致后端Pod无法获取到客户端的IP。

为了解决客户端IP丢失的问题kubernetes引入externalTrafficPolicy参数来控制负载均衡器的调度策略,它的默认值为Cluster,代表流量会在整个集群内得到均衡,也就是上边我们分析的情况。它还有另外一个参数local,代表负载均衡器只会将流程转发到那些本机运行有Endpoint的节点上,本机如果没有Endpoint,那么该节点就不参与做负载。

那么就有一个问题:负载均衡器是怎么知道节点上有没有service的Endpoint呢?

这就是healthCheckNodePort参数的作用。

healthCheckNodePort的作用是kube-proxy会暴露一个http接口供前端负载均衡器来做健康检测,当节点上运行有Endpoint时该接口就返回200,该节点就能正常参与做流量转发,反之,如果节点上没有运行Endpoint时接口就会返回503状态码,负载均衡器就会将该节点从集群中移除不参与做流量转发。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
~ curl -i 127.0.0.1:32075
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 09 Mar 2020 14:34:04 GMT
Content-Length: 110

{
	"service": {
		"namespace": "kube-system",
		"name": "nginx-ingress-lb-intranet"
	},
	"localEndpoints": 2
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
~ curl -i 127.0.0.1:32075
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Date: Mon, 09 Mar 2020 14:38:24 GMT
Content-Length: 110

{
	"service": {
		"namespace": "kube-system",
		"name": "nginx-ingress-lb-intranet"
	},
	"localEndpoints": 0
}

healthCheckNodePort虽然解决了不让负载均衡器转发流量到无Endpoint的节点,但是有Endpoint的节点,外部流量进入到节点后,节点上的ipvs又是如何保证流量不会转发到其它无Endpoint的节点呢?

这主要是靠Proxier的处理逻辑,当externalTrafficPolicy值为local时,Proxier在处理Service的Endpoint时只会处理运行在本地的Pod,只把这些Pod添加为ipvs的real server,具体处理逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func (proxier *Proxier) syncEndpoint(svcPortName proxy.ServicePortName, onlyNodeLocalEndpoints bool, vs *utilipvs.VirtualServer) error {
    
    ...//省略

    for _, epInfo := range proxier.endpointsMap[svcPortName] {
        //@xnile 如果Endpoint和Kube-proxy不是运行在同一节点上,且externaltrafficpolicy值为local时,ipvs在添加Realserver时会略过该Endpoint。 
        if onlyNodeLocalEndpoints && !epInfo.GetIsLocal() {
            continue
        }
        newEndpoints.Insert(epInfo.String())
    }

    ...//省略
  
    return nil
}

externalTrafficPolicy值为Cluster 时,流量又是如何在整个集群中做负载的呢?

  • Proxier在处理endpoint将不再判断是否在本地,而是将所有endpoint作为ipvs的后端real server。
  • 当流量在转发到其它节点上的endpoint时,Proxier会利用iptablesipset做SNAT。

kube-proxy运行在ipvs模式时的iptables规则

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */
DOCKER     all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */
DOCKER     all  --  0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
KUBE-POSTROUTING  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes postrouting rules */
MASQUERADE  all  --  169.254.123.0/24     0.0.0.0/0
RETURN     all  --  10.4.0.0/16          10.4.0.0/16
MASQUERADE  all  --  10.4.0.0/16         !224.0.0.0/4
RETURN     all  -- !10.4.0.0/16          10.4.2.0/24
MASQUERADE  all  -- !10.4.0.0/16          10.4.0.0/16

Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  0.0.0.0/0            0.0.0.0/0

Chain KUBE-FIREWALL (0 references)
target     prot opt source               destination
KUBE-MARK-DROP  all  --  0.0.0.0/0            0.0.0.0/0

Chain KUBE-LOAD-BALANCER (1 references)
target     prot opt source               destination
RETURN     all  --  0.0.0.0/0            0.0.0.0/0            /* Kubernetes service load balancer ip + port with externalTrafficPolicy=local */ match-set KUBE-LOAD-BALANCER-LOCAL dst,dst
KUBE-MARK-MASQ  all  --  0.0.0.0/0            0.0.0.0/0

Chain KUBE-MARK-DROP (1 references)
target     prot opt source               destination
MARK       all  --  0.0.0.0/0            0.0.0.0/0            MARK or 0x8000

Chain KUBE-MARK-MASQ (3 references)
target     prot opt source               destination
MARK       all  --  0.0.0.0/0            0.0.0.0/0            MARK or 0x4000

Chain KUBE-NODE-PORT (1 references)
target     prot opt source               destination
RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            /* Kubernetes nodeport TCP port with externalTrafficPolicy=local */ match-set KUBE-NODE-PORT-LOCAL-TCP dst
KUBE-MARK-MASQ  tcp  --  0.0.0.0/0            0.0.0.0/0            /* Kubernetes nodeport TCP port for masquerade purpose */ match-set KUBE-NODE-PORT-TCP dst

Chain KUBE-POSTROUTING (1 references)
target     prot opt source               destination
MASQUERADE  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000
MASQUERADE  all  --  0.0.0.0/0            0.0.0.0/0            /* Kubernetes endpoints dst ip:port, source ip for solving hairpin purpose */ match-set KUBE-LOOP-BACK dst,dst,src

Chain KUBE-SERVICES (2 references)
target     prot opt source               destination
KUBE-LOAD-BALANCER  all  --  0.0.0.0/0            0.0.0.0/0            /* Kubernetes service lb portal */ match-set KUBE-LOAD-BALANCER dst,dst
KUBE-MARK-MASQ  all  -- !10.4.0.0/16          0.0.0.0/0            /* Kubernetes service cluster ip + port for masquerade purpose */ match-set KUBE-CLUSTER-IP dst,dst
KUBE-NODE-PORT  all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            match-set KUBE-CLUSTER-IP dst,dst
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            match-set KUBE-LOAD-BALANCER dst,dst

咱们来看下iptables的处理过程

1
2
 /* Kubernetes service load balancer ip + port with externalTrafficPolicy=local */
-> PREROUTING -> KUBE-SERVICES -> KUBE-SERVICES -> KUBE-LOAD-BALANCER -> (RETURN或KUBE-MARK-MASQ)

如果externalTrafficPolicy不为Local时,外边进来的包在经过PREROUTING链时就会打上标记,在经过ipvs处理后发现应该转发到其它节点上的endpoint,那么数据包在被送出去前经过POSTROUTING时会为已经打上标记包做SNAT,修改源IP为本机IP。这里需要注意一下,为了让iptables能给数据包打上标记有一个很重要的内核参数需要启用:net.ipv4.vs.conntrack=1,关于这个参数的作用可以参见我的另一篇文章LVS+Iptables实现FULLNAT及原理分析

另外需要说明一下,上边分析的逻辑仅适用于legacy-cloud-providers内的云厂商,aws,azure,gce这些。像阿里云的ACK虽然也有这两个参数,但是它的内部实现并没有遵循上边的处理逻辑,它并不会默认将全部节点都加到负载均衡集群,而是仅加入那些有edpoint的节点,所以就不需要healthCheckNodePort来充当健康检测的作用。

参考

http://ja.ssi.bg/nfct/HOWTO.txt

https://netfilter.org/documentation/HOWTO/netfilter-hacking-HOWTO.txt

https://www.kernel.org/doc/Documentation/networking/ipvs-sysctl.txt

https://zhuanlan.zhihu.com/p/94418251