1. 程式人生 > >OpenStack Nova 高效能虛擬機器之 NUMA 架構親和

OpenStack Nova 高效能虛擬機器之 NUMA 架構親和

目錄

寫在前面

最近太忙了,筆者實在懶得畫圖,文章的圖片大多來源於網際網路,感謝創作者們(可惜找不到源出處)。

這篇博文與其說是介紹 OpenStack Nova 的高效能虛擬機器,倒不如說是介紹 CPU 相關的硬體架構與應用程式之間的愛恨情仇更加貼切一些。

計算平臺體系結構

SMP 對稱多處理結構

SMP(Sysmmetric Multi-Processor,對稱多處理器),顧名思義,SMP 由多個具有對稱關係的處理器組成。所謂對稱,即處理器之間是水平的映象關係,無有主從之分。SMP 的出現使一臺計算機不再由單個 CPU 組成。

SMP 的典型特徵為「多個處理器共享一個集中式儲存器」

,且每個處理器訪問儲存器的時間片相同,使得工作負載能夠均勻的分配到所有可用處理器上,極大地提高了整個系統的資料處理能力。

這裡寫圖片描述

雖然 SMP 具有多個處理器,但由於只有一個共享的集中式儲存器,所以 SMP 只能執行一個作業系統和資料庫系統的副本(例項),依舊保持了單機特性。同時,SMP 也會要求多處理器保證共享儲存器的資料一致性。如果多個處理器同時請求訪問共享資源,就需要由軟體或硬體實現的加鎖機制來解決資源競態的問題。由此,SMP 又稱為 UMA(Uniform Memory Access,一致性儲存器訪問),所謂一致性指的是:

  • 在任意時刻,多個處理器只能為儲存器的每個資料儲存或共享一個唯一的數值。
  • 每個處理器訪問儲存器所需要的時間都是一致的

顯然,這樣的架構設計註定沒法擁有良好的處理器數量擴充套件性,因為共享儲存的資源競態總是存在的,處理器利用率最好的情況只能停留在 2 到 4 顆。綜合來看,SMP 架構廣泛的適用於 PC 和移動裝置領域,能顯著提升平行計算效能。但 SMP 卻不適合超大規模的伺服器端場景,例如:雲端計算。

這裡寫圖片描述

NUMA 非統一記憶體訪問結構

現代計算機系統中,處理器的處理速度已經超過了主存的讀寫速度,限制計算機計算效能的瓶頸轉移到了儲存器頻寬之上。SMP 由於集中式共享儲存器的設計限制了處理器訪問儲存器的頻次,導致處理器可能會經常處於對資料訪問的飢餓狀態。

NUMA(Non-Uniform Memory Access,非一致性儲存器訪問)的設計理念是將處理器和儲存器劃分到不同的節點(NUMA Node),它們都擁有幾乎相等的資源。在 NUMA 節點內部會通過自己的儲存匯流排訪問內部的本地記憶體,而所有 NUMA 節點都可以通過主機板上的共享匯流排來訪問其他節點的遠端記憶體。

這裡寫圖片描述

很顯然,處理器訪問本地記憶體和遠端記憶體的時耗並不一致,NUMA 非一致性儲存器訪問由此得名。而且因為節點劃分並沒有實現真正意義上的儲存隔離,所以 NUMA 同樣只會儲存一份作業系統和資料庫系統的副本。

這裡寫圖片描述

NUMA「多節點」的結構設計能夠在一定程度上解決 SMP 低儲存頻寬的問題。假如有一個 4 NUMA 節點的系統,每一個 NUMA 節點內部具有 1GB/s 的儲存頻寬,外部共享匯流排也同樣具有 1GB/s 的頻寬。理想狀態下,如果所有的處理器總是訪問本地記憶體的話,那麼系統就擁有了 4GB/s 的儲存頻寬能力,此時的每個節點可以近似的看作為一個 SMP(這種假設為了便於理解,並不完全正確);相反,在最不理想的情況下,如果所有處理器總是訪問遠端記憶體的話,那麼系統就只能有 1GB/s 的儲存頻寬能力。

除此之外,使用外部共享匯流排時可能會觸發 NUMA 節點間的 Cache 同步異常,這會嚴重影響記憶體密集型工作負載的效能。當 I/O 效能至關重要時,共享總線上的 Cache 資源浪費,會讓連線到遠端 PCIe 總線上的裝置(不同 NUMA 節點間通訊)作業效能急劇下降。

由於這個特性,基於 NUMA 開發的應用程式應該儘可能避免跨節點的遠端記憶體訪問。因為,跨節點記憶體訪問不僅通訊速度慢,還可能需要處理不同節點間記憶體和快取的資料一致性。多執行緒在不同節點間的切換,是需要花費大成本的。

雖然 NUMA 相比於 SMP 具有更好的處理器擴充套件性,但因為 NUMA 沒有實現徹底的主存隔離。所以 NUMA 遠沒有達到無限擴充套件的水平,最多可支援幾百個 CPU。這是為了追求更高的併發效能所作出的妥協,一個節點未必就能完全滿足多併發需求,多節點間執行緒切換實屬一個折中的方案。這種做法使得 NUMA 具有一定的伸縮性,更加適合應用在伺服器端。

MPP 大規模並行處理結構

MPP(Massive Parallel Processing,大規模並行處理),既然 NUMA 擴充套件性的限制是沒有完全實現資源(e.g. 儲存器、互聯模組)的隔離性,那麼 MPP 的解決思路就是為處理器提供徹底的獨立資源。

這裡寫圖片描述

MPP 擁有多個真正意義上的獨立的 SMP 單元,每個 SMP 單元獨佔並只會訪問自己本地的記憶體、I/O 等資源,SMP 單元間通過節點網際網路絡進行連線(Data Redistribution,資料重分配),是一個完全無共享(Share Nothing)的 CPU 計算平臺結構。

