$30 off During Our Annual Pro Sale. View Details »

やさしいActiveRecordのDB接続のしくみ

kubo ayumu
October 27, 2023

 やさしいActiveRecordのDB接続のしくみ

kubo ayumu

October 27, 2023
Tweet

More Decks by kubo ayumu

Other Decks in Programming

Transcript

  1. 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接続のしくみ

    View Slide

  2. ANDPAD Inc.
    DBRE
    Who I am
    @amamanamam
    Ayumu Kubo

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. クライアント/サーバー間の通信
    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 無断転載・無断複製の禁止

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. クライアント/サーバー側でやること
    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 無断転載・無断複製の禁止

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  23. 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 無断転載・無断複製の禁止

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  27. 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?

    View Slide

  28. 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?

    View Slide

  29. 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?

    View Slide

  30. 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?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  35. mysql2
    Application DB
    adapter
    Mysql2Adapter Mysql2::Client

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  48. 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 無断転載・無断複製の禁止

    View Slide

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

    View Slide

  50. 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?

    View Slide

  51. 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?
    まずは接続要求の部分に注目してみる

    View Slide

  52. 接続要求
    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

    View Slide

  53. 接続要求
    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の情報)

    View Slide

  54. 接続要求
    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

    View Slide

  55. 接続要求
    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 の情報をもとに接続要求

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  59. 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?

    View Slide

  60. 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?
    次に接続作成の部分に注目してみる

    View Slide

  61. 接続作成
    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

    View Slide

  62. 接続作成
    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作成

    View Slide

  63. 接続作成
    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作成に繋がる)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  67. 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?

    View Slide

  68. 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?
    次に接続プールからの取り出しの部分に注目してみる

    View Slide

  69. 接続プールからの接続取得
    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

    View Slide

  70. 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
    接続プールから接続を取り出す
    または新規接続を作成する
    接続プールからの接続取得

    View Slide

  71. 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
    接続プールからの接続取得

    View Slide

  72. 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メソッドに繋がる)
    接続プールからの接続取得

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  76. 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?

    View Slide

  77. 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作成までのバックトレース(再掲)
    最後に接続プール作成の部分に注目してみる

    View Slide

  78. 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
    ・・・
       ・・・

    View Slide

  79. 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の作成

    View Slide

  80. 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

    View Slide

  81. 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

    View Slide

  82. 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

    View Slide

  83. 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
    ・・・
       ・・・

    View Slide

  84. 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の作成

    View Slide

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

    View Slide

  86. 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
    ・・・
       ・・・

    View Slide

  87. 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の作成

    View Slide

  88. 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

    View Slide

  89. 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

    View Slide

  90. 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

    View Slide

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

    View Slide

  92. 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

    View Slide

  93. 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

    View Slide

  94. 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

    View Slide

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

    View Slide

  96. 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
    ・・・
       ・・・

    View Slide

  97. 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の作成

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  102. まとめ
    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 無断転載・無断複製の禁止

    View Slide

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

    View Slide

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

    ● OKパケットかERR
    パケットの送信

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  112. ご清聴ありがとうございました
    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 無断転載・無断複製の禁止

    View Slide

  113. 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 無断転載・無断複製の禁止

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide