Slack针对加载时间的重构实践介绍

Slack的前Staff Engineer Bing Wei在2018年的一次讲座中详细介绍了Slack是如何通过重构来进行优化的,本文尝试从笔者理解的角度讲一讲她是如何在slack上进行相应的重构的。

问题简介

首先我们简单介绍一下Slack,相信很多人都用过,它说白了就是有各种不同主题的channel,大家可以在里面进行交流,分享文件,查找内容等,如下图所示:

在slack早期的时候,每个team的人数量并不是很多,最多只有几百几千人,而随着时间的推移,就出现越来越多的大team,下图是Slack上最大team人数的变迁:

我们可以看到在15年的时候,最大的team也只有8000人左右,而到了18年的时候最大的team人数已经到了266000人,这已经不是一个量级了,多了30+倍。也正是这种变化导致了原本的一些系统设计出现了问题,而其中比较突出的问题就是Slack的初始连接变得很慢,我们来仔细看看为什么会出现这个问题以及Slack是如何解决的。

初始连接慢

在分析问题之前,我们先来看看原来的design是怎样的,也就是用户开始访问Slack的时候会做些什么,在2015年的时候,整个流程如下所示:

这个流程总得来说比较简单,就是用户在启动Slack的时候会调用一个HTTPS request,然后server端做一些检查(比如token),再返回一个team的snapshot给client端,最后就是建立一个long-live的WebSocket connection,这样client端就可以实时收到team的消息了。

我们这里说的初始连接慢就是指整个connect time比较长,这个connect time是指从https发出去到整个WebSocket连接建立完成的时间。其实很容易就想到,随着team人数的增大,这个snapshot of the team的数据量就会变大,从而使得整个connect time变大。这个snapshot包含了很多信息,比如有哪些人在你的team里面,以及你自己有多少个channel等信息,下表是snapshot大小和这两个主要因素之间的关系:

我们可以看到当team中用户变多,或者你所在的channel变多的时候,snapshot的大小会有一个量级的变化,达到了大几十M的大小。

这时候也许你会问我们真的需要在初始化的时候就加载这么多snapshot的信息吗?这个决定其实在最初的Slack的设计里面是很好的,因为那个时候整个snapshot的大小不大,在一次性加载了所有的信息之后,client端任何别的操作需要的meta data都已经获取了,可以不需要额外进行任何获取就能实现各种功能了。对于这个snapshot的处理,Slack在发现有问题的时候也尝试了一些优化手段,比如只返回一些必要的数据域,修改返回的格式以便更好进行压缩等等来手段来缩小这个大小(这里强调一下,在我们决定做一些大的架构改变之前,最好做一些尝试,假如能通过更简单的手段来达到目的,那么所谓fancy的架构其实是不需要的)。然而不幸的是,这些手段终归是有一些极限,在某一个时间之后这个size就越来越靠近理论的极限大小了,这个时候需要做的就是从架构上来思考是否有更好地方法来获取这些信息。

Flannel:一个Edge Cache Service

整个优化的思想有以下两点:

  1. 启动的时候下载尽量少的必须的数据。
  2. 其它数据都通过lazy load来加载。

这个思想有了之后,就需要在client层面做一些修改,比如说原来我们在client层面启动之后加载某个component的时候,会假设它需要的所有的meta data在启动的时候都有了,可以直接使用,而引入lazy load之后就意味着这种假设不再正确,所以你需要在启动某个component之前检查相应的meta data是否已经获取,如果没有成功获取到可能需要等待。

另外一个方面的优化就是我们其实还可以从server端数据准备的角度来进行优化,就是尽量让server端能快速返回我们需要的数据,最好是不管哪个地区的client来调用,我们都可以快速地返回这些需要的数据。要想达到这个目的也很简单,就是加一层cache,然后把这个cache在每一个geo地址都部署。

Slack内部称这个Cache service为Flannel,这个名字据说是因为当时有一个leader engineer当天正好穿了Flannel的衣服,所以就取了这个名字,果然取名字在技术世界是一件很严谨的事情,哈哈。

下面我们来看看这个cache在整个架构中的位置,如下图所示:

这个cache是team level的,也就是说一个team会使用一个cache,当team中的第一个用户进行连接的时候,它就会通过Web API来得到所有的信息,并保存在Cache中,这样有新的team member再需要snapshot数据的时候就可以从Cache中获取,而不需要再调用Web API来获取。当team中的所有用户都disconnect的时候,会unload cache。这样一来,对于除了第一个用户之外的所有用户来说,整个加载速度得到了极大的优化。

那么为什么又要把这个cache放到client和real time message API之间呢,这个是因为在第一个用户获取了数据之后,整个snapshot可能是会变化的,比如说创建了一个新的用户,或者创建了一个新的channel等等,而这些操作都是可以通过Real Time Message API的WebSocket来获取的,把cache也放在这两者之间就可以及时更新snapshot的变化,从而保证cache中的内容总是最新的。

同时Flannel的部署也是会在不同 的region,这样可以进一步降低网络带来的延迟:

用户会根据GeoDNS来确定应该访问哪一个Region,然后在每个Region里面HAProxy会根据team ID来做consistent hash,从而保证同一个team总是访问同一个Flannel。

整个这一套优化下来,最大team的加载时间下降到原来的1/10。

上面整个优化的过程很不错,但是细细思考一下,还是有几个问题的:

  1. 每个team中的第一个用户总是很慢,因为他没有cache可以使用。
  2. Real time的message其实很多时候是一个broadcast的message,也就是说一个message原本是需要分发给所有不同的client的,现在你在中间加了一个Cache,也就意味着这个cache接收了很多重复的message,这个重复message的处理显然是没有必要而且很耗CPU等各种资源的。

Slack是如何来解决这个问题的呢?他们引入了一个新的pub/sub的架构,如下图所示:

就是类似Kafka的message(当然他们没有使用Kafka,而是使用的自己内部的一套系统)publish/subscribe机制,cache会subscribe到team level,这样一来同样的message就只会接收一次了,而且它和WebSocket不再紧密联系了,就可以做一些pre-warmup的cache,从而也就不需要等到team中有用户创建WebSocket才能进行bootstrap的数据获取,也就进行解决了第一个用户加载总是很慢的问题。

下面这张图显示了这个优化之后user_change event(这个event是用来通知Cache用户object变化了,我们需要进行更新cache的一个event)的减少,我们可以看到减少了近500倍的event数量。

Flannel不仅可以用来做初始化的snapshot数据的优化,还可以使用它的数据来serve一些query,比如可以把channel member的query改到Flannel上来查询,有了这个改变之后,这些query的性能得到了很大的提升:P99从2000ms减到了近200ms。

总结

本文介绍了Slack是如何通过引入Flannel这个cache服务来优化初始的加载时间的,整体思想值得我们在类似产品中借鉴。

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *