Slack 架构设计

一、发展历史

2017年, Slack的CTO 卡尔·亨德森 (Cal Henderson ),畅销书《构建可扩展的网站》的作者, 在接受 lifehacker采访时, 提到为什么要做slack。Cal从小痴迷于游戏, 2009年和Stewart Butterfield,Eric Costello和Serguei Mourachov一起离开雅虎Flickr公司, 创建了一个名为Glitch的大型多人在线游戏。该游戏的开发者分散在三个不同的城市中, 为了方便协作,团队开发了一个IM工具,这就是Slack的原型。之后他们把这个工具分享给更多的朋友使用, 同时也意识到, 做Slack可能比做游戏更有价值。

Cal在启动Slack后很快就认知到,无论一个组织的规模有多大,工作实际上都是由小团队完成的——你每天与之交流的几十个人。没有人真正在工作中与5000人交流!人们能够轻松地与核心团队沟通是至关重要的,这也是Cal认为找到Slack的最佳定位。

和Cal一同创业的Stewart Butterfield 则成为Slack的CEO, 图片分享服务网站Flickr也是他创建的。这是Stewart第二次将一个失败的游戏项目转变为一个成功的科技产品。2002年,他的视频游戏工作室Ludicorp开始开发游戏“永无止境”,但从未推出。该游戏的一些功能被用来创建Flickr,他在2004年以2500万美元的价格卖给了雅虎。2012年,Stewart关闭Glitch,全心投入Slack开发工作。在开发时,这个软件被称之为Linefeed,Stewart在上线时将它改名为Slack, 它代表“Searchable Log of All Conversation and Knowledge”。

经过一年的私下测试,Slack于2014年2月公开发布。市场立刻就接受了这一款软件,第一天收到8000多个请求,第二周收到15000多个。Slack不得不错开发布时间,因为它需要增加了更多的服务器容量来满足需求。越来越多的组织开始使用Slack,其中包括许多媒体组织,他们给出相当证明的评价, 口口相传的结果是,Slack以每周5%~10%的速度快速增长。2014年底, Slack被公认为领域独角兽,2015年估值28亿美元,翻了3倍。

Slack最初的几年是由该应用的用户体验引领的,与当时使用的另外两个著名的在线聊天工具Hipchat或Campfire相比,它更容易、更现代。2016年,Slack推出了一系列新功能,包括应用程序和机器人生态系统,进一步巩固其地位。机器人的使用使得经理们更容易将他们的大部分业务转移到Slack。经理可以跟踪员工休假时间,发送调查,接收和转发电子邮件,并通过该应用程序与客户交谈。业务工具,如谷歌驱动、GitHub、Asana、Zapier和Salesforce,也都集成到Slack中。一些人在Slack中构建了一个应用程序,让用户留在平台上,而另一些人只是通知用户文件的任何更改或更新。Slack应用程序目录上有2000多个应用程序和750个机器人。

然而Slack的好运气并没持续太久,2017年,微软推出了竞争产品Teams。在Office 365平台助力下,很快得就取得了对Slack竞争优势。在此期间, Slack的DAU从600万增长到2020年的1200万,但Teams更是领先一步,DAU在2020年达到了7500万。

2019年6月,Slack通过直接公开发行上市,市值达到195亿美元。2020年12月,Slack被Salesforce以277亿美元收购

以下为Slack的一些统计数据:

  • Slack在2020年3月至2021年4月期间创造了9.02亿美元的收入,同比增长43%;同期净亏损也从5.67亿美元降至2.92亿美元
  • Slack 的DAU(每天活跃用户)为 1200万。
  • Slack的注册机构数为75万,其中15.6万为付费机构。
  • 按照Slack的说法, 全球财富100强企业中有65家使用Slack。
  • 根据Slack在2019年的数据,用户在工作日每天活跃90分钟。
  • 根据Slack的说法,使用该应用程序可以减少32%的电子邮件和27%的会议
  • 每周都有15亿的信息发送
  • 超过50万开发者注册到Slack成为开发者。
  • Slack应用程序目录中有2000个应用程序和750个机器人

和微信、WhatsApp不同, Slack是面向中小企业的协作工具, 可以说它是钉钉、飞书等国内同类企业软件等鼻祖。潘乱在《飞书的前世今生》一文中也提到,飞书也从Slack中参考不少设计要点。而微软在开发Teams的时候,则毫不掩饰地复制了Slack的大量功能。

二、产品特性

Slack提供许多IRC风格的功能,包括按主题、私人群组和直聊组织的持久聊天室(频道)。包括文件、对话和人在内的所有内容都可以在Slack中搜索。用户可以在他们的消息中添加表情符号按钮,其他用户可以点击按钮来表达他们对消息的反应。

