[toc]

0x00 前言简述

Service - 服务介绍

描述: K8s中的Service实际上是微服务框架中的微服务,Service定义了一个服务的访问入口,可以通过该入口访问其背后一组的有Pod副本组成的集群实例;

Q: 什么是Service服务?

答: kubernetes 通过Labels(标签)选择的方式来匹配一组pod,然后提供对外访问的一种机制,一组pod可以对应到多个svc的, 每一个service(svc)都可以理解为一个微服务

Service有且只有一个算法 RB 轮询, 它能够提供负载均衡的能力但是在使用上有以下限制:

  • 提供4层负载均衡能力【只能基于ip地址和端口进行转发】
  • 提供7层功能【不能通过主机名及域名的方案去进行负载均衡】,但有时我们可能需要更多的匹配规则来转发请求,这点上4层负载均衡是不支持的;
WeiyiGeek.Service服务连接图

WeiyiGeek.Service服务连接图


Q: Service 如何与后端Pod关联?

答: SVC 与其后端的Pod多个副本集群通过Lable Selector进行关联(采用等式或者集合进行过滤), 而构建这些Pod的RS、Deployment、StatefulSet等控制器是保证Pod数量满足预选定义值;

关联图示如下:

1
2
3
frontend pod -> Service(SVC) ->                   -> Pod `lable:app=backend`
Label Selector -> pod `lable:app=backend`
Controller -> -> pod `lable:app=backend`


Q: 微服务给我们带来来什么好处?

答: 复杂的系统微服务服务化,由多个提供不同业务服务而彼此独立的微服务(Kubernetes Service)组成,服务之间通过TCP/IP协议进行通信,便拥有了强大的分布式、水平弹性扩展能力;


Q: 什么是EndPoint?

答: 它实际上就是我们所说的端点,默认情况下每个Pod将提供一个独立端点供SVC或者其它Pod进行访问,而端点实际上就是Pod IP + Container Port的组合;

1
2
3
4
5
$ kubectl get ep -o wide
NAME ENDPOINTS AGE
deploy-blog-svc 10.244.0.209:80,10.244.1.196:80,10.244.2.99:80 # 对应则三个Pod 34d
deploy-maven-svc 10.244.0.236:8080,10.244.1.221:8080,10.244.2.124:8080 32d
kubernetes 10.10.107.202:6443 83d


Q: 采用Lable关联Controller创建的Pod后如何进行EndPoint访问?

答: 传统做法是前端部署一个负载均衡器(比如Nginx)并为该组开放一个对外端口例如8080,并将这些Pod的EndPoint转发到Nginx上,之后客户端将可以通过Nginx负载均衡(LB)对外的IP和Port来访问服务, 而对于Client的请求最终会被分发到那个Pod是由负载均衡器算法决定的;

1
2
3
4
5
6
7
8
9
$ kubectl get pod -o wide --show-labels
NAME READY STATUS AGE IP NODE LABELS # 关键点
deploy-blog-html-0 1/1 Running 34d 10.244.0.209 weiyigeek-ubuntu app=blog-html,controller-revision-hash=deploy-blog-html-b56f6cf65,release=stabel,statefulset.kubernetes.io/pod-name=deploy-blog-html-0
deploy-blog-html-1 1/1 Running 34d 10.244.1.196 k8s-node-4 app=blog-html,controller-revision-hash=deploy-blog-html-b56f6cf65,release=stabel,statefulset.kubernetes.io/pod-name=deploy-blog-html-1
deploy-blog-html-2 1/1 Running 34d 10.244.2.99 k8s-node-5 app=blog-html,controller-revision-hash=deploy-blog-html-b56f6cf65,release=stabel,statefulset.kubernetes.io/pod-name=deploy-blog-html-2

$ kubectl get svc -o wide --show-labels
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR # 关键点 LABELS
deploy-blog-svc NodePort 10.104.74.36 <none> 80:30088/TCP 34d app=blog-html,release=stabel <none>


Q: k8S 如何实现负载均衡?

答: 在K8s架构中我们提到过Kube-Proxy组件它实际上实现了一个软件负载均衡器的作用,负责把Service的请求转发到某一个具体后端Pod实例上,并且在内部实现了回话保持的机制;

Tips : Pod 的EndPoint点可能会随着Pod的销毁或重新创建而发生改变,K8s为每个Service都分配了一个Cluster IP的全局虚拟IP,一旦Service被创建其生命周期内该IP会一直保持不变, 所以只要Service Name 与 Service 的 Cluster IP 做一个DNS域名映射就可以轻松的解决服务发现的问题;

0x01 服务发现

描述: 谈到微服务的服务发现,那么久离不开服务发现这个棘手的问题,前面说了Service与Cluster IP关联原理,但随之而来的是K8S如何做到四层/七层服务发现的呢?

四层服务发现

描述: 四层服务发现主要有两种方式环境变量或者DNS;

环境变量-environment
说明: 在K8s早期采用了Linux环境变量的方式,即每个Service生成一些对应的Linux环境变量,并在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
kubectl exec -it deploy-blog-html-0 env
# PATH=/usr/local/nginx/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# HOSTNAME=deploy-blog-html-0
# TERM=xterm
# DEPLOY_BLOG_SVC_SERVICE_PORT=80
# DEPLOY_BLOG_SVC_PORT_80_TCP_ADDR=10.104.74.36
# KUBERNETES_SERVICE_PORT=443
# KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
# KUBERNETES_PORT_443_TCP_PROTO=tcp
# DEPLOY_BLOG_SVC_SERVICE_HOST=10.104.74.36
# DEPLOY_BLOG_SVC_PORT_80_TCP=tcp://10.104.74.36:80
# DEPLOY_BLOG_SVC_PORT_80_TCP_PORT=80
# KUBERNETES_SERVICE_PORT_HTTPS=443
# KUBERNETES_PORT=tcp://10.96.0.1:443
# DEPLOY_BLOG_SVC_PORT=tcp://10.104.74.36:80
# DEPLOY_BLOG_SVC_PORT_80_TCP_PROTO=tcp
# KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
# DEPLOY_BLOG_SVC_SERVICE_PORT_HTTP=80
# KUBERNETES_SERVICE_HOST=10.96.0.1
# KUBERNETES_PORT_443_TCP_PORT=443
# NGINX_VERSION=1.19.4
# NJS_VERSION=0.4.4
# PKG_RELEASE=1~buster
# IMAGE_VERSION=3.0
# HOME=/root

Tips : 然而采用环境变量注入的方式有很大的局限性,环境变量信息只能注入到后于该Service启动的Pod,而先启动的Pod则是查询不到后启动的Service环境变量即


