构建工具如何让我在滴滴少掉一半头发

完美探险家
2025-12-23 09:40
阅读 660

去年双11前夜,我盯着 CI/CD 流水线里又红了一次的构建任务,心里直骂娘。那会儿已经是凌晨两点,司机端核心服务因为一个莫名其妙的依赖冲突直接挂了,测试群里@我的消息刷得比我心跳还快。我一边狂灌冰美式,一边在脑子里疯狂复盘:我们明明只是加了一个新功能模块,怎么构建过程就崩成这样?

我是滴滴后端团队的老油条了,干了快四年,其中两年多都在司机端核心业务组摸爬滚打。说“核心”,其实也就是天天和订单状态机、接单匹配、计价引擎这些玩意儿死磕。代码写得不算多优雅,但至少能跑——前提是构建系统别抽风。

说实话,在加入这个组之前,我对构建工具的理解基本停留在 mvn clean installnpm run build 这个层面。直到被现实狠狠教育了一顿,才意识到:在大型项目里,构建工具不是辅助,而是基础设施的核心一环


从“能跑就行”到“不敢动配置”

刚接手司机端项目时,我就发现它的构建脚本简直是个缝合怪:Maven + Shell 脚本 + Jenkinsfile 混搭,还有几个祖传的 Python 脚本藏在角落里,注释写着 “DO NOT TOUCH - by 张工(2019)”。每次改点东西,都得先烧香拜佛。

最离谱的是有一次,产品经理临时要加个灰度开关,我改完代码提了 PR,结果 CI 卡在 test 阶段整整两小时。查日志才发现,某个测试用例偷偷启动了内嵌的 Kafka 实例,而那个实例居然没关!资源耗尽,后面所有 job 全堵着。运维兄弟过来拍我肩膀:“你这构建是不是把测试环境当本地 IDE 用了?”

那一刻我悟了:构建流程的稳定性,直接决定了项目的交付节奏。尤其在滴滴这种高并发、强实时的业务场景下,一次构建失败可能就意味着几万司机接不到单。

于是我和组里几个老哥一合计,决定搞一次“构建现代化改造”。目标很朴素:让构建快、稳、可重复,最好还能让新来的实习生看懂


选型:Gradle 还是 Bazel?现实往往更骨感

一开始我们野心勃勃,想直接上 Bazel —— Google 出品,增量构建快如闪电,依赖管理精准到字节。但现实很快泼了冷水:

  • 团队里没人真正在生产环境用过 Bazel
  • 司机端项目有大量历史 Maven 插件(比如自研的代码生成器、安全扫描 hook)
  • 基础设施团队只对 Maven/Gradle 提供官方支持

折腾两周后,我们不得不承认:技术先进 ≠ 适合当前项目。最终我们选择了 Gradle,理由很务实:

  1. 兼容性好:能无缝集成现有 Maven 仓库和插件
  2. DSL 灵活:Groovy/Kotlin DSL 写逻辑比 XML 爽太多
  3. 社区生态成熟:Spring Boot、JOOQ、Flyway 这些我们都用,Gradle 支持一流

更重要的是,我们组有个共识:不要为了炫技引入团队掌控不了的技术。毕竟,谁也不想半夜被 PagerDuty 叫醒,只因为构建缓存失效导致全量编译超时。


实战:三板斧搞定构建痛点

第一斧:统一依赖版本,告别“薛定谔的包”

以前项目里经常出现这种魔幻场景:A 模块依赖 guava 30.0,B 模块依赖 31.1,结果 runtime 里加载了哪个版本全靠 classpath 顺序。线上偶尔 NPE,日志里还看不出问题。

我们引入了 Gradle 的 Platform/BOM(Bill of Materials)机制,把所有第三方库的版本集中管理:

// buildSrc/src/main/kotlin/Versions.kt
object Versions {
    const val GUAVA = "32.1.3-jre"
    const val SPRING_BOOT = "3.1.5"
}

// platform/build.gradle.kts
dependencies {
    api(platform("com.google.guava:guava:${Versions.GUAVA}"))
    api(platform("org.springframework.boot:spring-boot-dependencies:${Versions.SPRING_BOOT}"))
}

