disgare 的博客
首页
博客
分类
标签
首页
博客
分类
标签
  • 网络

    • 计算机网络学习笔记
    • 网络安全相关
    • 域名和子网掩码
    • CORS 跨域资源共享
    • DNS、HTTP 与 HTTPS
    • Server-Sent Events (SSE)
    • WebSocket 长连接
  • 计算机基础

    • 操作系统 IO 相关知识
    • 操作系统学习笔记
    • 程序的机器级表示
    • 音频文件基础
    • 正则表达式相关概念
    • ffmpeg 的安装以及实现音频切分功能
    • Hex 和 Base64 编码
    • XML 的使用
  • 数据结构与算法

    • 动态规划算法学习笔记
    • 基于比较的排序算法的最坏情况下的最优下界为什么是O(nlogn)
    • 集合与数据结构学习笔记
    • 面试常见算法总结
    • 算法导论第二部分排序学习笔记
    • 算法导论第一部分学习笔记
  • Java

    • 对象之间的映射与转换
    • 反射学习笔记
    • 泛型相关概念
    • 关于 boolean 类型的坑
    • 如何使用 lambda 表达式实现排序
    • CompletableFuture 相关用法
    • CompletableFuture 源码浅要阅读
    • FutureTask 源码阅读
    • Guava 常用 API
    • Guava 源码阅读:Multimap 相关
    • Jackson 的各种使用
    • Java 的 Excel 相关操作
    • java 的常见性能问题分析以及出现场景
    • java 基础知识
    • JAVA 枚举的基础和原理
    • Java 图片文件上传下载处理
    • Java 序列化
    • Java 异常
    • Java 语法糖
    • Java 中关于字符串处理的常用方法
    • Java 中强、软、弱、虚引用
    • JAVA 注解小结
    • Java Http 访问框架
    • Java Stream 的使用
    • Java8 新特性
    • netty 学习笔记
    • Scanner 的各种用法
    • Servlet 学习笔记
    • String、StringBuffer、StringBuilder 学习笔记
  • JVM

    • 虚拟机执行子系统
    • JVM 自动内存管理
    • Linux 中 JVM 常用工具以及常见问题解决思路
  • Linux

    • crontab 表达式
    • Linux 常见命令
    • Linux 文件系统
  • 中间件

    • 关于定时任务原理
    • 详解 kafka
    • ES 搜索引擎
    • flink 提交流程
    • Grape-RAG
    • Hadoop 基础原理
  • 多线程

    • 多线程基础学习笔记
    • 简单了解并发集合
    • 如何手写单例
    • 深入理解 java 多线程安全
    • 生产者消费者问题
    • 线程池作用、用法以及原理
    • AQS 组件
    • ThreadLocal 原理以及使用
  • 非关系型数据库

    • Redis 集群
    • Redis 数据结构、对象与数据库
    • Redis 学习笔记
  • 关系型数据库

    • B+ 树的插入、删除和数据页分裂机制
    • MySQL 的 binglog、redolog、undolog
    • MySQL 的记录存储结构、存储引擎与 Buffer Pool
    • MySQL 基本的特性
    • MySQL 开发规范
    • MySQL 事务与锁与 MVCC
    • MySQL 数据类型、字符集相关内容
    • MySQL 索引与索引优化
    • PostgreSQL 更新数据时 HOT优化
    • PostgreSQL 相关用法
  • Python

    • Python 基础语法
    • Python 学习
  • Spring 项目

    • Lombok 的常用注解
    • maven 小结
    • MyBatis 框架的使用
    • MyBatis 重要知识点总结
    • MybatisPlus 的使用
    • Spring 框架基础使用
    • Spring 事务相关
    • Spring IOC 的原理及源码
    • Spring AOP 的使用和原理
    • SpringBoot 的原理
    • SpringBoot 基础使用
    • SpringWeb 重要知识点
  • 分布式

    • 初步了解 docker
    • 从 ACID 到 BASE 事务处理的实现
    • 访问远程服务
    • 分布式 id
    • 分布式缓存相关问题
    • 分布式集群理论和分布式事务协议
    • 分布式架构的观测
      • 日志
        • 日志的输出
        • 收集与缓冲
        • 加工与聚合
        • 存储与查询
        • Kibana 标准查询语法
      • 追踪
        • 数据收集
        • 业务追踪
      • 度量(监控和预警)
        • 指标
        • 数据采集方式
        • 异常打点最佳实践
    • 分布式一致性算法
    • 负载均衡 Load Balancing
    • 关于分布式系统 RPC 中高可用功能的实现
    • 集群间数据同步的目的
    • 三高问题下的系统优化
    • 数据库分库分表
    • 详解 Spring Cloud
    • Dubbo 基础概念
    • Gossip 协议
    • nginx 学习笔记
    • Protobuf 通信协议
    • Zookeeper 基础学习
  • 架构设计

    • 参数校验与异常处理
    • 抽象方法与设计模式
    • 代码整洁之道
    • 权限系统设计
    • 用低内存处理大量数据
    • 设计模式——策略模式
    • 设计模式——过滤器模式在 Spring 中的实践
    • 状态模式
    • 统一结果返回
    • 为什么要打日志?怎么打日志?打什么日志?
    • 运维监控常见指标含义
    • 资深研发进阶
    • DDD 架构学习笔记
    • Java 常用的规则引擎
    • MVC 架构学习笔记
  • AI

    • 如何编写 Prompt
    • Agent 工程架构
    • LLM 相关内容
    • NLP 相关知识
    • vibe coding 最佳实践
    • windows 下 ollama 迁移到 D 盘
  • 开发工具

    • 如何画时序图、流程图、状态流转图
    • excel 关于 =vlookup 的用法
    • git 的学习以及使用
    • IDEA 插件推荐
    • IDEA 常用快捷键以及调试
    • Shell 脚本
    • swagger 的使用
  • 前端

    • 简单了解前端页面开发
    • 伪静态是什么
    • GitHub Pages 部署教程
    • Vercel 部署教程
    • vue-admin-template 简单使用
    • VuePress 博客搭建指南
  • 项目

    • 面试刷题网——技术方案
    • 影视资源聚合站——技术方案
  • 问题记录

    • 定时任务单线程消费 redis 中数据导致消费能力不足
    • 提供可传递的易受攻击的依赖项
    • Liteflow 在 SpringBoot 启动时无法注入组件问题 couldn‘t find chain with the id[THEN(NodeComponent)]
  • 金融

    • 股票分析——关于电力
    • 股票技术面——量价关系
    • 股票技术面——盘口
    • 股票技术面——基础
    • 基础的金融知识
    • 基金与股票
    • 韭菜的自我总结
    • 聊聊价值投资
  • 其他

    • 程序员职场工作需要注意什么
    • 创业全链路SOP:从灵光一现到系统化增长的实战指南
    • 观罗翔讲刑法随笔
    • 价格和价值
    • 立直麻将牌效益理论
    • 梅花易数学习笔记
    • 压力管理
