numad通过pick_numa_nodes
为进程选择合适的node,这个函数是numad的精髓部分。之前获取的各种参数,在这里将作为算法的入参计算得到一个最优的node。具体的逻辑如下:
id_list_p pick_numa_nodes(int pid, int cpus, int mbs, int assume_enough_cpus) { if (log_level >= LOG_DEBUG) { numad_log(LOG_DEBUG, "PICK NODES FOR: PID: %d, CPUs %d, MBs %d\n", pid, cpus, mbs); } char buf[BUF_SIZE]; uint64_t proc_avg_node_CPUs_free = 0; int pid_ix; process_data_p p = NULL; //如果还记得上面提到的numad可以通过二进制运行-w参数获取建议的numa node list,这里就对daemon和二进制运行两种方式作出区分。 //如果是daemon运行,处理的是已经存在的进程,必须要获取进程当前已经占用的CPU和内存信息。 if ((pid > 0) && ((pid_ix = process_hash_lookup(pid)) >= 0)) { //从进程的hash table中找到当前的进程信息。 p = &process_hash_table[pid_ix]; char fname[FNAME_SIZE]; //从/proc/%d/numa_maps中获取进程内存在numa node上的分布情况 snprintf(fname, FNAME_SIZE, "/proc/%d/numa_maps", pid); FILE *fs = fopen(fname, "r"); if (!fs) { numad_log(LOG_WARNING, "Tried to research PID %d numamaps, but it apparently went away.\n", p->pid); return NULL; // Assume the process terminated } 初始化进程的process_MBs,用于存储进程在每个node上的内存占用情况。额外多申请一个node的空间,用于存储跨node分配的内存信息。 p->process_MBs = realloc(p->process_MBs, (num_nodes + 1) * sizeof(uint64_t)); if (p->process_MBs == NULL) { numad_log(LOG_CRIT, "p->process_MBs realloc failed\n"); exit(EXIT_FAILURE); } memset(p->process_MBs, 0, (num_nodes + 1) * sizeof(uint64_t)); int process_has_interleaved_memory = 0; while (fgets(buf, BUF_SIZE, fs)) { int interleaved_memory = 0; uint64_t page_size = page_size_in_bytes; const char *delimiters = " \n"; char *str_p = strtok(buf, delimiters); while (str_p) { if (!strncmp(str_p, "interleave", 10)) { //进程内存使用了interleaved_memory interleaved_memory = 1; process_has_interleaved_memory = 1; } else if (!strcmp(str_p, "huge")) { page_size = huge_page_size_in_bytes; } else if (*str_p++ == 'N') { int node; uint64_t pages; CONVERT_DIGITS_TO_NUM(str_p, node); if (*str_p++ != '=') { numad_log(LOG_CRIT, "numa_maps node number parse error\n"); exit(EXIT_FAILURE); } CONVERT_DIGITS_TO_NUM(str_p, pages); 统计当前进程在每个node上使用的内存 p->process_MBs[node] += (pages * page_size); if (interleaved_memory) { //interleave memory统计在最后一个元素中 p->process_MBs[num_nodes] += (pages * page_size); } } str_p = strtok(NULL, delimiters); } } fclose(fs); //统计所有node上剩余的CPU计算能力和当前进程使用的CPU计算能力 proc_avg_node_CPUs_free = p->CPUs_used; for (int ix = 0; (ix <= num_nodes); ix++) { p->process_MBs[ix] /= MEGABYTE; if ((log_level >= LOG_DEBUG) && (p->process_MBs[ix] > 0)) { if (ix == num_nodes) { numad_log(LOG_DEBUG, "Interleaved MBs: %ld\n", ix, p->process_MBs[ix]); } else { numad_log(LOG_DEBUG, "PROCESS_MBs[%d]: %ld\n", ix, p->process_MBs[ix]); } } if (ID_IS_IN_LIST(ix, p->node_list_p)) { proc_avg_node_CPUs_free += node[ix].CPUs_free; } } //平均每个numa node剩余的CPU计算能力 proc_avg_node_CPUs_free /= NUM_IDS_IN_LIST(p->node_list_p); //如果进程使用了interleave memory并且参数指定不合并interleave memory //标记进程的内存flag,更新进程的绑定时间并退出,不做任何处理 if ((process_has_interleaved_memory) && (keep_interleaved_memory)) { p->flags |= PROCESS_FLAG_INTERLEAVED; p->bind_time_stamp = get_time_stamp(); if (log_level >= LOG_DEBUG) { numad_log(LOG_DEBUG, "Skipping evaluation of PID %d because of interleaved memory.\n", p->pid); } return NULL; } }
//拷贝一份node信息,用于做各种计算 static node_data_p tmp_node; tmp_node = realloc(tmp_node, num_nodes * sizeof(node_data_t) ); if (tmp_node == NULL) { numad_log(LOG_CRIT, "tmp_node realloc failed\n"); exit(EXIT_FAILURE); } memcpy(tmp_node, node, num_nodes * sizeof(node_data_t) ); uint64_t sum_of_node_CPUs_free = 0; for (int ix = 0; (ix < num_nodes); ix++) { //假装将当前进程使用的资源退回给每个node(方便再分配,这也是为什么要申请一个临时变量保存node信息的原因,毕竟不是真的把资源释放出来了) //仅获取放置建议的话,不需要做这个操作,因为没有进程在运行 if (pid > 0) { if (NUM_IDS_IN_LIST(p->node_list_p) >= num_nodes) { //如果进程还没有绑定numa node,就这样计算其在当前node上占用的内存资源,多1/16 //不明白的说。。。 tmp_node[ix].MBs_free += ((p->process_MBs[ix] * 17) / 16); //回退与内存比例相等的CPU计算能力到当前node tmp_node[ix].CPUs_free += ((p->CPUs_used * p->process_MBs[ix]) / p->MBs_used); } else { //如果已经绑定过numa node,又要回退这些内存给当前node,多1/4。 tmp_node[ix].MBs_free += ((p->process_MBs[ix] * 5) / 4); //已经绑定numa node,并且当前node是其中之一的话,node剩余的CPU计算能力即为之前计算的proc_avg_node_CPUs_free if (ID_IS_IN_LIST(ix, p->node_list_p)) { tmp_node[ix].CPUs_free = proc_avg_node_CPUs_free; } } sum_of_node_CPUs_free += tmp_node[ix].CPUs_free; //当前node剩余的内存和CPU不允许超过node的最大内存和CPU值 if (tmp_node[ix].CPUs_free > tmp_node[ix].CPUs_total) { tmp_node[ix].CPUs_free = tmp_node[ix].CPUs_total; } if (tmp_node[ix].MBs_free > tmp_node[ix].MBs_total) { tmp_node[ix].MBs_free = tmp_node[ix].MBs_total; } } //CPU计算能力的最小单位是1/100个CPU核(一个CPU核按100算),因此最小要设置为这个值。 if (tmp_node[ix].CPUs_free < 1) { tmp_node[ix].CPUs_free = 1; } // numad_log(LOG_DEBUG, "Raw Node[%d]: mem: %ld cpu: %ld\n", ix, tmp_node[ix].MBs_free, tmp_node[ix].CPUs_free); //++计算node的权重(划重点了啊~)++ //算法:weight=(mem*10 + free_mem*4) * (cpu*6 + free_cpu*1) tmp_node[ix].magnitude = combined_value_of_weighted_resources(ix, mbs, cpus, tmp_node[ix].MBs_free, tmp_node[ix].CPUs_free); } //初始化一个node list,存储给出的放置建议nodes static id_list_p target_node_list_p; CLEAR_NODE_LIST(target_node_list_p); if ((pid > 0) && (cpus > sum_of_node_CPUs_free)) { assume_enough_cpus = 1; } //初始化一个混淆因子,这个的作用是假设进程尽量少使用一些CPU计算能力。 //当前初始化的值为node预留值的一半。 int cpu_flex = 0; if ((pid > 0) && (target_utilization < 100)) { cpu_flex = ((100 - target_utilization) * node[0].CPUs_total) / 200; } //计算最少需要几个node才能满足进程的资源需求,CPU计算能力已经减去了混淆因子 int mem_req_nodes = ceil((double)mbs / (double)node[0].MBs_total); int cpu_req_nodes = ceil((double)(cpus - cpu_flex) / (double)node[0].CPUs_total); int min_req_nodes = mem_req_nodes; if (min_req_nodes < cpu_req_nodes) { min_req_nodes = cpu_req_nodes; } if (min_req_nodes > num_nodes) { min_req_nodes = num_nodes; } //检索出tmp_node中每个node与其他node的最小距离,距离相等的情况下,权重大的优先 int index[num_nodes]; uint64_t totmag[num_nodes]; for (int ix = 0; (ix < num_nodes); ix++) { // Reset the index each time for (int n = 0; (n < num_nodes); n++) { index[n] = n; } // Sort by minimum relative NUMA distance from node[ix], // breaking distance ties with magnitude of available resources for (int ij = 0; (ij < num_nodes); ij++) { int best_ix = ij; for (int ik = ij + 1; (ik < num_nodes); ik++) { int ik_dist = tmp_node[index[ik]].distance[ix]; int best_ix_dist = tmp_node[index[best_ix]].distance[ix]; if (best_ix_dist > ik_dist) { best_ix = ik; } else if (best_ix_dist == ik_dist) { if (tmp_node[index[best_ix]].magnitude < tmp_node[index[ik]].magnitude ) { best_ix = ik; } } } if (best_ix != ij) { int tmp = index[ij]; index[ij] = index[best_ix]; index[best_ix] = tmp; } } //之前计算了最少需要多少node, //在总量为这些node的情况下计算每个node与其他node之间(权重/距离^2)之和 totmag[ix] = 0; for (int ij = 0; (ij < min_req_nodes); ij++) { int dist = tmp_node[index[ij]].distance[ix]; totmag[ix] += (tmp_node[index[ij]].magnitude / (dist * dist)); } numad_log(LOG_DEBUG, "Totmag[%d]: %ld\n", ix, totmag[ix]); } //找到权重最大的node,这个node就是将要选择的最优node。 int best_node_ix = 0; for (int ix = 0; (ix < num_nodes); ix++) { if (totmag[best_node_ix] < totmag[ix]) { best_node_ix = ix; } } numad_log(LOG_DEBUG, "best_node_ix: %d\n", best_node_ix); for (int n = 0; (n < num_nodes); n++) { index[n] = n; } //根据距离排序,排序的结果是距离较小,权重较高的在前。 for (int ij = 0; (ij < num_nodes); ij++) { int best_ix = ij; for (int ik = ij + 1; (ik < num_nodes); ik++) { int ik_dist = tmp_node[index[ik]].distance[best_node_ix]; int best_ix_dist = tmp_node[index[best_ix]].distance[best_node_ix]; if (best_ix_dist > ik_dist) { best_ix = ik; } else if (best_ix_dist == ik_dist) { if (tmp_node[index[best_ix]].magnitude < tmp_node[index[ik]].magnitude ) { best_ix = ik; } } } if (best_ix != ij) { int tmp = index[ij]; index[ij] = index[best_ix]; index[best_ix] = tmp; } } if (log_level >= LOG_DEBUG) { for (int iq = 0; (iq < num_nodes); iq++) { numad_log(LOG_DEBUG, "Node: %d Dist: %d Magnitude: %ld\n", tmp_node[index[iq]].node_id, tmp_node[index[iq]].distance[best_node_ix], tmp_node[index[iq]].magnitude); } } //从已经排序完成的node列表中消费掉进程需要的资源(其实是预消费,并没有真正的消耗掉这些资源) best_node_ix = 0; while ((min_req_nodes > 0) || (mbs > 0) || ((cpus > cpu_flex) && (!assume_enough_cpus))) { if (log_level >= LOG_DEBUG) { numad_log(LOG_DEBUG, "MBs: %d, CPUs: %d\n", mbs, cpus); } numad_log(LOG_DEBUG, "Assigning resources from node %d\n", index[best_node_ix]); //当前node上需要分配资源,加入目标node list ADD_ID_TO_LIST(tmp_node[index[best_node_ix]].node_id, target_node_list_p); min_req_nodes -= 1; //如果已经分配了所有的node,就没有必要继续分配了。 if (EQUAL_LISTS(target_node_list_p, all_nodes_list_p)) { break; } #define CPUS_MARGIN 0 #define MBS_MARGIN 100 if (tmp_node[index[best_node_ix]].MBs_free >= (mbs + MBS_MARGIN)) { ////当前node已经满足需求,内存分配完毕 tmp_node[index[best_node_ix]].MBs_free -= mbs; mbs = 0; } else { //当前node资源不满足需要,扣除当前node的资源,在下一个node里面继续分配 mbs -= (tmp_node[index[best_node_ix]].MBs_free - MBS_MARGIN); tmp_node[index[best_node_ix]].MBs_free = MBS_MARGIN; } //cpu资源与内存资源的分配类似 if (tmp_node[index[best_node_ix]].CPUs_free >= (cpus + CPUS_MARGIN)) { tmp_node[index[best_node_ix]].CPUs_free -= cpus; cpus = 0; } else { cpus -= (tmp_node[index[best_node_ix]].CPUs_free - CPUS_MARGIN); tmp_node[index[best_node_ix]].CPUs_free = CPUS_MARGIN; } //刷新扣除资源后node的权重 tmp_node[index[best_node_ix]].magnitude = combined_value_of_weighted_resources(0, mbs, cpus, tmp_node[index[best_node_ix]].MBs_free, tmp_node[index[best_node_ix]].CPUs_free); best_node_ix += 1; } //处理进程不在目标node list上的内存 if ((pid > 0) && (p != NULL)) { uint64_t nonlocal_memory = 0; for (int ix = 0; (ix < num_nodes); ix++) { if (!ID_IS_IN_LIST(ix, target_node_list_p)) { //统计不在目标node list上的内存 nonlocal_memory += p->process_MBs[ix]; } } int disp_percent = (100 * nonlocal_memory) / p->MBs_used; //如果目标node list之外的内存比例不高于传入的参数,已经绑定过numanode并且绑定的numa node与本次计算结果一致 //认为已经处于合适的位置,不需要再做调整了,更新一下bind time就行。 if ((disp_percent <= (100 - target_memlocality)) && (p->bind_time_stamp) && (EQUAL_LISTS(target_node_list_p, p->node_list_p))) { if (log_level >= LOG_DEBUG) { numad_log(LOG_DEBUG, "Process %d already %d percent localized to target nodes.\n", p->pid, 100 - disp_percent); } p->bind_time_stamp = get_time_stamp(); return NULL; } } //numad二进制形式-w参数运行时,如果上面没有计算到任何一个node,就随便返回一个node //这种情况也只有传入参数没有指定需要的资源才回产生 if ((pid <= 0) && (NUM_IDS_IN_LIST(target_node_list_p) <= 0)) { ADD_ID_TO_LIST(node[0].node_id, target_node_list_p); } // Log advice, and return target node list if ((pid > 0) && (p->bind_time_stamp)) { str_from_id_list(buf, BUF_SIZE, p->node_list_p); } else { str_from_id_list(buf, BUF_SIZE, all_nodes_list_p); } char buf2[BUF_SIZE]; str_from_id_list(buf2, BUF_SIZE, target_node_list_p); char *cmd_name = "(unknown)"; if ((p) && (p->comm)) { cmd_name = p->comm; } numad_log(LOG_NOTICE, "Advising pid %d %s move from nodes (%s) to nodes (%s)\n", pid, cmd_name, buf, buf2); //更新进程绑定的numa node list if (pid > 0) { COPY_LIST(target_node_list_p, p->node_list_p); } return target_node_list_p; }
进程已经拿到了numad给出的放置建议,接下来需要根据建议重新放置。
int bind_process_and_migrate_memory(process_data_p p) {
uint64_t t0 = get_time_stamp();
// Parameter p is a pointer to an element in the hash table
if ((!p) || (p->pid < 1)) {
numad_log(LOG_CRIT, "Bad PID to bind\n");
exit(EXIT_FAILURE);
}
if (!p->node_list_p) {
numad_log(LOG_CRIT, "Cannot bind to unspecified node(s)\n");
exit(EXIT_FAILURE);
}
//根据进程绑定的node list初始化进程绑定的cpu list
static id_list_p cpu_bind_list_p;
CLEAR_CPU_LIST(cpu_bind_list_p);
int nodes = NUM_IDS_IN_LIST(p->node_list_p);
int node_id = 0;
while (nodes) {
if (ID_IS_IN_LIST(node_id, p->node_list_p)) {
OR_LISTS(cpu_bind_list_p, cpu_bind_list_p, node[node_id].cpu_list_p);
nodes -= 1;
}
node_id += 1;
}
char fname[FNAME_SIZE];
struct dirent **namelist;
snprintf(fname, FNAME_SIZE, "/proc/%d/task", p->pid);
int num_tasks = scandir(fname, &namelist, name_starts_with_digit, NULL);
if (num_tasks <= 0) {
numad_log(LOG_WARNING, "Could not scandir task list for PID: %d\n", p->pid);
return 0;
}
//从/proc/%d/task中拿到该进程所有的线程信息,针对每个线程绑定CPU亲和性
//使用系统调用sched_setaffinity绑定线程cpu亲和性
for (int namelist_ix = 0; (namelist_ix < num_tasks); namelist_ix++) {
int tid = atoi(namelist[namelist_ix]->d_name);
int rc = sched_setaffinity(tid, ID_LIST_BYTES(cpu_bind_list_p), ID_LIST_SET_P(cpu_bind_list_p));
if (rc < 0) {
if (errno == ESRCH) {
numad_log(LOG_WARNING, "Tried to move PID %d, TID %d, but it apparently went away.\n", p->pid, tid);
}
numad_log(LOG_ERR, "Bad sched_setaffinity() on PID %d, TID %d -- errno: %d\n", p->pid, tid, errno);
}
free(namelist[namelist_ix]);
}
free(namelist);
//接下来将进程在非目标node上的内存到目标node
static unsigned long *dest_mask;
static unsigned long *from_mask;
static int allocated_bytes_in_masks;
// Lie about num_nodes being one bigger because of kernel bug...
int num_bytes_in_masks = (1 + ((num_nodes + 1) / BITS_IN_LONG)) * sizeof(unsigned long);
if (allocated_bytes_in_masks < num_bytes_in_masks) {
allocated_bytes_in_masks = num_bytes_in_masks;
dest_mask = realloc(dest_mask, num_bytes_in_masks);
from_mask = realloc(from_mask, num_bytes_in_masks);
if ((dest_mask == NULL) || (from_mask == NULL)) {
numad_log(LOG_CRIT, "bit mask malloc failed\n");
exit(EXIT_FAILURE);
}
}
int prev_from_node_id = -1;
for (;;) {
int min_dest_node_id = -1;
int max_from_node_id = -1;
for (int node_ix = 0; (node_ix < num_nodes); node_ix++) {
node_id = node[node_ix].node_id;
if (ID_IS_IN_LIST(node_id, p->node_list_p)) {
//获取最佳的目的node(进程在这个node上占用的内存最少)
if ((min_dest_node_id < 0) || (p->process_MBs[min_dest_node_id] >= p->process_MBs[node_id])) {
// The ">=" above is intentional, so we tend to move memory to higher numbered nodes
min_dest_node_id = node_id;
}
} else {
//内存迁移的最佳节点(进程不应该占用这个node的内存,并且在这个node上占用的内存最多)
if ((max_from_node_id < 0) || (p->process_MBs[max_from_node_id] < p->process_MBs[node_id])) {
max_from_node_id = node_id;
}
}
}
//已经没有内存可以迁移,表明所有的内存都已经迁移到正确的node,退出循环
if ((p->process_MBs[max_from_node_id] == 0) || (max_from_node_id == prev_from_node_id)) {
break;
}
memset(dest_mask, 0, num_bytes_in_masks);
memset(from_mask, 0, num_bytes_in_masks);
SET_BIT(max_from_node_id, from_mask);
SET_BIT(min_dest_node_id, dest_mask);
numad_log(LOG_DEBUG, "Moving memory from node: %d to node %d\n", max_from_node_id, min_dest_node_id);
//通过系统调用__NR_migrate_pages将进程占用的内存迁移到目标node上
int rc = syscall(__NR_migrate_pages, p->pid, num_nodes + 1, from_mask, dest_mask);
if (rc > 2) {
numad_log(LOG_WARNING, "Tried to move PID %d, but %d pages would not move.\n", p->pid, rc);
} else if (rc < 0) {
if (errno == ESRCH) {
numad_log(LOG_WARNING, "Tried to move PID %d, but it apparently went away.\n", p->pid);
return 0;
}
}
//更新进程在各node上占用的内存信息
p->process_MBs[min_dest_node_id] += p->process_MBs[max_from_node_id];
p->process_MBs[max_from_node_id] = 0;
prev_from_node_id = max_from_node_id;
}
//更新进程的绑定时间
snprintf(fname, FNAME_SIZE, "/proc/%d", p->pid);
if (access(fname, F_OK) < 0) {
numad_log(LOG_WARNING, "Could not migrate pid %d. Apparently it went away.\n", p->pid);
return 0;
} else {
uint64_t t1 = get_time_stamp();
p->bind_time_stamp = t1;
char node_list_str[BUF_SIZE];
str_from_id_list(node_list_str, BUF_SIZE, p->node_list_p);
numad_log(LOG_NOTICE, "PID %d moved to node(s) %s in %d.%d seconds\n", p->pid, node_list_str, (t1-t0)/100, (t1-t0)%100);
return 1;
}
}
至此,终于把numad的源码翻了一遍,内心中的疑惑也基本上都有了答案。总算松了一口气。
为什么要花两天时间写这样一篇文档呢?还不是因为掉坑里了!
之前我们提供的性能优化方案中使用了默认配置的numad服务优化云主机的内存放置策略。更新到联调环境之后,qa测试sriov云主机的iops出现了大幅下降。根据我们的方案,优化之后云主机内存和CPU分布在同一个numa node上,性能应该是提升的才对。在测试环境上经过各种调试也没有找到原因,最后无奈的回退版本之后测试居然恢复正常了。一个一个组件排除之后才发现numad可能会引起云主机性能的大幅下降。这是怎么一回事呢?简单分析numad的代码之后才揭开了这个神坑~
首先,默认情况下numad会自动绑定所有进程的cpu亲和性。我们的dpdk线程从预留的核上被绑定到其它核上,导致dpdk转发性能严重下降。
另外,numad服务并不能在云主机启动的时候就要求内存分布在一个numa node上,而是在启动之后通过系统调用__NR_migrate_pages
强制把云主机的内存迁移到目标numa node上,numad的默认配置要求至少90%的内存在目标numa node上才会停止内存迁移。但是实际上我们的内存可能分布在多个node上并且不满足90%的要求,这时候就会出现大量的内存迁移操作。更坑的是,部分内存页是不能跨node迁移的。也就是说numad会反复迁移这部分内存,从物理机上观察在最差的时候云主机qemu进程有一整个vcpu核都在处理迁移操作。
针对这些问题,我们后面在性能优化方案中会修改numad的运行参数
踩过这个坑之后我也深刻的认识到一定一定一定要搞清楚新增依赖项的影响,也因此才有了这篇总结文档。前车之鉴,诸君共勉。
相关阅读:
本文来自网易实践者社区,经作者岳文远授权发布。