一个在局域网内不到一秒就能打开的界面,走 VPN 却要五秒,而同一条链路上的文件复制却始终跑满速度。差别不在带宽。界面要按顺序等待一连串应答,每一步都付出完整的往返代价;文件复制则让字节持续处于飞行状态。在动接入线路之前,先把这些等待数清楚。

症状是一个界面,而不是一条查询

那个界面上的每条查询都可以很快,界面却依然很慢,因为总耗时是等待之和,而不是执行时间之和。一条两毫秒就能跑完的语句,如果客户端必须收到它的结果才能发下一条,仍然要付出整整一次往返。在延迟为四十毫秒的链路上,上百次相互依赖的调用在任何渲染工作开始之前就累积到大约四秒。这套算法是等待的模型,而不是对你链路的基准测试。

这就是速度测试看起来无辜的原因。批量传输衡量吞吐量,话多的界面衡量延迟,而 VPN 可以在一方面表现优异,在另一方面却平平无奇。因此诊断界面意味着衡量界面,而不是衡量链路。

在改动任何东西之前,先记录一次用户操作的端到端耗时。只有这个数字能告诉你后来的修复究竟有没有生效。

数清一次用户操作中的串行等待

不要猜,直接在客户端埋点。在 psql 里,\timing on 会打印每条语句的耗时,而 EXPLAIN (ANALYZE, BUFFERS) 显示服务器真正花在这项工作上的时间占多少。

这两个数字之间的差距就是你的网络与客户端开销。如果一条语句执行只用两毫秒,而客户端记录到六十毫秒,剩下的五十八毫秒是路途与本地处理,没有任何索引能把它们去掉。对界面发出的每条语句重复这个对比,累加的是差距而不是执行时间。

把结果记成串行等待的次数。这正是接下来两节能够削减的量,而且它是一个可以用证据来争论的数字。

把独立的调用批处理,保留有依赖的调用

两条互不读取对方结果的语句可以一起上路、一次收取,从而把两次等待变成一次。两条语句中如果后者需要前者的值,就不能这样,因为依赖是真实的,合并会改变程序的行为。在重写任何东西之前,先把调用分成这两堆,每一堆的大小会告诉你什么是可行的。

基于集合的做法比批处理更彻底地消除等待。一条查询返回原本由循环逐行取回的行,既消除了循环,也消除了往返。先找循环,因为它通常是更大的收益,也是更容易评审的改动。

批处理仍然必须保留错误处理和 事务边界。把互不相关的语句拼成一个字符串不是安全的优化,而把失败语句藏在一次成功提交后面的批处理,比它省下的等待更糟。

先复用连接,再买带宽

新建一条连接要付出名称解析、TCP 握手、TLS 协商和身份验证的代价,在高延迟链路上,其中每一项都是第一条查询运行之前的又一次往返。连接池的存在,就是为了让这份代价每个进程只付一次,而不是每次调用付一次。

在 ADO.NET 中,关闭或 dispose 池中的连接会把它归还以便复用,而不是打开一条新的物理连接,所以看起来像泄漏的写法往往才是正确用法。要验证连接池确实在用,而不是想当然:记录物理连接的创建,或者在界面运行时观察服务器上的会话数。SQL 性能指南在减少往返和批处理频繁的临时调用上讲的是同一件事,并给出了针对具体服务的示例,你应当把它们映射到自己的部署上。

连接池去掉了建立连接的开销,却没有动串行查询等待。它是干净测量的前提,而不是单独的修复。

权衡把执行移近数据

把应用跑在数据库附近,会用一次远程显示会话取代大量远程数据库交互。对话多的工作负载来说,这可能是很大的变化,而且它既是性能变化,也是运维变化:会话管理、访问控制、软件部署和用户体验都会随应用一起搬迁。

在假定某个方向更快之前,先测量真正的接口和它传输的数据。远程桌面会话搬运的是像素,图形密集的界面在同一条链路上可能比它所取代的查询表现更差,尤其是在客户端持续重绘的时候。

然后重做最初的操作,并比较三个数字:串行等待次数、结果数据量和端到端完成时间。既改变了被测的用户任务,又保持了数据正确性和访问策略的改进,才是值得留下的那个。

相关文章