建议将服务接口、服务模型、服务异常等均放在 API 包中,因为服务模型和异常也是 API 的一部分,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则(CRP)。
如果需要,也可以考虑在 API 包中放置一份 Spring 的引用配置,这样使用方只需在 Spring 加载过程中引用此配置即可。配置建议放在模块的包目录下,以免冲突,如:com/alibaba/china/xxx/dubbo-reference.xml。
在这里,不管你工程使用的是左边的,还是右边的分层,dubbo的接口的外部生命和异常都放在 Api 模块就行了
api package分类也可以采用下面的格式:
Api:
----com.xxx.xxx.xxx.api
----enums
----common
----biz1
----req
----common
----biz1
----req1
Biz1Api
Biz2Api
服务接口尽可能大粒度,每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,Dubbo 暂未提供分布式事务支持。
服务接口建议以业务场景为单位划分,并对相近业务做抽象,防止接口数量爆炸。
不建议使用过于抽象的通用接口,如:Map query(Map),这样的接口没有明确语义,会给后期维护带来不便。
每个接口都应定义版本号,为后续不兼容升级提供可能,如: @DubboService(version="1.0.0")。
建议使用三位版本号
第一位表示不同业务,或者不同逻辑的版本,如面向旧架构系统和新架构体系系统是两个系统,edi对外提供时候可以通过接口时候可以通过不同的不同的version进行区分, 1.0.0 和 2.0.0
第二位表示逻辑改动性升级,对外部有影响的升级。
第一位表示表示兼容性,或者逻辑影响较小的升级对于外部没影响的版本,可以不进行version改动
当不兼容时,先升级一半提供者为新版本,再将消费者全部升为新版本,然后将剩下的一半提供者升为新版本。
服务接口增加方法,或服务模型增加字段,可向后兼容,删除方法或删除字段,将不兼容,枚举类型新增字段也不兼容,需通过变更版本号升级。
如果是完备集,可以用 Enum,比如:ENABLE, DISABLE。如果是业务种类,以后明显会有类型增加,不建议用 Enum,可以用 String 代替。如果是在返回值中用了 Enum,并新增了 Enum 值,建议先升级服务消费方,这样服务提供方不会返回新值。如果是在传入参数中用了 Enum,并新增了 Enum 值,建议先升级服务提供方,这样服务消费方不会传入新值。
服务参数及返回值建议使用 POJO 对象,即通过 setter, getter 方法表示属性的对象。服务参数及返回值不建议使用接口,因为数据模型抽象的意义不大,并且序列化需要接口实现类的元信息,并不能起到隐藏实现的意图。服务参数及返回值都必须是传值调用,而不能是传引用调用,消费方和提供方的参数或返回值引用并不是同一个,只是值相同,Dubbo 不支持引用远程对象。
建议使用异常汇报错误,而不是返回错误码,异常信息能携带更多信息,并且语义更友好。如果担心性能问题,在必要时,可以通过 override 掉异常类的 fillInStackTrace() 方法为空方法,使其不拷贝栈信息。查询方法不建议抛出 checked 异常,否则调用方在查询时将过多的 try...catch,并且不能进行有效处理。服务提供方不应将 DAO 或 SQL 等异常抛给消费方,应在服务实现中对消费方不关心的异常进行包装,否则可能出现消费方无法反序列化相应异常。
不要只是因为是 Dubbo 调用,而把调用 try...catch 起来。try...catch 应该加上合适的回滚边界上。Provider 端需要对输入参数进行校验。如有性能上的考虑,服务实现者可以考虑在 API 包上加上服务 Stub 类来完成检验。
dubbo参数配置 dubbo:provider | Apache Dubbo
原因如下:
作为服务的提供方,比服务消费方更清楚服务的性能参数,如调用的超时时间、合理的重试次数等
在 Provider 端配置后,Consumer 端不配置则会使用 Provider 端的配置,即 Provider 端的配置可以作为 Consumer 的缺省值 1。否则,Consumer 会使用 Consumer 端的全局设置,这对于 Provider 是不可控的,并且往往是不合理的
Provider 端尽量多配置 Consumer 端的属性,让 Provider 的实现者一开始就思考 Provider 端的服务特点和服务质量等问题。
示例:
//对于我们,默认只需要配置上 actives,可以在配置中心配置。
@DubboService(version="1.0.0", timeout=5000, actives = 2, validation = "true")
//如果需要对method进行单独配置
@Method(timeout=3000)
建议可以在 Provider 端配置的 Consumer 端属性有:
timeout:方法调用的超时时间
retries:失败重试次数,缺省是 2 2
loadbalance:负载均衡算法 3,缺省是随机 random。还可以配置轮询 roundrobin、最不活跃优先 4leastactive 和一致性哈希 consistenthash 等
actives:消费者端的最大并发调用限制,即当 Consumer 对一个服务的并发调用到上限后,新调用会阻塞直到超时,在方法上配置 dubbo:method 则针对该方法进行并发限制,在接口上配置 dubbo:service,则针对该服务进行并发限制
executes:一个服务提供者并行执行请求上限,即当 Provider 对一个服务的并发调用达到上限后,新调用会阻塞,此时 Consumer 可能会超时。在方法上配置 dubbo:method 则针对该方法进行并发限制,在接口上配置 dubbo:service,则针对该服务进行并发限制
建议在 Provider 端配置的 Provider 端属性有:
dubbo.protocol.threads=200 服务线程池大小(固定大小),缺省值200,具体线程数,建议压测配置。
dubbo.registry.simplified = true 防止因注册名过长导致注册失败
dubbo.protocol.io*threads = cpu数*1 ~ cpu数*2*
dubbo.protocol.threadpool = fixed(缺省值) 使用cached,减少开销,但是如果流量标识,会导致机器资源耗尽,建议一般使用fixed即可
dubbo.provider.retries = 0 不建议重试的原因是大部分post接口不会做幂等和分布式限制,容易做成脏数据,可配合dubbo.provider.timeout一并使用,增加超时时长
3. 在consumer端配置合理的消费属性
check: 建议可以合理进行覆盖,比如check,我认为开启true情况更加有利于服务启动前provider的校验,但是如果机房性级大面积重启,可能导致检查失败蝴蝶效应
timeout: 尽可能在 consumer 端配置上,如果 provider端也没有配置timeout情况,有可能会导致hung住
validation:建议可以不配置
对于一般数据接口,默认dubbo协议即可,dubbo协议单连接和异步通讯机制有利于provider资源耗损
如果需要传输大文件或者大数据,建议使用 rmi 或者 hession。
对性能有较高要求,建议基于不同协议花点时间压一把
服务协议对比
协议 | 介绍 | 序列化 | 优缺点 | 兼容性 | 适用范围 |
---|---|---|---|---|---|
dubbo | 缺省协议。Dubbo 缺省协议采用单一长连接和 NIO 异步通讯。 | Hessian二进制序列化 | 适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。反之,Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。 | 接口增加方法,对客户端无影响;输入参数和结果集中增加属性,对客户端无影响;服务器端和客户端对领域对象并不需要完全一致,而是按照最大匹配原则。 | 传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo 协议传输大文件或超大字符串 |
rmi | RMI 协议采用 JDK 标准的 java.rmi.* 实现,多连接,采用阻塞式短连接和 JDK 标准序列化方式。 | Java 标准二进制序列化 | 如果正在使用 RMI 提供服务给外部访问 1,同时应用里依赖了老的 common-collections 包 2 的情况下,存在反序列化安全风险 | dubbo 配置中的超时时间对 RMI 无效,需使用 java 启动参数设置:-Dsun.rmi.transport.tcp.responseTimeout=3000; | 传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。 |
hession | Hessian 1 协议用于集成 Hessian 的服务,Hessian 底层采用 Http 通讯,采用 Servlet 暴露服务,Dubbo 缺省内嵌 Jetty 作为服务器实现。 | Hessian二进制序列化 | 可用于页面传输,文件传输,或与原生hessian服务互操作 | Hessian 是 Caucho 开源的一个 RPC 框架,其通讯效率高于 WebService 和 Java 自带的序列化。 | 传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件 |
http | 基于 HTTP 表单的远程调用协议,采用 Spring 的 HttpInvoker 实现 | 表单序列化 | 传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。 | ||
thrift | thrift 协议是对 thrift 原生协议 1 的扩展,在原生协议的基础上添加了一些额外的头信息,比如 service name,magic number 等 | thrift | 使用 dubbo thrift 协议同样需要使用 thrift 的 idl compiler 编译生成相应的 java 代码,后续版本中会在这方面做一些增强。 | Thrift 不支持 null 值,即:不能在协议中传递 null 值;与原生Thrift不兼容 |
各个协议性能测试:性能测试报告 | Apache Dubbo
基准测试包:基准测试工具包 | Apache Dubbo
5.1 Dubbo线程池扩展接口 ThreadPool 的SPI实现有如下几种:
fixed:固定大小线程池,启动时建立线程,不关闭,一直持有(缺省)。
cached:缓存线程池,空闲一分钟自动删除,需要时重建。
limited:可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然带来大流量引起性能问题。
eager 优先创建Worker线程池。在任务数量大于corePoolSize但是小于maximumPoolSize时,优先创建Worker来处理任务。当任务数量大于maximumPoolSize时,将任务放入阻塞队列中。阻塞队列充满时抛出RejectedExecutionException。(相比于cached:cached在任务数量超过maximumPoolSize时直接抛出异常而不是将任务放入阻塞队列)
默认采用fixed即可,fixed对应的FixedThreadPool使用ThreadPoolExecutor创建了核心线程数=最大线程池数=threads的线程池。
提供一个线程池定律,对于IO密集型应用,线程数位 cpu 逻辑数 * 2,对于计算密集型应用,线程数为 cpu 逻辑数
Dubbo线程池扩展,这些扩展可以满足我们绝大多数的需求,但是您可以根据自己的需要进行扩展定制。在服务提供者启动流程时,我们会看到什么时候加载的线程池扩展实现
5.2 Dubbo协议线程模型
如果事件处理的逻辑能迅速完成,并且不会发起新的 IO 请求,比如只是在内存中记个标识,则直接在 IO 线程上处理更快,因为减少了线程池调度。
但如果事件处理逻辑较慢,或者需要发起新的 IO 请求,比如需要查询数据库,则必须派发到线程池,否则 IO 线程阻塞,将导致不能接收其它请求。
如果用 IO 线程处理事件,又在事件处理过程中发起新的 IO 请求,比如在连接事件中发起登录请求,会报“可能引发死锁”异常,但不会真死锁。
根据请求的消息类被IO线程处理还是被业务线程池处理,Dubbo协议提供了下面几种线程模型:
all (默认): AllDispatcher,所有消息都派发到业务线程池,这些消息包括请求/响应/连接事件/断开事件/心跳等
direct : DirectDispacher,所有消息都不派发到业务线程池,全部在IO线程上直接执行
message:MessageOnlyDispatcher,只有请求响应消息派发到业务线程池,其他连接断开事件/心跳等消息,直接在IO线程上执行
execution:ExecutionDispatcher,只把请求类消息派发到业务线程池处理,但是响应和其它连接断开事件,心跳等消息直接在IO线程上执行
connection:ConnectionOrderedDispatcher,在IO线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到业务线程池处理
Dubbo 中所有的配置项都可以配置在 Spring 配置文件中,并且可以针对单个服务配置。
默认的情况下,每个Dubbo消费者与Dubbo提供者建立一个长连接,Dubbo官方对此的建议是:
Dubbo 缺省协议采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。
反之,Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。
(http://dubbo.apache.org/zh-cn/docs/user/references/protocol/dubbo.html)
以下也是Dubbo官方提供的一些常见问题回答:
因 dubbo 协议采用单一长连接,假设网络为千兆网卡,根据测试经验数据每条连接最多只能压满 7MByte(不同的环境可能不一样,供参考),理论上 1 个服务提供者需要 20 个服务消费者才能压满网卡。
因 dubbo 协议采用单一长连接,如果每次请求的数据包大小为 500KByte,假设网络为千兆网卡,每条连接最大 7MByte(不同的环境可能不一样,供参考),单个服务提供者的 TPS(每秒处理事务数)最大为:128MByte / 500KByte = 262。单个消费者调用单个服务提供者的 TPS(每秒处理事务数)最大为:7MByte / 500KByte = 14。如果能接受,可以考虑使用,否则网络将成为瓶颈。
因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,可能整个网站都在访问该服务,比如 Morgan 的提供者只有 6 台提供者,却有上百台消费者,每天有 1.5 亿次调用,如果采用常规的 hessian 服务,服务提供者很容易就被压跨,通过单一连接,保证单一消费者不会压死提供者,长连接,减少连接握手验证等,并使用异步 IO,复用线程池,防止 C10K 问题。