KafkaKRU Meetup Review
KafkaKRU 밋업 리뷰: Event Sourcing부터 리더 파티션 밸런싱까지
2024년 11월 21일, 서울 중구 삼화타워에서 열린 KafkaKRU 밋업에 참석했습니다. 사실 대기자 명단에 있었어서 참석이 어려운 상태였던 것 같지만, 열정으로 봐주셔서 다행히 쫓겨나지는 않았습니다. 결과적으로는 예상을 훨씬 뛰어넘는 값진 시간이었어요.
2024년 11월 21일, 서울 중구 삼화타워에서 열린 KafkaKRU 밋업에 참석했습니다. 사실 대기자 명단에 있었어서 참석이 어려운 상태였던 것 같지만, 열정으로 봐주셔서 다행히 쫓겨나지는 않았습니다. 결과적으로는 예상을 훨씬 뛰어넘는 값진 시간이었어요.
複数のデバイスでクラウドストレージを使用していますか?それなら、おそらく衝突ファイルが少しずつ増えていく経験をしたことがあるでしょう。
隙あらば増えていく衝突ファイル
ファイルが同期される前に編集作業を行ったり、ネットワークの問題で同期が少し遅れたりするなど、さまざまな理由で衝突ファイルは増え続けます。
個人的には常にきれいな状態を好むので、こうしたダミーファイルを定期的に削除しています。
しかし、今日は何だか繰り返しの作業が面倒に感じます。久しぶりにシェルスクリプトを書いて、開発者らしさを出してみようと思います。
最近、ブログを新しいプラットフォームに移行する作業を行いました。様々な問題に直面し、その解決策をメモしておいたので、他の人にも役立つかもしれないと思い、ここに詳細な移行プロセスを記録します。
miseを使えば、どの言語やツールを使っても正確に必要なバージョンを使用でき、他のバージョンに切り替えたり、プロジェクトごとにバージョンを指定することも可能です。ファイルで明示するため、チームメンバー間でどのバージョンを使うか議論するなどのコミュニケーションコストも減らせます。
これまでこの分野で最も有名だったのはasdfでした[^fn-nth-1]。しかし、最近miseを使い始めてからは、miseの方がUXの面で少し優れていると感じています。今回は簡単な使用例を紹介しようと思います。
意図的かどうかは分かりませんが、ウェブページさえも似ています。
mise
(「ミーズ」と発音するようです)は開発環境設定ツールです。この名前はフランス料理の用語に由来し、大まかに「設定」または「所定の位置に置く」と訳されます。料理を始める前にすべての道具と材料が所定の位置に準備されている必要があるという意味だそうです。
簡単な特徴を列挙すると以下の通りです。
複数のクライアントリクエストを同時に処理できるサーバアプリケーションの実装は、今や非常に簡単です。Spring MVCを使うだけで、すぐに実現できます。しかし、エンジニアとして、その基礎原理に興味があります。本記事では、明らかに見えることを問い直しながら、マルチコネクションサーバを実装するための考慮事項について考察していきます。
例のコードはGitHubで確認できます。
最初の目的地は「ソケット」です。ネットワークプログラミングの観点から、ソケットはネットワーク上でデータを交換するための通信エンドポイントです。「ファイルのように使用される」という説明が重要です。これは、ファイルディスクリプタ(fd)を通じてアクセスされ、ファイルと同様のI/O操作をサポートするためです。
ソケットはIP、ポート、および相手のIPとポートを使用して識別できますが、fdを使用する方が好まれます。これは、接続が受け入れられるまでソケットには情報がなく、単純な整数(fd)以上のデータが必要だからです。
ソケットを使用してサーバアプリケーションを実装するには、次の手順を踏む必要があります:
PostgreSQLでは、FOR UPDATEロックはトランザクション内でSELECTクエリを実行する際にテーブルの行を明示的にロックするために使用されます。このロックモードは、選択された行がトランザクションが完了するまで変更されないようにし、他のトランザクションがこれらの行を変更したり、競合するロックをかけたりするのを防ぐために使用されます。
例えば、特定の顧客がチケット予約プロセスを進めている間に他の顧客がデータを変更するのを防ぐために使用されることがあります。
この記事で検討するケースは少し特殊です:
select for update
はどのように動作するのか?PostgreSQLでは、select for update
句はトランザクション分離レベルによって異なる動作をします。したがって、各分離レベルでの動作を確認する必要があります。
以下のデータが存在する場合にデータが変更されるシナリオを仮定します。
id | name |
---|---|
1 | null |
データをネットワーク経由で送信するにはどうすれば良いでしょうか?受信者と接続を確立し、一度にすべてのデータを送信するのが最も簡単な方法のように思えます。しかし、この方法は複数のリクエストを処理する際に非効率的です。なぜなら、1つの接続は1回のデータ転送しか維持できないからです。大きなデータ転送のために接続が長引くと、他のデータは待たなければなりません。
データ送信プロセスを効率的に処理するために、ネットワークはデータを複数の部分に分割し、受信側がそれらを再構成する必要があります。これらの分割されたデータ構造をパケットと呼びます。パケットには、受信側がデータを正しい順序で再構成できるようにするための追加情報が含まれています。
複数のパケットでデータを送信することで、パケットスイッチングを通じて多くのリクエストを効率的に処理できますが、データの損失や誤った順序での配信など、さまざまなエラーが発生する可能性もあります。こうした問題をどのようにデバッグすれば良いのでしょうか?🤔
bootRun
を使用してテストする必要がある場合もあります。.env
ファイルは通常Gitで無視されるため、バージョン管理が難しく、断片化しやすい。
.env
ファイルのバージョン管理は可能ですか?.env
ファイルを更新するのは便利です。.env
ファイルのバージョン管理はスナップショットを通じて行えます。.
..
...
....
それだけだと、記事が少し退屈に見えるかもしれませんね?もちろん、まだいくつかの問題が残っています。
S3を使用する際、ファイル構造の最適化やビジネス特有の分類のために多くのバケットが作成されることが一般的です。
aws s3 cp s3://something.service.com/enviroment/.env .env
もし.env
ファイルが見つからない場合、上記のようにAWS CLIを使用してダウンロードする必要があります。事前に誰かがバケットを共有してくれない限り、環境変数ファイルを見つけるためにすべてのバケットを検索する必要があり、不便です。共有を避けるつもりでしたが、再度共有するために何かを受け取るのは少し面倒に感じるかもしれません。
バケットが多すぎる。envはどこにあるのか?
S3内のバケットを探索して必要な.env
ファイルを見つけてダウンロードするプロセスを自動化すると、非常に便利です。これはfzfやgumのようなツールを使用してスクリプトを書くことで実現できます。
.env
ではない...一部の方はすでにお気づきかもしれませんが、Spring Bootはシステム環境変数を読み取ってYAMLファイルのプレースホルダーを埋めます。しかし、単に.env
ファイルを使用するだけではシステム環境変数が適用されず、Spring Bootの初期化プロセス中に拾われません。
簡単にその仕組みを見てみましょう。
# .env
HELLO=WORLD
# application.yml
something:
hello: ${HELLO} # OSのHELLO環境変数から値を取得します。
@Slf4j
@Component
public class HelloWorld {
@Value("${something.hello}")
private String hello;
@PostConstruct
public void init() {
log.info("Hello: {}", hello);
}
}
SystemEnvironmentPropertySource.java
@Value
のプレースホルダーが解決されず、Beanの登録が失敗し、エラーが発生します。
単に
.env
ファイルがあるだけでは、システム環境変数として登録されません。
.env
ファイルを適用するには、export
コマンドを実行するか、IntelliJの実行構成に.env
ファイルを登録する必要があります。しかし、export
コマンドを使用してローカルマシンに多くの変数をグローバルに登録すると、上書きなどの意図しない動作が発生する可能性があるため、IntelliJのGUIを通じて個別に管理することをお勧めします。
IntelliJはGUIを介して
.env
ファイルの設定をサポートしています。
プレースホルダーが解決され、正しく適用されました。
ふう、問題の特定と範囲設定の長いプロセスが終わりました。もう一度ワークフローをまとめ、スクリプトを紹介しましょう。
.env
ファイルを見つけてダウンロードします。.env
をシステム環境変数として設定します。シェルスクリプトはシンプルでありながら、gumを使用してスタイリッシュに書かれています。
#!/bin/bash
S3_BUCKET=$(aws s3 ls | awk '{print $3}' | gum filter --reverse --placeholder "Select...") # 1.
# デプロイ環境を選択
TARGET=$(gum choose --header "Select a environment" "Elastic Container Service" "EC2")
if [ "$TARGET" = "Elastic Container Service" ]; then
TARGET="ecs"
else
TARGET="ec2"
fi
S3_BUCKET_PATH=s3://$S3_BUCKET/$TARGET/
# envファイルを検索
ENV_FILE=$(aws s3 ls "$S3_BUCKET_PATH" | grep env | awk '{print $4}' | gum filter --reverse --placeholder "Select...") # 2.
# 確認
if (gum confirm "Are you sure you want to use $ENV_FILE?"); then
echo "You selected $ENV_FILE"
else
die "Aborted."
fi
ENV_FILE_NAME=$(gum input --prompt.foreground "#04B575" --prompt "Enter the name of the env file: " --value ".env" --placeholder ".env")
gum spin -s meter --title "Copying env file..." -- aws s3 cp "$S3_BUCKET_PATH$ENV_FILE" "$ENV_FILE_NAME" # 3.
echo "Done."
gum filter
を使用して、目的のS3バケットを選択します。env
という単語を含むアイテムを検索し、ENV_FILE
という変数に割り当てます。.env
ファイルのオブジェクトキーを最終決定し、ダウンロードを進めます。実行プロセスのデモビデオを作成しました。
デモ
これが終わったら、先ほど述べたように、現在のディレクトリにコピーされた.env
ファイルをIntelliJに適用するだけです。
direnvとIntelliJのdirenvプラグインを使用すると、さらに便利に適用できます。
この記事では、既存の非効率な実装について議論し、それを改善するために試みた方法を記録します。
複数のデータベースに分散されたテーブルを単一のクエリで結合することは不可能ではなかったが、困難だった...
データベース結合ができなかった主な理由が解決されたため、ジオメトリ処理にインデックススキャンを活用することを積極的に検討しました。
このプロセスをシミュレートするために、本番DBと同じデータを用意し、実験を行いました。
まず、インデックスを作成しました:
CREATE INDEX idx_port_geom ON port USING GIST (geom);
次に、PostGISのcontains
関数を実行しました:
SELECT *
FROM ais AS a
JOIN port AS p ON st_contains(p.geom, a.geom);
素晴らしい...
1分47秒から2分30秒
0.23ミリ秒から0.243ミリ秒
キャプチャは用意していませんが、インデックス適用前のクエリは1分30秒以上かかっていました。
結論から始めて、なぜこれらの結果が得られたのかを掘り下げていきましょう。
複雑なジオメトリデータのクエリに非常に有用なインデックスで、その内部構造は以下の通りです。
R-treeのアイデアは、平面を長方形に分割してすべてのインデックスされたポイントを包含することです。インデックス行は長方形を格納し、次のように定義できます:
"探しているポイントは指定された長方形の中にある。"
R-treeのルートには、いくつかの最大の長方形(交差することもある)が含まれます。子ノードには、親ノードに含まれる小さな長方形が含まれ、すべての基本ポイントを包含します。
理論的には、リーフノードにはインデックスされたポイントが含まれるべきですが、すべてのインデックス行は同じデータ型を持つ必要があるため、ポイントに縮小された長方形が繰り返し格納されます。
この構造を視覚化するために、R-treeの3つのレベルの画像を見てみましょう。ポイントは空港の座標を表しています。
レベル1:2つの大きな交差する長方形が見えます。
2つの交差する長方形が表示されています。
レベル2:大きな長方形が小さなエリアに分割されています。
大きな長方形が小さなエリアに分割されています。
レベル3:各長方形には1つのインデックスページに収まるだけのポイントが含まれています。
各長方形には1つのインデックスページに収まるポイントが含まれています。
これらのエリアはツリー構造になっており、クエリ中にスキャンされます。詳細な情報については、次の記事を参照することをお勧めします。
この記事では、具体的な条件、遭遇した問題、それを解決するために行った努力、およびこれらの問題に対処するために必要な基本概念を簡単に紹介しました。要約すると: