Rails Discourse 出品的 Active Record (半) 代替品

ksec · 2018年06月21日 · 最后由 darkbaby123 回复于 2018年06月24日 · 2807 次阅读

数星期前在 twitter, Discourse 大神 SamSaffron 和 Jeff Atwood 发牢骚对 Rails Performance 的不满。

结果出现了这个 Post.

https://samsaffron.com/archive/2018/06/01/an-analysis-of-memory-bloat-in-active-record-5-2

及后引发了

https://mobile.twitter.com/samsaffron/status/1002446941156884480 https://mobile.twitter.com/codinghorror/status/1002680669925924864

虽然引起很多人的不满,不过至少 Ruby / Rails 对 malloc 的问题又再正视了。( Discourse 现时正赞助 Eric Wong 解决很多 Ruby 速度的问题 )

然后 Jeff Atwood 就出现了个 idea, 加速甚至取替 Active Record. Mini_SQL 就是这个成果

https://github.com/discourse/mini_sql

暂时看来速度快得惊人,不过只支援 Pg.

 Comparison:
#  wide topic mini sql:                 7535.8 i/s
#  wide topic pg:                         7345.1 i/s - same-ish: difference falls within error
#  wide topic sequel:                   3449.4 i/s - 2.18x  slower
#  wide topic ar:                          2382.9 i/s - 3.16x  slower

#   sequel title id pluck:                    1181.0 i/s
#   mini_sql query_single title id:     1171.1 i/s - same-ish: difference falls within error
#   mini_sql select title id:                1105.6 i/s - 1.07x  slower
#   pg select title id:                         1002.2 i/s - 1.18x  slower
#   sequel title id select:                    916.8 i/s - 1.29x  slower
#   ar select title id pluck:                  696.2 i/s - 1.70x  slower
#   ar select title id:                            131.6 i/s - 8.98x  slower

不得不说,Sam Saffron 和 Mike Perham 频频对 Ruby 还有 Rails core team 就性能问题发难,而且有理有据,已经成功引起了重视。

另外我忘了在哪看到的他有提到 Discourse 会出钱赞助 AR 6 的重构,有钱事情应该就好办多了...

2 楼 已删除
jasl 回复

有錢事情應該就好辦多了

我反而觉得这个不是钱的问题,原因是根本没有人肯去处理。世界上最难解决的问题就是人们不觉得这是一个问题。至少 DHH 不觉得是。

ksec 回复

DHH 这种架构师角色的人,不会去关注那么细的工作,开源社区,谁行谁上

很多事情都需要社区推动的,比如 Shopify、Discourse 这种大厂去贡献,Basecamp 说白了还是体量太小,很多场景他们没需求自然也没有意识去做

jasl 回复

很多事情都需要社區推動的,比如 Shopify、Discourse 這種大廠去貢獻,Basecamp 說白了還是體量太小,很多場景他們沒需求自然也沒有意識去做

感动,😭 难得有人有相同想法。我经常在想怎么能够让 Shopify , Discourse, Cookpad, Github, AirBnB, Bloomberg, 每边找一两个人一同商讨。( 其实 Apple Music 也是 Ruby Rails, 不过相信要他们派人简直难过登天 )

ksec 回复

你看 5.2 的发行注记,已经提到了是 Shopify 引入的 Bootsnap 加速应用启动,还有 SamSaffron 和 Mike Perham 他们“逼宫”Ruby team 引入一些有利于常驻服务场景的优化,可能之后 Ruby 会引入单独的编译参数把这些优化开启,类似 Java Hotspot VM 分服务器版本和标准版本一样。

CRuby 发展不如 NodeJS 其实道理也很简单,就是没钱... Ruby Together 算是一个社区组织改善公共基础设施的,但是那些开发也要吃饭,要赞助到一定规模(按月出钱)才能考虑全职去做事情。NodeJS 依赖 V8,Chrome 保持话语权的关键组件,Google 投人投钱去改进 V8,Node 自然坐享其成

一个裸 sql 就能代替 ar?标题党啊

@hooopofind_by_sql 可以对付注入,就是不大好看,必须用某个具体的 model 来调用该方法。加上结果是该 model 的实例加上从 SELECT 部分动态生成的属性就非常奇怪了。这个问题可以通过建立一个专门的 stat model 来解决,避免尴尬。另外 sanitize_sql_xxx 的那批方法应该也可以解决注入问题(没试过),不过用起来就非常麻烦了。

虽然统计不是 ORM 需要解决的问题,一个是 OLAP 一个是 OLTP。但有时候两者都需要共用一些查询条件和表关联,而且有时候相似的统计 SQL 也会有些代码碎片是重复的,这时候就又回到如何重用的问题上来了。query builder 本身用在构造 SQL 上是合适的,问题是 AR 的 query builder 抽象程度还不够,经常写成了大批的 SQL 片段,也就是你说的 SQL + Rails 的方式。

darkbaby123 回复

Yes.

对于简单场景,用 ORM 来做 reporting 也是可行的。对复杂场景,不能说 AR 抽象程度不够,本质上每个 gem 有自己的解决的领域。

# Sam: complicated reports are really hard in AR

代码重复问题,其实这就是一个 tradeoff,ORM 里的方法对于 reporting 来说,理论上确实可以共用,但实现上是很难共用,硬要共用的话,必然是写成 N+1 的方式,之前做过一个项目,reporting 部分就是这种思路写的,数据量还不大的时候一个页面就可以达到几十秒。

如果 reporting 部分想共用的话,其实需要写 UDF 和 view,试过几次效果还不错。

刚看了这篇文章,mini sql 的用途写的非常清晰了:https://meta.discourse.org/t/new-official-direct-sql-access-api-for-discourse-mini-sql/90441

 # Sam: complicated reports are really hard in AR
 builder = DB.build <<-SQL
SELECT ftl.url,
       COALESCE(ft.title, ftl.title) AS title,
       ftl.link_topic_id,
       ftl.reflection,
       ftl.internal,
       ftl.domain,
       MIN(ftl.user_id) AS user_id,
       SUM(clicks) AS clicks
FROM topic_links AS ftl
LEFT JOIN topics AS ft ON ftl.link_topic_id = ft.id
LEFT JOIN categories AS c ON c.id = ft.category_id
/*where*/
GROUP BY ftl.url, ft.title, ftl.title, ftl.link_topic_id, ftl.reflection, ftl.internal, ftl.domain
ORDER BY clicks DESC, count(*) DESC
LIMIT 50
SQL

 builder.where('ftl.topic_id = :topic_id', topic_id: topic_id)
hooopo 回复

所以呢我说的其实是 query builder 的抽象能力。因为大部分 ORM 都有个 query builder 因此它也算是 ORM 的一部分。如果完全没有抽象的话对查询的共用部分就只能拼接 SQL 字符串了。mini_sql 的 builder.where 看起来已经够用了。我再举个 Ecto(Elixir 语言的 data wrapper)的例子,实现 Sam 文中提到的完整 SQL:

from(
  ftl in TopicLink,
  left_join: ft in assoc(ftl, :link_topic),
  left_join: c in assoc(ft, :category),
  group_by: [ftl.url, ft.title, ftl.title, ftl.link_topic_id, ftl.reflection, ftl.internal, ftl.domain],
  order_by: [desc: sum(ftl.clicks), desc: count(ftl.id)],
  limit: 50,
  select: %{
    url: ftl.url,
    title: coalesce(ft.title, ftl.title),
    link_topic_id: ftl.link_topic_id,
    reflection: ftl.reflection,
    internal: ftl.internal,
    domain: ftl.domain,
    user_id: min(ftl.user_id),
    clicks: sum(clicks)
  }
)
|> where([ftl, ...], ftl.topic_id == ^topic_id)
|> where([_, ft, ...], is_nil(ft.deleted_at))
|> where([ftl, ...], not (ilike(ftl.url, "%.png") or ilike(ftl.url, "%.jpg") or ilike(ftl.url, "%.gif")))
|> where([_, ft, ...], coalesce(ft.archetype, "regular") != ^Archetype.private_message)
|> secure_category(guardian.secure_category_ids)
|> Repo.all()

拿它举例并不是抬杠或者吹 Ecto 如何,我相信以 Ruby 的灵活性可以做到同样的事情(只是感觉没多少人觉得它很重要)。只是说明语言层面提供的抽象可以用来更好的组织和重用代码。这点我个人还是觉得很重要的。毕竟 SQL 作为声明式的语言不善于表达过程式的代码,而生成 query 的过程恰好是过程式的,应用层的语言擅长此道。

另外拿我最近做的一个统计做例子:

query_1 =
  FunnelItem
  |> filter_by_subject(subject_id)
  |> filter_by_type(:fam)
  |> score_btw(3, 4)

query_2 =
  FunnelItem
  |> filter_by_subject(subject_id)
  |> filter_by_type(:ove)
  |> score_btw(6, 7)

query_3 =
  FunnelItem
  |> filter_by_subject(subject_id)
  |> filter_by_type(:puc)
  |> score_eq(4)

[query_1, query_2, query_3]
|> join_queries()
|> stat_by_filters(filter_key, filter_value_ids)

上面的例子把部分 where 条件抽成了共用的函数,再自定义了一些函数来组合查询,比如 join_queries

def join_queries(queries) do
  Enum.reduce(queries, fn query, acc ->
    from(
      [f1, ...] in acc,
      join: ft in ^query,
      on: ft.sample_id == f1.sample_id and ft.subject_id == f1.subject_id
    )
  end)
end
hooopo Mini SQL 支持 MySQL 了 提及了此话题。 09月20日 11:58
需要 登录 后方可回复, 如果你还没有账号请 注册新账号