「キャッシュレス・ロードマップ 2023」を読んで

一般社団法人キャッシュレス推進協議会が毎年発表している、キャッシュレス・ロードマップの2023年版が発表されたので読みました。振り返えれるように関心が高い部分だけまとめます。

はじめに

「キャッシュレス・ロードマップ2023」の策定に向けては、社会の共通課題であるSDGsの達成においてもキャッシュレスが貢献できることを明確にするとともに、環境問題の原因として取り上げられる二酸化炭素の排出量におけるキャッシュレスの関与についても議論を重ねてきた。

1 キャッシュレスの動向

1.1 キャッシュレス決済比率

1.1.1 日本のキャッシュレス決済比率の推移

日本のキャッシュレス決済比率は、2022年に36.0%まで到達した。

期間 キャッシュレス決済の増加比率
2008~2013年 前年比 +1.0%pt 以下
2013~2017年 前年比 +2.0%pt 未満
2017~2021年 前年比 +2.0%pt 以上

政府は「2025年6月までに、キャッシュレス決済比率を倍増し、4割程度とすることを目指す。」というKPIを掲げており、達成はかなり現実味を帯びている。

我が国のキャッシュレス決済額及び比率の推移(2022年)

2022年のキャッシュレス決済比率を算出しました (METI/経済産業省) から転載)

コード決済は、2020年にはデビットカード、2022年には電子マネーを抜き、日本で2番目に利用されている決済手段となった。増加率も50%を2年連続で超え、急激に普及してきた。電子マネーのキャッシュレスに占める割合は現象傾向が続いている。要因としては、コロナ禍の影響に伴い電車等による人の移動が減少したことにより、日常的に交通系を始めとする電子マネーへのチャージや利用の機会の減少が影響していると考えられる。デビットカードは堅調に割合を増やしてきており、増加率も2番目の高水準で推移している。また、クレジットカードの増加率は2010年以降で最も大きな値となっている。クレジットカードのタッチ決済も普及しており、クレジットカードをより日常利用する機会が増えてきたことも影響として考えられる。

2022年はすべての決済手段で増加率がプラスとなっており、キャッシュレス決済手段を利用するシーンが着実に増えてきていることがわかる。2022年にはキャッシュレス決済において、おおよそ4回に1回がコード決済で行われていることとなる。

キャッシュレス全体の平均利用金額は約3,400円となっている。利用金額に合わせて決済手段を選択している可能性が高く、日々の生活シーンに合わせて使い分けが進んでいると考えられる。

1.1.2 一般社団法人全国銀行協会における調査

2022年は、個人の給与受取口座等からの出金のうち、57.5%が振込等を含めたキャッシュレスで行われている。なお、2021年調査では、55.4%であった。

キャッシュレスによる払出し比率の調査結果( 2022年通期)
キャッシュレスによる払出し比率の調査結果( 2022年通期)から転載)

1.2 主要国におけるキャッシュレスの状況

2021年の世界主要国におけるキャッシュレス決済比率

国名 決済比率
韓国 95.3%
中国 83.8%
オーストラリア 72.8%
イギリス 65.1%
シンガポール 63.8%
カナダ 63.6%
アメリ 53.2%
フランス 50.4%
スウェーデン 46.6%
日本 32.5%
ドイツ 22.2%

直近5年では、日本が1番キャッシュレス決済比率の成長が大きい。他国も同様に上昇傾向にあり、グローバル全体でのキャッシュレス利用が進展している。

1.2.2 各国の主要なキャッシュレス決済手段

主要国におきえるキャッシュレス決済手段保有状況について、2021年時点で日本は一人当たり平均してクレジットカードの保有数が2.4枚、デビットカードが3.7枚、そして電子マネーが4.2枚と、合計して約10.2枚を保有しており、諸外国と比べ、一人当たり多くのキャッシュレス決済手段保有している。

日本のキャッシュレス決済手段保有状況

決済手段 2018年 2019年 2020年 2021年
クレジットカード 2.3枚 2.3枚 2.3枚 2.4枚
デビットカード 3.5枚 3.6枚 3.6枚 3.7枚
電子マネー 3.2枚 3.6枚 3.9枚 4.2枚
合計 9.0枚 9.5枚 9.9枚 10.2枚

各国のキャッシュレス決済額を %とした場合の手段別の割合だが、ヨーロッパでは、デビットカードの利用割合が高く、その他の国では、おおよそクレジットカードとデビットカードの利用が均衡している。それに比べ、日本はクレジットカードの利用割合が非常に高いことに加え、電子マネーの利用割合も大きい。 海外、特にヨーロッパでは、比較的クレジットカードの審査が厳しい等が指摘されており、保有に制約の少ないデビットカードの利用が進んでいると考えられる。

2 我が国におけるキャッシュレスの動向

2.1 総務省「マイナポイント事業」

2.1.1 事業の概要

消費の活性化、生活の質の向上、マイナンバーカードの普及促進及び官民キャッシュレス決済基盤の構築を行うことを目的としている。取得したマイナンバーカードを用いて、スマートフォンやパソコン、所定の店舗等において手続きを行うと、自身が選択したキャッシュレス決済手段のポイントが付与される。

2.1.2 消費者の状況

2022年度当初 43.3%であった人口に対するマイナンバーカードの交付枚数率は、2023年3月末時点で、67.0%まで増加した。

2.1.3 決済事業者の参加状況

マイナポイント事業には、2023年3月末現在、101の決済サービスにおいてマイナポイントの受取が可能となっている。これらの決済サービスのうち、電子マネーが約半数を占め、次いで、コード決済、プリペイドカードとなる。

2.2 経済産業省「キャッシュレスの将来像に関する検討会」

経済産業省は、2022 年 9 月より「キャッシュレスの将来像に関する検討会」を 開催した。本検討会における議論内容については、「キャッシュレスの将来像に関 する報告書」としてとりまとめられ、2023 年 3 月に公表された。

2.2.1 日本におけるキャッシュレスの現状

何らかのキャッシュレス決済手段を導入している店舗の割合は、80%であった。一方、クレジットカード、電子マネー、コード決済の全てを導入している店舗は23.4%となっており、店舗のおかれた状況により、導入する決済手段を選別していることがうかがえる。

キャッシュレス決済の導入状況

キャッシュレスの将来像に関する検討会 とりまとめより)

顧客単価が高まるにつれクレジットカードの導入率が高まり、反対に顧客単価が低い店舗ではコード決済の導入が進んでいる。また、顧客単価が5,000円以下となるとキャッシュレス決済そのものの導入率が低くなることが示されている。

店舗がキャッシュレス決済を導入した結果として、「レジ決済時間の短縮」といった効率化に関する要素が大きい。ただし、約半数の店舗では効果を実感されていないこともわかった。

2.2.2 キャッシュレスの社会的意義

検討会では、まずキャッシュレスには、「物理的な制約である『モノ・空間・時間・情報量』から解放される」特性があると分析している。 このような特性から、望ましいキャッシュレスの姿を「請求・認証・精算全てがデジタル化され、請求データに基づき精算が自動実行されるもの」と設定し、キャッシュレスの社会的意義を「『人々と企業の活動』に密接に関わる『決済』を変革することで、既存の課題を解決し、新たな未来を想像する」ことにあるとしている。

2.2.3 キャッシュレスによって目指す社会

キャッシュレスの目指す姿(将来像)に求められるコンセプトが、「支払を意識しない決済が広がり、データがシームレスに連携されるデジタル社会」となっていくと指摘している。また、目指す姿に変化していくことで、現状に おけるキャッシュレスの利用領域や決済方法、提供される主な付加価値も変化が生 じると想定されている。

2.3 その他の政府等における取組

2.3.1 インターチェンジフィーの公開

クレジットカードや他の決済方法の加盟店管理市場における加盟店・アクワイアラ間の加盟店手数料の交渉や、アクワイアラ間の競争を促進する観点から、自らがカード発行や加盟店管理を行わない国際ブランドにあっては、我が国においても、インターチェンジフィーの標準料率を公開することが適当である等の考え方が示されている。 これを踏まえ、経済産業省および公正取引委員会では、国際ブランドにおけるイ ンターチェンジフィーの標準料率の公開に向けた取組を進め、Mastercard、Union Pay (銀聯) 及び Visa がクレジットカードのインターチェンジフィーの標準料率 を公開している。

Amazon Auroraのフェイルオーバーに向けたMySQL JDBCドライバーの検証

業務でAmazon Auroraを利用しています。フェイルオーバーをサポートしてくれていたMariaDB Connector/Jを利用していたのですが、1.3.0からフェイルオーバーのサポートをしなくなったので、乗り換え先を探していました。

About MariaDB Connector/J - MariaDB Knowledge Base

調べてみたら、aws-mysql-jdbcと、aws-advanced-jdbc-wrapperが選択肢として上がりました。どちらもAWS謹製なのですが、ざっくり言うと前者がJDBCドライバーで、後者がJDBCドライバーのWrapperでMySQLPostgreSQLなどのJDBCドライバーと併せて使うものになります。

検証

先に挙げた2つのライブラリの検証をしました。結論から言うと、Read-Writeで接続先を振り分ける(読み込み時はReader、書き込み時はWriterに接続する)という要件を満たしているのが、aws-advanced-jdbc-wrapperだけだったので、これをを利用することを決めました。 aws-mysql-jdbcのこれに関する議論はこちらで行われています。

今回の環境

- Java 17
- Spring Boot 3.1.0
- aws-mysql-jdbc 1.1.7
- aws-advanced-jdbc-wrapper 2.2.2
- Amazon Aurora MySQL 8.0.mysql_aurora.3.03.1
  - Readerは1台

検証手順

1. アプリケーションの接続先URLをクラスターエンドポイントにする
2. APIを起動
3. curlで連続的にデータ書き込み用エンドポイントを叩く
4. curlで連続的にデータ参照用エンドポイントを叩く
5. 手動でフェイルオーバーを起こす

MySQL Connector/J

比較のために、MySQL Connector/Jについても検証してみました。

アプリケーションの設定

application.yml

spring:
  datasource:
    url: jdbc:mysql:replication://${クラスターエンドポイント},${Read-Onlyクラスターエンドポイント}/test_db

build.gradle

dependencies {
    runtimeOnly 'com.mysql:mysql-connector-j'
}
フェイルオーバー実行

フェイルオーバー実行すると、DB接続に失敗後に、降格した(WriterからReaderになった)インスタンスに接続しつづけるためread-onlyのトランザクションで書き込みしようとしていると怒られました。もちろんAuroraのフェイルオーバーには対応していませんでした。

2023-06-25T19:26:17.301+09:00  WARN 26196 --- [nio-8080-exec-1] com.zaxxer.hikari.pool.ProxyConnection   : HikariPool-1 - Connection com.mysql.cj.jdbc.ConnectionImpl@61e86192 marked as broken because of SQLSTATE(08S01), ErrorCode(0)

com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet successfully received from the server was 34 milliseconds ago. The last packet sent successfully to the server was 37 milliseconds ago.

...

