Rails ActiveRecord Schema cache dump

piecehealth · 2020年05月12日 · 2922 次阅读

ActiveRecord 可以自动映射数据库字段到对应类属性,是一个非常酷的特性,但是也带来一个问题:这个特性是否是免费的?

Schema Cache

可以做一个简单的实验:

  1. 在你的 Rails 项目中添加一个 initalizer 用来输入所有执行过的数据库 query:

    # config/initializers/print_sql.rb
    ActiveSupport::Notifications.subscribe("sql.active_record") { |*args| puts args[-1][:sql] }
    
  2. 打开一个rails console,查看一个 model 对象:

    ➜  rails_app git:(master) ✗ rails console
    development main · > User
    => User (call 'User.connection' to establish a connection)
    

    此时的 User model 并不知道自己有哪些属性。

  3. 运行User.connection或者任何用到数据库属性的操作:

    development main · > User.new
    SET client_min_messages TO 'warning'
    SET standard_conforming_strings = on
    SET SESSION statement_timeout TO '29s'
    SET SESSION timezone TO 'UTC'
            SELECT t.oid, t.typname
            FROM pg_type as t
            WHERE t.typname IN ('int2', 'int4', 'int8', 'oid', 'float4', 'float8', 'bool')
              SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
              FROM pg_type as t
              LEFT JOIN pg_range as r ON oid = rngtypid
              WHERE
                t.typname IN ('int2', 'int4', 'int8', 'oid', 'float4', 'float8', 'text', 'varchar', 'char', 'name', 'bpchar', 'bool', 'bit', 'varbit', 'timestamptz', 'date', 'money', 'bytea', 'point', 'hstore', 'json', 'jsonb', 'cidr', 'inet', 'uuid', 'xml', 'tsvector', 'macaddr', 'citext', 'ltree', 'line', 'lseg', 'box', 'path', 'polygon', 'circle', 'interval', 'time', 'timestamp', 'numeric')
                OR t.typtype IN ('r', 'e', 'd')
                OR t.typinput = 'array_in(cstring,oid,integer)'::regprocedure
                OR t.typelem != 0
    SHOW TIME ZONE
              SELECT a.attname, format_type(a.atttypid, a.atttypmod),
                     pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
                     c.collname, col_description(a.attrelid, a.attnum) AS comment
                FROM pg_attribute a
                LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
                LEFT JOIN pg_type t ON a.atttypid = t.oid
                LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation
               WHERE a.attrelid = '"users"'::regclass
                 AND a.attnum > 0 AND NOT a.attisdropped
               ORDER BY a.attnum
    SHOW max_identifier_length
    SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = ANY (current_schemas(false)) AND c.relkind IN ('r','v','m','p','f')
    SELECT a.attname
    FROM (
         SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx
           FROM pg_index
          WHERE indrelid = '"users"'::regclass
            AND indisprimary
       ) i
    JOIN pg_attribute a
    ON a.attrelid = i.indrelid
    AND a.attnum = i.indkey[i.idx]
    ORDER BY i.idx
    

    在这时,ActiveRecord 会查询数据库来获得 User model 的属性以及其他 metadata(以上是 postgreSQL 用到的 query,不同数据库略有不同),真正完成 User model 的初始化工作。

  4. 每个数据库表相关的属性都会存到数据库 connection 中,这样下次再遇到 User 表就不用再次查询数据库,存这些 metadata 的地方就是schema cache

Schema Cache Dump

上述 ActiveRecord 的动作方式,对程序员比较友好,可以少些很多 “模版代码”,但是对数据库并不友好:schema cache 是 connection pool 共享的,不同的 rails 进程是不会共享的。换句话说,当你的应用有很多 rails 进程的时候,读取每个表结构的 query 都要在每个 rails 进程中执行一次。

这些 query 是没法被优化、命中索引的,所以在你的应用重新部署后,基本是所有的 rails 进程都在并发执行不效率的 query。

解决这个问题的方法也很简单:在跑完rake db:migrate之后,把此刻数据库的schema cache导出一个文件,新的 rails 进程直接通过访问导出的文件来获取表结构等 metadata 即可。

导出文件的命令是rake db:schema:cache:dump(https://github.com/rails/rails/blob/v6.0.3/activerecord/lib/active_record/railties/databases.rake#L409-L418) 默认会导出到db/schema_cache.yml

读取schema_cache是 rails默认的行为,只要监测到db/schema_cache.yml文件就会尝试解析,如果db/schema_cache.yml中的 migration 版本跟当前数据库版本一致,则把 schema_cache 的内容加载到 connection pool 中,从而省去很多没有必要的 query。

在实践中跑rake db:migrate的机器跟真正启动 rails 进程的机器不是一台,把db/schema_cache.yml同步到跑 rails 进程的机器需要自己或者运维人员做点工作,我们的做法是把文件内容写到 memcache 中,rails 进程启动时再生成db/schema_cache.yml(读取 schema cache 是在 after_initialize 之后,所以在 initializer 里生成文件即可)

如果你正在使用 Resque

Resque 的工作方式是每个 job 都 fork 一个进程出来,即每个 job 都要新初始化一个数据库连接,初始化表结构这些事都要重新做一遍,所以使用 schema cache dump 功能对 Resque 提升巨大。

如果你正在使用 PostgreSQL

当初始化一个数据库连接时,除了查询表结构的 query,PostgreSQL 还会执行一系列 set 操作,如

SET client_min_messages TO 'warning'
SET standard_conforming_strings = on
SET SESSION statement_timeout TO '29s'

使用 pgbouncer 可以省去上面的查询 http://www.pgbouncer.org/ https://github.com/remind101/activerecord-pgbouncer

还有一些对 postgreSQL 特有/自定义类型字段的查询

SELECT t.oid, t.typname
FROM pg_type as t
WHERE t.typname IN ('int2', 'int4', 'int8', 'oid', 'float4', 'float8', 'bool')

SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
FROM pg_type as t
LEFT JOIN pg_range as r ON oid = rngtypid
WHERE
  t.typname IN ('int2', 'int4', 'int8', 'oid', 'float4', 'float8', 'text', 'varchar', 'char', 'name', 'bpchar', 'bool', 'bit', 'varbit', 'timestamptz', 'date', 'money', 'bytea', 'point', 'hstore', 'json', 'jsonb', 'cidr', 'inet', 'uuid', 'xml', 'tsvector', 'macaddr', 'citext', 'ltree', 'line', 'lseg', 'box', 'path', 'polygon', 'circle', 'interval', 'time', 'timestamp', 'numeric')
  OR t.typtype IN ('r', 'e', 'd')
  OR t.typinput = 'array_in(cstring,oid,integer)'::regprocedure
  OR t.typelem != 0

这类 query 对某些 Resque job 比较多应用也是一笔不小的开销 https://github.com/rails/rails/issues/35311

把这些 query 结果放到 schema_cache.yml 中也许是个可以接受的方案 https://github.com/rails/rails/pull/39077

参考

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