2023-08-08
架构设计
目录

分布式架构的观测

在一个分布式应用中,如果出现了某个异常,那我们必然不可能只依靠 awk、grep 等命令来查看日志分析问题,往往分布式架构的一个异常都贯通多个节点,我们需要将多个节点联系起来排查问题。这就引出了分布式架构的可观测性,可观测性越高,排查问题越轻松

学术界一般会将可观测性分解为三个更具体方向进行研究,分别是:事件日志、链路追踪和聚合度量,这三个方向各有侧重,又不是完全独立

# 日志

日志的职责是记录离散事件,通过这些记录事后分析出程序的行为,譬如曾经调用过什么方法,曾经操作过哪些数据

如何打出优秀的日志是程序员的基本功,如果日志量太大会造成 OOM,如果日志经常打 error 会导致监控报警特别大

只要稍微复杂点的系统,尤其是复杂的分布式系统,往往还要有专门的全局查询和可视化功能。此时,从打印日志到分析查询之间,还隔着收集、缓冲、聚合、加工、索引、存储等若干个步骤。我们简单的梳理一下日志存储与打印的流程,下面所说的流程,就是 ELK 技术栈

# 日志的输出

日志输入就是我们在代码中使用 log 提供的方法输出,当然 print 等方法也可以,不过不推荐。这块主要是开发的工作,打出优秀的日志可以便于排查问题,我们应该尽可能的避免如下几点:

  • 日志不能太多,也不能太少,不必把上下文的所有消息都打进去,否则会造成 IO 问题。有些不必要的 info 日志,在测试的时候可以打出来用于排查问题,但是项目上线后就需要删掉
  • 避免打印敏感信息。不用专门去提醒,任何程序员肯定都知道不该将密码,银行账号,身份证件这些敏感信息打到日志里
  • 避免引用慢操作。日志中打印的信息应该是上下文中可以直接取到的,如果当前上下文中根本没有这项数据,需要专门调用远程服务或者从数据库获取,又或者通过大量计算才能取到的话,那应该先考虑这项信息放到日志中是不是必要且恰当的

