37

微服务的灾难-依赖地狱

 5 years ago
source link: http://xargin.com/disaster-of-microservice-dephell/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

微服务模式下,我们的系统中往往需要集成进各种各样的 SDK,这些 SDK 部分来自于非功能性的业务需求,例如 bool 表达式解析,http router,日期时间解析;一部分来自于对公司内基础设施的绑定,如 MQ Client,配置下发 Client,其它服务的调用 SDK 等等。

一般的观点会认为公司内的 SDK 是较为可靠的,而开源库的稳定性不可控,所以人们在升级公司内部库时往往较为激进,开源库版本升级较为保守。具体到 Go 语言,公司内的库,我们可能会直接指定依赖的版本为 master(glide 中每次构建会使用 master 分支的代码)。

实际上真的如此么?业界有个名词叫 dependency hell,指的是软件系统因依赖过多,或依赖无法满足时会导致软件无法运行。导致依赖地狱的问题有:

  1. 依赖过多
    一个软件包可能依赖于众多的库,因此安装一个软件包的同时要安装几个甚至几十个库包。
  2. 多重依赖
    指从所需软件包到最底层软件包之间的层级数过多。这会导致依赖性解析过于复杂,并且容易产生依赖冲突和环形依赖。
  3. 依赖冲突
    即两个软件包无法共存的情况。除两个软件包包含内容直接冲突外,也可能因为其依赖的低层软件包互相冲突。因此,两个看似毫无关联的软件包也可能因为依赖性冲突而无法安装。
  4. 依赖循环
    即依赖性关系形成一个闭合环路,最终导致:在安装A软件包之前,必须要安装A、B、C、D软件包,然而这是不可能的。

我们编写的服务也属于软件系统的范畴,所以也难以摆脱依赖地狱的问题。在微服务场景下,因为本文开头所述的原因,我们必然会依赖一大堆外部 SDK。对于开发者来说,实际上真正有选择权力的就只有我可以使用什么样的开源库。公司内的 SDK 是没有自己造轮子的价值的。毕竟自己造的司内 SDK 也没有人会帮你修 bug,原生 SDK 至少有单独的团队维护。

在开发 lib 时,比较好的做法是尽量引入少的依赖,以避免上面提到的问题 1。实际上没有几个提供 SDK 的团队能做得到,想想当初 javascript 圈子的 leftpad 事件吧,即使是一行代码的库,被人删除就引起了无数大公司系统无法 build 的悲剧。对于目前 Go 编写的系统,实际上存在同样的风险,我们的依赖大多来自于 github,如果你们没有使用 vendor 方案把依赖缓存到自己的系统中,别人删库了你有辙么?

一般 star 数比较高的开源库作者还是比较有节操的,删库的人毕竟是少,所以我们先假装这个问题不存在。公司内的实际开发过程中,我们遇到的依赖地狱大多体现在依赖冲突上,这个比较好理解,比如:

A --> B --> D.v1
A --> C --> D.v2

A 模块依赖 B 和 C,而 B 和 C 分别依赖 D 的不同版本,如果 D.v1 和 D.v2 恰好进行了 API 不兼容的更新,且都是在 github.com/xxx/D 路径下,通过 tag 来区分版本。那么就会对我们造成很大的麻烦,B 和 C 如果又恰好是公司内的不同部门的不同团队,要求他们因为这种原因进行兼容性更新就像是去求大爷一样难。

印象中之前 Go 社区是没有办法解决这种问题的,为了解决这个问题,我司还专门对 glide 进行了定制,以在依赖冲突的时候提醒用户,阻止构建,防止之后出现问题。Go 的社区在这方面也做得确实不太好,gomod 我还没有试用,不清楚是否对依赖冲突有优雅的解决方案。之前社区里大多数人对 Go 的依赖管理也一直颇有微词,希望 gomod 能彻底解决这些问题吧。

除了语言本身的问题,我发现公司内的 library 研发们,根本没有任何开源界的节操,版本升级时根本不考虑向前兼容或者向后兼容的问题,并且出现问题的时候也不会做任何提示,连日志都基本不打印。经常会有配置管理 v1 和配置管理 v2 的 sdk 同时存在模块中时,发生同一个全局变量初始化两次,发生冲突,逻辑不能正常运行,结果启动阶段没有任何 warning,直到执行阶段才出现诡异错误,导致用户在线上埋下定时炸弹的问题。

实在是不知道该说什么好。

多模块之间的循环依赖就更不用说了,如果循环依赖出现在单机系统中,至少在 Go 语言中是没法编译通过的,而因为微服务的关系,循环依赖往往会存在那些没有合理划分业务边界的系统当中。据我观察,出现得还不少。

这时候你能重新划分职责,让循环依赖消失么?

显然是不行的。程序员在当前的微服务架构下,将持续地被外部的垃圾 SDK 和各种莫名其妙的依赖问题所困。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK