概要
とあるマイクロサービス開発に携わることになり、取り巻く技術要素について調査した内容から良TIPSをブックマーク。サマリを追記。
とあるサービスの特徴
- Javaベースのマイクロサービス
- 今後別言語のサービスを追加する可能性もあるためREST APIベースのインターフェース
- Javaにおいては高速ながDI必須
- Dockerコンテナの利用
- Gradleによるマルチプロジェクトビルド
- 設計思想はドメイン駆動設計を採用
👉マイクロサービス
- 複数の小さなサービスをAPIを介することで連結させたシステム
- サービス間の依存がないため、言語や基盤に関係なくサービスを追加・削除可能
- 代表的なAPIはREST APIで、http通信にてJsonを介したデータのやり取りを行う
- サービスが複数となるため、以下の課題をどう解決していくかが求められる
- サービスごとに参照する永続化層(DB、ファイルなど)をAPIを介してしか通信できないためスキーマ構成に悩む。もしそこを許容してしまうと、一番最下層で依存が発生し、マイクロサービスの意味を成さない
- 複数サービスとなるため、Dockerなどのコンテナ環境を利用して複数サービスをオーケストレーションする必要がある。やらない場合、手作業でビルド・デプロイが面倒。数個のサービスであれば問題ないが、数十個別の言語のシステムではまず無理
- サービス間で共通した仕組みをどう汎用的に作るか。汎用的に作りすぎると依存が発生して意味を成さないが、冗長にしたくないのも事実。例えば、全サービス同一DB(別スキーマ)を見に行く場合、永続化モジュールについては分ける必要があるが、DB接続定義(ConnectionやTransaction)については共有したい。
👉Dagger
- 高速なDIを実現するために、ビルド時にInjection用のモジュールを生成する
- マイクロサービスを構築する場合など、従来のランタイム時インジェクションでは遅い
- Android界隈では使われることの多い技術であるが、今回はWebアプリケーションに組み込む
- DIのメリットは言わずもがな、UTのしやすさとプロジェクトごとのライブラリ・フレームワークへの依存関係を下げること
android-multi-module-with-dagger
- 一番重要な概念であるModuleとComponentについて分かりやすく記載されており、DIの概念と以下が理解できればDaggaerの7割は理解できている
- Moduleは、"何を"注入するかを定義する
- Componentは、"どこに"注入するかを定義する
👉helidon
公式
- 登場してから日が浅いため、Git-Repositoyと公式が一番わかりやすい
- nettyベースな高速・軽量なウェブサービスを実現可能
- SEとMPと存在し、SE+MicroProfileがMPのイメージ
- プロジェクトではSEを採用。理由としてはMPに含まれるjersey(JAX-RS)ではランタイムにリフレクションを利用したインジェクションが発生するためマイクロサービスとしては遅くなるため。jerseyの2大機能といえるインジェクションとルーティングについて、それぞれdagger2とhelidonSEにて実装している。
MicroProfile参考
- Microservicesアーキテクチャー用にEnterprise Javaを最適化することを目的としたイニシアチブのことで、どういった要素から構成されるかが書かれている
👉Docker
Windows7環境なのにdocker入れて開発することになった話【①環境設定編】
- 軽量な仮想マシン(のようなもの)を簡単に構築することができるが、仮想マシンとはそもそもの思想が異なっているため、Dockerを利用して構築した環境については明確に「コンテナ」という別の呼び方をする。実態はtarボール。
- コンテナを扱えるサービスは数多(LXC等)あるが、勝ち残っているソフトウェアがDocker。
- 仮想マシンとの大きな違いは、自分でカーネルを持つかどうか。コンテナはあくまでホストOSのカーネルの一部を共有して利用する。
- Dockerfileという仮想マシンのイメージを記載することで、環境を共有することが容易。
- すでに構築された環境がDocker Hubで提供されている。
👉Javaの諸々
オブジェクト等価性
JavaにおけるequalsとhashCodeを理解する
- 一番重要なのは、等価性は実装するプログラマ自身がビジネスルールにのっとって等価性の実装をする必要があるということ
- ドメインモデル設計では、Valueオブジェクトが多く生成されるため、各Valueオブジェクトがどのような等価性を持つかを定義してあげる必要がある
- equalsメソッドを実装した場合は、hashCodeメソッドも実装する必要があり、正しくhashCodeを実装しない場合はHashMap等のオブジェクトにした際に期待した動作を得られない
- 実装しない場合は最上位のObjectクラスの各メソッドが呼び出され、インスタンスが全く同じ場合のみ等価性が判断されるためビジネスロジックに基づいたオブジェクト等価性の判断はできない
Factory Method
デザインパターン入門 | Factory Method(ファクトリメソッド)パターン
- Staticファクトリメソッドをコールすることで、コンストラクタ代わりにインスタンス生成が可能
- メリット
- 1つのクラスから生成するインスタンスを分けたい場合に、コンストラクタ名はクラス名と同じにしなければいけないため直感的な名称をつけることができないが、ファクトリメソッドであれば名称を分けることが可能
- スーパークラスをインスタンス化する際に、コンストラクタではそのものしか返せないがサブクラスのインスタンスを生成することも可能となる
- コンストラクタ生成と違ってエラー処理が通常のメソッドのように書ける
- 内部で生成されたインスタンスを保持しておけば、インスタンス化生成コストを下げることが可能
ドメイン駆動設計
ドメイン駆動設計 モデリング/実装ガイド
- ドメインとは、問題領域、業務領域とも訳されます。ソフトウェアが表現する対象全体のこと。例えば、金融業務、人事系業務、会計業務等
- モデルとは、ドメインを解決するためのオブジェクト1つ1つを指す。例えば金融業務であれば、通貨や支店、債券等。
- モデルは大きく2に分かれ、ここからソフトウェアの概念が出てくる。1つはエンティティと呼ばれ、可変な実態を指す。1つはバリューオブジェクトと呼ばれ、不変な実態を指す。エンティティとバリューオブジェクトの関係は、エンティティがバリューオブジェクトを保持している形になる。Javaのクラスでいうと、エンティティ内の変数がバリューオブジェクトになっているイメージ。DDD基礎解説:Entity、ValueObjectってなんなんだ
- モデル・ドメインはユビキタス言語と呼ばれ、業務メンバー・エンジニアの両方が理解できる言語であるべき。
マイクロサービスパターン
趣向を変えてマイクロサービスパターンを読んだ際のメモ書き。
👉Big ball of mud(大きな泥団子)パターン
例として、メール基盤をAmazon SES、メッセージングをTwilio、支払いサービスはStripeで提供等を一つの大きなWarで提供するようなモノリシックなJavaアーキテクチャ。めちゃくちゃな依存関係とスパゲッティコードのジャングル。論理設計ではどれほどモジュールが分断されていようが、物理的に1つになっているというのが大きな問題。これがJavaであればwarであり、Goであれば一つの実行ファイル、NodeJS等であれば1つのディレクトリにファイルが羅列される構造となる。
利点もある
- IDEなどの開発ツールは、基本は一つのアプリケーションの構築を元にして構成されているため初期の開発への入りは容易
- 1つのアーキテクチャでできているため初期はテスト・設計などが一枚岩で全体を見渡せる
- 1つのwar等実行形式であるため、デプロイが容易
今となっては欠点が多い
- 時間がたつにつれ、開発チーム・規模が増えた場合の管理~デプロイまでのコストの肥大化
- Lib管~デプロイマスターのような役割を持つメンバーが必要となる
- 規模が大きくなるにつれビルドへの時間が大きくなり生産性が低下
- 徹底的なテスト不足による信頼性の低下
- 陳腐化するテクノロジスタックに縛り付けられる
- チームメンバーが増えることでのコミュニケーションコストの増加
- フレッド・ブルックスによると、チーム規模Nに対してコミュニケーションコストはO(N^2)
👉マイクロサービスアーキテクチャパターン
モノリシックなパターンに比べた利点
- 大規模・複雑なアプリケーションでも分断して継続デリバリ・デプロイ・メンテナンス可能
- サービス個別デプロイ・個別スケーリング
- 各チームごとの品質の分断、チームへの自主性・自立性
- 新しいテクノロジの積極的な採用が可能
- 個々のサービスコードベースが小さいためビルド・デプロイももちろん短時間、IDEへの負担減
欠点
- 初期のアーキテクチャ構想の難易度が高い
- 設計を明確に行っていないと論理的に意味の分からないサービスが発生する
- そのうえ、サービスの適切な分断範囲を見つけるのが難しい
- 分断方法を間違えると結局依存が発生し、分断モノリスというアンチパターンができあがる
- 複数サービス間のデータ整合性を担保するのが難しい(sagaパターンを後に記載)
- 携わる開発メンバーのソフトウェア開発・デリバリースキルが高くないと難しい
モノリシックアーキテクチャから流用できない技術パターン
- トランザクション管理として、別スレッドに対する分散トランザクションは使えない
- 代わりにsagaパターンを使う
- データを手に入れるために別サービスのスキーマと結合してはいけない
- API Compositionパターンを利用してAPIを介したデータの収集
- CQRSパターンを利用して簡単なクエリで得たデータのレプリカを維持する
- 可観測性の担保やレイテンシ問題について、従来のログファイルを確認するだけでは網羅でき兄
- Health check APIの提供
- 統合Log aggregationサービスの提供
- Distributed Tracing, Audit Loggingにてユーザの行動をトラッキング
なぜマイクロサービスアーキテクチャが必要なのか
世間がシステムに求めるものは、これまではスケーラビリティ、リライアビリティ、セキュリティであったが、今はスピーディで安全なデリバリー(メンテナンス・テスト・デプロイの速度に依存する)が求められている。
👉継続的デリバリー
マイクロサービスアーキテクチャを実現するにあたって、ウォータフォール開発で行うことは愚の骨頂といえる。規模が大きくなるにつれウォータフォールという開発工程が必要であるが、コミュニケーションコストや管理コストを積み増す必要が出てくる。そういった本来のソフトウェア開発には不要なコストを削減するために1つ1つのサービスを小さしたのであれば、カンバン形式のIssue管理、Scrumを利用し、DevOpsの一部である継続的デリバリー・デプロイ(CI/CD)を導入することは必須といえる。
定義
あらゆるタイプのシステム変更(機能追加・構成変更・バグフィックス・テストデプロイ)を安全・スピーディに持続可能なかたちで本番システム、ユーザへ送り届ける能力を維持することをいう。
これを定量的に図る指標は以下の4つ
- デプロイ頻度
- リードタイム
- 平均修復時間
- 変更失敗率
👉Transition Management
継続的デリバリーへ向けた指標や、開発の手法がそもそも過去のモノリシックアーキテクチャの開発とは全く異なるため、組織の意思改革から行う必要がある。これらの過去の遺産からの遷移(transition)を行うためには、組織やメンバーへのTransition managementが必要。つまり、以下の3段階のフェーズに対して適切なフォローアップをする必要がある
終わり、喪失、別れ
多くの人々は、古いやり方が失われることに対して嘆き悲しむ。
- 大規模な機能横断型チームからの別れ
- 体制として管理コスト・Lib管コスト・PMO等を廃止する動きからの嘆きや脅威
- 各々のチームでスキーマを管理しなければいけないことによるインフラ知識の不足からくる恐れ
ニュートラルゾーン
新しい方法を提供され、古い手法との狭間で漂う人々は混乱し、苦闘する
新しい方法の始まり
人々は新しい方法を熱烈に支持し、利点を感じ始める
👉サービスの分割
アーキテクチャに求められる要件
- 機能要件 どのようなアーキテクチャでも満たすことが可能な要件
- サービス品質要件 アーキテクチャの種類に応じて満たすことができない場合もある要件
モノリシックアーキテクチャの例
階層化アーキテクチャ
プレゼンテーション層・ビジネスロジック層・永続化層に分かれるもっとも一般的なモノリシックアーキテクチャの例であり、現在では以下のデメリットもわかっている。
- 単一プレゼンテーション層⇒複数のソフトウェアやシステムから呼び出されることを考慮していない
- 単一永続化層⇒複数データベースを操作することを考慮していない
- 永続化層に依存したビジネスロジック(ソートナンバーやコード定義等をデータベースに持つことや、SQLでのビジネスロジックの実装を指す)
モノリシックアーキテクチャではない例
ヘキサゴナルアーキテクチャ
中心にビジネスロジックが存在し、データベースや外部のアプリケーション、メッセージング等の外部への永続化モジュールは、アダプタを介してビジネスロジックを呼び出す。
マイクロサービスアーキテクチャのサービス分割
- サービスとは
- サービスとは、何らかの役に立つ機能を実装するスタンドアロンの個別にデプロイできるソフトウェアコンポーネントのこと。しばしばユースケースの機能単位とイコールになる。
- 間違っていけないのは、個別にビルドできることではなく個別にデプロイできることである。つまりインフラ的な要素も含まれる。
- 疎結合
- サービス間のデータのやりとりはAPIを介してのみ行われ、サービスはデータベースをそのまま公開してはいけない。データに依存せず、サービス単位でスキーマを自由に変更できる。
- ただしサービスをまたがったデータの整合性・維持方針を考える必要がある。
- 単一責任の原則(Single Responsibility Principle, SRP):クラスを書き換える理由は、1つだけでなければならない。
- 閉鎖性共通の原則(Common Closure Principle, CCP):1つのパッケージにまとめられるクラス群は、同じ種類の変更に対して共に影響をうけるものであるべき。
- 共有ライブラリ
- とはいえ各サービスで共通したモジュールを利用したい場合も有る。Mavenなどを介して共通モジュールを利用できるようにすると良い。
- Decompose by business capability と Decompose by sub-domain
- サービスの分割方法として、業務による分割とサブドメインによる分割の2パターン存在する。
- 業務を1つのサービスとして考えることは直感で容易であるが、サブドメインによる分割(サブドメインに対応するドメインモデルを作成すれば、1つのサービスとなる)はここでは割愛。
👉サービス分割する上での障害
- ネットワークレイテンシ
- あるサービス間のラウンドトリップが膨大になる可能性がある
- 結果依存しあうサービスである可能性があるのでレイテンシによってはサービスの結合を考える
- 同期通信による可用性低下
- 非同期メッセージングを検討する
- サービス間データ整合性の維持
- 不可分性・原子性・アトミック性と呼ばれる、トランザクション内のタスクがすべて実行されるか、あるいはまったく実行しないかを保証することが難しい
- 2相コミットによる分散トランザクションはマイクロサービスでは不適切で、sagaを利用する。ただし結果整合性しか保証しない
- 分割を妨害するゴッドクラス
- 口座アカウントや保険証券などをクラスにしている場合、非常に大きな要素を抱えるゴッドクラスとなっているが、本来は中身はサービスとして分割可能である場合が多い
👉マイクロサービスにおけるトランザクション管理
なぜXAトランザクションがダメか
従来の分散取らなくションでぇあ、X/Open分散トランザクションという2相コミットを使ってトランザクション管理を行っていた。SQL可能なデータベースや、JavaEEでさえJTAを使ってXA準拠の分散トランザクションを利用できる。
ダメな理由
- NoSQLデータベースを含む新しいテクノロジの多くはXAをサポートしていない
- KafkaやRabbitMQのようなメッセージブローカも分散トランザクションをサポートしていない
- 同期IPCであるため、可用性が低下する(可用性は、すべてのサービスの可用性の積である)
sagaとは
saga自体は単純で、各サービスが処理の最後にsagaに対するメッセージを送信する。メッセージの受信をもって、sagaは次のサービスを実行する。しかし以下の課題がある。
- sagaには分離性(他のトランザクションに影響を与えない)が無い。
- sagaのロールバックには補償トランザクションを設計・実装する必要がある。
sagaには、以下のコーディネート種類があり、密結合を防ぐために、一般的にはオーケストレーションベースのsagaを実装する。
- コレオグラフィ
- サーガの参加サービスに、次のステップの判断を委ねる分散管理方法
- オーケストレーション
- サーガによって、コーディネートロジックを一元管理する方法
カウンターメジャー
分離性の欠如に対応する設計のこと。分離性が欠如すると、以下の異常が発生する。
- 更新の消失
- ほかのサーガが加えた変更をサーガが上書きしてしまう
- ダーティリード
- 更新を終えていない(失敗してロールバックする可能性もある)サーガが加えた変更をほかのサーガが読み取ってしまう
- 反復不能読み取り・ファジー
- 同一サーガの2つのステップで同一データを読む間に、別のサーガがデータを書き換えてい閉まった