kubernetes 设计理念及主要概念之Service(三)

服务发现与负载均衡

Kubernetes提供有服务发现和负载均衡机制,提供了Service资源,并通过kube-proxy配合cloud provider来适应不同的应用场景。

目前,Kubernetes中的负载均衡大致可以分为以下几种机制,每种机制都有其特定的应用场景:

  • Service:直接用Service提供cluster内部的负载均衡,并借助cloud provider提供的LB提供外部访问
  • Ingress Controller:还是用Service提供cluster内部的负载均衡,但是通过自定义LB提供外部访问
  • Service Load Balancer:把load balancer直接跑在容器中,实现Bare Metal的Service Load Balancer
  • Custom Load Balancer:自定义负载均衡,并替代kube-proxy,一般在物理部署Kubernetes时使用,方便接入公司已有的外部服务

Service

service

Kubernetes设计了Service的抽象:逻辑上的一组Pod,一种可以访问它们的策略。这一组Pod能够被Service访问,并为它们提供一个统一的入口。通常是通过Label Selector实现的。借助Service,应用可以方便的实现服务发现与负载均衡,实现应用的零宕机升级。

Service通过标签选取服务后端,一般配合Replication Controller或者Deployment来保证后端容器的正常运行。这些匹配标签的Pod IP和端口列表组成endpoints,由kube-proxy负责将服务IP负载均衡到这些endpoints上。

Service由四种类型:

  • ClusterIP:默认类型,自动分配一个仅cluster内部可以访问的虚拟IP
  • NodePort:在ClusterIP基础上为Service在每台机器上绑定一个端口,这样就可以通过<NodeIP>:NodePort来访问该服务
  • LoadBalancer:在NodePort的基础上,借助cloud provider创建一个外部的负载均衡器,并将请求转发到<NodeIP>:NodePort
  • ExternalName:将服务通过DNS CNAME记录方式转发到指定的域名(通过spec.externalName设定)。需要kube-dns版本在1.7以上。

另外,也可以将已有的服务以Service的形式加入到Kubernetes集群中来,只需要在创建Service的时候不指定Label selector,而是在Service创建好后手动为其添加endpoint。

  1. Service定义

通过yaml或json定义,比如下面定义一个名为nginx的服务,将服务的80端口转发到default namespace中带有标签run=nginx的Pod的80端口,

在此之前,首先要部署Pod,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-app
spec:
selector:
matchLabels:
run: nginx-app
replicas: 2
template:
metadata:
labels:
run: nginx-app
spec:
containers:
- name: nginx-app
image: nginx
ports:
- containerPort: 80

其次再定义Service,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Service
metadata:
labels:
run: nginx-app
name: nginx-app
namespace: default
spec:
type: ClusterIP
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: nginx-app
sessionAffinity: None

Service自动分配了Cluster IP

1
2
3
 kubectl get service nginx-app       
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-app ClusterIP 10.108.166.254 <none> 80/TCP 22s

自动创建的endpoint

1
2
3
kubectl get endpoints nginx-app
NAME ENDPOINTS AGE
nginx-app 10.244.1.38:80,10.244.3.28:80 82s

自动关联endpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
kubectl describe service nginx-app  
Name: nginx-app
Namespace: default
Labels: run=nginx-app
Annotations: <none>
Selector: run=nginx-app
Type: ClusterIP
IP: 10.108.166.254
Port: <unset> 80/TCP
TargetPort: 80/TCP
Endpoints: 10.244.1.38:80,10.244.3.28:80
Session Affinity: None
Events: <none>
  1. 不指定Selectors服务

在创建Service的时候,也可以不指定Selectors,用来将Service转发到Kubernetes集群外部的服务(而不是Pod)。目前有两种方法

一是, 自定义endpoint,即创建同名的service和endpoint,在endpoint中设置外部服务的IP和端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376
---
kind: Endpoints
apiVersion: v1
metadata:
name: my-service
subsets:
- addresses:
- ip: 1.2.3.4
ports:
- port: 9376

二是,通过DNS转发,在Service定义中指定externalName。此时DNS服务会给<service-name>.<namespace>.svc.cluster.local创建一个CNAME记录,其值为my.database.example.com。并且,该服务不会自动分配Cluster IP,需要通过Service的DNS来访问(这种服务也称为Headless Service)。

1
2
3
4
5
6
7
8
kind: Service
apiVersion: v1
metadata:
name: my-service
namespace: default
spec:
type: ExternalName
externalName: my.database.example.com
  1. Headless服务

Headless服务即不需要Cluster IP的服务,即在创建Service的时候,指定spec.clusterIP=None,包括两种类型,

  • 不指定Selectors,但设置externalName,通过CNAME记录处理
  • 指定Selectors,通过DNS A记录设置后端endpoint列表
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
apiVersion: v1
kind: Service
metadata:
labels:
app: nginx-app
name: nginx-app
spec:
clusterIP: None
ports:
- name: tcp-80-80-3b6tl
port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx-app
sessionAffinity: None
type: ClusterIP
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: nginx-app
name: nginx-app
namespace: default
spec:
replicas: 2
revisionHistoryLimit: 5
selector:
matchLabels:
app: nginx-app
template:
metadata:
labels:
app: nginx-app
spec:
containers:
- image: nginx:latest
imagePullPolicy: Always
name: nginx
resources:
limits:
memory: 128Mi
requests:
cpu: 200m
memory: 128Mi
dnsPolicy: ClusterFirst
restartPolicy: Always

查询构建的nginx服务,可以看到Cluster IP是None

1
2
3
4
5
6
kubectl get service --all-namespaces=true
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 36h
default nginx-app ClusterIP None <none> 80/TCP 43s
kube-system kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP 36h
kube-system kubernetes-dashboard NodePort 10.96.197.202 <none> 443:31234/TCP 27h

查询部署的Pod信息,

1
2
3
4
 kubectl get pods -n default -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-app-7d77b84f86-rsmrp 1/1 Running 0 2m1s 10.244.3.30 node03.kubernetes.io <none> <none>
nginx-app-7d77b84f86-snx4p 1/1 Running 0 2m1s 10.244.2.19 node02.kubernetes.io <none> <none>
1
dig @10.96.0.10 nginx-app.default.svc.cluster.local

dig-nginx

这类Headless Service资源,一般通过Ingress Controller进行负载,

  1. 服务暴露

Service的IP地址仅在集群内可达,然而有些服务需要暴露外部网络。此时需要在集群边缘为其添加一层转发机制,以实现将外部请求流量接入到集群Service资源上,

Kubernetes的Service共有四种类型, ClusterIP、NodePort、LoadBalancer和ExternalName:

  • ClusterIP Service:使用iptables模式,集群内部的源IP会保留(不做SNAT)。如果client和server pod在同一个Node上,那源IP就是client pod的IP地址;如果在不同的Node上,源IP则取决于网络插件是如何处理的,比如使用flannel时,源IP是node flannel IP地址。
  • NodePort Service:源IP会做SNAT,server pod看到的源IP是Node IP。为了避免这种情况,可以给service加上annotation service.beta.kubernetes.io/external-traffic=OnlyLocal,让service只代理本地endpoint的请求(如果没有本地endpoint则直接丢包),从而保留源IP。
  • LoadBalancer Service:源IP会做SNAT,server pod看到的源IP是Node IP。在GKE/GCE中,添加annotation service.beta.kubernetes.io/external-traffic=OnlyLocal后可以自动从负载均衡器中删除没有本地endpoint的Node。
  • ExternalName:主机名被DNS服务解析至CNAME类型的记录。它并不是由Kubernetes集群提供的服务,而是把集群外部的某服务以DNS CNAME记录的方式映射到集群。因此这种类型的Service没有ClusterIP、NodePort、label、也不会有endpoint。

为了减缓IP地址空间枯竭问题,NAT被引入提出,主要有两种方式,

SNAT,源地址转换,Source Network Address Translation,在NAT路由中,将ipv4的源地址转换为公网可访问的IP
DNAT,目标地址转换,Destination Network Address Translation,在NAT路由中,将ipv4的目标地址转换为私有网络的可访问IP

  1. Ingress Controller

Service虽然解决了服务发现和负载均衡的问题,但它在使用上还是有一些限制,比如

  • 只支持4层负载均衡,没有7层功能
  • 对外访问的时候,NodePort类型需要在外部搭建额外的负载均衡,而LoadBalancer要求Kubernetes必须跑在支持的cloud provider上面

Ingress就是为了解决这些限制而引入的新资源,主要用来将服务暴露到cluster外面,并且可以自定义服务的访问策略。比如想要通过负载均衡器实现不同子域名到不同服务的访问:

1
2
3
foo.bar.com --|                |-> foo.bar.com s1:80
| 178.91.123.132 |
bar.foo.com --| |-> bar.foo.com s2:80

可以这样来定义Ingress:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nginx-ingress
annotations:
ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /nginx
backend:
serviceName: nginx-app
servicePort: 80

注意Ingress本身并不会自动创建负载均衡器,cluster中需要运行一个ingress controller来根据Ingress的定义来管理负载均衡器。目前设计提供了nginx和gce的参考实现。

Kubernetes中,Service资源和Pod资源的IP地址仅能用于集群网络内部的通信,所有的网络流量都无法穿透边界路由器(Edge Router)以实现集群内外通信。即使Service中使用NodePort或LoadBalancer通过节点引入外部流量,它依然是4层流量转发,可用的负载均衡器也为传输层负载均衡机制。

Ingress是Kubernetes API的标准资源类型之一,Ingress控制器可以由任何具有反向代理(HTTP/HTTPS)功能的服务程序实现,例如Nginx、Envoy、HAProxy、Vulcand和Traefik等。

Traefik提供了易用的Ingress Controller,使用方法见https://docs.traefik.io/user-guide/kubernetes/

不管怎样,你都需要先安装Ingress Controller,

首先确保nginx-ingress是否部署,

1
2
3
helm list
NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
rolling-rattlesnake 1 Wed Feb 6 05:09:06 2019 DEPLOYED nginx-ingress-0.9.5 0.10.2 default

如果没有安装,部署一份,

1
kubectl create -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml

另外还需要手动创建一份Service为其创建相关的NodePort或LoadBalancer,并明确指定端口和IP地址,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Service
metadata:
name: nginx-ingress-controller
namespace: ingress-nginx
spec:
selector:
app.kubernetes.io/name: ingress-nginx
ports:
- port: 80
name: http
nodePort: 30080
- port: 443
name: https
nodePort: 30443
clusterIP: 10.99.99.99
type: NodePort

这是因为Ingress规则需要由一个Service资源对象辅助识别相关的所有Pod对象,但ingress-nginx控制器可经由host规则的定义直接将流量调度,

下面动手实践一下,部署两份Ingress,负载到这个nginx-ingress-controller下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: tomcat-deploy
namespace: testing
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: tomcat.kubernetes.io
http:
paths:
- backend:
serviceName: tomcat-svc
servicePort: 80
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nginx-ingress
namespace: default
labels:
app: nginx-app
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: nginx.kubernetes.io
http:
paths:
- backend:
serviceName: nginx-app
servicePort: 80

通常情况下,servcie和pod仅可在集群内部网络中通过IP地址访问。所有到达边界路由器的流量或被丢弃或被转发到其它地方。从概念上讲,可能是,

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

Ingress是授权入站连接到达集群服务的规则集合,

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

Ingress控制器自身也是运行于集群中的Pod资源对象,它与被代理的运行为Pod资源的应用运行在同一网络中,

在实际使用中,在集群之外应该存在一个用于调度用户请求至个节点上Ingress控制器相关的NodePort的负载均衡器。如果不具有LBaaS的使用条件,用户也可以基于Nginx、Haproxy、LVS等手动构建,并通过Keepalived等解决方案实现其服务的高可用配置。

nginx-lb

  1. Service Load Balancer

在Ingress出现以前,Service Load Blancer是推荐的解决Service局限性的方式。Service Load Balancer将haproxy跑在容器中,并监控service和endpoint的变化,通过容器IP对外提供4层和7层负载均衡服务。

社区提供的Service Load Balancer支持四种负载均衡协议:TCP、HTTP、HTTPS和SSL TERMINATION,并支持ACL访问控制。

  1. Custom Load Balancer

虽然Kubernetes提供了丰富的负载均衡机制,但在实际使用的时候,还是会碰到一些复杂的场景是它不能支持的,比如

  • 接入已有的负载均衡设备
  • 多租户网络情况下,容器网络和主机网络是隔离的,这样kube-proxy就不能正常工作

这个时候可以自定义组件,并代替kube-proxy来做负载均衡。基本的思路是监控Kubernetes中Service和endpoint的变化,并根据这些变化来配置负载均衡器。比如weave flux、nginx plus、kube2haproxy等。