Dubbo的一些编码约定和设计原则

编码约定

代码风格

Dubbo 的源代码和 JavaDoc 遵循如下的规范:html

异常和日志

  • 尽量携带完整的上下文信息,好比出错缘由,出错的机器地址,调用对方的地址,连的注册中心地址,使用 Dubbo 的版本等。
  • 尽可能将直接缘由写在最前面,全部上下文信息,在缘由后用键值对显示。
  • 抛出异常的地方不用打印日志,由最终处理异常者决定打印日志的级别,吃掉异常必需打印日志。
  • 打印 ERROR 日志表示须要报警,打印 WARN 日志表示能够自动恢复,打印 INFO 表示正常信息或彻底不影响运行。
  • 建议应用方在监控中心配置 ERROR 日志实时报警,WARN 日志每周汇总发送通知。
  • RpcException 是 Dubbo 对外的惟一异常类型,全部内部异常,若是要抛出给用户,必须转为 RpcException。
  • RpcException 不能有子类型,全部类型信息用 ErrorCode 标识,以便保持兼容。

配置和 URL

  • 配置对象属性首字母小写,多个单词用驼峰命名 。
  • 配置属性所有用小写,多个单词用"-"号分隔 。
  • URL参数所有用小写,多个单词用"."号分隔 。
  • 尽量用 URL 传参,不要自定义 Map 或其它上下文格式,配置信息也转成 URL 格式使用。
  • 尽可能减小 URL 嵌套,保持 URL 的简洁性。

单元和集成测试

  • 单元测试统一用 JUnit 和 EasyMock,集成测试用 TestNG,数据库测试用 DBUnit。
  • 保持单元测试用例的运行速度,不要将性能和大的集成用例放在单元测试中。
  • 保持单元测试的每一个用例都用 try...finally 或 tearDown 释放资源。
  • 减小 while 循环等待结果的测试用例,对定时器和网络的测试,用以将定时器中的逻辑抽为方法测试。
  • 对于容错行为的测试,好比 failsafe 的测试,统一用 LogUtil 断言日志输出。

扩展点基类与 AOP

  • AOP 类都命名为 XxxWrapper,基类都命名为 AbstractXxx。
  • 扩展点之间的组合将关系由 AOP 完成,ExtensionLoader 只负载加载扩展点,包括 AOP 扩展。
  • 尽可能采用 IoC 注入扩展点之间的依赖,不要直接依赖 ExtensionLoader 的工厂方法。
  • 尽可能采用 AOP 实现扩展点的通用行为,而不要用基类,好比负载均衡以前的 isAvailable 检查,它是独立于负载均衡以外的,不须要检查的是URL参数关闭。
  • 对多种类似类型的抽象,用基类实现,好比 RMI, Hessian 等第三方协议都已生成了接口代理,只需将将接口代理转成 Invoker 便可完成桥接,它们能够用公共基类实现此逻辑。
  • 基类也是 SPI 的一部分,每一个扩展点都应该有方便使用的基类支持。

模块与分包

  • 基于复用度分包,老是一块儿使用的放在同一包下,将接口和基类分红独立模块,大的实现也使用独立模块。
  • 全部接口都放在模块的根包下,基类放在 support 子包下,不一样实现用放在以扩展点名字命名的子包下。
  • 尽可能保持子包依赖父包,而不要反向。

设计原则

1、魔鬼在细节

http://javatar.iteye.com/blog/1056664java

最近一直担忧 Dubbo 分布式服务框架后续若是维护人员增多或变动,会出现质量的降低, 我在想,有没有什么是须要你们共同遵照的,根据平时写代码时的一习惯,总结了一下在写代码过程当中,尤为是框架代码,要时刻牢记的细节。可能下面要讲的这些,你们都会以为很简单,很基础,但要作到时刻牢记。在每一行代码中都考虑这些因素,是须要很大耐心的, 你们常常说,魔鬼在细节中,确实如此。ios

防止空指针和下标越界

这是我最不喜欢看到的异常,尤为在核心框架中,我更愿看到信息详细的参数不合法异常。这也是一个健状的程序开发人员,在写每一行代码都应在潜意识中防止的异常。基本上要能确保一次写完的代码,在不测试的状况,都不会出现这两个异常才算合格。web

保证线程安全性和可见性

对于框架的开发人员,对线程安全性和可见性的深刻理解是最基本的要求。须要开发人员,在写每一行代码时都应在潜意识中确保其正确性。由于这种代码,在小并发下作功能测试时,会显得很正常。但在高并发下就会出现莫明其妙的问题,并且场景很难重现,极难排查。算法

尽早失败和前置断言

尽早失败也应该成为潜意识,在有传入参数和状态变化时,均在入口处所有断言。一个不合法的值和状态,在第一时间就应报错,而不是等到要用时才报错。由于等到要用时,可能前面已经修改其它相关状态,而在程序中不多有人去处理回滚逻辑。这样报错后,其实内部状态可能已经混乱,极易在一个隐蔽分支上引起程序不可恢复。spring

分离可靠操做和不可靠操做

这里的可靠是狭义的指是否会抛出异常或引发状态不一致,好比,写入一个线程安全的 Map,能够认为是可靠的,而写入数据库等,能够认为是不可靠的。开发人员必须在写每一行代码时,都注意它的可靠性与否,在代码中尽可能划分开,并对失败作异常处理,并为容错,自我保护,自动恢复或切换等补偿逻辑提供清晰的切入点,保证后续增长的代码不至于放错位置,而致使原先的容错处理陷入混乱。数据库

异常防护,但不忽略异常

这里讲的异常防护,指的是对非必须途径上的代码进行最大限度的容忍,包括程序上的 BUG,好比:获取程序的版本号,会经过扫描 Manifest 和 jar 包名称抓取版本号,这个逻辑是辅助性的,但代码却很多,初步测试也没啥问题,但应该在整个 getVersion() 中加上一个全函数的 try-catch 打印错误日志,并返回基本版本,由于 getVersion() 可能存在未知特定场景异常,或被其余的开发人员误修改逻辑(但通常人员不会去掉 try-catch),而若是它抛出异常会致使主流程异常,这是咱们不但愿看到的。但这里要控制个度,不要随意 try-catch,更不要无声无息的吃掉异常。apache

缩小可变域和尽可能 final

若是一个类能够成为不变类(Immutable Class),就优先将它设计成不变类。不变类有自然的并发共享优点,减小同步或复制,并且能够有效帮忙分析线程安全的范围。就算是可变类,对于从构造函数传入的引用,在类中持有时,最好将字段 final,以避免被中途误修改引用。不要觉得这个字段是私有的,这个类的代码都是我本身写的,不会出现对这个字段的从新赋值。要考虑的一个因素是,这个代码可能被其余人修改,他不知道你的这个弱约定,final 就是一个不变契约。编程

下降修改时的误解性,不埋雷

前面不停的提到代码被其余人修改,这也开发人员要随时紧记的。这个其余人包括将来的本身,你要总想着这个代码可能会有人去改它。我应该给修改的人一点什么提示,让他知道我如今的设计意图,而不要在程序里面加潜规则,或埋一些容易忽视的雷,好比:你用 null 表示不可用,size 等于 0 表示黑名单,这就是一个雷,下一个修改者,包括你本身,都不会记得有这样的约定,可能后面为了改某个其它 BUG,不当心改到了这里,直接引爆故障。对于这个例子,一个原则就是永远不要区分 null 引用和 empty 值。json

提升代码的可测性

这里的可测性主要指 Mock 的容易程度,和测试的隔离性。至于测试的自动性,可重复性,非偶然性,无序性,完备性(全覆盖),轻量性(可快速执行),通常开发人员,加上 JUnit 等工具的辅助基本都能作到,也能理解它的好处,只是工做量问题。这里要特别强调的是测试用例的单一性(只测目标类自己)和隔离性(不传染失败)。如今的测试代码,过于强调完备性,大量重复交叉测试,看起来没啥坏处,但测试代码越多,维护代价越高。常常出现的问题是,修改一行代码或加一个判断条件,引发 100 多个测试用例不经过。时间一紧,谁有这个闲功夫去改这么多形态万千的测试用例?长此以往,这个测试代码就已经不能真实反应代码如今的情况,不少时候会被迫绕过。最好的状况是,修改一行代码,有且只有一行测试代码不经过。若是修改了代码而测试用例还能经过,那也不行,表示测试没有覆盖到。另外,可 Mock 性是隔离的基础,把间接依赖的逻辑屏蔽掉。可 Mock 性的一个最大的杀手就是静态方法,尽可能少用。

2、一些设计上的基本常识

http://javatar.iteye.com/blog/706098

最近给团队新人讲了一些设计上的常识,可能会对其它的新人也有些帮助,把暂时想到的几条,先记在这里。

API 与 SPI 分离

框架或组件一般有两类客户,一个是使用者,一个是扩展者。API (Application Programming Interface) 是给使用者用的,而 SPI (Service Provide Interface) 是给扩展者用的。在设计时,尽可能把它们隔离开,而不要混在一块儿。也就是说,使用者是看不到扩展者写的实现的。

好比:一个 Web 框架,它有一个 API 接口叫 Action,里面有个 execute() 方法,是给使用者用来写业务逻辑的。而后,Web 框架有一个 SPI 接口给扩展者控制输出方式,好比用 velocity 模板输出仍是用 json 输出等。若是这个 Web 框架使用一个都继承 Action 的 VelocityAction 和一个 JsonAction 作为扩展方式,要用 velocity 模板输出的就继承 VelocityAction,要用 json 输出的就继承 JsonAction,这就是 API 和 SPI 没有分离的反面例子,SPI 接口混在了 API 接口中。

mix-api-spi

合理的方式是,有一个单独的 Renderer 接口,有 VelocityRenderer 和 JsonRenderer 实现,Web 框架将 Action 的输出转交给 Renderer 接口作渲染输出。

seperate-api-spi

服务域/实体域/会话域分离

任何框架或组件,总会有核心领域模型,好比:Spring 的 Bean,Struts 的 Action,Dubbo 的 Service,Napoli 的 Queue 等等。这个核心领域模型及其组成部分称为实体域,它表明着咱们要操做的目标自己。实体域一般是线程安全的,无论是经过不变类,同步状态,或复制的方式。

服务域也就是行为域,它是组件的功能集,同时也负责实体域和会话域的生命周期管理, 好比 Spring 的 ApplicationContext,Dubbo 的 ServiceManager 等。服务域的对象一般会比较重,并且是线程安全的,并以单一实例服务于全部调用。

什么是会话?就是一次交互过程。会话中重要的概念是上下文,什么是上下文?好比咱们说:“老地方见”,这里的“老地方”就是上下文信息。为何说“老地方”对方会知道,由于咱们前面定义了“老地方”的具体内容。因此说,上下文一般持有交互过程当中的状态变量等。会话对象一般较轻,每次请求都从新建立实例,请求结束后销毁。简而言之:把元信息交由实体域持有,把一次请求中的临时状态由会话域持有,由服务域贯穿整个过程。

ddd

在重要的过程上设置拦截接口

若是你要写个远程调用框架,那远程调用的过程应该有一个统一的拦截接口。若是你要写一个 ORM 框架,那至少 SQL 的执行过程,Mapping 过程要有拦截接口;若是你要写一个 Web 框架,那请求的执行过程应该要有拦截接口,等等。没有哪一个公用的框架能够 Cover 住全部需求,容许外置行为,是框架的基本扩展方式。这样,若是有人想在远程调用前,验证下令牌,验证下黑白名单,统计下日志;若是有人想在 SQL 执行前加下分页包装,作下数据权限控制,统计下 SQL 执行时间;若是有人想在请求执行前检查下角色,包装下输入输出流,统计下请求量,等等,就能够自行完成,而不用侵入框架内部。拦截接口,一般是把过程自己用一个对象封装起来,传给拦截器链,好比:远程调用主过程为 invoke(),那拦截器接口一般为 invoke(Invocation),Invocation 对象封装了原本要执行过程的上下文,而且 Invocation 里有一个 invoke() 方法,由拦截器决定何时执行,同时,Invocation 也表明拦截器行为自己,这样上一拦截器的 Invocation 实际上是包装的下一拦截器的过程,直到最后一个拦截器的 Invocation 是包装的最终的 invoke() 过程;同理,SQL 主过程为 execute(),那拦截器接口一般为 execute(Execution),原理同样。固然,实现方式能够任意,上面只是举例。

