George V. Reilly
微軟公司
2月22, 1999
目錄
序言
應用程序服務器
IIS的靈活性和性能
扼殺服務器性能的十條戒律
結論
--------------------------------------------------------------------------------
序言
現在,服務器性能問題是許多書寫桌面應用程序的人所要面對的問題。組件對象模型(Component Object Model,COM)和Component ware的成功產生了一個意想不到的結果,這就是如果使用像ASP(IIS的一個擴展)這樣的應用程序服務器,就不用編寫主機代碼了,其實以前的主機代碼都不是在真正的服務器環境下編寫的。桌面環境和服務器環境之間有許多重要的不同,這些不同會在性能上產生不可預測的影響。
桌面應用程序服務器
影響桌面應用程序性能的因素是眾所周知的。長指令路徑意味著更慢的代碼,這是性能方面的一個主要缺陷。使用大量資源會使應用程序變得更加臃腫,這樣系統中的其他應用程序可用的資源就會更少。減慢啟動時間會激怒用戶。太多的運行設置會使機器的頁錯誤率增高,使它們變慢而且反映遲鈍。服務器應用程序也常受到這些因素影響,另外還有一些其他因素介紹如下:
通常,服務器應用程序同時處理的客戶沒有幾百也有幾十。對桌面應用程序來說,如果能在1/10秒內對用戶做出反應就算是很快的了。假設一個操作需要整整100ms的話,那么這個應用程序在一秒中只能進行10個操作。大多數服務器應用程序需要比每秒鐘十次請求大得多的通量。高延遲時間網絡(延遲時間=消息的傳輸時間)加長了反應時間,這就需要服務器的反應更快以滿足要求。
服務器應用程序經常處理大量的數據設置。效率低下的,尤其是那些浪費運行時間的方法,是不能用于處理上百萬條數據的。
服務器機器比桌面機器更強大。服務器機器有更多的內存,更大的磁盤,更快的CPUs,并且通常有多個處理器。但是這些仍然不夠。桌面機器處理的是零星的突發性業務,大部分時間是空閑的,而服務器的負載是連續不斷的。服務器機器很昂貴,必須運行得很好才行。
服務器應用程序需要具有以月計算的正常運行時間。過了一段時間后,服務器的性能必須不會由于資源泄露或 cruft(一種需要周期性清除的數據結構和統計結果)的積聚而降低。
大多數服務器應用程序都需要采用多線程結構?紤]一個一次只處理一個請求。而將大部分時間都化在I/O上的單線程服務器,這樣的性能是很難讓人接受的。線程池可以利用其他空閑的處理器時鐘周期同時處理幾個請求。為了充分利用多處理器系統,服務器應用程序必須是多線程的。不幸的是,多線程應用程序很難編寫,很難調試,而且很難運行得好,尤其是在多處理器系統中。但是一旦正確地得到它,其性能會遠遠超過同樣的單線程應用程序,從這一點來說,使用多線程應用程序還是值得的。
單線程應用程序相對簡單,很容易理解:程序中某一時刻只有一個事件發生。在多
線程應用程序中,并發行為導致復雜的相互作用,其影響很難預測。另外,這些相
互作用,不管是否是災難性的,都很難再生。桌面應用程序很少有多于一個線程
的,即使有,這些線程也只是用于分立的后臺業務,例如打印。
IIS的靈活性和性能
Internet Information Server(IIS)是一個應用程序服務器。在很多方面,它像是一個虛擬操作系統,因為有許多ASP和ISAPI應用程序在處理間隔中運行。
IIS使用一個I/O線程池來處理所有到來的請求。對靜態文件(.htm,.jpg等文件)的請求會馬上得到滿足,而對動態內容的請求被分派到適當的ISAPI擴展動態連接庫。ASP擴展利用一個工人線程池運行ASP頁。因為ASP是基于COM的,所以所有組件都是在我們的處理過程中執行的。這是一個好壞摻半的事情。它對開發者來說是好極了,因為它允許組件的簡單重用,使ASP非常靈活,因此使ASP和IIS非常成功。但是,這個靈活性導致了性能問題。因為許多組件是為桌面系統編寫的,并且許多專門為ASP創建的組件是由那些不是十分會寫高性能服務器組件的人編寫的。
對ISAPI擴展和過濾器也是一樣。不同組件之間及同一組件的不同實例中都存在著嚴重的相互影響。
下面的所有說明都適用于IIS,其中的大多數也適用于其他服務器應用程序。
扼殺服務器性能的10條戒律
下面的每一條戒律都將有效地影響代碼的性能和可伸縮性。換句話說,盡可能不要照著戒律去做!下面,我將解釋如何破壞他們以便提高性能和可伸縮性。
應該分配和釋放多個對象
你應該盡量避免過量分配內存,因為內存分配可能是代價高昂的。釋放內存塊可能更昂貴,因為大多數分配算符總是企圖連接臨近的已釋放的內存塊成為更大的塊。直到Windows NT? 4.0 service pack 4.0,在多線程處理中,系統堆通常都運行得很糟。堆被一個全局鎖保護,并且在多處理器系統上是不可擴展的。
不應該考慮使用處理器高速緩存
大多數人都知道由虛擬內存子系統導致的hard 頁錯誤代價很高,最好避免。但是許多人認為其他內存訪問方法沒有什么區別。自從80486以后,這一觀點就不對了,F代的CPUs比RAM要快得多,RAM至少需要兩級內存緩存 ,高速L1 緩存能保存8KB數據和8KB指令,而較慢的L2 緩存能保存幾百KB的數據和代碼,這些數據和代碼混合在一起。L1 緩存中內存區域的一個引用需要一個時鐘周期,L2 緩存的引用需要4到7個時鐘周期,而主內存的引用需要許多個處理器時鐘周期。后一數字不久將會超過100個時鐘周期。在許多方面,緩存像一個小型的,高速的,虛擬內存系統。
至于和緩存有關的基本內存單元不是字節而是緩存列。Pentium 緩存列有32個字節寬。Alpha 緩存列有64個字節寬。這意味著在L1 緩存中只有512個slot給代碼和數據。如果多個數據一起使用(時間位置)而并不存儲在一起(空間位置),性能會很差。數組的空間位置很好,而相互連接的列表和其他基于指針的數據結構的位置往往很差。
把數據打包到同一個緩存列中通常會有利于提高性能,但是它也會破壞多處理器系統的性能。內存子系統很難協調處理器間的緩存。如果一個被所有處理器使用的只讀數據,和一個由一個處理器使用并頻繁更新的數據共享一個緩存 列,那么緩存將會花費很長時間更新這個緩存列的拷貝。這個Ping-Pong高速游戲通常被稱為"緩存 sloshing"。如果只讀數據在一個不同的緩存 列中,就可以避免sloshing。
對代碼進行空間優化比進行速度優化效率更高。代碼越少,代碼所占的頁也越少,這樣需要的運行設置和產生的頁錯誤也會更少,同時占據的緩存 列也會更少。然而,某些核心函數應該進行速度優化?梢岳胮rofiler去識別這些函數。
決不要緩存頻繁使用的數據。
軟件緩存可以被各種應用程序使用。當一個計算代價很高時,你會保存結果的一個拷貝。這是一個典型的時空折中方法:犧牲一些存儲空間以節省時間。如果做得好,這種方法可能非常有效。
你必須正確地進行緩存。如果緩存了錯誤數據,就會浪費存儲空間。如果緩存得太多,其他操作可以使用的內存將會很少。如果緩存得太少,效率又會很低,因為你必須重新計算被緩存 遺漏的數據。如果將時間敏感數據緩存得時間過長,這些數據將會過時。一般,服務器更關心的是速度而不是空間,所以他們要比桌面系統進行更多的緩存。一定要定期去除不用的緩存,否則將會有運行設置問題。
應該創建多個線程,越多越好。
調整服務器中起作用的線程數目是很重要的。如果線程是I/O-bound的,將會花費很多時間用來等待I/O的完成-一個被阻塞的線程就是一個不做任何有用工作的線程。加入額外的線程可以增加通量,但是加入過多的線程將會降低服務器的性能,因為上下文交換將會成為一個重大的overhead。上下文交換速度應該低的原因有三個:上下文交換是單純的overhead,對應用程序的工作沒有任何益處;上下文交換用盡了寶貴的時鐘周期;最糟的是,上下文交換將處理器的緩存填滿了沒用的數據,替換這些數據是代價高昂的。
有很多事情是依靠你的線程化結構的。每個客戶端一個線程是絕對不合適的。因為對于大量用戶端,它的擴展性不好。上下文交換變得難以忍受,Windows NT用盡了資源。線程池模型會工作得更好,在這種方法中一個工人線程池將處理一條請求列,因為Windows 2000提供了相應的APIs,如QueueUserWorkItem。
應該對數據結構使用全局鎖
使數據線程安全的最簡單方法是把它套上一把大鎖。為簡單起見,所有的東西都用同一把鎖。這種方法會有一個問題:序列化。為了得到鎖,每一個要處理數據的線程都必須排隊等候。如果線程被一把鎖阻塞,它沒有在做任何有用的事。當服務器的負載較輕時,這個問題并不常見,因為一次可能只有一個線程需要鎖。在負載很重的情況下,對鎖的激烈爭奪可能就會成為一個大問題。
設想在多車道高速公路上發生了一個意外事故,這條高速公路上的所有車輛都被轉向一條狹窄的道路。如果車輛很少,這一轉換對交通流的速率的影響可以忽略。如果車輛很多,當車輛慢慢并入那條單通道時,交通阻塞會延伸幾英里。
有幾種技術能夠減少鎖競爭。
· 不要過分保護,也就是說,不是非常必要不要鎖住數據。只有需要時才去持有鎖,而且時間不要過長。不要在大段代碼周圍或頻繁執行的代碼中沒必要地使用鎖,這一點很重要。
· 對數據進行分割,使它能夠用一套獨立的鎖保護。例如,一個符號表可以按標識符的第一個字母分割,這樣在修改名字以Q開頭的符號的值時,就不會去讀名字以H開頭的符號的值。
· 使用APIs的Interlocked 系列(InterlockedIncrement,InterlockedCompareExchangePointer等)自動修改數據而不需要鎖。
· 當數據不是經常被修改時可以使用多讀者/單作者(multi-reader/single-writer)鎖。你將獲得更好的并發性,盡管鎖操作的代價將更高并且你可能會冒餓死作者的危險。
· 在關鍵部分使用循環計數器。參見Windows NT 4.0 service pack 3中的SetCriticalSectionSpinCount API。
· 如果你不能得到鎖,使用TryEnterCriticalSection并做一些其他的有用的工作。
高競爭導致serialization,serialization導致降低CPU的利用率,這促使用戶加入更多的線程,結果事情變得更糟。
不必注意多處理器機器
你的代碼在多處理器系統上比在單處理器系統上運行得還要糟,這可能是件令人惡心的事。一個很自然的想法是,在一個N維系統上運行N次會更好。性能很差的原因是競爭:鎖競爭,總線競爭,和/或緩存列競爭。處理器都在是爭奪共享資源的所有權,而不是做更多的工作。
如果你一定要編寫多線程應用程序的話,你應該在多處理器盒上對你的應用程序進行強度測試和性能測試。單處理器系統通過時間分片地執行線程而提供一個并發性的假象。多處理器盒具有真正的并發性,競爭環境和競爭更容易發生。
應該始終使用模塊化調用;他們很有趣。
利用同步模塊化調用來執行I/O操作對大多數桌面應用程序來說是合適的。但是,他們不是使用服務器上的CPU(s)的好方法。I/O操作要花費上百萬個時鐘周期來完成,這些時鐘周期本來可以被更好地利用。利用異步I/O你能得到顯著提高的用戶請求率和I/O通量,不過增加了額外的復雜性。
如果你有需要花費很長時間的模塊化調用或I/O操作,你應該考調撥多少資源給他們。你想使用所有的線程還是有個限制?一般地,使用有限的幾個線程要好些。構建一個小的線程池和隊列,利用隊列來安排線程的工作完成模塊化調用。這樣,其他線程就可以拾取和處理你的非模塊化的請求。
不要進行測量
當你能夠測量你所談論的事情并用數字表達它時,這就表示你對他有了一定的了解;但是如果你不能用數字表達時,你的知識是貧瘠的不能令人滿意的;這可能是知識的開始,但這時你簡直不可能將你的思想提高到科學的水平。
- Lord Kelvin (William Thomson)
如果不測量你就不能了解應用程序的特性。你在黑暗中摸索,一半是靠猜測。如果不識別性能問題,你就不能做任何改進或做出工作量計劃。
測量包括黑匣子測量和profiling。黑匣子測量的意思是收集由性能計數器(內存使用,上下文交換,CPU利用等)和外部檢測工具(通量,反映時間等)所顯示的數據。為了profile你的代碼,你編譯代碼的一個工具版,然后在各種條件下運行它,并收集關于執行時間和過程調用頻率的統計數據。
測量如果不用于分析的話就一點用都沒有。測量將不僅告訴你有問題,而且甚至能幫助你找到問題發生在哪,但它不能告訴你為什么會有問題。對問題進行分析以便你能正確地改正他們。要從根本上解決問題而不是停留在表面現象。
當你進行改動后,要重新測量。你要知道你的改動是否有效。改動也可能會暴露其他性能問題,測量-分析-改正-再測量的循環就會重新開始。你也必須要有規律地進行測量,以便發現性能衰退問題。
應該使用單一用戶,單一請求的測試方法。
書寫ASP和ISAPI應用程序的一個通病是只用一個瀏覽器去測試應用程序。當他們在Internet上應用他們的程序時,他們才發現他們的應用程序不能處理高負載,并且通量和反應時間另人可憐。
用一個瀏覽器測試是必要的但是不夠的。如果瀏覽器反應得不夠快,你就知道你有麻煩了。但即使它在使用一個瀏覽器時很快,你也不知道它處理負載的能力如何。如果十幾個用戶同時請求會發生什么事?一百個呢?你的應用程序能容忍什么樣的通量?它能提供什么樣的反應時間?在輕載時這些數字會怎樣?中等負載呢?重載呢?在多處理器機器上你的應用程序會如何?對你的應用程序進行強度測試,這對于找出bugs發現性能問題來說是基本的。
類似的負載測試考慮適用于所有的服務器應用程序。
不應使用實際環境。
人們往往只在幾個特定的,人工的環境(如下benchmarks)下調整應用程序。選擇和實際情況相對應的各種情況,并為針對各種操作進行優化,這一點很重要。如果你不這樣做,你的用戶和評論家一定會這樣做,并且他們將依此來評判你的應用程序的好壞。
結論
自從我們開始開發IIS以來,我們已經對扼殺服務器性能和伸縮性有了一定的了解。書寫高性能的服務器應用程序是不容易的。除了在書寫桌面應用程序時遇到的傳統問題外,你必須特別注意內存分配,緩存列,緩存數據,線程原型化,加鎖策略,多處理器機器,模塊化調用,測量和分析,多用戶測試,和實際環境的問題。這些問題可能會造就你也可能毀了你。
祝你好運!
George V. Reilly在IIS開發小組工作,他負責IIS和ASP的性能問題。自從1982年他在本國Ireland的Dublin發明BBC Micro后,他一直在致力于編寫嚴禁的代碼;叵肫饋,他幾乎不相信自己有足夠的時間合寫了Beginning ATL COM Programming ,Wrox Press, 1998。
本文是受我的同事Murali R. Krishnan的Top Ten Killers of Server Performance列表啟發而寫成的。
延伸閱讀
文章來源于領測軟件測試網 http://www.kjueaiud.com/