dubbo 用户手册初读
需求
- 服务注册管理,提供负载均衡和故障转移;
- 自动治理服务间的依赖关系;
- 动态扩容;
架构
- 注册中心和消费者、服务提供者之间是长连接,消费者和提供者与监控中心间使用短连接;
- 每分钟同步一次监控数据到监控中心;
快速启动
- 疑问:一个服务可以是提供者也可以是消费者,如果没有提供 dubbo:protocol 配置,即没有使用dubbo协议暴露端口,那肯定不是提供者?
- 疑问:使用multicast广播注册中心作为服务发现地址,写死的配置如何避免注册中心单点故障,如果需要调整注册中心地址怎么办?
- multicast广播注册中心适用于开发测试环境,优点是无需安装注册中心服务,缺点是依赖于网络路由,跨机房有风险;
依赖
- 疑问:依赖于spring,如果应用中spring使用其他版本会不会有冲突?
- 疑问:理论上说可以不依赖任何第三方包,要如何配置,有何缺点?
策略选择成熟度
- 注册中心
- zookeeper,推荐,使用广,依赖于zk的稳定性
- redis,支持基于客户端双写的方式,性能高
- multicast,无需安装,去中心化,依赖于网络拓扑,适用于开发测试环境
- simple,没有集群支持,可能单点故障,试用
- 远程调用协议
- dubbo协议,推荐,NIO复用长连接,连接池并发处理,减少握手次数,加大并发效率较高,缺点是单个连接传输大文件时会成为瓶颈;
- Rmi协议,基于TCP,可与原生RMI互操作,偶尔会连接失败,需要重建stub;
- Hession协议,基于http,可以原生hession互操作,需要hession.jar支持,http短连接消耗大;
- NIO框架
- netty,推荐,性能好
- Mina,老牌框架,稳定
- 序列化
- Hession序列化,推荐,性能较好,多语言支持,缺点是Hession各版本间可能存在兼容问题,dubbo使用hessian3.2.1;
- dubbo序列化,不传送pojo信息,在大量pojo需要传输时性能较好,缺点是当参数对象增加字段时,需要外部文件声明;
- json序列化,纯文本,跨语言,缺省采用FastJson解析,缺点是性能相对较差;
- java序列化,原生支持,缺点是性能较差;
- java代理
- Javassist代理,推荐,通过字节码生成代理类,代替反射,性能好,缺点是依赖于javassist.jar,perm内存配置大一些,如128M;
- java代理,jdk原生支持,性能较差;
- 集群模式
- Failover 集群,失败自动切换,读操作时推荐,缺点是重试会增加延迟;
- Failfast 集群,快速失败,只发起一次,失败立即报错,适合非幂等的写操作,缺点是如果有机器正在重启,可能会发生调用失败;
- Failsafe 集群,失败安全,出现失败时直接忽略,适用于收集日志等,缺点是会丢消息;
- Failback 集群,失败自动恢复,后台记录失败请求定时重发,适用于消息通知等,缺点是不可靠,重启丢失;
- Forking 集群,并行调用多个服务器,一个成功即可,适用于实时要求高的请求,缺点是会浪费资源;
- Broadcast 集群,广播调用所有提供者,有一个失败即报错,适用于更新本地服务状态,如本地日志或缓存等,速度慢;
- 负载均衡策略
- 随机,推荐,按照权重设置随机比例,缺点是重试时可能造成瞬间的压力不均;
- 轮询,按照公约后权重设置轮询比例,缺点是存在慢的机器积累请求的问题,极端情况下会出现雪崩现象;
- 最少活跃调用数,使慢的机器收到更少请求,缺点是不支持权重;
- 一致性hash,相同参数总是发到同一个提供者,如果提供者挂了,通过虚拟节点分摊到其他提供者,缺点是压力不均;
- 路由规则
- 条件路由,推荐,基于条件判断的路由,简单易用,缺点是复杂场景下很难使用条件描述;
- 脚本路由,基于脚本,功能强大,缺点是有可能成为后门;
- java容器
- spring容器,推荐,自动加载META-INF/spring下的配置文件;
- Jetty容器,启动内嵌的Jetty,缺点是大量访问页面时,会影响服务器的线程和内存;
- Log4j容器,自动配置log4j,自动按照进程划分目录,缺点是用户不能控制log4j的配置,不够灵活;
配置
- dubbo:provider,dubbo:protocol 和 dubbo:service中某属性没有配置时,使用此缺省配置;
- dubbo:consumer,dubbo:reference中某属性没有配置时,使用此缺省配置;
- dubbo:reference,引用配置默认是延迟加载,即只有被注入到其他bean或者getbean时才会初始化,可以加上init=”true”表示立即加载;
- 服务属性优先级,方法优于接口和全局,消费优于提供:消费方方法级>提供方方法级>消费方接口级>提供方方法级>消费方全局配置>提供方全局配置
- 建议由服务提供方设置超时属性,因为服务提供方最清楚可能的消耗时间;
集群容错
- 如何选择到最终的提供方?
线程模型
- server收到请求后,是直接在IO线程中处理响应(A),还是提交到线程池中(B)?
- 如果只是简单处理那A响应更快,因为省去了提交到B后的线程调度,但是如果这个请求要发起别的IO请求或者处理逻辑较慢,会导致接收不到其他请求;
- Dispatcher 策略,all-所有消息都派发到线程池,direct-所有消息都在IO线程上直接执行,message-请求响应派发到线程池,连接断开时间、心跳检测等直接在IO线程执行,..
- ThreadPool 策略,fixed-默认策略,固定线程池,不关闭一直持有,cached-缓存线程池,空闲一分后清理,limited-只增不减,防止大流量突袭造成性能问题;
回声测试
- 用于检测服务是否可用,可用于监控等场景;
- 所有暴露的服务均实现EchoService接口,直接强转后调用$echo方法判断即可;
本地存根
- 适用于本地调用前后做ThreadLocal缓存、参数验证、失败后伪造容错数据等;
- 讲proxy实例传递给Stub(Stub需包含可传入Proxy的构造方法);
<dubbo:service interface="com.foo.BarService" stub="com.foo.BarServiceStub" />
本地伪装
- Mock,用于服务降级,是Stub的子集,只有在调用过程中出现RpcException时才执行,此时可返回失败提示信息而不是抛错,包含无参的构造方法;
<dubbo:service interface="com.foo.BarService" mock="com.foo.BarServiceMock" />
并发控制
- 客户端或者服务端控制并发数量,executes-总的调用并发不能超过指定数量,actives-每个客户端的调用并发不能超过指定数量;
连接控制
- 服务端连接控制,acceptes-接受的连接数不超过指定数量;
- 客户端连接控制,conntections-使用的连接数不超过指定数量;
粘滞连接
- 让调用者尽可能请求同一个提供者,除非该提供者挂了才换另一台;
<dubbo:protocol name="dubbo" sticky="true" />
默认开启延迟连接以减少长连接数;- 疑问:只在dubbo协议下才有效?
令牌验证
- 防止消费者绕过注册中心直接调用提供者;
- 注册中心统一管理,灵活管理授权方式,不需要修改提供者;
- 有随机token(UUID)和固定token(指定的)两种;
路由规则
- 支持条件路由和脚本路由,脚本路由更灵活,但是有后门的风险;
- 可以实现黑白名单、读写分离(按照方法名路由)、重要应用隔离、分网段隔离等;
配置规则
- 可以实现禁用提供者、调整权重、调整负载均衡策略、服务降级(临时屏蔽出错的非关键服务)等;
服务降级
- 配置规则中的一种,用于降低服务不可用时对调用方的影响;
mock=force:return+null
在消费者方直接返回null,不发起调用;mock=fail:return+null
失败后返回null;
优雅停机
- 停机时,提供方不在接受新请求,等待已有请求运行完毕或超时,调用方不在发起新请求,或等待已有响应运行完毕或超时;
- 原理是使用JDK的ShutdownHook,所以强制关闭(kill -9 PID)是不会触发的;
注册中心
ZooKeeper 注册中心
- zk是一个树形的目录服务,支持消息订阅、推送,异常恢复等;
- provider启动时将URL写入到 /dubbo/ServiceA/providers下;
- consumer启动时订阅/dubbo/ServiceA/providers下的提供者地址,同时将URL写入到/dubbo/ServiceA/consumers下;
- 监控中心启动时,订阅/dubbo/ServiceA下的所有提供者和消费者URL地址;
- 提供者断电时,监控中心能自动删除提供者信息;
- 注册中心重启时,自动恢复注册数据及订阅请求;
- 支持订阅或注册失败时后台保存,定时重启;
在线运维
- 同时支持telnet命令和http,支持查看服务提供者和消费者状态、服务上下线等;
最佳实践
分包
- 接口、pojo、异常都放在api包中;
粒度
- 服务接口尽可能大粒度,表示某一个功能而不是某一个功能的一个步骤,减少分布式事务的出现;
- 使用表义的接口命名;
版本
- 每个服务都要定义版本,建议两段版本号,第三段版本号只做兼容升级时使用;
- 升级时先升一半提供者,再升所有消费者,最后升剩下的一半提供者;
- 疑问:为什么?如果升级后的提供者有问题需要回滚,可以直接将消费者切换到使用旧版的提供者?
兼容性
- 增加接口或pojo属性,可向后兼容,如果删除或更新,或者enum新增字段也不兼容,需变更版本号;
枚举值
- 不建议使用肯定会扩展的字段作为enum,此时建议使用String;
- 新增enum类型,如果是接口返回新增类型,先升级消费者,如果是接口入参新增类型,先升级提供者;
序列化
- 所有服务参数和返回值都是值传递,不支持引用传递;
异常
- 建议服务间使用异常而不是错误码;
- 封装如dao、sql异常等,防止消费者序列化错误;
- 如果出于性能优化考虑,重写异常的fillInStack方法,不填充后续异常堆栈;
- 不建议抛出checked异常,消费者通常不知道如何处理;
调用
try...catch
应加在合适的回滚边界上,而不是只因为是dubbo服务就加;- 提供者方要进行参数校验,如果要提高性能,可以在api包中添加Stub完成校验;
推荐用法
- 在Provider上多配置Consumer端属性,因为提供者对于服务性能参数配置更了解,如 timeout、loadbalance(默认random)、retry(默认2,即一共1+2=最多3次调用)、actives(消费者超过最大并发限制时新的调用请求进入WAIT,直到timeout);
- 在Provider上多配置Provider端属性,如executes、threads(线程池大小);
- 配置负责人(应用、服务、引用等),ower属性;
- 配置dubbo缓存文件,
<dubbo:registry file="${user.home}/output/dubbo.cache"/>
,用于注册中心挂掉时从缓存文件加载提供者列表,保证可用性,注意多进程使用不同的目录文件; - 监控配置,服务推荐使用固定端口,这样在注册中心挂掉后可以通过缓存文件保证可用性;
- 推荐使用xml配置而不是properties;