這裡寫圖片描述

MPP 的典型特徵就是「多 SMP 單元組成,單元之間完全無共享」。除此之外,MPP 結構還具有以下特點:

  • 每個 SMP 單元內都可以包含一個作業系統副本,所以每個 SMP 單元都可以執行自己的作業系統
  • MPP 需要一種複雜的機制來排程和平衡各個節點的負載和並行處理過程,目前一些基於 MPP 技術的伺服器往往通過系統級軟體(e.g. 資料庫)來遮蔽這種複雜性
  • MPP 架構的區域性區域記憶體的訪存延遲低於遠地記憶體訪存延遲,因此 Linux 會自定採用區域性節點分配策略,當一個任務請求分配記憶體時,首先在處理器自身節點內尋找空閒頁,如果沒有則到相鄰的節點尋找空閒頁,如果還沒有再到遠地節點中尋找空閒頁,在作業系統層面就實現了訪存效能優化

因為完全的資源隔離特性,所以 MPP 的擴充套件性是最好的,理論上其擴充套件無限制,目前的技術可實現 512 個節點互聯,數千個 CPU,多應用於大型機。

Linux 上的 NUMA

基本物件概念

  • Node:包含有若干個 Socket 的組
  • Socket:表示一顆物理 CPU 的封裝,簡稱插槽。Intel 為了避免將物理處理器和邏輯處理器混淆,所以將物理處理器統稱為插槽。
  • Core:Socket 內含有的物理核。
  • Thread:在具有 Intel 超執行緒技術的處理器上,每個 Core 可以被虛擬為若干個(通常為兩個)邏輯處理器,邏輯處理器會共享大多數核心資源(e.g. 記憶體快取、功能單元)。邏輯處理器被統稱為 Thread。
  • Processor:處理器的統稱,不區分物理處理器還是邏輯處理器,對於大多數應用程式而言,它們並不關心處理器是物理的還是邏輯的。

這裡寫圖片描述
(一個 Socket 4 個 Core)

包含關係NUMA Node > Socket > Core > Thread

這裡寫圖片描述

EXAMPLE:上圖為一個 NUMA Topology,表示該伺服器具有 2 個 numa node,每個 node 含有一個 socket,每個 socket 含有 6 個 core,每個 core 又被超執行緒為 2 個 thread,所以伺服器總共的 processor = 2*1*6*2 = 24 顆。

NUMA 排程策略

Linux 的每個程序或執行緒都會延續父程序的 NUMA 策略,優先會將其約束在一個 NUMA node 內。當然了,如果 NUMA 策略允許的話,程序也可以呼叫其他 node 上的資源。

NUMA 的 CPU 分配策略有下列兩種:

  • cpunodebind:約束程序執行在指定的若干個 node 內
  • physcpubind:約束程序執行在指定的若干個物理 CPU 上

NUMA 的 Memory 分配策略有下列 4 種:

  • localalloc:約束程序只能請求訪問本地記憶體
  • preferred:寬鬆地為程序指定一個優先 node,如果優先 node 上沒有足夠的記憶體資源,那麼程序允許嘗試執行在別的 node 內
  • membind:規定程序只能從指定的若干個 node 上請求訪問記憶體,並不嚴格規定只能訪問本地記憶體
  • interleave:規定程序可以使用 RR 演算法輪轉地從指定的若干個 node 上請求訪問記憶體

獲取宿主機的 NUMA 拓撲

Bash 指令碼

#!/bin/bash

function get_nr_processor()
{
   grep '^processor' /proc/cpuinfo | wc -l
}

function get_nr_socket()
{
   grep 'physical id' /proc/cpuinfo | awk -F: '{
           print $2 | "sort -un"}' | wc -l
}

function get_nr_siblings()
{
   grep 'siblings' /proc/cpuinfo | awk -F: '{
           print $2 | "sort -un"}'
}

function get_nr_cores_of_socket()
{
   grep 'cpu cores' /proc/cpuinfo | awk -F: '{
           print $2 | "sort -un"}'
}

echo '===== CPU Topology Table ====='
echo

echo '+--------------+---------+-----------+'
echo '| Processor ID | Core ID | Socket ID |'
echo '+--------------+---------+-----------+'

while read line; do
   if [ -z "$line" ]; then
       printf '| %-12s | %-7s | %-9s |\n' $p_id $c_id $s_id
       echo '+--------------+---------+-----------+'
       continue
   fi

   if echo "$line" | grep -q "^processor"; then
       p_id=`echo "$line" | awk -F: '{print $2}' | tr -d ' '`
   fi

   if echo "$line" | grep -q "^core id"; then
       c_id=`echo "$line" | awk -F: '{print $2}' | tr -d ' '`
   fi

   if echo "$line" | grep -q "^physical id"; then
       s_id=`echo "$line" | awk -F: '{print $2}' | tr -d ' '`
   fi
done < /proc/cpuinfo

echo

awk -F: '{
   if ($1 ~ /processor/) {
       gsub(/ /,"",$2);
       p_id=$2;
   } else if ($1 ~ /physical id/){
       gsub(/ /,"",$2);
       s_id=$2;
       arr[s_id]=arr[s_id] " " p_id
   }
}

END{
   for (i in arr)
       printf "Socket %s:%s\n", i, arr[i];
}' /proc/cpuinfo

echo
echo '===== CPU Info Summary ====='
echo

nr_processor=`get_nr_processor`
echo "Logical processors: $nr_processor"

nr_socket=`get_nr_socket`
echo "Physical socket: $nr_socket"

nr_siblings=`get_nr_siblings`
echo "Siblings in one socket: $nr_siblings"

nr_cores=`get_nr_cores_of_socket`
echo "Cores in one socket: $nr_cores"

let nr_cores*=nr_socket
echo "Cores in total: $nr_cores"

