架构演进
单一应用架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。
流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键
Dubbo概述
Dubbo是阿里巴巴公司开源的一个高性能优秀的分布式服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和Spring框架无缝集成.
Dubbo架构
Dubbo架构如下图, Dubbo 架构具有以下几个特点,分别是连通性、健壮性、伸缩性、以及向未来架构的升级性。
节点角色说明
Provider : 暴露服务的服务提供方
Consumer : 调用远程服务的服务消费方
Registry : 服务注册与发现的注册中心
Monitor: 统计服务的调用次数和调用时间的监控中心
Container: 服务运行容器
调用关系说明
服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
连通性
注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小
监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示
服务提供者向注册中心注册其提供的服务,并汇报调用时间到监控中心,此时间不包含网络开销
服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者,同时汇报调用时间到监控中心,此时间包含网络开销
注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外
注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者
注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表
注册中心和监控中心都是可选的,服务消费者可以直连服务提供者
健壮性
监控中心宕掉不影响使用,只是丢失部分采样数据
数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
注册中心对等集群,任意一台宕掉后,将自动切换到另一台
注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
服务提供者无状态,任意一台宕掉后,不影响使用
服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
伸缩性
注册中心为对等集群,可动态增加机器部署实例,所有客户端将自动发现新的注册中心
服务提供者无状态,可动态增加机器部署实例,注册中心将推送新的服务提供者信息给消费者
升级性
当服务集群规模进一步扩大,带动IT治理结构进一步升级,需要实现动态部署,进行流动计算,现有分布式服务架构不会带来阻力。
Dubbo应用
Dubbo采用全Spring配置方式,透明化接入应用,对应用没有任何API侵入, 在本地服务的基础上只需做简单配置,即可完成服务化:
Dubbo应用示例
传统Spring服务扩展为Dubbo服务的步骤比较简单.
- 将传统服务中接口的实例化定义拆分为两部分: 服务提供方、服务消费方
- 将服务定义部分放在服务提供方
remote-provider.xml
, - 将服务引用部分放在服务消费方
remote-consumer.xml
。 - 并在提供方增加暴露服务配置 dubbo:service,在消费方增加引用服务配置 dubbo:reference
假设我们有一个服务接口(DemoService)和接口实现(DemoServiceImpl), 对其做服务化.
服务提供方
1 |
|
服务消费方
1 |
|
在服务消费方我们就可以正常使用DemoService的实例化Bean demoService了.
在开发及测试环境下,经常需要绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连.
1 | <dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" url="dubbo://localhost:20890"/> |
Dubbo配置方式
Dubbo服务化配置提供了4种方式: 硬编码方式、基于XML配置、基于属性文件配置、基于注解配置.
配置示例
编码方式: http://dubbo.apache.org/zh-cn/docs/user/configuration/api.html
XML方式: http://dubbo.apache.org/zh-cn/docs/user/configuration/xml.html
属性配置: http://dubbo.apache.org/zh-cn/docs/user/configuration/properties.html
注解配置: http://dubbo.apache.org/zh-cn/docs/user/configuration/annotation.html
Dubbo模块特性
启动检查
Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 check=”true”。
可以通过 check=”false” 关闭检查,比如,测试时,有些服务不关心,或者出现了循环依赖,必须有一方先启动.
说明: 如果你的 Spring 容器是懒加载的,或者通过 API 编程延迟引用服务,请关闭 check,否则服务临时不可用时,会抛出异常,拿到 null 引用,如果 check=”false”,总是会返回引用,当服务恢复时,能自动连上.
关闭特定服务
1 | <!-- 没有提供者时报错 --> |
关闭所有服务
1 | <!-- 没有提供者时报错 --> |
关不注册中心
1 | <!-- 注册订阅失败时报错 --> |
集群容错
在集群调用失败时,Dubbo 提供了多种容错方案,缺省为Failover Cluster
重试策略.
Failover Cluster
: 失败自动切换(默认策略),当出现失败,重试其它服务器, 通常用于读操作,但重试会带来更长延迟。可通过 retries=”2” 来设置重试次数(不含第一次)Failfast Cluster
: 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。Failsafe Cluster
: 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。Failback Cluster
: 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。Forking Cluster
: 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2” 来设置最大并行数.Broadcast Cluster
: 广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
下面示例展示默认策略Failover Cluster
的配置应用:
服务端示例
1 | <!-- 针对接口 --> |
消费端示例
1 | <!-- 针对接口 --> |
负载均衡
在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random 随机调用, 用户也可以自行扩展负载均衡策略.
随机 Random LoadBalance
随机,按权重设置随机概率。
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重
轮询 RoundRobin LoadBalance
轮询,按公约后的权重设置轮询比率。
存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上
最少活跃调用数 LeastActive LoadBalance
最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
一致性哈希 ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者。
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key=”hash.arguments” value=”0,1” />
缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key=”hash.nodes” value=”320” />
配置示例
服务端配置
1 | <!-- 针对接口 --> |
消费端配置
1 | <!-- 针对接口 --> |
线程模型
如果事件处理的逻辑能迅速完成,并且不会发起新的 IO 请求,比如只是在内存中记个标识,则直接在 IO 线程上处理更快,因为减少了线程池调度。
但如果事件处理逻辑较慢,或者需要发起新的 IO 请求,比如需要查询数据库,则必须派发到线程池,否则 IO 线程阻塞,将导致不能接收其它请求。
如果用 IO 线程处理事件,又在事件处理过程中发起新的 IO 请求,比如在连接事件中发起登录请求,会报“可能引发死锁”异常,但不会真死锁。
因此,需要通过不同的派发策略和不同的线程池配置的组合来应对不同的场景:
1 | <dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="100" /> |
Dispatcher
all: 所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。
direct: 所有消息都不派发到线程池,全部在 IO 线程上直接执行。
message: 只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在 IO 线程上执行。
execution: 只请求消息派发到线程池,不含响应,响应和其它连接断开事件,心跳等消息,直接在 IO 线程上执行。
connection: 在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。
ThreadPool
fixed: 固定大小线程池,启动时建立线程,不关闭,一直持有。(缺省)
cached: 缓存线程池,空闲一分钟自动删除,需要时重建。
limited: 可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然来了大流量引起的性能问题。
eager: 优先创建Worker线程池。在任务数量大于corePoolSize但是小于maximumPoolSize时,优先创建Worker来处理任务。当任务数量大于maximumPoolSize时,将任务放入阻塞队列中。阻塞队列充满时抛出RejectedExecutionException。(相比于cached:cached在任务数量超过maximumPoolSize时直接抛出异常而不是将任务放入阻塞队列)
静态服务
有时候希望人工管理服务提供者的上线和下线,此时需将注册中心标识为非动态管理模式。
1 | <dubbo:registry address="10.20.141.150:9090" dynamic="false" /> |
或者
1 | <dubbo:registry address="10.20.141.150:9090?dynamic=false" /> |
服务提供者初次注册时为禁用状态,需人工启用。断线时,将不会被自动删除,需人工禁用.
多协议
Dubbo 允许配置多协议,在不同服务上支持不同协议或者同一服务上同时支持多种协议。
不同服务不同协议
不同服务在性能上适用不同协议进行传输,比如大数据用短连接协议,小数据大并发用长连接协议
1 |
|
多协议暴露服务
需要与 http 客户端互操作
1 |
|
多注册中心
Dubbo 支持同一服务向多注册中心同时注册,或者不同服务分别注册到不同的注册中心上去,甚至可以同时引用注册在不同注册中心上的同名服务。另外,注册中心是支持自定义扩展的。
服务分组
当一个接口有多种实现时,可以用 group 区分.
服务方
1 | <dubbo:service group="feedback" interface="com.xxx.IndexService" /> |
消费方
1 | <dubbo:reference id="feedbackIndexService" group="feedback" interface="com.xxx.IndexService" /> |
任意组
1 | <dubbo:reference id="barService" interface="com.foo.BarService" group="*" /> |
多版本
当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。
可以按照以下的步骤进行版本迁移:
在低压力时间段,先升级一半提供者为新版本
再将所有消费者升级为新版本
然后将剩下的一半提供者升级为新版本
服务方
1 | <!-- 老版本服务提供者配置 --> |
消费方
1 | <!-- 老版本服务提供者配置 --> |
如果不需要区分版本,可如下配置
1 | <dubbo:reference id="barService" interface="com.foo.BarService" version="*" /> |
参数验证
参数验证功能是基于JSR303实现的, 用户只需标识 JSR303 标准的验证 annotation,并通过声明 filter来实现验证.
Maven依赖
1 | <dependency> |
参数标注
1 |
|
参数验证
1 | import javax.validation.constraints.Min; |
配置示例
客户端验证
1 | <dubbo:reference id="validationService" interface="com.alibaba.dubbo.examples.validation.api.ValidationService" validation="true" /> |
服务端验证
1 | <dubbo:service interface="com.alibaba.dubbo.examples.validation.api.ValidationService" ref="validationService" validation="true" /> |
结果缓存
结果缓存,用于加速热门数据的访问速度,Dubbo 提供声明式缓存,以减少用户加缓存的工作量.
缓存类型
lru
: 基于最近最少使用原则删除多余缓存,保持最热的数据被缓存。threadlocal
: 当前线程缓存,比如一个页面渲染,用到很多 portal,每个 portal 都要去查用户信息,通过线程缓存,可以减少这种多余访问。jcache
: 与 JSR107 集成,可以桥接各种缓存实现
配置示例
1 | <!-- 针对接口 --> |
隐式参数
可以通过 RpcContext 上的 setAttachment 和 getAttachment 在服务消费方和提供方之间进行参数的隐式传递。
说明: RpcContext 是一个 ThreadLocal 的临时状态记录器,当接收到 RPC 请求,或发起 RPC 请求时,RpcContext 的状态都会变化。比如:A 调 B,B 再调 C,则 B 机器上,在 B 调 C 之前,RpcContext 记录的是 A 调 B 的信息,在 B 调 C 之后,RpcContext 记录的是 B 调 C 的信息
消费端设置隐式参数
setAttachment 设置的 KV 对,在完成下面一次远程调用会被清空,即多次远程调用要多次设置。
1 | RpcContext.getContext().setAttachment("index", "1"); // 隐式传参,后面的远程调用都会隐式将这些参数发送到服务器端,类似cookie,用于框架集成,不建议常规业务使用 |
服务端获取隐式参数
1 | public class XxxServiceImpl implements XxxService { |
异步调用
基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小.
消费端配置
1 | <dubbo:reference id="fooService" interface="com.alibaba.foo.FooService"> |
代码调用
1 | // 此调用会立即返回null |
也可以设置是否等待消息发出:
sent=”true” 等待消息发出,消息发送失败将抛出异常。
sent=”false” 不等待消息发出,将消息放入IO队列,即刻返回.
1 | <dubbo:method name="findFoo" async="true" sent="true" /> |
如果只是想异步,完全忽略返回值,可以配置 return=”false”,以减少 Future 对象的创建和管理成本
1 | <dubbo:method name="findFoo" async="true" return="false" /> |
本地调用
本地调用使用了 injvm 协议,是一个伪协议,它不开启端口,不发起远程调用,只在 JVM 内直接关联,但执行 Dubbo 的 Filter 链.
定义 injvm 协议
1 | <dubbo:protocol name="injvm" /> |
设置默认协议
1 | <dubbo:provider protocol="injvm" /> |
设置服务协议
1 | <dubbo:service protocol="injvm" /> |
优先使用injvm
1 | <dubbo:consumer injvm="true" .../> |
或
1 | <dubbo:reference injvm="true" .../> |
注意:服务暴露与服务引用都需要声明 injvm=”true”
从 2.2.0 开始,每个服务默认都会在本地暴露。在引用服务的时候,默认优先引用本地服务。如果希望引用远程服务可以使用一下配置强制引用远程服务。
1 | <dubbo:reference ... scope="remote" /> |
参数回调
事件通知
延迟连接
延迟连接用于减少长连接数。当有调用发起时,再创建长连接。
1 | <!-- 该配置只对使用长连接的 dubbo 协议生效 --> |
延迟暴露
如果你的服务需要预热时间,比如初始化缓存,等待相关资源就位等,可以使用 delay 进行延迟暴露。
1 | <!-- 延迟 5 秒暴露服务 --> |
并发控制
连接控制
粘滞连接
粘滞连接用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非该提供者挂了,再连另一台。
粘滞连接将自动开启延迟连接,以减少长连接数.
1 | <dubbo:protocol name="dubbo" sticky="true" /> |
令牌验证
线程栈自动Dump
当业务线程池满时,我们需要知道线程都在等待哪些资源、条件,以找到系统的瓶颈点或异常点。dubbo通过Jstack自动导出线程堆栈来保留现场,方便排查问题.
默认策略:
导出路径,user.home标识的用户主目录
导出间隔,最短间隔允许每隔10分钟导出一次
配置示例
1 | <dubbo:application ...> |
优雅停机
Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的,所以如果用户使用 kill -9 PID 等强制关闭指令,是不会执行优雅停机的,只有通过 kill PID 时,才会执行.
1 | /** |