一个每秒超过3万请求的微服务开发经历

导读:本文介绍的是一个国外的外卖平台 ifood 的微服务案例。

ifood 是一家巴西外卖平台公司,平均每天送出 100 多万份订单,每年增长 110% 左右。作为一家外卖平台,访问峰值大多出现在午餐和晚餐前后,周末的时候会更高一些。

在一些特殊的日子里(比如由于营销活动),访问量曾打破记录,平台获得了历史最高峰值,去年 6 月 12 日。我们一个微服务达到了每分钟 20 0万请求的峰值。

故事背景

我在公司平台部门的账号与身份团队工作了大约一年半的时间,这是一个相当长的旅程,由于公司的快速发展,我们时常会面临很多挑战。在设计新的解决方案时,我们总是要牢记这样一个想法:在几个月后,系统的使用量会增长 2 - 3 倍。

今天要讲的故事就是上面案例的一种情况,这个系统是在 2018 年左右开发的,当时公司每个月的订单量是 1300 万。如今已经超过 3000 万了。在这个案例中,系统的使用量是随着公司用户的增长比例而增长的,当然后来增长速度更加迅猛。

在内部,我们把这个微服务称为账号元数据。尽管这是一个通用的名字,但它也解释了这个服务的目的:它处理账号的元数据。什么是账号元数据?主要是指那些非关键的用户信息。举个例子:如果用户喜欢通过短信或邮件收到通知,喜欢的食物类型(比如汉堡、意大利面、日本料理等),一些功能标志,为该用户做的订单数量等等。它就像一个通用的存储,把不同地方的数据汇总起来,方便地服务于客户端调用,同时也服务于其他微服务,这样他们只需要调用一个微服务,而不是 10 个微服务。

最早在 2018 年时,账号元数据的建立主要是为了放一些杂乱的(并不怎么用的)信息,说实话,没有其他地方可以放。我们需要一点结构和查询能力,而且很容易扩展,所以我们选择了 AWS 提供的 DynamoDB。在这里要说明一下,我们明白系统可能会增长,当时公司也已经相当大了,平均负载是有挑战的。但是,我们还是没有预估到,我们会从每分钟 1 万个请求增长到 20 万,然后最终达到了 200万。

这个微服务刚发布后,并没有多少人使用(与账号团队的其他微服务相比)。然而,几周后做出的一些新的架构调整,让这个系统变得非常重要,它将成为客户端获取用户所有信息的首批调用目标之一。

在此之后的几个月,其他团队开始把账号元数据看作是一个很好的功能,可以把分散存储在多个地方的信息都搬来这里,依赖多个服务毕竟很麻烦。另外,我们开始创建更多的聚合,让其他微服务的调用变得非常简单,也让其他团队更多的了解了它的知名度和重要性。现在,账号元数据除了用户每次打开应用的时候都会被调用,而且被很多团队在很多其他不同的场景下访问。

上面就是一个非常简单的总结,介绍了从 2018 年到现在发生的事情,系统为何变得如此重要。在这期间,团队(我加上八个非常优秀的同事,非常幸运地与他们一起工作)积极地进行了工作,但工作并未停止,我们仍然在为我们负责的其他十个微服务进行优化、开发以及维护。

我们做了一大堆的改动,如果要展开我们经历的所有场景,会花费太多时间,所以我只把当前要说的这个架构描述清楚,我们需要能够稳定地每分钟处理 200万个请求。是时候深入到技术部分了。

深入技术

正如前面所说,这个微服务存储了账号的元数据。在数据库中,我们将这些元数据分割成不同的上下文(在代码中称之为 namespace 命名空间)。一个客户(customer_id 作为分区键)可以有 1 到 N 个 namespace(作为排序键),而且每个 namespace 都有一个固定的、强制性的 schema,插入前通过  jsonschema 来定义和检查。有了它,我们就可以确保无论如何将数据插入到哪个 namespace 中(后面会有更多的细节),都会遵从它的模式和正确的用法。

我们使用这种方法,是因为这个系统中,读和写是由不同的团队来完成的。

