微服务架构之我们应该从Dubbo中学到什么

松花皮蛋me 2019-05-15 22:24
文章首发于公众号 松花皮蛋的黑板报松花皮蛋的黑板报,作者就职于京东,在稳定性保障、敏捷开发、高级JAVA、微服务架构有深入的理解

一、 模块分包

整体上按分层进行分包,然后每个包又分API包和具体的方案包,从中提炼出三个需要注意的点

  • 1.1复用度
    1) 包中的类应具有相同的重用可能性
    2) 紧密协作的类应放在同一包
    3) 对于变化因子,包中的类应全改或全不改
    4) 变化应在包内终止,不应传播到其他包
  • 1.2 稳定度
    1) 被依赖的包总是比依赖者更稳定
    2) 不要让一个稳定的包依赖于不稳定的包
    3) 单向依赖,无环依赖
  • 1.3 抽象度
    1) 越稳定的包应越抽象
    2) 抽象的包不稳定导致其所有依赖包处于经常的变化中

二、 框架扩展之微核和插件

大凡发展的比较好的框架,都遵守微核的理念, Eclipse的微核是OSGi(依赖META-INF/MANIFEST.MF配置), Spring的微核是BeanFactory,Maven的微核是Plexus,Dubbo的微核是SPI(依赖META-INF/services/com.xxx.xxx配置)。通常核心是不应该带有功能性的,而是一个生命周期和集成容器,负责加载、卸载、运行插件模块,这样各功能可以通过相同的方式交互及扩展,并且任何功能都可以被替换。如果做不到微核,至少要平等对待第三方,即原作者能实现的功能,扩展者应该可以通过扩展的方式全部做到,原作者要把自己也当作扩展者,这样才能保证框架的可持续性及由内向外的稳定性。

三、 框架扩展之平等对待第三方

  • 3.1 Dogfoodin-吃自己的狗粮
    1) 框架自己的功能具备扩展点实现
    2) 微核的加载方式也可以扩展
  • 3.2 Autowire-依赖注入
    1) 装配逻辑由扩展点之间互助完成
    2) 杜绝硬编码的桥接和中间代码
  • 3.3 Cascading-层叠
    1) 层叠扩展粒度,逐级细分
    2) 由大的扩展点加载小的扩展点
  • 3.4 Low of Demeter-最少知识原则
    1) 只与触手可及的扩展点交互,间接转发
    2) 保持行为单一,输入输出明确
    3) 一个对象应该对其他对象有最少的了解

四、 框架扩展之Filter-Chain模型

将一个事件处理流程分派到一组执行对象上,这一组执行对象形成一个链式结构,事件处理在这一组对象上进行传递

五、 框架扩展之外置生命周期

框架不应该控制实现类的生命周期,框架最多提供工具类辅助管理,而不是绝对控制,应满足下面的规范

  • 1. 尽量引用外部对象的实例,不是类元。正确示例:usrInstance.xxx(),Spring IoC,错误示例:Class.forName(userClass).newInstance().xxx
  • 2. 使用IoC注入,减少静态工厂方法。正确示例:setXxx(xxx),错误示例:XxxFactory.getXxx(),applicationContext.getBean(“xxx”)

六、 框架扩展之一致性数据模型

URL作为Dubbo一个公共契约,所有的扩展点都包含URL参数,URL作为上下文信息贯穿整个扩展点设计体系。所有的配置信息都转换成URL的参数,所有的元信息传输都采用URL,所有的接口都可以获取到URL

七、 领域模型划分

  • 1. 服务域:也称为行为域,作为组件的功能集,同时负责实体域和会话域的生命周期管理,如Velocity的Engine\Spring的BeanFactory
  • 2. 实体域:表示操作的对象模型,任何产品都有核心概念,围绕它转,如Velocity的Templcat\Spring的Bean
  • 3. 会话域: 表示每次操作或运行的瞬时状态,操作前创建,操作后销毁,如Spring中的Invocation

领域模型划分好处:结构清晰,可直接套用;充血模型,实体域带行为;可变和不可变状态分离,可变状态集中;所有领域线程安全,不需要加锁

八、 Dubbo核心领域模型

  • 1. 服务域Protocol: Invoker暴露和引用的主功能入口,负责Invoker的生命周期管理
  • 2. 实例域Invoker: 它是Dubbo的核心模型,任何模型都向它靠拢或者转换成它,它代表一个可执行体,可它向发起Invoke调用,可能是一个本地实现,也可能是远程调用,也有可能是集群实现
  • 3. 会话域Invocation: 持有调用过程的变量,比如方法名和参数等

