观测云链路追踪分析最佳实践

背景

如果要在开发、运维和工程层面持续改进一个涉及多服务的应用,以链路追踪、日志检索、指标收集、用户体验监测、性能剖析、关联分析等作为代表性技术的可观测性必不可少,这一看法已成为共识,但在采用这项技术的过程中,如何分析相关数据对很对人来说任然存在一些困难和误区,本文尝试通过几个浅显的例子说明分析链路追踪数据时会遇到的一些问题,并不会严谨论述相关概念。

示例应用

本文采用一个部署在 K8s 的前后端分离应用 RuoYi-Vue 作为案例介绍相关议题,应用拓扑如下:

各个组件介绍如下:

  • ruoyi_vue_web,WEB SPA 应用,用户通过浏览器访问,引入观测云用户体验监测 SDK;
  • ruoyi-vue-ui,前端应用,Vue 框架,运行于 Nginx,引入 DataDog ddtrace 探针;
  • ruoyi-vue-admin,后端应用,SpringBoot 框架,引入 DataDog ddtrace 探针;
  • ruoyi-vue-mysql,持久存储;
  • ruoyi-vue-redis,缓存;

观测云作为一个数据平台,支持接入市面上主流的开源探针,并将数据转换为观测云定义的规范,指出这一点的原因是,不同探针在链路传播细节、Span 粒度上存在差异,本例将遵循 DataDog ddtrace 探针的 Span 粒度设定。

实验场景

我们将分析示例应用在用户登录过程中产生的追踪信息,用户视角的具体操作为:

  1. 浏览器访问应用;
  2. 输入用户名、错误密码和验证码;
  3. 点击登录按钮;
  4. 输入用户名、正确密码和验证码;
  5. 点击登录按钮;

基本概念

当我们以用户视角执行实验场景中指出的操作时,整个过程产生以下 Trace:

它们并不能全部映射到用户在浏览器上的每一次操作。实际上,Trace 是一个事务,这就意味着它有一个明确的开始,可能还具有一些可拆分的细节,在传统的链路追踪中,将从对某后端接口的调用开始追踪,假设一个登录接口作为事务的开始被调用,那么查询会话缓存、验证密码等都将作为这个事务的细节,直到该登录接口定义的逻辑被全部完成时事务结束(错误终止也是逻辑的一部分),这些细节在追踪中被称为 Span,具备明确的开始和结束时间,显然,第一个 Span 就是对登录接口的调用,被称为 Root Span,以图形化的语言简单描述 Trace 和 Span 的关系如下:

简单分析可知,追踪体系中包含以下维度:

  • Trace 中涉及的服务;
  • 不同 Span 的执行时序,表现为横轴;
  • Trace 中 Span 的拓扑结构,表现为纵轴;

时序方面比较容易理解,它的挑战在于无法跨基础设施维持时间同步,因此引入了拓扑来描述 Trace 的结构,具备相同 Trace ID 的 Span 之间通过父子标识进行关联,即每个 Span 除自身的 Span ID 外,还包含其父级 Span 的 ID,当某个 Span 的父级 Span ID 为 0 时,它将被认为是整个 Trace 的开始,即 Root Span,但 Root Span 并不作为一种 Span 的类型存在,而是被视为父级 Span ID 为 0 的 Entry Span,Entry Span 是调用进入某个服务时的第一个 Span,即观测云界面中的顶层 Span,我们将在后续的分析实践中详细说明这些问题。

本场景的不同之处在于,追踪的起始点前移至 WEB 应用,用户在页面的点击、浏览器运行 js 程序、前端应用逻辑执行等都可能触发接口调用作为链路的起始点,比如 Trace 列表中资源名为“GET /prod-api/profile/avatar/2024/08/24/月之暗面_20240824210241A001.jpg”的 Trace 就是由前端应用的逻辑触发的,它完成了获取用户头像的行为,其他 Trace 在浏览器端触发。

