一、为什么需要服务发现
在k8s集群中,应用是通过pod去部署,对于传统的应用部署在指定的物理机上,我们是只是物理机的ip,但是在k8s中,pod的生命周期是短暂的,在创建或者销毁后,其ip地址都会发生变化。我们知道应用服务最终都是要暴露给外部访问,pod的网络跟机器不是同一个段网络,这就需要服务发现pod暴露给外部访问。
我们知道使用deployment方式来部署应用,可以创建多个副本,然后这些副本提供统一的访问入口,怎么去控制流量负载均衡到这个副本中。
二、Service
在k8s集群中,服务发现与负载均衡是通过service对象来实现的。Service可以看作是一组提供相同服务的Pod对外的访问接口。一个Serivce下面包含的Pod集合一般是由Label Selector来决定的。这样我们可以不管pod的ip是否变化,相当于通过服务发现的中间解决了上面的问题。pod在创建或者销毁后,都会将pod的ip注册到服务中心(ectd),service通过label获取一组pod。
1. k8s的IP分类
- Node IP:Node节点的IP地址,即物理网卡的IP地址。每个Service都会在Node节点上开通一个端口,外部可以通过NodeIP:NodePort即可访问Service里的Pod,和我们访问服务器部署的项目一样,IP:端口/项目名
- Pod IP:Pod的IP地址,即docker容器的IP地址,此为虚拟IP地址。他是Docker Engine根据docker网桥的IP地址段进行分配的。同Service下的pod可以直接根据PodIP相互通信。不同Service下的pod在集群间pod通信要借助于 cluster ip。pod和集群外通信,要借助于node ip
- Cluster IP:Service的IP地址,此为虚拟IP地址。外部网络无法ping通,只有kubernetes集群内部访问使用。
2. 使用
创建deployment和service
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
labels:
app: my-nginx
spec:
replicas: 3
selector:
matchLabels:
app: my-nginx
template:
metadata:
labels:
app: my-nginx
spec:
containers:
- name: my-nginx
image: daocloud.io/library/nginx:1.13.0-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: my-nginx-svc
spec:
selector:
app: my-nginx
ports:
- protocol: TCP
port: 8080
targetPort: 80
root@master:/home/pod-yaml# kubectl get pods
NAME READY STATUS RESTARTS AGE
my-nginx-95cc94cff-7mjxp 1/1 Running 0 80s
my-nginx-95cc94cff-9267d 1/1 Running 0 80s
my-nginx-95cc94cff-f9lbl 1/1 Running 0 80s
root@master:/home/pod-yaml# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 40d
my-nginx-svc ClusterIP 10.101.75.115 <none> 8080/TCP 3m3s
root@master:/home/pod-yaml#
上面已经创建了标签为app=my-nginx
的三个副本pod,同时创建的service的spec.selector指定选择的标签为my-nginx
,启动后,可以看到service分配的ClusterIP为10.101.75.115
,我们可以执行curl 10.101.75.115:8080
可以访问nginx了,该service会一直监听selector选择的标签的pod,会把这些pod更新到一个名为my-nginx-svc的Endpints对象上面,也就是pod集合了。
需要注意的是,Service能够将一个接收端口映射到任意的targetPort。 默认情况下,targetPort将被设置为与port字段相同的值。 可能更有趣的是,targetPort 可以是一个字符串,引用了 backend Pod 的一个端口的名称。 因实际指派给该端口名称的端口号,在每个 backend Pod 中可能并不相同,所以对于部署和设计 Service ,这种方式会提供更大的灵活性。
另外Service能够支持 TCP 和 UDP 协议,默认是 TCP 协议。
3.如何在集群内访问service
- 通过service的ClusterIP+Port访问
- 通过访问服务名,依靠DNS解析,就是同一个namespace里的pod可以直接通过serviceName:port。不同的namespace里面,可以通过serviceName.namespace.svc.cluster.local
- 通过环境变量访问,在同一个 namespace 里的 pod 启动时,K8s 会把 service 的一些 IP 地址、端口,以及一些简单的配置,通过环境变量的方式放到 K8s 的 pod 里面。在 K8s pod 的容器启动之后,通过读取系统的环境变量比读取到 namespace 里面其他 service 配置的一个地址,或者是它的端口号等等。比如在集群的某一个 pod 里面,可以直接通过 curl $ 取到一个环境变量的值,比如取到 MY_NGINX_SERVICE_HOST 就是它的一个 IP 地址,MY_NGINX 就是刚才我们声明的 MY_NGINX,SERVICE_PORT 就是它的端口号,这样也可以请求到集群里面的 MY_NGINX 这个 service
4.kube-proxy
在k8s集群中,每个node会运行一个kube-proxy,负责为Service实现一种VIP(虚拟IP,即Cluster IP)的代理形式。Service是kube-proxy和iptables来实现。
使用iptables模式,kube-proxy会监视k8s master对service对象和Endpoints(端点)对象的添加和移除。对于每个service,它会添加上iptablesg规则,从而捕获到达该service的Cluster IP和端口的请求,从而将请求重定向到指定到service的一组backend(后端)中的某个backend上面。对于每个Endpoints对象,它也会安装iptables规则,这个规则会选择一个backend Pod。
默认的策略是,随机选择一个 backend。 我们也可以实现基于客户端 IP 的会话亲和性,可以将 service.spec.sessionAffinity 的值设置为 “ClientIP” (默认值为 “None”)。
另外需要了解的是如果最开始选择的 Pod 没有响应,iptables 代理能够自动地重试另一个 Pod,所以它需要依赖 readiness probes(准备就绪探测器)。
5.Headless Service
Headless Service:无头服务,在创建service的时候指定ClusterIP:None,pod通过service_name用DNS的方式解析到所有后端pod的IP地址,通过DNS的A记录的方式会解析到所有后端的pod地址,由客户端选择一个后端的IP地址,这个A记录会随着pod的生命周期变化,返回的A记录列表也发生变化,这样就要求客户端应用要从A记录把所有DNS返回到A记录的列表里面IP地址中,客户端自己选择一个合适的地址去访问pod
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
labels:
app: my-nginx
spec:
replicas: 3
selector:
matchLabels:
app: my-nginx
template:
metadata:
labels:
app: my-nginx
spec:
containers:
- name: my-nginx
image: daocloud.io/library/nginx:1.13.0-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: my-headless
labels:
app: my-headless
spec:
selector:
app: my-nginx
clusterIP: None
ports:
- protocol: TCP
port: 8090
targetPort: 80
root@master:/home/pod-yaml# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 41d
my-headless ClusterIP None <none> 8090/TCP 6m20s
root@master:/home/pod-yaml# kubectl describe svc my-headless
Name: my-headless
Namespace: default
Labels: app=my-headless
Annotations: <none>
Selector: app=my-nginx
Type: ClusterIP
IP: None
Port: <unset> 8090/TCP
TargetPort: 80/TCP
Endpoints: 10.244.1.36:80,10.244.2.203:80,10.244.2.204:80
Session Affinity: None
Events: <none>
从这里可以看到headless service有三个IP地址,可以随意访问任意一个。
6.Service类型
ClusterIP
通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的ServiceType
NodePort
NodePort 的方式就是在集群的 node 上面(即集群的节点的宿主机上面)去暴露节点上的一个端口,这样相当于在节点的一个端口上面访问到之后就会再去做一层转发,转发到虚拟的 IP 地址上面,就是刚刚宿主机上面 service 虚拟 IP 地址。
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
labels:
app: my-nginx
spec:
replicas: 3
selector:
matchLabels:
app: my-nginx
template:
metadata:
labels:
app: my-nginx
spec:
containers:
- name: my-nginx
image: daocloud.io/library/nginx:1.13.0-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: my-nginx-svc
spec:
selector:
app: my-nginx
type: NodePort
ports:
- protocol: TCP
port: 8080
targetPort: 80
nodePort: 30088
root@master:/home/pod-yaml# kubectl get pods
NAME READY STATUS RESTARTS AGE
my-nginx-95cc94cff-8rdsw 1/1 Running 0 11s
my-nginx-95cc94cff-bb58v 1/1 Running 0 11s
my-nginx-95cc94cff-v6r2t 1/1 Running 0 11s
root@master:/home/pod-yaml# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 41d
my-nginx-svc NodePort 10.111.15.217 <none> 8080:30088/TCP 15s
root@master:/home/pod-yaml#
可以看my-nginx-svc的type类型为NodePort,并且分配了30088的端口映射,可以通过Node IP+30088去访问这个服务
LoadBalancer
使用云提供商的负载局衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务,这个需要结合具体的云厂商进行操作。
ExternalName
通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持
ExternalName 是 Service 的特例,它没有 selector,也没有定义任何的端口和 Endpoint。 对于运行在集群外部的服务,它通过返回该外部服务的别名这种方式来提供服务。
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
type: ExternalName
externalName: my.database.example.com
root@master:/home/pod-yaml# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 41d
my-service ExternalName <none> my.database.example.com <none> 10m
当查询主机 my-service.svc.cluster.local ,集群的 DNS 服务将返回一个值为 my.database.example.com 的 CNAME 记录。 访问这个服务的工作方式与其它的相同,唯一不同的是重定向发生在 DNS 层,而且不会进行代理或转发。 如果后续决定要将数据库迁移到 Kubernetes 集群中,可以启动对应的 Pod,增加合适的 Selector 或 Endpoint,修改 Service 的 type,完全不需要修改调用的代码,这样就完全解耦了。