0%

Bazel 是 Google 开发的一款跨平台编译工具,是内部 blaze 的开源版本。本文不探讨 bazel 的使用,如果想了解如何用 Bazel 构建 go project,可以参考 这篇文章。本文主要探讨 Bazel 开发 go project 和纯使用 go modules 开发会有哪些优劣比较,我们如何去同时利用二者的优劣。

1. Bazel 开发的优势

1.1. 多语言支持

利用 Bazel 开发的 go project 能够支持非常好的可扩展性,对多语言的支持也比较友好,如果是一个公司内部的大项目,希望统一风格,统一 CI,统一构建,那么会存在这种状况:以 go 语言为主,其他语言为辅助。举一个例子,如果你的项目中包含了前端,那么 Bazel 的多语言处理就可以非常方便的让你同时编译 go project 和 frontend project。同理,如果你的项目中有 C++ 等其他语言也可以类似的用 Bazel 统一处理。这样在 monorepo 的管理上会方便很多。

1.2. 便于复用缓存和其他 bazel 的功能

bazel 自身是支持 remote cache 和 remote executor,个人认为 remote cache 功能是比较易用的一个功能。假如我们在 CI 中编译 go 项目,其实很难做到 go remote cache,基本上每次都得重新编译,但是 bazel 可以通过搭建 remote cache 服务重用缓存,极大的加大编译的速度。

另外,如果机器比较多,也可以使用 bazel 的 remote executor 服务搭建代码执行集群,让远端来跑 bazel 代码,这样也可以加速 go 的构建。不过实践下来一般 remote executor 可能不会比 remote executor 加速太多,除非专门对 build cluster 做过优化。

1.3. 镜像打包便捷而可复现

如果是一个 go modules 项目,那么镜像打包我可能就得写这么一个 Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM golang:1.14.2

ENV GOPATH=/go
ENV GOPROXY=https://goproxy.cn,direct
COPY . /go/src/my.project

WORKDIR /go/src/my.project
RUN go build -o main ./

FROM busybox

COPY --from=0 /go/src/my.project/main /usr/local/bin

CMD ["main"]

可以看到,这个 Dockerfile 的问题在于,在 ci 上每次编译都会导致重新编译,不会有任何的缓存,这就回到了上一个问题——无法复用缓存。

但是如果使用 Bazel,我们就可以使用 Bazel rules_docker 的 container_imagecontainer_push 方法,实现镜像的打包和上传。而且 rules_docker 本身是不需要有 docker 环境的,因此我们的 ci 甚至不用装 docker,只需要一个 bazel 就行了

1.4. 第三方包引用和更改比较方便

我自己并不是一个 go modules 深度用户,所以假如我们引用了一个第三方包,要对第三方包打 patch,就会比较麻烦,当然可以说,我们可以使用 vendor,但这并不符合 go modules 的哲学,那这样其实就会比较麻烦。

bazel 利用 gazelle 把所有第三方包显式的声明在 WORKSPACE 中,如果需要改动,只需给这个包加上 .patch 文件的 patch 即可实现更改,这样其实比起 vendor 来说更改也更容易被记录。

2. Bazel 开发相较于 go modules 的劣势

2.1 IDE 提示没有 go modules 友好

众所周知,go modules 很多 IDE 有非常好的支持,例如 vscode 和 goland,对于跳转等待支持得也非常好。但是 Bazel 就难办了,目前我尝试下来,只有 goland(或者 Jetbrains 系列) 的 bazel 插件是比较完善的,跳转基本能支持。

另一个方面,由于 Bazel 的尿性,每次本地执行 bazel build 会删除某些不必要的执行结果,因此可能会导致本地自己执行 bazel build 等指令之后,需要在 goland 中重新 sync 一下,这样会造成一些时间成本。

2.2 Bazel 依赖管理错乱

go modules 有自己一套依赖管理,但是 Bazel 的依赖,很多时候可能都是无脑 gazelle update 添加依赖,不会考虑依赖之间的版本关系,因此会导致 Bazel 项目中的版本依赖很错乱。

2.3 一些 go modules 相关的工具无法支持

一个例子就是 golangci-lint 这个包。这个东西是不支持 Bazel 的,因此只有 go modules 项目可以支持。

2.4 有一定的学习成本

对于团队中的同学来说,如何使用 Bazel,如何添加依赖,是有一定的学习成本的。

3. 同时支持 Bazel 和 go modules

可以看到二者有他的优劣,我们能否合并二者,取长补短,发挥二者的优势呢?答案是肯定的。

我们可以以 bazel 为主,go modules 为辅。以下针对上面说的 Bazel 3 个劣势来说明如何解决。

对于第一个劣势,虽然我个人觉得 goland 的 Bazel 插件其实还行,但是如果实在不能忍受,可以直接使用我们提供的 go.mod 来基于 go modules 开发。

对于第二个劣势,鉴于 bazel-gazelle 的 update 命令有 --from-file=go.mod flag,使得我们可以从 go.mod 中生成 Bazel 版本依赖,这样就可以保证 Bazel 的依赖管理和 go modules 的依赖管理是一致的,不会使得依赖管理错乱,解决了上面的第二个问题。这个技巧在 k8s repo-infra 的 update-deps.sh 中有使用。

对于第三个劣势,我们可以书写一些 Bazel 脚本,在 Bazel 的沙箱中使用 go modules 模式来使用 golangci-lint,这个方式在 k8s repo-infra 的 verify-golangci-lint.sh 中有使用。

对于第四个劣势,就要求项目维护者提供详尽的文档说明,如何 update 依赖,如何处理 Bazel 的一些问题,如何优化项目结构。当然我认为这并不是太大的问题,因为对于一个大项目来说,规范是必须的。

另外,k8s 有使用 Bazel 来构建 go projects,他们也采用的是 go modules 和 Bazel 同时支持的方案来实现。这样对于开发者也非常友好,同时对于大项目的管理、构建和测试也非常的友好。

由此,我书写了一个 bazel-go-scaffold,基于 repo-infra 扩展了一个基于 Bazel 的 go 大项目模板,供大家参考

初始化 fish 和 tmux 配置

一般来说,拿到一台 Linux 服务器,没有好用的 shell,没有好用的 tmux 配置,用起来很让人头疼。特别是有时候不知道 sudo 密码,但是可以不用密码 sudo,设置默认终端也是很麻烦的一件事情。

下面我就分享一下我的开机配置方案:

  • fish 并设置默认终端
  • .tmux.conf

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# install software-properties-common
sudo apt-get update -y && \
sudo apt install -y software-properties-common

# install fish
sudo apt-add-repository -y ppa:fish-shell/release-3 && \
sudo apt-get update -y && \
sudo apt-get install -y fish && \
sudo rm /etc/apt/sources.list.d/fish-shell-*

# use fish as default shell
command -v fish | sudo tee -a /etc/shells
sudo chsh -s "$(command -v fish)" "${USER}"

# install tmux and tmux config
sudo apt update -y && sudo apt install tmux
cd && git clone https://github.com/gpakosz/.tmux.git && \
ln -s -f .tmux/.tmux.conf && \
cp .tmux/.tmux.conf.local .

退出重新 ssh 服务器,或者重新打开终端,即可有一个非常爽 fish+tmux 环境

更改 .tmux.conf 的 tmux prefix 配置

由于 https://github.com/gpakosz/.tmux.git 的 .tmux 配置可以使用 C-b 也可以使用 C-a,但是作为 emacs 终端快捷键使用者,这两个键分别是左移和回到行首,不是很方便,我一般改成 C-x,更改方案是,进入 ~/.tmux.conf,更改下述两行

1
2
set -g prefix2 C-a                        # GNU-Screen compatible prefix
bind C-a send-prefix -2

1
2
3
4
set -g prefix2 C-x                        # GNU-Screen compatible prefix
bind C-x send-prefix -2
set -g prefix C-x # GNU-Screen compatible prefix
bind C-x send-prefix

即可。

0. 安装 docker 和 docker-compose

安装 docker 请参考

安装 docker 和 k8s 相关命令行工具

同时参考 daocloud 上的 docker-compose 安装文档,可以使用下述语句安装 docker-compose:

1
2
3
curl -L https://get.daocloud.io/docker/compose/releases/download/1.24.1/docker-compose-`uname -s`-`uname -m` > /tmp/docker-compose
chmod +x /tmp/docker-compose
sudo mv /tmp/docker-compose /usr/local/bin/docker-compose

