翟志军 Jack Zhai 👋

软件工程师,10 年+ 经验:5 年 Java 后端开发 + 5 年 DevOps / SRE 实践。

📖 《Jenkins 2.x 实践指南》作者(销量 5000+)。自 2015 年笔耕至今,已写下 120+ 篇文章。

✍️ 这里记录我对 DevOps、SRE、Cloud Native、持续交付、领域驱动设计(DDD) 以及 职场成长 的思考与实践。信奉 Everything as Code

Precise Testing Is Wrong — It's Just a Byproduct of Incremental Builds

There’s a popular idea in the testing world called precise testing (精准测试). In short, it is the ability to run only the tests affected by a change, instead of running the entire suite every time. I think the idea is right — but the way our industry usually implements it is wrong. In this post I’ll first explain what precise testing is and why the mainstream approach is heading in the wrong direction, and then show a different path: precise testing turns out to be nothing more than a byproduct of incremental builds. ...

2026-06-29 · 9 min · 1818 words · 翟志军 Jack Zhai

The Hidden Trap in Technical Decisions: Trading a One-Time Cost for Continuous Debt

Our team uses more and more AI services—OpenAI, Deepseek, Tongyi, Doubao, Claude… Each platform has its own API, its own keys, its own billing model. The configuration gets messier, the calls get more scattered, and so sooner or later someone proposes: Why don’t we build our own AI Gateway? Either build it ourselves, or buy one off the shelf. It’s a very natural idea. I’ve had it too. Build an AI Gateway and solve all the problems at once—what a great KPI story. ...

2026-06-14 · 8 min · 1651 words · 翟志军 Jack Zhai

技术决策的隐藏陷阱:用持续负债消除一次性成本

团队里用的 AI 服务越来越多——OpenAI、Deepseek、通义、豆包、Claude……每个平台一套 API、一套 Key、一套计费口径。配置越来越乱,调用越来越散,于是总有人会提出: 要不我们自己搭一个 AI Gateway 吧?自建,或者买个现成的。 这个想法非常自然。我也有过。 搭建一个 AI Gateway 一次解决所有的问题,多好的 KPI 叙事。 但是,仔细一想,把账一算,我决定放弃在公司里自建 AI Gateway。 先给结论 对绝大多数公司来说,自建或购买 AI Gateway 的 ROI 都很低。 注意,决定性的因素不是你用了多少种 AI 服务。哪怕你接了十几个平台,只要项目规模没到、团队能力没到,这笔投入依然不划算。 真正划算的前提只有一个组合:项目数足够多,且你本来就有一支扛得住 7×24 的运维/平台团队。 否则,你大概率是在用一个永久性的负债,去换一两天就能消化掉的麻烦。 下面展开说为什么。 一、AI Gateway 到底解决了什么 先得承认,AI Gateway 的确能解决公司的一些问题: 多平台 fallback:同一个模型要在多个供应商之间兜底。比如 Deepseek,要做 Deepseek 官方 → 火山引擎 → 阿里云百炼 的 fallback 路径,每个业务都得自己写一遍。 API Key 轮换要重启服务:换一次 Key 就得重启应用。三个服务用到了就至少轮换三次,再乘上多环境(dev / staging / prod),数字很快就上去了。 请求监控各自为政:对 AI 调用的监控,每个业务都得自己实现一套。 接口不统一:想换模型,就得重新对接一个平台的 SDK,而不是一套接口打天下。 申请 Key 流程慢:业务向运维申请 Key,走流程要时间。 成本统计粒度粗:很多 AI 平台的成本统计很弱,做不到按 Key 或按 Project 维度归集,只能在业务代码里自己埋点。 这听起来非常的美好,也很能打动老板们。 ...

2026-06-14 · 2 min · 225 words · 翟志军 Jack Zhai

从法庭到代码:谁主张谁举证的工程实践

2023年,与开发商打了不少官司。学习了不少庭审知识。发现有些实践或者原则放到软件工程中也是有效的。就比如谁主张谁举证这一基本原则。 谁主张谁举证就是当事人对自己提出的主张提供证据并加以证明。 举例来说,张三说隔壁老王侵占了自己的宅基地。张三必须自己拿证据来证明自己说的事实。而不是让老王拿证据出来证明自己没有侵占。 在软件工程中,服务A调用服务B,出问题了。服务A认为是服务B的问题,这时,根据谁主张谁举证原则,服务A的开发应该自己找出证据来证明自己的观点是正确的,而不应该让服务B来提供证据证明自己没有问题。 因为如果让服务B证明自己没有问题,怎么证明自己没有问题呢?怎么证明,证据都不够充分。而且会浪费服务B的团队人力。 所以,在团队中,让大家达成“谁主张谁举证”的共识会带来以下好处: 节约团队的人力。如果服务A能提供有效证据,那么就不需要服务B去证明自己没有问题了。 倒逼团队形成故障现场保留的习惯,比如打印良好的日志等 不过,我们需要注意的是,“谁主张谁举证”并不意味着,服务B团队不配合服务A一起排查问题。在一个公司里,基本的团队协作原则与底线是要有的。 那是不是所有的场景都是谁主张谁举证? 也有特殊情况。有些场景,是需要进行举证责任倒置的。开发商要拿证据来证明自己的转供电是合法的,而不应该让业主拿证据来证明开发商转供电不合法。 也就是证据只能是“被告方”能提供的情况下,我们就不是谁主张谁举证了。 以上只是我个人的思维实验。 在工作中,作为DevOps工程师,我经常遇到开发者质疑云厂商的Redis或者RDS出了问题。 根据我过往的经验,只是质疑,没有证据的情况,大多是开发者自己代码的问题。通常我会自己看看监控的同时,会要求他们加日志,然后想办法复现问题。 最后,一个公司一个团队里,如何低成本的达成谁主张谁举证这个共识? 我目前还没有答案。

2025-09-13 · 1 min · 17 words · 翟志军 Jack Zhai

An Example Implement Ansible Deployment on Github Action

- name: write secrets into json run: | echo "${{ toJSON(secrets) }}" > _github_secrets.json - name: write github repo vars into json run: | echo "${{ toJSON(vars) }}" > _github_vars.json - name: write ssh private key run: | echo "${{ secrets.STAG_SSH_PRIVATE_KEY }}" > ${{ github.workspace }}/.ssh_private_key.pem chmod 0400 ${{ github.workspace }}/.ssh_private_key.pem - name: write ssl certificate run: | echo "${{ secrets.showmecodes_TLS_CERTIFICATES }}" > ${{ github.workspace }}/showmecodes.ai.pem echo "${{ secrets.showmecodes_TLS_KEY }}" > ${{ github.workspace }}/showmecodes.ai.key - name: deploy showmecodes to stag uses: dawidd6/action-ansible-playbook@v2 with: playbook: playbook-showmecodes.yml key: ${{ secrets.STAG_SSH_PRIVATE_KEY }} options: | --inventory env_vars/${{env.APP_ENV}}/hosts.yaml --extra-vars "app_backend_zip_path=${{ needs.init_build_version.outputs.backendArtifactName }} app_frontend_zip_path=${{ needs.init_build_version.outputs.fontendStagArtifactName }} app_version=${{ needs.init_build_version.outputs.VERSION }} ansible_ssh_private_key_file=${{ github.workspace }}/.ssh_private_key.pem showmecodes_tls_certificate_file=${{ github.workspace }}/showmecodes.ai.pem showmecodes_tls_private_key_file=${{ github.workspace }}/showmecodes.ai.key" --extra-vars=@_github_vars.json --extra-vars=@_github_secrets.json

2024-04-22 · 1 min · 114 words · 翟志军 Jack Zhai

Web前端构建之依赖版本管理最佳实践

本文需要读者懂一点点前端的构建知识: package.json文件的作用之一是管理外部依赖; .npmrc是npm命令默认配置,放在工程根目录。 Web前端构建一直都是一个不难,但是非常烦人的问题,在DevOps、CI/CD领域。 烦人的是偶尔发生这样的事情: 开发在本地构建通过,但是流水构建失败。这时前端开发人员会经常报怨Pipeline不稳定; 流水线构建通过,但是在生产环境上启动不了,或者出现运行错误; 不使用Docker可以启动,但是打包成Docker镜像后启动就失败。 这类问题,不是今天解决了,明天就不会发生。而是你根本不知道它什么时候又发生。 据我观察,绝大多数时候都是依赖版本管理没有做好导致的。 Web前端的依赖版本管理包括以下几个维度: node的版本 外部依赖的版本 我们需要在开发环境,构建环境,运行环境保证它们的版本是一致的。这样,在本地开发环境测试通过,那么,在其它环境就理论上也应该能通过。 接下来是具体的最佳实践。 保证Node版本一致 要保证Node版本一致,就要保证所有的环境使用同一个版本的node。而且是要具体到某一个精确的版本,如v20.11.1,而不是20这样一个粗略版本。 以下是我们以v20.11.1为例。 设置开发环境 设置开发环境的node的版本,需要在package.json中加入: { "engines": { "node": "v20.11.1", "npm": "10.2.4" }, } 这时,如果存在开发环境与配置的版本不匹配的情况,执行npm install,会出现以下警告,但是命令还是会继续执行: npm WARN EBADENGINE Unsupported engine { npm WARN EBADENGINE package: '[email protected]', npm WARN EBADENGINE required: { node: 'v20.10.1', npm: '10.2.4' }, npm WARN EBADENGINE current: { node: 'v20.11.1', npm: '10.2.4' } npm WARN EBADENGINE } 希望强制要求版本一致,就在根目录的.npmrc文件加入: engine-strict=true 发生版本不一致的情况,报错日志如下,且命令会停止执行: npm ERR! code EBADENGINE npm ERR! engine Unsupported engine npm ERR! engine Not compatible with your version of node/npm: [email protected] npm ERR! notsup Not compatible with your version of node/npm: [email protected] npm ERR! notsup Required: {"node":"v20.10.1","npm":"10.2.4"} npm ERR! notsup Actual: {"npm":"10.2.4","node":"v16.0.1"} 设置构建环境 我们以Github Actions为例。在设置node环境时,应设置为: ...

2024-03-12 · 1 min · 168 words · 翟志军 Jack Zhai

优秀的DevOps工程师应该具有什么特质?

这是我最近面试时遇到的一个非常好的问题: 在你的心目中,优秀的DevOps工程师应该是什么样的? 很长一段时间里,我没有想到这个问题。所以,当HR问起时,我边思考边回答。 我已经不记得原话,本文就当作从性格角度思考“优秀的DevOps工程师应该具有什么特质”。 首先,优秀的DevOps工程师,TA应该是严谨的。 一个严谨的特质的人才会主动考虑软件工程化过程中的各种可能。 其次,TA应该对手工操作产生“生理性上的不适”,即追求自动化极致。 一个再怎么严谨的人,如果是手工操作,面对复杂的线上环境的运维,TA的能力也是有限的。TA必须自动化所有能自动化的东西,并争取自动化所有的内容,TA才能有可能“驯服野兽”。 然后,TA应该是一个节约的人,即节俭。 因为浪费导致企业不必要损失,是否要避免这个损失,很多时间里是一个DevOps工程师的选择问题。 最后,TA应该是一个热爱这个领域的人。我常常忘记自己对这个领域的热爱。以致于我忘记回答。 只有热爱,才能产生自驱力,驱动TA去成长,去不断思考。即使你再严谨,你也有考虑不到的地方,只有热爱,TA才会探索到TA不知道他不知道的。 以上只是我个人的看法,欢迎一起讨论。

2024-03-12 · 1 min · 13 words · 翟志军 Jack Zhai

Two Patterns for Rollback

Rollback is an operations and maintenance procedure. It usually occurs when a problem is discovered during deployment, and the target environment needs to be reverted to its pre-deployment state. In my opinion, there are two patterns for rollback. One of them is to perform a reverse operation step by step, which I call the Reverse Operation Pattern. Rollback Pattern Based on Reverse Operation Probably due to the inertia of the past manual operation mindset, I found that quite a few people only know this one pattern. ...

2024-03-06 · 4 min · 783 words · 翟志军 Jack Zhai

回滚的两种模式