我们应当尽可能做到如下几点:

  • 处理请求时的 TraceID
  • 系统运行过程中的关键事件。日志的职责就是记录事件,进行了哪些操作、发生了与预期不符的情况、运行期间出现未能处理的异常或警告、定期自动执行的任务,等等,都应该在日志中完整记录下来
  • 启动时输出配置信息

# 收集与缓冲

写日志是在服务节点中进行的,但我们不可能在每个节点都单独建设日志查询功能。这不是资源或工作量的问题,而是分布式系统处理一个请求要跨越多个服务节点,为了能看到跨节点的全部日志,就要有能覆盖整个链路的全局日志系统。这个需求决定了每个节点输出日志到文件后,必须将日志文件统一收集起来集中存储、建立索引,由此便催生了专门的日志收集器

为了处理多个节点的协调,除了今天流行的日志收集器,轻量高效的 Filebeat 以外,还需要有专门用于其他功能的组件,比如用于审计数据的 Auditbeat、用于无服务计算架构的 Functionbeat、用于心跳检测的 Heartbeat 等等

至于缓冲,是指流量大的时候,会有过多的日志打进 DB,导致数据库压力过大。一种最常用的缓解压力的做法是将日志接收者从 Logstash 和 Elasticsearch 转移至抗压能力更强的队列缓存,譬如在 Logstash 之前架设一个 Kafka 或者 Redis 作为缓冲层

# 加工与聚合

将日志集中收集之后,存入 Elasticsearch 之前,一般还要对它们进行加工转换和聚合处理。这是因为日志是非结构化数据,一行日志中通常会包含多项信息,如果不做处理,那在 Elasticsearch 就只能以全文检索的原始方式去使用日志,既不利于统计对比,也不利于条件过滤

为了加快查询速度,我们必须要建立索引,但是对一串巨大的字符串直接建立索引肯定不是优秀的解法,我们可以根据输出日志的特征、形式,来对字符串做拆分,拆出来的数据就可以建立不同的索引了

这就引出了我们处理加工聚会的工具 Logstash。Logstash 的基本职能是把日志行中的非结构化数据,通过 Grok 表达式语法转换为上面表格那样的结构化数据,然后以 JSON 格式输出到 Elasticsearch 中(这是最普遍的输出形式,Logstash 输出也有很多插件可以具体定制不同的格式)

经过 Logstash 转换,已经结构化的日志,Elasticsearch 便可针对不同的数据项来建立索引,进行条件查询、统计、聚合等操作的了

# 存储与查询

在经过一系列的数据处理后,我们终于可以把数据放进 DB 了,也就是 ES Elasticsearch

Elasticsearch 只提供了 API 层面的查询能力,它通常搭配同样出自 Elastic.co 公司的 Kibana 一起使用,可以将 Kibana 视为 Elastic Stack 的 GUI 部分

Kibana 尽管只负责图形界面和展示,但它提供的能力远不止让你能在界面上执行 Elasticsearch 的查询那么简单。Kibana 宣传的核心能力是“探索数据并可视化”,即把存储在 Elasticsearch 中的数据被检索、聚合、统计后,定制形成各种图形、表格、指标、统计,以此观察系统的运行状态,找出日志事件中潜藏的规律和隐患

# Kibana 标准查询语法

一般来说,直接加上双引号查询肯定没错,比如:

"hello"
1

标准的查询需要按照 key:value 形式。key 用于指定查询的字段类型

local_ip:111.111.111.111
traceid:EMqATv-DAooV
1
2

数字范围查询和字段查询通过 AND、OR 等结合使用,注意连接符需要大写

"value1" AND "value2"
1

# 追踪

微服务时代,追踪就不只局限于调用栈了,一个外部请求需要内部若干服务的联动响应,这时候完整的调用轨迹将跨越多个服务,同时包括服务间的网络传输信息与各个服务内部的调用堆栈信息。追踪的主要目的是排查故障,如分析调用链的哪一部分、哪个方法出现错误或阻塞,接口的输入输出是否符合预期

