likes
comments
collection
share

二方库管理困境

作者站长头像
站长
· 阅读数 3

背景

笔者在字节工作,负责小团队的二方库维护与一些技术底座的选型,一直在推动团队中各个项目进行二方库的维护和升级,这其中也遇到诸多阻力。恰好字节的内部论坛上也有一些关于团队内二方库依赖版本过老、不同服务依赖的版本号跨度过长等问题的讨论。自己的工作实践与这次讨论引发了我的一些思考,我尝试以一种相对系统的方式梳理出二方库管理困境的原因,并尝试给出一些解决方案。

先解释一下讨论的范围:

何为二方库?

二方库通常指由团队内自己的需求出发,自己开发的一些lib、sdk、framework。既包括业务小团队开发的公共代码,也包括公司级别内开发的公共代码。

在本篇文章中,这是讨论的主要对象。

何为三方库?

三方库通常是不由自己团队掌控的代码,比如github上的开源代码。

虽然有些公司级别的公共代码也不由自己团队掌握,但是考虑到这种类型的代码库通常有着非常好的维护、方便的oncall渠道,且是一部分人专职的东西(这是重点!),所以我也分类到了二方库里。

针对业务开发同学,在下面的部分讨论中并不需要区分二方库与三方库,此时会以“二/三方库”这种写法加以区分。

写给谁看

因为我恰好既要维护小团队的二方库,又要使用公司范围内的二方库,同时偶尔也会开发一些业务代码,所以我既是业务开发同学,也是二方库开发同学。因此这篇文档同时面向了二方库开发同学和业务开发同学。这有助于双方增进对对方的了解,也可以把整个事情说得更加全面,但是不免得逻辑有些混乱和复杂,请谅解。

何为困境

所谓困境,就意味着这是一个在比较长时间内,多方诉求都无法得到满足与平衡,以至于大家都怨声不断而又无力改变的一个场景。而一眼可以看出,困境中主要的角色是业务开发同学二方库开发同学

二方库开发

二方库开发同学通常会按照roadmap,同时结合各个业务方的功能诉求、性能诉求、bug反馈,不断完善二方库。在这个过程中,二方库开发同学天然会有动力去推动大家升级二方库:

  1. 二方库开发同学也背负着KPI、OKR、ROI之类的压力,希望业务开发同学尽快升级二方库到最新版本,以便在节省XX CPU资源、新功能被XX个团队使用到等指标上体现出自己工作的价值。

  2. 很多对feature与bug的oncall都是基于老版本的,升级到最新的版本就能减少这些oncall。

  3. 最新版本的问题的oncall更容易解决,版本越老,相关oncall越难解决。

  4. 部分break change的改动可以做兼容,但是需要2-3个版本进行渐进式的升级。而如果业务方升级不及时,则这种渐进式的兼容升级就无法实施。

  5. 如果有v0到v1这种大版本升级,如果业务方不及时升级到最新的v1,则二方库开发同学需要同时维护多个大版本。维护压力大。

  6. 渴望自己开发的东西可以上到生产环境中实际运行,实现个人价值。

业务开发

业务开发同学通常会按照排期完成相应feature的开发和bug的修复,而二/三方库的升级并不在工作范围内。面对二/三方库的升级,开发同学通常遇到了这些问题:

  1. 业务同学负责feature开发与bug修复,而花时间研究二/三方库新版本并升级并不一定能提高开发效率,也与绩效无关。因此不愿意投入时间去了解二方库新版本。

  2. 二/三方库的升级成功了也没有肉眼可见的收益,升级出bug了还要自己负责来修复,对绩效有(潜在的)负面影响。

  3. 看不懂二/三方库的release note,有些甚至没有release note,导致不敢升级。

  4. 不了解升级的收益在哪里,不仅是单次升级的收益,也包括随着二/三方库的规划持续升级的收益。如果二/三方库自己的长期规划也没有说清楚,则大家就没有持续升级的动力了。

  5. 担心二/三方库的最新版本不稳定。有些大家都依赖的二/三方库的测试也没有那么完善,单测覆盖率并不高(更别提场景覆盖、分支覆盖了)。

  6. 就是不想依赖最新版本,只想依赖相对稳定的次新版本。希望二/三方库自己可以区分实验版本与稳定版本。

升级真的有收益吗?

从业务开发同学的角度去看,升级并不能总是真的带来收益。常见的升级收益有这些:

  1. 二/三方库升级能带来性能提升

  2. 二/三方库新版本解决了已知的bug

  3. 二/三方库提供了新功能,并且新功能恰好可以支撑业务开发

遗憾的是,当我们去看一个二/三方库的release note的时候,我们并不是每次都会得到如上信息。有时候是因为release note没有把事情说清楚,有时候是真的这次升级和自己没啥关系。

那么,如果我们升级没有这些收益,我们就不升级了吗?有些二/三方库确实是这样的。但是那些被项目深刻依赖的二/三方库会是例外。**被项目深刻依赖的二/三方库在没有确定性收益的情况下,也值得定期升级。**首先解释下什么是“被项目深刻依赖的二/三方库”,通常而言,可以称为framework的东西、是业务逻辑的底层二/三方库都是,比如:

  1. go、Java、Python这样的语言版本,以及随版本发布的核心库

  2. kitex、fasthttp、gin这些web server框架

  3. gorm、gen这些ORM框架,以及其底层驱动,比如mysql-driver

  4. 异步任务、工作流框架

  5. go-valid等用于API校验的库

  6. logger、metrics、promonitor之类的和监控有关的库

其次我想用一个例子来论证被项目深刻依赖的二/三方库在没有确定性收益的情况下,也值得定期升级

代码的功能、性能的优化是没有止境的。业务代码是如此,二/三方库的代码也是如此。有时候我们想实现特定功能,或者做特定优化,就是要依赖新版本的二/三方库(或者依赖二/三方库的生态链工具,而这些生态链工具依赖了新版本的二/三方库)。但是,我们不能等到有这些需求的时候再做二/三方库的升级,因为在此时,服务依赖的版本距离最新的版本已经差太多了,有太多的不兼容改动,升级的风险被累积和放大了。就算是把保障兼容性放在生命信条里的golang,也会因为各种各样的原因作出不兼容的改动:

  1. 因为bugfix和安全问题而导致的不兼容。这种情况下二/三方库的维护同学没有办法做兼容。比如golang因为安全问题要修改os/exec的默认执行,这打破了兼容性:go.dev/blog/path-s…

  2. 随着功能迭代而逐渐不兼容,比如golang要用go install来代替go get进行程序安装,go get命令在1.17安装程序时会warning,go get命令在1.18时就无法用于安装程序了。ioutil的废弃也会经历类似的过程。

  3. 其他潜在的因为功能迭代或者代码重构导致的不兼容。

而如果我们不只是依赖了二/三方库,还fork下来做了一些魔改,那么事情会更加糟糕:我们的修改可能会和最新版本有冲突,而修复冲突又让整个事情麻烦了一重。

我们就实际遇到了这样的问题:

我们使用google的grpc框架,其使用protoc定义API,并使用gateway来兼容http请求。在功能迭代中,我们需要针对不同的用户返回不同的结构体,一开始我们用oneof来实现这个功能,即多个结构体都实现一个interface,然后返回任意一个都可以。但是随着白名单的变多,有二三个字段都是根据开白的情况动态决定是否返回的,那么2个字段就需要定义4个不同的结构体,3个字段就需要定义8个不同的结构体,这给我们维护代码带来了巨大的麻烦。

后来我们发现最新版本的gateway支持了字段粒度的可见度定义,可以很好地解决我们的问题。但是不幸的是,我们不仅落后了最新的版本太多,我们还魔改了最新的版本。至此,我们没有人力和能力去做跨如此之长版本的升级,下面两个困难,光是想想就打退了我们所有人:

  1. rebase我们的魔改到最新的版本,并且保障我们需要的功能、grpc原本的功能都不出错。

  2. 把我们跨越的版本的release note全都看一遍,看看有没有对我们不利的影响。