回滚是一种运维操作。通常发生在部署过程中发现问题,需要将目标环境恢复到部署前的状态。 在我看来,回滚有两种模式。其中一种是一步步执行反向操作,我称之为反向操作模式。 基于反向操作的回滚模式 可能是由于过去手工运维的思维方式的惯性,我发现不少人只知道这一种模式。 比如使用手工部署Nginx的配置的操作如下: SSH登录到目标服务器 进入到存放Nginx的/etc/nginx/sites-enabled/目录 编辑目标配置文件vim example.443.conf 增加一个location配置 reload nginx使配置生效 在反向操作模式的回滚方案中,我们应该该如何回滚呢?1,2,3,5步骤与部署时一致。第4步,需要操作人员找到该配置,并删除。 这时,操作人员在操作时就可能会出错,而且出错了,你可能很难觉察到。因为他是手工操作的。 那么,有人就想,以上步骤能自动回滚就好了。 可是,该如何自动回滚呢?我们需要在部署时就设计好相应的自动化回滚脚本。当需要时,就触发其自动回滚。 然而这个回滚方案的方案是无法通用的,而且增加了运维成本。因为普通的运维人员对于一个Nginx的配置的变更,是非常不愿意写回滚的脚本的,而且,他本人也不一定能写出正确的、可靠的自动化回滚脚本。 那么,有人就会想了,我能否实现自动生成回滚代码的平台? 答案是可以的。你必须预先定义每一个步骤动作,比如在平台上将Nginx配置的修改作为一个动作定义。然后再定义它的反操作。 如果你是实现过类似平台项目,你会知道,这工作量是无穷无尽的。因为运维的操作是无穷无尽的。 你可以说平台可以提供自定义步骤动作的能力,那么你同样会遇到“他本人也不一定能写出正确的、可靠的自动化回滚脚本”的问题。而且,既然是平台了,定义操作的责任就应该是平台的责任。 所以,这是一个业界的难题。 在面试过程中,面试官通常假设我也会遇到同样的难题。然而,我根本不会遇到这个问题。 基于版本的回滚模式 我在解释这个模式时,很多人无法理解。 还是以部署Nginx配置为案例。但是通过Ansible来实现自动化部署。假设已经存在以下部署脚本: - hosts: prod-nginx gather_facts: yes become: true vars_files: # Nginx的配置 - common_vars/nginx.yaml roles: # Nginx的部署逻辑,是声明式的、幂等的。 - ansible-role-nginx 以上代码含义大概是:部署Nginx到prod-nginx主机列表上,并使用common_vars/nginx.yaml文件中的配置。 nginx.yaml的配置如下: nginx_vhosts: - listen: "80" server_name: "*.example.com" return: "301 https://{{example_domain}}$request_uri" filename: "example.80.conf" 我们对以上代码版本化(提前到Git中,并通过自动化构建),得到版本号:v1.0.1。 现在我们需要像“反向操作的回滚模式”中的案例那样修改线上的配置时,我们的做法是在nginx.yaml配置增加相应的配置,最终效果如下: nginx_vhosts: - listen: "80" server_name: "*.example.com" return: "301 https://{{example_domain}}$request_uri" filename: "example.80.conf" - listen: "80" server_name: "*.abc.com" return: "301 https://{{example_domain}}$request_uri" filename: "abc.80.conf" 然后将代码push到代码库中,经过构建,我们得到版本号:v1.1.0。 ...

2024-03-05 · 1 min · 88 words · 翟志军 Jack Zhai

Jenkins kubernetes插件的原理

如何使用 使用Kubernetes插件时,我们需要做三件事情: 根据官方文档,在Jenkins上加入kubernetes配置。 在Jenkinsfile中加入kubernetes agent的申明。 指定容器执行你的业务脚本。 关于第2点,kubernetes agent的申明又有两种方式。一种是脚本式的,代码样例如下: podTemplate(containers: […]) { node(POD_LABEL) { stage('Run shell') { container('mycontainer') { sh 'echo hello world' }}}} 一种是申明式,代码样例如下: pipeline { stages { stage('Run maven') { agent { kubernetes { yaml """ apiVersion: v1 kind: Pod metadata: labels: app: jenkins-agent spec: containers: - name: maven image: maven:alpine command: - cat tty: true - name: busybox image: busybox command: - cat tty: true """ }} steps { container('maven') { sh 'mvn -version' }}}}} 笔者推荐使用申明式。yaml配置部分看起来并不优雅,这是另一个话题。咱们今后再讲。 ...

2024-02-26 · 1 min · 150 words · 翟志军 Jack Zhai