九、 领域模型线程安全性

  • 1. 服务域:通常无状态,是线程安全的
  • 2. 实体域:通过设计为不变类,所有属性只读,或整个类引用替换,是线程安全的
  • 3. 会话域:保持所有可变状态,且会话域只在线程栈内使用,每次调用都在线程栈内创建实例,调用完即销毁,是线程安全的

十、 API和SPI分离

Dubbo中的API如ServiceConfig\ReferenceConfig\RpcContext是给使用者使用的,Dubbo中的SPI如Protocol\Transporter\LoadBalance,是给扩展者使用的,API应该是声明式的,描述需要什么,SPI应该是过程式的,描述怎么实现。它们不应该混在一起,使用者不应该看到扩展者写的实现

十一、 API可配置一定可编程

  • 1. 配置用于简化常规使用
  • 2. 编程接口用于框架集成

十二、管道和派发

管道一般适用于组合行为,主功能以截面AOP实现,比如Servlet。派发一般适用于策略行为,主功能以事件Event实现,比如Flux

十三、主过程拦截

没有哪个公用的框架可以Cover住所有的需求,不管是Web框架的请求响应流、ORM框架的SQL-Mapping过程,还是Service框架的调用过程,允许外置行为是框架的基本扩展方式,不然如果需要添加安全、日记或者修改分页SQL等不得不hack源代码了

十四、Dubbo调用过程拦截

Dubbo中使用全管道设计,框架自身逻辑,均使用截面拦截实现,比如常见于消费端的context\collect\generic\activelimit\monitor\future等链式过滤器,常见于生产端的token\exception\echo\accesslog\trace\executelimit等链式过滤器

十五、事件派发

拦截器是在切点执行前后生效的,它是干预过程的,会触发非关键行为,而事件是基于状态数据的,会触发状态观察者行为

十六、Reactor和Proactor事件驱动模型

Reactor模型关注就绪状态,比如可读了就通知我们主动去读,类似Linux epoll,而proactor关心的是完成状态,比如我们指定存放数据的内存地址(Buffer)和读事件,当它读完了就会通知我们,类似Windows IO completion port

十七、 暴露/引用/调用事件

Dubbo在关键路径上采用拦截链分离职责,保持界面功能单一,不易出问题。在非关键路径上,采用后置派发,即使派发失败也不会影响主流程运行

十八、协作防御

  • 1. 可靠性分离。不可靠操作尽量缩小
  • 2. 状态分离。有状态尽量缩小可变域,不可变类尽量声明为final
  • 3. 状态验证。尽早失败,在有传入参数或者状态变化时,均在入口处全部断言
  • 4. 异常防御。不要生吃异常,应该尽量保证异常信息给出解决方案,日记信息包含上下文
  • 5. 降低修改时的无界性,不埋雷。避免基于异常类型的分支流程,同时保持NULL和Empty语义一致

十九、开闭原则

开闭原则,对扩展开放,对修改关闭,因为风险往往来自于修改。拥抱变化时应该继承原有类然后重写方法扩展逻辑,而不是修改原来的类

二十、增量式和扩充式

如果现有一个无状态消息发送的场景,后来新增一个会话消息发送需求,如果采用增量式扩展,无状态消息发送原封不动,同步消息发送,在无状态消息基础上加一个 Request/Response 处理,会话消息发送,再加一个 SessionRequest/SessionResponse 处理

二十一、Dubbo增量式扩展

传统的C\S模型是一问一答模式,而Dubbo的RPC模型中包括了Proxy\Custer\Protocol, Protocol只负责协议实现,它是不透明的、点对点的,Cluster只负责将集群中多个提供者伪装成一个,Proxy只负责透明化接口,桥接动态代理,整体的架构非常容易扩展

二十二、在高阶附加功能

尽可能少的依赖低阶契约,用最少的抽象概念实现功能。当低阶切换实现时,高阶功能可以继续复用

二十三、Dubbo高阶泛化调用

以PHP到Router的request body中的方法名和方法参数作为Router远程调用后端Java服务的入参,最后将远程调用的result返回给PHP端就是兼容Restful服务的高阶泛化调用

二十四、总结


文章已于2019-05-19 12:40修改,变动:添加文章摘要
阅读 229 次