預期狀態系統

語言: CN / TW / HK

在2021年12月份,我在 NDC Oslo 2021會議 上演講了“預期狀態:React,Kubernetes和控制理論有諸多相似之處”,本文是這次演講的文字版。

我想分享這些年學習了多個技術棧之後遇到的一種特定型別的抽象。這是一種在計算機的多個領域一次次出現的模型,從UI工程到基礎架構管理,資料庫,程式語言理論等等。

因為缺少更好的術語,我稱這種抽象為 預期狀態 ,但是這個名稱只能描述這種抽象的一部分。我會介紹看待這種抽象的多種方式,並且展示一些使用到它的示例。希望最終你不僅僅能夠在各種工具和API裡能認出這種抽象,而且能夠評估在你自己的專案中是否值得使用它。

總概

我會從一些通用的目的開始,然後介紹簡單的例子,展示如何看待即使在我們知識邊界之外的系統中的這種抽象。這會讓我們瞭解構成這個模型基礎的一些通用原則。然後我會展示一些看待這個抽象的不同視角,用常用的工具,比如React,Kubernetes和Terraform來舉例。最後會提到應用這種抽象時應該思考的一些事情。

Mental模型

所有模型都是錯誤的,但其中有一些是有用的 。在軟體領域,我們總是在和模型打交道,描述事情是如何工作的mental模型,它在不同的領域,行業裡都挺有用的,並且試圖發現它們概念上的聯絡並找到共同之處。有時你會發現看似不相關的東西卻彼此相通。

你的模型永遠不會是完美的,但這並不重要。當學習新事物時,可以更容易地對映到已有的mental模型。就像學習語言的時候——你懂了更多門語言,你就更容易學習新的(無論是人類的語言還是程式語言)。語法有所變化,但是底層的模型通常並不怎麼變。這讓大家能夠更清楚地思考。

抽象還是介面?

我想要描述的預期狀態系統在前端web開發,後端開發,資料庫,基礎架構,GUI等領域常用的工具和庫函式裡都有其蹤跡。這是一種抽象的模型,但是它和介面緊密相關,因為抽象根本性地改變了開發人員或者使用者或其他系統和某個系統互動的方式。

抽象到底是什麼?《計算程式設計的理念,技術和模型》一書的作者將其定義為解決某個特定問題的工具或者裝置[1]。

這個定義非常籠統和正確,但是我尤其喜歡Joel Spolsky的定義,他認為抽象就是偽裝。他說:“string庫是什麼?它是計算機假裝自己可以像運算元字一樣操作string的方式。什麼是檔案系統?它是假裝硬碟其實不是一堆能夠在特定位置儲存位元的旋轉磁碟,而是包含單個檔案的資料夾巢狀資料夾的層級系統,這些單個檔案則包含一個或者多個位元的string。”[2]

抽象是我們工作的核心。我發現最有價值的工作不是編寫程式,而是設計這些抽象層。計算機程式設計主要就是設計和使用抽象來實現新的目標。當可以構建出一些將底層複雜性隱藏掉的系統,併為使用此係統的人或其他系統提供更為簡單的介面時,這是令人興奮的。

這就是抽象,讓我們看一個每天都會遇到的例子。

電梯

想象一個普通的電梯。

你來到電梯前,看到指上和指下的按鈕。我不知道你是如何想的,但是在我小時候,我的大腦總會將上下的箭頭解釋為“我要讓電梯上去”和“我要讓電梯下去”,而不是“我要上去”和“我要下去”。也就是說,我想直接控制它。直到今天,有時我仍然需要花一點腦細胞才能記住電梯的實際規則。

對於來到電梯前的人來說,這些按鈕意味著什麼?它們就是電梯的 介面 。允許使用者使用這個裝置和系統互動。我們可以把使用者看成另外一個系統,它遇到了一個通用定義的介面,這裡允許兩個系統發生互動。現在,我的困惑就是這個指向哪裡的箭頭可能意味著“我想要去那裡”以及“我想要這個東西去那裡”。當然並不是很多人都有這個困惑。讓我們忽略解釋使用者介面的問題,並比較這兩種方案,就當它們都是有效的。

我們可以稱這兩種方案什麼呢?

