背景
最初想到这个问题的原因是,编写代码的时候,有时候会用@Loadbalance的RestTemplate,有时候则直接用@Bean的方式获取,所以才会产生这个疑问,在学习过程中,逐步发现以Kubernetes为容器编排和SpringCloud作为服务治理的系统中,服务间的请求还涉及到了kube-proxy/kube-dns/iptables/eureka/ribbon/ingress等内容,因此逐个梳理一下,这些进程/组件在服务间调用过程中的作用到底是啥。
为便于理解,分两种场景,一步步的解释一次Http请求的过程。
- 来自客户端的一次http请求
- 来自集群内的一次http请求
场景一:来自客户端的一次http请求
Step1:客户端对域名www.xxx.com发出请求,本质上是一个TCP连接,所以需要通过客户端所属网络DNS获取域名机器的IP地址,然后建立连接,发出请求。
Step2:域名节点即为服务端的一台机器,在Kubernetes集群中就是其中的一个node,可以是master也可以不是,此时该机器需要有一个Service进程接受Client的请求,所以域名节点会有一个nginx
Step3:一般来说,此时nginx启动后,就能接收并响应Client的请求,不过在Kubernetes集群中,就需要将这个nginx运行在一个pod里,所以可以选择Deployment/Statefulset任意的一种方式,部署nginx镜像,同时通过hostnetwork的方式,监听主机的80端口,但是这种方式存在一个弊端:当我们有新服务或者原有nginx的路由配置需要变更,此时nginx需要重启,会导致服务中断,为解决这个问题,引入了Ingress,所以就变成了下面的样子。
Step4:在使用Ingress的情况下,nginx的配置修改就不需要再重启了,而是每次通过ingress-controller + ingress的方式来更新,Ingress是Kubernetes的资源类型之一,因此通过k8s-api-server可以查询到(在etcd中持久化),ingress-controller就可以通过定时拉取更新的方式感知到ingress的变化,当识别到更新时,还是通过k8s-api-server获取到nginx所在pod,然后进入pod去动态修改nginx的配置。需要注意的是:ingress是kubernetes资源,ingress-controller和nginx一样是运行的进程,早期的nginx是不支持ingress-controller,后来才出的nginx-ingress-controller,相当于将上述的ngxin + ingress-controller合并在一起,镜像启动后即可实现对ingress资源的监听和对nginx配置的刷新(你也可以选择traefik-ingress-controller作为网关)
一个相对比较详细的流程如下图:
集成ingress-controller的网关横向对比
Step5:请求已经顺利到达网关,下一步就是如何达到我们的Service了。在ingress-controller已经生效的情况下,通过配置ingress来指定路由和服务的关系,如果系统中使用了网关服务,可以直接在nginx-ingress-controller的配置文件直接配置backend,指向网关服务,这样相当于所有的流量经过nginx,都会默认请求到网关服务。如果没有这种场景,可以按照如下方式配置ingress。
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: test-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /testpath
backend:
serviceName: test
servicePort: 80
Step6:nginx是Kubernetes中的一个pod,网关或我们的其他服务也同样是pod,那么无论是通过nginx配置backend或ingress配置路由,都是pod和pod之间的请求转发(通信),是如何做到的?这里先mark一下,答案是通过Kubernetes的网络实现,这部分内容将在场景2中展开。
回顾一下“来自客户端的一次http请求”过程,主要包括以下几个阶段:
- http请求根据域名解析到达对应的主机(途中Nginx-ingress-controller所在机器)
- 该主机Nginx-ingress-controller是主机网络部署(yaml中hostnetwork=true),监听80
- Nginx收到请求后,通过ingress-controller过滤K8S集群中的ingress,根据ingress配置的rule查询是否匹配
- 如果匹配则转发请求到对应ingress配置的服务(在K8S集群内)
- 如果未匹配则直接转发至back-end(Nginx-ingress-controller的daemonSet yaml中配置)
常见的几个问题:
-
问题1:Ingress的配置如何更新
ingress-controller会定期通过k8s api server更新当前已有的ingress(不用重启服务的情况下修改路由规则) -
问题2:Nginx-ingress-controller和ingress、nginx、ingress-controller啥关系
较早版本中ingress、nginx、ingress-controller是分离的,nginx接收请求,ingress-controller负责更新ingress,ingress配置独立的路由规则,后来版本将nginx和ingress-controller合并为Nginx-ingress-controller -
问题3:转发请求的时候,如何实现LB
请求转发发生在识别到ingress或通过back-end转发,这两部分配置的均为K8S的Service(简称svc),LB通过svc到pod来实现(这部分内容将在场景2中展开)
场景二:来自集群内的一次http请求
集群内服务间的请求,例如服务A和服务B相互请求,就需要A和B都知道对方的ip地址才行,因此需要服务注册发现,服务的注册发现,可以通过两种方式来实现,为了能够弄清楚集群内服务间请求过程,我们要分开讨论:
- 通过Kubernetes(kube-dns)作为服务注册发现实现集群内服务间通信
- 通过SpringCloud(Eureka)作为服务注册发现实现集群内服务间通信
在场景一种,一个http请求到达后,会通过kube-dns的方式将请求从nginx转发到网关(第一种方式),此时网关就可以选择上述的两种方式来实现集群内的通信。
场景二的第一种情况,通过Kubernetes(kube-dns)实现集群内服务间通信
集群内的服务A,请求服务B,发出一个http请求,过程如下:
Step1:服务B在pod中,而pod的IP是可变的,DNS是服务发现的最基本手段,服务A得预先知道服务B的名称,通过B的服务名称向服务B发送请求,通过DNS实现从服务名到IP的映射
Step2:从服务A向服务B的请求,首先通过DNS解析获取对应pod的IP,在一个pod中,我们可以看到/etc/resolve.conf文件配置了DNS,通过对比发现DNS的IP地址就是kube-dns的IP地址,也就是说,当我们通过B服务的service名称访问时,就会通过kube-dns获取到对应服务的IP地址,此时就可以通过IP地址建立连接,发送请求
Step3:通过比对我们发现,DNS的IP地址并不是kube-dns的pod的ip地址,而是kube-dns的Service的IP地址,而且我们使用nslookup可以看到,访问B服务名称返回的IP地址也不是B服务的pod的IP地址,而是B服务的Service的IP地址,也就是说,在请求发送给B服务的Service的IP地址后,又经过了一次转发过程。
Step4:Service本身用于解决服务发现时pod的IP可变的问题,解决的方法是通过建立如下的关系,当pod的IP发生改变时,更新从EndPoint到pod的映射关系,而Service到EndPoint的关系不变,Service的IP本身也不会改变,通过kubectl get svc、kubectl get endpoints可以看到他们的关系。因此请求的目标IP虽然是Service B,但最终请求还是会根据EndPoint指向pod B的IP地址。
Step5:当请求转发到Service B的IP地址后,是通过iptables进行了请求的转发,实现转发请求到pod B的IP,关于iptables,我们在kubernetes网络中也可以配置视同ipvs、eBPF等其他方式来实现,小规模集群的情况下iptables就可以了。
- 逻辑链路上看,向pod B的请求实际上是通过了Service B转发到了pod B
- 物理链路上看,向pod B的请求经过Kubernetes网络(这个不详细展开了,参考CNI概念),从node1到node2,然后经过iptables的处理转发给了pod B的IP
下面这个图解释着两种过程:
回顾一下“通过Kubernetes(kube-dns)实现集群内服务间通信”过程,主要包括以下几个阶段:
- pod A向pod B发送,先为pod B创建Service
- Service B创建完成后,会自动关联pod B并为其创建Endpoint B
- kube-proxy通过轮询api-server发现Service B并为其修改所有node上的iptables
- kube-dns通过轮询api-server发现Service B并为其增加DNS记录
- Service-Controller、Endpoint-Controller通过轮询api-server发现Service B,及时更新他们的变化
- 此时请求从pod A发出,pod A中通过resolve.conf配置的nameService查询kube-dns,获取B的Service IP
- 向B的Service IP发出请求,经过kubernetes网络(基于CNI),首先转发到Service B对应物理节点,对应物理节点通过iptables规则,经请求转发到pod B所在节点
常见的几个问题:
-
问题1:为什么kube-dns可以返回服务B对应的Service的IP地址
kube-dns在运行时,会不断通过api-server查询更新当前集群的所有Service资源,并根据Service资源更新Kubernetes集群内的Service和IP映射关系 -
问题2:请求向Service发出后如何实现转发到对应的pod所在节点
-
问题3:负载均衡在哪个阶段发生
场景二的第二种情况,通过SpringCloud(Eureka)实现集群内服务间通信
我们也可以选择不通过kube-dns转发请求,本文开头提到的@Loadbalance就是本节要讨论的方式。对照上述流程,本节其实对应的DNS角色就是Eureka,在使用过程中配合Ribbon共同完成功能。分工角色类似于:
每个微服务在使用Eureka会在本地启动一个EurekaDiscoveryClient,通过心跳的方式和EurekaServer保