跳到主要内容

SRE 要有工程洁癖

· 阅读需 7 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

做研发、做架构我们通常会追求工程优雅性,讲究代码质量、架构设计、方案合理性等,但 SRE 领域好像很少有人谈这些?

实际上,SRE 领域对工程规范的追求同样重要,甚至更需要,因为 SRE 的工作直接关系到系统的稳定性和可靠性。线上出了问题,我们是第一责任人,理应要有更高的要求。

当然,有同学可能会说,当前绝大部分的 SRE 工作还是偏向于运维,偏向于事务型工作,这种好像没办法追求工程优雅性?

其实不是的,细节中有魔鬼,比如:

  • 你们的配置文件中,服务间调用有没有直接用 IP、写死端口的情况? 如果有这样的情况,你是听之任之将就继续,还是追根溯源,思考和推动使用更优雅的方案?这种配置方式看似简单直接,省了一时之事。但一旦服务器迁移或扩容,就需要手动修改大量配置,更糟糕的是,如果忘记修改某些配置,甚至可能导致事故。

  • 你有没有遇到服务下线,机器上的配置,仓库中的配置清理不干净的情况?这种时候,在解决当前问题的同时,我们要不要思考为什么会出现这种情况,是哪里做的不到位,还是有人没有遵守 SOP 规范?配置文件残留可能导致新服务启动时读取到错误的配置,代码仓库中的废弃配置会误导其他同事,增加维护成本,甚至机器上的残留配置可能占用系统资源,甚至可能被误用,这些很明显有可能引发问题。

  • 谈到 SOP 规范,SRE 同学一定觉得司空见惯,但它一定就合理吗?出一个问题,就来一个 SOP 规范,这么多 SOP 规范,我们真的能记住吗?对于不合理、不友好的 SOP 规范,你有没有去 Challenge,有没有思考更好的方案?

  • 某些业务服务参数,比如数据库连接池大小、线程池配置、缓存参数等,每次都是研发让 SRE 手动去调。这种时候,你是选择被动接受,还是主动思考,每次都这么搞,不仅效率低下,而且容易出错。有没有想过,也许有更好的解决方案?比如业务程序自身实现自适应机制,根据负载自动调整参数;比如开发自动化工具,实现参数的智能调整等等。

  • 大家每天处理的告警数有多少?被 OnCall 几次?如果是半夜被叫醒,难不难受?这里面的合理性到底在哪?真的不能 0 告警或者趋近于 0 吗?

此类场景在 SRE 日常工作中有很多,看似都是"小问题",实际上都是工程实践不完美的体现,都是可改进的。

将就的方案可以临时解决问题,短期内也许不会有明显的负担,但长期来看,这些工程债务会不断累积,最终导致系统维护成本越来越高,进而引发事故。

当然,即使最终出问题,也许不会归咎于 SRE。但 SRE 作为最密切的接触者,直接的受害者,是有责任,也应该有动力去推动改进的。

所以我认为,SRE 要有工程洁癖

什么意思? 简单讲,就是你得嫌它"脏"啊

因为有工程洁癖,我们才会:

  • 能发现不优雅的地方,会想着改进
  • 会不满足于现状,精益求精
  • 会追求完美,不断进步

这种"洁癖"不是吹毛求疵,而是一种对工程质量的追求。

这些"脏"的问题,很多时候我们不提,可能就没人会提了。因为研发侧更关注业务功能的实现,对运维工程层面的问题不够敏感,动力也没那么足,毕竟不是他在难受。管理层也可能更关注业务指标,线上不出问题,他们可能天然会觉得没有问题。这时候,就需要依赖 SRE 的不断的提出问题。

当然提出问题的同时,也不能仅仅停留于此,还需要思考更好的解决方案。

我在之前的文章提到, SRE 本质上也是软件工程师, 应该习惯于通过软件工程师的思维来解决问题,这是 SRE 领域的第一性原理。

太阳底下没有新鲜事。

在工程领域,几乎没有问题是不可解决的,也没有问题是前人没遇到过的。站在巨人的肩膀上,我们可以少走很多弯路。

尤其现在是 AI 时代,知识平权,AI 就是最好的老师

只要我们保持品味,不断探索,一切皆有可能。

大家觉得呢?

SRE 如何走向成功

· 阅读需 15 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

首先, 应该明确一个观点: SRE 本质上也是软件工程师, 应该习惯于通过软件工程师的思维来解决问题。这个观点,我认为可以是 SRE 领域的第一性原理

对于不接触代码, 不愿意深入到代码实现细节的工作方式, 我认为在当前这个阶段是不行的。

线上的很多问题,本质上是业务问题,单靠运维手段是做不好稳定性保障的。而如果能从业务角度出发,从根源上解决问题,那稳定性天然就会得到保障。

所以从这个角度, SRE 花时间花精力去学习业务,理解实现,修复代码,可能反而是性价比更高的一种方式。

当然, SRE 本身就很忙,需要解决各种线上问题,可能并没有太多时间去深入理解业务,更没时间去深入到代码实现。

这是个问题,但不管是从组织侧,还是 SRE 团队内部,都可以有一定的优化空间。此处我们暂且不表,只说个人应该如何做。

从个人角度,如果说以前我们还有各种理由去拖延,比如不熟悉语言,不熟悉框架,不熟悉工具的话,但是现在已经是 AI 时代,借助 AI, 这些都不是问题。

知识平权,人人都可以成为专家。

我个人体验也是如此。以前我是搞测开的,没怎么做过线上相关工作,但是现在干了一两个月,仍感觉信心满满。因为我发现,工程技术领域没有秘密,只要有正确的认知和做事方式,没有什么是搞不定的。

那什么才是正确认知和做事方式呢?

践行最佳工程实践

我认为最重要的一点是: 寻找和践行最佳工程实践

得过且过要不得,只解决表面问题也要不得。

拿到一件事,我们首先应该思考的是,这到底是什么问题?本质是啥?有没有上下文依赖项?到底要不要做?如果需要做,那应该如何做?是直接上手干,还是先调研一下?

其实在我看来,很多司空见惯的事情,背后都有很多值得思考的地方。如果只是按部就班的做,那可能永远只能停留在表面,无法深入,同样的问题还是可能会反复出现。

太阳底下没有新鲜事,很多问题,其实早就已经有人遇到过,甚至已经总结出了最佳实践。我们只需要站在巨人的肩膀上,就可以少走很多弯路。

那如何落实这个工作法呢?

这同样也不是个新鲜事,最重要的做法就是要有设计文档。不管是 aws,google,还是国内的其他优秀公司,我们都能看到对方案设计的重视。

我们大家可以思考下,你所在的公司、团队,有没有非常重视设计文档?你个人最近又有几篇设计文档输出?

如果数字不那么好看,那可能就需要反思下,这里面是不是有可以优化的地方。

当然,不能光看数量,更重要的是质量。

