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

SRE-DevOps不得不懂的:Prometheus的配置工程化

背景 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子命令即可,不需要过多说明。 ...

2024-02-26 · 2 min · 390 words · 翟志军 Jack Zhai

云原生部署之Helm最佳实践

半年多前,我们从传统的Ansible自动化部署迁移到了云原生部署。我们没有通过Rancher或者KubeSphere这些平台的可视化界面部署,而是选择了Helm这个命令行工具。原因有以下几点: 坚持一切版本化,一切自动化的原则; Helm在声明式思维方面相对其它工具更友好; 方便配置与制品分离; Helm目前有两个版本:v2和v3。幸运的是,我们正准备大规模使用时,v3版本发布。所以,我们没有经历升级之苦。特此说明以下最佳实践基于Helm3。 注:本文针对对Helm有一定基础的同学,如果没有基础,可以先收藏。 正片开始: 自行版本化chart maven、npm等构建工具的包会有一个唯一的官方源,但是,Helm的chart包似乎没有,你会遇到很多不同的源。这对chart的版本控制非常不利,因为你不知道哪天,远端的源就不见了。所以,最好的做法,使用helm pull命令将chart下载本地,然后指定一个版本上传制品库Nexus的Helm仓库中。上传命令为: curl -s -u '${USER_PASS}' --upload-file ${chart_name}-${charts_version}.tgz http://xxx-nexus.com/repository/helm-repo/ 使用upgrade —install子命令部署应用 刚开始学习Helm时,我们通常使用helm install来安装chart。但是,第二次执行helm install,就会报错,因为K8s中已经存在了该chart的release了。这个过程对流水线是不友好的,所以,在流水线,我们使用的是helm upgrade —install xx ./xx.tgz来部署。 尽早标准化应用,标准化chart 如果存在100个微服务,我们是不是要创建100个chart呢?事实上,一开始,我们团队就是这样的。这是因为我们的微服务一开始不够标准化,所以,chart也跟着不同。后来,我们逐渐标准化了应用。chart也变成了标准。也就是所有的后端服务使用的是同一个chart。这样做的还有利于提高我们创建新的微服务的速度。 所谓标准化,指的是pod对外提供服务的端口号、优雅停机、设置环境变量的方法等等这些通用的领域的配置都应该是统一的。 尽量少使用if-else判断 以chart中,我们应该尽量少使用if-else判断。有时,宁愿多写几个YAML也不要在同一个文件嵌套if-else。因为要尽可能的让chart本身所见即所得。 使用template子命令快速调试chart 当我们在开始chart时,每次修改都要执行一次helm upgrade来验证正确性是很不经济的。Helm提供了template子命令,用于验证我们的chart的语法的正确性。示例:helm template <chart的地址>。 定义一个全局的values.yaml chart中的values.yaml文件为我们提供了chart的默认配置。同时,我们可以在执行helm upgrade —install部署chart时,加入-f values.yaml来指定另外的values文件,比如: helm upgrade --install -f ./abc.yaml abc ./abc-chart.tgz 但是,有些配置,是全局性的,比如mysql的url。我们不希望它重复写在不同的应用的配置中。所以,我们定义一个全局的values.yaml。比如:global-value.yaml。helm的命令将变成: helm upgrade --install -f ./global-value.yaml -f ./abc.yaml abc ./abc-chart.tgz 利用helm的-f参数的顺序实现配置的优先级 当全局values文件与应用的values存在配置冲突的时候,通过会采用应用的values文件中的配置。需要注意的是 -f 参数的顺序。后一个 -f 参数的配置会覆盖前一个-f参数的配置。 多版本的实现 过去,我们通常是一个应用一个版本。但是,现在我们更多的是一个应用线上同时存在多个版本。所以,一个chart能同时部署多个版本的应用。 helm upgrade --install -f ./global-value.yaml -f ./abc.yaml --set 'image.tag={1.2.1,1.2.3}' abc ./abc-chart.tgz chart中的deployment文件: ...

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