filter-chain

重要的状态的变动发送事件并留出监听接口

这里先要讲一个事件和上面拦截器的区别,拦截器是干预过程的,它是过程的一部分,是基于过程行为的,而事件是基于状态数据的,任何行为改变的相同状态,对事件应该是一致的。事件一般是过后通知,是一个 Callback 接口,方法名一般是过去式的,好比 onChanged()。好比远程调用框架,当网络断开或连上应该发出一个事件,当出现错误也能够考虑发出一个事件,这样外围应用就有可能观察到框架内部的变化,作相应适应。

event-listener

扩展接口职责尽量单一,具备可组合性

好比,远程调用框架它的协议是能够替换的。若是只提供一个总的扩展接口,固然能够作到切换协议,但协议支持是能够细分为底层通信,序列化,动态代理方式等等。若是将接口拆细,正交分解,会更便于扩展者复用已有逻辑,而只是替换某部分实现策略。固然这个分解的粒度须要把握好。

微核插件式,平等对待第三方

大凡发展的比较好的框架,都遵照微核的理念。Eclipse 的微核是 OSGi, Spring 的微核是 BeanFactory,Maven 的微核是 Plexus。一般核心是不该该带有功能性的,而是一个生命周期和集成容器,这样各功能能够经过相同的方式交互及扩展,而且任何功能均可以被替换。若是作不到微核,至少要平等对待第三方,即原做者能实现的功能,扩展者应该能够经过扩展的方式所有作到。原做者要把本身也看成扩展者,这样才能保证框架的可持续性及由内向外的稳定性。

不要控制外部对象的生命周期

好比上面说的 Action 使用接口和 Renderer 扩展接口。框架若是让使用者或扩展者把 Action 或 Renderer 实现类的类名或类元信息报上来,而后在内部经过反射 newInstance() 建立一个实例,这样框架就控制了 Action 或 Renderer 实现类的生命周期,Action 或 Renderer 的生老病死,框架都本身作了,外部扩展或集成都无能为力。好的办法是让使用者或扩展者把 Action 或 Renderer 实现类的实例报上来,框架只是使用这些实例,这些对象是怎么建立的,怎么销毁的,都和框架无关,框架最多提供工具类辅助管理,而不是绝对控制。

可配置必定可编程,并保持友好的 CoC 约定

由于使用环境的不肯定因素不少,框架总会有一些配置,通常都会到 classpath 直扫某个指定名称的配置,或者启动时容许指定配置路径。作为一个通用框架,应该作到凡是能配置文件作的必定要能经过编程方式进行,不然当使用者须要将你的框架与另外一个框架集成时就会带来不少没必要要的麻烦。

另外,尽量作一个标准约定,若是用户按某种约定作事时,就不须要该配置项。好比:配置模板位置,你能够约定,若是放在 templates 目录下就不用配了,若是你想换个目录,就配置下。

区分命令与查询,明确前置条件与后置条件

这个是契约式设计的一部分,尽可能遵照有返回值的方法是查询方法,void 返回的方法是命令。查询方法一般是幂等性的,无反作用的,也就是不改变任何状态,调 n 次结果都是同样的,好比 get 某个属性值,或查询一条数据库记录。命令是指有反作用的,也就是会修改状态,好比 set 某个值,或 update 某条数据库记录。若是你的方法即作了修改状态的操做,又作了查询返回,若是可能,将其拆成写读分离的两个方法,好比:User deleteUser(id),删除用户并返回被删除的用户,考虑改成 getUser() 和 void 的 deleteUser()。 另外,每一个方法都尽可能前置断言传入参数的合法性,后置断言返回结果的合法性,并文档化。

增量式扩展,而不要扩充原始核心概念

参见:谈谈扩充式扩展与增量式扩展

3、谈谈扩充式扩展与增量式扩展

http://javatar.iteye.com/blog/690845

咱们平台的产品愈来愈多,产品的功能也愈来愈多。平台的产品为了适应各 BU 和部门以及产品线的需求,势必会将不少不相干的功能凑在一块儿,客户能够选择性的使用。为了兼容更多的需求,每一个产品,每一个框架,都在不停的扩展,而咱们常常会选择一些扩展的扩展方式,也就是将新旧功能扩展成一个通用实现。我想讨论是,有些状况下也能够考虑增量式的扩展方式,也就是保留原功能的简单性,新功能独立实现。我最近一直作分布式服务框架的开发,就拿咱们项目中的问题开涮吧。

好比:远程调用框架,确定少不了序列化功能,功能很简单,就是把流转成对象,对象转成流。但因有些地方可能会使用 osgi,这样序列化时,IO 所在的 ClassLoader 可能和业务方的 ClassLoader 是隔离的。须要将流转换成 byte[] 数组,而后传给业务方的 ClassLoader 进行序列化。为了适应 osgi 需求,把原来非 osgi 与 osgi 的场景扩展了一下,这样,无论是否是 osgi 环境,都先将流转成 byte[] 数组,拷贝一次。然而,大部分场景都用不上 osgi,却为 osgi 付出了代价。而若是采用增量式扩展方式,非 osgi 的代码原封不动,再加一个 osgi 的实现,要用 osgi 的时候,直接依赖 osgi 实现便可。

再好比:最开始,远程服务都是基于接口方法,进行透明化调用的。这样,扩展接口就是, invoke(Method method, Object[] args),后来,有了无接口调用的需求,就是没有接口方法也能调用,并将 POJO 对象都转换成 Map 表示。由于 Method 对象是不能直接 new 出来的,咱们不自觉选了一个扩展式扩展,把扩展接口改为了 invoke(String methodName, String[] parameterTypes, String returnTypes, Object[] args),致使无论是否是无接口调用,都得把 parameterTypes 从 Class[] 转成 String[]。若是选用增量式扩展,应该是保持原有接口不变,增长一个 GeneralService 接口,里面有一个通用的 invoke() 方法,和其它正常业务上的接口同样的调用方式,扩展接口也不用变,只是 GeneralServiceImpl 的 invoke() 实现会将收到的调用转给目标接口,这样就能将新功能增量到旧功能上,并保持原来结构的简单性。

