Slide 1

Slide 1 text

Copyright © 2020 Present ANDPAD Inc. This information is confidential and was prepared by ANDPAD Inc. for the use of our client. It is not to be relied on by and 3rd party. Proprietary & Confidential 無断転載・無断複製の禁止 2023.10.27 @Kaigi on Rails くぼ(@amamanamam)/ 株式会社アンドパッド やさしいActiveRecordのDB接続のしくみ 


Slide 2

Slide 2 text

ANDPAD Inc. DBRE Who I am @amamanamam Ayumu Kubo

Slide 3

Slide 3 text

今日持ち帰っていただきたいこと ● データベース接続確立までの全体像 ● データベース接続に関わるRailsのクラス・モジュール ● Railsの接続プールの仕組み ● 内部構造知るの楽しいなぁのお気持ち

Slide 4

Slide 4 text

モチベーション ● DBRE(Database Reliability Engineering)の仕事をしてい る中でアプリケーション開発者の方からの相談を受けるこ とが多い ● AcitveRecordの仕組みを知らないので、具体的な挙動につ いて言及したりアドバイスすることができない ● AcitveRecordを実際に使ってみること・内部構造を知るこ とで、アプリケーション視点の議論の一助になる ● そして内部実装を知るのが純粋に楽しい

Slide 5

Slide 5 text

環境 ● Ruby : 3.2.0 ● Rails : 7.1.0.alpha ● mysql2 : 0.5.5 ● MySQL : 8.0.28-debug

Slide 6

Slide 6 text

INDEX 1 クライアント/サーバー間の通信 2 クライアント/サーバー側でやること 3 ActiveRecordの接続までの過程(クラスとモジュールの説明) 4 ActiveRecordの接続までの過程(ソースの説明) 5 まとめ

Slide 7

Slide 7 text

INDEX 1 クライアント/サーバー間の通信 2 クライアント/サーバー側でやること 3 ActiveRecordの接続までの過程(クラスとモジュールの説明) 4 ActiveRecordの接続までの過程(ソースの説明) 5 まとめ

Slide 8

Slide 8 text

クライアント/サーバー間の通信 Copyright © 2020 Present ANDPAD Inc. This information is confidential and was prepared by ANDPAD Inc. for the use of our client. It is not to be relied on by and 3rd party. Proprietary & Confidential 無断転載・無断複製の禁止

Slide 9

Slide 9 text

接続と接続確立とは 接続とはクライアント・サーバー間の物理的な通信経路を指す SELECT * FROM hogehoge … Response OK

Slide 10

Slide 10 text

そのような接続が確立されるまでの クライアント・サーバー間の プロトコルのやりとりを見ていく

Slide 11

Slide 11 text

接続と接続確立とは TCPプロトコルの接続確立 MySQLプロトコルの接続確立 Request Query

Slide 12

Slide 12 text

接続と接続確立とは 3-way hand shake Server Greeting Login Request Response OK Request Query

Slide 13

Slide 13 text

接続と接続確立とは 以降ではServerGreetingやログイン認証が 終わった状況を「接続が確立した」という 3-way hand shake Server Greeting Login Request Response OK Request Query

Slide 14

Slide 14 text

クライアントとサーバー それぞれのやることを理解して もう少し全体像を明らかにする

Slide 15

Slide 15 text

INDEX 1 クライアント/サーバー間の通信 2 クライアント/サーバー側でやること 3 ActiveRecordの接続までの過程(クラスとモジュールの説明) 4 ActiveRecordの接続までの過程(ソースの説明) 5 まとめ

Slide 16

Slide 16 text

クライアント/サーバー側でやること Copyright © 2020 Present ANDPAD Inc. This information is confidential and was prepared by ANDPAD Inc. for the use of our client. It is not to be relied on by and 3rd party. Proprietary & Confidential 無断転載・無断複製の禁止

Slide 17

Slide 17 text

クライアント側とサーバー側のやることから 接続確立までの全体像をざっくり掴む

Slide 18

Slide 18 text

クライアント側の処理 ● ActiveRecordはアダプター(今回の場合だとmysql2)を使用して MySQLへの接続を確立させる ● アダプターはActiveRecordとMySQLの仲介役を担っており、 mysql2は内部では libmysqlclient ライブラリのAPIを使用して通信を 行う ● ActiveRecordは内部ではmysql2のクライアントのインスタンスを作 成して接続要求を行なっている

Slide 19

Slide 19 text

サーバー側の処理 ● MySQL側ではイニシャルハンドシェイクパケット送信やユーザ認証と認 証結果の送信などを行う ● ユーザ認証では認証プラグイン(mysql_native_passwordなど)の交換と決 定を行う ● これらはMySQLプロトコルを通じてクライアント側とやり取りをする ○ MySQLプロトコルはMySQL独自のアプリケーション層のプロトコル

Slide 20

Slide 20 text

全体図 Active Record mysql2 Application DB C API (libmysqlclient) ORM adapter mysql_real_connect(..) ● イニシャルハンド シェイクパケットの 送信 ● 認証方式の決定 ● 決定した方式で認証 ● OKパケットかERRパ ケットの送信

Slide 21

Slide 21 text

ざっくり全体像が分かったので ActiveRecord内の主要なクラスを見て より詳細に理解していく

Slide 22

Slide 22 text

INDEX 1 クライアント/サーバー間の通信 2 クライアント/サーバー側でやること 3 ActiveRecordの接続までの過程(クラスとモジュールの説明) 4 ActiveRecordの接続までの過程(ソースの説明) 5 まとめ

