分散システムはしばらく存在しており、それらを設計する際にはすでに確立された一般的なパターンがあります。今日は、そのうちの1つ、”ロック”について話し合います。
単純に言えば、ロックはプロセスが特定のアクションを実行するためにリソースに排他的にアクセスする方法です。たとえば、ストレージアカウントにたくさんのBlobがあるとしましょう。重複処理を避けるために各Blobを処理するために、サービスの1つのインスタンスが必要です。その方法は、Blobにロックを取得し、処理を完了して解放することです。ただし、プロセスがロックを解放する前に失敗する場合、プロセスがダウンしたためかネットワークの分断のためかにかかわらず、リソースが無期限にロックされたままになるという潜在的な問題が発生します。これによりデッドロックやリソース競合が発生する可能性があります。
デッドロックを防ぐために採用できる戦略の1つは、タイムアウトまたはリースベースのロックを使用することです。
タイムアウトロック
- この場合、プロセスはロックをリクエストする際に事前に定義されたタイムアウトがあります。ロックがタイムアウト前に解放されない場合、システムは最終的にロックが解放されることを保証します。
リースロック
-
リースベースのロックでは、タイムアウト機構とともにリニューアルリースAPIが提供されます。ロックを保持しているプロセスは、リースが切れる前にこのAPIを呼び出してリソースへの排他的なアクセスを維持する必要があります。プロセスが時間内にリースを更新できない場合、ロックは自動的に解放され、他のプロセスが取得できるようになります。
タイムアウトとリースベースのロックの長所と短所
Pros | Cons | |
---|---|---|
タイムアウトベースのロック | 実装が簡単 | タイムアウトの注意深い選択が必要 |
永久ロックを防止 | 処理が完了していない場合、リースを更新する手段がない | |
リースベースのロック | 早期のロックの期限切れのリスクを軽減 | リースの更新機構が必要 |
処理が完了するまでプロセスはリースのリクエストを継続できる |
上記の両戦略は、分散システムにおけるプロセスの障害やネットワークの分割から迅速に回復する方法です。
Azure Storageとのリースロック戦略
Azure Storageとのリースロック戦略の使用方法を見てみましょう。これはタイムアウトロック戦略もカバーしています。
ステップ1: Storage Blob Nugetをインポート
この記事の執筆時点での最新バージョンは”12.23.0″です。最新バージョンは<Azure Storage Blobsで見つけることができます。
<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.23.0" />
</ItemGroup>
ステップ2: リースを取得
以下はリースを取得するためのコードです。
public async Task<string> TryAcquireLeaseAsync(string blobName, TimeSpan durationInSeconds, string leaseId = default)
{
BlobContainerClient blobContainerClient = new BlobContainerClient(new Uri($"https://{storageName}.blob.core.windows.net/processors"), tokenCredential, blobClientOptions);
BlobLeaseClient blobLeaseClient = blobContainerClient.GetBlobClient(blobName).GetBlobLeaseClient(leaseId);
try
{
BlobLease lease = await blobLeaseClient.AcquireAsync(durationInSeconds).ConfigureAwait(false);
return lease.LeaseId;
}
catch (RequestFailedException ex) when (ex.Status == 409)
{
return default;
}
}
- まず、Blob Container Clientを作成し、取得したい特定のBlobに対するBlob Clientを取得します。
- 次に、”Acquire Async”メソッドは特定の期間のためにリースを取得しようとします。取得が成功した場合、リースIDが返されます。取得に失敗した場合は409 (競合のステータスコード)がスローされます。
- ここで重要なのは”Acquire Async”メソッドです。コードの残り部分は必要に応じて調整/編集できます。
ステップ3: リースを更新
- “Renew Async”はリースを更新するために使用されるStorage .NET SDKのメソッドです。
- 更新が失敗した場合、例外がスローされ、失敗の原因が表示されます。
public async Task ReleaseLeaseAsync(string blobName, string leaseId)
{
BlobLeaseClient blobLeaseClient = this.blobContainerClient.GetBlobClient(blobName).GetBlobLeaseClient(leaseId);
await blobLeaseClient.RenewAsync().ConfigureAwait(false);
}
ステップ4: リース取得およびリース更新メソッドのオーケストレーション
- 最初に、「Try Acquire Lease Async」を呼び出して、ステップ2からリース識別子を取得します。成功すると、バックグラウンドタスクが開始され、「Renew Lease Async」をステップ3からX秒ごとに呼び出します。タイムアウトとリース更新メソッドの呼び出し間に十分な時間があることを確認してください。
string leaseId = await this.blobReadProcessor.TryAcquireLeaseAsync(blobName, TimeSpan.FromSeconds(60)).ConfigureAwait(false);
Task leaseRenwerTask = this.taskFactory.StartNew(
async () =>
{
while (leaseId != default && !cancellationToken.IsCancellationRequested)
{
await Task.Delay(renewLeaseMillis).ConfigureAwait(false);
await this.blobReadProcessor.RenewLeaseAsync(blobName, leaseId).ConfigureAwait(false);
}
},
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
- ステップ3
では、不要になった場合にリースの更新タスクを適切に停止するために使用されます。
ステップ5: リースのキャンセル
- “Cancel Async”メソッドが呼び出されると、ステップ4の「IsCancellationRequested」がtrueになり、もはやwhileループに入らずにリースの更新を要求しません。
await cancellationTokenSource.CancelAsync().ConfigureAwait(false);
await leaseRenwerTask.WaitAsync(Timeout.InfiniteTimeSpan).ConfigureAwait(false);
ステップ6: リースの解放
最後に、リースを解放するには、「Release Async」メソッドを呼び出すだけです。
public async Task ReleaseLeaseAsync(string blobName, string leaseId)
{
BlobLeaseClient blobLeaseClient = this.blobContainerClient.GetBlobClient(blobName).GetBlobLeaseClient(leaseId);
await blobLeaseClient.ReleaseAsync().ConfigureAwait(false);
}
結論
ロックは、リソースへの排他的アクセスを得るための分散システムにおける基本的なパターンの1つです。オペレーションのスムーズな実行のために、それらを扱う際に落とし穴を心に留めておくことが必要です。Azure Storageを使用することで、無期限のブロッキングを防ぎ、同時にロックの維持方法に弾力性を提供する効率的なロックメカニズムを実装できます。
Source:
https://dzone.com/articles/locks-in-distributed-systems-timeout-lease-based