一个数据库性能规模化的传说

文摘   科技   2025-01-10 00:00   山东  

跟随新员工Joan,观察她在从无法扩展的内部数据库系统迁移到分布式数据库的过程中如何调试问题。

译自A Tale of Database Performance at Scale,作者 Piotr Sarna。

被诸如“混合云”、“无服务器”和“边缘优先”等令人印象深刻的流行语所吸引,Joan 欣然加入了一家新公司,并开始了解其技术栈。她最近的第一个项目是从公司内部数据库系统的实现迁移到行业标准的数据库管理解决方案之一,因为内部数据库系统无法与客户数量同步扩展。

新的选择是一个分布式数据库,与 NoSQL 相反,它力求保持 SQL 世界中已知的原始 ACID(原子性、一致性、隔离性、持久性)保证。

由于如今每年都会出现一些新的数据保护法案,公司董事会决定维护自己的数据中心,而不是使用流行的云供应商来存储敏感信息。

从非常高的层面来看,公司的主要产品仅包含两层:

  • 前端,用户的入口点,运行在其自身的浏览器中,并与系统的其余部分通信以交换和持久化信息。

  • 其他所有内容,通常称为“后端”,但实际上包括负载均衡器、身份验证、授权、多层缓存、数据库、备份等等。

Joan 的第一个入门任务是实现一个简单的服务,用于从数据库收集和汇总各种统计信息,并将该服务集成到整个生态系统中,以便它实时从数据库获取数据,并允许 DevOps 团队实时检查统计信息。

为了给管理层留下深刻印象,并让他们确信在本季度聘用她是他们做出的最佳决定,Joan 决定在第一天交付一个概念验证实现。公司的不成文规定是使用 Rust 编写软件,所以她从 crates.io 上快速搜索到第一个数据库驱动程序,然后坐下来进行她自己组织的黑客马拉松。

这一天进行得很顺利,Rust 以人为本的生态系统提供了卓越的开发体验。但随后 Joan 在真实系统上运行了她的第一个冒烟测试。当她意识到平均每三个请求就有一个请求最终出错时,怀疑变成了失望和无助,尽管整个数据库集群都报告处于健康、可操作的状态。这意味着需要进行调试会话。

不幸的是,Joan 匆忙选择的作为她工作基础的驱动程序,即使本身是开源的,也只是一个对预编译的遗留 C 代码的薄包装器,找不到源代码。在强烈渴望解开谜团和健康的愤怒的驱使下,Joan 花了几个小时使用 Wireshark 检查网络通信,她推测错误一定在哈希键实现中(确实如此)。

在公司的数据库中,键被哈希化以随后将请求路由到相应的节点。如果哈希值计算错误,请求可能会被转发到错误的节点,该节点可能会拒绝请求并返回错误。

由于缺少源代码而无法验证该说法,Joan 决定采用更简单的路径——放弃最初选择的驱动程序,并在数据库供应商支持的、具有可靠用户群和定期更新发布计划的官方支持的开源驱动程序之一上重新实现该解决方案。

Joan 的经验教训日记,第一部分

