ShardingSphere inline表达式线程安全问题定位

ShardingSphere inline表达式线程安全问题定位

问题背景

春节期间发现 ShardingSphere 事务 E2E 偶发执行失败问题,并且每次执行失败需要执行很久,直到超时。最终定位发现 inline 表达式存在线程安全问题。本文记录定位并解决 inline 表达式线程安全问题的过程。

问题原因

1.GroovyInlineExpressionParser 里有成员变量,存在并发修改,不能使用单例 SPI 实现;
2.执行 Groovy 表达式时,需要执行 rehydrate 方法 copy Closure,使得每个 Closure 都有独立的执行环境,避免属性赋值时产生线程安全问题。

问题定位

构造测试用例尝试复现问题

构造测试用例,且在测试用例中添加线程相关信息,观察执行结果。

@Test
@SneakyThrows({ExecutionException.class, InterruptedException.class})
void assertThreadSafety() {
    int threadCount = 2;
    ExecutorService pool = Executors.newFixedThreadPool(threadCount);
    List<Future<?>> futures = new ArrayList<>(threadCount);
    for (int i = 0; i < threadCount; i++) {
        Future<?> future = pool.submit(this::createInlineExpressionParseTask);
        futures.add(future);
    }
    for (Future<?> future : futures) {
        future.get();
    }
    pool.shutdown();
}

private void createInlineExpressionParseTask() {
    for (int j = 0; j < 5; j++) {
        // 传入线程信息
        String resultSuffix = Thread.currentThread().getName() + "--" + j;
        String actual = TypedSPILoader.getService(InlineExpressionParser.class, "GROOVY", PropertiesBuilder.build(
                new PropertiesBuilder.Property(InlineExpressionParser.INLINE_EXPRESSION_KEY, "ds_${id%2}"))).evaluateWithArgs(Collections.singletonMap("id", 1));
        // 断言执行出来的结果是不是当前线程的
        assertThat(actual, is(String.format("ds_%s", resultSuffix)));
        String actual2 = TypedSPILoader.getService(InlineExpressionParser.class, "GROOVY", PropertiesBuilder.build(
                new PropertiesBuilder.Property(InlineExpressionParser.INLINE_EXPRESSION_KEY, "account_${id}"))).evaluateWithArgs(Collections.singletonMap("id", resultSuffix));
        assertThat(actual2, is(String.format("account_%s", resultSuffix)));
    }
}

通过修改线程数测试几组,结果如下:
1: 100个线程,一个线程里一个inline表达式,报错
2: 1个线程,一个线程里两个不同inline表达式,没问题
3: 2个线程,一个线程里两个不同inline表达式,报错

那么只需要最少两个线程并发执行即可稳定复现这个 bug。

对象属性线程间共享问题

初步看代码,GroovyInlineExpressionParser 之前是单例的,线程之间会共享同一个实例,GroovyInlineExpressionParser 类中发现有一处成员变量 inlineExpression 多个线程中共享,并发修改的话会导致数据不正确。

// InlineExpressionParser 的 spi 之前是单例的,所有线程对于同一个类型的 spi 实现,共享同一个对象。
@SingletonSPI
public interface InlineExpressionParser extends TypedSPI {}

// GroovyInlineExpressionParser 实现
public final class GroovyInlineExpressionParser implements InlineExpressionParser {
    
    private static final String INLINE_EXPRESSION_KEY = "inlineExpression";
    
    private static final Map<String, Script> SCRIPTS = new ConcurrentHashMap<>();
    
    private static final GroovyShell SHELL = new GroovyShell();
    
    // 可以看到此处是成员变量,多个线程之间会串。
    private String inlineExpression;
    
    @Override
    public void init(final Properties props) {
        inlineExpression = props.getProperty(INLINE_EXPRESSION_KEY);
    }
}

这个问题修复起来很简单,移除 @SingletonSPI 声明,每次创建新的实例即可。

添加日志观察执行流程

上述问题修复后,继续执行测试用例,发现还是会报错。报错日志如下:
在这里插入图片描述

看起来就是当 thread2 获取结果之前, thread1 执行了 inline 表达式,thread2 拿到了 thread1 执行的结果。
通过日志也能看出来 thread2 执行报错时 [pool-1-thread-2] ERROR 拿到了线程1的执行结果result:ds_pool-1-thread-1--0

