RDT技术初探之AMD机型小试牛刀

RDT技术初探之AMD机型小试牛刀

Posted by lwk on November 19, 2021

最近业务反馈虚机的LLC隔离问题,因为LLC是一个NODE上的所有CPU共享,因此就可能出现当分配的虚机cpu在一个NODE上,就会出现虚机之间争抢LLC的问题。

如下图所示: image

因此,针对LLC隔离问题,intel首先推出了RDT(Resource Director Technology)。关于RDT技术的更多介绍可参考intel官方网站和https://github.com/intel/intel-cmt-cat,当然与intel相对的AMD也同样推出了LLC的隔离技术,即PQOS(Platform Quality of Service)

https://www.phoronix.com/scan.php?page=news_item&px=AMD-Platform-QoS-RFC-Patches,实现原理几乎一样。

根据调研目前国内云厂商,包括百度云&腾讯云并没有针对LLC进行隔离,即没有对虚拟机使用LLC进行量化和监控等,虚机之间共享LLC,并且可能存在竞争问题。当然目前VM也同样存在这样的问题,因此本文探索性的针对RDT技术尝试应用于VM。

RDT技术除了需要硬件支持,还需要内核支持。

硬件是否支持主要看cpu是否支持:

lscpu | egrep “cat|cdp|rdt” image

内核从4.4版本已经支持RDT技术。

本文将通过5.10内核以及Intel&AMD机器探索RDT技术。

如何使用RDT呢。

首先在内核引导文件(grub)加上rdt=cmt,mbmtotal,mbmlocal,l3cat,l3cdp,l2cat,l2cdp,mba

通过实验发现,同样的内核版本(5.10),在AMD Milan机型不需要在引导文件加上述引导项,但在Intel机型必须加上述引导项,初步判断可能是硬件有关系。官方文档也是建议在引导项加上RDT配置。

其次,加完引导项之后重启机器,查看/proc/cmdlline是否有引导项rdt=cmt,mbmtotal,mbmlocal,l3cat,l3cdp,l2cat,l2cdp,mba,有则说明RDT生效了。

接着执行mount -t resctrl resctrl/sys/fs/resctrl,RDT提供kernfs文件系统的功能,类似于cgroup,通过文件系统就能实现对RDT的配置,为RDT操作提供了便捷性。

然后在/sys/fs/resctrl目录就能看到如下信息: image

/sys/fs/resctrl下的目录和文件具体作用,这里就不做详细阐述,具体可查官方文档。

这就完成了RDT的初步配置。

我们现在AMD机器上做如上配置。

接着就是生成一个VM,在VM配置上RDT,如何配置呢?VM的接口操作都是通过libvirt完成的,因此从libvirt官方文档可以看到有对RDT的配置项。本文主要是使用RDT的MBA(Memory Bandwidth Allocation)功能实现对内存带宽的限制。

https://libvirt.org/formatdomain.html image 在虚拟机的xml文件增加如下标签:


 <cputune>
   <memorytune vcpus='10-15'>
     <node id='0' bandwidth='60'/>
   </memorytune>
 </cputune>

然后启动虚机:virsh start domain***

报了如下错误:

error: Failed to start domain’ins-kpclutzdyc’

error: internal error: Missing orinconsistent resctrl info for memory bandwidth allocation

字面意思是控制内存带宽分配的resctrl信息丢失或不一致。那这个具体是什么意思?又是什么原因导致的呢?

由于是libvirt报的错误,所以首先查看对应错误所在libvirt源码位置。

找到了报错的代码位置在src/util/virresctrl.c的virResctrlAllocParseMemoryBandwidthLine函数内。

image 从代码可知,当if条件为真是才会report这个错误。resctrl是个指针,先看下resctrl变量对应类型的结构信息,


struct _virResctrlInfo {
   virObject parent;
 
   virResctrlInfoPerLevelPtr *levels;
   size_t nlevels;
 
   virResctrlInfoMemBWPtr membw_info;
 
   virResctrlInfoMongrpPtr monitor_info;
};

成员membw_info也是个结构体指针:

/* Information about memory bandwidthallocation */
struct _virResctrlInfoMemBW {
   /* minimum memory bandwidth allowed */
   unsigned int min_bandwidth;
   /* bandwidth granularity */
   unsigned int bandwidth_granularity;
   /* Maximum number of simultaneous allocations */
   unsigned int max_allocation;
   /* level number of last level cache */
   unsigned int last_level_cache;
   /* max id of last level cache, this is used to track
    * how many last level cache available in host system,
    * the number of memory bandwidth allocation controller
    * is identical with last level cache. */
   unsigned int max_id;
};

因此根据resctrl结构分析可以当if里的resctrl为空指针或resctrl->membw_info为空指针或resctrl->membw_info->min_bandwidth为0或resctrl->membw_info->bandwidth_granularity为0时(其中任意一个或多个条件为真),都会抛出该错误。那么接下来就是重点分析到底哪个成员空指针或0,以及为什么会是空指针或0。

我们根据virResctrlAllocParseMemoryBandwidthLine函数,反向查找上层调用信息。

static int
virResctrlAllocParse(virResctrlInfoPtrresctrl,
                     virResctrlAllocPtr alloc,
                     const char *schemata)
{
   char **lines = NULL;
   size_t nlines = 0;
   size_t i = 0;
   int ret = -1;
 
   lines = virStringSplitCount(schemata, "\n", 0, &nlines);
   for (i = 0; i < nlines; i++) {
       if (virResctrlAllocParseCacheLine(resctrl, alloc, lines[i]) < 0)
           goto cleanup;
       if (virResctrlAllocParseMemoryBandwidthLine(resctrl, alloc, lines[i])< 0)
           goto cleanup;
 
   }   
 
   ret = 0;
 cleanup:
   g_strfreev(lines);
   return ret;
}

首先排除resctrl指针不可能为空,因为virResctrlAllocParseProcessCache()函数也有对resctrl的判断,如果为空的话,在函数virResctrlAllocParseProcessCache()就已经抛出错误并返回了,根本不会执行到virResctrlAllocParseMemoryBandwidthLine()函数。

那就只剩resctrl->membw_info、resctrl->membw_info->min_bandwidth以及resctrl->membw_info->bandwidth_granularity三个条件了为false。

接下来根据调用链路,画出函数调用拓扑关系图:

image

以上函数逻辑主要是在创建domain初始化和分配resctrl,最上面的分支是初始化caps->host.resctrl;下面的分支主要是在创建qemu线程(domain)用初始后的resctrl对domain的MBA进行配置。

从上面的可以看出resctrl->membw_info也不为空指针。所以resctrl->membw_info->min_bandwidth为0或resctrl->membw_info->bandwidth_granularity为0。那这两个变量到底是哪个为0以及为什么为0呢?

在查这个问题之前,先要找到这两个变量的数据是从哪来的。

通过上面的函数调用图可以看到,membw_info的初始化是在virResctrlGetInfo()函数中进行初始化的。

static int
virResctrlGetInfo(virResctrlInfoPtrresctrl)
{
   g_autoptr(DIR) dirp = NULL;
   int ret = -1;
 
   ret = virDirOpenIfExists(&dirp, SYSFS_RESCTRL_PATH"/info");
   if (ret <= 0)
        return ret;
 
   if ((ret = virResctrlGetMemoryBandwidthInfo(resctrl)) < 0) //重点是这个函数
       return -1;
 
   if ((ret = virResctrlGetCacheInfo(resctrl, dirp)) < 0)
       return -1;
 
   if ((ret = virResctrlGetMonitorInfo(resctrl)) < 0)
       return -1;
 
   return 0;
}
#define SYSFS_RESCTRL_PATH"/sys/fs/resctrl"
static int
virResctrlGetMemoryBandwidthInfo(virResctrlInfoPtrresctrl)
{
   int ret = -1;
   int rv = -1;
   virResctrlInfoMemBWPtr i_membw = NULL;
 
   /* query memory bandwidth allocation info */
   i_membw = g_new0(virResctrlInfoMemBW, 1);创建virResctrlInfoMemBW
   rv = virFileReadValueUint(&i_membw->bandwidth_granularity,
                             SYSFS_RESCTRL_PATH "/info/MB/bandwidth_gran");// SYSFS_RESCTRL_PATH=/sys/fs/resctrl
   if (rv == -2) {
       /* The file doesn't exist, so it's unusable for us,
        * probably memory bandwidth allocation unsupported */
       VIR_INFO("The path '" SYSFS_RESCTRL_PATH"/info/MB/bandwidth_gran'"
                 "does not exist");
       ret = 0;
       goto cleanup;
    }else if (rv < 0) {
       /* Other failures are fatal, so just quit */
       goto cleanup;
    }
 
   rv = virFileReadValueUint(&i_membw->min_bandwidth,
                              SYSFS_RESCTRL_PATH"/info/MB/min_bandwidth");
   if (rv == -2) {
       /* If the previous file exists, so should this one. Hence -2 is
        * fatal in this case (errors out in next condition) - the kernel
        * interface might've changed too much or something else is wrong. */
       virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
                       _("Cannot get minbandwidth from resctrl memory info"));
    }
   if (rv < 0)
       goto cleanup;
 
   rv = virFileReadValueUint(&i_membw->max_allocation,
                             SYSFS_RESCTRL_PATH "/info/MB/num_closids");
   if (rv == -2) {
        /* Similar reasoning to min_bandwidth above. */
       virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
                       _("Cannot get maxallocation from resctrl memory info"));
    }
