跳到主要内容

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 即可。

总结

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

参考链接

TopoLVM - 基于LVM的Kubernetes本地持久化方案,容量感知,动态创建PV,轻松使用本地磁盘

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

正文

研发测试场景下,一般追求的是一键快速起环境,横向动态复制,一人一套,随起随用,用完即走。作为使用方,其不用关心实际的物理资源是怎样的,环境起在哪里,只要声明自己的使用需求即可。但作为方案构建者以及 infrastructure 支撑,我们却深知,要想提供更好的解决方案,个中问题还有很多,且颇为不易。

比如在过去,笔者就曾一度困扰于如何优雅的放开本地物理盘给业务使用这个问题,尤其是本地 HDD 数据盘。

这里有个背景,我们的 Kubernetes 研发测试集群是用线上退下来的过保机器搭建,然后七牛又搞云存储,所以我们的机器中很多那种多盘位的存储密集型机器(比如挂 12 块 4T 的盘)。所以如何更好的利用这些磁盘,就是个问题。 方案之一是把这些盘组成网络存储,然后通过七牛自身的云服务或者 ceph 等系统提供出去,这当然是可行的。不过有些业务其实就想单纯使用物理盘,那该怎么办呢?

纵观 Kubernetes 目前原生提供的几种方案,笔者发现都不完美:

  • Emptydir 非持久化方案,Pod 删除,EmptyDir 也会被清空。另外 Emptydir 使用的是 rootfs 的存储空间,而这个空间有可能是放在在系统盘上的,所以对它的使用当慎之又慎。
  • HostPath 持久化方案,但安全风险很高,官方不推荐。另外,从使用角度也不方便。比如作为用户,你首先要清楚知道目标系统的情况,知道具体的盘符和目录位置然后才能在 HostPath 里正确引用。但现实是从集群安全角度,用户不一定有直接登录机器的权限。另外即使你的 Pod 占了某个宿主机的目录,也不能排除别人二次占用,或者误操作。所以 HostPath 的使用场景实际很受限。
  • Local Persistent Volume 持久化方案,以 PVC/PV 的形式来使用本地存储。想法很好,但是不能动态创建 PV,对于 PV 提供者来说,心智负担较高。当然社区现在也有提供 Local Static Provisioner,某种程度上简化了这块的心智负担,但是仍然需要预先规划目录或者分区,略显不足。

笔者以为,理想中的本地磁盘使用方案,应当是按需申请,空间隔离,且自动化生命周期管理。这样才能既方便终端用户使用,也能减少运维支撑,提高效率。这里的关键技术点有三个:

  • 按需申请 意味着最好以 PVC+StorageClass 的模式来提供服务,做到 PV 动态创建。而要实现这点,容量感知 是关键,因为若调度到空间不足的节点上很明显是不合理的。最好能结合 PVC 申请的容量+Node 上的剩余容量,综合选择最优的节点来做绑定。
  • 空间隔离 要确保用户申请的空间大小一定是足额的,不被侵占的。从这里看,单纯的把文件系统的目录用作 PV 但容量上彼此不隔离显然不合适。
  • 自动化生命周期管理 动态 Provisioning 是强需。

综合以上三点,我们会发现基于 LVM 或分区技术+CSI 的实现,当是比较符合上述用户体验的,而TopoLVM就是这样一个项目。

地址: https://github.com/topolvm/topolvm

TopoLVM 是基于 LVM 的 Kubernetes 本地化磁盘方案,以 CSI 形式提供给用户使用。目前主要支持以下功能:

  • 动态 Provisioning
  • 支持原生数据块卷(Raw Block Volume)
  • Volume 伸缩

整体架构如下:

值得注意的是,在早期版本,为了能够动态感知节点上的剩余存储容量,TopoLVM 设计了个自定义扩展调度器(上图 topolvm-scheduler 部分),方便在 Scheduling 阶段为 Pod 绑定合适的 Node。而在 Kubernetes 1.21 之后,Kubernete 已经原生支持了 Storage Capacity Tracking的能力,这块的实现整体就变的优雅很多,topolvm-Scheduler 也就不再需要了。

当然,要想认知到 TopoLVM 的核心原理,除了了解 CSI 编写规范外,最重要的就是需要了解 LVM 相关技术。而正是因为通过 LVM 能够动态创建 LV,动态扩缩容,TopoLVM 才能支持动态 Provisioning 相关的能力。

不过,虽然作为开源项目 TopoLVM 已基本够用,但丰富度略显不足。而博云近期也开源了他们的云原生本地磁盘管理方案 Carina,看起来更完善一些。

项目地址: https://github.com/carina-io/carina

Carina 除了提供基于 LVM 的方案外,还支持裸盘分区方式,以及 IOPS 限制等,功能更加丰富。代码组织规范也更贴合云原生社区的方式,整体非常值得一探。

参考链接

往期推荐

Linux Troubleshooting 超实用系列 - Disk Analysis

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

关于磁盘的使用,实际生产中以下问题会较为常见:

  • No space left on device - 空间不足
  • Disk utilization 100% - 磁盘 I/O 过载
  • Too many open files - 文件句柄过多
  • Input/output error - 读写错误

而掌握常见的分析套路会事半功倍。

Disk usage

第一时间明确磁盘容量及使用情况总是没错的,这时候df -h 命令就比较方便:

$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 48G 4.0K 48G 1% /dev
tmpfs 9.5G 8.55G 9.5G 90% /run
/dev/sda1 275G 234G 28G 90% /
/dev/sdd1 2.7T 1.6T 1.2T 57% /disk3
/dev/sdc1 3.6T 2.6T 1.1T 72% /disk1
/dev/sdb1 3.6T 4.2G 3.6T 1% /disk2

Use% 这个指标就比较清晰展示目标磁盘已经使用多少了。

注意,第三行的tmpfs文件系统比较特殊,其数据实际是存储在内存中而非磁盘。

Inode usage

有时候我们会发现明明磁盘有容量,但是程序仍然报No space left on device,这是因为什么呢?

答案大概率是 Inode 耗尽了。这时候可以通过df -i 来确认,比如:

$ df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
udev 12370103 518 12369585 1% /dev
tmpfs 12372788 611 12372177 1% /run
/dev/sda1 18317312 1941821 16375491 11% /
/dev/sdd1 183148544 181317058 1831468 99% /disk3
/dev/sdc1 244195328 153483 244041845 1% /disk1
/dev/sdb1 244195328 7496 244187832 1% /disk2

可以看到/disk3对应的目录其 Inode 已经使用 99%,很快就会耗尽。Inode 代表的是文件的 metadata 信息,若 inode 使用过多,通常意味着目录里小文件太多了。

PS: 不规范的容器化姿势比较容易出现这个问题,比如 Pod 一直在产生日志,且使用的是系统盘又不定期回收。

Disk utilization high

磁盘使用率高,一般是已经知道是哪个盘了,但如果不知道,使用iostat -x 1也能较清晰的查看到:

$ iostat -x 1
Linux 3.19.0-80-generic 2022年05月12日 _x86_64_ (24 CPU)

avg-cpu: %user %nice %system %iowait %steal %idle
5.85 0.00 3.60 4.83 0.00 85.72

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 237.00 441.00 48.00 56448.00 1388.00 236.55 0.97 1.98 1.02 10.83 1.00 48.80
sdb 0.00 26.00 2.00 186.00 8.00 93876.00 998.77 44.51 348.13 466.00 346.86 5.32 100.00
sdc 0.00 0.00 155.00 7.00 18132.00 16.00 224.05 6.62 47.95 46.71 75.43 4.02 65.20
sdd 0.00 30.00 8.00 8.00 900.00 212.00 139.00 0.10 6.25 3.50 9.00 6.00 9.60

PS: iostat -xd <device> 1 可以只查看某个设备。

可以看到 sdb 这块盘,其%util指标已经 100%。

但要注意,%util高并不严格意味着磁盘已经过载了,因为现代硬盘设备都有并行处理多个 I/O 请求的能力。要关注磁盘利用率,还需要关注await(再具体就是读r_await和写w_await指标),这个指标大致等于单个 I/O 所需的平均时间,所以如果它也很大,那磁盘一定是很繁忙了。

Which processes are using the specific disk?

实际场景中,面对磁盘负载高,我们通常需要做的是找到"罪魁祸首",判断其行为是否符合预期。

粗略的可以通过 iotop -oP 直接查看当前正在读写的进程。一般机器上有哪些程序,我们应该比较清楚,所以这时候可以大致判断出来:

$ iotop -oP
Total DISK READ : 173.26 M/s | Total DISK WRITE : 177.38 M/s
Actual DISK READ: 175.77 M/s | Actual DISK WRITE: 85.50 M/s
PID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
6929 be/4 root 168.67 M/s 168.57 M/s 0.00 % 76.51 % dd if=/dev/sda bs=4M count=100000 of=mbr.img
379 be/3 root 0.00 B/s 15.61 K/s 0.00 % 2.01 % [jbd2/sda1-8]

当然这种方式也存在一个问题,你是看不出目标进程具体使用哪块磁盘的。那怎么办呢?可以借助lsof +D <目录>命令,通过正在打开的文件句柄来识别进程:

$ lsof +D /disk2
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
prometheu 1705 root mem REG 8,17 72567556 234356807 /disk2/prometheus_dir/data/01G2J2YMJPY9HXMP5KSPW30MM1/chunks/000001
prometheu 1705 root mem REG 8,17 73620431 234356815 /disk2/prometheus_dir/data/01G19H692F7JN796CBQDSFVV1W/chunks/000001
prometheu 1705 root mem REG 8,17 73173252 234356814 /disk2/prometheus_dir/data/01G13QSNA21PYK2R6SC0BFYZYM/chunks/000001

然后通过pidstat -d 进一步分析这些进程的读写情况 :

$ pidstat -d
Linux 3.19.0-80-generic 2022年05月15日 _x86_64_(24 CPU)
16时21分37秒 UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
16时21分59秒 0 1705 64.00 67.19 0.00 prometheus

kB_rd/skB_wr/s 这两个指标,能基本代表进程读写磁盘的速度。

Too many open files

相信后端同学大多都遇到过Too may open files的错误,因为高并发场景下,服务会建立很多连接,这时候就会很容易遇到这个错误。

可以通过ls -1 /proc/<pid>/fd | wc -l命令来查看当前进程已经打开了多少个文件:

$ ls -1 /proc/1705/fd | wc -l
1258

而若想查看某进程具体的句柄限制,可以通过命令cat /proc/<pid>/limits:

$ cat /proc/1705/limits
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 386565 386565 processes
Max open files 20240 20240 files

而若要调整这个限制,可以通过ulimit命令或修改系统文件/etc/security/limits.conf.

EIO (input/output error)

遇到这个错误,一般是物理磁盘坏了。可能是整个盘坏了不能读写,也有可能是某个 block 有问题。这时候通过dmesg -T查看内核日志,通常会有相应的 error 信息。

参考资料

  1. http://linuxperf.com/?p=156
  2. http://linuxperf.com/?p=40
  3. https://man7.org/linux/man-pages/man1/pidstat.1.html
  4. https://engineering.saltside.se/linux-troubleshooting-disk-analysis-2dc40c6c49b4

聊聊领导力与带团队的那些事

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

傅盛说 "人和人最大的差别是认知",说的是认知的力量,罗伯特·清崎说"赚不到认知以外的钱",描述了认知的局限。很明显,好的认知是力量,不好的认知是局限。但是认知又没法量化,所以有时候我们就会发现,很难认清自己,也很难学习别人。

曾经在一次内部会议上,我感慨"如何才能达到老师和老大们的认知高度呢?感觉光凭看书学习,好难啊。" 当时老大给出了回应,说 "关键在于选择"。

确实,如果能坚持选择正确的事情,不避艰难,选择长期价值,不趋小利,选择务实奋斗,不浮躁,进步是必然,只在早晚。

回首 2021 年,文章输出略少。来到 2022,新年新气象,希望给自己立个 flag,争取 Break。

作为今年的开篇,想聊聊工作上的一些认知、看法,谓之自省。

找到企业价值与团队成长的最佳战略落地点是团队 Leader 的核心工作

这应该是笔者近几年关于领导力和带团队方面最深的体悟了,没有之一。

作为团队 Leader,企业价值和团队成长必须两手抓,两手都要硬。产出不了价值,团队就不会有回报,做的事情没有一定的高度,大家个人方面就会成长不足,长期也会缺乏进步。而寻找最佳战略落地点,就是要找到那个团队关键着力点,要既能很好的达成职责和目标,还要能在技术先进上有一定的突破。

这点很重要,值得花精力慎重对待。

回头来看,云原生就是近几年我给团队找的主要战略方向了。抛开成绩不谈,通过这个战略,团队人员技术水平提高很多,且行业优势明显,这一点还是挺让我自豪的。

视人为人,超越伯乐

对于很多技术管理者来讲,这可能是最难的,包括我自己。

人心难测,但人性中最有力量。每个人都有自己的想法和诉求,而如何能团结一群人,发挥 1+1 > 2 的作用,非常考验功力。我们知道,现实中绝大多数问题归根结底是人的问题,但越是如此,领导者越不应该把问题简单定性在人上。因为,没有人是完美的,修人修心才是正途。

但,用人之长容易,培人之短甚难。我以前也有过尝试培人之短,深感不易。这也许就是为什么,很多人更认同“选对人比培养人更重要”的原因吧。

但,无论如此,任何时候都不应该丢失以人为本的心。

组织先行,倡导质量全员建设

自始至终,测试只是质量保障的手段,而不是目的。我们谈质量,实际谈的是结果质量,是产品能不能服务好最终用户,即使是面临突发情况,异常情况。

以终为始,架构设计合不合理,代码实现优不优雅,产品姿势贴不贴切,都会影响最终的服务质量。

质量保障没有银弹,也不会有一劳永逸的解决方案,功夫都在平时。而如果能调动更多的人,心系质量,参与质量建设,不管从企业 ROI 还是技术文化构建上,都有一定的积极意义。

工程效率本质是为提升工程生产力服务的,面向的是全体工程人员,而不是单指 QA

很多时候,工程效率同学从组织分布上会离 QA 较近,这会导致工效同学的目光过于关注测试的一亩三分地,局限性比较大。但是,企业内工程属性更多的其实是研发人员,解决他们的痛点从价值规模上来看才是最大的。

不过,这类人又是最难服务的,有很多"古怪"的爱好。比如,一个不爽,分分钟钟就自己搞个轮子。所以应该如何做呢?

一定要深入到研发群体去,多观察,多交流,深挖痛点。真正把他们当作实际客户,以服务的心态来面对,才容易找到破局点,并形成突破。

很多时候,我个人是不推荐建设专服务于手动测试同学的所谓自动化测试平台的。作为技术人,以代码的形式呈现用例,管理用例,感觉已经足够了。

而保持团队技术密度,倡导技控先于人控,长期角度也会有更多的惊喜。

往期推荐

聊聊 Kubernetes Pod or Namespace 卡在 Terminating 状态的场景

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

这个话题,想必玩过 kubernetes 的同学当不陌生,我会分 Pod 和 Namespace 分别来谈。

开门见山,为什么 Pod 会卡在 Terminating 状态?

一句话,本质是 API Server 虽然标记了对象的删除,但是作为实际清理的控制器 kubelet, 并不能关停 Pod 或相关资源, 因而没能通知 API Server 做实际对象的清理。

原因何在?要解开这个原因,我们先来看 Pod Terminating 的基本流程:

  1. 客户端(比如 kubectl)提交删除请求到 API Server
    • 可选传递 --grace-period 参数
  2. API Server 接受到请求之后,做 Graceful Deletion 检查
    • 若需要 graceful 删除时,则更新对象的 metadata.deletionGracePeriodSeconds 和 metadata.deletionTimestamp 字段。这时候 describe 查看对象的话,会发现其已经变成 Terminating 状态了
  3. Pod 所在的节点,kubelet 检测到 Pod 处于 Terminating 状态时,就会开启 Pod 的真正删除流程
    • 如果 Pod 中的容器有定义 preStop hook 事件,那 kubelet 会先执行这些容器的 hook 事件
    • 之后,kubelet 就会 Trigger 容器运行时发起TERMsignal 给该 Pod 中的每个容器
  4. 在 Kubelet 开启 Graceful Shutdown 的同时,Control Plane 也会从目标 Service 的 Endpoints 中摘除要关闭的 Pod。ReplicaSet 和其他的 workload 服务也会认定这个 Pod 不是个有效副本了。同时,Kube-proxy 也会摘除这个 Pod 的 Endpoint,这样即使 Pod 关闭很慢,也不会有流量再打到它上面。
  5. 如果容器正常关闭那很好,但如果在 grace period 时间内,容器仍然运行,kubelet 会开始强制 shutdown。容器运行时会发送SIGKILL信号给 Pod 中所有运行的进程进行强制关闭
  6. 注意在开启 Pod 删除的同时,kubelet 的其它控制器也会处理 Pod 相关的其他资源的清理动作,比如 Volume。而待一切都清理干净之后,Kubelet 才通过把 Pod 的 grace period 时间设为 0 来通知 API Server 强制删除 Pod 对象。

参考链接: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination

只有执行完第六步,Pod 的 API 对象才会被真正删除。那怎样才认为是**"一切都清理干净了"**呢?我们来看源码:

// PodResourcesAreReclaimed returns true if all required node-level resources that a pod was consuming have
// been reclaimed by the kubelet. Reclaiming resources is a prerequisite to deleting a pod from theAPI Server.
func (kl *Kubelet) PodResourcesAreReclaimed(pod *v1.Pod, status v1.PodStatus) bool {
if kl.podWorkers.CouldHaveRunningContainers(pod.UID) {
// We shouldn't delete pods that still have running containers
klog.V(3).InfoS("Pod is terminated, but some containers are still running", "pod", klog.KObj(pod))
return false
}
if count := countRunningContainerStatus(status); count > 0 {
// We shouldn't delete pods until the reported pod status contains no more running containers (the previous
// check ensures no more status can be generated, this check verifies we have seen enough of the status)
klog.V(3).InfoS("Pod is terminated, but some container status has not yet been reported", "pod", klog.KObj(pod), "running", count)
return false
}
if kl.podVolumesExist(pod.UID) && !kl.keepTerminatedPodVolumes {
// We shouldn't delete pods whose volumes have not been cleaned up if we are not keeping terminated pod volumes
klog.V(3).InfoS("Pod is terminated, but some volumes have not been cleaned up", "pod", klog.KObj(pod))
return false
}
if kl.kubeletConfiguration.CgroupsPerQOS {
pcm := kl.containerManager.NewPodContainerManager()
if pcm.Exists(pod) {
klog.V(3).InfoS("Pod is terminated, but pod cgroup sandbox has not been cleaned up", "pod", klog.KObj(pod))
return false
}
}

// Note: we leave pod containers to be reclaimed in the background since dockershim requires the
// container for retrieving logs and we want to make sure logs are available until the pod is
// physically deleted.

klog.V(3).InfoS("Pod is terminated and all resources are reclaimed", "pod", klog.KObj(pod))
return true
}

源码位置: https://github.com/kubernetes/kubernetes/blob/1f2813368eb0eb17140caa354ccbb0e72dcd6a69/pkg/kubelet/kubelet_pods.go#L923

是不是很清晰?总结下来就三个原因:

  1. Pod 里没有 Running 的容器
  2. Pod 的 Volume 也清理干净了
  3. Pod 的 cgroup 设置也没了

如是而已。

自然,其反向对应的就是各个异常场景了。我们来细看:

  • 容器停不掉 - 这种属于 CRI 范畴,常见的一般使用 docker 作为容器运行时。笔者就曾经遇到过个场景,用docker ps 能看到目标容器是Up状态,但是执行docker stop or rm 却没有任何反应,而执行docker exec,会报no such container的错误。也就是说此时这个容器的状态是错乱的,docker 自己都没法清理这个容器,可想而知 kubelet 更是无能无力。workaround 恢复操作也简单,此时我只是简单的重启了下 docker,目标容器就消失了,Pod 的卡住状态也很快恢复了。当然,若要深究,就需要看看 docker 侧,为何这个容器的状态错乱了。
    • 更常见的情况是出现了僵尸进程,对应容器清理不了,Pod 自然也会卡在 Terminating 状态。此时要想恢复,可能就只能重启机器了。
  • Volume 清理不了 - 我们知道在 PV 的"两阶段处理流程中",Attach&Dettach 由 Volume Controller 负责,而 Mount&Unmount 则是 kubelet 要参与负责。笔者在日常中有看到一些因为自定义 CSI 的不完善,导致 kubelet 不能 Unmount Volume,从而让 Pod 卡住的场景。所以我们在日常开发和测试自定义 CSI 时,要小心这一点。
  • cgroups 没删除 - 启用 QoS 功能来管理 Pod 的服务质量时,kubelet 需要为 Pod 设置合适的 cgroup level,而这是需要在相应的位置写入合适配置文件的。自然,这个配置也需要在 Pod 删除时清理掉。笔者日常到是没有碰到过 cgroups 清理不了的场景,所以此处暂且不表。

现实中导致 Pod 卡住的细分场景可能还有很多,但不用担心,其实多数情况下通过查看 kubelet 日志都能很快定位出来的。之后顺藤摸瓜,恢复方案也大多不难。

当然还有一些系统级或者基础设施级异常,比如 kubelet 挂了,节点访问不了 API Server 了,甚至节点宕机等等,已经超过了 kubelet 的能力范畴,不在此讨论范围之类。

还有个注意点,如果你发现 kubelet 里面的日志有效信息很少,要注意看是不是 Log Level 等级过低了。从源码看,很多更具体的信息,是需要大于等于 3 级别才输出的。

那 Namespace 卡在 Terminating 状态的原因是啥?

显而易见,删除 Namespace 意味着要删除其下的所有资源,而如果其中 Pod 删除卡住了,那 Namespace 必然也会卡在 Terminating 状态。

除此之外,结合日常使用,笔者发现 CRD 资源发生删不掉的情况也比较高。这是为什么呢?至此,那就不得不聊聊 Finalizers 机制了。

官方有篇博客专门讲到了这个,里面有个实验挺有意思。随便给一个 configmap,加上个 finalizers 字段之后,然后使用kubectl delete删除它就会发现,直接是卡住的,kubernetes 自身永远也删不了它。

参考: https://kubernetes.io/blog/2021/05/14/using-finalizers-to-control-deletion/#understanding-finalizers

原因何在?

原来 Finalizers 在设计上就是个 pre-delete 的钩子,其目的是让相关控制器有机会做自定义的清理动作。通常控制器在清理完资源后,会将对象的 finalizers 字段清空,然后 kubernetes 才能接着删除对象。而像上面的实验,没有相关控制器能处理我们随意添加的 finalizers 字段,那对象当然会一直卡在 Terminating 状态了。

自己开发 CRD 及 Controller,因成熟度等因素,发生问题的概率自然比较大。除此之外,引入 webhook(mutatingwebhookconfigurations/validatingwebhookconfigurations)出问题的概率也比较大,日常也要比较注意。

综合来看,遇 Namespace 删除卡住的场景,笔者认为,基本可以按以下思路排查:

  1. kubectl get ns $NAMESPACE -o yaml, 查看conditions字段,看看是否有相关信息
  2. 如果上面不明显,那就可以具体分析空间下,还遗留哪些资源,然后做更针对性处理
    • 参考命令: kubectl api-resources --verbs=list --namespaced -o name | xargs -n 1 kubectl get --show-kind --ignore-not-found -n $NAMESPACE

找准了问题原因,然后做相应处理,kubernetes 自然能够清理对应的 ns 对象。不建议直接清空 ns 的 finalizers 字段做强制删除,这会引入不可控风险。

参考: https://github.com/kubernetes/kubernetes/issues/60807#issuecomment-524772920

相关阅读

前同事也有几篇关于 kubernetes 资源删除的文章,写的非常好,推荐大家读读:

往期推荐