最初的教训包括:

  1. 仔细选择驱动程序。它是代码性能、健壮性和可靠性的核心。

  2. 驱动程序也会有错误——而且不可能避免它们。尽管如此,仍有一些良好的实践需要遵循:


  • 除非有充分的理由,否则优先选择官方支持的驱动程序(如果存在)。
  • 开源驱动程序具有优势:它们不仅经过社区验证;它们还允许对代码进行深入检查(甚至修改驱动程序代码以获得更多调试见解)。
  • 最好依赖具有完善发布计划的驱动程序,因为它们更有可能在合理的时间内收到错误修复(包括安全漏洞修复)。
  • Wireshark 是一个用于解释网络数据包的优秀开源工具。如果您想深入了解程序的内部工作原理,请尝试一下。

  • 入门任务最终成功完成,这使得 Joan 准备好接受她的第一个真正任务。

    调优

    凭借在入门任务中获得的经验,Joan 开始计划如何处理她的新任务:一个行为异常的应用程序。其中一个应用程序臭名昭著地导致整个系统的稳定性问题,每次出现问题都会扰乱其他工作负载。这个异常的应用程序已经基于官方支持的驱动程序,所以Joan 可以将它从潜在根本原因的列表中排除。

    这项特定服务负责将从旧系统备份的数据注入新的数据库。由于公司并不着急,因此该应用程序在设计时考虑了低并发性,以降低优先级,并且不会干扰用户工作负载。

    不幸的是,每隔几天就会有一些事情触发异常。这个通常平静的应用程序似乎试图对其自身的数据库进行拒绝服务攻击,用请求淹没它,直到后端过载到足以导致生态系统其他部分出现问题。

    当Joan 观察 Grafana 仪表板中显示的指标清楚地表明该应用程序生成的请求速率在异常发生时开始飙升时,她想知道这个工作负载怎么会这样。毕竟,它明确地实现为只有在进行中的请求少于 100 个时才发送新的请求。

    由于在与现场教练的入职会议中,协作被大力宣传为公司的“精神和文化基础”之一,因此她决定最好与她的同事 Tony 讨论此事。

    “看,Tony,我搞不明白,”她解释道。“当已经有 100 个请求正在处理时,这项服务不会发送任何新的请求。而且看这里日志:100 个请求正在进行中,一个返回了超时错误……”她停了下来,被她自己的顿悟吓了一跳。

    “好吧,谢谢 Tony,你真是个亲爱的——有史以来最好的rubber duck!”她说,然后回到修复代码的工作中。

    导致发现根本原因的观察相当简单:请求实际上并没有返回超时错误,因为数据库服务器从未发送回这样的响应。请求只是被驱动程序判定为超时,然后被丢弃。

    但是,驱动程序不再等待特定请求的响应并不意味着数据库已经完成了对它的处理。请求可能只是停滞了,花费的时间比预期长,但驱动程序放弃了等待其响应。

    有了这些知识,很容易想象,一旦客户端有 100 个请求超时,应用程序可能会错误地认为它们不再正在进行中,并愉快地向数据库提交另外 100 个请求,将进行中请求的总数(并发性)增加到 200。重复这个过程,你就可以在你的数据库集群上实现极高的并发级别——即使应用程序应该将其限制在一个较小的数字。

    Joan 的经验教训日记,第二部分

    教训仍在继续:

    1. 客户端超时对程序员来说很方便,但它们可能与服务器端超时发生不良交互。经验法则:除非你有充分的理由这样做,否则将客户端超时设置为服务器端超时的两倍左右。一些驱动程序可能能够在检测到客户端超时小于服务器端超时时发出警告,甚至修改服务器端超时以匹配,但总的来说,最好仔细检查一下。

    2. 似乎具有固定并发的任务实际上在某些意外情况下可能会导致峰值。检查日志和仪表板有助于调查此类案例,因此请确保在数据库集群和所有客户端应用程序中都提供可观察性工具。如果使用分布式跟踪,例如OpenTelemetry集成,则可以获得额外分数。

    通过正确修改客户端超时,应用程序的阻塞频率和程度大大降低,但它仍然不是分布式系统中的完美公民。它偶尔会选择一个受害者数据库节点,并不断用过多的请求打扰它,而忽略了其他七个节点的负载要轻得多,可以帮助处理工作负载这一事实。

    在其他时候,据报告其并发性比配置预期高出 200%。每当这两个异常同时发生时,可怜的节点都无法处理所有对其进行轰炸的请求。它不得不放弃相当一部分请求。

    对驱动程序文档(幸运的是,它以mdBook格式提供,并保持合理更新)的长时间研究帮助 Joan 也减轻了这些痛苦。

    第一个问题仅仅是默认负载均衡策略的错误配置,该策略试图基于启发式和数据库自身偶尔更新的统计数据,从所有可用数据库节点中选择“负载最轻”的节点。不幸的是,此策略也是“尽力而为”的,它依赖于从数据库接收到的统计数据始终有效,但是压力过大的数据库节点可能会变得过载,以至于无法及时发送更新的统计数据。

    这导致驱动程序错误地认为该特定服务器实际上根本不忙。Joan认为此设置是过早的优化,结果却适得其反,因此她只是恢复了原始的默认策略,该策略按预期工作。

    第二个问题(并发量暂时翻倍)是由另一个错误配置引起的:过于积极的推测性重试策略。在等待预配置的时间段后没有从数据库收到确认,驱动程序会推测性地重新发送请求以最大限度地提高其成功的机会。此机制有助于提高请求的成功率。但是,如果原始请求也成功,则意味着推测性请求是徒劳的。

    为了平衡利弊,应将推测性重试配置为仅在原始请求很可能失败时才重新发送请求。否则,就像Joan的案例一样,推测性重试可能会过早地执行,使发送的请求数量加倍(从而也使并发量加倍),而根本不会提高成功率。

    哇,没有什么比高质量的调试会话以惊人的成功告终更能同时带来内啡肽和多巴胺的激增了(当然,除了在深度技术出版物中编写俗套的故事)。Joan,干得好!

    想了解更多?“大规模数据库性能,”一本免费的开源书籍,提供了一个类似俗套的数据库性能故事以及大量关于理解和克服您自己数据库性能挑战的实用建议。

    此外,Piotr Sarna 将在Monster Scale Summit上发言,一个新的(免费和虚拟)关于极端规模工程和数据密集型应用程序的会议,会议将于3月11日至12日举行。来自Canva、Slack、Disney+/Hulu、Netflix、Salesforce、Atlassian等公司的工程师将分享策略和案例研究。

           

    云云众生s
    关注云原生时代的普通人 - 云原生 | 平台工程 | AI
     最新文章