狀態存放區通訊協定
狀態存放區是 Azure IoT 作業叢集中的分散式記憶體系統。 狀態存放區提供與 MQTT 代理程式中 MQTT 訊息相同的高可用性保證。 根據 MQTT5/RPC 通訊協定指導方針,客戶端應該使用 MQTT5 與狀態存放區互動。 本文提供通訊協定指引給需要實作自己狀態存放區用戶端的開發人員。
概觀
狀態存放區支援下列命令:
SET
<keyName><keyValue><setOptions>GET
<keyName>DEL
<keyName>VDEL
<keyName><keyValue> ## 只有在其值是 <keyValue> 時,才會刪除指定的 <keyName>
通訊協定會使用下列要求-回應模型:
- 要求。 用戶端會將要求發佈至定義完善的狀態存放區系統主題。 若要發佈要求,用戶端會使用下列各節中所述的必要屬性和承載。
- Response: 狀態存放區會以非同步方式處理要求,並在用戶端最初提供的回應主題上回應。
下圖顯示要求和回應的基本檢視:
狀態存放區系統主題、QoS 及必要的 MQTT5 屬性
若要與狀態存放區通訊,用戶端必須符合下列需求:
若要與狀態儲存通訊,用戶端必須向系統主題 statestore/v1/FA9AE35F-2F64-47CD-9BFF-08E2B32A0FE8/command/invoke
發出 PUBLISH
要求。 因為狀態存放區是 Azure IoT 操作的一部分,所以會在啟動時對本主題執行隱含 SUBSCRIBE
。
若要建置要求,需要下列 MQTT5 屬性。 如果這些屬性不存在,或要求不是 QoS 1 類型,要求就會失敗。
- 回應主題。 狀態存放區會使用此值回應初始要求。 最佳做法是將回應主題格式化為
clients/{clientId}/services/statestore/_any_/command/invoke/response
。 在狀態存放區要求上,不允許將回應主題設定為statestore/v1/FA9AE35F-2F64-47CD-9BFF-08E2B32A0FE8/command/invoke
,或是以clients/statestore/v1/FA9AE35F-2F64-47CD-9BFF-08E2B32A0FE8
作為開頭的主題。 狀態存放區會中斷使用無效回應主題的 MQTT 用戶端連線。 - 相互關聯資料。 當狀態存放區傳送回應時,它會包含初始要求的相互關聯資料。
下圖顯示要求和回應的展開檢視:
支援的命令
命令 SET
、 GET
和 DEL
如預期般運作。
SET
命令集合和 GET
命令擷取的值都是任意二進位資料。 值的大小只會受限於 MQTT 承載大小上限,以及 MQTT 訊息代理程式和客戶端的資源限制。
SET
選項
SET
命令除了基本 keyValue
和 keyName
之外,還提供更有選擇性的旗標:
NX
. 只有在索引鍵不存在時,才允許設定索引鍵。NEX <value>
. 只有在索引鍵不存在,或索引鍵的值已經設定為 <value> 時,才能設定索引鍵。NEX
旗標通常用於用戶端更新金鑰的到期時間 (PX
)。PX
. 金鑰在到期前應該持續多久,以毫秒為單位。
VDEL
選項
VDEL
命令是 DEL
命令的特殊案例。 DEL
無條件刪除指定的 keyName
。 VDEL
需要另一個名為 keyValue
的引數。 如果給定的 keyName
具有相同的 keyValue
,則 VDEL
僅刪除該 keyName
。
承載格式
狀態存放區 PUBLISH
承載格式受到 RESP3的啟發,這是 Redis 使用的基礎通訊協定。 RESP3 對動詞 (如 SET
或 GET
) 和參數 (如 keyName
和 keyValue
) 進行編碼。
區分大小寫
用戶端必須同時傳送動詞和大寫的選項。
要求格式
要求的格式如下範例所示。 在 RESP3 之後,*
代表陣列中的項目數。 $
字元是下列行中的字元數,不包括後置 CRLF。
RESP3 格式支援的命令為 GET
、SET
、DEL
及 VDEL
。
*{NUMBER-OF-ARGUMENTS}<CR><LF>
${LENGTH-OF-NEXT-LINE}<CR><LF>
{COMMAND-NAME}<CR><LF>
${LENGTH-OF-NEXT-LINE}<CR><LF> // This is always the keyName with the current supported verbs.
{KEY-NAME}<CR><LF>
// Next lines included only if command has additional arguments
${LENGTH-OF-NEXT-LINE}<CR><LF> // This is always the keyValue for set
{KEY-VALUE}<CR><LF>
下列範例輸出顯示狀態存放區 RESP3 承載:
*3<CR><LF>$3<CR><LF>set<CR><LF>$7<CR><LF>SETKEY2<CR><LF>$6<CR><LF>VALUE5<CR><LF>
*2<CR><LF>$3<CR><LF>get<CR><LF>$7<CR><LF>SETKEY2<CR><LF>
*2<CR><LF>$3<CR><LF>del<CR><LF>$7<CR><LF>SETKEY2<CR><LF>
*3<CR><LF>$4<CR><LF>vdel<CR><LF>$7<CR><LF>SETKEY2<CR><LF>$3<CR><LF>ABC<CR><LF>
注意
SET
需要額外的 MQTT5 屬性,如版本設定和混合式邏輯時鐘一節所述。
回應格式
當狀態儲存存放區偵測到無效的 RESP3 承載時,它仍然會向要求者的 Response Topic
回傳回應。 無效承載的範例包括無效的命令、不正確 RESP3 或整數溢位。 無效的承載開頭為字串 -ERR
,並包含更多詳細資料。
注意
對不存在索引鍵的 GET
、DEL
或 VDEL
要求不會被視為錯誤。
如果用戶端傳送不正確承載,狀態存放區會傳送承載,如下列範例所示:
-ERR syntax error
SET
回應
當 SET
要求成功時,狀態存放區會傳回下列承載:
+OK<CR><LF>
如果 SET 要求失敗,因為 NX 或 NEX 集合選項中指定的條件檢查表示無法設定金鑰,狀態存放區會傳回下列承載:
-1<CR><LF>
GET
回應
在不存在的索引鍵上提出 GET
要求時,狀態存放區會傳回下列承載:
$-1<CR><LF>
找到索引鍵時,狀態存放區會以下列格式傳回值:
${NumberOfBytes}<CR><LF>
{KEY-VALUE}
傳回值 1234
的狀態存放區輸出看起來像下列範例:
$4<CR><LF>1234<CR><LF>
DEL
和 VDEL
回應
狀態存放區會傳回在刪除要求上刪除的值數目。 目前,狀態存放區一次只能刪除一個值。
:{NumberOfDeletes}<CR><LF> // Will be 1 on successful delete or 0 if the keyName is not present
下列輸出是成功 DEL
命令的範例:
:1<CR><LF>
如果 VDEL 要求失敗,因為指定的值不符合與索引鍵相關聯的值,狀態存放區會傳回下列承載:
-1<CR><LF>
-ERR
反應
以下是目前的錯誤字串清單。 用戶端應用程式應該處理 未知的錯誤 字串,以支援狀態存放區的更新。
從狀態存放區傳回的錯誤字串 | 說明 |
---|---|
要求時間戳在未來太遠;確定客戶端和訊息代理程式系統時鐘已同步 | 狀態存放區和用戶端時鐘所造成的非預期要求時間戳不會同步。 |
此要求需要隔離令牌 | 如果索引鍵標示為隔離令牌,但用戶端未指定隔離令牌,就會發生錯誤。 |
要求隔離令牌時間戳在未來太遠;確定客戶端和訊息代理程式系統時鐘已同步 | 狀態存放區和用戶端時鐘所造成的非預期的隔離令牌時間戳不會同步。 |
要求隔離令牌是保護資源的隔離令牌較低版本 | 要求隔離令牌版本不正確。 如需詳細資訊,請參閱 [版本設定和混合式邏輯時鐘]。(#versioning 和混合式邏輯時鐘) |
已超過配額 | 狀態存放區具有可儲存的密鑰數目配額,這是根據所指定 MQTT 訊息代理程式記憶體設定檔而定。 |
語法錯誤 | 傳送的承載不符合狀態存放區的定義。 |
未獲授權 | 授權錯誤 |
未知的命令 | 無法辨識命令。 |
錯誤的自變數數目 | 預期的自變數數目不正確。 |
遺漏時間戳 | 當用戶端執行 SET 時,必須將 MQTT5 使用者屬性設定為代表其時間戳的 HLC __ts。 |
格式不正確的時間戳 | __ts或隔離令牌中的時間戳不合法。 |
索引鍵長度為零 | 狀態存放區中的索引鍵長度不能為零。 |
版本設定和混合式邏輯時鐘
本節說明狀態存放區如何處理版本控制。
作為混合式邏輯時鐘的版本
狀態存放區會針對它所儲存的每個值維護版本。 狀態存放區可以使用單純遞增的計數器來維護版本。 相反地,狀態存放區會使用混合式邏輯時鐘 (HLC) 來代表版本。 如需詳細資訊,請參閱有關 HLC 的原始設計和 HLC 背後意圖的文章。
狀態存放區會使用下列格式來定義 HLC:
{wallClock}:{counter}:{node-Id}
wallClock
是 Unix epoch 之後的毫秒數。 counter
和 node-Id
一般作為 HLC 工作。
當用戶端執行 SET
時,他們必須根據用戶端目前的時鐘,將 MQTT5 使用者屬性 __ts
設定為代表其時間戳記的 HLC。 狀態存放區會傳回其回應訊息中的值版本。 回應也會指定為 HLC,也會使用 __ts
MQTT5 用戶使用者屬性。 傳回的 HLC 一律大於初始要求的 HLC。
設定和擷取值版本的範例
本節顯示設定和取得值版本的範例。
用戶端會設定 keyName=value
。 用戶端時鐘為格林威治標準時間 10 月 3 日下午 11:07:05。 時鐘值是 unix epoch 之後的 1696374425000
毫秒。 假設狀態存放區的系統時鐘與用戶端系統時鐘相同。 用戶端會執行上述 SET
命令。
下圖說明 SET
命令:
初始集合上的 __ts
(時間戳記) 屬性包含 1696374425000
作為用戶端時鐘、計數器為 0
,以及其節點識別碼為 CLIENT
。 在回應中,狀態存放區傳回的 __ts
屬性包含 wallClock
、計數器遞增一,以及節點識別碼昨為 StateStore
。 如果狀態存放區的時鐘超前,則根據 HLC 更新的工作方式,狀態存放區可能會傳回更高的 wallClock
值。
此版本也會在成功 GET
、DEL
及 VDEL
要求時傳回。 在這些要求上,用戶端不會指定 __ts
。
下圖說明 GET
命令:
注意
狀態存放區傳回的時間戳記 __ts
與其在初始 SET
要求上傳回的內容相同。
如果指定索引鍵稍後會以新的 SET
更新,則流程很類似。 用戶端應根據其目前的時鐘來設定其要求 __ts
。 狀態存放區會更新值的版本,並傳回 __ts
,遵循 HLC 更新規則。
時鐘誤差
狀態存放區拒絕比狀態存放區本機時鐘早一分鐘以上的 __ts
(以及 __ft
)。
狀態存放區接受落後於狀態存放區本機時鐘的 __ts
。 如 HLC 演算法中所指定,狀態存放區會將索引鍵的版本設定為其本機時鐘,因為它更大。
鎖定和隔離權杖
本節說明鎖定和隔離權杖的用途和使用方式。
背景
假設有兩個以上的 MQTT 用戶端使用狀態存放區。 這兩個用戶端都想要寫入指定的索引鍵。 狀態存放區用戶端需要一個機制來鎖定索引鍵,讓一次只有一個用戶端可以修改指定的索引鍵。
此情節的範例發生在使用中和待命系統中。 可能有兩個用戶端執行相同的作業,而且作業可以包含相同的狀態存放區索引鍵集合。 在指定的時間,其中一個用戶端處於作用中狀態,另一個用戶端處於待命狀態,以便在作用中系統停止響應或當機時立即接管。 在理想情況下,只有一個用戶端應該在指定時間寫入狀態存放區。 不過,在分散式系統中,這兩個用戶端的行為可能就像是作用中一樣,而且它們可能會同時嘗試寫入相同的索引鍵。 此情節會建立競爭條件。
狀態存放區會使用隔離權杖來提供防止此競爭條件的機制。 如需隔離權杖及其設計來防範之競爭條件類別的詳細資訊,請參閱此文章。
取得隔離權杖
此範例假設我們有下列元素:
Client1
和Client2
。 這些用戶端是狀態存放區用戶端,可作為作用中和待命配對。LockName
. 狀態存放區中做為鎖定的索引鍵名稱。ProtectedKey
. 需要從多個寫入器保護的索引鍵。
用戶端會嘗試取得鎖定做為第一個步驟。 他們藉由執行 SET LockName {CLIENT-NAME} NEX PX {TIMEOUT-IN-MILLISECONDS}
來取得鎖定。 重新叫用集合選項,NEX
旗標意味著僅當滿足以下條件之一時 SET
才會成功:
- 索引鍵是空的
- 索引鍵的值已經設定為 <value>,並且
PX
指定逾時 (以毫秒為單位)。
假設 Client1
先使用 SET LockName Client1 NEX PX 10000
的要求。 此要求會為 LockName
提供 10,000 毫秒的所有權。 如果 Client2
在 Client1
擁有鎖定時嘗試 SET LockName Client2 NEX ...
,則 NEX
旗標表示 Client2
要求失敗。 如果 Client1
想要繼續擁有所有權,Client1
需要透過傳送用於取得鎖定的相同 SET
命令來更新此鎖定。
注意
SET NX
在概念上相當於 AcquireLock()
。
在 SET 要求上使用隔離權杖
成功在 上執行 (“AcquireLock”) 時Client1
,狀態存放區會在 MQTT5 使用者屬性__ts
中傳回 作為混合式邏輯時鐘 (HLC) 的版本LockName
。SET
LockName
當用戶端執行 SET
要求時,可以選擇是否包含 MQTT5 使用者屬性 __ft
來代表「隔離權杖」。 __ft
會以 HLC 表示。 與指定機碼值組相關聯的隔離權杖會提供鎖定所有權檢查。 隔離權杖可以來自任何地方。 在此情節中,它應該來自 LockName
版本。
下圖顯示 Client1
在 LockName
上執行 SET
要求的流程:
接下來,Client1
使用未修改的 __ts
屬性 (Property=1696374425000:1:StateStore
) 作為要求中 __ft
屬性的基礎,以修改 ProtectedKey
。 如同所有 SET
要求,用戶端必須設定 ProtectedKey
的 __ts
屬性。
下圖顯示 Client1
在 ProtectedKey
上執行 SET
要求的流程:
如果請求成功,從此時起,ProtectedKey
需要一個等於或大於 SET
要求中指定的隔離權杖。
隔離權杖演算法
如果值在最大時鐘誤差範圍內,狀態存放區會接受機碼值組 __ts
的任何 HLC。 不過,對於隔離權杖來說並非如此。
隔離權杖的狀態存放區演算法如下所示:
- 如果機碼值組沒有與其相關聯的隔離權杖,且
SET
要求集合__ft
,狀態存放區會儲存與機碼值組組相關聯的__ft
。 - 如果機碼值組有與其相關聯的隔離權杖:
- 如果
SET
要求未指定__ft
,請拒絕要求。 - 如果
SET
要求指定的__ft
,其 HLC 值比與機碼值組相關聯的隔離權杖還舊,請拒絕要求。 - 如果
SET
要求指定的__ft
的 HLC 值等於或更新於與機碼值組對關聯的隔離權杖,則接受該要求。 如果機碼值組對的隔離權杖較新,則狀態存放區會將其更新為要求中設定的權杖。
- 如果
在索引鍵標記有隔離權杖後,為了讓要求成功,DEL
和 VDEL
要求也需要包含 __ft
屬性。 此演算法與上一個演算法相同,不同之處在於隔離權杖不會儲存,因為正在刪除索引鍵。
用戶端行為
這些鎖定機制依賴用戶端運作良好。 在上一個範例中,Client2
行為錯誤無法擁有 LockName
,而且仍然藉由選擇比 ProtectedKey
權杖更新的隔離權杖,成功執行 SET ProtectedKey
。 狀態存放區不知道 LockName
和 ProtectedKey
具有任何關聯性。 因此,狀態存放區不會執行 Client2
實際擁有值的驗證。
用戶端能夠寫入他們實際上並沒有擁有鎖定的索引鍵,這是不可取的行為。 您可以藉由正確實作用戶端並使用驗證來限制對受信任用戶端的索引鍵存取,以防止這類用戶端錯誤。
通知
用戶端可以向狀態存放區註冊,以接收索引鍵受到修改的通知。 舉例而言,某個控溫器使用狀態存放區索引鍵 {thermostatName}\setPoint
。 其他狀態存放區用戶端可以變更此索引鍵的值,藉此變更該控溫器的 setPoint。 控溫器可以向狀態存放區註冊,以在 {thermostatName}\setPoint
修改時接收訊息,而不是輪詢變更。
KEYNOTIFY 要求訊息
狀態存放區用戶端會透過傳送 KEYNOTIFY
訊息,要求狀態存放區監視指定的 keyName
變更。 就像所有狀態存放區要求一樣,用戶端會透過 MQTT5 將具有此訊息的 QoS1 訊息發佈至狀態存放區系統主題 statestore/v1/FA9AE35F-2F64-47CD-9BFF-08E2B32A0FE8/command/invoke
。
要求承載的格式如下:
KEYNOTIFY<CR><LF>
{keyName}<CR><LF>
{optionalFields}<CR><LF>
其中:
- KEYNOTIFY 是指定命令的字串常值。
{keyName}
是要接聽通知的索引鍵名稱。 目前不支援萬用字元。{optionalFields}
目前支援的選擇性欄位值如下:{STOP}
如果有現有通知的keyName
和clientId
與這個要求相同,狀態存放區就會將其移除。
下列範例輸出顯示監視索引鍵 SOMEKEY
的 KEYNOTIFY
要求:
*2<CR><LF>
$9<CR><LF>
KEYNOTIFY<CR><LF>
$7<CR><LF>
SOMEKEY<CR><LF>
KEYNOTIFY 回應訊息
如同所有狀態存放區 RPC 要求,狀態存放區會將回應傳回至 Response Topic
,並使用初始要求中指定的 Correlation Data
屬性。 針對 KEYNOTIFY
,成功的回應表示狀態存放區已處理要求。 狀態存放區成功處理要求之後,便會監視目前用戶端的索引鍵,或停止監視。
成功時,狀態存放區的回應與成功的 SET
相同。
+OK<CR><LF>
如果用戶端傳送 KEYNOTIFY SOMEKEY STOP
要求,但狀態存放區並未監視該索引鍵,狀態存放區的回應會與嘗試刪除不存在的索引鍵相同。
:0<CR><LF>
其餘失敗都會遵循狀態存放區的一般錯誤報告模式:
-ERR: <DESCRIPTION OF ERROR><CR><LF>
KEYNOTIFY 通知主題和生命週期
當透過 KEYNOTIFY
監視的 keyName
遭到修改或刪除時,狀態存放區會傳送通知給用戶端。 主題是依照慣例決定 - 用戶端在 KEYNOTIFY
程序期間不會指定主題。
本主題定義於下列範例中。 clientId
是起始 KEYNOTIFY
要求的用戶端 MQTT ClientId 大寫十六進位編碼表示法,而 keyName
是已變更索引碼的十六進位編碼表示法。 狀態存放區遵循 RFC 4648 的 Base 16 編碼規則 - 此編碼的 Base16、Base32 和 Base64 數據編碼 。
clients/statestore/v1/FA9AE35F-2F64-47CD-9BFF-08E2B32A0FE8/{clientId}/command/notify/{keyName}
例如,MQTT 訊息代理程式會將NOTIFY
已修改金鑰名稱SOMEKEY
傳送至 client-id1
的訊息發佈至 主題:
clients/statestore/v1/FA9AE35F-2F64-47CD-9BFF-08E2B32A0FE8/636C69656E742D696431/command/notify/534F4D454B4559`
使用通知的用戶端應透過 SUBSCRIBE
訂閱此主題,並在傳送任何 KEYNOTIFY
要求「之前」等待收到 SUBACK
,以免遺失任何訊息。
如果用戶端中斷連線,則必須重新訂閱 KEYNOTIFY
通知主題,並針對需要繼續監視的任意索引鍵重新傳送 KEYNOTIFY
命令。 不同於可在非清除工作階段之間保存的 MQTT 訂用帳戶,當指定的用戶端中斷連線時,狀態存放區會從內部移除所有 KEYNOTIFY
訊息。
KEYNOTIFY 通知訊息格式
修改透過 KEYNOTIFY
監視的索引鍵時,狀態存放區會遵循針對變更註冊的狀態存放區用戶端格式,透過 PUBLISH
將訊息發佈至通知主題。
NOTIFY<CR><LF>
{operation}<CR><LF>
{optionalFields}<CR><LF>
該訊息中包含下列詳細資料:
NOTIFY
是包含在承載中作為第一個引數的字串常值,表示通知已送達。{operation}
是發生的事件。 目前這些作業如下:SET
已修改值。 這項作業只會因狀態存放區用戶端的SET
命令而發生。DEL
已刪除值。 這項作業可能會因為狀態存放區用戶端的DEL
或VDEL
命令而發生。
optionalFields
VALUE
和{MODIFIED-VALUE}
。VALUE
是字串常值,表示下一個欄位 ({MODIFIED-VALUE}
),包含索引鍵所變更的目標值。 這個值只會傳送來回應因 的SET
索引鍵而遭到修改。
下列範例輸出顯示當索引鍵 SOMEKEY
修改為 abc
值時,將會傳送的通知訊息,其中會包含 VALUE
,因為初始要求已指定 GET
選項:
*4<CR><LF>
$6<CR><LF>
NOTIFY<CR><LF>
$3<CR><LF>
SET<CR><LF>
$5<CR><LF>
VALUE<CR><LF>
$3<CR><LF>
abc<CR><LF>
通知 KEYNOTIFY
訊息包含通知用戶端有關SET要求(已更新值)或通知用戶端有關 DEL 或 VDEL 要求(已刪除值)時的時間戳。 時間戳包含在訊息的 MQTT v5 使用者屬性__ts中。 如需詳細資訊,請參閱一節。