首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

数据库连接从15000降到100以下:DigitalOcean如何解决技术债

本文最初发布于DigitalOcean官方博客,经原作者授权由InfoQ中文站翻译并分享。

最近,一位新员工在午餐时问我:“DigitalOcean的技术债务是什么情况?”

听到这个问题时,我忍不住笑了。软件工程师询问一家公司的技术债务就相当于询问其信用评分。这是他们衡量一家公司过去的遗留问题以及他们所要背负的包袱的方法。DigitalOcean对技术包袱并不陌生。

作为云提供商,我们管理着自己的服务器和硬件,我们面临着许多其他初创公司在这个云计算的新时代尚未遇到的复杂性。这些艰难的情况最终迫使我们不得不早做权衡。任何快速成长的公司都知道,你早期做出的技术决定往往会在以后影响到你。

盯着桌子对面的新员工,我深吸了一口气,然后开始说。“让我告诉你,我们有15000个与数据库的直接连接……”

我给这位新员工讲的故事是DigitalOcean迄今为止最大的技术重组。这是整个公司多年来的努力,教会了我们很多东西。我希望,讲述这个故事可以帮助未来的DigitalOcean开发者——或任何发现自己陷入棘手的技术债务问题的开发者。

故事缘起

DigitalOcean从一开始就痴迷于简单性。这是我们的核心价值观之一:力求简单而优雅的解决方案。这不仅适用于我们的产品,也适用于我们的技术决策。这一点在我们最初的系统设计中最为明显。

与GitHub、Shopify和Airbnb一样,DigitalOcean也是在2011年从一个Rails应用程序开始的。我们内部称为Cloud的Rails应用程序管理着UI和公共API中的所有用户交互。Rails服务有两个Perl编写的辅助服务:调度器和DOBE(DigitalOcean的后端)。调度器将调度并分配Droplet给管理程序,而DOBE负责创建实际的Droplet虚拟机。Cloud和调度器作为独立的服务运行,而DOBE则运行在fleet中的每台服务器上。

无论是Cloud、调度器,还是DOBE,彼此之间都不能直接对话。他们通过MySQL数据库通信。这个数据库有两个作用:存储数据和代理通信。这三个服务都使用单个数据库表作为消息队列来传递信息。

每当用户新建一个Droplet时,Cloud就会向队列插入一个新的事件记录。调度器每秒一次不断地在数据库中轮询新的Droplet事件,并将它们的创建工作安排给一个可用的管理程序。最后,每个DOBE实例将等待新调度的Droplet创建并完成任务。为了能检测到所有新的更改,这些服务器都需要在数据库中轮询表中的新记录。

虽然在系统设计方面,无限循环和让每个服务器直接连接到数据库可能比较低级,但它很简单,而且很有效——特别是在人手短缺的技术团队面临紧迫的期限和快速增长的用户群时。

四年来,数据库消息队列一直是DigitalOcean技术的支柱。在此期间,我们采用了微服务体系结构,用gRPC代替HTTPS进行内部通信,并取消了Perl,代之以Golang作为后端服务。然而,所有的方法都指向MySQL数据库。

值得注意的是,不能仅仅因为某些东西是“遗产”就认为它功能不完善,应该被取代。Bloomberg和IBM都有用Fortran和COBOL编写的遗留服务,这些服务产生的收入超过整个公司。另一方面,每个系统都有一个扩展限制。我们就要达到这个限制了。

从2012年到2016年,DigitalOcean的用户流量增长超过了1000%。我们在目录中添加了更多的产品,在基础设施中添加了更多的服务。这增加了数据库消息队列上进入的事件。对Droplet需求增加意味着调度器要加班加点地把它们分配到服务器上。不幸的是,对于调度器,可用服务器的数量不是静态的。

为了满足Droplet需求的不断增长,我们不断地增加服务器来处理流量。每个新的管理程序都意味着一个新的数据库持久连接。截至2016年初,该数据库拥有超过1.5万个直接连接,每一个连接每1到5秒就查询一次新事件。如果这还不够糟糕的话,每个管理程序用来获取新Droplet事件的SQL查询也变得越来越复杂。它已经变成了一个巨人,有150多行,18张表关联在一起。令人印象深刻的是,它不稳定,而且很难维持。

