色んな事を書く

シンプルさを極めたエンジニアになりたい

.NET tools を使ってみる

.NET Core ツールとは

一言でまとめると Console App を Nuget Package 化したものです。詳細は以下に記載があります。

learn.microsoft.com

マニフェストファイルを作る

パッケージのバージョン管理を行うためのファイルを作成します。下記のコマンドを実行すると、

dotnet new tool-manifest

.config/dotnet-tools.json が作成され、下記のような中身になっています。

{
  "version": 1,
  "isRoot": true,
  "tools": {
  }
}

tool をインストールしていくと、こちらの file に追加されていきます。

Strawberry Shake のツールを入れてみる

Strawberry Shake .NET の GrapchQL Client SDK です。まずは以下のコマンドでインストールされているツールを調べます。

dotnet tool list

パッケージ ID      バージョン      コマンド      マニフェスト
-----------------------------------------

上記のように何もインストールされていないことを確認します。この状態で下記コマンドを実行すると、

dotnet tool install StrawberryShake.Tools
dotnet tool list

パッケージ ID                   バージョン       コマンド                マニフェスト
-------------------------------------------------------------------------------------------------------------------------------------
strawberryshake.tools      13.9.0      dotnet-graphql    .config\dotnet-tools.json

インストールされました。version は執筆時 (2024/4/14) の最新の安定板の 13.9.0 となっています。install コマンドの構文はこちらにあります。

マニフェストファイルにも反映されました。

{
  "version": 1,
  "isRoot": true,
  "tools": {
    "strawberryshake.tools": {
      "version": "13.9.0",
      "commands": [
        "dotnet-graphql"
      ]
    }
  }
}

update してみる

tool の version を上げるには update を使います。まずは適当な version の tool をインストールします。

dotnet tool install --version 13.8.1  StrawberryShake.Tools
dotnet tool list

パッケージ ID                   バージョン       コマンド                マニフェスト
-------------------------------------------------------------------------------------------------------------------------------------
strawberryshake.tools      13.8.1      dotnet-graphql    .config\dotnet-tools.json

update をコマンドを使って version を上げます。

dotnet tool update --version 13.9.0 StrawberryShake.Tools
dotnet tool list

パッケージ ID                   バージョン       コマンド                マニフェスト
-------------------------------------------------------------------------------------------------------------------------------------
strawberryshake.tools      13.9.0      dotnet-graphql      .config\dotnet-tools.json

version は指定しなければ最新の安定板に更新されます。構文はこちらにあります。

downgrade したい場合に update コマンドは利用できません。例えば version を 13.8.0 として 13.8.1 -> 13.8.0 の downgrade は出来ません。その場合はまず uninstall コマンドを使い、13.8.0 をインストールする必要があります。

dotnet tool を自作してみる

ぜっかくなので dotnet tool を自作してみます。tutrial もあるので割と簡単に入門できます。

learn.microsoft.com

では、引数に受け取った文字列をコンソールに出力する tool を作ってみます。dotnet tool は Console App なので、Console App として project を作成します。

そして project file に以下を追加します。

    <TargetFrameworks>net6.0;net8.0</TargetFrameworks>
    <PackAsTool>true</PackAsTool>
    <ToolCommandName>Toy</ToolCommandName>
    <PackageOutputPath>./nupkg</PackageOutputPath>

そして Program.cs を以下のようにします。

// See https://aka.ms/new-console-template for more information
Console.WriteLine($"Hello! {string.Join(",", args)}");

この状態で run を実行します。

dotnet run "Hoge" "Fuga"
Hello! Hoge,Fuga

すると以下のような出力になります。

では tool として利用できるようにしていきましょう。まずは dotnet pack で NuGet 化させます。

dotnet pack

すると project file の PackageOutputPath に指定したディレクトリ (今回だと nupkg) に project 名と同じ名前の nupkg file が出来上がっているはずです。ではこれを別の prject に tool としてインストールしましょう。

dotnet tool install --add-source path-to-nupkg --version 1.0.0  {Package 名}
dotnet tool list
パッケージ ID                   バージョン       コマンド                マニフェスト
-------------------------------------------------------------------------------------------------------------------------------------
toytool                    1.0.0       Toy                 .config\dotnet-tools.json

--add-source を使って NuGet Package の Source を明示的に指定しています。今回はローカルでの検証なので、参照パスを指定しています。

ToolCommandName に指定したコマンド (今回だと Toy) を使って tool を実行してみましょう。

dotnet Toy "Hoge" "Fuga"
Hello! Hoge,Fuga

コンソールにパラメータが出力されます。意外と簡単ですね。

Azure Function の Timer Trigger の色々

TimerTrigger Attribute で指定出来る事

ここにまとまってます。

learn.microsoft.com

Schedule には CRON 式か TimeSpan を指定出来る

Schedule は Function の実行間隔を定義できるものです。その定義の仕方として CRON 式と TimeSpan が使えるよという事ですな。

CRON 式は苦手なので覚書をせんとなぁと思っています。毎回 Chat GPT に作ってもらってるので、それでいいかなぁという気持ちにもなっていますが。

秒 分 時 日 月 曜日 という順番で定義するとだけ覚えておこう。複雑なのが書けなくても最低限読めるように。

TimeSpan は正確に言うと TimeSpan 値を指定出来る、です。なので TimeSpan.FromMinutes(1) とかではなく、00:01:00 のようにせねばいけません。TimerTrigger Attributestring しか受け付けないようになっています。

1 年に一回の起動をさせたければ、365.00:00:00 のような感じになりますね。これはこれでわかりにくい。

ちなみにですが、設定名を % で囲んでおくと Schedule に設定できます。例えば、

[Function("TimerTriggerSample")]
public void Run([TimerTrigger("%SampleTimerSchedule%")] TimerInfo myTimer)
{
    _logger.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");

    if (myTimer.ScheduleStatus is not null)
    {
        _logger.LogInformation($"Next timer schedule at: {myTimer.ScheduleStatus.Next}");
    }
}

のように定義すると、%SampleTimerSchedule% の設定値をそのまま使えます。AppService 上から簡単に起動間隔を修正できます。

参考:

https://github.com/Azure/azure-webjobs-sdk-extensions/blob/030eb3966f2f8569abf010c9b5ac0b5130205b53/src/WebJobs.Extensions/Extensions/Timers/TimerTriggerAttribute.cs#L25

RunOnStartup を true に指定すると、Function Runtime の起動時に Function を実行できる

なので開発環境では true にしておくのが便利かと思います。1h に一度起動するみたいな Function の動作確認がだるいですからね。

Function Runtime の起動時という事なので、Azure にデプロイすると例えば Function App 自体の再起動時やインスタンスのスケールアウト時に Function が実行できるということになります。本番環境で true にするのは推奨されていないみたいですね。

ま、Timer Trigger の場合はスケールアウトしても単一のインスタンスのみでの実行になるみたいですけどね。

UseMonitor で Blob にスケジュールを記録するか切り替える

規定では true となっていて Timer Trigger Atrbute では false にすることが出来ません。ただドキュメントには実行間隔が 1 分以上の場合は true と書かれているので、未満にすると記録されないのかもしれません。

以下に答えが書いてありましたが、1 分未満の場合は false になるようです。つまり 1 分未満の場合は Blob に実行履歴が記録されません。前回の実行状態を把握できなくなるため、推奨されていません。

https://github.com/Azure/azure-webjobs-sdk-extensions/blob/030eb3966f2f8569abf010c9b5ac0b5130205b53/src/WebJobs.Extensions/Extensions/Timers/Scheduling/TimerSchedule.cs#L68-L87

このコードはどんな順番で呼び出されるのか?

https://github.com/Azure/azure-webjobs-sdk-extensions/blob/030eb3966f2f8569abf010c9b5ac0b5130205b53/src/WebJobs.Extensions/Extensions/Timers/Bindings/TimerTriggerAttributeBindingProvider.cs#L47

Blob に記録する実行状態

Blob に以下のような json を記録して実行状態を管理しています。

{
  "Last": "2023-03-01T16:00:35.0115543+09:00",
  "Next": "2023-03-01T16:01:35+09:00",
  "LastUpdated": "2023-03-01T16:00:35.0115543+09:00"
}

それぞれ

  • Last: 前回メソッド実行日時
  • Next: 前回 ScheduleStatus 更新した時点での、次のスケジュール実行予定日時
  • LastUpdated: 前回 ScheduleStatus 更新した日時

という値になってます。この json が TimerTrigger Function に Bind される TimerInfo.SchedleStatusマッピングされます。

と書いてもあんまりよくわからないので、実際にどのタイミングで値が決まり更新されるのかコードを追っていきます。

何故スケールアウトされても単一のインスタんで実行されるのか

Blob lease の話。

.NET の MetricsQueryClient を使って Azure Resource のメトリクスを取得する

下記を使います。

www.nuget.org

Repository はこちら。

github.com

この SDK を使えば Azure Monitor に対して ReadOnly なクエリをよしなに実行して結果を得られるみたいですね。これまでは Azure Portal 上からメトリクス見て~と判断していたものを、プログラム上から簡単に出来そうな期待が持てます。README を参考に Service Bus のメトリクスを取得してみます。

  • Service Bus のメトリクスを取ってみる
    • 特定 Queue/Topic のメトリクスを取得したい
    • 過去 3 時間分のメトリクスを 30 分間隔で集計したい
    • 過去 1 時間分のメトリクスの最大値を取得して最大値の降順に並べたい
    • カスタムメトリクスを取得する
  • Queue の状態によってどうなるのか
  • MetricResult の Error
  • Service Bus のメトリクスと MetricsId
続きを読む

Elasticsearch の index とか doc_value とか

Elasticsearch の Mapping に定義出来る下記の項目について調べたことをまとめます。Elasticsearch の version は 6.8 です。

  • index
  • doc_values
  • fielddata
  • enabled

www.elastic.co

各項目の用途のまとめです。

index doc_values fielddata enabled
用途 検索 集計/ソート/Script 集計/ソート/Script _sourceのみの保存
field 検索 text以外 textのみ objectのみ
保存先 ディスク ディスク ヒープ
キャッシュ ファイルシステムキャッシュ ファイルシステムキャッシュ フィールドデータキャッシュ

index

doc_values

www.elastic.co

検証のために以下の mapping 定義を持つ Index を作成。

{
  "doc_value_index": {
    "mappings": {
      "person": {
        "properties": {
          "age": {
            "type": "long"
          },
          "first_name": {
            "type": "keyword"
          },
          "id": {
            "type": "long"
          },
          "last_name": {
            "type": "keyword",
            "doc_values": false
          }
        }
      }
    }
  }
}

first_namedoc_valuestrue とし、last_namefalse としている。この Index に以下の 4 つのドキュメントを作成。

{
  "id": 1,
  "age": 10,
  "first_name": "山田",
  "last_name": "太郎"
},
{
  "id": 2,
  "age": 10,
  "first_name": "山田",
  "last_name": "花子"
},
{
  "id": 3,
  "age": 10,
  "first_name": "佐藤",
  "last_name": "花子"
},
{
  "id": 4,
  "age": 10,
  "first_name": "佐藤",
  "last_name": "太郎"
}

この状態で last_name, first_name それぞれに Aggregartion を行うリクエストを投げてみる。まずは first_name に対する aggregation

{
  "size": 0,
  "aggs" : {
    "first_name_agges": {
      "terms" : {
        "field" : "first_name"
      }
    }
  }
}

期待通りに集計がなされて、それぞれの名字の件数も正しい。

{
  "aggregations": {
    "first_name_agges": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "佐藤",
          "doc_count": 2
        },
        {
          "key": "山田",
          "doc_count": 2
        }
      ]
    }
  }
}

次に last_name に対する aggregation

{
  "size": 0,
  "aggs" : {
    "last_name_agges": {
      "terms" : {
        "field" : "last_name"
      }
    }
  }
}

すると下記のようなエラーメッセージ(抜粋)が返ってきた。

Can't load fielddata on [last_name] because fielddata is unsupported on fields of type [keyword]. Use doc values instead.

aggregation を行うために fielddata を読み込もうとしたみたい。keyword 型の fieldfielddata をサポートしていないとのこと。だから doc_values を使って aggregation をせねばいかんが、それを無効にしてるから失敗するって事ですね。

元々 doc_valuesfielddata がメモリを使いすぎてしまうため実装されたという前提を考えるとまぁそうなるよねって感じ。

enabled

_source について深ぼる。

www.elastic.co

【読書メモ】データ指向アプリケーションデザイン

5 章 レプリケーション

レプリケーションの目的は

  • データを地理的にユーザの近くで保持しておくことでレイテンシを下げる
  • 一部に障害があってもシステムが動作し続け可用性を高める
  • 読み取りのクエリを処理するマシン数をスケールアウトし、スループットを高める

リーダーとフォロワー

レプリカとはデータベースのコピーを保存する各ノードのこと。全てのレプリカにはデータベースのコピーが行きわたっている必要がある。そのための仕組みがリーダーとフォロワー。

  1. レプリカの一つはリーダーと呼ばれる。書き込みリクエストは必ずリーダーに送られる。リーダーは新しいデータをローカルに書き込む。
  2. 他のレプリカをフォロワーと呼ぶ。リーダーはローカルに書き込むとその変更データを各フォロワーに送信する。各フォロワーは受け取ったデータを順番にローカルに書き込む。
  3. クライアントがデータの読み取りをしたい場合は、どのレプリカから行っても良い。ただし、書き込みはリーダーのみ行える。

Cosmos の物理パーティションは最低でも 1 つのリーダーと 3 つのフォロワーにより構成されている。コンテナがどれほど小さくでも、物理パーティション一つに付き最低でも 4 つのレプリカを持つ。そのうち 1 つがリーダーで、残り 3 つがフォロワーとなる。このレプリカのまとまりをレプリカセットと呼んだりする。

5 つの一貫性レベルがあるが、あれはレプリカセット内での話か?もちろんリージョン跨いで分散されるものだろうけど。

Cosmos DB は同一論理パーティションであればトランザクション処理が行えるが、それって

って理由なのじゃなかろうか?論理パーティションキーから物理パーティションを逆引きも出来るから異なる論理パーティションでも可能かもしれないけど、SDK 側でそのチェックやると実行時エラー置きまくって使い勝手悪そう...。アプリ側で考慮すると複雑化するし、そもそも意識する必要ないものだし?

learn.microsoft.com

リーダーは変更データを各フォロワー向けに送信するとあったが、これは Change Feed の仕組みと関係あったりするのかな?この辺の解像度を上げるのは別でやる必要がある。

learn.microsoft.com

SQL Server には Always On availability group というものがある。この辺は Hyperscale と関係してんのかなぁ。これも別でやる。

learn.microsoft.com

レプリケーションは同期でやるのか非同期でやるのか問題。同期でやるメリットは、常にリーダーとフォロワーの間で読み取れるデータが一致する事。一貫性が保たれるというわけですな。非同期でやる場合はフォロワーが壊れてもリーダーへの書き込みは成功する事。可用性が高まるという事ですな。CAP 定理の話だね。

この本では一つのフォロワーを同期的に更新し、残りのフォロワーを非同期にする方法が紹介されていた。同期的に更新していたフォロワーが壊れると、非同期でやっていたフォロワーをどれか一つ同期的に変更する。これで一貫性と可用性のバランスを取る。この方法は準同期型 (semi-synschronous) って呼ばれてるらしい。

Azure Storage はチェーンレプリケーションが採用されている。これは同期型の発展形。

https://sigops.org/s/conferences/sosp/2011/current/2011-Cascais/printable/11-calder.pdf

http://www.umbrant.com.s3-website-us-west-1.amazonaws.com/blog/2016/windows_azure_storage.html

チェーンレプリケーションっていうのは、書き込みリクエストと読み取りリクエストを受けつけるノードを分け、レスポンスを返すノードを一つにするもの。書き込みはHEADから始まり、差分を全ノードに順番に送る。TAILがその差分を受け付けると、そのままレスポンスを返す。同期的なレプリケーションは書き込みリクエストを受け付けたノードが、レプリケーション先のノードからACKが返ってくるのを待機している。チェーンレプリケーションは ACKは非同期で返すからスループットが良いって事なのかな?

Chain replication : how to build an effective KV-storage (part 1/2) | by Anton Zagorskii | Coinmonks | Medium

フォロワーのセットアップはある時点のスナップショットの適応と、それ以降の変更の要求により行う。だいたいはストレージの機能として用意されてるんじゃないか?増分バックアップとかに似た考え方かも。ストレージ自体を別者に置き換えるとかなら、スナップショットを適応して W write をするとかあるなぁ。

ノード障害への対応

  1. フォロワーの障害: キャッチアップリカバリ
  2. フォロワーはローカルにリーダーからの変更通知の処理結果と、どこまで処理したのかの情報を持っている。それ以降の情報を障害からの復旧時にリーダーに要求すればよい。
  3. リーダーの障害: ファイルオーバー
    • フォロワーのいずれかを新しいリーダーとして設定しなおさなければならない
    • リーダーの障害検出には、ノード間で通信を行い一定時間以内にレスポンスが返ってくるかどうかで判断している
      • Elasticsearch のノードの間の監視も同じ考え方だろう
    • 新しいリーダーの選定にもいくつか方法がある
      • ノード同士の投票   - これも Elasticsearch で採用されているはず
      • 事前にコントローラノードを決めておき、それをリーダーにする
        • これは新しいリーダーを決めた時に一緒に新しいコントローラノードも決めるのだろうか?
  4. 新しいリーダーを書き込み先として設定する
    • 旧リーダーが復旧した時に、そのノードにも新しいリーダーを通達する必要がある

フェイルオーバーの問題

リーダーに書き込まれたが、他ノードのレプリケーションが終わる前にリーダーに障害があった場合。新しくリーダーになったノードには、その書き込み結果が失われ、旧リーダーが復旧した時にデータが競合する可能性がある。その場合は、旧リーダーへの書き込みを破棄するという手がある。クライアントから見たら最新の書き込みが結果に反映されることになるはず。ただ、新しい書き込みがなければ、書き込んだはずのデータが見えないって事が起きうるのかぁ。

Git Hub で起きたインシデントがわかりやすかった。異なるストレージ間で整合性を保たなきゃならん時って事例。MySQL のフォロワーをリーダーに昇格させた。あるテーブルは PK にオートインクリのカラムを使っており、レプリケーションが間に合わずに新リーダーが PK を再利用してしまった。PK は Redis にも保存していたので、同じ PK なのに中身が違うという不整合が発生した。MySQL -> Redis の順に保存してたんだろうけど、MySQL のリーダー -> フォロワーへのレプリケーションが間に合わなかったんだろうな。

新しいリーダーを作った時に、複数のフォロワーをリーダーにしてしまうという障害もある。複数のリーダーが誕生してしまうという事。これはスプリットブレインという問題。これが起きた場合は片方をシャットダウンさせるアプローチ (フェンシング) がとられる。そのアルゴリズムの検討もむずい。

リーダーに障害があったと判定するには、レスポンスタイムアウトをいくつに設定すればよいだろう?長くとれば復旧までの時間 (気付くまでの時間) がかかってしまうし、短くとれば不必要なフェイルオーバーが発生してしまうかもしれない (リーダーの一時的な過負荷で遅れたとかネットワークの問題とか)。その場合のフェイルオーバーは問題をさらに悪化させてしまうかもしれない。過負荷が別のノードに移るだけで、またタイムアウトを起こしてしまうとか、無限フェイルオーバーに繋がりそう?

Elasticsearch のノードの監視のアルゴリズムはどうなってるんやろう。-> 調べてみたけどわからんかった...。minimum_master_nodes をいくつにするのかとかは勉強になった。

https://www.elastic.co/jp/blog/how-to-configure-elasticsearch-cluster-better

レプリケーションログの実装

ステートメントベースのレプリケーション

リーダーが受け取った SQL をフォロワーにも送信し実行させる方法。SQL の中に NOW()RAND() といった実行時に値が確定するような式が含まれていると、リーダーとフォロワーで結果が変わる可能性がある。フォロワーでも同じ SQL を実行するため。

オートインクリメントのカラムを使ってたりすると、リーダーとフォロワー間でカウンタがずれていると不整合が生じる。Git Hub のインシデントがそんな感じだったね。複数トランザクションが並列で走って競合エラーが追起きた時、予期せぬ形でカウンタがインクリメントされることがあるからなぁ。直列にしないと怖い。

ストアドプロシージャのような非決定的な操作も不整合のきっかけになりうる。非決定的にあまりなじみがなかったが、簡単に言うと実行するたびに値が変わる可能性のあること。下の記事が勉強になった。

Deterministic and Nondeterministic Functions - SQL Server | Microsoft Learn

WAL の転送

リーダーがトランザクションログをディスクに書き込むと同時にフォロワーに送信する方法。フォロワーはトランザクションログからデータをレプリケーションすればよい。ロールフォワードみたいなもんだね。

トランザクションログってディスクブロック状のどのバイトが変更されたのかっていうかなり低レベルな内容が書かれているらしい。ディスク上の配置を変えるような操作をしたらどうなるのだろう?それ含めてフォロワーに通達されるのかな?トランザクションログは知ってたけど、具体的に中身がどんなものかは想像した事がなかった。

論理ログレプリケーション

ストレージエンジンに依存しない形のログフォーマットをを使う。WAL とは対照的?論理ログと言われたりする。

一ログが一行の変更を表す。一ログに変更のあった行を特定する情報 (PK とか)や変更後の各列の値が入っている。論理ログでテーブルが出来そうなイメージ。ログのタイプ (Insert/Update/Delete) も持っているんだろう。

トランザクションを貼って更新された場合は、ログとトランザクションのコミットログがセットになる。トランザクション ID とかで関連が作られるのだろうか?コミットログがなければその変更は破棄されるって事かなぁ。SQL Server の CDC ってこの仕組みが関係してたりして?-> 読んだけどトランザクションログをベースにしてるらしい。

What is change data capture (CDC)? - SQL Server | Microsoft Learn

論理ログには依存性がないので、ストレージエンジンが異なってもレプリケーションが出来ると。

トリガーベースレプリケーション

データベースのトリガやストアドプロシージャを使ってレプリケーションを行う方法。一部のテーブルや一部の行のみをレプリケーションしたい時に使う。アプリケーションコードをかますことも出来るので柔軟性が高い。

レプリケーションラグに関する問題

そもそもレプリーションの目的は耐障害性だけでなく、スケーリングなどもある。読み取りのスケーリングを高めたければフォロワーを追加することもある。