15:12:29.630 [pool-1-thread-2] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParser - inlineExpression:ds_${id}, this:2086977125, closure:1142533146, map:{id=pool-1-thread-2--0}
15:12:29.689 [pool-1-thread-2] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParserTest - ds_id, j:0,result:ds_pool-1-thread-2--0
15:12:29.689 [pool-1-thread-1] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParser - inlineExpression:ds_${id}, this:629624949, closure:595824174, map:{id=pool-1-thread-1--0}
15:12:29.689 [pool-1-thread-1] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParserTest - ds_id, j:0,result:ds_pool-1-thread-1--0
15:12:29.703 [pool-1-thread-2] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParser - inlineExpression:account_${id}, this:1499502307, closure:778755431, map:{id=pool-1-thread-2--0}
15:12:29.704 [pool-1-thread-2] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParserTest - account_id, j:0,result:account_pool-1-thread-2--0
15:12:29.705 [pool-1-thread-2] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParser - inlineExpression:ds_${id}, this:1218528180, closure:667406814, map:{id=pool-1-thread-2--1}
15:12:29.704 [pool-1-thread-1] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParser - inlineExpression:account_${id}, this:2009787398, closure:1068736255, map:{id=pool-1-thread-1--0}
15:12:29.705 [pool-1-thread-1] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParserTest - account_id, j:0,result:account_pool-1-thread-1--0
15:12:29.705 [pool-1-thread-2] ERROR org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParserTest - ds_id, j:1,result:ds_pool-1-thread-1--0
15:12:29.705 [pool-1-thread-1] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParser - inlineExpression:ds_${id}, this:1072633959, closure:478686467, map:{id=pool-1-thread-1--1}
15:12:29.705 [pool-1-thread-1] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParserTest - ds_id, j:1,result:ds_pool-1-thread-1--1
15:12:29.709 [pool-1-thread-1] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParser - inlineExpression:account_${id}, this:2138009411, closure:1272220298, map:{id=pool-1-thread-1--1}
15:12:29.709 [pool-1-thread-1] INFO org.apache.shardingsphere.infra.expr.groovy.GroovyInlineExpressionParserTest - account_id, j:1,result:account_pool-1-thread-1--1

DEBUG模拟复现流程

通过 debug 控制两个线程执行流程,当 thread1 在 result.call().toString() 执行之前等待 thread2 执行完成后再返回时,thread1 得到的结果不正确。

public String evaluateWithArgs(final Map<String, Comparable<?>> map) {
    // 1.thread1执行
    // 3.thread2执行
    Closure<?> result = (Closure<?>) evaluate("{it -> \"" + handlePlaceHolder(inlineExpression) + "\"}");
    log.info("inlineExpression:{}, this:{}, closure:{}, map:{}", inlineExpression, System.identityHashCode(this), System.identityHashCode(result), map);
    result.rehydrate(new Expando(), null, null)
            .setResolveStrategy(Closure.DELEGATE_ONLY);
    map.forEach(result::setProperty);
    // 2.thread1在此等待
    // 5.thread1执行完毕返回结果,观察到结果不对
    return result.call().toString();    
    // 4.thread2执行完毕返回结果
}

结合后面的分析也能得出,因为线程之间共享了 context,上面 thread2 执行完把 context 里共享的属性给改了,导致 thread1 执行出现问题。

// 原有有问题的代码,只调用了 rehydrate 方法,但是没有获取 rehydrate 方法里 clone 后的结果。所有线程之间共享一个执行环境,导致属性会串。
public String evaluateWithArgs(final Map<String, Comparable<?>> map) {  
    Closure<?> result = (Closure<?>) evaluate("{it -> \"" + handlePlaceHolder(inlineExpression) + "\"}");  
    // 这里会 copy Closure,返回新的执行环境,但是结果被忽略了
    result.rehydrate(new Expando(), null, null)  
            .setResolveStrategy(Closure.DELEGATE_ONLY);
    // 所以这里设置属性时,共用的同一个 context, 线程之间会串
    map.forEach(result::setProperty);  
    return result.call().toString();  
}

// 修改后的代码如下:获取 copy 的 Closure,那么每个 Closure 使用单独的执行环境。
Closure<?> result = ((Closure<?>) evaluate("{it -> \"" + handlePlaceHolder(inlineExpression) + "\"}")).rehydrate(new Expando(), null, null);

问题分析

DEBUG分析问题

由于上面发现执行 evaluateWithArgs 方法会有线程安全问题,所以 DEBUG 分析里面的逻辑。

有问题的代码如下:
result.rehydrate(new Expando(), null, null) 方法作用:重新实例化一个闭包对象,设置 delegate 为 Expando。这样闭包的执行环境将改变,不再依赖于原始闭包的环境。
但是明显下面的代码里没有使用 rehydrate 返回的 copy 的 Closure 对象,最终导致 result::setProperty 设置的属性在线程之间共享了。

Closure<?> result = ((Closure<?>) evaluate("{it -> \"" + handlePlaceHolder(inlineExpression) + "\"}"));
// 有问题的调用代码,没有获取 rehydrate() copy后的 Closure,导致线程之间属性串了。
result.rehydrate(new Expando(), null, null);
map.forEach(result::setProperty);
return result.call().toString();    

// Closure#rehydrate 方法逻辑如下:
public Closure<V> rehydrate(Object delegate, Object owner, Object thisObject) {
    // clone 当前 Closure
    Closure<V> result = (Closure<V>) this.clone();
    // 设置传入的 Expando
    result.delegate = delegate;
    result.owner = owner;
    result.thisObject = thisObject;
    return result;
}

result::setProperty 会调用 groovy.lang.Closure#setProperty 方法将用户传入的属性设置到 context 里,这个 context 是线程间共享的。
如果使用 rehydrate 返回的Closure对象,则属性会设置到各自的 Expando 对象里,不存在线程安全问题。

// 设置属性
groovy.lang.Closure#setProperty
->
// 其中 delegate 就是上面的 Expando 对象。如果没设置,默认使用同一个 context
InvokerHelper.setProperty(this.delegate, property, newValue);

context 初始化逻辑:

// 默认的 context 是在 GroovyShell 构造方法中初始化的,由于复用了同一个 GroovyShell,所以 context 是一份。
private final Binding context;

public GroovyShell(ClassLoader parent, Binding binding, final CompilerConfiguration config) {
    this.context = binding;
    this.config = config;
}

如果使用有问题的代码执行,执行结果如下,由于没有使用 rehydrate 执行 copy 后的 Closure,导致各个线程之间属性会串:
可以看到 thread1 和 thread7 都将属性设置到了共享的 context Bing@3954 对象里,所以线程之间属性会串,导致执行结果有问题。
在这里插入图片描述
在这里插入图片描述

Groovy表达式执行流程

Groovy 根据用户脚本,生成相关类代码,再编译成 Class 对象,然后创建闭包执行脚本。

调用api如下:

private static final GroovyShell SHELL = new GroovyShell();

// 1.解析 groovy script
// 生成scriptName;通过 groovy classloader 编译类,返回Class实例;通过Class反射创建Script对象返回
Script script = SHELL.parse(expression);

// 2.运行script。会调用到 会调用 Closure.run -> Closure.call -> 自定义script类的doCall方法
Closure<?> result = (Closure<?>) script.run();

// 3.重新实例化一个闭包对象,设置 delegate 为 Expando。这样闭包的执行环境将改变,不再依赖于原始闭包的环境
result.rehydrate(new Expando(), null, null);
result.setResolveStrategy(Closure.DELEGATE_ONLY);

// 4.设置参数
map.forEach(result::setProperty);

// 5.运行闭包
return result.call().toString();

查看Groovy生成的类

可以通过 arthas 反编译,查看 groovy 生成的 Script 类的代码。其中 doCall 方法就是用来具体处理 inline 表达式计算结果的方法。

[arthas@97658]$ sc Script*

[arthas@92326]$ jad Script1$_run_closure1

ClassLoader:
+-groovy.lang.GroovyClassLoader$InnerLoader@2960c0ad
  +-groovy.lang.GroovyClassLoader@22c53a48
    +-jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7
      +-jdk.internal.loader.ClassLoaders$PlatformClassLoader@76f18a45

Location:
/groovy/shell

/*
* Decompiled with CFR.
*/
import groovy.lang.Closure;
import groovy.lang.MetaClass;
import java.lang.invoke.MethodHandles;
import org.codehaus.groovy.reflection.ClassInfo;
import org.codehaus.groovy.runtime.GStringImpl;
import org.codehaus.groovy.runtime.GeneratedClosure;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.vmplugin.v8.IndyInterface;

public final class Script1._run_closure1
extends Closure
implements GeneratedClosure {
    private static /* synthetic */ ClassInfo $staticClassInfo;
    public static transient /* synthetic */ boolean __$stMC;

    public Script1._run_closure1(Object _outerInstance, Object _thisObject) {
        super(_outerInstance, _thisObject);
    }

    // call时会调用 doCall 方法
    public Object doCall(Object it) {
        return new GStringImpl(new Object[]{IndyInterface.bootstrap("getProperty", "id", 12, this)}, new String[]{"ds_", ""});
    }

    protected /* synthetic */ MetaClass $getStaticMetaClass() {
        if (this.getClass() != Script1._run_closure1.class) {
            return ScriptBytecodeAdapter.initMetaClass(this);
        }
        ClassInfo classInfo = $staticClassInfo;
        if (classInfo == null) {
            $staticClassInfo = classInfo = ClassInfo.getClassInfo(this.getClass());
        }
        return classInfo.getMetaClass();
    }

    public /* synthetic */ MethodHandles.Lookup $getLookup() {
        return MethodHandles.lookup();
    }
}

Affect(row-cnt:1) cost in 468 ms.

当执行 inline 表达式,调用 Closure.call() 方法时,会调用到 GString 构造方法,values 是传入的参数值替换占位符,strings 是 inline 表达式去除占位符部分,最终返回计算后的结果。
在这里插入图片描述

在这里插入图片描述

PR

https://github.com/apache/shardingsphere/pull/30295

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

实验笔记之——Ubuntu20.04配置nvidia以及cuda并测试3DGS与SIBR_viewers

之前博文测试3DGS的时候一直用服务器进行开发&#xff0c;没有用过笔记本&#xff0c;本博文记录下用笔记本ubuntu20.04配置过程&#xff5e; 学习笔记之——3D Gaussian Splatting源码解读_3dgs运行代码-CSDN博客文章浏览阅读3.2k次&#xff0c;点赞34次&#xff0c;收藏62次…

编写科技项目验收测试报告需要注意什么?第三方验收测试多少钱?

科技项目验收测试是一个非常重要的环节&#xff0c;它对于确保科技项目的质量和可用性起着至关重要的作用。在项目完成后&#xff0c;进行科技项目验收测试可以评估项目的功能、性能和可靠性等方面&#xff0c;并生成科技项目验收测试报告&#xff0c;以提供给项目的相关方参考…

keil uv5 map文件解析

map参考博客&#xff1a;https://www.csdn.net/tags/MtjaYgwsMTY2NzUtYmxvZwO0O0OO0O0O.html 配置外部flash存储代码&#xff1a;https://strongerhuang.blog.csdn.net/article/details/51485903?spm1001.2101.3001.6650.4&utm_mediumdistribute.pc_relevant.none-task-bl…

使用 Helm 安装 极狐GitLab

本篇作者 徐晓伟 使用 Helm 简便快捷的部署与管理 极狐GitLab 前提条件 k8s 完成 helm 的配置 k8s 完成 ingress 的配置 内存至少 10G 演示环境是 龙蜥 Anolis 8.4&#xff08;即&#xff1a;CentOS 8.4&#xff09;最小化安装k8s 版本 1.28.2calico 版本 3.26.1nginx ingre…

Dockerfile(5) - CMD 指令详解

CMD 指定容器默认执行的命令 # exec 形式&#xff0c;推荐 CMD ["executable","param1","param2"] CMD ["可执行命令", "参数1", "参数2"...]# 作为ENTRYPOINT的默认参数 CMD ["param1","param…

高瓴张磊入籍新加坡,这代表了什么?

文&#xff5c;新熔财经 作者&#xff5c;显洋 这两天&#xff0c;海外媒体报道了中国投资大佬与企业家拿到新加坡永居的事儿。本来乏善可陈的文章&#xff0c;却因为一个人名的出现变得有趣起来——高瓴创始人张磊&#xff0c;一位曾经在国内如日中天&#xff0c;但今天鲜少…

论文阅读:2020GhostNet华为轻量化网络

创新&#xff1a;&#xff08;1&#xff09;对卷积进行改进&#xff08;2&#xff09;加残差连接 1、Ghost Module 1、利用1x1卷积获得输入特征的必要特征浓缩。利用1x1卷积对我们输入进来的特征图进行跨通道的特征提取&#xff0c;进行通道的压缩&#xff0c;获得一个特征浓…

解放设计师的创造力:免版的图片素材

