ActiveRecord 可以自动映射数据库字段到对应类属性,是一个非常酷的特性,但是也带来一个问题:这个特性是否是免费的?
可以做一个简单的实验:
在你的 Rails 项目中添加一个 initalizer 用来输入所有执行过的数据库 query:
# config/initializers/print_sql.rb
ActiveSupport::Notifications.subscribe("sql.active_record") { |*args| puts args[-1][:sql] }
打开一个rails console
,查看一个 model 对象:
➜ rails_app git:(master) ✗ rails console
development main · > User
=> User (call 'User.connection' to establish a connection)
此时的 User model 并不知道自己有哪些属性。
运行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 的初始化工作。
每个数据库表相关的属性都会存到数据库 connection 中,这样下次再遇到 User 表就不用再次查询数据库,存这些 metadata 的地方就是schema cache
上述 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 的工作方式是每个 job 都 fork 一个进程出来,即每个 job 都要新初始化一个数据库连接,初始化表结构这些事都要重新做一遍,所以使用 schema cache dump 功能对 Resque 提升巨大。
当初始化一个数据库连接时,除了查询表结构的 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
参考