if [ "$nr_cores" = "$nr_processor" ]; then
   echo "Hyper-Threading: off"
else
   echo "Hyper-Threading: on"
fi

echo
echo '===== END ====='

OUTPUT:

===== CPU Topology Table =====

+--------------+---------+-----------+
| Processor ID | Core ID | Socket ID |
+--------------+---------+-----------+
| 0            | 0       | 0         |
+--------------+---------+-----------+
| 1            | 1       | 0         |
+--------------+---------+-----------+
| 2            | 2       | 0         |
+--------------+---------+-----------+
| 3            | 3       | 0         |
+--------------+---------+-----------+
| 4            | 4       | 0         |
+--------------+---------+-----------+
| 5            | 5       | 0         |
+--------------+---------+-----------+
| 6            | 0       | 1         |
+--------------+---------+-----------+
| 7            | 1       | 1         |
+--------------+---------+-----------+
| 8            | 2       | 1         |
+--------------+---------+-----------+
| 9            | 3       | 1         |
+--------------+---------+-----------+
| 10           | 4       | 1         |
+--------------+---------+-----------+
| 11           | 5       | 1         |
+--------------+---------+-----------+
| 12           | 0       | 0         |
+--------------+---------+-----------+
| 13           | 1       | 0         |
+--------------+---------+-----------+
| 14           | 2       | 0         |
+--------------+---------+-----------+
| 15           | 3       | 0         |
+--------------+---------+-----------+
| 16           | 4       | 0         |
+--------------+---------+-----------+
| 17           | 5       | 0         |
+--------------+---------+-----------+
| 18           | 0       | 1         |
+--------------+---------+-----------+
| 19           | 1       | 1         |
+--------------+---------+-----------+
| 20           | 2       | 1         |
+--------------+---------+-----------+
| 21           | 3       | 1         |
+--------------+---------+-----------+
| 22           | 4       | 1         |
+--------------+---------+-----------+
| 23           | 5       | 1         |
+--------------+---------+-----------+

Socket 0: 0 1 2 3 4 5 12 13 14 15 16 17
Socket 1: 6 7 8 9 10 11 18 19 20 21 22 23

===== CPU Info Summary =====

Logical processors: 24
Physical socket: 2
Siblings in one socket:  12
Cores in one socket:  6
Cores in total: 12
Hyper-Threading: on

===== END =====

DPDK 官方提供的 Python 指令碼:

#!/usr/bin/env python
# SPDX-License-Identifier: BSD-3-Clause
# Copyright(c) 2010-2014 Intel Corporation
# Copyright(c) 2017 Cavium, Inc. All rights reserved.

from __future__ import print_function
import sys
try:
    xrange # Python 2
except NameError:
    xrange = range # Python 3

sockets = []
cores = []
core_map = {}
base_path = "/sys/devices/system/cpu"
fd = open("{}/kernel_max".format(base_path))
max_cpus = int(fd.read())
fd.close()
for cpu in xrange(max_cpus + 1):
    try:
        fd = open("{}/cpu{}/topology/core_id".format(base_path, cpu))
    except IOError:
        continue
    except:
        break
    core = int(fd.read())
    fd.close()
    fd = open("{}/cpu{}/topology/physical_package_id".format(base_path, cpu))
    socket = int(fd.read())
    fd.close()
    if core not in cores:
        cores.append(core)
    if socket not in sockets:
        sockets.append(socket)
    key = (socket, core)
    if key not in core_map:
        core_map[key] = []
    core_map[key].append(cpu)

print(format("=" * (47 + len(base_path))))
print("Core and Socket Information (as reported by '{}')".format(base_path))
print("{}\n".format("=" * (47 + len(base_path))))
print("cores = ", cores)
print("sockets = ", sockets)
print("")

max_processor_len = len(str(len(cores) * len(sockets) * 2 - 1))
max_thread_count = len(list(core_map.values())[0])
max_core_map_len = (max_processor_len * max_thread_count)  \
                      + len(", ") * (max_thread_count - 1) \
                      + len('[]') + len('Socket ')
max_core_id_len = len(str(max(cores)))

output = " ".ljust(max_core_id_len + len('Core '))
for s in sockets:
    output += " Socket %s" % str(s).ljust(max_core_map_len - len('Socket '))
print(output)

output = " ".ljust(max_core_id_len + len('Core '))
for s in sockets:
    output += " --------".ljust(max_core_map_len)
    output += " "
print(output)

for c in cores:
    output = "Core %s" % str(c).ljust(max_core_id_len)
    for s in sockets:
        if (s,c) in core_map:
            output += " " + str(core_map[(s, c)]).ljust(max_core_map_len)
        else:
            output += " " * (max_core_map_len + 1)
    print(output)

OUTPUT:

[email protected]:~# python cpu_layout.py
======================================================================
Core and Socket Information (as reported by '/sys/devices/system/cpu')
======================================================================

cores =  [0]
sockets =  [0, 1, 2, 3, 4, 5, 6, 7]

       Socket 0    Socket 1    Socket 2    Socket 3    Socket 4    Socket 5    Socket 6    Socket 7
       --------    --------    --------    --------    --------    --------    --------    --------
Core 0 [0]         [1]         [2]         [3]         [4]         [5]         [6]         [7]

NOTE 1:Processors / Cores = 每個物理核心超執行緒出來的邏輯處理器數量,一般為 2 個。
NOTE 2:上述具有相同 Socket ID 和 Core ID 的 2 個 Processors 就是由同一個 Core 超執行緒出來的兩個邏輯處理器。

Nova 實現的 NUMA 親和

在 Icehouse 版本之前,Nova 定義的 libvirt.xml,不會考慮 Host NUMA 的情況。導致 Libvirt 在預設情況下,有可能發生跨 NUMA node 獲取 CPU/Memory 資源的情況,導致 Guest 效能下降。Openstack 在 Juno 版本中新增 NUMA 特性,使用者可以通過將 Guest 的 vCPU/Memory 繫結到 Host NUMA Node上,以此來提升 Guest 的效能。