if (rv < 0)
        goto cleanup;
 
resctrl->membw_info= g_steal_pointer(&i_membw);
ret = 0;
cleanup:
 VIR_FREE(i_membw);
 return ret;
}

从virResctrlGetMemoryBandwidthInfo()函数可知:membw_info-> min_bandwidth 的值是从获取的/sys/fs/resctrl/info/MB/min_bandwidth;membw_info-> bandwidth_gran是从/sys/fs/resctrl/info/MB/bandwidth_gran获取的。

那么我们就看下这两个文件的真实值是多少。

image

从图可以看出min_bandwidth的值是0,因此可以确定是由于membw_info->min_bandwidth的值是0导致在设置完memorytune之后启动vm报错。

找到了是因为min_bandwidth为0导致的报错,那接下来就要找为什么是0.

根据上面的分析/sys/fs/resctrl/info/MB/min_bandwidth的内容是0,也就是在执行mount挂载resctrl的时候,内核将min_bandwidth设置为0,那接下来就从内核分析下min_bandwidth到底是不是被设置了0.

在内核5.5代码中,resctrl对应的创建在arch/x86/kernel/cpu/resctrl/rdtgroup.c文件中。其中res_common_files结构主要记录resctrl公共文件,例如/sys/fs/resctrl/info以及cpus、cpus_list、schemata等

static struct rftype res_common_files[] = {
   {   
       .name       ="last_cmd_status",
       .mode       = 0444,
       .kf_ops     =&rdtgroup_kf_single_ops,
       .seq_show   =rdt_last_cmd_status_show,
       .fflags     = RF_TOP_INFO,
   },  
   {   
       .name       ="num_closids",
       .mode       = 0444,
       .kf_ops     =&rdtgroup_kf_single_ops,
       .seq_show   = rdt_num_closids_show,
       .fflags     = RF_CTRL_INFO,
   },  
   {   
       .name       ="mon_features",
       .mode       = 0444,
       .kf_ops     =&rdtgroup_kf_single_ops,
       .seq_show   =rdt_mon_features_show,
       .fflags     = RF_MON_INFO,
},   
    {
       .name       ="min_bandwidth",
       .mode       = 0444,
       .kf_ops     =&rdtgroup_kf_single_ops,
       .seq_show   = rdt_min_bw_show,
       .fflags     = RF_CTRL_INFO |RFTYPE_RES_MB,
   },
    {
       .name       ="bandwidth_gran",
       .mode       = 0444,
       .kf_ops     =&rdtgroup_kf_single_ops,
       .seq_show   = rdt_bw_gran_show,
       .fflags     = RF_CTRL_INFO |RFTYPE_RES_MB,
    }
}

这里主要看min_bandwidth结构

 {
       .name       ="min_bandwidth",
       .mode       = 0444,
       .kf_ops     =&rdtgroup_kf_single_ops,
       .seq_show   = rdt_min_bw_show,
       .fflags     = RF_CTRL_INFO |RFTYPE_RES_MB,
}
 
static int rdt_min_bw_show(structkernfs_open_file *of,
                 struct seq_file *seq, void *v)
{
   struct rdt_resource *r = of->kn->parent->priv;
 
   seq_printf(seq, "%u\n", r->membw.min_bw);
   return 0;
}

从rdt_min_bw_show()函数可以看出,r->membw.min_bw对应的值就是/sys/fs/resctrl/info/MB/min_bandwidth的内容。接下来重点看r->membw.min_bw是如何来的。

因为/sys/fs/resctrl是kernfs文件系统,因此文件内容都是在内存中的,因此从kernfs入手查看resctrl的初始化。初始化的流程大概如下所示:

image

image

重点看get_mem_config()函数,该函数分别对Intel和AMD机器获取对应的内存配置。

static __init bool get_mem_config(void)
{
   if (!rdt_cpu_has(X86_FEATURE_MBA))
       return false;
 
   if (boot_cpu_data.x86_vendor == X86_VENDOR_INTEL)
       return __get_mem_config_intel(&rdt_resources_all[RDT_RESOURCE_MBA]);
   else if (boot_cpu_data.x86_vendor == X86_VENDOR_AMD)
       return__rdt_get_mem_config_amd(&rdt_resources_all[RDT_RESOURCE_MBA]);
 
   return false;
}
static bool __rdt_get_mem_config_amd(structrdt_resource *r)
{  
   union cpuid_0x10_3_eax eax;
   union cpuid_0x10_x_edx edx;
   u32 ebx, ecx;
 
   cpuid_count(0x80000020, 1, &eax.full, &ebx, &ecx,&edx.full);
   r->num_closid = edx.split.cos_max + 1;
   r->default_ctrl = MAX_MBA_BW_AMD;
   
   /* AMD does not use delay */
   r->membw.delay_linear = false;
       
   r->membw.min_bw = 0;
   r->membw.bw_gran = 1;
   /* Max value is 2048, Data width should be 4 in decimal */
   r->data_width = 4;
   
   r->alloc_capable = true;
   r->alloc_enabled = true;
           
   return true;
}
 
