一、口罩预约项目背景
穗康是广州市政府提供的一个战役小程序,于1月30号上线,最开始版本只有三个功能:个人健康上报、疫情线索上报以及医疗物资上报,后续我们快速迭代了包括口罩预约、在线问诊、健康码等等功能。
说起口罩预约的诞生背景,其实在春节假期前疫情爆发就已经初现端倪。在大年三十那天,我们团队开了电话会议,决定我们要通过互联网工具的方式帮助政府支持疫情的防控。
而小程序具有开发快,门槛低,用户易上手的特点,是最适合做这种工具的渠道。经过沟通后,广州市政府很快决定要做这样的官方战役小程序,并且在1月30号就上线了第1个版本。
广州市政府在全国首次提出用小程序来做口罩预约,这为我们带来了很大的挑战,除了时间紧迫以外,因为口罩的社会关注度非常高,民众的需求很大,对系统的稳定性要求也会很高。
其次,相对于之前第1版上线的三个功能而言,口罩预约功能的业务逻辑更加复杂。这个项目是由市政府去主导,腾讯负责产品研发,广药集团负责口罩物资的提供、调配、以及线上线下的业务运营。
在政府的指导和推进下,三方很快研讨并确认了整个的业务流程,还有初步的产品方案。在两天时间内就完成了从需求设计,到开发、测试、上线的整个过程。
二、由完美体验到有损服务
之所以说业务流程相对复杂?是因为存在这样一个线上到线下业务闭环的过程,这里会涉及到多方。
首先,广药集团的线上对接人,会在每天19点前,把当天可以预约的药店以及对应的库存数据给到我们,我们再将数据导进系统,在每天的20点开放预约。预约完之后,在每天的24点前,再把结果数据返回给广药对接人,再由他去分发到各门店,最后群众第2天就可以去门店凭预约码购买。
这就是我们收到的第1个需求版本,可以称得上是一个相对完美的无损体验版本。用户进来首先看到的是购买说明,然后进行注册,可以自动定位到用户当前所在区域,选择对应药店,同时会显示当前药店各个型号口罩的库存状态,进行选购预约。
另外考虑到可能会形成聚集,我们需要把到店购买的人群平均分配到上午和下午,所以这里也提供给用户时间段的选择,提交后会返回一个预约成功或失败的结果给到用户。
这个方案的每个步骤几乎都是实时查询、处理并返回结果的。
但是到1月30日,我们陆续从各个渠道收到反馈,发现口罩预约关注度远超预期,这个时候也开始觉得有点不妙,很多人担心口罩的需求量很大,系统会不会崩掉。
在这种情况下,我们重新调整了产品策略,决定要提升我们的性能,来扛住极有可能出现的大规模并发访问。
新的方案整体有4层缓冲:
第1层,在原来的方案里,要求能实时查询到药店的库存,而现在我们把药店、口罩等基础数据放到CDN里面,同时用户选择药店和型号时也不再做库存校验。
第2层,我们做了分批随机放行的策略,提交时前端分批随机放行,可动态设置阈值。这两个参数都是可配置的,我们可以预判流量的走势,做动态的调整。
这个策略就像我们平时去坐地铁,当遇到高峰期的时候,工作人员会拦住人进行分批放行,等到上一批乘客都已经上车之后,下一批才会放行进来,就类似这样一个策略。
第3层缓冲是在用户提交预约的时候,我们会先去一个缓存里面去判断,当前是不是已经结束预约了?结束预约有两个条件:
- 可能已经到了之前设定的预约结束时间;
- 口罩整体的库存已经用完。
所以我们可以在缓存里面做一个标记,预约结束,将状态写入缓存中。
第4层缓冲就是当用户提交之后我们其实并没有马上进行处理,而是会将请求先丢到一个队列里面,有一个后台程序会按照先进先出的机制做处理,最后返回用户最终结果。
整个策略调整下来,相比原来的无损方案来看,其实我们算是提供了一个有损的服务。
三、前后端技术方案
1. 后端设计方案
为了解决这种短时间高并发的问题,我们后端是通过三个方面的设计来应对的。
第一,借助消息队列的技术,利用Kafka进行削峰的排队处理,保证服务的响应速度。
第二,在业务数据入库的时候,采用的是顺序插入的方式去捕捉、修改和删除,最大化保证数据库文件的写入速度。
第三,我们采用了Redis来做缓存,减少数据库的并发查询操作,保证高并发状态下的查询速度。
具体来说,在我们的设计里面分了两层,一个是Preorder Servives,另一个是 Spoorder Services。Preorder Servives主要是给前端去做提交预约、登记以及预约查询等服务提供接口。可以避免在超高并发的情况下去查询数据库,而且我们也通过这种分配的技术,将数据分散到不同的分片,后续更容易实现水平的扩容。
Spoorder Services 是用来处理用户预约请求的一个服务,它通过订阅消息队列的方式去处理这些数据,将预约的结果写入数据库,另外它也会把相关的一些结果信息写到缓存里面。分层之后,对外服务的过程其实就相当于一个异步的过程了。
2. 前端设计方案
前端部分,通过各个关键节点的开关控制,动态调节服务器压力。
第一,在首页读取 CDN 配置,为应对相对较小的集群出现负载过高情况,我们将缓存时间改为10分钟。
第二,在预约说明页我们会去引导用户做自查上报,通过这种方式去分流这部分没有登记做过自查上报的用户,从而减低了口罩服务器的压力,起到削峰的作用。
第三,使用微信的 Autofill 能力。Autofill 是可以跨小程序保存用户上次填写过的一些信息,包括姓名、身份证、地址、护照等等,这样用户就可以很方便选择他上一次提交过的信息,而不需要每次再重新填写,提升了用户体验。
第四,实行分批放行,不管是放行的比例,还是用户预约失败后重置的秒数都是可以配置的。当后续发现流量瓶颈远远没达到阀值的时候,我们可以选择放开配置。
同时对于经常变动的文案、开关、配置、时间变量等等,基本都是放到了配置文件里面,这样就使得我们可以非常便捷地去做一些动态运营。
四、首日上线效果
口罩预约功能上线以后,首日访问量达1.7亿,抗住了10W+的QPS。
上线过程并不是一帆风顺的,口罩预约上线不久后,开始陆续收到同事或者朋友反馈说他们都成功预约上了。我们当时还觉得很纳闷,因为预约结果是异步处理的,我们有一个开关,等处理完后把开关打开,用户才能看得到结果。为什么我们还没有把预约结果的开关打开,他们就看得到结果呢?
一看截图才发现,原来是文案出现了问题。之前的提示文案“预约已经登记完成”很容易会误导用户,让他们以为已经预约成功。其实这里其实还有下半句:预约已登记完成,结果会稍后公布。但是因为文案太长,我们把它写到下面的小字上了。所以很多用户就误会了。
当政府知道这个消息之后,非常担心会导致线下的人群聚集。所以我们紧急开了个会,决定要通过短信的方式通知用户,让他们第2天不要去线下门店取口罩。于是我们就跟政府一起去紧急协调这种可以发短信通知的部门或渠道,最终找到了相关的部门帮我们把所有短信给发出去了。
第2天紧急调整了一些文档,明确告诉用户,现在只是排队中,同时我们后面的版本陆续增加了订阅通知,不再依赖短信,改用微信的订阅通知。同时也增加了线上支付快递到家等功能。
五、架构师如何做好有损服务
下文会着重展开介绍,什么是有损服务,我们又该如何去做好有损服务呢?
有损服务其实是腾讯海量之道的两大技术价值观之一。
腾讯经历了十几年的互联网服务运营的经验,见证了互联网从窄带到宽带,用户规模从几千万到几亿,从定制的Web1.0到以UGC为主的Web2.0高速发展期。腾讯拥有海量的服务量级,在实战过程中积累了很多成功的经验和失败的教训,由此沉淀出了腾讯的海量服务价值观。
1. 为什么要去做有损服务?
为什么要去做有损服务呢?是因为我们现在面对的是海量用户,架构师需要在用户体验和服务成本之间去做平衡。用户体验包括可用性、速度,还有一致性。服务成本,包括投入的带宽、设备,还有人力。
平衡用户体验和服务成本的原则是 CAP 理论。CAP 理论告诉我们:一个分布式系统,是不可能同时满足一致性、可用性、分区容错性这三个要素的。
三个要素里面最多只能同时满足两点,而不能兼顾。在云时代,可分布性和分区容错性已经成为了基本配置。因此作为架构师,更多要在一致性和可用性之间做取舍和平衡。
其实 CAP 理论早在20多年前就已经被学者证明了它的正确性。我们在此之上延伸出来另外两个概念。
第一是ACID,指在关系型数据库管理系统里,事物所具备的4个特性:原始性、一致性、隔离性、持久性。在数据库系统里指的就是一系列数据库操作组成的完整逻辑过程。
ACID比较典型的业务是应用于金融业,比如银行转账。用户从原账户里面扣除金额转到目标账户。增加金额过程,应当从整体考量,库存的完整逻辑过程是不可拆分的,一旦出现问题,必须回滚,这个过程就具备ACID的特性。
另外一个是 BASE 理论,BASE 理论来自于互联网,在互联网的电商领域最早被提出来,它最核心的特性就是最终一致。它的核心思想可以这样理解:即便不能达到强一致,也可以根据应用的特点,利用实践方式实现基本可用,然后提供一个柔性状态,最终达到最终一致。
在金融业、电子商务,海量UGC三种不同的业务类型当中,CAP三要素是如何取舍的呢?
首先对于金融业,很明显需要保证数据的强一致性,所以全部都要是ACID,因此需要投入巨大的IT成本。
电子商务是少量的ACID事务处理,加上大量BASE的最终一致性。比如用户在电商网站搜索商品、浏览商品,还有评论商品,其实是可以容忍数据在一定的时间窗口范围内不一致,而这类行为的并发量恰恰又非常大。高可用性是优先考虑的因素,这个时候BASE是非常合适的。
海量UGC其实基本放弃了联机交易的事务,追求的是高性能和高可用,以及用户体验。
我们用一个案例来进一步理解 CAP 理论以及最终一致性。这是一个比较典型的分布式系统的原型,是我当年做网站开发的时候,做的一个投票系统产品。它需要支持这样的需求:要求腾讯的用户能就相关的议题自由投票支持不同的观点,并将这种投票情况实时准确地反馈给用户。
开始的时候,我们根据这个要求,设计了一个相对无损的产品原型,如下图左边所示,通过跨越南北IDC分布的系统,实现投票系统,两地中间是通过一个专线连接的,实时交换投票的数据,保证数据的一致性,让用户得到一个一致的展示结果。
这个系统的并发是比较差的,性能也不高,因为它整个过程是串行的,还有跨地域的事务处理,响应会比较慢,用户体验也比较差。而且它的成本也比较高,系统依赖专线做大量的数据互相同步,需要消耗很多资源,有时候专线发生抖动或者故障,就会影响用户体验。
有没有更好的方案呢?我们尝试用 CAP 理论包括最终一致性的思路来看待整个产品,就可以获得一个新的视角。在这样一个并发量非常大的的投票系统里面,用户对投票数据的最终一致性窗口的敏感时间其实是比较长的。因为在这种高频发的投票活动中,数据无时无刻都在变化,所以我们只需要做小小的调整,就可以做出一个更好的实现。
我们先在一个区里面把用户的票数加上去,另外设置一个定时器在后台,到一定时间,再把增量数据同步到另外一个分区的机房。虽然可能用户看到的数据会不一致,但是只要这个时间间隔过了之后,他们最终看到的还是一样的结果,这就是所谓的追踪一致。
这样一个简单的流程体验的调整,既提高了系统并发的性能,也满足了系统快速响应用户请求的体验需求,降低了系统对专线的依赖,提高了系统的容错性,可以说是两全其美。
其实还可以在前端做一些优化,用户只要一点击,就先让用户看到票数的增加,给到他们及时的反馈,而真正的数据甚至可能都没有到数据库这一层。
2. 如何去做有损服务?
前文介绍了我们为什么要做有损服务,下文介绍我们如何做到有损服务?
有损服务的价值观里面有三大核心思想。
第一,放弃绝对一致,追求高可用和快速响应。在我们提供的服务里面,甚至在高并发的情况下,也要保证高可用,还有快速给用户应答。
第二,万有一失,一旦出现问题,能够让用户发起重试。
第三,伸缩调度,降级服务。伸缩调度可以说是有损服务里面的最高境界了。
简单来说,我们要在技术、产品上对业务做好这种结构的考虑,细分好业务模块,根据重要性的程度去做划分。回到口罩预约的例子中来,我们其实是放弃了实时一致,做到了快速响应。在他提交以后,告知用户已经在排队,过段时间也能看到结果,最终结果保持一致。如果用户一进来就全部给他们实时的接口,可能一开始就会导致雪崩。
另外一个从成本跟快速反应之间做权衡例子就是 QQ相册,QQ相册业务在07年到08年,处于一个快速的发展过程,一年时间内出口带宽增长了4倍,特别是在每天的7:30—9:30两个小时内,进入峰值,就如下图所示。
团队一开始是想继续保持这种刚性的系统,通过扩容增加预算,为用户提供百分百的无损体验,但是会花费巨大的带宽和存储成本。如果只是为了满足这两个小时的尖峰体验,团队可能会疲于奔命难以维持,因为它的业务发展非常快,量级也一直在增加。
在有损体验思想的指导下,团队实行了新的策略,放弃绝对一致,追求速度一致。
如下图所示:右边伪代码显示的就是这样一个策略,节省了20%的带宽,但用户在高峰期也可以快速得到页面的反馈,付出的代价仅仅是在这两个小时内,用户可能损失了快速查看下一张高品质大图的这样一点点的体验,但是换来的却是在有限的资源情况下,能提供高峰下这种一秒反应的体验。
右边部分是发送消息的场景,像IM这样的即时通讯系统,及早响应发送成功,异步尽力投递,绝大部分消息及时送达对方,极少量消息延迟或者丢弃,让用户自己重试,简单、快速,而又高效 。
就像微信那样,如果你发一个消息,在还没成功的时候,它会一直转圈圈,如果发送失败会有个小感叹号,再点击一次就能重新发布了。
在产品设计上我们也要给用户一个正向的、可感知的反馈,然后用户就会根据相关的提示来做响应的动作。其实没必要完全由系统去做这个事情,只要一失败就不断重试,用户他自己就会做,完全可以通过产品的设计来进行弥补。
再看看口罩预约,其实我们是分批放行,还有失败重试策略也是参考这个思路。
下面再来看一下伸缩调度降级服务,伸缩调度降级服务是有损服务里面的最高境界,要求我们在技术上把系统单元化、模块化,每个逻辑单元做到高内聚、低耦合。
不单单是技术上,产品上我们也要做一样的考虑,去细分业务的模块,根据重要程度去分级,承载不同的资源场景。比如服务器端的计算资源、带宽存储专线等等,实现分级控制。
以QQ相册为例,就做了非常细致的切分,它可以根据用户的接入省份、用户的等级或者现在的所处的时间等等场景,给不同的用户或针对当时的实际情况做差异化的服务,如下图所示。
Q&A
Q:英文版和中文版的小程序能预约的口罩种类不一样是真的吗?
A:当天已经处理。为了减轻对服务器的压力,可预约的口罩型号数据是生成静态文件后放到CDN的。而英文版是后上线的,和中文版是分别放在两个静态文件中,当时先更新了中文版,英文版是后更新,再加上CDN有一定缓存的时间。所以在这个间隙,就出现了中英文版不一致的情况。
Q:第二层缓冲中的分批策略前端怎么实现的呢?
A:这里的实现逻辑如下:
- cdn下发概率配置,例如:20%
- 用户点击提交预约按钮时,前端程序让用户只有20%的几率提交成功。
这样就达到了简单的分批效果
Q:请问后面换成摇号如何保障公正性的?
A:主要有三个方面来保障公正性:
- 摇号程序保证随机性。为了解决在数分钟从上千万预约登记人员中随机选取数十万人,我们设计了二次Roll点的摇号算法。保障摇号性能的同时,确保随机性。
- 引入广州公证处做公证。每晚12点前将第二天要参与摇号的数据封存。并分别同步给广药集团及广州公证处封存,已备查。
- 提供摇号后台。每天下午3点,邀请市民在公证处、电视台录制下进行摇号,并在摇号结果,在现场通过摇号后台导出封存。
Q:“穗康”口罩预约有没有考虑过安全问题?
A:有的,穗康小程序每个版本上线,都会经过两轮安全测试,一轮是腾讯内部的安全团队测试,一轮是外部的政府部门指定的安全团队测试。
Q:如果数据丢失 或者队列处理失败,有什么补偿策略吗?
A:队列处理失败会放入死信队列,由人工干预处理。
Q:不涉及支付操作的话,全用缓存,不走队列是不是也可以,数据异步持久化到DB中?
A:可以的,我们在第一个版本,用户提交的预约信息也是先放到redis里的,然后再由一个定时程序,异步写入DB。
Q:为什么要整点开始秒杀抢购,不选择随时都来预约的机制,这样可以缓解高并发不合理,随时预约也一样可以实现用户是否预约成功,为什么要大家同一时间来抢?
A:是的,这就是昨天最后提到的,换一种思路,整个方案可能都不一样了。我们后续也是改为了摇号,用户24小时都可以提交预约申请,提交一次后10天内都有效,整个并发就降下来了。
因为中间涉及到摇号程序开发、公证处规则协调等工作,耗时较长,是没办法在第一个版本上线的,所以现在回看似乎觉得抢购不合理,但这也是当时可选的唯一方案了。
Q:随机放行,也就是说先到的可能反而抢不到
A:是有这种可能,但规则是统一的,整体来说还是公平的。
Q:数据一致性怎么保证的?
A:通过一个服务保证一致性,一方面读取消息队列的数据,将数据写入数据库;另一方面将前端需要的结果数据写入缓存,供前端读取。虽然不是实时返回,但做到“最终一致”
Q:缓存预热是怎么做的?
A:通过一个服务实现,一方面读取消息队列的数据,将数据写入数据库;另一方面将前端需要的结果数据写入缓存,供前端读取。
Q:是不是前端有时候点击按钮根本就不发请求?
A:是的
Q:CDN配置可以不请求后端服务器?
A:药店、口罩型号等配置都是放到CDN,不需要请求后端服务器。只有提交预约和查看预约结果需要请求后端服务器。
Q:请问下,整个场景中,最薄弱的资源瓶颈在那一块?
A:如果不做优化策略,最薄弱的环节,是在后端的接入层,包括提交预约、读取预约结果、拉取库存信息等服务。
Q:老师,最终用了多少台服务器实现这个预约功能?
A:16台服务器
Q:实际参加开发的大概是多少人呢,包括产品,UI,研发,测试?
A:第一个版本,核心成员7人:1产品、1UI、1前端、3后台、1测试。
Q:摇号,就不需要考虑库存了?
A:也要考虑的,每天都会根据广药可以提供的口罩数量来设定库存。
Q:如果不考虑扩容的情况下,这样的策略,是可以做得很优化的。是不是也可以讲讲其它扩容思路?
A :这是在性能和成本之间的平衡,如果不考虑成本,架构图上的各个层级都可以做水平扩容的,腾讯云的服务就支持。
Q:刚才有讲到页面配置化,是怎样做的,可以具体讲讲吗?
A:将需要动态调整的文案、开关、时间配置、阀值配置都写到配置文件,并发到cdn,前端通过拉取配置来做相关控制。当然更好的方式,是通过一个配置平台来实现,并增加审批机制,避免容易出错。
Q:做压测了吗?怎么确定服务器数量?
A:有做压测,但是我们是按后端6万QPS来确认服务器数量的。
Q:前后端架构是技术架构经理是还是产品经理设计的
A:应该说是产品和技术一起设计的。
技术上,将系统单元化、模块化,逻辑单元间解耦(即高内聚、低耦合,像集成芯片一样),遵循Unix世界的KISS理念,保持接口简单、稳定。技术上能支持单元化、模块化的伸缩部署,业务功能分级。
产品上,对业务做同样的解耦考虑,细分业务模块,根据重要程度分级划分,在不同的资源场景(比如服务侧的计算、带宽、存储、专线等资源,用户侧的接入带宽、计算能力等资源)下具备分级控制、灵活组合的能力。产品设计和系统运营都要有灰度的思想,世界不是只有0、1的状态,还有0.1、0.2....0.9,用户不用只在【葡萄美酒】和【饥渴难耐】之间抉择;基于对用户需求/行为分析的细致分析理解,细分业务模型,做从基本核心需求到完美体验的细粒度拆解。