# 数据收集

追踪数据一般可以使用以下三种方式来实现

  • 基于日志的追踪:我们将 TrackID 打进日志里,然后随着所有节点的日志归集过程汇聚到一起,再从全局日志信息中反推出完整的调用链拓扑关系。打入日志这个操作完全可以由插件实现,做到对开发透明,但是缺点是由于日志归集不及时或者精度丢失,导致日志出现延迟或缺失记录的话,会进而产生追踪失真
  • 基于服务的追踪:这是目前最主流的追踪方式,服务追踪的实现思路是通过某些手段给目标应用注入追踪探针(Probe),探针在结构上可视为一个寄生在目标服务身上的小型微服务系统,把从目标系统中监控得到的服务调用信息,通过另一次独立的 HTTP 或者 RPC 请求发送给追踪系统
  • 基于边车代理的追踪:我们知道边车模式是对服务加一层代理,通过代理来对外层进行交互。边车代理是服务网格的专属方案,也是最理想的分布式追踪模型。它对应用完全透明,无论是日志还是服务本身都不会有任何变化;它与程序语言无关,无论应用采用什么编程语言实现,只要它还是通过网络来访问服务就可以被追踪到

# 业务追踪

Dapper 提出了追踪与跨度两个概念。从客户端发起请求抵达系统的边界开始,记录请求流经的每一个服务,直到到向客户端返回响应为止,这整个过程就称为一次追踪(Trace)。由于每次 Trace 都可能会调用数量不定、坐标不定的多个服务,为了能够记录具体调用了哪些服务,以及调用的顺序、开始时点、执行时长等信息,每次开始调用服务前都要先埋入一个调用记录,这个记录称为一个跨度

让我举个例子来说明上面文字的重要性:

现在有一个很长的业务场景:用户下单,其中必定经过很多流程,比如拿产品信息,校验用户信息,生单,回调,通知其他系统,调支付接口,等等等等,如果用户生单失败了,他们很有可能过来问我们发生了什么问题,这时候如果我们的日志打的很杂很乱,不记录关键步骤,这时候查这个问题就会非常麻烦

而如果我们将这些关键节点记录下来(比如拿完商品信息记录一下,生单完毕记录一下),记录到一个专门的业务日志中,比如 core.log,这时候我们查问题就会非常简单了,只需要拿一个可以贯穿整个流程的 ID(比如用户 ID)去这个日志中搜一下,就可以拿到数据了。这个日志甚至可以放权给运营或者用户自己使用

# 度量(监控和预警)

度量是指对系统中某一类信息的统计聚合。譬如,证券市场的每一只股票都会定期公布财务报表,通过财报上的营收、净利、毛利、资产、负债等等一系列数据来体现过去一个财务周期中公司的经营状况,这便是一种信息聚合。像是任务管理器,就是度量的一种

度量(Metrics)的目的是揭示系统的总体运行状态,因此可以拆分为监控(Monitoring)和预警(Alert),如某些度量指标达到风险阈值时触发事件,以便自动处理或者提醒管理员介入

打完了日志之后,别忘了监控报警。在我们做好核心日志追踪后,可以接着搭建业务追踪告警。这样在项目发布时,如果变更比较多的情况下,可以通过观察告警指标来观察我们的系统是否健康。而告警指标可以通过项目的核心链路来搭建

# 指标

指标收集部分要解决两个问题:如何定义指标以及如何将这些指标告诉服务端, 如何定义指标这个问题听起来应该是与目标系统密切相关的,必须根据实际情况才能讨论,其实并不绝对,无论目标是何种系统,都是具备一些共性特征。确定目标系统前我们无法决定要收集什么指标,但指标的数据类型(Metrics Types)是可数的,所有通用的度量系统都是面向指标的数据类型来设计的:

  • 计数度量器(Counter):这是最好理解也是最常用的指标形式,计数器就是对有相同量纲、可加减数值的合计量,譬如业务指标像销售额、货物库存量、职工人数等等;技术指标像服务调用次数、网站访问人数等都属于计数器指标;预警指标是监控系统健康性最重要的数据,包含发生异常次数以及时间
  • 吞吐率度量器(Meter):吞吐率度量器顾名思义是用于统计单位时间的吞吐量,即单位时间内某个事件的发生次数。譬如交易系统中常以 TPS 衡量事务吞吐率,即每秒发生了多少笔事务交易
  • 时间度量器(Timer):用来统计一个方法或者一个接口的执行时间,然后我们收集 P98 或者 P95 的信息来进行预警,对接口优化