从组织侧,我认为把控设计文档的质量,是非常重要的一件事。

一定要推行强 review 文化。团队内部的 review 是基本的,如果能做到跨团队 review,那更好。

甚至,如果能向公司内部更高级别的技术领导者寻求建议,那无疑是最佳的。

一人计短,达者为先。

review 的好处在于能补充更多的上下文,也能学习到不同的思考方式,同时也能减少方案后续推广的心智负担,因此性价比很高。

不断提升 SRE 工作专业性

软件工程没有银弹, 稳定性更是个系统性工程。SRE 工作内容很多,甚至很杂,这些都是事实,但总有做的好的行业前辈值得我们学习。

比如,就拿设立团队目标来说,可能会有人这么设计: 提升业务稳定性,确保 P1/P2 故障单季度次数小于 xx 次。

这个目标看似合理,但怎么定义 P1/P2 优先级?有没有具体的量化标准?若有的话,是不是全公司都认同?如果没有形成共识,那这个目标说服力就不足。

其次, 以事故数量来定义工作目标也不够科学, 如果说目标是 0 事故到还可以接受,但若目标定义在发生 2 次或者 3 次,谁又真正分辨出他们之间的区别和好坏?

所以,从这个角度,稳定性目标应该要更客观、更具有指导性。

参照《SRE: Google 运维解密》一书,构建面向 SLO 的工作方式,就显得相对科学和专业。

SLO 是 Service Level Objective 的缩写,是指 服务质量目标

这本书,有花一大段文字来讲清楚,如何在指标建模、指标选择、指标分析等维度上的思考框架,我认为非常有参考性, 比如:

  • SLO 目标应该从用户最关心的方面入手,而不是看现在能度量什么。与其选择指标,再想出对应的目标,不如从想要的目标反向推导具体的指标。
  • 选择目标 SLO 不是一个纯粹的技术活动,必然涉及到产品和业务层面的决策。因为现实中,经常可能牺牲某些产品特性,或者承受一定的风险,以满足商业化的需求。所以 SLO 目标不是 SRE 一方就能决策的,务必前后拉通后,大家认知一致。
  • SLO 越少越好,一定要确保每个 SLO 目标都是必不可少的。如果我们无法针对某个 SLO 目标说服开发团队,那么可能这个目标就是不必要的。

确定了 SLO 目标,也就确定了 SRE 在稳定性方面的工作目标。在这样一个可量化的目标中,我们可以看到我们的工作效果,也更能指导我们工作的改进。

黄金 4 指标

SLO 目标要做好,一定要有监控,而对任何用户可见的系统来讲,如果只能监控 4 个指标,那一定是 Latency(延迟)、Traffic(流量)、Errors(错误) 和 Saturation(饱和度).

相信对所有专业同学来讲,这个认知一定不陌生。不过,这里面有一些细节认知,我觉得还是需要注意的,比如:

  • 延迟指标要区分成功和错误的情况。尤其一些慢错误情况,对系统的伤害更大,甚至有可能拖垮整个系统,要特别注意。

  • 流量要基于不同的业务来看,比如有些系统适合监控 QPS(每秒查询数),有些则需要关注带宽使用率或数据传输量。例如,对于 Web 服务可以看每秒 HTTP 请求数;对于流媒体系统,可能更关注网络 I/O 速率和并发会话数;对于数据库或存储系统,可能要监控 IOPS(每秒 I/O 操作数)和吞吐量;对于消息队列系统,则重点关注消息处理速率。监控流量不仅要看总量,还需要分析流量模式和趋势,这有助于容量规划和异常流量的及时发现。

  • 错误率指标统计显示失败的情况,可能比较普遍,但极容易忽视隐式失败。比如返回请求是 200,但内容有问题。或者客户端对时间有要求,超过 1s 的请求都认为失败等等。

    • 我就遇到过 CDN 相关服务,因为网络波动,导致在大文件场景下,返回码是 200,但后续吐数据极易失败的场景。这种时候如果只看返回码,就会遗漏。
  • 饱和度指标,反应当前服务的容量有多 "满",通常是系统中最受限的那个资源决定的,也就是符合 "木桶理论"。当然复杂系统下,挺难一下知道哪个是短板,但我们可以通过压力测试大略得到。实际上,饱和度指标通常都可以通过其他高层次的间接指标代替,比如 延迟增加就可能是饱和度的前导现象,我们就可以将 99% 的请求延迟(在某一个小的时间范围内,例如一分钟)可以作为一个饱和度早期预警的指标。

黄金四指标对用户可见系统的重要性不言而喻,它其实就是入口指标,日常 SRE 要做好这块的监控和响应,那监控方面基本就问题不大。

专业性也体现在如何对待琐事上

琐事是指运维服务中手动性的,重复性的,可以被自动化的,战术性,没有持久价值的工作。Google 会把 SRE 的工作时间中用于琐事的比例低于 50%作为很重要的管理目标。

我觉得虽然我们今天可能达不到这个目标,但是每一位 SRE 从业者都应该往这个目标靠齐。因为,这体现了我们作为软件工程师的追求问题。只有不断从技术工程中要创新,要效率,我们才能从繁琐的手动操作中解放出来,所谓'咖啡运维'才能成为可能。

SRE 本身是个技术含量很高的职业选择,做得越好,接触的工程技术越复杂。因为从业务服务角度,它需要的东西是恒定的,无外乎硬件资源、网络资源、系统中间件等。而如何提供这些资源却非常讲究,资源和服务提供的越方便、越便捷,那对业务一定越有利。我相信没有任何一个业务方愿意花大精力无脑造轮子的,除非支持不到位。而这理论上都是 SRE 可以解决的问题范畴。所以你看《SRE: Google 运维解密》这本书,有大量的篇幅在讲啥?在讲他们如何做负载均衡,如何应对过载;在讲如何提供分布式共识服务(Chubby);在讲他们的分布式任务系统是啥样;也在前面的章节讲了他们如何管理物理机系统(Borg)等等,而这些领域,我们深入想想就会发现,其实是个必然,对吧?毕竟问题是一样的。

当然,想做好的 SRE 真的也不容易,不管是技术还是认知,都需要花大力气不断学习,不断成长。

大家一起加油!

参考资料:

这里有一份 golangci-lint 的最佳配置实践

· 阅读需 5 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

TD;DR: 配置在这里: https://github.com/qiniu/reviewbot/blob/main/.golangci.yml

为什么会有这份配置呢?

提到 go 领域的静态检查,除了 go 官方提供的 go vet 之外,大家一般都会选择使用 golangci-lint,因为这个工具集合了当前 go 领域的几乎所有主流的 lint 工具。且使用也相对简单。不光在命令行场景下可以直接使用,在各种 CI 场景中,也都有方便的支持。

但就像我在之前文章中提到的,golangci-lint 的 linter 和配置项非常多,且很多配置项的默认值,并不是最佳实践。所以,在实际使用中,大家往往需要根据自身项目的情况,进行针对性的配置。