2023-06-25T19:26:17.397+09:00  WARN 26196 --- [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@29bd6f67 (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
Hibernate: insert into shop (name) values (?)
2023-06-25T19:26:27.762+09:00  WARN 26196 --- [nio-8080-exec-2] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1836, SQLState: HY000
2023-06-25T19:26:27.763+09:00 ERROR 26196 --- [nio-8080-exec-2] o.h.engine.jdbc.spi.SqlExceptionHelper   : Running in read-only mode

Amazon Web Services (AWS) JDBC Driver for MySQL

Amazon Web Services (AWS) JDBC Driver for MySQLクラスタリングされたMySQLデータベースを利用したアプリケーションにメリットがあります。これは、MySQL Connector/J driverをベースにしており、ドロップイン互換性があります。

The AWS JDBC Driver for MySQLは、MySQL互換のAmazon Auroraのための素早いフェイルオーバーをサポートしています。クラスタリングされたデータベースの機能に加えて、Amazon RDS for MySQLやオンプレミス環境のMySQLのための機能も含んでサポートしています。

より詳しいことは、公式ドキュメントを参照してください。

アプリケーションの設定

application.yml

spring:
  datasource:
    url: jdbc:mysql:aws://${クラスターエンドポイント}/test_db
    driver-class-name: software.aws.rds.jdbc.mysql.Driver

build.gradle

dependencies {
   runtimeOnly "software.aws.rds:aws-mysql-jdbc:1.1.7"
}
フェイルオーバー実行

常にクラスターエンドポイントに接続しているので、フェイルオーバーを実行後もそのままクラスターエンドポイントに接続し続けます。ただ、フェイルオーバー中に書き込みしていたコネクションは失われます。

2023-07-09T17:16:36.030+09:00  INFO 26316 --- [nio-8080-exec-6] c.b.j.controller.ShopController          : saved shop.(id = 27420)
2023-07-09T17:16:48.181+09:00  WARN 26316 --- [nio-8080-exec-3] com.zaxxer.hikari.pool.ProxyConnection   : HikariPool-1 - Connection software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ConnectionImpl@28814a75 marked as broken because of SQLSTATE(08007), ErrorCode(0)
java.sql.SQLException: Transaction resolution unknown. Please re-configure session state if required and try restarting transaction.

2023-07-09T17:16:48.204+09:00 ERROR 26316 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.orm.jpa.JpaSystemException: Unable to commit against JDBC Connection] with root cause
java.sql.SQLException: Transaction resolution unknown. Please re-configure session state if required and try restarting transaction.

2023-07-09T17:16:48.645+09:00  WARN 26316 --- [nio-8080-exec-7] com.zaxxer.hikari.pool.ProxyConnection   : HikariPool-1 - Connection software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ConnectionImpl@61d94a02 marked as broken because of SQLSTATE(08S02), ErrorCode(0)

java.sql.SQLException: The active SQL connection has changed due to a connection failure. Please re-configure session state if required.

2023-07-09T17:16:50.323+09:00  WARN 26316 --- [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ConnectionImpl@7708a0c5 (The active SQL connection has changed due to a connection failure. Please re-configure session state if required.). Possibly consider using a shorter maxLifetime value.
2023-07-09T17:16:50.763+09:00  WARN 26316 --- [io-8080-exec-10] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ConnectionImpl@540afcc8 (The active SQL connection has changed due to a connection failure. Please re-configure session state if required.). Possibly consider using a shorter maxLifetime value.
2023-07-09T17:16:52.421+09:00  WARN 26316 --- [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ConnectionImpl@33480b36 (The active SQL connection has changed due to a connection failure. Please re-configure session state if required.). Possibly consider using a shorter maxLifetime value.
2023-07-09T17:16:52.845+09:00  WARN 26316 --- [io-8080-exec-10] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ConnectionImpl@7a3aa305 (The active SQL connection has changed due to a connection failure. Please re-configure session state if required.). Possibly consider using a shorter maxLifetime value.
2023-07-09T17:16:54.516+09:00  WARN 26316 --- [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ConnectionImpl@2adac41f (The active SQL connection has changed due to a connection failure. Please re-configure session state if required.). Possibly consider using a shorter maxLifetime value.
2023-07-09T17:16:54.929+09:00  WARN 26316 --- [io-8080-exec-10] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ConnectionImpl@4f352185 (The active SQL connection has changed due to a connection failure. Please re-configure session state if required.). Possibly consider using a shorter maxLifetime value.
2023-07-09T17:16:56.601+09:00  WARN 26316 --- [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ConnectionImpl@23c98d1f (The active SQL connection has changed due to a connection failure. Please re-configure session state if required.). Possibly consider using a shorter maxLifetime value.
2023-07-09T17:16:56.641+09:00  INFO 26316 --- [nio-8080-exec-2] c.b.j.controller.ShopController          : saved shop.(id = 27421)

Amazon Web Services (AWS) JDBC Driver

Amazon Web Services (AWS) JDBC Driverは、進化したJDBC wrapperとして再設計されました。このwrapperは、Amazon Auroraのようなクラスタリングされたデータベースの機能を活用するアプリケーションをサポートするために存在するJDBCドライバーを機能拡張したものです。AWS JDBC Driverはいくつかのデータベース自身に接続する実装はしていないですが、ユーザが選択しているJDBCドライバーをAWSやAuroraの機能性をサポートを有効にできます。

詳しいことは、公式ドキュメントを参照してください。

アプリケーションの設定

application.yml

spring:
  datasource:
    url: jdbc:aws-wrapper:mysql://${クラスターエンドポイント}/test_db
    driver-class-name: software.amazon.jdbc.Driver
    hikari:
      data-source-properties:
        # プラグインを有効化。
        # readWriteSplitting:readerとwriterの接続切り替え
        # failover: Auroraのフェイルオーバーサポート
        # efm: ホストコネクション失敗のモニタリング。失敗発見率の向上
        wrapperPlugins: readWriteSplitting,failover,efm
        # フェイルオーバー時、Readerにアクセスできない場合はWriterにアクセスする(デフォルトはstrict-writer)
        failoverMode: reader-or-writer
      # フェイルオーバーに関する例外をハンドリングするため
      exception-override-class-name: software.amazon.jdbc.util.HikariCPSQLException

# 挙動確認のためのログ出力
logging:
  level:
    software.amazon.jdbc.Driver: trace
    software.amazon.jdbc.plugin.readwritesplitting: trace
    software.amazon.jdbc.plugin.staledns: trace
    software.amazon.jdbc.plugin.failover: trace

build.gradle

dependencies {
    implementation 'software.amazon.jdbc:aws-advanced-jdbc-wrapper:2.2.2'
    runtimeOnly 'com.mysql:mysql-connector-j'
}
フェイルオーバー実行

クラスターエンドポイントを指定するだけで、ライターインスタンスエンドポイントとリーダーインスタンスエンドポイントを検出してくれます。 連続的にデータ書き込み&読み込み用エンドポイントを叩き中のログを見ると、ReadOnlyのコネクションはReaderに接続していることがわかりました。念の為、CloudWatchメトリクスでDatabaseConnectionsを見てみましたが、ちゃんとReaderに接続されていました。 フェイルオーバー開始から完了までほぼダウンタイムが発生しないことがわかりました。

# アプリケーション起動中
> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.0)


 2023-07-05T00:32:43.445+09:00 TRACE 44369 --- [           main] software.amazon.jdbc.Driver              : Opening connection to jdbc:aws-wrapper:mysql://${クラスターエンドポイント}/test_db
2023-07-05T00:32:43.509+09:00 DEBUG 44369 --- [           main] s.a.j.p.f.FailoverConnectionPlugin       : failoverMode=READER_OR_WRITER
2023-07-05T00:32:43.513+09:00 TRACE 44369 --- [           main] s.a.j.p.f.FailoverConnectionPlugin       : Changes:
        Host '${クラスターエンドポイント}/': [NODE_ADDED]
2023-07-05T00:32:43.971+09:00 TRACE 44369 --- [           main] s.a.j.p.staledns.AuroraStaleDnsHelper    : Cluster endpoint resolves to x.x.x.x.
2023-07-05T00:32:43.986+09:00 TRACE 44369 --- [           main] s.a.j.p.f.FailoverConnectionPlugin       : Changes:
        Host '${クラスターエンドポイント}': [NODE_DELETED]
        Host '${リーダーインスタンスエンドポイント}': [NODE_ADDED]
        Host '${ライターインスタンスエンドポイント}': [NODE_ADDED]
2023-07-05T00:32:43.988+09:00 TRACE 44369 --- [           main] s.a.j.p.staledns.AuroraStaleDnsHelper    : Topology:
   HostSpec[host=${リーダーインスタンスエンドポイント}, port=-1, READER, AVAILABLE, weight=2450, 2023-07-04 15:32:44.108693]
   HostSpec[host=${ライターインスタンスエンドポイント}, port=-1, WRITER, AVAILABLE, weight=4, 2023-07-04 15:32:43.890895]
2023-07-05T00:32:43.989+09:00 TRACE 44369 --- [           main] s.a.j.p.staledns.AuroraStaleDnsHelper    : Writer host: HostSpec[host=${ライターインスタンスエンドポイント}, port=-1, WRITER, AVAILABLE, weight=4, 2023-07-04 15:32:43.890895]
2023-07-05T00:32:43.991+09:00 TRACE 44369 --- [           main] s.a.j.p.staledns.AuroraStaleDnsHelper    : Writer host address: x.x.x.x
2023-07-05T00:32:43.998+09:00 TRACE 44369 --- [           main] s.a.j.p.r.ReadWriteSplittingPlugin       : Writer connection set to '${クラスターエンドポイント}'
2023-07-05T00:32:44.027+09:00  INFO 44369 --- [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection software.amazon.jdbc.wrapper.ConnectionWrapper@53a50b0a - com.mysql.cj.jdbc.ConnectionImpl@74ba6ff5
2023-07-05T00:32:44.029+09:00  INFO 44369 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2023-07-05T00:32:44.068+09:00  INFO 44369 --- [           main] o.f.c.i.database.base.BaseDatabaseType   : Database: jdbc:mysql://${クラスターエンドポイント}/test_db (MySQL 8.0)
2023-07-05T00:32:44.131+09:00 TRACE 44369 --- [onnection adder] software.amazon.jdbc.Driver              : Opening connection to jdbc:aws-wrapper:mysql://${クラスターエンドポイント}/test_db
2023-07-05T00:32:44.132+09:00 DEBUG 44369 --- [onnection adder] s.a.j.p.f.FailoverConnectionPlugin       : failoverMode=READER_OR_WRITER
2023-07-05T00:32:44.133+09:00 TRACE 44369 --- [onnection adder] s.a.j.p.f.FailoverConnectionPlugin       : Changes:
        Host '${リーダーインスタンスエンドポイント}': [NODE_ADDED]
        Host '${ライターインスタンスエンドポイント}': [NODE_ADDED]
2023-07-05T00:32:44.217+09:00 TRACE 44369 --- [onnection adder] s.a.j.p.staledns.AuroraStaleDnsHelper    : Cluster endpoint resolves to x.x.x.x.
2023-07-05T00:32:44.218+09:00  INFO 44369 --- [           main] o.f.core.internal.command.DbValidate     : Successfully validated 1 migration (execution time 00:00.067s)
2023-07-05T00:32:44.224+09:00 TRACE 44369 --- [onnection adder] s.a.j.p.staledns.AuroraStaleDnsHelper    : Topology:
   HostSpec[host=${リーダーインスタンスエンドポイント}, port=-1, READER, AVAILABLE, weight=2450, 2023-07-04 15:32:44.108693]
   HostSpec[host=${ライターインスタンスエンドポイント}, port=-1, WRITER, AVAILABLE, weight=4, 2023-07-04 15:32:43.890895]
2023-07-05T00:32:44.225+09:00 TRACE 44369 --- [onnection adder] s.a.j.p.staledns.AuroraStaleDnsHelper    : Writer host: HostSpec[host=${ライターインスタンスエンドポイント}, port=-1, WRITER, AVAILABLE, weight=4, 2023-07-04 15:32:43.890895]
2023-07-05T00:32:44.225+09:00 TRACE 44369 --- [onnection adder] s.a.j.p.staledns.AuroraStaleDnsHelper    : Writer host address: x.x.x.x
2023-07-05T00:32:44.231+09:00 TRACE 44369 --- [onnection adder] s.a.j.p.r.ReadWriteSplittingPlugin       : Writer connection set to '${クラスターエンドポイント}'
2023-07-05T00:32:44.248+09:00 TRACE 44369 --- [onnection adder] software.amazon.jdbc.Driver              : Opening connection to jdbc:aws-wrapper:mysql://${クラスターエンドポイント}/test_db
2023-07-05T00:32:44.249+09:00 DEBUG 44369 --- [onnection adder] s.a.j.p.f.FailoverConnectionPlugin       : failoverMode=READER_OR_WRITER
2023-07-05T00:32:44.250+09:00 TRACE 44369 --- [onnection adder] s.a.j.p.f.FailoverConnectionPlugin       : Changes:
        Host '${リーダーインスタンスエンドポイント}': [NODE_ADDED]
        Host '${ライターインスタンスエンドポイント}': [NODE_ADDED]

# 連続的にデータ書き込み&読み込み用エンドポイントを叩き中
2023-07-05T00:32:58.666+09:00  INFO 44369 --- [nio-8080-exec-4] c.b.j.controller.ShopController          : saved shop.(id = 22272)
2023-07-05T00:32:58.704+09:00  INFO 44369 --- [nio-8080-exec-6] c.b.j.controller.ShopController          : saved shop.(id = 22273)
2023-07-05T00:32:58.737+09:00 TRACE 44369 --- [nio-8080-exec-8] s.a.j.p.r.ReadWriteSplittingPlugin       : Successfully connected to a new reader host: ${リーダーインスタンスエンドポイント}'
2023-07-05T00:32:58.738+09:00 TRACE 44369 --- [nio-8080-exec-8] s.a.j.p.r.ReadWriteSplittingPlugin       : Reader connection set to '${リーダーインスタンスエンドポイント}'
2023-07-05T00:32:58.744+09:00  INFO 44369 --- [nio-8080-exec-9] c.b.j.controller.ShopController          : saved shop.(id = 22274)
2023-07-05T00:32:58.753+09:00 TRACE 44369 --- [nio-8080-exec-8] s.a.j.p.r.ReadWriteSplittingPlugin       : Reader connection set to '${リーダーインスタンスエンドポイント}'
2023-07-05T00:32:58.754+09:00 TRACE 44369 --- [nio-8080-exec-8] s.a.j.p.r.ReadWriteSplittingPlugin       : Setting the current connection to '${リーダーインスタンスエンドポイント}'
2023-07-05T00:32:58.755+09:00 DEBUG 44369 --- [nio-8080-exec-8] s.a.j.p.r.ReadWriteSplittingPlugin       : Switched from a writer to a reader host. New reader host: '${リーダーインスタンスエンドポイント}'
2023-07-05T00:32:58.782+09:00  INFO 44369 --- [nio-8080-exec-1] c.b.j.controller.ShopController          : saved shop.(id = 22275)
2023-07-05T00:32:58.822+09:00  INFO 44369 --- [nio-8080-exec-3] c.b.j.controller.ShopController          : saved shop.(id = 22276)
2023-07-05T00:32:58.860+09:00  INFO 44369 --- [nio-8080-exec-5] c.b.j.controller.ShopController          : saved shop.(id = 22277)
2023-07-05T00:32:58.900+09:00  INFO 44369 --- [nio-8080-exec-7] c.b.j.controller.ShopController          : saved shop.(id = 22278)
2023-07-05T00:32:58.961+09:00  INFO 44369 --- [io-8080-exec-10] c.b.j.controller.ShopController          : saved shop.(id = 22279)
2023-07-05T00:32:59.010+09:00  INFO 44369 --- [nio-8080-exec-2] c.b.j.controller.ShopController          : saved shop.(id = 22280)
2023-07-05T00:32:59.020+09:00 TRACE 44369 --- [nio-8080-exec-8] s.a.j.p.r.ReadWriteSplittingPlugin       : Writer connection set to '${ライターインスタンスエンドポイント}'
2023-07-05T00:32:59.020+09:00 TRACE 44369 --- [nio-8080-exec-8] s.a.j.p.r.ReadWriteSplittingPlugin       : Setting the current connection to '${ライターインスタンスエンドポイント}'
2023-07-05T00:32:59.020+09:00 DEBUG 44369 --- [nio-8080-exec-8] s.a.j.p.r.ReadWriteSplittingPlugin       : Switched from a reader to a writer host. New writer host: '${ライターインスタンスエンドポイント}'

# フェイルオーバー開始時
2023-07-05T00:33:33.372+09:00  INFO 44369 --- [nio-8080-exec-5] c.b.j.controller.ShopController          : saved shop.(id = 23087)
2023-07-05T00:33:33.387+09:00 DEBUG 44369 --- [nio-8080-exec-6] s.a.j.p.f.FailoverConnectionPlugin       : Detected an exception while executing a command: Communications link failure

# フェイルオーバー中
The last packet successfully received from the server was 124 milliseconds ago. The last packet sent successfully to the server was 137 milliseconds ago.
2023-07-05T00:33:33.387+09:00 TRACE 44369 --- [nio-8080-exec-6] s.a.j.p.f.FailoverConnectionPlugin       : Changes:
        Host '${フェイルオーバー前のリーダーインスタンスエンドポイント}': [WENT_DOWN, NODE_CHANGED]
2023-07-05T00:33:33.388+09:00 DEBUG 44369 --- [nio-8080-exec-6] s.a.j.p.f.FailoverConnectionPlugin       : Starting reader failover procedure.
2023-07-05T00:33:33.390+09:00 DEBUG 44369 --- [pool-4-thread-1] .j.p.f.ClusterAwareReaderFailoverHandler : Trying to connect to reader: '${フェイルオーバー前のライターインスタンスエンドポイント}', with properties '{failoverMode=reader-or-writer, password=rootroot, database=test_db, wrapperPlugins=readWriteSplitting,failover,efm, user=root}'
2023-07-05T00:33:33.390+09:00 DEBUG 44369 --- [pool-4-thread-2] .j.p.f.ClusterAwareReaderFailoverHandler : Trying to connect to reader: '${フェイルオーバー前のリーダーインスタンスエンドポイント}', with properties '{failoverMode=reader-or-writer, password=rootroot, database=test_db, wrapperPlugins=readWriteSplitting,failover,efm, user=root}'
2023-07-05T00:33:33.416+09:00  INFO 44369 --- [nio-8080-exec-2] c.b.j.controller.ShopController          : saved shop.(id = 23088)
2023-07-05T00:33:33.455+09:00  INFO 44369 --- [nio-8080-exec-9] c.b.j.controller.ShopController          : saved shop.(id = 23089)
2023-07-05T00:33:33.463+09:00 DEBUG 44369 --- [pool-4-thread-1] .j.p.f.ClusterAwareReaderFailoverHandler : Connected to reader: '${フェイルオーバー前のライターインスタンスエンドポイント}'
2023-07-05T00:33:33.463+09:00 DEBUG 44369 --- [pool-4-thread-1] .j.p.f.ClusterAwareReaderFailoverHandler : New reader connection object: com.mysql.cj.jdbc.ConnectionImpl@1312cce0
2023-07-05T00:33:33.464+09:00 TRACE 44369 --- [nio-8080-exec-6] s.a.j.p.r.ReadWriteSplittingPlugin       : Writer connection set to '${フェイルオーバー前のライターインスタンスエンドポイント}'
2023-07-05T00:33:33.471+09:00 DEBUG 44369 --- [nio-8080-exec-6] s.a.j.p.f.FailoverConnectionPlugin       : Connected to: HostSpec[host=${フェイルオーバー前のライターインスタンスエンドポイント} port=-1, WRITER, AVAILABLE, weight=4, 2023-07-04 15:32:43.890895]
2023-07-05T00:33:33.472+09:00 ERROR 44369 --- [nio-8080-exec-6] s.a.j.p.f.FailoverConnectionPlugin       : The active SQL connection has changed due to a connection failure. Please re-configure session state if required.
2023-07-05T00:33:33.475+09:00  WARN 44369 --- [nio-8080-exec-6] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 0, SQLState: 08S02
2023-07-05T00:33:33.475+09:00 ERROR 44369 --- [nio-8080-exec-6] o.h.engine.jdbc.spi.SqlExceptionHelper   : The active SQL connection has changed due to a connection failure. Please re-configure session state if required.
2023-07-05T00:33:33.482+09:00 DEBUG 44369 --- [nio-8080-exec-6] s.a.j.p.f.FailoverConnectionPlugin       : Detected an exception while executing a command: Can't call rollback when autocommit=true
2023-07-05T00:33:33.483+09:00 TRACE 44369 --- [nio-8080-exec-6] s.a.j.p.f.FailoverConnectionPlugin       : Changes:
        Host '${フェイルオーバー前のライターインスタンスエンドポイント}': [WENT_DOWN, NODE_CHANGED]
2023-07-05T00:33:33.483+09:00 DEBUG 44369 --- [nio-8080-exec-6] s.a.j.p.f.FailoverConnectionPlugin       : Starting reader failover procedure.
2023-07-05T00:33:33.484+09:00 DEBUG 44369 --- [pool-6-thread-1] .j.p.f.ClusterAwareReaderFailoverHandler : Trying to connect to reader: '${フェイルオーバー前のライターインスタンスエンドポイント}', with properties '{failoverMode=reader-or-writer, password=rootroot, database=test_db, wrapperPlugins=readWriteSplitting,failover,efm, user=root}'
2023-07-05T00:33:33.484+09:00 DEBUG 44369 --- [pool-6-thread-2] .j.p.f.ClusterAwareReaderFailoverHandler : Trying to connect to reader: '${フェイルオーバー前のリーダーインスタンスエンドポイント}', with properties '{failoverMode=reader-or-writer, password=rootroot, database=test_db, wrapperPlugins=readWriteSplitting,failover,efm, user=root}'
2023-07-05T00:33:33.499+09:00  INFO 44369 --- [nio-8080-exec-8] c.b.j.controller.ShopController          : saved shop.(id = 23090)
2023-07-05T00:33:33.515+09:00 DEBUG 44369 --- [pool-6-thread-2] .j.p.f.ClusterAwareReaderFailoverHandler : Failed to connect to reader: '${フェイルオーバー前のリーダーインスタンスエンドポイント}'
2023-07-05T00:33:33.517+09:00 DEBUG 44369 --- [pool-4-thread-2] .j.p.f.ClusterAwareReaderFailoverHandler : Failed to connect to reader: '${フェイルオーバー前のリーダーインスタンスエンドポイント}'
2023-07-05T00:33:33.536+09:00 TRACE 44369 --- [pool-6-thread-1] s.a.j.p.f.FailoverConnectionPlugin       : Changes:
        Host '${フェイルオーバー前のライターインスタンスエンドポイント}': [WENT_UP, NODE_CHANGED]
2023-07-05T00:33:33.537+09:00 DEBUG 44369 --- [pool-6-thread-1] .j.p.f.ClusterAwareReaderFailoverHandler : Connected to reader: '${フェイルオーバー前のライターインスタンスエンドポイント}'
2023-07-05T00:33:33.537+09:00 DEBUG 44369 --- [pool-6-thread-1] .j.p.f.ClusterAwareReaderFailoverHandler : New reader connection object: com.mysql.cj.jdbc.ConnectionImpl@55e4bfde
2023-07-05T00:33:33.537+09:00 TRACE 44369 --- [nio-8080-exec-6] s.a.j.p.r.ReadWriteSplittingPlugin       : Writer connection set to '${フェイルオーバー前のライターインスタンスエンドポイント}'
2023-07-05T00:33:33.538+09:00  INFO 44369 --- [nio-8080-exec-1] c.b.j.controller.ShopController          : saved shop.(id = 23091)
2023-07-05T00:33:33.542+09:00 DEBUG 44369 --- [nio-8080-exec-6] s.a.j.p.f.FailoverConnectionPlugin       : Connected to: HostSpec[host=${フェイルオーバー前のライターインスタンスエンドポイント}, port=-1, WRITER, AVAILABLE, weight=4, 2023-07-04 15:32:43.890895]
2023-07-05T00:33:33.542+09:00 ERROR 44369 --- [nio-8080-exec-6] s.a.j.p.f.FailoverConnectionPlugin       : The active SQL connection has changed due to a connection failure. Please re-configure session state if required.
2023-07-05T00:33:33.548+09:00 ERROR 44369 --- [nio-8080-exec-6] o.s.t.i.TransactionInterceptor           : Application exception overridden by rollback exception

2023-07-05T00:33:33.581+09:00  INFO 44369 --- [nio-8080-exec-3] c.b.j.controller.ShopController          : saved shop.(id = 23092)
2023-07-05T00:33:33.620+09:00  WARN 44369 --- [nio-8080-exec-9] s.a.j.p.r.ReadWriteSplittingPlugin       : Failed to connect to reader host: '${フェイルオーバー前のリーダーインスタンスエンドポイント}'
2023-07-05T00:33:33.623+09:00  INFO 44369 --- [nio-8080-exec-4] c.b.j.controller.ShopController          : saved shop.(id = 23093)
2023-07-05T00:33:33.628+09:00  WARN 44369 --- [nio-8080-exec-9] s.a.j.p.r.ReadWriteSplittingPlugin       : Failed to connect to reader host: '${フェイルオーバー前のリーダーインスタンスエンドポイント}'

# フェイルオーバー完了後
2023-07-05T00:33:39.373+09:00 TRACE 44369 --- [ool-11-thread-2] s.a.j.p.f.FailoverConnectionPlugin       : Changes:
        Host '${フェイルオーバー前のライターインスタンスエンドポイント}': [WENT_UP, NODE_CHANGED]
2023-07-05T00:33:39.374+09:00 DEBUG 44369 --- [ool-11-thread-2] .j.p.f.ClusterAwareReaderFailoverHandler : Connected to reader: '${フェイルオーバー前のライターインスタンスエンドポイント}'
2023-07-05T00:33:39.374+09:00 DEBUG 44369 --- [ool-11-thread-2] .j.p.f.ClusterAwareReaderFailoverHandler : New reader connection object: com.mysql.cj.jdbc.ConnectionImpl@5d8bcb8e
2023-07-05T00:33:39.374+09:00 TRACE 44369 --- [nio-8080-exec-8] s.a.j.p.r.ReadWriteSplittingPlugin       : Reader connection set to '${フェイルオーバー前のライターインスタンスエンドポイント}'
2023-07-05T00:33:39.384+09:00 TRACE 44369 --- [nio-8080-exec-8] s.a.j.p.f.FailoverConnectionPlugin       : Changes:
        Host '${フェイルオーバー前のリーダーインスタンスエンドポイント}': [PROMOTED_TO_WRITER, NODE_CHANGED]
        Host '${フェイルオーバー前のライターインスタンスエンドポイント}': [NODE_DELETED]
2023-07-05T00:33:39.384+09:00 DEBUG 44369 --- [nio-8080-exec-8] s.a.j.p.f.FailoverConnectionPlugin       : Connected to: HostSpec[host=${フェイルオーバー前のリーダーインスタンスエンドポイント}, port=-1, READER, AVAILABLE, weight=2450, 2023-07-04 15:32:44.108693]
2023-07-05T00:33:39.384+09:00 ERROR 44369 --- [nio-8080-exec-8] s.a.j.p.f.FailoverConnectionPlugin       : The active SQL connection has changed due to a connection failure. Please re-configure session state if required.
2023-07-05T00:33:39.387+09:00 DEBUG 44369 --- [nio-8080-exec-8] s.a.j.p.f.FailoverConnectionPlugin       : Detected an exception while executing a command: Can't call rollback when autocommit=true
2023-07-05T00:33:39.388+09:00 TRACE 44369 --- [nio-8080-exec-8] s.a.j.p.f.FailoverConnectionPlugin       : Changes:
        Host '${フェイルオーバー前のライターインスタンスエンドポイント}': [WENT_DOWN, NODE_CHANGED]
2023-07-05T00:33:39.388+09:00 DEBUG 44369 --- [nio-8080-exec-8] s.a.j.p.f.FailoverConnectionPlugin       : Starting reader failover procedure.
2023-07-05T00:33:39.389+09:00 DEBUG 44369 --- [ool-14-thread-1] .j.p.f.ClusterAwareReaderFailoverHandler : Trying to connect to reader: '${フェイルオーバー前のライターインスタンスエンドポイント}', with properties '{failoverMode=reader-or-writer, password=rootroot, database=test_db, wrapperPlugins=readWriteSplitting,failover,efm, user=root}'
2023-07-05T00:33:39.391+09:00 DEBUG 44369 --- [ool-12-thread-2] .j.p.f.ClusterAwareReaderFailoverHandler : Connected to reader: '${フェイルオーバー前のライターインスタンスエンドポイント}'
2023-07-05T00:33:39.391+09:00 DEBUG 44369 --- [ool-12-thread-2] .j.p.f.ClusterAwareReaderFailoverHandler : New reader connection object: com.mysql.cj.jdbc.ConnectionImpl@6db7986c
2023-07-05T00:33:39.392+09:00 TRACE 44369 --- [io-8080-exec-10] s.a.j.p.r.ReadWriteSplittingPlugin       : Reader connection set to '${フェイルオーバー前のライターインスタンスエンドポイント}'
2023-07-05T00:33:39.398+09:00 TRACE 44369 --- [io-8080-exec-10] s.a.j.p.f.FailoverConnectionPlugin       : Changes:
        Host '${フェイルオーバー前のリーダーインスタンスエンドポイント}': [PROMOTED_TO_WRITER, NODE_CHANGED]
        Host '${フェイルオーバー前のライターインスタンスエンドポイント}': [NODE_DELETED]
2023-07-05T00:33:39.398+09:00 DEBUG 44369 --- [io-8080-exec-10] s.a.j.p.f.FailoverConnectionPlugin       : Connected to: HostSpec[host=${フェイルオーバー前のリーダーインスタンスエンドポイント}, port=-1, READER, AVAILABLE, weight=2450, 2023-07-04 15:32:44.108693]
2023-07-05T00:33:39.398+09:00 ERROR 44369 --- [io-8080-exec-10] s.a.j.p.f.FailoverConnectionPlugin       : The active SQL connection has changed due to a connection failure. Please re-configure session state if required.
2023-07-05T00:33:39.399+09:00 DEBUG 44369 --- [io-8080-exec-10] s.a.j.p.r.ReadWriteSplittingPlugin       : Detected a failover exception while executing a command: 'Connection.setReadOnly'
2023-07-05T00:33:39.399+09:00 TRACE 44369 --- [io-8080-exec-10] s.a.j.p.r.ReadWriteSplittingPlugin       : Closing all internal connections except for the current one.
2023-07-05T00:33:39.444+09:00 TRACE 44369 --- [ool-14-thread-1] s.a.j.p.f.FailoverConnectionPlugin       : Changes:
        Host '${フェイルオーバー前のリーダーインスタンスエンドポイント}': [WENT_UP, NODE_CHANGED]
2023-07-05T00:33:39.445+09:00 DEBUG 44369 --- [ool-14-thread-1] .j.p.f.ClusterAwareReaderFailoverHandler : Connected to reader: '${フェイルオーバー前のリーダーインスタンスエンドポイント}'
2023-07-05T00:33:39.445+09:00 DEBUG 44369 --- [ool-14-thread-1] .j.p.f.ClusterAwareReaderFailoverHandler : New reader connection object: com.mysql.cj.jdbc.ConnectionImpl@4cadd5e1
2023-07-05T00:33:39.446+09:00 TRACE 44369 --- [nio-8080-exec-8] s.a.j.p.r.ReadWriteSplittingPlugin       : Writer connection set to '${フェイルオーバー前のリーダーインスタンスエンドポイント}'
2023-07-05T00:33:39.451+09:00 DEBUG 44369 --- [nio-8080-exec-8] s.a.j.p.f.FailoverConnectionPlugin       : Connected to: HostSpec[host=${フェイルオーバー前のリーダーインスタンスエンドポイント}, port=-1, WRITER, AVAILABLE, weight=25, 2023-07-04 15:33:39.510702]
2023-07-05T00:33:39.451+09:00 ERROR 44369 --- [nio-8080-exec-8] s.a.j.p.f.FailoverConnectionPlugin       : The active SQL connection has changed due to a connection failure. Please re-configure session state if required.
2023-07-05T00:33:39.452+09:00  WARN 44369 --- [nio-8080-exec-8] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 0, SQLState: 08S02
2023-07-05T00:33:39.452+09:00 ERROR 44369 --- [nio-8080-exec-8] o.h.engine.jdbc.spi.SqlExceptionHelper   : The active SQL connection has changed due to a connection failure. Please re-configure session state if required.

Liks

LocalStack 2.0でAWSクラウドリソースをローカル環境でシミュレート

先日、LocalStack 2.0がリリースされたので、Spring Bootアプリケーションで利用してみます。 LocalStack 1.0と2.0の違いをChatGPTに聞いてみました。

LocalStack 1.0とLocalStack 2.0は、AWSクラウドリソースをローカル環境でシミュレートするために使用されるオープンソースプロジェクトです。これにより、開発者はオフラインでAWSサービスをテストできます。LocalStack 2.0は、1.0に比べていくつかの重要な改善点があります。

パフォーマンスと安定性:LocalStack 2.0は、より高速で安定したパフォーマンスが期待されるように設計されています。1.0に存在するいくつかのバグや問題が修正されています。
サポートされるサービス:LocalStack 2.0では、新しいAWSサービスと機能のサポートが追加されています。これにより、開発者は最新のAWS機能をローカルで利用できるようになります。
プロジェクト構造とアーキテクチャ:LocalStack 2.0では、プロジェクトの構造とアーキテクチャが再設計され、より維持管理が容易になり、拡張性が向上しています。
プロセス分離:LocalStack 1.0では、すべてのサービスが1つのプロセス内で実行されていましたが、LocalStack 2.0では、各サービスが独自のプロセス内で実行されるようになりました。これにより、サービス間の依存関係や問題が軽減され、安定性が向上しています。
コンフィギュレーションとデプロイメント:LocalStack 2.0では、設定とデプロイメントのプロセスが改善され、より使いやすくなっています。
コミュニティサポート:LocalStack 2.0は、1.0と比べてアクティブな開発とサポートが続いており、開発者が新しい機能やバグ修正を利用できるようになっています。

これらの改善点により、LocalStack 2.0は1.0よりも高速で安定し、より多くのAWSサービスと機能をサポートし、開発者がAWSアプリケーションをローカル環境でテストする際に優れた選択肢となります。

LocalStackの料金体系は、無料と有料があります。有料版でしか利用できないAWSクラウドリソースもあります。詳しくは、AWS Service Feature Coverageに書いてあります。

LocalStackを利用したSpring Bootアプリケーション

本記事の環境
- Java 17
- Spring Boot 3.0.6
- AWS SDK for Java 2.20.58
- LocalStack 2.0.2
- Testcontainers 1.18.0

Docker上でLocalStackを起動する

Docker上でLocalStackを起動するために、次のdocker-compose.ymlを用意しました。

version: "3.9"

services:
  localstack:
    image: localstack/localstack:2.0.2
    ports:
      - "127.0.0.1:4566:4566"
    environment:
      # LocalStackのログを出力するかどうかを指定
      - DEBUG=1
      # Docker Composeで起動されるコンテナがDockerデーモンと通信するために使用するDockerホストのアドレスを指定
      - DOCKER_HOST=unix:///var/run/docker.sock

起動します。

$ docker-compose up -d
[+] Running 1/1
 ✔ Container localstack2-java-localstack-1  Started 

AWS環境のセットアップ

AWS CLIのコマンドで、LocalStack内の全サービスにアクセスすることができます。また、LocalStackに特化したLocalStack AWS CLIというものもあります。

CLIのインストール

AWS CLIかLocalStack AWS CLIのどちらかをインストールしてください。

# AWS CLI
$ pip install awscli

# LocalStack AWS CLI
$ pip install awscli-local

クレデンシャルの設定

クレデンシャルとデフォルトリージョンの環境変数を設定してください。

$ export AWS_ACCESS_KEY_ID="test"
$ export AWS_SECRET_ACCESS_KEY="test"
$ export AWS_DEFAULT_REGION="ap-northeast-1"

このクレデンシャルはダミーなので、direnvを利用して、このリポジトリディレクトリのみ環境変数が適用されるようにしました。

export AWS_ACCESS_KEY_ID="test"
export AWS_SECRET_ACCESS_KEY="test"
export AWS_DEFAULT_REGION="ap-northeast-1"

動作確認

AWS CLIかLocalStack AWS CLIのコマンドを叩いて動作確認をします。

$ aws --endpoint-url=http://localhost:4566 kinesis list-streams

    "StreamNames": []
}
$ awslocal kinesis list-streams

    "StreamNames": []
}

これでLocalStackを利用する環境は整いました。

Spring Boot アプリケーションの実装

サンプルとして、Amazon SQSを利用してキューにメッセージを格納するコマンドラインアプリケーションを実装します。 細かい実装は、GitHubをご覧ください。

SqsClientをBean登録します。今回、LocalStackを4566ポートで起動しているのでエンドポイントを書き換える必要があります。

@Configuration(proxyBeanMethods = false)
public class SqsConfig {

    @Bean
    public SqsClient sqsClient(SqsProperties properties) {
        var builder = SqsClient.builder()
                .region(Region.AP_NORTHEAST_1)
                .endpointOverride(properties.endpointUrl());
        if (properties.endpointUrl() != null) {
            // LocalStackのエンドポイントに書き換える
            builder.endpointOverride(properties.endpointUrl());
        }
        return builder.build();
    }
}

SQSのキューにメッセージを格納する処理を実装。

@Component
public class SqsQueueMessageSender implements ApplicationRunner {

    static final String DEFAULT_MESSAGE = "test-message";

    private final SqsClient sqsClient;
    private final SqsProperties sqsProperties;

    public SqsQueueMessageSender(SqsClient sqsClient, SqsProperties sqsProperties) {
        this.sqsClient = sqsClient;
        this.sqsProperties = sqsProperties;
    }

    @Override
    public void run(ApplicationArguments args) {
        var queueUrl = sqsClient.getQueueUrl(builder -> builder.queueName(sqsProperties.queueName()))
                .queueUrl();
        sqsClient.sendMessage(builder -> builder.queueUrl(queueUrl).messageBody(DEFAULT_MESSAGE));
    }
}

SQSキューの作成

AWS CLIかLocalStack AWS CLIのコマンドを叩いてキューを作成します。

$ aws --endpoint-url=http://localhost:4566 sqs create-queue --queue-name sample-queue
$ awslocal sqs create-queue --queue-name sample-queue
{
    "QueueUrl": "http://localhost:4566/000000000000/sample-queue"
}

アプリケーションの実行

Spring Bootアプリケーションを起動して、キューにメッセージを格納します。

./gradlew clean bootRun --args='--spring.profiles.active=local'                    

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.6)

2023-05-05T13:40:25.871+09:00  INFO 21202 --- [           main] c.b1a9idps.localstack2java.Application   : Starting Application using Java 17.0.6 with PID 21202 (/Users/ryosuke/workspace/localstack2-java/build/classes/java/main started by ryosuke in /Users/ryosuke/workspace/localstack2-java)
2023-05-05T13:40:25.873+09:00  INFO 21202 --- [           main] c.b1a9idps.localstack2java.Application   : The following 1 profile is active: "local"
2023-05-05T13:40:26.617+09:00  INFO 21202 --- [           main] c.b1a9idps.localstack2java.Application   : Started Application in 1.145 seconds (process running for 1.402)

BUILD SUCCESSFUL in 2s
5 actionable tasks: 5 executed

AWS CLIかLocalStack AWS CLIのコマンドを格納したメッセージを受信できることを確認します。

$ aws --endpoint-url=http://localhost:4566 sqs receive-message \
  --queue-url=http://queue.localhost.localstack.cloud:4566/000000000000/sample-queue
$ awslocal sqs receive-message \
  --queue-url=http://queue.localhost.localstack.cloud:4566/000000000000/sample-queue

無事にLocalStackを利用してAWSクラウドリソースをローカル環境でシミュレートすることができました。

Testcontainersを利用したテストコード

LocalStackをサポートしているTestcontainersを利用してテストコードを書きます。 SqsClientのクレデンシャル等をTestcontainers用のものを利用することで、テストコードを書くことができます。

@ExtendWith(SpringExtension.class)
@ActiveProfiles("unittest")
@ContextConfiguration(classes = {TestConfig.class}, initializers = ConfigDataApplicationContextInitializer.class)
@Testcontainers
class SqsQueueMessageSenderTest extends AbstractContainerBaseTest {

    @Container
    final LocalStackContainer localStack = new LocalStackContainer(LOCALSTACK_IMAGE_NAME)
            .withServices(LocalStackContainer.Service.SQS);

    SqsQueueMessageSender sqsQueueMessageSender;
    SqsClient sqsClient;
    @Autowired
    SqsProperties sqsProperties;

    @BeforeEach
    void beforeEach() {
        sqsClient = SqsClient.builder()
                .endpointOverride(localStack.getEndpointOverride(LocalStackContainer.Service.SQS))
                .credentialsProvider(
                        StaticCredentialsProvider.create(
                                AwsBasicCredentials.create(localStack.getAccessKey(), localStack.getSecretKey())
                        )
                )
                .region(Region.of(localStack.getRegion()))
                .build();
        sqsClient.createQueue(builder -> builder.queueName(sqsProperties.queueName()));

        sqsQueueMessageSender = new SqsQueueMessageSender(sqsClient, sqsProperties);
    }

    @Test
    void sendMessage() {
        sqsQueueMessageSender.run(null);

        var queueUrl = sqsClient.getQueueUrl(builder -> builder.queueName(sqsProperties.queueName()))
                .queueUrl();

        var actual = sqsClient.receiveMessage(builder -> builder
                        .queueUrl(queueUrl)
                        .maxNumberOfMessages(10));
        Assertions.assertThat(actual.messages())
                .hasSize(1)
                .extracting(Message::body)
                .containsExactly(DEFAULT_MESSAGE);
    }

}

Links

ECS Fargate上で動いているJavaアプリケーションにDatadogを導入する

ECS Fargate上で動いているJavaアプリケーションにDatadogを導入する手順をまとめます。

本記事の環境
- Java 11
- Gradle 7.0.2
- Spring Boot 2.5.1
- Jib 3.3.1

大まかな流れ

  1. Gradleプロジェクトでアプリケーションを作る
  2. ECS Fargate環境を用意する
  3. ECS FargateにデプロイできるようにGitHub Actionsワークフローを用意する
  4. ECS Faragte上で動いているJavaアプリケーションにDatadogを導入する

本記事では4についてまとめます。JibのGradleプラグインを利用してDockerイメージを生成しています。

手順

1. Javaクライアントのインストール

Javaクライアントをインストールして、任意の場所に配置(この記事では .datadog/)します。

$ wget -O dd-java-agent.jar 'https://dtdg.co/latest-java-tracer'
もしくは
$ curl -L -o dd-java-agent.jar https://dtdg.co/latest-java-tracer

Java アプリケーションのトレースclasspath に dd-java-agent を追加しないでください。予期せぬ挙動が生じる場合があります。」とあるので、classpathには追加しないようにしてください。

2. タスク定義にDatadog Agent用サイドカーコンテナを追加

タスク定義にDatadog Agent用サイドカーコンテナを追加します。

{
  # ECS Fargate全体の設定は省略
  "containerDefinitions": [
    {
      # アプリケーション用コンテナの設定
    },
    {
      "name": "datadog-agent",
      "image": "public.ecr.aws/datadog/agent:latest",
      "cpu": 128,
      "memory": 512,
      "memoryReservation": 512,
      "portMappings": [
        {
          "containerPort": 8126
        }
      ],
      "essential": true,
      "environment": [
        {
          "name": "ECS_FARGATE",
          "value": "true"
        },
        # Trace Agentを有効に
        {
          "name": "DD_APM_ENABLED",
          "value": "true"
        },
        # Datadog上での環境識別子
        {
          "name": "DD_ENV",
          "value": "dev"
        }
      ],
      "secrets": [
        # AWS Systems Manager Parameter Storeに格納したAPI Key
        {
          "name": "DD_API_KEY",
          "valueFrom": "/datadog/DD_API_KEY"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-create-group": "true",
          "awslogs-group": "app-datadog",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "/datadog"
        }
      }
    }
  ]
}

3. dd-java-agentをDockerイメージに含めるように設定を追加

build.gradleに、dd-java-agentをDockerイメージに含めるように設定を追加してください。

jib {
    from {
        image = 'amazoncorretto:11-alpine-jdk'
    }
    to {
        // 任意のコンテナイメージ名
        image = 'app'
    }
    container {
        // アプリケーション実行時にJVMに渡すJVMフラグ
        jvmFlags = ['-javaagent:/opt/datadog/dd-java-agent.jar']
    }
    extraDirectories {
        paths {
            path {
                // dd-java-agent.jarをプロジェクトディレクトリからDockerイメージに追加
                from = file("${project.rootDir}/.datadog")
                into = '/opt/datadog'
            }
        }
    }
}

4. デプロイ

ECS Fargeteにデプロイし、Datadogダッシュボードで確認するとメトリクスが取れるようになります。

RestTemplateのエラーハンドリング

Spring FrameworkRestTemplate.class を使った通信のエラーハンドリングの仕方についてまとめます。

今回の環境

  • Java 17
  • Spring Boot 2.7.4

試してみた

Client APIからリクエストを受けるAPI(Server)

@RestController
@RequestMapping("/server")
public class ServerController {

    @GetMapping
    public ServerResponse index(@RequestParam(value = "status_code", required = false) Optional<Integer> statusCode) {
        HttpStatus status = statusCode
                .map(HttpStatus::resolve)
                .orElse(null);
        if (status != null && status.isError()) {
            throw new CustomException(status.getReasonPhrase(), status);
        } else {
            return new ServerResponse(1, "name");
        }
    }

    @ExceptionHandler(CustomException.class)
    public ResponseEntity<ErrorResponse> handle(CustomException e) {
        return ResponseEntity
                .status(e.getStatus())
                .body(new ErrorResponse(e.getMessage()));
    }
}
public record ServerResponse(Integer id, String name) {
}
public record ErrorResponse (String message) {
}

リクエストパラメータで指定したステータスコードが400系、500系だったらエラーレスポンスを、それ以外は正常系のレスポンスを返すAPIです。

Server APIにリクエストを投げるAPI(Client)

@RestController
@RequestMapping("/client")
public class ClientController {

    private final ServerService serverService;

    public ClientController(ServerService serverService) {
        this.serverService = serverService;
    }

    @GetMapping("/default")
    public ResponseEntity<ClientResponse> defaultHandler(@RequestParam(value = "status_code", required = false) Optional<Integer> statusCode) throws ServerRestTemplateException {
        ResponseEntity<ServerResponse> serverResponse = serverService.defaultHandlerGet(statusCode);
        return ResponseEntity.status(serverResponse.getStatusCode())
                .body(ClientResponse.newInstance(serverResponse.getBody()));
    }

    @GetMapping("/custom")
    public ResponseEntity<ClientResponse> customHandler(@RequestParam(value = "status_code", required = false) Optional<Integer> statusCode) throws ServerRestTemplateException {
        ResponseEntity<ServerResponse> serverResponse = serverService.customHandlerGet(statusCode);

        if (serverResponse.getStatusCode().isError()) {
            // 4xx, 5xxの場合、ResponseEntityのレスポンスボディは、new ServerResponse(null, null)となる
            throw new ServerRestTemplateException(new ErrorResponse("custom handler error"), serverResponse.getStatusCode());
        }

        return ResponseEntity.status(serverResponse.getStatusCode())
                .body(ClientResponse.newInstance(serverResponse.getBody()));
    }

    @ExceptionHandler(ServerRestTemplateException.class)
    public ResponseEntity<ErrorResponse> handleServerRestTemplateException(ServerRestTemplateException e) {
        return ResponseEntity.status(e.getStatus())
                .body(e.getResponse());
    }

}
@Service
public class ServerService {
    private static final String BASE_URI = "http://localhost:8081/api";
    private static final String BASE_PATH = "/server";

    private final RestTemplate defaultHandlerRestTemplate;
    private final RestTemplate customHandlerRestTemplate;
    private final ObjectMapper objectMapper;

    public ServerService(
            RestTemplateBuilder defaultHandlerRestTemplateBuilder,
            RestTemplateBuilder customHandlerRestTemplateBuilder,
            ObjectMapper objectMapper) {
        this.defaultHandlerRestTemplate = defaultHandlerRestTemplateBuilder.rootUri(BASE_URI).build();
        this.customHandlerRestTemplate = customHandlerRestTemplateBuilder.rootUri(BASE_URI).build();
        this.objectMapper = objectMapper;
    }

    /**
     * RestTemplateを使った通信のエラーハンドリングに {@link org.springframework.web.client.DefaultResponseErrorHandler} を利用
     */
    public ResponseEntity<ServerResponse> defaultHandlerGet(Optional<Integer> statusCode) throws ServerRestTemplateException {
        var requestEntity = buildRequestEntity(statusCode);

        try {
            return defaultHandlerRestTemplate.exchange(requestEntity, ServerResponse.class);
        } catch (HttpStatusCodeException e) {
            try {
                var errorResponse = objectMapper.readValue(e.getResponseBodyAsString(), ErrorResponse.class);
                throw new ServerRestTemplateException(errorResponse, e.getStatusCode());
            } catch (IOException ioException) {
                throw new ServerRestTemplateException("invalid response");
            }
        } catch (RestClientException e) {
            throw new ServerRestTemplateException("error");
        }
    }

    /**
     * RestTemplateを使った通信のエラーハンドリングに {@link com.b1a9idps.client.externals.handler.RestTemplateResponseErrorHandler} を利用
     */
    public ResponseEntity<ServerResponse> customHandlerGet(Optional<Integer> statusCode) {
        var requestEntity = buildRequestEntity(statusCode);

        return customHandlerRestTemplate.exchange(requestEntity, ServerResponse.class);
    }

    private RequestEntity<Void> buildRequestEntity(Optional<Integer> statusCode) {
        var builder = UriComponentsBuilder.fromPath(BASE_PATH);
        if (statusCode.isPresent()) {
            builder.queryParam("status_code", statusCode.get());
        }
        var uri = builder.toUriString();

        return RequestEntity.get(uri).build();
    }
}

実行

Server API、Client APIともに起動し、Server APIにリクエストを投げてみます。

GET /api/client/defaultDefaultResponseErrorHandlerでエラーハンドリングするエンドポイントで、 GET /api/client/customResponseErrorHandlerを実装したクラスでエラーハンドリングするエンドポイントです。

% curl 'http://localhost:8080/api/client/default' | jq .
{
  "id": 1,
  "name": "name"
}

% curl 'http://localhost:8080/api/client/custom' | jq .
{
  "id": 1,
  "name": "name"
}

% curl 'http://localhost:8080/api/client/default?status_code=403' | jq .
{
  "message": "Forbidden"
}

% curl 'http://localhost:8080/api/client/custom?status_code=403' | jq .
{
  "message": "custom handler error"
}

解説

それぞれのエラーハンドリングについて解説します。

DefaultResponseErrorHandlerでエラーハンドリング

デフォルトだと、RestTemplateで通信して起きたエラー(ステータスコード400系か500系)は、DefaultResponseErrorHandlerでハンドリングされます。

該当コードを見てみるとこのように実装されており、通信してステータスコード400系と500系が返ってくるとHttpClientErrorExceptionを継承した例外クラスが投げられます。

@Override
public void handleError(ClientHttpResponse response) throws IOException {
    HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
    if (statusCode == null) {
        byte[] body = getResponseBody(response);
        String message = getErrorMessage(response.getRawStatusCode(),
                response.getStatusText(), body, getCharset(response));
        throw new UnknownHttpStatusCodeException(message,
                response.getRawStatusCode(), response.getStatusText(),
                response.getHeaders(), body, getCharset(response));
    }
    handleError(response, statusCode);
}

protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
    String statusText = response.getStatusText();
    HttpHeaders headers = response.getHeaders();
    byte[] body = getResponseBody(response);
    Charset charset = getCharset(response);
    String message = getErrorMessage(statusCode.value(), statusText, body, charset);

    switch (statusCode.series()) {
        case CLIENT_ERROR:
            throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
        case SERVER_ERROR:
            throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
     default:
            throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
    }
}

なので、HttpStatusCodeExceptionをcatchして、エラー時のレスポンスをHttpStatusCodeException#getResponseBodyAsStringで取得しています。

public ResponseEntity<ServerResponse> defaultHandlerGet(Optional<Integer> statusCode) throws ServerRestTemplateException {
    var requestEntity = buildRequestEntity(statusCode);

    try {
        return defaultHandlerRestTemplate.exchange(requestEntity, ServerResponse.class);
    } catch (HttpStatusCodeException e) {
        try {
            var errorResponse = objectMapper.readValue(e.getResponseBodyAsString(), ErrorResponse.class);
            throw new ServerRestTemplateException(errorResponse, e.getStatusCode());
        } catch (IOException ioException) {
            throw new ServerRestTemplateException("invalid response");
        }
    } catch (RestClientException e) {
        throw new ServerRestTemplateException("error");
    }
}

ResponseErrorHandlerを実装したクラスでエラーハンドリング

RestTemplateで通信してエラーが発生した際に例外を投げてほしくないという場合には、ResponseErrorHandlerを実装したクラスを用意します。 この例では、ステータスコード400系と500系が返ってきても何もしないように実装しています。

public class RestTemplateResponseErrorHandler extends DefaultResponseErrorHandler {
    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
    }
}

