服务注册与发现
在单体应用中调用往往局限在语言层面,也就是方法的调用。当服务被拆解,单体应用变成了多个服务之后,服务间“对话”就变成了一个重要的话题。目前最常见的方式是借助REST或RPC。还有一种方式是通过队列,其原理与RPC类似。
不论REST还是RPC,原来的方法调用都要变成了网络调用。这种转变也带来了很多问题:
- 效率问题。网络调用的开销一定是比本地方法调用大,网络传输,协议转化等等都会造成效率下降。
- 稳定性。服务化后服务本身的稳定性,以及网络稳定性直接决定了服务调用的成功率。
- 服务注册与发现。随着服务数量的增多,各个服务之间的调用变得错综复杂,一个服务可能依赖外部多个服务,当一个服务的域名或IP地址改变了之后如何通知依赖方,或者依赖方如何快速的发现服务提供方的地址变化。
- 服务监控。原来单体应用我们只需要观察这一台服务器就行了。但是服务化之后,每个服务都需要监控。而且还需要考虑限流这些事情。运维成本比原来多了几倍。
这些都是在服务化过程中需要考虑的问题。今天主要说一下第三点:服务注册与发现。
为什么需要服务注册与发现?没有这个东西就不能搞服务化了吗?没有这个东西也能搞服务化,但是效果会受影响。假设A是一个基础服务,随着越来越多的服务要依赖A,A服务器负载越来越高,亟需新增服务器。如果没有服务注册与发现,那么我们就要把新的服务器地址配置到所有依赖A的服务,并相继重启它们。显然这是不合理的。有人说我把A服务前边挂一个Nginx不就行了吗?这确实能解决某个服务加机器的问题,这就需要一个100%可用的Nginx?还有当服务少的时候这种思路是可行的,但是当服务器增加到了几十几百台,而且服务器动态变动的时候(促销前加几台服务,等促销过了再摘掉),这种思路也不合适。还有如果服务消费方配置了多个服务地址,那么这两个服务该怎么轮换着用呢?也就是怎么做负载均衡。
服务注册就是维护一个登记簿,它管理系统内所有的服务地址。当新的服务启动后,它会向登记簿交待自己的地址信息。服务的依赖方直接向登记簿要Service Provider地址就行了。当下用于服务注册的工具非常多ZooKeeper,Consul,Etcd, 还有Netflix家的eureka等等。不论使用那种工具,服务注册一定是要确保高可用的,否则服务调用就会收到影响。重则的是所有的服务都没法调用,轻则新的服务不能上线,假如调用方有缓存服务地址。同时服务注册还要负责一件事情,服务状态的维护。假如一个服务突然down掉,它应该能够感知,并把down掉的服务摘掉。然后主动或被动的把这个信息告知服务消费方。
服务注册有两种形式:客户端注册和第三方注册。
客户端注册是服务自身要负责注册与注销的工作。当服务启动后向注册中心注册自身,当服务下线时注销自己。期间还需要和注册中心保持心跳。心跳不一定要客户端来做,也可以由注册中心负责(这个过程叫探活)。这种方式的缺点是注册工作与服务耦合在一起,不同语言都要实现一套注册逻辑。
第三方注册由一个独立的服务Registrar负责注册与注销。当服务启动后以某种方式通知Registrar,然后Registrar负责向注册中心发起注册工作。同时注册中心要维护与服务之间的心跳,当服务不可用时,向注册中心注销服务。这种方式的缺点是Registrar必须是一个高可用的系统,否则注册工作没法进展。
Selection_023.png说完了服务注册,接下来看一下服务发现。服务发现也分为客户端发现和服务端发现。
客户端发现是指客户端负责查询可用服务地址,以及负载均衡的工作。这种方式最方便直接,而且也方便做负载均衡。再者一旦发现某个服务不可用立即换另外一个,非常直接。缺点也在于多语言时的重复工作,每个语言实现相同的逻辑。
服务端发现需要额外的Router服务,请求先打到Router,然后Router负责查询服务与负载均衡。这种方式虽然没有客户端发现的缺点,但是它的缺点是保证Router的高可用。
Selection_021.png再来提一下探活。探活分成两个层面:主动探活和本地探活。主动探活是由注册中心发起的,consul本身集成了service check的功能,可以用来做探活。本地探活是在真正的RPC调用之前进行探活,这时探活的结果是非常精准的,直接决定当前请求是否可以发送到这台服务器。对于探活失败的服务本地可以做一个故障投票,然后决定其是否存活。
参考与引用:
Service Discovery in a Microservices Architecture
服务注册发现与调度