Ruby on Rails ActiveRecord 的连接池

MyDataFlow · 发布于 2017年10月08日 · 496 次阅读
96

基础知识

文件描述符共享

在Unix的世界中,文件描述符在fork之后会完全的复制到子进程中的,并且是可以正常使用的。子进程默认会继承父进程打开的文件描述符,并在发生写操作的时候进行copy-on-write,如果父进程退出后让出了socket,子进程中的那一份就有可能一直持有不释放。

同时在Linux上,可以使用 sendmsgSCM_RIGHTS 来发送句柄给令一个进程,所以Nginx这种多进程的服务器可以流畅的工作。BSD后来推出了一个黑 科技一般的Socket Option: SO_REUSEPORT,有了这个选项,我们可以让任意进程( linux下限制必须是同一用户的进程)同时bind相同的source address和port而不报错! 切记不要用成SO_REUSEADDR,这两个Option目的不同。Linux在3.9版本以后正式支持了SO_REUSEPORT,并且提供了进程间负载均衡的隐形福利。

线程TLS

在一个线程修改的内存内容,对所有线程都生效。这是一个优点也是一个缺点。说它是优点,线程的数据交换变得非常快捷。说它是缺点,一个线程死掉了,其它线程也性命不保; 多个线程访问共享数据,需要昂贵的同步开销,也容易造成同步相关的BUG。

如果需要在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量(被称为static memory local to a thread 线程局部静态变量),就需要新的机制来实现。这就是TLS(Thread Local Storage)。

它主要是为了避免多个线程同时访存同一全局变量或者静态变量时所导致的冲突,尤其是多个线程同时需要修改这一变量时。为了解决这个问题,我们可以通过TLS机制,为每一个使用该全局变量的线程都提供一个变量值的副本,每一个线程均可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。而从全局变量的角度上来看,就好像一个全局变量被克隆成了多份副本,而每一份副本都可以被一个线程独立地改变。

ActiveRecord

ActiveRecord是什么

ActiveRecord 是 Rails 的 ORM 组件,负责与数据库沟通,让我们可以用面向对象的语法操作数据库。我们都知道ORM (Object-relationalmapping ) 是一种对映设关系型数据与对象数据的程序技术。面向对象和从数学理论发展出来的关系数据库,有着显著的区别,而 ORM 正是解决这个不匹配问题所产生的工具。它可以让你使用面向对象语法来操作关系数据库,非常容易使用、撰码十分有效率,不需要撰写繁琐的SQL语法,同时也增加了程序代码维护性。

简单的说,ActiveRecord就是一个让我们快速对数据库CRUD的超级工具集,因为要操作数据库,就必然要建立数据库连接和连接池。

ActiveRecord中的TLS使用

# file: active_record/runtime_registry.rb

  class RuntimeRegistry # :nodoc:
    extend ActiveSupport::PerThreadRegistry

    attr_accessor :connection_handler, :sql_runtime

    [:connection_handler, :sql_runtime].each do |val|
      class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__
      class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__
    end
 end

从这里可以清晰的看到,ActiveRecord通过使用ActiveSupport::PerThreadRegistry将一个RuntimeRegistry的对象和线程绑定,每次通过类方法去访问connection_handler和sql_runtime的时候总是取出当前线程绑定的RuntimeRegistry对象

establish_connection

# file: active_record/connection_adapters/abstract/connection_pool.rb

      def establish_connection(config)
        resolver = ConnectionSpecification::Resolver.new(Base.configurations)
        spec = resolver.spec(config)

        remove_connection(spec.name)
        ## instrument intercept
        message_bus = ActiveSupport::Notifications.instrumenter
        payload = {
          connection_id: object_id
        }
        if spec
          payload[:spec_name] = spec.name
          payload[:config] = spec.config
        end

        message_bus.instrument("!connection.active_record", payload) do
          ## map spacename in config to connection pool of database
          owner_to_pool[spec.name] = ConnectionAdapters::ConnectionPool.new(spec)
        end

        owner_to_pool[spec.name]
      end

其中比较重要的一个类就是ConnectionHandler,`ConnectionHandler就是我们前面提到RuntimeRegistry中的connection_handler,它在抽象的层面上负责连接池的建立。从establish_connection的代码我们可以看出,ConnectionHandler`使用的是一个map来保存连接池,并且在每次建立连接池之前,都会将同名的连接池清理掉。

retrieve_connection_pool

# file: active_record/connection_adapters/abstract/connection_pool.rb
  def retrieve_connection_pool(spec_name)
        ## if fetc spacename successfully
        ## the block won't execute
        owner_to_pool.fetch(spec_name) do
          # Check if a connection was previously established in an ancestor process,
          # which may have been forked.
          ## get connection pool from map
          if ancestor_pool = pool_from_any_process_for(spec_name)
            # A connection was established in an ancestor process that must have
            # subsequently forked. We can't reuse the connection, but we can copy
            # the specification and establish a new connection with it.
            establish_connection(ancestor_pool.spec.to_hash).tap do |pool|
              pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache
            end
          else
            ## spacename don't exists in any process
            owner_to_pool[spec_name] = nil
          end
        end
      end

当我们的model想操作数据库的时候,都是通过retrieve_connection函数来获取数据库连接的,而retrieve_connection函数的核心是retrieve_connection_pool。它会从@owner_to_pool这个实例变量中,找到当前进程的中的连接池。其中重点的一个地方是,如果当前进程是子进程,它并不会重用父进程的连接池,而是从父进程中取出连接池的配置,然后重新建立一个新的连接池。
@owner_to_pool这个实例变量,是一个双重Map,第一重映射的是进程ID到一个Map,第二重是将配置文件中的配置名映射到连接池上。

暂无回复。
需要 登录/注册 后方可回复, 如果你还没有账号请点击这里 注册