摘要:对于移动技术而言,2017年是继往开来之年。一方面是移动技术领域进入深水区,另一方面移动技术边界和内涵被不断重塑。作为专为中国企业打造的免费沟通和协同的多端平台,钉钉在发展和功能的不断完善过程中沉淀了很多实战经验,2017年杭州云栖大会上,钉钉技术专家格夫结合钉钉发展历程为大家分享了企业沟通场景的消息服务挑战以及系统推送链路的优化经验。
演讲嘉宾简介:格夫,钉钉技术专家。入职阿里巴巴之前在百度云服务平台部门,2014年加入钉钉创业团队,参与钉钉消息系统设计开发,主导了IM系统性能和稳定性提升项目。目前专注于异地容灾单元化系统构建。
以下内容根据演讲视频以及PPT整理而成。
本次的分享主要分为以下四个方面:
钉钉发展历程
基于企业沟通场景的消息服务挑战
系统推送链路优化
单元容灾及未来挑战
了解钉钉的人都知道,钉钉的团队是从来往过度来的。2014年,来往分为了产品和技术两条线,想做一些不同方面的尝试。但是后来发现个人社交这条路基本上走不通了,所以产品线希望看看在企业服务方面是否能有所作为,于是打造了企业办公移动平台。在技术线上,来往已经沉淀了多年的无线技术能力,并通过阿里悟空的SaaS服务面向中小APP提供IM能力。随着产品线和技术线的共同深入,发现在企业社交场景下大有可为,后来工作圈和阿里悟空这两个组织合并到了一起,诞生了现在的钉钉。钉钉的目标就是打造一个全新的工作方式。
从2015年初钉钉的第一个版本上线到现在,已经发布了几十个版本,并且即将要发布4.0版本,从当初简单的沟通协同功能到现在整个企业的一站式管理功能都已经可以在钉钉上实现。截止到2016年底,钉钉上活跃的企业数已经突破了300万家,可以看到这一数字增长得非常迅猛,从另一个角度也可以说明目前中国的中小企业对移动办公存在特别强烈的需求。
二、基于企业沟通场景的消息服务挑战消息服务在钉钉中所处的位置
消息服务主要分为三方面:IM主要负责消息管理和会话管理;同步协议主要负责长连接的推送;第三方通知则是通过第三方的链路做消息保活以及提高消息服务到达率。消息服务上层承接了钉钉的整个业务层,可以看到企业OA以及通讯录都位于消息服务的上游,而消息服务的下游就是客户端了。可以看到消息服务所处在的位置基本上连接了业务层到客户端整个的通讯能力,所以消息服务的可用性基本上就代表了钉钉的可用性。在消息服务中也有一个完善的监控系统可以实时反馈系统的健康度。“火眼”系统就是钉钉自研的一套系统,它记录一条消息从发到收的整个生命周期以及垂直链路,这些信息都可以在后台实现完全展示,当发生消息丢失或者延迟过大等情况时可以快速定位问题所在。去年,钉钉消息服务的稳定性已经达到99.99%,今年消息服务的目标是可用性达到99.995%,推算时间,也就是全年只能有27分钟的故障时间,这一目标对于消息服务团队也是非常严峻的挑战。
消息存储模型
下图所示的就是消息的存储模型。在业界,消息存储模型可以主要归为两类:一类是读扩散;另一类是写扩散。读扩散就相当于一个人在群组中发了一条消息,存储的时候仅存储一份,每个群成员都到群组里拉去最新的消息;写扩散就相当于为每个人建立一个收信箱,每个群组产生了消息之后,就向每个人的收信箱提交一条消息。钉钉在选择使用存储模型的时候进行了权衡,从下图中可以很明显地感觉到与写扩散相比,读扩散所具备的优势非常明显,无论是扩散的存储成本还是读写效率上,读扩散都具有非常明显的优势。但是钉钉却偏偏选择了写扩散的方式,这是因为在梳理了钉钉的业务功能需求后,发现钉钉群聊中每个人的每条消息状态都是完全不同的,比如钉钉有已读的功能、消息转接功能、会话消息的清除和撤回功能等等,所以每个人在会话中的状态都是不同的。而采用写扩散的方式更易于维护每个人的消息状态,所以钉钉最终采用了写扩散模型。但是很明显,写扩散的成本非常大,所以也为以后的架构演进埋了不少坑。
消息发送流程
消息发送流程主要分为消息的接收和消息的处理。在消息的接收链路上,尽可能地让逻辑越少越好,保证用户发消息的体验比较顺畅。这里主要实现了三个功能:流控、安全过滤和关系校验,完成这三步之后就代表消息已经发送成功了。消息接收和消息处理之间通过了异步的MQ实现了逻辑的解耦。在消息的处理上实现了消息可重录,也就是每条消息可以至少被消费一次,做到幂等。消息处理中有五个核心功能,除了持久化的依赖比较强之外,其他的功能当出现故障的时候都可以实现主动降级,消息从发出到推送到客户端的延时可以做到保证,目前消息的延时都是低于200毫秒的。
接下来分享基于以上模型遇到的挑战。
挑战一:万人全员群节日场景
目前,越来越多的万人企业开始使用钉钉,并且钉钉也提供了全员群。所以当这样的群异常活跃的时候,特别在发红包的场景下,大家抢的非常迅猛,因为采用了写扩散模型,这时候存储的压力就会比较大,而更进一步扩散的成本也会比较大。如图所示的是某个企业在2015年钉钉上线红包功能之后统计出来推送量,当发红包时推送量立即突增了20倍,这所造成的存储压力是非常大的。
解决这种情况的方案主要分成了以下三个方面:
第一,分而治之。虽然给用户感觉是万人群是一个大群,但是其实在后台将万人群切分成了若干个小群,每条消息都会通过小群并行地推到客户端,这样一来就降低整个消息入库的延迟。
第二,降级服务&消息管控。当群突然变得异常活跃时,可以主动地将一些已读、未读等附属功能移掉、推荐群主打开群禁言,这样就能在客户端上将消息量大大地降低,并且也会在服务端做群粒度的频次控制,可以根据群成员的数量限制每分钟所能发送的消息条数。
第三,用户登录推送消息阈值。当检测到用户所收到的消息量超过一定的阈值的时候立即从推的模式变为推拉结合的方式,通过这种方式降低客户端流量,保证客户端收消息体验的顺畅性。
挑战二:考勤打卡提醒、运营活动推送
通过下图的曲线大家可以看到,流量出现峰值的时候往往是早高峰或者晚高峰,还有钉钉运动会在每天9点半将大家的运动结果推送给各个端。早晚高峰会出现流量的突增,而平时的流量却没有这么多,这样就只能靠在服务端堆积服务器才能够满足早晚高峰的流量需求,但是服务端的资源是不可控的,随着企业和用户数量不断增加,依靠堆积服务器来满足推送需求很明显是不合理的。
针对于这种场景,钉钉抽象出了两种概念:
第一,针对考勤打卡场景,其时效性很强,必须要在打卡前的几分钟推送完毕,同时它也是一种重复类型的消息,基本上每天都会推一次考勤打卡的提醒。对于这个场景消息钉钉采取了Local Push策略,首先这种消息会通过Server下发一个定时任务,基于每个端Native的通知任务机制将任务埋在客户端,到时间就触发任务,之后推送一个本地通知,这样就将服务端的推送需求转嫁给客户端了,进而化解峰值推送需求。
第二,对于时效性需求不那么强的运营类场景而言,比如钉钉运动从每天9点半开始推送,而实际上10点半推送完毕对于用户而言也没有特别大的影响。针对于这种情况,钉钉的消息服务抽象出了一个任务实时调度系统,实现了分布式的LoadController,并通过QS的限制起到削峰平谷的作用。与此同时也起到了推送的安全性检查的作用,比如某个业务由于Bug向用户反复推送同一条消息,这很明显是有问题的,而通过疲劳度控制、黑名单的过滤的机制可以降低业务端出现Bug所造成的影响以及客户端体验的问题。当任务推送完成以后,还可以将这种错误的消息撤回,比如说某一类运营消息推送错了,但是用户已经收到了,通过这套系统可以瞬间地经由服务端下发将消息撤回,这样一来用户也是没有感知的,不会影响用户使用钉钉的体验。
挑战三:企业信息安全保障
一般而言,企业的信息都具有较高的私密性需求,为了帮助企业用户降低使用钉钉的疑虑,让企业可以更加放心地使用钉钉,钉钉团队也从产品和技术两个层面上进行了多种尝试。
首先,钉钉支持内部群的概念,当员工离职之后可以自动地从企业群中退出,这样以来在职员工所能够接收到的信息在离职之后就变得完全不可见了。另外,对于单聊消息而言,支持阅后即焚功能,可以实现单聊消息在被接收者阅读之后从客户端到服务端这样全端地被清除掉,其他人无法感知到。在客户端,钉钉推送消息到服务端的过程基于SSL/TLS协议的加密方式在整条链路上进行数据传输;在服务端,钉钉也通过阿里巴巴集团强大的加密技术保证内部数据流转和持久化都可以实现服务端的全链路加密。
以上的几点基本满足了一般企业的信息安全保障需求。此外,钉钉还提供了第三方加密的方式,也就是根据企业的申请,在客户端上通过第三方加密的SDK,为每个企业分配一个加密的Key。企业用户在进行数据传输时会根据这个Key进行数据加密,数据传到服务端就是一堆乱码,完全无法感知。而且当员工离职的时候加密的Key会在其客户端上立刻失效,这样离职员工就完全无法破解和获得之前通过Key加密的消息,进而保障了企业的消息在钉钉中流转的安全性。
经过上述几点的优化,钉钉已经通过了ISO27001:2013信息安全管理体系标准认证以及公安部信息系统三级等级保护认证,所以现在政企、事业单位以及对安全性要求比较高的企业都可以更加放心地使用钉钉这款产品了。
挑战四:协同工具信息流整合
一个企业不可能只用钉钉这一款产品,因为他们可能会有自己的OA系统、审批系统等,那么如何进行信息流转也是一个问题。当时钉钉团队也参考了一些国内外比较优秀的软件产品,他们提出了chatbot这个概念,而chatbot存在两种方式:Incoming和Outgoing,所以钉钉也借鉴了这两种方式抽象出了自己的方案。钉钉的Incoming方案就是第三方服务的信息聚合到群聊中,实现由外而内的自动化信息同步;而Outgoing则是将钉钉会话内容输出到指定机器人,和外部系统做信息交换。通过Incoming和Outgoing这两种方式基本上可以实现跨APP、跨平台的信息流转,起到了信息整合的作用,这样企业就可以放心地将自己的系统和钉钉进行对接。
钉钉的信息流整合现在也提供了一些通用的bot,比如互联网公司比较喜欢使用的Gitlab和Github等工具,当代码发生变更的时候可以通过Incoming机制将代码的变更快速地同步到群中,这样大家都可以看到变更以及提交的日志等信息,进而实现了一种外部系统和钉钉的交互方式。同时,钉钉引用了企业专有的chatbot。举两个例子就是内外小蜜和报警助手,内外小蜜是阿里巴巴内部经常使用的工具,比如需要查询自己的税号、福利以及园区地图等都可以在钉钉上通过内外小蜜的chat方式进行交互,进而快速得到自己所需要的信息,内外小蜜实际上就是由钉钉和阿里巴巴信息事业部进行信息整合所产生的产品;第二例子就是报警助手,阿里巴巴有自己的一套报警信息管理平台,之前当故障发生之后都是通过短信、电话等方式快速将系统问题报警给负责人的,现在则是通过Incoming机制将报警平台与钉钉进行整合,当问题出现之后的第一时间通过钉钉发消息给负责人,如果负责人没有确认还可以转成ding的提醒方式,这样报警系统就实现了通过钉钉将问题快速地反馈出来。
三、推送链路优化推送链路优化——同步协议推送
之所以需要实现同步协议推送,主要是基于以下两点:
产品角度。在早期的IM 1.0的时代,钉钉的消息和会话状态不能做到多端同步,比如当使用PC端接收消息之后,在ios端或者Android端就无法看到这条消息了,这对于企业沟通场景而言是完全无法接受的。并且当在一个端读了消息之后,还会在另外一个端标又注了未读的信息,造成了一些沟通方面的误导,所以整体的用户体验也是比较糟糕的。
系统角度。早期的时候,消息和状态会无序地推到端,导致了移动端处理起来非常复杂。比如在使用钉钉进行沟通的时候创建了一个群聊并发送了一条消息,这个过程就涉及到了两个事件:创建群聊和发送消息。在早期,这两个事件会无序地推到端上,所以如果发送消息的事件先于创建群聊事件达到端上就会出现问题,此时就需要回到服务端上拉取会话状态,再把消息呈现出来,而且端上还要做相当于回滚的策略。针对于上面所提到问题,早期钉钉在端上做了大量的兼容工作,所以实现起来非常复杂。由于整体是无序的状态,当特别的大量消息推送到客户端的时候无法做到下行的拥塞控制,导致了手机端的CPU被占满,进而出现卡顿的情况。
基于以上几点,钉钉参考了业界主流的协议,比如说微信和支付宝等使用的协议,在2015年8月份立项开展同步协议系统的实现工作,2016年3月份,钉钉开始灰度同步协议推送的功能,6月份IM业务开始全量地接入到了同步协议。所以现在大家如果使用钉钉就会发现体验较两年前有了较大提升。
简单介绍一下同步协议推送的思路。如下图示例,ID为21051的用户向ID为21254的用户发送了一条IM文本消息,这条消息首先会进入到同步协议系统中,同步协议会将这个消息打包进行分发,这里存在一个顺序队列,并且每个用户只能哈希到一个顺序队列,这是为了保证同一个用户所产生的消息和状态能够顺序地进入到一个队列中。之后handler会消费这个顺序队列,当消费到了ID为21254的用户有一个消息推送的时候,计数器就会为这条消息分配一个自身位点,这个位点在系统内部叫pts,此时原来的pts 1004就会做自增操作,为新消息分配了pts为1005。在分配完pts之后,会将这条消息插入到存储中,这样就完成了同步消息入库的流程。那么,消息如何下发到客户端呢?每个端上都会记录下当前它接受收过的最大pts位点,当发生重连或者推送超时的情况就会触发它到服务端上去拉取新消息,当服务端存储里面有大于最大pts位点的消息时,钉钉就会把大于最大pts的所有消息都推送到客户端,这样就实现了多端的同步。早期的同步协议大致就是这样的一个信息流转的过程。
但是在后期的全量业务上线之后,上述的信息流转过程也出现了问题。前面也提到钉钉使用了顺序队列解决了用户消息的排序问题,而当某个用户消息量一大之后,整个顺序队列会非常容易地堆积起来,进而影响其他用户的消息延时。此外,还需要为每个用户提供一个pts计数器。早期钉钉利用缓存机制实现了pts计数器,但是当这样的方案遇到容灾故障以后,极易造成pts的回退,而再次分配pts则有可能使得入库顺序变得混乱,进而导致消息丢失。面对性能瓶颈,当时钉钉和阿里云的Table Store团队共创引入了自增列的概念。具体而言就是将生成pts的过程和消息入库的过程进行了整合,将两个过程整合为一个原子性操作,由存储分配pts,这样就解决了性能瓶颈,并且极大地提升同步协议推送效率。如果大家想要更加具体地了解关于自增列的相关内容,阿里云的官网上面也提供了更加详细介绍,并且目前这一功能已经对外输出了。
推送链路优化——消息到达率提升
众所周知,消息的到达率一直都是IM开发人员心中的痛,在消息到达率的定义上,每个公司有自己的标准,钉钉是对每天新产生的消息,从产生到推送给端上的时间进行了分片,用每个分片除以一天内产生的消息总量作为每个分片的推送到达率,这样就统计出了钉钉每天产生消息的到达率情况。而且钉钉将10秒内产生的消息推送到端上的到达率作为衡量IM的到达率和可靠性的重要指标。2015年,钉钉的10秒到达率才30%,经常收到用户的投诉和吐槽,那时的到达率也确实很低。后来,钉钉成立了一个专门的优化小组来对消息到达率进行专项优化。
对于ios设备,大家都知道通过APNS推送是不存在消息到达率问题的,因为ios的消息推送体验还是相当好的。但是当时也存在另外一个问题,因为开始时国内是直联到苹果服务器的,当APNS量非常大的时候,丢包率就会非常高。针对这个问题,钉钉搭建了海外的加速节点,在消息产生之后先通过阿里巴巴内部专线投递到海外节点,再投递给苹果服务器,这样ios上的消息到达率就有了很好的保证。
但是提到Android,大家往往会默默地流泪,这是因为国内推送的环境实在太糟糕了,所以钉钉针对安卓设备做了重点设备的适配。钉钉团队将使用Android系统的手机产生的整体占比拉出来,针对重点机型,比如排在前几位的华为、VIVO以及小米等进行了重点适配。钉钉还和华为厂商进行合作,接入到了华为智能心跳,与华为自己的推送通道结合保证了钉钉在华为Android端的消息到达率。OPPO和VIVO对于应用存在严格的功耗测试,需要保障待机时间等性能指标。而小米的系统有自己的Push,可以实现类似于APNS的系统级推送,接入到小米Push之后基本上就可以保证消息的到达率。针对海外用户,钉钉使用了Firebase推送链路确保海外设备高可用的到达率保障。2016年,钉钉的10秒消息到达率提升到60%多,而且使用Android设备的用户的投诉率也大大降低了。
四、单元容灾及未来挑战钉钉目前主要在单元化容灾方面进行发力。之所以需要实现单元化支持,主要是因为三点:第一,企业高可用需求;第二,国际化战略;第三,政企专有云。基于以上三点,钉钉采取了单元化的方式进行实现,各个单元之间的流量和数据都是不能互通的,基本上满足了这三点的需要。
在单元的可用性方面抽象出了三个概念:高可用单元、普通可用单元和低可用单元。高可用单元的实现类似于“两地三中心”的思想,当主机房挂掉之后,异地机房可以立即承担起来自主机房的流量,数据在异地单元也有完整的热备。普通可用单元基本上在同城的双机房进行数据互备。而低可用单元就是数据单点了。通过以上的三个概念,基本上可以满足企业对于不同层次的高可用需求,同时也降低了钉钉的消息存储成本。
钉钉IM系统未来的挑战与规划
万人大群推送策略。钉钉现在采用的是写扩散方式,而目前使用钉钉的万人企业越来越多,写扩散的推送策略可能无法承接大群的需要,所以未来需要思考是否可以采用读扩散的方式。
消息存储成本管控。钉钉支持消息路由,用户在钉钉上近一年的消息记录都是持久化保存下来的,可以随时拉取到。显然,数据的存储成本也会成为需要重点考虑的问题,消息成本的管控也是未来需要优化的方向。
用户跨单元的数据迁移需求。目前钉钉已经实现单元化,势必会带来某个用户从低单元迁移到高单元或者从高单元迁移到低单元的场景,确保数据一致性迁移过程的用户体验也是比较严峻的考验。
IM群组能力开放。目前钉钉的群组能力是通过chatbot机制实现的,未来需要通过更多的方式与外界进行信息流流转,比如通过群微应用打造更加完整的群聊生态,最终目的是保障整个企业在钉钉上的沟通实现高效率和高可用。