暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

NUMA的逆行人生:一文讲清什么是NUMA(第四弹)

IT知识刺客 2024-10-30
583

没想到我认为颇有难度的“FBI警告”系列第三篇,有4000多的浏览量:

图1

排在我所有技术类文章的第一位。既然大家这么爱学习,“FBI警告”系列我一定会继续下去,定要把牛马(NUMA)的底裤扒掉,把她一览无余、玉体横陈在大伙面前。

NUMA是近些年来,体系结构领域最大的变化。这个变化来的如此之快,如此之猛,以至于我们的基础软件、教学体系,都没来得急跟上。
NUMA其实是分布式CPU。是的,你没有听错,就算只有一颗物理CPU,它也是分布式的(严格来说,是它内部的访存子系统采用NUMA架构,它就是分布式CPU)。
我们常看到的NUMA架构如图2所示:
图2
这样的图未必精确,但也反应出了NUMA的核心本质:不对等的内存访问。
数据被分为Local(本地)与Remote(远端),Remote访问的延迟,数倍于Local。
(数倍,到底有多少倍,不要急,这一篇主要就是讲这个问题的)
如果我们把内存换成“数据”,把Core 0/1/2/3换成computing resource,我们将得到如下的架构图:

图3

每个computing resource有本地数据,也能访问远端数据。当然,本地与远端的延迟并不对等。
这些概念,听起来是不是耳熟!是不是像分布式数据库!
这样吧,再来一张超融合分布式数据库架构图:

图4
这里有四台物理主机,连在高速互联网络中。每一台物理主机的存储,都是CPU的Local数据库资源,而其他主机中的存储,则是它的Remote数据资源。
其实你细品一下,从架构上看,这样的分布式数据库,不就是“非一致数据访问”架构吗。
非一致数据访问,英文:Non-Uniform Data Access,简称:NUDA,牛大。
只要把“数据”换成“内存”,也就是Data换成Memory,即:
非一致内存访问,英文:Non-Uniform Memory Access,简称:NUMA,牛马。
好了,闲片不扯了,进入正题。很多朋友对上一篇中,神奇的numa测试程序很感兴趣。我的测试方式,可不是分别在“打开”NUMA、“关闭”NUMA时跑个压测软件,统计下数据库的TPS/QPS。我们是要把对底层的理解调动起来,通过测试程序,取得测试数据,印证我们对底层的理解是否正确,这样的测试,才是真正理解NUMA的开始。这一节,我们细说一下这个测试程序:numa1.c
numa1.c不仅可以用来测试NUMA延迟,还可以测量不同Core和L3 Cache间的延迟,这么神奇的程序,一共也就224行,包括了大页、进程、CPU亲和性、内嵌汇编等,十分值得详聊。
源代码在:

牛马测试程序:numa1.c