我们现在定义了如何收集这些数据,但是还没有定义如何使用这些数据。优秀的使用方式同样重要,如果错误的使用指标(比如只要每个计数度量器一分钟记了一次就报警一次),监控会瞎报警,从而让程序员忽视掉真正有用的报警。这个问题被称作报警噪音

# 数据采集方式

而如何将这些指标告诉服务端这个问题,通常有两种解决方案:拉取式采集和推送式采集,所谓 Pull 是指度量系统主动从目标系统中拉取指标,相对地,Push 就是由目标系统主动向度量系统推送指标。不管是拉取还是推送,在进行操作之前,指标数据会记在内存中

存放指标一般不会选择 MySQL 或者 PG 等关系型数据库,而是时序数据库。时序数据库用于存储跟随时间而变化的数据,并且以时间(时间点或者时间区间)来建立索引的数据库

时间序列数据是历史烙印,具有不变性、唯一性、有序性。时序数据库同时具有数据结构简单,数据量大的特点

时序数据通常只是追加,很少删改或者根本不允许删改。针对数据热点只集中在近期数据、多写少读、几乎不删改、数据只顺序追加这些特点,时序数据库被允许做出很激进的存储、访问和保留策略:

  • 以日志结构的合并树代替传统关系型数据库中的 B+Tree 作为存储结构,LSM 适合的应用场景就是写多读少,且几乎不删改的数据
  • 设置激进的数据保留策略,譬如根据过期时间自动删除相关数据以节省存储空间,同时提高查询性能。对于普通数据库来说,数据会存储一段时间后就会被自动删除这种事情是不可想象的
  • 对数据进行再采样以节省空间,譬如最近几天的数据可能需要精确到秒,而查询一个月前的数据时,只需要精确到天,查询一年前的数据时,只要精确到周就够了,这样将数据重新采样汇总就可以极大节省存储空间

# 异常打点最佳实践

异常打点是计数度量器中一种比较特殊的打点,他可以让我们观测到系统中的问题,他可能会打在接口出现异常、系统抛出异常、用户被异常拦截时,如何规整他们成了一个问题

为了更好的度量我们的系统,以下有一些类似最佳实践的经验,先说一下在什么地方打点,最最简单的度量就是帮助我们快速召回系统中的问题,即让系统中的每个 bug 都由我们程序员提前发现,这是非常有用的,用户没有发现的 bug 就不算 bug。因此,系统中每次抛出异常的时候,我们可以在以下位置打点:

  • 1,定时任务 try catch 单独打点,如果可以抛出异常,可以使用切面打点
  • 2,rpc 接口如果不能直接把异常抛出,并且返回值没有统一的格式,也是单独打点。如果可以抛出异常,则使用切面打点
  • 3,MQ 的处理和上面一样
  • 4,如果是 http 接口并且抛出异常的话,可以使用全局异常处理器打点
  • 5,如果 http 接口是正常的返回,但是返回的业务码不是正常的,可以切面打点(需要收集系统中存量的所有返回结构数据,比如什么 CommonQueryResult、CommonResult 等,一个系统中往往有多个统一结果返回,但是每个返回都有 code,我们可以根据 code 来区分是异常还是正常返回)
  • 6,不要在 service 层拦截,会打两次点的
  • 7,请注意一些拦截或者异常信息,我们需要做三件事情:详细的打点、日志、返回给用户合理的提示

知道了在什么地方打点,我们还需要知道针对什么异常打点,如果每次出现已经预知的异常或者业务异常都给我们发告警,未免太影响休息了,因此我们可以对异常分级,分级的意思是将异常分为 info、error、warn 级别,比如:

  • 用户传入参数没有通过校验、用户无权限、输入的字符串过长等业务异常问题,可以将异常记为 info 级别。这些异常不需要重点关注,但是如果一个小时内出现了十万条这样的点,那说明要么是代码写错了,要么是有人在刷我们接口,无论如何,此时都需要通知我们处理
  • 下游接口调用失败、RPC 调用失败等问题,可以将异常标记为 warn,因为这不是我们系统导致的,确保我们做好了异常降级,然后打点即可。我们可以在看到这些异常后疯狂艾特下游,但是不用过多关注这些 warn
  • 空指针等系统异常都需要标记为 error,但凡是系统异常影响用户体验的问题,都必须是 error,一旦出现就需要报警,快速处理,不让用户感知