可落地的云原生应用规范

应用的规范定义是一个权衡的过程,你不能一下把规范定义得太死,太死了导致无法很好的在不同团队推广,最后可能导致规范失去信用。你也不能把规范定义得太泛,导致人们不知道如何下手。 在经历了传统部署(使用Ansible自动化部署应用到虚拟机)和Kubernetes的部署(使用Helm实现自动化部署)后,我们总结出一套云原生应用规范。它无关语言,无关框架,无关部署方式。 定义此云原生应用规范,我们有以下几个目的: 节约人员沟通成本:你不需要像以前那样需要反复的问对方的服务的端口; 节约运维成本:因为应用是标准的,所以,对于所有的应用,只需要使用统一的部署方式、统一的监控方式; 节约开发新应用的成本:根据规范,我们可以搭建各种语言或者框架的工程的脚手架; 以下是规范正文: 业务端口规范 所有的Pod或部署在虚拟机上的应用要求: http协议的服务使用8000端口 grpc协议的服务使用9000端口 如果有其它协议可以在此添加 所有的Service:使用80端口 实践Tips1:遗留工作通常没有统一的端口,我们可以在部署环节通过环境变量来覆盖应用本身的端口来实现统一端口的目的。 实践Tips2:对于虚拟机上的部署,过去,一台机器上我们常常部署多个应用,所以要求每个应用的端口都不能相同。我们的做法,缩小虚拟机的配置,一个虚拟机只部署一个应用。 监控端口规范 监控端口统一使用:30000。监控端口与业务端口分离是基于安全的考虑而设计。而且监控端口只允许内部访问。 提供Prometheus监控接口/private/prom:返回 prometheus标准数据结构; 提供优雅停机接口/private/shutdown:POST请求即代表发起停机操作; 提供健康检查的接口/private/health:http code返回200代表健康; 提供应用ready接口(可以与健康检查接口相同)/private/ready:http code返回200代表ready; 提供实时修改日志级别的接口:/private/loggers 。这个接口,我们可以参考Java的Logback框架的实现。 Docker镜像规范 提供 curl 命令行,因为我们需要使用命令进行优雅机器:curl -XPOST <http://127.0.0.1:30000/private/shutdown; 应用需要提供应用进程的环境配置入口。比如JVM应用需要提供 JAVA_OPTS 的环境变量设置 实践Tips:实际工作中,可创建一些基础镜像方便开发人员使用。 日志规范 日志要求统一输出到console。输入日志统一使用json结构。json结构中必须包含字段: @timestamp: 日志打印时间 thread_name:进程名 level:日志级别 appId: 应用标识,同一个namespace全局唯一 namespace:用于隔离app,租户的功能 env:环境标识 ver:应用版本 msg:帮助debug问题的 traceId: 其实就是traceId 同时,我们建议使用以下通用字段: event:代表事件,建议不要使用带空格的字符串 method:方法名 result:代表执行结果,可以是方法的返回结果,也可以http方法的response req:代表请求参数体 以下是日志示例: {"@timestamp":"2021-07-15T17:24:07.912+08:00","userName":"Foobar","thread_name":"http-nio-8080-exec-150","level":"INFO","appId":"UserService","env":"prod","ver":"v1.0-5598","event":"register_user"} 实践Tips1:在实际工作,我们需要为不同的语言实现符合此日志规范的框架。 实践Tips2:日志规范需要配合日志处理环节考虑,在日志处理环节没有准备好之前,保持原样是更明智的选择。 小结 此规范已经在我们团队实践一年多。正在向其它团队延伸的过程。不敢说它是一套面面俱到的规范,但是它是一套能在一些团队进行落地的规范。 每个人都存在认知不足的情况,我也是人,所以我也不例外。此规范只是版本1.0。将来发现不足,持续改进。

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