云原生技术之Kubernetes的service
10-service、ingress-找到你并不容易
Service
什么是 Service
在 Kubernetes 集群里 Pod 的生命周期是比较“短暂”的,虽然 Deployment 和 DaemonSet 可以维持 Pod 总体数量的稳定,但在运行过程中,难免会有 Pod 销毁又重建,这就会导致 Pod集合处于动态的变化之中。 Pod 的 IP 地址经常变来变去,客户端该怎么访问呢?如果不处理好这个问题,Deployment 和 DaemonSet 把 Pod 管理得再完善也是没有价值的。
其实,这个问题在业内早就有解决方案来针对这样“不稳定”的后端服务,那就是“负载均衡”,典型的应用有 LVS、Nginx 等等。它们在前端与后端之间加入了一个“中间层”,屏蔽后端的变化,为前端提供一个稳定的服务。
使用 YAML 描述 Service
用命令 kubectl api-resources 查看它的基本信息,可以知道它的简称是svc,apiVersion 是 v1。注意,这说明它与 Pod 一样,属于 Kubernetes 的核心对象。
我们就可以写出 Service 的 YAML 文件头了
Bash |
使用 kubectl expose 指令来创建service模板文件,需要用参数 --port 和 --target-port 分别指定映射端口和容器端口,而 Service 自己的 IP 地址和后端 Pod 的 IP 地址可以自动生成,用法上和Docker 的命令行参数 -p 很类似。
Bash |
YAML |
Service 的定义非常简单,在“spec”里只有两个关键字段,selector 和 ports。
selector 是用来过滤出要代理的那些 Pod,因为我们指定要代理 Deployment,所以 Kubernetes 就为我们自动填上了 nginx-dep 的标签,会选择这个 Deployment 对象部署的所有 Pod。
在 Kubernetes 里使用 Service
为了方便查看 Service 的效果,我们添加一个Nginx的配置文件,使用ConfigMap来定义,通过Volume 挂载到 Pod中。
YAML |
在 Deployment 中通过 “template.volumes”定义存储卷,使用“volumeMounts” 挂载到容器中。
YAML |
部署 Deployment 之后,就可以 kubectl apply 创建 Service 对象了
YAML |
使用命令 kubectl get svc 查看对象状态
Kubernetes 会自动为 Service 对象分配一个IP地址“10.111.193.124”,这个地址段是独立于Pod地址段的(在kubeadm 安装的配置文件里指定的)。Service 对象的 IP 地址还有一个特点,它是一个“虚地址”,不存在实体,只能用来转发流量。
通过 kubectl describe 命令可以看到 Service 代理了哪些后端的 Pod:
YAML |
通过截图可以看到 Service管理了两个 endpoint 对象,10.244.171.7:80 和 10.244.184.72:80,那这两个地址是不是实际Pod的地址呢?
我们可以通过以下命令来验证
Bash |
通过上面的截图我们就能够验证 Service 确实用一个静态 IP 地址代理了两个 Pod 的动态 IP 地址。
测试 Service 的负载均衡效果
我们使用 curl 命令在 master 节点或是 worker 节点上执行以下命令来访问 Service:
Bash |
用 curl 访问 Service 的 IP 地址,就会看到它把数据转发给后端的 Pod,输出信息会显示具体是哪个 Pod 响应了请求,就表明 Service 确实完成了对 Pod 的负载均衡任务。
我们再删除一个 Pod,看看 Service 是否会更新后端 Pod 的信息,实现自动化的服务发现:
Bash |
Pod 被删除后,Deployment 对象会自动创建一个新的 Pod,Service 会实时监控 Pod 的变化,所以它也会立即更新后端代理的 Pod 地址。这样后端的 Pod 数量就可以按业务需要自由伸缩,对用户是无感的。
以域名的方式使用 Service
Service 对象的 IP 地址是静态的,保持稳定,不过数字形式的 IP 地址用起来还是不太方便,如果要能有域名就更好了,DNS 在 Kubernetes 里能不能实现呢?
Kubernetes 有一个插件来实现 DNS 的功能,在早期这个插件普遍使用 kube-dns,现在用 coredns 比较多。
因为 DNS 是一种层次结构,为了避免太多的域名导致冲突,Kubernetes 就把名字空间作为域名的一部分,减少了重名的可能性。
Service 对象的域名完全形式是“对象. 命名空间.svc.cluster.local”,但很多时候也可以省略后面的部分,直接写“对象. 命名空间”甚至“对象名”也可以 。
DNS 是在 Kubernetes 集群内部生效,所以要测试需要在 Pod 内验证。
可以看到,现在我们就不再关心 Service 对象的 IP 地址,只需要知道它的名字,就可以用DNS 的方式去访问后端服务。
如何让 Service 对外暴露服务
Service 是一种负载均衡技术,它不仅能够管理 Kubernetes 集群内部的服务,还能够向集群外部暴露服务。
Service 对象有一个关键字段“type”,表示 Service 是哪种类型的负载均衡。前面我们创建 Service 都使用的默认值“ClusterIP”,这种类型的 Service 地址只能在集群内访问。
我们可以通过命令查看这个字段的属性
Bash |
我们可以看到 默认值是 “ClusterIP”,另外还有三种类型分别是“ExternalName” 、“NodePort” 和 “LoadBalancer”。 其中 “LoadBalancer” 是由云服务商提供的,需要借助云服务商用才能实现完整的效果。
我们分别测一下这几种类型的 Service。
NodePort
我们修改一下 Service 的 YAML 文件,加上 “type” 字段:
YAML |
更新对象,查看状态:
Bash |
可以看到 nginx-svc 的 “TYPE” 变成了 “NodePort”,而在 “PORT” 列里的端口信息也不一样,除了集群内部使用的“80”端口,还多出了一个“32119”端口,这就是 Kubernetes 在节点上为 Service 创建的专用映射端口。
因为这个端口号属于节点,外部能够直接访问,所以现在我们就不需要登录集群节点或者进入 Pod 内部,直接在集群外使用任意一个节点的 IP 地址,就能够访问 Service 和它代理的后端服务了。
ExternalName
ExternalName Service 是 Kubernetes 中一个特殊的 service 类型,它不需要指定 selector 去选择哪些 Pod 实例提供服务,而是使用 DNS CNAME 机制把自己 CNAME 到你指定的另外一个域名上,你可以提供集群内的名字,比如mysql.db.svc 这样的建立在db命名空间内的 MySQL 服务,也可以指定 http://www.baidu.com 这样的外部真实域名。
我们需要使用 ping 和nslookup 命令,所以部署一个 busybox 的 deployment,busybox 镜像中提供很多常用的命令可以使用。
YAML |
创建一个 External Service
YAML |
登录到 Pod 中验证 External Service
可以看到 external-svc 会被解析到一个 CNAME 指向 www.baidu.com
LoadBalancer
我们修改 service 的类型为 LoadBalancer 看一下效果
YAML |
更新SVC
YAML |
PORT 列没有变化,但是 EXTERNAL-IP 列会显示 pending,因为 LoadBalancer 类型的负载均衡需要云服务商提供,我们的环境会显示为 pending状态,但是 LoadBalancer 也是用了 NodePort 的实现方式,因为 PORT 列还保留了 NodePort 的端口。
不同 Service 类型的对比
- ClusterIP:只能在集群内部使用。
- NodePort:可以作为集群流量入口,但也有一些缺点
- 端口数量有限。默认只在“30000~32767”这个范围内随机分配,只有 2000 多个,而且都不是标准端口号。
- 需要在每个节点上都开端口,然后使用 kube-proxy 路由到真正的后端 Service,这对于大规模集群来说如果Pod频繁变更,Service 收敛速度就会变慢。
- 需要向外界暴露节点的 IP 地址,这在很多时候是不可行的,为了安全还需要在集群外再搭一个反向代理,增加了方案的复杂度。
虽然有这些缺点,但 NodePort 仍然是 Kubernetes 对外提供服务的一种简单易行的方式,在没有更好的方案出现之后,我们暂且使用这种方式。
- ExternalName: 只是一个CNAME,应用场景有限。
- LoadBalancer:依赖云厂商实现。
Service 的三个 port
YAML |
Service 的几个 port 的概念很容易混淆,它们分别是 port 、 targetPort 和 NodePort 。
- port 表示 Service 暴露的服务端口,也是客户端访问用的端口,例如 Cluster IP:port 是提供给集群内部客户访问 Service 的入口 。 需要注意的是, port 不仅是 Cluster IP 上暴露的端口,还可以是 external IP 和 Load Balancer IP 。 Service 的 port 并不监听在节点 IP 上,即无法通过节点 IP:port 的方式访问 Service 。
- NodePort 是 Kubemetes 提供给集群外部访问 Service 的入口的一种方式(另 一种方式是 Load Balancer ),所以可以通过 Node IP:nodePort 的方式提供集群外访问 Service 的入口 。 需要注意的是,我们这里说的集群外指的是 Pod 网段外,例如 Kubemetes 节点或因特网 。
- targetPort 很好理解,它是应用程序实际监听 Pod 内流量的端口,从 port 和 NodePort 上到来的数据,最终经过 Kube-proxy 流入后端 Pod 的 targetPort 进入容器。
Ingress
什么是 Ingress
Service 本质上是一个由 kube-proxy 控制的四层负载均衡,但在四层上的负载均衡功能还是太有限了,只能够依据 IP 地址和端口号做一些简单的判断和组合,而现在的绝大多数应用都是跑在七层的 HTTP/HTTPS 协议上的,有更多的高级路由条件,比如主机名、URI、请求头、证书等等。
Service 比较适合代理集群内部的服务。如果想要把服务暴露到集群外部,就只能使用 NodePort 或者 LoadBalancer 这两种方式 ,而这两种方式也都有各自的缺点,不能满足所有的场景。
Kubernetes 就引入一个新的 API 对象,在七层上做负载均衡。
Ingress 作为流量的总入口,统管集群的进出口数据
什么是 Ingress Controller
我们前面讲过 Service 本身是没有服务能力的,它只是一些 iptables 规则,真正配置、应用这些规则的实际上是节点上的 kube-proxy 组件。
同样的,Ingress 也只是声明了一些 HTTP 路由规则,相当于一份静态的配置文件,要把这些规则在集群里实施运行,还需要有另外一个组件,这就是 Ingress Controller,它的作用就相当于 Service 的 kube-proxy,能够读取、应用 Ingress 规则,处理、调度流量。
Ingress Controller 主要由社区来实现,比如我们熟悉的 Nginx, 就有 Nginx Ingress Controller。
下图比较清楚地展示了 Ingress Controller 在 Kubernetes 集群中的地位。
为什么要有 IngressClass
有了 Ingress 和 Ingress Controller,是不是就可以完美地管理集群的进出流量了呢?
最初 Kubernetes 也是这么想的,一个集群里有一个 Ingress Controller,再给它配上许多不同的 Ingress 规则,应该就可以解决请求的路由和分发问题了。
但随着 Ingress 在实践中的大量应用,很多用户发现这种用法会带来一些问题,比如:
- 由于某些原因,项目组需要引入不同的 Ingress Controller,但 Kubernetes 不允许这样做;
- Ingress 规则太多,都交给一个 Ingress Controller 处理会让它不堪重负;
- 多个 Ingress 对象没有很好的逻辑分组方式,管理和维护成本很高;
- 集群里有不同的用户,他们对 Ingress 的需求差异很大甚至有冲突,无法部署在同一个 Ingress Controller 上。
Kubernetes 又提出了一个 Ingress Class 的概念,让它插在 Ingress 和 IngressController 中间,让它来协调流量规则和控制器,解除了 Ingress 和 Ingress Controller 的强绑定关系。
现在,Kubernetes 用户可以转向管理 Ingress Class,用它来定义不同的业务逻辑分组,简化 Ingress 规则的复杂度。比如说,我们可以用 Class A 处理订单流量、Class B 处理物流流量、Class C 处理购物流量。
创建 Ingress Controller
Ingress Controller 是一个要实际干活、处理流量的应用程序,由Deployment 对象来管理。
我们部署 Kubernetes 官网维护的 Ingress NGINX Controller
我们部署 v1.6.4 的版本
Bash |
Ingress-nginx-controller 默认会创建到 ingress-nginx 的命名空间中。
使用 YAML 描述 Ingress/Ingress Class
首先用命令 kubectl api-resources 查看它们的基本信息:
Bash |
Ingress 和 Ingress Class 的 apiVersion 都是“networking.k8s.io/v1”,而且Ingress 有一个简写“ing”。
我们先来创建一个 Ingress 对象
Bash |
YAML |
在这份 Ingress 的 YAML 里,有两个关键字段:“ingressClassName”和“rules”,分别对应了命令行参数,含义还是比较好理解的。
Ingress Class 本身并没有什么实际的功能,只是起到联系 Ingress 和 Ingress Controller 的作用,所以它的定义非常简单,在“spec”里只有一个必需的字段“controller”,表示要使用哪个 Ingress Controller。
YAML |
创建 Ingress
YAML |
使用命令 kubectl describe 可以看到更详细的 Ingress 信息:
Bash |
Ingress Class 在创建 nginx-ingress-controller 时已经自动创建了。
Bash |
通过 ingress 来访问 我们部署的Nginx应用
因为实际干活的是 ingress controller,所以我们看一下 nginx-ingress-controller 对外提供服务的地址是什么
我们可以使用 ClusterIP 或者 NodePort地址 来测试,因为我们的环境里没有LoadBalance,如果是在云上部署,建议使用 LoadBalance。
我们创建的 Ingress rules 是通过域名来访问 ,因此测试时需要修改 hosts 文件
在服务器上使用 ClusterIP
使用 NodePort 地址
Case 1 HTTP
创建TLS Secret
Bash |
Case 2 HTTPS
Case 3: annotaion
Bash |
Annotations - Ingress-Nginx Controller
Service 实现原理
在 Kubernetes 集群中,每个 Node 运行一个 kube-proxy 进程。kube-proxy 负责为 Service 实现了一种 VIP(虚拟 IP)。
Service 底层技术
- userspace 0.1 0.2
- iptables 0.2
- LVS
iptables
这种模式,kube-proxy 会监视 apiserver 对 Service 对象和 Endpoints 对象的添加和移除。对每个 Service,它会添加上 iptables 规则,从而捕获到达该 Service 的 clusterIP(虚拟 IP)和端口的请求,进而将请求重定向到 Service 的一组 backend 中的某一个 Pod 上面。我们还可以使用 Pod readiness 探针 验证后端 Pod 是否可以正常工作,以便 iptables 模式下的 kube-proxy 仅看到正常运行的后端,这样做意味着可以避免将流量通过 kube-proxy 发送到已知失败的 Pod 中,所以对于线上的应用来说一定要做 readiness 探针。
iptables 模式的 kube-proxy 默认的策略是,随机选择一个后端 Pod。
比如当创建 backend Service 时,Kubernetes 会给它指派一个虚拟 IP 地址,比如 10.0.0.10。假设 Service 的端口是 1234,该 Service 会被集群中所有的 kube-proxy 实例观察到。当 kube-proxy 看到一个新的 Service,它会安装一系列的 iptables 规则,从 VIP 重定向到 Service 规则。 该 Service 规则连接到 Endpoint 规则,该 Endpoint 规则会重定向(目标 NAT)到后端的 Pod。
查看 Kube-proxy 当前运行模式
YAML |
iptables 规则分析
Bash |
kube-proxy 针对 service 流量入口专门创建了 KUBE-SERVICES 链,
Bash |
如果 访问地址 10.107.48.138/32,目标端口是 80,则会进入 KUBE-SVC-DVHFM6YVY2RW3DPQ 链
Bash |
这里利用了 iptables 的 random 模块,使连接有 33.3% 的概率进入 KUBE-SEP-3NPNUAJQIUYNQ6GJ 链,
50% 概率进入 KUBE-SEP-62HFJKX5HLTTQHKU 链,最后会匹配 KUBE-SEP-I5YFUSH4L5RO5MII 链。
因此,kube-proxy 的 iptables 模式采用随机数实现了服务的负载均衡。
KUBE-SEP-3NPNUAJQIUYNQ6GJ 链的作用是 通过 DNAT 将请求发送到 10.244.171.57 的 8080 端口。
同理,KUBE-SEP-62HFJKX5HLTTQHKU 链的作用是通过DNAT 将请求发送到 10.244.171.58 的 8080 端口。
KUBE-SEP-I5YFUSH4L5RO5MII 链的作用是通过DNAT 将请求发送到 10.244.37.239 的 8080 端口。
Bash |
分析完 ClusterIP 的 iptables 规则后,接下来看一下 NodrPort 的访问方式。NodePort 的访问入口链是 KUBE-NODEPORTS,通过节点的 31668 端口访问 NodePort,则会进入 KUBE-SVC-I277KLBDTTJWT3KA 链,接下来的跳转跟 ClusterIP 方式类似。
Bash |
综上所述, iptables 模式最主要的链是 KUBE-SERVICES 、 KUBE-SVC-*和 KUBE-SEP-* 。
- KUBE-SERVICES 链是访问集群内服务的数据包入口点,它会根据匹配到的目标 IP :port 将数据包分发到相应的 KUBE-SVC-*链;
- KUBE-SVC-*链相当于一个负载均衡器,它会将数据包平均分发到 KUBE-SEP-* 链 。 每个 KUBE-SVC-* 链后面的 KUBE-SEP-*链都和 Service 的后端 Pod 数量一样;
- KUBE-SEP-*链通过 DNAT 将连接的目的地址和端口从 Service 的 IP:port 替换为后端Pod 的 IP : port ,从而将流量转发到相应的 Pod 。
我们用下图总结 Kube-proxy iptables 模式的工作流 ,演示了从客户端 Pod 到不同节点上的服务器 Pod 的流量路径 。 客户端通过 172.16.12.100:80 连接到服务 。
Kubernetes API Server 会维护一个运行应用的后端 Pod 列表 。 每个节点上的 Kube-proxy 进程会根据 Service 和对应的 Endpoints 创建一系列的 iptables 规则 ,以将流量重定向到相应Pod ( 例如 10. 255. 255. 202: 8080 ) 。 整个过程客户端 Pod 无须感知集群的拓扑或集群内Pod 的任何 IP 信息 。
iptables 模式与 userspace 模式相比虽然在稳定性和性能上均有不小的提升,但因为 iptable 使用 NAT 完成转发, 也存在不可忽视的性能损耗 。 另外,当集群 中存在上万服务 时,Node 上的 iptables rules 会非常庞大,对管理是个不小的负担,性能还会大打折扣 。
IPVS
除了 iptables 模式之外,kubernetes 也支持 ipvs 模式,在 ipvs 模式下,kube-proxy 监视 Kubernetes 服务和端点,调用 netlink 接口相应地创建 IPVS 规则, 并定期将 IPVS 规则与 Kubernetes 服务和端点同步。该控制循环可确保 IPVS 状态与所需状态匹配。访问服务时,IPVS 将流量定向到后端 Pod 之一。
IPVS 代理模式基于类似于 iptables 模式的 netfilter 钩子函数,但是使用哈希表作为基础数据结构,并且在内核空间中工作。 所以与 iptables 模式下的 kube-proxy 相比,IPVS 模式下的 kube-proxy 重定向通信的延迟要短,并且在同步代理规则时具有更好的性能。与其他代理模式相比,IPVS 模式还支持更高的网络流量吞吐量。所以对于较大规模的集群会使用 ipvs 模式的 kube-proxy,只需要满足节点上运行 ipvs 的条件,然后我们就可以直接将 kube-proxy 的模式修改为 ipvs,如果不满足运行条件会自动降级为 iptables 模式,现在都推荐使用 ipvs 模式,可以大幅度提高 Service 性能。
IPVS 提供了更多选项来平衡后端 Pod 的流量,默认是 rr,有如下一些策略:
- rr:轮询调度
- lc:最小连接数
- dh:目标哈希
- sh:源哈希
- sed:最短期望延迟
- nq: 不排队调度
kube-proxy会监视Kubernetes Service对象和Endpoints,调用netlink接口以相应地创建ipvs规则并定期与Kubernetes Service对象和Endpoints对象同步ipvs规则,以确保ipvs状态与期望一致。访问服务时,流量将被重定向到其中一个后端Pod。
与iptables类似,ipvs基于netfilter 的 hook 功能,但使用哈希表作为底层数据结构并在内核空间中工作。这意味着ipvs可以更快地重定向流量,并且在同步代理规则时具有更好的性能。
注意: ipvs模式假定在运行kube-proxy之前在节点上都已经安装了IPVS内核模块。当kube-proxy以ipvs代理模式启动时,kube-proxy将验证节点上是否安装了IPVS模块,如果未安装,则kube-proxy将回退到iptables代理模式。
iptabales vs IPVS
iptables 和 IPVS 在刷新服务路由规则上的时延对比
Kube-proxy 切换到 IPVS 模式
YAML |
Bash |
重启 kube-proxy
YAML |
iptables 和 IPVS 模式下 ping service VIP
Endpoint
Endpoint 对象用于将服务与实际运行的Pod实例进行关联。
附录:
Kubernetes 社区维护: https://github.com/kubernetes/ingress-nginx
Nginx官方维护: https://docs.nginx.com/nginx-ingress-controller/
Traefik: https://github.com/traefik/traefik
- 点赞
- 收藏
- 关注作者
评论(0)