Nova 定義的 NUMA 物件概念

除了上文中提到的 NUMA 基本概念之外,Nova 還自定義一些物件概念:

  • Cell:NUMA Node 的通名詞,供 Libvirt API 使用
  • vCPU:虛擬機器的 CPU,根據虛擬機器 NUMA 拓撲的不同,一個虛擬機器 CPU 可以是一個 socket、core 或 thread。
  • pCPU:宿主機的 CPU,根據宿主機 NUMA 拓撲的不同,一個物理機 CPU 同樣可以是一個 socket、core 或 thread。
  • Siblings Thread:兄弟執行緒,即由同一個 Core 超執行緒出來的 Threads。
  • Host NUMA Topology:宿主機的 NUMA 拓撲。
  • Guest NUMA Topology:虛擬機器的 NUMA 拓撲。

NOTE 1:vCPU 和 pCPU 的定義具有一定的迷惑性,簡單來理解:虛擬機器實際是宿主機的一個程序,虛擬機器 CPU 實際是宿主機程序中的一個特殊的執行緒。引入 pCPU 和 vCPU 的概念是為了讓上層邏輯能夠遮蔽機器 NUMA 拓撲的複雜性。

NOTE 2:Thread siblings 物件的引入是為了無論伺服器是否開啟了超執行緒,Nova 同樣能夠支援物理 CPU 繫結的功能。

實現 NUMA 親和的背景

作業系統發行版許可證(Licensing)

根據不同的作業系統發行版許可證,可能會嚴格約束作業系統能夠支援的最大 sockets 數量,同時也就約束了伺服器上可執行虛擬機器的數量。所以,此時應該更加偏向於使用 core 來作為 vCPU,而不是 socket。

因為許可證的影響,建議使用者在上傳映象到 Glance 時,指明一個執行映象最佳的 CPU 拓撲。雲平臺管理員也可以通過修改 CPU 拓撲的預設值來避免使用者超出許可限制。也就是說,對於一個 4 vCPU 的虛擬機器,如果使用的預設值限制最大 socket 為 2,則可以設定其 core 為 2(在 Socket 數量沒有超出限制的前提下,虛擬機器也能達到具有 4 Core 的效果)。

NOTE:OpenStack 管理員應該遵從作業系統許可需求,限制虛擬機器使用的 CPU 拓撲(e.g. max_sockets==2)。設定預設的 CPU 拓撲引數,在保證 GuestOS 映象能夠滿足許可證的同時,又不必讓每個使用者都單獨去設定映象屬性。

CPU 拓撲對效能的影響

宿主機 CPU 拓撲的方式對其自身效能(Performance)具有很大影響。

  • 單 Socket 單 Core 拓撲(單核結構):一個 Socket 只集成了一個 Core。對於多執行緒程式,主要是通過時間片輪轉來獲得 CPU 的執行權,實際上是序列執行,沒有做到並行執行。
    這裡寫圖片描述

  • 單 Socket 多 Core 拓撲(多核結構):一個 Socket 集成了多個水平對稱(映象)的 Core,Core 之間通過 CPU 內部資料匯流排通訊。對於多執行緒程式,可以通過多 Core 實現真正的並行執行。不過對於併發數或執行緒數要大於 Core 數的程式而言,多核結構存線上程(上下文)切換問題。這會帶來一定的開銷,但好在使用的是 CPU 內部資料匯流排,所以開銷會比較低。除此之外,還因為多 Core 是水平映象的,所以每個 Core 都有著自己的 Cache,在某些需要使用共享資料(共享資料很可能會被 Cache 住)的場景中,存在多核 Cache 資料一致性的問題,這也會帶來一些開銷。
    這裡寫圖片描述

  • 多 Socket 單 Core 拓撲: 多 Socket 之間通過主機板上的匯流排進行通訊,整合為一個統一的計算平臺。每一個 Socket 都擁有獨立的內部資料匯流排和 Cache。對於多執行緒程式,可以通過多 Socket 來實現並行執行。不同於單 Socket 多 Core 拓撲,多 Socket 單 Core 拓撲的執行緒切換以及 Socket 間通訊走的都是外部匯流排,所以開銷會比使用 CPU 內部資料匯流排高得多、延時也更長。當然,在使用共享資料的場景中,也同樣存在多 Socket 間 Cache 一致性的問題。多 Socket 拓撲的效能瓶頸在於 Socket 間的 I/O 通訊成本。
    這裡寫圖片描述

  • 超執行緒拓撲(Hyper-Threading):將一個 Core 虛擬為多個 Thread(邏輯處理器),實現一個 Core 也可以並行執行多個執行緒。Thread 擁有自己的暫存器和中斷邏輯,不過 Thread 間會共享執行單元(ALU 邏輯運算單元)和 Cache,所以效能提升是比較有限的,但也非常極致了。

    • 超執行緒結構
      這裡寫圖片描述
    • 普通 CPU 內部結構
      這裡寫圖片描述
  • 多 Socket 多 Core 超執行緒拓撲:具有多個 Socket,每個 Socket 又包含有多個 Core,每個 Core 有虛擬出多個 Thread。是上述拓撲型別的集大成者,擁有最好的效能和最先進的工藝,常見於企業級的伺服器產品,例如:MPP、NUMA 計算平臺系統。

NOTE 1:「多 Socket 單 Core 拓撲」的多執行緒,Socket 間協作要通過外部匯流排通訊,在不同 Socket 上執行的執行緒間的共享資料可能會同時存放在不同的 Socket Cache 上,所以要保證不同 Cache 的資料一致性。具有通訊開銷大,執行緒切換開銷大,Cache 資料一致性難維持,多 Socket 佔位面積大,整合佈線工藝難等問題。

