Ruby 2024 年了,我还在用薄荷 18 年分享的 10 条最差实践建功立业😂

sidekiq · 2024年12月06日 · 最后由 wedxt 回复于 2024年12月13日 · 1212 次阅读
本帖已被管理员设置为精华贴

引言(喝酒后的碎碎念,可以跳过,直接从前提看起)

  • 离开薄荷 3 年了,从 18 年初进入薄荷到 21 年 9 月离开,在薄荷将近 4 年的时间,我从团队拿工资最低的技工程师,历经火箭升迁,压力成长,成为独当一面的部门主管,在项目管理(需求评审、架构设计、人员分配、跨端配合、安全审计)、团队管理(招聘淘汰、成员成长、梯队建设、制度文化)、发展创新(新技术调研落地、项目可行性分析、立项决策)上,都留下了不少的痕迹;
  • 后面加入大型外企,半退休远程了,我才发现自己不适合做远程,我更喜欢融入一群人中默默贡献,比起老家深山中的幽静、智者乐水更适合我,况且在薄荷做 leader 后养成了一些权限依赖习惯,我在束手束脚的审批、infra、响应时间上难以适应,最后新工作两年不到就草草收场了;
  • 个人的兴趣点一直在 iot 上,刚过去的 11 月底我终于鼓起勇气到了深圳,加入了一家高速发展的中型软硬件公司(1000 人+),CTO 如是对我说:“我希望你不要做体力活,做最能发挥你能力的事情就行了”,开放了几条业务线的代码,我便开始工作,用 8 年坚持每天刷 github trending(过年除外)的技术鼻子开始嗅嗅代码;
  • 在迅速提交了 4 次重大解决方案后,我不到两周便迅速站稳了脚跟,得到了同事和创始人的信任。我才发现自己原来还在吃在薄荷的老本,于是打算把这些大家习以为常的东西整理一下再讲一遍,温故知新;
  • 其实我没完全记住薄荷 10 条,因为几个太基础,我就忘记了,但鲁迅说过,中国人就喜欢凑个十数(真的,我基本上全文阅读过鲁迅全集,出自《再论雷峰塔的倒掉》),还是凑一些需要作为后端常识来认知的东西;
  • 我现在写 go,ruby 和 rails 留下的最佳实践在其他语言的学习者中鲜有了解和实践,所以给了我降维打击的机会。

前提

  • 互联网企业,尤其是面向终端用户和合作商的企业,安全是第一考虑要素;
  • 软件工程师的上限是由努力和天赋决定的,但底线是由公司基因、研发流程和文化决定的;
  • 很少有软件工程师注意到安全的话题,大多是在繁重的需求压力下完成开发,安全无小事,出事就是大问题。

正题索引

  1. 【数据库】不要在任何区分度低的字段上加索引(如 status、各种 enum)
  2. 【数据库】发布前一定要检查 migration,大表手动处理
  3. 【数据库】在 rails 中,transaction 开始的地方在第一个查询的地方,需要优化的话,将 before_action 挪后
  4. 【缓存】不要滥用 Rails.cache 方法,超过 1k byte 的 redis key,一定要做二级缓存
  5. 【业务】不要信任所有的前端传入 id,必须要做归属校验
  6. 【异步任务】尽可能实现幂等,至少本地数据库一定要实现幂等
  7. 【代码】任何 secret 不能硬编码到代码,小到短信 template_id,大到 jwt_secret,优先放环境变量,其次放数据库
  8. 【代码】开发前,先建立分支,写设计,review 后再编码
  9. 【常识】ruby 并不慢,大多数性能问题都是数据库、外部依赖的问题(先查 n+1,做好监控)
  10. 【常识】内存飙升问题

【数据库】不要在任何区分度低的字段上加索引(如 status、各种 enum)

大多数情况下(如果你清楚你的区分度,并且完全理解),不管 MySQL 还是 PG 都适用,在 status 上加索引只有坏处

  • 即使命中,也大概率是半表扫描;
  • 如果 status 和 id 等业务参数一起查,有概率会走 status 索引而不走业务参数的索引,需要知道详情请将这句问 GPT。 这个不多解释,GPT 能告诉怎么做才是最优。

【数据库】发布前一定要检查 migration,大表手动处理

各种数据记录表,如用户登陆记录等,如果需要增加字段,一定要清楚数据量,我们一般认为(有科学依据,以 B+ 树深度未依据)百万级、千万级、亿级数据锁表时间是有明显差距,百万级的表想要不停机发布,就需要上工具了,比如 percona-toolkit 等,先增加字段后,再手动添加 migration 记录