そして、RestTemplateでこのErrorHandlerを利用するようにします。

@Configuration(proxyBeanMethods = false)
public class RestTemplateConfig {
    @Bean
    public RestTemplateBuilder defaultHandlerRestTemplateBuilder() {
        return new RestTemplateBuilder();
    }

    @Bean
    public RestTemplateBuilder customHandlerRestTemplateBuilder() {
        return new RestTemplateBuilder()
                .errorHandler(new RestTemplateResponseErrorHandler());
    }
}

先に説明したように、ステータスコード400系や500系が返ってきても何もしないので、 new ServerResponse(null, null) がレスポンスボディとして返されます。

public ResponseEntity<ServerResponse> customHandlerGet(Optional<Integer> statusCode) {
    var requestEntity = buildRequestEntity(statusCode);

    return customHandlerRestTemplate.exchange(requestEntity, ServerResponse.class);
}

レスポンスボディからはエラーかどうかを判断できないので、ステータスコードでエラーかどうかを判断しています。

GetMapping("/custom")
public ResponseEntity<ClientResponse> customHandler(@RequestParam(value = "status_code", required = false) Optional<Integer> statusCode) throws ServerRestTemplateException {
    ResponseEntity<ServerResponse> serverResponse = serverService.customHandlerGet(statusCode);

    if (serverResponse.getStatusCode().isError()) {
        // 4xx, 5xxの場合、ResponseEntityのレスポンスボディは、new ServerResponse(null, null)となる
        throw new ServerRestTemplateException(new ErrorResponse("custom handler error"), serverResponse.getStatusCode());
    }

    return ResponseEntity.status(serverResponse.getStatusCode())
            .body(ClientResponse.newInstance(serverResponse.getBody()));
}

@ExceptionHandler(ServerRestTemplateException.class)
public ResponseEntity<ErrorResponse> handleServerRestTemplateException(ServerRestTemplateException e) {
    return ResponseEntity.status(e.getStatus())
            .body(e.getResponse());
}

まとめ

ResponseErrorHandlerを実装したクラスでエラーハンドリングを行うと、例外処理を書かなくて良くなるなる反面、呼び出したAPIからのエラーレスポンスが失われるかなと思います。ステータスコードさえわかって呼び出す側のシステムでエラーレスポンスを完全に決める場合なら有効かなと思います。

DefaultResponseErrorHandlerでエラーハンドリングする場合は、例外処理を書くのが億劫ですが呼び出したAPIからのエラーレスポンスを返すことができます。(あまり戻ってきたエラーレスポンスをそのまま返すというケースはないかもしれないですが)

Links

Spring Bootアプリケーションが起動するまでにやっていること

本記事は、Spring Boot 2.7.3について書いてあります。

Spring Bootとは

Spring Bootはフレームワークでなく、Spring Frameworkを利用してアプリケーションを作りやすくしている仕組みのこと。 Spring Boot単体では何も作ることはできないので、Spring WebやSpring Dataなどのフレームワークを利用する。(Spring BootではStarterを利用することがほとんど)

