不知道运维在忙什么

前段时间,我从领导那里得知,有其他团队的人说:“不知道运维在忙什么”。我懒得打听是谁说的,所以就暂时把这个“谁”称为:说话人。 而我作为运维的 leader,很早前就预料到有人会说这句话。在我自己看来,就两种情况:有人想搞运维团队(坏),或者那个人太不懂职场(菜)。 为了避免我个人情绪,我问了一下 AI:“有人向领导说这话”是什么含义,得到的答案是: “不知道运维在忙什么”通常不是中性询问,而是在领导面前暗示: - 运维工作不透明; - 运维没有及时响应; - 运维可能没做出成果,甚至“瞎忙”。 这是一种比较隐晦的负面评价。尤其当对方没有先向运维了解情况、也没有指出具体事项,却直接在领导面前说,确实带有甩锅、质疑或贬低的意味。 不论是哪种答案,本质上并不是我预料有人会说这句话,而是预料到信息不透明、话语权不对等的情况,发生这样的事情,我觉得是必然的。 我在世界500强、初创公司、上市公司、跨国公司待过,我见过一些世面了。 因此,我管理团队时,选择通过看板管理来公开团队的所有工作,一方面管理自己的团队,一方面预防在出现“情况”时,好有数据来自证。我统计了引入 AI 工具后,最近半年我们完成的工作量提高了 40% 多。 既然,运维团队公开了所有的工作,为什么还有人说不知道运维在忙什么?有几种可能: 说话人没有看,或者没有心思主动去看我们的工作。 运维团队没有同步到位。 情况1,没有什么好说的,大家都是成年人。 关于情况2。运维团队每天都会在看板前进行站会。 当其他团队来找运维团队提需求后,运维团队要多久向其他团队的人同步呢?要运维团队每打一个字就同步吗?当然不是,而是根据不同的团队的“同步频率需求”,来同步。 但是,反过来想,站在公司整体经营成本的角度来想,与其让运维团队花成本来解决“同步”的问题,不如其他团队主动问一句运维团队,这样是不是成本更低?我懒得深究这个情况了。 最终,我开发了一个 bot。v1 版本有2个功能: 只要有人在公司群里发:@infra 在忙什么,bot 就会把运维团队看板上所有 doing 的任务都列出来。 @infra subscribe <ticket> 就可以订阅任何一张卡的状态变更通知。我们运维也不用思考要多久同步,你关心就自己订阅。 这个 bot 只是为了堵住说话人的口,也是职场上的一种正面的回应。 本来,本文就应该停止了。好奇心让我深入思考了这类事件的背后。本质就是话语权不对等的结果,是不同成员影响决策、定义问题和表达异议的能力不平等。 假设软件工程会经过以下流程: 产品经理 -> 设计 -> 开发 -> 运维 -> 测试 这个流程中,谁有向上说话的权力,谁就有权力判定下游工作。假如开发有向上级说话的权力,当开发说:目前卡在运维。 那么,说一次还好,每次开发都这么说。信息传到上级的意思就是:运维工作不行。把“运维”换成“测试”,结果是一样的。 但是,我知道的是,当上级预期是10天完成上线时,而开发占用了9天,留给运维和测试的时间只有1天,也就是最后一天才通知运维要进行上线工作。那么,我们会如何看待开发,以及开发说的“目前卡在运维”呢? 这里只是以开发角色举例,换成其它角色,同样的问题。 那么,我们应该如何解决“话语权不对等”所带来的问题呢?是让话语权平等吗?我也不知道答案。我会继续研究下去。 最后,希望世界和平。

2026-08-22 · 1 min · 59 words · 翟志军 Jack Zhai

什么是手工运维