title: 解放设计师的创造力&#xff1a;免版的图片素材 date: 2024/2/29 15:10:19 updated: 2024/2/29 15:10:19 tags: 版权无忧创意自由设计效率视觉提升广告设计UI/UX素材移动应用 在设计领域&#xff0c;设计师常常需要使用图片素材来增加作品的视觉效果。然而&#xff0c;…

Docker技术概论(1):Docker与虚拟化技术比较

Docker技术概论&#xff08;1&#xff09; Docker与虚拟化技术比较 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https:…

从 Flask 切到 FastAPI 后,起飞了!

我这几天上手体验 FastAPI&#xff0c;感受到这个框架易用和方便。之前也使用过 Python 中的 Django 和 Flask 作为项目的框架。Django 说实话上手也方便&#xff0c;但是学习起来有点重量级框架的感觉&#xff0c;FastAPI 带给我的直观体验还是很轻便的&#xff0c;本文就会着…

LeetCode34.在排序数组中查找元素的第一个和最后一个位置

题目 给你一个按照非递减顺序排列的整数数组 nums&#xff0c;和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标值 target&#xff0c;返回 [-1, -1]。 你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。 示例 输入…

尚硅谷Java数据结构--希尔排序

插入排序的问题&#x1f388;&#xff1a; arr{2,3,4,5,6,0,9,7,8}; 当0作为插入元素的时候&#xff0c;其待插入下标与原下标相差很远&#xff0c;需要进行多次比较和移动。 希尔排序则是先将下标相差一定距离gap的元素分为一组&#xff0c;进行插入排序&#xff1b;再逐渐将距…

Flutter(四):SingleChildScrollView、GridView

SingleChildScrollView、GridView 遇到的问题 以下代码会报错: class GridViewPage extends StatefulWidget {const GridViewPage({super.key});overrideState<GridViewPage> createState() > _GridViewPage(); }class _GridViewPage extends State<GridViewPage&g…

Maven下载、安装、配置教程

maven是一个项目管理的工具&#xff0c;maven自身是纯java开发的&#xff0c;可以使用maven对java项目进行构建、依赖管理。 通常我们靠手动下载jar包引入项目中是非常浪费时间的&#xff0c;我们可以通过maven工具帮我们导入jar包提高开发效率。 第一步&#xff1a;下载Mave…

Docker技术概论(3):Docker 中的基本概念

Docker技术概论&#xff08;3&#xff09; Docker 中的基本概念 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://…

vivo 在离线混部探索与实践

作者&#xff1a;来自 vivo 互联网服务器团队 本文根据甘青、黄荣杰老师在“2023 vivo开发者大会"现场演讲内容整理而成。 伴随 vivo 互联网业务的高速发展&#xff0c;数据中心的规模不断扩大&#xff0c;成本问题日益突出。在离线混部技术可以在保证服务质量的同时&…

【探索AI】十二 深度学习之第2周:深度神经网络(一)深度神经网络的结构与设计

第2周&#xff1a;深度神经网络 将从以下几个部分开始学习&#xff0c;第1周的概述有需要详细讲解的的同学自行百度&#xff1b; 深度神经网络的结构与设计 深度学习的参数初始化策略 过拟合与正则化技术 批标准化与Dropout 实践&#xff1a;使用深度学习框架构建简单的深度神…

红队基础设施建设

文章目录 一、ATT&CK二、T1583 获取基础架构2.1 匿名网络2.2 专用设备2.3 渗透测试虚拟机 三、T1588.002 C23.1 开源/商用 C23.1.1 C2 调研SliverSliver 对比 CS 3.1.2 CS Beacon流量分析流量规避免杀上线 3.1.3 C2 魔改3.1.4 C2 隐匿3.1.5 C2 准入应用场景安装配置说明工具…

安卓cpu内存监控,大厂首发

开头 很多人工作了十年&#xff0c;但只是用一年的工作经验做了十年而已。 高级工程师一直是市场所需要的&#xff0c;然而很多初级工程师在进阶高级工程师的过程中一直是一个瓶颈。 移动研发在最近两年可以说越来越趋于稳定&#xff0c;因为越来越多人开始学习Android开发&…

适用Java SpringBoot项目的分布式锁

在分布式系统中&#xff0c;常用到分布式锁&#xff0c;它有多中实现方式&#xff0c;如&#xff1a;基于redis&#xff0c;database&#xff0c;zookeeper等。Spring integration组件有这三种服务的分布式锁实现&#xff0c;今天来看看用的比较多的redis和database实现方式。 …