为什么各个语言都会有这么多的依赖问题?
软件包的分发规模产生了巨大的变化
大部分主流编程语言都诞生于上个世纪,代码包的分发范围在当时仅限于小规模的团体,例如公司内部或者单个软件项目内部,这种分发规模 只要内部有良好的代码约定就不会导致模块依赖冲突,但今天我们已经广泛运用github社区来分发软件代码包,分发的规模已经跨越国界跟种族以及不同的语言,这在当时是无法想象的。
例如现如今依然非常流行的C语言,就存在经典的函数命名冲突的问题,在小规模团体内部可以通过约定函数命名前缀来避免编译时的冲突。C++在后续引入了命名空间解决了这一问题
C++编译器版本众多,又有很多公司或者团体采用二进制闭源发布代码,又导致了二进制ABI依赖问题, 在今天你依旧可以看到golang swift这些新兴的编译型的语言都是依赖源码编译的,因为它们大多各自语言的各个版本之间的并不是二进制兼容的,例如X86 就有fastcall stdcall 等等函数调用约定,之前我翻了一下golang的内部实现,发现go语言在内部又搞了自己一套汇编调用约定,在go使用标准的C函数库的时候还需要做一些转换。
动态语言在运行时链接函数
动态语言天生就有的毛病,所有的调用都是在运行时才能确认被调用的模块的位置以及代码,因为在动态语言中,并不像C/C++那样在编译期有大量的静态检查,当然你要是喜欢用C/C++的void * ,上帝也没法拯救你。
- Java通过虚拟机字节码兼容性约定以及命名空间,在字节码层面使用操作数栈屏蔽了编译型语言的二进制调用约定问题, 但其动态链接,依旧没有避免运行时依赖缺失的问题,例如包A依赖了包B的v1版本,但是在项目中引入的包C的依赖了包B的v2版本,在maven的仲裁机制下,编译后只导入了包B的v1版本,当程序跑进包C里面的代码就会因为包B的v1版本缺失了一些v2版本的特性而报错,而且这些因为maven仲裁机制导致的依赖缺失的问题并不会在编译期被发现,大多只能等到线上运行的时候才会被发现,如果包C的代码并不是热点代码,大部分时候程序并不会跑进包C,很有可能你会在深夜因为报错日志而被领导催促起床来解决版本依赖问题。解决这个问题的办法只能依靠版本语义管理,在每次发布前都要检测maven版本仲裁是否存在版本号不兼容的情况。
例如 aa.bb.cc 这种版本号,cc代表bugfix的版本,
大多时候出现这种依赖冲突,人工仲裁选择使用高版本即可,而bb代表大的改动,可能存在接口不兼容的情况,
这个时候就要对依赖进行代码检查,确保Java动态链接调用没有问题才能上线使用。
当然很多公司内部的包并不存在版本语义化管理的规范,这个时候你只能祈求上帝,编写你依赖模块的那个老哥,没有修改外部接口
- 在Python里面同样存在类似Java这样的版本冲突导致的依赖缺失问题,而且这些问题大多时候很难被发现,只能等到运行时,例如你在代码中使用模块A的高版本才有的python方法,但是你运行时import了一个低版本的模块A,这个低版本的模块A里面刚好缺失了模块A高版本才有的python方法,而恰好这个代码并不是热点代码(在服务器上老半年不一定会跑到的地方),那就等着这颗雷在线上随时爆炸吧。
微服务下完全失控的依赖管理
在微服务下,以我个人的观察,大部分公司的微服务接口,可能只采用了简单的人肉管理,而且微服务大多都是跨部门维护接口的情况,这个时候如果沟通不畅,更容易出现问题。当然跨服务的RPC调用大多都是热点代码,如果出现问题,大多很容易被暴露出来。
- Java中使用了feign 通过静态编译约束大概率上解决了 接口参数的问题, 但如果你的feign模块是依赖方的低版本,而对方在生产发布流程中部署了高版本的服务,照旧会出现问题,当然这个时候需要强有力的跨部门跨技术组的协调方案,不然依旧会暴露依赖冲突这个问题。