複数のフォロワーが存在するときに同期的なレプリケーションを選択してしまうと信頼性が低くなる可能性がある。一つのノードでもダウンすると、書き込みが行えなくなってしまうため。スケールさせようとノードを増やせばダウンする確率は上がってしまう。

非同期的なレプリケーションを採用するしかないが、それでも問題はある。それがレプリケーションのラグ。リーダーに書き込みは成功したが、フォロワーへの反映が奥rてしまってクライアントが古いデータを読み取ってしまうことがある。

自分が書いた内容の読み取り

read-after-write 一貫性の説明。これはユーザがページを読み込みなおしたときに、自分の行った更新が必ず反映されているということ。

ユーザのプロフィールみたいに、特定のユーザしか更新ができないようなデータは話が単純。自分自身のプロフィールを読み取りたい時は確定でリーダーから読み取れば良い。

他にもいくつか実装方法はあって、

  • 最終の更新から 1 分以内 (レプリケーションのラグ考慮) のデータは全てリーダーから読み取る
  • フォロワーのレプリケーションをモニタリングし、遅れている場合にはそのフォロワーからは読み取らない
  • クライアント側にデータの更新日時を持たせておき、その日時までのレプリケーションが完了しているフォロワーから読み取るようにする
    • セッションとかに持たせておくのだろうか
    • バイスまたぎのアクセスを考慮して、別ストレージへ持たせておくこともある

一貫性にもいろんな種類があるんや。

Eventual Consistencyまでの一貫性図解大全 #分散システム - Qiita

モノトニックな読み取り

非同期にレプリケーションを行っているフォロワーから読み取る場合に、過去のデータを読み取ってしまう問題がある。

複数のフォロワーがあって、それぞれのレプリケーションが完了するまでに差があるとする。あるフォロワーは完了してて、そこからデータを読み取ったのち、完了していないフォロワーからデータを読み取ってしまったとする。するとクライアントはなぜかデータが古いものに戻ってしまうという体験をする。

この問題の解決策として、あるユーザが読み取るフォロワーを一つに固定してしまうというものがある。ユーザが異なれば読み取るフォロワーが異なっても OK。

一貫性のあるプレフィックス読み取り

レプリケーションのラグにより、書き込みを行った順序でデータが反映されなかったことで読み取り者にとって不可解な状態が生じてしまうこと。

例えば、チャットアプリがあって、書き込みとしては A さん -> B さんの順に行ったのに、レプリケーションが B さん -> A さんの順になり観測者から不可解な状態になっているように見えてしまうこと。

パーティション跨ぎのデータベースで問題になる。対策としては因果関係のある書き込みは同じパーティションに書き込むようにするということ。因果関係のある書き込みというのがピンとこない。

例えばステートメントベースのレプリケーションで、書き込み順序が入れ替わったことにより不整合な状態が発生してしまうとかなら想像がつく。順序の問題て、where 句に引っ掛からなくなってしまったとかね。

マルチリーダーレスアプリケーション

ユースケース

前提として得られるメリットと加わる複雑さにより、単一のデータセンター内では実装されない。複数のデータセンターにそれぞれリーダーを配置する構成が取られる。

以下の観点からマルチリーダ構成の特徴を考える。

単一リーダー マルチリーダー
パフォーマンス 書き込みは必ずリーダに行われるので、データセンターを跨いだリクエストが飛ぶ。レイテンシを防ぐためにユーザとの距離を近くしたいという目的があれば、それに反することにもなる。 ローカルのデータセンターのリーダーにのみ書き込みを行えば良いので、単一リーダーより優れる可能性がある。
耐障害性 データセンターに障害があれば、別データセンターのフォロワーをリーダーにする 各データセンターは独立して動き、データセンターが復旧したらレプリケーションを追いつかせるだけで良い

それぞれのリーダーで同じデータが更新された時のコンフリクト解消はどうやってるんだろうなぁ。

クライアント用のデバイスもある種のリーダーとして考えることができる。アプリがオフラインになったときに書き込まれたデータがオンラインになった瞬間に反映されなければならないとする。この場合、デバイスのローカルストレージがリーダーであり、オンラインになった瞬間に別リーダーにレプリケーションしていると見ることもできる。

PWA とか作ってたけど、この辺全然考えたことなかったなぁ。

書き込みの衝突処理

同じデータに対して複数のクライアントから同時に書き込みを行う場合を考える。マルチリーダーの場合、ローカルリーダーをそれぞれ持つので、両書き込みリクエストは成功する。問題はそれをレプリケーションしたときにコンフリクトが発生してしまうということ。

シングルリーダーであれば先の書き込みリクエストを受け付けたときに対象データにロックをかけられるが、マルチリーダーの場合はそんなことできない。

衝突を発生させなければいいという考え方もあって、その場合はあるデータに対する書き込みは全て同じリーダーにルーティングするという方法がある。内部実装はシングルリーダーだね。データが異なれば別リーダーに書き込めばいい。

一貫した状態への収束

レプリケーション時に衝突が発生したデータは、最終的にどの状態になるのか全てのリーダーで収束させなければならない。でないと不整合な状態になってしまう。収束の方法はいくつかあって、

  1. 書き込み時にタイムスタンプを記録し、最大のデータで収束させる
  2. タイムスタンプが古いデータはロストしてしまう
  3. リーダーに ID をつけておき、ID が最大のリーダーに書き込まれたデータに収束させる
  4. ID の小さなリーダーへの書き込みはロストしてしまう
  5. 書き込まれたデータをソートし、結合させる (B, C みたいに)
  6. 同じデータへの書き込み内容を全て記録しておき、後でユーザに選択させる

ユーザの属性によって何に収束させるのか決めてもいいと思った。例えばユーザの権限とか (管理者であればそれに収束させる)、作成者の書き込みを優先させるとか。複雑なのと、仕様としてどうなんだろうという気持ちになる。

トポロジー

リーダーたちが取りうるトポロジーはいくつか種類がある。

SQL Server はどのトポロジーだろうと思って調べたけど、いい記事が見つからなかった。代わりに骨太な記事を見つけたから、どこかで読まねば。

Types of Replication - SQL Server | Microsoft Learn

以下のような時系列を考える。

  1. リーダー A へ 1 という値を書き込む
  2. リーダー A の変更を B,C,D へレプリケーションする
  3. リーダー B に A の変更を反映させる
  4. リーダー B に 1 を 3 にするという書き込みを行う
  5. リーダー B の変更を A,C,D にレプリケーションする
  6. リーダー C に A, B の変更が逆の順序で到着する

これは一貫性のあるプレフィクス読み取りに似た因果律の問題。解決方法としてはバージョンベクトルと呼ばれる方法がある。

リーダーレスレプリケーション

AmazonDynamo はこのアーキテクチャを採用している。クライアントは書き込みリクストと読み取りリクエストを並列に複数のレプリカに送っている。この時、帰ってくるデータは異なる可能性があるが、最も新しいものをクライアントに返す。そのためにデータにバージョンを持たせている。MVCC の分散システムみたいなイメージ?

複数レプリカのうち、いくつのレプリカへの書き込み失敗なら許容できるのか、また読み取りを行うレプリカ数を幾つにすれば良いのか。この辺りは定義がなされていて、全レプリカ数を n、書き込み時の成功数の最低数を w、最低読み取り数を r とすると、w + r > n が成り立っていれば書き込みに失敗しても許容できる。

ワークロードが読取に特化しているなら、w1rn-1 と調整しても良い。一般的には n は奇数で、w = r = (n+1)/2 (切り上げ) が成り立つように調整される。

この式どこかで見たことあると思ったが、Elasticsearch の master node を決める投票数の推奨値と似てるな。関係ない気がするが。

Elasticsearchの運用に関する典型的な4つの誤解 | Elastic Blog

レプリカがダウンしてこの士気を満たさなくなった時、書き込みや読み取りをエラーとする

KQL メモ

KQL を書くことが多くなってきたので自分用のメモを残しておきます。構文とか便利メソッドのまとめ集です。基本的には公式ドキュメントにあるものを色々試した結果を載せていきます。

クエリの対象は Azure Function の実行ログとしています。

一分単位で Error Log の件数を集計し、折れ線グラフにする

requests
| where customDimensions['LogLevel'] == 'Error'
| summarize count() by format_datetime(timestamp, 'yyyy/mm/dd hh:mm')
| order by  timestamp
| render timechart 

where の代わりに filter でも同じことが出来ます (filter customDimensions['LogLevel'] == 'Error')。

format_datetime で datetime 型の値のフォーマットを決めています。

format_datetime() - Azure Data Explorer & Real-Time Analytics | Microsoft Learn

render operator ではグラフの種類を入力します。timechart は折れ線グラフで、必ず datetime を x 軸に指定しなければいけません。既定の値は table で KQL の結果が表になっているのはこのためです。

render operator - Azure Data Explorer & Real-Time Analytics | Microsoft Learn

【読書メモ】自分の意見で生きていく

1 章 意見とは何か

意見とは「正解のない問題」に対して自分なりの考えを持つこと。「正解のある問題」には意見は持てない。正解か誤答しかない。

正解のない問題とは、「開発生産性を高めるにはどうすれば良いのか」といったようなもの。こういった類の問いは答えがないので、調べても意味がない。自分の頭で考え意見を出す必要がある。

正解のある問題とは、「1+1=2」といったようなもの。こういった類の問いは調べることで答えを見つけ出す必要がある。

正解のない問題には正解のある問題が前提となっている場合がある。仕様や制限といったものも該当しそう。論理的に正しい、というのも正解になると思う。

こういった材料を調べ、最終的な自分の意見は自分の頭で考える必要がある。

問へ向き合う最初の一歩目は正解があるかないかを見極めること。どこまで歯応えがあり、どこからが答えがないのか見極めること。アプローチが異なるため。

意見には正解がないので、正しい誤りを議論するのは時間の無駄。ただし、意見の前提となっている正解のある問題には誤りが存在する。意見を正しく導くには、正しい情報を集める事が必要なのだと思う。

2 章 反応だけではだめな理由

反応には価値がないからだね。意見にするためには自分の立場を明確にしないといけない。

3 章 SNS 時代に自分を創る

そもそも不特定多数の人間に認めてもらいたいという欲求がないので、承認欲求のために SNS ~という入りが近い出来ない。

という風に読み替えてみる。

発信すべき内容や情報がどれほど有益化はあまり関係がない。大切なのは「その人がどんな人なのか」が明確になる内容であること。読み手に解釈をゆだねるようなものはダメなんだろうな。セルフブランディングって感じ。

発信プラットフォームは絞った方が良い。Youtube のコメント欄、X での投稿、ブログのポストなど散らばっていては全部自分のものであるという判定をしてもらいにくい。X をベースに色々なメディアを使い分けるのがよさそう。

4 章 生きづらさから脱却しよう

生きづらさの原因が「本来答えのない問題 (生き方など) に答えを定義されてしまう」や「他人の意見を正解と思い込み、それと異なることが恥ずかしいと感じてしまう」ということにあるのではないかという事。

答えの無い問題に対して、答えを定義してくれる人が現れると安心するって側面もある。

「なぜ?」と良く人に問うのだけど、問い方を変えたほうが良いかもしれない。私の根底には「どうしてそういう意見を持ったのか教えてください」というのがあるが、他の人が「正解と違っていることを言ったから問い詰められている」と感じてしまうとやばい。「素晴らしい意見ですね!ぜひその意見に至った過程を教えてください!」みたいな聞き方をすると気持ちいかも。

他人に認めてもらう事の前にどんな自分を認めてもらいたいのか考えよう。そのために自己認識を高めていこう。いくら他人に認められても、それが自分の自己認識と異なっていればつらみが生まれる。

5 章 リーダーシップの最初の一歩

リーダーシップの第一歩は自分の意見を持つこと。集団やコミュニティに属している以上は何かしらの自分の意見を持つべき。言われたことをやるや常に誰かの意見に賛成ではその人がその集団に所属する価値がなくなってしまう。何事においても自分なりの意見を持つことが大切。

自分の意見を考える訓練としては、今の自分には関係のない題材のほうが良い。いざ当事者として何かを考えないといけなくなると、状況を冷静に見られない可能性があるため。関係ないようなことは冷静に情報を集め、複雑にしすぎずに考えることが出来る。

知識がないから意見が言えない、違うと思う。でも絶対にこれだと言いきれる意見を持つにはそれなりに勉強は必要だと思う。勉強して空じゃないと意見は言っちゃいけないとかじゃなくて、自分の意見に自信を持つための勉強は必要だよね、とは思う。でないと議論にならん。

議論をするためにはまず全員の意見が出そろう必要があると言っている?反応として紹介されているコメントも誰かの意見を深ぼっていくためのものじゃないのと思ったけど、どうなんだろう。質問も反応なのであれば、反応のない議論はないだろうね。各々が意見を主張しかしない場だと何もまとまらんだろう。立場を明確にしたうえでってのが大事なんでしょうね。

お互い気持ちよくコミュニケーションをとるために反応ばっかするなよって事かな。会議を開いている以上結局やりたい事は何かをよりよくすることで、そのためにみんなリーダーシップを持ちましょうって事かな。

意見が言えないのは思考をしていないからね。試行がまとまらないというのも含まれると思うけど。特に色んな事を知りすぎると足踏みが遅くなってしまう事ってあるから、そういう人は考える事が多すぎて意見にするまで時間がかかっているのではないだろうか。それでも意見を出せと言われればフィーリングで出せるんだけどね。

6 章 オリジナルの人生へ

時代は変わったんだね。