NOTE 2:「單 Socket 多 Core 拓撲」的多執行緒,每個 Core 處理一個執行緒,支援併發。具有多 Core 之間通訊開銷小,Socket 佔位面積小等優勢。但是,當需要執行多個 “大程式”(一個程式就可以將記憶體、Cache、Core 佔滿)的話,就相當於多個大程式需要通過分時切片來使用 CPU。此時,程式間的上下文(指令、資料替換)切換消耗將會是巨大的。所以「單 Socket 多 Core 拓撲」在多工、高併發、高消耗記憶體的程式執行環境中效率會變得非常低下(大程式會獨佔一個 Socket)。

綜上,對於程式規模小的應用場景,建議使用「單 Socket 多 Core 拓撲」,例如個人 PC 的 Dell T3600(單 CPU 6 核,超執行緒支援虛擬出 12 顆邏輯核心);對於多大規模程式的應用場景(e.g. 雲端計算伺服器端),建議使用「多 Socket 單 Core」甚至是「多 Socket 多 Core 超執行緒」的組合,為每個程式分配到單個 CPU,為每個程式的執行緒分配到單個 CPU 中的 Core。

CPU 架構對效能的影響

CPU 架構對於併發程式設計而言,主要需要考慮兩個問題,一個是記憶體可見性問題,一個是 Cache 一致性問題。前者屬於併發安全問題,後者則屬於效能範疇的問題。

這裡寫圖片描述

  • 記憶體可見性問題:該問題在單處理器或單執行緒情況下是不會發生的。但在多執行緒環境中,因為執行緒會被分配到不同的 Core 上執行,所以會出現 Core1 和 Core2 可能會同時把主存中某個位置的值 load 到自己的一級快取中,而 Core1 修改了自己一級快取中的值後,卻不更新主存中對應的值,這樣對於 Core2 來說,將永遠看不到 Core1 對值的修改,從而導致不能保證併發安全性。

  • Cache 一致性問題:假如 Core1 和 Core2 同時把主存中的值 load 到自己的一級快取,Core1 將值修改後,會通過 BUS 匯流排讓 Core2 中的值失效。Core2 發現自己一級快取中的值失效後,會再通過 BUS 匯流排從主存中得到最新的值。但是,匯流排的通訊頻寬是固定的,通過匯流排來進行各 CPU 一級快取資料同步的動作會產生很大的流量,從而匯流排成為了效能的瓶頸。可以通過減小資料同步競爭來減少 Cache 一致性的流量。

超執行緒對效能的影響

需要注意的是,超執行緒技術並非萬能藥。從 Intel 和 VMware 對外公開的資料看,開啟超執行緒後,Core 的總計算能力是否提升以及提升的幅度和業務模型相關,平均提升在 20%-30% 左右。但超執行緒對 Core 的執行資源的爭搶,業務的執行時延也會相應增加。當超執行緒相互競爭時,超執行緒的計算能力相比不開超執行緒時的物理核甚至會下降 30% 左右。所以,超執行緒應該關閉還是開啟,主要還是取決於應用模型。

現在很多應用,比如 Web App,大多會採用多 Worker 設計,在超執行緒的幫助下,兩個被排程到同一個 Core 下不同 Thread 的 Worker,由於 Threads 共享 Cache,TLB(Translation Lookaside Buffer,轉換檢測緩衝區),所以能夠大幅降低 Workers 執行緒切換的開銷。另外,在某個 Worker 不忙的時候,超執行緒允許其它的 Worker 先使用物理計算資源,以此來提升 Core 的整體吞吐量。

這裡寫圖片描述

從上圖可以看出,應用了 HT 技術的場景,處理器執行單元閒置的情況被有效減少了,而且 Thread 1 和 Thread 2 兩個執行緒是被交叉處理的。

但由於 Threads 之間會爭搶 Core 的物理執行資源,導致單個 Thread 的執行時延也會相應增加,響應速度不如當初。對於 CPU 密集型任務而言,當存在超執行緒競爭時,超執行緒計算能力大概是物理核的 60% 左右(非官方資料)。

這裡寫圖片描述

NOTE

  • 對於時延敏感型任務,比如使用者需要及時響應任務執行結果的場景,在節點負載過高,引發超執行緒競爭時,任務的執行時長會顯著增加,導致影響使用者體驗。所以,不推薦計算密集型和時延敏感型任務使用超執行緒技術。
  • 對於後臺計算型任務,它不要求單個任務的響應速度,比如超算中心上執行的後臺計算型任務(一般要執行數小時或數天),就建議開啟超執行緒來提高整個計算節點的吞吐量。

回到虛擬機器應用場景,當我們在 vSphere 的 ESXi 主機上執行兩個 1 vCPU 的虛擬機器,分別繫結到一個 Core 的兩個 Thread 上,在虛擬機器內部執行計算密集型的編譯任務,並確保虛擬機器內部 CPU 佔用率在 50% 左右。從 ESXi 主機上看,兩個 Thread 使用率在 45% 左右,但 Core 的負載就已經達到了 80%。可見,超執行緒競爭問題會讓執行計算密集型應用的虛擬機器效能損耗非常嚴重。

由此,需要注意,如果使用者對虛擬機器的效能要求比較高,那麼不應該讓虛擬機器的 vCPU 執行在 Thread 上,而應該將 vCPU 執行在 Socket 或者 Core 上。對於開啟了超執行緒的 Compute Node,應該提供一種機制能夠將 Threads 過濾掉或抽象為一個 “Core”,這就是引入 Siblings Thread 的意義。

即便在對虛擬機器效能要求不高的場景中,除非我們將虛擬機器的 CPU 和宿主機的超執行緒一一繫結,否則並不建議應該使用超執行緒技術,pCPU 應該被對映為一個 Socket 或 Core。換句話說,如果我們希望開啟 Nova Compute Node 的超執行緒功能,那麼我會建議你使用 CPU 繫結功能來將虛擬機器的 vCPU 繫結到某一個 pCPU(此時 pCPU 對映為一個 Thread)上。

