前两篇学习了Kubernetes的架构和最重要的单元Pod,了解了POD的运行原理以及POD内部容器的通讯以及共享存储,每个POD有自己的内部IP,但POD不能直接对外提供服务,因为每个实例(Pod)的IP地址由网络插件动态随机分配(Pod重启后IP地址会改变)。为屏蔽这些后端实例的动态变化和对多实例的负载均衡,引入了Service这个资源对象。这篇将回答关于Service的一系列问题,深入浅出的加深对Service的理解。
问题一:为什么需要Service
我们知道每个Pod都会被分配一个单独的IP地址,而且每个Pod都提供了一个独立的Endpoint(Pod IP+ContainerPort)以被客户端访问,但这种访问仅限于集群内部,外部没法访问集群内部的IP地址,而且最重要的是Pod随时会重新部署在另一台机器上,IP地址也会改变,因此需要一种能对外提供服务,而且有负载均衡能力保证多个POD作为一个集群对外提供统一服务,这就是Service的作用,是发现后端pod服务;是为一组具有相同功能的容器应用提供一个统一的入口地址;是将请求进行负载分发到后端的各个容器应用上的控制器。如下图所示,Service与其后端Pod副本集群之间则是通过Label Selector来实现"无缝对接"。而RC的作用实际上是保证Service 的服务能力和服务质量处于预期的标准。
问题二:Service是如何和后端POD进行关联
service通过selector和pod建立关联,k8s会根据service关联到pod的podIP信息组合成一个endpoint,若service定义中没有selector字段,service被创建时,endpoint controller不会自动创建endpoint。如下yaml文件定义了一个Nginx的服务,selector定义了Label为nginx的Pod。
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
labels:
app: nginx
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
selector:
app: nginx
Service与后端的关联实际上是通过K8S的负载均衡器KubeProxy代理到后端的Pod上的,这个负载均衡器代理的EndPoint是动态变化的,由endpoints controller的watcher service负责监控Pod和Service的变化,维护对应的Endpoint信息。kube-proxy根据Service和Endpoint来维护本地的路由规则。当Endpoint发生变化,即Service以及关联的pod发生变化,kube-proxy都会在每个节点上更新iptables,实现一层负载均衡。
问题三:Service是如何对外发布的
现在很多业务都是以微服务方式部署,采用微服务架构时,作为服务所有者,除了实现业务逻辑以外,还需要考虑如何把服务发布到k8s集群或者集群外部,使这些服务能够被k8s集群内的应用、其他k8s集群的应用以及外部应用使用。因此k8s提供了灵活的服务发布方式,用户可以通过ServiceType来指定如何来发布服务,类型有以下几种:
第一种,ClusterIP:提供一个集群内部的虚拟IP以供Pod访问(service默认类型)。
如下定义:
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 8080
targetPort: 9376
服务结构如下:
访问service的请求可能来自集群内部的Pod,也可能来自集群外部。
这里涉及到几个概念如下,
clusterip
虚拟IP地址,没有网络设备承载这个地址。它是一个虚拟地址,由kube-proxy使用iptables规则重新定向到其本地端口,再均衡到后端Pod。当kube-proxy发现一个新的service后,它会在本地节点打开一个任意端口,创建相应的iptables规则,重定向服务的clusterIP和port到这个新建的端口,开始接受到达这个服务的连接。运行在每个Node上的kube-proxy进程就是一个智能的软件负载均衡器,它负责把对Service的请求转发到后端的某个pod实例上,并在内部实现服务的负载均衡与会话保持机制。但K8S使用了一种很巧妙的设计:Service部署不是共用一个负载均衡器的IP地址,而是每个Service分配了一个全局唯一的虚拟IP地址,即ClusterIp,这样每个服务就变成了具备唯一IP地址的“通信节点”,服务调用就变成了最基础的TCP网络通信问题。ClusterIP仅仅作用于Kubernetes Service这个对象,并由Kubernetes管理和分配IP地址(来源于ClusterIP地址池)。ClusterIP无法被Ping,因为没有一个“实体网络对象"来响应。
Port
这里的port表示service暴露在clusterIP上的端口,clusterIP:Port 是提供给集群内部访问kubernetes服务的入口。
targetPort
targetPort是pod上的端口,从port和nodePort上到来的数据最终经过kube-proxy流入到后端pod的targetPort上进入容器。
第二种,NodePort:在每个Node上打开一个端口以供外部访问
通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。外部系统只要用任意Node的IP地址+具体的NodePort端口即可访问服务,在任意node上运行netstat服务,我们可以看到NodePort端口被监听。NodePort类型的服务如下定义:
apiVersion: v1
kind: Service
metadata:
labels:
name: app1
name: app1
namespace: default
spec:
type: NodePort
ports:
- port: 8080
targetPort: 8080
nodePort: 30062
selector:
name: app1
如果设置 type 的值为 "NodePort",Kubernetes master 将从给定的配置范围内(默认:30000-32767)分配端口,每个 Node 将从该端口(每个 Node 上的同一端口)代理到 Service。该端口将通过 Service 的 spec.ports[*].nodePort 字段被指定。但NodePort还没有完全解决外部访问Service的所有问题,比如负载均衡的问题,假如说我们集群中有10个Node,则此时最好有一个负载均衡,外部的请求只需要访问此负载均衡器的IP地址,由负载均衡负责转发流量到后面某个NodePort上,如下图
第三种,LoadBalancer:使用外部的LoadBalancer来实现外部访问
同上图,只是使用第三方的负载均衡机制实现LoadBalance,现在很多的云提供商都有此服务,比如,google,amazon,阿里云等等。LoadBalancer方式定义如下:
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "my-service"
},
"spec": {
"selector": {
"app": "MyApp"
},
"ports": [
{
"protocol": "TCP",
"port": 80,
"targetPort": 9376,
"nodePort": 30061
}
],
"clusterIP": "10.0.171.239",
"loadBalancerIP": "78.11.24.19",
"type": "LoadBalancer"
},
"status": {
"loadBalancer": {
"ingress": [
{
"ip": "146.148.47.155"
}
]
} }
第三种,ExternalName
将服务通过DNS CNAME记录方式转发到指定的域名(通过 spec.externlName 设定)。需要kube-dns版本在1.7以上。定义方式如下:
kind: Service
apiVersion: v1
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: my.database.example.com
问题四:Service是如何发现的
Kubernetes 支持2种基本的服务发现模式 —— 环境变量和 DNS。
方式一:环境变量
当Pod运行在NOde上,kubelet会为每个活跃的Service添加一组环境变量。它同时支持Docker links兼容变量,简单的{SVCNAME}_SERVICE_HOST 和 {SVCNAME}_SERVICE_PORT 变量,这里 Service 的名称需大写,横线被转换成下划线。举个例子,一个名称为 "redis-master" 的 Service 暴露了 TCP 端口 6379,同时给它分配了 Cluster IP 地址 10.0.0.11,这个 Service 生成了如下环境变量:
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=6379REDIS_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" 的端口号。
问题五:Ingress是什么作用
Service虽然解决了服务发现和负载均衡的问题,但它在使用上还是有一些限制,
比如- 只支持4层负载均衡,没有7层功能。对外访问的时候,NodePort类型需要在外部搭建额外的负载均衡,而LoadBalancer要求kubernetes必须跑在支持的cloud provider上面。Ingress就是为了解决这些限制而引入的新资源,主要用来将服务暴露到cluster外面,并且可以自定义服务的访问策略。Ingress不是某种产品、组件的名称,它应该是kubernetes向集群外暴露服务的一种思路、技术,用户完全可以根据这种思路提供自己的Ingress实现,当然kubernetes提供了默认Ingress实现还有其它第三方实现,一般无需自己开发。它的思路是这样的,首先在集群内运行一个服务或者pod也可以是容器,不管是什么它至少应该有一个外网可以访问的IP,至少向外网开放一个端口号,让它充当反向代理服务器。当外网想要访问集群内service时,只需访问这个反向代理服务器并指定相关参数,代理服务器根据请求参数并结合内部规则,将请求转发到service。这种思路与LoadBalancer的不同之处是它就位于集群内,而LoadBalancer位于集群外。与NodePort的不同之处是集群只向外暴露一个服务或者pod等,而NodePort是暴露全部service。Kubernetes用nginx实现反向代理服务器,称为Ingress Controller,是pod类型资源。同时提供了Ingress类型对象,通过创建Ingress对象配置nginx反向代理服务器的转发规则。Nginx反向代理服务器收到来自外网的请求后,用请求的URL地址、请求头字段区别不同service,然后转发请求。
部署Ingress Controller
在Kubernetes中,Ingress Controller典型是pod类型资源,其部署方式与普通pod相同,通过Deployment、DaemonSet等副本控制器部署,其中更值推荐的是DaemonSet方式。Ingress Controller需要部署在具备连通外网能力的节点上,首先在目标节点打上Ingress Controller专用标签,然后在DaemonSet的配置文件中配置节点选择器选中此类标签,控制pod实例可以部署的节点,通过为节点增减相关标签控制Ingress Controller的pod实例个数。Ingress Controller一般占用两个节点主机端口,http用80,https用443。详细参考:https://git.k8s.io/ingress-nginx/README.md。外网通过http://节点外网ip:80/…或者https://节点外网ip:80/…就可以访问内部服务了,当前首先需要创建Ingress对象配置访问策略。
创建Ingress对象
本节通过创建各种Ingress对象,展示Ingress的各种典型用法。
场景一:Single Service Ingress
配置文件:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test-ingress
spec:
backend:
serviceName: testsvc
servicePort: 80
创建的对象如下:
$ kubectl get ing
NAME RULE BACKEND ADDRESS
test-ingress - testsvc:80 107.178.254.228
以上配置中没有具体的rule,所以诸如http(s)://107.178.254.228/xxx之类的请求都转发到testsvc的80端口。
场景二:基于URL转发
假如打算实现如下目标:
foo.bar.com -> 178.91.123.132 -> / foo s1:80
/ bar s2:80
其中foo.bar.com是http请求体头部中的host字段,178.91.123.132是Ingress Controller外网地址,当请求路径与/foo匹配时转发到s1服务的80端口,当与/bar匹配时转发到s2服务的80端口,其最核心逻辑是用URL区分不同服务。
配置如下:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: foo.bar.com
http:
paths:
- path: /foo
backend:
serviceName: s1
servicePort: 80
- path: /bar
backend:
serviceName: s2
servicePort: 80
创建对象:
$ kubectl get ing
NAME RULE BACKEND ADDRESS
test -
foo.bar.com
/foo s1:80
/bar s2:80
场景三:基于名称的虚拟主机
实现如下目标:
foo.bar.com --| |-> foo.bar.com s1:80
| 178.91.123.132 |
bar.foo.com --| |-> bar.foo.com s2:8
这种方式的核心逻辑是用http请求中的host字段区分不同服务,而不是URL。如host: foo.bar.com的请求被转发到s1服务80端口,如host: bar.foo.com的请求被转发到s2服务80端口。
配置如下:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test
spec:
rules:
- host: foo.bar.com
http:
paths:
- backend:
serviceName: s1
servicePort: 80
- host: bar.foo.com
http:
paths:
- backend:
serviceName: s2
servicePort: 80
写在最后
这篇文章的内容绝大部分来自互联网博客和Kubernetes权威指南,主要的目的是梳理关于Kubernetes Service的各方面细节。参考的文章如下:
https://blog.csdn.net/dkfajsldfsdfsd/article/details/81224905(Kubernetes之Ingeress)
https://blog.csdn.net/liyingke112/article/details/76022267(使用kube-proxy让外部网络访问K8S service的ClusterIP)
https://blog.csdn.net/qq_21816375/article/details/81507196
https://blog.csdn.net/liukuan73/article/details/82585732(Kubernetes详解)《Kubernetes权威指南》