好的,最后我们放弃了升级,并且计划和PM商量减少白名单的数量,而现有的需求只能硬着头皮上了。

复盘这个例子,在项目立项之初,谁能想到我们未来会根据白名单动态决定某个字段是否需要返回呢?(当我们需要这个功能的时候,距离立项选择grpc已经过去了3年多了)。但是从一个混沌的角度来说,我们未来需要的功能,别人也会需要,而且可能比我们更早需求,并实现了,我们只需要定期升级,就可以享受到别人的成果。而且我相信grpc实现的对我们业务有帮助的功能和优化远不止这些。

因此,如果是一个还在频繁开发的项目,那么外部依赖的二/三方库或是出于功能、或是出于性能,或是出于生态链工具依赖新版本,总是要升级依赖的。其中二方库因为和开发诉求贴合更近,更容易频繁升级。那么迟迟不做依赖升级,等到堆积了足够多的版本才做升级,升级自然变成一个很复杂的事情(长期不升级堆积历史债务)。所以我觉得对于还在频繁开发的项目,不做依赖版本升级是一个没有远见的懒政。

“能用就不要动”

我想特别讨论一下“能用就不要动”这个观点。

首先,服务的稳定性依赖的是系统性的dev ops,而不是“不改代码”。对于一个还在演进的项目,修改的数量并不能与风险的大小画等号,指望通过不改代码就能提高服务的稳定性,是一个很荒谬的事情。

有些同学会担心升级SDK会导致服务出bug,进而被追责,影响绩效,我觉得大可不必担心。如果一个功能重要,那么必然会有完善的回归测试来覆盖,不用担心改代码就会导致重大故障。(当然如果没有dev ops那就QAQemmmm……= =!)而小bug本身不是重点(谁家系统里没点bug呢)。另外,只要代码有变更,就可能会导致升级挂了,那么凭什么二方库的升级导致的事故,比加新功能导致的事故要罪加一等呢?一般来说,只要不是触发红线导致的事故,都不会追责到个人。

所以我们看到,提高服务稳定性的关键是系统性的dev ops。如果一个具有旺盛生命力的项目会信奉“能用就不要动”而不升级依赖,那么说明这个项目的dev ops一定有比较大的问题,其本身质量也堪忧。不过还是要限定一下范围,如果在版本末期,回归测试都快结束了,那么此时暂时不升级依赖,等下个版本再升级也是合理的。

其次,“看任何原则、理念、方法的时候都不能脱离上下文和适用场景,做教条式的理解”。我们刚刚讨论了什么情况下,不应该“能用就不要动”。那么情况下适用“能用就不要动”呢?

如果是一个已经很少有改动的项目,不升级就不升级,问题不大。这类项目通常不只是一个二方库的版本落后了,甚至编译器、官方库、所需要的运行环境都可能已经落后了;甚至情况可以更遭:这个项目没有自动化的单元测试和系统测试,也没有CI/CD(或者有一个非常混乱,有了还不如没有的CI/CD系统),甚至没有测试用例!对这种项目的任何改动,在现代的软件开发方法论和要求中,都需要比较大的精力去完善和实施测试。此外,因为这个项目不频繁改动了,我们并不能享受到二方库升级带来的开发效率提升。

或者,如果是一个已经长期没有新代码的项目,并且没有人对代码足够熟悉,那么“能用就不要动”这个观点也勉强可以成立。

已有的解决方案

在公司的范围内,肯定也有看到这些问题,同时给出了一些解决方案。

用户群

常用的二方库都有自己的用户群,群内有讨论,也有版本升级的一些通知。群公告内也有通知、教程之类的文档。

二方库管理困境

二方库管理困境

不过看的出来,这种论坛性质的用户群,只能推动有闲有空的同学去了解下二方库的新版本。其更大的作用还是减少二方库开发同学的oncall压力,提供一个讨论的环境与氛围。

发布卡点

