Rails Puma 的线程数量与数据库连接池的关系

breeze · 2018年03月07日 · 最后由 jetspeed 回复于 2020年06月20日 · 7934 次阅读
本帖已被管理员设置为精华贴

puma 是 rails5 的标配可是你知道 puma 的线程数与数据库连接池的关系吗?

当我们配置 rails 的时候应注意 puma 的线程数与数据库连接池数一致。

否则会出来一些奇怪的现象:

例如在并发大,但是在内存,cpu,mysql 压力不大的情况下,ruby 耗时特别的长。使用 Newrelic 与 ab 工具 尝试了解项目的性能

例如得到一个错误:ActiveRecord::ConnectionTimeoutError (could not obtain a connection from the pool within 5.000 seconds (waited 5.002 seconds); all pooled connections were in use)

下面我们通过 connection_pool 的源码来了解一下

这里有着一个缓存,缓存着一个 conn, 这个 conn,在此次的请求中可以被使用任意次数。缓存不存在时,将从连接池中获取相应的 conn

#/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#363
def connection
  @thread_cached_conns[connection_cache_key(Thread.current)] ||= checkout
end

#/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#389

# If all connections are leased and the pool is at capacity (meaning the
# number of currently leased connections is greater than or equal to 
# size limit set), an ActiveRecord::ConnectionTimeoutError exception will be raised.
def checkout(checkout_timeout = @checkout_timeout)
  checkout_and_verify(acquire_connection(checkout_timeout))
end

获取 conn 的方式有以下三种,第一种是直接返回可用的 conn, 我们现在先关注第二种,尝试创建 conn,并控制着进程中所有的线程创建的连接数。

#/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#706

# Acquire a connection by one of 1) immediately removing one
# from the queue of available connections, 2) creating a new
# connection if the pool is not at capacity, 3) waiting on the
# queue for a connection to become available.
def acquire_connection(checkout_timeout)
  if conn = @available.poll || try_to_checkout_new_connection
    conn
  else
    reap
    @available.poll(checkout_timeout)
  end
end

#/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#739

# If the pool is not at a +@size+ limit, establish new connection. Connecting
# to the DB is done outside main synchronized section.
def try_to_checkout_new_connection
  # first in synchronized section check if establishing new conns is allowed
  # and increment @now_connecting, to prevent overstepping this pool's @size
  # constraint
  do_checkout = synchronize do
    if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @size
      @now_connecting += 1
    end
  end
  if do_checkout
    begin
      conn = checkout_new_connection
    ensure
      synchronize do
        if conn
          adopt_connection(conn)
          # returned conn needs to be already leased
          conn.lease
        end
        @now_connecting -= 1
      end
    end
  end
end

当没有可用的 conns,并且也已经达到创建 conn 的上限时,该怎么办呢?这时就会等待使用中的 conns 的释放,等待超时了就会报错

#/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#147
def poll(timeout = nil)
  synchronize { internal_poll(timeout) }
end

def internal_poll(timeout)
  no_wait_poll || (timeout && wait_poll(timeout))
end

def wait_poll(timeout)
  @num_waiting += 1

  t0 = Time.now
  elapsed = 0
  loop do
    @cond.wait(timeout - elapsed)

    return remove if any?

    elapsed = Time.now - t0
    if elapsed >= timeout
      msg = 'could not obtain a connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use' %
        [timeout, elapsed]
      raise ConnectionTimeoutError, msg
    end
  end
ensure
  @num_waiting -= 1
end
end

总结

  1. 当使用多线程的 rails 服务器时,应当注意:连接池的数量与线程数量相同。
  2. 当连接池数小于线程数并且高负载时,某些线程会等待 conn 的释放,而影响访问速度与性能。具体情况可以查看该帖子:使用 Newrelic 与 ab 工具 尝试了解项目的性能
  3. 当偶尔发生 ActiveRecord::ConnectionTimeoutError 错误时,请检查连接池数
  4. puma 配置中默认线程是 0-16,数据库连接池默认是 5。可能大多人在并发高的时候都需要增加连接池。
