API网关、微服务和SOA
SOA:将紧耦合的系统,划分为面向业务的,粗粒度,松耦合,无状态的服务。ESB企业服务总线连接各个服务,为了集成不同系统,不同协议的服务,ESB做了消息的转换解释与路由等工作,让不同的服务互联互通,使用SOA和ESB能够灵活实现业务流程管理。
微服务:业务系统组件化和服务化,单个业务系统拆分为多个可以独立开发、设计、运行和运维的小应用,这些小应用之间通过服务完成交互和集成。微服务不再强调传统SOA架构里面比较重的ESB企业服务总线,同时SOA的思想进入到单个业务系统内部实现真正的组件化。
API网关:API 网关是进入系统的唯一节点,类似设计模式中的Facade模式。API 网关封装内部系统的架构,并且提供 API 给各个客户端,负责服务请求的路由、组合及协议转换。它还可能还具备授权、监控、负载均衡、缓存、请求分片和管理、静态响应处理等功能。API 网关的最大优点是,它封装了应用程序的内部结构,客户端只需要同网关交互。API 网关的性能和可扩展性都非常重要,因此,将 API 网关通常构建在异步、I/O非阻塞的框架只上,如Netty、Vertx或Spring Reactor。
架构
- 服务容器负责启动,加载,运行服务提供者。
- 提供者在启动时,向注册中心注册自己提供的服务。
- 消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回提供者地址列表给消费者,提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,如果有变更或宕机,注册中心将基于长连接推送事件给消费者。注册中心为对等集群,可动态增加机器;任意一台宕掉后,将自动切换到另一台;注册中心全部宕掉后,提供者和服务消费者仍能通过本地缓存通讯。
- 监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示。
- 消费者从提供者地址列表中,基于软负载均衡算法(默认随机算法),选一台提供者进行调用,如果调用失败,再选另一台调用。提供者全部宕掉后,消费者应用将无法使用,并无限次重连等待提供者恢复。
- 注册中心,提供者,消费者三者之间均为长连接,监控中心除外。
- 注册中心和监控中心都是可选的,消费者可以直连提供者。
请求
注意表中参数与图中的对应关系:
1、当consumer发起一个请求时,首先经过active limit(参数actives)进行方法级别的限制,其实现方式为CHM中存放计数器(AtomicInteger),请求时加1,请求完成(包括异常)减1,如果超过actives则等待有其他请求完成后重试或者超时后失败;
2、从多个连接(connections)中选择一个连接发送数据,对于默认的netty实现来说,由于可以复用连接,默认一个连接就可以。不过如果你在压测,且只有一个consumer,一个provider,此时适当的加大connections确实能够增强网络传输能力。但线上业务由于有多个consumer多个provider,因此不建议增加connections参数;
3、连接到达provider时(如dubbo的初次连接),首先会判断总连接数是否超限(acceps),超过限制连接将被拒绝;
4、连接成功后,具体的请求交给io thread处理。io threads虽然是处理数据的读写,但io部分为异步,更多的消耗的是cpu,因此iothreads默认cpu个数+1是比较合理的设置,不建议调整此参数;
5、数据读取并反序列化以后,交给业务线程池处理,默认情况下线程池为fixed,且排队队列为0(queues),这种情况下,最大并发等于业务线程池大小(threads),如果希望有请求的堆积能力,可以调整queues参数。如果希望快速失败由其他节点处理(官方推荐方式),则不修改queues,只调整threads;
6、execute limit(参数executes)是方法级别的并发限制,原理与actives类似,只是少了等待的过程,即受限后立即失败;
7、tps,控制指定时间内(默认60s)的请求数。注意目前dubbo默认没有支持该参数,需要加一个META-INF/dubbo/com.alibaba.dubbo.rpc.Filter文件,文件内容为:tps=com.alibaba.dubbo.rpc.filter.TpsLimitFilter
从上面的分析,可以看出如果consumer数*actives>provider数*threads且queues=0,则会存在部分请求无法申请到资源,重试也有很大几率失败。 当需要对一个接口的不同方法进行不同的并发控制时使用executes,否则调整threads就可以。
配置
配置覆盖策略:JVM启动参数 -> XML配置 -> dubbo.properties
注解配置需要版本2.5.7+
启动时检验:
<!-- 关闭某个服务的启动时检查 (没有提供者时报错):-->
<dubbo:reference interface="com.foo.BarService" check="false" />
<!-- 关闭所有服务的启动时检查 (没有提供者时报错):-->
<dubbo:consumer check="false" />
<!-- 关闭注册中心启动时检查 (注册订阅失败时报错):-->
<dubbo:registry check="false" />
集群容错:
<dubbo:service cluster="failsafe" />
<dubbo:reference cluster="failsafe" />
6种集群容错策略,缺省failover cluster
- Failover Cluster
默认容错机制,失败自动切换,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过retries="2"来设置重试次数(不含第一次),retries="-1"不重试,默认retries="2"。 - Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 - Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 - Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 - Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。 - Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
负载均衡:
<dubbo:service interface="..." loadbalance="roundrobin" />
<dubbo:reference interface="..." loadbalance="roundrobin" />
<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>
4种负载均衡策略,缺省random loadbalance
- Random LoadBalance
随机,按权重设置随机概率。 - RoundRobin LoadBalance
轮循,按公约后的权重设置轮循比率。 - LeastActive LoadBalance
最少活跃调用数,相同活跃数的随机。活跃数指并发调用数,减少的响应慢的 Provider 的调用,因为响应慢更容易累积并发的调用 - ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者。当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
算法参见:http://en.wikipedia.org/wiki/Consistent_hashing
缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key="hash.arguments" value="0,1" />
缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" value="320" />
线程模型
<dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="100" />
dispatcher消息分发策略:(IO线程处理快,处理快请求,满请求扔进线程池)
all
缺省,所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。direct
所有消息都不派发到线程池,全部在 IO 线程上直接执行。message
推荐,只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在 IO 线程上执行。
* dispatcher使用all时,一旦Provider线程池被打满,由于异常处理也需要用业务线程池,如果此时业务线程池有空闲线程,那么Consumer将收到线程池已满的异常;但如果此时线程池已满,没有线程执行返回响应或异常,则Consumer只能等到超时。可使用dispatcher="message"解决此问题,因为默认IO线程池是无界的,一定会有IO线程来处理异常execution
只请求消息派发到线程池,不含响应,响应和其它连接断开事件,心跳等消息,直接在 IO 线程上执行。connection
在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。
threadpool线程池配置:
fixed
固定大小线程池,默认threads="100",启动时建立线程,不关闭,一直持有。(缺省)
调优:并发量大于100时,部分线程会请求不到资源超时的问题,应提高线程池连接数;然而大量线程栈会占用大量内存空间(Linux系统默认ThreadStackSize=1024K),应增加内存或减少线程栈空间。cached
缓存线程池,空闲一分钟自动删除。limited
可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然来了大流量引起的性能问题。
多协议
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="hessian" port="8080" />
<dubbo:service id="helloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" protocol="dubbo,hessian" />
服务分组
当一个接口有多种实现时,可以用 group 区分
<dubbo:service group="feedback" interface="com.xxx.IndexService" />
<dubbo:service group="member" interface="com.xxx.IndexService" />
参数校验**
接口
POJO
结果缓存**
<dubbo:reference interface="com.foo.BarService" cache="lru" />
<dubbo:reference interface="com.foo.BarService">
<dubbo:method name="findBar" cache="lru" />
</dubbo:reference>
- lru Least recently used,缓存淘汰算法:最近最少使用,保持热数据被缓存。
- threadlocal 当前线程缓存,比如一个页面渲染,用到很多 portal,每个 portal 都要去查用户信息,通过线程缓存,可以减少这种多余访问。
- jcache 与 JSR107 集成,可以桥接各种缓存实现。
泛化
上下文
RpcContext:一个ThreadLocal,自带只有local/remote address,但可以自由存数据,还可以传递隐式参数setAttachment("k","v")/getAttachment("k","v"),不可使用保留字段(path, group, version, dubbo, token, timeout)。
异步调用**
参数回调**
事件通知**
本地存根
远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,比如:做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在 API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub 1,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。
<dubbo:service interface="com.foo.BarService" stub="true" />
<dubbo:service interface="com.foo.BarService" stub="com.foo.BarServiceStub" />
在 interface 旁边放一个 Stub 实现,它实现 BarService 接口,并有一个传入远程 BarService 实例的构造函数。
package com.foo;
public class BarServiceStub implements BarService {
private final BarService barService;
// 构造函数传入真正的远程代理对象
public (BarService barService) {
this.barService = barService;
}
public String sayHello(String name) {
// 此代码在客户端执行, 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等
try {
return barService.sayHello(name);
} catch (Exception e) {
// 你可以容错,可以做任何AOP拦截事项
return "容错数据";
}
}
}
本地伪装
本地伪装通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过 Mock 数据返回授权失败。
Mock 是 Stub 的一个子集,便于服务提供方在客户端执行容错逻辑,因经常需要在出现 RpcException (比如网络失败,超时等)时进行容错,而在出现业务异常(比如登录用户名密码错误)时不需要容错,如果用 Stub,可能就需要捕获并依赖 RpcException 类,而用 Mock 就可以不依赖 RpcException,因为它的约定就是只有出现 RpcException 时才执行。
<dubbo:service interface="com.foo.BarService" mock="true" />
<dubbo:service interface="com.foo.BarService" mock="com.foo.BarServiceMock" />
<!-- 仅忽略异常 -->
<dubbo:service interface="com.foo.BarService" mock="return null" />
在 interface 旁放一个 Mock 实现,它实现 BarService 接口,并有一个无参构造函数 。
package com.foo;
public class BarServiceMock implements BarService {
public String sayHello(String name) {
// 你可以伪造容错数据,此方法只在出现RpcException时被执行
return "容错数据";
}
}
延迟暴露
<!-- 延迟 5 秒暴露服务 -->
<dubbo:service delay="5000" />
<!-- 延迟到 Spring 初始化完成后,再暴露服务 -->
<dubbo:service delay="-1" />
Spring 2.x 初始化死锁问题:
Spring 解析到 <dubbo:service /> 时,就已经向外暴露了服务,而 Spring 还在接着初始化其它 Bean。如果这时有请求进来,并且服务的实现类里有调用 applicationContext.getBean() 的用法。会导致线程死锁,不能提供服务,启动不了。
规避方法:
1. Dubbo 的配置放在 Spring 的最后加载
2. 如果不想依赖配置顺序,可以使用 <dubbo:provider deplay=”-1” />,使 Dubbo 在 Spring 容器初始化完后,再暴露服务。
** 如果大量使用 getBean(),相当于已经把 Spring 退化为工厂模式在用,可以将 Dubbo 的服务隔离单独的 Spring 容器。
Netty4
2.5.6版本开始支持
<!-- provider -->
<dubbo:protocol server="netty4" />
<dubbo:provider server="netty4" />
<!-- consumer -->
<dubbo:consumer client="netty4" />
性能调优
Provider:
application:
delay=0
protocol:
name=dubbo 协议
threadpool=fixed
threads=100
iothreads=cpu+1
codec=dubbo
serialization=hessian2 (dubbo协议的dubbo,hessian2,java,compactedjava,以及http协议的json等)
Consumer:
timeout=1000
cluster=failover
retries=2
loadbalance=random
connections=100
async=false 不可靠异步,只是忽略返回值,不阻塞执行线程
性能报告
根据Dubbo性能测试报告,Dubbo协议适用于高并发小数据量的rpc调用,大数据量下性能远不如http/rmi。Dubbo序列化(试用)性能高于默认的hessian2序列化。
tps | 1k pojo | 1k String | 50k String | 200k String |
Dubbo | 13620 | 15096 | 1966 | 569 |
HTTP | 8179 | 8919 | 3247 | 1011 |
RMI | 2461 | 11136 | 3349 | 1031 |
zookeeper注册中心
zk是一个树型的目录服务,支持变更推送
- 当提供者出现断电等异常停机时,注册中心能自动删除提供者信息
- 当注册中心重启时,能自动恢复注册数据,以及订阅请求
- 当会话过期时,能自动恢复注册数据,以及订阅请求
- 当设置
<dubbo:registry check="false" />
时,记录失败注册和订阅请求,后台定时重试 - 可通过
<dubbo:registry username="admin" password="1234" />
设置 zookeeper 登录信息 - 可通过
<dubbo:registry group="dubbo" />
设置 zookeeper 的根节点,不设置将使用无根树 - 支持
*
号通配符<dubbo:reference group="*" version="*" />
,可订阅服务的所有分组和所有版本的提供者
推荐用法
Provider 上尽量多配置 Consumer 端属性
- 作服务的提供者,比服务使用方更清楚服务性能参数,如调用的超时时间,合理的重试次数,等等
- 在 Provider 配置后,Consumer 不配置则会使用 Provider 的配置值,即 Provider 配置可以作为 Consumer 的缺省值。
<dubbo:service interface="com.alibaba.hello.api.WorldService" version="1.0.0" ref="helloService"
timeout="300" retry="2" loadbalance="random" actives="0" >
<dubbo:method name="findAllPerson" timeout="10000" retries="9" loadbalance="leastactive" actives="5" />
<dubbo:service/>
timeout
方法调用超时retries
失败重试次数,缺省是 2loadbalance
负载均衡算法,缺省是随机random
actives
消费者端,最大并发调用限制,即当 Consumer 对一个服务的并发调用到上限后,新调用会 Wait 直到超时
Provider 上配置合理的 Provider 端属性
<dubbo:protocol threads="200" />
<dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService"
executes="200" >
<dubbo:method name="findAllPerson" executes="50" />
</dubbo:service>
threads
服务线程池大小executes
一个服务提供者并行执行请求上限,即当 Provider 对一个服务的并发调用到上限后,新调用会 Wait,这个时候 Consumer可能会超时。
配置 Dubbo 缓存文件
提供者列表缓存文件(默认路径//home/[user]/.dubbo/dubbo-registry-10.199.103.195.cache),注册中心不可用时则应用会从这个缓存文件读取服务提供者列表的信息,进一步保证应用可靠性
<dubbo:registry file=”${user.home}/output/dubbo.cache” />