【数据库】在 rails 中,transaction 开始的地方在第一个查询的地方,需要优化的话,将 before_action 挪后

一个请求开始后,rails 会在第一次进行数据查询的地方,开启 transaction,待请求结束后,有框架方法执行兜底的 commit,如果请求中有网络操作,而你正好用的是 pg,那在并发上来后,会有长事务问题,最好先把网络请求进行完,确保 transaction 的过程时间短且可控。根据业务自行取舍,因为 before_action 挪后意味着 current_user 的查询也会要挪后。

【缓存】不要滥用 Rails.cache 方法,超过 1k byte 的 redis key,一定要做二级缓存

如果不是经常更新的内容,比如首页的 Banner 和广告这类首页接口,并发量最大,而且更新少,就太适合全部放到 Rails 应用中进行缓存,而不是每次都消耗内网带宽和延时从 redis 中拿,能将接口返回速度提升到微秒级别。

【业务】不要信任所有的前端传入 id,必须要做归属校验

千万不要写 Order.find(params[:id])这样的代码,只要知道 order_id 任何人都可以访问这个资源,current_user.orders.find(params[:id]) 是最佳方式。

【异步任务】尽可能实现幂等,至少本地数据库一定要实现幂等

幂等大家都知道,不过多解释,为什么说尽可能,因为可能有外部网络操作。网络操作一定要放到最后进行,而且不能太慢返回,在 web api 的 k8s 的 preStop 生命周期函数中传入类似 sleep 10,给 sidekiq 10s 做完退出大多数情况也能搞定。

【代码】任何 secret 不能硬编码到代码,小到短信 template_id,大到 jwt_secret,优先放环境变量,其次放数据库

安全无小事,希望大家所在的公司一辈子都不要被公众视野注意到(上市除外,打新记得叫我🤣)

【代码】开发前,先建立分支,写设计,review 后再编码

改天另写一篇,有完整且成熟、适合中国宝宝的流程推荐,如果你已经在这种流程了,那说一声恭喜,业务开发也可以很有趣的。

【常识】ruby 并不慢,大多数性能问题都是数据库、外部依赖的问题

首先完善监控,uptrace 这些社区工具都蛮好用的,买国内的 grafana 包装服务观测云这些也不错,至少能掌控系统整体运行状况,如果自研,一定要符合 open-telemetry 规范,不然轮子得很多。

【常识】内存飙升问题

一般是有外部服务,比如 qrcode、分享等图片生成、大 excel、网页生成 pdf 等,一方面我们需要在 dockerfile、服务器安装一大堆依赖,另外还会再占用一堆内存,优化方法也很简单,用 golang 或者就用 ruby 写一个服务,打包放到 serverless 里面去,按量计费,既不占用宝贵的 connection pool 资源,也能移除这种低频次高消耗接口对业务系统带来的损耗。

致谢

感谢薄荷健康的联合创始人、CTO @Vincent 4 年来的谆谆教诲和言传身教,作为华东 Ruby 黄埔军校毕业的一员,感谢你给一个热爱技术的年轻人的机会。

讨论

今天临时起意写这篇文章,有感于“回流”团队 CTO 智恒在公众号的技术和管理分享,觉得作为老 RubyConf 讲师,今年没能参加会议,有必要将这些年的沉淀分享下,线下不能线上总行嘛(我现在 996😂)。喝了 3 瓶乌苏,就开始敲键盘了,标点符号、排版、错别字等问题一堆,大家能就好啦🙏,有任何问题欢迎讨论,如果不便公开分享也请加我的 w:plugine

非常实用的经验总结,已经分享给自己团队。

而且更新少,就太适合全部放到 Rails 应用中进行缓存,而不是每次都消耗内网带宽和延时从 redis 中拿

这是指放进程的内存缓存吗?

我才发现自己不适合做远程,我更喜欢融入一群人中默默贡献,比起老家深山中的幽静、智者乐水更适合我,况且在薄荷做 leader 后养成了一些权限依赖习惯,我在束手束脚的审批、infra、响应时间上难以适应,最后新工作两年不到就草草收场了

痛点上有同感,不过我目前还都能忍受。。。

一看就有很多实战经验。

【代码】任何 secret 不能硬编码到代码,小到短信 template_id,大到 jwt_secret,优先放环境变量,其次放数据库

现在的 rails 自带 credentials 管理。大部分 secret 都能放里面,然后用 Rails.application.credentials 获取。

