一、为什么需要服务发现

在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。

有k8s还需要注册中心吗 有k8s还需要服务发现吗_IP

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(准备就绪探测器)。

有k8s还需要注册中心吗 有k8s还需要服务发现吗_nginx_02

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,完全不需要修改调用的代码,这样就完全解耦了。