1. 服务发现机制与类型
简单来说,服务发现就是服务或者应用之间互相定位的过程。不过,服务发现并非什么新概念,传统的单体应用架构时代也会用到,只不过单体应用的动态性不强,更新和重新发布频度较低,通常以月甚至以年计,基本不会进行自动伸缩,因此服务发现的概念无须显性强调。在传统的单体应用网络位置发生变化时,由IT运维人员手工更新一下相关的配置文件基本就能解决问题。但在微服务应用场景中,应用被拆分成众多的小服务,它们按需创建且变动频繁,配置信息基本无法事先写入配置文件中并及时跟踪反映动态变化,服务发现的重要性便随之凸显。
服务发现机制的基本实现,一般是事先部署好一个网络位置较为稳定的服务注册中心(也称为服务总线),服务提供者(服务端)向注册中心注册自己的位置信息,并在变动后及时予以更新,相应地,服务消费者则周期性地从注册中心获取服务提供者的最新位置信息从而“发现”要访问的目标服务资源。复杂的服务发现机制还能够让服务提供者提供其描述信息、状态信息及资源使用信息等,以供消费者实现更为复杂的服务选择逻辑。
实践中,根据其发现过程的实现方式,服务发现还可分为两种类型:客户端发现和服务端发现。
客户端发现:由客户端到服务注册中心发现其依赖到的服务的相关信息,因此,它需要内置特定的服务发现程序和发现逻辑。
服务端发现:这种方式额外要用到一个称为*路由器或服务均衡器的组件;服务消费者将请求发往*路由器或者负载均衡器,由它们负责查询服务注册中心获取服务提供者的位置信息,并将服务消费者的请求转发给服务提供者。
由此可见,服务注册中心是服务发现得以落地的核心组件。事实上,DNS可以算是最为原始的服务发现系统之一,不过,在服务的动态性很强的场景中,DNS记录的传播速度可能会跟不上服务的变更速度,因此它不并适用于微服务环境。另外,传统实践中,常见的服务注册中心是ZooKeeper和etcd等分布式键值存储系统。不过,它们只能提供基本的数据存储功能,距离实现完整的服务发现机制还有大量的二次开发任务需要完成。另外,它们更注重数据一致性,这与有着更高的服务可用性要求的微服务发现场景中的需求不太相吻合。
Netflix的Eureka是目前较流行的服务发现系统之一,它是专门开发用来实现服务发现的系统,以可用性目前为先,可以在多种故障期间保持服务发现和服务注册的功能可用,其设计原则遵从“存在少量的错误数据,总比完全不可用要好”。另一个同级别的实现是Consul,它是由HashiCorp公司提供的商业产品,不过还有一个开源基础版本提供。它于服务发现的基础功能之外还提供了多数据中心的部署能力等一众出色的特性。
尽管传统的DNS系统不适于微服务环境中的服务发现,但SkyDNS项目(后来称kubedns)却是一个有趣的实现,它结合古老的DNS技术和时髦的Go语言、Raft算法并构建于etcd存储系统之上,为Kubernetes系统实现了一种服务发现机制。Service资源为Kubernetes提供了一个较为稳定的抽象层,这有点类似于服务端发现的方式,于是也就不存在DNS服务的时间窗口的问题。
Kubernetes自1.3版本开始,其用于服务发现的DNS更新为了kubeDNS,而类似的另一个基于较新的DNS的服务发现项目是由CNCF(Cloud Native Computing Foundation)孵化的CoreDNS,它基于Go语言开发,通过串接一组实现DNS功能的插件的插件链进行工作。自Kubernetes 1.11版本起,CoreDNS取代kubeDNS成为默认的DNS附件。不过,Kubernetes依然支持使用环境变量进行服务发现。
2. 服务发现方式:环境变量
创建Pod资源时,kubelet会将其所属名称空间内的每个活动的Service对象以一系列环境变量的形式注入其中。它支持使用Kubernetes Service环境变量以及与Docker的links兼容的变量。
(1)Kubernetes Service环境变量
Kubernetes为每个Service资源生成包括以下形式的环境变量在内一系列环境变量,在同一名称空间中创建的Pod对象都会自动拥有这些变量:
{SVCNAME}_SERVICE_HOST
{SVCNAME}_SERVICE_PORT
注意:如果SVCNAME中使用了连接线,kubernetes会在定义为环境变量时将其转换为下划线。
(2) Docker Link形式的环境变量
Docker使用--link选项实现容器连接时所设置的环境变量形式,具体使用方式请参考Docker的相关文档。在创建Pod对象时,Kubernetes也会把与此形式兼容的一系列环境变量注入到Pod对象中。
例如,在Service资源myapp-svc创建后创建的Pod对象中查看可用的环境变量,其中以MYAPP_SVC_SERVICE开头的为kubernetes Service环境变量,名称中不包含“SERVICE”字符串的环境变量为Docker Link形式的环境变量。
/ # printenv | grep MYAPP
MYAPP_SVC_PORT_80_TCP_ADDR=10.107.208.93
MYAPP_SVC_PORT_80_TCP_PORT=80
MYAPP_SVC_PORT_80_TCP_PROTO=tcp
MYAPP_SVC_PORT_80_TCP=tcp://10.107.208.93:80
MYAPP_SVC_SERVICE_HOST=10.107.208.93
MYAPP_SVC_SERVICE_PORT=80
MYAPP_SVC_PORT=tcp://10.107.208.93:80
基于环境变量的服务发现功能简单、易用,但存在一定的局限,例如仅那些与创建的Pod对象在同一名称空间中且事先存在的Service对象的信息才会以环境变量形式注入,那些非同一名称空间,或者是Pod资源创建之后才创建的Service对象的相关环境变量则不会被添加。幸而,基于DNS的发现机制并不存在此类限制。
3. ClusterDNS和服务发现
Kubernetes系统之上用于名称解析和服务发现的ClusterDNS是集群的核心附件之一,集群中创建的每个Service对象,都会由其自动生成相关的资源记录。默认情况下,集群内各Pod资源会自动配置其作为名称解析服务器,并在其dns搜索列表中包含它所属名称空间的域名后缀。
无论使用kubeDNS还是CoreDNS,它们提供的基于DNS的服务发现解决方案都会负责解析以下资源记录(Resource Record)类型以实现服务发现。
(1)拥有ClusterIP的Service资源,要具有以下类型的资源记录
A记录:<service>.<ns>.svc.<zone>. <ttl> IN A <cluster-ip>
SRV记录:_<port>._<proto>.<service>.<ns>.svc.<zone>. <ttl> IN SRV <weight> <priority> <port-number> <service>.<ns>.svc.<zone>.
PTR记录:<d>.<c>.<b>.<a>.in-addr.arpa. <ttl> IN PTR <service>.<ns>.svc.<zone>.
(2)Headless类型的Service资源,要具有以下类型的资源记录
A记录:<service>.<ns>.svc.<zone>. <ttl> IN A <endpoint-ip>
SRV记录:_<port>._<proto>.<service>.<ns>.svc.<zone>. <ttl> IN SRV <weight> <priority> <port-number> <hostname>.<service>.<ns>.svc.<zone>.
PTR记录:<d>.<c>.<b>.<a>.in-addr.arpa. <ttl> IN PTR <hostname>.<service>.<ns>.svc.<zone>
(3)ExternalName类型的Service资源,要具有CNAME类型的资源记录
CNAME记录:<service>.<ns>.svc.<zone>. <ttl> IN CNAME <extname>.
名称解析和服务发现是Kubernetes系统许多功能得以实现的基础服务,它通常是集群安装完成后应该立即部署的附加组件。使用kubeadm初始化一个集群时,它甚至会自动进行部署。
4. 服务发现方式:DNS
创建Service资源对象时,ClusterDNS会为它自动创建资源记录用于名称解析和服务注册,于是,Pod资源可直接使用标准的DNS名称来访问这些Service资源。每个Service对象相关的DNS记录有两个:
{SVCNAME}.{NAMESPACE}.{CLUSTER_DOMAIN}
{SVCNAME}.{NAMESPACE}.svc.{CLUSTER_DOMAIN}
另外,在前面第2章的部署参数中,“--cluster-dns”指定了集群DNS服务的工作地址,“--cluster-domain”定义了集群使用的本地域名,因此,系统初始化时默认会把“cluster.local.”和主机所在的域“ilinux.io.”作为DNS的本地域使用,这些信息会在Pod创建时以DNS配置的相关信息注入到它的/etc/resolv.conf配置文件中。例如,在此前创建的用于交互式Pod资源客户端中查看其配置:
/
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local ilinux.io
上述search参数中指定的DNS各搜索域,是以次序指定的几个域名后缀,如下所示。
{NAMESPACE}.svc.{CLUSTER_DOMAIN}:例如default.svc.cluster.local。
svc.{CLUSTER_DOMAIN}:例如svc.cluster.local。
{CLUSTER_DOMAIN}:例如cluster.local。
{WORK_NODE_DOMAIN}:例如ilinux.io。
例如,在此前创建的用于交互式Pod客户端中尝试请求解析myapp-svc的相关DNS记录:
/ # nslookup myapp-svc.default
Server: 10.96.0.10
Name: myapp-svc
Address 1: 10.107.208.93 myapp-svc.default.svc.cluster.local
解析时,“myapp-svc”服务名称的搜索次序依次是default.svc.cluster.local、svc.cluster.local、cluster.local和ilinux.io,因此基于DNS的服务发现不受Service资源所在的名称空间和创建时间的限制。上面的解析结果也正是默认的default名称空间中创建的myapp-svc服务的IP地址。