sixty

面向 Ruby 和 Rails 应用

没有第二步。

Rails 有一个有文档的启动钩子,还有一条本来就会广播每条查询的事件总线,所以这个 agent 既不需要 initializer 也不需要包装层:加上 gem,设两个环境变量,控制器就成了操作,它们的查询也归到了各自名下。下面这一切的发生,都不需要你写一行埋点代码。

它测量什么

每一个控制器 action
按 controller#action 命名,所以这条动态读起来就像你的路由文件。railtie 会在启动时把它们包住 —— 没有东西要 include,下一个控制器也没有东西要记着做。
ActiveRecord 发出的每一条查询
通过 sql.active_record 订阅,那本来就是 Rails 在发布消息的总线。行数、规范化后的语句、响应大小,以及是哪个 action 发出的。
三种数据库,各按各自的规矩
pg 带 EXPLAIN 出来的查询计划;mysql2 和 trilogy 按 MySQL 的引号规则读,而不是按 Postgres 的,因为一个双引号字符串在一边是字面量、在另一边是标识符;还有 Mongoid 用的 mongo,以命令的形状作为 identity。
数据库为了回答做了什么
会向 Postgres 索要每条语句的执行计划 —— 一次通用计划的 EXPLAIN,按语句缓存,并且发在调用方的 span 之外,所以它永远不会被算作调用方的工作;而且从不绑定参数,所以不涉及你的任何一个值。
你自己的类,只要你想
include Sixty::Instrumented 会把每一个公有实例方法变成一个操作,同时不碰私有方法和访问器。或者用 Sixty.instrument(Stripe::Charge, :create) 只针对别人类上的某一个方法。

它能找到什么

这里的每一项都会和上一个版本作比较,所以发现指向的是那次改动,而不是某一天。

一条不再走索引的查询

这里唯一一条点名原因而不是症状的发现,而且它常常在还没有任何东西变慢时就到了:表还小到全表扫描也无所谓,而几周之后,当它不再小时,这就成了一次故障。

上个版本还没有的 N+1

Rails 最常被指责的那个缺陷,靠次数而不是靠时钟抓到:每个 action 一条查询变成了二十条,每一条都很快,而 includes 是在三周前的一次重构里掉的。

一条开始读整张表的查询

每次调用的行数从 30 变成了 30,000 —— 一个 where 挪进了 Ruby 的 scope,它返回同样的答案,读起来也完全合理。

你自己的代码在变慢

方法自身里的时间,而不是它调用的东西里的时间,于是「是我的代码变慢了还是我调的东西变慢了」是一个数字,而不是一个下午。

一条悄悄什么都不返回的查询

成功了,没有抛异常,只是从原本有行变成了空。ActiveRecord 完全满意;空的是页面。

某个东西现在被放在循环里调用

每次调用没有变化,但被调用了几百次,而以前只调用一次 —— 也包括通过后台任务发生的,那会作为它自己的进程上报。

你的动态里会出现什么

不是一块需要你去读的仪表盘。每条发现一张卡片,上面有数字、有改变了它们的那个版本,还有你的编码助手写出修复所需要的证据。

这是 apps/demo-rails 故意带上的两个回归,对着 bin/rails db:prepare 灌入的数据 —— 大约 30,000 条订单。你可以跑起来,看着它们出现。

行数OrdersQuery#for_user
30 行30,000 行1000×

有人重构了这个 scope,where 挪进了 Ruby,于是每次调用都把整张表读进来、在内存里过滤。它返回同样的答案,而且在笔记本上只要几毫秒。

自版本 v2 起,对比 v1

N+1OrdersQuery#enrich
1 条查询20 条查询20×

一次批量查找变成了逐个订单查。每条查询单独看都很快、索引也都对,而这正是这类 bug 能挺过代码评审和 staging 的原因。

自版本 v2 起,对比 v1

如何安装

在 Rails 上,这就是全部安装:一个 gem,两个环境变量。railtie 会在启动时加上中间件、订阅查询总线并包住控制器的 action,所以没有 initializer 要写,也没有东西要调用。

claude mcp add sixty \
  -e SIXTY_API_KEY=sixty_sk_… \
  -e SIXTY_ENDPOINT=https://ingest.sixty.sh \
  -- npx -y @sixty-sh/mcp

# or by hand:
gem 'sixty'                      # Gemfile

export SIXTY_API_KEY=sixty_sk_…
export SIXTY_SERVICE=checkout-api

# On Rails, that is the install. Outside it:
require 'sixty'
Sixty.init
use Sixty::Instrument::Rack      # config.ru — Sinatra, Hanami, a bare app

# Your own classes, if you want them measured:
class OrdersQuery
  include Sixty::Instrumented
end

版本标识会自动从 Vercel、Render、Railway、Fly、Heroku 或 GitHub Actions 拿到,其他地方用 SIXTY_RELEASE 指定。第一次比较会在你下一次部署之后到来,因为一个版本没有可比的对象。

它不做什么

  • 查询计划只有 Postgres 有。MySQL 没有通用计划的 EXPLAIN,MongoDB 也没有可以解释的语句 —— 而要解释它们中的任何一个,都意味着保留某个人查询里的值,再用我们拼出来的命令把它送回他们自己的数据库,这件事这个 agent 不会做。
  • CPU 和等待没有分开。那需要一个按线程走的时钟,以及一个独占自己线程的 span,而只有 Python 的 agent 有这个条件。
  • 在带 worker 的 Puma 下,或者在 Sidekiq 下,每个进程各自上报,由收集端合并。这是对的,但当你在读一个按进程算的数字时,值得知道这一点。
  • include Sixty::Instrumented 测的是公有实例方法。一个真正干活的私有方法,在你用别的方式把它变成一个操作之前是看不见的 —— 这是「不去猜什么值得测量」所付的代价。

如果你是因为下面某一条来的

看看你的上一次改动做了什么。

试试看!

在做别的东西?可以看看 Node 服务、PHP 和 Laravel 应用、Python 服务 —— 那些页面不一样,我们能看到的东西也不一样。

面向 Rails 的性能监控 —— Postgres、MySQL 和 MongoDB — Sixty