再再好比:无状态消息发送,很简单,序列化一个对象发过去就行。后来有了同步消息发送需求,须要一个 Request/Response 进行配对,采用扩展式扩展,天然想到,无状态消息实际上是一个没有 Response 的 Request,因此在 Request 里加一个 boolean 状态,表示要不要返回 Response。若是再来一个会话消息发送需求,那就再加一个 Session 交互,而后发现,原来同步消息发送是会话消息的一种特殊状况,全部场景都传 Session,不须要 Session 的地方无视便可。

open-expand

若是采用增量式扩展,无状态消息发送原封不动,同步消息发送,在无状态消息基础上加一个 Request/Response 处理,会话消息发送,再加一个 SessionRequest/SessionResponse 处理。

close-expand

4、配置设计

http://javatar.iteye.com/blog/949527

Dubbo 如今的设计是彻底无侵入,也就是使用者只依赖于配置契约。通过多个版本的发展,为了知足各类需求场景,配置愈来愈多。为了保持兼容,配置只增不减,里面潜伏着各类风格,约定,规则。新版本也将配置作了一次调整,去掉了 dubbo.properties,改成全 spring 配置。将想到的一些记在这,备忘。

配置分类

首先,配置的用途是有多种的,大体能够分为:

  1. 环境配置,好比:链接数,超时等配置。
  2. 描述配置,好比:服务接口描述,服务版本等。
  3. 扩展配置,好比:协议扩展,策略扩展等。

配置格式

一般环境配置,用 properties 配置会比较方便,由于都是一些离散的简单值,用 key-value 配置能够减小配置的学习成本。

而描述配置,一般信息比较多,甚至有层次关系,用 xml 配置会比较方便,由于树结构的配置表现力更强。若是很是复杂,也能够考自定义 DSL 作为配置。有时候这类配置也能够用 Annotation 代替, 由于这些配置和业务逻辑相关,放在代码里也是合理的。

另外扩展配置,可能不尽相同。若是只是策略接口实现类替换,能够考虑 properties 等结构。若是有复杂的生命周期管理,可能须要 XML 等配置。有时候扩展会经过注册接口的方式提供。

配置加载

对于环境配置,在 java 世界里,比较常规的作法,是在 classpath 下约定一个以项目为名称的 properties 配置,好比:log4j.properties,velocity.properties等。产品在初始化时,自动从 classpath 下加载该配置。咱们平台的不少项目也使用相似策略,如:dubbo.properties,comsat.xml 等。这样有它的优点,就是基于约定,简化了用户对配置加载过程的干预。但一样有它的缺点,当 classpath 存在一样的配置时,可能误加载,以及在 ClassLoader 隔离时,可能找不到配置,而且,当用户但愿将配置放到统一的目录时,不太方便。

Dubbo 新版本去掉了 dubbo.properties,由于该约定常常形成配置冲突。

而对于描述配置,由于要参与业务逻辑,一般会嵌到应用的生命周期管理中。如今使用 spring 的项目愈来愈多,直接使用 spring 配置的比较广泛,并且 spring 容许自定义 schema,配置简化后很方便。固然,也有它的缺点,就是强依赖 spring,能够提编程接口作了配套方案。

在 Dubbo 即存在描述配置,也有环境配置。一部分用 spring 的 schame 配置加载,一部分从 classpath 扫描 properties 配置加载。用户感受很是不便,因此在新版本中进行了合并,统一放到 spring 的 schame 配置加载,也增长了配置的灵活性。

扩展配置,一般对配置的聚合要求比较高。由于产品须要发现第三方实现,将其加入产品内部。在 java 世界里,一般是约定在每一个 jar 包下放一个指定文件加载,好比:eclipse 的 plugin.xml,struts2 的 struts-plugin.xml 等,这类配置能够考虑 java 标准的服务发现机制,即在 jar 包的 META-INF/services 下放置接口类全名文件,内容为每行一个实现类类名,就像 jdk 中的加密算法扩展,脚本引擎扩展,新的 JDBC 驱动等,都是采用这种方式。参见:ServiceProvider 规范

Dubbo 旧版本经过约定在每一个 jar 包下,放置名为 dubbo-context.xml 的 spring 配置进行扩展与集成,新版本改为用 jdk 自带的 META-INF/services 方式,去掉过多的 spring 依赖。

可编程配置

配置的可编程性是很是必要的,无论你以何种方式加载配置文件,都应该提供一个编程的配置方式,容许用户不使用配置文件,直接用代码完成配置过程。由于一个产品,尤为是组件类产品,一般须要和其它产品协做使用,当用户集成你的产品时,可能须要适配配置方式。

Dubbo 新版本提供了与 xml 配置一对一的配置类,如:ServiceConfig 对应 <dubbo:service />,而且属性也一对一,这样有利于文件配置与编程配置的一致性理解,减小学习成本。

配置缺省值

配置的缺省值,一般是设置一个常规环境的合理值,这样能够减小用户的配置量。一般建议以线上环境为参考值,开发环境能够经过修改配置适应。缺省值的设置,最好在最外层的配置加载就作处理。程序底层若是发现配置不正确,就应该直接报错,容错在最外层作。若是在程序底层使用时,发现配置值不合理,就填一个缺省值,很容易掩盖表面问题,而引起更深层次的问题。而且配置的中间传递层,极可能并不知道底层使用了一个缺省值,一些中间的检测条件就可能失效。Dubbo 就出现过这样的问题,中间层用“地址”作为缓存 Key, 而底层,给“地址”加了一个缺省端口号,致使不加端口号的“地址”和加了缺省端口的“地址”并无使用相同的缓存。

配置一致性