https://gitee.com/itkassassin/numa_-test1/blob/master/numa1.c
我们从main函数开始聊。
182行,有如下的函数调用:
    cur_cpu = sched_getcpu();
    sched_getcpu()是glibc函数,man一下可以看到详细的说明,这里简单说一下,它返当前CPU编号。
    它对测试程序的神奇功能,并无帮助,只是为了多输出点信息。
    接下来的184行,就比较重要了:
      setcpu(start_cpu_id); // 按参数指定CPU运行
      这是一个自定义函数,它在152行 到 164行。它封装了CPU亲和性函数sched_setaffinity(),主要的代码都是围绕sched_setaffinity()。
      只要使用sched_setaffinity()、set_mempolicy()设置了CPU亲合性和NUMA策略,你的软件就可以声称是 “NUMA Aware”了,这是国内数据库厂商的惯用套路。
      man一下它,就能获得这两个函数的详细说明,很简单,此处不再详述。花很小的代价,你也是“NUMA Aware”了,还是值得花一点点时间看看的。
      好了,不批判爱吹水的国产数据库了。我对国产数据库并无恶意,只是希望大家可以沉下心来做事,少玩虚头八脑的概念。美中之间这场基础软件之争,爱玩概念可能一时飞的很高,其结果吗,谁也不知道。也可能真的越吹飞的越高。那真应该看看咱们的“基础软件”开发系列,吹点更高级的牛,飞的更高更远。
      拐回来说回184行的setcpu(start_cpu_id),其中start_cpu_id来自于命令行参数,是你希望在哪个CPU上完成分配内存等初始化工作。
      接下来,看188行:
        mem=(TYPE *)smem();
        它用来分配内存。184行用setcpu()指定了CPU,188行分配内存,如果NUMA策略是默认的localalloc,内存将从当前CPU所在的NUMA节点中分配。
        smem()也是自定义函数,它在142到150行。它很简单,只不过是用mmap(),分配了大页内存。146行还注释掉一行,是使用普通4KB页内存。
        测试程序优先使用大页内存。原因在NUMA第三弹中已经有讲述:
        NUMA的逆行人生:一文讲清什么是NUMA(第三弹)
        由于间隔太久,这里再重述一下吧。

        5 来自上一篇

        我想分一大块内存,这块内存来自于两个NUMA节点,然后测试跨不同节点的延迟。

        但我每个NUMA节点都有几十G内存,太大了,怎么办。像图6这样:

        图6 图片来自上一篇
        分配了1000个大页,这1000个大页,本来是平均的来自两个NUMA节点。而且现在NUMA 0有311个大页,NUMA 1只有13个大页。
        也就是说,如果我在NUMA 1上,分配超过13个页(26MB),必然就跨NUMA节点了。如图7所示:

        7 图片来自上一篇

        上一篇的测试中,在NUMA 1CPU上,分配70MB内存,26MB来自NUMA 1,剩下的来自NUMA 0

        这就是优先使用大页的原因,容易控制所能使用的内存大小。

        继续回来说测试程序。

        195行:

          myinit(memcontrol_number[1]);

          myinit()3552行,主要作用是用0填充所得内存。

          然后再调用_mm_clflush(),将所有修改全局化、并标记对应Cache Line无效。

          _mm_clflush()gcc编译器针对特定CPU提供的函数,目前用于x64微架构(也就是IntelAMD)。它是以下指令的封装:

            clflush (内存地址)

            clflush的主要作用,是把L1/L2/L3 Cache中的Cache Line,标记为Invalidate(无效)。同时,如果此Cache Line如果是脏Line(就是之前修改过),会把此脏Line写入内存(内存对于CPU就是全局的,虽然访问延时不一致),这就是全局化。

            myinit()结束,初始化也就结束了。

            所以,在紧接着的196行:

              setcpu(control_number[0]); // control_number[0] : 运行CPU

              又调用setcpu()调整CPU,这次设置CPU,就是测试程序真正跑的CPU了。

              这里的control_number[0],来自于命令行第二个参数:设置运行CPU

              接下来,从198行到208行的for循环。这里用循环其实没意义了,因为写死了,只循环1次。

              这里for循环的本意,是测试多个进程时Cache&Memory的底层原理,当时是针对MESI的。对于NUMA,暂时不用多个进程,所以循环只会运行1次。

              之所以把循环还保留在这里,说不定后面其他测试,会有多进程呢。

              这里主要做的工作,就是200行的fork:

                pid[t] = fork();

                返回的pid放在数组中,也是为了循环多次、多个进程。现在只会fork一次、1个子进程。

                然后,201行到205行,是子进程。

                子进程调用work,开始工作。工作就是测试。

                下面,进入work()

                work()中主要工作是72行到137行的for循环。

                但限于本篇的篇幅,work()的主要工作,我还是想放到下一篇中讲。

                毕竟太长的技术文章,又太硬核,啃起来太费力。

                下一篇不会等太久,第四弹之所以等这么久,因为我在全力开发PG Shared  Pool

                我是2023 DTCC上,公布要做PG Shared  Pool这么件事的:

                图8

                很多著名的软件,都源自高校,典型的如PostgreSQL,追根溯源,源自Stonebraker 石破天老爷子在加州大学伯克利分校的 Ingres 项目。

                正好这两年在北大讲课,台下都是全国选拔出的尖子,为什么不带着有兴趣的同学,也做点东西呢?

                然后,PG Shared Pool项目就启动了。

                但因为大家都是兼职做,学生的学习任务也很重,所以进展并没有那么快。一年后,终于可以简单看看效果了:

                图9

                图9 PG 自身的Prepare,使用Cache Plan的时间,一条简单SQL,最快0.539ms。

                图10 

                图10是Plan和相关数据都在我们的Shared Pool中后,相同SQL的耗时,只需要0.296ms。

                比 Prepare 后的SQL,快了差不多1倍。

                当然,这个特性主要针对OLTP型的简单SQL啊,OLAP就没这么明显的提升了。

                这里全当一个广告,PG Shared Pool正式面世的时间快了。当然,NUMA的逆行人生系列第5弹,更快。

                文章转载自IT知识刺客,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                评论