状態ストア プロトコル
状態ストアは、Azure IoT Operations クラスター内の分散ストレージ システムです。 状態ストアでは、MQTT ブローカーの MQTT メッセージと同じ高可用性の保証が提供されます。 MQTT5/RPC プロトコルのガイドラインに従って、クライアントは MQTT5 を使用して状態ストアと対話する必要があります。 この記事では、独自の状態ストア クライアントを実装する必要がある開発者向けのプロトコル ガイダンスを提供します。
概要
状態ストアでは、次のコマンドがサポートされています。
SET
<keyName><keyValue><setOptions>GET
<keyName>DEL
<keyName>VDEL
<keyName><keyValue> ## 所定の <keyName> を削除します。これは、値が <keyValue> である場合にのみ行います
このプロトコルでは、次の要求応答モデルが使用されます。
- 要求。 クライアントは、明確に定義された状態ストア システム トピックに要求を発行します。 要求を発行するために、クライアントは、次のセクションで説明する必要なプロパティとペイロードを使用します。
- 応答。 状態ストアは、要求を非同期的に処理し、クライアントが最初に指定した応答トピックで応答します。
次の図は、要求と応答の基本的なビューを示しています。
状態ストアのシステム トピック、QoS、必要な MQTT5 プロパティ
状態ストアと通信するには、クライアントが次の要件を満たしている必要があります。
- MQTT5 を使用します。 詳細については、MQTT 5 仕様を参照してください。
- QoS 1 (サービス品質レベル 1) を使用します。 QoS 1 は MQTT 5 仕様で説明されています。
- MQTT ブローカーのクロックから 1 分以内のクロックがあります。
状態ストアと通信するには、PUBLISH
クライアントがシステム トピックに要求statestore/v1/FA9AE35F-2F64-47CD-9BFF-08E2B32A0FE8/command/invoke
する必要があります。 状態ストアは Azure IoT Operations の一部であるため、起動時に暗黙的 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 クライアントを切断します。 - 相関関係データ。 状態ストアが応答を送信すると、初期要求の関連付けデータが含まれます。
次の図は、要求と応答の展開ビューを示しています。
サポートされているコマンド
The commands SET
コマンド、GET
コマンド、DEL
コマンドは、予期したとおりに動作します。
SET
コマンドが設定し、GET
コマンドが取得する値は、任意のバイナリ データです。 値のサイズは、MQTT ペイロードの最大サイズと、MQTT ブローカーとクライアントのリソース制限によってのみ制限されます。
SET
オプション
SET
コマンドは、基本 keyValue
フラグと keyName
フラグ以外にも、次のようなオプションのフラグを提供します。
NX
. キーがまだ存在しない場合にのみ、キーを設定できるようにします。NEX <value>
. キーが存在しない場合、またはキーの値が既に < 値>に設定されている場合にのみ、キーを設定できるようにします。NEX
フラグは通常、クライアントがキーの有効期限 (PX
) を更新するために使用されます。PX
. キーの有効期限が切れるまでの期間 (ミリ秒単位)。
VDEL
オプション
VDEL
コマンドは、DEL
このコマンドの特殊なケースです。 DEL
は無条件で指定の keyName
を削除します。 VDEL
には、keyValue
という別の引数が必要です。 VDEL
は、指定の keyName
を、同じ keyValue
を持っている場合にのみ削除します。
ペイロード形式
状態ストア PUBLISH
のペイロード形式は、RESP3 に着想を得た形式です。これは、Redis が使用する基になるプロトコルです。 RESP3 は、GET
や SET
などの動詞と、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>
Note
SET
には、「バージョン管理とハイブリッド論理クロック」セクションで説明されているように、MQTT5 の追加のプロパティが必要です。
応答形式
状態ストアが無効な RESP3 ペイロードを検出しても、要求者の Response Topic
に対する応答が返されます。 無効なペイロードの例としては、無効なコマンド、無効な RESP3、整数オーバーフローなどがあります。 無効なペイロードは文字列 -ERR
で始まり、詳細が含まれています。
Note
存在しないキーに対する GET
、DEL
、VDEL
要求は、エラーとは見なされません。
クライアントが無効なペイロードを送信した場合、状態ストアは次の例のようなペイロードを送信します。
-ERR syntax error
SET
の応答
SET
要求が成功すると、状態ストアは次のペイロードを返します。
+OK<CR><LF>
NX または NEX 設定オプションで、キーを設定できないことを意味する条件チェックが指定されているために SET 要求が失敗した場合、状態ストアは次のペイロードを返します。
-1<CR><LF>
GET
の応答
存在しないキーに対して GET
要求が行われると、状態ストアは次のペイロードを返します。
$-1<CR><LF>
キーが見つかると、状態ストアは次の形式で値を返します。
${NumberOfBytes}<CR><LF>
{KEY-VALUE}
1234
値を返す状態ストアの出力は、次の例のようになります。
$4<CR><LF>1234<CR><LF>
DEL
と VDEL
応答
状態ストアは、削除要求で削除した値の数を返します。 現在、状態ストアで削除できる値は一度に 1 つだけです。
:{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-and-hybrid-logical-clocks) を参照してください。 |
クォータを超えました | 状態ストアには、それに格納できるキーの数のクォータがあり、このクォータは、指定された MQTT ブローカーのメモリ プロファイルに基づいています。 |
構文エラー | 送信されたペイロードが、状態ストアの定義に準拠していません。 |
承認されていません | 認証エラー |
不明なコマンド | コマンドが認識されません。 |
誤った数の引数 | 想定されている引数の数が正しくありません。 |
タイムスタンプがありません | クライアントで SET を実行する場合、そのタイムスタンプを表す HLC として MQTT5 ユーザー プロパティ _ts を設定する必要があります。 |
形式に誤りがあるタイムスタンプ | _ts またはフェンス トークンのタイムスタンプが不正です。 |
キーの長さがゼロです | 状態ストアでは、キーの長さをゼロにすることはできません。 |
バージョン管理とハイブリッド論理クロック
このセクションでは、状態ストアがバージョン管理を処理する方法について説明します。
ハイブリッド論理クロックとしてのバージョン
状態ストアは、格納する各値のバージョンを保持します。 状態ストアでは、単調に増加するカウンターを使用してバージョンを維持できます。 代わりに、状態ストアはハイブリッド論理クロック (HLC) を使用してバージョンを表します。 詳細については、HLC の元の設計に関する記事と、HLC の背後にある意図に関する記事を参照してください。
状態ストアでは、次の形式を使用して HLC を定義します。
{wallClock}:{counter}:{node-Id}
wallClock
は Unix エポック以降のミリ秒数です。 counter
と node-Id
は、一般的に HLC として機能します。
クライアントは SET
を実行する場合、クライアントの現在のクロックに基づいて、そのタイムスタンプを表す HLC として __ts
MQTT5 ユーザー プロパティを設定する必要があります。 状態ストアは、応答メッセージ内の値のバージョンを返します。 応答は HLC としても指定され、__ts
MQTT5 ユーザー プロパティも使用されます。 返される HLC は、最初の要求の HLC より常に大きくなります。
値のバージョンの設定と取得の例
このセクションでは、値のバージョンの設定と取得の例を示します。
クライアントは keyName=value
を設定します。 クライアント クロックは 10 月 3 日午後 11:07:05 GMT です。 Unix エポック以降、クロック値は 1696374425000
ミリ秒です。 状態ストアのシステム クロックがクライアント システム クロックと同じであるとします。 クライアントは、前述のように SET
コマンドを実行します。
次の図は、SET
コマンドを示しています。
初期セットの __ts
(タイムスタンプ) プロパティには、クライアント ウォール クロックとしての 1696374425000
、0
としてのカウンター、CLIENT
としてのノード ID が含まれます。 応答では、状態ストアが返す __ts
プロパティには、1 ずつインクリメントされるカウンター wallClock
と、ノード ID が StateStore
として含まれます。 状態ストアは、HLC 更新の動作方法に基づいて、クロックが先行している場合は、より高い wallClock
値を返す可能性があります。
このバージョンは、成功した GET
、DEL
、VDEL
の各要求でも返されます。 これらの要求では、クライアントは __ts
を指定しません。
次の図は、GET
コマンドを示しています。
Note
状態ストアから返されるタイムスタンプ __ts
は、最初の SET
要求で返されたものと同じです。
特定のキーが後で新しい SET
で更新された場合、プロセスは似ています。 クライアントは、現在のクロックに基づいて要求 __ts
を設定する必要があります。 状態ストアは値のバージョンを更新し、HLC 更新規則に従って __ts
を返します。
時刻のずれ
状態ストアは、状態ストアのローカル クロックより 1 分以上前の __ts
(および __ft
) を拒否します。
状態ストアは、状態ストアのローカル クロックの背後にある __ts
を受け入れます。 HLC アルゴリズムで指定されているように、状態ストアはキーのバージョンをローカル クロックに設定します。これは、こちらのほうが大きいためです。
ロック トークンとフェンス トークン
このセクションでは、ロック トークンとフェンス トークンの目的と使用方法について説明します。
背景
状態ストアを使用する MQTT クライアントが 2 つ以上あるとします。 どちらのクライアントも、特定のキーに書き込もうとします。 状態ストア クライアントには、一度に 1 つのクライアントのみが特定のキーを変更できるように、キーをロックするメカニズムが必要です。
このシナリオの例は、アクティブ システムとスタンバイ システムで発生します。 両方とも同じ処理を実行する 2 つのクライアントがあり、この処理に同じ状態ストア キーのセットが含まれる可能性があります。 特定の時点で、いずれかのクライアントがアクティブであり、そのアクティブなシステムが停止またはクラッシュしたときにすぐに引き継ぐように、もう一方のクライアントがスタンバイしています。 理想的には、一度に 1 つのクライアントのみが、状態ストアに書き込むようにする必要があります。 ただし、分散システムでは、両方のクライアントがアクティブであるかのように動作し、同時に同じキーへの書き込みを試みる可能性があります。 このようなシナリオでは、競合状態が生じます。
状態ストアには、フェンス トークンを使用してこの競合状態を防ぐメカニズムが用意されています。 フェンス トークンと、競合状態から保護するために設計されたクラスの詳細については、この記事を参照してください。
フェンス トークンの取得
この例では、次の要素があることを前提としています。
Client1
およびClient2
。 これらのクライアントは、アクティブとスタンバイのペアとして機能する状態ストア クライアントです。LockName
. ロックとして機能する状態ストア内のキーの名前。ProtectedKey
. 複数の書き込み元から保護する必要があるキー。
クライアントは、最初の手順としてロックを取得しようとします。 これらは SET LockName {CLIENT-NAME} NEX PX {TIMEOUT-IN-MILLISECONDS}
を行うことによって、ロックを取得します。 Set オプションで NEX
フラグは、次のいずれかの条件が満たされた場合にのみ SET
が成功するのを意味することを思い出してください。
- このキーが空です
- このキーの値は既に<値>に設定されており、
PX
はタイムアウトをミリ秒単位で指定しています。
Client1
が最初に SET LockName Client1 NEX PX 10000
の要求を受け取るとします。 この要求により、LockName
の所有権が 10,000 ミリ秒で付与されます。 Client2
が SET LockName Client2 NEX ...
を試行したときに、Client1
がロックを所有していた場合、NEX
フラグは、Client2
要求が失敗することを意味します。 Client1
は、ロックの取得に使用したのと同じ SET
コマンドを送信して、このロックを更新する必要があります (Client1
が引き続き所有権を持つ場合)。
Note
SET NX
は概念的には AcquireLock()
と同等です。
SET 要求でのフェンス トークンの使用
Client1
が LockName
に対して SET
("AcquireLock") を正常に行うと、状態ストアは、MQTT5 ユーザー プロパティ __ts
でハイブリッド論理クロック (HLC) として LockName
のバージョンを返します。
クライアントは、SET
要求を実行するときに、必要に応じて "フェンス トークン" を表すための MQTT5 ユーザー プロパティ __ft
を含むことができます。 __ft
は HLC として表されます。 特定のキーと値のペアに関連付けられているフェンス トークンは、ロックの所有権チェックを提供します。 フェンス トークンはどこからでも取得できます。 このシナリオでは、LockName
のバージョンから取得する必要があります。
次の図は、LockName
で SET
要求を実行する Client1
プロセスを示しています。
次に、Client1
は、要求内の __ft
プロパティの基礎として変更されていない __ts
プロパティ (Property=1696374425000:1:StateStore
) を使用して、ProtectedKey
を変更します。 すべての SET
要求と同様に、クライアントは ProtectedKey
の __ts
プロパティを設定する必要があります。
次の図は、ProtectedKey
で SET
要求を実行する Client1
プロセスを示しています。
要求が成功した場合、ProtectedKey
のこの時点から、SET
要求で指定されたトークン以上のフェンシング トークンが必要です。
フェンス トークン アルゴリズム
値が時刻のずれの最大値内である場合、状態ストアはキーと値のペアの __ts
のために HLC を受け入れます。 ただし、フェンス トークンについても同じことが当てはまるわけではありません。
フェンス トークンの状態ストア アルゴリズムは次のとおりです。
- キーと値のペアに関連付けられたフェンス トークンがなく、
SET
要求が__ft
を設定する場合、状態ストアは、関連付けられた__ft
をキーと値のペアと一緒に格納します。 - キーと値のペアにフェンス トークンが関連付けられている場合:
SET
要求で__ft
が指定されていない場合、要求を拒否します。SET
要求で、キーと値のペアに関連付けられているフェンス トークンよりも古い HLC 値を持つ__ft
が指定されている場合は、要求を拒否します。SET
要求で、キーと値のペアに関連付けられているフェンシング トークンと等しいか新しい HLC 値を持つ__ft
が指定されている場合は、要求を受け入れます。 状態ストアは、キーと値のペアが新しい場合は、そのフェンス トークンを更新して、要求に設定されるトークンにします。
キーがフェンス トークンでマークされた後、要求が成功するためには、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 の大文字の 16 進エンコード表現であり、keyName
は、変更されたキーの 16 進エンコード表現です。 このエンコードの場合、状態ストアは「RFC 4648 - The Base16, Base32, and Base64 Data Encodings」の Base 16 エンコード規則に従います。
clients/statestore/v1/FA9AE35F-2F64-47CD-9BFF-08E2B32A0FE8/{clientId}/command/notify/{keyName}
例として、MQTT ブローカーでは、変更されたキー名 SOMEKEY
を含む、client-id1
に送信される NOTIFY
メッセージをトピックにパブリッシュします。
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
に変更されたときに送信される通知メッセージを示しており、初期要求で GET
オプションが指定されたために VALUE
が含まれています。
*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 の一部に含まれています。 詳細については、セクション「ハイブリッド論理クロックとしてのバージョン」を参照してください。