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
时,默认会将集群中除了Unschedulable
、Master
、NodeReady
外的其它节点都加入负载均衡器,成为负载均衡的后端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
模式下,外部的请求要进到集群里边会经过以下几个步骤:
- 客户端请求流量到达负载均衡器
- 负载均衡器将流量均衡转发到节点上的Node Port
- 节点上的Node Port再通过Ipvs将流量均衡转发到
endpoint
(Pod)上
当service
的endpoint
不在本机时,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
会利用iptables
和ipset
做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