——以及一首献给 ORM 的怨曲
作者:Andrei Taranchenko
原文链接:Your Database Skills Are Not ‘Good to Have’
一段 MySQL 战斗史
那是 2006 年,《纽约杂志》(New York Magazine)的数字团队要为旗下的时装周门户网站打造一个全新的搜索体验。
这是那种“根本没跟技术团队确认可行性”的项目——在当时非常常见。那时敏捷开发还只是个新名词,更别提出版业了。整个项目从头到尾只有一个疯狂的目标——在 10 到 12 周内做出一个可运行的线框原型。且几乎没有留给质量保障的缓冲时间。时装周的启动节奏不是慢慢热身,而是一瞬间从 0 加速到到 60。
目标是什么?要能展示数千张几乎实时上传的时装秀图片,每张图片都带有分类标签,比如 “2006”“包袋”“红色”“皮革” 等等。用户进入搜索页后,可以逐步筛选结果,并根据这些属性缩小范围。更困难的是——每个属性还得显示准确的计数。
整个工作流非常紧凑:摄影师从纽约市中心把存储卡快递到我们在麦迪逊大道的办公室;图像被处理、实习生打上标签,然后由我们的 Perl 脚本每小时索引一次,从 EXIF 信息中读取这些标签。如果我们没能按时上线,这个整个已经准备好的链条都会断裂。
《纽约杂志》的时装周可是大事,就像游戏行业的圣诞节,是那种死线不可动摇、全员待命的时刻。比如 2008 年的 Lindsay Lohan 专题那次,我们的 CTO 和系统管理员甚至打赌说现场的路由器能不能撑得住(那时候还没有云,AWS 还在襁褓中)。结果呢?流量图一路飙升,平直地贴在 100%
—— CTO 输了赌赌注,系统管理员赢了一瓶蓝方威士忌
“直接用 Solr 的 facet 就好了啊?” ——兄弟,没那么快。2006 年,Solr 的 facets 根本还没诞生。我看过多个企业级搜索引擎的演示,但那些昂贵的产品没有一个能支持深层次的分面搜索。我们虽然有 Autonomy 授权,但第一次尝试后就发现它做不到。理论上可以,但计数全错。Endeca(后来被 Oracle 收购)刚刚从隐身模式出来,而我们设计阶段已经开始了。太新、太不稳定、太冒险。总之,这个想法超前了一点点,尤其对于一个在非技术公司工作的三人小团队。
于是,我们三人一组,我和两位顾问,负责用 Perl 编写索引脚本、查询解析逻辑以及数据建模——运行在 MySQL 4 上。这种项目属于那种只要出现一个无法逾越的技术风险,就可能让整个项目功亏一篑。我就不讲过程的刺激细节了,我们最终完成了任务,然后去卡拉 OK 酒吧庆祝(也是我人生中第一次因工作压力而严重宿醉)🤮
作为负责 SQL 模型和查询的人,我整整花了几天时间在调优上:计时每一条查询,研究 EXPLAIN 的输出,看能不能再挤出 50ms 的性能。最终的结果,是一堆让人头晕的 GROUP BY 查询,加上反复调 MySQL 的服务器参数。当时 MySQL 的查询优化器很脆弱,有时候换一下 SELECT 字段顺序都会影响性能。想象一下:
SELECT field1, field2 FROM my_table;
比
SELECT field2, field1 FROM my_table;
更快。为什么?我到今天都不知道,也不想知道。
我遗失了那些代码的例子,但幸好 Wayback Machine 上还有最终成品的证据。
重点是——如果你真的懂数据库,你能做出各种疯狂的事。而在如今的存储技术和硬件性能下,你甚至不需要极限优化,数据库就能轻松应对我所谓的常规规模级别的数据。
SQL 式微的艺术
近些年来,我注意到一个令人不安的趋势——不少软件工程师热衷于对非常基础的问题使用各种新奇的“行星级”数据库,与此同时,他们对自己很可能已经在使用的强大关系型数据库引擎却缺乏足够了解,更不用说去掌握这项技术更高级、更有用的功能了。SQL 层被深深地埋藏在各种类库和那些自作聪明的 ORM 之下,以至于所有操作都变成了高级代码的抽象。
“为什么系统慢?”
“不知道,直接上 Cassandra 不就完了!”
现代硬件确实让我们得以从CPU层面跃升至更高抽象层,而在过去,为了榨干处理器性能将特定函数转写成汇编代码非常常见。如今计算和存储确实便宜了。可我们也因此养成了懒惰和自满的习惯。于是账单飙升、能源浪费、无数自动生成的 “Squeel(作者讽刺 SQL)” 查询在云端燃烧。
2004 年我去面试第一份工作那天早上,我在地铁上背“数据库范式的九个级别”(其实是五个?我也忘了)。如今,没有人会在面试里问你这些。但你只要翻翻 PostgreSQL 的目录,就会发现那是一个宝藏——足以应对除了“行星规模”之外的几乎一切问题。现在,TB 甚至 PB 级的 Postgres 集群正在轻松运行。
关键在于,别指望你的数据库或 ORM 能读心。说到这个……
ORM 不是魔法
当时我刚入职一家电商公司,立刻就被扔去解决产品目录页的性能问题。不过是个简单的分页商品图片网格,能有多难?信不信由你——还真能要命。页面加载超过 10 秒,甚至更久,数据库不堪重负,而解决方案竟是“加个缓存就行”。最关键的是——这并非高流量网站。即便毫无访问量,页面依旧很慢。这种反常迹象说明系统某些地方出了大问题。
错误 1:没有索引
每个关键查询都频繁调用的数据列竟然没有索引。一个都没有。在生产环境补建急需的索引后,你几乎能听见 MySQL 如释重负的喘息。但性能仍未达标,于是我继续深入代码层探察。
错误 2:认为每次 ORM 调用都是免费的
我打开查询日志,刷新一次页面——200、300、甚至 500 条查询!原因是经典的 ORM 误用:通过循环遍历每条记录,导致的效果是:
for product_id in product_ids:
product = my_orm.products.get(id=product_id)
products.append(product)
查询量居高不下的另一个原因,是部分逻辑存在嵌套。显而易见的解决方案是利用ORM的关联查询功能,将数据整合成单个数据集,严格控制每个请求的查询次数——这本来就是关系型数据库的天职——名字里就体现了这一点。
上述场景的症结在于:每个独立查询都需要经历数据库往返、解析、转换、分析、规划、执行的全流程。这是计算成本最高的操作之一,而 ORM 却会"热心"地为你执行最影响性能的操作。
那个 ORM 调用会生成什么 SQL?如果结果不符合预期,究竟是 ORM 的限制还是你没用对库方法?是否因为特定数据库的非标准 SQL 方言导致 ORM 难以解析?最终是否需要对某个调用改用原生 SQL?这些问题接踵而至。
错误 3:全字段拉取
更糟的是,这个表虽然记录少,但有几十个字段。ORM 为了方便,默认会把所有字段全取出来,塞满网络传输。这是典型的技术债,最终会反噬性能。
我花了数小时深耕 Django 管理后台的隐秘角落,重写默认 ORM 查询以降低其"贪婪加载"特性。这显著改善了内部办公系统的使用体验。
性能本身就是一种特性
数十年来,关键业务系统始终稳定运行在经典而"乏味"的关系型数据库之上,每秒处理数千次请求。这些系统日益精进、功能愈加强大、且始终不可替代。堪称计算机科学的奇迹。你或许认为像 Postgres 这样古老的数据库(自1982年开发至今)已进入遗产维护阶段,但事实恰恰相反。其发展步伐持续加速,规模与功能提升令人惊叹——几年前需要多次查询的操作,如今只需单次即可完成。
为什么这很重要?亚马逊早就发现:每多 100ms 的页面加载延迟,就意味着营收损失。而从用户体验看,网页的理想响应时间应不超过 100ms:
延迟在 100 毫秒以内,用户会感觉几乎是瞬间响应;延迟在 100 到 300 毫秒之间,用户能够察觉到;延迟在 300 到 1000 毫秒之间,用户会感觉像是在等机器处理;而如果延迟超过 1000 毫秒,用户很可能开始在心理上切换注意力,也就是进行“上下文切换”。
那种速度慢就堆 CPU 加内存的做法或许能奏效一时,但越来越多的人正在付出代价后醒悟:在崇尚节俭、注重成本效益的商业环境中,这种惰性思维终将不可持续
数据库反模式
知道不该做什么,和知道该做什么一样重要。下面的一些错误非常普遍:
反模式 1:因为错误的原因使用异域数据库
诸如 DynamoDB 这类技术,其设计初衷就是为了应对 Postgres 和 MySQL 难以承载的超大规模场景。它们通过激进的反规范化与数据冗余来实现这一目标,在这种架构下数据库几乎不进行实时数据关联或表连接操作。此时的数据建模完全基于查询需求而非实体关系,传统的关系型概念在如此惊人的数据规模下已然瓦解。毋庸置疑,若将这类存储方案用于"常规规模"的场景,无异于用高射炮打蚊子——解决根本不存在的难题。
反模式 2:不必要的缓存
缓存是一种不得已而为之的解决方案——但并非总是必需。有一大类程序错误和运维问题恰恰源于缓存数据过期。只读数据库副本作为经典架构模式至今毫不过时,它能带来惊人的性能提升,让你在应对更复杂方案前高枕无忧。成熟的关系型数据库早已内置查询缓存功能——我们只需根据具体需求进行调优即可。
缓存失效是个棘手难题。它不仅给系统增加了更多复杂性和不确定状态,还会让调试工作难上加难。在我的职业生涯中,屡次收到内容团队发来邮件质疑“为什么数据还没更新?我明明半小时前就改过了?!”,这类邮件多到令人厌烦。
缓存不应成为低劣架构和低效代码的遮羞布。
反模式 3:把数据库当垃圾场
即便行业标准数据库的承载能力再强,你也不能将所有数据都存入数据库。一旦数据库体量大幅增长,管理、查询、备份和迁移等操作都将变得非常困难。即便使用云托管数据库无需操心运维,高昂的成本也值得警惕——关系型数据库作为精密技术产物,存储数据的代价从来都不低廉。
先弄清楚什么是常规规模
若指望 Postgres 或 MySQL 这类重型数据库不做任何优化就会自动创造奇迹,让它们陷入瘫痪简直易如反掌。
“老板,这系统扛不住网络级流量啊!才200万条记录就不堪重负了,我们必须上马 DynamoDB、Kafka 和事件溯源方案!”
关系型数据库并不是那种过时的技术,不是只有我们这些技术老古董才选择钻研的东西,也不是可以像驱赶恼人的昆虫一样随便忽视的东西。“老头,我们用 React 和 GraphQL 全部搞定就行了。” 用法律术语来说,现代关系型数据库在被证明有罪之前都是清白的,而举证责任必须极其严苛——且几乎完全落在质疑者身上。
最后,假如我要排查数据库查询为什么这么慢,我的大致流程是:
- 收集唯一查询列表(日志、慢查询日志等)
- 先看最频繁的查询
- 用
EXPLAIN语句检查是否使用索引 - 只取必要字段
- 如果 ORM 在捣乱,那就直接上原生 SQL
最重要的是:研究你的数据库和 SQL。去了解它,热爱它,折腾它。花两天时间读一读 Postgres 手册,收获远比看下一个爆火的 JavaScript 框架大得多。