但当你想启用更多的 linter 时,你可能随之会发现 "golangci-lint 官方" 好像并没有给出一份最佳实践的配置。

咦,这是为什么呢?

大概率有两方面原因:

  • 工具定位,golangci-lint 是 linter 聚合器。所以其必然不能有太过明确的倾向,不然其他的 linter 作者哪还有动力往 golangci-lint 里添加新的 linter,对吧?

  • 静态分析问题,难就难在权衡 False Positive(误报) 和 False Negative(漏报) 的问题。所谓鱼和熊掌不可兼得,所有的 linter 的实现者,都需要做抉择。如果一个 linter 过于严格,则可能会导致误报变多,影响开发体验。反之,如果一个 linter 过于宽松,则可能会检测不出问题,那这个 linter 也就失去了意义。

另外就是,不同的项目,其关注点可能也会不同,所以比较难有通用的最佳实践。

不过呢, qiniu/reviewbot 这边,经过调研和实践后,还是总结出了一份配置,当然仅供参考。

这份配置的严肃性

这份配置是怎么来的呢?

  • 首先参考了一个关注比较多的配置实践。

https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322

  • 然后,我将 golangci-lint 集成的所有 linter,按其 star 数排序,重点选取了 100+ star 的 linter,并参考了其配置。

  • 然后又参考了几个著名公司/项目的配置。

https://github.com/golangci/golangci-lint/blob/master/.golangci.yml

https://github.com/golangci/golangci-lint/blob/master/.golangci.yml

  • 最后,就是基于七牛内部实践和倾向,形成的一份配置。
    • 比如安全相关的 lint,我们倾向于严格一些。

如此,这份配置就形成了。

当然,这份配置并不适合所有项目,但它可以作为你项目的初始配置,然后根据项目的实际情况,进行必要调整。

实际上配置项的选择上,难就难在挺难权衡 False Positive(误报) 和 False Negative(漏报) 的问题。

因为很多 linter 的实现,并没有那么精准,误报是很常见的。

所以这就会导致,筛选的严格意味着可用的 linter 会变少,就有可能漏掉一些问题。

而筛选的宽松,启用太多的 linter,则会导致误报变多,影响开发体验。

所以通常大家需要做的是在 False Positive(误报) 和 False Negative(漏报) 之间找到一个平衡点,让其适合你的项目。

当然,并不是说这是一劳永逸的,随着项目的发展,你可能会发现一些新的问题,或者一些老的问题,可能需要调整。

同样,社区的 linter 也在不断发展,新的 linter 也在不断加入,所以这份配置也需要不断的更新。

感谢阅读。

Reviewbot 开源 | 这些写 Go 代码的小技巧,你都知道吗?

· 阅读需 10 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

Reviewbot 是七牛云开源的一个项目,旨在提供一个自托管的代码审查服务, 方便做 code review/静态检查, 以及自定义工程规范的落地。


自从上了 Reviewbot 之后,我发现有些 lint 错误,还是很容易出现的。比如

dao/files_dao.go:119:2: `if state.Valid() || !start.IsZero() || !end.IsZero()` has complex nested blocks (complexity: 6) (nestif)
cognitive complexity 33 of func (*GitlabProvider).Report is high (> 30) (gocognit)

这两个检查,都是圈复杂度相关。

圈复杂度(Cyclomatic complexity)是由 Thomas McCabe 提出的一种度量代码复杂性的指标,用于计算程序中线性独立路径的数量。它通过统计程序控制流中的判定节点(如 if、for、while、switch、&&、|| 等)来计算。圈复杂度越高,表示代码路径越多,测试和维护的难度也就越大。

圈复杂度高的代码,往往意味着代码的可读性和可维护性差,非常容易出 bug。

为什么这么说呢?其实就跟人脑处理信息一样,一件事情弯弯曲曲十八绕,当然容易让人晕。

所以从工程实践角度,我们希望代码的圈复杂度不能太高,毕竟绝大部分代码不是一次性的,是需要人来维护的。

那该怎么做呢?

这里我首先推荐一个简单有效的方法:Early return

Early return - 逻辑展平,减少嵌套

Early return, 也就是提前返回,是我个人认为最简单,日常很多新手同学容易忽视的方法。

举个例子:

func validate(data *Data) error {
if data != nil {
if data.Field != "" {
if checkField(data.Field) {
return nil
}
}
}
return errors.New("invalid data")
}

这段代码的逻辑应该挺简单的,但嵌套层级有点多,如果以后再复杂一点,就容易出错。

这种情况就可以使用 early return 模式改写,把这个嵌套展平:

func validate(data *Data) error {
if data == nil {
return errors.New("data is nil")
}
if data.Field == "" {
return errors.New("field is empty")
}
if !checkField(data.Field) {
return errors.New("field validation failed")
}
return nil
}

是不是清晰很多,看着舒服多了?

记住这里的诀窍:如果你觉得顺向思维写出的代码有点绕,且嵌套过多的话,就可以考虑使用 early return 来反向展平。

当然,严格意义上讲,early return 只能算是一种小技巧。要想写出高质量的代码,最重要的还是理解 分层、组合、单一职责、高内聚低耦合、SOLID 原则等 这些核心设计理念 和 设计模式了。

Functional Options 模式 - 参数解耦

来看一个场景: 方法参数很多,怎么办?

比如这种:

func (s *Service) DoSomething(ctx context.Context, a, b, c, d int) error {
// ...
}

有一堆参数,而且还是同类型的。如果在调用时,一不小心写错了参数位置,就很麻烦,因为编译器并不能检查出来。

当然,即使不是同类型的,参数多了可能看着也不舒服。

怎么解决?

这种情况,可以选择将参数封装成一个结构体,这样在使用时就会方便很多。封装成结构体后还有一个好处,就是以后增删参数时(结构体的属性),方法签名不需要修改。避免了以前需要改方法签名时,调用方也需要跟着到处改的麻烦。

不过,在 Go 语言中,还有一种更优雅的解决方案,那就是Functional Options 模式

不管是 Rob Pike 还是 Dave Cheney 以及 uber 的 go guides 中都有专门的推荐。

这种模式,本质上就是利用了闭包的特性,将参数封装成一个匿名函数,有诸多妙用。

Reviewbot 自身的代码中,就有相关的使用场景(https://github.com/qiniu/reviewbot/blob/c354fde07c5d8e4a51ddc8d763a2fac53c3e13f6/internal/lint/providergithub.go#L263),比如:

// GithubProviderOption allows customizing the provider creation.
type GithubProviderOption func(*GithubProvider)
func NewGithubProvider(ctx context.Context, githubClient *github.Client, pullRequestEvent github.PullRequestEvent, options ...GithubProviderOption) (*GithubProvider, error) {
// ...
for _, option := range options {
option(p)
}
// ...
if p.PullRequestChangedFiles == nil {
// call github api to get changed files
}
// ...
}

这里的 options 就是 functional options 模式,可以灵活地传入不同的参数。

当时之所以选择这种写法,一个重要的原因是方便单测书写。

为什么这么说呢?

看上述代码能知道,它需要调用 github api 去获取 changed files, 这种实际依赖外部的场景,在单测时就很麻烦。但是,我们用了 functional options 模式之后,就可以通过 p.PullRequestChangedFiles 是否为 nil 这个条件,灵活的绕过这个问题。

Functional Options 模式的优点还有很多,总结来讲(from dave.cheney):

  • Functional options let you write APIs that can grow over time.
  • They enable the default use case to be the simplest.
  • They provide meaningful configuration parameters.
  • Finally they give you access to the entire power of the language to initialize complex values.

现在大模型相关的代码,能看到很多 functional options 的影子。比如 https://github.com/tmc/langchaingo/blob/238d1c713de3ca983e8f6066af6b9080c9b0e088/llms/ollama/options.go#L25

type Option func(*options)
// WithModel Set the model to use.
func WithModel(model string) Option {
return func(opts *options) {
opts.model = model
}
}
// WithFormat Sets the Ollama output format (currently Ollama only supports "json").
func WithFormat(format string) Option {
// ...
}
// If not set, the model will stay loaded for 5 minutes by default
func WithKeepAlive(keepAlive string) Option {
// ...
}

所以建议大家在日常写代码时,也多有意识的尝试下。

善用 Builder 模式/策略模式/工厂模式,消弭复杂 if-else

Reviewbot 目前已支持两种 provider(github 和 gitlab),以后可能还会支持更多。

而因为不同的 Provider 其鉴权方式还可能不一样,比如:

  • github 目前支持 Github APP 和 Personal Access Token 两种方式
  • gitlab 目前仅支持 Personal Access Token 方式

当然,还有 OAuth2 方式,后面 reviewbot 也也会考虑支持。

那这里就有一个问题,比如在 clone 代码时,该使用哪种方式?代码该怎么写?使用 token 的话,还有个 token 过期/刷新的问题,等等。

如果使用 if-else 模式来实现,代码就会变得很复杂,可读性较差。类似这种:

if provider == "github" {
// 使用 Github APP 方式
if githubClient.AppID != "" && githubClient.AppPrivateKey != "" {
// 使用 Github APP 方式
// 可能需要调用 github api 获取 token
} else if githubClient.PersonalAccessToken != "" {
// 使用 Personal Access Token 方式
// 可能需要调用 github api 获取 token
} else {
return err
}
} else if provider == "gitlab" {
// 使用 Personal Access Token 方式
if gitlabClient.PersonalAccessToken != "" {
// 使用 Personal Access Token 方式
// 可能需要调用 gitlab api 获取 token
} else {
return errors.New("gitlab personal access token is required")
}
}

但现在 Reviewbot 的代码中,相关代码仅两行:

func (s *Server) handleSingleRef(ctx context.Context, ref config.Refs, org, repo string, platform config.Platform, installationID int64, num int, provider lint.Provider) error {
// ...
gb := s.newGitConfigBuilder(ref.Org, ref.Repo, platform, installationID, provider)
if err := gb.configureGitAuth(&opt); err != nil {
return fmt.Errorf("failed to configure git auth: %w", err)
}
// ...
}

怎么做到的呢?

其实是使用了 builder 模式,将 git 的配置和创建过程封装成一个 builder,然后根据不同的 provider 选择不同的 builder,从而消弭了复杂的 if-else 逻辑。

当然内部细节还很多,不过核心思想都是将复杂的逻辑封装起来,在主交互逻辑中,只暴露简单的使用接口,这样代码的可读性和可维护性就会大大提高。

最后

到底如何写出高质量的代码呢?这可能是很多有追求的工程师,一直在思考的问题。

在我看来,可能是没有标准答案的。不过呢,知道一些技巧,并能在实战中灵活运用,总归是好的。

你说是吧?

Reviewbot 开源 | 有些 git commit 记录真的不敢恭维, 我推荐每位工程师都常用 git rebase 和 git commit --amend

· 阅读需 4 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

Reviewbot 是七牛云开源的一个项目,旨在提供一个自托管的代码审查服务, 方便做 code review/静态检查, 以及自定义工程规范的落地。


在日常的编程协作中,Git commit 记录的质量往往反映了一个工程师的工程素养。然而,我经常能看到一些不太规范的 commit 记录。有时,真的不敢恭维。

比如这种:

这种大概率是提交 commit 之后,又有变动,就随手重新复用上一条 git commit 命令了。

这种记录如果出现在个人仓库,可能还好. 但如果是多人协作的仓库,就有点不专业了。

在我看来,这些 commit 记录完全没必要,是非常不好的习惯,完全可以避免。

好在 Git 为我们提供了优雅的解决方案。如果没必要生成新的 commit,那直接使用 git commit --amend 就可以避免。

git commit amend

少用 git merge 多用 git rebase

比如这种:

Merge branch 'feature-A' of https://github.com/qiniu/reviewbot into feature-B

说的是把远程分支 feature-A 的代码合并到 feature-B 里。这里的 feature-A 通常是主分支。

这种 Commit 信息如果出现在你的 PR 里,那是完全没必要。PR 里的 commit 信息应当仅包含针对本次改动的有用信息。

我个人日常几乎不使用 git merge,即使是为了同步远程分支,我一般都会使用 git rebase

比如:

git rebase

git rebase 除了上述好处外,还可以保持主仓库的 commit history 非常干净。所以强烈推荐大家使用。

Reviewbot 的 git commit check

为了更好的规范上述两种行为,Reviewbot 也添加了 git commit check 能力,就是用来检查 git commit 记录是否符合规范的。

如果不符合规范,Reviewbot 就会提示你:

git commit check

更多 git flow 使用规范和技巧

当然 git 操作其实有很多实用技巧,建议大家有兴趣的话可以去研究下。我在 1024 实训营的时候,有给同学们做个相关分享:

超实用! 从使用视角的 Git 协作实战,告别死记硬背

文档里面有视频链接,感兴趣的同学可以去看下。

最后,作为专业的工程师,我们应该始终追求卓越的工程实践。良好的 commit 记录不仅体现了个人的专业素养,更是提升团队协作效率的重要基石。

通过合理使用 git rebase 和 git commit --amend,我们可以维护一个更清晰、更专业的代码提交历史。这不仅让代码审查变得更加轻松,也为后续的代码维护和问题追踪带来极大便利。

你觉得呢?

Reviewbot 开源 | 为什么我们要打造自己的代码审查服务?

· 阅读需 10 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

Reviewbot 是七牛云开源的一个项目,旨在提供一个自托管的代码审查服务, 方便做 code review/静态检查, 以及自定义工程规范的落地。


静态检查不是个新鲜事。

我记得早在几年前,我们就调研并使用过 sonarqube 做静态检查,但当时并没有大范围的推广。主要原因在于,一是发现的问题多数是风格问题,较少能发现缺陷; 二是 sonarqube 社区版的 worker 数有限制,满足不了我们大规模代码扫描的需求。当然,也是因为前一个问题,感觉付费并不是很划算。

而由于七牛主要使用 golang 语言,所以在静态检查方面,我们基本使用 go vet/fmt/lint 等,再就是后来的 golangci-lint,好像也够用了。

但随着代码仓库的增多,以及对工程规范的不断强化,我们越来越发现当前的落地方式,已经开始无法满足我们的需求。

Linter 工具的引入与更新问题

以 golangci-lint 为例,它是 go 语言 linters 聚合器,内置了 100+ linters,使用也很简单, golangci-lint run 一条命令即可。但你知道吗?如果没有特殊配置,你这条命令其实仅仅执行其中的 6 个 linter,绝大部分 linters 都没有执行!

另外,工具本身需要更新,且很多时候我们也会自己定制 linter 工具,这种时候该怎么做呢?如果仅有少量仓库,可能还好,但如果仓库很多,那维护成本就上去了。

还有就是新业务,新仓库,如何保证相关的检查能够及时配置,相关的规范能够正确落地?

靠自觉一定是不行的。

Linter 问题的发现与修复情况

如何确保发现的问题能够被及时修复?如何让问题能更及时、更容易的被修复?

埋藏在大量 raw log 中的问题,一定是不容易被发现的,查找起来很麻烦,体验很差。

历史代码仓库的存量问题,谁来改?改动就需要时间,但实际上很多业务研发可能并没有动力来跟进。同样,变动总是有风险的,有些 lint 问题修复起来也并不简单,如果因修复 lint 问题而引入风险,那就得不偿失了。

如果想了解当前组织内 lint 问题的分布及修复情况,又该怎么办呢?

如何解决,方向在哪里?

不可否认,linter 问题也是问题,如果每行代码都能进行充分的 lint 检查,那一定比不检查要强。

另一方面,组织内制定好的工程规范,落地在日常的开发流程中,那一定是希望被遵守的,这类就是强需。

所以这个事情值得做,但做的方式是值得思考的,尤其是当我们有更高追求时。

参考 CodeCov 的服务方式,以及 golangci-lint reviewdog 等工具的设计理念,我们认为:

  • 如果能对于新增仓库、历史仓库,不需要专人配置 job,就能自动生效,那一定是优雅的
  • 如果能只针对 PR/MR 中的变动做分析和反馈,类似我们做 Code Review 那样,那对于提 PR 的同学来讲一定是优雅的,可接受的,随手修复的可能性极大
    • 而进一步,针对 PR/MR 中涉及的文件中的历史代码进行反馈,在合理推动下,支持夹带修改,持续改进的可能性也会大大增强
  • Lint 工具多种多样,或者我们自己开发出新工具时,能够较为轻松的让所有仓库都自动生效,那也一定是非常赞的,不然就可能陷入工具越多负担越重的风险

基于上面的思考,我认为我们需要的是: 一个中心化的 Code Review/静态检查服务,它能自动接受整个组织内 PR/MR 事件,然后执行各种预定义的检查,并给与精确到变动代码行的有效反馈。它要能作为代码门禁,持续的保障入库代码质量。

Reviewbot 就是这样一个项目。

Reviewbot 在设计和实现上有哪些特点?

面向改进的反馈方式

这将是 Reviewbot 反馈问题的核心方式,它会尽可能充分利用各 Git 平台的自身能力,精确到变动的代码行,提供最佳的反馈体验。

  • Github Check Run (Annotations) github-check-run
  • Github Pull Request Review (Comments) github-pr-review-comments

支持多种 Runner

Reviewbot 是自托管的服务,推荐大家在企业内自行部署,这样对私有代码更友好。

Reviewbot 自身更像个管理服务,不限制部署方式。而对于任务的执行,它支持多种 Runner,以满足不同的需求。比如:

  • 不同的仓库和 linter 工具,可能需要不同的基础环境,这时候你就可以将相关的环境做成 docker 镜像,直接通过 docker 来执行
  • 而当任务较多时,为了执行效率,也可以选择通过 kubernetes 集群来执行任务。

使用也很简单,在配置文件中的目标仓库指定即可。类似:

dockerAsRunner:
image: "aslan-spock-register.qiniu.io/reviewbot/base:go1.22.3-gocilint.1.59.1"
kubernetesAsRunner:
image: "aslan-spock-register.qiniu.io/reviewbot/base:go1.23.2-gocilint.1.61.0"
namespace: "reviewbot"

零配置+定制化

本质上,Reviewbot 也是个 webhook 服务,所以我们只需要在 git provider 平台配置好 Reviewbot 的回调地址即可(github 也可以是 Github App)。

绝大部分的 linter 的默认最佳执行姿势都已经固定到代码中,如无特殊,不需要额外配置就能对所有仓库生效。

而如果仓库需要特殊对待,那就可以通过配置来调整。

类似:

org/repo:
linters:
golangci-lint:
enable: true
dockerAsRunner:
image: "aslan-spock-register.qiniu.io/reviewbot/base:go1.22.3-gocilint.1.59.1"
command:
- "/bin/sh"
- "-c"
- "--"
args:
- |
source env.sh
export GO111MODULE=auto
go mod tidy
golangci-lint run --timeout=10m0s --allow-parallel-runners=true --print-issued-lines=false --out-format=line-number >> $ARTIFACT/lint.log 2>&1

可观察

Reviewbot 是在对工程规范强管理的背景下产生的,那作为工程规范的推动方,我们自然有需求想了解组织内当前规范的执行情况。比如, 都有哪些问题被检出?哪些仓库的问题最多?哪些仓库需要特殊配置?

目前 Reviewbot 支持通过企业微信来接收通知,比如:

  • 检出有效问题

found-valid-issue

  • 遇到错误

found-unexpected-issue

当然,未来可能也会支持更多方式。

其他更多的功能和姿势,请参考仓库: https://github.com/qiniu/reviewbot

Reviewbot 的未来规划

作为开源项目,Reviewbot 还需要解决很多可用性和易用性问题,以提升用户体验,比如最典型的,接入更多的 git provider(gitlab/gitee 等),支持 CLI 模式运行。

但我个人认为,作为 code review 服务,提供更多的检测能力,才是重中之重。因为这不光是行业需求,也是我们自身需要。

所以后面我们除了会引入七牛内部推荐的规范,也会调研和探索更多的行业工具,同时会考虑引入 AI,探索 AI 在 code review 中的应用等等。

Anyway,Reviewbot 还很年轻,我们在持续的改进中,非常欢迎大家试用并提出宝贵意见。当然,更欢迎大家一起参与到项目建设中来。

为了方便沟通,我建了 微信 和 QQ 群,欢迎大家扫码加入,一起交流。

reviewbot-wechat-group reviewbot-qq-group

感谢大家。

Reviewbot - Boost Your Code Quality with Self-Hosted Automated Analysis and Review

· 阅读需 3 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

Looking to build a self-hosted code review service? Try Reviewbot, now open-sourced!

When do you need a self-hosted code review service?

You might need one when:

  • You have many repos but still want tight control over code quality
  • Your repos are private, and commercial services seem overkill
  • You want to continuously improve the process and rules, with full customization

Benefits of a self-hosted code review service

While many linter tools and engineering practices exist, they're often underutilized:

  • Powerful tools like golangci-lint (with 100+ integrated linters) are often used with default settings, ignoring most features
  • Linter outputs get buried in logs, making issue-finding a chore
  • Configuring CLI-based linters for multiple repos is tedious, especially for ongoing improvements
  • Monitoring code quality across repos can be daunting

A self-hosted service can automate all this. As a DevOps or QA team member, you can easily centralize control, monitoring, and customization of code quality across all repos.

Enter Reviewbot - your solution for self-hosted code review.

What can Reviewbot do?

Reviewbot helps you quickly set up a self-hosted code analysis and review service, supporting multiple languages and coding standards. It's perfect for organizations with numerous private repos.

Issues are reported during Pull Requests as Review Comments or Github Annotations, pinpointing exact code lines.

  • Github Check Run (Annotations)

    Github Check Run Github Check Run Annotations

  • Github Pull Request Review Comments

    Github Pull Request Review Comments

This approach saves PR authors from sifting through lengthy logs, streamlining problem-solving.

Reviewbot's Design Philosophy

Focused on:

  • Security - Self-hosting for data control
  • Improvement-Oriented - Issues reported as Review Comments or Github Annotations for easy fixes
  • Flexibility - Multi-language support with easy tool integration
  • Observability - Alert notifications for timely issue awareness
  • Configurable - Customizable linter commands, parameters, and environments

Built with Golang, Reviewbot boasts simple logic and clear code for easy maintenance.

Main Flow

Reviewbot primarily operates as a GitHub Webhook/App service, accepting GitHub Events, executing various checks, and providing precise feedback on the corresponding code if issues are detected.

Github Event -> Reviewbot -> Execute Linter -> Provide Feedback

And you can easily Add a New Linter or do Customization.

Monitoring Detection Results

Reviewbot supports notification of detection results through WeWork (企业微信) alerts.

found valid issue

If unexpected output is encountered, notifications will also be sent, like this:

found unexpected issue

More

Check out Reviewbot. Feel free to have a try.

Btw, this article is also published on medium website.

Stay tuned for more updates!

聊聊测试开发工程师的职责定位问题

· 阅读需 6 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

网上有人会把测开定位成为 测试工具开发,主要是开发自动化测试工具或平台,用以帮助手动验收的同学提升效率。存在即合理,确实有一些团队或组织是这样建设的。但作为行业从业者,我们也应该认识到,这样是不全面的,有误导之嫌。

现实中的绝大部分测开还是定位在 保障业务迭代质量 上,因为这才是业务最需要的部分,也是最能直接产生价值的部分。而工具开发,属于 提效 范畴,当然也需要,但其紧迫性以及价值会因企业而不同,也会与企业所属的发展阶段息息相关。所以很多时候,工具具体能提效多少,都需要工具开发者能够想清楚,甚至要能显式证明。

也许大家会觉得,保障业务质量招聘测试就可以了啊,为啥要招聘测开呢?其实这是把质量保障这件事想 Low 了,从 最终服务于客户的质量 出发,要想做好质量保障真没那么容易。很多质量领域的事情,都不是单纯的测试能搞定的,但又是客户必须的,比如速度,可靠性等。测试仅仅只是质量保障的手段之一,还有很多其他的手段也仍然有效。就拿架构评审,代码 Review,发布审核等事项来说,要想参与无疑都需要质量同学对技术有更深的认知。笔者最近也在尝试通过发布审核来把控全局质量,所以也非常想看到有更多的质量同学往这方面发展,大家能彼此探讨。

不过,毕竟时代局限性仍在,从事质量领域的同学还是以测试为主。所以,此时如果企业想招聘一位技术比较好的测试同学,她一般 JD 都会选择叫 "测试开发工程师",因为这样能更好的匹配预期。要说明的是,在企业内部其实叫什么真的不重要,也没太有人关心,企业看中的始终是价值产出。私下里,我其实更喜欢 QA(Quality Assurance)这个称呼.因为我会觉得QA 包含 Ownership 意味,寓意我在负责某件事。而"测开"的叫法感觉跟"开发"一样,字面上更像个执行者,总是欠缺了那么一点主观能动性。

另外,当团队里所有的 QA 同学都有不错的开发水平时,你会发现,很多常见的"测开工具" "测开平台"需求就会显得不是那么刚需,甚至可能没必要。比如,相比通过 Web 或者 Excel 管理用例,直接用代码+Github 管理对经常写代码的同学来说可能更自然;执行用例也是,通过命令行,或者 Jenkins/Prow,也很方便;另外,Postman 可能会用的很少,因为对于经常与服务器打交道的同学,使用 curl 会更方便,其也基本够用;甚或者想搞个简单的压测,用系统自带的 AB 工具(Apache benchmarking tool)随手就完成了。凡此种种,笔笔皆是。

说到这里,有同学可能会问: 测开同学除了测试任务,就没有提效的要求吗?会不会大材小用?当然不是,在这种情况下,一些提效需求反而可能更具挑战性。因为这时候的提效目标就不是单指 QA 了,而是要面向全体工程师,甚至后者优先级更高,毕竟群体规模越大,投入产出比越高。另外就是需求越往上,需求的边界与传统的测试会越模糊,与业务越直接。比如发布灰度、质量运营、监测打点等等,这些系统都与质量有关,测开同学有能力的话当然也可以上。

总结来讲,对于测试开发的职责定位,我始终认为应该聚焦在 保障业务迭代质量,提升迭代效率 上,这点不应该变。而更具体的做法我会倾向于宣导:

  • 做业务的质检者, 关注检出率、漏出率,把控全局质量,为企业把好生产交付关。
  • 做工程研发专家, 保障业务迭代规范,加速迭代效率,关注软件工程技术的研发角色。

好像看起来挺难的,但不难的话又如何进步?

Go1.20 新版覆盖率方案解读

· 阅读需 9 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

玩过 Go 覆盖率的同学当有所了解,Go 的覆盖率方案最初的设计目标仅是针对单测场景,导致其局限性很大。而为了适配更多的场景,行业内各种博客、插件、黑科技介绍也层出不穷。当然,过去我们也开源过 Go 系统测试覆盖率收集利器 - goc,算其中比较完善,比较系统的了。且从用户使用角度来看,goc 也确实解决了行业内很多同学的痛点。

而现在,Go 官方终于开始正式这个问题了。作者Than McIntosh 于今年 3 月份提出了新的覆盖率提案,且看当前实现进度,最快 Go1.20 我们就能体验到这个能力,非常赞。

基于作者的 Proposal,我们先来看看这个提案细节。

新姿势: go build -cover

需要明确的是,本次提案不会改变原来的使用姿势go test -cover,而是新增go build -cover使用入口。从这一变化我们不难看出,新提案主要瞄准的是 "针对程序级的覆盖率收集" ,而旧版的实际是 "仅针对包级别的覆盖率收集" ,二者设计目标有明显的差别。

在新姿势下,使用流程大体是:

$ go build -o myapp.exe -cover ...
$ mkdir /tmp/mycovdata
$ export GOCOVERDIR=/tmp/mycovdata
$ <run test suite, resulting in multiple invocations of myapp.exe>
$ go tool covdata [command]

整体逻辑也比较清晰:

  1. 先编译出一个经过插桩的被测程序
  2. 配置好覆盖率输出的路径,然后执行被测程序。到这一步程序本身就会自动的输出覆盖率结果到上述路径了
  3. 通过 go tool covdata 来处理覆盖率结果

这里的子命令 covdata 是新引入的工具。而之所需要新工具,主要还是在新提案下,输出的覆盖率文件格式与原来的已有较大的差别。

新版覆盖率格式

先来看旧版的覆盖率结果:

  mode: set
cov-example/p/p.go:5.26,8.12 2 1
cov-example/p/p.go:11.2,11.27 1 1
cov-example/p/p.go:8.12,10.3 1 1
cov-example/p/p.go:14.27,20.2 5 1

大家当比较熟悉,其是文本格式,简单易懂。

每一行的基本语义为 "文件:起始行.起始列,结束行.结束列 该基本块中的语句数量 该基本块被执行到的次数"

但缺点也明显,就是 "浪费空间". 比如文件路径 cov-example/p/p.go, 相比后面的 counter 数据,重复了多次,且在通常的 profile 文件,这块占比很大。

新提案在这个方向上做了不少文章,实现细节上稍显复杂,但方向较为清晰。

通过分析旧版的每一行能看出,本质上每一行会记录两类信息,一是定位每个基本块的具体物理位置,二是记录这个基本块的语句数量和被执行的次数。虽然执行的次数会变化,但是其他的信息是不变的,所以全局上其实只要记录一份这样的信息就好,而这就能大大的优化空间,

所以,新版覆盖率它实际会实际输出两份文件,一份就是 meta-data 信息,用于定位这个被测程序所有包、方法等元信息,另一份才是 counters,类似下面:

➜  tmp git:(master) ✗ ls -l
total 1280
-rw-r--r-- 1 jicarl staff 14144 Nov 28 17:02 covcounters.4d1584597702552623f460d5e2fdff27.8120.1669626144328186000
-rw-r--r-- 1 jicarl staff 635326 Nov 28 17:02 covmeta.4d1584597702552623f460d5e2fdff27

这两份文件都是二进制格式,并不能直观的读取。但是借助covdata工具,可以轻松转化为旧版格式,比较优雅。类似:

go tool covdata textfmt -i=tmp -o=covdata.txt

ps: tmp 是覆盖率文件所在目录。

真 • 全量覆盖率

一个标准的 go 程序,基本上由三种类型的代码包组成:

  • 自身代码
  • 第三方包,通过 mod 或者 vendor 机制引用
  • go 标准库

在过去,几乎所有的工具都只关注业务自身代码的插桩,鲜少关注第三方包,更别说 go 官方标准库了。这在大部分场景下是没问题的,但有时有些场景也有例外,比如 SDK 相关的项目。因为这时候 SDK 会作为 Dependency 引入,要想对其插桩就需要额外的开发量。还比如一些 CLI 程序,执行完命令之后,立马就结束了,也是非常不利于覆盖率收集的。

这些问题都是很现实的,且我们在 goc 项目中也收到过真实的用户反馈:

不过,现在好了,新版覆盖率方案也有实际考虑到这些需求,它实际会做到 支持全量插桩+程序退出时主动输出覆盖率结果 的原生方式,非常值得期待。

更多覆盖率使用场景支持: 合并(merge)、删减(subtract)、交集(intersect)

在实际处理覆盖率结果时,有很多实用的场景,在新提案中也有提及,比如支持:

  • 合并多次覆盖率结果 go tool covdata merge -i=<directories> -o=<dir>
  • 删减已经覆盖的部分 go tool covdata subtract -i=dir1,dir2 -o=<dir>
  • 得到两份结果的交集 go tool covdata intersect -i=dir1,dir2 -o=<dir>

在过去,这些场景都需要依赖第三方工具才行,而在新方案中已经无限接近开箱即用了。

不过更复杂的场景,类似远程获得覆盖率结果等(类似 goc 支持的场景),看起来新方案并没有原生支持。这个问题,笔者也在 issue 讨论中提出,看看作者是否后续有解答。

展望与不足

值得注意的是新提案的实现是通过 源码插桩+编译器支持 的方式来混合实现的,与原来go test -cover 纯源码改写的方式有了较大的变化。

另外作者提到的 test "origin" queries 功能还是非常让我兴奋的,因为有了它,若想建立 测试用例到源码的映射 会变得简单很多,甚至更进一步的 精准测试,也变的更有想象空间。不过这个功能不会在 Go1.20 里出现,只能期待以后了。

作者还提到了一些其他的限制和将来可能的改进,比如 Intra-line coverage, Function-level coverage, Taking into account panic paths 等,感兴趣的同学可以自行去 Proposal 文档查看。

聊聊如何让办公网络直连Kubernetes集群PodIP/ClusterIP/Service DNS等

· 阅读需 9 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

想象一下,如果您日常使用的研发测试 Kubernetes 集群,能够有以下效果:

  • 在办公网络下直接访问 Pod IP
  • 在办公网络下直接访问 Service Cluster IP
  • 在办公网络下直接访问集群内部域名,类似 service.namespace.svc.cluster.local

会不会很方便,很优雅?

笔者近期就给内部的一个新集群做过类似的调整,特此分享一些心得。

PS: 这里的 直接访问/直连 指的是不借助 Ingress/hostnetwork:true/NodePort 等常规方式,直接访问 k8s 内部 IP or DNS,起到 网络拉平 的效果。

先决条件 - 三层路由方案

办公网段跟 Kubernetes 集群大概率是不同的网段,所以要想打通最自然的想法是依赖路由。相应的,Kubernetes 跨主机网络方案,我们最好也选择三层路由方案或者 Host-GW,而非 Overlay,不然数据包在封包解包过程中可能会失去路由方向。

我们的集群选用的是 Calico,且关闭了 IPIP 模式。具体的 IPPool 配置如下:

-> calicoctl get IPPool -o yaml
apiVersion: projectcalico.org/v3
items:
- apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
name: default-pool
spec:
blockSize: 24
cidr: 10.233.64.0/18
# 关闭IPIP模式
ipipMode: Never
natOutgoing: true
nodeSelector: all()
vxlanMode: Never
kind: IPPoolList

Calico RR(Route Reflectors)or Full-Mesh 模式?

网上的很多类似教程,上来都会引导大家先把集群改为 RR 模式,其实这不是必须的。大家可以思考下,RR 模式解决的问题是什么?是为了防止所有节点间都做 BGP 连接交换,浪费资源。但如果你的集群很小, 且已经是按 Full Mesh 模式部署了,到也没必要非得改为 RR 模式。Full Mesh 下所有的节点都是类似 RR 节点的效果,所以如果我们想选择作为 BGPPeer 交换的节点,选择任意节点就行。 比如,笔者的集群就选择了 Ingress 所在的节点,作为 BGPPeer。

~ calicoctl get BGPPeer -o yaml
apiVersion: projectcalico.org/v3
items:
- apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
name: peer-switch
spec:
# 交换机配置
asNumber: 65200
peerIP: 10.200.20.254
# 这个label是Ingress节点特有的
nodeSelector: node-role.kubernetes.io/ingress == 'ingress'
kind: BGPPeerList

从集群外部访问 Pod IP vs 从集群内部访问?

这个问题很关键,如果我们想从外部直接访问到集群内部的 Pod IP,那么首先需要搞清楚集群内的节点是如何畅通访问的。

以下面的节点为例,我们来看它的路由信息:

~ ip r
# 默认路由
default via 10.200.20.21 dev bond0 onlink
# 宿主机数据包路由
10.200.20.0/24 dev bond0 proto kernel scope link src 10.200.20.105
# 黑洞,防止成环
blackhole 10.233.98.0/24 proto bird
# 目的地址是10.233.98.3的数据包,走cali9832424c93e网卡
10.233.98.3 dev cali9832424c93e scope link
# 目的地址是10.233.98.4的数据包,走cali4f5c6d27f17网卡
10.233.98.4 dev cali4f5c6d27f17 scope link
# 目的地址是10.233.98.8的数据包,走cali8f10abc672f网卡
10.233.98.8 dev cali8f10abc672f scope link
# 目的地址是10.233.110.0/24网段的数据包,从bond0网卡出到下一跳10.200.20.107上
10.233.110.0/24 via 10.200.20.107 dev bond0 proto bird
# 目的地址是10.233.112.0/24网段的数据包,从bond0网卡出到下一跳10.200.20.106上
10.233.112.0/24 via 10.200.20.106 dev bond0 proto bird
# 目的地址是10.233.115.0/24网段的数据包,从bond0网卡出到下一跳10.200.20.108上
10.233.115.0/24 via 10.200.20.108 dev bond0 proto bird

相信看了笔者的注释,大家应该很容易了解到以下信息:

  • 这台宿主机 IP 是 10.200.20.105,集群内其他的宿主机还有 10.200.20.106, 10.200.20.107, 10.200.20.108 等
  • 主机 10.200.20.105 上的 Pod IP 段是 10.233.98.0/24, 10.200.20.106 上是 10.233.112.0/24,10.200.20.107 上是 10.233.110.0/24
  • 目的地址是 10.233.98.3 的数据包走 cali9832424c93e 网卡,目的地址 10.233.98.4 的数据包走 cali4f5c6d27f17 网卡等

而这些信息实际解答了,容器数据包的 出和入 这个关键问题:

  • 比如想访问 Pod IP 为 10.233.110.7 的容器,宿主机自然知道下一跳是 10.200.20.107 上
  • 比如接收到了目的地址是 10.233.98.8 的数据包,宿主机自然也知道要把这个包交给 cali8f10abc672f 网卡。而这个网卡是 veth pair 设备的一端,另一端必然在目标 Pod 里

那这些路由信息是哪里来的呢?自然是 Calico 借助 BGP 的能力实现的。我们进一步想,如果外部节点也有这些信息,是不是也就自然知道了 Pod IP 在哪里了? 答案确实如此,其实总结基于 Calico 的网络打平方案,核心原理就是 通过 BGP 能力,将集群路由信息广播给外部。

而在具体的配置上,就比较简单了,只需要在两端配置好 BGP Peer 即可。

  • 先是集群这一侧,前面笔者已给出:

    ~ calicoctl get BGPPeer -o yaml
    apiVersion: projectcalico.org/v3
    items:
    - apiVersion: projectcalico.org/v3
    kind: BGPPeer
    metadata:
    name: peer-switch
    spec:
    # 交换机配置
    asNumber: 65200
    peerIP: 10.200.20.254
    # 这个label就是Ingress节点特有的
    nodeSelector: node-role.kubernetes.io/ingress == 'ingress'
    kind: BGPPeerList
  • 再就是外部,一般是交换机,使用类似下面的命令:

    [SwitchC] bgp 64513       # 这是k8s集群的ASN
    [SwitchC-bgp] peer 10.200.20.107 as-number 64513
    [SwitchC-bgp] peer 10.200.20.108 as-number 64513

    PS: 具体的交换机操作方式可以参考各品牌交换机官方文档

到这里,基本上我们已经打通了外部直接访问 Pod IP 的能力。当然,如果您的办公网络到交换机这一侧还有多个网关,您还需要在这些网关上设置合适的路由才行。

为什么 Service Cluster IP 还不能访问?

也许这时候您会发现,可以直连 Pod IP,但 Cluster IP 不可以,这是为什么呢?原来,默认情况 Calico 并没有广播 Service IP,您可以在交换机这一侧通过查看交换过来的 IP 段来确认这一点。

PS: 您是否注意到,k8s 主机节点上也没有 service 的 ip 路由,但为啥在集群内部访问 service 没有问题呢?

解决方案也简单,只要打开相关的设置即可, 类似如下:


~ calicoctl get bgpconfig default -o yaml
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
name: default
spec:
asNumber: 64513
listenPort: 179
logSeverityScreen: Info
nodeToNodeMeshEnabled: true
# 这就是需要广播的service cluster IP 段
serviceClusterIPs:
- cidr: 10.233.0.0/18

打通内网 DNS,直接访问 Service 域名

直连 IP 虽然方便,但有时若想记住某服务的具体 IP 却不是那么容易。所以,我们将 K8s 内部的 DNS 域名也暴漏出来了,类似下面:

<service>.<namespaces>.svc.cluster.local

而这块的设置也相对简单,一般企业都有内网 DNS,只需要添加相应解析到 K8s 内部 DNS Server 即可。

总结

其实若想打造一个好用的研发测试集群,有很多的细节需要处理,笔者后续也会继续分享类似的经验,希望对大家有用。

参考链接