テックキャンプ卒の弱々エンジニア日記

エンジニアとして働く中での学びをちょっとでも記録していきます。

SSTableとLSMツリー

こんにちは、Tochiです。 しばらくサボっていたので、久しぶりにアウトプット

はじめに

最近、「データ思考アプリケーションデザイン」という本を読み進めています。 www.oreilly.co.jp

第三章にて、SSTableという単語とLSMツリーという知らない単語を見たので軽く深掘っていきます

前提

Hash IndexやB-Treeインデックスのこと知っているといいかも

SSLTableとは

ソート済み文字列テーブル(Sorted String Table)の略です。

Hash Index のsegment fileのフォーマットに対してキーバリューのペアの並びが、 キーでソートされているという条件と マージされたセグメント内のキーに重複がないという条件を付け加えたもの。

SSLTableの構築と管理

キーをソートしてデータを保存するにはどうするのか?

赤黒木やAVLツリーなどのツリー型のデータ構造を使えば、できます。

  • 書き込み要求が来たら、インメモリのバランスドツリーデータ構造に追加する

    • このインメモリツリーをmemtableという
  • memtableがなんらかの閾値よりも大きくなった場合はSSLTableとしてディスクに書き出す

    • この時点ですでにツリーはソートされたキーと値のペアとして管理されているので効率的に下記出せる
    • 新しく作成されたSSLTableは最新尾DBのセグメントになる
  • 読み取り要求が来たら、そのキーのmemtableを探す。その次にSSLTableの最新のセグメントを見に行く。

  • マージとコンパクションのプロセスをバックグラウンドで実行し、セグメントファイルの結合と上書きされた値や削除された値を破棄する

この管理方法は基本的にはうまくいくが、データベースがクラッシュした際に直近の書き込みが失われます。 そのため、全ての書き込みのログを即座に追記しておくことが必要

参考: データはどのように書き込まれるか

SSLTableのメリット

このSSLTableと、Bitcastのようなストレージエンジのログセグメント(ハッシュインデックス)と比較すると大きく3つの利点がある。

  • ファイルが利用可能なメモリ量よりも大きくなっても、セグメントのマージをシンプルに行える。

マージソートアルゴリズムに似ています。すでにソートされているので、各セグメントの最初のキーを見て出力ファイルに移していきます。 複数ある場合は、最新のものだけを残します

  • 特定のキーを探す際は、全てのキーをメモリに保持しておく必要はない。

例えば、handiworkというキーを探しているときは、正確なセグメントファイルのオフセットをわかっている必要はないです。handbagとhandsomeというキーのオフセットがわかっていればその間にあることは明確なので、handbagのオフセットに飛んでからスキャンすれば良いわけです。

一般には、キロバイトごとに一つのキーがわかっていればいい。

  • ディスクに書き込む前にレコードをブロックとしてグループ化して圧縮しておける。 読み取りリクエストの処理では、要求された範囲内にある複数のキーと値のペアをスキャンしないといけないです。 この場合、インメモリインデックスの各エントリは圧縮されたブロックの開始地点を指すことになります。 圧縮はディスクの領域削減や、I/Oの帯域削減にもなります。

LSMツリー

上記で述べたアルゴリズムがLevelDBやRocksDBで使われている このインデックス構造をLSMツリー(Log-Structured Merge-Tree)という。

ソート済みのファイルのあマージコンパクションという原理を基盤とするストレージエンジンはLSMストレージエンジンと言います。

ログ構造化マージ ツリー( LSM ツリー、またはLSMT [1]とも呼ばれる) は、トランザクション ログなど、挿入量の多いファイルへのインデックス付きアクセスを提供するのに魅力的なパフォーマンス特性を持つデータ構造です。データ。LSM ツリーは、他の検索ツリーと同様に、キーと値のペアを維持します。LSM ツリーは 2 つ以上の個別の構造でデータを維持し、それぞれが基礎となるストレージ メディアに合わせて最適化されています。データは 2 つの構造間でバッチで効率的に同期されます。 参考: Log-structured merge-tree - Wikipedia

Bツリーとの比較

一般にインデックスといえば、Bツリーです。 こちらと比較をされていたのでまとめます。

要素 Bツリー LSM
書き込みの増幅 ディスクへの書き込みが最低2回発生 (WAL&ページ) 書き込みは1回のみ
ページが分割された場合はさらに書き込みが必要 コンパクション&マージがバックグラウンドで発生するため、
負荷は小さい
書き込みスループット ランダムな書き込みのためLSM-Treeより遅い 連続した書き込みのため高速
データ量 フラグメンテーションにより未使用のディスク領域が生じる 圧縮率を高めることが可能で、ファイルサイズが小さくなる
ページが分割されたり、行が既存のページに収まらない場合 定期的なコンパクション&マージにより未使用領域は削除される
インデックス 必ず一意であること コンパクション&マージはバックグラウンドで行われるため、
複数のセグメントに存在することがあります
トランザクション管理 範囲内のキーのロックを使用して実装されている 直接ツリーにアタッチ可能

参考: Storage Engine(Hash Index, LSM-Tree, B-Tree)

まとめ

SSTableとLSMツリーについて軽く見ました。

一般的にインデックスといえばBツリーを思い出します。 Bツリーを深く理解する比較対象としてLSMストレージエンジンを見てみるのもいいなあと思いました。

概念設計の思考のポイント

こんにちは、Tochiです。 最近は、かなり忙しく、、というのは本当なんですけど ちょいサボっていたの業務での学びをアウトプットしていきたいと思います。

はじめに

今日は概念設計の重要性についてまとめます。 一般に言われる概念設計とは少し異なるかもしれませんが、そこらへんは悪しからず。

概念設計とは

一般に概念設計とは、システムやソフトウェアの開発プロセスにおいて、 アプリケーションやシステムの基本的な機能や構造を定義する段階を指します。

ただし、私が普段業務でやっている概念設計は、 アプリケーションやシステムの基本的な機能や構造を定義する前の事業的観点と技術的観点を洗い出し、 タスクの本質を見出した上で設計をおこうなうというものです。

具体的には、タスクを与えられたら以下の項目について整理していきます。

  • 一言で何をするのか
  • どんな背景があるのか
    • 事業的背景は?
      • これをやることで事業として何を目指すのか
      • ユーザにとってどんな利益がもたされるか
    • 技術的背景は?
      • どんな技術課題を解消できるのか
  • 理想はどんな状態なのか
  • 現状はどんな状態なのか
  • 理想と現状を埋める仕様はなにか
  • 仕様を実現する上での課題はなにか
  • 課題と解決案の選択肢
    • 各選択肢のメリデリ
    • どの選択肢を選び、なぜその意思決定をしたか
  • 上記の意思決定を踏まえた実装方針

一般にPdMの責務になる部分があるかもしれませんが 私の場合は上記のようなことを洗い出してさまざまな意思決定をする必要があります。

思考ポイント

上記で記載した概念設計では思考の幅と深さがとても大切になります。

何も考えずに行うと、例えば「ソート機能を実装する」というタスクがあったときに 下記のような浅い思考での概念設計になってしまいます。

  • 事業的背景は、「ユーザがソートをしたいから」
  • 技術的背景は、「ソートできるようになる」

なので、私なりに実務で学んだ 概念設計時の思考のポイントをまとめてみました。

思考のポイント1: 視点を変えてみてみること

1つ目のポイントは視点を変えてみることです。

具体的には、「誰が」の視点を変えてみることです。

例えば、ソート機能1つとっても、視点を変えるだけでソート機能の目的が全然変わってきます

  • 経理の人
    • 金額関係のカラムで全レコードを整理してみたいなあ
  • 事業部の人
    • 管理者
      • 自分と部下の分のレコードを見たいなあ
    • 一般者
      • 自分のレコードだけを見たいなあ

視点を変えることで、機能がなんのためにあるかを考えやすくなります。

思考ポイント2: シナリオを知る

2つ目はシナリオを知るということです。 砕けた言い方でいえば、「いつどのように」を知るということです。

例えばソート機能は、いつどんな時にどのように使われるのでしょうか。

  • 自分に関連するレコードをいち早く見つけるため?
  • ある特定のカラムの値でランキングを見たいため?(金額・日付・量)
  • 特定の値でグルーピングしてみたいため?

などなど、使い方もそれぞれ異なります。

その機能がいつどのように使われるかを考えるだけでもタスクの本質が大きく変わってくるので、 自分が作る機能が「いつどのように」使われるのか具体的な運用やシナリオを知ることが概念設計では重要です。

