干货 | 携程中转交通方案拼接性能优化

2023-11-04 06:10

本文主要是介绍干货 | 携程中转交通方案拼接性能优化,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

作者简介

简言,携程后端开发经理 ,关注技术架构、性能优化、交通规划等领域。

一、背景介绍

由于交通规划和运力资源的限制,用户查询的两地之间可能没有直达交通,或者在重大节假日时,直达交通都已售罄。不过,通过火车、飞机、汽车、船舶等两程或多程中转的方式,用户仍然可以到达目的地。此外,中转交通有时在价格和耗时方面更具有优势。例如,对于从上海到运城,通过火车中转可能比直达火车更加快捷和便宜。

fab0aec6c24b1944efffdc1d6ee21570.png

图1 携程火车中转交通列表

在提供中转交通方案时,很重要的一个环节是将两程或多程的火车、飞机、汽车、船舶等拼接起来组成可行的中转方案。而中转交通拼接的第一个难点是拼接空间极大,仅考虑上海做中转城市,就可以产生近亿种组合;另一个难点在于对实时性有要求,因为产线数据随时变化,需要不断地查询火车、飞机、汽车、船舶的数据。中转交通拼接需要大量的计算资源和IO开销,因此,对其性能进行优化显得尤为重要。

本文将结合实例,介绍在中转交通拼接性能优化过程中所遵循的原则、分析和优化方法,旨在为读者提供有价值的参考和启示。

二、优化原则

性能优化需要在满足业务需求的前提下,在各种资源和约束条件下去平衡和取舍,遵循一些大的原则有助于消除不确定性,去逼近解决问题的最优解。具体来说,中转交通拼接优化过程中主要遵循以下三个原则:

2.1 性能优化是手段而不是目的

虽然本文是关于性能优化的,但仍需要在最开始强调:不要为了优化而优化。满足业务需求的方式有很多,性能优化只是其中一种。有时候问题非常复杂,限制也很多,在不显著影响用户体验的前提下,通过放宽限制或采用其他流程来减少对用户的影响,这也是解决性能问题的好方法。在软件开发中,存在许多通过牺牲少量性能来实现大幅降低成本的事例。例如,在Redis中用于基数统计(去重)的HyperLogLog算法,它在标准误差为0.81%的前提下,只需要12K空间就能够统计264的数据。

回到问题本身,由于需要频繁地查询产线数据,并且进行海量的拼接操作,那么如果要求每个用户查询时都立刻返回最新鲜的中转方案,成本将会非常高。为了降低成本,需要在响应时间和数据新鲜度之间进行平衡。经过仔细考虑选择可以接受分钟级的数据不一致,对于一些冷门线路和日期,可能在首次查询时没有好的中转方案,此时引导用户重新刷新页面即可。

2.2 不正确的优化是万恶之源

Donald Knuth在《Structured Programming With Go To Statements》中提到:“程序员们浪费大量的时间去思考、担忧非关键路径的性能,而尝试优化这部分性能,对整体代码的调试和维护都有非常严重的负面影响,因此97%的情况,我们应该忘记小的优化点”。简而言之,在没有发现真正的性能问题之前,在代码层面过度炫技式的优化,不仅不会提高性能,反而可能会导致更多的错误。然而作者同样也强调:“对于剩下关键的3%,我们也不要错过优化的机会”。因此,需要时刻关注性能问题,不做会影响性能的决策,并在必要的时候做正确的优化。

2.3 量化分析性能,明确优化方向

正如前一节所述,在进行优化之前,首先要量化性能并找出瓶颈,这样优化的才更有针对性。量化分析性能可以借助耗时监控、Profiler性能分析工具、Benchmark基准测试工具等,重点关注耗时特别长或者执行频率特别高的地方。正如阿姆达尔定律所述:“系统中对某一部件采用更快执行方式所能获得的系统性能改进程度,取决于这种执行方式被使用的频率,或所占总执行时间的比例”。

此外,还需要注意到性能优化是一场持久战。随着业务的不断发展,架构和代码也不停地变化,因此更需要持续量化性能,不断分析瓶颈和评估优化效果。

三、性能分析之路

3.1 梳理业务流程

在性能分析之前,首先要梳理业务流程。中转交通方案拼接主要包含以下四个步骤:

a.  加载线路图,如北京经南京中转到上海,只考虑线路本身的信息,与具体的班次无关;

b.  查火车、飞机、汽车、船舶的产线数据,包括出发时间、到达时间、出发站、到达站、价格和余票信息等;

c.  拼接出所有可行的中转交通方案,主要是考虑换乘时间不能过短,以免无法完成换乘;同时也不宜过长,以免等待太久。拼接出可行的方案后,还需要完善业务字段,例如总价格、总耗时和换乘信息等;

d.  根据一定的规则,从拼接出的所有可行中转方案中筛选出一些用户可能感兴趣的方案。

3.2 量化分析性能

(1)增加耗时监控

耗时监控是一种最直观的从宏观角度观察各个阶段耗时情况的手段。它不仅可以查看业务流程各阶段的耗时值与耗时占比,还可以长期观察耗时变化趋势。

耗时监控可以借助公司内部的指标监控告警系统,在中转交通方案拼接的主要流程中增加耗时打点。这些流程包括加载线路图、查询班次数据并进行拼接、筛选和保存拼接方案等。各个阶段的耗时情况如图2所示,可以看到,拼接(含查产线数据)的耗时占比最高,因此成为未来重点优化的目标。

60a2231be15ba1eb4fa90ba66173142f.png

图2 中转交通拼接耗时监控

(2)Profiler性能分析

耗时打点可能会侵入业务代码,并对性能产生影响,因此不宜过多,更适合监控主要流程。与之对应的Profiler性能分析工具(例如Async-profiler),可以生成更具体的调用树以及各函数的CPU占用比例,从而帮助关键路径和性能瓶颈的分析与定位。

46a3ee23a4e75d2b26116b3a9f763090.png

图3 拼接调用树与CPU占比

如图3所示,拼接方案(combineTransferLines)占53.80%,查产线数据(querySegmentCacheable,已使用缓存)占21.45%。在拼接方案中, 计算方案评分(computeTripScore,占48.22%)、创建方案实体(buildTripBO,占4.61%)和检查拼接可行性(checkCombineMatchCondition,占0.91%)是占比最大的三个环节。

b49dd89ef81015caf354f4333f97b108.png

图4 方案打分调用树和CPU占比

继续分析占比最高的计算方案评分(computeTripScore)时,发现主要与自定义的字符串格式化函数(StringUtils.format)有关,包括直接调用(用于展示方案评分细节),以及通过getTripId间接调用(用于生成方案的ID)。自定义的StringUtils.format中占比最高的是java/lang/String.replace,Java 8原生的字符串替换是通过正则实现的,效率比较低(这一问题在Java9之后已经改进了)。

// 计算方案评分(computeTripScore) 中调用的StringUtils.format代码示例
StringUtils.format("AAAA-{0},BBBB-{1},CCCC-{2},DDDD-{3},EEEE-{4},FFFF-{5},GGGG-{6},HHHH-{7},IIII-{8},JJJJ-{9}",aaaa, bbbb, cccc, dddd, eeee, ffff, gggg, hhhh, iiii, jjjj)// getTripId 中调用StringUtils.format代码示例
StringUtils.format("{0}_{1}_{2}_{3}_{4}_{5}_{6}", aaaa, bbbb, cccc, dddd, eeee, ffff)// 通过Java replace实现的自定义format函数
public static String format(String template, Object... parameters) {for (int i = 0; i < parameters.length; i++) {template = template.replace("{" + i + "}", parameters[i] + "");}return template;
}

(3)Benchmark基准测试

借助Benchmark基准测试工具可以更准确地测量代码的执行时间。在表1中,我们通过JMH(Java Microbenchmark Harness)对三种字符串格式化方法和一种字符串拼接方法进行耗时测试。测试结果表明,使用Java8的replace方法实现的字符串格式化性能最差,而使用Apache的字符串拼接函数性能最佳。

表1 字符串格式化与拼接性能对比

实现

执行1000次平均耗时(us)

使用Java8的replace实现的StringUtils.format

1988.982

使用Apache replace实现的StringUtils.format

656.537

Java8自带String.format

1417.474

Apache的StringUtils.join

116.812

四、性能优化之路

通过以上的性能分析,我们发现拼接和查询产线数据是性能瓶颈,字符串格式化影响尤其大。因此,我们将致力于优化这些部分,以提高性能表现。

4.1 优化代码逻辑

优化代码逻辑是最简单且性价比最高的方法,可以是修正有问题的代码或替换为更好的实现。不同的实现,哪怕减上几纳秒,累加起来也是很可观的。借助一些经典算法或数据结构(如快速排序、红黑树等)可以在时间和空间复杂度方面带来显著优势。回到中转交通方案拼接性能优化本身,优化的代码逻辑主要包括:

(1)优化字符串拼接性能

如前面的JMH的结果所示,自定义的字符串格式化函数性能最差,因此作为重点优化目标。优化前后的对比如下所示:

// 优化前,通过Java replace实现的format函数
public static String format(String template, Object... parameters) {for (int i = 0; i < parameters.length; i++) {template = template.replace("{" + i + "}", parameters[i] + "");}return template;
}
// 优化后,通过Apache replace实现的format函数
public static String format(String template, Object... parameters) {for (int i = 0; i < parameters.length; i++) {String temp = new StringBuilder().append('{').append(i).append('}').toString();template = org.apache.commons.lang3.StringUtils.replace(template, temp, String.valueOf(parameters[i]));}return template;
}

根据JMH的测试结果,即使是优化后的格式化函数,其性能也不是最优的。在不显著影响可读性的前提下,应尽量使用性能更优的StringUtils.join函数。

// 优化前
StringUtils.format("{0}_{1}_{2}_{3}_{4}_{5}_{6}", aaaa, bbbb, cccc, dddd, eeee, ffff)// 优化后
StringUtils.join("_", aaaa, bbbb, cccc, dddd, eeee, ffff)

为进一步提升性能,可以在computeTripScore 函数中添加一个开关,仅在调试模式下才展示评分细节,这将确保该字符串格式化函数仅在需要时才被调用。

if (Config.getBoolean("enable.score.detail", false)) {scoreDetail = StringUtils.format("AAAA-{0},BBBB-{1},CCCC-{2},DDDD-{3},EEEE-{4},FFFF-{5},GGGG-{6},HHHH-{7},IIII-{8},JJJJ-{9}",aaaa, bbbb, cccc, dddd, eeee, ffff, gggg, hhhh, iiii, jjjj);
}

优化后的CPU占比如图5所示,此时字符串格式化已经不再是性能瓶颈。

b7fb50ddff58ed05b3a4aa8844cf788f.png

图5 优化后的拼接调用树和CPU占比

(2)增加索引降低拼接时间复杂度

801310432df7ae6adb012d8f08515542.png

图6 增加索引降低拼接时间复杂度

在中转拼接过程中,我们需要将第一程每个班次的到达时间与第二程每个班次的出发时间进行比较,以判断中转时间是否过短或过长。为简化说明,假设换乘时间间隔需要满足大于30分钟且小于6小时。以北京到上海经南京中转的两程火车为例,3月9日北京到南京有66个班次,南京到上海有275个班次,考虑到隔夜车,还需要算上3月10日南京到上海的275个班次,那么最多需要比较36300(66*275*2)次。

为避免频繁比较,参考了MySQL B+树索引的思想,将第二程南京到上海的所有火车班次数据构建成红黑树。其中,树的键为秒级时间戳,例如2023-03-09 11:29出发的G367键为1677247680,值为G367的班次数据。有了索引树,最多只需要10次比较,就可以找到最近的满足最小换乘时间要求的班次。同理,最多需要10次比较,就能找到满足最大换乘时间要求的最晚班次。两者之间的所有班次都满足耗时要求,直接进行拼接即可。改进后最多需要比较1320(66*(10+10))次,约为原来的1/27.5。

(3)使用多路归并求Top-K算法

在筛选方案时,会存在以下场景:有多个中转点,每个中转点都有数百个得分较高的方案(内部已按得分由高到低排序,通过小根堆实现)。最终需要将这些方案合并,并从中筛选出得分最高的K个方案。

最简单的方法是使用快速排序将所有的方案排序,然后选取前K个,时间复杂度约为O(nlog2n)。然而,这并没有利用到每个队列自身有序的特点。通过多路归并算法时间复杂度可降为O(nlog2k),具体步骤为:

a.  从每个队列中拿出第一个元素(得分最高的方案),放入大根堆中;

b.  从大根堆堆顶拿出最大的元素,放到结果集中;

c.  如果该元素所在的队列还有剩余元素,则将下一个元素加入堆中;

d.  重复步骤2和3,直到结果集中包含K个元素或所有的队列都为空。

59f52a50c6be95b0ad6f10f97718f813.png

图7 多路归并求Top-K算法

4.2 构建多级缓存

缓存是一种典型的以空间换时间策略,可以缓存数据和计算结果,缓存数据可以提高访问效率,缓存结果避免了重复计算。缓存在带来性能提升的同时,又会引入新的问题:

  • 缓存容量有限,需要仔细斟酌数据的加载、更新、失效和替换策略;

  • 缓存架构的设计:通常来说内存缓存(如HashMap、Caffeine等)性能最高,而Redis等分布式缓存次之,RocksDB相对较慢,容量上限则正好相反,需要仔细选型并搭配使用;

  • 缓存不一致问题如何解决,能接受多久的不一致。

在中转交通方案拼接过程中,需要使用大量的基础数据(如车站、行政区域等),以及海量的动态数据(例如班次数据)。综合以上因素并结合中转交通拼接的业务特点,缓存架构做如下设计:

  • 基础数据(如车站、行政区域等),因数据量小,变化频率低,全量保存到HashMap中,周期全量更新;

  • 部分火车、飞机、汽车、船舶的班次数据缓存到Redis中,以提高访问效率和稳定性。不同产线采取的缓存策略稍有不同,但总的来说是定时更新与搜索触发更新相结合的方式;

  • 一次拼接过程中可能查询数百次产线数据,Redis毫秒级的延迟累加起来也是非常大的。因此,希望在Redis之上再构建一层内存缓存以提高性能。通过分析发现拼接过程中存在非常明显的热点数据,热门日期和线路的查询占比非常高且数量相对有限。因此可以将这部分热点数据保存到内存缓存中,使用LFU(Least Frequently Used)替换,最终产线数据内存缓存命中率达到45%以上,相当于降低近一半的IO开销。

  • 因为可以接受分钟级的数据不一致,所以将拼接结果缓存起来,在有效期内,如果下一个用户查询同一出发日期的相同线路,直接使用缓存数据即可。因为拼接的中转方案数据相对较大,所以将拼接结果保存到RocksDB中,虽然性能不如Redis,但是对于单次查询影响还可以接受。

a5d9c992aadc42d13c71e3d41fd0bf5b.png

图8 多级缓存结构

4.3 预处理

尽管理论上可以选择任意城市作为两地的中转点,但实际上大部分中转城市都无法拼接出优质的方案。因此,先通过离线预处理筛选出部分高质量的中转点,从而将求解空间从几千降至数十。相对于动态变化的班次,线路数据是相对固定的,每天计算一次即可。此外离线预处理可以借助大数据技术,处理海量数据,相对对耗时不敏感。

4.4 多线程处理

在一次拼接过程中,需要处理数十条不同中转点的线路。每个线路的拼接是相互独立的,因此可以采用多线程处理,这样可以最大程度地降低处理时间。但受线路班次数量和缓存命中率的影响,不同线路的拼接耗时很难一致。很多时候,分配相同任务数量的两个线程,即使一个线程很快执行完,也要等待另外一个线程执行完才能进行下一步操作。为避免这种情况,这里借助ForkJoinPool的work-stealing机制。这个机制可以确保每个线程在完自己的任务后,还会分担其他线程未完成的工作,提高并发效率,减少空闲时间。

但是多线程也不是万能的,使用时需要注意:

  • 子任务的执行需要相互独立、互不影响。如果存在依赖关系,则需要等待前一个任务执行完才能开始下一个任务,这样会使多线程失去意义;

  • CPU核数决定了并发能力的上限,过多的线程会因频繁切换上下文而降低性能,需要特别关注线程数、CPU使用率、CPU Throttled time等指标。

4.5 延迟计算

