模块化golang工程_golang 模块化
为什么要模块化
微服务本身是比较彻底的模块化方案,但是在部分场景下微服务不是最好的方案,例如:
- 事务比较复杂
- 希望内存占用尽量低(比如客户就给1c2g)
- 数据库连接数量已经过大,不希望引入中间件去解决连接数问题
- 运维能力不足
- 开源项目,希望便于部署
在这些条件下,单服务+模块化变成了最好的方案。
模块化方式对比
作为云原生的默认语言,微服务化似乎应该是与生俱来的能力。相比其他语言,golang 的模块化方案被较少的讨论。
对于模块的需求,我们大致上有: - 可以比较方便的载入,最好能是运行时的 - 可以卸载 - 可以有能力去在某次业务逻辑中启用和禁用
golang官方实际有模块化方案:go plugin。有如下特点:
- - 1.8版本引入
- - 模块被打包为 so 文件,用代码动态加载
- - 只能加载,不能卸载
- - 主程序与plugin的共同依赖包的版本必须一致
- - 如果采用mod=vendor构建,那么主程序和plugin必须基于同一个vendor目录构建
- - 主程序与plugin使用的编译器版本必须一致
这些条件中,在常见的业务开发中,最难做到的就是依赖版本一致 假设主程序与模块都使用了内部 log库v1,现在 v1 提供了升级 v1.1,某模块需要这个升级。也就意味着主程序和所有的模块必须一起升级才能解决这个问题。如果有几百个模块呢。。。
因此 go plugin 这么久了也很少有人使用这样的方案去做模块化,有兴趣的可以看这个例子:
https://github.com/pingcap/tidb/blob/master/docs/design/2018-12-10-plugin-framework.md
不用 go plugin,还有什么其他方案吗?
github.com/hashicorp/go-plugin 使用了“假微服务”方案,通过子主进程进行模块化启动,通信使用 go 喜闻乐见的 grpc。这个方案有效的解决了运行时、卸载、依赖版本的问题。 但是他引入了新的问题:性能。 假设一个调用需要调用其他 1 个模块的方法,传了指针进去。在 go 层面只占用了一块内存,经由 grpc 传输后,内存占用变成了 3 块,也就是说一个调用内存就要翻 3 倍。普通业务开发其实到也影响不大,在涉及大流量网络开发中,这个量级可能太吓人了。
编辑
image
对性能要求比较高,或需要内存占用比较低的场景中,这个方案不太行。如果要使用 rpc,为什么不真的使用微服务呢,毕竟微服务体系下是另一套完整的生态了。监控、链路、编排等各类需求都有完整便捷的解决方案。
如果用 go mod 作为模块化方案,优势有:
- - 完全支持多版本
- - 没有学习成本
- - 没有额外的性能开销
相对的,缺点有:
- - 不可能运行时载入
- - 不能卸载
- - 编译的时候不能做单元测试
- - 分支管理不能按照普通的方式来进行
- - 配置管理需要升级(普通的配置方式对模块化不够灵活)
- - 不能支持 swagger
go mod模块化实现细节
模块化方案是微服务的前奏,如果模块化成功,起码有了向微服务发展的基础。 从这个角度出发,模块化方案就必须满足一些要求:
- - 依赖 interface 而不是指针
- - 不使用导出变量的方式使用单例模式,尽量控制模块权限
- - 每个模块的配置应该是独立的
- - 尽管不要求运行时载入和卸载,起码载入和卸载应该很方便
因此,模块化方案在 go mod 的基础上,还应该有:
- - 全面 ioc 的使用和支持,这样才能够依赖 interface,尽量控制模块权限
- - 配置覆盖或组合能力,每个模块有独立的配置,同时可被其上层模块的配置覆盖,或提供配置组合能力。
模块的设计
模块的设计和微服务的划分可以对应,甚至可以更细粒度,毕竟还是单服务,没有微服务的劣势。 常见的,按照流程/对象或者抽象/实现的方式去划分,可以解决大部分的问题。
什么是好的模块(基本和微服务重叠):
- - 可管理(动态装载和卸载,golang做不到)
- - 原生可重用
- - 可组合
- - 无状态
一个模块设计的例子
需求:支付系统。
根据商品的不同,总价会有不同的折扣。但是折扣的计算比较复杂,会根据商品的不同属性计算不同折扣。例如商品上架时间、库存、供应商、批次等
第一次设计
有一个独立的服务,支付服务,来处理所有的需求。其会拉取商品服务的数据进行逻辑判断。
第一次模块化重构
抽取其中的商品拉取部分、数据库存储部分进行独立模块。现在逻辑部分在上,商品拉取、存储部分在下。
遵循一个原则:上层模块依赖下层,反之则不行
现在有2个模块:商品拉取、数据库存储
第二次模块化重构
逻辑部分在直接调用商品拉取,抽象其中部分为interface,商品拉取模块实现了这个interfcae。 同样,商品折扣的计算也独立出了一个模块,同样也实现了一个抽象的interface
遵循一个原则:只能依赖抽象而不是实现,实现依赖了本来应该依赖它的业务,依赖倒置了。
现在有3个模块:商品折扣计算、商品拉取、数据库存储
第三次模块化重构
折扣的计算包含多种多样,且可能随时增加或变更的逻辑。把它们全部分开成不同的实现,实现了相同的interface。用同一个管理器进行管理
遵循一个原则:适配器模式的使用,带来扩展的便利性
现在有3+n个模块:n个商品折扣计算、折扣计算管理、商品拉取、数据库存储
第四次模块化重构
支付的流程也成为了一个独立的模块,调用了一个通用的interface。折扣计算管理器实现了这个interface,因此将来不仅可以计算折扣,还通过扩展其他模块
遵循一个原则:main里面只剩下了di框架的初始化和配置,代码不超过20行了
现在有4+n个模块:n个商品折扣计算、支付模块、折扣计算管理、商品拉取、数据库存储
结果
经过4次重构后,系统从以前的一坨变成了一堆,也没有进行微服务拆分,但是谁都能看出来他的可维护性已经增加了太多。
更多精彩内容
研发团队新鲜事儿,来公众号「迷路idea」找我一起探讨