Spring Bootと非Spring Bootの大きな違いは、Starterという依存関係便利パックが提供されている&デフォルトの設定(必要そうなクラスのBean登録)がされていること。

Starters

アプリケーションを作るときにあったら必要であろう依存関係を含んだ便利パック。例えば、SpringとJPAを利用してアプリケーションを作りたいとなったら、spring-boot-starter-data-jpaを依存関係に追加すればよい。 Starterを利用すると後述するauto-configurationの仕組みが提供されており、単にライブラリを依存関係に追加するよりも設定項目が減り、楽にアプリケーション開発が行える。 ※ 公式のスターターはspring-boot-starter-*という名前になっており多くのIDEでそれに適した設定が入っているため、サードパーティのスターターは頭にspring-bootという名前をつけてはいけない。

Starterの例

  • Spring Boot application starters
    • Spring Bootでアプリケーションを作るために利用
    • spring-boot-starter-data-jpaspring-boot-starter-webなど
  • Spring Boot production starters
    • production readyな機能を提供する
    • spring-boot-starter-actuator
  • Spring Boot technical starters
    • デフォルトで利用されているライブラリ等を置き換えたり除外したりするときに利用
    • spring-boot-starter-loggingspring-boot-starter-tomcatなど

コーディング

Spring Bootアプリケーションにおけるエントリーポイントは@SpringBootApplicationで、これをメインクラスに付与する。 なお、@SpringBootApplicationはパッケージ最上位に置くのがよい。

一般的なメインクラス。

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

設定クラス

Spring Bootの設定にはJavaベースの設定を推奨している。XMLを利用することもできる。

Auto-configuration

Spring Boot auto-configurationは、プロジェクトに追加されている依存関係に基づいて、Spring アプリケーションに自動的に設定を行う仕組みのことである。 @EnableAutoConfigurationまたは@SpringBootApplicationを付与すると、auto-configurationを有効にすることができる。

デフォルトで全AutoConfigurationが適用されるが、以下のように書くとauto-configurationから除外することができる。

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class Application {
}

@SpringBootApplicationについて

@SpringBootApplicationは、@SpringBootConfiguration@EnableAutoConfiguration@ComponentScanを含み持つアノーテーションでこれらと同じ機能を提供する。ちなみに、複数の機能を持つアノテーションのことを複合アノテーションと呼ぶ。

  • @EnableAutoConfiguration
    • 先に説明したauto-configurationを有効にする
  • @ComponentScan
  • @SpringBootConfiguration
    • このクラス自身をBean登録する。
    • @Configurationとほぼ同じ機能ではあるが、インテグレーションテストを書く際に役立つ。

Auto configuration

AutoConfigurationのクラスに@AutoConfigurationを付与するとBean登録される。 auto-configurationを適用させるクラスはMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 2.7より前はMETA-INF/spring.factoriesだった)に列挙されている。

AutoConfigurationImportSelector#getCandidateConfigurationsを参照。

Condition Annotations

auto-configurationクラスに書いてある@Conditonalに従ってBean登録が行われる。 @Conditionalの仲間には次のものがある。

Class Condition

クラスパスに指定したクラスがあるかないか。@ConditionalOnClass@ConditionalOnMissingClassJacksonAutoConfiguration.javaだと、ObjectMapper.javaがクラスパスに存在したらauto-configurationが適用される。

@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
public class JacksonAutoConfiguration {

Bean Conditions

指定したBeanがあるかないか。@ConditionalOnBean@ConditionalOnMissingBean

RestTemplateAutoConfiguration#restTemplateBuilderConfigurerだと、RestTemplateBuilderConfigurerがBean登録されてなかったらこのメソッドでRestTemplateBuilderConfigurerのBean登録が行われる。

@Bean
@Lazy
@ConditionalOnMissingBean
public RestTemplateBuilderConfigurer restTemplateBuilderConfigurer(

Property Conditions

application.ymlや起動オプションなどで指定するSpring 環境プロパティによってBean登録が行われる。

JacksonHttpMessageConvertersConfiguration.MappingJackson2HttpMessageConverterConfiguration.javaだと、spring.mvc.converters.preferred-json-mapper=jacksonのときBean登録が行われる

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnBean(ObjectMapper.class)
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
        havingValue = "jackson", matchIfMissing = true)
static class MappingJackson2HttpMessageConverterConfiguration {

Resource Conditions

指定したリソースが存在したらBean登録する。@ConditionalOnResource

このようにresourcesにパスを指定する。(ProjectInfoAutoConfiguration#buildProperties

@ConditionalOnResource(resources = "${spring.info.build.location:classpath:META-INF/build-info.properties}")

Web Application Conditions

Webアプリケーションかどうか。@ConditonalOnWebApplication@ConditionalOnNotWebAplication。 その他、クラウドプラットフォームかどうかで判定する@ConditionalOnCloudPlatformなどもある。CloudPlatform.javaに定義されている、CloudFoundry、Heroku、SAP、Kubernetes、Azure App Serviceが対象。

SpEL Expression Conditions

@ConditionalOnExpressionはSpEL(SPring Expression Language)で書かれたプロパティに応じてBean登録する。

Spring Boot が起動するまでにやっていること

@EnableAutoConfiguraion(@SpringBootApplication)が行っていることから、Spring Bootの特徴であるAutoConfigureの仕組みを説明する。

1. DIコンテナ(ApplicationContext)の作成

ApplicationContextは、BeanFactoryを継承したアプリケーションの設定を提供する主要なインターフェース。メイン機能の1つにDIコンテナがある。

2. spring.factories、AutoConfiguration.imports読込

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsにauto-configureするクラス(@AutoConfigureをつけたクラス)を列挙する。 以前はspring.factoriesに書いていたが、このファイルはauto-configureを利用するために必要なクラスを書くようになった(ぽい)

これらに書かれたクラスを読み込む。

3. Bean登録するAutoConfigurationクラスをソート

定義された順序で、Bean登録するAutoConfigurationクラスをソートする。AutoConfigurationSorter#getInPriorityOrderで行う。

  1. アルファベット順
  2. @Orderの順
  3. @AutoConfigureBefore@AutoConfigureAfterの順
    1. @AutoConfigureBeforeは指定したクラスの前
    2. @AutoConfigureAfterは指定したクラスの後

4. Bean登録

ConfigurationClassParser#processGroupImportsでDIコンテナにBean登録を行う

application.properties(yml)について

application.propterties(yml)は、設定値を書くファイル。 Spring Bootが提供している設定値の一覧はここに書いてある。

このファイルに書いても確実に設定されるわけではなく、@ConfigurationPropertiesが付与されたクラスに値がバインドされるだけである。

設定が行われる条件は、次の2つが満たされていることです。

  • バインドされる@ConfigurationPropertiesのクラスがBean登録されていること
  • そのAutoConfigurationが有効となっていこと

大抵、@AutoConfigurationを付与したクラスに@EnableConfigurationPropertiesも付与されている。つまり、AutoConfigurationが有効になっていればapplication.properties(yml)に書いた設定は適用される。

これはDataSourceAutoConfiguration.javaの例である。

@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import(DataSourcePoolMetadataProvidersConfiguration.class)
public class DataSourceAutoConfiguration {

Banner

Bannerとは、Spring Bootを起動すると見るこれのことで地味に変更がされ続けている。

歴史

バージョン 内容
1.0.x Bannerを表示するかどうかの設定のみ
1.1.x Text Bannerを表示できるようになる
1.2.x Banner変数が登場
1.3.x Text Bannerが色付け可能に
1.4.x Image Bannerを表示可能に
2.0.x アニメーションgifをサポート
2.2.x ASCII バナーは256色利用可能に

カスタマイズ

バナーのカスタマイズ方法は3通りで、デフォルトバナーは3で実現しているおり、SpringBootBanner.javaが実装。

  1. resources/banner.[ gif | jpg | png ]を置く
  2. resources/banner.txtを置く
  3. org.springframework.boot.Bannerインターフェースを実装する

優先順位

SpringApplicationBannerPrinter#getBannerで表示すバナーを決定している。

  1. Image BannerとText Bannerを探す
  2. 1がなかったら自作のBannerクラス
  3. 1も2もなかったらDefault Banner

Image Banner と Text Bannerは一緒に表示可能。

Bannerインターフェース実装してみた

public class StoresBanner implements Banner {

    private static final String[] BANNER = {
            "  ______    _________     ___     _______      ________    ______   \\n" +
            ".' ____ \\\\  |  _   _  |  .'   `.  |_   __ \\\\    |_   __  | .' ____ \\\\  \\n" +
            "| (___ \\\\_| |_/ | | \\\\_| /  .-.  \\\\   | |__) |     | |_ \\\\_| | (___ \\\\_| \\n" +
            " _.____`.      | |     | |   | |   |  __ /      |  _| _   _.____`.  \\n" +
            "| \\\\____) |    _| |_    \\\\  `-'  /  _| |  \\\\ \\\\_   _| |__/ | | \\\\____) | \\n" +
            " \\\\______.'   |_____|    `.___.'  |____| |___| |________|  \\\\______.' \\n"
    };

    @Override
    public void printBanner(Environment environment, Class<?> sourceClass, PrintStream printStream) {
        for (String line : BANNER) {
            printStream.println(line);
        }
    }
}
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application11.class);
        app.setBanner(new StoresBanner());
        app.run(args);
    }

}

※ バナーが好きだと思われる人が作ったOnline Spring Boot Banner Generatorというサイトがある。

MySQL徹底入門 第4版を読んで

運用に必要な知識

MySQLのプロセスアーキテクチャ

  • MySQLはシングルプロセス・マルチスレッドモデルのデータベース。
    • psコマンドのauxや-elfなどのオプションではmysqldのプロセスは1つしか見えない。
    • 内部的には複数のスレッドを使い分けている
  • スレッドは、フォアグランドスレッドとバックグランドスレッドに分けられる
    • フォアグランドスレッドは外部のクライアントと1対1で紐づく
      • 1つの接続が1つのスレッドになり、接続が切断されるまでの同一のスレッドが処理をする
    • 現在のコネクション(= スレッド)の情報はperformance_schemaのthreadsテーブルまたはSHOW PROCESSLISTで確認できる
      • 実行時のオーバーヘッドや情報の粒度などからperformance_schema.threadsテーブルの方が推奨されている

MySQLのパラメータ

  • MySQLにはたくさんのパラメータ(設定可能項目)があり、分類ごとにいくつかの特性を持っている。
    • オンラインで変更が可能なパラメータと設定変更にMySQLの再起動が必要なパラメータがある。
  • my.cnfの読み込み順序
    • /etc/my.cnf
    • /etc/mysql/my.cnf
    • SYSCONFDIR/my.cnf
    • $MYSQL_HOME/my.cnf
    • -default-extra-file
    • $HOME/my.cnf
    • $HOME/.mylogin.cnf
    • DATADIR/mysqld-auto.cnf
    • コマンドラインオプションで与えられた引数
  • オンラインで変更したパラメータを永続化するにはSET PERSISTステートメントが使える
    • SET PERSIST/SET PERSIST_ONLYで変更されたパラメータはDATADIR/mysqld-auto.cnfにJSONの形式で記録される
      • このファイルは管理者が直接編集することを想定していない。
      • mysqld-auto.cnfに記録されたパラメータの設定を削除するには、RESET PERSISTステートメントを利用する
      • 各パラメータがどこで設定されたかはperformance_schema.variables_infoテーブルで確認できる

MySQLのバージョン情報

MySQLのバージョンは、SELECT VERSION(); や SHOW VARIABLES LIKE 'version'; などで調べることができる。

運用設計

バージョン選定

MySQL 8.0系列は同一リリース系列内でも非互換性があるため、バージョンの選定は慎重にされなくてはならない。 クライアントやライブラリのバージョンはMySQLサーバと同じかそれ以上であることが必須。MySQL 8.0系列ではバージョン番号の信仰に伴って新機能が追加されてしまうため、 その機能やその周辺に新規にバグが内在する可能性がある。

現時点で確実に進められることは次の通り。

  • 導入時点での最新バージョンを選び動作確認をする
  • 導入以後のアップグレードに備えてSQLレベルでの非互換性を検出できるように回帰テストを作成する
  • アップグレードをする際にはクライアントのバージョンを先にあげる

バックアップ設計

  • バックアップの間隔
    • バックアップにはフルバックアップと増分バックアップがあり、両方揃って初めて任意の時点のデータを復元できる
    • フルバックアップの頻度
    • 増分バックアップの頻度
      • レプリケーション構成を組んでいる場合、間違いなくマスターではバイナリログが出力されているが、これをマスターのストレージ以外の場所に保管しておくことが必要。
        • マスターだけにバイナリログが保管されている(増分バックアップを取得しない)場合、MySQLが稼働しているマシンが完全に壊れ、ストレージからMySQLのデータはおろかバイナリログすら取り出せなくなってしまうと最後のフルバックアップの時点までしか復元できない
    • リストアにかかる時間の見積もり
      • フルバックアップの頻度を決める上で重要なのが「フルバックアップをリストアするのにかかる時間」と「一定期間のバイナリログを適用するのにかかる時間」。これらをできるだけ正しく見積もることは大切。
      • フルバックアップのリストア時間はデータの総容量に依存し、バイナリログの適用はバイナリログの量(= データが更新される頻度)に依存する
    • バックアップの運用計画に関するパラメータ
      • 増分バックアップの計画に大切なパラメータとしてbinlog_expire_logs_secondsという値がある。
        • これは、現在のバイナリログがスイッチした時点で過去の古いバイナリログを削除するためのパラメータ。
      • バイナリログがスイッチするタイミングは、max_binlog_sizeというパラメータで決定される。
        • 追記されていったバイナリログのサイズがこのパラメータで設定されて値よりも大きくなると、バイナリログのスイッチが発生する。
      • log_slave_updates
        • デフォルトはONで、スレーブはマスターからレプリケーションで受け取った更新差分を自身(= スレーブ)のバイナリログに出力する。これによりマスターとスレーブで二重にバイナリログが保管され、バイナリログの喪失への耐性が上がる。

モニタリング計画

MySQLが機能を提供しているかどうかを確認する「死活監視」と、中長期的にリソースの利用状況がどうなっていくかを確認する「リソース監視」の2つが必要。

  • 死活監視
    • MySQLの機能を提供するプロセスはmysqld。これが起動していない場合、MySQLはその機能を提供できない
    • mysqld_safeというプロセスが起動している場合もあるが、これはmysqldプロセスの起動を補助するラッパースクリプトなのでmysqldではない
    • MySQLにバンドルされているmysqladminコマンドにはpingサブコマンドがある
      • MySQLプロトコルでmysqldに対してアクセスし、応答があれば成功、なければ失敗」と判定する
    • MySQLとしては異常はないが、MySQLに原因がある」ような状況を発見するには、アプリケーションログをしっかり監視するのが有効
    • 死活監視の内容
      • MySQLプロトコルでの接続が可能か
      • SHW PROCESSLIST上で長時間実行され続けているクエリがないか
      • 現在の接続数とmax_connectionsを比較して、100%に近づいていないか
      • スレーブであれば、SHOW SLAVE STATUSのSeconds_Behind_Master(= 更新がマスターに実行されてからスレーブの反映までかかっている時間)が大きくなっていないか
      • 各テーブルのAUTO_INCREMENT属性カラムが、データ型の最大値近くまで払い出されていないか
      • Uptimeが小さすぎないか
  • リソース監視
    • 直接的なエラー以外の、レスポンス速度低下の原因究明や性能限界の計測に役立つ
    • 「問題発生以前」、「問題発生中」、「問題終息後」の3時点のリソースを比較するためにも継続的なデータの蓄積は必須
  • MySQLの各種ログ
    • エラーログ
      • MySQLサーバはプロセスに発生した異常をエラーログに出力する
    • スロークエリログ
      • mysqldがSQLを処理するのにかかった時間を記録するログ
      • クエリの処理にかかった時間はMySQLサーバを利用する環境にとって非常に重要
      • できる限り有効にしておくのがよい
      • my.cnfにslow_query_logオプションを記載する or サーバ変数のslow_query_logをONに設定
    • ジェネラルログ
      • 一般クエリログとも呼ばれ、mysqldが受け取ったすべてのクエリを記録するためのログ
      • アプリケーションのデバッグ用途に用いるためのログであり、恒常的にONにしておくようなログではない
    • ログファイルのローテーション
      • MySQLサーバはそれ単体ではログローテーションの仕組みを持たない
    • その他のログと名のつくもの
      • MySQLサーバが内部動作を記録するためのログであり、テキストとして開くことはないが、削除しないようにしないといけない
        • InnoDB ログ
        • バイナリログ
        • リレーログ

実際の運用作業

パラメータチューニング

すこしくらいパラメータを変更したところでパフォーマンスが劇的によくなることはない。事前に綿密な計算のもと、パラメータを算出するよりも運用中に効果を測定しながら継続的にパラメータの最適値に近づけていくことが大切。

  • 事前に計算しておくべきパラメータ
    • innodb_buffer_pool_sizeは重要なオプション
      • InnoDBのすべてのテーブルで利用されるバッファプール(メモリ上に配置)のサイズを指定する
      • SELECTのときにキャッシュとして使われるのみならず、InnoDBのすべての操作を一度バッファプールを経由して処理されるため、あらゆるDMLDDLの速度に影響を及ぼす。
      • 物理メモリが小さい(例えば4GB以下)ときは50%程度、十分大きい(例えば12GB以上)ときは80%などと単純な割合ではなく、InnoDBバッファプール以外にどれくらいメモリを残しておくべきかから算出するのがよい
      • innodb_buffer_pool_sizeはオンラインで変更できる(MySQL 5.6とそれ以前は再起動が必要だった)

クエリチューニング

MySQLのパフォーマンス問題の多くはクエリ単位に分解して考えることがベター。単一のクエリが遅い場合はもちろん、複数のクエリがロックやリソースの競合を起こしている場合でも、個々のクエリのスピードをあげてやることで全体としてのパフォーマンを改善できる パラメータのチューニングがせいぜい数倍程度にしかパフォーマンスの問題を改善できないのに対し、正しくチューニングされたクエリは数百倍から数千倍のパフォーマンスが出る。

