Trino 并发查询卡死问题排查

背景

目前实现了一个异步读的需求,大概逻辑就是在构造pageSource的时候,就把读取数据的请求发送出去,然后把数据缓存在内存中。并根据异步读的完成情况,设置这个pageSource的block状态。这样在调用getNextPage的时候,线程就不会因为读取数据而被阻塞住,可以直接从缓存中读取数据。提高线程的利用率。

为了防止并发场景下多个查询读取同一个stripe的时候,会出现重复去读相同数据的场景,造成资源浪费。我们用一个static caffeine cache来保存stripe信息,key是对应的stripe, value是一个queue, 保存每个查询对应的future对象。

在每个stripe读之前,会判断这个cache里面是不是已经有这个key了,如果有说明其他查询已经开始异步读这个stripe, 那这个查询则无需再异步读,直接把这个查询的future放入value中,等第一个查询完成了异步读,则会把value里所有的future取出来并设置为完成状态。这样所有读取这个stripe的查询都完成该stripe的异步读。

问题现象及分析

现象

当并发跑多个查询,且这几个查询读取的数据相同时,会偶发的出现个别查询卡死现象

分析

通过查看Trino webUI,看到失败的查询是在某个worker上卡住导致超时的

进到对应worker里面去搜该查询的split finished日志,因为每个split结束时,都会打印相应的信息,从而可以筛选出卡住的那个split

再根据这个split去查对应的日志,我会给每个stripe在结束异步读时都打印对应日志,进而找到是哪个stripe一直卡住没有完成,因为这个stripe一直没有完成,就会导致读取数据一直不会结束,进而导致查询超时失败。

找到卡住的stripe后,查找对应日志, query2发现该stripe已经有query1正在异步读了,因此把query2的future放入了cache的value中。而query1完成该stripe异步读时,打印cache中value的size值为1,说明在并发情况下,query2把自己的future放入队列,而query1获取队列时并没有感知到。两个操作之间有gap。

解决

为了解决写入和读取之间的gap, 其实就是要保证读写之间的原子性。之前的代码实现中读就是从cache中拿value, 然后遍历value中的future设置完成,写就是从cache中拿value, 然后往里加数据。读和写的过程中拿value的操作和处理value的操作不具备原子性,因此中间是存在并发问题的。

有问题的代码:

java 复制代码
//写入
cache.get(stripeIdentifier, s -> {  
    return new ConcurrentLinkedDeque<>();  
}).add(readFinishedSignal);

//读取
ConcurrentLinkedDeque<CompletableFuture> targetStripe = cache.getIfPresent(stripeIdentifier);  
if (targetStripe != null) {  
    targetStripe.forEach(finishSignal -> finishSignal.complete(null));  
    cache.invalidate(stripeIdentifier);  
}

caffeine cache 可以使用asMap()方法拿到cache内部的concurrentHashMap。然后读和写使用compute和computeIfPresent方法,因为在这两个方法里面的代码是保证原子性的,而且使用这两个方法的时候是会加锁的,这样就可以保证放入队列中的future是能别的线程拿到的。

改正后的代码:

java 复制代码
//写入
cache.asMap().compute(stripeIdentifier, (k, v) -> {  
    if (v == null) {  
        ConcurrentLinkedDeque<CompletableFuture> deque = new ConcurrentLinkedDeque<>();  
        deque.add(readFinishedSignal);  
        return deque;  
    } else {  
        v.add(readFinishedSignal);  
        return v;  
    }  
});

//读取
cache.asMap().computeIfPresent(stripeIdentifier, (k, v) -> {  
    v.forEach(finishSignal -> finishSignal.complete(null));  
    return null;  
});

改正后再测试,确实没有再出现并发卡死的现象了。

相关推荐
程序员小崔日记1 分钟前
WebSocket 全面解析:让浏览器“实时说话”的黑科技(建议收藏)
后端·websocket·实时通信
-Da-15 分钟前
【操作系统学习日记】《现代处理器性能的三重奏:ISA架构、流水线与缓存系统》
后端·缓存·架构·系统架构
李慕婉学姐19 分钟前
Springboot养老服务管理系统c0t92vu6(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
Eward-an26 分钟前
华为ModelEngine全流程评测:从智能体开发到应用编排,解锁企业级AI开发新范式
大数据·人工智能
tang7778939 分钟前
哪些行业用动态代理ip?哪些行业用静态代理IP?怎样区分动态ip和静态ip?(互联网人必码·实用长文)
大数据·网络·爬虫·python·网络协议·tcp/ip·智能路由器
码农刚子1 小时前
.NET 权限系统(RBAC)怎么设计?直接可复用
后端·.net
九硕智慧建筑一体化厂家1 小时前
DDC:看似普通的存在,在楼宇自控系统中却主宰智能建筑高效运行?
大数据·运维·人工智能·网络协议·制造·设计规范
把你毕设抢过来1 小时前
基于Spring Boot的演唱会购票系统的设计与实现(源码+文档)
java·spring boot·后端
Apache Flink1 小时前
(二)走进阿里云实时计算Flink版|场景案例篇
大数据·阿里云·flink·云计算
yiyaozjk1 小时前
Go基础之环境搭建
开发语言·后端·golang