
如何快速實現(xiàn)REST API集成以優(yōu)化業(yè)務(wù)流程
人們習(xí)慣于談?wù)搼?yīng)用程序和 API 實現(xiàn)之間的功能約定,以便在調(diào)用 API 函數(shù)時得到正確的行為表現(xiàn)。調(diào)用方必須滿足某些初始要求,然后函數(shù)必須按照指定的要求執(zhí)行。雖然如今的 API 規(guī)范并沒有以一種正確性證明的方式來明確正確性的標(biāo)準(zhǔn),但是 API 函數(shù)/接口的類型聲明和文檔描述了其邏輯行為的確定性。
然而,API 函數(shù)/接口的意義不僅只有功能的正確性。它消耗了什么資源,速度有多快?人們常常根據(jù)自己對某個函數(shù)的實現(xiàn)做出假設(shè),對于任何復(fù)雜的API函數(shù)或者接口,不同的人可能會給出不同的性能預(yù)期,而API 文檔很少提示執(zhí)行成本高昂或者低廉。更復(fù)雜的是,當(dāng)我們將應(yīng)用程序針對API調(diào)整到 性能預(yù)期之后,新版本的 API 或者新的遠(yuǎn)程服務(wù)很可能會導(dǎo)致整體性能的變化,甚至?xí)?dǎo)致系統(tǒng)的崩潰。
因此,軟件系統(tǒng)中API的性能約定值得更多的關(guān)注。
先看一段C語言的代碼:
fs = fopen("~abel/mydata.txt", "r");
for ( i=0; i<10000; i++) {
ch = fgetc(fs);
//處理 ch
}
函數(shù)fopen的執(zhí)行預(yù)計需要一段時間,fgetc的執(zhí)行預(yù)計成本較低。這在直觀上是有意義的,為了處理一個文件,一個流只需要打開一次,但是“獲取下一個字符”函數(shù)將經(jīng)常被調(diào)用,也許會成千上萬次。這兩個流函數(shù)是由庫實現(xiàn)的,庫文檔清楚地說明了函數(shù)的功能,是函數(shù)的功能性約定。但沒有提到性能,也沒有向程序員暗示這兩個函數(shù)在性能上有著本質(zhì)的不同。因此,我們基于經(jīng)驗判斷性能,而不是規(guī)范。
并非所有函數(shù)都有明顯的性能屬性,例如,fseek(fs, ptr, SEEK_SET);
當(dāng)目標(biāo)文件的數(shù)據(jù)已經(jīng)在緩沖區(qū)里時,這個函數(shù)可能性能很好。在一般情況下,它將涉及一個操作系統(tǒng)的調(diào)用,也許還包括 I/O操作。在冷存儲的極端條件下,這個API的執(zhí)行可能需要卷動上千米的磁帶。即使在簡單的情況下,這個函數(shù)也可能成本不低,具體的實現(xiàn)可能只是存儲指針,并設(shè)置一個標(biāo)記,這將在下一個讀取或?qū)懭氲牧髡{(diào)用上比較困難,從而導(dǎo)致性能的不確定性。
鑒于此,我們可以簡單根據(jù)經(jīng)驗對API的性能進行分類。
這類API函數(shù)的性能表現(xiàn)是恒定的,例如,isdigit 和toupper, 這兩個函數(shù)是性能恒定的。Java.util.HashMap.get在正常大小哈希表中的查找應(yīng)該很快,但是哈希沖突可能會偶爾減慢的訪問速度,類似的函數(shù)還有很多。
許多API函數(shù)被設(shè)計成大多數(shù)時候都很快,但是偶爾需要調(diào)用復(fù)雜的代碼,例如,java.util.HashMap.put 在哈希表中存儲一個新條目可能會超出當(dāng)前表的大小,以至于會整表放大并重新哈希所有條目。
java.util.HashMap 在公開API的性能約定方面是一個很好的例子: “這個實現(xiàn)為基本操作(get 和 put)提供了常量時間性能,假設(shè)哈希函數(shù)將元素正確地分散存儲桶中。對集合視圖的迭代需要與 HashMap 的‘容量’成比例的時間… ”
fgetc 的性能取決于底層流的屬性。如果是一個磁盤文件,那么該函數(shù)通常從用戶的內(nèi)存緩沖區(qū)讀取,而不需要操作系統(tǒng)調(diào)用,但它必須偶爾調(diào)用操作系統(tǒng)來讀取新的緩沖區(qū)。如果是從鍵盤讀取輸入,那么實現(xiàn)可能會調(diào)用操作系統(tǒng)來讀取每個字符。
一些函數(shù)的性能隨其參數(shù)的屬性而變化,例如,要排序的數(shù)組的大小或要搜索的字符串長度。這些函數(shù)通常是數(shù)據(jù)結(jié)構(gòu)或算法的實用程序,使用眾所周知的算法,不需要系統(tǒng)調(diào)用。我們通常可以根據(jù)對底層算法的期望來判斷性能,例如,qsort排序的平均計算復(fù)雜度是 nlog n。當(dāng)使用復(fù)雜的數(shù)據(jù)結(jié)構(gòu)例如 b 樹的變體等,在這些地方可能很難確定底層的具體實現(xiàn),可能更難估計性能。重要的是,可預(yù)測性可能只是可能的,例如 regexec 通常是可預(yù)測的,但是有一些變態(tài)的表達(dá)會導(dǎo)致指數(shù)時間的爆發(fā)。
像open、 fseek、 pthread_create、許多“初始化”函數(shù)以及任何遍歷網(wǎng)絡(luò)的調(diào)用,大多是成本未知的。這些函數(shù)的執(zhí)行成本較高,而且它們的性能常常有很大的差異。它們從池(線程、內(nèi)存、磁盤、操作系統(tǒng)對象)中分配資源,通常需要對操作系統(tǒng)或I/O 資源的獨占訪問,常需要大量的初始化工作。通過網(wǎng)絡(luò)的調(diào)用相對于本地訪問總是昂貴的,但是成本的差異可能更大,這使得性能模型的形成變得更加困難。
線程庫是性能問題的明顯標(biāo)志。Posix 標(biāo)準(zhǔn)花了很多年才穩(wěn)定下來,并且在實現(xiàn)仍然被各種問題所困擾,基于線程的應(yīng)用程序可移植性仍然存在風(fēng)險。線程難以使用的一些原因有:
(1)與操作系統(tǒng)緊密集成,幾乎所有操作系統(tǒng)(包括 Unix 和 Linux)最初設(shè)計時都沒有考慮到線程;
(2)與其他庫的交互,特別是保證線程安全而導(dǎo)致的性能問題;
(3)線程的實現(xiàn)不同,表現(xiàn)為輕量級和重量級。
有些庫提供了執(zhí)行一個函數(shù)的多種方法,通常是因為這些方法的性能差別很大。
對于API函數(shù)fgetc而言,大多數(shù)程序員被告知使用這個庫函數(shù)來獲取每個字符并不是最快的方法,注重性能的人會讀取一個大型的字符數(shù)組,并使用不同編程語言中的數(shù)組或指針操作提取每個字符。在極端情況下,應(yīng)用程序可以將文件頁映射到內(nèi)存頁,以避免將數(shù)據(jù)復(fù)制到數(shù)組中。例如fseek的調(diào)用,給調(diào)用方帶來了更大的負(fù)擔(dān)。
程序員總是被建議避免在程序中過早地進行優(yōu)化,從而推遲了對性能的修訂。確定性能的唯一方法就是衡量性能,通常先編寫整個程序,然后再面對性能預(yù)期與實際交付之間的不匹配。
“可預(yù)測成本”的API函數(shù)性能可以根據(jù)其參數(shù)的屬性進行估計,”成本未知”的API函數(shù)也可能因為要求它們做什么而有很大的不同。在存儲設(shè)備上打開流所需的時間當(dāng)然取決于底層設(shè)備的訪問時間,或許還取決于數(shù)據(jù)傳輸?shù)乃俾?。通過網(wǎng)絡(luò)協(xié)議訪問的存儲可能成本較高, 但也是可變的。
許多API函數(shù)只是在大多數(shù)時候成本較低,或者有一個低成本的預(yù)期。由于各種原因,具有“成本未知”的API函數(shù)可能表現(xiàn)出很大的性能差異,原因之一是函數(shù)蠕變 ,其中一般函數(shù)隨著時間的推移變得更加強大。I/O流就是一個很好的例子: 打開一個流會調(diào)用操作系統(tǒng)和庫中非常不同的代碼,這取決于流的類型(本地磁盤文件、網(wǎng)絡(luò)服務(wù)文件、管道、網(wǎng)絡(luò)流、內(nèi)存中的字符串等)。隨著 I/O設(shè)備和文件類型范圍的擴展,性能的差異只會增加。大多數(shù) API 有著相同的命運,隨著時間的推移逐步增加功能,不可避免地增加了性能變化。
另一個很大的變化來源是不同平臺間庫的移植差異。當(dāng)然,平臺的底層硬件和操作系統(tǒng)會有所不同,但是庫的移植可能會導(dǎo)致 API 內(nèi)的相對性能或 API 間性能的變化。對于一個初始的庫移植版本而言,存在許多性能問題并不罕見,這些問題都是逐步修復(fù)的。有些線程庫的移植性能差異非常大,線程異??赡芤詷O端的形式出現(xiàn),應(yīng)用程序可能會極其緩慢甚至是死鎖。
這些差異可能是難以建立API性能約定的原因,通常不需要精確地了解性能,但是需要根據(jù)預(yù)期行為的極端變化考慮可能會導(dǎo)致的問題。
API 的說明一般包括了調(diào)用失敗時的行為細(xì)節(jié)。返回錯誤代碼和拋出異常是告訴調(diào)用方API未執(zhí)行成功的常用方法。但是,與正常的API行為一樣,沒有指定故障發(fā)生時的性能。這里有三個典型的場景:
對于API調(diào)用失敗時的性能,在直覺上很少像對于正常調(diào)用時性能的直覺那樣好。原因之一是編寫、調(diào)試和調(diào)優(yōu)程序提供的處理故障事件的經(jīng)驗遠(yuǎn)遠(yuǎn)少于處理普通事件的經(jīng)驗。另一個原因是,API調(diào)用可能在許多方面出現(xiàn)故障,其中一些是致命的,而且并非所有的調(diào)研失敗都會在 API 規(guī)范中描述。即使是精確地描述了錯誤處理的異常機制,也不能使所有可能的異常都可見。此外,隨著庫功能的增加和增強,失敗的機會也在增加。例如,封裝了網(wǎng)絡(luò)服務(wù)的API (ODBC/JDBC/UPnP …)訂閱了大量的網(wǎng)絡(luò)故障機制。一個勤奮的程序員會盡量處理可能的調(diào)用失敗用例。一種常見的技術(shù)是用 try… catch 塊包圍程序的大部分,這些塊可以重試失敗的整個部分。
處理暫停或死鎖的唯一方法是設(shè)置一個看門狗線程,該線程期望一個正常運行的應(yīng)用程序定期向看門狗發(fā)送通知,說明“我仍在正常運行。”如果間隔的時間過長,看門狗就會采取行動,例如,保存狀態(tài)、中止主線程或者重新啟動整個應(yīng)用程序等。如果一個交互式程序調(diào)用可能緩慢失敗的API函數(shù)來響應(yīng)用戶的命令,可以使用看門狗終止整個命令,并返回到一個已知的狀態(tài),允許用戶繼續(xù)執(zhí)行其他命令。這就產(chǎn)生了一種防御式的編程風(fēng)格。
為什么 API 必須遵守性能約定呢?因為應(yīng)用程序的主要結(jié)構(gòu)可能取決于 API 是否遵守了這樣的性能約定。程序員根據(jù)性能期望選擇 API、數(shù)據(jù)結(jié)構(gòu)和整個程序結(jié)構(gòu)。如果預(yù)期或性能嚴(yán)重錯誤,程序員不能僅僅通過調(diào)優(yōu) API 調(diào)用來恢復(fù),而是必須重寫程序的主要部分。
實際上, 明確性能約定的程序較難與不遵守性能約定的APi相配合。當(dāng)然,有許多程序的結(jié)構(gòu)和性能很少受到庫性能的影響。然而,如今許多的“常規(guī) 業(yè)務(wù)程序”,特別是基于 web 服務(wù)的軟件,廣泛使用了對整體性能至關(guān)重要的庫。
即使性能上的微小變化也會導(dǎo)致用戶對程序的感知發(fā)生重大變化,在處理各種媒體的程序中尤其如此。偶事實上,比起允許幀速率滯后而言,而放棄視頻流的幀可能是可以接受的,但是人們可以檢測到音頻中的輕微中斷,因此音頻媒體性能的微小變化可能會產(chǎn)生重大影響。這種擔(dān)憂引起了人們對服務(wù)質(zhì)量概念的極大興趣,在許多方面,服務(wù)質(zhì)量是為了確保高性能。
盡管違反性能約定的情況較少,而且較少出現(xiàn)災(zāi)難性的事故,但在使用軟件庫時注意性能可以幫助我么生成更健壯的軟件。以下是一些關(guān)注點和使用策略。
如果我們有幸從頭開始編寫一個程序,那么在開始編寫時,最好考慮一下性能約定的含義。如果這個程序一開始是一個原型,然后在服務(wù)中保持一段時間,那么毫無疑問它至少會被重寫一次; 重寫是一個重新思考 API 和結(jié)構(gòu)選擇的機會。
一個新的實驗性 API 也會吸引某些用戶。此后,更改性能約定肯定會激怒開發(fā)人員,并可能導(dǎo)致他們重寫自己的程序。一旦 API 成熟,性能約定的不變性就很重要了。事實上,大多數(shù)通用 API (例如 libc)之所以變得如此,部分原因在于它們的性能約定在 其API 發(fā)展的過程中是穩(wěn)定的。
人們可能希望 API 的開發(fā)者能夠定期測試新版本,以驗證它們沒有引入性能衰退。不幸的是,這樣的測試很少進行。但是,這并不意味著我們不能對依賴的 API 進行自己的測試。使用分析器,通??梢园l(fā)現(xiàn)程序依賴的那些API。編寫一個性能測試套件,將一個庫的新版本與早期版本的性能記錄進行比較,這樣可以給程序員提供一個早期警告,隨著新庫的發(fā)布,他們自己代碼的性能將發(fā)生變化。
許多程序員希望計算機及其軟件能夠一致地隨著時間的推移而變得更快。也就是說,希望一個庫或一個計算機系統(tǒng)的每個新版本都能平等地提高所有 API 函數(shù)的性能,這實際上對于供應(yīng)商來說是很難保證的。許多用戶希望圖形庫、驅(qū)動程序和硬件的新版本能夠提高所有圖形應(yīng)用程序的性能,但他們同樣熱衷于多種功能的改進,這通常會降低舊功能的性能,可能只是輕微地降低。
人們也可以希望 API 規(guī)范將性能約定明確化,這樣在使用、修改或移植代碼的時候就能遵守約定。注意,函數(shù)對動態(tài)內(nèi)存分配的使用,無論是隱式的還是自動的,都應(yīng)該是API文檔的一部分。
在調(diào)用性能未知或高可變的 API 函數(shù)時,程序員可以使用特殊的注意事項,異常處理優(yōu)先。我們可以將初始化移到性能關(guān)鍵區(qū)域之外,并嘗試預(yù)熱 API 可能使用的任何緩存數(shù)據(jù)(例如字體)。對于表現(xiàn)出大量性能差異或擁有大量內(nèi)部緩存數(shù)據(jù)的 API 而言, 可以通過提供助手函數(shù)將關(guān)于如何分配或初始化這些結(jié)構(gòu)的提示從應(yīng)用程序傳遞給 API。健康檢測可以建立一個可能不可用的服務(wù)器列表,從而避免一些長時間的故障暫停。
有些庫提供了影響其API性能的明確方法,例如,分配給文件的緩沖區(qū)大小、表的初始大小或緩存的大小等。操作系統(tǒng)還提供了調(diào)優(yōu)選項,調(diào)整這些參數(shù)可以在性能約定的范圍內(nèi)提高性能。調(diào)優(yōu)雖然不能解決總體問題,但可以減少嵌入在庫中的固定選項,那些選項可能會嚴(yán)重影響性能。
有些庫提供具有相同語義函數(shù)的替代實現(xiàn),通過選擇最好的具體實現(xiàn)進行調(diào)優(yōu)會比較容易。Java Collection就是這種結(jié)構(gòu)的一個很好的例子。越來越多的 API被設(shè)計用于動態(tài)地適應(yīng)使用,使程序員無需選擇最佳的參數(shù)設(shè)置。如果一個哈希表滿了,它會自動擴展并重新哈希。如果一個文件是按順序讀取的,那么就可以分配更多的緩沖區(qū),以便在更大的塊中讀取。
定期進行概要分析,從可信賴的基礎(chǔ)上衡量性能偏差。
常見建議是檢測關(guān)鍵數(shù)據(jù)結(jié)構(gòu),以確定每個結(jié)構(gòu)是否正確使用。例如,可以測量哈希表的完整程度或發(fā)生哈希沖突的頻率?;蛘?,可以驗證一個以寫性能為代價而設(shè)計的快速讀取結(jié)構(gòu)。添加工具來準(zhǔn)確地度量許多 API 調(diào)用的性能是困難的,這需要大量的工作,而且可能不值得。然而,在那些對應(yīng)用程序的性能至關(guān)重要的 API 調(diào)用上添加工具 ,可以在出現(xiàn)問題時會節(jié)省大量時間。
所有這些都不是為了阻止開發(fā)自動化儀表和測量的工具,或者開發(fā)詳細(xì)說明性能約定的方法。這些目標(biāo)并不容易實現(xiàn),回報可能也不會很大。通常可以在沒有事先檢測軟件的情況下進行性能度量,例如,使用 DTrace等工具,優(yōu)點是在出現(xiàn)問題之前不需要任何工作。它們還可以幫助診斷當(dāng)修改代碼或庫影響性能時出現(xiàn)的問題。
當(dāng)分布式服務(wù)組成一個復(fù)雜的系統(tǒng)時,可能會出現(xiàn)越來越多的違反性能約定的行為。在許多配置中,度量過程偶爾會發(fā)出服務(wù)請求,以檢查 SLA 是否滿足由于這些服務(wù)對性能的要求,例如, XML-RPC、 SOAP 或 REST在網(wǎng)絡(luò)連接上的調(diào)用。應(yīng)用程序會檢測這些服務(wù)的失敗,并且通常會適應(yīng)得當(dāng)。然而,響應(yīng)緩慢,特別是當(dāng)有許多這樣的服務(wù)互相依賴時,會很快破壞系統(tǒng)性能。
如果這些服務(wù)的客戶端能夠記錄他們所期望的性能,并生成有助于診斷問題的日志 ,那將會很有幫助。當(dāng)你的文件備份看起來不合理的慢,那是不是比昨天慢呢?比操作系統(tǒng)更新之前還要慢?或者是否有一些合理的解釋,例如,備份系統(tǒng)發(fā)現(xiàn)一個損壞的數(shù)據(jù)結(jié)構(gòu)并開始一個長的過程來重新構(gòu)建它)?
診斷不透明軟件組合中的性能問題需要軟件在報告性能和發(fā)現(xiàn)問題方面發(fā)揮作用。雖然我們不能在軟件內(nèi)部解決性能問題 ,但可以對操作系統(tǒng)和網(wǎng)絡(luò)進行調(diào)整或修復(fù)。如果備份設(shè)備由于磁盤幾乎已滿而速度較慢,那么我們會斷定可以添加更多的磁盤空間。好的日志和相關(guān)的工具會有所幫助,日志在計算機系統(tǒng)演進中是一個被低估和忽視的領(lǐng)域,可以參考《日志分析的那些挑戰(zhàn)》和《全棧必備 Log日志》。
軟件系統(tǒng)依賴于各種獨立組件的組合來工作,這意味著它們以可接受的速度執(zhí)行所需的計算。靜態(tài)檢查是難保證系統(tǒng)的性能的,軟件工程實踐已經(jīng)開發(fā)出了測試組件和組合的方法,這些方法可以工作得非常好。每次應(yīng)用程序綁定到動態(tài)庫或在操作系統(tǒng)接口上時,都需要驗證組合的正確性和API的性能約定。
誠然,API的性能約定沒有功能正確性約定那么重要,但是軟件系統(tǒng)的核心體驗往往取決于它。
文章轉(zhuǎn)自微信公眾號@喔家ArchiSelf