本文档记录,k8s如何暴露公网服务以及负载均衡

主要涉及两个知识点:k8s service、k8s ingress controller

对比传统单体垂直的架构,可以使用k8s service、k8s ingress controller 进行替代HAproxy+Nginx

K8s Service

基本每次创建k8s pod最好都提前定义好service对象,其实service的概念可以同等于CMDB中的服务组的概念。

直接通过Pod的IP地址+端口可以访问到容器应用,但是Pod的IP是动态的,当发生故障时k8s会重新调度,这时候Service服务自发现的作用就体现出来了。

其实这个概念和微服务非常类似。一个Serivce下面包含的Pod集合一般是由Label Selector来决定的。

要想外部可以访问创建的应用,就使用service吧。

K8S暴露服务的方法有3种:

  • ClusterIP:集群内可访问,但外部不可访问
  • NodePort:通过NodeIP:NodePort方式可以在集群内访问,结合EIP或者云服务VPC负载均衡也可在集群外访问,但开放NodePort一方面不安全,另一方面随着应用的增多不方便管理
  • LoadBalancer:某些云服务提供商会直接提供LoadBalancer模式,将服务对接到负载均衡,其原理是基于kubernetes的controller做二次开发,并集成到K8S集群,使得集群可以与云服务SDK交互

我们也了解到Pod的生命是有限的,死亡过后不会复活了。我们后面学习到的RC和Deployment可以用来动态的创建和销毁Pod。尽管每个Pod都有自己的IP地址,但是如果Pod重新启动了的话那么他的IP很有可能也就变化了。这就会带来一个问题:比如我们有一些后端的Pod的集合为集群中的其他前端的Pod集合提供API服务,如果我们在前端的Pod中把所有的这些后端的Pod的地址都写死,然后去某种方式去访问其中一个Pod的服务,这样看上去是可以工作的,对吧?但是如果这个Pod挂掉了,然后重新启动起来了,是不是IP地址非常有可能就变了,这个时候前端就极大可能访问不到后端的服务了。

遇到这样的问题该怎么解决呢?在没有使用Kubernetes之前,我相信可能很多同学都遇到过这样的问题,不一定是IP变化的问题,比如我们在部署一个WEB服务的时候,前端一般部署一个Nginx作为服务的入口,然后Nginx后面肯定就是挂载的这个服务的大量后端,很早以前我们可能是去手动更改Nginx配置中的upstream选项,来动态改变提供服务的数量,到后面出现了一些服务发现的工具,比如Consul、ZooKeeper还有我们熟悉的etcd等工具,有了这些工具过后我们就可以只需要把我们的服务注册到这些服务发现中心去就可以,然后让这些工具动态的去更新Nginx的配置就可以了,我们完全不用去手工的操作了。

在k8s体系中”有三种IP”

  • Node IP: Node节点的IP
  • Pod IP:Pod的IP
  • Cluster IP:service的IP

k8s 提供两种负载分发策略,RoundRobin、SessionAffinity(默认是RoundRobin)

集群外部访问pod或service

将容器应用端口映射到物理机

  • 通过设置容器级别的hostport
  • 设置pod级别的HostNetwork=true

将Service的端口映射到物理机(type=NodePort)

Service分类:

名称 说明
ClusterIP 只在集群内部可达的私网地址,集群外部无法访问
NodePort 通过节点端口映射使得集群外部可达,是在ClusterIP基础上添加一层节点端口映射,访问路径:集群外客户端–>节点ip:port–>ClusterIP:port–>PodIP:port
LoadBalancer 如果客户端只访问一个节点,会对该节点造成很大压力,一般在NodePort之前加入负载均衡,加入负载均衡器有两种方式:一是使用公有云的LBaaS,二是自己搭建负载均衡器
ExternalName 当Pod客户端想访问集群之外的服务时,使用前几种类型Service是无法绕过集群直接访问外部服务的,使用ExternalName类型的主机名或域名映射方式可以和集群外通信
HeadLess 特殊类型Service(无头服务),直接把服务名称映射到Pod的Ip上

默认创建的类型是:ClusterIP

kubernetes网络分为三类:node network、pod network、cluster network,前两种是真实存在的,而cluster network是虚拟的,仅用于Service资源。

发布服务 —— 服务类型

对一些应用(如 Frontend)的某些部分,可能希望通过外部(Kubernetes 集群外部)IP 地址暴露 Service。

Kubernetes ServiceTypes 允许指定一个需要的类型的 Service,默认是 ClusterIP 类型。