在我小時候的方案裡,我想要告訴機器它需要做什麼。也就是說,如何實現我想要它完成的事情。我想給他 指示 。懂拉丁文的人可能會建議我們稱之為 命令式 方案。

在另一種方案裡,我們只是告訴機器我們需要做什麼,讓它自己判斷它需要如何去做。我們 宣告 我們想要的。因此可以稱之為 宣告式 方案。

使用命令式方案,我們告訴機器怎麼去做我們想要的——如何到達我的樓層。使用宣告式方案,我們告訴它我們想要什麼。

然後它執行一小段邏輯來決定實際如何移動電梯。這段邏輯需要考慮一些事情,比如 - 電梯在哪裡,呼叫者在哪一層等等。如果電梯在我們上面,它需要下來。如果它在我們下面,它需要上去。

使用命令式介面,實際需要我們來負責判斷這些。因此,我們需要獲得電梯在哪裡這些資訊,可能可以通過螢幕展示出來。(顯然這不是生活中的實際場景,這也很快地證偽了我小時候的理解)只有掌握了這些 狀態 ,我們才能夠決定發給機器的具體指令。

在宣告式方案裡,我們不需要知道電梯狀態。

  • 觀察1: 宣告式介面是無狀態的。

注意在這個例子裡,使用宣告式介面時,我們實際提供給電梯兩類資訊 - 我們想要上電梯(不管它現在在什麼位置),以及我們想要上去或者下去。

簡化下示例,分開兩種概念並且僅關注於第一種 - 我們想要上電梯。在這個場景裡我們僅僅需要一個按鈕,對吧?這樣簡單的電梯是存在的。

我們將兩個按鈕簡化成了一個。這意味著什麼呢?

  • 觀察2: 宣告式介面讓控制減少

與之對比,命令式方案給了你更多控制權 - 你可以讓電梯上或者下,不管你站在哪裡(還是兩個按鈕),雖然在實際日常生活的電梯裡沒啥意義,但是你確實可以這麼做。宣告式方案讓你有更少的控制權 - 電梯總是來到我們的地方(一個按鈕)。

但是,你很可能注意到這不是全部。

我小時候沒有想到的是電梯可以 服務多層 。電梯介面不僅僅是你所在樓層的按鈕,而是所有樓層都有按鈕,還有電梯內部的按鈕。

當你考慮到這所有的情況,就會發現命令式方案根本無法工作。怎麼處理相同時間多人併發控制呢?如果你不是足夠的快,在你看到螢幕上電梯當前位置的瞬間就按下按鈕,電梯可能已經呼嘯而過,而你則把它送到更遠的地方,這該怎麼辦?如果不同樓層的兩個人一直在做相反的輸入,使電梯在原地振盪,又怎麼辦?

雖然宣告式方案拿走了部分控制權,但是它解決了這些問題。比如,我們可以基於按下按鈕的時間,簡單地根據先來先服務的機制處理不同樓層的人。

  • 觀察3:宣告式介面簡化了併發處理

但這效率不高 - 如果多個樓層的人想要去同一個方向,我們需要在中途停止。好吧,如果使用者事先告訴我們他們想去哪裡呢?

那麼我們可以優化電梯的執行,在電梯下行或上行的時候帶上更多的人。可以稱這樣的資訊為域特定引數。最後,這就是為什麼需要兩個按鈕,為什麼它們代表我們想去那裡,而不是直接表示我們想要電梯去哪裡。

在一些非常高的建築裡,甚至可以在上電梯之前就選擇想要到達的樓層,這就讓系統可以做進一步的優化。

  • 觀察4:宣告式介面有助於優化

重點是拋開命令式控制,並且加入域特定引數,我們能夠進一步優化操作。傳送到引擎的指令可以排列,重新排序等等,這讓我們能夠讓電梯併發支援多個樓層的多人操作。讓我們能夠構建更好的優化,並且徹底隱藏底層系統的機制。

  • 觀察5: 宣告式介面提供了封裝

注意,電梯也在做同樣的事情。上下,停在這層樓或那層樓。因此,電梯裡得有什麼東西能夠直接對電梯發出命令式控制,否則我們想去哪裡就不重要了,它不會移動。我喜歡這麼認為,宣告性介面封裝了命令式介面。