不出所料,正是在这个时期,问题开始显现。单点故障和成千上万的依赖关系攫取了共享资源,不可避免地导致了一段时间的混乱。表锁和查询积压会导致停机和性能下降。

由于系统的紧耦合,我们没能找到一个清晰简单的解决方案来解决这个问题。Cloud、调度器和DOBE都是瓶颈。如果只修补其中一两个组件,就会将负载转移到其余的瓶颈组件上。因此,经过深思熟虑,工程师们想出了一个三管齐下的方案来解决这个问题:

  1. 减少数据库上直接连接的数量;
  2. 重构调度器的排名算法,改进可用性;
  3. 移除数据库的消息队列职责。

开始重构

为了解决数据库依赖,DigitalOcean的工程师创建了事件路由器。事件路由器充当区域代理,代表每个数据中心中的每个DOBE实例轮询数据库。这样,就只有少数代理在做查询,而不是数以千计的服务器。每个事件路由器代理将获取特定区域中的所有活动事件,并将每个事件委派给适当的管理程序。事件路由器还将庞大的轮询查询拆分成更小、更容易维护的轮询查询。

当事件路由器上线后,数据库连接的数量从15000个减少到不足100个。

接下来,工程师们把目光投向了下一个目标:调度器。如前所述,调度器是一个Perl脚本,它决定哪个管理程序将托管一个创建好的Droplet。它使用一系列查询来对服务器进行排序。每当用户创建一个Droplet时,调度器就用最好的机器更新表行。

虽然听起来很简单,但该调度器有一些缺陷。它的逻辑很复杂,很难处理。它是单线程的,在流量高峰期间性能会受影响。最后,该调度器只有一个实例,却必须为整个fleet服务。这是一个不可避免的瓶颈。为了解决这些问题,工程团队创建了调度器V2。

更新后的调度器彻底修改了排名系统。它不从数据库中查询服务器指标,而是从管理程序中聚合它们,并将它们存储在自己的数据库中。此外,调度器团队使用并发和复制保证新服务的负载性能。

事件路由器和调度器V2取得了许多了不起的成果,消除了当时的许多架构缺陷。尽管如此,还是有一个明显的障碍。到2017年初,集中式MySQL消息队列仍然在使用——甚至还很频繁。它每天处理40万条新记录,每秒更新20次。

遗憾的是,移除数据库消息队列并不容易。第一步是避免服务直接访问它。数据库需要一个抽象层。它还需要一个API来聚合请求并代它执行查询。任何服务想要创建一个新事件,都需要通过API来实现。于是,Harpoon诞生了。

不过,为事件队列构建接口这部分比较容易。事实证明,从其他团队那里获得支持更加困难。与Harpoon集成意味着团队将不得不放弃他们的数据库访问,重写他们的部分代码库,并最终改变他们一直以来做事的方式。这可不是件容易的事。

Harpoon的工程师们逐个团队、逐个服务地将整个代码库迁移到他们的新平台上。这花了差不多一年的时间,但到2017年底,Harpoon成为数据库消息队列的唯一发布者。

现在真正的工作开始了。对事件系统的完全控制意味着Harpoon可以自由地重新设计Droplet工作流了。

Harpoon的第一个任务是将消息队列职责从数据库提取到自身。为此,Harpoon创建了自己的内部消息队列,由RabbitMQ和异步worker组成。当Harpoon在一侧把新事件推入队列时,worker在另一侧拉取它们。由于RabbitMQ取代了数据库队列,worker可以方便地直接与调度器和事件路由器交互。因此,调度器V2和事件路由器不是通过轮询从数据库获取新变化,而是由Harpoon直接将更新推送给它们。截止到2019年本文撰写时,这就是Droplet事件架构所处的位置。

一路向前

在过去的7年里,DigitalOcean已经从一家小型创业公司成长为今天这样的成熟的云提供商。与其他转型中的科技公司一样,DigitalOcean会定期处理遗留代码和技术债务。无论是拆分单体应用、创建多区域服务,还是消除单点故障,我们DigitalOcean的工程师们始终致力于打造优雅而简单的解决方案。

这就是我们的基础设施如何随着用户群的发展而扩展的故事,希望你觉得这是个有趣而又有启发性的故事。我很乐意在下面的评论中看到你的想法!

原文链接:

From 15,000 database connections to under 100: DigitalOcean’s tale of tech debt

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/GyBqNNq4R6ULMAAt02yJ
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券