引擎启动超时问题

引擎的启动超时问题是一个老顽疾,之前也尝试过但没有解决掉仍然超时。这个问题比较深,需要深入了解OSRM使用的http框架和OSRM加载地图数据的逻辑。

启动问题:
服务刚发布后的短时间内会出现大量的超时报错;正常请求rt为2-3ms,启动后第一个请求rt达到1000ms左右。这是因为RoutingMachineAPP在刚启动的时候处理请求的时间过长导致的。这个问题在本地也可以复现,初步考虑不是容器的问题,是存在于服务本身的问题。

尝试解决:
这个问题是个老问题了,之前采取的解决措施是内部预热的方式;即在服务刚刚启动时,APP内部会启动一个线程池给该服务循环发送请求,以达到预热的目的。随后外部服务请求的rt就不会出现超时问题。但是上线后并没有发现有改善,仍然会报错。

排查思路:
1.可能是APP的http框架问题。在服务启动时http框架会启动一个线程池来处理请求,可能是激活线程或线程池需要时间。
2.地图数据的加载问题。服务刚启动时加载地图数据的方法可能是懒加载,或者加载速度比较慢。

现有结论:
1.该超时问题在本地可以复现。
2.内部预热无效。预热日志显示刚开始的请求超时,随后恢复正常。但如果是http框架的问题,为什么内部预热后仍然会报错呢?如果是数据的问题,预热的rt已经正常了,为何还会报错呢?
3.使用CLion中的profile,可以监控请求在调用链路的每个对象中消耗的时间。我们可以检查超时请求的调用链路各个部分的耗时,先定位问题所在的层次。

问题原因:
通过深入排查以及的讨论,目前大致认为是内存加载的问题。引擎对于地图部分底层数据的加载方式采用懒加载,使得流量过来时需要进行IO把数据读取到内存。
在使用route服务时,首先将路径的起点和终点吸附到边图的点(edge based node)上,然后才开始计算最优路径。所以在吸附过程中需要从外存加载所需的数据,主要是在吸附这一阶段最费时间。
详细来说:粘附过程中,路径引擎是如何查找千万级的点呢,将所有的点组合成为一个R树,极大地方便我们索引找点。但是引擎在加载数据时并不会直接加载这个R树而是热加载R树的所有非叶子结点,是我们查找速度很快,但是所有叶子结点是通过懒加载的方式加载的,所以刚开始的请求需要加载叶子结点的数据而很费时。
高维数据存储 —— R 树_r树-CSDN博客
解决思路:
1.进一步确定这个原因是否属实:可以在本地测试,在服务启动前我们采用内部预热的方式发送请求;不同于之前的预热,之前预热的请求点都是固定的,不能起到预热数据的目的。所以这次我们会选取一些有代表性的点,将数据提前入热加载到内存里。然后看看启动请求的耗时。
2.修复:预热的方式治标不治本。如果确定了原因是这个,我们将去修改代码让引擎在初始化的时候就把R树数据全量加载即可。

第一次尝试:
预热措施。
引擎处理一个路径规划请求的步骤是:

目前我们认为在第一步,也就是将起点终点进行吸附时需要查询r树,但是osrm的r树是采用懒加载的方式,可能耗时增加是因为出现了还没有加载过的点使得线程阻塞,IO耗时导致的:

image.png

image.png

此处使用MMAP方式映射外存

image.png


m_object是一个类似于列表的容器,支持下表运算符取数据m_objects[index]。
1.首先我们想是否可以通过直接打点的方式强行预热数据,把热点数据加载到内存中来。
预热结果:
采集了广州市内10000个walkcase点,使用nearest预热。
测试集:顺风车广州5000case
超时条件:5ms

是否预热

1

2

3

4

N

991

1012

1063

1033

Y

988

965

1087

976

并无明显提高,先pass点预热方式。可能是因为r树底层非叶子结点太小,10000个case的预热仍然不够充分。
2.所以我们在引擎初始化时遍历m_object所有元素存入一个大容器list中:

image.png


后续引擎的所有操作将使用v_object,此时已经将fileindex内容全部加载到内存。
修改了rtree的效果:
测试集:顺风车广州5000case
超时条件:5ms

是否预热

1

2

3

4

N

1021

1116

991

1015

Y

931

905

944

931

很难理解为什么还是没有明显提升,是不是我们的思路错了。
看来需要重新盘一下逻辑。

单个超时请求火焰图(超时rt:72ms)

image.png


似乎耗时的时间并没有集中在吸附过程中。
预热5000请求火焰图(超时:1039个)

image.png



一个正常请求是0.1-1ms,但是目前超时的请求都在5ms以上。如果是某个过程的问题,那么这个过程的耗时将rt提高了5-50倍,火焰图上应该可以看到明显的长条。但是目前来看并没有某个过程很长,而是三个过程类似均匀的变长了。

第二次尝试:
目前我们理解是:因为Rtree的叶子结点是通过mmap的方式加载使app在提供服务的过程中还需要IO读取数据造成了耗时。但是火焰图的结果不如人意,完全不是我们要的结果;打点预热的结果也无法解决问题。
刚开始就错了。分析问题的工具使用错了,定位出的问题自然也错了。
最开始对于火焰图的理解仅仅在于它可以记录一个函数的运行时间。
火焰图:Profiler | CLion Documentation

image.png


pref命令,采用固定频率来暂停程序,在cpu上取样得到当前正在运行的函数,根据函数信息把所有采样数据分类归纳,产生一张火焰图。是一个性能分析工具。
举个例子:A{B{C{D}}},A{B{E{F}}}

perf record -F 99 -p 3887 -g -- sleep 30
解释参数,perf record 表示采集系统事件,-F 99 表示每秒 99 次,-p 3887 是进程号,即对哪个进程进⾏分析,-g 表示记录调⽤栈,–sleep 30 则是持续采集 30 秒。如果 99次都返回同⼀个函数名,那就说明 CPU 这⼀秒钟都在执⾏同⼀个函数,说明可能存在性能问题。正常情况下,采样对机器性能的影响较小,大约5%,所以不用考虑采样本身的影响。
jetbrains旗下的cpp和java开发工具都集成了火焰图的功能,可放心食用。大家以后可以使用这种方式来分析性能瓶颈。

收!pref虽好,但不适用于这个场景
已知我们的问题在于叶子节点的懒加载问题,火焰图没有给出合理的结果。这是因为火焰图是对cpu采集数据的,他只是把cpu上所有的当前运行线程的采样结果进行聚类分析;也就是说,如果一个线程发生了阻塞被挂起,那火焰图是不会记录阻塞时间的。也就是说:我们刚刚看到的火焰图是没有阻塞IO时间的,只是函数占用cpu运行时间的占比(占比均匀是正常的)。
而叶子结点的懒加载耗时就是IO阻塞产生的耗时,火焰图是不会记录的。这才导致火焰图结果于我们预期不符。

使用正确的分析工具:
使用正确的分析工具是结局问题的第一步,至此我们推倒之前的结果重新来过。
我们采用计时器的方式,通过记录函数开始运行和结束运行的时间差,来准确记录整个函数的真实耗时;传统的分析方式依然靠谱。

对三个核心方法和父方法进行了计时来统计每个方法的真实耗时。然后将代码发到fat环境的group1分组,使用qps为1000的压力机来模拟线上环境。

image.png

image.png


果然,对于超时的请求来说整个吸附函数的总耗时(IO阻塞➕cpu运行)占比都是偏高的,几乎都达到60%以上;和火焰图的结果结合分析得出,吸附函数的超时确实都用来IO了;这就和我们的预期一致了。

分析结果与猜想一致,那解决方案也和之前差不多
考虑打点预热和内部预热两种方式。
就我个人来看,打点预热的方式不好:
1.之前就有采用过3000case的预热方式,确实有一定效果。但是,这根本不是根因,我们没有分析出问题的本质,这是治标不治本的方法。就像人总是胃疼,该开始可以开点止痛药;但如果频繁出现,正常人都会考虑去大医院做个胃镜。就这点来看,原来的打点预热远远不能满足目前的需求。现在我们的目的是深入地全面地根除这个困扰已久的问题。
2.Rtree的叶子结点文件是5个G,里面有多少个节点?3000点是否足够用来预热5个G的数据?5G的数据是如何被懒加载的?如果是以对象加载,一个对象多大?就算使用打点预热也不能不能盲目地使用。
3.mmap是如何加载数据的?我们如何能有效预热数据?
基于上述原因,我认为:本次解决预热报错问题,一定是一次对于路径引擎底层的探索;要摸清问题的起因,找出合适的解决方案,还要对路径引擎加载数据的方式有更全面的了解。

加载什么数据:
路径引擎需要加载所有的路网数据,主要是两个图:NodeBasedGraph和EdgeBasedGraph。其中NodeBasedGraph是extract过程中将OSM数据进行压缩处理得到的优化后的,理想的路网数据(比如说红绿灯压缩,多条道路压缩等);EdgeBasedGraph是extract过程中将边进行抽象变为点,得到的边拓展图。

边拓展图(OSRMEDGE-EXPANDEDGRAPH)

EXTRACTION的目标

image.png


最终的所有数据存储在24个文件中。其中fileindex用来存r树的所有叶子节点。

image.png

image.png


各个文件的存储的具体信息不再讲述,可以看这个网址:
Toolchain file overview · Project-OSRM/osrm-backend Wiki · GitHub
然后我们可以在源码中找到这个文件的加载方式:

image.png

image.png

image.png


叶子结点确实是使用mmap的方式来读取的;这也是为什么吸附函数耗时的元凶,就是这里使用了mmap的方式读取数据。

mmap是啥?

image.png

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。

image.png

同时mmap还有一个特点:并非一次性加载数据到内存,而是采用懒加载的方式;且一次加载后就不会销毁,会一直存在在内存中。(耗时的就是第一次加载的IO,后续使用该数据直接内存里取,不需要IO了)

试错初体验:
有了这些基础知识,问题就简单多了。我们不要采用mmap的方式来读取了,我们可以使用iostream,直接把这个文件读入内存不就完了吗,全量加载到一个容器中即可。后面取数据就在这个容器里面操作。
直接把目标地址的文件全量读入到一个vector中(类似于java的List),后面整个rtree使用这个vector就行了。


本地测试通过!吸附函数的耗时有明显降低。
推入到fat环境中发现,实例无法启动,疯狂重启失败。
经过排查发现:Rtree并不只是要初始化一次的!会被初始化6次!
全量加载fileindex需要5G内存,7次就是35G,而且都是重复的数据,这是很没有必要的。容器内存只有48个G,除了其他路网数据只剩10个G,重复加载fileindex会占用35个G,导致容器内存爆了。这种方法pass。
那为什么Rtree会被初始化7次呢?因为有7个服务。每个服务内部都有一个自己的R树,所以每初始化一个服务都是初始化一个服务内部的rtree。

为什么osrm可以初始化6次Rtree而内存不会爆呢?
osrm中rtree的内部容器使用的是自己写的vector_view容器,和std标准库的vector大有不同。理解了osrm中vector的工作方式应该就能解决这个问题。

原来,vector_view这个容器本身并不占有多么大,只储存一个指向真实数据的指针和真实数据的长度。经过后面的debug才明白:osrm中的vector_view只是一个视图
可以理解为所有rtree的叶子节点都是共用一份底层数据的。所以我刚刚全量加载的办法是错误的,会造成重复加载浪费内存。



此路不通再寻他法:
面对懒加载问题可选方案只有两种:1.全量加载,2.内部预热。
全量加载的方法已经失败,而且这种方法是最简单且暴力的。通俗说就是无脑的。
考虑内部预热的方法:
预热,也就是在服务初始化的过程中,把数据加载到内存中。而且mmap具有读取一次全程有效的特点,所以我们只要在服务初始化的时候把这个mmap数据全部遍历一遍即可。
这样做既不用大幅度修改代码,也不用新加入任何变量,只需一个函数遍历这个vector_view即可,同时还能保证其他的共享该数据的vector_view也能使用预热过的数据。



依然没有效果:
但是感觉离胜利很接近了。于是我就继续搜索,为什么这个方法没有成功。最后把问题锁定到了C++编译器的ROV(返回值优化)。这里不做过多解释。在我们这个case中,对于mmap指针的解引用是没有进行赋值和进一步操作的,编译器会忽略(优化)掉这个*解引用,*iter.m_value变成了iter.m_value。但是我们需要的是加载指针指向的对象,如果没有接引用那这个预热将没有任何意义。简单,我们稍微操作一下。



这样编译器就不会优化掉解引用了,数据就会被全部加载到内存中了。
然后我们把代码发布到group1上看看效果,没有一条超时日志,压测结果rt也很低,除了最高的50ms都稳定在20左右。差不多成功地解决了这个问题。

而且,在debug过程中我们发现一个城市的rtree叶子结点的数量是300W左右,毛估估全国的Rtree数据应该在5000W左右。

“乌龙”:
接下来应该发布fat的所有分组,然后使用压力机器测试所有分组的rt。结果大跌眼镜,我们看其中一个分组的rt表现。


这和之前的结果完全不同,耗时太高了。没理由啊。
排查了将近一天也没结果。晚上想起来看了一眼堡垒机的内存:

怎么使用的内存这么低???对比一下rt正常的分组的内存。

瞬间知道了答案!启动参数有问题!果然:

正常的启动参数是没有--map这个参数的。
这个参数的含义是说,服务启动后所有的数据都要通过mmap的映射的方式懒加载到内存。所以内存从37G减少到了200多Mb,换来的就是大量请求读取数据都需要IO,使RT快速上升。

乌云后的晴空:
随后,修改了所有分组的启动参数,去除掉--map,然后使用压测机测试:

image.png


效果相当好,没有一个分组超时,最高只有80ms。可以上pro测试了。
面对drvimpt-ft分组,这个报错大户(原来发布都是这个分组的报错最多),我们从上游服务(LBS)来监控其RT变化

image.png

image.png


效果比fat好得多,只有极小的,毫秒级的影响。原来发布一次会有大量的超时,上万个超时报错;现在,这个分组的整个发布过程都没有任何超时。
自此,我们可以说:路径引擎的启动报错已经被修复好了。

还有后续?
我们是否违背了osrm设计者的初衷?这样的修改是盲目的,我们需要知道为什么这样设计。




 

  • 19
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
若依(RuoYi) 是一个基于 Spring Boot 和 Spring Cloud 的企业级微服务框架,它提供了流程引擎的封装,可以很方便地集成 Flowable。在 RuoYi 中,可以通过以下步骤来设置任务超时通知: 1. 在流程定义文件中,设置任务的超时时间。这可以通过设置流程定义的 timers 属性来实现。例如: ``` <userTask id="task1" name="Task 1" flowable:assignee="${assignee}" flowable:dueDate="${dueDate}"> <extensionElements> <flowable:timerEventDefinition flowable:timeDuration="${timeout}" /> </extensionElements> </userTask> ``` 这个例子中,任务的超时时间是通过定义一个 timer 事件来实现的,时间长度由 timeout 变量决定。 2. 在流程实例开始运行时,启动一个定时器。当任务超时时,定时器会触发一个事件。这可以通过使用 Flowable 的 JobService 来实现。例如: ``` ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine(); JobService jobService = processEngine.getManagementService().getJobService(); jobService.createTimerJobQuery() .processInstanceId(processInstanceId) .executionId(executionId) .timers() .singleResult(); ``` 这个例子中,我们使用 JobService 创建一个定时器事件,用于在任务超时时触发。 3. 当定时器事件触发时,发送超时通知。这可以通过调用 RuoYi 提供的消息通知接口来实现。例如: ``` Notification notification = new Notification(); notification.setMsgType(MsgTypeEnum.EMAIL.getValue()); notification.setSubject("任务超时提醒"); notification.setContent("任务 " + taskId + " 已超时,请及时处理。"); notification.setReceiver(receiver); notificationService.sendNotification(notification); ``` 这个例子中,我们通过调用 notificationService.sendNotification 方法来发送超时通知,通知内容包括任务编号和接收人等信息。 通过以上步骤,我们可以在 RuoYi 中实现任务超时通知的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值