1. Service 为何存在
Pod的生命是有限的,如果Pod重启IP很有可能会发生变化。
如果我们的服务都是将Pod的IP地址写死,Pod的挂掉或者重启,后端其他服务也将会不可用,当然我们可以通过手动修改如nginx的upstream配置来改变后端的服务IP。
现在我们可以通过,Consul,ZooKeeper还有我们熟悉的etcd等工具,有了这些工具过后我们就可以只需要把我们的服务注册到这些服务发现中心去就可以,然后让这些工具动态的去更新Nginx的配置就可以了,我们完全不用去手工的操作了。
Kubernetes集群就为我们提供了这样的一个对象 - Service,Service是一种抽象的对象,它定义了一组Pod的逻辑集合和一个用于访问它们的策略,其实这个概念和微服务非常类似。一个Serivce下面包含的Pod集合一般是由Label Selector来决定的。
我们这样就可以不用去管后端的Pod如何变化,只需要指定Service的地址就可以了,因为我们在中间添加了一层服务发现的中间件,Pod销毁或者重启后,把这个Pod的地址注册到这个服务发现中心去。Service的这种抽象就可以帮我们达到这种解耦的目的。
2. Kubernetes的三种IP
- Node IP: Node节点的IP地址
- Cluster IP: Service的IP地址
- Pod IP: Pod的IP地址
Node IP是Kubernetes集群中节点的物理网卡IP地址(一般为内网),属于这个网络的服务器之间都可以直接通信,所以Kubernetes集群外要想访问Kubernetes集群内部的某个节点或者服务,肯定得通过Node IP进行通信(这个时候一般是通过外网IP了)
Pod IP是每个Pod的IP地址,它是Docker Engine根据docker0网桥的IP地址段进行分配的(我们这里使用的是flannel这种网络插件保证所有节点的Pod IP不会冲突)
Cluster IP是一个虚拟的IP,仅仅作用于Kubernetes Service这个对象,由Kubernetes自己来进行管理和分配地址,当然我们也无法ping这个地址,他没有一个真正的实体对象来响应,他只能结合Service Port来组成一个可以通信的服务,相当于VIP地址,来代理后端服务。
3. 创建Service
现在我们先创建一组Pod
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: httpd
labels:
app: httpd-demo
spec:
replicas: 3
template:
metadata:
labels:
app: httpd
spec:
containers:
- name: httpd
image: httpd
ports:
- containerPort: 80
上面这个Pod 我们定义了三个副本,标签为app=httpd
,下面我们来创建这个Pod的Service,通过spec.selector来指定标签为httpd的Pod。
其中被selector 选中的 Pod,就称为 Service 的Endpoints,可以使用kubectl get ep
命令查看。
[root@master testyml]# kubectl get ep
NAME ENDPOINTS AGE
fuseim.pri-ifs <none> 21d
httpd-svc 10.244.0.24:80,10.244.1.112:80,10.244.2.2:80 19h
我们可以看到httpd-svc
的后端端点IP10.244.0.24:80,10.244.1.112:80,10.244.2.2:80
,也就是httpd应用Pod的IP地址。
需要注意的是,只有处于Running状态且readlinessProbe检查通过的Pod,才会出现在Service的Endpoints列表里,同样当某个Pod出现问题时,Service也会把这个Pod从Endpoints中摘掉。
现在我们创建上面Pod集合所对应的Service
apiVersion: v1
kind: Service
metadata:
name: httpd-svc
spec:
selector:
app: httpd
ports:
- protocol: TCP
port: 8080
targetPort: 80
[root@master testyml]# kubectl create -f svc.yml
service "httpd-svc" created
[root@master testyml]# kubectl get svc --all-namespaces
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
blog mysql ClusterIP 10.101.134.172 <none> 3306/TCP 30d
blog wordpress NodePort 10.107.173.113 <none> 80:32255/TCP 30d
default httpd-svc ClusterIP 10.108.195.47 <none> 8080/TCP 3m
default kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 114d
default mysql-production ClusterIP 10.102.208.69 <none> 3306/TCP 13d
default order NodePort 10.99.99.88 <none> 8080:30080/TCP 16d
kube-ops jenkins2 NodePort 10.111.112.3 <none> 8080:30005/TCP,50000:30340/TCP 17d
kube-system kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP 114d
kube-system kubernetes-dashboard NodePort 10.108.149.176 <none> 443:30002/TCP 111d
[root@master testyml]#
现在我们看到已经创建了名为httpd-svc
的Service,分配的Cluster IP 为 10.108.195.47
,该Service还会持续的监听selector下面的Pod,会把这些Pod信息更新到一个名为httpd-svc的Endpoints对象上去,这个对象就类似于我们上面说的Pod集合了。
需要注意的是,Service能够将一个接收端口映射到任意的targetPort。 默认情况下,targetPort将被设置为与port字段相同的值。 可能更有趣的是,targetPort 可以是一个字符串,引用了 backend Pod 的一个端口的名称。 因实际指派给该端口名称的端口号,在每个 backend Pod 中可能并不相同,所以对于部署和设计 Service ,这种方式会提供更大的灵活性。
另外Service能够支持 TCP 和 UDP 协议,默认是 TCP 协议。
4. kube-proxy
在Kubernetes集群中,每个Node会运行一个kube-proxy进程, 负责为Service实现一种 VIP(虚拟 IP,就是clusterIP)的代理形式。
Service其实是由kube-proxy组件和Iptables来实现的。
现在的Kubernetes中默认是使用的iptables这种模式来代理
这种模式,kube-proxy会监视Kubernetes master对 Service 对象和 Endpoints (端点)对象的添加和移除。 对每个 Service,它会添加上 iptables 规则,从而捕获到达该 Service 的 clusterIP(虚拟 IP)和端口的请求,进而将请求重定向到 Service 的一组 backend(后端) 中的某一个个上面。 对于每个 Endpoints 对象,它也会安装 iptables 规则,这个规则会选择一个 backend Pod。
默认的策略是,随机选择一个 backend。 我们也可以实现基于客户端 IP 的会话亲和性,可以将 service.spec.sessionAffinity 的值设置为 "ClientIP" (默认值为 "None")。
另外需要了解的是如果最开始选择的 Pod 没有响应,iptables 代理能够自动地重试另一个 Pod,所以它需要依赖 readiness probes(准备就绪探测器)。
5. Service 的类型
ClusterIP:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的ServiceType。
NodePort:通过每个 Node 节点上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 :,可以从集群的外部访问一个 NodePort 服务。
LoadBalancer:使用云提供商的负载局衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务,这个需要结合具体的云厂商进行操作。
ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。
NodePort
如果设置 type 的值为 "NodePort",端口可以通过 Service 的 spec.ports[*].nodePort 字段来指定,如果不指定的话会自动生成一个端口,Kubernetes master 将从给定的配置范围内(默认:30000-32767)分配端口,每个 Node 将从该端口(每个 Node 上的同一端口)代理到 Service。
需要注意的是,Service 将能够通过 :spec.ports[].nodePort 和 spec.clusterIp:spec.ports[].port 而对外可见。
现在我们修改上面的Service,将Cluster IP 改为NodePort,指定nodeport为30088
apiVersion: v1
kind: Service
metadata:
name: httpd-svc
spec:
selector:
app: httpd
type: NodePort
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30088
现在我们更新这个Service
[root@master testyml]# kubectl apply -f svc.yml
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
service "httpd-svc" configured
[root@master testyml]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpd-svc NodePort 10.108.195.47 <none> 8080:30088/TCP 16h
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 115d
mysql-production ClusterIP 10.102.208.69 <none> 3306/TCP 14d
order NodePort 10.99.99.88 <none> 8080:30080/TCP 17d
可以看到httpd-svc
的TYPE类型已经变成了NodePort,并且自动分配了30088
的端口映射,现在我们可以通过访问Node IP
和30088
端口访问这个httpd服务
ExternalName
ExternalName 是 Service 的特例,它没有 selector,也没有定义任何的端口和 Endpoint。 对于运行在集群外部的服务,它通过返回该外部服务的别名这种方式来提供服务。
kind: Service
apiVersion: v1
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: my.database.example.com
当查询主机 my-service.prod.svc.cluster.local ,集群的 DNS 服务将返回一个值为 my.database.example.com 的 CNAME 记录。 访问这个服务的工作方式与其它的相同,唯一不同的是重定向发生在 DNS 层,而且不会进行代理或转发。 如果后续决定要将数据库迁移到 Kubernetes 集群中,可以启动对应的 Pod,增加合适的 Selector 或 Endpoint,修改 Service 的 type,完全不需要修改调用的代码,这样就完全解耦了。