继续来聊一聊微服务的坏味道,这次的主题是共享库(Shared Library)。说起共享库,相信很多同学都在自己的项目上看到过类似common.jar或者common.so等类似的共享库,如果说它是个坏味道,估计有些同学会说这种方法应该没啥问题吧,大家都这么干。让我们一起来看看共享库会带来哪些问题。
共享库带来的问题
给大家举一个真实项目上的例子,当时我们的项目有很多个微服务,也有这么一个叫common的工程,几乎所有的微服务都要依赖它,目的是共享代码,减少重复,这种做法看起来貌似是非常合理的。
我们来看看这个common库里都包含了哪些内容:
- 代码的基础架构相关,如权限控制、日志、Redis/MessageQueue/Slack等的集成
- DTO相关
- 枚举值
- 异常处理
- 工具类(如日期转换、价格计算等)
- 业务逻辑代码
这样的库职责不单一,承载了太多知识,当微服务数量变多,同时依赖一个过大的共享库会是一个灾难:
微服务不再独立
共享库在微服务之间建立了一个隐藏的耦合关系,当一个服务需要对共享库进行升级时,很难确定修改是否会对其他服务产生影响,这无疑增加了微服务开发团队之间的沟通成本。
共享库成了微服务之间共享代码的中介,极端情况下会出现两个服务通过共享库共享了一些业务代码,微服务的边界会变得越来越模糊,逐渐的我们开始怀疑微服务划分的合理性,大而全的共享库破坏了微服务的独立性。
意外的馈赠
当一个服务需要日志的功能依赖common库时,即使不需要其他功能,这个服务也会获得这个库里面所有的功能,这会产生一些我们不期望的后果。
举个例子,当前工程中有和common库中相同命名的工具类,在增加新代码时很容易选择了错误的依赖,导致代码没有按预期的工作。
版本冲突
不同的服务需求往往是不同的,随着业务的变化,共享库中的代码也会不断更新,如下图的更新方式:
假设服务A和服务B最初都依赖common_1.0.0.jar
,随着服务A的需求变化,common库从1.0.0版本升级到1.0.2版本,这时服务B想通过升级common库来更新到枚举V2,只能一步升级到1.0.2版本,但1.0.2版本中附带的工具类V2是服务B不期望的,就会产生版本冲突。
在微服务比较多的情况下,这种大而全的共享库版本的管理是非常困难的。
如何在微服务间正确的共享代码
那么该如何在微服务之间共享代码呢?我们可以参照上文中提到的共享库中可能出现的不同内容分别说明:
职责单一的基础架构库
对于功能稳定通用且和业务本身无关的代码,推荐以职责单一的共享库的形式存在,比如日志、权限控制等分别以单独的日志库和权限库的形式存在,各个微服务按需添加依赖即可。
复制代码
针对DTO、枚举、工具类和异常处理等,在不同的微服务上下文中并不一定能完全复用,因此建议以复制代码的方式存在在不同的微服务中,且可以根据微服务自己的特点进行修正。
比如上下游服务间的API通信,上游服务在接口的DTO中提供了10个字段,下游服务只需要其中的5个,下游服务的DTO定义没有必要完全按上游服务的来定义;同样对于枚举值,如果枚举的定义来自某个上游服务,我更倾向于建议下游服务不要以枚举的形式(而以字符串的形式)来接收这个字段,因为一旦上游系统增加了枚举值的种类,下游系统很可能就会遇到问题。
不要共享业务逻辑代码片段
有时候还会在共享库中出现一些业务逻辑代码片段,一旦这样的共享发生,这是一个信号,很可能微服务之间的边界并不清楚,这时候要回来审视下微服务的划分是否合理或者微服务间的职责是否清晰。
小结
微服务一直强调独立自治,滥用共享库让微服务之间的耦合变的紧密,微服务的优势也就不复存在。对于稳定通用且和业务无关的功能模块,可以分别放到独立的共享库中;对于业务相关的代码,不要共享,这部分的冗余是可接受的。
相关文章推荐
- 迭代开发中的微服务拆分
- 为什么每个微服务要有自己独立的数据库?
- 微服务架构下你的数据一致了吗?
- 如何提升系统可用性?
- 微服务架构下的系统集成
- 消灭微服务的坏味道 之 循环依赖
- BFF避坑指南
- BFF治理与优化