Gopher China大会《bazel build //:go》演讲

2,357 阅读4分钟
原文链接: mp.weixin.qq.com

作者简介

何 源

古典互联网从业者

2014年底加入英语流利说,目前主要负责 Platform Team。来流利说工作之前,在 the Plant 杭州工作。

1.程序包管理(Package Management)

2.代码管理(Code Management  Multi languages )

3.bazel build //:go

4.Demo

1. 程序包管理(Package Management)

Vendor 

Go 从 1.5 版本开始引进了 Vendor,  1.7 的时候默认已经从 vendor 下面加载代码了,如果你所使用的 lib 的安装的 Go 版本提供不一致,放在 vendor下面是一个好的选择, 举个例子,这是 Rob Pike 一个开源项目 upspin, upspin  一个文件共享项目,用 tree 看一下他的 Vendor 目录:

 

因为 upspin 使用了 fuse,所以理所当然 fuse 被全部拿了进来,还有 protobuf、crypto..., 基本上用到的都 copy 了一次,这是大家使用 vendor 最常见的方式,

但仔细想想, vendor 下面的代码本来是开源的代码,为什么要提交到自己的库里面呢?如何保持 Vendor 下面 lib 的版本更新?如何解决依赖的问题?

今年2月份的时候,Russ Cox有话要讲,说我们这么多年用 vendor 辛苦大家了,Golang 的确需要一个依赖和版本管理工具 ,所以他就发了一篇文章,说了三件事情

1. Rust 的 Cargo 做的不错,于是我们做了 dep

2. 八年的 go install 和 go get 幸苦大家了,go module 了解一下

3. go module 会干掉大部分的 vendor 目录

Vgo

   然后就有了 Vgo

 

写了 7 篇博客解释,为什么要做 vgo,为什么用 Go module。但是当一个技术或版本管理需要这么多博客把它讲清楚的时候,我相信 使用者也不一定能在较短的时间搞明白。

回到前面, Package Managment 有三个问题需要解决:

  1.Packges Versioned。我不希望 Packges 里面出现V1、V2、V3,

  2.希望每次的 build 代码, 结果都是一致的

  3. Work outside of $GOPATH

 

Russ 提了一个 issue 来阐述这个问题,目前 Github 上面 Golang 大概有4万个 star,这个 issue 点赞的有 141 个人,4个人减了1, 当然这也不能说明什么。

在看一件事情的时候,通常带着问题去理解比被动接受好很多,在流利说叫心怀好奇,在这里我们的问题是:

Golang 在谷歌用了这么多年,这么多年谷歌是怎么 build Go 代码

带着这个问题,我们进入下一个话题

2.代码管理(Code Management )

   

   代码管理是蛮难的事情,工作中除了写代码就是在管理代码,要想怎么组织,放在这里是不是合适,而且业务增长的时候,要想这个代码是不是需要抽象,是不是要整理出去做其他的 team 用。

   流利说一直在快速增长,遇到很多的技术问题。比如当公司快速 增长的时候,怎么支持你的业务?再比如说 codebase 里面有C++,有一些机器学习的库就是用 Python 写的,codebase 里面不光光只有 Golang 的代码,那我们怎么做 build 呢?

   再回到刚刚的话题,流利说业务最近增长的确比较快,我来之前统计一下线上的跑的服务里面,现在大概有213个 services 在跑,但是这是一个虚高的数字,里面包含了 k8s 自己的一些服务,比如 kube-proxy ,所以线上大概有 160+ 或者180+单独的项目,基本上一个开发要同时 管多个项目是常态。

 

   另外,我们其实有一些非常复杂的系统在演进,递变,比如说我们 adpative learning 有算法参与、数据参与有后端参与,这么多工种在一起,合作会出现问题,还有紧密的耦合,我们正在变成一个 big data company,big data 的意思是越来越需要通过数据反馈告诉我们下一步怎么做,这个反馈的回路非常重要,而且要 及时,不同的数据之间数据的交互应该怎么做,不同的语言之间如何交互,异构的项目怎么去编译,你不能说我给你一个 makefile 去编译,那这个 makefile 可能写得又臭又长,甚至没有办法维护。

2.1 代码管理挑战

 快速增长的时候,管理代码会有哪些挑战呢?比如说,我们在流利说内部大规模的使用 protobuf 来定义数据结构,当我废弃了某个 字段后我怎么告诉你?希望的方式是所有依赖这个 proto 的 repo CI build 失败。

 

 我们可以看一个例子:

 

大家如果有用懂你英语的话,就知道懂你英语其实有很多题型 (Activity),我们把这些 Activity 的 Type 定义了一个 enum, 由于历史的原因大家在不同的系统中写了不同的 enum,不同的 tag 在不同的系统中代表的意思不一样,在后面使用过程当中造成的问题是不同的系统拿到 同样的 tag 但是代表的意思不一样, 比如 0 在有的系统里面代表 PRESENTATION_TYPE, 但是其他系统里面却是  UNKNOWN。

2.2 如何重新使用代码和solution