static bool __get_mem_config_intel(structrdt_resource *r)
{  
   union cpuid_0x10_3_eax eax;
    union cpuid_0x10_x_edx edx;
   u32 ebx, ecx;
 
   cpuid_count(0x00000010, 3, &eax.full, &ebx, &ecx,&edx.full);
   r->num_closid = edx.split.cos_max + 1;
   r->membw.max_delay = eax.split.max_delay + 1;
   r->default_ctrl = MAX_MBA_BW;
   if (ecx & MBA_IS_LINEAR) {
       r->membw.delay_linear = true;
       r->membw.min_bw = MAX_MBA_BW - r->membw.max_delay;
       r->membw.bw_gran = MAX_MBA_BW - r->membw.max_delay;
    }else {
       if (!rdt_get_mb_table(r))
           return false;
    }
    r->data_width= 3;
       
   r->alloc_capable = true;
   r->alloc_enabled = true;
   
   return true;
}

其中MAX_MBA_BW=100,r->membw.max_delay=eax.split.max_delay + 1,重点就是eax.split.max_delay信息,这个是通过cpuid指令获取到的数据,由于kernel代码不能直观看到结果,因此我抠出核心代码,并编译成二进制,通过执行二进制获取对应的信息。

抠出其中核心代码如下:

#include <stdio.h>
 typedef unsigned int u32;

 union cpuid_0x10_3_eax {

   struct {
       unsigned int max_delay:12;
    }split;
   unsigned int full;
};
 

union cpuid_0x10_x_edx {
   struct {
       unsigned int cos_max:16;
    }split;
   unsigned int full;
};
 
static inline void __cpuid(unsigned int*eax, unsigned int *ebx, unsigned int *ecx, unsigned int *edx)
{
   /* ecx is often an input as well as an output. */
   asm volatile("cpuid"
       : "=a" (*eax),
         "=b" (*ebx),
         "=c" (*ecx),
         "=d" (*edx)
       : "0" (*eax), "2" (*ecx)
       : "memory");
} 
  
static inline void cpuid_count(unsigned intop, int count,
                   unsigned int *eax, unsignedint *ebx,
                   unsigned int *ecx, unsignedint *edx)
{
   *eax = op;
   *ecx = count;
   __cpuid(eax, ebx, ecx, edx);
}
 
int main(int argc, char *argv[]) {
 
              unioncpuid_0x10_3_eax eax;
              unioncpuid_0x10_x_edx edx;
              u32ebx, ecx;
 
              cpuid_count(0x00000010,3, &eax.full, &ebx, &ecx, &edx.full);
              printf("%d\n",eax.split.max_delay);
 
              return0;
}

outputs:”=a”(a), “=b” (b), “=c” (c), “=d” (d)。表示有4个输出参数——参数0为eax(绑定到变量a)、参数1为ebx(绑定到变量b)、参数2为ecx(绑定到变量c)、参数3为edx(绑定到变量d)。

inputs:”0”(eax), “2” (ecx))。表示有2个输入参数——参数0(eax),参数2(ecx)。

调用cpuid指令 // asmstatements

将返回的eax赋给a //outputs

将返回的ebx赋给b

将返回的ecx赋给c

将返回的edx赋给d

结合intel的说明文档可以看出,cpuid指令,当eax=16,ecx=3的时候,获取到的是MBA(memory bandwidth allocation)信息

https://www.felixcloutier.com/x86/cpuid

因此在Intel(R) Xeon(R) Platinum 8260平台上获取到的MBA结果是89

当然上面的程序只能在intel平台上跑起来,如果是AMD平台,则需要修改op,即传给cpuid指定的操作数eax的值。所以针对AMD平台,只需要将cpuid_count(0x00000010,3, &eax.full, &ebx, &ecx, &edx.full);改为cpuid_count(0x80000020,1, &eax.full, &ebx, &ecx, &edx.full);即可。

从__rdt_get_mem_config_amd()函数可以看到,membw.min_bw赋值是0,membw.bw_gran赋值是1,这两个值分别对应的就是/sys/fs/resctrl/info/MB/min_bandwidth和/sys/fs/resctrl/info/MB/bandwidth_gran的内容。

因此,从libvirt到kernel的分析可知,在AMD机器上设置resctrl但启动失败的原因是由于在mount挂载的时候,内核将/sys/fs/resctrl/info/MB/min_bandwidth设置为0,从而导致domain创建时解析resctrl配置时失败。