1. 利用 docker-compose 部署单机单 osd ceph

主要参考官方 docker 镜像文档,但最新的 ceph/daemon 镜像似乎有点奇怪的 bug,导致 osd 起不来,因此,我们使用一个 2019年年初的镜像,已经 tag 成 huangwx/ceph-daemon 供使用。ceph 的 docker-compose 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# docker-compose-ceph.yaml
version: '3.7'
services:
ceph-mon:
image: huangwx/ceph-daemon
network_mode: host
privileged: true
volumes:
- ./ceph:/etc/ceph
- cephdata:/var/lib/ceph
container_name: ceph-mon
command:
- mon
environment:
- MON_IP=${MON_IP}
- CEPH_PUBLIC_NETWORK=${CEPH_PUBLIC_NETWORK}
ceph-mgr:
image: huangwx/ceph-daemon
network_mode: host
privileged: true
volumes:
- ./ceph:/etc/ceph
- cephdata:/var/lib/ceph
container_name: ceph-mgr
command:
- mgr
depends_on:
- ceph-mon
ceph-osd:
image: huangwx/ceph-daemon
network_mode: host
privileged: true
volumes:
- ./ceph:/etc/ceph
- cephdata:/var/lib/ceph
container_name: ceph-osd
command:
- osd
depends_on:
- ceph-mgr
environment:
- OSD_TYPE=directory
pid: host
volumes:
cephdata: {}
networks:
default:
external: true
name: host

docker-compose-ceph.yaml 的相同目录下新建一个 ceph 目录

注意设置 MON_IPCEPH_PUBLIC_NETWORK 两个环境变量,两个环境变量分别为主机 IP 地址,允许访问 ceph 的网段。下面仅作为演示,MON_IP 和 CEPH_PUBLIC_NETWORK 请设置成你自己的:

1
export MON_IP=192.168.2.2 CEPH_PUBLIC_NETWORK=192.168.0.0/16

同时请确认 ./ceph 目录下没有任何 ceph 的配置文件,然后启动 ceph:

1
docker-compose -f docker-compose-ceph.yaml up -d

启动后会在 ./ceph 目录下生成下述3个 ceph 配置文件:

1
2
3
ceph.client.admin.keyring
ceph.conf
ceph.mon.keyring

启动成功之后,稍等片刻,然后执行下述命令 check ceph 是否正常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker exec -it ceph-mon ceph -s
cluster:
id: e3cdecce-16f7-4c0f-9909-ab94a5e7c1b5
health: HEALTH_OK

services:
mon: 1 daemons, quorum new
mgr: new(active)
osd: 1 osds: 1 up, 1 in

data:
pools: 0 pools, 0 pgs
objects: 0 objects, 0 B
usage: 2.9 GiB used, 1020 GiB / 1023 GiB avail
pgs:

如果成功看到类似上述的返回,即说明成功。

2. 初始化 rbd 配置

接下来初始化 rbd 配置。由于是单 osd 单 monitor 集群,因此我们需要创建并设置 rbd:

1
2
3
4
docker exec -it ceph-mon ceph osd pool create rbd 64 64 replicated
docker exec -it ceph-mon ceph osd pool application enable rbd rbd
docker exec -it ceph-mon rbd pool init rbd
docker exec -it ceph-mon ceph osd pool set rbd size 1

即可成功初始化并设置 rbd,可以利用 rbd ls 命令 check 是否能够不卡住返回:

1
docker exec -it ceph-mon rbd ls

如果能够成功不卡住返回空值,说明 rbd 集群正常,执行 ceph -s 可以看到初始化了 1 个 pool 和 64 个 pgs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker exec -it ceph-mon ceph -s
cluster:
id: e3cdecce-16f7-4c0f-9909-ab94a5e7c1b5
health: HEALTH_OK

services:
mon: 1 daemons, quorum new
mgr: new(active)
osd: 1 osds: 1 up, 1 in

data:
pools: 1 pools, 64 pgs
objects: 0 objects, 0 B
usage: 2.9 GiB used, 1020 GiB / 1023 GiB avail
pgs: 64 active+clean

本文主要参考 Kubernetes 官方文档 Creating a single control-plane cluster with kubeadm (v1.16),但由于国内实在是访问 Google 困难,之前的教程都是从国内各种镜像中拉取 k8s 镜像,再 tag 成官方的,实在是不太优雅。好在 K8S 新版支持了指定镜像源,可以方便的指定国内源。

0. 安装 docker 和 k8s 相关命令行工具

0.1 安装 docker

利用 daocloud 上的 docker 安装方法在国内安装 docker,由于默认会从官方拉,我们可以考虑利用 Aliyun 的镜像

1
curl -sSL https://get.daocloud.io/docker | sh -s -- --mirror Aliyun

然后配置 docker 加速器,这里我们选择网易的 docker 源,如果有其他国内源,你也可以自行更改

1
curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://hub-mirror.c.163.com

根据提示重启 docker

1
sudo systemctl restart docker.service

0.2 安装 k8s 相关命令行工具——kubelet, kubeadm, kubectl

在这里我们利用阿里云的 ubuntu k8s 源下载 k8s 相关包

1
2
3
4
5
6
7
sudo apt-get update && sudo apt-get install -y apt-transport-https curl
curl -s https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add -
sudo tee -a /etc/apt/sources.list.d/kubernetes.list > /dev/null <<EOT
deb http://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main
EOT

sudo apt install -y kubelet=1.16.2-00 kubeadm=1.16.2-00 kubectl=1.16.2-00

我们这里安装的是目前 K8S 最新的版本 v1.16.2

1. 初始化 Kubernetes

1.1 初始化 k8s master

K8s 1.11 支持了 kubeadm 传入配置文件,我们可以新建下述配置文件 kubeadm.conf,注意我们采用的是 k8s 1.16.1 的相关配置,这个版本支持的 etcd 镜像 tag 为 3.3.15-0,如果有必要,dnsDomain 可以改成自己需要的,这里我们采用默认的 cluster.local

如果你的 k8s 版本不一致,可以采用 kubeadm config images list 查看你的 k8s 所需要的 etcd 镜像 tag。

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
etcd:
# one of local or external
local:
imageRepository: "registry.aliyuncs.com/google_containers" # origin is "k8s.gcr.io"
imageTag: "3.3.15-0"
networking:
serviceSubnet: "10.96.0.0/12"
podSubnet: "10.244.0.0/16" # for flannel
dnsDomain: "cluster.local"
kubernetesVersion: "v1.16.2"
imageRepository: "registry.aliyuncs.com/google_containers" # origin is "k8s.gcr.io"

这里我们用到了阿里云的 k8s 镜像源。上面的 kubernetesVersion 写明自己的 kubeadm 版本,不然会去 google 官方请求版本,可能会卡住。然后运行

1
kubeadm init --config kubeadm.conf

即可初始化集群。

完成之后,初始化成功,会输出类似于下面的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Your Kubernetes master has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/

You can now join any number of machines by running the following on each node
as root:

kubeadm join 172.17.49.197:6443 --token zq59ch.wrr80vejg49091mw --discovery-token-ca-cert-hash sha256:197810ac44d29e4f311a639b22f1dac7dced81604c489be390a1fa5cf0b96f8d

执行

1
2
3
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

最后,为了让 master 也可以调度,可以加入下面的命令:

1
kubectl taint node $(kubectl get nodes -o name) node-role.kubernetes.io/master-

1.2 添加网络配置

上述信息做好了之后,在 master 上加入 flannel 插件,flannel 插件版本我们目前采用文档中针对 v1.16 的版本,由于国内网络环境缓慢,建议下载该文件更改镜像之后再 kubectl apply:

1
2
curl https://raw.githubusercontent.com/coreos/flannel/2140ac876ef134e0ed5af15c65e414cf26827915/Documentation/kube-flannel.yml | sed 's/quay.io/quay.azk8s.cn/g' > /tmp/kube-flannel.yml
kubectl apply -f /tmp/kube-flannel.yml

注意上述命令只需要在主节点执行。

1.3 节点加入

kubeadm init成功后,命令行会显示 Join 命令,命令格式如下:

1
kubeadm join --token <token> <master-ip>:<master-port> --discovery-token-ca-cert-hash sha256:<hash>

但如果之后忘记了命令,可以用下述命令获取 <token>

1
kubeadm token list

然后可以用下述命令获得 <hash>

1
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'