软件工程师,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。
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可以由三部分组成的: ...
“本质”类的文章,通常很难带流量。而且写起来非常吃力。 那我为什么还要写?写作是对自己的锻炼。写作是让自己的思想更有深度的一种有效方式。 如果你觉得这篇文章对你有帮助,也你麻烦你转发这篇文章,这是对我的帮助。谢谢。 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。 ...
背景 Prometheus有两个最基本的组件:一个是Prometheus程序,一个是Alertmanager程序。 它们的职责分工很明确: Prometheus程序负责:定时拉取监控指标数据、存储指标数据、根据告警规则发起告警通知; Alertmanager程序负责:负责告警通知的路由,即当接收到Prometheus程序的通知后,该将通知以何种方式通知给谁。 Prometheus程序的配置最核心的配置是: # ... # 当指标数据符合什么规则进行告警通知。 # 在其它文件定义,这里只是引用该文件的路径。 rule_files: [ - <filepath_glob> ... ] # 从哪里,该如何拉取指标 scrape_configs: [ - <scrape_config> ... ] # ... Alertmanager程序的配置最核心的配置是: # ... # 告警通知路由规则 route: [- <route_config>-] # 告警通知的接收者列表,部分监控告警平台也称之为channel receivers: [- <receivers>-] # ... 在实际工作中,Prometheus和Alertmanager的配置会非常大。 严谨的软件工程要求我们在真正部署这些配置前,对其进行有效性和正确性的检查。否则SRE/DevOps的工程效率就会很低,因为你需要手工调试庞大的配置。 所以,我们需要有一种高效率的方式来保证配置的有效性和正确性。 保证Prometheus程序配置的有效性和正确性 promtool Prometheus程序提供了一个叫promtool的命令行程序。解压Prometheus的程序包后,你会发现它和Prometheus程序放在一个文件夹中。 promtool提供了一些子命令来保证Prometheus程序配置的有效性和正确性: # 校验Prometheus配置的有效性,它支持--lint="duplicate-rules"参数,用于检查重复的rule配置 check config [<flags>] <config-files>... # 校验rule配置的有效性 check rules [<flags>] <rule-files>... # 执行rules单元测试用例 test rules <test-rule-file>... 至于有效性检查,只需要执行check子命令即可,不需要过多说明。 ...