生产 Rails 排查修数据:kamal exec、全角问号、Base64、thread 连贯。
生产 400/500 和本地完全不同。本地报错你重跑测试、改代码、再跑;生产报错的时候,用户已经点了"付款"然后看着空白屏幕。
三条路:
本文讲第 3 条怎么做。两个真实案例:一个是全角 ? 塞进 credentials 导致 x402 支付 500,一个是 thread 中间一条推文 status: :failed 要重发还得保证 thread 连贯。让 Claude 做这些事时它默认会走错 4 个方向,每一个都得你拦下来。
kamal app exec --reuseKamal 是 37signals 出的 Rails 部署工具。它有一个命令:
kamal app exec --reuse 'bin/rails runner "..."'
--reuse 的意思是:不起新容器,直接在正在跑的那个 web 容器里执行。没有新 build、没有 docker pull、没有重启、没有 ENV 重新注入。执行完命令,容器继续原来的请求处理。
它的输出直接走 stdout 回到你终端,相当于你在生产机的 Rails console 里打了一行 puts——只是不用 SSH、不用 tmux、不用离开本地。
一个典型会话:
$ kamal app exec --reuse 'bin/rails runner "puts User.count"'
Launching command with version abc123 from existing container...
INFO [ok] Finished in 3.8 seconds with exit status 0
App Host: deploy.how2claude.com
12847
3-6 秒一次。比 deploy 快两个数量级。
? 引发的生产 500commit eba9ac9。
症状:上线 x402 支付几小时后,发现每次付款请求都 500。日志里是 HTTP client 的 Net::HTTPBadResponse。本地一切正常。
诊断:让 Claude 先在生产环境打印 x402 配置:
kamal app exec --reuse 'bin/rails runner "puts X402.configuration.wallet_address.inspect"'
输出:
"0xAbC123...def?"
尾巴多了一个 ?——全角问号(U+FF1F),不是半角 ?。某次编辑 config/credentials/production.yml.enc 的时候输入法切换过一次,塞了这个字符进去。
本地的 config/credentials.yml.enc(用 dev/test 那个 master.key 解的)没这个问题——production 和 dev 在 Rails 8 里是两套独立的 encrypted credentials,内容不共享。
修法:不能直接 ssh 进去改文件(加密的)、也不能拉下来本地改(master.key 不在本地)。正解是让 Claude 写一个一次性 ruby 脚本,通过 EDITOR= 注入到 credentials:edit:
# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# 去掉尾部全角问号
content.gsub!(/(wallet_address: 0x[0-9a-fA-F]+)?\s*$/, '\1')
File.write(ARGV[0], content)
EDITOR="ruby script/fix_prod_wallet.rb" \
bin/rails credentials:edit --environment production
credentials:edit 流程:解密 → 写到临时文件 → 调 $EDITOR 编辑 → 重新加密 → 删临时文件。把 EDITOR 换成我们的 ruby 脚本,就能自动化这一步,不需要在本地肉眼看密文。
改完 git commit + kamal deploy 一次——这次 deploy 是必须的,因为 production.yml.enc 变了。但诊断那一步没花 deploy。
规律:生产出问题先用 kamal app exec --reuse 读,不要先猜、更不要先 deploy。
症状:文章发布后推文排期进 x_queue_tweets 表。有一条因为 X API 限流或内容校验失败变成 status: :failed,但它是 thread 中间第 2 条,需要重发并保证接在第 1 条后面。
查失败的那条:
kamal app exec --reuse 'bin/rails runner "
XQueue::Tweet.where(status: :failed).each do |t|
puts \"#{t.id}: thread=#{t.thread_id} pos=#{t.thread_position} content=#{t.content.inspect}\"
end
"'
找到是 id=87,thread_id=15,thread_position=2。
坑 1:shell 转义。推文内容常常有引号、换行、backtick。如果你直接写:
# 会炸,引号和反斜杠被 shell 啃掉
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'
Base64 dance——本地把新内容编码,传 base64 串,在 runner 里解:
# 本地生成
echo -n '重写后的推文内容...' | base64
# => 6YeN5YaZ5ZCO55qE5o6o5paH5YaF5a655Li7...
# 传进去
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
t.update!(content: Base64.decode64('6YeN5YaZ5ZCO55qE5o6o5paH5YaF5a655Li7...'), status: :scheduled)
puts t.status
\""
base64 串是 ASCII-only,shell 安全。
坑 2:thread 连续性。XQueue::PostTweetJob.perform_later(87) 会发一条独立推文,不会接在 thread 第 1 条后面——因为 X API 需要 reply_to_tweet_id 参数,而 Job 默认不知道。
找前一条的 x_tweet_id(已发成功的会填这个字段):
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
prev = XQueue::Tweet.where(thread_id: t.thread_id, thread_position: t.thread_position - 1).first
puts 'prev x_tweet_id: ' + prev&.x_tweet_id.to_s
\""
# => prev x_tweet_id: 1834567890123456789
入队时带上:
kamal app exec --reuse 'bin/rails runner "
XQueue::PostTweetJob.perform_later(87, reply_to_tweet_id: \"1834567890123456789\")
puts \"enqueued\"
"'
worker 的 polling_interval 是 0.1 秒,入队后几乎立即执行。几秒后再 kamal app exec 一次查 status 从 scheduled 变 posted、x_tweet_id 被填上——说明 thread 续上了。
规律:生产数据操作要考虑数据层的语义约束,不只是"记录更新成功"。thread 连续性是业务约束,Rails runner 不会替你检查。
做这类工作时 Claude 的第一反应经常错。拦下来:
"我 ssh 到服务器上看一下..."
拦下来:kamal app exec --reuse 比 SSH 好——在容器内、有 Rails 环境、命令有审计(kamal 日志保留)、不碰主机 shell、不用担心容器漂移(reuse 保证是当前生产版本)。
"我写个 migration 把 wallet_address 里的全角 ? 替换掉..."
拦下来:改一条 credentials 不需要 migration(数据库都没碰)。一次性 Rails runner 10 秒搞定,migration 要 deploy + 留在 schema 里永久。只有你要改的事未来可能再来一次才用 migration——这次是打字错误,没有"未来"。
"我直接
update!(content: "...")就行..."
拦下来:任何用户生成内容(推文、评论、用户输入的 markdown)都走 Base64 中转。shell 对引号、反斜杠、$、backtick 的解析是经典地雷——在生产上拿来练手成本太高。
perform_later 不带业务参数"我重新
PostTweetJob.perform_later(87)就行..."
拦下来:先问一句"这条和别的条有没有关系"。thread 有 reply_to 关系、批量任务有 batch_id 关系、带分页的任务有 cursor 关系——Job 的参数列表就是业务关系的载体,少一个就断一个链。
让 Claude 在生产 Rails 上排查 + 修数据的 6 条:
kamal app exec --reuse 'bin/rails runner "puts X"',先把问题定位清楚再动。kamal app exec --reuse 是默认手段,不是 SSH、不是 deploy。容器内跑、有 Rails 环境、3-6 秒响应。EDITOR=ruby-script bin/rails credentials:edit --environment production。Ruby 脚本自动改,不在本地肉眼看密文。echo -n 'X' | base64 本地生成,Rails runner 里 Base64.decode64。生产排查的本质不是把开发流程搬到生产上——你既没有迭代空间、也没有错误容忍。真正要用的是生产本身提供的内省面(Rails runner + kamal exec + credentials:edit),每一步只做该做的最小改动。Claude 能写对 Ruby 代码,但"在生产上哪些事可以直接做、哪些必须先诊断"这个判断它不会——那是你的生产纪律。