一个夏天的午后,知了嘈杂,窗下的我 review AI 写好 IaC 代码后,敲下部署到线上环境的命令。 看着终端的日志滚动,一个问题突然蹦入我的大脑:什么是手工运维? 像我这样使用 IaC 来管理基础设施: 过去是手打代码,这算吗? 现在是 AI 写的代码了,但是 prompt 依然是手打,这算吗? 代码是 AI 写了,但是执行,还是需要手工的,这算吗? 如果不使用 IaC,改成使用 GUI,人们可以在界面上点一下“部署”按钮,就完成了整个服务的部署,这算不算手工运维呢? 像 Cloudflare 或者 Vercel 上,开发者都不用点,只需要将写好的业务代码 push 到远程的 git 仓库就可以了,部署和自动化的域名都给自动化完成了,这算不算手工运维呢? 先说结论:不论是使用 GUI 还是 IaC,如果无法做到自动编排部署任务,都是手工运维。 过去,运维人员通过 SSH 一条条命令的输入到服务器,大家都认为那是手工运维。但是,如果把这些命令写成 bash 脚本,可以一键执行,这算自动化运维了吗? 现在,人们在 GUI 界面上,一个个按钮的点击界面,最后点击“部署”按钮,我认为这与通过 SSH 一条条命令运维没有什么区别,也是手工运维。只不过 GUI 的动作的粒度比 SSH 命令大一些。 本质上,以上两种方式,都需要人工编排部署任务,也就属于手工运维。 而 Cloudflare 或者 Vercel。单服务时,不需要编排部署,所以人们觉得自动化运维。但是如果是多个服务,Cloudflare 或者 Vercel无法做到自动化编排部署任务,它依然是手工运维。 所以,使用 IaC 来管理基础设施,算手工运维吗?取决于你的 IaC 代码:当人表达意图后,它能否自动输出部署计划,并且能自动化运行。 如何做到?这是另一个话题了。 希望这篇文章给你带来启发。多谢。

2026-07-10 · 1 min · 63 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

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

回滚的两种模式

回滚是一种运维操作。通常发生在部署过程中发现问题,需要将目标环境恢复到部署前的状态。 在我看来,回滚有两种模式。其中一种是一步步执行反向操作,我称之为反向操作模式。 基于反向操作的回滚模式 可能是由于过去手工运维的思维方式的惯性,我发现不少人只知道这一种模式。 比如使用手工部署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

K8s工程化:K8s中的Java应用出现OOM后怎么办?

完整代码在文末 背景 前段时间,线上系统出现了两次持续时间比较长的事故。这两次事故暴露我在某些方面的不足。同时,也意识到在SRE这个领域,经验的重要性。 事故过程中,我们发现大量的FullGC。当时,我们想到了要dump内存出来分析,可惜发现没有加-XX:HeapDumpPath参数。同时,我们也发现,如果dump出来了,我们也没法拿到dump出来的文件。因为我们的应用是跑在K8s中的。 方案调研 经复盘,我们得到一个action:在Java应用出现OOM时,将内存dump出来,并持久化,并且方便分析。 这个action可以细分为两个任务: OOM时,dump内存出来; 提供一种途径方便分析。 经过权衡,任务2的优先级是可以降低的。puvad只要把任务1做好就可以。所以,这两个任务最终变成:在Java应用出现OOM时,将内存dump到NAS中。 笔者在网上搜索一通,看到的方案基本就是启动一个sidecar容器,与应用共享一个目录。然后监控这个目录,发现内容就上传到s3这类对象存储中。 这种方案的问题在于: sidecar在传输过程,有出现问题的风险; 为了OOM这个小概率事件启动一个sidecar,资源有点浪费。 个人觉得,Java应用的Pod应该只负责将OOM时的内存dump到NAS即可,其它事情应该由其它Pod完成。 具体实现 以下方案是基于Helm自动化部署。如果你使用的是其它自动化部署工具,思路大体相同。 准备NFS服务 这部分不是本文范畴。 Java应用启动时参数配置 在Dockerfile中必须将变量$JAVA_OPTS加入到启动参数中。 FROM openjdk:11.0.12-jre-buster COPY target/app.jar /app.jar CMD java -jar $JAVA_OPTS /app.jar 加入InitContainers 作用:创建符合指定规则的Dump目录(注意DUMP_FOLDER变量的定义)。如下代码,在init容器启动后,它会创建目录:/nfs/dump/default/jvm-oom-example/10.233.66.38 。在应用出现OOM,内存文件会被dump在此目录下。 initContainers: - name: init image: registry.cn-shenzhen.aliyuncs.com/aliacs-app-catalog/busybox:1.30.1 command: ['sh', '-c', 'echo $DUMP_FOLDER;mkdir -p $DUMP_FOLDER'] {{- with .Values.volumeMounts }} volumeMounts: {{- toYaml . | nindent 12 }} {{- end }} env: - name: MY_NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: MY_POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: MY_POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: MY_POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: DUMP_FOLDER value: "/nfs/dump/$(MY_POD_NAMESPACE)/{{ include "app.fullname" . }}/$(MY_POD_IP)" 配置应用容器 我们要做的,其实就是设置JAVA_OPTS环境变量。这里要注意的是JAVA_OPTS可以由三部分组成的: ...

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

