普通视图

发现新文章,点击刷新页面。
昨天 — 2025年8月21日掘金专栏-得物技术

营销会场预览直通车实践|得物技术

作者 得物技术
2025年8月21日 11:15

一、背景:活动会场的配置走查之痛

在电商营销中,会场是承载活动流量的核心阵地。得物的营销会场不仅覆盖520、七夕等活动节点,也支撑日常的"天天领券"、"疯狂周末"等高频运营场景。数据显示,会场的UV占比、GMV贡献、订单量均占平台重要比重。

然而,随着业务复杂度提升,会场配置面临三大挑战。

1.1 三大挑战

※ 多目标耦合

同一会场需同时满足不同运营GMV提升、拉新、促活等不同目标,导致配置策略叠加,复杂度激增。

※ 验证滞后性

传统方式需活动生效后才能验证效果,配置错误可能导致资损,修复成本高昂。

※ 跨团队协作低效

涉及搭建、招商、优惠、资产等6大系统,联调成本高,走查覆盖率仅60%。

1.2 会场的配置举例

二、  解决方案:全链路"痛点穿越"

2.1 痛点梳理

2.2 核心思路

通过模拟未来时间、指定用户人群、强制命中AB实验,实现**"上线未对外先验证"**,让运营和技术在配置完成后即可预览真实效果。

分层架构设计

方案选型

某一线电商大厂穿越  VS  得物-时间穿越 VS 其他。

成本范围可控性,以及业务特性使用效率考量;原理即定义预览模式,传参即为true来消费。

关键改造点

  • 搭建系统: 低成本高便捷自查和走查。
  • 投放系统 :新增travel_mode参数,透传至下游。
  • 招商系统 :各类型招商活动查询逻辑,支持未来时间过滤。
  • 优惠试算 :兼容"虚拟资产"参与计算,确保价格准确性。
  • 风险管控 :限制仅白名单用户可触发,禁止真实下单。

三、 落地效果

3.1 应用姿势

活动预演

模拟不同人群用户不同时间点的价格计算及会场效果及稳定性。

优惠叠加校验

验证"跨店满减+品类券+平台补贴+商家自建优惠+商家代金券"的组合逻辑。

人群定向测试

人群定向测试 :对比新老用户、成熟非成熟及特殊类目新等的价格分层效果。

3.2 效率提升

不需要重新复制相同活动模拟提前开始,加之商家自建活动和平台活动较多,模拟相同时间的各类活动成本较大,且不可能做到完全相同,使运营配合测试线上验证配置工作量下降50%(少配置一套)

 提前穿越预览可提前感知活动期间各类价格、价格标签及各类活动叠加的优惠试算,检查配置问题,让活动走查场景覆盖度从历史60%覆盖度提升到80%以上(历史走查只能走查商品流、活动开始后的价格、标签、资源位无法走查到,活动叠加类型不够全),也方便运营预览预期实际效果并时调整策略,同时减少配置风险。

一个账号即可实现所有人群、实验、组件会场的预览,资产与走查更高效

线上风险规避:避免如过往活动生效才能感知效果,风险前置;如有问题只能下线活动及资源位的止损;减少资损风险,避免多类型活动叠加破价M类事件。

快速check不同排期下不同人群、不同实验组用户在不同时间段的活动下的商品优惠价、营销标签以页面组件呈现。

3.3 落地效果分析

做得好的

我们的"穿越"方案通过轻量级改造,实现了全链路验证能力 ,为复杂营销系统的配置管理提供了标准化解法。其核心价值在于:

※ 风险前置化

将问题发现节点从"上线后"提前至"配置阶段"。

※ 效率最大化

一个二维码即可验证所有人群、实验、时间组合。

※ 成本最优

仅需接口参数改造,无需搭建完整灰度环境。

有待提升

  • 权益投放的咨询和领取暂未实现穿越。
  • 会场存在与商品详情页的价格试算、标签不一致问题。

四、 未来规划

扩展可应用的穿越场景:

  1. 频道穿越:承载产品化运营的频道同活动会场实现痛点穿越,提效自查走查。
  2. 商详页一致性 :建立价格版本号机制,解决会场与商详页价标不一致问题。
  3. 活动资源位:建立活动核心资源位排期可监听,可自动穿越预览。
  4. 权益投放 :在沙箱环境实现"领取→使用"全流程验证。

绿色部分是已经具备的基础能力,红色边框是未来规划去实现的业务线,如下方案非最终方案,基于改动范围和成本考量:

4.1频道

频道穿越概述:

  1. 痛点:较多频道偏产品线运营,每周末都会提前招商提前配置。
  2. 穿越实现方式:同会场,通过sence区分。
  3. 价值:频道实现后,可同理无成本拓展新品频道、补贴频道、打牌低价等。

App入口管控

测试包安装有名单管控,天然支持了白名单。

资源位

资源位穿越:

  1. 痛点:活动c端引流入口、重体验,对外前的配置走查费力。
  2. 范围:首页弹窗、活动tab、活动中通、购买feeds商卡、我的tab、穹顶。
  3. 价值:时间+人群+实验穿越减少运营流量计划重复配置,提前预览活动氛围和投放效果。

商详

商详穿越:

  1.  商详:商详价格与会场一致性、氛围、标签、导购自身商详样式实验等。
  2. 价值:时间+人群+实验穿越减少运营流量计划重复配置,提前预览活动氛围和投放效果。

五、  总结

穿越类型

  • 仅传时间:即业务处理上假定到了某一时间,uid由App自动获取,是否命中人群、实验,按真实查询星云、AB。
  • 仅传人群:即业务处理上按照当前时间处理,假定用户属于入参人群,去定位计划或招商活动。
  • 仅传实验:即业务处理上按照当前时间,用户实际人群,时间为入参实验value处理。
  • 都设定:即业务处理上按照目标时间、假定命中目标入参人群和目标AB实验value来处理业务。
  • 消费穿越入参方:严格按照接收什么,即命中什么,未接收的走实际业务查询来处理。

风险管控

  • App测试包的安装现有管控:加入测试白名单的得物账号才可以下载测试包,默认可安装测试包的机器都可穿越。
  • 穿越目的是检验个业务配置正确性、素材效果、全链路验证等,供咨询查询,避免写操作:比如创单支付、核销。

能力沉淀

  • 从客户端上developer工具的透传穿越(时间、人群、实验),基础能力沉淀后,各业务域拓展性强,对于新增业务穿越工作量大大降低,接入成本也相对较低。

往期回顾

1.基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术

2.AI质量专项报告自动分析生成|得物技术

3.Rust 性能提升“最后一公里”:详解 Profiling 瓶颈定位与优化|得物技术

4.Java volatile 关键字到底是什么|得物技术

5.eBPF 助力 NAS 分钟级别 Pod 实例溯源|得物技术

文 / 东陌

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

昨天以前掘金专栏-得物技术

基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术

作者 得物技术
2025年8月19日 10:20

一、 背景

客服知识库是一个集中管理和存储与客服相关的信息和资源的系统,在自研知识库上线之前,得物采用的承接工具为第三方知识库系统。伴随着业务的发展,知识的维护体量、下游系统的使用面临的问题愈发明显,而当前的第三方采购系统,已经较难满足内部系统间高效协作的诉求,基于以上业务诉求,我们自研了一套客服知识库。

二、富文本编辑器的选型

以下是经过调研后列出的多款富文本编辑器综合对比情况:

2.1 编辑器的选择

  • 自研知识库要求富文本编辑器具备表格的编辑能力,由于Quill不支持表格编辑能力(借助表格插件可以实现该能力,但经过实际验证,插件提供的表格编辑能力不够丰富,使用体验也较差),被首先被排除。
  • wangEditor体验过程中发现标题和列表(有序、无序)列表两个功能互斥,体验不太好,而这两个功能都是自研知识库刚需功能,也被排除。
  • Lexical是facebook推出的一款编辑器,虽功能很丰富,但相较于CKEditorTinyMCE,文档不够完善,社区活跃性较低,插件不成熟,故优先选择CKEditorTinyMCE

CKEditorTinyMCE经过对比,由于当前正在使用的第三方知识库采用的是TinyMCE编辑器,选择TinyMC在格式兼容上会更友好,对新老知识库的迁移上更有利。且TinyMCE在功能丰富度上略占优势,故最终选择TinyMCE作为本系统文档知识库的编辑器

2.2 TinyMce编辑器模式的选择

经典模式(默认模式)

基于表单,使用表单某字段填充内容,编辑器始终作为表单的一部分。内部采用了iframe沙箱隔离,将编辑内容与页面进行隔离。

※ 优势

样式隔离好。

※ 劣势

由于使用iframe,性能会差点,尤其对于多实例编辑器。

内联模式(沉浸模式)

将编辑视图与阅读视图合二为一,当其被点击后,元素才会被编辑器替换。而不是编辑器始终可见,不能作为表单项使用。内容会从它嵌入的页面继承CSS样式表。

※ 优势

性能相对较好,页面的编辑视图与阅读视图合二为一,提供了无缝的体验,实现了真正的所见即所得。

※ 劣势

样式容易受到页面样式的影响。

三、系统总览

3.1 知识创建链路

3.2 知识采编

结构化段落

为了对知识文档做更细颗粒度的解析,客服知识库采用了结构化段落的设计思想,每个段落都会有一个唯一标志 ,且支持对文档的每个段落单独设置标签,这样在后期的知识检索、分类时,便可以精确定位到知识文档的具体段落,如下图所示。

知识文档编辑页面

3.3 应用场景

客服知识库的主要应用场景如下:

知识检索

基于传统的ES检索能力,用于知识库的检索,检索要使用的知识,且可以直接在工作台打开对应的知识并浏览,并可以定位、滚动到具体的知识段落。同时还会高亮显示知识文档中匹配到的搜索关键字

智能问答(基于大模型能力和知识库底层数据的训练)

※ RAG出话

辅助客服了解用户的真实意图,可用于客服作业时的参考。

原理阐述: RAG是一种结合了检索和生成技术的人工智能系统。它是大型语言模型的一种,但特别强调检索和生成的结合。RAG的最主要的工作流程包括:

  • 检索阶段:系统会根据用户的查询,从客服知识库中检索出相关信息。这些信息可能包括知识库内容、订单信息和商品信息等
  • 生成阶段:RAG使用检索到的信息来增强其生成过程。这意味着,生成模型在生成文本时,会考虑到检索到的相关信息,以生成更准确、更相关的回答。你可以直接将搜索到的内容返回给用户也可以通过LLM模型结合后生成给用户。

※ 答案推荐

可以根据用户搜索内容、上下文场景(如订单信息、商品信息)辅助客服更高效的获取答案。

流程示意:

※ 联网搜索

当RAG出话由于拒识没有结果时,便尝试进行联网搜索给出结果,可作为RAG能力失效后的补充能力。

原理阐述: 底层使用了第三方提供的联网问答Agent服务。在进行联网搜索之前,会对用户的查询信息进行风控校验,风控校验通过后,再进行 【指定意图清单】过滤,仅对符合意图的查询才可以进行联网搜索。

四、问题和解决方案

4.1 解决图片迁移问题

背景

在新老知识迁移的过程中,由于老知识库中的图片链接的域名是老知识库的域名,必须要有老知识库的登录台信息,才能在新知识库中访问并渲染。为了解决这个问题,我们对用户粘贴的动作进行了监听,并对复制内容的图片链接进行了替换。

时序图

核心逻辑

/**
 * 替换编辑器中的图片URL
 * @param content
 * @param editor 编辑器实例
 * @returns 替换后的内容
 */
export const replaceImgUrlOfEditor = (content, editor) => {
  // 提取出老知识中的图片访问链接
  const oldImgUrls = extractImgSrc(content);
  // 调用接口获取替换后的图片访问链接
  const newImageUrls = await service.getNewImageUrl(oldImgUrls);
  // 将老知识库的图片链接替换成新的可访问的链接
  newContent = replaceImgSrc(newContent, replacedUrls.imgUrls);
  // 使用新的数据更新编辑器视图
  editor.updateView(newContent);
};

4.2 解决加载大量图片带来的页面卡顿问题

背景

知识库内含有大量的图片,当我们打开一篇知识时,系统往往因为在短时间内加载、渲染大量的图片而陷入卡顿当中,无法对页面进行其他操作。这个问题在老知识库中尤为严重,也是研发新知识库过程中我们需要重点解决的问题。

解决方案

我们对图片进行了懒加载处理:当打开一篇知识时,只加载和渲染可见视图以内的图片,剩余的图片只有滚动到可见视图内才开始加载、渲染。

由于我们要渲染的内容的原始数据是一段html字符串,一篇知识文档的最小可渲染单元是段落(结构化段落),而一个段落的内容大小事先是不知道的,因此传统的滚动加载方式在这里并不适用:比如当滚动到需要加载下一段落的位置时,如果该段落的内容特别大且包含较多图片时,依然会存在卡顿的现象。

我们采用正则匹配的方式,识别出知识文档的html中所有的  标签(将文档的html视作一段字符串),并给  标签插入 loading="lazy" 的属性,具备该属性的图片在到达可视视图内的时候才会加载图片资源并渲染,从而实现懒加载的效果,大大节省了知识文档初次渲染的性能开销。并且该过程处理的是渲染知识文档前的html字符串,而非真实的dom操作,所以不会带来重绘、重排等性能问题。

知识文档渲染的完整链路

4.3 模板缩略图

背景

在知识模板列表页或者在创建新知识选择模板时,需要展示模板内容的缩略图,由于每个模板内容都不一样,同时缩略图中需要可以看到该模板靠前的内容,以便用户除了依靠模板标题之外还可以依靠一部分的模板内容选择合适的模板。

解决方案

在保存知识模板前,通过截屏的方式保存一个模板的截图,上传截图到cdn并保存cdn链接,再对截图进行一定的缩放调整,即可作为模板的缩略图。

时序图

实际效果

模板列表中缩略图展示效果:

新建知识时缩略图展示效果:

4.4 全局查找/替换

背景

知识库采用了结构化段落的设计思想,技术实现上,每个段落都是一个独立的编辑器实例。这样实现带来一个弊端:使用编辑器的搜索和替换功能时,查找范围仅限于当前聚焦的编辑器,无法同时对所有编辑器进行查找和替换,增加了业务方的编辑费力度。

解决方案

调研、扩展编辑器的查找/替换插件的源码,调度和联动多编辑器的查找/替换API从而实现全局范围内的查找/替换。

※ 插件源码剖析

通过对插件源码的分析,我们发现插件的查找/替换功能是基于4个基本的API实现的: find 、 replace 、 next 、 prev 、 done 。

※ 设计思路

通过在多个编辑器中加入一个调度器来控制编辑器之间的接力从而实现全局的查找/替换。同时扩展插件的API辅助调度器在多编辑器之间进行调度

※ 插件源码API扩展

  1. hasMatched: 判断当前编辑器是否匹配到关键字。
  2. hasReachTop:判断当前编辑器是否已到达所查找关键字的最前一个。
  3. hasReachBottom:判断当前编辑器是否已到达所查找关键字的最后一个。
  4. current: 滚动到编辑器当前匹配到的关键字的位置。
  5. clearCurrentSelection: 对编辑器当前匹配到的关键字取消高亮效果。

UI替换

屏蔽插件自带的查找/替换的弹窗,实现一个支持全局操作的查找/替换的弹窗:使用了react-rnd组件库实现可拖拽弹窗,如下图所示:

「查找」

※ 期望效果

当用户输入关键字并点击查找时,需要在文档中(所有编辑器中)标记出(加上特定的背景色)所有匹配到该关键字的文本,并高亮显示出第一个匹配文本。

※ 流程图

「下一个」

※ 期望效果

当用户点击「下一个」时,需要高亮显示下一个匹配结果并滚动到该匹配结果的位置。

※ 流程图

五、总结

在新版客服知识库的研发和落地过程中,我们基于TinyMce富文本编辑器的基础上,进行了功能扩展和定制。这期间既有参考过同类产品(飞书文档、语雀)的方案,也有根据实际应用场景进行了创新。截止目前已完成1000+老知识库的顺利迁移,系统稳定运行。

自研过程中我们解决了老版知识库系统的卡顿和无法满足定制化需求的问题。并在这些基本需求得到满足的情况下,通过优化交互方式和知识文档的加载、渲染性能等方式进一步提升了使用体验

后续我们会结合用户的反馈和实际使用需求进一步优化和扩展客服知识库的功能,也欢迎有同样应用场景的同学一起交流想法和意见。

往期回顾

1.AI质量专项报告自动分析生成|得物技术

2.Rust 性能提升“最后一公里”:详解 Profiling 瓶颈定位与优化|得物技术

3.Valkey 单点性能比肩 Redis 集群了?Valkey8.0 新特性分析|得物技术

4.Java SPI机制初探|得物技术

5.社区搜索离线回溯系统设计:架构、挑战与性能优化|得物技术



文 / 煜宸

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

AI质量专项报告自动分析生成|得物技术

作者 得物技术
2025年8月14日 15:14

一、背景

在日常工作中,常需要通过各项数据指标,确保驱动版本项目进展正常推进,并通过各种形式报表数据,日常总结日报、周会进展、季度进行总结输出归因,分析数据变化原因,做出对应决策变化,优化运营方式,目前在梳理整理校准分析数据需要大量的时间投入、结合整体目标及当前进展,分析问题优化的后续规划。

常见形式

人工收集

数据来源依赖于各系统平台页面,通过人工收集校准后填写再通过表格公式计算,或者可以通过多维表格工作流触发通知等功能。

图片

quickbi报表

通过ODPS搭建自定义报表,实现快速收集数据,复制报表到飞书文档内进行异动分析。

图片

平台能力开发

通过代码开发文档导出能力,根据固定模板生成数据分析,该能力开发人力成本较高,需要针对不同平台数据源定制化开发。

图片图片

AI Studio智能体平台

研发效能团队基于开源Dify项目社区部署,可以根据需求自定义sop,多模型的可选项,选择最适合业务的模型。每个工作流节点可自定义流程的判断分析,轻松上线可投产的AI Agents。

Dify是一个支持工作流编排的AI智能体平台,轻松构建并部署生产级 AI 应用。其核心功能包含:

  1. 以工作流的方式编排AI应用,在工作流中可以添加LLM、知识库、Agent工具、MCP服务等节点,工作流支持分支流转、节点循环、自定义节点等高级能力项。

  2. 支持在工作流中调用公司内部的Dubbo/gRPC服务。(插件实现)

  3. 知识库管理,通过构建私有知识库以增强 LLM 的上下文。

  4. 与内部平台集成,支持H5页面嵌入、API的方式与内部平台集成。

  5. 主流模型集成,支持使用多种主流模型如DeepSeek、OpenAI等,支持多模态模型。

对标的业界产品有:

✅ 多模型选择(适配不同业务场景)

✅ 可视化工作流搭建(支持自定义SOP)

✅ 全链路可观测性(实时调试优化)

综上本期实践利用AI工作流平台针对报告进行生成分析输出,让使用方回归到聚焦数据归因分析上,减少数据收集分析、文档编写成本。

图片

二、应用实践

实践效果

整体分析数据从哪来->需要输出什么样的格式->优化模型输出结果,三步骤针对输出结果进行调优。

图片

自动化成熟度分析工作流搭建案例

图片

运用效果

图片图片

报告效果

图片

飞书机器人通知归因分析

图片图片

数据处理

图片

LLM:通过用户输入分析获取数据源请求格式,配置好对应数据的映射关系模型自行获取对应数据。

提示词输入

图片

格式化输出配置

图片

http请求:通过用户输入分析后的参数构造请求参数,通过固定接口拉取数据,支持curl导入功能。

图片图片

代码执行:支持python、js代码对结果数据进行处理过滤,提升分析结果准确性。

图片

模型提示词

如文档整体分为不同模块可设定不同模型节点处理,每个模块增加特定提示词处理节点内容,模型并行分析处理,提升输出稳定性和输出效率,再通过LLM输出整合进行整体输出。

图片

在模型输入上下文及用户输入,通过获取的数据指定输出格式,设定提示词,提供AI结合模板输出对应形式。

图片

通过衔接上下节点返回内容最终整合报表输出结果,统一输出样式格式。

图片

优化输出

切换可用模型

遇到模型输出不稳定或者未达到预期效果,可切换可用模型,寻找适配模型。

图片

设定模型预载参数

设定模型预载参数,提升模型输出准确度。

图片

优化增加提示词

优化增加提示词提升输出形式稳定性:角色定义 ->  字段映射 -> 模板说明 -> 实际数据填充 -> 输出格式定义。

`## 角色定义 你是一位接口自动化测试专家以及报告生成专家,负责将接口返回的数据映射字段结合模板输出一份有效的自动化成熟度报告-稳定性部分。

接口返回数据字段映射关系:

基础字段: bu_name:业务域名称。 parent_bu_id:业务域。

稳定性指标字段: total_auto_stability_score:稳定性评分 iter_case_success_rate: 迭代自动化成功率 iter_case_success_rate_cpp: 迭代自动化成功率环比 auto_case_failed_rate: 自动化失败率 auto_case_failed_rate_cpp: 自动化失败率环比 case_aigc_avg_score: 用例健壮有效性评分 case_aigc_avg_score_cpp: 有效性评分环比

模板:

2.2 自动化稳定性 用表格展示自动化稳定性,表格内容包含所有一级业务域、二级业务域。 表头按照顺序输出: 1、业务域 2、自动化稳定性评分 3、迭代自动化成功率 4、迭代自动化成功率环比 5、自动化失败率 6、失败率环比 7、用例健壮有效性评分 8、有效性评分环比

重点关注项:xxx --仅分析二级业务域的稳定性性指标字段,列出需重点关注指标。

模板说明:

1、以html格式输出,增加内容丰富度,不输出任何多余内容。 2、表格说明:表格需要包含所有业务域数据。不要省略或者缺少任何业务域数据,将所有业务域展示在同一个表格内。 3、表格行排序:根据评分从高到低排序。 4、环比字段说明:指标环比下降环比字段标记红色,环比提升字段标记绿色,不标记背景色。

任务说明

1、用户将提供接口返回的JSON数据。 2、根据接口数据和匹配字段映射关系。 3、结合模板以及模板说明html形式输出,不输出任何多余内容。 请你根据以上内容,回复用户,不需要输出示例。`

模板转换

输出的表格形式通过模板转化固定输出html表格形式,提升模型输出稳定性。

图片

输出形式

以markdown形式或以html形式输出,复制到飞书文档上进行输出。

html最终效果

图片

markdown最终效果

图片

飞书机器人通知归因分析

图片

生成飞书文档

支持飞书应用直接新建飞书文档,markdown形式输出。

图片

对话返回生成后的飞书文档地址及分析:

图片

三、总结

在日常工作中如何有效利用数据指标驱动项目进展,现有数据收集和分析流程中面临的挑战。通过手动收集数据、生成报表、平台开发等传统方式,需要投入大量时间和人力资源,导致工作效率低下。

为此,引入了研发效能AI 智能体平台,AI工作流平台不仅改进了数据处理方式,还提升了报告生成的效率和准确性,从而增强了业务洞察力。进一步丰富工作流和知识库,提高对核心数据指标的分析能力,并针对异常数据指标进行细致剖析,为团队提供更深入的指导和支持。

此外,相似场景的处理也可以借助AI工作流进行优化,有望在多个业务领域推广应用。

四、后续规划

  • 丰富工作流:丰富结合知识库,针对每项核心数据指标提升建议以及业务域现状给予业务域具体指导建议。

  • 明细下钻分析:获取对应数据指标异常后,结合明细数据进行分析,具体到用例、人员级别。

  • 类似场景可通过AI工作流处理:固定模板数据源报告类、周会均可使用该方法减少人工投入成本。

往期回顾

1.Rust 性能提升“最后一公里”:详解 Profiling 瓶颈定位与优化|得物技术

2.Valkey 单点性能比肩 Redis 集群了?Valkey8.0 新特性分析|得物技术

3.社区搜索离线回溯系统设计:架构、挑战与性能优化|得物技术

4.正品库拍照PWA应用的实现与性能优化|得物技术

5.得物社区活动:组件化的演进与实践

文 / 笠

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

Rust 性能提升“最后一公里”:详解 Profiling 瓶颈定位与优化|得物技术

作者 得物技术
2025年8月12日 13:49

一、Profiling:揭示性能瓶颈的“照妖镜”

在过去的一年里,我们团队完成了一项壮举:将近万核的 Java 服务成功迁移到 Rust,并收获了令人瞩目的性能提升。我们的实践经验已在《RUST练习生如何在生产环境构建万亿流量》一文中与大家分享。然而,在这次大规模迁移中,我们观察到一个有趣的现象:大多数服务在迁移后性能都得到了显著提升,但有那么一小部分服务,性能提升却不尽如人意,仅仅在 10% 左右徘徊。

这让我们感到疑惑。明明已经用上了性能“王者”Rust,为什么还会遇到瓶颈?为了解开这个谜团,我们决定深入剖析这些“低提升”服务。今天,我就来和大家分享,我们是如何利用 Profiling 工具,找到并解决写入过程中的性能瓶颈,最终实现更高性能飞跃的!

在性能优化领域,盲目猜测是最大的禁忌。你需要一把锋利的“手术刀”,精准地找到问题的根源。在 Rust 生态中,虽然不像 Java 社区那样拥有 VisualVM 或 JProfiler 这类功能强大的成熟工具,但我们依然可以搭建一套高效的性能分析体系。

为了在生产环境中实现高效的性能监控,我们引入了 Jemalloc 内存分配器和 pprof CPU 分析器。这套方案不仅支持定时自动生成 Profile 文件,还可以在运行时动态触发,极大地提升了我们定位问题的能力。

二、配置项目:让Profiling“武装到牙齿”

首先,我们需要在 Cargo.toml 文件中添加必要的依赖,让我们的 Rust 服务具备 Profiling 的能力。以下是我们的配置,Rust 版本为 1.87.0。

[target.'cfg(all(not(target_env = "msvc"), not(target_os = "windows")))'.dependencies]
# 使用 tikv-jemallocator 作为内存分配器,并启用性能分析功能
tikv-jemallocator = { version = "0.6", features = ["profiling""unprefixed_malloc_on_supported_platforms"] }
# 用于在运行时控制和获取 jemalloc 的统计信息
tikv-jemalloc-ctl = { version = "0.6", features = ["use_std""stats"] }
# tikv-jemallocator 的底层绑定,同样启用性能分析
tikv-jemalloc-sys = { version = "0.6", features = ["profiling"] }
# 用于生成与 pprof 兼容的内存剖析数据,并支持符号化和火焰图
jemalloc_pprof = { version = "0.7", features = ["symbolize","flamegraph"] }
# 用于生成 CPU 性能剖析数据和火焰图
pprof = { version = "0.14", features = ["flamegraph""protobuf-codec"] }

简单来说,这几个依赖各司其职:

※ tikv-jemallocator

基于 jemalloc 的 Rust 实现,以其高效的内存管理闻名。

※ jemalloc_pprof

负责将 jemalloc 的内存剖析数据转换成标准的 pprof 格式。

※ pprof

用于 CPU 性能分析,可以生成 pprof 格式的 Profile 文件。

三、  全局配置:启动Profiling开关

接下来,在 main.rs 中进行全局配置,指定 Jemalloc 的 Profiling 参数,并将其设置为默认的全局内存分配器。

// 配置 Jemalloc 内存分析参数
#[export_name = "malloc_conf"]
pub static malloc_conf: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:16\0";


#[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc;


// 将 Jemalloc 设置为全局内存分配器
#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;

这段配置中的 lg_prof_sample:16 是一个关键参数。

它表示 jemalloc 会对大约每 2^16 字节(即 64KB)的内存分配进行一次采样。这个值越大,采样频率越低,内存开销越小,但精度也越低;反之则精度越高,开销越大。在生产环境中,我们需要根据实际情况进行权衡。

四、实现Profile生成函数:打造你的“数据采集器”

我们将 Profile 文件的生成逻辑封装成异步函数,这样就可以在服务的任意时刻按需调用,非常灵活。

内存Profile生成函数

