服务发现概述

通常,稍有规模的系统架构需要抽象楚相当数量的服务,这些服务间可能存在复杂的依赖关系和通信模型,考虑到容器编排环境的动态特性,让客户端获知服务端的地址便成了难题之一。kubernetes系统上的Service为pod中的服务类应用提供了一个固定的访问入口,但pod客户端中的应用还需要借助服务发现机制获取特定服务的IP和端口。

简单来说,服务发现就是服务或者应用之间相互定位的过程,它并非什么新概念,传统的单体应用动态性不强,更新和重新发布频度较低,通常以月甚至以年计,几乎无须进行自动伸缩,因此服务发现的概念无须显性强调。通常,传统的单体应用网络位置发生变化时,由IT人员手动更新一下相关的配置文件基本就能解决问题,但在遵循微服务架构设计理念的场景中,应用被拆分众多小服务,各服务实例按需创建且变动频繁,配置信息基本无法事先写入配置文件中并及时跟踪反应动态变化,服务发现的重要性便随之凸显。

服务发现机制的基本实现,一般是事前部署好一个网络位置较为稳定的服务注册中心(也叫服务总线),由服务提供者(服务端)向注册中心注册自己的位置信息,并在变动后即时予以更新,相应地,服务消费方周期性地从注册中心获取服务提供者的最新位置信息,从而发现要访问的目标服务资源。复杂的服务发现机制还会让服务提供者其描述信息、状态信息及资源使用信息等,以供消费者实现更为复杂的服务选择逻辑。

实践中,根据其发现过程的实现方式,服务发现还可分为两种类型:客户端发现和服务端发现。

  客户端发现:由客户端到服务注册中心发现其依赖的服务的相关信息,因此,它需要内置特定的服务发现程序和发现逻辑。
  服务端发现:这种方式额外要用到一个称为中央路由器或服务均衡器的组件;服务消费者将请求发往中央路由器或者负载均衡器,由它们负载查询服务注册中心获取服务提供者的位置信息,并将服务消费者的请求转发给服务提供者。

由此可见,服务注册中是服务发现得以落地的核心组件。事实上,DNS可以算是最为原始的服务发现系统之一,但在服务的动态性很强的场景中,DNS记录的传播速度可能会跟不上服务的变更速度,因此它并不适用于微服务环境中。

在传统实践中,常见的服务注册中心是ZooKeeper和etcd等分布式键值存储系统,它们可提供基本的数据存储功能,但距离实现完整的服务发现机制还有大量的二次开发任务需要完成,而且,它们更注重数据一致性而不得不弱化可用性,这背离了微服务发现场景中更注重服务可用性的需求。

云原生服务发现

如果你想要在应用程序中使用 Kubernetes API 进行服务发现,则可以查询 API 服务器用于匹配 EndpointSlices。 只要服务中的这组 Pod 发生变化,Kubernetes 就会为服务更新 EndpointSlices。

对于非本机应用程序,Kubernetes 提供了在应用程序和后端 Pod 之间放置网络端口或负载均衡器的方法。

服务发现模式

Kubernetes 支持两种基本的服务发现模式 —— 环境变量和 DNS。

环境变量

当 Pod 运行在某 Node 上时,kubelet 会为每个活跃的 Service 添加一组环境变量。 kubelet 为 Pod 添加环境变量 {SVCNAME}_SERVICE_HOST 和 {SVCNAME}_SERVICE_PORT。 这里 Service 的名称被转为大写字母,横线被转换成下划线。 它还支持与 Docker Engine 的 "legacy container links" 特性兼容的变量

Kubernetes Service环境变量

kubernetes为每个Service资源生成包括以下形式的环境变量在内的一系列环境变量,在同一名称空间中创建的pod对象都会自动拥有这些变量。
{SVCNAME}_SERVICE_HOST
{SVCNAME}_SERVICE_PORT

Docker Link形式的环境变量

Docker使用--link选项实现容器连接时所设置的环境变量形式。在创建pod对象时,kubernetes也会把与此形式兼容的一系列环境变量注入pod对象中。

基于环境变量的服务发现功能简单、易用,但存在一定局限,例如只有那些与新建pod对象在同一名称空间中且事先存在的Service对象的信息才会以环境变量形式注入,而那些不在同一名称空间,或者在pod资源创建之后才创建的Service对象的相关环境变量则不会被添加。
REDIS_PRIMARY_SERVICE_HOST=10.0.0.11
REDIS_PRIMARY_SERVICE_PORT=6379
REDIS_PRIMARY_PORT=tcp://10.0.0.11:6379
REDIS_PRIMARY_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_PRIMARY_PORT_6379_TCP_PROTO=tcp
REDIS_PRIMARY_PORT_6379_TCP_PORT=6379
REDIS_PRIMARY_PORT_6379_TCP_ADDR=10.0.0.11

基于DNS的服务发现

