今天,我打算以直播互动作为例子,引领大家一同去了解在面对读多写多的情况时,应当怎样去应对所产生的流量压力。通常而言,这类服务在多数情况下都属于实时互动服务。由于其对时效性有着极高的要求,这就致使在许多场景当中,我们没办法借助读缓存的方式来减轻核心数据所承受的压力。
那么,为了有效降低这类互动服务器所面临的压力,我们能够从架构层面着手,开展一些具有灵活性的拆分操作,并对其进行相应的设计改造。
实际上,这些设计是通过混合的方式来实现对外提供服务的。为了能够让大家更为清晰地理解这其中的原理,我将会针对直播互动里特定的一些场景展开详细讲解。
一般来讲,直播场景是可以被划分成两种不同类型的,即可以预估用户量的场景以及不可预估用户量的场景。这两种场景下的设计存在着很大的差异,接下来,我们就分别对它们进行深入的探讨。
可预估用户量的服务:游戏创建房间
想必不少热衷于对战游戏的小伙伴都曾有过这样的经历:在联网玩游戏时,首先得创建房间才行。这种游戏设计方式呢,主要是依靠设定一台服务器所能开启的房间数量上限,以此来对一台服务器可同时服务的用户数量加以限制。
接下来,咱们从服务器端的资源分配这个角度出发,深入剖析一下创建房间这一设计究竟是怎样进行资源调配的。
当房间创建完成之后,用户凭借房间号便能够邀请其他伙伴加入游戏,从而展开对战。无论是房主呢,还是后续加入的伙伴,都会依据房间的标识,由调度服务统一安排到同一服务集群之上,进而开展互动交流。
这里得给大家提个醒哦,开房间这个动作并非一定要由游戏用户亲自去完成呀,我们完全可以将其设置成用户开启游戏之时就自动为其分配房间。如此一来,不但能够提前对用户量进行预估,而且还能极为出色地对我们的服务资源加以规划与掌控呢。
那么,要怎样去评估一台服务器能够支持多少人同时在线呢?其实呀,我们可以通过压力测试的方法,测出单台服务器所能服务的在线人数,进而依据这个数据来精确地预估所需要的带宽以及服务器资源,从而算出一个集群(要知道,这个集群可是包含了若干台服务器哦)究竟需要多少资源,又能够承担多少人在线开展互动活动。最后呢,再借助调度服务来对资源进行分配,把新来的房主分配到尚有空闲的服务集群当中。
下面给大家展示一下最终的实现效果:
图片
就像前面所展示的那样,在创建房间的这个阶段呀,我们的客户端在进入区域服务器集群之前呢,都是依靠向调度服务发起请求,进而由调度服务来完成相应调度工作的。
调度服务器会按照一定的周期,去接收来自各个组服务器的服务用户在线情况方面的信息哦。通过对这些信息的分析与处理,调度服务器就能够评估出究竟需要调配多少用户进入到不同的区域集群当中啦。
与此同时呢,客户端在收到调度指令之后呀,会拿着调度服务所给予的 token,前往不同的区域去申请创建房间呢。
等到房间成功创建之后呀,调度服务就会在本地的集群内部,对这个房间的列表以及相关信息进行维护管理哦。这些信息呢,还会提供给其他那些想要加入游戏的玩家进行查看展示呢。
而那些后续加入游戏的玩家呀,同样也会接入到对应房间所在的区域服务器当中,从而能够和房主以及同房间的其他玩家开展实时的互动交流呢。这种通过限定配额房间个数的方式来进行服务器资源调度的设计呀,可不单单是在对战游戏里面才会用到哦,在很多其他的场景当中呀,也都采用了类似的设计呢,就比如说在线小课堂这类涉及教学互动的场景呀。
我们完全可以预见到呀,通过采用这样的设计呢,就能够对资源实现精准的把控啦,如此一来,用户的数量也就不会超出我们服务器所设计的容量范围啦。
不可预估用户量的服务
然而呢,在很多场景当中,情况是具有随机性的,我们根本没办法确切地把握会有多少用户进入到这个服务器来进行互动交流。就拿全国直播来说吧,根本就无法确定究竟会有多少用户来访问呀。
鉴于这种情况呢,很多直播服务首先会依据主播过往的情况来预测用户量哦。通过对这个预估量的分析呢,提前把他们的直播安排到相对比较空闲的服务器群组里面。同时呢,还会提前准备好一些调度工具哦,比如说通过控制曝光度的方式来延缓用户进入直播。通过这样的操作呢,就能够为服务器调度争取到更多的时间,以便进行动态扩容啦。
由于这一类服务没办法预估到底会有多少用户,所以之前那种服务器小组的模式呀,并不适用于这种情况哦,而是需要更高级别的调度呢。下面我们来分析一下这个场景哦。对于直播而言呢,用户常见的交互形式包含了聊天、答题、点赞、打赏以及购物等等。考虑到这些交互形式各自具有不同的特点,我们接下来针对不同的关键点依次进行分析。
聊天:信息合并
聊天的内容普遍比较短,为了提高吞吐能力,通常会把用户的聊天内容放入分布式队列做传输,这样能延缓写入压力。另外,在点赞或大量用户输入同样内容的刷屏情境下,我们可以通过大数据实时计算分析用户的输入,并压缩整理大量重复的内容,过滤掉一些无用信息。
压缩整理后的聊天内容会被分发到多个聊天内容分发服务器上,直播间内用户的聊天长连接会收到消息更新的推送通知,接着客户端会到指定的内容分发服务器群组里批量拉取数据,拿到数据后会根据时间顺序来回放。请注意,这个方式只适合用在疯狂刷屏的情况,如果用户量很少可以通过长链接进行实时互动。
答题:瞬时信息拉取高峰
除了交互流量极大的聊天互动信息之外,还存在一些特殊的互动形式,比如做题互动。在直播间里,当老师发送一个题目时,题目消息会被广播给所有用户,而客户端收到消息后会从服务端拉取题目的数据。
想象一下,如果有 10 万用户同时在线,那么很有可能会出现瞬间有 10 万人在线同时请求服务端拉取题目的情况。如此庞大的数据请求量,若要承受得住,就需要我们投入大量的服务器和带宽资源,但这样做的性价比其实并不高。
从理论上来说,我们可以将数据静态化,并通过 CDN 来阻挡这个流量。然而,为了避免出现瞬时的高峰情况,推荐在客户端拉取时加入随机延迟几秒的操作,然后再发送请求。这样做能够大大延缓服务器的压力,从而获得更好的用户体验。
请务必牢记,对于客户端而言,如果这种服务请求失败了,就不要频繁地进行请求重试,否则会将服务端 “打沉”。如果确实必须要进行重试,那么建议对重试的时间采用退火算法。通过这样的方式,可以保证服务端不会因为一时的故障而收到大量的请求,进而避免服务器崩溃。
如果是在教学场景的直播中,有两个可以缓解服务器压力的技巧。
第一个技巧是在上课当天,把抢答题目提前交给客户端做预加载下载。这样一来,就能够减少实时拉取的压力。
第二个方式是针对题目抢答的情况。当老师发布题目的时候,提前设定发送动作生效后 5 秒再弹出题目。如此操作,能够让所有直播用户的接收端 “准时” 地收到题目信息,而不至于出现用户题目接收时间不一致的情况。
至于非抢答类型的题目,当用户回答完题目后,我们可以先在客户端本地先做预判卷,把正确答案和解析展示给用户。然后,在直播期间异步缓慢地将用户答题结果提交到服务端。通过这样的方式,能够保证服务器不会因用户瞬时的流量而被冲垮。
点赞:客户端互动合并
接下来,针对点赞的场景,我打算从客户端以及服务端这两个不同的角度来为大家详细介绍一下。
咱们先来看客户端这边的情况。在很多时候呀,客户端其实并不需要实时地去提交用户所做出的全部交互动作哦。这是因为呀,有不少交互动作属于那种机械性的重复动作,它们对于实时性的要求并没有那么高呢。
给大家举个例子吧,比如说用户在本地特别快速地连续点击了 100 下赞,在这种情况下呢,客户端就完全可以把这些点赞操作合并起来,将其转化为一条消息进行处理呀,就好比是 “用户在 3 秒内点赞 10 次” 这样的表述形式。
我相信呀,像大家这么聪明的人,肯定能够把这种将互动动作进行合并的小妙招运用到更多的情景当中去哦。比如说,当用户连续打赏 100 个礼物的时候,同样也可以采用这样的方式来处理呀。
通过运用这种方式呢,能够极大幅度地降低服务器所承受的压力哦。这样一来呀,既可以确保直播间依旧保持那种火爆的氛围,同时呢,还能够节省下大量的流量资源呢,这可真是一件一举多得的好事呀,大家何乐而不为呢?
点赞:服务端树形多层汇总架构
我们回头再看看点赞的场景下,如何设计服务端才能缓解请求压力。如果我们的集群 QPS 超过十万,服务端数据层已经无法承受这样的压力时,如何应对高并发写、高并发读呢?微博做过一个类似的案例,用途是缓解用户的点赞请求流量,这种方式适合一致性要求不高的计数器,如下图所示:
图片
这个方式可以将用户点赞流量随机压到不同的写缓存服务上,通过第一层写缓存本地的实时汇总来缓解大量用户的请求,将更新数据周期性地汇总后,提交到二级写缓存。之后,二级汇总所在分片的所有上层服务数值后,最终汇总同步给核心缓存服务。接着,通过核心缓存把最终结果汇总累加起来。最后通过主从复制到多个子查询节点服务,供用户查询汇总结果。
打赏 & 购物:服务端分片及分片实时扩容
前面的互动只要保证最终一致性就可以,但打赏和购物的场景下,库存和金额需要提供事务一致性的服务。因为事务一致性的要求,这种服务我们不能做成多层缓冲方式提供服务,而且这种服务的数据特征是读多写多,所以我们可以通过数据分片方式实现这一类服务,如下图:
图片
看过图之后,是不是感觉理解起来轻松多了呀?下面我再详细说一说哦。
我们可以依据用户的 id 来进行 hash 拆分操作呢。具体做法是,通过网关把不同用户的 uid 进行取模处理,然后按照取模所得的数值范围,将用户分配到不同的分片服务上去。之后呢,处于各个分片内的服务就会针对类似的请求开展内存实时计算更新的工作啦。
通过采用这样的方式呀,能够较为快速且便捷地实现负载的切分哦。不过呢,这种 hash 分配的方式也存在一定的弊端哦,那就是容易出现个别热点的情况呢。当我们面临的流量压力大到服务器扛不住的时候呀,就需要对服务器进行扩容处理啦。
而且呀,要是采用 hash 这种方式,一旦出现个别服务器发生故障的情况,就会导致 hash 映射出现错误哦,这样一来,请求就可能会被发送到错误的分片上去啦。
针对这些问题呀,其实是有很多类似的解决方案的哦。比如说一致性 hash 算法吧,这种算法的优势在于它可以针对局部的区域进行扩容操作,而且不会对整个集群的分片造成影响哦。但是呢,这个方法在很多时候呀,由于其算法本身不够通用,并且无法由人来进行有效控制,所以使用起来就会显得特别麻烦呢,还需要专门开发配套的工具才行哦。
除此之外呀,我再给大家推荐另外一种方式哦,那就是树形热迁移切片法啦。这是一种类似于虚拟桶的方式哦。打个比方来说吧,我们可以把全量数据拆分成 256 份呀,每一份就代表一个桶哦。假如有 16 个服务器的话,那么每个服务器就可以分到 16 个桶啦。
当我们发现个别服务器的压力过大的时候呀,就可以给这个服务器增加两个订阅服务器哦,让它们去做主从同步的工作呢,也就是把这个服务器上的 16 个桶的数据进行迁移操作啦。
等到同步迁移的工作成功完成之后呀,就可以把原本发送到这个服务器的请求流量进行拆分处理啦,然后分别转发到两个各有 8 个桶的服务器上去哦。之后呢,就让这两个订阅服务器分别接收请求并继续对外提供服务啦,而原来那个压力过大的服务器呢,就可以把它摘除并进行回收处理啦。
在服务成功完成切换之后呀,因为进行的是全量迁移,所以这两个新的服务会同时同步到原本并不属于它们各自的 8 个桶的数据哦。在这种情况下呢,新服务器只需要去遍历自己所存储的数据,然后把那些不属于自己的数据给删除掉就可以啦。
当然啦,还有另外一种做法哦,那就是在同步来自 16 桶服务的数据的时候呢,就直接把那些不属于自身的相关数据给过滤掉呀。需要说明的是,这个方法对于 Redis、MySQL 等所有存在有状态分片数据的服务来说,都是适用的哦。
不过呢,这个服务存在一个难点哦,那就是请求的客户端并不会直接去请求分片哦,而是要通过代理服务来对数据服务发起请求呢。只有借助代理服务呀,才能够实现对调度流量进行动态更新,进而达到平滑且无损地转发流量的目的哦。
最后呀,咱们再来探讨一下这样一个问题哦,那就是如何让客户端知道应该去请求哪个分片才能够找到它所需要的数据呢?在这里呀,我给大家分享两种比较常见的方式哦。
第一种方式是这样的哦,客户端可以通过特定的算法来找到分片哦。比如说呢,可以采用这样的算法:用户 hash (uid) % 100 = 桶 id 哦。然后呢,在配置文件当中,通过这个桶 id 就能够找到与之对应的分片啦。
第二种方式则是呢,当数据服务端接收到客户端的请求之后呀,会把这个请求转发到存有相关数据的分片那里哦。比如说吧,客户端一开始请求的是 A 分片,然后再根据相应的数据算法以及对应的分片配置,发现所需要的数据其实是在 B 分片那里哦。这个时候呢,A 分片就会把这个请求转发到 B 分片哦。等到 B 分片处理完这个请求之后呢,就会把数据返回给客户端啦(这里的数据返回方式呢,是由 A 返回还是由 B 返回,这就要取决于客户端是进行跳转操作还是由服务端来进行转发操作啦)。
服务降级:分布式队列汇总缓冲
即使通过这么多技术来优化架构,我们的服务仍旧无法完全承受过高的瞬发流量。对于这种情况,我们可以做一些服务降级的操作,通过队列将修改合并或做网关限流。虽然这会牺牲一些实时性,但是实际上,很多数字可能没有我们想象中那么重要。像微博的点赞统计数据,如果客户端点赞无法请求到服务器,那么这些数据会在客户端暂存一段时间,在用户看数据时看到的只是短期历史数字,不是实时数字。十万零五的点赞数跟十万零三千的点赞数,差异并不大,等之后服务器有空闲了,结果追上来最终是一致的。但作为降级方案,这么做能节省大量的服务器资源,也算是个好方法。