這樣的封裝也就是抽象。這是一種到處可見的抽象。我們可以稱這種抽象為預期狀態。我們告訴系統預期狀態 - 需要電梯到我們的樓層 - 讓系統處理如何將電梯的實際狀態改變為預期狀態。為了實現這個目標,電梯必須能夠查詢底層系統的當前狀態,將其和實際狀態做對比,並且相應地發出命令,更新實際狀態。為了區分這些概念,我使用動詞“應用(apply)”來表示告訴系統預期狀態的操作。

讓我們概括一下這些原則。

預期狀態系統

預期狀態系統封裝了命令式可變介面的底層 API 或系統,並允許其使用者為此底層系統指定所需狀態。然後,封裝器負責找出底層系統的實際狀態,將其與使用者提供給它的預期狀態進行比較,並應用必要的更改以使實際狀態與預期狀態保持一致。

中間的部分,它迴圈式地觀察底層系統,將其實際狀態與預期狀態進行比較,並相應地採取行動,這個過程稱之為reconciliation。

正如示例中所提到的,我們可以用幾種不同的方式來思考這種抽象。

1.宣告式 vs. 命令式

首先,作為封裝在命令式介面上的宣告式介面。只提供介面,讓使用者只需宣告他們想要做什麼,這會更簡單。通過限制 API 並將使用者引導到所需的使用模式中,這也是有益的。許多工具還利用了預期狀態可以進行版本控制的特性,來提高可測試性和可審計性。

2.有狀態 vs. 無狀態

也可以將其視為封裝在有狀態介面上的無狀態介面。為什麼要儘量減少跟蹤狀態的必要性?因為這樣,系統才更易於理解,而且更容易測試。API變得更簡單,就更易於維護,這也就意味著它更加可靠。能夠推斷變更對特定系統的影響非常重要,尤其是在處理併發性時。

3. 不可變 vs. 可變

另一種看待它的方式是,我們用可變語義的API封裝了一個不可變語義的API。在某些情況下,這種抽象允許我們將底層系統視為無法更改的不可變物件。如果某些東西發生了變化,可以將其視為一個新物件。這再次有助於推理變更所造成的影響。

這些只是我們可以用來分析預期狀態系統的眾多可能角度中的三個,這對理解之後的示例是有用的。

讓我們首先看一下 React,它使用了預期狀態系統的簡化版本。

React

React是一種流行的構建使用者介面的JavaScript庫。它讓使用者定義想要的應用程式的外觀和行為,都是用JavaScript實現的。

將應用定義為可組合元件的樹型結構,這些元件是React 的輸入。樹型結構定義了應用程式的結構,而外觀由應用於這些樹元件的各種屬性控制,行為由各種回撥函式控制,這些函式在使用者輸入或元件生命週期的某個階段觸發。樹型結構的葉子表示要顯示的實際 HTML 元素。

這棵樹本質上是 React 的 createElement 函式的一個巨大的巢狀呼叫。此函式的第一個引數是要使用的節點的型別,這可以是我們自己定義的元件,也可以是具有特定 HTML 元素的葉子節點。第二個引數是要傳送到此元件的屬性,第三個引數指定樹中元件節點的子級。這個介面讓元件非常容易組合,因為可以非常動態地傳遞資料,回撥甚至其他元件(樹中的父元件到子元件)。這使得 React 可以表達許多有用的設計模式。

為了節省手動輸入並使語法更符合實際的HTML,許多人使用稱為JSX的語法糖,這使得函式呼叫更加緊湊。

React 圍繞瀏覽器文件物件模型或 DOM 進行包裝,DOM 是一種所有 Web 瀏覽器中都存在的重要的API。此 API 允許我們以程式設計的方式更改頁面的內容。它將 HTML 頁表示為具有屬性、引數、事件處理程式的節點樹,並提供用於查詢、建立新元素、在樹中追加新子級等的方法。React 的元件樹也有類似的結構。

React作為預期狀態系統