举个例子:

public enum ExceptionLevelEnum {
    // 系统异常、重要的业务异常,打一次就会报警
    ERROR,
    // 降级的异常
    WARN,
    // 正常的返回
    INFO,
}

	......
	SERVICE_ERROR(0, "系统错误", ExceptionLevelEnum.ERROR),
    BIZ_ERROR(101, "业务错误", ExceptionLevelEnum.WARN),
    LOCK_ALREADY_EXISTS(102, "操作太频繁,请稍后再重试", ExceptionLevelEnum.INFO),
1
2
3
4
5
6
7
8
9
10
11
12
13

我们拿到枚举值中的 LevelEnum 后,就知道需要打什么级别的日志了

我们在打点的时候所关注的核心数据是什么呢?我们在打点需要打出哪些数据呢:哪里的方法抛出的异常(方法名),异常内容,异常分级级别,要是分不了也没关系,只打印最核心的系统异常(因为代码问题导致流程中断的异常)即可

这里推荐将一些通用的问题规整到同一个打点中,这样做后续新增异常指标时就不用新增报警了。一些有业务语意的才需要做特殊的打点

我们还可以将异常返回和统一返回进行收口,这么做的好处是状态码统一了,异常返回具备以下信息:错误码、错误信息、异常类型;统一返回具备以下信息:业务状态码(可统一为错误码)、message(对应错误信息)。因此两个返回可以收口到同一个错误码中

// 系统错误
public class SystemException extends Exception {
    private long code;
    private String message;
    private ExceptionLevelEnum exceptionLevelEnum;

    public SystemException(long code, String message, ExceptionLevelEnum exceptionLevelEnum) {
        this.message = message;
        this.code = code;
        this.exceptionLevelEnum = exceptionLevelEnum;
    }

    public SystemException(SystemExceptionEnum exceptionEnum) {
        this.message = exceptionEnum.getMessage();
        this.code = exceptionEnum.getCode();
        this.exceptionLevelEnum = exceptionEnum.getLevel();
    }
}

// 系统异常枚举
@Getter
public enum SystemExceptionEnum {
    // 基础错误码
    NO_ERROR(0, "成功", ExceptionLevelEnum.INFO),
    BUSINESS_EXCEPTION(1, "业务异常", ExceptionLevelEnum.ERROR),
    OTHERS(2, "其他", ExceptionLevelEnum.ERROR);

    private final long code;
    private final String message;
    private final ExceptionLevelEnum level;

    SystemExceptionEnum(long code, String message, ExceptionLevelEnum level) {
        this.code = code;
        this.message = message;
        this.level = level;
    }

    public static SystemExceptionEnum getByCode(long code) {
        for (SystemExceptionEnum value : SystemExceptionEnum.values()) {
            if (value.getCode() == code) {
                return value;
            }
        }
        return OTHERS;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

当你打点的时候,可以使用系统异常和返回码打点,下面是一个例子:

    public static final String CODE = "code";
    public static final String MESSAGE = "message";
    public static final String URL = "url";
    public static final String LEVEL = "level";

    // 普通异常打点,用于监控系统中出现的业务异常和系统异常
    public static void dot(SystemExceptionEnum exceptionEnum, String url) {
        MetricUtil.dot(ERROR_CNT_EXCEPTION,
                CODE, String.valueOf(exceptionEnum.getCode()),
                MESSAGE, exceptionEnum.getMessage(),
                URL, url,
                LEVEL, exceptionEnum.getLevel().name());
    }

    public static void dot(SystemException exception, String url) {
        MetricUtil.dot(ERROR_CNT_EXCEPTION,
                CODE, String.valueOf(exceptionEnum.getCode()),
                MESSAGE, exception.getMessage(),
                URL, url,
                LEVEL, exception.getLevel().name());
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#可观测性
最后更新: 2/28/2026, 11:31:29 AM
分布式集群理论和分布式事务协议
分布式一致性算法

← 分布式集群理论和分布式事务协议 分布式一致性算法→

最近更新
01
vibe coding 最佳实践
02-24
02
立直麻将牌效益理论
02-23
03
伪静态是什么
02-08
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式