NUMA Topology

現在的伺服器基本都支援 NUMA 拓撲,上文已經提到過,主要驅動 NUMA 體系結構應用的因素是 NUMA 具有的高儲存訪問頻寬、有效的 Cache 效率以及靈活 PCIe I/O 裝置的佈局設計。但由於 NUMA 跨節點遠端記憶體訪問不僅延時高、頻寬低、消耗大,還可能需要處理資料一致性的問題。因此,虛擬機器的 vCPU 和記憶體在 NUMA 節點上的錯誤佈局,將會導宿主機資源的嚴重浪費,這將抹掉任何記憶體與 CPU 決策所帶來的好處。所以,標準的策略是儘量將一個虛擬機器完全侷限在單個 NUMA 節點內

Guest NUMA Topology

將虛擬機器的 vCPU/Mem 完全侷限在單個 NUMA 節點內是最佳的方案,但假如分配給虛擬機器的 vCPU 數量以及記憶體大小超過了一個 NUMA 節點所擁有的資源呢?此時必須針對大資源需求的虛擬機器設計出合適的策略,Guest NUMA Topology 的概念也是為此而提出。

這些策略或許禁止建立超出單一 NUMA 節點拓撲的虛擬機器,或許允許虛擬機器跨多 NUMA 節點執行。並且在虛擬機器遷移時,允許更改這些策略。也就是說,在對宿主機(Compute Node)進行維護時,接收臨時降低效能而選擇次優的 NUMA 拓撲佈局。當然了,NUMA 拓撲佈局的問題還需要考慮到虛擬機器的具體使用場景,例如,NFV 虛擬機器的部署就會強制的要求嚴格的 NUMA 拓撲佈局。

如果虛擬機器具有多個 Guest NUMA Node,為了讓作業系統能最大化利用其分配到的資源,宿主機的 NUMA 拓撲就必須暴露給虛擬機器。讓虛擬機器的 Guest NUMA Node 與宿主機的 Host NUMA Node 進行關聯對映。這樣可以對映大塊的虛擬機器記憶體到宿主機記憶體,和設定 vCPU 與 pCPU 的對映。

Guest NUMA Topology 實際上是將一個大資源需求的虛擬機器劃分為多個小資源需求的虛擬機器,將多個 Guest NUMA Node 分別繫結到不同的 Host NUMA Node。這樣做是因為虛擬機器內部執行的工作負載同樣會遵守 NUMA 節點原則,最終的效果實際上就是虛擬機器的工作負載依舊有效的被限制在了一個 Host NUMA Node 內。也就是說,如果虛擬機器有 4 vCPU 需要跨兩個 Host NUMA Node,vCPU 0/1 繫結到 Host NUMA Node 1,而 vCPU 2/3 繫結到 Host NUMA Node 2 上。然後虛擬機器內的 DB 應用分配到 vCPU 0/1,Web 應用分配到 vCPU 2/3,這樣實際就是 DB 應用和 Web 應用的執行緒始終被限制在了同一個 Host NUMA Node 上。但是,Guest NUMA Topology 並不強制將 vCPU 與對應的 Host NUMA Node 中特定的 pCPU 進行繫結,這可以由作業系統排程器來隱式完成。只是如果宿主機開啟了超執行緒,則要求將超執行緒特性暴露給虛擬機器,並在 NUMA Node 內繫結 vCPU 與 pCPU 的關係。否則 vCPU 會被分配給 Siblings Thread,由於超執行緒競爭,效能遠不如將 vCPU 分配到 Socket 或 Core 的好。

NOTE:如果 Guest vCPU/Mem 需求超過了單個 Host NUAM Node,那麼應該將 Guest NUMA Topology 劃分為多個 Guest NUMA Node,並分別對映到不同的 Host NUMA Node 上。

在 Nova 上應用 NUMA 親和來建立高效能虛擬機器

NUMA 親和的原則是:將 Guest vCPU/Mem 都分配在同一個 NUMA Node 上,充分使用 NUMA node local memory,避免跨 node 訪問 remote memory。

openstack flavor set FLAVOR-NAME \
    --property hw:numa_nodes=FLAVOR-NODES \
    --property hw:numa_cpus.N=FLAVOR-CORES \
    --property hw:numa_mem.N=FLAVOR-MEMORY
  • FLAVOR-NODES(整數):設定 Guest NUMA nodes 的個數。如果不指定,則 Guest vCPUs 可以在任意可用的 Host NUMA nodes 上浮動。
  • N:整數,Guest NUMA nodes ID,取值範圍在 [0, FLAVOR-NODES-1]。
  • FLAVOR-CORES(逗號分隔的整數):設定分配到 Guest NUMA node N 上執行的 vCPUs 列表。如果不指定,vCPUs 在 Guest NUMA nodes 之間平均分配。
  • FLAVOR-MEMORY(整數):單位 MB,設定分配到 Guest NUMA node N 上 Memory Size。如果不指定,Memory 在 Guest NUMA nodes 之間平均分配。

設定 Guest NUMA Topology 的兩種方式:

  • 自動設定 Guest NUMA Topology:僅僅需要指定 Guest NUMA nodes 的個數,然後由 Nova 根據 Flavor 設定的虛擬機器規格平均將 vCPU/Mem 分佈到不同的 Host NUMA nodes 上(預設從 Host NUMA node 0 開始分配)。

