作者 | Dan Luu
译者 | Sambodhi
策划 | Tina
本文最初发表于 Wave 官网,经原作者 Dan Luu 授权,InfoQ 中文站翻译并分享。
Wave 是一家价值 17 亿美元的公司,拥有 70 名工程师,该公司的产品是一款加减数字的 CRUD 应用程序。为了与此保持一致,我们的架构是一种标准的 CRUD 应用架构,基于 Postgres 的 Python 单体架构。先从一个简单的架构入手,然后尽量用最简单的方式来解决问题,这使得我们的业务范围能够扩大到这种规模,而工程师们大多专注于为用户提供价值的工作。
Stackoverflow 扩大了单体的规模,取得了良好的效果(2013 年的架构 /2016 年的架构),最后以 18 亿美元的价格被收购。如果我们关注的是流量而非市场市值,那么 Stackoverflow 就是互联网上流量最高的前 100 个网站之一(关于其他许多建立在单体之上的有价值的公司的案例,请参考这条 Twitter 主题的回复(https://twitter.com/danluu/status/1462607028585525249)。我们没有很多网络流量,因为我们是一个移动应用,但 Alexa 还是将我们的网站列在了前 75000 名,尽管我们的网站基本上只是人们查找 APP 的一种途径,而大部分人并没有从我们的网站中获得这些 APP)。
有些应用的要求,使得在一个枯燥的数据库中构建出一个简单的单体应用是不可能的,但对大部分应用来说,即便是在前 100 个网站的流量水平上,计算机的运行速度也足以满足使用简单的架构来提供服务,通常创建简单的架构比复杂的架构更便宜、更容易。
尽管简单的架构具有不合理的有效性,但是大部分的新闻报道都是围绕着复杂的架构展开的。举例来说,在最新的通用技术会议上,就有六场演讲讨论了怎样构建或处理基于微服务的复杂结构的负面影响,却没有一场演讲讨论如何构建简单的单体。甚至关于量子计算的演讲也有一场。更大规模的会议也一样;最近旧金山的一次以企业为导向的会议上,关于处理复杂架构的演讲,场次就高达两位数;却没有一场关于如何构建简单的单体的演讲。我上次去的那次会议给我留下了很深的印象,就是许多公司的员工,他们的应用程序规模很小,本来可以用简单的架构就能完成,但是他们使用的都是会议圈子和网络上流行的最新、最复杂的技术。
我们的架构是如此简单,以至于我都懒得去做一个架构图。我会讨论我们所做的使一切乏味的事。
我们目前使用的是乏味的、同步的 Python,这意味着,当我们的服务器进程在等待 I/O 时被阻塞,比如网络请求。我们之前尝试过 Eventlet,这是一种理论上能使我们从 Python 中获得更高效率的异步框架,但是我们碰到了大量的 Bug,我们觉得,等待事件的 CPU 和延迟成本,都不值得我们为处理 Eventlent 问题而承担操作上的痛苦。其他知名的 Python 框架也有类似的情况,但是大规模使用它们的用户往往也会报告大规模使用这些框架带来的严重后果。使用同步的 Python 代价很高,因为我们需要支付 CPU 的费用,而在网络请求期间,CPU 除了等待之外什么都不做,但是,现在,我们每个月只能处理几十亿个请求,因此,即便是使用 Python 这种缓慢的语言,也要支付公共云的零售费用,这样的成本也很低。我们工程团队的成本完全决定了我们所运营的系统的成本。
我们将长时间运行的任务(我们不想让响应阻塞)分配到一个队列中,而不是承担使我们的单体异步的复杂性。
我们不能像我们想的那样无聊的地方,就是我们的内部数据中心。当我们只在塞内加尔和科特迪瓦运营时,我们完全是在云端中运营,但是,随着我们的业务范围扩大到乌干达(以及未来更多的国家 / 地区),我们不得不拆分后端,部署到当地的内部数据中心,以遵守当地的数据存储法律和法规。这并非一项简单的操作,但正如那些在面向服务的复杂架构中做过相同工作的人所知道的那样,这种操作要比使用复杂的服务导向的架构要简单得多。
另外一个方面是我们必须研发的软件,而非购买。刚起步时,我们强烈地倾向于购买软件,而非研发软件,因为一个由少数工程师组成的团队无法承担研发软件的时间成本。虽然“购买”这一选项,通常会给你提供一些无效的工具(https://danluu.com/nothing-works/),但这在那个时候是正确的选择。如果我们不能说服供应商修复 Showstopper 错误,而这个错误对我们至关重要,那么在这种情况下,构建更多的自己的工具,并且在更多的方面保留内部的专业知识,这的确是很有意义的(https://danluu.com/in-house/),但这与公司应只选择“构建”其核心能力的标准建议相悖。这种复杂性的大部分都是我们不愿意承担的,但是对于某些类别的产品,即便是进行了相当广泛的研究,我们仍然找不到供应商能够提供适合我们的产品。公平地说,我们的供应商需要解决的问题比我们需要解决的问题复杂得多,因为我们的供应商承担着为每个客户解决问题的复杂性,而我们只需要为一个客户解决问题,那就是我们自己。
译注:Showtopper 错误是导致执行停止并基本上变得无用的硬件或软件错误。必须修复此严重错误,以使开发过程进一步进行。
我们在运营的头几个月里,就犯了一个错误,就是没有仔细地界定数据库事务的边界,这在今天已经付出了一定的代价。在 Wave 的代码库中,SQLAlchemy 数据库会话是一个请求全局变量;在任何时候访问 DB 对象的属性时,它都隐含地开始一个新的数据库事务,并且 Wave 代码库中的任何函数都可以在会话上调用 commit,使其提交所有挂起、的更新。这使得我们很难控制数据库更新发生的时间,从而增加了出现微妙的数据完整性错误的概率,并且很难依靠数据库来构建类似于幂等键(idempotency key)或事务性暂存的作业流失。这样做还会增加我们意外地持有打开的长时间运行的数据库事务的风险,这可能使模式迁移操作变得困难。
一些我们不确定的选择(因为我们在考虑更改,或建议其他从零起步的团队考虑另一种方式)有:使用 RabbitMQ(就我们的目的而言,Redis 可能同样适用于任务队列,只需要 Redis 就可以减轻操作负担);使用 Celery(这对于我们的用例来说过于复杂,并且已经出现了好几次故障,比如在版本升级过程中出现了向后兼容性问题);使用 SQLAlchemy(它使开发人员难以理解自己的代码将会产生怎样的数据库查询,从而导致各种难以调试的情况,同时也带来了不必要的操作痛苦,尤其是与上面提到的数据库事务边界的观点有关);以及使用 Python(由于我们的创始 CTO 的技术背景,这是最初的正确选择,但其并发支持、性能和广泛的动态性使我们质疑它是否是大规模后端代码库的正确选择)。以上所有这些都不是主要的错误,而且对于一些(例如 Python) 来说,缺陷已经很少了,因此,与投资到理论上更好的迁移相比,我们将花费更少的费用去进行更多的维护,但如果我们现在就从头编写一套类似的代码库,那么我们就会认真考虑,它们是否正确的选择。
在某些方面,我们很满意能做出这样的选择,虽然这些听上去并不像是最简单可行的解决方案,比如我们的 API,我们使用 GraphQL;我们的传输协议,我们有一段时间使用自定义协议;还有我们的主机管理,我们使用 Kubernetes。对于我们的传输协议,我们曾经使用了一种基于 UDP 的自定义协议,并带有 SMS 和 USSD 后备功能,这也是这场讲座所提到的性能理由。在 HTTP/3 发布后,我们已经能够用 HTTP/3 来替代我们的自定义协议,通常我们只需要 USSD 就可以解决像最近在马里发生的互联网关闭这样的事件)。
对于 GraphQL 的使用,我们相信其优点多于缺点:
优点:
缺点:
对于 Kubernetes,我们之所以选择 Kubernetes,是因为我们清楚,如果业务成功(确实如此),而且我们不断扩张,我们最终会扩张到那些要求我们在该国内运营服务的国家。各国之间的具体规定各不相同,但是我们在非洲的主要市场上拓展了业务,这就要求我们在该国运营我们的 “主要数据中心”,还有其他一些规定,例如,要求我们能够将故障转移到该国的数据中心。
电信集成是我们无法回避的复杂性的一个方面。从理论上讲,我们将使用 SaaS SMS 提供商来完成这一切,但是,非洲各大 SaaS SMS 提供商的业务并没有遍及整个非洲(https://youtu.be/6tb8ALAvodM?t=196),因此在那里使用这些服务的费用都让人望而却步。如果我们利用 SaaS SMS 提供商来解决我们所有的短信需求,那么以前的那些说工程师的薪酬成本如何主导我们系统成本的说法是站不住脚的;提供电信集成服务的团队为此付出了数倍的代价。
通过将应用架构尽量简化,我们就可以将复杂性(以及人力)预算用于有利于业务发展的领域。如果没有足够的理由去提高复杂性,那么我们就可以基于尽可能简单地做事的这一想法,用少量的工程师,创建一个规模不小的业务,虽然我们所经营的非洲金融业务通常被视为难以涉足的业务,我们会在以后的文章中谈到(我们的早期和最有帮助的咨询顾问之一,他给我们提出的建议,对于 Wave 的成功非常关键,起初提出 Wave 是一个糟糕的商业点子,而创始人应该选择另一个,因为他预见到了许多潜在的困难)。
原文链接:
https://www.wave.com/en/blog/simple-architecture/index.html