插入工作是由数据科学团队完成的,他们每天都会从内部工具中导出数百万条记录到这个微服务中,并通过 API 将这数百万条记录分割成每次 500 条进行批量调用。所以,一天中的某个特定时间,这个微服务会收到数百万次的调用(间隔 10 到 20 分钟),将数据插入到 DynamoDB 中。如果接收 API 直接将数据写入数据库,就会碰到 Dynamo 扩展的一些问题,而且响应时间过慢也是个问题。解决这个瓶颈的方法是数据团队直接将数据写入数据库,但是,我们必须检查这些记录是否符合命名对应空间的 jsonschema,这是此微服务的责任。

所以解决方案是,API 接收这批记录,并将它们发布在 SNS/SQS 消息队列上,而 SNS/SQS 将被另外一个模块来消费,然后验证这些记录,如果没问题,就保存在 Dynamo 上。通过这种方式,接收到这批记录的接口可以非常快速地响应,我们不依赖 HTTP 连接进行写入(这一点相当重要,因为与 Dynamo 的通信可能会失败,再次尝试可能会使 HTTP 响应时间变得非常慢)。另一个好处是,我们可以通过调整队列消费程序,来控制从 SQS 读取数据以及在 Dynamo 上写入数据的快慢。

在这个流程之外,账号元数据也会被另一个服务调用,每当平台收到一个新订单就会调用它,并更新这个订单的一些信息。鉴于 ifood 每天订单量超过 100 多万,微服务也会接受到这个数量的调用。

虽然上面提到有一个非常繁重的写的过程,但是这个服务 95% 的负载来自于 API 调用读取的请求。前面也提到,读写数据是由公司不同的团队完成的,读的调用牵涉到非常多的团队,包括客户端的调用。比较幸运的是,这个微服务读请求要比写请求多得多,因此它的扩展就更容易一些。因为任何一个大量读取数据的系统都需要一个缓存,这个也是如此,AWS 没有使用 Redis 之类的东西,而是提供了 DAX 作为 DynamoDB 的 "内置" 缓存。要使用它,你只需要让客户端理解不同查询操作中可能存在有复制延迟。

在这样的调用量下,出现一些异常的情况也是很正常的。在我们的案例中,我们看到 Dynamo 中的一些查询耗时超过 2 - 3 秒,而 99.99% 的调用都在 17ms 以下。尽管这些慢查询每天只有几千次,但我们希望为团队提供更好的 SLA。所以我们决定如果碰到 Dynamo 超时就进行重试。也相关团队也讨论过,让他们在调用我们的 API 时配置一个低超时。他们大多数 HTTP 客户端的默认时间是 2s,所以我们改成了大约 100ms。如果他们碰到超时(比方说微服务对 dynamo 做了重试,但又失败了),他们可以重试,并且很可能会马上得到响应。

为了部署它,我们使用 k8s(达到 70 个左右的 pod),并随着每秒请求的增长而进行扩展。DynamoDB 被设置为供应(provision)而非按需。

一个重要的步骤是确保系统能够在真正高吞吐量的情况下健康地工作,我们每天对它进行负载/压力测试,以确保新版本部署没有降低性能。通过这个负载测试的结果,我们可以跟踪一个接口是随着时间的推移和它的发展而变好还是变坏。

随着时间的推移,这个微服务变得越来越重要,如果由于某些原因出现故障,那就是一个不可承受的问题。为了解决这个问题,我们要求团队通过 Kong(我们的 API 网关)来调用微服务,并在那里配置了一个 fallback。如果微服务宕机或返回 500,Kong 会激活回调,客户端会得到一个默认结果。在这种情况下,fallback 目前指向一个 S3 bucket,里面有系统会提供的数据副本。它可能是一些过时数据,但这总比不返回任何数据要好。

总结

最后总结一下,本文简单描述了一个高性能微服务工作方式。虽然微服务还包括一些其他的工作流程,跟主题无关就不展开赘述。

再谈下微服务接下来的工作,尽管目前还不是完全清晰。微服务的使用量可能会更多,我们可能会达到一个点,开始变得越来越难让它 scale。一个替代方案可能是将其拆分成不同的微服务(甚至可能使用不同的数据库),或者聚合更多的数据以更好地服务于他们。不管如何,我们还是会不断地测试,找到瓶颈,并持续优化它们。

英文原文:

https://medium.com/swlh/developing-a-microservice-to-handle-over-30k-requests-per-second-at-ifood-3e2d7b822b0e

参考阅读:

本文由高可用架构翻译,技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。

高可用架构

改变互联网的构建方式


长按二维码 关注「高可用架构」公众号


版权声明:本文为weixin_45583158原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。