#[cfg(not(target_env = "msvc"))]
async fn dump_memory_profile() -> Result<StringString> {
    // 获取 jemalloc 的 profiling 控制器
    let prof_ctl = jemalloc_pprof::PROF_CTL.as_ref()
        .ok_or_else(|| "Profiling controller not available".to_string())?;


    let mut prof_ctl = prof_ctl.lock().await;
    
    // 检查 profiling 是否已激活
    if !prof_ctl.activated() {
        return Err("Jemalloc profiling is not activated".to_string());
    }
   
    // 调用 dump_pprof() 方法生成 pprof 数据
    let pprof_data = prof_ctl.dump_pprof()
        .map_err(|e| format!("Failed to dump pprof: {}", e))?;


    // 使用时间戳生成唯一文件名
    let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
    let filename = format!("memory_profile_{}.pb", timestamp);


    // 将 pprof 数据写入本地文件
    std::fs::write(&filename, pprof_data)
        .map_err(|e| format!("Failed to write profile file: {}", e))?;


    info!("Memory profile dumped to: {}", filename);
    Ok(filename)
}

CPU Profile生成函数

类似地,我们使用 pprof 库来实现 CPU Profile 的生成。

#[cfg(not(target_env = "msvc"))]
async fn dump_cpu_profile() -> Result<String, String> {
    use pprof::ProfilerGuard;
    use pprof::protos::Message;


    info!("Starting CPU profiling for 60 seconds...");


    // 创建 CPU profiler,设置采样频率为 100 Hz
    let guard = ProfilerGuard::new(100).map_err(|e| format!("Failed to create profiler: {}", e))?;


    // 持续采样 60 秒
    tokio::time::sleep(std::time::Duration::from_secs(60)).await;


    // 生成报告
    let report = guard.report().build().map_err(|e| format!("Failed to build report: {}", e))?;


    // 使用时间戳生成文件名
    let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
    let filenameformat!("cpu_profile_{}.pb", timestamp);


    // 创建文件并写入 pprof 数据
    let mut file = std::fs::File::create(&filename)
        .map_err(|e| format!("Failed to create file: {}", e))?;


    report.pprof()
        .map_err(|e| format!("Failed to convert to pprof: {}", e))?
        .write_to_writer(&mut file)
        .map_err(|e| format!("Failed to write profile: {}", e))?;


    info!("CPU profile dumped to: {}", filename);
    Ok(filename)
}
  •  ProfilerGuard::new()   100  Hz 意味着每秒钟会随机中断程序 100 次,以记录当前正在执行的函数调用栈
  • tokio::time::sleep(std::time::Duration::from_secs(60)).await 表示 pprof 将会持续采样 60 秒钟
  •  guard.report().build() 这个方法用于将收集到的所有采样数据进行处理和聚合,最终生成一个 Report 对象。这个 Report 对象包含了所有调用栈的统计信息,但还没有转换成特定的文件格式
  •  report.pprof() 这是 Report 对象的一个方法,用于将报告数据转换成 pprof 格式

五、 触发和使用 Profiling:随时随地捕捉性能数据

有了上述函数,我们实现了两种灵活的触发方式。

※ 定时自动生成

通过异步定时任务,每隔一段时间自动调用 dump_memory_profile() 和  dump_cpu_profile() 。

fn start_profilers() {
    // Memory profiler
    tokio::spawn(async {
        let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
        loop {
            interval.tick().await;
            #[cfg(not(target_env = "msvc"))]
            {
                info!("Starting memory profiler...");
                match dump_memory_profile().await {
                    Ok(profile_path) => info!("Memory profile dumped successfully: {}", profile_path),
                    Err(e) => info!("Failed to dump memory profile: {}", e),
                }
            }
        }
    });
    // 同理可以实现CPU profiler
}

※ 手动 HTTP 触发

通过提供 /profile/memory 和 /profile/cpu 两个 HTTP 接口,可以随时按需触发 Profile 文件的生成。

async fn trigger_memory_profile() -> Result<impl warp::Reply, std::convert::Infallible> {
    #[cfg(not(target_env = "msvc"))]
    {
        info!("HTTP triggered memory profile dump...");
        match dump_memory_profile().await {
            Ok(profile_path) => Ok(warp::reply::with_status(
                format!("Memory profile dumped successfully: {}", profile_path),
                warp::http::StatusCode::OK,
            )),
            Err(e) => Ok(warp::reply::with_status(
                format!("Failed to dump memory profile: {}", e),
                warp::http::StatusCode::INTERNAL_SERVER_ERROR,
            )),
        }
    }
}
//同理也可实现trigger_cpu_profile()函数
fn profile_routes() -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
    let memory_profile = warp::post()
        .and(warp::path("profile"))
        .and(warp::path("memory"))
        .and(warp::path::end())
        .and_then(trigger_memory_profile);
    
    
    let cpu_profile = warp::post()
        .and(warp::path("profile"))
        .and(warp::path("cpu"))
        .and(warp::path::end())
        .and_then(trigger_cpu_profile);
    memory_profile.or(cpu_profile)
}

现在,我们就可以通过 curl 命令,随时在生产环境中采集性能数据了:

curl -X POST http://localhost:8080/profile/memory
curl -X POST http://localhost:8080/profile/cpu

生成的 .pb 文件,我们就可以通过 go tool pprof 工具,启动一个交互式 Web UI,在浏览器中直观查看调用图、火焰图等。

go tool pprof -http=localhost:8080 ./target/debug/otel-storage ./otel_storage_cpu_profile_20250806_032509.pb

六、性能剖析:火焰图下的“真相”

通过 go tool pprof 启动的 Web UI,我们可以看到程序的火焰图

如何阅读火焰图

※ 顶部: 代表程序的根函数。

※ 向下延伸; 子函数调用关系。

※ 火焰条的宽度: 代表该函数在 CPU 上消耗的时间。宽度越宽,消耗的时间越多,越可能存在性能瓶颈

CPU Profile

Memory Profile

在我们的 CPU 火焰图中,一个令人意外的瓶颈浮出水面:OSS::new 占用了约 19.1% 的 CPU 时间。深入分析后发现, OSS::new 内部的 TlsConnector 在每次新建连接时都会进行 TLS 握手,这是导致 CPU 占用过高的根本原因。

原来,我们的代码在每次写入 OSS 时,都会新建一个 OSS 实例,随之而来的是一个全新的 HTTP 客户端和一次耗时的 TLS 握手。尽管 oss-rust-sdk 内部有连接池机制,但由于我们每次都创建了新实例,这个连接池根本无法发挥作用!

七、优化方案:从“每次新建”到“共享复用”

问题的核心在于重复创建 OSS 实例。我们的优化思路非常清晰:复用 OSS 客户端实例,避免不必要的 TLS 握手开销

优化前

每次写入都新建 OSS 客户端。

fn write_oss() {
    // 每次写入都新建一个OSS实例
    let oss_instancecreate_oss_client(oss_config.clone());
    tokio::spawn(async move {
        // 获取写入偏移量、文件名
        // 构造OSS写入所需资源和头信息
        // 写入OSS
        let result = oss_instance
            .append_object(data, file_name, headers, resources)
            .await;
}
fn create_oss_client(config: OssWriteConfig) -> OSS {
    OSS::new(
    ……
    )
}

这种方案在流量较小时可能问题不大,但在万亿流量的生产环境中,频繁的实例创建会造成巨大的性能浪费。

优化前

※ 共享实例

让每个处理任务( DecodeTask )持有 Arc 共享智能指针,确保所有写入操作都使用同一个 OSS 实例。

let oss_client = Arc::new(create_oss_client(oss_config.clone()));
let oss_instance = self.oss_client.clone()
// ...
let result = oss_instance
    .append_object(data, file_name, headers, resources)
    .await;

※ 自动重建机制

为了应对连接失效或网络问题,我们引入了自动重建机制。当写入次数达到阈值或发生写入失败时,我们会自动创建一个新的 OSS 实例来替换旧实例,从而保证服务的健壮性。

// 使用原子操作确保多线程环境下的计数安全
let write_countself.oss_write_count.load(std::sync::atomic::Ordering::SeqCst);
let failure_countself.oss_failure_count.load(std::sync::atomic::Ordering::SeqCst);


// 检查是否需要重建实例...
fn recreate_oss_client(&mut self) {
 
    let new_oss_client = Arc::new(create_oss_client(self.oss_config.clone()));
    self.oss_client = new_oss_client;
    self.oss_write_count.store(0, std::sync::atomic::Ordering::SeqCst);
    self.oss_failure_count.store(0, std::sync::atomic::Ordering::SeqCst);
    // 记录OSS客户端重建次数指标
    OSS_CLIENT_RECREATE_COUNT
        .with_label_values(&[])
        .inc();
    info!("OSS client recreated");
}

八、优化效果:性能数据“一飞冲天”

优化后的服务上线后,我们观察到了显著的性能提升。

CPU 资源使用率

同比下降约 20%

OSS 写入耗时

同比下降约 17.2% ,成为集群中最短的写入耗时。

※ OSS写入耗时

※ OSS相关资源只占千分之一

内存使用率

平均下降 8.77% ,这部分下降可能也得益于我们将内存分配器从 mimalloc 替换为 jemalloc 的综合效果。

这次优化不仅解决了特定服务的性能问题,更重要的是,它验证了在 Rust 中通过 Profiling 工具进行深度性能分析的可行性。即使在已经实现了初步性能提升的 Rust 服务中,仍然存在巨大的优化空间。

未来,我们将继续探索更高效的 Profiling 方案,并深入挖掘其他潜在的性能瓶颈,以在万亿流量的生产环境中实现极致的性能和资源利用率。

引用

往期回顾

1.Valkey 单点性能比肩 Redis 集群了?Valkey8.0 新特性分析|得物技术

2.Java volatile 关键字到底是什么|得物技术

3.社区搜索离线回溯系统设计:架构、挑战与性能优化|得物技术

4.正品库拍照PWA应用的实现与性能优化|得物技术

5.得物社区活动:组件化的演进与实践

文 / 炯帆 南风

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

❌
❌