摘要:1 無所不在,API 的空間視角 2 良好與糟糕,API 的真面目 3 API 設計的經驗性原則 3.1 功能的完整性 3.2 調用的簡單性 3.3 設計的場景化 3.4 有無策略性的設置 3.5 面向用戶的設計 3.6 推卸責任源於無知 3.7 清晰的文檔化 3.8 API的人體工程學 4 性能約定,API的時間視角 4.1 API的性能分類 4.2 按性能規劃API 4.3 API的性能變化 4.4 API調用失敗時的性能 5 確保API 性能的經驗性方法 5.1 謹慎地選擇API 和程序結構 5.2 在新版本發佈時提供一致的性能約定 5.3 防禦性編程可以提供幫助 5.4 API 公開的參數調優 5.5 測量性能以驗證假設 5.6 使用日誌檢測和記錄異常 6 面對API,開發者的苦惱 6.1 沒有 API 6.2 繁瑣的註冊 6.3 多收費的API 6.4 隱藏 API 文檔 6.5 糟糕的私有協議 6.6 單一的 API 密鑰 6.7 手動維護文檔 6.8 忽略運維環境 6.9 非冪等性 7 API 設計中的文化認知 7.1 API意識的訓練 7.2 API設計人才的流失 7.3 開放與控制。當然,平臺的底層速度(硬件和操作系統)會有所不同,但是庫接口可能會導致 API 內函數的性能或 API 間的性能變化。

即便做了20多年的軟件開發,仍然發現自己經常會低估完成一個特定的編程任務所需要的時間。有時,錯誤的時間表是由於自己的能力不足造成的: 當深入研究一個問題時,會發現它比最初想象的要難得多,因此解決這個問題需要更長的時間ーー這就是程序員的生活。

即使自己清楚地知道想要實現什麼以及如何實現它,還會經常比預期的要花費更多的時間,這種情況往往因爲與API的糾纏。

目錄

1 無所不在,API 的空間視角
2 良好與糟糕,API 的真面目
3 API 設計的經驗性原則
 3.1 功能的完整性
 3.2 調用的簡單性
 3.3 設計的場景化
 3.4 有無策略性的設置
 3.5 面向用戶的設計
 3.6 推卸責任源於無知
 3.7 清晰的文檔化
 3.8 API的人體工程學
4 性能約定,API的時間視角
 4.1 API的性能分類
 4.2 按性能規劃API
 4.3 API的性能變化
 4.4 API調用失敗時的性能
5 確保API 性能的經驗性方法
 5.1 謹慎地選擇API 和程序結構
 5.2 在新版本發佈時提供一致的性能約定
 5.3 防禦性編程可以提供幫助
 5.4  API 公開的參數調優
 5.5 測量性能以驗證假設
 5.6 使用日誌檢測和記錄異常
6 面對API,開發者的苦惱
 6.1 沒有 API
 6.2 繁瑣的註冊
 6.3 多收費的API
 6.4 隱藏 API 文檔
 6.5 糟糕的私有協議
 6.6 單一的 API 密鑰
 6.7 手動維護文檔
 6.8 忽略運維環境
 6.9 非冪等性
7 API 設計中的文化認知
 7.1 API意識的訓練
 7.2 API設計人才的流失
 7.3 開放與控制

個人認爲,現在所普遍使用的API 與二十年前C語言編寫的API 並沒有本質的不同。關於API的設計,似乎有些難以捉摸的東西。

API(Application Programming Interface,應用編程接口)是一些預先定義的函數,或軟件系統不同組成部分之間的銜接約定。API 提供了基於軟件或硬件得以訪問一組例程的能力,而無需使用源代碼,也無需理解其內部的工作機制。

1 無所不在,API 的空間視角

一看到API,很多人首先想到的是Restful API,基於HTTP協議的網絡編程接口。實際上, API的概念外延很大,從微觀到宏觀,會發現API在計算機的世界裏無處不在。

拿起顯微鏡,如果Rest API 面向的是網絡通信,可以想把空間限制在單機上。一臺主機上的IPC同樣由各種各樣的API組成,而一切代碼的執行都會歸結到系統調用上來,操作系統提供的系統調用同樣是API。走進操作系統,走進函數指針的API,調整顯微鏡的鏡頭,API 可能體現在Jump 指令上,在深入就會進入電路與系統的領域了。

抬起望遠鏡,感謝通信與網絡技術的發展,一切軟件都幾乎演變成了分佈式系統。API 成爲了分佈式系統中的血管和關節,Restful API 只是 RPC的一種。節點內子系統之間的API通信往往形成了東西流量,節點間子系統之間的API通信形成了南北流量。調整顯微鏡的鏡頭,這個系統通過開放平臺提供了API,逐漸形成了生態系統。生態系統間的異構API正在隨着網絡世界的延伸而形成新的世界。

從空間的視角來看,計算機的世界幾乎就是通過API連接的世界。

2 良好與糟糕,API 的面目

好的 API給我們帶來樂趣,幾乎可以忽略他們的存在,它們能給對一個特定任務在合理的時間內完成,可以很容易地被發現和記憶,有良好的文檔記錄,有一個直觀的界面使用,並能夠正確處理邊界條件。

然而,對於每一種正確設計 API 的方法,通常都有幾十種不正確的設計方法。簡單地說,創建一個糟糕的 API 非常容易,而創建一個好的 API 則比較困難。即使是很小很簡單的設計缺陷也有被誇大的傾向,因爲 API會被多次調用。設計缺陷會導致代碼笨拙或效率低下,那麼在調用 API 的每個點都會出現由此產生的問題。此外,一個設計缺陷在孤立的情況下是微小的,但通過相互作用的方式具有驚人的破壞性,並迅速導致大量的間接傷害。

對於糟糕的 API 設計,後果衆多且嚴重,難於編程,通常需要編寫額外的代碼。如果沒有別的原因,這些額外的代碼會使程序變大,效率更低,因爲每一行不必要的代碼都會增加工作集合的大小,並可能減少 CPU 緩存的命中率。此外,糟糕的API還可能會強制進行不必要的數據複製,或者對預期的結果拋出異常,從本質上產生效率低下的代碼。糟糕的API更難理解,也更難使用。面對糟糕的API,程序員編寫代碼的時間會更長,直接導致了開發成本的增加。

舉個例子,如果一個設計不當的 API 會導致Microsoft PowerPoint常常崩潰的話,那可能會有海量用戶受到影響。類似地,筆者見過的大量安全漏洞都是由標準 c 庫(如 strcpy)中不安全的IO操作或者字符串操作造成的。這些設計缺陷的API,造成的累積成本很容易達到數十億資金。

從開發的視角看API的話,大多數軟件開發都是關於不同層次的抽象,而API是這些抽象的可見接口。抽象減少了複雜性, 應用程序調用較高級別的庫,通常再調用較低級別的庫提供的服務來實現,而較低級別的庫又調用操作系統的系統調用接口提供的服務。這種抽象層次結構非常強大,沒有它,咱們所知道的軟件就可能不存在,程序員會被複雜性完全壓垮。

有悖常理的是,抽象層常常被用來淡化糟糕 API 的影響: “這不重要,我們可以編寫一個API來隱藏問題。” 這種說法大錯特錯,首先,就內存和執行速度而言,即使是效率最高的API封裝也會增加一些成本,其次,本來就是設計良好的API的份內工作, 通常情況下,API 封裝會產生一系列的問題。因此,儘管API的封裝可以是糟糕的API可用,這並不意味着這個糟糕的API無關緊要,這裏沒有“負負得正”,不必要的API封裝只會導致軟件更加臃腫。

3 API 設計的經驗性原則

有些時候,自己新的認知可能只是別人的常識。在設計 API 的 時候 ,有一些經驗性原則可以使用。 很遺憾。 仍然無法提煉到方法論的高度。

3.1 功能的完整性

API要提供完整的功能,這似乎是顯而易見的,提供不足功能的 API 是不完整的。在設計過程中仔細檢查一個功能清單,不斷地問自己: “是否有遺漏呢?”

3.2 調用的簡單性

API 使用的類型、函數和參數越少,學習、記憶和正確使用就越容易。許多 API最終成爲了助手函數的組合器,C+ + 標準字符串類及其超過100個的成員函數就是一個例子。在使用 C + + 編程多年之後,自己仍然無法在不查閱使用手冊的情況下使用標準字符串來處理任何重要的事情。

爲了很好地設計 API,設計人員必須瞭解使用 API 的環境,並對該環境進行設計。是否提供一個非基本的便利函數取決於設計者預期這個API被使用的頻率。如果頻繁使用,就值得添加。對 API 向下兼容的擔憂隨着時間的推移而侵蝕 API , API 累積起來最終造成的損害比它保持向後兼容所帶來的好處還要大。

3.3 設計的場景化

考慮一個類,它提供了對一組字符串鍵值對的訪問,比如環境變量:

class KVPairs { public string lookup(string name); // ... }

lookup方法提供了對命名變量值的訪問。顯然,如果設置了具有給定名稱的變量,函數將返回其值。但是,如果沒有設置變量,函數應該如何運行?可能有幾種選擇:

  • 拋出 VariableNotSet 異常

  • 返回 null