breeze 服务器负载过高怎样优化? 提及了此话题。 03月07日 23:33
huacnlee 将本帖设为了精华贴。 03月08日 00:25
gihnius [该话题已被删除] 提及了此话题。 03月08日 08:54

... 标题里面有个看不见的字符 <BS> 结果导致编辑的时候,浏览器 Input 组件 Bug 了,看不到 input 输入的内容

huacnlee 回复

我现在可以修改标题😂

使用 sidekiq 的时候也要注意,sidekiq 的并发数一定要小于或等于连接池数

一般是会计算 puma 和 sidekiq 额外的需要的连接数,然后也会考虑数据库本身配置的最大连接数,给数据库留一些余地,因为免不了要自己冲上去 rails console 什么的

hegwin 回复

配置的连接池是指定一个进程里有多少连接池。rails c 那是额外打开一个进程。

其实,这个连接池只是 IO 用的,执行任务(到 rack 层交给 Rails 去 Route 去 Controller.method 是另一边),提升连接池超过 16 之后并不能显著提升并发,反而很多线程空余,虽然不阻塞。然而 Ruby 的线程并不 Parallel,开多了抢占 管道在 IO uds(好像以前是用 pipe)的另一头获取信号的时候卡 shi 你。

补充: puma 的连接池是轮询的,发现 readable 的 连接 就会再次丢进去 任务队列,通知让其中一个线程抢占到之后执行。其实终归比起条件变量,性能还是差一些。

然后再加上有 GIL,我也估计是因为 GIL puma 才选择 管道式通知的,因为走条件变量实现方式的路,不如 C 的高效,说了 Ruby 多线程不并行。

然后很早以前很多人对 Fiber 有误解,以为 Fiber 能取代 Thread 啦,其实 Fiber 并不是解决并行问题的,而是解决异步同步的,跟并发关系大一些,跟 Goroutine Generator(生成器)是差不多的。

Thread 本来应该并行的,但是不代表 Python Ruby 给它实现并行

breeze 回复

嗯…我意思是数据库比如 pg 本身自己也是有最大连接数的;不超过这个数值,也是需要考虑的因素之一

hegwin 回复

嗯,明白你的意思了。之前是我理解错了。😄

请教一下,puma 的配置文件里面还有个 worker,如果 worker 设置成 2,threads 设置成 2 16,那么 database.yml 里面的配置连接池是不是需要设置成 2x16(最大值)=32(最大值),另外设置 worker 为 2 和 threads 为 2 有什么区别,一般怎么设置的,根据 cpu 内核数么。

xiaoxiao 回复

设置的连接池数是指一个进程中最大拥有的数量。所以你的 hreads 设置成 2 16,连接池最大设置成 16 就行了。

woker 是根据你的 cpu 核数设置的。threads 数可以根据你的系统情况设置 (如果平均请求速度比较快,threads 可以设高一点,否则反之)。

设置 worker 为 2 和 threads 为 2,你这是问进程和线程的区别吗?

breeze 回复

下面有几个概念,你指的核数应该第二个值把,cpu core

查看物理 CPU 个数

[root@AAA ~]# cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l

查看每个物理 CPU 中 core 的个数 (即核数)

[root@AAA ~]# cat /proc/cpuinfo| grep "cpu cores"| uniq

查看逻辑 CPU 的个数

[root@AAA ~]# cat /proc/cpuinfo| grep "processor"| wc -l

xiaoxiao 回复

指你回复中的 查看逻辑 CPU 的个数

总逻辑 CPU 数 = 物理 CPU 个数 × 每颗物理 CPU 的核数 × 超线程数

如果你的服务器中,还有其它的耗费资源多的服务 (例如:mysql)。你可以根据实际情况,减少 woker 数量。

bighuzi Rails 并发问题 提及了此话题。 07月17日 09:42

当得到那个数据库连接占满的错误时,怎么知道是哪些线程哪些语句占用的呢?

需要 登录 后方可回复, 如果你还没有账号请 注册新账号