性能和可伸縮性
為了可以伸縮,您必須了解這對于您特定的方案意味著什么。例如,對于 Web 服務器,可伸縮性意味著可以為與所連接的用戶數量相關的頁面提供服務。請將其考慮為線圖表。
![]() |
圖1.線性比例示例 |
隨著用戶數量的增加,每秒的頁面數量也應該增加。上圖顯示了一個線性比例。當用戶數量達到 3 倍時,每秒提供的頁面也相應增加。
另一個比例的定義與硬件相關。如果我將系統上的處理器數量增加一倍,那么我的 Web 服務器是否也會將輸出增加一倍呢?RAM 或磁盤等情況如何呢?應用程序也需要根據這個想法設計。您所創建的線程數量應該基于系統中的處理器數量以及每個線程進行的工作類型。用于緩存網頁內容的內存量應該與可用于應用程序的內存量成一定比例,等等。這個概念通常被稱為“向上擴展”。如果我將框構建得越來越大,那么可以相應的生成越來越多嗎?
伸縮的其他形式是當您談論分布式計算或服務器場時。這通常被稱為“向外伸縮”。如果我將服務器場中的計算機數量增加一倍,那么我的輸出也會增加一倍嗎?
當設計可伸縮的系統時,需要考慮這些方案。當前,硬件變得越來越大(Itanium 最多支持 64 個處理器計算機),因此向上擴展需要在開發人員頭腦中處于最重要的位置。如果您的圖表轉為水平、甚至隨著您增加資源開始下降,這尤為正確。如果整個系統的某個部分無法伸縮,它可能會對整個系統產生負面的影響。
線程:如何有效地使用它們
在線程間分割您的工作可以簡化代碼,在多處理器系統上可以使您的代碼更有效,但是如果您不知道自己在做什么的話,還會降低性能和可伸縮性。例如,如果應用程序中的所有線程都需要獲得相同的全局關鍵部分,那么對該關鍵部分的爭用可能會使您的線程花費其大部分休眠時間。它還可能導致發生過多的上下文轉換,進而可能會引起應用程序占用系統內核中相當比例的處理時間,甚至根本沒有運行您的代碼。如果在多處理器的系統上,這些問題會尤其糟糕,您額外的處理器可能會結束當前閑置,等待訪問共享數據。
要使用的線程的理想數量等于系統中處理器的數量。如果您的線程相互獨立并受到處理器的限制,那么它們應該能夠每次都消耗掉其整個時間片。如果您具有可能執行阻止操作的線程,那么您可能希望增加線程的數量,以便當一個線程休眠時,另一個線程可以取代其位置。您將要確定線程阻塞的位置及頻率。意識到這一點后,您就可以知道應該運行的線程數量。您始終要為每個處理器準備好一個線程。否則,您就浪費了處理能力。當然,這些僅僅是指導原則,并且確定應用程序是否以盡可能高的效率運行的唯一方法就是對其進行分析和測試。
異步 I/O:不會阻塞等待數據
基于 NT 內核的 Windows 系統支持異步 I/O,又稱重疊的 I/O。大多數形式的 I/O 都可以異步完成。這包括文件 I/O 和網絡 I/O。對于文件 I/O,您可以使用 ReadFile/WriteFile API。當讀/寫時,通過借助 FILE_FLAG_OVERLAPPED 標記打開文件并指定 OVERLAPPED 結構,您將使系統在 I/O 完成時通知您。這使您可以在等待的過程中完成其他工作。對于使用 Windows Socket (WinSock) 的網絡 I/O,您可以使用 WSASocket 創建套接字,并指定 WSA_FLAG_OVERLAPPED 標記,然后當調用 WSARecv/WSASend API 時,您可以指定一個 OVERLAPPED 結構或一個回調函數。當您編寫網絡服務器時,異步 I/O 尤為有效。您可以將多個接收請求“排入隊列”,然后去休息,等待其中一個完成。當一個完成時,就會處理傳入的數據,然后將另一個接收“排入隊列”。這比使用 select API 來輪詢數據好得多,并且它可以更有效地使用系統資源。
等待異步 I/O 請求完成有幾種選項:
調用 GetOverlappedResult API
在發出異步 I/O 請求后,您可以使用 GetOverlappedResult API 來輪詢請求的狀態,或者僅僅等待請求的完成。當請求完成時,GetOverlappedResult 將返回請求過程所傳輸的字節數。
使用 HasOverlappedIoCompleted 宏
您可以使用 HasOverlappedIoCompleted 宏來有效地進行輪詢與 OVERLAPPED 結構相關聯的請求是否已經完成。請求完成后,您就可以使用 GetOverlappedResult API 來獲得有關請求的更多信息(例如傳輸的字節數)。
指定 OVERLAPPED 結構中的事件
通過在 OVERLAPPED 結構的 hEvent 字段中指定一個事件,您可以執行自己的輪詢或等待請求的完成,方法是在對 WaitForSingleObject 或 WaitForMultipleObjects 的調用中指定一個事件。當重疊操作完成時,內核將發信號通知該事件。
將內核對象綁定到 I/O完成端口
I/O 完成端口是系統提供的非常有用的工具。有關信息,請參閱下面的部分。對于事件驅動的系統(例如網絡服務器等待輸入),I/O 完成端口提供了用于等待和處理傳入事件的完美機制。
I/O 完成端口:事件驅動 I/O
大多數 Windows 開發人員都熟悉窗口消息和消息隊列。將 I/O 完成端口理解為高性能、高可伸縮性的超級消息隊列。如果您具有一個事件驅動的系統,則您需要使用完成端口。完成端口從根本上設計用于提供性能。如果您從頭開始編寫代碼,您絕對應該使用 I/O 完成端口。它們要求進行一些嘗試才能正確完成,但是只要您熟悉了它們的工作方式,使用起來就會非常簡單。如果從另一個系統或使用異步 I/O 的代碼庫遷移應用程序,那么您必須提前完成一些工作,但由此帶來的好處證明所做的努力是完全值得的。
您可以使用 CreateIoCompletionPort API 來創建完成端口。這也是您用于關聯內核對象與完成端口的 API。在文件句柄或套接字句柄與完成端口相關聯后,在該句柄上完成的所有 I/O 請求都將排列到完成端口隊列中。
通知可以被排列到完成端口隊列中,或者按照先進先出 (FIFO) 順序進行處理。您還可以使用 PostQueuedCompletionStatus API 將自定義的通知排列到完成端口隊列中。使用這個自定義通知方法是一個很好的方法,用于向線程發出信號通知其關機或插入任何其他自定義外部事件。在下面的示例代碼中,PostQueuedCompletionStatus 用于通知工作線程退出:
HRESULT StopCompletionThreads() |
請注意,為 dwNumberOfBytesTransferred 和 dwCompletionKey 參數傳遞零,而為 OVERLAPPED 參數傳遞 NULL。這些組合的值是工作線程檢查用于關機的值:
UINT __stdcall CompletionThread(PVOID param) |
I/O 完成方法的核心是 OVERLAPPED 結構。OVERLAPPED 結構包含特定于每個 I/O 請求的上下文信息。通常情況下,將結構進行擴展以添加自己的上下文信息。當處理完成通知時,可以獲得對該結構(以及您的上下文數據)的訪問。
通過從 OVERLAPPED 結構繼承或將其包括為自己結構的第一個字段來擴展 OVERLAPPED 結構,如下所示:
//C++ |
OVERLAPPED 結構包含下列字段:
typedef struct _OVERLAPPED { |
當讀取文件或寫入文件時,Offset 和 OffsetHigh 字段用于指定偏移量。Internal 字段包含操作的狀態(或錯誤)。InternalHigh 字段包含在 I/O 請求過程中傳輸的字節數。在 GetOverlappedResult 返回 TRUE(或者完成通知排列到完成端口隊列中)之前,Internal 和 InternalHigh 字段都是無效的。
可以擴展該結構以包括您可能需要的任何其他字段。但是,請牢記,結構必須在 I/O 請求的生存期中保持可用。
下面的代碼片段顯示了 OVERLAPPED 和 OverlappedBase 結構是如何為網絡 I/O 操作進行擴展的:
#define SOCKET_BUFFER_SIZE 128 |
這允許每個請求信息可以與已啟動的每個 I/O 請求一起存儲。op 字段存儲正在啟動、發送、接收或接受的操作。numberOfBytes 字段包含有效的(用于發送或接收)buffer 字段中的字節數。
劃分和征服:讓線程獨立工作
可伸縮性的弊端在于爭用。例如,當一個線程必須等待另一個線程以獲取鎖定時,該線程就在浪費時間,并且潛在地可以完成的工作必須等待。這會引起線程關系和非一致的內存訪問 (NUMA)。如果您的處理可以在線程之間進行分割(在線程之間沒有實際的依存關系),那么可以將每個線程鎖定到其自己的處理器上。在 NUMA 系統上,您還可以分割每個線程使用的內存,這樣對于 NUMA 節點,內存是本地的。
線程關系
Windows Server 2003 使您可以指定允許某個線程在哪個處理器上運行。這稱為設置線程的處理器關系。您可以使用 CODE>SetThreadAffinityMask/GetThreadAffinityMask 函數來進行設置并檢查特定線程的關系。設置線程的關系在降低處理器間總線通訊方面很有用。當線程從一個處理器移動到另一個處理器時,當前處理器的緩存必須與新的處理器進行同步。處理器之間的線程跳轉可能會引起性能問題。另外,某些系統使您可以將特定的設備中斷綁定到特定的處理器。在您的軟件中,您可以將特定的線程“綁定”到該處理器,并且從該線程發出/處理該設備的所有 I/O,因此通過增加潛在的系統并發(即,在多處理器之間快速傳播如網卡這樣的活動設備)。
NUMA
NUMA 表示非一致的內存訪問。在傳統的對稱多處理 (SMP) 系統上,系統中的所有處理器對整個范圍的物理內存具有相同的訪問權限。傳統 SMP 系統的問題在于添加越多的處理器和內存,總線通訊量就會越高。也就會抑止性能。在 NUMA 系統上,處理器分組成較小的系統,每個小系統都有其自己的“本地”內存。訪問“本地”內存成本很低,然而訪問另一個節點上的內存代價可能會非常昂貴。Windows 將嘗試在正在使用內存的節點上計劃線程,但是可以使用 NUMA 函數來改進線程計劃和內存使用情況來幫助 Windows。使用下列功能來確定哪個處理器屬于哪個節點,以及為特定的處理器/節點設置線程的關系:
另外,大量利用內存的應用程序可以使用以下函數來改進它們在 NUMA 系統上的內存使用情況:
開始考慮 NUMA 和大型多處理器系統以及從頭開始為它們進行設計是非常重要的。大多數最初的 64 位部署都將用于大型多處理器系統,該系統的處理器多于 8 個,運行諸如 Secure Audio Path (SAP) 這樣的巨型企業應用程序。NUMA 對于整體可伸縮性和性能非常關鍵。
WinSock Direct
在大型的數據中心中,服務器之間的通信量可能會超出傳統基于 TCP/IP 網絡的帶寬。通過卸載一些來自 CPU 的網絡協議處理,系統區域網 (SAN) 設計用于解決這個問題。在服務器之間提供更快速的通訊對服務器應用程序很有好處,這樣就改進了向外擴展解決方案的性能。大多數 SAN 要求直接針對供應商的 API 編寫應用程序,這就導致很少有應用程序可以用于在 SAN 環境中進行部署。Microsoft 開發的 Windows Sockets (WinSock) Direct 針對低級 SAN 實現提供了一個通用編程接口。WinSock Direct 位于標準 WinSock API 下,但是繞過了內核網絡層直接與 SAN 硬件進行對話。因為 WinSock Direct 位于現有的 WinSock API 下,所以 IT 部門可以在 SAN 環境中部署應用程序,而無需對應用程序進行修改。
SAN 通過兩種標準的傳輸模式,提供了可靠的、順序的數據提交,這兩種模式是:消息和遠程直接內存訪問 (RDMA)。消息很像傳統的網絡協議,其中數據包發送到某個對等方,而該對等方會從網絡中請求數據包。RDMA 允許指定數據包的目標緩沖區。
通常情況下,SAN 硬件將直接在硬件中實現大部分其數據傳輸功能。這使得 SAN 實現可以完成諸如繞過內核這樣的操作。通常由內核提供的處理直接卸載到硬件中。
WinSock Direct 避免了應用程序直接編程到 SAN 特定的 API 的要求。只通過安裝 SAN 硬件以及為硬件安裝 WinSock Direct 驅動程序,現有的應用程序就可以利用由 SAN 提供的更高的性能。