配置总会隐含一些风格或潜规则,应尽量保持其一致性。好比:不少功能都有开关,而后有一个配置值:

  1. 是否使用注册中心,注册中心地址。
  2. 是否容许重试,重试次数。

你能够约定:

  1. 每一个都是先配置一个 boolean 类型的开关,再配置一个值。
  2. 用一个无效值表明关闭,N/A地址,0重试次数等。

无论选哪一种方式,全部配置项,都应保持同一风格,Dubbo 选的是第二种。类似的还有,超时时间,重试时间,定时器间隔时间。若是一个单位是秒,另外一个单位是毫秒(C3P0的配置项就是这样),配置人员会疯掉。

配置覆盖

提供配置时,要同时考虑开发人员,测试人员,配管人员,系统管理员。测试人员是不能修改代码的,而测试的环境极可能较为复杂,须要为测试人员留一些“后门”,能够在外围修改配置项。就像 spring 的 PropertyPlaceholderConfigurer 配置,支持 SYSTEM_PROPERTIES_MODE_OVERRIDE,能够经过 JVM 的 -D 参数,或者像 hosts 同样约定一个覆盖配置文件,在程序外部,修改部分配置,便于测试。

Dubbo 支持经过 JVM 参数 -Dcom.xxx.XxxService=dubbo://10.1.1.1:1234 直接使远程服务调用绕过注册中心,进行点对点测试。还有一种状况,开发人员增长配置时,都会按线上的部署状况作配置,如:<dubbo:registry address="${dubbo.registry.address}" /> 由于线上只有一个注册中心,这样的配置是没有问题的,而测试环境可能有两个注册中心,测试人员不可能去修改配置,改成: <dubbo:registry address="${dubbo.registry.address1}" />, <dubbo:registry address="${dubbo.registry.address2}" />,因此这个地方,Dubbo 支持在 ${dubbo.registry.address} 的值中,经过竖号分隔多个注册中心地址,用于表示多注册中心地址。

配置继承

配置也存在“重复代码”,也存在“泛化与精化”的问题。好比:Dubbo 的超时时间设置,每一个服务,每一个方法,都应该能够设置超时时间。但不少服务不关心超时,若是要求每一个方法都配置,是不现实的。因此 Dubbo 采用了方法超时继承服务超时,服务超时再继承缺省超时,没配置时,一层层向上查找。

另外,Dubbo 旧版本全部的超时时间,重试次数,负载均衡策略等都只能在服务消费方配置。但实际使用过程当中发现,服务提供方比消费方更清楚,但这些配置项是在消费方执行时才用到的。新版本,就加入了在服务提供方也能配这些参数,经过注册中心传递到消费方, 作为参考值,若是消费方没有配置,就以提供方的配置为准,至关于消费方继承了提供方的建议配置值。而注册中心在传递配置时,也能够在中途修改配置,这样就达到了治理的目的,继承关系至关于:服务消费者 --> 注册中心 --> 服务提供者

configuration-override

配置向后兼容

向前兼容很好办,你只要保证配置只增不减,就基本上能保证向前兼容。但向后兼容,也是要注意的,要为后续加入新的配置项作好准备。若是配置出现一个特殊配置,就应该为这个“特殊”状况约定一个兼容规则,由于这个特殊状况,颇有可能在之后还会发生。好比:有一个配置文件是保存“服务=地址”映射关系的,其中有一行特殊,保存的是“注册中心=地址”。如今程序加载时,约定“注册中心”这个Key是特殊的,作特别处理,其它的都是“服务”。然而,新版本发现,要加一项“监控中心=地址”,这时,旧版本的程序会把“监控中心”作为“服务”处理,由于旧代码是不能改的,兼容性就很会很麻烦。若是先前约定“特殊标识+XXX”为特殊处理,后续就会方便不少。

向后兼容性,能够多向HTML5学习,参见:HTML5设计原理

5、设计实现的健壮性

http://oldratlee.com/380/tech/java/robustness-of-implement.html

Dubbo 做为远程服务暴露、调用和治理的解决方案,是应用运转的经络,其自己实现健壮性的重要程度是不言而喻的。

这里列出一些 Dubbo 用到的原则和方法。

日志

日志是发现问题、查看问题一个最经常使用的手段。日志质量每每被忽视,没有日志使用上的明确约定。重视 Log 的使用,提升 Log 的信息浓度。日志过多、过于混乱,会致使有用的信息被淹没。

要有效利用这个工具要注意:

严格约定WARN、ERROR级别记录的内容

  • WARN 表示能够恢复的问题,无需人工介入。
  • ERROR 表示须要人工介入问题。

有了这样的约定,监管系统发现日志文件的中出现 ERROR 字串就报警,又尽可能减小了发生。过多的报警会让人疲倦,令人对报警失去警戒性,使 ERROR 日志失去意义。再辅以人工按期查看 WARN 级别信息,以评估系统的“亚健康”程度。

日志中,尽可能多的收集关键信息

哪些是关键信息呢?

  • 出问题时的现场信息,即排查问题要用到的信息。如服务调用失败时,要给出使用 Dubbo 的版本、服务提供者的 IP、使用的是哪一个注册中心;调用的是哪一个服务、哪一个方法等等。这些信息若是不给出,那么过后人工收集的,问题事后现场可能已经不能复原,加大排查问题的难度。
  • 若是可能,给出问题的缘由和解决方法。这让维护和问题解决变得简单,而不是寻求精通者(每每是实现者)的帮助。

同一个或是一类问题不要重复记录屡次

同一个或是一类异常日志连续出现几十遍的状况,仍是经常能看到的。人眼很容易漏掉淹没在其中不同的重要日志信息。要尽可能避免这种状况。在能够预见会出现的状况,有必要加一些逻辑来避免。

如为一个问题准备一个标志,出问题后打日志后设置标志,避免重复打日志。问题恢复后清除标志。

虽然有点麻烦,可是这样作保证日志信息浓度,让监控更有效。

界限设置

资源是有限的,CPU、内存、IO 等等。不要由于外部的请求、数据不受限的而崩溃。

线程池(ExectorService)的大小和饱和策略