然后所有子项目只需声明依赖名,无需指定版本:

dependencies {
    implementation("com.google.guava:guava")
}

效果立竿见影:依赖冲突告警从每周十几次降到近乎为零。连 QA 同学都夸我们“最近提测的包终于不随机崩了”。

第二斧:缓存一切能缓存的

Gradle 的 build cache 是个神器,但我们一开始根本没开。为啥?因为早期项目里有些 task 带副作用(比如往本地写临时文件),开启缓存后反而导致脏数据。

解决办法很简单:重构那些“不纯”的 task。我们做了两件事:

  1. 所有输出路径显式声明(outputs.dir("build/generated")
  2. 禁止 task 直接读写非输入/输出目录

配合远程 build cache(我们用了公司自建的 Gradle Enterprise),本地冷构建从 8 分钟降到 2 分钟,CI 平均耗时减少 60%

场景 优化前 优化后 提升
本地首次构建 8m 12s 2m 05s ~75%
CI 平均耗时 6m 40s 2m 30s ~62%
增量构建(改一行代码) 1m 20s 8s ~90%

数据不会骗人。现在我改完一个小 bug,喝口水的功夫构建就完了——这种体验,真的会上瘾。

第三斧:构建即文档

以前新人入职,光搞明白怎么本地构建就得花两天。Shell 脚本里一堆魔法变量,README 还是三年前的。

我们干了件看似简单但极其有效的事:把构建入口统一成一个 ./gradlew 命令,并用 task 描述替代注释

# 以前
./scripts/build-with-test.sh --skip-integration

# 现在
./gradlew assemble -x integrationTest

同时,每个关键 task 都加了描述:

tasks.register("generateDriverProto") {
    description = "Generate Java classes from driver.proto (used in order matching)"
    // ...
}

运行 ./gradlew tasks 就能看到清晰说明:

Other tasks
-----------
generateDriverProto - Generate Java classes from driver.proto (used in order matching)
runLocalDevServer - Start local server with mock dependencies

现在新人第一天就能跑起来,再也不用追着我问“这个脚本到底干啥的”。


血泪教训:别在构建里玩“聪明”

有一次,我想“优化”一下流程:在构建时自动拉取最新的配置中心元数据,生成代码。听起来很酷,对吧?

结果上线当天,配置中心 API 瞬间限流,所有 CI job 卡住,发版窗口错过。Leader 看着我,眼神里写满了“你是来搞笑的吗”。

从此我牢牢记住一条原则:构建过程必须是纯函数式的——给定相同输入,必须产生相同输出,且不依赖外部服务状态

网络请求、数据库连接、实时时间……统统禁止出现在构建逻辑里。如果非要动态内容,也得提前下载好作为输入 artifact。


综合来看,构建工具是“代码人生”的隐形骨架

在滴滴这两年,我越来越觉得:一个项目的健康度,很大程度上取决于它的构建体验

  • 构建快,开发者就有动力频繁提交、小步快跑
  • 构建稳,线上事故就少,值班压力就小
  • 构建透明,团队协作成本就低,新人上手就快

这些东西看起来“不性感”,不像高并发架构或者 AI 推荐算法那样能写进简历亮点。但正是这些基建细节,决定了你每天是轻松下班,还是凌晨三点对着 red build 抓狂。

说到底,我们写代码不只是为了机器执行,更是为了人协作。而构建工具,就是连接“人”与“机器”的第一道桥梁。

上周五,我又看到一个新同事提交 PR 后,CI 绿得飞快。他转头对我说:“这构建也太丝滑了吧!” 我笑了笑,默默喝了口已经凉透的咖啡——这,就是代码人生的小小胜利。


最后一点真心话

如果你也在维护一个“祖传项目”,别怕动手改造。哪怕只是把构建脚本从 Shell 迁到 Gradle,或者加上一个 build scan,都是进步。

记住:没有完美的构建系统,只有不断适配团队和业务演进的构建实践

至于我?现在终于敢在周五晚上关掉电脑,而不是守着 CI 等构建结果了。虽然产品经理还是会半夜发微信问“这个需求明天能上线吗”,但至少,我不用再担心是因为构建崩了而上不了线。

这,就够了。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