软件工程师,10 年+ 经验:5 年 Java 后端开发 + 5 年 DevOps / SRE 实践。
📖 《Jenkins 2.x 实践指南》作者(销量 5000+)。自 2015 年笔耕至今,已写下 120+ 篇文章。
✍️ 这里记录我对 DevOps、SRE、Cloud Native、持续交付、领域驱动设计(DDD) 以及 职场成长 的思考与实践。信奉 Everything as Code。
软件工程师,10 年+ 经验:5 年 Java 后端开发 + 5 年 DevOps / SRE 实践。
📖 《Jenkins 2.x 实践指南》作者(销量 5000+)。自 2015 年笔耕至今,已写下 120+ 篇文章。
✍️ 这里记录我对 DevOps、SRE、Cloud Native、持续交付、领域驱动设计(DDD) 以及 职场成长 的思考与实践。信奉 Everything as Code。
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. ...
团队里用的 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 维度归集,只能在业务代码里自己埋点。 这听起来非常的美好,也很能打动老板们。 ...
2023年,与开发商打了不少官司。学习了不少庭审知识。发现有些实践或者原则放到软件工程中也是有效的。就比如谁主张谁举证这一基本原则。 谁主张谁举证就是当事人对自己提出的主张提供证据并加以证明。 举例来说,张三说隔壁老王侵占了自己的宅基地。张三必须自己拿证据来证明自己说的事实。而不是让老王拿证据出来证明自己没有侵占。 在软件工程中,服务A调用服务B,出问题了。服务A认为是服务B的问题,这时,根据谁主张谁举证原则,服务A的开发应该自己找出证据来证明自己的观点是正确的,而不应该让服务B来提供证据证明自己没有问题。 因为如果让服务B证明自己没有问题,怎么证明自己没有问题呢?怎么证明,证据都不够充分。而且会浪费服务B的团队人力。 所以,在团队中,让大家达成“谁主张谁举证”的共识会带来以下好处: 节约团队的人力。如果服务A能提供有效证据,那么就不需要服务B去证明自己没有问题了。 倒逼团队形成故障现场保留的习惯,比如打印良好的日志等 不过,我们需要注意的是,“谁主张谁举证”并不意味着,服务B团队不配合服务A一起排查问题。在一个公司里,基本的团队协作原则与底线是要有的。 那是不是所有的场景都是谁主张谁举证? 也有特殊情况。有些场景,是需要进行举证责任倒置的。开发商要拿证据来证明自己的转供电是合法的,而不应该让业主拿证据来证明开发商转供电不合法。 也就是证据只能是“被告方”能提供的情况下,我们就不是谁主张谁举证了。 以上只是我个人的思维实验。 在工作中,作为DevOps工程师,我经常遇到开发者质疑云厂商的Redis或者RDS出了问题。 根据我过往的经验,只是质疑,没有证据的情况,大多是开发者自己代码的问题。通常我会自己看看监控的同时,会要求他们加日志,然后想办法复现问题。 最后,一个公司一个团队里,如何低成本的达成谁主张谁举证这个共识? 我目前还没有答案。
- 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
本文需要读者懂一点点前端的构建知识: 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环境时,应设置为: ...
这是我最近面试时遇到的一个非常好的问题: 在你的心目中,优秀的DevOps工程师应该是什么样的? 很长一段时间里,我没有想到这个问题。所以,当HR问起时,我边思考边回答。 我已经不记得原话,本文就当作从性格角度思考“优秀的DevOps工程师应该具有什么特质”。 首先,优秀的DevOps工程师,TA应该是严谨的。 一个严谨的特质的人才会主动考虑软件工程化过程中的各种可能。 其次,TA应该对手工操作产生“生理性上的不适”,即追求自动化极致。 一个再怎么严谨的人,如果是手工操作,面对复杂的线上环境的运维,TA的能力也是有限的。TA必须自动化所有能自动化的东西,并争取自动化所有的内容,TA才能有可能“驯服野兽”。 然后,TA应该是一个节约的人,即节俭。 因为浪费导致企业不必要损失,是否要避免这个损失,很多时间里是一个DevOps工程师的选择问题。 最后,TA应该是一个热爱这个领域的人。我常常忘记自己对这个领域的热爱。以致于我忘记回答。 只有热爱,才能产生自驱力,驱动TA去成长,去不断思考。即使你再严谨,你也有考虑不到的地方,只有热爱,TA才会探索到TA不知道他不知道的。 以上只是我个人的看法,欢迎一起讨论。
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. ...
回滚是一种运维操作。通常发生在部署过程中发现问题,需要将目标环境恢复到部署前的状态。 在我看来,回滚有两种模式。其中一种是一步步执行反向操作,我称之为反向操作模式。 基于反向操作的回滚模式 可能是由于过去手工运维的思维方式的惯性,我发现不少人只知道这一种模式。 比如使用手工部署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。 ...
如何使用 使用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配置部分看起来并不优雅,这是另一个话题。咱们今后再讲。 ...
完整代码在文末 背景 前段时间,线上系统出现了两次持续时间比较长的事故。这两次事故暴露我在某些方面的不足。同时,也意识到在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可以由三部分组成的: ...