思考ポイント3: 前提が崩れる場合を考える

3つ目のポイントは、前提が崩れる場合を考えるです。

あらゆる思考は、ある前提に基づいて構成させれます。

例えば、ソート機能は「並び順を変えることができたら便利」という大前提に基づいて提案されるものです。

このとき、概念設計では逆のこと、つまり「並び替えられると不便」になる場合がどういう場合かを考えてみましょう。 前提が崩れる恐れがある、あるいは崩れると諸々影響が大きそうだぞ、と思った時は十分に注意してください。

なぜなら、ソフトウェアの開発は常に前提が変わっていくので、今考えたその前提の逆が前提になりうるからです。 ゆえに今はソートが必須!と言われていても、後々にソートされると困るなんてことが発生しえます。

(あまり考えにくいのですが)例えば、、ソートの状態を保持できる仕組みを入れた場合に、 ソートさせたくないものが鬱陶しくなるなんてことがあるかもしれません。

このように、概念設計時には原点に戻って前提を疑うことが、 タスクの本質を見出すのに大切になってきます。

まとめ

本当はもっと書きたいのですが、 割り込みタスクが入り込んできたので一旦これで終わり。

【Rails】トランザクションとロックの関係、今更ながらまとめてみた。

こんにちは、Tochiです。 最近忙しくてあまり更新できておらず、久しぶりの更新です。 先日業務しているとき、「トランザクションをしておけば行ロックって意識しなくてもいいのかな?それともDBによるのかな??」「そもそもトランザクションとロックって別物だよな・・ 」といった感じで混乱しました。 なので今更ながら、トランザクションとロックの関係についてまとめていきたいと思います。

はじめに

題名にRailsと書いていますが、実際はDBMSの話です、、笑

ただ、トランザクションやロックの話をRailsから入るとイマイチ理解なく、 なんとなくで実装できてしまいます笑

なので基礎の基礎から理解していこうと思います。

参考

www.postgresql.jp

トランザクションとは

トランザクションとは、関連する複数の処理を1つの処理として実行・管理する仕組みで、 DBに要求される以下4つの特性を維持する仕組みです。

  • 原子性(Atomicity)
    • トランザクションは「すべて実行される」か「一つも実行されない」のどちらかの状態になる
  • 一貫性(Consistency)
    • 実行前と実行後のデータベースのデータに矛盾を生じさせない。
  • 独立性(Isolation)
  • 永続性(Durability)
    • トランザクション完了後、その結果は記録される。ハードウェアに障害が生じても、処理結果はデータベースに永続的に保存される

めっちゃ簡単に言えば、データ不整合が起きないようにする仕組みですねということです。

トランザクションの分離とは?

トランザクションには4つの分離レベルがあります(※ Postgresを前提としています) この分離レベルは、それぞれのレベルによって禁止される現象(リード現象)が異なります。

この分離レベルを理解するには、大前提にどんな禁止される現象が発生するかを確認しておくと良さそうです。

リード現象の種類

ダーティーリード

ダーティーリードとは、同時に実行されている他のトランザクションが書き込んで未だコミットしていないデータを読み込んでしまう現象です。

下の例でいうと、トランザクション2はコミットせずにロールバックしたはずのUPDATEの内容を、トランザクション1が読み込んでしまい、登録されてない「オレンジ」を読み込んでしまいます。

ダーティーリード

反復不能読み取り(ファジーリード・ノンリピータブルリード)

トランザクションが、以前読み込んだデータを再度読み込み、そのデータが(最初の読み込みの後にコミットした)別のトランザクションによって更新されたことを見出してしまう現象です。

反復不能読み取り

図を見ていると、なんだか正しい挙動のように思えます。 ただ、最初の冒頭で記載したトランザクションの定義にあった 「関連する複数の処理を1つの処理として」処理するということを考えると、その1つの処理の中で読み取った結果が違うということはバグの温床になるというのが直感的に理解できます。

たとえば、銀行口座で「残高表示→出金→残高表示」のトランザクションがあったときに 途中で誰かから入金があったときに出金したのに増えている!!!なんてこともありうるわけです

ファントムリード

トランザクションが、複数行のある集合を返す検索条件で問い合わせを再実行した時、別のトランザクションがコミットしてしまったために、同じ検索条件で問い合わせを実行しても異なる結果を得てしまう現象です。

