Dubbo框架里的微服务组件?
微服务的架构主要包括服务描述、服务发现、服务调用、服务监控、服务追踪以及服务治理这几个基本组件。
服务发布与引用
三种常用方式:RESTful API、XML 配置以及 IDL 文件,其中 Dubbo 框架主要是使用 XML 配置方式,接下来我通过具体实例,来给你讲讲 Dubbo 框架服务发布与引用是如何实现的。
首先来看服务发布的过程,下面这段代码是服务提供者的 XML 配置。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="hello-world-app" />
<!-- 使用multicast广播注册中心暴露服务地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" />
<!-- 和本地bean一样实现服务 -->
<bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl" />
</beans>
再来看下服务引用的过程,下面这段代码是服务消费者的 XML 配置。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
<dubbo:application name="consumer-of-helloworld-app" />
<!-- 使用multicast广播注册中心暴露发现服务地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />
<!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
<dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" />
</beans>
服务调用
专栏前面我讲过在服务调用的过程中,通常把服务消费者叫作客户端,服务提供者叫作服务端,发起一次服务调用需要解决四个问题:
客户端和服务端如何建立网络连接?
服务端如何处理请求?
数据传输采用什么协议?
数据该如何序列化和反序列化?
服务端:
<dubbo:protocol server=“netty4” />
客户端:
<dubbo:consumer client=“netty4” />
Dubbo 不仅支持私有的 Dubbo 协议,还支持其他协议比如 Hessian、RMI、HTTP、Web Service、Thrift 等。下面这张图描述了私有 Dubbo 协议的协议头约定。
至于数据序列化和反序列方面,Dubbo 同样也支持多种序列化格式,比如 Dubbo、Hession 2.0、JSON、Java、Kryo 以及 FST 等,可以通过在 XML 配置中添加下面的配置项。
服务监控
服务监控主要包括四个流程:数据采集、数据传输、数据处理和数据展示,其中服务框架的作用是进行埋点数据采集,然后上报给监控系统。在 Dubbo 框架中,无论是服务提供者还是服务消费者,在执行服务调用的时候,都会经过 Filter 调用链拦截,来完成一些特定功能,比如监控数据埋点就是通过在 Filter 调用链上装备了 MonitorFilter 来实现的,
服务治理
服务治理手段包括节点管理、负载均衡、服务路由、服务容错等,下面这张图给出了 Dubbo 框架服务治理的具体实现
图中的 Invoker 是对服务提供者节点的抽象,Invoker 封装了服务提供者的地址以及接口信息。
节点管理:Directory 负责从注册中心获取服务节点列表,并封装成多个 Invoker,可以把它看成“List” ,它的值可能是动态变化的,比如注册中心推送变更时需要更新。
负载均衡:LoadBalance 负责从多个 Invoker 中选出某一个用于发起调用,选择时可以采用多种负载均衡算法,比如 Random、RoundRobin、LeastActive 等。
服务路由:Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离、机房隔离等。
服务容错:Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,比如采用 Failover 策略的话,调用失败后,会选择另一个 Invoker,重试请求。
服务发布与引用:对应实现是图里的 Proxy 服务代理层,Proxy 根据客户端和服务端的接口描述,生成接口对应的客户端和服务端的 Stub,使得客户端调用服务端就像本地调用一样。
服务注册与发现:对应实现是图里的 Registry 注册中心层,Registry 根据客户端和服务端的接口描述,解析成服务的 URL 格式,然后调用注册中心的 API,完成服务的注册和发现。
服务调用:对应实现是 Protocol 远程调用层,Protocol 把客户端的本地请求转换成 RPC 请求。然后通过 Transporter 层来实现通信,Codec 层来实现协议封装,Serialization 层来实现数据序列化和反序列化。
服务监控:对应实现层是 Filter 调用链层,通过在 Filter 调用链层中加入 MonitorFilter,实现对每一次调用的拦截,在调用前后进行埋点数据采集,上传给监控系统。
服务治理:对应实现层是 Cluster 层,负责服务节点管理、负载均衡、服务路由以及服务容错。
以 Dubbo 框架下一次服务调用的过程为例,先来看下客户端发起调用的过程。
首先根据接口定义,通过 Proxy 层封装好的透明化接口代理,发起调用。
然后在通过 Registry 层封装好的服务发现功能,获取所有可用的服务提供者节点列表。
再根据 Cluster 层的负载均衡算法从可用的服务节点列表中选取一个节点发起服务调用,如果调用失败,根据 Cluster 层提供的服务容错手段进行处理。
同时通过 Filter 层拦截调用,实现客户端的监控统计。
最后在 Protocol 层,封装成 Dubbo RPC 请求,发给服务端节点。
XML 配置方式的服务发布和引用流程
- 服务提供者定义接口
服务提供者发布服务之前首先要定义接口,声明接口名、传递参数以及返回值类型,然后把接口打包成 JAR 包发布出去。
比如下面这段代码,声明了接口 UserLastStatusService,包含两个方法 getLastStatusId 和 getLastStatusIds,传递参数一个是 long 值、一个是 long 数组,返回值一个是 long 值、一个是 map。
package com.weibo.api.common.status.service;
public interface UserLastStatusService {
* @param uids
* @return
*/
public long getLastStatusId(long uid);
/**
*
* @param uids
* @return
*/
public Map<Long, Long> getLastStatusIds(long[] uids);
}
- 服务提供者发布接口
服务提供者发布的接口是通过在服务发布配置文件中定义接口来实现的。下面我以一个具体的服务发布配置文件 user-last-status.xml 来给你讲解,它定义了要发布的接口 userLastStatusLocalService,对外暴露的协议是 Motan 协议,端口是 8882。并且针对两个方法 getLastStatusId 和 getLastStatusIds,通过 requestTimeout=“300” 单独定义了超时时间是 300ms,通过 retries=“0” 单独定义了调用失败后重试次数为 0,也就是不重试。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
">
<motan:service ref="userLastStatusLocalService"
requestTimeout="50" retries="2" interface="com.weibo.api.common.status.service.UserLastStatusService"
basicService="serviceBasicConfig" export="motan:8882">
<motan:method name="getLastStatusId" requestTimeout="300"
retries="0" />
<motan:method name="getLastStatusIds" requestTimeout="300"
retries="0" />
</motan:service>
</beans>
然后服务发布者在进程启动的时候,会加载配置文件 user-last-status.xml,把接口对外暴露出去。
- 服务消费者引用接口
服务消费者引用接口是通过在服务引用配置文件中定义要引用的接口,并把包含接口定义的 JAR 包引入到代码依赖中。下面我再以一个具体的服务引用配置文件 user-last-status-client.xml 来给你讲解,它定义服务消费者引用了接口 commonUserLastStatusService,接口通信协议是 Motan。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
">
<motan:protocol name="motan" default="true" loadbalance="${service.loadbalance.name}" />
<motan:basicReferer id="userLastStatusServiceClientBasicConfig"
protocol="motan" />
<!-- 导出接口 -->
<motan:referer id="commonUserLastStatusService" interface="com.weibo.api.common.status.service.UserLastStatusService"
basicReferer="userLastStatusServiceClientBasicConfig" />
</beans>
如何将注册中心落地
注册中心如何存储服务信息
服务信息除了包含节点信息(IP 和端口号)以外,还包含其他一些信息,比如请求失败时重试的次数、请求结果是否压缩等信息。因此服务信息通常用 JSON 字符串来存储,包含多个字段,每个字段代表不同的含义。
服务一般会分成多个不同的分组,每个分组的目的不同。一般来说有下面几种分组方式。
核心与非核心,从业务的核心程度来分。
机房,从机房的维度来分。
线上环境与测试环境,从业务场景维度来区分。
注册中心存储的服务信息一般包含三部分内容:分组、服务名以及节点信息,节点信息又包括节点地址和节点其他信息。从注册中心中获取的信息结构大致如下图所示
注册中心具体是如何工作的,包括四个流程。
服务提供者注册流程。
服务提供者反注册流程。
服务消费者查询流程。
服务消费者订阅变更流程。
- 如何注册节点
首先查看要注册的节点是否在白名单内?如果不在就抛出异常,在的话继续下一步。
其次要查看注册的 Cluster(服务的接口名)是否存在?如果不存在就抛出异常,存在的话继续下一步。
然后要检查 Service(服务的分组)是否存在?如果不存在则抛出异常,存在的话继续下一步。
最后将节点信息添加到对应的 Service 和 Cluster 下面的存储中。
-
如何反注册
3.
查看 Service(服务的分组)是否存在,不存在就抛出异常,存在就继续下一步。
查看 Cluster(服务的接口名)是否存在,不存在就抛出异常,存在就继续下一步。
删除存储中 Service 和 Cluster 下对应的节点信息。
更新 Cluster 的 sign 值。 -
如何查询节点信息
首先从 localcache(本机内存)中查找,如果没有就继续下一步。这里为什么服务消费者要把服务信息存在本机内存呢?主要是因为服务节点信息并不总是时刻变化的,并不需要每一次服务调用都要调用注册中心获取最新的节点信息,只需要在本机内存中保留最新的服务提供者的节点列表就可以。
接着从 snapshot(本地快照)中查找,如果没有就继续下一步。这里为什么服务消费者要在本地磁盘存储一份服务提供者的节点信息的快照呢?这是因为服务消费者同注册中心之间的网络不一定总是可靠的,服务消费者重启时,本机内存中还不存在服务提供者的节点信息,如果此时调用注册中心失败,那么服务消费者就拿不到服务节点信息了,也就没法调用了。本地快照就是为了防止这种情况的发生,即使服务消费者重启后请求注册中心失败,依然可以读取本地快照,获取到服务节点信息。 -
如何订阅服务变更
服务消费者从注册中心获取了服务的信息后,就订阅了服务的变化,会在本地保留 Cluster 的 sign 值。
服务消费者每隔一段时间,调用 getSign() 函数,从注册中心获取服务端该 Cluster 的 sign 值,并与本地保留的 sign 值做对比,如果不一致,就从服务端拉取新的节点信息,并更新 localcache 和 snapshot。
开源服务注册中心如何选型?
当下主流的服务注册与发现的解决方案,主要有两种:
应用内注册与发现:注册中心提供服务端和客户端的 SDK,业务应用通过引入注册中心提供的 SDK,通过 SDK 与注册中心交互,来实现服务的注册和发现。
应用外注册与发现:业务应用本身不需要通过 SDK 与注册中心打交道,而是通过其他方式与注册中心交互,间接完成服务注册与发现。
两种典型的注册中心实现
- 应用内
Eureka Server:注册中心的服务端,实现了服务信息注册、存储以及查询等功能。
服务端的 Eureka Client:集成在服务端的注册中心 SDK,服务提供者通过调用 SDK,实现服务注册、反注册等功能。
客户端的 Eureka Client:集成在客户端的注册中心 SDK,服务消费者通过调用 SDK,实现服务订阅、服务更新等功能。 - 应用外采用应用外方式实现服务注册和发现,最典型的案例是开源注册中心 Consul,它的架构图如下。
Consul:注册中心的服务端,实现服务注册信息的存储,并提供注册和发现服务。
Registrator:一个开源的第三方服务管理器项目,它通过监听服务部署的 Docker 实例是否存活,来负责服务提供者的注册和销毁。Consul Template:定时从注册中心服务端获取最新的服务提供者节点列表并刷新 LB 配置(比如 Nginx 的 upstream),这样服务消费者就通过访问 Nginx 就可以获取最新的服务提供者信息。
注册中心选型要考虑的两个问题
- 高可用性
实现高可用性的方法主要有两种:
集群部署,顾名思义就是通过部署多个实例组成集群来保证高可用性,这样的话即使有部分机器宕机,将访问迁移到正常的机器上就可以保证服务的正常访问。
多 IDC 部署,就是部署在不止一个机房,这样能保证即使一个机房因为断电或者光缆被挖断等不可抗力因素不可用时,仍然可以通过把请求迁移到其他机房来保证服务的正常访问。 - 数据一致性为了保证注册中心的高可用性,注册中心的部署往往都采用集群部署,并且还通常部署在不止一个数据中心,这样的话就会引出另一个问题,多个数据中心之间如何保证数据一致?如何确保访问数据中心中任何一台机器都能得到正确的数据?
CAP 理论,即同时满足一致性、可用性、分区容错性这三者是不可能的,其中 C(Consistency)代表一致性,A(Availability)代表可用性,P(Partition Tolerance)代表分区容错性。
而注册中心一般采用分布式集群部署,也面临着 CAP 的问题,根据 CAP 不能同时满足,所以不同的注册中心解决方案选择的方向也就不同,大致可分为两种。
CP 型注册中心,牺牲可用性来保证数据强一致性,最典型的例子就是 ZooKeeper,etcd,Consul 了。ZooKeeper 集群内只有一个 Leader,而且在 Leader 无法使用的时候通过 Paxos 算法选举出一个新的 Leader。这个 Leader 的目的就是保证写信息的时候只向这个 Leader 写入,Leader 会同步信息到 Followers,这个过程就可以保证数据的强一致性。但如果多个 ZooKeeper 之间网络出现问题,造成出现多个 Leader,发生脑裂的话,注册中心就不可用了。而 etcd 和 Consul 集群内都是通过 raft 协议来保证强一致性,如果出现脑裂的话, 注册中心也不可用。
AP 型注册中心,牺牲一致性来保证可用性,最典型的例子就是 Eureka 了。对比下 Zookeeper,Eureka 不用选举一个 Leader,每个 Eureka 服务器单独保存服务注册地址,因此有可能出现数据信息不一致的情况。但是当网络出现问题的时候,每台服务器都可以完成独立的服务。
开源框架如何选型?
Dubbo:国内最早开源的 RPC 框架,由阿里巴巴公司开发并于 2011 年末对外开源,仅支持 Java 语言。
Motan:微博内部使用的 RPC 框架,于 2016 年对外开源,仅支持 Java 语言。Tars:腾讯内部使用的 RPC 框架,于 2017 年对外开源,仅支持 C++ 语言。
Spring Cloud:国外 Pivotal 公司 2014 年对外开源的 RPC 框架,仅支持 Java 语言,最近几年生态发展得比较好,是比较火的 RPC 框架。
如何搭建一个可靠的监控系统?
ELK
ELK 是 Elasticsearch、Logstash、Kibana 三个开源软件产品首字母的缩写,它们三个通常配合使用,所以被称为 ELK Stack,它的架构可以用下面的图片来描述。
Logstash 负责数据收集和传输,它支持动态地从各种数据源收集数据,并对数据进行过滤、分析、格式化等,然后存储到指定的位置。Elasticsearch 负责数据处理,它是一个开源分布式搜索和分析引擎,具有可伸缩、高可靠和易管理等特点,基于 Apache Lucene 构建,能对大容量的数据进行接近实时的存储、搜索和分析操作,通常被用作基础搜索引擎。Kibana 负责数据展示,也是一个开源和免费的工具,通常和 Elasticsearch 搭配使用,对其中的数据进行搜索、分析并且以图表的方式展示。
ELK 的技术栈比较成熟,应用范围也比较广,除了可用作监控系统外,还可以用作日志查询和分析。
Graphite 是基于时间序列数据库存储的监控系统,并且提供了功能强大的各种聚合函数比如 sum、average、top5 等可用于监控分析,而且对外提供了 API 也可以接入其他图形化监控系统如 Grafana。
TICK 的核心在于其时间序列数据库 InfluxDB 的存储功能强大,且支持类似 SQL 语言的复杂数据处理操作。
Prometheus 的独特之处在于它采用了拉数据的方式,对业务影响较小,同时也采用了时间序列数据库存储,而且支持独有的 PromQL 查询语言,功能强大而且简洁。