Slack的免费计划限制用户只能查看和搜索最近的10,000条消息。

  1. 团队:社区、组或团队可以通过Slack的团队管理员或所有者发送的特定URL或邀请加入“工作区Workspace”。尽管Slack是面向组织的沟通而开发的,但它已被用作社区平台,取代了留言板或社交媒体组。
  2. 消息传递:
    1. 公共频道允许团队成员在不使用电子邮件或群发短信的情况下进行交流。公共频道向工作区的每个人开放。实际上, Slack目标之一就是替代电子邮件。
    2. 私有频道 允许较小的子组之间的私人对话。这些私人渠道可以用来组织大型团队。
    3. 私信(Direct messages)允许用户向特定用户而不是一组人发送私人消息。私信最多可以发给九个人。私信组也可以转换成私有频道。
  3. 应用集成:这是Slack的特性。Slack可以集成了许多第三方服务,支持面向社区的集成,包括Google Drive, Trello, Dropbox, Box, Heroku, IBM Bluemix, Crashlytics, GitHub, Runscope, Zendeskand Zapier。2015年12月,Slack推出了他们的软件应用程序(“应用程序”)目录,由用户可以安装150多个集成应用。2018年3月,Slack宣布与财务和人力资本管理公司Workday建立合作伙伴关系。这种集成允许Workday客户直接从Slack界面访问Workday功能。
  4. API:Slack为用户提供了一个API用于创建应用程序和自动化流程,例如根据人类输入发送自动通知,在指定条件下发送警报,以及自动创建内部支持票证。Slack的API的兼容性设计不错,可以与各种类型的应用程序、框架和服务的兼容性。

三、技术架构

3.1 基本架构

Slack实现了客户端-服务器架构,其中客户端(移动端、桌面端、Web端和应用端)与两个后端系统对话:

  1. WebApp服务器,处理HTTP请求/响应周期,并与主数据库和作业队列等其他系统通信
  2. 实时消息服务器,它向客户端发送消息、配置文件更改、用户状态更新和一系列其他事件

在频道中发布的新消息通过API调用发送到webapp服务器,在那里它被发送到消息服务器并保存在数据库中。实时消息服务器接收这些新消息,遍历频道中的人员,通过Web Socket将其发送到连接的客户端。

图片

Slack 概要架构

3.2 技术栈

  1. 对于Web端,使用带有ReactJS的Javascript和ES6作为前端语言。ReactJS是Facebook开发的流行Javascript框架之一。
  2. 桌面端,使用Electron以及HTML、CSS、Javascript 以及Chromium。Electron是跨平台的, Slack桌面端支持Windows、Mac、Linux。
  3. Android端使用Java和Kotlin。Kotlin比Java更灵活,有助于构建高性能的应用程序。
  4. IOS端使用Objective C 和 Swift
  5. Slack使用PHP/HacklangJava作为后端编程语言。PHP/Hacklang是Facebook开发的HipHop虚拟机(HHVM)的编程语言,它支持原生PHP不支持的动态类型和静态类型。它类似于Javascript中的TypeScript支持。
  6. 在Slack中,最初,PHP 5被用作后端,后来在2016年切换到HHVM,这有助于更快地运行PHP代码。Hack类似PHP的超集,在PHP基础上做了很多改进。
  7. 早期Slack使用MysQL做线上配置。之后,考虑到性能和缩放问题,MySQL上引入了分片架构。后来为了支持伸缩,采用Vitess数据库, 2017年Slack开始迁移到Vitness,到现在已经全部迁移完成。Vitess是一个数据库集群系统,用于水平扩容、部署和管理开源数据库实例的大型集群。Vitess与MySQL完美配合。高可用性、可扩展性、可操作性、可扩展性和性能对于Slack至关重要,所以Vitess很适合它。今天,Slack在世界各地的不同地理区域运行多个Vitess集群。
  8. Slack使用Memcached,MCRouter作为缓存。
  9. Flannel用于应用程序级边缘缓存。Flannel用于在加载Slack、切换频道和重新连接到Slack时减少连接时间。它是在应用程序级别缓存应用程序的服务。Flannel在客户端启动时缓存用户、频道、bots等的相关数据。然后,它按需向客户端提供查询API,以快速提供结果。
  10. 对于搜索,Slack使用Solr。Solr用于Slack中的全文搜索。Solr在后台使用了LuceneJava搜索库。
  11. 对于实时消息传递,使用Websocket。通过Web API提供历史信息,通过WebSocket提供实时数据,以便人们获得团队中正在发生的事情的最新信息。
  12. 用户和频道首先将通过Cloud Font和AWS ELB从Web API中接收关于团队的信息,并连接到Web Socket服务以接收最新信息。
  13. Slack使用Consul来发现和配置服务。Consul可以维护可靠和安全的连接,每个服务在其网络中的位置的集中注册表,即使引入或删除新的服务节点,也可以降低从一个网络应用或服务移动到另一个应用或服务的复杂性。当Consul与HAProxy结合使用时,负载均衡器配置就可以实现自动化。
  14. 服务器配置和管理的工具:
    1. Terraform是一个开源的基础架构代码(IAC)工具,把基础架构视为代码,由此实现对基础架构全生命周期的安全、高效的管理。
    2. Chef是一个开源云架构体系自动化平台,可以在任何环境(本地(私有)托管、虚拟托管或云托管)和多个平台(如Windows、Ubuntu、Solaris等)中轻松设置、配置、部署、测试和管理服务器。借助它可以通过编码来管理架构体系,而不是繁琐的人工管理。
    3. Kubernetes 是一种虚拟机替代方案,可以自动管理资源,轻松实现对应用程序的阔渣。它允许开发人员与IT操作共享依赖关系和软件,从而实现更快的代码操作和交付。
  15. Kafka和Redis用于异步任务排队。
  16. Presto、Hive、Spark是为数不多的用于数据仓库的工具,Presto是专为交互式查询设计的分布式SQL引擎。这是一个快速的方法来回答特别的问题和探索较小的数据集等。

四、核心模块设计

4.1 工作区Workspace 设计

从2014年Slack推出开始,这个核心系统架构就围绕着“工作空间”的概念来设计。从逻辑上讲,工作区包含了系统中的所有其他对象,包括用户、频道、消息、文件、表情符号、应用程序等。它们还提供了实现访问控制的管理和安全边界,以及策略和可见性首选项。

以工作空间为边界,可以建立分片系统来分散负载,使得服务的性能提升就变得非常方便。实现上, 当一个工作空间创建之后, 它就被指定到某个特定的数据库分片、消息服务器分片和搜索服务分片上。这种设计支持Slack可以很容易的通过增加更多的服务器来实现水平扩展, 承载更多的工作空间。这样在应用中如果需要访问某个内容,则首先要找到这个内容所在的工作空间,之后通过工作空间上下文来访问特定的数据库或者服务器分片。本质上, 工作空间是多租户服务中的租户单元,用来实现对数据的分片。

这种设计非常简单有效:要查找数据或发送消息,我们的代码只需要查找对应的工作空间所在分片,并将请求路由到那里,以验证请求用户是否可以访问给定的频道。多年来,越来越多的应用程序代码和服务围绕着数据活在在工作空间中的核心假设进行开发,进一步巩固了这种封闭边界的设定。

4.2 共享频道:跨组织的沟通设计

共享通道是连接两个独立组织的通道。不再需要在外部电子邮件和内部Slack渠道之间来回穿梭,也不需要向Slack工作区提供无穷无尽的外部联系人:共享频道为两家公司的人员创建了一个高效的沟通空间。然而,共享频道的设计挑战了Slack的基本假设,即工作空间是划分客户数据的原子单元。因为共享频道使用户能够跨工作空间边界访问一些频道、消息和文件。这需要对Slack的基本权限、可见性和数据分片功能进行改造,其中有不少设计挑战和权衡。

共享频道要解决的主要问题是消息应该如何在工作区之间流动。如何分发和存储消息,以便两个工作区的成员可以加入频道、发送消息并正常使用Slack。

如果继续假设用户在Slack上与之交互的所有内容都要放在用户的工作区,这意味着两个工作区在各自的数据库分片中都有共享频道的副本,消息将被写入发送用户的分片和接收用户的分片上。这样的优点在于底层的逻辑保持不变,但是缺点也很明显。考虑到Slack每周发送超过10亿条消息,为两个工作区复制数据将限制共享频道的扩展能力。这不仅仅局限于消息——它还包括pin、reactor和其他特定于频道的信息。我们还希望写入数据尽可能实时和一致,但是写入多个数据库和多个实时消息服务器可能会导致写入时间不一致,从而导致频道数据不一致。

Slack采用了单副本的方案。引入了新的shared_channels数据库,用于桥接共享频道中的不同的工作空间。

图片

Slack的所有频道,包括共享和非共享的, 都记录在对应的workspace工作区分片上channels表中。在这个案例中,如果Ben发起了共享频道,则会在Ben所在的workspace工作区分片上channels表中记录这个频道信息,同时,在这个工作区分片上的shared_channels中记录共享频道的扩展信息,包括:

  1. 频道ID,即对应channels表的外键
  2. 工作区ID,当前工作区ID,用于分片路由。
  3. 源和目标工作区ID,即发起共享和加入共享的人所在的工作区。
  4. 频道名称,主题和隐私,这三个字段意味着不同工作空间的人可以为频道设置不同的名称和主题。
  5. 另外和这个频道相关的其他信息,如消息、reactions、pins等,也都会记录到发起人Ben的这个工作空间中。

对于加入频道的人,比如Jerry,在其工作空间中的channels和shared_channels表也分别有一条共享频道记录,不一样的是当前频道是共享频道的目标频道。这样通过shared_channels表的源工作空间id和源频道id 就可以找到源频道的信息。

这种设计很好的解决隐私和性能上的问题。

4.3 角色和权限体系设计

类似Slack这样系统的角色和权限体系设计并不容易,挑战在于如何平衡易用性和权限管理的精细度。如 对频道管理员授权时,如何避免管理员执行超过预期的范围和操作,避免其查看仪表盘等和管理无关的事情。当前Slack中内置的角色包括:

  1. 访客:使用Slack的能力受到限制,并且只允许查看一个或多个授权的频道。
  2. 会员:这是基本类型的用户,不具有任何特定的管理功能,但对本单位的Slack工作区具有基本访问权限。如需管理功能,需要找管理员或者所有者来处理。
  3. 管理员:Slack内组织的基本管理员, 能够在Slack内部进行各种管理变更,如重命名频道、对频道做存档、设置首选项和各种策略、邀请新用户、安装应用程序等。管理员能够执行团队内的绝大部分管理任务。
  4. 业主(所有者):能够执行包括管理员在那的各种管理工作, 此外还能执行合规性的功能, 比如设置数据丢失预防(DLP)。
  5. 主业主(所有者, Primary Owner):该组织的首席管理员,能够执行任何管理操作。

这种设计导致管理员的权限过大, 需要一个细粒度的角色系统来分解管理员的核心能力。Slack采用基于角色的访问控制系统, 这样用户可以被授予一个或多个角色,这些角色被授予与这些角色相关的权限。Slack需要能够在组织级别(对于我们的企业网格客户层)或工作区级别来设置这些角色。

当用户执行操作时,先检查该操作所需的权限。如果用户已通过其分配的角色委托了这些权限,则允许他们执行该操作。如果他们没有显式地拥有这些权限,将返回到预定义的角色来确定他们是否有能力执行该操作。在客户端,默认都会有一个无权限的用户可以看到的界面。如果由于Flannel的缓存导致用户权限更新不及时,则会默认显示这个界面。

角色权限管理是一个独立的模块,使用Go语言开发, 和Slack 的WebApp 是分离的, 并通过gRPC来调用。新的架构中增加了三个角色:

  1. 频道管理:这种类型的用户有权存档频道、重命名频道、创建私人频道以及将公共频道转换为私人频道。
  2. 用户管理:这种类型的用户能够从工作区中添加和删除用户,以及查看组织的用户组。
  3. 角色管理:这种类型的用户能够管理角色,并将用户赋予其关联的角色。

角色信息保存在Vitess数据库中, 通过user_id来分片,这样就可以根据用户 id来高效查询。

举个例子,在客户端,如果Bob是频道管理员,并希望对频道归档。为了确认是否可以执行这个操作, 客户端首先从服务器端获取基本的权限设置, 并保存得redux中并缓存一段时间。当管理员Bob重新分配一个角色时,客户端会收到一个实时消息,其中包含相关的其他权限。系统将显示一个更新操作,并将所有的UI组件做对应的更新。

4.4 Vitess:存储架构设计

Slack一开始就使用MySQL作为数据存储引擎,采用双活架构。2017年开始迁移到Vitess,到2020年底已经完成了所有迁移工作。这里重点分析这个架构调整的设计考虑因素和技术挑战。

数据存储层的可用性、性能和可伸缩性对于Slack至关重要。举个例子,在Slack中发送的每条消息在通过实时WebSocket发送并显示给频道的其他成员之前都是需要持久化。这意味着存储访问需要非常快速和非常可靠。在2020年底,Slack在高峰期的QPS是230万,其中200万读, 30万写。查询延迟中位数是2ms,P99查询延迟是11ms。这是采用Vitess之后的数据。

早期Slack采用LAMP技术栈,所有数据存储在三个MySQL主集群上:

  1. 分片:存储Slack所有业务数据,如消息、频道和DMs,数据按照工作区ID做分区。
  2. 元数据集群:元数据集群用作查找表,将工作区id映射到基础分片id。这意味着要找到工作区中特定Slack域的分片,我们必须首先在这个元数据集群中查找记录。
  3. 下水道集群:这个集群存储了所有其他没有绑定到特定工作空间的数据,但这仍然是重要的Slack功能。一些示例包括第三方应用目录。任何没有与工作区ID关联的记录的表都将进入此群集。

分片是在Slack的单体应用webapp来管理和控制, 其中包括在特定工作空间中检索元数据,之后创建到底层数据库分片链接的逻辑。这样从数据集分布的视角来看, 这是一个工作空间分片模型, 每个数据库分片包含数千个工作区及其所有数据,包括消息和通道。从架构体系的角度来看,所有这些集群都是由一个或多个分片组成的,其中每个分片都配置了位于不同数据中心的至少两个MySQL实例,并使用异步复制相互备份。下图显示了原始数据库体系结构的概述。

图片

这种主动-主动配置有许多优点,很容易实现服务的升级:

  • 高可用性:在正常操作期间,应用程序总是倾向于基于简单的哈希算法从两个库中查询。当其中一台主机出现故障时,应用程序可以重试对另一台主机的请求,而不会对客户产生任何可见的影响,因为分片中的两个节点都可以进行读取写入。
  • 产品开发速度快:把某个工作空间上的所有数据存储在单个数据库主机上,基于这样的模型来设计新功能是直观的,并且易于扩展到新的产品功能。
  • 易于调试:Slack的工程师可以在几分钟内将客户报告的问题连接到数据库主机,问题调试速度快。
  • 易于扩展:随着越来越多的团队注册Slack,只要为新团队提供更多的数据库分片就可以跟上人员的增长。

随着越来越多用户和公司使用Slack,越来越多的产品团队来参与开发Slack功能,此时这个方案的缺点就显现出来了:

  • 首先是伸缩能力的限制:一些大公司加入,所需要的数据超过slack分片的上限,也就是分片所在的物理机器的上限,这就是个致命的问题。
  • 数据模型局限:随着业务的演进,Slack推出企业网格和Slack连接等新产品,这两种产品都挑战了团队所有数据都在同一个数据库分片上的范式。这种架构不仅增加了开发这些功能的复杂性,而且在某些情况下还会降低性能。
  • 热点问题:大客户加入,动不动就产生了千人群组,这些群组都罗道一个数据库分片上,群组的活动很容易产生热点。更严重的是,这些热点受架构限制无法分散到其他分片。同时,大量长尾分片的性能却无法利用起来。
  • 工作区和分片可用性问题:所有核心功能,如登录、消息传递和加入频道,都要求团队数据所在的数据库分片是可用的。这意味着,当数据库分片发生中断时,数据位于该分片上的客户也会经历完全的Slack中断。新的架构应该即可以分散负载以减少热点,又可以隔离不同的工作负载,这样非核心的功能不可用,也不会影响像消息发送这样的关键功能。
  • 操作:分片并不是标准的MySQL配置。这样导致开发团队要编写大量的内部工具来支持大规模线上集群维护。此外,这些配置得直接应用到生产环境,风险高。
图片

到2016年秋天,生产环境的MySQL的QPS达到数十万,MySQL分片达到数千个。应用程序性能团队经常遇到缩放和性能问题,需要为工作空间分片架构的局限性设计新的架构。现在问题是:在原架构上演进,还是另起炉灶?Slack团队这里做出了一个教科书版的选择:

  1. 将消息数据的分片依据从工作区 ID 调整为 频道ID, 这样可以更均匀分散负载。
  2. MySQL的使用应该继续,应用中已经大量使用了MySQL特定的查询;团队在MySQL的部署、数据持久性、备份、数据仓库ETL、合规性等操作方面有大量的经验和积累。

这样, 首先排除了类似DynamoDB或Cassandra这样的非关系型数据库数据存储,以及像Spanner或CockroachDB这样的NewSQL。这一点是和WhatsApp不一样的地方。后者使用Cassandra,而且是很早就开始用了。对Slack来说,这个调整已经来不及了。基于上述因素,Vitess的优势就显露出来了。Vitess的核心是提供了一个数据库集群系统,用于MySQL的水平扩容,它满足团队上述所有需求:

  • MySQL内核:Vitess建立在MySQL之上,这样多年积累的以MySQL为实际数据存储和复制引擎,其可信度、开发人员对MySQL的把握,以及信心都有保证。
  • 可伸缩性:Vitess将NoSQL数据库的可伸缩性融入MySQL的各种重要特性。它的内置分片功能允许您灵活地分片和扩展数据库,而无需向应用程序添加逻辑。
  • 操作性:Vitess自动处理主节点的故障转移和备份等功能。它使用锁服务器来跟踪和管理服务器,让数据库拓扑结构对应用透明。Vitess跟踪关于集群配置的所有元数据,保持集群视图始终是最新的,并且对于不同的客户端是一致的。
  • 可扩展性:Vitess是100%使用开源Go语言构建的,测试覆盖率高,开发者社区也比较活跃。

这样自2017年开始,三年时间, 基本完成了MySQL流量到Vitess的迁移。Slack在世界各地不同的地理区域运行多个Vitess集群,拥有数十个keyspaces。单体webapp以及其他的服务都迁移到Vitess上。每个kyespace都是一个逻辑数据集合,并按照某种因素来伸缩:用户数、团队或者频道,而不是仅仅按照工作空间来分片。热点问题也随之得到解决。非常幸运, Slack在新冠流行之前引入了Vitess,2019年冠状病毒病袭击美国,Slack的使用量空前增加。在数据存储方面,仅在一周内,查询率提高了50%。Vitess非常好的应对了这一次使用量的井喷 。Vitess部署简要架构如下:

图片

Vitess 运行时架构

4.5 Flannel:边缘缓存设计

Slack遇到的第一个挑战是连接到Slack的速度很慢。当用户加入越来越多、越来越大的团队时,连接到Slack的速度变得很慢。我们可以分析Slack启动流程来发现瓶颈所在:

  1. 客户端向服务器发送一个HTTP请求。
  2. 服务器验证发送过来的token,回复用户所在的群组的快照。
  3. 建立一个WebSocket连接。在该连接上,实时事件被发送到客户端, 群组信息就被更新到最新状态。

WebSocket连接是基于TCP的双层通信协议。用户连接时间是从发送第一个HTTP请求到WebSocket连接建立为止。这时候客户端就准备就绪了。这个流程的瓶颈在第二个步骤, 即发送群组快照。

群组快照包含如下内容:

  1. 团队成员;
  2. 用户所在的频道列表;
  3. 频道中的用户。

一旦涉及到有几千个用户或者频道, 这些信息动不动就增长到几十兆字节。为什么客户端在启动时需要这些用户和频道信息?这是早期做出的架构决策, 会让后面的功能实现更方便,用户体验也会更好。从当前Slack用户数量来看, 这个设计就不太合适了。Slack做了一些迭代来试图压缩快照的大小,比如从初始有效负载中删除一些字段,或者改变字段的格式以节省空间。不管怎么做, 快照的大小就无法减少, 所有有必要从架构上做优化。

实现策略也很简单, 就是引入懒加载方案:减少启动时加载的数据量;推迟到按需加载。这意味着客户端需要重写他们的数据访问层。它不能假设所有数据都在本地可用。

当远程获取数据时,会有网络往返时间消耗。所以Slack决定建立一个查询服务, 支持缓存, 就近部署,由此来保证数据访问速度。这个服务被称为Flannel服务。Slack分两阶段实现Flannel服务。

为什么叫Flannel?根据Slack工程师介绍,在项目启动的那天,首席工程师碰巧穿了一件法兰绒衬衫。这就是原因

第一阶段,中间人策略。将Flannel放在WebSocket连接上,这样Flannel也可以接收到所有事件并转发给客户端, 将其中一些事件用来更新其缓存。客户端可以向向Flannel发送查询请求。Flannel根据缓存中的数据回复这些查询。缓存按团队来组织。当团队中的第一个用户连接时,Flannel将团队数据加载到其缓存中。只要团队中有一个用户保持连接,Flannel将保持缓存为最新状态。当最后一个用户断开连接时,Flannel将卸载缓存。这种设计的优点在于它不需要修改后端系统的其他部分。在这个阶段,Slack还推出了一个即时标记功能。这是对客户端的优化。Flannel预测客户端下一步可能查询的对象,并主动将对象推送给客户端。

举个例子, 假设团队中的用户Alice发送了一条消息。消息被广播到所有通道成员。每个客户端都需要用户Alice的信息来呈现消息。Flannel维护一个ARU缓存来跟踪每个客户端最近查询的对象是什么。它检测客户端A最近没有查询Alice。所以它可能没有数据。所以它将带有消息的用户Alice对象推送到客户端。对于客户端B和C,他们最近查询了用户Alice,所以Flannel只是发送了一条消息。经过改进,用户连接流程的瓶颈被消除了。最大团队的用户连接时间下降到1/10。Flannel缓存处于进程内存中。Flannel被部署到多个敏捷的位置。它离用户很近,所以接近提供了更快的数据访问。客户端使用基于地理的DNS来确定他们应该连接到哪个区域。

Flannel是以团队亲和来组织的, 这意味着同一个团队中的所有用户都连接到同一个Flannel主机。Flannel前端设置了一个代理, 它使用一致的哈希根据团队ID来路由流量。在第一阶段实现完成之后,有几个需要改进的地方。

  1. 首先,作为一个中间人,Flannel留在每一个WebSocket连接上。对于许多事件,它们被广播给团队的所有成员或频道中的所有成员,所以有很多重复的事件。读取和处理此消息需要花费大量的CPU。
  2. 其次,缓存更新与WebSocket连接绑定。当第一个用户到来时,Flannel加载缓存。当最后一个用户离开时,它卸载缓存。这意味着对于团队中的第一个用户来说,它总是会命中代码缓存。对于团队中的第一个用户来说,这不是一个理想的体验。

第二阶段改进重点解决上述问题,核心是将Pub/Sub引入系统。引入实时消息API, Flannel可以订阅团队和频道列表的用户连接事件。这种设计带来的另一个好处是缓存更新不再与WebSocket连接绑定。Flannel甚至可以在第一个用户上线之前预热缓存。在Pub/Sub之前,Flannel需要处理的事件达到了每10秒50万次的峰值。在Pub/Sub之后,它只达到了1000次。减少了500倍。频道成员资格查询接口的P99延迟在迁移到Flannel后从2000毫秒下降到200毫秒。

这个实现有两个架构上的问题:

  1. 为什么不使用Memache来做缓存?原因是Flannel需要服务于自动完成查询,所以Flannel将索引保存在内存中,并且索引并不真正适合Memache的键值模式。
  2. Pub/Sub选型。Slack使用自研的Pub/Sub系统, 很早就建立起来的,稳定,性能不错。更换成Kafka的ROI不高。

4.6 EnvoyProxy:负载均衡设计

2020年,Hollywood的前一天, 在2分钟内, 由于Flannel的Bug, Slack突然掉了160万WebSocket链接。一些Flannel主机崩溃了。重新连接的流量流向了健康的主机。它们超载,崩溃,然后故障蔓延到整个集群。团队花了135分钟才将流量恢复到正常水平。这个故障暴露了几个问题。

首先发生故障的是负载均衡器EOB。它是托管在AWS上的网络层的前端。重新连接的流量淹没了EOB集群。整个集群变得无法访问之后, 团队花了45分钟时间来扩展另一个EOB集群,并将流量指向它。同时做了严格的限流措施。团队设置了几种限流配置文件,但大家对限流参数如何设置却拿不准,只能边测试边验证。这在事故发生时,要调整一组合适参数就更困难了。这也是导致事故恢复时间变长的一个原因。

Flannel引入了熔断和限流机制来避免被打垮。Flannel监控内存使用, 一旦超过阈值,就拒绝流量。直到内存下降到正常水平。另一个方法是使用断路器。当Flannel检测到这些上游服务的故障率开始上升时,它就开始拒绝流量。它使用反馈回路来控制它向后端服务发送多少请求。所以这给了后端服务一个恢复的机会。

解决链接风暴的关键在于如何避免流量被一下子全部被负载均衡服务切断。Slack一开始是使用HAProxy来做负载均衡,但HAProxy对热重启的支持是有问题的。

在Slack,更改后端服务接入点列表是常见事件(由于实例被添加或循环删除)。HAProxy提供了两种方法来支持接入点变更后的配置参数修改:

  1. 使用HAProxy运行时API,这种方式会导致链接雪崩,在Slack的可怕、可怕、不好、非常糟糕的一天一文中有详细描述。
  2. 在服务接入点变更后,直接修改HAProxy的配置文件,重新加载HAProxy。这个机制应用在WebSocket的负载均衡上。

HAProxy在重新加载后,新建的链接会使用新的配置来加载;老的链接线程还不能中断,继续保持运行,这样WebSocket的长链接不被中断。这样会导致有大量的长链接仍然使用老的配置信息。为了解决这个问题, Slack选择了Envoy Proxy。Envoy支持是以哦能够动态配置的集群和连接点。如果连接点列表发生变更,无需重新加载。如果代码或者配置发生变更, Envoy可以热重启,并不会丢弃任何链接。Envoy使用iNotify监视配置文件的变更。在热重启时,Envoy会从父进程中复制统计信息到子进程,这样各种计数器也都不会被重置。这一切都大大减少了使用特使的操作开销,并且不需要额外的服务来管理配置更改或重启。

Envo提供了几个高级负载平衡功能,例如:

  • 内置对区域感知路由的支持
  • 通过离群点检测实现被动健康检查
  • 恐慌路由-Envoy通常只会将流量路由到健康后端,但如果健康主机的百分比降至阈值以下,则可以配置为将流量发送到所有后端,无论是健康后端还是不健康后端。

为此,在2019年,Slack的入口负载均衡就从HAProxy逐步迁移到EnvoyProxy。

4.7 Solr:搜索设计

平均来说,一个知识工作者一天的20%时间都花在寻找完成工作所需的信息上,也就是一周的工作有一整天的时间花在搜索上。Slack的搜索、学习和智能团队把工作重点放在提升搜索结果的质量上。建立了一个个性化相关性排序机制和一个名为“头部结果”的搜索模块,在一个视图中显示个性化的、最新的结果。

和百度、谷歌为代表的Web搜索不同, Slack关注于内部搜索。Slack用户可以访问不同的文档, 文档内容还经常会变好。Web搜索大量使用的数据聚合技术在Slack搜索中无法使用,但它有自己的特性:

  1. Slack更了解用户与Slack中其他用户、频道、消息和用户界面元素的交互历史。
  2. Slack不需要处理垃圾邮件或游戏搜索引擎优化。
  3. 虽然文本语料库的总大小很大,但每个团队的语料库相对较小,因此允许在排名期间为每条消息投入更多的计算资源。
  4. 不仅可以控制搜索界面,还控制目标文档的呈现和结构

Slack提供了两种搜索策略:最近相关最近的搜索找到匹配所有关键词的消息,并按相反的时间顺序呈现它们。如果用户试图回忆刚刚发生的事情,最近搜索是一个有效的处理方式。

相关搜索放松了时间限制,并考虑了文档的Lucene分数——它与查询关键词的匹配程度。使用约17%的时间,相关搜索表现略差于最近搜索。这是按照如下方法度量出来的:每次搜索的点击次数和在搜索结果前几个位置的点击率。我们认识到,相关搜索可以受益于使用用户与频道及其他用户的交互历史——他们的“工作地图”

举个例子,假设你正在搜索“路线图”。你很可能在寻找你团队的路线图。如果您的团队成员在您经常阅读和撰写消息的渠道中共享了包含“路线图”一词的文档,则此搜索结果应该比另一个团队的2017年路线图更相关。通过将用户的工作地图合并到相关搜索中,搜索结果点击频率增加了9%, 搜索结果第一条的点击增加了27%。

此外,文档作者和搜索者之间的熟悉程度、频道的参与度、消息本身的特性等,都可以有助于提升搜索结果的命中率。这样在实现上, 对搜索处理就引入两阶段的方法:

  1. 利用Solr的自定义搜索排序功能, 提取一组消息,仅按照一些选定的特性来排序, 这样便于Solr执行搜索。
  2. 在应用层,对这些搜索结果按照全部特性重新排序,并赋予适当的权重。

在训练数据集上,搜索团队使用SparkML的内置SVM算法训练了一个模型,该模型推导出如下用于搜索排序的特性:

  1. 该消息的存续时间。
  2. Lucene在该查询中给出的分数。
  3. 搜索者对消息作者的亲和力(即该用户阅读另一个用户消息的倾向)
  4. 包含消息作者的搜索者DM频道的优先级分数
  5. 消息出现的频道的搜索者优先级分数
  6. 消息作者是否与搜索者相同
  7. 信息是否被pin、标星或者有emoji反馈。
  8. 搜索者从消息出现的频道点击其他消息的倾向
  9. 消息内容的各个方面,例如字数计数、换行符的存在、表情符号和格式。

值得注意的是,除了Lucene“匹配”分数之外,目前还没有在模型中引入消息本身的任何其他语义特征。

根据搜索团队发布的结果来看, 相关搜索有明显的提升。搜索点击量增加了9%;在至少获取一次点击的搜索中, 搜索结果第一位的点击量增加了27%。

4.8 本地化

