美文网首页
一周内构建分布式排行榜系统

一周内构建分布式排行榜系统

作者: Go语言由浅入深 | 来源:发表于2021-11-30 07:54 被阅读0次

    作为开发人员,经常会碰到这种情况:要开发一些服务,但没有足够的时间以想要的方式进行。现实不能允许总是推迟,因为“上市时间”有时对产品的成功起着至关重要的作用。那么我们该怎么办呢?答案是走捷径,承担一定的风险,放弃追求完美(例如追求最佳实践,单元测试覆盖率,等等)。我想这对大多数人来说都非常相似的。

    今天的故事是关于在一个三人团队中,如何用大约一周的时间构建一个大规模、可容错的分布式排行榜/评分系统。

    本文我们将学习到以下内容:

    • 技术方面:如何构建一个包含分布式系统设计、可伸缩性、弹性和可用性等特点的系统。
    • 工作时间:如何在短时间内完成工作,做出取舍,如何更快地做决定。
    • 开发周期:你将面临产品开发周期很短的情况,以及构建好的软件需要做些什么。
      所以跟着我,保证不会让你失望🤗

    1、需求

    这一切都始于我们的产品团队说现在是T20世界杯赛季,我们想为我们的用户提供一个在线赛事预测服务,用于短期预测。



    场景是:在问答开始时,要求用户将回答问题,时间只有20/30秒。最后,主持人会提交答案,这个过程中实际发生了什么。评分的依据是回答是否正确以及花了多少时间。

    所以在得到需求后,我们立即做了工作量估计,一周内完成产品开发显然非常紧急,所以我们削减一些功能。这一点很重要,因为在一个紧迫的期限内,你应该提供更少的功能,而不是提供半成品功能。

    本文主要关注后端,抱歉,不能解释这些漂亮的UI是如何制作的。

    2、设计

    接下来是设计系统。对我来说,这是最具挑战性也是最有趣的部分。我们没有进行大量的底层设计,而是在实现时动态地进行所有建模调用。

    第一步:规模评估

    在开始之前,我们做了一个粗略的估计,系统面临多大的流量。考虑到我们系统的规模,估计可能会有5万人参与测试。所以我们的目标是面向10万用户。但大多数人可能会在30秒内的前10秒内回答问题。所以我们的目标qps大概是2万。

    我的经验是,对于一个新系统,设计系统时总是要考虑预测用户数量的两倍。因此,如果你最终拥有更多的用户,系统就不会无法扩展。用户通常会随着时间增长,你不需要不断地改变系统。但这并不意味着在实际需要的地方投入两倍的基础设施。我通常会在系统中启用自动伸缩功能,这样当出现流量峰值时,系统就会自动伸缩。

    第2步:大规模API

    我们很快确定了提交答案是大规模API,处理过程可能会很长,这样就可以有选择地为这些API进行可伸缩的设计。剩下的小规模API我们不关心也不花太多时间。
    以下是我们确定的大规模被调用的API:
    1、获取用户分数和排名
    2、获取排行榜
    3、计算排行榜(主要是数据处理,而不是API)
    4、提交用户答案
    我们马上着手设计。

    第3步:设计要素

    在看到需求之后,有两件事对我来说非常明显。
    1、我们需要异步和分布式处理去计算用户分数和排行榜。如果您计划在单个节点上为100万或以上用户同步运行分数计算,那么祝您好运😏。这个问题的解决方案很简单,我们把一个大任务分成几个小任务,然后在不同的节点上运行,让它们按照自己的节奏来运行。同时我们也希望这个过程是容错的。
    2、缓存将成为我们阅读用户排名和排行榜的好帮手。主要有两个原因:速度和易于扩展。对于简单的读取操作,缓存要比DB快得多,通常来说,Redis/ Memcached/ Hazelcast集群比Postgres更容易扩展。
    所以我想主要围绕这些方面来设计系统。

    第4步:系统设计

    这次在做设计决策时,我选择使用我所知道的技术,而不想冒险去寻找适合这个项目的最佳实践。例如,我熟悉Redis,所以对于缓存,它是显而易见的选择。而且,每当我听到排行榜,它就会在我的脑海中自动转换成Redis有序集合。对于异步处理,Kafka仍然是我的首选。如果有时间,我可能会做更多的探索,但这一次,不会徘徊在寻找最好的技术上面,因为没有时间!!!!

    a、提交用户答案API

    这个API让用户提交测试答案。这里的主要挑战是会产生大量并发的写操作,这、将大大增加数据库的负载。所以我们必须做两件事:
    1、降低DB上的负载
    2、创建某种back-pressure,或者让数据库操作人员按照自己的节奏工作,这样数据库就不会被压垮

    如果你不知道什么是back-pressure可以阅读这篇文章

    减少写负载的最佳解决方案是批处理。因此,如果我必须做10个写操作,我将它们组合成批处理,然后运行一个数据库写操作。

    并且Back-pressure几乎就是等同于提示使用消息队列。

    所以结合两者,我们想出的解决方案是:



    别慌,让我解释一下这个框架图:
    1、Response reciever接收用户请求,并返回202 HTTP状态码。这就好比说,我收到了你的请求,我会处理它,但你继续做你该做的事。这是异步处理的第一步,不会阻塞调用者。
    2、Response reciever将用户请求放入消息队列中,消息队列出于可伸缩性/可用性/冗余目的进行了分区。如果你已经熟悉Kafka,可以很容易地理解分区。如果不熟悉,可以将它看作是一种将队列中的消息分发到多个较小的隔离队列中的方法,这些队列在技术上可以存储在不同的节点上。如果你知道DB分片,那么效果类似,但这里是针对消息队列。
    3、现在,Batcher服务将接收这些原始请求数据。然后它创建包含10条请求消息的批处理,并将它们推到下一个处理阶段。
    4、DB writer获取批处理数据并对DB进行插入。后端压力主要是由DB writer引入的,它以自己的处理速度接收消息,因为Batcher服务是基于拉的方式获取消息。因此,防止了数据库过载。而且由于它是批量工作,运行100个DB插入,我们只执行10个DB操作。
    由于在这里使用的是消息队列,加上不丢失用户请求的异步性,我们有内置重试能力,它还将负载分布到多个消费者。在需要的时候,我们可以将消息队列与服务一起扩展。
    这一步解决了30%的问题,让我们继续下一个。

    b、计算分数和排行榜

    现在的问题是,排行榜的计算。如果你看到这个系统,它不像传统的测试系统,事先知道正确答案。所以我们不能在用户回答问题后立即计算分数和排行榜。一旦主持人提交了正确的答案,才计算所有用户的分数和排名。所以一次要完成了大量的工作。很明显,无论是我们的节点还是我们的DB服务器都不能以同步方式来处理。那么我们该怎么办呢?我们再次回到Messages Queues进行异步处理。我们可以异步计算分数,但积分排行榜呢?这需要一直都是可用的,对吗?那么用户等级呢?除非所有用户的分数都计算出来否则你就不能真正地排名,对吧?具体计算排名可能是一个难题。

    现在,谁来拯救我们?不要担心,还记得我在谈到缓存时简要提到过Redis吗?Redis有一个数据类型:有序集合(多么神奇的创造,感谢Redis Labs😅)。在一个有序集合中,你可以添加带分数的键,Redis会按照O(log(N))对其进行排序。这将解决我们的排名问题😉。它还允许我们执行范围查询,比如给出前5名,或者得到特定键的排名,所有这些时间复杂度为O(log(N))。这正是我们需要的。

    啊哈,排行榜问题也解决了。
    这并不容易,我只是很高兴能够很快得到一个可行的解决方案。现在回到我们的架构图。



    看起来有点复杂,我们来解释下:
    1、一旦主持人提交了正确的答案,就会推送一个触发计算分数消息,这将启动整个处理流水线。
    2、Batcher接收到触发消息,生成一对数据库offset和limit对象,这个取决于多少人回答正确。例如,如果10个人回答正确,批处理大小为5的话,生成两个对象。批处理1{offset:0, limit:5},批处理2{offset:5, limit:5}。为什么这么做呢?我们可以运行批处理或分页DB查询,我们不会在没有任何限制的情况下调用DB。所以如果我要从DB中获得100万条记录,一次读取的话会在很多地方带来各种问题。因此,我们将其分解为更小的部分,并运行更小但多个查询,这将返回更少的行数。
    3、User batch processor程序将接收这些批处理消息,并相应地运行DB查询。接收到{offset: 0, limit: 5}消息,处理程序将从DB中获取前5个用户id(也将执行另一个批处理操作,但这里有点难以解释,因此跳过)。在这之后,我们将告别批处理,转向流处理。因为批处理程序现在将5个用户id放入队列中,该队列将由下一个处理器处理。
    4、现在, User score calculator接收单个用户id,运行评分逻辑来计算单个用户的分数。然后进行一次DB更新修改用户评分。然后,它会更新redis有序集合中用户的分数,Redis会在内部分更新排名。一旦这个阶段完成,我们将在数据库中得到所有用户的分数,并在Redis中得到所有用户的排名+分数。由于有序集合中包含每个人的排名,可以对其运行范围查询,以极快的速度获得排行榜。
    如果Redis集群故障,因为有在DB中维护了用户分数,我们可以触发分数计算流程,并在Redis上再次构建有序集合。实现弹性服务😬

    我们的大部分问题现在已经解决了,为了得到用户的分数和排名,可以只做一个Redis查询,而不用查询DB。这也使得服务反应速度更快。
    设计阶段就到这里。还有DB、API设计和其他东西,我在这里跳过。

    4、实现

    完成设计就解决了70%的问题,我们知道能解决所有问题,所以迅速进入开发阶段。

    由于时间紧迫,我们使用最拿手的语言完成开发,除此之外,我们还不得不减少单元测试和集成测试,这一问题至今仍困扰着我们。但我们会在发布后逐步回填测试。

    我猜在看到上面张贴的详细设计后,你应该能够实现自己的服务,因此就不做代码的深入了。

    5、部署和监控

    在修复了一些bug并结束了QA之后,我们便可以开始了提供服务了。但工作都完成了吗?不,还得设置监控。因为它是在很短的时间内开发出来的,我有点缺乏自信。默认情况下,我们通过LightStep在该服务上启用了跟踪。除了跟踪之外,我还设置了针对所有API的流量、错误率、延迟和警报的专用监控。上线后,我们对系统进行了至少一个小时的监控,从内存和CPU的使用到错误日志。所以一定要给可观测性和监控同等的权重。在生产系统上有一些小问题,使用监控,能及早发现它们。

    6、回溯

    这个系统是有效的,但在短暂的休息之后,做一个回顾,找出我们错过的东西并进行改进是很重要的。我敢肯定我们错过了很多东西,走了很多捷径,得到了很大的改进。例如,这里有一些…

    可改进地方

    1、使用Postgres DB,因为驱动程序、ORM和支持基础设施都已经存在了。但我可能会探讨一些数据库解决方案。
    2、NodeJS很棒,但我觉得Go会是更好的解决方案。我们本可以探索这个的。
    3、我试着写一个查询来计算DB中的分数,但不幸失败了。我可以那样写,为分数计算做批处理,进一步减少DB操作。
    4、我们无法运行大量的负载和性能测试,但这是必须的。
    5、我们可以为DB更新和Redis排序集更新分两个不同的阶段,这将是一个更简洁的实现。
    这是我想到的,可能还有更多。

    我们在这么短的时间内尽了最大的努力。甚至实现也略微偏离了最初的设计。但没关系,总会有权衡的时候。尽管我们在大约一周的时间内完成了核心功能,但我们还是需要打一些小补丁。

    相关文章

      网友评论

          本文标题:一周内构建分布式排行榜系统

          本文链接:https://www.haomeiwen.com/subject/filrxrtx.html