ファントムリード

こちらは、反復不能読み取りにかなり似ています。 ただし、既存レコードの変更であるか新規レコードが追加されたかの違いがあります。

ファントムリードが起きてしまうと、在庫管理などで「もうりんごしかないので発注せねば!」と思っていたのに、「さっきん発注したのに、なぜかオレンジ増えている!」といった混乱を生みます。

直列化異常

複数のトランザクションを並行して行なった結果と、1つずつ行なった場合とで結果が異なる場合を言います。 上記の例ではすべて並行にした場合と直列にした場合で結果が異なります。

分離レベルについて

さて、ようやく分離レベルの話に戻ってきました。

今あげたリード現象をどの程度許容するかを決定するのがトランザクション分離レベルです。

Postgresの分離レベルは以下の通りになっています

13.2. トランザクションの分離

分離レベル表

Railsにおいてのデフォルトは、リードコミッティッドになります。

ロックについて

一旦トランザクションがわかったところで、続いてロックについて調べてみたいと思います。

Postgresのドキュメントを眺めていると以下のようなロックがあることがわかります。

  • テーブルロック

  • 行ロックロック

  • ページレベルロック

    • テーブルは物理的には一つのファイルであり、8Kバイトのブロック単位で分割されて管理されます。データが増えてブロック内に保存しきれなくなると、ファイルの末尾にブロックを追加してファイルサイズを増やします。このブロックをページと呼びます。

テーブルロックを使う時ってあんまりなさそうだなあと思いつつも MySQLだとギャップロックによるデッドロックの回避方法の1つと提案されていたりしそうです。

(参考: 「トランザクション張っておけば大丈夫」と思ってませんか? バグの温床になる、よくある実装パターン)

アプリ開発する上では圧倒的に行レベルロックを使うことが多いように思います。

DBの世界では厳密に細かいロックの種類が色々あるのですが、 後述するlockメソッドなどは基本的にSELECT FOR UPDATEになり最も強いロックがデフォルトになるのでその他のSELECT FOR SHAREなどといったモードはあまり考えなくても大丈夫なようです。 (どんなときにFOR SHAEを使ったりするのか知りたい。。)

Railsにおけるロック

Railsにおけるロックは、

  • lock
  • lock!
  • with_lock

で実装されます。

これらは若干の使い方の違いがあれど、やっていることは同じです。

# lockメソッド
ActiveRecord::Base.transaction do
  User.lock.find(user_id)
end


# lock!メソッド
user = User.find(user_id)
ActiveRecord::Base.transaction do
  user.lock!
end

# with_lock
user.with_lock do # トランザクション作成&ロック取得
  # 処理
end

すでに述べているように、このlockメソッドで発行されるSQLは最も強いSELECT FOR UPDATEになります。

TRANSACTION (0.2ms)  BEGIN
User Load (0.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 FOR UPDATE
TRANSACTION (0.1ms)  COMMIT

このロックは、トランザクションの終了時に自動で開放されます。

デッドロックについて

これらのロックを明示的に使う場合は、デッドロックを起きる可能性が高まります。 しかし、Postgresの場合デッドロックを自動検知してくれて、関係するトランザクションの1つを中止してくれます。

そのためデッドロックによってプログラムが完全停止するといったことは通常はないです。

まとめ

トランザクションとロックの関係に混乱していた私ですが、大体理解できました。

まとめとしては、

  • トランザクションをしておけばいいというわけではない
    • 特に分離レベルによっては、得た結果が必ずしも高い信頼があるものではない
  • トランザクションを使えば、安全な更新ができるわけではない
    • トランザクションはACID特性を保持するものであって、予期せぬ結果を生まないようにするにはロックを使うのが確実

といった感じでしょうか。

普段あまり意識せずなんとなくでやらないように気をつけたいです

【PostgreSQL】ユニーク制約に違反しているのに登録できてしまう

こんにちは、Tochiです。 マジで知らんかった!となった話を共有です。

NULLはユニーク制約に縛られない!!!

「NULL」はユニーク制約に縛られません。 例えば、以下のような制約があったとしてます。

CREATE UNIQUE INDEX clients_on_organization_id__identifier__deleted_at ON public.clients USING btree (organization_id, identifier, deleted_at)

このとき、下記のようなレコードは存在しうるでしょうか?

organization_id identifier deleted_at
1 xxxxx NULL
1 xxxxx NULL

もちろん、ユニーク制約がかかるので違反状態にな......りません!!!

実はこのレコードは存在しうるのであります!!!

DBMSによってNULLの概念が違う

実はDBMSによってNULLの概念が違います。

「NULLはNULLに一致しない」というのが大原則です。 例えば、下記のSQLはWHRE句がfalseになるので何も表示されません

SELECT * FROM users c WHERE null = null

そのため、PostgreSQLMySQLではこの考えのもと、 NULL同士は違う値として認識されるので、先に述べた例はユニーク制約に引っかからないのです。

え、でもそうじゃないDBMSは?

そうではないDBMSもあると述べました。 例えば、SQL Serverがそうです。 これらのDBMSには「重複」という概念が別で存在します。

つまり「NULLとNULLは一致はしないが重複はする」というルールのもとうまいこと成り立っているわけです。

まとめ

今回、ユニーク制約がDBMSによって異なるということを発見しました。 多分、これを知っているだけでもかなり多くの人が助かるような気がします。

お役に立てるといいな、、

【Rails】論理削除の歴史を読み解きながらDiscardの内部実装まで見てみた件

こんにちは、Tochiです。 Railsの論理削除といえば、1社目ではdeletedカラムを追加してupdate処理で管理していました。 しかし世の中にはDiscardなる論理削除を簡単に管理できるgemがあると知りました。その中で、Discardができた背景がとても面白かったので歴史を紐解きながら内部実装を見ていきたいなと思います。

そもそも論理削除って??

「論理削除」とは、あるアイテムを即座に完全に削除する物理削除と異なり、 アイテムを保留場所に移動し削除されているように見せるアプローチを取るものです。 保留場所に移動したアイテムは、物理削除まえに一定期間保持され、手動または自動で削除されるものです。 (もちろん、永続的に保持しておくことも可能です)

論理削除であれば実際にはデータベースから削除されることはなく、復元も可能です。 メールのゴミ箱なども、いわゆる論理削除にあたります

論理削除はどのように実装される?

一般的なアプローチとしては、deleted_at/discarded_atのタイムスタンプ・カラムを個別に追跡します。論理削除されたレコードはそうでないレコードと同じテーブルに保持し、スコープクエリで区別することができます。

user = User.where(deleted_at: nil)

Discardより少し前に登場したParanoia

Discardより少し前の時代には、 Paranoiaというgemが流行していました。 これは、Rails3 ~ 5用のact_as_paranoidを再実装したもので、内部実装が整備されたものらしいです。

github.com

しかし、このParanoiaには問題がありました。 それは、既存メソッド(deleteメソッド / destroyメソッド)を上書きしてしまうのです。

実際に見てみます。

Githubの該当箇所

  def paranoia_delete
    raise ActiveRecord::ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
    if persisted?
      # if a transaction exists, add the record so that after_commit
      # callbacks can be run
      add_to_transaction
      update_columns(paranoia_destroy_attributes)
    elsif !frozen?
      assign_attributes(paranoia_destroy_attributes)
    end
    self
  end
  alias_method :delete, :paranoia_delete
  1. レコードが読み取り専用であれば、ActiveRecord::ReadOnlyRecord 例外が発生します。
  2. レコードがデータベースに保存されているものであれば、削除用の属性を使って update_columns メソッドが呼ばれます。(論理削除フラグとなるカラムを更新)
  3. レコードがデータベースに保存されていない場合かつ凍結されていない場合、assign_attributes メソッドが呼ばれ、論理削除用の属性がレコードに割り当てられます。
  4. 最後に self を返してメソッドが終了します。
  5. alias_method :delete, :paranoia_delete の一行でエイリアスをつけています。

5の処理で、deleteメソッドにエイリアスが付けられています。

またデフォルトスコープが変わってきます。 User.allとしたときに、発行されるSQLは以下の通りになります。

  SELECT `users`.* FROM `users` WHERE (`users`.`deleted_at` IS NULL)

論理削除されたものは暗黙的に取得しなくなります。 このような実装について、Discardの作者はというと、、、、

・ I've worked with and have helped maintain paranoia for a while. I'm convinced it does the wrong thing for most cases.

・These overrides of the default ActiveRecord behaviour, while nice and magical on the surface, can and do lead to some rather confusing debugging sessions.

【翻訳】

Paranoiaおよびacts_as_paranoidは、ほとんどのケースで誤った結果を生むと確信しています。

・デフォルトの ActiveRecord 動作のこれらのオーバーライドは、表面的には素晴らしく魔法のように見えますが、かなり混乱するデバッグ セッションを引き起こす可能性があり、実際に発生します。

Discardはどうなん?

Discardは、レコードに破棄済みのフラグをつける規則を追加する単純なAtiveRecordミックスインです。 公式ドキュメントはこちらです。 github.com

どんなふうに使うのか
事前準備

使い方はとても簡単です。 論理削除を施したい対象のモデルにDiscard::Modelをインクルードします。

class Post < ActiveRecord::Base
  include Discard::Model
end

Postモデルに対応するPostsテーブルには以下のようにインデックスとカラムを追加します。

class AddDiscardToPosts < ActiveRecord::Migration[5.0]
  def change
    add_column :posts, :discarded_at, :datetime
    add_index :posts, :discarded_at
  end
end

これで準備は完了です。

論理削除の実施方法

公式ドキュメントにあるこちらのコードを見るとよくわかります。

Post.all             # => [#<Post id: 1, ...>]
Post.kept            # => [#<Post id: 1, ...>]
Post.discarded       # => []

post = Post.first   # => #<Post id: 1, ...>
post.discard        # => true
post.discard!       # => Discard::RecordNotDiscarded: Failed to discard the record
post.discarded?     # => true
post.undiscarded?   # => false
post.kept?          # => false
post.discarded_at   # => 2017-04-18 18:49:49 -0700

Post.all             # => [#<Post id: 1, ...>]
Post.kept            # => []
Post.discarded       # => [#<Post id: 1, ...>]

これだけみてもデフォルトスコープが変わってないことがわかります。 実際に内部実装を見ていきます。

クラスメソッド(kept・undiscarded・discarded・with_discarded...)

Githubの該当箇所

module Discard
  module Model
    extend ActiveSupport::Concern

    included do
      class_attribute :discard_column
      self.discard_column = :discarded_at

      scope :kept, ->{ undiscarded }
      scope :undiscarded, ->{ where(discard_column => nil) }
      scope :discarded, ->{ where.not(discard_column => nil) }
      scope :with_discarded, ->{ unscope(where: discard_column) }

      define_model_callbacks :discard
      define_model_callbacks :undiscard
    end
end

かなりわかりやすいですね。。 シンプルにそれぞれのスコープで、discard_columnをどう扱うかを定義しているだけです。

インスタンスメソッド(discarded・undiscarded)

Githubの該当箇所

def discard
   return false if discarded?
   run_callbacks(:discard) do
     update_attribute(self.class.discard_column, Time.current)
   end
end

Githubの該当箇所

def undiscard
    return false unless discarded?
    run_callbacks(:undiscard) do
     update_attribute(self.class.discard_column, nil)
   end
end

説明不要なくらいシンプルです。

気づき

ParanoiaとDiscardの内部実装を見ていて、先日読んだ「UNIXという考え方」という本に書いてあったことを思い出しました。

www.ohmsha.co.jp

この本はUNIXがなぜ他のOSより優れているかを設計思想の観点から述べた本です。その中にこんなことが書いてありました。

  • 小さなプログラムはわかりやすい
  • 小さなプログラムは保守しやすい
  • 小さなプログラムは組み合わせしやすい

今回、ParanoiaとDiscardを比較していたときにDiscardのシンプルさには少し衝撃を覚えました。同じようなことをしようとしても、ここまで内部実装の美しさやシンプルさが変わるのかと、、、

本で読んでも「まあ、小さい方がいいわな」と思っていましたが 実際に当事者になると、この設計思想がいかに重要でありがたいことかを見にしみて感じました。。。

まとめ

内部実装見るのは結構勉強なりました!!!

サービスベースアーキテクチャ (ソフトウェアアーキテクチャの基礎13章)

こんにちは、Tochiです。 サービスベースアーキテクチャについてまとめていきます。

13章 サービスベースアーキテクチャ

サービスベースアーキテクチャは 、マイクロサービスアーキテクチャのハイブリッドであり、その柔軟性の高さから最もは実用的なアーキテクチャの1つ。

サービスアーキテクチャの構成は分散型のマクロなレイヤード構造をとり、

  • 個別のデプロイされたユーザインターフェース
  • 個別にデプロイされた粒度の洗いリモートサービス
  • モノリシックなデータベース

から編成される。

ただし単一のユーザインターフェースはドメインごとやドメインサービスごとのユーザインターフェースにも分割は可能。

またデータベースもインターフェース同様にドメインごとに分割することができる。

このドメインサービスは一般に粗い粒度で分割されたもの。 通常ドメインサービスは、APIファザード層・ビジネス層・永続化層などで構成されるレイヤードアーキテクチャで設計されるか、 モジュラーモノリスのようにサブドメインを利用して各ドメインサービスごとにドメインごとに分割する。

いずれにせよAPIPアクセス用にAPIファザード層は含む必要があり、ユーザーインターフェースからのビジネス要求をオーケストレーションする責任を負う。

ドメインの粒度が荒いことで、単一のドメインサービス内でデータベースの整合性を確保するのに、 データベースのコミットやロールバックを含む通常のACIDトランザクションが使用される。 一方、マイクロサービスのような高度な分散アーキテクチャは、一般にサービスの粒度は細かいため、 BASEトランザクションと呼ばれる分散トランザクション技術が使用される。

粒度が荒いので、データの完全性や一貫性は確かに向上するがトレードオフもある、 粒度が荒いことで、ある変更を加えた際にドメインサービス全体をテストする必要があり、より多くのコードがデプロイされるため 故障する可能性が高い。

まとめ

サービスベースアーキテクチャドメイン駆動設計にもフィットしそう。 DBもACIDを担保しつつ、分散アーキテクチャを実現できる点は実務的と感じた。

マイクロカーネルアーキテクチャ (ソフトウェアアーキテクチャの基礎12章)

こんにちは、Tochiです。 マイクロカーネルアーキテクチャについてまとめていきます。

12章 マイクロカーネルアーキテクチャ

マイクロカーネルアーキテクチャは、プラグインアーキテクチャとも呼ばれ、 パッケージ化され、単一のモノリシックなデプロイメントとしてダウンロードしてインストールできるようになっており、通常はサードパーティ製品として顧客側の環境にインストールされるような製品ベースのアプリケーションにうまくフィットする。

その構成は、コアシステムとプラグインの2つのコンポーネントで構成されている。 これによりアプリケーション機能やカスタム処理のロジックの拡張性・適応性・分離性を実現している。

コアシステムは、必要最低限の機能を形式的に定義したもの。 例えば、Eclipse IDEのコア機能は、「ファイルを開いて、テキストを変更して、ファイルを保存するだけ」という基本的なテキストエディタである。これにプラグインを追加して初めて便利なツールになるのだ。

このように、コアシステムの循環的複雑度を減らしてプラグインコンポーネントを切り出すことで、拡張性と保守性を実現し、テスト容易性を高められる。

コアシステムは、規模や複雑さによってレイヤードアーキテクチャやモジュラーモノリスとして実装できる。

プラグインコンポーネントは、コアシステムを強化・拡張するための特殊処理や追加機能を含む独立したコンポーネントだ。独立していることで保守性やテスト容易性が保たれる。このプラグインコンポーネントは一般にポイントツーポイントで通信するが、RESTやメッセージングを利用して各プラグインスタンドアローンのサービスにするという選択肢もある。

このリモートアクセスアプローチは、コンポーネント全体を疎結合にし、スケーラビリティとスループットを向上させる。一方、この利点がアーキテクチャ全体を分散アーキテクチャに変えてしまい全体的な複雑さとコストを生み出す。

このアーキテクチャは、冒頭に記したEclipseIDEの他ChromeやEdgeといったWebブラウザでも採用されている。 また保険金請求処理を行うようなシステムでは、司法管轄区ごといに規則が異なったりするのでこれらのアーキテクチャを利用する。

たしかにこれは相性よさそう。

まとめ

マイクロアーキテクチャを使うのはカスタマイズ処理とかが必須となるようなシステムの時に採用すると良いのかな??? ただ耐障害性の特性がないという点では、少し採用しづらさを感じる。。