目前slack支持8种语言,本地化一直都是不容易的事情。Slack的本地化第一步是将代码库中的字符串进行本地化。虽然移动平台为本地化提供了清晰的狂减,并实现了代码和字符串的分离, 但Web端和桌面端的代码和字符串却是混在一起的,嵌入到HTML模版和业务逻辑中。这也为本地化实现增加了难度。Slack本地化团队采用的两步走的策略:第一步是创建一个框架来表示使用的各种编程语言中的字符串。之后修改代码中的字符串。这个过程被称之为“包装字符串”,每个字符串都被包装在一个块或者函数调用中。

Slack选择ICUMessageFormat语法来表示字符串。和gettext相比,其处理复杂字符串的能力,特别是在选择和复数处理方面,更胜一筹。此外其对JavaScript和PHP的支持更灵活和健壮。开发团队为模版语言、 Handlebars 和 Smarty 创建了ICU的助手程序。但还有一个问题,现有的翻译管理系统( Translation Management System ,TMS)基本都不支持ICU,因而团队需要自己实现TMS的大部分内置的翻译解析和验证功能。

常见的本地化处理有两种方案:

  1. 类似Android的strings.xml的键值对,将代码中涉及到的字符串替换为索引键;
  2. 类似iOS的NSLocalizedString方案, 采用英文字符串作为索引键;

Slack采用第二种方案,其优势是可读性强,但也意味着英文字符串的任何改变都需要重新翻译。

下面是我们的通知电子邮件模板中本地化之前的字符串示例:

<p>

You have {if $activity.num_dms == 1}a new direct message{else}{$activity.num_dms|number_word} new direct messages{/if}.

</p>

And after:

<p>

{t

num_dms=$activity.num_dms

dms_number_word=$activity.num_dms|number_word_localized

}

You have {num_dms, plural, =1 {a new direct message} other {{dms_number_word} new direct messages}}.

{/t}

</p>


这个{t}块有两个目的

  1. 在静态分析中寻找它来提取字符串,上传到的翻译管理系统。
  2. 在运行时(或构建时),它散列字符串,查找其翻译,并使用ICUMessageFormat库来渲染它。

按照这个策略, Slack需要在2000个文件中对大约20,000个字符串进行此操作,这是一项艰巨的任务。最终的目标也很明确:Slack应该在每种语言中拥有一致的声音和高质量的翻译,本地化应该融入每个团队的工作流程,所有新功能都应该在发布时翻译。为此Slack雇佣了一个全职翻译团队,他们为每种语言编写词汇表和风格指南,并与承包商合作翻译所有的单词。此外,团队还对Slack的代码审核工具Linting增强了验证ICU每个源字符串的语法的正确性,并确保传递正确的参数。加上培训和代码审查,这些工具有助于避免开发过程中的本地化问题。

Slack的网络代码库每天更新并持续部署100多次。本地化团队构建了额外的工具,以确保新功能发布和副本更改不会导致用户在其他翻译体验中看到英文字符串。

对于功能发布,Slack使用功能标志系统,这允许我们尽早将新功能引入代码,并对大多数有条件块的用户保持禁用。功能只能在开发环境中启用,或者仅为我们自己的Slack团队启用,或者向一定比例的团队推出。本地化团队通过开发自动化工具来标识每个字符串所在的条件语句集,以便确定该字符串是否对生产中的用户可见。在前端迁移到React框架后, 这工作难度就更大了,需要对PHP、Smarty、JavaScript、Handlebar和React/JSX等语言进行识别。然而,带来的收益也是巨大的。这个工具允许我们添加一个检查,这样在所有字符串都被翻译之前,该功能就不能开放给用户。该工具还带有一个仪表板,显示与功能相关的所有字符串及其翻译状态。以下是本地化团队总结的一些经验总结:

  • 表情符号名称是在客户端本地化展示的,并以规范的英语形式存储在数据模型中。但是由于现有行为很难改变,即使转换了语言环境,也可以用英语输入它们。
  • 更新搜索引擎来支持每种语言的词干。
  • 为了保持跨平台的一致体验,我们选择覆盖移动设备区域设置,让Slack用户选择他们的区域设置偏好
  • 让Slackbot可以意识到要说哪种语言:给用户发消息时的用户区域设置,发布到频道时的团队区域设置
  • 为每个语言环境编译了一个支持的日期格式列表,并使用Moment.js构建一个库,将日期作为字符串传递到句子中
  • 构建了一个帮助程序,用于为每个区域设置逗号分隔的列表设置“和”和“或”大小写的格式
  • 为所有格构建了一个助手,支持特定语言的规则
  • 一些英语单词,如“它们”,是模糊的单数或复数,会导致字符串,在许多语言中无法翻译。创建了单独的单数和复数字符串,尽管在这两种情况下英语是相同的
  • 如果一个句子的主语可以是“你”或其他人,动词的形式在许多语言中都会改变,所以我们需要为“你”的大小写使用一个单独的句子

Slack架构一直在演进中。如上是从现有资料中分析的WhatsApp的架构, 仅代表现阶段的Slack的架构设计。对本文有兴趣的同学,可以评论下留言,欢迎交流。

发表评论

登录后才能评论
网站客服
网站客服
申请收录 侵权处理
分享本页
返回顶部