我们已经基本讨论完了 Trace 内 Span 的顺序问题,接下来讨论下 Trace 之间的顺序问题,系统的真实行为是按照前文 Trace 列表中的顺序发生的吗?可以看到获取用户头像竟然提前到了成功登录之前,其实这并不是一个安全漏洞,原因之前已经提到,获取用户头像是前端应用发起的链路,而其他是浏览器发起的链路,当浏览器与前端应用所在服务器的时间不同步时就会出现这种混乱的情况,这并不是链路追踪应该解决的问题,实际上也很难解决,通过更丰富的上下文降低判断这种情况下 Trace 顺序的难度是一种解决方案,不再展开。那么真实的顺序是怎样的呢,RuoYi-Vue 是一个开源项目,相关逻辑在文件 ruoyi-ui/src/store/modules/user.js 中:

...
GetInfo({ commit, state }) {return new Promise((resolve, reject) => {getInfo().then(res => {const user = res.userconst avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
...

可知,获取用户头像发生在“/prod-api/getInfo”之后,如果用户头像为空则使用静态资源中的默认头像。从浏览器开发者工具的输出中很容易发现这些行为发生的正确时序:

链路分析

在这个章节,我们将具体分析实验场景中的一些 Trace,除了使用真实数据验证基本概念中的内容,还会说明一些数据分析过程中容易产生的困惑。

前端服务向后端服务请求用户头像

先从获取用户头像的过程说起,这是一则获取用户头像失败的例子,当 ruoyi-vue-ui 请求头像图片时,返回了 404 响应码,我们分析一下这个过程。

此 Trace 涉及两个服务,ruoyi-vue-ui 使用 HTTP 方式向 ruoyi-vue-admin 请求头像图片,Root Span 在 ruoyi-vue-ui 创建,它的状态被标记为绿色的“404 OK”,需要注意的是, Span 被填充为红色与 404 或任何错误无关,只是用于区分不同的服务。熟悉 HTTP 的人知道,404 意味着一个客户端错误,请求了一个不存在的资源,既然是错误,为什么会显示为“404 OK”,这与通常的“200 OK”在直觉上相悖,我们需要分析这个 Span 的细节,可以将 Span 在形式上简单理解为一个 Json,观测云界面的“链路详情”页签显示了它,摘取一些本例中需要的关键信息:

{"service": "ruoyi-vue-ui","resource": "GET /prod-api/profile/avatar/2024/08/24/月之暗面_20240824210241A001.jpg","http_status_code": "404","http_status_group": "4xx","trace_id": "5081596974455082565","parent_id": "0","span_type": "entry","span_id": "5081596974455082565","status": "ok","operation": "nginx.handle","message": {"meta": {"component": "nginx","nginx.worker_pid": "29","upstream.name": "ruoyi-vue-admin.ruoyi-vue",}}
}

可以看到,请求资源、HTTP 响应码等以键值对的形式存在于 Span 中,span_id 指当前 Span 的 ID,由于它是进入 ruoyi-vue-ui 服务的第一个 Span,所以它被标记为 entry 类型,parent_id 指当前 Span 的父级 Span,为 0 表示当前 Span 是整个链路的 Root Span,status 字段表示了当前 Span 的状态,ok 代表此 Span 未发生错误。

实际上,观测云界面显示的“404 OK”是 HTTP 响应码和 Span 状态的组合,他们并不存在冲突,关键是理解两种状态的含义和习惯用法,当服务端返回 404 时,只是在按照约定向客户端报告没有找到相关的资源,这可能是因为资源被意外删除(本文所示的情况),也可能是因为请求的资源路径存在错误,无论如何,习惯上服务端不会为 404 抛出异常,这也是 SpringBoot 的默认行为,实际上抛出错误时的栈追踪是要付出一定成本的。不过这种页面标记并不影响我们为 404 配置告警规则。

看下一个 Span:

照例,从链路详情页签中摘取一些关键信息:

{"service": "ruoyi-vue-admin","resource": "GET /prod-api/profile/avatar/2024/08/24/月之暗面_20240824210241A001.jpg","http_status_code": "404","http_status_group": "4xx","trace_id": "5081596974455082565","parent_id": "5081596974455082565","span_type": "entry","span_id": "3515512906328470393","status": "ok","operation": "servlet.request","message": {"meta": {"component": "tomcat-server","thread.name": "http-nio-8080-exec-40",}}
}

可以看到,当前 Span 是 ruoyi-vue-admin 服务的顶层 Span,Servlet 分配线程 http-nio-8080-exec-40 来完成请求,它的 parent_id 指向上一个 Span。

继续向下看,Span 的粒度不止于服务,而是深入了线程内部的细节:

{"service": "ruoyi-vue-admin","resource": "ResourceHttpRequestHandler.handleRequest","trace_id": "5081596974455082565","parent_id": "3515512906328470393","span_type": "local","span_id": "338096583765508850","status": "ok","operation": "spring.handler","message": {"meta": {"component": "spring-web-controller","thread.name": "http-nio-8080-exec-40",}}
}

{"service": "ruoyi-vue-admin","resource": "HttpServletResponse.sendError","trace_id": "5081596974455082565","parent_id": "338096583765508850","span_type": "exit","span_id": "3996052880274433699","status": "ok","operation": "servlet.response","message": {"meta": {"component": "java-web-servlet-response","thread.name": "http-nio-8080-exec-40",}}
}

可以看到,线程内非入口或末端的 Span 被标记为 local 类型,末端 Span 被标记为 exit 类型。我们不再详细查看剩余的两个 Span,他们实质上指出了 SpringBoot 将 404 错误返回为一个 Json 的处理过程,在基本概念章节的浏览器开发者工具截图中已经体现了这个 Json。

本节的最后,再来看下能够正常获取头像的两种情况:

1、客户端缓存了头像的图片资源,携带缓存资源的最后更新时间请求服务端,服务端发现缓存资源任然是最新的,响应码是 304,告知客户端使用缓存的资源:

2、客户端缓存的资源已被更新,返回更新后的资源,响应码是 200:

完成对这个 Trace 的分析后,一个重要的结论是:我们可以通过追踪了解请求在服务间和服务内触发的行为,并能获得过程中性能相关的统计数据,细致程度有赖于 Span 的粒度,不过越细的粒度就要付出越多的性能成本,权衡这种付出和收益是组织采用链路追踪技术时的考虑点之一。对于开发和运维角色来说,追踪的锋芒才刚刚展现。

使用错误的密码登录

接下来我们看一个更加复杂的 Trace,它的复杂性体现在以下几个方面:

  • 由用户在浏览器中的应用界面触发;
  • 对 Redis 和 MySQL 有多次调用;
  • Redis 和 MySQL 中并未引入探针;

我们将使用多种视图进行链路分析。火焰图快速揭示了一个完整的用户登录过程:

Trace 从前端 WEB 应用 ruoyi_vue_web 开始,经过了 WEB 服务器 ruoyi-vue-ui、后端服务 ruoyi-vue-admin,后端服务多次调用缓存 ruoyi-vue-redis 和存储 ruoyi-vue-mysql,校验用户名和密码的 Span 被标记为错误 Span。此例 Root Span 与上例最大的不同是下方显示了浏览器端资源加载的时间分布,例如建立连接用时、返回数据等待用时等,该 Span 包含了从前端收集的众多信息。

直接分析请求进入后端的入口 Span:

点击该 Span,下方的标签页新增“错误详情”页签,但该 Span 并未被标记为错误,实际上当前 Span 和后续 Span 都是在同一个线程内发生的,所以线程内其他 Span 的异常会抛出到栈底,也就是当前 Span 这里,真实的错误发生在下级 Span:

在这个 Span 的链路详情中 status 的值是 error,页面上也做了明显的标记。

以上过程包含了另外一种常见的误解,即用户密码校验环节后端报错,API 响应体中包含的错误码为什么没有体现在 Span 中:

HTTP 返回码 200

应用返回码 500

这种困惑实际上来自程序设计本身,在 Restful API 设计的过程中错误的使用了 HTTP 响应码,Restful 建议的、符合直觉的使用方法是 HTTP 响应码与应用响应码表达一致的含义,套用到这里就是后端用户名密码校验发生错误时,HTTP 响应码应该是对应此事件的 5xx,而不是 200,这样就可以通过 HTTP 响应码观测后端的错误情况,所以这并不是追踪系统的问题,应用程序设计应该避免混淆 Restful 与 HTTP,采用最佳实践,如果要在 Trace 中观测这种错误,可以分析 Sapn 中的 error_message 字段,以观测云界面的快速分析为例:

继续分析这个错误 Span,可以看到在校验密码的过程中程序以顺序的方式多次调用了 Redis 与 MySQL,如果需要查看更多的细节,火焰图并不能提供更多的支持,此时可切换到瀑布图,它提供了更多的细节:

在这个视图里可以更清晰的看到:

  • 串行还是并行;
  • 并行的具体情况,例如并行数量;
  • 不同调用之间的间隔时间;
  • 执行的具体操作,例如执行的 Redis 命令和 MySQL 查询;

以上信息可以帮助我们洞察程序执行的细节,持续优化性能问题。如果我们想要了解当前 Trace 中的黄金指标:调用量、耗时和错误,可以切换到服务调用关系:

像前一个例子一样,我们来对比下密码正确时正常登录的情况:

可以看到这里指出了一个异步执行:向 MySQL 中插入登录日志。摘取链路详情中的关键信息可以看到:

{"base_service": "ruoyi-vue-admin","service": "ruoyi-vue-mysql","resource": "insert into sys_logininfor (user_name, status, ipaddr, login_location, browser, os, msg, login_time) values (?, ?, ?, ?, ?, ?, ?, sysdate())","message": {"meta": {"thread.name": "schedule-pool-11","component": "java-jdbc-prepared_statement",}}
}

与其他访问 MySQL 步骤的 thread.name 不同,不再是其父级 Span 的 http-nio-8080-exec-73 而是 schedule-pool-11。

接下来,我们提出本例中的最后一个问题,Redis 和 MySQL 中并未引入追踪相关的组件,我们如何发现这些调用并且收集相关服务的 Span 呢,这就要依赖探针自身的技术栈集成能力了,简单说就是探针支持了应用程序中访问 MySQL 和 Redis 的库,当应用程序调用这些库时相关的数据就会报告为默认服务名的追踪信息,我们需要把默认服务名映射为应用架构中规范的服务名,例如将 mysql 映射为 ruoyi-vue-mysql,上述链路详情中的 base_service 和 service 说明了这种情况,base_service 指出了引入探针的位置。

创建 MySQL 连接

这个例子我们来说明一个不在应用逻辑中定义的 Trace:

Trace 位置

Trace 详情

它只包含一个 Span,从链路详情中的线程名称 Druid-ConnectionPool-Create-75483598 可知,这似乎是 Druid 创建连接的行为,它的 parent_id 为 0,实际上 Span 的连接有赖于跟踪上下文的传播,也就是说,在一个连接父子 Span 的 Trace 中,子 Span 在创建时至少知道父 Span ID,否则 Span 的 parent_id 被置为 0,它也就作为一个 Trace 的 Root Span 出现了,本例就是这种情况。Druid 作为底层的 JDBC 组件库,我们就不再细究其中的细节了。

补足追踪中缺少的细节

从前面提到的几个例子可以看出,追踪为我们进行性能分析提供了脉络,但要说清楚其中的细节仍然是困难的,这至少包含几个方面,例如程序中的调用栈、程序运行态的一些信息、网络连接、基础设施、日志情况等,值得高兴的是,目前的技术发展方向已经能够在这些方面提供很好的支持,观测云通过接入 Profilling 和 eBPF 网络数据来填充这些空隙。以前面的成功登录过程为例,在统一视图内查看代码热点的详情信息:

代码热点概览

Profilling 详情

可以看到处理请求的线程和写入登录日志的异步线程的调用栈,当然,在 Java 版本支持的情况下我们也能够以这种火焰图的方式看到程序运行时的信息,例如内存分配、堆内对象、异常等。

网络方面,任然在统一视图内下钻四层和七层的网络连接情况和关键指标,例如 TCP 延时、波动、重传等:

网络拓扑

网络指标

基础设施方面,观测云在 Span 的链路详情中附加了相关的字段用以自动关联基础设施和组件的指标:

Pod 指标

JVM 指标

日志方面,通过 Trace ID 从全局匹配跨服务的日志到统一视图:

跨服务的日志

总结

本篇文章通过对不同类型可观测数据的分析指出了一个事实,如果想要数据回答运营、故障、反常行为等提出的问题,从多种类型的数据中做关联分析是一个必选项,本例只是展示了可观测技术采用初期的情况。将可观测作为企业价值中的一环时,这种反馈路径将极大的驱动技术和工程能力的迭代,继续深入时,从可观测中受益的各方将通过各种方式暴露更多的数据,可观测性最终成为软件系统的一种特性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/433835.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

设备管理与点巡检系统

在现代企业管理中,设备的高效运作至关重要。为此,我们推出了设备管理与点巡检系统,通过自动化管理提升设备使用效率,保障生产安全。 系统特点 设备全生命周期管理 系统涵盖设备的各个阶段,从设备管理、点检、巡检、保…

vue.js 原生js app端实现图片旋转、放大、缩小、拖拽

效果图&#xff1a; 旋转 放大&#xff1a;手机上可以双指放大缩小 拖拽 代码实现&#xff1a; html <div id"home" class"" v-cloak><!-- 上面三个按钮 图片自己解决 --><div class"headImage" v-if"showBtn">&l…

【机器学习】---深入探讨图神经网络(GNN)

深入探讨图神经网络 1. 图的基本构成示例图邻接矩阵 2. GNN的基本原理消息传递机制更新公式 3. GNN的类型及应用3.1 Graph Convolutional Networks (GCN)GCN实现示例 3.2 Graph Attention Networks (GAT)GAT实现示例 3.3 GraphSAGEGraphSAGE实现示例 4. GNN的应用场景5. GNN的挑…

【湖南步联科技身份证】 身份证读取与酒店收银系统源码整合———未来之窗行业应用跨平台架构

一、html5 <!DOCTYPE html> <html><head><meta http-equiv"Content-Type" content"text/html; charsetutf-8" /><script type"text/javascript" src"http://51.onelink.ynwlzc.net/o2o/tpl/Merchant/static/js…

NSSCTF [HUBUCTF 2022 新生赛]simple_RE(变种base64编码)

文件无壳 拖入IDA中 shiftF12查看可疑字符串 发现两串字符串 一看这两个等于号就猜测是base64编码 进入主函数看看 这段代码是一个简单的 C 语言程序&#xff0c;主要功能是接受用户输入的字符串作为“flag”&#xff0c;然后通过对输入的字符串进行一些处理和比较来验证是否输…

深圳龙链科技:全球区块链开发先锋,领航Web3生态未来

【深圳龙链科技】是全球领先的Web3区块链技术开发公司&#xff0c;专注于为全球客户提供创新高效的区块链解决方案。 深圳龙链科技由币安资深股东携手香港领先的Web3创新枢纽Cyberport联袂打造&#xff0c;立足于香港这一国际金融中心&#xff0c;放眼全球&#xff0c;汇聚了华…

Python获取百度翻译的两种方法

一、引言 百度是我们常用的搜索工具&#xff0c;其翻译是与爱词霸合作&#xff0c;总体看其反应速度较快&#xff0c;可以作为项目中重要的翻译工具。根据大家的需要&#xff0c;现提供两种Python获取百度翻译的两种办法&#xff1a; 二、requests法 我们引用requests模块&a…

智能Ai语音机器人的应用价值有哪些?

随着时间的推移&#xff0c;人工智能的发展越来越成熟&#xff0c;智能时代也离人们越来越近&#xff0c;近几年人工智能越来越火爆&#xff0c;人工智能的应用已经开始渗透到各行各业&#xff0c;与生活交融&#xff0c;成为人们无法拒绝&#xff0c;无法失去的一个重要存在。…

SQL进阶技巧:如何获取状态一致的分组? | 最大、最小值法

目录 0 需求描述 1 数据准备 2 问题分析 方法1&#xff1a;最大、最小值法&#xff08;技巧&#xff09; 方法2&#xff1a;常规思路 3 小结 如果觉得本文对你有帮助&#xff0c;那么不妨也可以选择去看看我的博客专栏 &#xff0c;部分内容如下&#xff1a; 数字化建设通…

Trie树之字符串统计问题

这是C算法基础-数据结构专栏的第二十七篇文章&#xff0c;专栏详情请见此处。 引入 Trie树&#xff0c;即字典树&#xff0c;顾名思义&#xff0c;就是用类似字典的方式存储数据&#xff0c;而Trie树最经典也是最简单的一个应用就是字符串统计问题。 字符串统计问题就是维护一个…

【微服务即时通讯系统】——etcd一致性键值存储系统、etcd的介绍、etcd的安装、etcd使用和功能测试

文章目录 etcd1. etcd的介绍1.1 etcd的概念 2. etcd的安装2.1 安装etcd2.2 安装etcd客户端C/C开发库 3. etcd使用3.1 etcd接口介绍 4. etcd使用测试4.1 原生接口使用测试4.2 封装etcd使用测试 etcd 1. etcd的介绍 1.1 etcd的概念 Etcd 是一个基于GO实现的 分布式、高可用、一致…

分库分表还是分布式?如何用 OceanBase的单机分布式一体化从根本上解决问题

随着企业业务规模的不断增长&#xff0c;单机集中式的数据库系统逐渐难以承载企业日益增长的数据存储与处理需求。因此&#xff0c;MySQL 的分库分表方案成为了众多企业应对数据存储量激增及数据处理能力需求扩张的“止痛药”。尽管这一方案短期内有效缓解了企业面临的大规模数…

在新ARM板上移植U-Boot和Linux指南

序言 从支持一个定制板子在U-Boot和Linux中的过程中得到经验以一个带有知名SoC&#xff08;i.MX6&#xff09;且IP已经得到支持的板子为例&#xff0c;这次讨论几乎不涉及编码技能&#xff0c;更多地聚焦在U-Boot部分 一般原则 如果您有您的BSP&#xff08;板级支持包&#…

【全新课程】正点原子《基于GD32 ARM32单片机项目实战入门》培训课程上线!

正点原子《基于GD32 ARM32单片机项目实战入门》全新培训课程上线啦&#xff01;正点原子工程师手把手教你学&#xff01;彻底解决ARM32单片机项目入门难的问题&#xff01; 一、课程介绍 本课程专为ARM32单片机的入门学习者设计&#xff0c;涵盖了环境搭建、编程软件使用、模…

【数学分析笔记】第3章第4节闭区间上的连续函数(2)

3. 函数极限与连续函数 3.4 闭区间上的连续函数 3.4.4 中间值定理 【定理3.4.4】若 f ( x ) f(x) f(x)在 [ a , b ] [a,b] [a,b]上连续&#xff0c;则它一定能取到最大值 M M M与最小值 m m m之间的任何一个值。 M max ⁡ f ( x ) , x ∈ [ a , b ] , m min ⁡ f ( x ) , …

【前端框架对比和选择】React 与 Vue 框架设计思路对比

框架总览 前端框架繁多&#xff0c;在学习的时候也会陷入困惑&#xff0c;我们应该抓住最主流的内容 Vue/React&#xff0c;深入底层&#xff0c;尝试揣摩框架作者的设计思路&#xff0c;开阔前端培训自己的视野&#xff0c;大家也不要把自己限制在框架之中&#xff0c;认为工…

常见区块链数据模型介绍

除了加密技术和共识算法&#xff0c;区块链技术还依赖于一种数据模型&#xff0c;它决定了信息如何被结构化、验证和存储。数据模型定义了账户如何管理&#xff0c;状态转换如何发生&#xff0c;以及用户和开发者如何与系统交互。 在区块链技术的短暂历史中&#xff0c;数据…

数据治理005-血缘关系

数据血缘是元数据产品的核心能力&#xff0c;但数据血缘是典型的看起来很美好但用起来门槛很高的技术&#xff0c;只要你采买过元数据产品就知道了。这篇文章对数据血缘的特征、价值、用途和方法做了系统阐述&#xff1a; 1、特征&#xff1a;归属性、多源性、可追溯及层次性 2…

SAP已知事务码查询关联角色

运维期间客户就出现没有某些事务码的权限&#xff0c;要求添加&#xff1b; 想要添加事务码就必须知道这个事务码属于哪个角色&#xff1b;使用SUIM-角色-按菜单中的事务分配&#xff0c;输入事务码&#xff0c;点击执行就可以查看 找到相关的角色之后&#xff0c;用SU01添加至…

动态规划算法:12.简单多状态 dp 问题_打家劫舍_C++

目录 题目链接&#xff1a;LCR 089. 打家劫舍 - 力扣&#xff08;LeetCode&#xff09; 一、题目解析 题目&#xff1a; 解析&#xff1a; 二、算法原理 1、状态表示 状态表示&#xff1a; 2、状态转移方程 状态转移方程推理&#xff1a; 3、初始化 dp表初始化: 特殊…