DNS
描述: 鉴于环境变量的方式的局限性以及SVC的Cluster IP的可读性差等问题引入了DNS方式进行服务发现,利用Service Name做为DNS域名,应用程序或者集群中的其它服务可以通过域名+Port形式直接访问服务;

资源清单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cat > services-dns-test.yaml <<'END'
apiVersion: v1
kind: Pod
metadata:
name: busybox-dns-test
namespace: default
spec:
containers:
- name: busybox
image: busybox:latest
imagePullPolicy: "IfNotPresent"
command: ["sleep","3000"]
restartPolicy: Never
END

操作实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
~/K8s/Day13$ kubectl create -f services-dns-test.yaml
# pod/busybox-dns-test created

$ kubectl get pod -o wide | grep "dns"
# busybox-dns-test 1/1 Running 44s 10.244.0.237 weiyigeek-ubuntu

# Service
$ kubectl get svc
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# deploy-blog-svc NodePort 10.104.74.36 <none> 80:30088/TCP 34d

# 通过Nslookup命令查看前面创建的 deploy-blog-svc
~/K8s/Day13$ kubectl exec -it busybox-dns-test -- sh -c "nslookup -type=a deploy-blog-svc"
# Server: 10.244.0.185
# Address: 10.244.0.185:53

# Name: deploy-blog-svc.default.svc.cluster.local
# Address: 10.104.74.36


七层服务发现

描述: 在实际的应用场景中一定是由某些服务要暴露给用户或者集群外部访问的比如网站服务, 而此时前面所提的四层服务发现仅仅限于K8s集群内部访问, 通过需要我们的前端代理来进行实现,例如采用Nginx-Ingress 或者 Traefik-Ingress进行实现;

Q: K8s集群 Pod 间互访的原理

答: Pod IP 是由Docker Daemon(Docker Engine) 根据 docker0 网桥IP地址分配的或者是网络插件Flannel实现的, 即Pod间的通信是通过Pod IP所在虚拟二层网络通信的,而真实的TCP/IP流量是通过Node的网卡流出;

1
2
3
4
5
6
7
8
9
10
11
12
$ ip addr | grep flannel -B 3
# 6: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
# link/ether 2e:d6:4c:68:a3:24 brd ff:ff:ff:ff:ff:ff
# inet 10.244.0.0/32 brd 10.244.0.0 scope global flannel.1

# 7: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default qlen 1000
# link/ether 2e:5d:21:a6:1e:c2 brd ff:ff:ff:ff:ff:ff
# inet 10.244.0.1/24 brd 10.244.0.255 scope global cni0

kubectl get pod -o wide
# NAME READY STATUS AGE IP # 关键点 NODE
# busybox-dns-test 0/1 Completed 119m 10.244.0.237 weiyigeek-ubuntu

Tips: Service 的 Cluster IP 与 Pod IP 类似属于集群的内部地址属于虚拟IP,无法直接被其他K8S集群所访问;
Tips: Cluster IP 是通过集群的内部IP暴露服务,选择该值服务只能够在集群内部可以访问即默认为ServiceType = ClusterIP


Tips : 下面是几个常用于外部访问内部服务的方式:

  • NodePort : 采用Kube-Proxy组件每个节点将为其Service开放一个30000~32000的外部端口
  • LoadBalancer : 支持使用外部负载均衡的云提供商服务比如GCE或者AWS, lB是异步创建其信息将会通过Service的status.loadBalance字段发布出去;
  • ExternalName : 通过返回CNAME 和 其值,可以将服务映射到ExternalName字段内例如foo.example.com(注意没有任何可u下代理被创建/只有Kubernetes V1.7或者更高版本的Kube-DNS支持)


代理实现原理

描述: 访问k8s集群中创建的内部Pod端口流程示意图, 其中Pod中的容器端口需要加入到EndPoints端点控制器里面;

WeiyiGeek.访问内部Pod端口流程示意图

WeiyiGeek.访问内部Pod端口流程示意图

作用解析:

  • apiServer: 监听服务和端点通过kube-proxy去监控,以及通过监控kube-proxy去实现服务端点信息的发现;
  • kube-proxy: 通过选择标签去监控对应的pod并写入到iptable规则里面去;
  • client: 访问服务时通过iptables中的规则被定向到pod的地址信息(客户端访问pod是通过iptables去实现的);
  • iptables : 规则是通过kube-proxy去写入的;


k8s代理模式的分类
描述: 在Kubernetes集群中,每个Node 运行一个kube-proxy 进程。kube-proxy负责为service 实现了一种VIP(虚拟IP)的形式【可以在集群内部直接访问】,而不是ExternalName【返回集群外部的地址信息】 的形式。

  • 在Kubernetes v1.0 版本,代理完全由userspace实现。
  • 在Kubernetes v1.1 版本,新增了iptables代理,但并不是默认的运行模式。
  • 在Kubernetes v1.2 版本起,默认就是 iptables 代理。
  • 在Kubernetes v1.14 版本起,默认使用 ipvs 代理但是缺省还是IPtables。


Userspace - 代理模式
描述: 客户端首先访问 iptables,然后通过 iptables 访问到 kube-proxy 之后访问到具体的pod上,同时kube-apiserver也会监控kube-proxy服务更新及端点的维护;
从上面描述过程中我们知道每次访问的时候都需要 Kube-proxy 进行一次代理, 这会导致 kube-proxy 压力是非常大的;

WeiyiGeek.userspace-Proxy模式

WeiyiGeek.userspace-Proxy模式


IPtables - 代理模式
描述: 它在Userspace代理模式下进行改变,即所有的访问直接通过IPtables而不需要Kube-Proxy去调度访问; 此时 kube-apiserver 依然通过监控kube-proxy去实现iptables的端口的更新维护

优点: 访问速度大大增加以及Kube-Proxy稳定性会提高,并且承受的压力将会减少很多;
缺点: 性能方面还有待提高;

WeiyiGeek.IPtables-Proxy模式

WeiyiGeek.IPtables-Proxy模式


IPvs - 代理模式
描述: IPVS模式实际是将iptables代理模式中iptables变更为ipvs, 即把原本是通过iptables进行服务定向转发变成了通过IPVS模块去实现负载均衡以及流量导向,其他的方面与IPtables代理模式相同所有的访问也是不经过Kube-Proxy的;
该模式 kube-proxy 会监视 Kubernetes service 对象和 Endpoints,调用netlink 接口以相应地创建ipvs 规则并定期与Kubernetes service 对象和 Endpoints 对象同步 ipvs规则,以确保ipvs状态与期望一致。当访问服务时流量将被重定向到其中一个后端 Pod;

优点:ipvs基于Netfilter的hook功能,在内核空间中使用哈希表作为底层数据结构,使得 ipvs 可以更快地重定向流量,并且在同步代理规则时具有更好的性能,此外ipvs为负载均衡算法提供了更多的选项;

PS : 如果操作系统没有提前预安装ipvs模块以及其依赖需求不满足时,K8S将会默认使用iptables的代理模式;

假定在运行kube-proxy之前在节点上都已经安装了IPVS内核模块。
例如当 kube-proxy 以ipvs代理模式启动时,kube-proxy将验证节点上是否安装了IPVS模块,如果未安装则kube-proxy将回退到iptables代理模式

IPVS 负载均衡算法:

  • ·rr:轮询调度
  • ·1c:最小连接数
  • ·dh:目标哈希
  • ·sh:源哈希
  • ·sed: 最短期望延迟
  • ·nq:不排队调度
WeiyiGeek.ipvs-Proxy模式

WeiyiGeek.ipvs-Proxy模式


负载均衡

  • 在Kubernetes v1.0 版本,service 是“4层”(TCP/UDP over IP)即只能通过主机和端口进行负载概念。
  • 在Kubernetes v1.1 版本,新增了IngressAPI(beta版),用来表示“7层”(HTTP)服务!可以进行7层的负载均衡。正是因为有了Ingress的API接口,我们才有了7层调度的功能。

Q: 为何不使用 round-robin DNS?
答: k8s不管是历史还是现在都没有使用过DNS,不采用DNS负载均衡集群,最大最有意义的一点就是DNS会在很多的客户端里进行缓存,很对服务访问DNS进行域名解析的时候解析完成以后得到地址以后很多的服务都不会对DNS的缓存进行清除,也就意味着只要缓存存在服务在下次访问的时候还是这个地址信息,因此也就达不到我们负载均衡的要求了,因此DNS一般仅仅作为负载均衡的一种辅助手段;


Tips : 注意此处的四与七层不是OSI模型中的概念而是负载均衡中的概念, 你可以简单通过以下两个例子进行了解;

  • 四层负载均衡原理: 在接受到客户端请求后,通过修改数据包得目的/源地址信息与端口号(ip+端口号)将流量转发到应用服务器;
  • 七层负载均衡原理: 可以对同一个Web服务器进行负载,它除了根据IP加端口进行负载外,还可根据http协议中的URL/浏览器类别/语言来决定是否要进行负载均衡;

0x02 服务发现类型

描述: 在K8s集群中Service服务发现方式有以下四种类型ServiceType;

  • 1) ClusterIP : 默认类型,自动分配一个仅Cluster内部可以访问的虚拟IP(常常由 flannel/Calico 网络插件进行管理)【service创建一个仅集群内部可访问的ip,集群内部其他的pod可以通过该服务访问到其监控下的pod】

  • 2) NodePort :在ClusterlP基础上为Service在每台机器上绑定一个端口,这样就可以通过 NodePort来访问该服务【在service及各个node节点上开启端口,外部的应用程序或客户端访问node的端口将会转发到service的端口,而service将会依据负载均衡随机将请求转发到某一个pod的端口上。一般暴露服务常用的类型】

    • 注意: 为了防止某一个Node节点down掉建议将集群中所有Node节点地址+端口都设置进入负载均衡中;
  • 3) LoadBalancer :在NodePort的基础上,借助 cloud provider 创建一个外部负载均衡器(在云主机构建K8s基础上),并将请求转发到:NodePort【在NodePort基础之上,即各个节点前加入了负载均衡器实现了真正的高可用,一般云供应商提供的k8s集群就是这种,即本身自带负载均衡器】

  • 4) ExternalName : 把集群外部的服务引入到集群内部来在集群内部直接使用。没有任何类型代理被创建,这只有kubernetes 1.7 或更高版本的kube-dns 才支持【当我们的集群服务需要访问k8s之外的集群时,可以选择这种类型,然后把外部服务的IP及端口写入到k8s服务中来,k8s的代理将会帮助我们访问到外部的集群服务】


ClusterIP - Service

描述: 它主要在每个 Node 节点使用 iptables【新版本默认是ipvs代理模式,并且笔者安装的K8s集群时也采用的IPVS模块因此此处为ipvs,代理模式不同所使用的底层方案也是不一致的】,将发向clusterlP对应端口的数据,转发到kube-proxy中。
kube-proxy自己内部实现有负载均衡的方法,并可以查询到这个service下对应pod的地址和端口,进而把数据转发给对应的pod的地址和端口;

Tips : 采用IPVS模块替代了IPtables,其实还是采用IPtables中类似于netfilter的Hook功能进行实现的;

为了实现图上的功能,主要需要以下几个组件进行协同工作:

  • (1) ApiServer : 用户通过kubectl命令向apiserver发送创建service的命令,apiserver接收到请求后将数据存储到etcd中
  • (2) Kube-proxy : 每个节点中都有一个叫做kube-porxy的进程,这个进程负责感知service,pod的变化,并将变化的信息写入本地的iptables规则中
  • (3) IPtables : 使用NAT等技术将virtuallP的流量转至endpoint中

ClusterIP 资源清单示例(注意此处采用的是IPVS负载均衡技术与Flannel网络插件):

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
cat > ClusterIP-demo.yaml <<'EOF'
# Namespace :后面的 Service 演示的资源清单都将放入在该名称空间下
kind: Namespace
apiVersion: v1
metadata:
name: service-test
labels:
keys: service-test
---
# Deployment 资源控制器
apiVersion: apps/v1
kind: Deployment
metadata:
name: clusterip-deploy
namespace: service-test
spec:
replicas: 3
selector: # 选择器
matchLabels:
app: nginx-clusterip # 匹配的Pod标签非常重要
release: stabel
template:
metadata:
labels:
app: nginx-clusterip # 模板标签
release: stabel
env: test
spec:
containers:
- name: nginx
image: harbor.weiyigeek.top/test/nginx:v2.0
imagePullPolicy: IfNotPresent
ports:
- name: http # 此端口在服务中的名称
containerPort: 80 # 容器暴露的端口
---
# Services 服务发现
apiVersion: v1
kind: Service
metadata:
name: clusterip-deploy
namespace: service-test
spec:
type: ClusterIP # Service 类型
selector:
app: nginx-clusterip # 【注意】与deployment资源控制器创建的Pod标签进行绑定;
release: stabel # Service 服务发现不能缺少Pod标签,有了Pod标签才能与之SVC对应
ports: # 映射端口
- name: http
port: 80 # cluster 访问端口
targetPort: 80 # Pod 容器内的服务端口
EOF