Kubernetes包管理器Helm的本质

“本质”类的文章,通常很难带流量。而且写起来非常吃力。 那我为什么还要写?写作是对自己的锻炼。写作是让自己的思想更有深度的一种有效方式。 如果你觉得这篇文章对你有帮助,也你麻烦你转发这篇文章,这是对我的帮助。谢谢。 Kubernetes 的包管理器的本质 “Helm 是 Kubernetes 的包管理器”。Helm的官方网站如是说。 那什么是“Kubernetes 的包管理器”? 我们假设需要在没包管理器的场景下部署资源,你需要一个个文件手工地执行kubectl apply -f abc.yaml,abc.yaml就是Kubernetes的资源的定义文件。 文件内容如下: --- apiVersion: apps/v1 kind: Deployment metadata: name: abc labels: app.kubernetes.io/name: abc spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: abc 当需要卸载资源呢?你又需要手工执行kubectl delete -f abc.yaml。 所以每次发布,你都必须有一个发布记录,记录下哪些YAML要执行apply,哪些yaml要执行delete。而且delete后,你还要记得将那个文件从文件夹中删除。 如果每次手工执行,工作量大不说,还很容易出错。所以,有人会想到使用Shell脚本或者Python脚本来解决这些问题。 当你通过Shell脚本或者Python脚本能自动化解决以上问题时,实际上就等于实现了一个Kubernetes 的包管理器。 当我们真正理解以上所说的Kubernetes资源的部署问题后,你就明白了Kubernetes 的包管理器其实就两个核心功能: 自动化执行Kubenetes资源更新; 跟踪Kubenetes资源更新记录(本质还是版本化)。 我们在选择包管理器时,务必要从这两个角度考虑。像Grafana公司Tanka,并不是一开始就实现“跟踪Kubenetes资源更新记录”功能,具体可以看:https://github.com/grafana/tanka/issues/88 。 Helm是如何实现包管理的 注:本文讲的是Helm3。Helm2与Helm3存在较大差异。 Helm的包:Chart 假如存在一个微服务x,我们将其部署到Kubernetes中,需要准备Deployment、HPA、Service的这三种资源的YAML文件。这三个文件,统一放在一个文件夹中。 Helm本身是一个命令行工具。通过package子命令,可以将整个文件夹打包成一个tgz的压缩包。打包命令为:helm package x-service --version 1.0 。打包结果是一个tgz包。如下图: 这个tgz包,我们称之为Chart包。本质上它就是Kubernetes的资源文件的一个集合。 我们可以将Chart包上传到Nexus这类制品管理工具进行版本化控制。这涉及到Chart的管理的工程实践,不在本文范围。 在有了Chart包以后,我们可以通过命令helm install <release> <chart路径>将svc安装到指定的Kubernetes集群上。如x-svc的部署指令将会是:helm install x-svc ./x-svc-1.0.tgz。 ...

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