对于使用公司内统一基础设施进行发布的服务,在服务发布前都有诸多的检查项,其中一项就是依赖库的版本卡点。对于已经废弃的版本,或者明确知道有重大bug的版本,可以阻拦发布。

不过这种卡点也有诸多限制:

  1. 通常而言卡点不会很严格,就算是废弃的不维护的版本,只要没有严重的bug,也不会卡点。因此起不到推动升级的作用。

  2. 只有使用公司统一基础设施进行发布的服务才能被卡点,还有一些ToB业务没使用公司统一发布平台。

  3. 就算卡点到了,业务开发同学也不一定会升级,可能会申请豁免。常见的理由有:

    1. 你这个bug我们没踩到/不会踩到/没有测试出来,所以不升级。

    2. 已经是版本末期了,没有时间再去升级版本并再做一轮测试,所以下次一定(下次也不一定)

    3. 如何保障二方库的新版本又没有新的bug?(压力又来到了二方库版本开发同学)

自动升级

公司还开发了自动升级平台,能够提交mr/pr来更新依赖版本,来简化二方库版本更新。不过这个功能也有局限性。

  1. 如果有任何的break change,那么通常不能由工具自动完成更新。

  2. 就算mr/pr都提了,业务开发同学也不一定乐意合入。原因参考上一章节【发布卡点】

没有银弹

好吧,在公司有了这么多解决方案之后,还是有这么多吐槽,说明这可能又是一个没有银弹的事情了。我们来浅浅讨论一下可能可以缓解这个问题的一些解决方案吧~

说清楚收益,坦诚清晰

这一条是针对二方库开发同学的。业务同学的最主要诉求其实很简单:二方包可以把自己的版本内容讲清楚。因此二方库同学应该在版本管理上下一些功夫。针对版本管理问题,我发现有一些做起来很简单,且能有比较大作用的事情:

  1. 用中文讲清楚每个MR的改动面

对,中文!亲爱的母语!虽然英文看起来很专业,但是在实践中大家的英文表达能力有限,理解能力也有限,通过mr来声明改动面的作用被大大降低了。

举两个我们实践中的例子,大家可以看看英文的表达力有多么贫乏:

英文:fix: choice nil
中文:fix: choice回滚可能意外空指针的问题

英文:feat: lock error
中文:feat: 优化锁异常返回信息,现在可以通过errors.Is来判断锁失败的原因

具体而言,这个事情有很多种做法,比如mr的title改成中文的、mr的description添加中文信息等等,我们和下一步一起看:

  1. 为每个tag提供详实的变更文档

紧跟着上一步【用中文讲清楚每个MR的改动面】,我们可以使用一些工具来收集tag与tag之间的mr的信息,根据这些信息去生成tag的description。我们既可以使用已有的一些开源工具:

github.com/git-chglog/…

github.com/tj/git-extr…

github.com/hilaily/cmd…

我们也可以自己写一个小工具,并且这个事情在chatgpt之类的AI工具的帮助下,写起来很快。实测基本半天就可以写出来一个基本可用的工具。花个1-2天,就可以一键完成如下工作

1. 打tag
2. 收集tag与tag之间的mr信息,生成tag description
3. 触发流水线,发送飞书/钉钉/微信通知
4. 设置版本卡点(如果已有版本卡点工具的话)

下面是我们实现的效果的展示,只需要在本地运行 mr-chglog --next-tag v0.1.8,然后就会生成release note:

二方库管理困境

发送飞书通知:

二方库管理困境

版本卡点:

二方库管理困境

  1. 写一个文档,讲清楚二方库的长期规划

二方库在发布的时候也不会尽善尽美,一定还有一些演进的空间,可能还会有一些已知的缺陷。写一个详实的文档来把二方库的长期规划、现有问题、预计的解决时间讲清楚,不仅能减少一些oncall,还可以增进大家对你的了解,累计信任,自主升级。

保持兼容,积累信任

另一方面,有时候业务同学不升级二方库的版本是出于信任原因,我常见到这些吐槽:

  1. 上次升级你们的版本,结果服务出了bug。以后再也不升级了。

  2. 上次升级发现了一个逻辑不兼容的改动,你们也没说,结果程序挂了,以后再也不升级了。

  3. 上次升级你们的最新版本,就发现一个P0级别的bug,以后再也不升级了。

  4. ……

针对信任问题,我觉得只有不断提高代码的质量、提高覆盖率、长期坚持保证兼容性、有bug及时认错并周知使用方等等方式来不断获取信任。而在细节上,有这些小方法可以帮助你达成上述目标。

  1. 用单测和注释来记录每一个bug

这个灵感来自于偶然间看到的go官方库time库下的一个测试代码:

// golang.org/issue/4622
func TestLocationRace(t *testing.T) {
        ResetLocalOnceForTest() // reset the Once to trigger the race

        c := make(chan string, 1)
        go func() {
                c <- Now().String()
        }()
        _ = Now().String()
        <-c
        Sleep(100 * Millisecond)

        // Back to Los Angeles for subsequent tests:
        ForceUSPacificForTesting()
}

非常短小清晰,很好用。

  1. 如果有break change,则一定要暴露出来

其实大家没那么怕break change,都是现代化的IDE了,智能全局替换一般来说并不是什么麻烦事。怕的是你有一个隐形的break change,无人提醒也无人知晓。举个例子:

func max(i, j *int) *int {
    if *i > *j {
        return i
    }
    return j
} 

结果下个版本改成了:

func max(i, j *int) *int {
    if *i >= *j {
        return i
    }
    return j
} 

一个小小的符号,可能会带来大大的不兼容。这种情况建议直接新写一个函数。对于其他更难做兼容的场景,不如让所有的旧用法在代码编译阶段就报错,确保大家知晓你的不兼容改动。

  1. MR保持短小和专一

为了保障你的release note正确且精准,MR应该尽量保持短小和专一。这老生常谈了,但是,我们总是需要不断地被提醒:)

外科手术式的团队

对于开发同学而言,并不是每一个开发同学都有意愿去升级二/三方库版本的,免不了不少同学有惰性去依赖老版本。但是站在一个团队、一个项目的角度而言,优质二/三方库的重要性是不言而喻的。有时候替换一个json序列化库就可以带来巨大的性能优化,有时候一个好的二方库可以省去非常多重复编码的时间,有时候依赖的库的老版本爆出来了安全漏洞必须要升级。因此在一个团队内,势必要有一些同学熟悉二/三方库,能够帮助团队做技术选型和底层框架建设。

对于这种团队需求,我的经验是,借鉴外科手术式的研发团队的思路,在团队内部区分出一小批同学来“维护开发工具”(包括IDE、开发插件、二/三方库等等)。这一小批同学也是开发,只是他们会分一部分精力去“维护开发工具”,所以不太需要制定特殊的绩效规则。但是这部分同学需要有比较强的自驱能力或者编程兴趣,能自主地去了解二/三方库、在手头有非关键的项目去试用二/三方库、不定期做二/三方库新版本的分享和升级建议。

无形的压力,有限的人力

我们如果再回顾一下业务开发与二方库开发同学的诉求,就会发现大家最主要的诉求就是自己的付出要有对应绩效的体现。如果没有对应的绩效体现,那么非自己责任内的事情就不做。那么和绩效有关的事情应该找谁解决呢?找老板。

对于二方库同学而言,让大家升级到最新的版本是有天然驱动力的。老板只需要识别到这部分驱动力,给到对应的人力预留即可。比如去建设版本流水线、每个新功能预留足够的时间去保障兼容性、测试覆盖即可。

而对于业务开发的老板而言,就相对麻烦一些了,由浅到深,需要意识到这些事情:

  1. 意识到大家的工作产出必须得到认可(物质和心理),包括维护二/三方库。

  2. 认可二/三方库升级与维护的价值,认可“磨刀不误砍柴工”的软件开发效能原则。

  3. 找到合适的、有自驱力的同学来维护、分享、推广优质的二/三方库。

  4. 建立宽松的开发氛围,严格的系统的dev ops,提高开发的积极性的同时提高服务稳定性。