操作流程:

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
# (1) 资源清单的部署
~/K8s/Day6$ kubectl create -f ClusterIP-demo.yaml
# namespace/service-test created
# deployment.apps/clusterip-deploy created
# service/clusterip-deploy created

# (2) 查看资源控制器管理的pod
$ kubectl get ns | grep "service-test"
# service-test Active 5m23s
$ kubectl get deploy -n service-test -o wide --show-labels # deployment 控制器
# NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR LABELS
# clusterip-deploy 3/3 3 3 6m5s nginx harbor.weiyigeek.top/test/nginx:v2.0 app=nginx-clusterip,release=stabel <none>
$ kubectl get rs -n service-test -o wide --show-labels # 受到 deployment 资源控制器管理,但是由ReplicaSet资源控制器创建Pod
# NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR LABELS
# clusterip-deploy-76b69d5c5b 3 3 3 6m53s nginx harbor.weiyigeek.top/test/nginx:v2.0 app=nginx-clusterip,pod-template-hash=76b69d5c5b,release=stabel app=nginx-clusterip,env=test,pod-template-hash=76b69d5c5b,release=stabel
$ kubectl get pod -n service-test -o wide --show-labels # pod 相关信息
# NAME READY STATUS RESTARTS AGE IP NODE LABELS
# clusterip-deploy-76b69d5c5b-cnfrm 1/1 Running 0 8m33s 10.244.1.108 k8s-node-4 app=nginx-clusterip,env=test,pod-template-hash=76b69d5c5b,release=stabel
# clusterip-deploy-76b69d5c5b-cx8r6 1/1 Running 0 8m33s 10.244.1.107 k8s-node-4 app=nginx-clusterip,env=test,pod-template-hash=76b69d5c5b,release=stabel
# clusterip-deploy-76b69d5c5b-qkwjs 1/1 Running 0 8m33s 10.244.1.109 k8s-node-4 app=nginx-clusterip,env=test,pod-template-hash=76b69d5c5b,release=stabel

# (3) 查看 Service 资源控制器
~/K8s/Day6$ kubectl get svc -n service-test -o wide
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
# clusterip-deploy ClusterIP 10.108.129.19 <none> 80/TCP 10m app=nginx-clusterip,release=stabel

# (4) 访问 & 结果验证
~/K8s/Day6$ curl http://10.108.129.19/host.html
Hostname: clusterip-deploy-76b69d5c5b-cx8r6 <br> # 基于 IPVS 负载轮询机制(Round-Robin)
Image Version: <u> 2.0 </u>
Nginx Version: 1.19.4
~/K8s/Day6$ curl http://10.108.129.19/host.html
Hostname: clusterip-deploy-76b69d5c5b-cnfrm <br>
Image Version: <u> 2.0 </u>
Nginx Version: 1.19.4
~/K8s/Day6$ curl http://10.108.129.19/host.html
Hostname: clusterip-deploy-76b69d5c5b-qkwjs <br>
Image Version: <u> 2.0 </u>
Nginx Version: 1.19.4

$ sudo ipvsadm -Ln | grep "10.108.129.19" -A 3 # ipvs 负载均衡
# IP Virtual Server version 1.2.1 (size=4096)
# Prot LocalAddress:Port Scheduler Flags
# -> RemoteAddress:Port Forward Weight ActiveConn InActConn
# TCP 10.108.129.19:80 rr
# -> 10.244.1.107:80 Masq 1 # 权重 0 # 当前连接数 1 # 命中数
# -> 10.244.1.108:80 Masq 1 0 1
# -> 10.244.1.109:80 Masq 1 0 1


Headless - Service

描述:Headless Service 即无头服务,它也是一种特殊的Cluster IP, 当某应用不需要、不想要负载均衡(暂时不需要访问)以及单独的Service IP时;

简单的说: 即为了更好的转发性能, 我们希望可以自己控制负载均衡策略来替代K8s默认的负载策略, 或者一个应用期望知道同组服务的其他实例。

特点: 通过无头服务的方式去解决 hostname 和 portname 的变化问题也就是通过它去进行绑定;

配置: 通过指定 ClusterIP (.spec.clusterIP) 的值为 None 来创建 Headless Service。
PS : 这类 Service 并不会分配 Cluster IP 并且 kube-proxy 不会处理它们,所以平台也不会为它们进行负载均衡和路由;

Headless 资源清单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cat > headless-demo.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: myapp-headless
namespace: service-test
spec:
# type: Headless # Service 类型
clusterIP: "None" # 改变点实际上就是基于ClsterIP实现的
selector:
app: nginx-clusterip # 选择上面创建的Pod标签
release: stabel
ports:
- port: 80
targetPort: 80
EOF

操作流程:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# (1) 创建 svc 服务
~/K8s/Day6$ kubectl apply -f headless-demo.yaml
# service/myapp-headless created


# (2) 查看
~/K8s/Day6$ kubectl get svc -n service-test
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# clusterip-deploy ClusterIP 10.108.129.19 <none> 80/TCP 18h # app=nginx-clusterip,release=stabel
# myapp-headless ClusterIP None #(观察点) <none> 80/TCP 15s # app=nginx-clusterip,release=stabel
$ kubectl get pod -n service-test -o wide
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# clusterip-deploy-76b69d5c5b-cnfrm 1/1 Running 0 24h 10.244.1.108 k8s-node-4 <none> <none>
# clusterip-deploy-76b69d5c5b-cx8r6 1/1 Running 0 24h 10.244.1.107 k8s-node-4 <none> <none>
# clusterip-deploy-76b69d5c5b-qkwjs 1/1 Running 0 24h 10.244.1.109 k8s-node-4 <none> <none>

# (3) 在SVC中一旦创建成功后,他将写入到coreDNS中去,并且会有一个主机名被写入到coreDNS中;
# 写入格式 : svc的名称+命名空间的名称+当前集群的域名
~/K8s/Day6$ kubectl get pod -n kube-system -o wide | grep "coredns"
# coredns-6c76c8bb89-8cgjz 1/1 Running 1 7d18h 10.244.0.5 ubuntu <none> <none>
# coredns-6c76c8bb89-wgbs9 1/1 Running 1 7d18h 10.244.0.4 ubuntu <none> <none>


# (4) 意味着在无头服务中虽然它没有ip了,但可以通过访问域名的方案依然可以访问服务下的pod;
# 需要将 10.244.0.5 CoreDNS 设置到 /etc/resolv.conf
$ cat /etc/resolv.conf
# nameserver 127.0.0.53
# nameserver 10.244.0.5
# options edns0
# apt -y install bind-utils
dig -t A myapp-headless.service-test.svc.cluster.local. @10.244.0.5
# ;; ANSWER SECTION:
# myapp-headless.service-test.svc.cluster.local. 30 IN A 10.244.1.109 # 无头还是域名的方案进行访问
# myapp-headless.service-test.svc.cluster.local. 30 IN A 10.244.1.108
# myapp-headless.service-test.svc.cluster.local. 30 IN A 10.244.1.107

# ;; Query time: 92 msec
# ;; SERVER: 10.244.0.5#53(10.244.0.5)
# ;; WHEN: Fri Nov 13 16:14:59 CST 2020
# ;; MSG SIZE rcvd: 269


# (5) 验证
~/K8s$ curl http://myapp-headless.service-test.svc.cluster.local/host.html
# Hostname: clusterip-deploy-76b69d5c5b-cx8r6 <br>
# Image Version: <u> 2.0 </u>
# Nginx Version: 1.19.4
~/K8s$ curl http://myapp-headless.service-test.svc.cluster.local/host.html
# Hostname: clusterip-deploy-76b69d5c5b-cnfrm <br>
# Image Version: <u> 2.0 </u>
# Nginx Version: 1.19.4


# (6)可以看见访问没有走负载均衡的
$ sudo ipvsadm -Ln
# IP Virtual Server version 1.2.1 (size=4096)
# Prot LocalAddress:Port Scheduler Flags
# -> RemoteAddress:Port Forward Weight ActiveConn InActConn
# TCP 10.96.0.1:443 rr
# -> 10.10.107.202:6443 Masq 1 3 0
# TCP 10.96.0.10:53 rr
# -> 10.244.0.4:53 Masq 1 0 0
# -> 10.244.0.5:53 Masq 1 0 0
# TCP 10.96.0.10:9153 rr
# -> 10.244.0.4:9153 Masq 1 0 0
# -> 10.244.0.5:9153 Masq 1 0 0
# TCP 10.99.135.33:80 rr
# TCP 10.108.129.19:80 rr
# -> 10.244.1.107:80 Masq 1 0 0
# -> 10.244.1.108:80 Masq 1 0 0
# -> 10.244.1.109:80 Masq 1 0 0
# TCP 10.109.65.22:80 rr
# UDP 10.96.0.10:53 rr
# -> 10.244.0.4:53 Masq 1 0 0
# -> 10.244.0.5:53 Masq 1 0 0


# (7) 删除创建的SVC
~/K8s/Day6$ kubectl delete -f headless-demo.yaml


NodePort - Service

描述: nodePort的原理是在node上开放一个端口(30000~32000)间的随机端口,当客户端访问该SVC则将向该端口的流量导入到kube-proxy, 然后由 kube-proxy 进一步的根据SVC绑定的Pod标签将请求转发给对应的Pod容器;


NodePort 资源清单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cat > nodeport-demo.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: nodeport-demo
namespace: service-test
spec:
type: NodePort # 关键点: Service 类型
selector:
app: nginx-clusterip # 关键点: 利用标签绑定 Pod
release: stabel
ports:
- name: http # 此端口在服务中的名称这必须是一个DNS_LABEL所有ServiceSpec内的端口必须有唯一的名称
port: 80 # 集群访问端口
targetPort: 80 # Pod 保留的端口
nodePort: 32306 # 节点访问的端口,(注意如果不指定该字段属性) 端口范围30000~32000
protocol: TCP # 协议类型
EOF

操作流程:

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
# (1) Service 资源控制器部署
~/K8s/Day6$ kubectl apply -f nodeport-demo.yaml
# service "nodeport-demo" create


# (2) 查看创建的NodePort,其保留的端口 80 (集群IP访问的端口):32306(node节点访问的端口)/TCP
~/K8s/Day6$ kubectl get svc -n service-test -o wide
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
# clusterip-deploy ClusterIP 10.108.129.19 <none> 80/TCP 24h app=nginx-clusterip,release=stabel
# myapp-headless ClusterIP None <none> 80/TCP 6h1m app=nginx-clusterip
# nodeport-demo NodePort 10.98.144.122 <none> 80:32306/TCP 40s app=nginx-clusterip,release=stabel


# (3) 验证两种方式访问
~/K8s/Day6$ curl http://10.98.144.122/host.html
# Hostname: clusterip-deploy-76b69d5c5b-qkwjs <br>
# Image Version: <u> 2.0 </u>
# Nginx Version: 1.19.4
~/K8s/Day6$ curl http://10.10.107.214:32306/host.html
# Hostname: clusterip-deploy-76b69d5c5b-qkwjs <br>
# Image Version: <u> 2.0 </u>
# Nginx Version: 1.19.4
~/K8s/Day6$ curl http://10.10.107.202:32306/host.html
# Hostname: clusterip-deploy-76b69d5c5b-qkwjs <br>
# Image Version: <u> 2.0 </u>
# Nginx Version: 1.19.4

$ sudo ipvsadm -Ln
# Prot LocalAddress:Port Scheduler Flags
# -> RemoteAddress:Port Forward Weight ActiveConn InActConn
# --- 常规(Node节点与集群节点)
# TCP 10.10.107.202:32306 rr
# -> 10.244.1.107:80 Masq 1 0 0
# -> 10.244.1.108:80 Masq 1 0 1
# -> 10.244.1.109:80 Masq 1 0 0
# TCP 10.98.144.122:80 rr
# -> 10.244.1.107:80 Masq 1 0 0
# -> 10.244.1.108:80 Masq 1 0 1
# -> 10.244.1.109:80 Masq 1 0 0
# --- 特殊(也可以访问)
# TCP 172.17.0.1:32306 rr
# -> 10.244.1.107:80 Masq 1 0 0
# -> 10.244.1.108:80 Masq 1 0 0
# -> 10.244.1.109:80 Masq 1 0 0
# TCP 172.18.0.1:32306 rr
# -> 10.244.1.107:80 Masq 1 0 0
# -> 10.244.1.108:80 Masq 1 0 0
# -> 10.244.1.109:80 Masq 1 0 0
# TCP 10.244.0.0:32306 rr
# -> 10.244.1.107:80 Masq 1 0 0
# -> 10.244.1.108:80 Masq 1 0 0
# -> 10.244.1.109:80 Masq 1 0 0
# TCP 10.244.0.1:32306 rr
# -> 10.244.1.107:80 Masq 1 0 0
# -> 10.244.1.108:80 Masq 1 0 0
# -> 10.244.1.109:80 Masq 1 0 0

# (4) 删除创建的service
~/K8s/Day6$ kubectl delete -f nodeport-demo.yaml
# service "nodeport-demo" deleted

WeiyiGeek.NodePort节点访问

WeiyiGeek.NodePort节点访问

PS : 采用IPtables作为负载均衡时候可以利用iptables -t nat -nvL KUBE-NODEPORTS看见NAT转发链;


LoadBalancer - Service

描述:loadBalancer 和 nodePort 其实是同一种方式, 只是前者运行在云厂商服务器中的; 其两则区别在于 loadBalancer 就是可以调用 cloud provider【云供应商】去 创建 LB【负载均衡】来向节点导流, 但是这会增加额外的预算成本;

WeiyiGeek.LoadBalancer示意图

WeiyiGeek.LoadBalancer示意图


ExternalName - Service

描述: 该类型的 Service 通过返回 CNAME和它的值,可以将服务映射到externalName字段的内容(例如:hub.weiyigeek.top)。

它是 Service 的特例它没有 selector 也没有定义任何的端口和Endpoint, 相反的对于运行在集群外部的服务,它通过返回该外部服务的别名这种方式来提供服务, 所以其作用类似软链或者快捷方式;

PS : 其目标是为了将外部流量引入到集群内部(在正式的生产环境中在两个集群中可以利用该方式进行访问)。

Tips: 说 ExternalName 接受 IPv4 地址字符串,但作为包含数字的 DNS 名称,而不是 IP 地址。 类似于 IPv4 地址的外部名称不能由 CoreDNS 或 ingress-nginx 解析,因为外部名称旨在指定规范的 DNS 名称。 要对 IP 地址进行硬编码,请考虑使用 headless Services

ExternalName 资源清单示例:

1
2
3
4
5
6
7
8
9
10
cat > externalname-demo.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: externalname-demo
namespace: service-test
spec:
type: ExternalName
externalName: s.weiyigeek.top
EOF

Tips: 当查找主机 externalname-demo.service-test.svc.cluster.local 时,群集DNS服务返回 CNAME 记录,其值为 s.weiyigeek.top。 访问 my-service 的方式与其他服务的方式相同,但主要区别在于重定向发生在 DNS 级别而不是通过代理或转发。 如果以后您决定将数据库移到群集中,则可以启动其 Pod,添加适当的选择器或端点以及更改服务的类型。

操作流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# (1) 创建
~/K8s/Day6$ kubectl apply -f externalname-demo.yaml
# service/externalname-demo created

# (2) 查看
~/K8s/Day6$ kubectl get svc -n service-test -o wide
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
# externalname-demo ExternalName <none> self.weiyigeek.top <none> 25s <none>

# (3) 查询主机DNS解析集群的DNS服务器将返回一个值my.database.example.com的CNAME记录;
# 访问这个服务的工作方式和其他的相同,唯一不同的是重定向发生在DNS层,而且不会进行代理或转发。
~/K8s/Day6$ dig -t A externalname-demo.service-test.svc.cluster.local. @10.244.0.5
# ;; ANSWER SECTION:
# externalname-demo.service-test.svc.cluster.local. 30 IN CNAME s.weiyigeek.top.
# s.weiyigeek.top. 30 IN A 125.32.244.252

~/K8s/Day6$ telnet externalname-demo.service-test.svc.cluster.local. 80
# Trying 125.32.244.252...
# Connected to s.weiyigeek.top.
# Escape character is '^]'.


externalIPs 资源清单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tee svc-ExternalIP.yaml << 'EOF'
kind: Service
apiVersion: v1
metadata:
name: extip
namespace: test
spec:
externalIPs:
- "61.128.1.26"
ports:
- name: http
port: 8081
protocol: TCP
targetPort: 80
selector:
app: nginx-demo
EOF

Tips: externalIPs 字段一般设置为外部网卡地址。
Tips: 注意externalIPs字段千万不要使用kubernetes master的网卡地址,否则无法管理k8s集群。

操作流程:

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
# 1.快速部署由deployment管理的nginx应用
kubectl create deployment -n test --image=nginx:latest --replicas 1 nginx-demo

# 2.部署svc-ExternalIP与查看创建的 svc/extip
$ kubectl apply -f svc-ExternalIP.yam

$ kubectl describe svc/extip -n test
Name: extip
Namespace: test
Labels: <none>
Annotations: <none>
Selector: app=nginx-demo
Type: ClusterIP
IP: 10.108.92.122
External IPs: 61.128.1.26
Port: http 8081/TCP
TargetPort: 80/TCP
Endpoints: 172.16.100.120:80
Session Affinity: None
Events: <none>

$ kubectl get svc -n test blog1
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
extip ClusterIP 10.108.92.122 61.128.1.26 80/TCP 10m

# 3.验证服务,通过svc名称和EXTERNAL-IP地址访问
$ curl extip.test.svc:8081
$ curl 61.128.1.26:8081 # 集群中所有Node访问可以通过绑定的外部地址EXTERNAL-IP来访问nginx-demo应用,注意不能设置集群内部节点的地址。
Kubernetes Services Demo , WeiyiGeek

# 4.可以看见在执行的Master机器上的监听
$ netstat -tlnp | grep "61.128.1.26:8081"
tcp 0 0 61.128.1.26:8081 0.0.0.0:* LISTEN -


0x03 Proxy - 代理转发

1.使用port-forward访问集群中的应用程序

描述:在实际进行Debug时使用 kubectl port-forward 访问 Kubernetes 集群中的 Redis Server进行调试;

Step1.分别为Redis创建Deployment和Service
Deployment

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
cat > redis-master-deployment.yaml<<'END'
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-master-deployment
labels:
app: redis
spec:
selector:
matchLabels:
app: redis
role: master
tier: backend
replicas: 1
template:
metadata:
labels:
app: redis
role: master
tier: backend
spec:
containers:
- name: master
image: redis
resources:
requests:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 6379
END


Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cat > redis-master-service.yaml<<'END'
apiVersion: v1
kind: Service
metadata:
name: redis-master-service
labels:
app: redis
role: master
tier: backend
spec:
ports:
- port: 6379
targetPort: 6379
selector:
app: redis
role: master
tier: backend
END


Step2.执行apply命令以创建 Redis Deployment与Service:

1
2
3
4
5
kubectl apply -f redis-master-deployment.yaml
# deployment.apps/redis-master-deployment created