Slide 23

Slide 23 text

ActiveRecordの接続までの過程 (クラスとモジュールの説明) Copyright © 2020 Present ANDPAD Inc. This information is confidential and was prepared by ANDPAD Inc. for the use of our client. It is not to be relied on by and 3rd party. Proprietary & Confidential 無断転載・無断複製の禁止

Slide 24

Slide 24 text

ActiveRecordの接続過程の 主要なクラスやモジュールを炙り出す …どうやって?

Slide 25

Slide 25 text

主要なクラスを選び取るまでの流れ ● railsソースコード内のmysql2クライアントのインスタンス作成や それらしきところにブレークポイントを貼る ● 適当なSELECTクエリを投げるソースのspecを用意し、rdbgでブ レークポイントまでステップ実行 ● 得られたバックトレースから重要そうなクラスやモジュールを選 び取る

Slide 26

Slide 26 text

得られたバックトレースを眺めてみる

Slide 27

Slide 27 text

Mysql2::Client作成までのバックトレース ActiveRecord::ConnectionAdapters::Mysql2Adapter.new_client ActiveRecord::ConnectionAdapters::Mysql2Adapter#connect … ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#check_version ActiveRecord::ConnectionAdapters::ConnectionPool#new_connection … ActiveRecord::ConnectionAdapters::ConnectionPool#checkout ActiveRecord::ConnectionAdapters::ConnectionPool#connection block in with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#with_temporary_pool ActiveRecord::Tasks::DatabaseTasks#with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#schema_up_to_date?

Slide 28

Slide 28 text

Mysql2::Client作成までのバックトレース ActiveRecord::ConnectionAdapters::Mysql2Adapter.new_client ActiveRecord::ConnectionAdapters::Mysql2Adapter#connect … ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#check_version ActiveRecord::ConnectionAdapters::ConnectionPool#new_connection … ActiveRecord::ConnectionAdapters::ConnectionPool#checkout ActiveRecord::ConnectionAdapters::ConnectionPool#connection block in with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#with_temporary_pool ActiveRecord::Tasks::DatabaseTasks#with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#schema_up_to_date?

Slide 29

Slide 29 text

ConnectionPool作成までのバックトレース ActiveRecord::ConnectionAdapters::PoolConfig#pool ActiveRecord::ConnectionAdapters::ConnectionHandler#establish_connection ActiveRecord::ConnectionHandling#establish_connection ActiveRecord::Tasks::DatabaseTasks#with_temporary_pool ActiveRecord::Tasks::DatabaseTasks#with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#schema_up_to_date?

Slide 30

Slide 30 text

ConnectionPool作成までのバックトレース ActiveRecord::ConnectionAdapters::PoolConfig#pool ActiveRecord::ConnectionAdapters::ConnectionHandler#establish_connection ActiveRecord::ConnectionHandling#establish_connection ActiveRecord::Tasks::DatabaseTasks#with_temporary_pool ActiveRecord::Tasks::DatabaseTasks#with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#schema_up_to_date?

Slide 31

Slide 31 text

接続に関わる主なクラスとモジュール 主要と思われるのは以下の3つのクラス ● ActiveRecord::ConenctionAdapters::Mysql2Adapters ● ActiveRecord::ConenctionAdapters::ConnectionPool ● ActiveRecord::ConenctionAdapters::ConnectionHandler これらのクラスの役割を通じて、全体像から詳細を掴んでいく

Slide 32

Slide 32 text

主要なクラスが分かったので 1つずつの役割を調べてみる

Slide 33

Slide 33 text

接続に関わる主なクラスとモジュール ● ActiveRecord::ConenctionAdapters::Mysql2Adapters ● ActiveRecord::ConenctionAdapters::ConnectionPool ● ActiveRecord::ConenctionAdapters::ConnectionHandler

Slide 34

Slide 34 text

Mysql2Adapters ● 実際にmysql2アダプターを呼び出すクラス ● AbstractMysqlAdaptersを継承しており、mysql2を用いたデータ ベース接続を実装する ○ AbstractMysqlAdaptersはMySQLへの接続を抽象化したクラス ● クラス内のメソッドでmysql2のクライアントインスタンスである Mysql2::Clientを作成する

Slide 35

Slide 35 text

mysql2 Application DB adapter Mysql2Adapter Mysql2::Client

Slide 36

Slide 36 text

mysql2 Application DB adapter Mysql2Adapter Mysql2::Client Mysql2::Clientの作成 接続要求

Slide 37

Slide 37 text

接続について再定義 ● 接続はクライアント・サーバー間の物理的な通信経路 ● Mysql2AdapterやMysql2::Clientインスタンスごとにデータ ベース接続が存在するので、これら自身を「接続」と見做して も良い Mysql2::Client Mysql2::Client Mysql2::Client Mysql2Adapter Mysql2Adapter Mysql2Adapter 通信経路

Slide 38

Slide 38 text

接続に関わる主なクラスとモジュール ● ActiveRecord::ConenctionAdapters::Mysql2Adapters ● ActiveRecord::ConenctionAdapters::ConnectionPool ● ActiveRecord::ConenctionAdapters::ConnectionHandler

Slide 39

Slide 39 text

ConnectionPool ● 接続を管理するクラス ● 接続プールから接続を出し入れしたり、接続要求の競合を調節 したりする ● 接続要求を受けたら「接続プールから取り出す」か「接続を新 規作成」するように調整する

Slide 40

Slide 40 text

mysql2 Application DB adapter Mysql2Adapter Mysql2::Client ConnectionPool

Slide 41

Slide 41 text

mysql2 Application DB adapter Mysql2Adapter Mysql2::Client ConnectionPool 接続の新規作成か プールからの再利用

Slide 42

Slide 42 text

接続に関わる主なクラスとモジュール ● ActiveRecord::ConenctionAdapters::Mysql2Adapters ● ActiveRecord::ConenctionAdapters::ConnectionPool ● ActiveRecord::ConenctionAdapters::ConnectionHandler

Slide 43

Slide 43 text

ConnectionHandler ● 接続プールを管理するクラス ● データベースごとに接続プールを保持している ○ 接続プールの区分けの単位は次節で述べる ● 各モデルはこのハンドラーを通してどのデータベースに接続す るかを決定する

Slide 44

Slide 44 text

mysql2 Application DB adapter Mysql2Adapter Mysql2::Client ConnectionHandler ConnectionPool …. ConnectionPool

Slide 45

Slide 45 text

mysql2 Application DB adapter Mysql2Adapter Mysql2::Client ConnectionPool ConnectionHandler どの接続プール(ConnectionPool) を使うか決定する ConnectionPool ….

Slide 46

Slide 46 text

詳細が少し分かったので ActiveRecordのソースレベルで 深く理解していく

Slide 47

Slide 47 text

INDEX 1 クライアント/サーバー間の通信 2 クライアント/サーバー側でやること 3 ActiveRecordの接続までの過程(クラスとモジュールの説明) 4 ActiveRecordの接続までの過程(ソースの説明) 5 まとめ

Slide 48

Slide 48 text

ActiveRecordの接続までの過程 (ソースの説明) Copyright © 2020 Present ANDPAD Inc. This information is confidential and was prepared by ANDPAD Inc. for the use of our client. It is not to be relied on by and 3rd party. Proprietary & Confidential 無断転載・無断複製の禁止

Slide 49

Slide 49 text

先ほどのバックトレースを改めて見て 幾つかのメソッドのソースを眺めてみる

Slide 50

Slide 50 text

Mysql2::Client作成までのバックトレース(再掲) ActiveRecord::ConnectionAdapters::Mysql2Adapter.new_client ActiveRecord::ConnectionAdapters::Mysql2Adapter#connect … ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#check_version ActiveRecord::ConnectionAdapters::ConnectionPool#new_connection … ActiveRecord::ConnectionAdapters::ConnectionPool#checkout ActiveRecord::ConnectionAdapters::ConnectionPool#connection block in with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#with_temporary_pool ActiveRecord::Tasks::DatabaseTasks#with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#schema_up_to_date?

Slide 51

Slide 51 text

Mysql2::Client作成までのバックトレース(再掲) ActiveRecord::ConnectionAdapters::Mysql2Adapter.new_client ActiveRecord::ConnectionAdapters::Mysql2Adapter#connect … ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#check_version ActiveRecord::ConnectionAdapters::ConnectionPool#new_connection … ActiveRecord::ConnectionAdapters::ConnectionPool#checkout ActiveRecord::ConnectionAdapters::ConnectionPool#connection block in with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#with_temporary_pool ActiveRecord::Tasks::DatabaseTasks#with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#schema_up_to_date? まずは接続要求の部分に注目してみる

Slide 52

Slide 52 text

接続要求 module ConnectionAdapters # = Active Record MySQL2 Adapter class Mysql2Adapter < AbstractMysqlAdapter …… class << self def new_client(config) ::Mysql2::Client.new(config) rescue ::Mysql2::Error => error if error.error_number == ConnectionAdapters::Mysql2Adapter::ER_BAD_DB_ERROR raise ActiveRecord::NoDatabaseError.db_error(config[:database]) elsif error.error_number == ConnectionAdapters::Mysql2Adapter::ER_ACCESS_DENIED_ERROR raise ActiveRecord::DatabaseConnectionError.username_error(config[:username]) elsif [ConnectionAdapters::Mysql2Adapter::ER_CONN_HOST_ERROR, ConnectionAdapters::Mysql2Adapter::ER_UNKNOWN_HOST_ERROR].include?(error.error_number) raise ActiveRecord::DatabaseConnectionError.hostname_error(config[:host]) else raise ActiveRecord::ConnectionNotEstablished, error.message end end end

Slide 53

Slide 53 text

接続要求 module ConnectionAdapters # = Active Record MySQL2 Adapter class Mysql2Adapter < AbstractMysqlAdapter …… class << self def new_client(config) ::Mysql2::Client.new(config) rescue ::Mysql2::Error => error if error.error_number == ConnectionAdapters::Mysql2Adapter::ER_BAD_DB_ERROR raise ActiveRecord::NoDatabaseError.db_error(config[:database]) elsif error.error_number == ConnectionAdapters::Mysql2Adapter::ER_ACCESS_DENIED_ERROR raise ActiveRecord::DatabaseConnectionError.username_error(config[:username]) elsif [ConnectionAdapters::Mysql2Adapter::ER_CONN_HOST_ERROR, ConnectionAdapters::Mysql2Adapter::ER_UNKNOWN_HOST_ERROR].include?(error.error_number) raise ActiveRecord::DatabaseConnectionError.hostname_error(config[:host]) else raise ActiveRecord::ConnectionNotEstablished, error.message end end end mysql2のクライアントインスタンス作成 (config にはdatabase.ymlの情報)

Slide 54

Slide 54 text

接続要求 def initialize(opts = {}) raise Mysql2::Error, "Options parameter must be a Hash" unless opts.is_a? Hash opts = Mysql2::Util.key_hash_as_symbols(opts) ... user = opts[:username] || opts[:user] pass = opts[:password] || opts[:pass] host = opts[:host] || opts[:hostname] port = opts[:port] database = opts[:database] || opts[:dbname] || opts[:db] socket = opts[:socket] || opts[:sock] conn_attrs = parse_connect_attrs(opts[:connect_attrs]) ... connect user, pass, host, port, database, socket, flags, conn_attrs end mysq2/client.rb

Slide 55

Slide 55 text

接続要求 def initialize(opts = {}) raise Mysql2::Error, "Options parameter must be a Hash" unless opts.is_a? Hash opts = Mysql2::Util.key_hash_as_symbols(opts) ... user = opts[:username] || opts[:user] pass = opts[:password] || opts[:pass] host = opts[:host] || opts[:hostname] port = opts[:port] database = opts[:database] || opts[:dbname] || opts[:db] socket = opts[:socket] || opts[:sock] conn_attrs = parse_connect_attrs(opts[:connect_attrs]) ... connect user, pass, host, port, database, socket, flags, conn_attrs end mysq2/client.rb initializeメソッドで database.yml の情報をもとに接続要求

Slide 56

Slide 56 text

接続要求部分のまとめ ● Mysql2AdapterによってMysql2::Clientインスタンスが作成される ● Mysql2::Clientインスタンス生成時に、initializeメソッドで libmysqlclientのC APIを呼び出して接続要求を行う

Slide 57

Slide 57 text

mysql2 Application DB adapter Mysql2::Client Mysql2::Client Mysql2::Client Mysql2Adapter Mysql2Adapter Mysql2Adapter Mysql2::Client オブジェクトの作成 C APIで接続要求 database.yml

Slide 58

Slide 58 text

mysql2 Application DB adapter Mysql2::Client Mysql2::Client Mysql2::Client Mysql2Adapter Mysql2Adapter Mysql2Adapter

Slide 59

Slide 59 text

Mysql2::Client作成までのバックトレース(再掲) ActiveRecord::ConnectionAdapters::Mysql2Adapter.new_client ActiveRecord::ConnectionAdapters::Mysql2Adapter#connect … ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#check_version ActiveRecord::ConnectionAdapters::ConnectionPool#new_connection … ActiveRecord::ConnectionAdapters::ConnectionPool#checkout ActiveRecord::ConnectionAdapters::ConnectionPool#connection block in with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#with_temporary_pool ActiveRecord::Tasks::DatabaseTasks#with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#schema_up_to_date?

Slide 60

Slide 60 text

Mysql2::Client作成までのバックトレース(再掲) ActiveRecord::ConnectionAdapters::Mysql2Adapter.new_client ActiveRecord::ConnectionAdapters::Mysql2Adapter#connect … ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#check_version ActiveRecord::ConnectionAdapters::ConnectionPool#new_connection … ActiveRecord::ConnectionAdapters::ConnectionPool#checkout ActiveRecord::ConnectionAdapters::ConnectionPool#connection block in with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#with_temporary_pool ActiveRecord::Tasks::DatabaseTasks#with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#schema_up_to_date? 次に接続作成の部分に注目してみる

Slide 61

Slide 61 text

接続作成 module ActiveRecord module ConnectionAdapters ... ... class ConnectionPool ... def new_connection connection = Base.public_send(db_config.adapter_method, db_config.configuration_hash) connection.pool = self connection.check_version connection end end end

Slide 62

Slide 62 text

接続作成 module ActiveRecord module ConnectionAdapters ... ... class ConnectionPool ... def new_connection connection = Base.public_send(db_config.adapter_method, db_config.configuration_hash) connection.pool = self connection.check_version connection end end end database.ymlのadapterに記載されて るアダプターに対応する接続を作成 今回の場合はMysql2Adapter作成

Slide 63

Slide 63 text

接続作成 module ActiveRecord module ConnectionAdapters ... ... class ConnectionPool ... def new_connection connection = Base.public_send(db_config.adapter_method, db_config.configuration_hash) connection.pool = self connection.check_version connection end end end データベースのバージョンを確認する このタイミングで接続要求をする (先ほどのMysql2::Client作成に繋がる)

Slide 64

Slide 64 text

接続作成部分のまとめ ● ConnectionPoolのnew_connectionメソッドによってMysql2Adapter インスタンスが作成される ● database.ymlのadapterの内容によって対応するアダプタクラスが作 成される ● その後、データベースのバージョンを確認するために接続要求をする

Slide 65

Slide 65 text

mysql2 Application adapter Mysql2::Client Mysql2::Client Mysql2::Client Mysql2Adapter Mysql2Adapter Mysql2Adapter ConnectionPool Mysql2Adapter オブジェクトの作成 DB database.yml

Slide 66

Slide 66 text

mysql2 Application adapter Mysql2::Client Mysql2::Client Mysql2::Client Mysql2Adapter Mysql2Adapter Mysql2Adapter ConnectionPool DB

Slide 67

Slide 67 text

Mysql2::Client作成までのバックトレース ActiveRecord::ConnectionAdapters::Mysql2Adapter.new_client ActiveRecord::ConnectionAdapters::Mysql2Adapter#connect … ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#check_version ActiveRecord::ConnectionAdapters::ConnectionPool#new_connection … ActiveRecord::ConnectionAdapters::ConnectionPool#checkout ActiveRecord::ConnectionAdapters::ConnectionPool#connection block in with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#with_temporary_pool ActiveRecord::Tasks::DatabaseTasks#with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#schema_up_to_date?

Slide 68

Slide 68 text

Mysql2::Client作成までのバックトレース ActiveRecord::ConnectionAdapters::Mysql2Adapter.new_client ActiveRecord::ConnectionAdapters::Mysql2Adapter#connect … ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#check_version ActiveRecord::ConnectionAdapters::ConnectionPool#new_connection … ActiveRecord::ConnectionAdapters::ConnectionPool#checkout ActiveRecord::ConnectionAdapters::ConnectionPool#connection block in with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#with_temporary_pool ActiveRecord::Tasks::DatabaseTasks#with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#schema_up_to_date? 次に接続プールからの取り出しの部分に注目してみる

Slide 69

Slide 69 text

接続プールからの接続取得 module ActiveRecord module ConnectionAdapters ... ... class ConnectionPool ... def checkout(checkout_timeout = @checkout_timeout) connection = checkout_and_verify(acquire_connection(checkout_timeout)) connection.lock_thread = @lock_thread connection end end end

Slide 70

Slide 70 text

module ActiveRecord module ConnectionAdapters ... ... class ConnectionPool ... def checkout(checkout_timeout = @checkout_timeout) connection = checkout_and_verify(acquire_connection(checkout_timeout)) connection.lock_thread = @lock_thread connection end end end 接続プールから接続を取り出す または新規接続を作成する 接続プールからの接続取得

Slide 71

Slide 71 text

module ActiveRecord module ConnectionAdapters ... ... class ConnectionPool ... def acquire_connection(checkout_timeout)     if conn = @available.poll || try_to_checkout_new_connection       conn     else       reap       @available.poll(checkout_timeout)     end   end end end 接続プールからの接続取得

Slide 72

Slide 72 text

module ActiveRecord module ConnectionAdapters ... ... class ConnectionPool ... def acquire_connection(checkout_timeout)     if conn = @available.poll || try_to_checkout_new_connection       conn     else       reap       @available.poll(checkout_timeout)     end   end end end まず接続キュー(プール)を確認する 接続がなければ新規接続を試みる (先ほどのnew_connectionメソッドに繋がる) 接続プールからの接続取得

Slide 73

Slide 73 text

● ConnectionPoolのcheckoutメソッドによって接続プールから接続を 取得するか、新規接続を作成するかのどちらかが実行される ● 新規接続される場合はnew_connectionメソッドが呼び出される 接続プールからの接続取得部分のまとめ

Slide 74

Slide 74 text

mysql2 Application adapter Mysql2::Client Mysql2::Client Mysql2::Client Mysql2Adapter Mysql2Adapter Mysql2Adapter ConnectionPool available 接続キューの確認 キューから取り出し DB

Slide 75

Slide 75 text

mysql2 Application adapter Mysql2::Client Mysql2::Client Mysql2::Client Mysql2Adapter Mysql2Adapter Mysql2Adapter ConnectionPool available DB

Slide 76

Slide 76 text

ConnectionPool作成までのバックトレース(再掲) ActiveRecord::ConnectionAdapters::PoolConfig#pool ActiveRecord::ConnectionAdapters::ConnectionHandler#establish_connection ActiveRecord::ConnectionHandling#establish_connection ActiveRecord::Tasks::DatabaseTasks#with_temporary_pool ActiveRecord::Tasks::DatabaseTasks#with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#schema_up_to_date?

Slide 77

Slide 77 text

ActiveRecord::ConnectionAdapters::PoolConfig#pool ActiveRecord::ConnectionAdapters::ConnectionHandler#establish_connection ActiveRecord::ConnectionHandling#establish_connection ActiveRecord::Tasks::DatabaseTasks#with_temporary_pool ActiveRecord::Tasks::DatabaseTasks#with_temporary_connection ActiveRecord::Tasks::DatabaseTasks#schema_up_to_date? ConnectionPool作成までのバックトレース(再掲) 最後に接続プール作成の部分に注目してみる

Slide 78

Slide 78 text

ConnectionPoolの作成 def establish_connection(config, owner_name: Base, role: ActiveRecord::Base.current_role, shard: Base.current_shard) owner_name = determine_owner_name(owner_name, config) pool_config = resolve_pool_config(config, owner_name, role, shard) db_config = pool_config.db_config pool_manager = set_pool_manager(pool_config.connection_name) existing_pool_config = pool_manager.get_pool_config(role, shard) if existing_pool_config && existing_pool_config.db_config == db_config if owner_name.primary_class? && (existing_pool_config.connection_class != owner_name) existing_pool_config.connection_class = owner_name end existing_pool_config.pool else ・・・    ・・・

Slide 79

Slide 79 text

def establish_connection(config, owner_name: Base, role: ActiveRecord::Base.current_role, shard: Base.current_shard) owner_name = determine_owner_name(owner_name, config) pool_config = resolve_pool_config(config, owner_name, role, shard) db_config = pool_config.db_config pool_manager = set_pool_manager(pool_config.connection_name) existing_pool_config = pool_manager.get_pool_config(role, shard) if existing_pool_config && existing_pool_config.db_config == db_config if owner_name.primary_class? && (existing_pool_config.connection_class != owner_name) existing_pool_config.connection_class = owner_name end existing_pool_config.pool else ・・・    ・・・ database.ymlに記載のどのDBに接続するか を表す抽象クラス名を取得 ConnectionPoolの作成

Slide 80

Slide 80 text

owner_nameとは primary:   database: my_primary_database   username: app   password: <%= ENV['ROOT_PASSWORD'] %>   adapter: mysql2 world:   database: world   username: app   password: <%= ENV['WORLDS_ROOT_PASSWORD'] %>   adapter: mysql2 database.yml

Slide 81

Slide 81 text

primary:   database: my_primary_database   username: app   password: <%= ENV['ROOT_PASSWORD'] %>   adapter: mysql2 world:   database: world   username: app   password: <%= ENV['WORLDS_ROOT_PASSWORD'] %>   adapter: mysql2 class WorldRecord < ApplicationRecord   self.abstract_class = true   connects_to database: { writing: :world } end class ApplicationRecord < ActiveRecord::Base self.abstract_class = true connects_to database: { writing: :primary} end ↑primaryのDBに繋ぐように定義したコネクションモデル ↑worldのDBに繋ぐように定義したコネクションモデル owner_nameとは database.yml

Slide 82

Slide 82 text

primary:   database: my_primary_database   username: app   password: <%= ENV['ROOT_PASSWORD'] %>   adapter: mysql2 world:   database: world   username: app   password: <%= ENV['WORLDS_ROOT_PASSWORD'] %>   adapter: mysql2 class WorldRecord < ApplicationRecord   self.abstract_class = true   connects_to database: { writing: :world } end class ApplicationRecord < ActiveRecord::Base self.abstract_class = true connects_to database: { writing: :primary} end owner_name owner_nameとは database.yml

Slide 83

Slide 83 text

ConnectionPoolの作成 def establish_connection(config, owner_name: Base, role: ActiveRecord::Base.current_role, shard: Base.current_shard) owner_name = determine_owner_name(owner_name, config) pool_config = resolve_pool_config(config, owner_name, role, shard) db_config = pool_config.db_config pool_manager = set_pool_manager(pool_config.connection_name) existing_pool_config = pool_manager.get_pool_config(role, shard) if existing_pool_config && existing_pool_config.db_config == db_config if owner_name.primary_class? && (existing_pool_config.connection_class != owner_name) existing_pool_config.connection_class = owner_name end existing_pool_config.pool else ・・・    ・・・

Slide 84

Slide 84 text

def establish_connection(config, owner_name: Base, role: ActiveRecord::Base.current_role, shard: Base.current_shard) owner_name = determine_owner_name(owner_name, config) pool_config = resolve_pool_config(config, owner_name, role, shard) db_config = pool_config.db_config pool_manager = set_pool_manager(pool_config.connection_name) existing_pool_config = pool_manager.get_pool_config(role, shard) if existing_pool_config && existing_pool_config.db_config == db_config if owner_name.primary_class? && (existing_pool_config.connection_class != owner_name) existing_pool_config.connection_class = owner_name end existing_pool_config.pool else ・・・    ・・・ PoolConfigという接続設定クラス作成 それを管理するPoolManagerクラスを作成もしくは既存の再利用 PoolManagerはowner_nameで一意となる ConnectionPoolの作成

Slide 85

Slide 85 text

Application ConnectionHandler DB PoolManager PoolManager owner:ApplicationRecord owner:WorldRecord owner ごとに PoolManagerを作成

Slide 86

Slide 86 text

ConnectionPoolの作成 def establish_connection(config, owner_name: Base, role: ActiveRecord::Base.current_role, shard: Base.current_shard) owner_name = determine_owner_name(owner_name, config) pool_config = resolve_pool_config(config, owner_name, role, shard) db_config = pool_config.db_config pool_manager = set_pool_manager(pool_config.connection_name) existing_pool_config = pool_manager.get_pool_config(role, shard) if existing_pool_config && existing_pool_config.db_config == db_config if owner_name.primary_class? && (existing_pool_config.connection_class != owner_name) existing_pool_config.connection_class = owner_name end existing_pool_config.pool else ・・・    ・・・

Slide 87

Slide 87 text

def establish_connection(config, owner_name: Base, role: ActiveRecord::Base.current_role, shard: Base.current_shard) owner_name = determine_owner_name(owner_name, config) pool_config = resolve_pool_config(config, owner_name, role, shard) db_config = pool_config.db_config pool_manager = set_pool_manager(pool_config.connection_name) existing_pool_config = pool_manager.get_pool_config(role, shard) if existing_pool_config && existing_pool_config.db_config == db_config if owner_name.primary_class? && (existing_pool_config.connection_class != owner_name) existing_pool_config.connection_class = owner_name end existing_pool_config.pool else ・・・    ・・・ 既存のPoolConfigがないか確認 PoolConfigはroleとshardで一意になる ConnectionPoolの作成

Slide 88

Slide 88 text

world:   database: world   username: app   password: <%= ENV['ANIMALS_ROOT_PASSWORD'] %>   adapter: mysql2   host: hogehoge world_replica: database: world username: app_readonly password: <%= ENV['ANIMALS_READONLY_PASSWORD'] %> adapter: mysql2 host: hugahuga replica: true roleとは database.yml

Slide 89

Slide 89 text

class WorldRecord < ApplicationRecord   self.abstract_class = true   connects_to database: { writing: :world, reading: :world_replica } end ↑ ライターとリーダーで別々のDBに接続するように 定義したコネクションモデル world:   database: world   username: app   password: <%= ENV['ANIMALS_ROOT_PASSWORD'] %>   adapter: mysql2   host: hogehoge world_replica: database: world username: app_readonly password: <%= ENV['ANIMALS_READONLY_PASSWORD'] %> adapter: mysql2 host: hugahuga replica: true roleとは database.yml

Slide 90

Slide 90 text

roleとは world:   database: world   username: app   password: <%= ENV['ANIMALS_ROOT_PASSWORD'] %>   adapter: mysql2   host: hogehoge world_replica: database: world username: app_readonly password: <%= ENV['ANIMALS_READONLY_PASSWORD'] %> adapter: mysql2 host: hugahuga replica: true class WorldRecord < ApplicationRecord   self.abstract_class = true   connects_to database: { writing: :world, reading: :world_replica } end role database.yml

Slide 91

Slide 91 text

shardとは ● 大規模なテーブルを複数の小さなパートに分けてデー タベースごとに分割することをシャーディングという ● 行単位で分割する場合は水平シャーディング、列単位 で分割する場合は垂直シャーディングという ● これによって読み書きを分散し、負荷分散と拡張性を 向上させることができる ● 分割した1つのパートをシャードという単位で呼ぶ

Slide 92

Slide 92 text

shardとは primary: database: my_primary_database adapter: mysql2 primary_replica: database: my_primary_database adapter: mysql2 replica: true primary_shard_one: database: my_primary_shard_one adapter: mysql2 primary_shard_one_replica: database: my_primary_shard_one adapter: mysql2 replica: true database.yml

Slide 93

Slide 93 text

class ApplicationRecord < ActiveRecord::Base self.abstract_class = true connects_to shards: { default: { writing: :primary, reading: :primary_replica }, shard_one: { writing: :primary_shard_one, reading: :primary_shard_one_replica } } end primary: database: my_primary_database adapter: mysql2 primary_replica: database: my_primary_database adapter: mysql2 replica: true primary_shard_one: database: my_primary_shard_one adapter: mysql2 primary_shard_one_replica: database: my_primary_shard_one adapter: mysql2 replica: true ↑ シャードごとに接続先DBを定義したコネクションモデル shardとは database.yml

Slide 94

Slide 94 text

primary: database: my_primary_database adapter: mysql2 primary_replica: database: my_primary_database adapter: mysql2 replica: true primary_shard_one: database: my_primary_shard_one adapter: mysql2 primary_shard_one_replica: database: my_primary_shard_one adapter: mysql2 replica: true class ApplicationRecord < ActiveRecord::Base self.abstract_class = true connects_to shards: { default: { writing: :primary, reading: :primary_replica }, shard_one: { writing: :primary_shard_one, reading: :primary_shard_one_replica } } end shard shardとは database.yml

Slide 95

Slide 95 text

Application ConnectionHandler DB PoolManager PoolManager PoolConfig PoolConfig PoolConfig PoolConfig role: writer role, shard ごとに PoolConfigを作成 role: reader role: reader role: writer

Slide 96

Slide 96 text

ConnectionPoolの作成 def establish_connection(config, owner_name: Base, role: ActiveRecord::Base.current_role, shard: Base.current_shard) owner_name = determine_owner_name(owner_name, config) pool_config = resolve_pool_config(config, owner_name, role, shard) db_config = pool_config.db_config pool_manager = set_pool_manager(pool_config.connection_name) existing_pool_config = pool_manager.get_pool_config(role, shard) if existing_pool_config && existing_pool_config.db_config == db_config if owner_name.primary_class? && (existing_pool_config.connection_class != owner_name) existing_pool_config.connection_class = owner_name end existing_pool_config.pool else ・・・    ・・・

Slide 97

Slide 97 text

def establish_connection(config, owner_name: Base, role: ActiveRecord::Base.current_role, shard: Base.current_shard) owner_name = determine_owner_name(owner_name, config) pool_config = resolve_pool_config(config, owner_name, role, shard) db_config = pool_config.db_config pool_manager = set_pool_manager(pool_config.connection_name) existing_pool_config = pool_manager.get_pool_config(role, shard) if existing_pool_config && existing_pool_config.db_config == db_config if owner_name.primary_class? && (existing_pool_config.connection_class != owner_name) existing_pool_config.connection_class = owner_name end existing_pool_config.pool else ・・・    ・・・ 新規もしくは既存のPoolConfig に基づいたConnectionPool作成 ConnectionPoolの作成

Slide 98

Slide 98 text

Application ConnectionPool DB PoolConfig PoolConfig PoolConfig PoolConfig ConnectionPool ConnectionPool ConnectionPool ConnectionPool作成 ConnectionHandler PoolManager PoolManager

Slide 99

Slide 99 text

● ConnectionHandlerのestablish_connectionメソッドによって ConnectionPoolが作成される ● 具体的にはowner_nameごとにPoolManagerが作成され、 そのPoolManagerからrole, shardごとにPoolConfigが作成され、 そのPoolConfigからConnectionPoolが作成される ● つまり、owner_name,role, shardごとに別々のConnectionPoolが作 成される 接続プールからの接続取得

Slide 100

Slide 100 text

Application ConnectionPool ConnectionHandler DB PoolManager PoolManager PoolConfig PoolConfig PoolConfig PoolConfig ConnectionPool ConnectionPool ConnectionPool owner ごとに PoolManagerを作成 role, shard ごとに PoolConfigを作成 ConnectionPool作成

Slide 101

Slide 101 text

INDEX 1 クライアント/サーバー間の通信 2 クライアント/サーバー側でやること 3 ActiveRecordの接続までの過程(クラスとモジュールの説明) 4 ActiveRecordの接続までの過程(ソースの説明) 5 まとめ

Slide 102

Slide 102 text

まとめ Copyright © 2020 Present ANDPAD Inc. This information is confidential and was prepared by ANDPAD Inc. for the use of our client. It is not to be relied on by and 3rd party. Proprietary & Confidential 無断転載・無断複製の禁止

Slide 103

Slide 103 text

まとめ ● ActiveRecordはMySQLへの接続を確立するために、アダプター (今回の場合だとmysql2)を使用する ● Mysql2Adapterクラスによって、mysql2のクライアントインス タンスが作成される ● ConnectionPoolクラスによって、接続が接続プールから取り出 される、もしくは新規で作成される ● ConnectionHandlerクラスによって、role,shard,owner_name ごとで接続プールが作成される

Slide 104

Slide 104 text

全体図(再掲) Active Record mysql2 Application DB C API (libmysqlclient) ORM adapter mysql_real_connect(..) ● イニシャルハンド シェイクパケット の送信 ● 認証方式の決定 ● 決定した方式で認 証 ● OKパケットかERR パケットの送信

Slide 105

Slide 105 text

mysql2 Application DB adapter Mysql2::Client Mysql2::Client Mysql2::Client Mysql2Adapter Mysql2Adapter Mysql2Adapter Mysql2::Client オブジェクトの作成 接続要求

Slide 106

Slide 106 text

mysql2 Application adapter Mysql2::Client Mysql2::Client Mysql2::Client Mysql2Adapter Mysql2Adapter Mysql2Adapter ConnectionPool available 接続キューの確認 キューから取り出し DB

Slide 107

Slide 107 text

Application ConnectionPool ConnectionHandler DB PoolManager PoolManager PoolConfig PoolConfig PoolConfig PoolConfig ConnectionPool ConnectionPool ConnectionPool owner ごとに PoolManagerを作成 role, shard ごとに PoolConfigを作成 ConnectionPool作成

Slide 108

Slide 108 text

接続確立の全体像を把握した上で得た知見

Slide 109

Slide 109 text

知見その1(エラー原因の切り分け方法) ● エラーメッセージをパッと見ても原因箇所の想像がつかない場合があった ○ 例:ActiveRecord::ConnectionNotEstablished: No connection pool with hogehoge for the hugahuga role ● 注目すべきポイントがどこなのか原因を切り分けることができれば、問題 解決の難しさが軽減される ● Mysql2Adapterのnew_clientメソッド時点で接続確立を行うので、バック トレースを見て、それ以前・その時点・その以後かどうかで原因の切り分 けができる

Slide 110

Slide 110 text

知見その1(エラー原因の切り分け) ● new_clientメソッド以前の時点でのエラーは 「接続確立する前のアプリケーション側の処理」の段階 ○ role/shardの記載が適切でない ○ 接続の新規作成or再利用の待ち時間超過 ● new_clientメソッド時点のエラーは「接続確立する」段階 ○ host/user/passwordの記載が適切でない ○ MySQL側の接続上限に達する ● new_clientメソッド以後の時点でのエラーは 「接続確立後のMySQLとのやりとり」の段階 ○ ロック待ち時間の超過 ○ デッドロック発生 接続前 接続後 接続時

Slide 111

Slide 111 text

知見その2(DBホスト変更の注意点) ● Mysql2Adaptersのクライアントインスタンスはdatabase.ymlの 情報を保持している ● 接続プールで保持しているのはこのMysql2Adaptersのクライア ントインスタンスである ● DBホストの変更を行う際は、プールで保持されている変更以前 のホストの接続が利用される可能性がある ● 特にフェイルオーバーした時は、降格したリーダーに更新クエリ が投げられることが起こりかねない ● プールから接続を得る際にバリデーションクエリ(read_onlyの確 認など)を流す方法があったりする https://qiita.com/hmatsu47/items/605f7edaf390d52ec828

Slide 112

Slide 112 text

ご清聴ありがとうございました Copyright © 2020 Present ANDPAD Inc. This information is confidential and was prepared by ANDPAD Inc. for the use of our client. It is not to be relied on by and 3rd party. Proprietary & Confidential 無断転載・無断複製の禁止

Slide 113

Slide 113 text

Appendix Copyright © 2020 Present ANDPAD Inc. This information is confidential and was prepared by ANDPAD Inc. for the use of our client. It is not to be relied on by and 3rd party. Proprietary & Confidential 無断転載・無断複製の禁止

Slide 114

Slide 114 text

MySQLプロトコル ● MySQL独自のアプリケーション層のプロトコル ● 各種コネクタ(Connector/J等)やMySQL Proxy、レプリケーション などに用いられている ● MySQLプロトコルのやりとりは大きく接続フェーズとコマンドフェー ズに分かれる

Slide 115

Slide 115 text

MySQLプロトコル https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_lifecycle.html

Slide 116

Slide 116 text

MySQLプロトコル ● Connection LifecycleのConnection Phaseの話 ○ https://amamanamam.hatenablog.com/entry/2023/09/29/223944 ● 接続要求を受けたときのメインスレッドの働きの話 ○ https://amamanamam.hatenablog.com/entry/2023/10/05/100502