在日常软件建设中,我们有时会遇到一个新旧逻辑、新旧组件灰度切流的过程。如何能够平滑的切流,相信会是每个开发者面临的问题。
好的,我们进入正题。
灰度切流的用例图很简单。
根据这个用例最直观的设计就是:
在一个gateway中包含一个Router,通过读取配置去选择究竟是实现A还是实现B。
使用的是策略模式最直接的解决问题。
但是现在Gateway的直接和Router的职责绑定到了一起,带来维护成本的提高,所以为了更加明确就变成了如下的设计:
Gateway包含了一个包含所有实现的Router,由Router进行根据配置进行调用。
这样的设计对于功能易用性已经OK。假如还需要一些结果合并逻辑怎么办?
从现在结构里,需要在Router里增加除了开关之后额外的合并逻辑。
从现在看,整个切流过程是没问题。不论是如何切流都是能够支持的。
在这种情况下,我们如何增加一个新的切流逻辑或者将清理旧逻辑或者旧组件?
或者我们说一下这种设计的缺点在哪里。
- 违反SRP原则。Router的功能不纯粹,除了切流还参与了业务逻辑,在切流完成后需要将部分逻辑移到实现中。
- 重复。有多少切流就需要多少的Router
好吧。那如果针对上述问题,可以有更好的设计吗?
首先我们可以将Router有关于业务部分的抽离出来, 将Router设计成为:
基于配置实现切流逻辑。
有了核心对象,我们就可以设计基于Spring拦截器去实现动态反射调用了。
基于这种方案,我们实现了基于注解配置的路由开关配置。在集成默认云配置的时候,通过Spel解析的方式实现,保留扩展自定义路由实现能力。
现在增加一个切流或者去掉切流,只需要将注解增加或者移除,不需要改动其他代码。
世界上肯定没有完美的东西,那这种方案缺点有没有呢?
答案是肯定的。
- 复杂度上升,在考虑的时候需要考虑可能带来的风险。
- 对性能会有影响。
基于这种方案,我们在生产系统上实现了一个路由组件,来灵活支撑我们的切流。这里也总结下遇到的坑
- 需要考虑拦截器的顺序,来保证其他已有注解的正常工作
- 对于批量接口,因为走多次拦截器,造成性能下降了10ms左右
- 路由组件各个过程中使用的外部调用结果多次使用,只能通过缓存解决
- 异常处理粗糙。对于反射调用异常
InvocationTargetException
直接wrap成了组件的异常,造成应用里的监控拦截器捕获异常失败。没有针对各种情况去设置异常类型,方便应用程序去处理。 - 将一些基础之外的能力变更可选,避免违反ISP原则