kubectl apply -f redis-master-service.yaml
# service/redis-master-service created


Step3.分别查看deployment与service部署情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# redis-master-deployment-7d557b94bb-bzw2v 1/1 Running 0 3m2s

#查看 Deployment状态
kubectl get deployment
# NAME READY UP-TO-DATE AVAILABLE AGE
# redis-master-deployment 1/1 1 1 3m49s

# 查看 ReplicaSet(副本) 状态
kubectl get rs
# NAME DESIRED CURRENT READY AGE
# redis-master-deployment-7d557b94bb 1 1 1 4m46s

# 检查 Service 创建结果
kubectl get svc -o wide | grep redis
# redis-master-service ClusterIP 10.99.192.159 <none> 6379/TCP 9m27s app=redis,role=master,tier=backend

# 验证 Redis Service已经运行并监听了 6379 端口
kubectl get pods redis-master-deployment-7d557b94bb-bzw2v --template='{{(index (index .spec.containers 0).ports 0).containerPort}}{{"\n"}}'
# 6379

Step4.使用kubectl port-forward 命令转发本地端口到Pod的端口,用户可以使用资源的名称来进行端口转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#(1)下面的命令中的任意一行,都可以实现端口转发的效果:
kubectl port-forward redis-master-deployment-7d557b94bb-bzw2v 7000:6379
kubectl port-forward pods/redis-master-deployment-7d557b94bb-bzw2v 7000:6379 # kubectl get pods 获取资源名称
kubectl port-forward deployment/redis-master-deployment 7000:6379 # kubectl get deployments 获取资源名称
kubectl port-forward svc/redis-master-service 7000:6379 # kubectl get svc 获取资源名称
kubectl port-forward rs/redis-master-deployment-7d557b94bb 7000:6379 # kubectl get rs 获取资源名称

#(2)以上命令的输出结果类似:
[root@master-01 ~]$ kubectl port-forward --address 127.0.0.1,10.10.107.191 redis-master-deployment-7d557b94bb-bzw2v 7000:6379
# Forwarding from 127.0.0.1:7000 -> 6379
# Handling connection for 7000 (连接到此端口反应)
redis-cli -h 10.10.107.191 -p 7000
# 10.10.107.191:7000> ping
# PONG

Step5.总结本机 7000 端口的连接被转发到集群中 Redis Server 所在 Pod 的 6379 端口。利用该命令可以方便开发或者运维人员进行Debug调试;

注意事项:

  • 1) 由于已知的限制,目前的端口转发仅适用于 TCP 协议。 在 issue 47862 中正在跟踪对 UDP 协议的支持。


0x04 映射外部服务到集群内

场景 1.集群外的数据库映射到集群内部(IP地址)

描述: 如果您在 Kubernetes 内部和外部分别运行一些服务应用,此时应用如果分别依赖集群内部和外部应用时,通过采用将集群外部服务映射到K8s集群内部。

希望未来某个时候您可以将所有服务都移入集群内,但在此之前将是“内外混用”的状态。幸运的是您可以使用静态 Kubernetes 服务来缓解上述痛点。

在本例中,假如有一个集群外的 MySQL 服务器, 由于此服务器在与 Kubernetes 集群相同的网络(或 VPC)中创建,因此可以使用高性能的内部 IP 地址映射到集群内部以供Pod访问。

  • 第一步,我们创建一个将从此服务接收流量的 Endpoints 对象并将该对象与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
    # 非常注意: service和endpoint名字要相同属于同一个名称空间.
    tee mapping-svc-ep.yaml << 'EOF'
    kind: Endpoints
    apiVersion: v1
    metadata:
    name: mysqldb
    namespace: test
    subsets:
    - addresses:
    - ip: 192.168.12.50
    ports:
    - port: 3306
    protocol: TCP
    ---

    kind: Service
    apiVersion: v1
    metadata:
    name: mysqldb
    namespace: test
    spec:
    type: ClusterIP
    ports:
    - port: 13306
    protocol: TCP
    targetPort: 3306
    EOF
  • 第二步,创建Service和Endpoints,然后您可以看到 Endpoints 手动定义了数据库的 IP 地址,并且使用的名称与服务名称相同。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    $ kubectl apply -f mapping-svc-ep.yaml
    # endpoints/mysqldb created
    # service/mysqldb created
    $ kubectl describe svc/mysql -n test
    # Name: mysqldb
    # Namespace: test
    # Labels: <none>
    # Annotations: <none>
    # Selector: <none>
    # Type: ClusterIP
    # IP: 10.102.172.40
    # Port: <unset> 13306/TCP # 集群内部访问的映射端口
    # TargetPort: 3306/TCP # 目标服务端口
    # Endpoints: 192.168.12.50:3306 # 目标服务IP及端口
    # Session Affinity: None
    # Events: <none>

    $ kubectl get svc -n test mysqldb
    NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    mysql ClusterIP 10.102.172.40 <none> 13306/TCP 23s
  • 第三步,集群内部Pod访问映射的MySQL服务。

    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
    # Telnet cluster_ip mapping_port
    $ telnet 10.102.172.40 13306
    # Trying 10.102.172.40...
    # Connected to 10.102.172.40.
    # Escape character is '^]'.

    # 5.5.5-10.4.18-MariaDB-1:10.4.18+maria~bionic-log&

    # 创建测试Pod
    ~$ kubectl run busybox-demo -n test --image=busybox:latest --command -- sleep 3600
    ~$ kubectl get pod -n test
    # NAME READY STATUS RESTARTS AGE
    # busybox-demo 1/1 Running 0 11s

    # 进入Pod的终端
    $ kubectl exec -it -n test busybox-demo -- sh
    ping mysql.test.svc -c 1
    # PING mysql.test.svc (10.102.172.40): 56 data bytes
    # 64 bytes from 10.102.172.40: seq=0 ttl=64 time=0.062 ms
    telnet mysql.test.svc 13306 # 连接方式1
    # Connected to mysql.test.svc
    # t
    # 5.5.5-10.4.18-MariaDB-1:10.4.18+maria~bionic-log(AxY7G<8C▒'8&SPv&TU0/8mysql_native_passwordxterm-256color
    # !#08S01Got packets out of orderConnection closed by foreign host
    telnet mysql 13306 # 连接方式2
    Connected to mysql
    ....
    5.5.5-10.4.18-MariaDB-1:10.4.18+maria~bionic-log
    ...

    Kubernetes 将 Endpoints 中定义的所有 IP 地址视为与常规 Kubernetes Pod 一样。现在您可以用一个简单的连接字符串访问数据库:mysql://mysql.test.svc 或者 mysql://mysql

