Hawkeyes: x86软件迁移Arm的弱内存序问题解决方案

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 本文介绍了x86软件迁移到Arm过程中可能遇到的弱内存序问题的解决方案,解析了弱内存序问题的根因,介绍了Hawkeyes的架构和实现原理。欢迎有需求的团队发送邮件咨询

背景介绍

将多核多线程程序从x86架构的CPU迁移到Arm架构的CPU上往往会面临弱内存序问题。这个问题是迁移过程中的重大阻碍,也是很多业务方斟酌是否应该迁移到Arm机器上的一个关注焦点。因此如何正确且高效地解决这个问题意义重大,关乎Arm和倚天的生态建设。

有许多团队曾经遇到过此类问题,给业务稳定性带来隐患。

倚天团队针对弱内存序问题追本溯源,提供一个可以从根本上能够解决业务弱内存序困扰并能充分体验倚天高性能的解决方案。

弱内存序问题本质剖析

弱内存序问题产生的根本原因是两种架构的CPU具有不同的内存模型(x86:Total Store Order,Arm:Weak Memory Order)。

如下图,x86架构下write memory操作写入内存必须经过write buffer,这是一个FIFO结构,可以严格保证顺序;只有read memory操作可以直接从write buffer或内存中读取,因此可能乱序到write memory之前。而Arm架构下不存在这样的数据结构保证顺序,所有的write memory和read memory操作都可能互相被重排。这导致迁移后的程序在多核的环境中往往会出现由弱内存模型引发的内存读写的乱序现象,很多情况下这种乱序现象与程序原本的逻辑相违背。

x86-arm.png

下面是一个程序示意,在x86上该程序不会出现assert错误由TSO保证了内存访问按照代码逻辑顺序执行,而迁移到Arm上时,该程序很可能出现assert断言错误,原因是thread1对变量a和变量b的访问出现了乱序,变量a在变量b被赋值之前并没有被正确赋值,乱序到了b = 1对应的指令之后。

int a = 0;
    int b = 0;
    ------------------------------------
    /* thread1 CPU1 */
    a = 1;
    // Need Barrier
    b = 1;
    ------------------------------------
    /* thread2 CPU2 */
    while(b != 1);
    // Need Barrier
    assert(a == 1);

简单的解决方案是在程序中的乱序风险位置添加memory barrier,在Arm架构下我们通常使用类似“dmb ish”这样的指令。

一般我们在迁移过程中都是通过专家对程序进行逻辑分析和排查来解决这类问题,然而人工排查程序中存在的乱序问题对于大型程序来说费力且无法在正确性上得到保证,存在大量的漏报现象,因此亟需自动化的工具协助人工进行定位和检测。

我们的方案:Hawkeyes

Hawkeyes工具即是我们带来的弱内存序问题解决方案

workflow说明

Hawkeyes架构.png

原理介绍

基于Tsan抓取memory access conflicts

可以在前面的例子中看到出现弱内存序问题的场景实际上可以分解成多线程对于多个全局变量的异步访问存在逻辑顺序规定的问题。因此要定位弱内存序问题必须首先定位内存访问冲突,在此基础上我们可以通过分析同一个线程内。

我们基于Thread Sanitizer(Tsan)这一集成在gcc中的Data Race检测器来实现我们的内存冲突检测工具。通过定制化Tsan,使其在程序运行时能够动态地抓取并输出所有对于同一块内存区域进行过访问的线程及其调用栈信息

具体的技术细节是通过编译时对所有内存访问指令位置插桩,在运行时通过shadow memory存储记录线程访问相关的信息,再在每次对shadow memory作更新时对这些存储的信息和新记录的信息进行处理并进行冲突分析。

插桩示意图如下:

Instrumentation.png

shadow memory检测

instrumentation2.png

基于Instruction window对冲突区域作过滤

指令窗口(Instruction Window)是现代处理器的硬件架构中被广泛使用的一个结构,可以简单理解为处于同一个指令窗口大小内的指令会被乱序发射,而指令窗口外的指令则互相之间不会存在乱序现象,指令窗口的大小在不同的处理器中是不一样的。

利用这个特性,我们可以在每一个线程中对前面抓取到的所有内存冲突位置进行指令区间大小的分析,分解所有指令为微指令micro-instruction,再与指令窗口大小作比较进行判断,位于同一个指令窗口内的指令即为潜在的存在乱序的位置。再将指令对应到源码层级,即可给出源码层级对于内存屏障的修改建议,方便开发者进行检查和修改。

Inst-win2.png

检查不同线程间的乱序区间来进行精准定位

上一步的结果实际上只是帮助我们找到了程序中所有可能出现“乱序情况”的内存访问指令区间,而不能直接帮助我们定位乱序情况导致的弱内存序错误。弱内存序问题实际上需要多个线程包含相同的乱序区间才会真正产生,因为只有这种情况才会出现逻辑上的依赖关系。(当然直接在所有此类乱序区间添加屏障也不失为一种简单粗暴的解决办法,并且在Tsan无法输出足够多的信息时是更好的办法)

同样举典型的弱内存序问题例子来看,我们新增一个全局变量c,让thread3进行变量b和变量c的读操作,thread3进行变量c的写操作。我们可以从定制后的Tsan的结果中得到变量b和变量c均存在内存访问冲突的现象(因为都被不同的thread在不同的时间先后读取或写入),假设thread3的读b和读c的指令都在同一个指令窗口内,那么可以说明这两条指令之间会存在乱序现象,然而这种乱序情况对程序逻辑并没有任何影响,因为在thread4中并不存在对变量b的读写操作,因此他们之间实际上没有逻辑依赖关系。

int a = 0;
    int b = 0;
  int c = 0;
    ------------------------------------
    /* thread1 CPU1 */
    a = 1;
    // Need Barrier
    b = 1;
    ------------------------------------
    /* thread3 CPU3 */
    while(b != 1)     // read b instruction 
    ;
    //......
    assert(c == 0);   // read c instruction
  ------------------------------------
    /* thread4 CPU4 */
    c = 1;

因此单纯的乱序情况实际上是被我们所允许的,可以说Arm架构下更多的乱序情况本身就相对x86严格的保序情况有更大的性能提升,这也是Arm用作高性能计算的优势之一。

因此进一步的,我们需要对不同线程之间所有对应相同内存访问区域的乱序区间进行匹配,如果存在诸如thread1 和thread2的情况那样有相同的乱序区间(thread1中write a 和write b区间,thread2中read b和 read a区间),即可判定为严重的弱内存序问题风险位置,输出报告并由开发者进行进一步的分析判断。

Case演示

我们以内部某数据库团队遇到的无锁队列问题为例,使用我们的工具对源码进行重编译和检测。

源代码关键部分如下:

tair-case演示.png

具体工具使用步骤:

  • 在替换定制后的libtsan.so并在编译时加上-fsanitize=thread选项重新编译后,程序运行时输出中会有如下片段,将所有输出记录为output.log
==================
WARNING: ThreadSanitizer: data race (pid=2491502)
  Write of size 4 at 0xffffa48ff004 by thread T2:
    #0 __rte_ring_mp_enqueue /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/rte_ring.c:106 (rte_case+0x4011dc)
    #1 rte_ring_enqueue /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/rte_ring.c:170 (rte_case+0x4011dc)
    #2 doEnqueue /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/main.c:27 (rte_case+0x400e68)
  Previous write of size 4 at 0xffffa48ff004 by thread T1:
    #0 __rte_ring_mp_enqueue /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/rte_ring.c:106 (rte_case+0x4011dc)
    #1 rte_ring_enqueue /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/rte_ring.c:170 (rte_case+0x4011dc)
    #2 doEnqueue /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/main.c:27 (rte_case+0x400e68)
  Location is heap block of size 8388736 at 0xffffa48ff000 allocated by main thread:
    #0 malloc /root/zhuzhangqi/obj/../gcc/libsanitizer/tsan/tsan_interceptors_posix.cpp:692 (libtsan.so.2+0x448e8)
    #1 rte_ring_create /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/rte_ring.c:34 (rte_case+0x400eec)
    #2 main /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/main.c:35 (rte_case+0x400bec)
  Thread T2 (tid=2491505, running) created by main thread at:
    #0 pthread_create /root/zhuzhangqi/obj/../gcc/libsanitizer/tsan/tsan_interceptors_posix.cpp:1048 (libtsan.so.2+0x45818)
    #1 main /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/main.c:39 (rte_case+0x400c0c)
  Thread T1 (tid=2491504, running) created by main thread at:
    #0 pthread_create /root/zhuzhangqi/obj/../gcc/libsanitizer/tsan/tsan_interceptors_posix.cpp:1048 (libtsan.so.2+0x45818)
    #1 main /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/main.c:39 (rte_case+0x400c0c)
SUMMARY: ThreadSanitizer: data race /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/rte_ring.c:106 in __rte_ring_mp_enqueue
==================
==================
WARNING: ThreadSanitizer: data race (pid=2491502)
  Atomic write of size 4 at 0x0000004200c8 by thread T3:
    #0 doEnqueue /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/main.c:30 (rte_case+0x400e94)
  Previous atomic write of size 4 at 0x0000004200c8 by thread T1:
    #0 doEnqueue /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/main.c:30 (rte_case+0x400e94)
  Location is global 'g_count' of size 4 at 0x0000004200c8 (rte_case+0x4200c8)
  Thread T3 (tid=2491506, running) created by main thread at:
    #0 pthread_create /root/zhuzhangqi/obj/../gcc/libsanitizer/tsan/tsan_interceptors_posix.cpp:1048 (libtsan.so.2+0x45818)
    #1 main /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/main.c:39 (rte_case+0x400c0c)
  Thread T1 (tid=2491504, running) created by main thread at:
    #0 pthread_create /root/zhuzhangqi/obj/../gcc/libsanitizer/tsan/tsan_interceptors_posix.cpp:1048 (libtsan.so.2+0x45818)
    #1 main /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/main.c:39 (rte_case+0x400c0c)
SUMMARY: ThreadSanitizer: data race /root/zhuzhangqi/memory_barrier/cases/rte_ring_case/main.c:30 in doEnqueue
==================
  • 将二进制文件进行反汇编输出到obj.log中
  • 将output.log和obj.log作为输入提供给我们的工具,最终我们的工具会提供类似如下形式的输出:

tair-结果.png

  • 报告提供了源代码中存在风险的具体源文件和对应的行数,并提供了建议插入的源码,提供了相应的汇编形式的乱序区域供开发者进行进一步判断。这里建议27行和30行源码之间插入dmb内存屏障,符合问题的人工定位结果

使用说明

环境配置和要求

gcc版本:GCC 10以上 (低版本GCC有时会存在Tsan输出调用栈信息不全的情况)

架构环境:aarch64

运行指南

此工具还在不断开发完善中,当前工具可以完成基本的弱内存序问题检测功能,欢迎有程序迁移需求的团队发送邮件到zhuzhangqi.zzq@alibaba-inc.com

我们会提供当前最新版本的使用方式和环境配置等技术支持,欢迎在使用过程中提出的一切反馈与建议!

相关文章
|
3月前
|
Ubuntu Linux
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
838 3
|
2天前
|
存储 架构师 Java
内存溢出原因与解决方案(4大主流方案详解)
本文详解内存溢出(OOM)的原因及解决方案。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
内存溢出原因与解决方案(4大主流方案详解)
|
3月前
|
存储 网络协议 安全
【Azure 环境】ARM部署模板大于4MB的解决方案及Linked Template遇见存储账号防火墙无法访问
【Azure 环境】ARM部署模板大于4MB的解决方案及Linked Template遇见存储账号防火墙无法访问
|
27天前
|
Docker 容器
docker:记录如何在x86架构上构造和使用arm架构的镜像
为了实现国产化适配,需将原x86平台上的Docker镜像转换为适用于ARM平台的镜像。本文介绍了如何配置Docker buildx环境,包括检查Docker版本、安装buildx插件、启用实验性功能及构建多平台镜像的具体步骤。通过这些操作,可以在x86平台上成功构建并运行ARM64镜像,实现跨平台的应用部署。
486 2
|
2月前
|
C语言 Android开发 C++
基于MTuner软件进行qt的mingw编译程序的内存泄漏检测
本文介绍了使用MTuner软件进行Qt MinGW编译程序的内存泄漏检测的方法,提供了MTuner的下载链接和测试代码示例,并通过将Debug程序拖入MTuner来定位内存泄漏问题。
基于MTuner软件进行qt的mingw编译程序的内存泄漏检测
|
1月前
|
机器学习/深度学习 弹性计算 编解码
阿里云服务器计算架构X86/ARM/GPU/FPGA/ASIC/裸金属/超级计算集群有啥区别?
阿里云服务器ECS提供了多种计算架构,包括X86、ARM、GPU/FPGA/ASIC、弹性裸金属服务器及超级计算集群。X86架构常见且通用,适合大多数应用场景;ARM架构具备低功耗优势,适用于长期运行环境;GPU/FPGA/ASIC则针对深度学习、科学计算、视频处理等高性能需求;弹性裸金属服务器与超级计算集群则分别提供物理机级别的性能和高速RDMA互联,满足高性能计算和大规模训练需求。
|
3月前
|
机器学习/深度学习 算法 数据库
阿里云服务器架构区别解析:从X86计算、Arm计算到高性能计算架构的区别参考
在我们选择阿里云服务器的架构时,选择合适的云服务器架构对于提升业务效率、保障业务稳定至关重要。阿里云提供了多样化的云服务器架构选择,包括X86计算、ARM计算、GPU/FPGA/ASIC、弹性裸金属服务器以及高性能计算等。本文将深入解析这些架构的特点、优势及适用场景,以供参考和选择。
阿里云服务器架构区别解析:从X86计算、Arm计算到高性能计算架构的区别参考
|
2月前
ARM64技术 —— MMU处于关闭状态时,内存访问是怎样的?
ARM64技术 —— MMU处于关闭状态时,内存访问是怎样的?
|
3月前
|
设计模式 uml
在电脑主机(MainFrame)中只需要按下主机的开机按钮(on()),即可调用其它硬件设备和软件的启动方法,如内存(Memory)的自检(check())、CPU的运行(run())、硬盘(Hard
该博客文章通过一个电脑主机启动的示例代码,展示了外观模式(Facade Pattern)的设计模式,其中主机(MainFrame)类通过调用内部硬件组件(如内存、CPU、硬盘)和操作系统的启动方法来实现开机流程,同时讨论了外观模式的优缺点。
|
3月前
|
Web App开发 缓存 JavaScript
技术分享:深入探索内存泄漏——识别、分类与解决方案
【8月更文挑战第27天】在软件开发的浩瀚星海中,内存管理始终是程序员们必须面对的重要课题。内存泄漏,作为内存管理不善的典型症状,不仅影响应用性能,还可能导致系统崩溃,是每位开发者都需警惕的“暗礁”。本文将带您深入探索内存泄漏的本质、常见类型及有效的解决策略,助力您的工作学习之旅更加顺畅。
51 0