Server 端用于处理请求的 ExectorService 设置上限。ExecutorService 的任务等待队列使用有限队列,避免资源耗尽。当任务等待队列饱和时,选择一个合适的饱和策略。这样保证平滑劣化。

在 Dubbo 中,饱和策略是丢弃数据,等待结果也只是请求的超时。

达到饱和时,说明已经达到服务提供方的负荷上限,要在饱和策略的操做中日志记录这个问题,以发出监控警报。记得注意不要重复屡次记录哦。(注意,缺省的饱和策略不会有这些附加的操做。)根据警报的频率,已经决定扩容调整等等,避免系统问题被忽略。

集合容量

若是确保进入集合的元素是可控的且是足够少,则能够放心使用。这是大部分的状况。若是不能保证,则使用有有界的集合。当到达界限时,选择一个合适的丢弃策略。

容错-重试-恢复

高可用组件要容忍其依赖组件的失败。

Dubbo 的服务注册中心

目前服务注册中心使用了数据库来保存服务提供者和消费者的信息。注册中心集群不一样注册中心也经过数据库来之间同步数据,以感知其它注册中心上提供者。注册中心会内存中保证一份提供者和消费者数据,数据库不可用时,注册中心独立对外正常运转,只是拿不到其它注册中心的数据。当数据库恢复时,重试逻辑会内存中修改的数据写回数据库,并拿到数据库中新数据。

服务的消费者

服务消息者从注册中心拿到提供者列表后,会保存提供者列表到内存和磁盘文件中。这样注册中心宕后消费者能够正常运转,甚至能够在注册中心宕机过程当中重启消费者。消费者启动时,发现注册中心不可用,会读取保存在磁盘文件中提供者列表。重试逻辑保证注册中心恢复后,更新信息。

重试延迟策略

上一点的子问题。Dubbo 中碰到有两个相关的场景。

数据库上的活锁

注册中心会定时更新数据库一条记录的时间戳,这样集群中其它的注册中心感知它是存活。过时注册中心和它的相关数据 会被清除。数据库正常时,这个机制运行良好。可是数据库负荷高时,其上的每一个操做都会很慢。这就出现:

A 注册中心认为 B 过时,删除 B 的数据。 B 发现本身的数据没有了,从新写入本身的数据的反复操做。这些反复的操做又加剧了数据库的负荷,恶化问题。

能够使用下面逻辑:

当 B 发现本身数据被删除时(写入失败),选择等待这段时间再重试。重试时间能够选择指数级增加,如第一次等 1 分钟,第二次 10 分钟、第三次 100 分钟。

这样操做减小后,保证数据库能够冷却(Cool Down)下来。

Client 重连注册中心

当一个注册中心停机时,其它的 Client 会同时接收事件,而去重连另外一个注册中心。Client 数量相对比较多,会对注册中心形成冲击。避免方法能够是 Client 重连时随机延时 3 分钟,把重连分散开。

6、防痴呆设计

http://javatar.iteye.com/blog/804187

最近有点痴呆,由于解决了太多的痴呆问题。服务框架实施面超来超广,已有 50 多个项目在使用,天天都要去帮应用查问题,来来回回,发现大部分都是配置错误,或者重复的文件或类,或者网络不通等,因此准备在新版本中加入防痴呆设计。估且这么叫吧,可能很简单,但对排错速度仍是有点帮助,但愿能抛砖引玉,也但愿你们多给力,想出更多的防范措施共享出来。

检查重复的jar包

最痴呆的问题,就是有多个版本的相同jar包,会出现新版本的 A 类,调用了旧版本的 B 类,并且和JVM加载顺序有关,问题带有偶然性,误导性,遇到这种莫名其妙的问题,最头疼,因此,第一条,先把它防住,在每一个 jar 包中挑一个必定会加载的类,加上重复类检查,给个示例:

static { Duplicate.checkDuplicate(Xxx.class); }

检查重复工具类:

public final class Duplicate { private Duplicate() {} public static void checkDuplicate(Class cls) { checkDuplicate(cls.getName().replace('.', '/') + ".class"); } public static void checkDuplicate(String path) { try { // 在ClassPath搜文件 
            Enumeration urls = Thread.currentThread().getContextClassLoader().getResources(path); Set files = new HashSet(); while (urls.hasMoreElements()) { URL url = urls.nextElement(); if (url != null) { String file = url.getFile(); if (file != null &amp;&amp; file.length() &gt; 0) { files.add(file); } } } // 若是有多个,就表示重复 
            if (files.size() &gt; 1) { logger.error("Duplicate class " + path + " in " + files.size() + " jar " + files); } } catch (Throwable e) { // 防护性容错 
 logger.error(e.getMessage(), e); } } }

检查重复的配置文件

配置文件加载错,也是常常碰到的问题。用户一般会和你说:“我配置的很正确啊,不信我发给你看下,但就是报错”。而后查一圈下来,原来他发过来的配置根本没加载,平台不少产品都会在 classpath 下放一个约定的配置,若是项目中有多个,一般会取JVM加载的第一个,为了避免被这么低级的问题折腾,和上面的重复jar包同样,在配置加载的地方,加上:

Duplicate.checkDuplicate("xxx.properties");

检查全部可选配置

必填配置估计你们都会检查,由于没有的话,根本无法运行。但对一些可选参数,也应该作一些检查,好比:服务框架容许经过注册中心关联服务消费者和服务提供者,也容许直接配置服务提供者地址点对点直连,这时候,注册中心地址是可选的,但若是没有配点对点直连配置,注册中心地址就必定要配,这时候也要作相应检查。

异常信息给出解决方案

在给应用排错时,最怕的就是那种只有简单的一句错误描述,啥信息都没有的异常信息。好比上次碰到一个 Failed to get session 异常,就这几个单词,啥都没有,哪一个 session 出错? 什么缘由 Failed? 看了都快疯掉,因是线上环境很差调试,并且有些场景不是每次都能重现。异常最基本要带有上下文信息,包括操做者,操做目标,缘由等,最好的异常信息,应给出解决方案,好比上面能够给出:"从 10.20.16.3 到 10.20.130.20:20880 之间的网络不通,请在 10.20.16.3 使用 telnet 10.20.130.20 20880 测试一下网络,若是是跨机房调用,多是防火墙阻挡,请联系 SA 开通访问权限" 等等,上面甚至能够根据 IP 段判断是否是跨机房。另一个例子,是 spring-web 的 context 加载,若是在 getBean 时 spring 没有被启动,spring 会报一个错,错误信息写着:请在 web.xml 中加入: <listener>...<init-param>...,多好的同窗,看到错误的人复制一下就完事了,咱们该学学。能够把常见的错误故意犯一遍,看看错误信息可否自我搞定问题, 或者把平时支持应用时遇到的问题及解决办法都写到异常信息里。

日志信息包含环境信息

每次应用一出错,应用的开发或测试就会把出错信息发过来,询问缘由,这时候我都会问一大堆套话,用的哪一个版本呀?是生产环境仍是开发测试环境?哪一个注册中心呀?哪一个项目中的?哪台机器呀?哪一个服务? 累啊,最主要的是,有些开发或测试人员根本分不清,没办法,只好提供上门服务,浪费的时间可不是浮云,因此,日志中最好把须要的环境信息一并打进去,最好给日志输出作个包装,统一处理掉,省得忘了。包装Logger接口如:

public void error(String msg, Throwable e) { delegate.error(msg + " on server " + InetAddress.getLocalHost() + " using version " + Version.getVersion(), e); }

获取版本号工具类:

public final class Version { private Version() {} private static final Logger logger = LoggerFactory.getLogger(Version.class); private static final Pattern VERSION_PATTERN = Pattern.compile("([0-9][0-9\\.\\-]*)\\.jar"); private static final String VERSION = getVersion(Version.class, "2.0.0"); public static String getVersion(){ return VERSION; } public static String getVersion(Class cls, String defaultVersion) { try { // 首先查找MANIFEST.MF规范中的版本号 
            String version = cls.getPackage().getImplementationVersion(); if (version == null || version.length() == 0) { version = cls.getPackage().getSpecificationVersion(); } if (version == null || version.length() == 0) { // 若是MANIFEST.MF规范中没有版本号,基于jar包名获取版本号 
                String file = cls.getProtectionDomain().getCodeSource().getLocation().getFile(); if (file != null &amp;&amp; file.length() &gt; 0 &amp;&amp; file.endsWith(".jar")) { Matcher matcher = VERSION_PATTERN.matcher(file); while (matcher.find() &amp;&amp; matcher.groupCount() &gt; 0) { version = matcher.group(1); } } } // 返回版本号,若是为空返回缺省版本号 
            return version == null || version.length() == 0 ? defaultVersion : version; } catch (Throwable e) { // 防护性容错 // 忽略异常,返回缺省版本号 
 logger.error(e.getMessage(), e); return defaultVersion; } } }

kill 以前先 dump

每次线上环境一出问题,你们就慌了,一般最直接的办法回滚重启,以减小故障时间,这样现场就被破坏了,要想过后查问题就麻烦了,有些问题必须在线上的大压力下才会发生,线下测试环境很难重现,不太可能让开发或 Appops 在重启前,先手工将出错现场全部数据备份一下,因此最好在 kill 脚本以前调用 dump,进行自动备份,这样就不会有人为疏忽。dump脚本示例:

JAVA_HOME=/usr/java OUTPUT_HOME=~/output DEPLOY_HOME=`dirname $0` HOST_NAME=`hostname` DUMP_PIDS=`ps  --no-heading -C java -f --width 1000 | grep "$DEPLOY_HOME" |awk '{print $2}'` if [ -z "$DUMP_PIDS" ]; then echo "The server $HOST_NAME is not started!" exit 1; fi DUMP_ROOT=$OUTPUT_HOME/dump if [ ! -d $DUMP_ROOT ]; then mkdir $DUMP_ROOT fi DUMP_DATE=`date +%Y%m%d%H%M%S` DUMP_DIR=$DUMP_ROOT/dump-$DUMP_DATE if [ ! -d $DUMP_DIR ]; then mkdir $DUMP_DIR fi echo -e "Dumping the server $HOST_NAME ...\c"  
for PID in $DUMP_PIDS ; do $JAVA_HOME/bin/jstack $PID > $DUMP_DIR/jstack-$PID.dump 2>&1 echo -e ".\c" $JAVA_HOME/bin/jinfo $PID > $DUMP_DIR/jinfo-$PID.dump 2>&1 echo -e ".\c" $JAVA_HOME/bin/jstat -gcutil $PID > $DUMP_DIR/jstat-gcutil-$PID.dump 2>&1 echo -e ".\c" $JAVA_HOME/bin/jstat -gccapacity $PID > $DUMP_DIR/jstat-gccapacity-$PID.dump 2>&1 echo -e ".\c" $JAVA_HOME/bin/jmap $PID > $DUMP_DIR/jmap-$PID.dump 2>&1 echo -e ".\c" $JAVA_HOME/bin/jmap -heap $PID > $DUMP_DIR/jmap-heap-$PID.dump 2>&1 echo -e ".\c" $JAVA_HOME/bin/jmap -histo $PID > $DUMP_DIR/jmap-histo-$PID.dump 2>&1 echo -e ".\c"  
    if [ -r /usr/sbin/lsof ]; then /usr/sbin/lsof -p $PID > $DUMP_DIR/lsof-$PID.dump echo -e ".\c" fi done if [ -r /usr/bin/sar ]; then /usr/bin/sar > $DUMP_DIR/sar.dump echo -e ".\c" fi if [ -r /usr/bin/uptime ]; then /usr/bin/uptime > $DUMP_DIR/uptime.dump echo -e ".\c" fi if [ -r /usr/bin/free ]; then /usr/bin/free -t > $DUMP_DIR/free.dump echo -e ".\c" fi if [ -r /usr/bin/vmstat ]; then /usr/bin/vmstat > $DUMP_DIR/vmstat.dump echo -e ".\c" fi if [ -r /usr/bin/mpstat ]; then /usr/bin/mpstat > $DUMP_DIR/mpstat.dump echo -e ".\c" fi if [ -r /usr/bin/iostat ]; then /usr/bin/iostat > $DUMP_DIR/iostat.dump echo -e ".\c" fi if [ -r /bin/netstat ]; then /bin/netstat > $DUMP_DIR/netstat.dump echo -e ".\c" fi echo "OK!"

7、扩展点重构

http://javatar.iteye.com/blog/1041832

随着服务化的推广,网站对Dubbo服务框架的需求逐渐增多,Dubbo 的现有开发人员能实现的需求有限,不少需求都被 delay,而网站的同窗也但愿参与进来,加上领域的推进,因此平台计划将部分项目对公司内部开放,让你们一块儿来实现,Dubbo 为试点项目之一。

既然要开放,那 Dubbo 就要留一些扩展点,让参与者尽可能黑盒扩展,而不是白盒的修改代码,不然分支,质量,合并,冲突都会很难管理。

先看一下 Dubbo 现有的设计:

design-step-1

这里面虽然有部分扩展接口,但并不能很好的协做,并且扩展点的加载和配置都没有统一处理,因此下面对它进行重构。

第一步,微核心,插件式,平等对待第三方

即然要扩展,扩展点的加载方式,首先要统一,微核心+插件式,是比较能达到 OCP 原则的思路。

由一个插件生命周期管理容器,构成微核心,核心不包括任何功能,这样能够确保全部功能都能被替换,而且,框架做者能作到的功能,扩展者也必定要能作到,以保证平等对待第三方,因此,框架自身的功能也要用插件的方式实现,不能有任何硬编码。

一般微核心都会采用 Factory、IoC、OSGi 等方式管理插件生命周期。考虑 Dubbo 的适用面,不想强依赖 Spring 等 IoC 容器。自已造一个小的 IoC 容器,也以为有点过分设计,因此打算采用最简单的 Factory 方式管理插件。

最终决定采用的是 JDK 标准的 SPI 扩展机制,参见:java.util.ServiceLoader,也就是扩展者在 jar 包的 META-INF/services/ 目录下放置与接口同名的文本文件,内容为接口实现类名,多个实现类名用换行符分隔。好比,须要扩展 Dubbo 的协议,只需在 xxx.jar 中放置文件:META-INF/services/com.alibaba.dubbo.rpc.Protocol,内容为 com.alibaba.xxx.XxxProtocol。Dubbo 经过 ServiceLoader 扫描到全部 Protocol 实现。

并约定全部插件,都必须标注:@Extension("name"),做为加载后的标识性名称,用于配置选择。

第二步,每一个扩展点只封装一个变化因子,最大化复用

每一个扩展点的实现者,每每都只是关心一件事,如今的扩展点,并无彻底分离。好比:Failover, Route, LoadBalance, Directory 没有彻底分开,全由 RoutingInvokerGroup 写死了。

再好比,协议扩展,扩展者可能只是想替换序列化方式,或者只替换传输方式,而且 Remoting 和 Http 也能复用序列化等实现。这样,需为传输方式,客户端实现,服务器端实现,协议头解析,数据序列化,都留出不一样扩展点。

拆分后,设计以下:

design-step-2

第三步,全管道式设计,框架自身逻辑,均使用截面拦截实现

如今不少的逻辑,都是放在基类中实现,而后经过模板方法回调子类的实现,包括:local, mock, generic, echo, token, accesslog, monitor, count, limit 等等,能够所有拆分使用 Filter 实现,每一个功能都是调用链上的一环。 好比:(基类模板方法)

public abstract AbstractInvoker implements Invoker { public Result invoke(Invocation inv) throws RpcException { // 伪代码 
        active ++; if (active > max) wait(); doInvoke(inv); active --; notify(); } protected abstract Result doInvoke(Invocation inv) throws RpcException }

改为:(链式过滤器)

public abstract LimitFilter implements Filter { public Result invoke(Invoker chain, Invocation inv) throws RpcException { // 伪代码 
        active ++; if (active > max) wait(); chain.invoke(inv); active --; notify(); } }

第四步,最少概念,一致性概念模型

保持尽量少的概念,有助于理解,对于开放的系统尤为重要。另外,各接口都使用一致的概念模型,能相互指引,并减小模型转换,

好比,Invoker 的方法签名为:

Result invoke(Invocation invocation) throws RpcException;

而 Exporter 的方法签名为:

Object invoke(Method method, Object[] args) throws Throwable;

但它们的做用是同样的,只是一个在客户端,一个在服务器端,却采用了不同的模型类。

再好比,URL 以字符串传递,不停的解析和拼装,没有一个 URL 模型类, 而 URL 的参数,却时而 Map, 时而 Parameters 类包装,

export(String url) createExporter(String host, int port, Parameters params);

使用一致模型:

export(URL url)  
createExporter(URL url);

再好比,现有的:Invoker, Exporter, InvocationHandler, FilterChain 其实都是 invoke 行为的不一样阶段,彻底能够抽象掉,统一为 Invoker,减小概念。

第五步,分层,组合式扩展,而不是泛化式扩展

缘由参见:谈谈扩充式扩展与增量式扩展

泛化式扩展指:将扩展点逐渐抽象,取全部功能并集,新加功能老是套入并扩充旧功能的概念。

组合式扩展指:将扩展点正交分解,取全部功能交集,新加功能老是基于旧功能之上实现。

上面的设计,不自觉的就将 Dubbo 现有功能都当成了核心功能。上面的概念包含了 Dubbo 现有 RPC 的全部功能,包括:Proxy, Router, Failover, LoadBalance, Subscriber, Publisher, Invoker, Exporter, Filter 等, 但这些都是核心吗?踢掉哪些,RPC 同样能够 Run?而哪些又是不能踢掉的?基于这样考虑,能够将 RPC 分解成两个层次,只是 Protocol 和 Invoker 才是 RPC 的核心。其它,包括 Router, Failover, Loadbalance, Subscriber, Publisher 都不核心,而是 Routing。因此,将 Routing 做为 Rpc 核心的一个扩展,设计以下:

design-step-3

第六步,整理,梳理关系

整理后,设计以下:

design-step-4

摘自:http://dubbo.apache.org/books/dubbo-dev-book

相关文章
相关标签/搜索