Kubernetes-服务暴露-ingress
Ingress 简介
在客户端访问我们k8s服务时,四层调度器本身是没有办法解除ssl会话的,这就意味着客户端必须与后端服务器(pod)之间直接建立ssl会话,这里还有个显著的问题在于如果调度器在ssl会话建立以后的下一个请求被调度到第二台服务器上那么这个ssl还要重新建立,因此我们只要认为内部网络是安全的那么我们可以把会话在前端调度器上卸载,但是四层调度是不能卸载的,因此我们需要七层的负载均衡机制。因此如果他们是http服务我们又期望构建https,那么我们只需要他在互联网上这个不安全的网络中传输实现https,内网中使用http,因此我们需要使用卸载器,但是我们Service调度时,无论是iptables还是ipvs都只是四层调度,因此也就意味着如果你想要在k8s上运行一个应用基于https提供服务,我们就必须得在后端每一个pod上配置https,因为只有这样他们才会建立起https联系,所以我们现在也期望在接入那一层上就能够卸载ssl,向内部调度时就不再是ssl了。对这种需求,k8s采用一种很独特的方式来实现,我们在整个集群中,在进行调度时后端被代理的pod资源是不配置https的,就是明文的http,但是我使用一个独特的调度器(运行在pod中),对于此pod来讲,其是一个运行在七层(用户空间)的正常的应用程序,比如nginx,haproxy等,当用户试图访问某一服务时,我们不让他先去到达前端的service,而是先到这个pod,然后pod与pod之间不需要service而是直接通信来完成反向代理,我们用一个pod来反代至后端我们真正提供服务的pod,此前我们用service代理的,现在用pod,而这个pod如果需要被访问到那么还是需要经过service,因此客户端经过此pod的service调度以后,与我们专门配置了https的此pod进行交互,这里的service我们定义成nodePort,依然没有什么问题,依然老路径还是存在的,但是等到达这个pod以后由此pod直接代理至两个明文的pod,因此此pod就成为https的会话卸载器了。如果这种方式进行调度那么调度方式如下: client –> LB –> nodePort –> service –>会话卸载器pod –> 后端pod 这种方式性能肯定会非常非常差,如图。
其实我们还可以这样干,可以让我们pod直接共享我们节点的网络名称空间,于是,上述中的会话卸载器pod我们可以直接让他共享节点网络名称空间,这就意味着其监听着宿主机的地址,这样我们客户端的请求就可以之间到达这个pod,然后再由他进行调度。
在一个节点上运行的容器,容器可以使用自己的虚拟网络,也可以共享宿主机的网络,如果这个容器共享宿主机的网络也就意味着这个容器内的进程一旦监听套接字时它监听的是宿主机的地址,相当于一个进程运行在宿主机上一样的,这样一来这个pod在每个节点上就只能运行一个了,一般来讲集群中只有一个,那么其只需要运行在集群的某一个节点即可,但是如果监听在节点的端口时就会有问题,service时无论访问哪一个节点的端口都行,因为你访问哪一个节点的nodePort他都能通过它的ip地址送达到后端pod上,但是现在这个Pod要监听节点的网络名称空间,并且通过这个节点的网络名称空间直达这个pod,那么如果运行这种类型的pod那么就一定只能运行在一个节点上,访问时客户端就只能访问这个节点,并且如果这个节点挂了呢?
要解决这个问题,我们可以用DaemonSet控制器,它可以在每个节点上都运行相应的pod 副本并且只运行一个,这样假如我们有三个节点那么我们三个节点上都可以运行这个pod。这样就都能实现代理和负载均衡,但是又回到了调度到哪一个节点都可以的问题,这样无论哪个节点挂掉了都还是可以访问到,但是如果我们有太多的节点那么每个节点都运行一个这样的pod就太占资源。daemonset还可以让pod运行在有限的节点的范围上(部分节点),于是再将来做k8s集群时可以这样干,比如我们有三千个节点,那么我们专门拿三个节点出来做pod的接入式的负载均衡的专用主机,并且给这三个节点打上污点让其它pod无法调度上来,然后我们就定义这个DaemonSet控制器上的pod只运行在这三个节点上各自运行一份并且能容忍这些污点,所以这三个主机在集群中只允许这一个类型的pod,专门负责为集群接入外部的七层调度的流量。而这个pod在k8s中有个专门的称呼叫 Ingress Controller。这个Ingress Controller比较独特,之前讲的DaemonSet,deployment,replacSet等控制器都不一样,DaemonSet deployment,replaciSet等等都是作为Controller manager的一部分存在,众多控制器都是作为Controller manager的一个子组件作为其组成部分组成的。而Ingress Controller却是自己独立运行的一个或一组pod资源,它通常就是一个应用程序,这个应用程序就是拥有七层代理能力和调度能力的应用程序,目前k8s上的选择有四种,其中最不受待见的就是Haproxy,一般默认是nginx,现在在服务网格中大家比较倾向Envoy,当然还有其它与nginx相竞争的据说本来就是为微服务而生的Traefik,所以用Ingress Controller时会发现我们有三种选择:
nginx:这是后来改造的
Traefik:这种设计就是为微服务这种动态生成而生的
Envoy :去做微服务的大家都比较倾向于Envoy
作为调度器,有时候要调度不只一个后端服务,假如有一个调度器pod,后端有一组pod提供电商服务,第二组pod提供了社交服务,第三组pod提供了论坛服务,第四组pod提供了网关服务。那么我们这四组http服务怎么能够分别调度呢?
a、从nginx的角度来讲,接入服务端时我们通过uptream_server即可,但是接入客户端时我们应该如何标识这四种不同的请求呢,首先我们可以在nginx上做四个虚拟主机,在四个主机名上做四个主机,每一个主机名对应一组后端的pod。那万一我们没那么多主机名呢?此时我们可以通过不同的路径来做url映射,然后每一组路径就映射到一组后端pod上,但是pod存在生命周期,随时都可能挂掉替换为一个新pod,新pod的ip就会变了,另外我们pod的应用的规模也可以动态伸缩的,我们简单改一下副本数其数量也会变,这种一变前面代理的配置也就无效了,我们service通过关联标签来解决这个问题,而且service随时 watch着api server上的api时刻来关注自己关联的slector的资源是否变动了,只要变动我们api server就会立即通知service然后service立即改变。 所以service通过label selector始终关联着对应label能适配的后端pod,无论怎么变都能应付而且能及时作出反应,那么此处nginx运行在pod中也就意味着这个配置是在pod内部并且后端pod随时还会发生变动,Ingress 也会时刻watch着api 中的后端pod资源的改变。那它怎么知道这是哪个pod资源呢?Ingress Controller自己没有这个能力,它并不知道目前符合自己条件关联的被代理的pod资源有哪些,它必须借助于service来实现,我们要想定义一个这种调度能力功能其实还是需要建service,这个service通过label selector关联至后端的pod上来,但这个service不是被当做被代理时的中间节点,它仅仅是帮忙分类的,这个service关联了几个pod那么我们就将这几个pod配置写在这个upstream中,调度时是不会经过service的,service在此处仅仅是帮忙分组的,分完组以后我们需要得到的也不是service的IP,而是pod的ip,因此此时可以使用headless service,但是这个service却没有用,是不是headless都无所谓,它只要帮忙完成分组知道找哪几个pod就可以了,pod一变,service对应的资源也就变了,问题是变了后这个结果怎么反应到这个配置文件中来,此时需要依赖于一个专门的资源 Ingress。
在k8s上有一种特殊的资源叫Ingress,Ingress 和Ingress Controller是两回事,我们定义一个Ingress 时就是说了它其实就是定义我们期望这个Ingress Controller是如何给我们建一个前端(可能是一个虚拟主机,也可能是一个url映射)接入层,同时又给我们定义一个后端upstream_server,这个upstream_server中有几个主机 Ingress是通过这个service 得到的,并且Ingress有一个特点,作为资源来讲他可以通过编辑注入到Ingress Controller里面来,直接把它注入并保存为配置文件,而且一旦Ingress发现Service选定的后端的pod资源发生改变了,这个改变一定会及时返回到Ingress中,这个Ingress会及时注入到这个前端调度器pod中,就是注入到upstream配置文件中,而且还能触发这个pod中的主容器中的进程重载配置文件。所以我们要想使用这个功能我们需要在集群中:
- 要有个service去对后端某一个特定类型的pod资源进行分类,这个service只是起分类作用的。
- Ingress基于这个分类识别出有几个pod,并且识别其ip地址是什么,并且将这个ip地址返回的结果生成配置信息注入到upstream_server(以nginx为例),但是nginx又不是特别适用这个场景,每次变动都在重载,如果是Traefik和Envoy 等天生就是为这种场景而生的,只要动了就加载生效,不需要重载。(它可以监控这个配置文件发生变化,只要发生变化就会动态重载)。
如果我们想要使用代理,我们应该怎么做?
需要先部署一个Ingress Controller,然后部署一个pod进来,这个pod可能本来是空的,没有什么有效的东西,接下来我们要根据自己的需要通过虚拟主机的方式或者通过url代理的方式来配置一个前端,然后再根据我们Service收集到的后端pod的IP定义成upstream_server,把这些信息反应在Ingress当中由Ingress动态注入到Ingress Controller中才可以
从图中我们可以看到,当访问我们服务时首先由外部的负载均衡器将请求调度到我们nodePort的Service上,而nodePort上的Service再将请求调度至我们内部的一个pod 叫IngressController上来,Ingress Controller根据Ingress中的定义(虚拟主机或url),每一个主机名对应后面的一组pod资源(通过service分组),因此此处用到两组Service,第一个Service是帮集群接入外部流量的(当然也可以不用,可以把这个Ingress Controller pod运行为共享网络节点网络名称空间方式并且将其定义为Dameset方式运行在特定节点上就可以了),那么我们在定义pod资源时只需要在pod的一个配置选项中加入hostnetwork即可。第二个Service只用来做pod归组不被调度时使用。
接下来我们要使用ingress的功能得先去安装部署Ingress Controller 这个pod,而后再定义Ingress,而后再定义后端pod生成Service,然后再建立关联关系。
总结:所以可以简单理解为
- ingress对象:
指的是k8s中的一个api对象,一般用yaml配置。作用是定义请求如何转发到service的规则,可以理解为配置模板。 - ingress-controller:
具体实现反向代理及负载均衡的程序,对ingress定义的规则进行解析,根据配置的规则来实现请求转发。
Ingress 定义
1、Ingress 也是标准的kubernetes资源对象,因此也拥有相应的对象属性。
1 | KIND: Ingress |
1 | KIND: Ingress |
Ingress部署方式
ingress的部署,需要考虑两个方面:
- ingress-controller是作为pod来运行的,以什么方式部署比较好
- ingress解决了把如何请求路由到集群内部,那它自己怎么暴露给外部比较好
下面列举一些目前常见的部署和暴露方式,具体使用哪种方式还是得根据实际需求来考虑决定。
Deployment+LoadBalancer模式的Service
如果要把ingress部署在公有云,那用这种方式比较合适。用Deployment部署ingress-controller,创建一个type为LoadBalancer的service关联这组pod。大部分公有云,都会为LoadBalancer的service自动创建一个负载均衡器,通常还绑定了公网地址。只要把域名解析指向该地址,就实现了集群服务的对外暴露。
Deployment+NodePort模式的Service
同样用deployment模式部署ingress-controller,并创建对应的服务,但是type为NodePort。这样,ingress就会暴露在集群节点ip的特定端口上。由于nodeport暴露的端口是随机端口,一般会在前面再搭建一套负载均衡器来转发请求。该方式一般用于宿主机是相对固定的环境ip地址不变的场景。
NodePort方式暴露ingress虽然简单方便,但是NodePort多了一层NAT(iptables),在请求量级很大时可能对性能会有一定影响。
DaemonSet+HostNetwork+nodeSelector
用DaemonSet结合nodeselector来部署ingress-controller到特定的node上,然后使用HostNetwork直接把该pod与宿主机node的网络打通,直接使用宿主机的80/433端口就能访问服务。这时,ingress-controller所在的node机器就很类似传统架构的边缘节点,比如机房入口的nginx服务器。该方式整个请求链路最简单,性能相对NodePort模式更好。缺点是由于直接利用宿主机节点的网络和端口,一个node只能部署一个ingress-controller pod。比较适合大并发的生产环境使用。