通过将计算推迟到必要的时刻,可能避免很多多余的开销。例如,在拼接完中转方案后,需要构建方案实体并完善业务字段,这部分也比较消耗资源。而且并非所有拼接的方案都会被筛选出来,这意味着这部分未被筛选的方案仍然需要耗费计算资源。因此延迟完整方案实体对象的构建,先将拼接过程中的数以万计的方案保存为轻量的中间对象,只对筛选之后的数百个中间对象构建完整的方案实体。

4.6 JVM优化

中转交通拼接项目是基于Java 8的,并使用G1(Garbage-First)垃圾收集器,部署在8C8G机器上。G1在实现高吞吐量的同时尽可能满足停顿时间的要求,系统架构部门设置的默认参数已经能够适用于大多数场景,通常不需要专门的优化。

但有些线路中转方案过多,导致报文太大,超过Region大小的一半(8G 默认Region大小是2M),导致很多应该进入年轻代的大对象直接进入了老年代,为了避免这种情况,将Region大小改为16M。

五、总结

通过以上的分析和优化,拼接耗时变化如图9所示:

56d80d21db1472568a2797ab39350bec.png

图9 中转交通方案拼接性能优化效果

虽然每个业务和场景都有各自的特点,性能优化时也需要具体分析。但原理是相通的,依然可以参考本文所述的分析和优化方法。本文所有的分析和优化方法总结如图10所示。

a081d8a0045111533486a8f22f17ac6a.png

图10 中转交通方案拼接优化总结

b71b5f4a1ac017f8b316b00f8e98b313.jpeg

 “携程技术”公众号

  分享,交流,成长

这篇关于干货 | 携程中转交通方案拼接性能优化的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/343783

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

性能测试介绍

性能测试是一种测试方法,旨在评估系统、应用程序或组件在现实场景中的性能表现和可靠性。它通常用于衡量系统在不同负载条件下的响应时间、吞吐量、资源利用率、稳定性和可扩展性等关键指标。 为什么要进行性能测试 通过性能测试,可以确定系统是否能够满足预期的性能要求,找出性能瓶颈和潜在的问题,并进行优化和调整。 发现性能瓶颈:性能测试可以帮助发现系统的性能瓶颈,即系统在高负载或高并发情况下可能出现的问题

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

HDFS—存储优化(纠删码)

纠删码原理 HDFS 默认情况下,一个文件有3个副本,这样提高了数据的可靠性,但也带来了2倍的冗余开销。 Hadoop3.x 引入了纠删码,采用计算的方式,可以节省约50%左右的存储空间。 此种方式节约了空间,但是会增加 cpu 的计算。 纠删码策略是给具体一个路径设置。所有往此路径下存储的文件,都会执行此策略。 默认只开启对 RS-6-3-1024k

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

高效+灵活,万博智云全球发布AWS无代理跨云容灾方案!

摘要 近日,万博智云推出了基于AWS的无代理跨云容灾解决方案,并与拉丁美洲,中东,亚洲的合作伙伴面向全球开展了联合发布。这一方案以AWS应用环境为基础,将HyperBDR平台的高效、灵活和成本效益优势与无代理功能相结合,为全球企业带来实现了更便捷、经济的数据保护。 一、全球联合发布 9月2日,万博智云CEO Michael Wong在线上平台发布AWS无代理跨云容灾解决方案的阐述视频,介绍了

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

MySQL高性能优化规范

前言:      笔者最近上班途中突然想丰富下自己的数据库优化技能。于是在查阅了多篇文章后,总结出了这篇! 数据库命令规范 所有数据库对象名称必须使用小写字母并用下划线分割 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来) 数据库对象的命名要能做到见名识意,并且最后不要超过32个字符 临时库表必须以tmp_为前缀并以日期为后缀,备份

黑神话,XSKY 星飞全闪单卷性能突破310万

当下,云计算仍然是企业主要的基础架构,随着关键业务的逐步虚拟化和云化,对于块存储的性能要求也日益提高。企业对于低延迟、高稳定性的存储解决方案的需求日益迫切。为了满足这些日益增长的 IO 密集型应用场景,众多云服务提供商正在不断推陈出新,推出具有更低时延和更高 IOPS 性能的云硬盘产品。 8 月 22 日 2024 DTCC 大会上(第十五届中国数据库技术大会),XSKY星辰天合正式公布了基于星