  • インデックスの仕組み
    • 最もコストパフォーマンスが高いのは「インデックスを使わせること」
    • INSERT/UPDATE/DELETEといった更新処理はインデックスも同時にメンテナンスしなければならないため、更新の負荷はインデックスがない場合よりも大きくなる
    • 特定のクエリに対してベストなインデックスであっても、クエリの形が変わると同じインデックスがベストとは限らない
    • 複数カラムからなるインデックスはカラムの順番で効率が変わることがある、インデックスの大部分を操作しなければならないアクセスパターンは効率がよくない
  • インデックスによるWHERE句のチューニング
    • カードをめくって目視が1番コストの高い動作になるため、可能な限りこれを避けるようにインデックスを追加していく
    • どのくらいカードをめくって目視が発生する見積もりであるかは、実行したいクエリの先頭にEXPLAINキーワードをつけることで確認できる
      • rowsがカードをめくって目視する見積もりを表しており、filteredがカードをめくってチェックしたうち、何%程度が実際にマッチする行である見積もりを表している
      • ExtraのUsing whereがインデックスだけではWHERE句を解決しきれないので目視のフィルタリングが必要であることを表している
    • 不要なインデックスを作ってしまうことを重要視するよりも、不要になったインデックスを安全に検出して削除することを継続指定実践していいくのがよい
  • WHERE句のチューニングによる改善すべきクエリの特定
    • WHERE句にインデックスが使えずに非効率になっているクエリの特定は容易
      • 特定にはスロークエリログまたはperformance_schemaが使える
        • 実行時にフィルタがかけられる前者の方が便利
        • 後者はWHERE句などの定数をノーマライズしたダイジェストと呼ばれる単位で蓄積していくため、「リソースに余裕がある何の問題もないが、トラフィックがバーストすると問題として顕在化する」ようなクエリを探すのに便利
        • スロークエリログの Rows_examined / Rows_sent の値が1 に近ければ近いほどWHERE句の効率が良く、大きければ大きいほど非効率
  • インデックスによる ORDER BY ... LIMIT のチューニング
    • MySQLのインデックスはWHERE句による絞り込み以外に、ORDER BY句に対する効率化にも利用可能
  • ORDER BY ... LIMIT のための「優先度付きキュー」によるソート
    • 「優先度付きキュー」はMySQL 5.6とそれ以降で追加された比較的新しいORDER BY がLIMIT句とともに使われている場合の最適化方式
    • ソートバッファを優先度付きキューとして扱うことでORDER BY ... LIMITを省メモリで効率的に処理する最適化が働く。
      • この最適が行われたとしても「ソートに必要なカラムの値をすべて読み取らなければならない」という点は変わらない
      • この最適化はオプティマイザが自動で選択するので、「チューニング」ではなく「そういうもの」と知っていればよい
  • ORDER BY ... LIMIT のチューニングによる改善すべきクエリの特定
    • ORDER BY句をインデックスで解決できているか、いなかを判断するためにもスロークエリログまたはperformance_schemaを使う
  • クエリの書き換えによるチューニング
    • アプリケーション側の発行するクエリを変えることによって高速化する
    • ループ構造による繰り返しクエリ
      • N+1問題
        • Nの数が十分に小さいあいだはアプリケーション側でも遅いと感じられることがなく、データの増加にしたがってNが大きくなったときに初めて顕在化する
        • JOINを使ってSQLレベルで処理できる。アプリケーションとMySQLの通信は1回だけになり、ネットワーク負荷とCPU負荷を下げられる
    • 大量データフェッチへの対処
      • 大量データのフェッチによってI/Oを枯渇させる
      • アプリケーション側でフェッチする範囲を狭めてループで複数回実行することが負荷の軽減につながる
    • LIMIT句とOFFSET句
      • ORDER BY句とLIMIT句を併用することで、ソートに優先度付きキューを使った最適化やORDER BY狙いのキーを使った追加ソートを不要とする方法がある。
      • OFFSET句がカーソル操作をする対象は生成済みの結果セットに対してのみ
      • 巨大な1つのクエリを分割するときにLIMIT句とOFFSET句を併用した場合、個々のクエリで削減できるのはネットワーク転送量だけ
      • LIMIT句とORDER BY句の組み合わせは、パフォーマンス上の問題を引き起こしやすく、ORDER BY句の考慮漏れなどで正しい結果セットを得られなくなる可能性もあるため、注意が必要
    • ALTER TABLE実行時のTIPS
      • MySQLは以前のバージョンからオンラインALTER TABLEと呼ばれる機能を備えている
        • オンラインの意味はALTER TABLE中に読み書きが可能という意味
      • オンラインALTER TABLEがサポートされているかどうかを確認する
        • LOCK句はALTER TABLE中のDMLに対してロックを指定するための構文で
        • LOCK=NONEがALTER TABLE中のDMLを制限しない、すなわちオンラインALTER TABLEを表す
        • LOCK=SHAREDは、ALTER TABLE中には読み取りDMLのみ許可する(= 読み取り専用の)ロックを置くということで、、旧来と同じ読み取り専用のALTER TABLEになる
      • ALTER TABLEとレプリケーション遅延
        • マスターで1時間かかったクエリはスレーブでも1時間かかること、マルチスレッドスレープを有効にしてもALTER TABLEは追い越せない
          • マスターでALTER TABLEに要した時間と同じだけスレーブでへの反映が遅延する
        • レプリケーションの遅延が許容できない場合、オンラインALTER TABLEであってもアプリケーションを停止してからメンテナンスとしてALTER TABLEを実行する必要がある
      • 安全なインデックス削除のための不可視インデックス
        • インデックスを削除するDROP INDEXオペレーションは比較的高速に実行できるが、ADD INDEXオペレーションはそれなりに重い処理になるため、切り戻しが必要となった場合に時間がかかることがある
        • このような事態を避けるための「不可視インデックス」
          • 不可視とは、テーブル定義及びデータ構造上には存在するものの、オプティマイザから決して選択されない(= 見えない)ことを指している
          • 通常のインデックスと不可視インデックスの切り替えは一瞬で済むため、インデックスが削除されたあとに影響が出ないかを確認でき、万一影響が出たとしても切り戻しも一瞬でできる

ユーザ管理

  • グローバル権限はNG
    • MySQL 8.0.16以降では partial_revokes オプションによってブラックリスト的に制御できるようになったが、過大な権限は危険
    • 現実的な権限制御
      • データベース単位で原則の権限を付与(GRANT)
      • 必要に応じてテーブル単位で拒否(REVOKE/partial_revokes オプションが必要)

MySQLのさまざまな状態をみる

STATUSコマンド

  • 動作中のMySQLサーバーや、接続に関する基本的な情報を表示
  • Current user
    • 接続ユーザ
  • Threads
    • 現在MySQLサーバーに接続中のスレッド数
  • Question
    • MySQLサーバーが起動してから発生したクエリーの数
  • Slow queries
    • MySQLサーバーが起動してから発生したスロークエリー数
  • Opens
    • MySQLサーバーが起動してからオープンされたクテーブルの数
  • Open tables
    • 現在オープンされているテーブルの数

SHOWコマンド

  • データベースオブジェクトの情報やさまざまな状態を表示する
  • SHOW DATABASES
    • 現在接続中のユーザーがアクセス可能なデータベースの一覧を表示
  • SHOW TABLES
    • カレントデータベース内に存在するテーブル及びビューの一覧を表示
  • SHOW CREATE TABLE table_name
    • 指定したテーブルのCREATE文を返す
  • SHOW COLUMNS FROM table_name
    • 指定したテーブルまたはビューを構成するカラムの情報を表示
    • DESC table_name でも同じ結果を表示

サーバーや接続の動作オプションを見る

SHOW GLOBAL VARIABLES, SHOW SESSION VARIABLES(SHOW VARIABLES)

MySQLシステム変数の値を確認したいときには、SHOW VARIABLESを使用する。 セッション変数は@@SESSION、グローバル変数は@@GLOBALそれぞれの要素として確認できる。

動作中の状態を見る

  • SHOW STATUSコマンド
    • MySQLサーバーの実行状態に関する値を表示。メモリやディスクの使用状態、接続数に関する情報などが含まれている。
  • SHOW PROCESSLIST
    • その時点で稼働中のプロセスを一覧表示

発生したエラーや警告を見る

最後に実行したクエリーで発生したエラーや警告の内容を確認するには、SHOW ERRORS, SHOW WARNINGS命令が便利

レプリケーション

レプリケーションは「複数のMySQLサーバのデータを同期する」ための仕組みで、3.23 以前から20年以上にわかって提供し続けている枯れた機能

レプリケーションの仕組み

MySQLレプリケーションの構成では「マスターとスレーブに同一のデータを用意する」「」マスターに更新SQLを実行する」「マスターからスレーブに更新SQLを転送する」「スレーブが転送された更新SQLの内容をリプレイする」ことでデータを同期する。 「まったく同じデータを持ったMySQLに対して」「まったく同じSQLを実行すれば」「実行後のデータはまったく同じになる」という「結果整合性モデル」でレプリケーションは動作する。

(非同期)レプリケーションのデータ同期

古くからあるこの方式では、「クライアントから更新SQLを受け杖ける(唯一の)サーバー」を「マスター」、「マスターから間接的に更新SQLの内容を受け取るサーバー」を「スレーブ」と呼び、その関係は非対称。 マスターにはBinlog Dumpと呼ばれるスレッドが待機しており、このスレッドがバイナリログの更新を検知してスレーブにバイナリログイベントを送信する。Binlog Dumpスレッドはマスターと接続しているスレーブに対応して1対1で常駐する。 Binlog Dumpスレッドに対応する形でスレーブには「(スレーブ)I/O」が起動しており、この2つのスレッドがTCP通信でコネクションを開いている。コネクションからバイナリログイベントが送信されてくると、それをI/Oスレッドが受信してリレーログと呼ばれるファイルに記録する。 スレーブにはさらに「(スレーブ)SQL」スレッドが常駐しており、リレーログの更新を待ち受けている。このSQLスレッドがリレーログからバイナリログイベントを取り出し、スレーブ上のデータに対してリプレイすることで、マスターとスレーブのデータが同一となる。 このリプレイ時にスレーブにはスレーブサーバー常に新たにバイナリログを記録する。

  • 非同期レプリケーションと準同期レプリケーション
    • 非同期レプリケーション
      • 「マスター上のデータとバイナリログの更新は同期を保証」「それ以降のスレーブへの電波については時間を保証しない」設定
    • 準同期レプリケーション
      • 「マスター上のデータとバイナリログ、および(最低1台の)スレーブのリレーログへの更新は同期を保証」「最初の1台以外のスレーブ、および最初のスレーブにおいてもリレーログからデータの反映は、時間を保証しない」設定
    • 非同期レプリケーションと準同期レプリケーションの違いは、(主にマスタークラッシュ時の)データの永続性にある
  • マルチソースレプリケーション
    • 「マスター多:スレーブ1」の構成のこと
      • 「ソース」はバイナリログイベントを受け付ける「元」を表す。つまり、複数マスター構成
    • 「テーブル構成は同等だがユーザーIDなどによって収容先のMySQLが分かれている」(= シャーディングされている)構成をひとつのMySQLにまとめる、などの使い方をすることも可能
  • 多段レプリケーション構成
    • スレーブがマスターから受け取りリプレイしたバイナリログイベントを、さらにスレーブ自身のバイナリログに記録することによって、「1次スレーブをマスターに見立てた2次スレーブ」を構築できる。
      • 「多段レプリケーション構成」、「カスケードレプリケーション」などと呼び、1次スレーブを「子スレーブ」「中間マスター」「中間スレーブ」、2次スレーブを「孫スレーブ」などと呼ぶ
  • グループレプリケーションのデータ同期
    • グループレプリケーションでは「クライアントからの更新SQLを受け付けるサーバー」を「プライマリー」と呼びそれ以外のサーバーを「セカンダリー」と呼びます。
    • 「プライマリーが1台のみ選出されるシングルプライマリーモード」と「すべてのサーバーがプライマリーになるマルチプライマリーモード」の両モードが存在し、とくに後者の場合はマスター/スレーブ構成とは大きく異なる。
    • 更新SQLを受け付けたプライマリーサーバーはグループコミュニケーションエンジンに更新SQLの内容を渡し、各サーバーのグループコミュニケーションエンジン同士がサーティフィケーションと呼ばれる処理を経てリレーログに書き込む。

レプリケーションはデータの複製のための機能。マスターへの更新はバイナリログから始まる形でスレーブに非同期に差分更新される。