Type 的取值以及行为如下:

  • ClusterIP:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的 ServiceType
  • NodePort:通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 <NodeIP>:<NodePort>,可以从集群的外部访问一个 NodePort 服务。
  • LoadBalancer:使用云提供商的负载均衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务。
  • ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。 没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。

服务发现

Kubernetes 支持2种基本的服务发现模式 —— 环境变量和 DNS。

环境变量

Pod 运行在 Node 上,kubelet 会为每个活跃的 Service 添加一组环境变量。 它同时支持 Docker links 兼容 变量(查看 makeLinkVariables)、简单的 {SVCNAME}_SERVICE_HOST{SVCNAME}_SERVICE_PORT 变量,这里 Service 的名称需大写,横线被转换成下划线。

举个例子,一个名称为 "redis-master" 的 Service 暴露了 TCP 端口 6379,同时给它分配了 Cluster IP 地址 10.0.0.11,这个 Service 生成了如下环境变量:

1
2
3
4
5
6
7
REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11

这意味着需要有顺序的要求 —— Pod 想要访问的任何 Service 必须在 Pod 自己之前被创建,否则这些环境变量就不会被赋值。DNS 并没有这个限制

DNS

一个可选(尽管强烈推荐)集群插件 是 DNS 服务器。 DNS 服务器监视着创建新 Service 的 Kubernetes API,从而为每一个 Service 创建一组 DNS 记录。 如果整个集群的 DNS 一直被启用,那么所有的 Pod应该能够自动对 Service 进行名称解析。

例如,有一个名称为 "my-service"Service,它在 Kubernetes 集群中名为 "my-ns"Namespace 中,为 "my-service.my-ns" 创建了一条 DNS 记录。 在名称为 "my-ns"Namespace中的 Pod 应该能够简单地通过名称查询找到 "my-service"。 在另一个 Namespace 中的 Pod 必须限定名称为 "my-service.my-ns"。 这些名称查询的结果是 Cluster IP。

Kubernetes 也支持对端口名称的 DNS SRV(Service)记录。 如果名称为 "my-service.my-ns"Service 有一个名为 "http"TCP 端口,可以对 "_http._tcp.my-service.my-ns" 执行 DNS SRV 查询,得到 "http" 的端口号。

Kubernetes DNS 服务器是唯一的一种能够访问 ExternalName 类型的 Service 的方式。 更多信息可以查看 DNS Pod 和 Service

创建ClusterIP类型的Service资源

创建ClusterIP类型的Service资源配置文件service-clusterIP-demo.yml,这个Service资源关联使用Deployment创建的3个Pod,内容如下:

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
# ---------------------- Deployment资源 ----------------------
apiVersion: apps/v1
kind: Deployment

metadata:
name: nginx-dm

# 下面是ReplicaSet的spec
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
name: nginx-pod
# 注意:这里标签一定要包括上面自定义的selector标签
labels:
app: nginx
tier: frontend
# 下面就是pod的spec
spec:
containers:
- name: nginx
image: nginx:1.15.2
imagePullPolicy: IfNotPresent
ports:
- name: nginx
containerPort: 80

# 资源定义分割符
---

# ---------------------- Service资源 ----------------------
apiVersion: v1
kind: Service

metadata:
name: nginx-svc

spec:
# 这里选择器要关联到哪些Pod资源
selector:
app: nginx
tier: frontend
type: ClusterIP
ports:
- name: nginx-ports
port: 80
targetPort: 80
1
2
3
4
5
6
7
8
9
# 查看select是否匹配
kubectl get deployments -o wide
kubectl get pod --show-labels -o wide
kubectl get service -o wide

# 判断是否关联成功,从获取service中得到虚拟ip地址10.106.176.161,
curl 10.106.176.161

# service资源内置负载均衡器,每次访问都会随机访问到3个nginx服务中的一个。

创建 NodePort类型的Service资源

NodePort类型是ClusterIp类型的增强版,是外部网络打通集群边界的一种方式。客户端访问流程:集群外的客户端 –> NodeIP:NodePort –> ClusterIP:servicePort –> PortIP:containerPort。

创建NodePort类型的Service资源配置文件service-notePort-demo.yml,这个Service资源关联使用Deployment创建的3个Pod,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Service

metadata:
name: nginx-svc-np

spec:
# 这里选择器要关联到哪些Pod资源
selector:
app: nginx
tier: frontend
type: NodePort
ports:
- name: nginx-ports
port: 80
targetPort: 80
# 确保节点端口没被使用过,也可不指定,让系统分配随机端口
nodePort: 30080

测试在集群外部浏览器 http://<集群节点ip>:30080 是否可以访问。

集群外部访问服务