NOTE:選擇使用自動設定方式時,建議一同使用 hw:numa_mempolicy 屬性,表示 NUMA 的 Mem 訪問策略,有嚴格訪問本地記憶體的 strict 和寬鬆的 preferred 兩種選擇,這樣可以最大程度降低配置引數的複雜性。而且對於某些特定工作負載的 NUMA 架構問題,比如:MySQL “swap insanity” 問題 ,或許 preferred 會是一個不錯的選擇。

  • 手動設定 Guest NUMA Topology:不僅指定 Guest NUMA nodes 的個數,還需要通過 hw:numa_cpus.Nhw:numa_mem.N 來指定每個 Guest NUMA nodes 上分配的 vCPUs 和 Memory Size。

Nova Scheduler 會根據引數 hw:numa_nodes 來決定如何對映 Guest NUMA node。如果沒有設定該引數,那麼 Scheduler 將自由的決定在哪裡執行虛擬機器,而無需關心單個 NUMA 節點是否能夠滿足虛擬機器 flavor 中的 vCPU/Mem 配置,但仍會優先考慮選出一個 NUMA 節點就可以滿足情況的計算節點。

  • 如果 numa_nodes = 1,Scheduler 將會選擇出單個 NUMA 節點能夠滿足虛擬機器 flavor 配置的計算節點。
  • 如果 numa_nodes > 1,Scheduler 將會選擇出 NUMA 節點數量以及 NUMA 節點中資源情況能夠滿足虛擬機器 flavor 配置的計算節點。

NOTE 1:只有在設定了 hw:numa_nodes 後,hw:numa_cpus.Nhw:numa_mem.N 才會生效。只有當 Guest NUMA nodes 存在非對稱訪問 vCPU/Mem 時(Guest NUMA Nodes 之間擁有的 vCPU 數量和 Mem 大小並非是映象的),才需要去設定這些引數。

NOTE 2:N 僅僅是 Guest NUMA node 的索引,並非實際上的 Host NUMA node 的 ID。例如,Guest NUMA node 0,可能會被對映到 Host NUMA node 1。類似的,FLAVOR-CORES 的值也僅僅是 vCPU 的索引。因此,Nova 的 NUMA 特性並不能用來約束 Guest vCPU/Mem 繫結到指定的 Host NUMA node 上。要完成 vCPU 繫結到指定的 pCPU,需要藉助 CPU Pinning policy 機制。

WARNING:如果 hw:numa_cpus.Nhw:numa_mem.N 的設定值大於虛擬機器本身可用的 CPUs/Mem,則觸發異常。

EXAMPLE:定義虛擬機器有 4 vCPU,4096MB Mem,設定 Guest NUMA topology 為 2 Guest NUMA node:

  • Guest NUMA node 0:vCPU 0、Mem 1024MB
  • Guest NUMA node 1:vCPU 1/2/3、Mem 3072MB
openstack flavor set aze-FLAVOR \ 
  --property hw:numa_nodes=2 \ 
  --property hw:numa_cpus.0=0 \ 
  --property hw:numa_cpus.1=1,2,3 \ 
  --property hw:numa_mem.0=1024 \ 
  --property hw:numa_mem.1=3072 \

NOTE:numa_cpus 指定的是 vCPUs 的序號,而非 pCPUs。

使用該 flavor 建立的虛擬機器時,最後由 Libvirt Driver 完成將 Guest NUMA node 對映到 Host NUMA node 上。

除了通過 Flavor extra-specs 來設定 Guest NUMA topology 之外,還可以通過 Image Metadata 來設定。e.g.

glance image-update --property \ 
    hw_numa_nodes=2 \ 
    hw_numa_cpus.0=0 \ 
    hw_numa_mem.0=1024 \ 
    hw_numa_cpus.1=1,2,3 \ 
    hw_numa_mem.1=3072 \ 
    image_name

注意,當映象的 NUMA 約束與 Flavor 的 NUMA 約束衝突時,以 Flavor 為準。

NOTE 1:KVM 的宿主機會暴露出 Host NUMA Topology 的細節(e.g. NUMA 節點數量,NUMA 節點的記憶體 total 和 free,NUMA 節點的 CPU total 和 free),但其他 Hypervisor 的作業系統平臺未必會將這些資訊暴露出來,比如 VMware 只能通過 vSphere WS API 來獲得並不 “完整” 的拓撲資訊。所以,NUMA 親和特性適配度最高的還是 KVM。

NOTE 2:nova-compute service 的 ResourceTracker 通過 Hyper Driver 定時收集宿主機的 Host NUMA Topology 資訊。

相關推薦

OpenStack Nova 高效能虛擬機器 NUMA 架構親和

目錄 寫在前面 最近太忙了,筆者實在懶得畫圖,文章的圖片大多來源於網際網路,感謝創作者們(可惜找不到源出處)。 這篇博文與其說是介紹 OpenStack Nova 的高效能虛擬機器,倒不如說是介紹 CPU 相關的硬體架構與應用程式之間的愛恨情仇更

openstack-nova原始碼虛擬機器建立流程

nova-api接收到訊息後,呼叫nova\api\openstack\compute\servers.py 中ServersController類的create()方法, 部分程式碼: try:             inst_type = flavors.get_f

OpenStack Nova虛擬機器初始化user-data & Cloud-init

    有的時候我們希望在boot虛擬機器的時候能夠對虛擬機器做些配置, 比如配置網路, 寫入檔案, 下載一些包並安裝等等, openstack中提供了實現這些的方法, 就是user-data 和Cloud-init。 user-data     在說user-data之前

深入理解OpenStack虛擬機器Metadata

前言: 剛接觸OpenStack的朋友都知道,我們在建立虛擬機器的時候選擇金鑰對,虛擬機器建立完畢後,直接使用ssh無密碼就可以登入到虛擬機器,那麼我們建立的my-key如何就這麼神奇的被放到了虛擬機器中呢? OpenStack metadata 要理解如何實現的,我們需要先了解OpenStack

你建立的OpenStack高效能虛擬機器能實現“零損耗”麼?

