最近业务反馈虚机的LLC隔离问题,因为LLC是一个NODE上的所有CPU共享,因此就可能出现当分配的虚机cpu在一个NODE上,就会出现虚机之间争抢LLC的问题。
如下图所示:
因此,针对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”
内核从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目录就能看到如下信息:
/sys/fs/resctrl下的目录和文件具体作用,这里就不做详细阐述,具体可查官方文档。
这就完成了RDT的初步配置。
我们现在AMD机器上做如上配置。
接着就是生成一个VM,在VM配置上RDT,如何配置呢?VM的接口操作都是通过libvirt完成的,因此从libvirt官方文档可以看到有对RDT的配置项。本文主要是使用RDT的MBA(Memory Bandwidth Allocation)功能实现对内存带宽的限制。
https://libvirt.org/formatdomain.html 在虚拟机的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函数内。
从代码可知,当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。
接下来根据调用链路,画出函数调用拓扑关系图:
以上函数逻辑主要是在创建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获取的。
那么我们就看下这两个文件的真实值是多少。
从图可以看出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的初始化。初始化的流程大概如下所示:
重点看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配置时失败。