Service 的 ClusterIP 是 Kubernetes 内部的虚拟 IP 地址,无法直接从外部直接访问。但如果需要从外部访问这些服务该怎么办呢,有多种方法

  • 使用 NodePort 服务在每台机器上绑定一个端口,这样就可以通过 <NodeIP>:NodePort 来访问该服务。
  • 使用 LoadBalancer 服务借助 Cloud Provider 创建一个外部的负载均衡器,并将请求转发到 <NodeIP>:NodePort。该方法仅适用于运行在云平台之中的 Kubernetes 集群。对于物理机部署的集群,可以使用 MetalLB 实现类似的功能。
  • 使用 Ingress Controller 在 Service 之上创建 L7 负载均衡并对外开放。

K8s Ingress Controller

Service属于四层网络模型,是在网络协议TCP或UDP之上,而http或https属于七层网络模型,无法使用Service来解析七层的http或https,为了解决这个问题,在节点运行一个代理服务的Pod,这个Pod里的容器共享宿主机网络(host network),外部可以直接访问,并且运行属于七层网络模型代理服务(Nginx、Traefik、Envoy),所有的客户端的请求都先经过代理服务的Pod,代理服务Pod把请求转发给其他Pod,而且各个Pod之间是同一个网段,可以直接通信。为了解决代理服务Pod单点问题,使用DaemonSet类型的Pod控制器,DaemonSet控制的Pod会在每个node节点运行一个代理服务Pod,如果集群的节点很多的话,选择3个节点专门用来运行服务代理即可,不必每个节点都运行代理服务。

这种代理服务的Pod属于ingress controller,ingress controller是独立于集群master中manager controller。而ingress是集群的一种特殊资源,定义ingress资源时需要指明ingress controller的前端是虚拟主机或url映射。集群中运行很多组的pod(例如:用户、电商、社交、交易、物流等),ingress controller怎么知道哪些请求是指向哪组Pod?可以使用虚拟主机或url映射(/usr/、/shop/等),因为Pod是有生命周期的,Pod的Ip地址有可能随时改变,ingress controller经过url映射的Pod就保证存在呢?解决方法是借助headless ClusterIp类型的Service资源,Service不是用来代理,只用来对Pod进行分组,ingress实时监控Service中Pod的ip地址是否变化,如果有变化,ingress实时获取到变化后Pod的ip信息,然后注入到ingress controller的配置文件中,并触发ingress controller中容器进程重载配置文件。

而ingress controller目前有几种:

  • k8s社区提供的ingress
  • nginx社区提供的ingress
  • Traefik

k8s-nginx-ingress 、nginxinc-ingress两者之间的区别

https://github.com/nginxinc/kubernetes-ingress/blob/master/docs/nginx-ingress-controllers.md

Ingress 架构图

Ingress-Nginx实现

参考文档:

https://kubernetes.github.io/ingress-nginx/deploy/

1
2
kubectl create -f /etc/ansible/manifests/ingress/nginx-ingress/nginx-ingress.yaml # 创建ingress
kubectl create -f /etc/ansible/manifests/ingress/nginx-ingress/nginx-ingress-svc.yaml # 创建ingress-svc
1
2
3
4
5
# 查看ingress-nginx各类资源列表
kubectl get ns | grep ingress-nginx
kubectl get cm -n ingress-nginx
kubectl get deployment -n ingress-nginx
kubectl get pod -n ingress-nginx

添加外网接入的service资源

在本地测试为了接入集群外的请求,需要另外创建一个NodePort类型service做转发,如果是在公有云部署的集群,建议使用LoadBalancer类型service,参考service

创建NodePort类型的service资源配置文件ingress-nginx-serivce.yml,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
YAMLapiVersion: v1
kind: Service
metadata:
name: ingress-nginx
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
type: NodePort
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
ports:
- name: http
port: 80
targetPort: 80
# 指定宿主机端口
nodePort: 30080
- name: https
port: 443
targetPort: 443
# 指定宿主机端口
nodePort: 30443

查看调度器nginx ingress controller是否工作正常:

1
2
3
4
# 所有从集群外进来的http使用30080,https使用30443
curl mater:30080

# 如果返回default backend - 404,说明调度器已经正常工作了,只是没有后端而已

ingress nginx代理http示例

开始部署app,创建headless类型的service资源和Deployment资源配置文件myapp-service-deployment.yml,其中service资源只用来对Deployment的Pod进行分组的,获取到Pod的ip给ingress资源使用,内容如下:

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
YAMLapiVersion: v1
kind: Service
metadata:
name: myapp-svc
spec:
selector:
app: myapp
clusterIP: None
ports:
- name: http
port: 80
targetPort: 80

---

apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-dm
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
name: myapp
labels:
app: myapp
spec:
containers:
- name: myapp
image: ikubernetes/myapp:v3
ports:
- name: http
containerPort: 80

创建ingress资源配置文件http-myapp-ingress.yml,ingress会监听service服务myapp-svc,一旦发现myapp-svc发生改变,ingress会从myapp-svc获取最新的Pod信息,把最新配置信息注入到nginx配置信息。内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
YAMLapiVersion: extensions/v1beta1
kind: Ingress
metadata:
name:
annotations:
# 声明使用类型:nginx、traefik、envoy,让ingress controller匹配对应的规则
kubernetes.io/ingress.class: "nginx"
spec:
rules:
# 这里使用虚拟主机,也可以使用url映射
- host: demo.myapp.com
http:
paths:
- path:
backend:
serviceName: myapp-svc
servicePort: 80

测试ingress是否能够动态注入nginx.conf,并触发nginx重载配置信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BASH# 获取ingress nginx的pod名称
kubectl get pod -n ingress-nginx

# 进入容器
kubectl exec -it -n ingress-nginx nginx-ingress-controller-6bd7c597cb-tf7gx -- /bin/sh

# 查看nginx配置是否包含虚拟主机demo.myapp.com配置信息,Deployment类型资源myapp-dm下的pod的ip列表是否也存在
cat nginx.conf

# 如果后端的Pod挂掉了,Pod的ip地址变化会被ingress通过service检测到,ingress把变化的信息重新注入到nginx配置文件中。

# 在节点宿主机添加测试域名demo.myapp.com
echo '192.168.8.90 demo.myapp.com' > /etc/hosts

# 在一个终端不断获取myapp名称,查看删除pod后获取的名称是否有更新
while true; do curl demo.myapp.com:30080/hostname.html; sleep 1;done

# 获取pod
kubectl get pod

# 删除两个pod
kubectl delete pod myapp-dm-59f7c855f7-5tmhw myapp-dm-59f7c855f7-5v6zw

# 从结果上看,pod后获取的名称是更新的,说明ingress能把最新pod的信息动态注入nginx配置中,并触发nginx服务重载配置。

创建成功后,Ingress会将信息注入到ingress-controller里面去,即:会自动转换为nginx的配置文件,可以看到pod内的nginx.conf文件确实被注入了myapp相关的信息。此时,在本地机器绑定myapp.test.com为两个node的ip,就可以在本地机器的浏览器中访问 http://myapp.test.com:30080/ 网页了。

配置阿里云SLB,添加nginx-ingress组,将带有nginx-ingress的node节点添加至阿里云SLB

traefik ingress实现

部署 traefik ingress-controller

1
kubectl create -f /etc/ansible/manifests/ingress/traefik/traefik-ingress.yaml

验证 traefik ingress-controller

1
2
3
4
5
6
7
8
# kubectl get deploy -n kube-system traefik-ingress-controller
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
traefik-ingress-controller 1 1 1 1 4m

# kubectl get svc -n kube-system traefik-ingress-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
traefik-ingress-service NodePort 10.68.69.170 <none> 80:23456/TCP,8080:34815/TCP 4m
可以看到traefik-ingress-service 服务端口80暴露的nodePort确实为23456

测试ingress

首先创建测试用K8S应用,并且该应用服务不用nodePort暴露,而是用ingress方式让外部访问

1
2
3
4
5
6
7
8
kubectl run test-hello --image=nginx --expose --port=80
##
# kubectl get deploy test-hello
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
test-hello 1 1 1 1 56s
# kubectl get svc test-hello
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
test-hello ClusterIP 10.68.124.115 <none> 80/TCP 1m

然后为这个应用创建 ingress对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kubectl create -f /etc/ansible/manifests/ingress/test-hello.ing.yaml

# test-hello.ing.yaml内容
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test-hello
spec:
rules:
- host: hello.test.com
http:
paths:
- path: /
backend:
serviceName: test-hello
servicePort: 80

集群内部尝试访问: curl -H Host:hello.test.com 10.68.69.170(traefik-ingress-service的服务地址) 能够看到欢迎页面 Welcome to nginx!;

在集群外部尝试访问(假定集群一个NodeIP为 192.168.1.1): curl -H Host:hello.test.com 192.168.1.1:23456,也能够看到欢迎页面 Welcome to nginx!,说明ingress测试成功

traefik WEB 管理页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# traefik-ui.ing.yaml内容
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: traefik-web-ui
namespace: kube-system
spec:
rules:
- host: traefik-ui.test.com
http:
paths:
- path: /
backend:
serviceName: traefik-ingress-service
servicePort: 8080

参考资料