使用預設引數建立的虛擬機器,虛擬機器的VCPU在物理CPU不同核心之間動態排程,另外,由於Linux還可能會將軟中斷,記憶體交換等程序排程到虛擬機器正在使用的物理核心上,這些因素導致這些虛擬機器相對於物理機的計算效能可能會產生較大的抖動,不能滿足一些對計算SLA要求很嚴格的業務,比如,很多金融業務就要求99.

nova】centos7下libety版本openstack動態遷移虛擬機器

openstack虛擬機器動態遷移有兩個方式,塊遷移和共享儲存遷移。 前提條件: 目標物理伺服器是有足夠的記憶體,虛擬CPU,磁碟。cpu同類型 說明: 本文用nfs用作共享儲存演示,共有四個節點controller、 computer1

Java虛擬機器‘靜態分派、動態分派’

Java是一門面向物件的語言,因為Java具備面向物件的三個特性:封裝、繼承、多型。分派的過程會揭示多型特性的一些最基本的體現,如“過載”和“重寫”在Java虛擬機器中是如何實現的,並不是語法上如何寫,我們關心的依然是虛擬機器如何確定正確的目標方法。 一、靜態分派 先看一段程式碼 pac

深入理解Java虛擬機器物件

一.物件的建立 1.類載入檢查和分配記憶體 虛擬機器遇到一條new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、 解析和初始化過。 如果沒有,那必須先執行相應的類載入過程。 在類載入檢查通過後,接下來虛擬機器將為新生物

深入理解Java虛擬機器執行時資料區域

一.執行時資料區域有哪些? 首先,我們先來看一張圖: 如上面的圖所示,執行時記憶體區域主要分為:1.程式計數器,2.Java虛擬機器棧,3.本地方法棧,4.Java堆,5.方法區等等,下面就一個個來剖析一下。 二.這些區域都有哪些作用? 首先我們熟悉一下一個一般性的 Java 程式的

深入瞭解Java虛擬機器Java虛擬機器

        與程式計數器(想了解計數器看我上一篇部落格)一樣,Java虛擬機器棧也是執行緒私有的,他的生命週期與執行緒相同,虛擬機器棧描述的是Java方法執行的記憶體模式:每個方法在執行的同時都會建立一個棧幀用於儲存區域性變量表,運算元棧,動態連結,方法出

CentOS高效能虛擬機器安裝及配置(KVM)

安裝 通過yum安裝 yum install kvm kmod-kvm qemu kvm-qemu-img virt-viewer virt-manager libvirt libvirt-python python-virtinst 檢視例項 virsh list --a

高效能負載均衡分類架構

今天跟大家分享一下,關於高效能負載均衡的分類架構相關的知識。 當然了,首先要強調一點,並不是所有的專案一開始就要求高效能的。前面我也提到過。如果沒讀的可以參考這篇文章:架構設計之六個複雜度來源 下面進入正題,說說高效能負載均衡之分類架構。 單伺服器無論如何優化,無論採用多好的硬體,總會有一個性能天花板,

深入理解java虛擬機器自動記憶體管理機制(二)

垃圾收集演算法     java中的記憶體是交給虛擬機器管理的。要實現垃圾回收,必須考慮如下三個問題:     1. 哪些記憶體需要回收?     2. 什麼時候回收?     3. 怎麼回收?     對於第一點,往大了來說,是堆和方法區的記憶體需要回收。往具體了來說,是堆中哪些物件的記憶體可以回

深入理解java虛擬機器自動記憶體管理機制(三)

  各類垃圾收集器與GC日誌 (一)垃圾收集器   一、Serial收集器     最基本、歷史最悠久的收集器。使用複製演算法,用在新生代,通常老年代用Serial old配合。GC過程需要stop the world。適用於client模式下的虛擬機器。   二、ParNew收集器  

深入理解java虛擬機器自動記憶體管理機制(四)

記憶體分配與回收策略 (一)記憶體分配策略     給誰分配?分配到哪?是記憶體分配策略必須解答的問題。     java物件是分配的物件,往大方向來說,是分配到堆中,更細一點說,根據物件不同的特點分配到新生代和老年代區域。如果啟動了本地執行緒分配緩衝,就按執行緒優先在TLAB上分配。     一、新

【轉】進入Android Dalvik虛擬機器Dalvik指令集

Dalvik指定在呼叫格式上模仿了C語言的呼叫約定。Dalvik指令的語法與助詞符有如下特點: 引數採用從目標(destination)到源(source)的方式。 根據位元組碼的大小與型別不同,一些位元組碼添加了名稱字尾以消除岐義。

Java虛擬機器Class檔案

對《深入理解Java虛擬機器》一書的類檔案結構進行總結(不關注細節,只總結): 一般一個類或者一個介面就對應一個class檔案,但有的類是用類載入器直接生成的,這些類就沒有class檔案 那麼,class檔案中都儲存了些什麼呢? 1.class檔案的版本資訊 用開頭4個位元組儲存,又

安裝虛擬機器Hyper-V

前言 為啥要用Hyper-V來弄虛擬機器呢,主要是我的電腦不知道有什麼問題,清理了無數遍VMware的登錄檔、安裝路徑,嘗試過8、12、14,以及清理器的1.0~1.4,就是安裝不了啊,一直報錯“failed to install usb inf file”。沒

Java虛擬機器搜尋class檔案

Java命令 Java虛擬機器的工作是執行Java應用程式。和其他型別的應用程式一樣,Java應用程式也需要一個入口點,這個入口點就是我們熟知的main()方法。如果一個類包含main()方法,這個類就可以用來啟動Java應用程式,我們把這個類叫作主類。最簡單的Java程式是隻有一個main()方法的類,如

深入理解java虛擬機器JVM調優配置

轉載文章:http://blog.csdn.net/sivyer123/article/details/17139443 堆記憶體設定 原理 JVM堆記憶體分為2塊:Permanent Space 和 Heap Space。 Permanent 即 持久代(Pe