概要
エンジニアの道に進んで5,6年経ちました
なかでもAWSは普段から使用しており、今ではシステム構築には欠かせないものとなっております
EC2やS3、RDSなど基本的なものを使用してきておりますが、果たしてそれらをうまく使いこなせているか、特性を理解して使えているかと、ふと考えたりします
例えばクラウドサービスを利用する上では避けられないEOLなどによるメンテナンスです
決められた日時が通知され、その日時が来るとEC2やRDSは自動的に停止されてメンテナンスが実施されたりします
結合度合いが強いシステムだった場合に、このメンテナンスの間中はシステム全体が応答できない状態になります
これを密結合なシステム構成というようなのですが、個人的にはシステム全体が止まらないようにするのが良いシステムではないかなと考えております
そこで、疎結合を実現するにはどのようなインフラ・システム構成を想定するとよいのかについて考えてみたいと思います
(疎結合が密結合より優れている・正義なのかと言われるとそうでないとも若干思っているので、これも後ほどメリットデメリット調べてみます)
本題
疎結合に適しているシステム・インフラ構成とは
Gemini曰く以下のようなもの
コンポーネント間が「メッセージ」や「イベント」を介して繋がっており、お互いの内部実装や稼働状態を意識しなくてよい状態。
さらに具体的な例で言うと以下のようです
ユーザーが画像をアップロードした(メッセージ送信) → 後でバックグラウンドでリサイズ処理をする(メッセージ受信)という流れをシミュレーションします。
抽象化したイメージとしてはリアルタイムでの処理を必要としないシステム構成といったところでしょうか?
あとは正確なトランザクションを必要とされる時には適していない模様です
ECサイトなどのログイン認証や銀行の振込処理などですね
あるAPIに依頼してすぐにその結果が反映されていなくても良いというものに適していそうです
疎結合・密結合のメリットデメリット
ここで改めてメリットデメリットについてそれぞれまとめてみようと思います
疎結合
呼び出し先のサービスがダウンしていても、メッセージがキューに溜まるので呼び出し元の処理は継続することができる
重たい処理を行うコンポーネントだけを、負荷に応じて個別に増やすことができる
メッセージの欠損、重複、順序の入れ替わりなどを考慮した設計が必要になる
処理が複数のサービスを跨ぐため、監視が複雑になってしまう
密結合
整合性が必要な一連の処理を、1つのトランザクションとして扱いやすい
コードが直感的で、デバッグも容易
呼び出し先のサービスがダウンしていると、呼び出し元もエラーになってしまうのでシステム全体が停止することになる(単一障害点ともいう)
特定のコンポーネントに負荷が集中すると、ほかのコンポーネントにも影響が波及する
結果としてはやはりシステム構成に正解はないのだなということが改めて理解できたと思います
システム要件に合わせて構成を考える必要があり、そういったところも経験や日頃利用しているシステムの構成を想像するなどして感覚を養う必要がありそうです
Pythonコードで書いてみた
せっかくなので疎結合を実現する際によく使われるSQSを扱うコードを最後に添えておきたいと思います
AWSの実際の環境を持ち出すのもアレなので、mockとして使用できるmotoというものを使用しています
以下をpipでインストールしてください
pip install boto3 moto
使用する方法としてはメソッドの前にデコレーターとして設定することで使うことができます
@mock_aws
def run_simulation():
SQSを作成して、作成したキューにメッセージを送信→キューからメッセージを取り出してそのメッセージをキューから削除するというコードが以下になります
import boto3
import json
from moto import mock_aws
@mock_aws
def run_simulation():
# モック環境内での設定(リージョンは任意)
region = "ap-northeast-1"
sqs = boto3.client("sqs", region_name=region)
# テスト用のキューを作成
queue_name = "MyDemoQueue"
sqs.create_queue(QueueName=queue_name)
queue_url = sqs.get_queue_url(QueueName=queue_name)["QueueUrl"]
print(f"--- [Setup] モックキューを作成しました: {queue_url} ---\n")
# キューに対してメッセージの送信
task_data = {
"image_id": "img_999",
"action": "resize",
"size": "thumbnail"
}
print(f"[Producer] タスクを送信中: {task_data}")
sqs.send_message(
QueueUrl=queue_url,
MessageBody=json.dumps(task_data)
)
print("[Producer] 送信完了\n")
# キューに存在するメッセージを取得する
print("[Consumer] メッセージを確認します...")
response = sqs.receive_message(
QueueUrl=queue_url,
MaxNumberOfMessages=1
)
# キューからメッセージを削除する
if "Messages" in response:
for message in response["Messages"]:
body = json.loads(message["Body"])
print(f"[Consumer] メッセージ受信成功: {body}")
sqs.delete_message(
QueueUrl=queue_url,
ReceiptHandle=message["ReceiptHandle"]
)
print("[Consumer] 処理完了メッセージを削除しました")
else:
print("[Consumer] メッセージが見つかりませんでした")
if __name__ == "__main__":
run_simulation()
おわりに
今回はシステム・インフラ構成についてまとめる記事となりました
こういったシステム構成が適しているかを考えるというところに目が向くようになってきたのは、コードだけを書きたくてエンジニアを志した頃よりも少しだけ成長したのではないかと思いました
これからもコードを書きつつ、さらにその先のシステム構成など幅広いところまで思考・配慮できるように努力を続けていきたいと思います