举一个例子,你写了一个 lib 和 makefile,你终于在本地跑成功之后你怎么让别人知道?比如,流利说有一个 fingerprint 的函数,我们有一些题目的内容,需要根据内容生成 ID, fingerprint 函数有不同的实现,有 JAVA 的 实现,C++有实现,我们怎么保证在 fingerprint 函数做了修改的之后,其他的语言都是也一起改掉, 如何你放在不同的 repo 可能会忘记的。

2.3 如何共享知识

我们希望对于系统的解决方案,做一次就够了。2017年流利说从谷歌挖过来了两个博士之后开始使用 bazel,bazel 基本上解决了上述的问题,bazel 是 Google Blaze 系统的开源版本,现在 bazel 还在 beta 阶段,没有发正式的1.0,  Golang 的 rules_go 其实现在还是阿尔法版本,没到 beta,所以踩了一些坑,但好在现在用起来基本没什么问题了。

先看一下谷歌的 Blaze 使用数据

 

谷歌所有代码是在一个  repo 里面,每天有 45K  commits/day,有 800K builds,当然他们有一个专门的团队做 Blaze。

Bazel 有别与 Makefile, 先看一下 BUILD 文件的对比(这个是 leveldb  的某个 build 片段)

 

右边 Bazel BUILD ,左边是 Makefile ,很明显可以看出来哪个清晰,哪个不清晰

 

   这是我们一个 repo 的蛮简单的 protobuf,里面有ABCD四个 proto,它们之间互相引用,这是 bazel 生成的 protoc 的 shell 脚本

 

   这就是刚刚前面的例子,我们把 ABCD 四个 proto 放在 deps 里面 ,还有gRPC,verbose 会把 protoc 命令打出来,通过 go_proto_library 去生成一个服务,大概24行, 最前面就是一个声明,引用了我们自己的 common / bazel,再看看上一张图感受一下,如果我们用 makefile 去写,这个所对应的其实是其中只有最下面这两部分,代码大的时候 是没有办法维护的。

2.4 如何使用Bazel

在 project root 里写一个 WORKSPACE 文件

   第一个是写一个 WORKSPACE 的文件,声明根目录是你的 WORKSPACE,所有 build规则是基于 WORKSPACE 的绝对路径,不需要知道上一层是什么,知道根目录就知道所有的路径是什么样子。

我们看 WORKSPACE 文件的例子

 

   分为三个部分,第一个部分有一个 name,名字是反过来写的。下一个是 bazel,由 很多的 rules 构成,我们这里引用 rules Go,指定一个版本,然后再引入 gazelle,Go 自动生成 BUILD 文件的工具。上面初始完之后,就把 dependencies 全部放进来,做一个初始化,整个语言脚本类似于 Python ,他们有一个名字叫做 skylark,这就是这个项目跑起来的初始化的东西。我们要求在 WORKSPACE 里面显式声明所有依赖,好处是,我们知道里面用了哪些代码,就算是间接依赖也要声明。这里是支持 branch 和 commit 的,我们内部不推荐用 master branch,因为 master 是 有缓存,如果本地有之前的  cache, 则不会线上拉新的  master。我们推荐你直接写 commit 。

在每一个目录写一个 BUILD

   在每一个目录下面写一个 BUILD 的文件,BUILD 显式规定 代码应该被怎样 build,这个有两个好处,一个就是我们通过 BUILD 文件,就知道这个里面哪些模块是相互依赖的,还有就是通过 BUILD 文件在同一个目录放不同的 package。

这是一个 BUILD 例子

 

这个声明 package visibility,如果是 private 则不能被引用,只有当前的目录是可用的,然后把流利说开源的 bazel_essentials 引进来,其实改了一些 rules,这里必须要声明 go_prefix,因为这是一个 build 时候用来指定前面的 prefix。 下面有两个声明 ,一个是 go_binary,一个是 go_ library,go_binary 是生产一个可执行的文件,go_library 是把go 源文件当成一个 library 放出去。

下面是一个比较复杂例子

   

这里显式列出了哪些是 test,可以把所有 test 后缀文件放进去, 但我不建议这么做,我们非常希望是说当你的 repo 不是足够大的,test 文件是可以数得过来的。这里有一个 timeout,可以设定这个 test 的最大执行时间,如果跑不完就直接报错了。

我们解释一下前面这些规则什么意思。

 

前面有 @ 的就是 external,后面的 protoc 是 target name。

   简单说几条build的脚本

1. build //...

第一个 `bazel build //...` 代表 build WORKSPACE 下面所有的 target,会扫所有的目录

2. build //:demo

单独的 build 的 target 则直接 // :demo, 这里 :demo 是 target name

3. run //:demo

执行 :demo

4. test //:demo_ test

跑 demo_test 测试

   下面是几个经常用到的 bazel 命令

 

第一个是命令是在编译其他平台(如 macOS)上 linux amd64,就可以把 experimental_platforms 打开,这个 feature 是比较新的,还是在内测期间。

bbazel query 会把你当前的依赖打出来,你可以通过 bazel query 把某个 package 下面所有的 target 列出来。

最后一个是如果你想忽略某个 target 或目录,在后面加一个减号就可以了。

 

还有一种情况是有些 package 已经下载好了,想单独的跑一个 target 试试看,则如第一条所示, 个 -(dash) 后面是自己传了参数,这个也是通用做法。

   你可以 run 单独 target (第二条),还可以查所有的  dependency (最后一条)。这样的好处就是不需要去依赖语言或者其他工具,就可以通过 bazel 显式告诉你代码是怎么build,也可以通过 bazel query 去查,画一个图出来。

3.bazel build //:go

   我们正式说到 bazel build//:Go。之前我们提到 "我们应该带着问题看,谷歌是怎么 build go 的?" 其实用的就是 blazel。

在用 bazel 之前,第一步是先删除 vendor:  cd<your_go_project> && rm-rf vendor,

(但是这样做的话,有点激进,不稳,做为开发我们知道做事一定要稳,你在 rm -rf 的时候要三思)。

另外说一下 rules_go, 目前现在还是在开发之中,现在支持这几种

 

   不支持的

 

3.1 bazel build //:gazelle

这是自动生成 BUILD 的工具,有 gazelle 之后,社区 package 就可以通过 gazelle 生成所有的 BUILD 文件. 那么怎么样才能让你自己的项目支持 bazel build 呢?分三步:

 

首先在 project root 创建一个 WORKSPACE 文件,WORKSPACE 声明要有哪些 rule 和依赖的 package。第二步就是跑一下 bazel run //:gazelle,生成 BUILD 文件,第三步更新还没有的依赖。

DEMO

本来是要现场 demo, 但由于网络等因素,我录了一个视频,我们 build 一个谢大的 beego,我们先在 Github下面 clone下来。现在是没有文件的,我们需要 WORKSPACE一个文件,直接打开编辑,我们可以从 rules_go 的 Github 上面去把一些规则拷进来,这有明显的说明,应该要有哪些rule,然后版本是什么样的。如果你有一个新的 WORKSPACE 的时候,就可以看一下显示的名字是什么。我们需要把  gazelle 放进去,可以帮我们自动生成所有的 BUILD文件,然后初始化一下。其实你希望所有的 rules 放在一起,这个做的的好处是不需要把所有的代码刻录下来,你只要 update 当前的文件就可以了。

   我们再回到前面提到的三个问题

1. [X] Packages Versioned

bazel 解决了这个问题,你针对 package 指定 commit

2. [X] Verifiable and verified build

这个也是OK的,有任何改动或者升级的时候,就算依赖,每次结果也是一样的。

3. [X] Work outside $GOPATH

虽然习惯把代码放在 $GOPATH,build 的时候没有设定和依赖 任何的 GOPATH。

3.2 最小版本选择(Minimal Version Selection)

处理的方式我们可以简单一点,我们只需要考虑 2 个版本, 一个是我们的代码需不需要升级到最新的,这时直接用 master / HEAD。另一个是我知道在哪个commit 之前之后可以work,指定 commit 就好了。因为虽然 sematic version 说明了的大版本可能有 break change,但是其实真正遵守的人不是那么多。

远程缓存(Remote Caching)

用 Bazel 是把 Go 的整个 lib 下载来的,Remote Caching 可以避免无必要的下载,所有build 的文件都会帮你做一下 Caching,很简单,用了 md5 hash,把 Bazel 当前的 WORKSPACE 目录移上去,做特殊的处理,做一些公共的集群而已。

远程执行(Remote Execution)

在本地 build 是在 sandbox 里面跑的,开多个线程去跑,如果你想更快的 build, 可以使用 remote Execution。

Bazel 还可以 build 其它的。比如说还可以 build docker,这个是我们一直想尝试的,还可以 build 安卓、i OS 和 web。当全部切到bazel之后,跨语言跨平台的 build 就不是问题,我们只需要知道 bazel build 什么。

3.3 流利说使用bazel遇到的问题

第一,需要都从 master/head pull 最新代码

谷歌是在同一个 repo ,就不存在这个问题。为了解决这个问题,博士用Skylark 写了一个 rule 。

 

这个代码已经开源,需要与环境变量 BAZEL_RUNID 配合使用,BAZEL_RUNID 的值变化是,则会更新 cache。

 

一般 BAZEL_RUNID  会设置成随机值,或者是当前的时间,这样每次跑 CI 的时候,拉的是最新的 代码。

 

第二,bazel 上手比较慢

我们发现 bazel 的想法好的,但是如果初次使用或者从其他工具刚转过来,上手比较慢,我们通过 Codelab 解决。

 

第三,第三方依赖问题

最近遇到一个问题

 

qiniu_x 加了 go_repository 时候会报错,不能 build,这个原因是什么呢?

 

错误提示是需要 `com_qiniupkg_x`,其实代码都在 @com github qiniu_x// 下面,为什么还会有 package?

 

原来是在 qiniu_x config.v7 的时候, 用了另一个 package name ,go build 是没有问题,因为这个 import path 是可以用的,但糟糕的是这个代码恰巧就在 同一个 repo 里面,反而要去另外一个 import path  log.v7, 解决方法就是重用 repo,这边其实就是 Github 的qiniu_x,代码跟它一样,直接拷贝,现在遇到第三方的问题如果有这样的问题只能这么做。