Rei 将本帖设为了精华贴。 12月06日 14:20

总结的很全面,索引确实是不能乱加的,之前做商城的时候,有过很大的并发,当时前辈用 redis 去处理的,方式不是很好,后面离职了也知道后面改没改

martin91 回复
$local_cache = ActiveSupport::Cache::MemoryStore.new

可以划分一块内存,来直接处理

之后默认 cache 是走 redis

但是注意哈,这个内存的缓存 只在 一个 puma 进程种共用

“数据库】发布前一定要检查 migration,大表手动处理”

可以试试 这个 gem https://github.com/ankane/strong_migrations

千万不要写 Order.find(params[:id]) 这样的代码,只要知道 order_id 任何人都可以访问这个资源,current_user.orders.find(params[:id]) 是最佳方式。

为了防止 Junior Developer 忘记加 scope 导致安全问题,我们现在数据库表的主键 id 都开始用 uuid 或者 type-id 了。这样即使有人犯错误,写了有安全隐患的代码,也不至于被黑客全表扫描数据。

备注:我们现在用的是 PG,没有 cluster index,所以使用 uuid 做主键不会导致太大性能问题。

学习到了

学到了,看完就解决一个索引问题😀

Quote: “如果不是经常更新的内容,比如首页的 Banner 和广告这类首页接口,并发量最大,而且更新少”

常更新的也没有问题,设置好过期时间就行了,一小时更新一次,一天也就 24 次,虽然每个 puma 进程都管理自己的 cache,比如说有 8 个 puma 进程,同一个缓存就要重复保存 8 次,那一天也只更新 24*8 = 192 次,但现在内存这么便宜,数据库又放在 ssd 上,不用太在乎。

Rails 官方的文档不建议在生产环境中使用 MemoryStore 是因为多进程或多服务器之间不能共享缓存,另外就是上面说的会浪费内存。理解了原因就可以打破规则了。

我经手的一个生产环境的 k8s 里有十几台 64GB 的 rails 服务器,直接配置的 MemoryStore 作为 Cache, 再用另外的全局变量 redis_client 连接 Redis cluster 用来处理经常变动 (1 小时以下,一般是分钟以下,秒级别) 的缓存,目前运行得非常稳。

对于那种又大又不定期更新的缓存,还希望能在各进程和服务器间同步更新,并且不希望把这么大的缓存存在 redis,也有办法:

  1. 例如把 Category.all 保存在 MemoryStore 的 :categories_cache 中,永不过期,另外再把缓存生成的时间保存在 :categories_lazy_updated_at 中,永不过期。
  2. 在更新 model 的同时,用 after_save 钩子在 Redis 中更新一个对应的键 categories_redis_updated_at,值就是当下的 timestamp,在各个服务器每个进程中的 MemoryStore 缓存这个 Redis 的键值对,键名 :categories_eager_updated_at,过期时间一分钟或你期望的间隔。

  3. 每次读取 :categories_cache 时都比较 :categories_lazy_updated_at:categories_eager_updated_at 的值,这个过程都在本地,时间忽略不计,如果前者小于后者,说明 Category 已经更新了,那重新生成缓存 :category_cache,再把 :categories_lazy_updated_at 的值更新为 categories_eager_updated_at 的值,其实也是categories_redis_updated_at 的值。

这样在 Redis 中只用了一个 timestamp 就在极小的间隔同步了各个进程和各个服务器之间的缓存更新。如果有多个这样的缓存,还可以优化,比如把多个缓存的 timestamps 组成一个 hash 放在 Redis 的一个键中。我最后还做了一个页面,所有的缓存都一目了然,包括值,占用空间大小,过期时间,当然页面中的本地缓存只是某一台服务器中某一个 puma 进程中的值 😅

Rails 的 MemoryStore 效率奇高,因为以前内存贵 HDD 慢,一直在业界没好好利用,我看完源码后觉得现在不在生产环境用真是太浪费了,特别是那些大块头又常年不更新的缓存,现在它们极大概率可以在 garbage collection 中 survive。

Peter 回复

其实放到内存里 也有这么一个原因 就是 内存比网络快

读 redis 大概率是要跨网络的,会牺牲一定的性能,特别是你遍历数组的时候,每个 item 都要去缓存去拿一个很无聊的数据的时候,网络 io 就是需要考虑的问题了,包括内网

当然 其实绝大多数程序来说 没啥本质的区别 同时 用 redis 还有额外的收益 原子性

非常有用,非常可用于实践中的经验,已分享给团队。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号