  • 返回空字符串

如果預期查找一個不存在的變量不是一個常見的情況,並且可能視爲一個錯誤,那麼拋出異常是適當的。異常會迫使調用方處理錯誤。另一種情況,調用者可能查找一個變量,如果沒有則替換一個默認值。如果是這樣的話,拋出異常完全是錯誤的,因爲處理異常會破壞正常的控制流,而且比測試 null 或空返回值更困難。

假設如果沒有設置變量時不拋出異常,有兩個方式表明查找失敗: 返回 null 或空字符串。哪一個是更合適呢?同樣,答案取決於預期的場景用例。返回 null 允許調用者區分沒有設置的變量和設置爲空字符串的變量,而返回未設置的變量的空字符串使得不可能區分從未設置的變量和顯式設置爲空字符串的變量。如果認爲能夠進行這種區分很重要,那麼返回 null 是必要的; 但是,如果這種區分不重要,那麼最好返回空字符串並且永遠不返回 null。

3.4 有無策略性的設置

API 設置策略的程度對其可用性有着深遠的影響 ,只有當調用者對 API 的使用與設計者預期的場景相一致時,API 才能最優地執行。如果對將要使用的 API 場景知之甚少,那麼只能保持所有選項的開放性,並允許 API 儘可能廣泛地應用。

實際上,什麼應該是錯誤和什麼不應該是錯誤之間的界限非常細微,而且錯誤地快速放置這個界限會導致更大的痛苦。對 API 的背景瞭解得越多,它可以制定的策略就越多。這樣做對調用方有利,因爲它捕獲了錯誤,否則就無法檢測到這些錯誤。通過仔細設計類型和參數,通常可以在編譯時捕獲錯誤,而不是延遲到運行時。在編譯時捕獲的每個錯誤都減少了一個錯誤,這個錯誤可能會在測試期間或生產環境中產生額外的成本。

通常情況下,人們對上下文知之甚少,因爲 API 是低級的,或者就其本質而言,必須在許多不同的上下文中工作。在這種情況下,策略模式往往可以用來取得良好的效果。它允許調用者提供策略,從而保持設計的開放性。根據編程語言的不同,調用方提供的策略可以使用回調、虛函數、代理或模板等來實現。如果 API 提供了合理的缺省值,那麼這種外部化的策略可以在不損害可用性和清晰性的情況下帶來更大的靈活性。

3.5 面向用戶的設計

程序員很容易進入解決問題的模式: 需要什麼數據結構和算法來完成這項工作,需要什麼輸入和輸出來完成這項工作?實現者專注於解決問題,而調用者的關注點很快被忘記。

獲得可用 API 的一個好方法是讓調用者編寫函數名,並將該API簽名交給程序員來實現。僅這一步就至少消除了一半糟糕的API,如果API的實現者從不使用他們自己的API,這對可用性會造成災難性的後果。此外,API 與編程、數據結構或算法無關,API 與 GUI 一樣,也是一個用戶界面。使用 API 的用戶是一個程序員,也就是一個人。儘管傾向於認爲API是機器接口,但它們不是: 它們是人機接口。

驅動 api 設計的不是實現者的需求,這意味着好的 api 是根據調用者的需求設計的,即使這會使實現者的工作變得更加複雜。

3.6 不推卸責任

“推卸責任”的一種方式是害怕設置策略: “好吧,調用者可能想要這樣或那樣做,但我不能確定是哪個,所以我會設置它。” 這種方法的典型結果是採用五個或十個參數的函數。因爲設計者沒有設置策略,也不清楚 API 應該做什麼和不應該做什麼,所以 API 最終的複雜性遠遠超過了必要的程度。一個好的 API 很清楚它想要實現什麼和不想要實現什麼,並且不害怕預先了解它。由此產生的簡單性通常可以彌補功能的輕微損失,特別是如果 API 具有良好的基本操作,可以很容易地組合成更復雜的操作。

另一種推卸責任的方式是犧牲可用性來提高效率。性能增益是一種錯覺,因爲它使調用者“幹髒活” ,而不是由 API 執行。換句話說,可以以零運行時開銷提供更安全的 API。通過僅對 API 內部完成的工作進行基準測試,而不是由調用方和 API 共同完成的任務 ,設計人員可以聲稱已經創建了性能更好的 API,是缺乏價值的。

即便是內核也不是沒有瑕疵,並且偶爾會推卸責任。某些系統調用會被中斷,迫使程序員明確處理並手動重啓被中斷的系統調用,而不是讓內核透明地處理。

推卸責任可以採取許多不同的形式,各種 API 的細節差別很大。對於設計者來說,關鍵的問題是: 有沒有什麼可以合理地爲調用者做的事情是我沒有做的?如果有,是否有正當理由不這樣做?明確地提出這些問題使得設計成爲一個有意識的過程,而不是“偶然的設計”。

3.7 清晰的文檔化

API 文檔的一個大問題是,它通常是在 API 實現之後編寫的,而且通常是由實現者編寫的。然而,實現者被實現所污染,並且傾向於簡單地寫下所做的事情。這通常會導致文檔不完整,因爲實現人員對 API 太熟悉了,並且假設有些事情是顯而易見的,而實際上並非如此。更糟糕的是,它經常導致API完全忽略重要的用例。另一方面,如果調用者編寫文檔,調用者可以從用戶的角度處理問題,不受當前實現的影響。這使得 API 更有可能滿足調用者的需求,並防止許多設計缺陷的出現。

最不適合編寫文檔的人是API的實現者,最不適合編寫文檔的時間是在實現之後。這樣做會增加接口、實現和文檔都出現問題的可能性。

確保文檔是完整的,特別是關於錯誤處理的文檔。當事情出錯時,API 的行爲是其中的一部分,也是事情進展順利時的一部分。文檔是否說明 API 是否維護異常?是否詳細說明了在出錯情況下輸出和輸入輸出參數的狀態?是否詳細說明了在錯誤發生後可能存在的任何副作用?是否爲調用者提供了足夠的信息來理解錯誤?程序員確實需要知道當出現錯誤時 API 的行爲,並且確實需要獲得詳細的錯誤信息,以便通過編程方式進行處理。

單元測試和系統測試對 API也有影響,因爲它們可以發現以前沒有人想到的東西。測試結果可以幫助改進文檔,從而改進 API,文檔是 API 的一部分。

3.8 API的人體工程學

人體工程學本身就是一個研究領域,也可能是 API 設計中最難確定的部分之一。關於這個主題,已經有了很多內容,例如定義命名約定、代碼佈局、文檔樣式等。除了單純的時尚問題,符合人體工程學的實現良好是困難的,因爲它提出了複雜的認知和心理問題。程序員是人,所以一個程序員認爲很好的 API 可能被另一個程序員認爲是一般的。

特別是對於大型和複雜的 api,人機工程學涉及到一致性的問題。例如,如果一個 API 總是以相同的順序放置特定類型的參數,那麼它就更容易使用。類似地,如果API建立命名規則,將相關函數與特定的命名風格組合在一起,那麼就更容易使用。同時, API 爲相關任務建立簡單統一的約定並使用統一的錯誤處理。

一致性不僅使事物更容易使用和記憶,而且還可以轉移學習。轉移學習不僅在API內部很重要,而且在API 之間也很重要。API之間可以採用的概念越多,就越容易掌握所有的概念。實際上,即便是Unix 中的標準IO庫也在許多地方違背了這一思想。例如,read ()和 write ()的系統調用將文件描述符放在第一個參數位置,但是標準庫中如 fgets ()和 fputs () ,將流指針放在最後,而 fscanf ()和 fprintf () 又將流指針放在第一個位置。這時候,往往要感謝IDE的代碼補全功能了。

4 性能約定,API的時間視角

在 API 中調用函數時,當然期望它們能夠正確工作,這種期望可以被稱爲調用方和實現之間的約定。同時,調用者對這些功能也有性能期望,軟件系統的成功通常也取決於滿足這些期望的 API。因此,除了正確性約定之外,還有性能約定。履行合同通常是隱含的,常常是模糊的,有時是被違反的(由調用者或執行者)。如何改進這方面的 API 設計和文檔?

當今任何重要的軟件系統都依賴於其他人的工作,通過API調用操作系統和各種軟件包中的函數,從而減少了必須編寫的代碼量。在某些情況下,要把工作外包給遠程服務器,這些服務器通過網絡與你連接。我們既依賴於這些函數和服務來實現正確的操作,也依賴於它們的執行性能以保證整個系統的性能。在涉及分頁、網絡延遲、共享資源(如磁盤)等的複雜系統中,性能必然會有變化。然而,即使是在簡單的設置中,比如在內存中包含所有程序和數據的獨立計算機中,當一個 API 或操作系統達不到性能預期時,也是令人沮喪的。

人們習慣於談論應用程序和 API 實現之間的功能約定。雖然如今的 API 規範並沒有以一種導致正確性證明的方式明確規定正確性的標準,但是 API 函數的類型聲明和文本文檔力求對其邏輯行爲毫不含糊。然而,API 函數的意義遠不止正確性。它消耗什麼資源,速度有多快?人們常常根據自己對某個函數的實現應該是什麼的判斷做出假設。遺憾的是,API 文檔沒有提示哪些函數有性能保證,哪些函數實際上代價高昂。

更復雜的是,當應用程序調整到 API 的性能特徵之後,一個新版本的 API 實現或者一個新的遠程存儲服務卻削減了軟件系統的整體性能。簡而言之,從時間的視角來看,API的性能契約值得更多關注。

4.1 API的性能分類

爲了實用有效,從計算複雜度來看,可以對API的性能做一個簡單的分類。

恆定的性能

例如 toupper, isdigit, java.util.HashMap.get等。前兩個函數總是計算廉價的,通常是內聯表查找。正確大小的哈希表查找應該也很快,但是哈希衝突可能會偶爾減慢訪問的速度。

通常的性能

例如fgetc, java.util.HashMap.put等。許多函數被設計成大多數時候都很快,但是偶爾需要調用更復雜的代碼; fgetc 必須偶爾讀取一個新的字符緩衝區。在哈希表中存儲一條新數據可能會使該表變滿,以至於會重對錶中所有條目進行哈希計算。

java.util.HashMap 在性能約定方面有一個很好的描述: “這個實現爲基本操作(get 和 put)提供了常量時間性能,假設散列函數將元素正確地分散在存儲桶中。對集合視圖的迭代,需要與 HashMap ‘容量‘成比例的時間...... “。fgetc 的性能取決於底層流的屬性。如果是磁盤文件,那麼該函數通常將從用戶內存緩衝區讀取,而不需要系統調用,但它必須偶爾調用操作系統來讀取新的緩衝區。如果是從鍵盤讀取輸入,那麼實現可能會調用操作系統來讀取每個字符。

程序員建立性能模型是基於經驗,而不是規範,並非所有函數都有明顯的性能屬性。

可預期的性能

例如 qsort, regexec等。這些函數的性能隨其參數的屬性(例如,要排序數組的大小或要搜索的字符串的長度)而變化。這些函數通常是數據結構或公共算法實用程序,使用衆所周知的算法,不需要系統調用。

我們通常可以根據對底層算法的期望來判斷性能(例如,排序將花費 nlogn 的時間)。當使用複雜的數據結構(例如 B 樹)或通用集合(在這些地方可能很難確定底層的具體實現)時,可能更難估計性能。重要的是,可預測性只是可能的; regexec 基於它的輸入通常是可預測的,但是有一些病態的表達會導致複雜計算的爆發。

未知的性能

例如fopen, fseek, pthread_create,很多初始化的函數以及所有網絡調用。這些函數的性能常常有很大的差異。它們從池(線程、內存、磁盤、操作系統對象)分配資源,通常需要對共享操作系統或 IO資源的獨佔訪問。通常需要大量的初始化工作, 通過網絡進行調用相對於本地訪問總是慢的,這使得合理性能模型的形成變得更加困難。

線程庫是性能問題的簡單標誌。Posix 標準花了很多年才穩定下來,然而如今仍然被性能問題所困擾。線程應用程序的可移植性仍然存在風險,原因是線程需要與操作系統緊密集成,幾乎所有操作系統(包括 Unix 和 Linux)最初設計時都沒有考慮到線程; 線程與其他庫的交互,爲了使線程安全而導致的性能問題等等。

4.2 按性能劃分API

有些庫提供了執行一個函數的多種方法,通常是因爲這些方法的性能差別很大。

大多數程序員被告知使用庫函數來獲取每個字符並不是最快的方法,更注重性能的人會讀取一個大型的字符數組,並使用編程語言中的數組或指針來操作提取每個字符。在極端情況下,應用程序可以將文件頁映射到內存頁,以避免將數據複製到數組中。作爲提高性能的副作用是,這些函數給調用方帶來了更大的負擔。例如,獲得緩衝區算法的正確性,調用 fseek需要調整緩衝區指針和可能的內容。

程序員總是被建議避免在程序中過早地進行優化,從而推遲修訂,直到更簡單的做法被證明是不滿足要求的。確定性能的唯一方法是測量。程序員通常在編寫完整個程序之後,纔會面對性能期望與所交付的實現之間的不匹配。

4.3 API的性能變化

可預測函數的性能可以根據其參數的屬性進行估計,未知函數的性能功能也可能因要求它們做什麼而有很大的不同。在存儲設備上打開流所需的時間當然取決於底層設備的訪問時間,或許還取決於數據傳輸的速率。通過網絡協議訪問的存儲可能特別昂貴,而且它是變化的。

由於各種原因,一般的函數隨着時間的推移變得越來越強大。I/O流就是一個很好的例子,根據打開的流類型(本地磁盤文件、網絡服務文件、管道、網絡流、內存中的字符串等) ,調用打開流在庫和操作系統中調用不一樣的代碼。隨着IO設備和文件類型範圍的擴展,性能的差異只會增加。大多數API的共同生命週期是隨着時間的推移逐步增加功能,從而不可避免地增加了性能變化。

一個很大的變化來源是不同平臺的庫接口之間的差異。當然,平臺的底層速度(硬件和操作系統)會有所不同,但是庫接口可能會導致 API 內函數的性能或 API 間的性能變化。有些庫(例如那些用於處理線程的庫)的移植性能差異非常大。線程異常可能以極端行爲的形式出現ーー極其緩慢的應用程序甚至是死鎖。

這些差異是難以建立精確的API性能約定的原因之一。我們往往不需要非常精確地瞭解性能,但是預期行爲的極端變化可能會導致問題。例如,使用 malloc ()函數的動態內存分配可以被描述爲“通常的性能” ,這將是錯誤的,因爲內存分配(尤其是 malloc)是程序員開始尋找性能問題時的首要嫌疑之一。作爲性能直覺的一部分,如果調用 malloc 數以萬計次,特別是爲了分配小的固定大小的塊,最好使用 malloc 分配一個更大的內存塊,將其分割成固定大小的塊,並管理自己的空閒塊列表。Malloc 的實現多年來一直在努力讓它變得高效,提供虛擬內存、線程和非常大的內存的系統都對malloc 和free構成了挑戰,必須權衡某些使用模式(如內存碎片)的效率和弊端。

一些軟件系統,如Java,使用自動內存分配和垃圾收集來管理空閒存儲。雖然這是一個很大的便利,但是一個關心性能的程序員必須意識到成本。例如,一個 Java 程序員應該儘早被告知 String 對象和 StringBuffer 對象之間的區別,String 對象只能通過在新內存中創建一個新的副本來修改,而 StringBuffer 對象包含容納字符串可以延長的空間。隨着垃圾收集系統的改進,它們使得不可預知的垃圾收集暫停變得不那麼常見; 這可能會讓程序員自滿,相信自動回收內存永遠不會成爲性能問題,而實際上這就是一個性能問題。

4.4 API調用失敗時的性能

API的規範包括了調用失敗時的行爲。返回錯誤代碼和拋出異常是告訴調用方函數未成功的常用方法。但是,與正常行爲的規範一樣,沒有指定故障的性能。主要有以下是三種形式:

  • 快速失敗。一個API調用很快就失敗了,和它的正常行爲一樣快或者更快。例如,調用 sqrt (- 1)會很快失敗。即使當一個 malloc 調用因爲沒有更多的內存可用而失敗時,這個調用也應該像任何 malloc 調用一樣快速地返回,因爲後者必須從操作系統請求更多的內存。爲了讀取一個不存在的磁盤文件而打開一個流的調用很可能與成功調用返回的速度一樣快。

  • 慢慢失敗。有時,一個API調用失敗的速度非常慢,以至於應用程序可能希望以其他方式進行。例如,打開到另一臺計算機的網絡連接請求只有在幾次長時間超時後纔會失敗。

  • 永遠失敗。有時候一個API調用只是暫停,根本不允許應用程序繼續運行。例如,其實現等待從未釋放的同步鎖的調用可能永遠不會返回。

對於失敗性能的直覺很少像對於正常性能的直覺那樣好。原因很簡單,編寫、調試和調優程序時處理故障事件的經驗遠遠少於處理普通事件。另一個原因是,API調用可能在許多方面出現故障,其中一些是致命的,而且並非所有的故障都在 API 規範中有所描述。即使是旨在更精確地描述錯誤處理的異常機制,也不能使所有可能的異常都可見。此外,隨着庫函數的增加,失敗的機會也在增加。例如,包裝網絡服務的API(ODBC、 JDBC、 UPnP等等)從本質上訂閱了大量的網絡故障機制。

一個勤奮的程序員會盡可能處理不可能的失敗。一種常見的技術是用 try... catch 塊包圍程序的大部分,這些塊可以重試失敗的整個部分。交互式程序可以嘗試保存用戶的工作,捕獲周圍的整個程序,其效果是減輕失敗的主程序造成的損失,例如保存在一個磁盤文件,關鍵日誌或數據結構等等。

處理暫停或死鎖的唯一方法可能是設置一個watchdog線程,該線程期望定期檢查一個正常運行的應用程序,如果健康檢查異常,watchdog就會採取行動,例如,保存狀態、中止主線程和重新啓動整個應用程序等。如果一個交互式程序通過調用可能緩慢失敗的函數來響應用戶的命令,它可能會使用watchdog終止整個命令,並返回到允許用戶繼續執行其他命令的已知狀態,這就產生了一種防禦性的編程風格。

5 確保API 性能的經驗性方法

程序員根據對 API 性能的期望選擇 API、數據結構和整個程序結構。如果預期或性能嚴重錯誤,程序員不能僅僅通過調優 API 調用來恢復,而必須重寫程序(可能是主要部分)。前面提到的交互式程序的防禦結構是另一個例子。

當然,有許多程序的結構和性能很少受到庫性能的影響(科學計算和大型模擬通常屬於這一類)。然而,今天的許多“常規 IT” ,特別是遍及基於 web 的服務的軟件,廣泛使用了對整體性能至關重要的庫。

即使性能上的微小變化也會導致用戶對程序的感知發生重大變化,在處理各種媒體的節目中尤其如此。偶爾放棄視頻流的幀可能是可接受的 ,但是用戶可以感知到音頻中哪怕是輕微的中斷,因此音頻媒體性能的微小變化可能會對整個節目的可接受性產生重大影響。這種擔憂引起了人們對服務質量的極大興趣,在許多方面,服務質量是爲了確保高水平的業績。

儘管違反性能契約的情況很少,而且很少是災難性的,但在使用軟件庫時注意性能可以幫助構建更健壯的軟件。以下是一些經驗性原則:

5.1 謹慎地選擇API 和程序結構

如果有幸從頭開始編寫一個程序,那麼在開始編寫程序時,要考慮一下性能約定的含義。如果這個程序一開始只是一個原型,然後在服務中保持一段時間,那麼毫無疑問它至少會被重寫一次; 重寫是一個重新思考 API 和結構選擇的機會。

5.2 在新版本發佈時提供一致的性能約定

一個新的實驗性 API 會吸引那些開始衍生 API 性能模型的用戶。此後,更改性能約定肯定會激怒開發人員,並可能導致他們重寫自己的程序。一旦 API 成熟,性能約定不變就很重要了。事實上,大多數通用 API (例如 libc)之所以變得如此,部分原因在於它們的性能約定在 API 發展過程中是穩定的。同樣的道理也適用於 api 端口

人們可能希望 API 提供者能夠定期測試新版本,以驗證它們沒有引入性能怪癖。不幸的是,這樣的測試很少進行。但是,這並不意味着不能對依賴的 API 部分進行自己的測試。使用分析器,通常可以發現程序依賴於少量的API。編寫一個性能測試工具,將一個API的新版本與早期版本的記錄性能進行比較,這樣可以給程序員提供一個早期預警警,即隨着API新版本的發佈,他們自己代碼的性能將發生變化。

許多程序員希望計算機及其軟件能夠一致地隨着時間的推移而變得更快。這實際上對於供應商來說是很難保證的,但是它們會讓客戶相信是這樣的。許多程序員希望圖形庫、驅動程序和硬件的新版本能夠提高所有圖形應用程序的性能,並熱衷於多種功能的改進,這通常會降低舊功能的性能,哪怕只是輕微的降低。

我們還可以希望 API 規範將性能約定明確化,這樣使用、修改或移植代碼的人就可以遵守約定。注意,函數對動態內存分配的使用,無論是隱式的還是自動的,都應該是API文檔的一部分。

5.3 防禦性編程可以提供幫助

在調用性能未知或高度可變的 API 時,程序員可以使用特殊的方式,對於考慮故障性能的情況尤其如此。我們可以將初始化移到性能關鍵區域之外,並嘗試預加載API 可能使用的任何緩存數據(例如字體)。表現出大量性能差異或擁有大量內部緩存數據的 API ,可以通過提供幫助函數將關於如何分配或初始化這些結構的提示從應用程序傳遞給 API。某個程序偶爾會向服務器發出 ping 信號,這可以建立一個可能不可用的服務器列表,從而避免一些長時間的故障暫停。

5.4 API 公開的參數調優

有些庫提供了影響性能的明確方法(例如,控制分配給文件的緩衝區的大小、表的初始大小或緩存的大小),操作系統還提供了調優選項。調整這些參數可以在性能約定的範圍內提高性能,調優不能解決總體問題,但可以減少嵌入在庫中的固定選項,這些選項會嚴重影響性能。

有些庫提供了具有相同語義函數的替代實現,通常是通用API的具體實現形式。通過選擇最好的具體實現進行調優通常非常容易,Java 集合就是這種結構的一個很好的例子。

越來越多的API被設計成動態地適應應用,使程序員無需選擇最佳的參數設置。如果一個散列表太滿,它會自動擴展和重新哈希(這是一種優點,但偶爾會降低性能)。如果文件是按順序讀取的,那麼可以分配更多的緩衝區,以便在更大的塊中讀取。

5.5 測量性能以驗證假設

常見方式是檢測關鍵數據結構,以確定每個結構是否正確使用。例如,可以測量哈希表的完整程度或發生哈希衝突的頻率。或者,可以驗證一個以寫性能爲代價的快速讀取結構實際上被讀取的次數多於被寫入的次數。

添加足夠的工具來準確地度量許多 API 調用的性能是困難的,這需要大量的工作,而且可能投入產出比較低。然而,在那些對應用程序的性能至關重要的 API 調用上添加工具(假設能夠識別它們,並且正確的識別) ,就可以在出現問題時節省大量時間。注意,這些代碼中的大部分可以在新版本的性能監視器中重用。

所有這些都不是爲了阻止完美主義者開發自動化儀表盤和測量的工具,或者開發詳細說明性能約定的方法,以便性能測量能夠建立對性能約定的遵守。這些目標並不容易實現,回報可能也不會很大。

通常可以在不事先檢測軟件的情況下進行性能度量,優點是在出現需要跟蹤的問題之前不需要任何工作還可以幫助診斷當修改代碼或庫影響性能時出現的問題。定期進行概要分析,從可信賴的基礎上衡量性能偏差。

5.6 使用日誌檢測和記錄異常

當分佈式服務組成一個複雜的系統時,會出現越來越多的違反性能約定的行爲。注意,通過網絡接口提供的服務有時具有指定可接受性能的SLA。在許多配置中,度量過程偶爾會發出服務請求,以檢查 SLA 是否滿足 。由於這些服務使用類似於 API 調用的方法(例如,遠程過程調用或其變體,如 XML-RPC、 SOAP 或 REST),因此可能是有性能約定的期望。應用程序會檢測這些服務的失敗,並且通常會應對得當。

然而,響應緩慢,特別是當有許多這樣的服務互相依賴時,可能會非常快地破壞系統性能。如果這些服務的客戶能夠記錄他們所期望的性能,並生成有助於診斷問題的日誌(這就是 syslog 的用途之一) ,那將會很有幫助。當文件備份看起來不合理的慢,那是不是比昨天慢呢?比最新的操作系統軟件更新之前還要慢?考慮到多臺計算機可能共享的備份設備,它是否比預期的要慢?或者是否有一些合理的解釋(例如,備份系統發現一個損壞的數據結構並開始一個長的過程來重新構建它) ?

在沒有源代碼,也沒有構成組合的模塊和API的細節的情況下,診斷不透明軟件組合中的性能問題可以在報告性能和發現問題方面發揮作用。雖然不能在軟件內部解決性能問題 ,但是可以對操作系統和網絡進行調整或修復。如果備份設備由於磁盤幾乎已滿而速度較慢,那麼肯定可以添加更多的磁盤空間。好的日誌和相關的工具會有所幫助; 遺憾的是,日誌在計算機系統演進中可能是一個被低估和忽視的領域。

誠然,性能約定沒有功能正確性約定那麼重要,但是軟件系統的重要用戶體驗幾乎都取決於它。

6 面對API,開發者的苦惱

對於向外部提供的API,有一些因素成爲了開發者的苦惱。

6.1 沒有 API

API 允許客戶實現你沒有想到的功能,允許客戶更多的使用產品。如果存在一個API,開發者可以自動使用API的產品,這將產生更多的應用。他們可以自動化整個公司的配置,可以基於你的 API 構建全新的應用程序。只要想想他們能夠通過 API 使用多少產品就可以了。

6.2 繁瑣的註冊

只有複雜的註冊過程才能保證API的安全麼?實際上只是自尋煩惱。要麼使整個過程完全自助服務,要麼根本不需要任何類型的註冊過程,這樣纔是良好的API使用開端

6.3 多收費的API

對服務收費是正常的,或者只在“企業版”中包含 API 訪問 。讓 API 訪問變得如此昂貴,以至於銷售部門認爲 API 代表額外的利潤激勵。事實上,API 不應該成爲一種收入來源,而是一種鼓勵人們使用產品的方式。

6.4 隱藏 API 文檔

沒有什麼比在搜索引擎中看不到API 文檔更糟糕的事了。那些將API文檔放在登錄之後的體驗屏幕後面,可以認爲是設計人員的大腦短路。通過某種註冊或登錄來阻止競爭對手查看API 並從中學習,這是一種幼稚的想法。

6.5 糟糕的私有協議

一個私有協議可能很難理解,也不可能調試。SOAP可能會變得臃腫和過於冗長,從而導致帶寬消耗和速度減慢。它也是基於 XML 的,尤其是在移動或嵌入式客戶端上,解析和操作起來非常昂貴。許多 API 使用 JSON API 或 JSON-rpc ,它們是輕量級的,易於使用,易於調試。

6.6 單一的 API 密鑰

如果只允許使用一個 API 密鑰,相當於創建了一個“第22條軍規”的情況。開發者無法更改服務器上的 API 密鑰,因爲客戶端也會在更新之前失去了訪問權限。他們也不能首先更改客戶端,因爲服務器還不知道新的 API 密鑰。如果有多個客戶端,那基本上就是一場災難。

API密鑰基本上就是用於識別和驗證客戶的密碼。也許是密鑰泄露,也許某個員工離開了公司帶走了鑰匙,或許每年輪換密鑰是安全策略的一部分,最終,開發者都將需要更改他們的 API 密鑰。

6.7 手動維護文檔

隨着 API 的發展,API 和文檔有可能脫離同步。一個錯誤的API說明會導致一個無法工作的系統,會令人極其的沮喪。一個與文檔不同步的 API,會讓人束手無策。

6.8 忽略運維環境

將基礎設施視爲代碼的能力正在成爲運維團隊的要事。它不僅使操作更容易、更可測試和更可靠,而且還爲諸如(支付行業所要求的安全性最佳實踐鋪平了道路。如果忽略了Ansible, Chef, Puppet等類似的系統,可能會導致一系列令人困惑的不兼容選項,使得生產環境的API調用難以爲繼。

6.9 非冪等性

假設有一個創建虛擬機的 API 調用。如果這個 API 調用是冪等的,那麼我們第一次調用它的時候就創建了 VM。第二次調用它時,系統檢測到 VM 已經存在,並簡單地返回,沒有錯誤。如果這個 API 調用是非冪等的,那麼調用10次就會創建10個 vm。

爲什麼有人要多次調用同一個 API?在處理 rpc時,響應可能是成功、失敗或根本沒有應答。如果沒有收到服務器的回覆,則必須重試請求。使用冪等協議,可以簡單地重發請求。對於非冪等協議,每個操作後都必須跟隨發現當前狀態並進行正確恢復的代碼,將所有恢復邏輯放在客戶機中是一種醜陋的設計。將此邏輯放在客戶機庫中可以確保客戶端需要更頻繁的更新,要求用戶實現恢復邏輯是令人難受的。

當 API 是冪等的時候,這些問題就會減少或消除。

如果網絡是不可靠的,那麼網絡 API 本質上也是不可靠的。請求可能在發送到服務器的途中丟失,而且永遠不會執行。執行可能已經完成,但是回覆的信息已經丟失了。服務器可能在操作期間重新啓動。客戶端可能在發送請求時重新啓動,在等待請求時重新啓動,或者在接收請求時重新啓動,在本地狀態存儲到數據庫之前重新啓動。在分佈式計算中,一切都有可能失敗。

程序員們以做好工作和完善的系統給用戶留下深刻印象而自豪,令開發者苦惱的API往往出於無知、缺乏資源或者不可能的最後期限。

7 API 設計中的文化認知

如果讓API 的設計可以做得更好的話,除了一些細節性的技術問題外,還可能需要解決一些文化問題。我們不僅需要技術智慧,還需要改變我們認識和實踐軟件工程的方式。

7.1 API的有意識訓練

自己唸書的時候,程序員的培訓主要側重於數據結構和算法。這意味着一個稱職的程序員必須知道如何編寫各種數據結構並有效地操作它們。隨着開源運動的發展,這一切都發生了巨大的變化。如今,幾乎所有可以想象到的可重用功能都可以使用開放源碼。因此,創建軟件的過程發生了很大的變化,今天的軟件工程不是創建功能,而是集成現有的功能或者以某種方式重新封它。換句話說,現在的 API 設計比20年前更加重要,不僅擁有了更多的 API,而且這些 API 提供了比以前更加豐富且複雜的功能。

從來沒有人費心去解釋如何決定某個值應該是返回值還是輸出參數,如何在引發異常和返回錯誤代碼之間做出選擇,或者如何決定一個函數修改它的參數是否合適。所以,期望程序員擅長一些他們從未學過的東西是不合理的。

然而,好的 API 設計,即使很複雜,也是可以訓練的,關鍵是認識到重要性並有意識的訓練。

7.2 API設計人才的流失

一個老碼農環顧四周,才發現周圍是多麼的不尋常: 所有的編程同事都比我年輕,當自己以前的同事或者同學,大多數人不再寫代碼; 他們轉到了不同的崗位比如各種經理、總監、CXO ,或者完全離開了這個行業。這種趨勢在軟件行業隨處可見: 年長的程序員很少,通常是因爲看不到職業生涯。如果不進入管理層,未來的加薪幾乎是不可能的。

一種觀點認爲,年長程序員的職業優勢在不斷喪失。這種想法可能是錯誤的: 年長的程序員可能不會像年輕人那樣熬夜,但這並不是因爲他們年紀大,而是因爲很多時候他們不用熬夜就能完成工作。

老程序員的流失是不幸的,特別是在 API 設計方面。雖然好的 API 設計是可以學習的,但是經驗是無法替代的。需要時間和不斷的挖坑填坑纔會做得更好。不幸的是,這個行業的趨勢恰恰是把最有經驗的人從編程中提拔出來。

另一個趨勢是公司將最好的程序員提升爲設計師或系統架構師。通常情況下,這些程序員作爲顧問外包給各種各樣的項目,目的是確保項目在正確的軌道上起步,避免在沒有顧問智慧的情況下犯錯誤。這種做法的意圖值得稱讚,但其結果通常是發人深省的: 顧問從來沒有經歷過自己的設計決策的後果,這是對設計的一種嘲諷。讓設計師保持敏銳和務實的方法是讓他們喫自己的狗糧, 剝奪設計師反饋的過程最終可能註定失敗。

7.3 開放與控制

隨着計算的重要性不斷增長,有一些API的正確功能幾乎是無法描述的。例如, Unix 系統調用接口、 C標準庫、 Win32或 OpenSSL。這些 API的接口或語義的任何改變都會帶來巨大的經濟成本,並可能引發漏洞。允許單個公司在沒有外部控制的情況下對如此關鍵的API進行更改是不負責任的。嚴格的立法控制和更開放的同行審查相結合,在兩者之間找到恰當的平衡對於計算機的未來和網絡經濟至關重要。

API設計確實很重要,因爲整個計算機世界都是由API連接的。然而,證明實現良好API所需的投入產出比可能是困難的,特別是當一個 API 沒有被客戶使用的時候。“當幾乎沒有人使用我們的 API 時,收益是多少? ” 產品經理或者老闆們經常可能會問這樣的問題。呵呵,也許你沒有做過這些事情,所以使用率很低。

【關聯閱讀】

淺析面向雲架構的SLA

服務可用性的一知半解

性能,10點系統性思考

分佈式系統的時間問題

淺談面向客戶端的性能優化

IoT的趨勢2020,見證智能音箱的發展

醉袖迎風受落花——好代碼的10條認知

紙上得來終覺淺——成長的10條...

關於軟件開發,都應該知道的10個常識

軟件架構的10個常見模式

讀書:《電路與系統簡史》

計算機網絡的元認知、實踐與未來

當你問代理機制的時候?指的是Agent,Proxy,Broker還是Delegate呢?

智能音箱場景下的性能優化

對開源的認知

阿里涉足零售IoT的猜想

一文弄清物聯網的OTA

令人激動的語音UI背後

老曹眼中的CRM 圖解

全棧的技術棧設想

相關文章