名称解析和服务发现是kubernetes系统许多功能得以实现的基础服务,ClusterDNS通常是集群安装完成后应该立即部署的附加组件。kubernetes集群上的每个Service资源对象在创建时都会被自动指派一个遵循<service>.<ns>.svc.<zone>格式的名称,并由ClusterDNS为该名称自动生成资源记录,service、ns和zone分别代表服务的名称、名称空间的名称和集群的域名。例如demoapp-svc的DNS名称为demoapp-svc.default.svc.cluster.local.,其中cluster.local.是未明确指定域名后缀 的集群默认使用的域名。

无论使用kubeDNS还是CoreDNS,它们提供了基于DNS服务发现解决方案都会负责为该DNS名称解析相应的资源记录类型以实现服务发现。以拥有ClusterIP的多种Service资源类型(ClusterIP、NodePort和LoadBalancer)为例,每个service对象都会有一下3个类型的DNS资源记录。
根据ClusterIP的地址类型,为IPv4生成A记录,为IPv6生成AAAA记录。

<service>.<ns>.svc.<zone>.<ttl> IN A <cluster-ip>
<service>.<ns>.svc.<zone>.<ttl> IN AAAA <cluster-ip>
为每个定义了名称的端口生成一个SRV记录,未命名的端口号则不具该记录。

_<port>._<proto>.<service>.<ns>.svc.<zone>.<ttl> IN SRV <weight> <priority> <port-number> <service>.<ns>.svc.<zone>.
对于每个给定的A记录(例如a.b.c.d)或AAAA记录(例如a1a2a3a4:b1b2b3b4:c1c2c3c4:d1d2d3d4:e1e2e3e4:f1f2f3f4:g1g2g3g4:h1h2h3h4)都要生成PTR记录,它们各自的格式如下:

<d>.<c>.<b>.<a>.in-addr-arpa.  <ttl> IN PTR <service>.<ns>.svc.<zone>.
h4.h3.h2.h1.g4.g3.g2.g1.e4.e3.e2.e1.d4.d3.d2.d1.c4.c3.c2.c1.b4.b3.b2.b1.a4.a3.a2.a1.ipv6.arpa <ttl> IN PTR <service>.<ns>.svc.<zone>.
例如,前面在defalut名称空间中创建的Service对象demoapp-svc的地址为10.97.72.1,且为TCP协议的80端口取名http,对于默认的cluster.local域名来说,它会拥有如下3个DNS资源记录。

  A记录:demoapp-svc.default.svc.cluster.local.30 IN A 10.97.72.1
  SRV记录:_http._tcp.demoapp-svc.default.svc.cluster.local.30 IN SRV 0 100 80 demoapp-svc.default.svc.cluster.local.
  PTR记录:1.72.97.10.in-addr.arpa.30 IN PTR demoapp-svc.default.svc.cluster.local。
kubelet会为创建的每一个容器在/etc/resolv.conf配置文件中生成DNS查询客户端依赖的必要配置,相关的配置信息源自kubelet的配置参数。各容器的DNS服务器由clusterDNS参数的值设定,它的取值为kube-system名称空间中的Service对象kube-dns的ClusterIP,默认为10.96.0.10,而DNS搜索域的值由clusterDomain参数的值设定,若部署kubernetes集群时未特别指定,其值为cluster.local、svc.cluster.local和NAMESPACENAME.svc.cluster.local。示例:
search default.svc.cluster.local svc.cluster.local
cluster.local
options ndots:5
上述search参数中指定的DNS个搜索域是以次序指定的几个域名后缀,它们各自的域名如下所示:

  <ns>.svc.<zone>:附带有特定名称空间 的域名,例如default.svc.cluster.local。
  svc.<zone>:附带了kubernetes表示Service专用子域svc的域名。例如svc.cluster.local。
  <zone>:集群本地域名,例如cluster.local。
各容器能够直接向集群上的ClusterDNS发起服务名称和端口名称解析请求完成服务发现,各名称也支持短格式,由搜索域自动补齐相关的后缀。
为了减少搜索次数,无论是否处于同一名称空间,客户端都可以直接使用FQDN格式的名称解析Service名称和端口名称,这也是在某应用的配置文件中引用其他服务时建议遵循的方式。

Pod的DNS解析策略与配置

kubernetes还支持在单个pod资源规范上自定义解析策略和配置,它们分别使用spec.dnsPolicy和spec.dnsConfig进行定义,并组合生效。目前,kubernetes支持如下DNS解析策略,它们定义在spec.dnsPolicy字段上。

  Default:从运行所在的节点继承DNS名称解析相关的配置。
  ClusterFirst:在集群DNS服务器上解析集群域内的名称,其它域名的解析则交由从节点继承而来的上游名称服务器。
  ClusterFirstWithHostNet:专用于在设置了hostNetwork的pod对象上使用的ClusterFirst策略,任何配置了hostNetwork的pod对象都应该显式使用该策略。
  None:用户忽略kubernetes集群的默认设定,而仅使用由dnsConfig自定义的配置。

pod资源的自定义DNS配置要通过嵌套在spec.dnsConfig字段中的如下几个字段进行,它们的最终生效结果要结合dnsPolicy的定义生成。

  nameservers <[]string>:DNS名称服务器列表,它附加在由dnsPolicy生成的DNS名称服务器之后。
  searches <[]string>:DNS名称解析时的搜索域,它附加在dnsPolicy生成的搜索域之后。
  options <[]Object>:DNS解析选项列表,它将会同dnsPolicy生成的解析选项合并成最终生效的定义。

示例

下面配置清单示例(pod-with-dnspolicy.yaml)中定义的Pod资源完全使用自定义的配置,它通过将dnsPolicy设置为None而拒绝从节点继承DNS配置信息,并在dnsConfig中自定义了要使用的DNS服务、搜索域和DNS选项。
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-dnspolicy
  namespace: default
spec:
  containers:
  - name: demo
    image: ikubernetes/demoapp:v1.0
    imagePullPolicy: IfNotPresent
  dnsPolicy: None
  dnsConfig:
    nameservers:
    - 10.96.0.10
    - 223.5.5.5
    - 223.6.6.6
    searches:
    - svc.cluster.local
    - cluster.local
    - ilinux.io
    options:
    - name: ndots
    value: "5"
将上述配置清单中定义的Pod资源创建到集群之上,它最终会生成类似如下内容的/etc/resolv.conf配置文件。
nameserver 10.96.0.10
nameserver 223.5.5.5
nameserver 223.6.6.6
search svc.cluster.local cluster.local ilinux.io
options ndots:5
上面配置中的搜索域要求,即便是客户端与目标服务位于同一名称空间,也要求在短格式的服务名称上显式指定其所处的名称空间。

配置CoreDNS

CoreDNS是高度模块化的DNS服务器,几乎全部功能均由可插拔插件实现,CoreDNS调用的插件及相关的配置定义在称为Corefile的配置文件中,CoreDNS主要用于定义各服务器监听地址和端口授权解析以及加载的插件等。配置格式如下:
ZONE:[PORT] {
     [PLUGIN]...
}

参数说明

  • ZONE:定义该服务器授权解析的区域,它监听由PORT指定的端口。
  • PLUGIN:定义要加载的插件,每个插件可能存在一系列属性,而每个属性还可能存在可配置的参数。

示例

apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |
    .:53 {
      errors # 将错误日志发往标准输出stdout
      health {
        lameduck 5s
      } # 通过http://localhost:8080/health报告健康状态
      ready # 待所有插件就绪后通过8181端口响应“200 OK”以报告就绪状态
      kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure
        fallthrough in-addr.arpa ip6.arpa
        ttl 30
      } # Kubernetes系统的本地区域及专用的名称解析配置
    prometheus :9153 # 通过http://localhost:9153/metrics输出指标数据
    forward . /etc/resolv.conf # 非Kubernetes本地域名的解析转发逻辑
    cache 30 # 缓存时长
    loop # 探测转发循环并终止其过程
    reload # Corefile内容改变时自动重载配置信息
    loadbalance # A、AAAA或MX记录的负载均衡器,使用round-robin算法
  }
在该配置文件中,专用于kubernetes系统上的名称解析服务由名为kubernetes的插件进行定义,该插件负载处理指定的权威区域中的所有查询,例如上面示例中的正向解析区域cluster.local,以及反向解析区域in-addr.arpa和ipv6.arpa。该插件支持多个配置参数,例如endpoint、tls、kubeconfig、namespace、labels、pods、ttl和fallthrough等。上面示例中用到的3个参数的功能如下:

  pods POD-MODE:设置用于处理基于pod IP地址的A记录的工作模式,以便在直接同pod建立SSL通信时验证证书信息;默认值为disable,表示不处理pod请求,总是相应NXDOMAIN;在其它可用值中,insecure表示直接响应A记录而无须向kubernetes进行校验,目标在于兼容kube-dns;而verified表示仅在指定名称空间中存在一个与A记录中的IP地址相匹配的pod对象时才会将结果响应给客户端。
  fallthrough [ZONES...]:常规情况下,该插件的权威区域解析结果为NXDOMAIN时即为最终结果,而该参数允许将该响应的请求继续转为后续的其它插件处理;省略指定目标区域时表示生效于所有区域,否则,将仅生效于指定的区域。
  ttl:自定义响应结果的可缓存时长,默认为5秒,可用值范围为[0,3600]。
那些非由kubernetes插件所负载解析的本地匹配的名称,将由forward插件定义的方式转发给其它DNS服务器进行解析,示例中的配置表示将根区域的解析请求转发给主机配置文件/etc/resolv.conf中指定的DNS服务器进行。若要将请求直接转发给指定的DNS服务器,则将该文件路径替换为目标DNS服务的IP地址即可,多个IP地址之间以空白字符分隔。例如,下面的配置示例表示将除了ilinux.io区域之外的其它请求转给223.5.5.5或223.6.6.6进行解析。
. {
   forward . 223.5.5.5 223.6.6.6 {
     except ilinux.io
   }
}

参考文档

https://coredns.io/plugins/