讓我們通過期望狀態的鏡頭來看看 React。當頁面載入時,一些初始預期狀態作為元件樹的形式給到React。在內部,React 在記憶體中保留此樹的表示形式,每當預期狀態發生變更時 - 基於使用者輸入或其他觸發器 - 它都會將舊狀態與新狀態進行比較。這種內部表示形式過去被稱為虛擬DOM,現在這個名稱已經用的不多了。

狀態的比較生成了一系列需要在實際 DOM 上執行的操作。用於生成將一個樹轉換為另一個樹所需的最小運算元的通用演算法具有 O(n^3) 級的複雜性,其中 n 是樹中的元件數。然而,React 必須非常快速地做到這一點,因此它使用一系列啟發式方法來計算所需的最少運算元。

這將時間複雜度降低到 O(n)。這些啟發式方法在很大程度上依賴於兩個假設 - 首先,兩個不同型別的元素將產生不同的樹;其次,開發人員可以使用稱為key的特殊屬性來提示哪些子元素在渲染中可能無需更改。對於列表和其他順序很重要的地方,這是必需的。React 還依賴於傳遞給子級的屬性是不可變的事實 - 它假設當物件的內容發生變化時,對該物件的引用也會發生變化。然後,React 可以執行簡單的引用比較,而無需執行深度比較,並在元件屬性更改時重新渲染元件。這讓 UI 保持可響應。

我之所以強調這一點,是因為狀態的結構以及比較它的兩個版本時如何改變它的難度,可能是構建理想的預期狀態系統的關鍵部分之一。

React 計算所需最少操作的原因是,DOM 上的操作非常慢。但是,整個比較機制是實現的細節,如果它是高效能的,React 可以選擇每次都從頭開始重建整個樹。

現在我想說,根據我們正在使用的模型,React 是一個簡化的預期狀態系統。讓我離題介紹一下控制理論的基礎知識。

控制理論:閉環系統 vs 開環系統

閉環系統是在一個迴圈中相互連線的系統。如果系統 1 向系統 2 傳送訊號,則系統 2 的輸出在某種程度上又是系統 1 輸入的一部分。這稱為 反饋 。反饋的一個關鍵特徵是為不確定性提供了魯棒性。閉環系統通過將所需的輸出條件與實際條件進行比較,自動實現並保持預期的輸出條件。

雖然反饋有很多優點,但它也帶來了一系列缺點。如果設計不當,系統可能會表現不穩定。這可能是正反饋的形式,例如當麥克風的放大器在房間中調得太高時。此外,反饋本質上也耦合了系統的不同部分。[3]

另一方面,在開環系統中,這種互連被切斷。

React是開環系統

雖然 React 是模型中的預期狀態系統,但它實際上是一個開環系統。React 不會不斷重新檢查瀏覽器 DOM 的當前狀態,以確定它是否正確。

首先,這可能會非常慢。它其實也不需要這麼做。與許多其他預期狀態系統不同,React 的執行假設是, 只有它能夠操作目標域 。它通常假設沒有其他庫函式或人繞過它去修改頁面。你可以使用瀏覽器中的開發工具自行測試。如果修改了由 React 控制的 HTML 元素,則庫函式不會嘗試覆蓋你的修改,除非發生變更的元素的父元素被重新渲染並替換了整個子樹。

React 的一個好處是它是以模組化的方式構建的。與 DOM 通訊的部分是一個名為 ReactDOM 的單獨模組,可以替換為不同的渲染目標,React 稱之為主機(host)。例如,在移動應用程式框架React Native中使用了不同的主機。在編寫 React Native 時,你仍然將應用的使用者介面指定為元件樹。然而,React不是與DOM對話,而是與移動作業系統的原生API互動。使用自定義的渲染目標來擴充套件 React 也相對容易。

React 的 API 最近添加了 Hooks。這是一種有趣的設計模式,允許開發人員以宣告性的方式管理元件中的狀態和副作用。建議大家閱讀 Dan Abramov 的部落格文章" 使用 React Hooks 讓setInterval 變成宣告式 ",他在其中用宣告式的hook包裝了一個固有的命令式 API,即瀏覽器的 setInterval 方法。

讓我們進一步研究些別的。

Terraform

Terraform 是一個開源的基礎架構即程式碼的工具,其目標是提供一個工作流來配置所有的基礎架構。它允許使用者指定基礎架構的各個部分,最常見的是雲提供商中不同型別和例項的資源,以及它們的配置,用Terraform的原生語言HCL編寫的文字檔案。

使用這些配置檔案,你可以執行所謂的 terraform 計劃,通過該計劃,可以在預配或更改實際基礎結構之前檢查配置的執行計劃是否符合預期。

首次執行 Terraform 時,它會建立一個名為 tfstate 的檔案,用於儲存資源的當前狀態。

每次想要對其進行更改時,它都會去獲取實際資源的當前狀態,並報告新計劃將造成的變更。然後就可以檢查變更是否正確並應用它們。

Terraform作為預期狀態系統

在生成的差異中,你可以看到新計劃將進行哪些更改,還可以檢視實際狀態是否已偏離儲存的 tfstate。換句話說,與 React 不同,Terraform 是一個閉環系統。本質上,這是因為它正在優化以解決不同的問題 - 雖然React需要快速更新並且可以假設沒有其他人接觸其目標域,但Terraform則可以花費更多的時間(並且確實)弄清楚與實際狀態的差異。至關重要的是,它不能假設它所管理的資源保持不變。

就像你可以用不同的主機擴充套件 React 一樣,Terraform 有一個稱為提供者(provider)的外掛集合。provider負責理解 API 與某種服務的互動,並基於該 API 將資源公開。當然,也可以建立自己的provider。與 React 不同,Terraform 在如何定義資源及其配置上增加了複雜性。可以將配置指定為自己的資源,也可以僅將其指定為父資源的一部分。這裡因provider而異,許多資源型別都同時支援這兩者。Terraform最困難的部分之一是如何管理這種耦合。

與 Chef 或 SaltStack 等配置管理工具相比,Terraform 遵循不可變部署的原則。更改計劃時,將重新建立資源並應用正確的狀態。這意味著操作本質上可能具有破壞性。在 VM 上進行配置的更改可能意味著銷燬原來的 VM 並預配新的 VM。哪些操作是破壞性的,哪些不是破壞性的,是由每個provider定義的。另一方面,使用可變部署的話,更有可能陷入實際狀態偏離預期狀態的情況。換句話說,對 Terraform 狀態的更改是冪等的 – 如果持續重新建立 10 個 VM 的相同狀態,則最終結果始終只有 10 個 VM。這是預期狀態系統的關鍵屬性。

讓我們從預配轉向容器編排。

Kubernetes

你們可能都聽說過 Kubernetes。這是一個由Google建立的開源系統,用於自動化容器化應用程式的部署,擴充套件和管理。為了實現其目標,它很有爭議地將預期狀態系統作為其最高層的設計理念。

Kubernetes 管理機器叢集內的容器化的工作流,這些機器稱為節點(node)。可以是物理機或虛擬機器。最小的工作單元稱為 pod,可以安排它們在節點上執行。最終,帶有我們程式碼的特定容器將在這個 pod 中執行。Pod 以多種資源進行組織,最常見的是部署(deployment)。可以用另一種資源(稱為服務,service)來定義網路。還有許多其他型別的資源,並且可以建立自定義資源。

Kubernetes 本身作為一系列服務,執行在這些節點上,稱為控制平面。控制平面負責為 API 提供服務,持續跟蹤資源和其他所需的任務,來保持系統的執行。

叢集可以用 命令列工具 kubectl 來控制,該工具是 Kubernetes 的介面。

雖然它還提供了一種類似命令式的API,但其用法的核心是用 yaml 配置檔案完成的,將其作為預期狀態應用到叢集裡。

Kubernetes作為預期狀態系統

我們也可以通過模型的鏡頭來看待 Kubernetes。與Terraform類似,你向系統提供叢集中特定資源的預期狀態,這次是以yaml檔案的形式。

Kubernetes 中稱為控制器(controller)的元件負責保證給定資源的實際狀態與預期狀態保持一致。與Terraform不同,這種情況是持續發生的。如果你嘗試刪除有 3 個預期副本的部署中的某個 pod 副本,Kubernetes 將立即啟動一個新副本。同樣,如果一個 pod 不斷崩潰,將繼續嘗試執行它,因為它試圖保持實際狀態與預期狀態一致。Kubernetes 是一個閉環系統。

控制理論中另一個有趣的概念,出現在持續的對預期狀態reconciliation的系統中,即滯後(hysteresis)。

控制理論:滯後(hysteresis)

滯後表示這個系統的行為不僅取決於它在時間t處的輸入,還取決於該輸入的歷史記錄。您也可以將其視為向系統添加了人為滯後。一個廣泛使用的例子是恆溫器。假設我們將恆溫器的溫度設定為20度。沒有滯後的話,一旦溫度達到預期狀態,加熱就會關閉。但這意味著很快溫度就會下降到20度以下,然後需要重新開啟暖氣。恆溫器系統就會開始振盪,快速地開啟和關閉加熱。當我們新增滯後時,恆溫器會等到溫度高於22度,然後再關閉加熱。同樣,恆溫器將等到溫度低於18度時才重新開啟加熱。這確保了系統更平穩、更可靠的執行。[3]

Kuberntes控制器

在 Kubernetes 中,當它必須決定是否將一些工作負載從計算資源正在減少的 Node 中移走時,這個概念就會發揮作用,這就是所謂的 pod 逐出。可以指定軟寬限期,這意味著 Kubernetes 將等待一段時間,然後再將 pod 從節點排程出去,因為資源約束可能是暫時的情況。這確保了排程的更可預測性和更順暢的操作。

Kubernetes實際上是由許多控制器組成的,它們協同工作,以使實際狀態接近預期狀態。每個控制器可以對一種或多種資源型別執行操作。

這些控制器實際通常是巢狀的 - 特定的控制迴圈(控制器)使用一種資源作為其預期狀態,並且控制另一種資源,設法達到預期狀態。

允許我再次轉向控制理論的基礎,這個概念來自電路板設計和CPU中斷。

控制理論:邊沿- vs 電平觸發邏輯

當一個系統需要通過電路向另一個系統提供資訊時,有兩種選擇。第一種稱為邊沿觸發邏輯 – 系統1通過短暫的高壓尖峰脈衝來傳遞電訊號。這種方法的問題在於,如果系統 2 當時不在偵聽,它可能就會錯過訊號。

第二種選擇是我們所說的電平觸發邏輯。在這種情況下,系統 1 將電壓調高並保持在那裡,直到確定它已被系統 2 接收。這種方法更可靠,因為系統2可以隨時檢查電線的狀態。[4]

你可能會看到這與事件驅動的通訊與輪詢更改之間的一些相似之處。

但是讓我們回到 Kubernetes 控制器。React 需要優化以便能夠快速更新瀏覽器 DOM,而 Kubernetes 需要優化reconciliation迴圈的觀察部分。這是因為在任何時候,大量的控制器都可能需要詢問特定資源的狀態。大規模範圍內持續輪詢這些資訊是低效的。因此,大多數控制器在邊沿和電平觸發邏輯之間選擇了混合方法,稱為列表-監視模式。

在此模式中,控制器首先請求當前狀態,然後對其進行快取,並保持事件流處於開啟狀態,以便立即更新到其快取。與純事件流不同,該系統是健壯的,可以應對崩潰和網路問題,因為控制器可以隨時重新斷言狀態。

讓我們再看一下另一種描述預期狀態模型的方法。

值 vs 引用語義

我們可以將其視為用值語義封裝了引用語義。當我們將物件視為值時,假設它們無法更改,它們是不可變的(在這種情況下,物件通常意味著使用的東西,一些資料)。在程式設計時,我們不必擔心整數5和整數4之後加了1這兩者之間的差異。五等於五。換句話說,是物件的內容提供了物件標識,而不是我們對它的引用。

預期狀態只是我們想要其存在於世界上的各種值的集合。與其像 React 一樣跟蹤瀏覽器 DOM 節點的引用,或者 Kubernetes 中的容器,或者 Terraform 中的虛擬機器,我們可以簡單地將它們視為值。值不能改變,如果我們需要另一個值,必須建立一個新的。底層系統負責處理下面的所有可變邏輯。在某種程度上,你最喜歡的程式語言中的不可變字串可以視為一個預期狀態系統,編譯器或直譯器確保以高效能的方式協調值語義與底層記憶體的引用。對複雜物件執行此操作並不容易,而且通常也不可能。

不過,我發現思考一下如何將網路套接字,資料庫或其他固有的有狀態物件視為值,是非常有趣的。

思考

最後,讓我們列出在設計這樣的系統時必須記住的一些注意事項。

1.增加這樣的複雜度是否值得?

首先,你必須問自己一個問題,即它是否必要。它是否解決了真正的問題。新增抽象總是會導致更多的複雜性,重要的是要對這額外的複雜性是否值得進行成本效益分析。對於使用它的人來說,封裝了命令式API後的宣告式API似乎要簡單得多,但其實它的內部複雜性總是更高的。

2.限制API

思考怎麼限制使用者使用我們的介面,讓他們可以做什麼和不能做什麼,這是很有用的。確定要給使用者更少的控制權嗎?是否確切地知道我們想要如何引導 API?

3.閉環還是開環系統

我們需要的是閉環還是開環系統?也就是說,我們是使用沙盒的唯一使用者嘛?是否確定我們是唯一可以控制底層API的人,沒有別人需要這些?

4.併發

我們是否需要解決來自多個使用者的對介面的併發更新?來自同一使用者的多個更新怎麼處理?如何協調多個相互衝突的預期狀態?

5.優化

無狀態的介面以及限制API的使用面是否能夠有助於系統的優化?

6.時間

如何處理需要一定時間才能完成的事情。雖然API可能會將某些物件或資源視為值,但在系統內部,可能需要等待才能順序地建立,銷燬,更新它們。如何將這些傳達給使用者,又是否需要傳達呢?

必須考慮時間是有狀態和無狀態系統之間的關鍵區別。當嘗試將優雅的數學模型對映到現實世界時,時間是系統複雜性的主要來源之一。

7.比較差異的效能

你的狀態的結構是什麼,如何能夠迅速地比較它們。這可能是,也可能不是瓶頸,取決於結構和底層API的情況。

8.觀察並且更新的效能

底層API有多快,是否是觀察或者更新的一部分。通常來說,預期狀態系統比直接操作底層API更慢。您將始終需要權衡效能影響與使用此類抽象的好處。

9.持續跟蹤資源

如何跟蹤底層API控制的資源。這在像 Kubernetes 這樣的分散式系統中尤其重要,因為同時執行著許多控制迴圈。

10.Escape hatches

我們是否需要在接口裡新增escape hatches,以便使用者可以在需要時切入到命令式介面?這可能是出於效能的考慮,也可能是為了在真正需要時為使用者提供更大的靈活性。React 允許在需要時直接與瀏覽器 DOM 對話。儘管不鼓勵這樣做,但偶爾也是有必要的。同樣地,Kubernetes 允許使用者在必要時可以直接進入Pod 中執行的應用程式的 shell命令列。

結論

本文介紹了什麼是預期狀態系統及其中心原則,向大家展示了一些使用到它的例子,並提出了在使用這種抽象時,應該記住的一些重要的考慮因素。

我在這裡提出的想法並不是什麼新鮮事,大多數人可能已經思考過這個問題。我希望通過結構化的方式思考它們,希望現在你的腦海中有一個更清晰的畫面或mental模型,從而有助於工作。

在當今時代,我們周圍的世界不是數學的,而是天生可變和有狀態的。限制我們的事實是,在所有程式的底層都有一臺馮·諾伊曼計算機,它有中央處理單元,資料儲存和可以在CPU和儲存之間傳輸單詞的連線通道。它是固有的可變的和命令式介面。

但是,由於抽象,有時在有限的範圍內,我們可以假裝情況並非如此,並簡化我們的介面。

參考資料

1. https://mitpress.mit.edu/books ... mming

2. https://www.joelonsoftware.com ... tions

3. https://fbswiki.org/wiki/index ... neers

4. https://speakerdeck.com/thocki ... logic

原文連結: Desired state systems (翻譯:崔婧雯)

===========================

譯者介紹

崔婧雯,現就職於IBM,高階軟體工程師,負責IBM WebSphere業務流程管理軟體的系統測試工作。曾就職於VMware從事桌面虛擬化產品的質量保證工作。對虛擬化,中介軟體技術,業務流程管理有濃厚的興趣。