根本不需要在代码中使用 IP 地址!如果以后 IP 地址发生变化,您可以为端点更新 IP 地址,而应用无需进行任何更改。


场景 2.具有 URI 的远程服务映射到集群内部

描述: 如果您使用的是来自第三方的托管网站,它们可能会为您提供可用于连接的统一资源标识符 (URI)。
如果它们为您提供 IP 地址,则可以使用场景 1 中的方法。

在本例中,我在 集群外部创建了一个网站,而我想在集群内部进行重定向访问。

第一步,编写部署的资源清单。

1
2
3
4
5
6
7
8
9
10
cat > mapping-svc-ExternalName.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: myweb
namespace: test
spec:
type: ExternalName
externalName: www.weiyigeek.top
EOF


第二步,部署ExternalName类型的services, 我们创建一个 “ExternalName” Kubernetes 服务,此服务为您提供将流量重定向到外部服务的静态 Kubernetes 服务。此服务在内核级别执行简单的 CNAME 重定向,因此对性能的影响非常小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~$ kubectl apply -f mapping-svc-ExternalName.yaml
~$ kubectl describe svc/myweb -n test
# Name: myweb
# Namespace: test
# Labels: <none>
# Annotations: <none>
# Selector: <none>
# Type: ExternalName
# IP:
# External Name: www.weiyigeek.top # 外部域名
# Session Affinity: None
# Events: <none>

~$ kubectl get svc -n test myweb
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
myweb ExternalName <none> www.weiyigeek.top <none> 48s


第三步,进入Pod中进行访问测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~$ kubectl exec -it -n test busybox-demo -- sh
ping myweb
# PING myweb (192.168.12.18): 56 data bytes
# 64 bytes from 192.168.12.18: seq=0 ttl=63 time=0.344 ms

wget myweb/api.json # 由于Busybox 中不带curl 此处简单采用wget进行演示
# Connecting to myweb (192.168.12.18:80)
# saving to 'api.json'
# api.json 100% 15 0:00:00 ETA
# 'api.json' saved

cat api.json
# {"status":"ok"}

wget --spider myweb.test.svc/api.json #
# Connecting to myweb.test.svc (192.168.12.18:80)
# remote file exists

Tips: 非常注意,由于 “ExternalName” 使用 CNAME 重定向,因此无法执行端口重映射我们无法使用port指定集群内部访问端口字段。


场景 3.具有 URI 和端口重映射功能的远程托管数据库

描述: CNAME 重定向对于每个环境均使用相同端口的服务非常有效,但如果每个环境的不同端点使用不同的端口,CNAME 重定向就略显不足,此时我们可以

幸运的是我们可以使用一些基本工具来解决这个问题,手动创建无头服务及endpoint,引入外部数据库,然后通过k8s集群中的域名解析服务访问,访问的主机名格式为[svc_name].[namespace_name].svc.cluster.local

在本例中,我在其它K8S集群外部创建了一个appspring的应用,而我想在当前集群通过集群services进行访问调用。

第一步,资源清单的创建,此处使用无头服务,对应的svc及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
tee svc-ep-clusterIP.yaml << 'EOF'
kind: Endpoints
apiVersion: v1
metadata:
name: appspring
namespace: test
labels:
app: appspring
subsets:
- addresses:
- ip: 10.41.40.21
- ip: 10.41.40.22
- ip: 10.41.40.23
ports:
- name: http
port: 32179
protocol: TCP
---
kind: Service
apiVersion: v1
metadata:
name: appspring
namespace: test
labels:
app: appspring
spec:
clusterIP: None
ports:
- name: http
port: 80
protocol: TCP
targetPort: 32179
type: ClusterIP
EOF


第二步,创建service及endpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ kubectl apply -f svc-ep-clusterIP.yaml
# endpoints/appspring created
# service/appspring created

$ kubectl describe svc/appspring -n test
# Name: appspring
# Namespace: test
# Labels: app=appspring
# Annotations: <none>
# Selector: <none>
# Type: ClusterIP
# IP: None
# Port: http 80/TCP # 映射到集群内部端口
# TargetPort: 32179/TCP
# Endpoints: 10.41.40.21:32179,10.41.40.22:32179,10.41.40.23:32179 # 目标负载IP以及应用端口
# Session Affinity: None
# Events: <none>

$ kubectl get svc -n test
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# appspring ClusterIP None <none> 80/TCP 4m45s


第三步,在k8s集群中启动一个alpine:latest容器进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
kubectl run alpine-demo -n test --image=alpine:latest --command -- sleep 3600
kubectl exec -it -n test alpine-demo -- sh
/ # ping appspring -c 1 # 可以看出是轮询请求的
PING appspring (10.41.40.22): 56 data bytes
64 bytes from 10.41.40.22: seq=0 ttl=60 time=1.196 ms

/ # ping appspring -c 1
PING appspring (10.41.40.21): 56 data bytes
64 bytes from 10.41.40.21: seq=0 ttl=60 time=1.273 ms

/ # ping appspring -c 1
PING appspring (10.41.40.23): 56 data bytes
64 bytes from 10.41.40.23: seq=0 ttl=60 time=1.304 ms

/ # wget appspring:32179
Connecting to appspring:32179 (10.41.40.22:32179)
Connecting to appspring:32179 (10.41.40.21:32179)
saving to 'index.html'
index.html 100% |*************************************************************************| 5275 0:00:00 ETA
'index.html' saved
/ # cat index.html

/ # ls -alh index.html
-rw-r--r-- 1 root root 5.2K Dec 6 14:58 index.htm

注:URI 可以使用 DNS 在多个 IP 地址之间进行负载平衡,因此,如果 IP 地址发生变化,这个方法可能会有风险!如果您通过上述命令获取多个 IP 地址,则可以将所有这些地址都包含在 Endpoints YAML 中,并且 Kubernetes 会在所有 IP 地址之间进行流量的负载平衡。


映射总结:
将外部服务映射到内部服务可让您未来灵活地将这些服务纳入集群,同时最大限度地减少重构工作。即使您今天不打算将服务加入集群,以后可能也会这样做!而且,这样一来,您可以更轻松地管理和了解组织所使用的外部服务。

如果外部服务具有有效域名,并且您不需要重新映射端口,那么使用 “ExternalName” 服务类型将外部服务映射到内部服务十分简便、快捷。如果您没有域名或需要执行端口重映射,只需将 IP 地址添加到端点并使用即可。

至此从K8s集群中引入外部服务实践完成。