关于手机移动数据未触发限速 却上网很慢?

Kafka在美团数据平台的现状

Kafka出色的I/O优化以及多处异步化设计,相比其他消息队列系统具有更高的吞吐,同时能够保证不错的延迟,十分适合应用在整个大数据生态中。

目前在美团数据平台中,Kafka承担着数据缓冲和分发的角色。如下图所示,业务日志、接入层Nginx日志或线上DB数据通过数据采集层发送到Kafka,后续数据被用户的实时作业消费、计算,或经过数仓的ODS层用作数仓生产,还有一部分则会进入公司统一日志中心,帮助工程师排查线上问题。

目前美团线上Kafka规模:

  • 集群规模:节点数达6000+,集群数100+。
  • 处理的消息规模:目前每天处理消息总量达8万亿,峰值流量为1.8亿条/秒
  • 提供的服务规模:目前下游实时计算平台运行了3万+作业,而这其中绝大多数的数据源均来自Kafka。

当前Kafka支撑的实时作业数量众多,单机承载的Topic和Partition数量很大。这种场景下很容易出现的问题是:同一台机器上不同Partition间竞争PageCache资源,相互影响,导致整个Broker的处理延迟上升、吞吐下降。

接下来,我们将结合Kafka读写请求的处理流程以及线上统计的数据来分析一下Kafka在线上的痛点。

对于Produce请求:Server端的I/O线程统一将请求中的数据写入到操作系统的PageCache后立即返回,当消息条数到达一定阈值后,Kafka应用本身或操作系统内核会触发强制刷盘操作(如左侧流程图所示)。

Broker接收到读数据请求时,会向操作系统发送sendfile系统调用,操作系统接收后,首先试图从PageCache中获取数据(如中间流程图所示);如果数据不存在,会触发缺页异常中断将数据从磁盘读入到临时缓冲区中(如右侧流程图所示),随后通过DMA操作直接将数据拷贝到网卡缓冲区中等待后续的TCP传输。

综上所述,Kafka对于单一读写请求均拥有很好的吞吐和延迟。处理写请求时,数据写入PageCache后立即返回,数据通过异步方式批量刷入磁盘,既保证了多数写请求都能有较低的延迟,同时批量顺序刷盘对磁盘更加友好。处理读请求时,实时消费的作业可以直接从PageCache读取到数据,请求延迟较小,同时ZeroCopy机制能够减少数据传输过程中用户态与内核态的切换,大幅提升了数据传输的效率。

但当同一个Broker上同时存在多个Consumer时,就可能会由于多个Consumer竞争PageCache资源导致它们同时产生延迟。下面我们以两个Consumer为例详细说明:

如上图所示,Producer将数据发送到Broker,PageCache会缓存这部分数据。当所有Consumer的消费能力充足时,所有的数据都会从PageCache读取,全部Consumer实例的延迟都较低。此时如果其中一个Consumer出现消费延迟(图中的Consumer Process2),根据读请求处理流程可知,此时会触发磁盘读取,在从磁盘读取数据的同时会预读部分数据到PageCache中。当PageCache空间不足时,会按照LRU策略开始淘汰数据,此时延迟消费的Consumer读取到的数据会替换PageCache中实时的缓存数据。后续当实时消费请求到达时,由于PageCache中的数据已被替换掉,会产生预期外的磁盘读取。这样会导致两个后果:

  1. 消费能力充足的Consumer消费时会失去PageCache的性能红利。
  2. 多个Consumer相互影响,预期外的磁盘读增多,HDD负载升高。

我们针对HDD的性能和读写并发的影响做了梯度测试,如下图所示:

可以看到,随着读并发的增加,HDD的IOPS和带宽均会明显下降,这会进一步影响整个Broker的吞吐以及处理延迟。

8min,可见当前Kafka服务整体对延迟消费作业的容忍性极低。该情况下,一旦部分作业消费延迟,实时消费作业就可能会受到影响。

同时,我们统计了线上实时作业的消费延迟分布情况,延迟范围在0-8min(实时消费)的作业只占80%,说明目前存在线上存在20%的作业处于延迟消费的状态。

总结上述的原理分析以及线上数据统计,目前线上Kafka存在如下问题:

  1. 实时消费与延迟消费的作业在PageCache层次产生竞争,导致实时消费产生非预期磁盘读。
  2. 传统HDD随着读并发升高性能急剧下降。
  3. 线上存在20%的延迟消费作业。

按目前的PageCache空间分配以及线上集群流量分析,Kafka无法对实时消费作业提供稳定的服务质量保障,该痛点亟待解决。

根据上述痛点分析,我们的预期目标为保证实时消费作业不会由于PageCache竞争而被延迟消费作业影响,保证Kafka对实时消费作业提供稳定的服务质量保障。

根据上述原因分析可知,解决目前痛点可从以下两个方向来考虑:

  1. 消除实时消费与延迟消费间的PageCache竞争,如:让延迟消费作业读取的数据不回写PageCache,或增大PageCache的分配量等。
  2. 在HDD与内存之间加入新的设备,该设备拥有比HDD更好的读写带宽与IOPS。

对于第一个方向,由于PageCache由操作系统管理,若修改其淘汰策略,那么实现难度较为复杂,同时会破坏内核本身对外的语义。另外,内存资源成本较高,无法进行无限制的扩展,因此需要考虑第二个方向。

SSD目前发展日益成熟,相较于HDD,SSD的IOPS与带宽拥有数量级级别的提升,很适合在上述场景中当PageCache出现竞争后承接部分读流量。我们对SSD的性能也进行了测试,结果如下图所示:

从图中可以发现,随着读取并发的增加,SSD的IOPS与带宽并不会显著降低。通过该结论可知,我们可以使用SSD作为PageCache与HDD间的缓存层。

在引入SSD作为缓存层后,下一步要解决的关键问题包括PageCache、SSD、HDD三者间的数据同步以及读写请求的数据路由等问题,同时我们的新缓存架构需要充分匹配Kafka引擎读写请求的特征。本小节将介绍新架构如何在选型与设计上解决上述提到的问题。

Kafka引擎在读写行为上具有如下特性:

  • 数据的消费频率随时间变化,越久远的数据消费频率越低。
  • 对于一个客户端而言,消费行为是线性的,数据并不会重复消费。

下文给出了两种备选方案,下面将对两种方案给出我们的选取依据与架构决策。

备选方案一:基于操作系统内核层实现

如下图所示,FlashCache以及OpenCAS二者的核心设计思路类似,两种架构的核心理论依据为“数据局部性”原理,将SSD与HDD按照相同的粒度拆成固定的管理单元,之后将SSD上的空间映射到多块HDD层的设备上(逻辑映射or物理映射)。在访问流程上,与CPU访问高速缓存和主存的流程类似,首先尝试访问Cache层,如果出现CacheMiss,则会访问HDD层,同时根据数据局部性原理,这部分数据将回写到Cache层。如果Cache空间已满,会通过LRU策略替换部分数据。

  • WriteThrough:数据写操作在写入SSD的同时会写入到后端存储。
  • WriteBack:数据写操作仅写入SSD即返回,由缓存策略flush到后台存储。
  • WriteAround:数据写入操作直接写入后端存储,同时SSD对应的缓存会失效。

更多详细实现细节,极大可参见这二者的官方文档:

备选方案二:Kafka应用内部实现

上文提到的第一类备选方案中,核心的理论依据“数据局部性”原理与Kafka的读写特性并不能完全吻合,“数据回刷”这一特性依然会引入缓存空间污染问题。同时,上述架构基于LRU的淘汰策略也与Kafka读写特性存在矛盾,在多Consumer并发消费时,LRU淘汰策略可能会误淘汰掉一些近实时数据,导致实时消费作业出现性能抖动。

可见,备选方案一并不能完全解决当前Kafka的痛点,需要从应用内部进行改造。整体设计思路如下,将数据按照时间维度分布在不同的设备中,近实时部分的数据缓存在SSD中,这样当出现PageCache竞争时,实时消费作业从SSD中读取数据,保证实时作业不会受到延迟消费作业影响。下图展示了基于应用层实现的架构处理读请求的流程:

当消费请求到达Kafka Broker时,Kafka Broker直接根据其维护的消息偏移量(Offset)和设备的关系从对应的设备中获取数据并返回,并且在读请求中并不会将HDD中读取的数据回刷到SSD,防止出现缓存污染。同时访问路径明确,不会由于Cache Miss而产生的额外访问开销。

下表对不同候选方案进行了更加详细的对比:

最终,结合与Kafka读写特性的匹配度,整体工作量等因素综合考虑,我们采用Kafka应用层实现这一方案,因为该方案更贴近Kafka本身读写特性,能更加彻底地解决Kafka的痛点。

根据上文对Kafka读写特性的分析,我们给出应用层基于SSD的缓存架构的设计目标:

  • 数据按时间维度分布在不同的设备上,近实时数据分布在SSD上,随时间的推移淘汰到HDD上。
  • Leader分区中所有数据均写入SSD中。
  • 从HDD中读取的数据不回刷到SSD中。

依据上述目标,我们给出应用层基于SSD的Kafka缓存架构实现:

根据上一小节的设计思路,我们首先将不同的LogSegment标记为不同的状态,如图所示(图中上半部分)按照时间维度分为OnlyCache、Cached以及WithoutCache三种常驻状态。而三种状态的转换以及新架构对读写操作的处理如图中下半部分所示,其中标记为OnlyCached状态的LogSegment只存储在SSD上,后台线程会定期将Inactive(没有写流量)的LogSegment同步到SSD上,完成同步的LogSegment被标记为Cached状态。最后,后台线程将会定期检测SSD上的使用空间,当空间达到阈值时,后台线程将会按照时间维度将距离现在最久的LogSegment从SSD中移除,这部分LogSegment会被标记为WithoutCache状态。

对于写请求而言,写入请求依然首先将数据写入到PageCache中,满足阈值条件后将会刷入SSD。对于读请求(当PageCache未获取到数据时),如果读取的offset对应的LogSegment的状态为Cached或OnlyCache,则数据从SSD返回(图中LC2-LC1以及RC1),如果状态为WithoutCache,则从HDD返回(图中LC1)。

对于Follower副本的数据同步,可根据Topic对延迟以及稳定性的要求,通过配置决定写入到SSD还是HDD。

上文介绍了基于SSD的Kafka应用层缓存架构的设计概要以及核心设计思路,包括读写流程、内部状态管理以及新增后台线程功能等。本小节将介绍该方案的关键优化点,这些优化点均与服务的性能息息相关。主要包括LogSegment同步以及Append刷盘策略优化,下面将分别进行介绍。

LogSegment同步是指将SSD上的数据同步到HDD上的过程,该机制在设计时主要有以下两个关键点:

  1. 同步的方式:同步方式决定了HDD上对SSD数据的可见时效性,从而会影响故障恢复以及LogSegment清理的及时性。
  2. 同步限速:LogSegment同步过程中通过限速机制来防止同步过程中对正常读写请求造成影响

关于LogSegment的同步方式,我们给出了三种备选方案,下表列举了三种方案的介绍以及各自的优缺点:

最终,我们对一致性维护代价、实现复杂度等因素综合考虑,选择了后台同步Inactive的LogSegment的方式。

LogSegment同步行为本质上是设备间的数据传输,会同时在两个设备上产生额外的读写流量,占用对应设备的读写带宽。同时,由于我们选择了同步Inactive部分的数据,需要进行整段的同步。如果在同步过程中不加以限制会对服务整体延迟造成较大的影响,主要表现在下面两个方面:

  • 从单盘性能角度,由于SSD的性能远高于HDD,因此在数据传输时,HDD写入带宽会被写满,此时其他的读写请求会出现毛刺,如果此时有延迟消费从HDD上读取数据或者Follower正在同步数据到HDD上,会造成服务抖动。
  • 从单机部署的角度,单机会部署2块SSD与10块HDD,因此在同步过程中,1块SSD需要承受5块HDD的写入量,因此SSD同样会在同步过程中出现性能毛刺,影响正常的请求响应延迟。

基于上述两点,我们需要在LogSegment同步过程中增加限速机制,总体的限速原则为在不影响正常读写请求延迟的情况下尽可能快速地进行同步。因为同步速度过慢会导致SSD数据无法被及时清理而最终被写满。同时为了可以灵活调整,该配置也被设置为单Broker粒度的配置参数。

除了同步问题,数据写入过程中的刷盘机制同样影响服务的读写延迟。该机制的设计不仅会影响新架构的性能,对原生Kafka同样会产生影响。

下图展示了单次写入请求的处理流程:

在Produce请求处理流程中,首先根据当前LogSegment的位置与请求中的数据信息确定是否需要滚动日志段,随后将请求中的数据写入到PageCache中,更新LEO以及统计信息,最后根据统计信息确定是否需要触发刷盘操作,如果需要则通过fileChannel.force强制刷盘,否则请求直接返回。

在整个流程中,除日志滚动与刷盘操作外,其他操作均为内存操作,不会带来性能问题。日志滚动涉及文件系统的操作,目前,Kafka中提供了日志滚动的扰动参数,防止多个Segment同时触发滚动操作给文件系统带来压力。针对日志刷盘操作,目前Kafka给出的机制是以固定消息条数触发强制刷盘(目前线上为50000),该机制只能保证在入流量一定时,消息会以相同的频率刷盘,但无法限制每次刷入磁盘的数据量,对磁盘的负载无法提供有效的限制。

如下图所示,为某磁盘在午高峰时间段write_bytes的瞬时值,在午高峰时间段,由于写入流量的上升,在刷盘过程中会产生大量的毛刺,而毛刺的值几乎接近磁盘最大的写入带宽,这会使读写请求延迟发生抖动。

针对该问题,我们修改了刷盘的机制,将原本的按条数限制修改为按实际刷盘的速率限制,对于单个Segment,刷盘速率限制为2MB/s。该值考虑了线上实际的平均消息大小,如果设置过小,对于单条消息较大的Topic会过于频繁的进行刷新,在流量较高时反而会加重平均延迟。目前该机制已在线上小范围灰度,右图展示了灰度后同时间段对应的write_bytes指标,可以看到相比左图,数据刷盘速率较灰度前明显平滑,最高速率仅为40MB/s左右。

对于SSD新缓存架构,同样存在上述问题,因此在新架构中,在刷盘操作中同样对刷盘速率进行了限制。

  • 验证基于应用层的SSD缓存架构能够避免实时作业受到延迟作业的影响。
  • 验证相比基于操作系统内核层实现的缓存层架构,基于应用层的SSD架构在不同流量下读写延迟更低。

  • 构建4个集群:新架构集群、普通HDD集群、FlashCache集群、OpenCAS集群。
  • 固定写入流量,比较读、写耗时。
  • 延迟消费设置:只消费相对当前时间10~150分钟的数据(超过PageCache的承载区域,不超过SSD的承载区域)。

测试内容及重点关注指标

  • Case1: 仅有延迟消费时,观察集群的生产和消费性能。
    • 重点关注的指标:写耗时、读耗时,通过这2个指标体现出读写延迟。
    • 命中率指标:HDD读取量、HDD读取占比(HDD读取量/读取总量)、SSD读取命中率,通过这3个指标体现出SSD缓存的命中率。
  • Case2: 存在延迟消费时,观察实时消费的性能。
    • 重点指标:实时作业的SLA(服务质量)的5个不同时间区域的占比情况。

从单Broker请求延迟角度看:

在刷盘机制优化前,SSD新缓存架构在所有场景下,较其他方案都具有明显优势。

刷盘机制优化后,其余方案在延迟上服务质量有提升,在较小流量下由于flush机制的优化,新架构与其他方案的优势变小。当单节点写入流量较大时(大于170MB)优势明显。

从延迟作业对实时作业的影响方面看:

新缓存架构在测试所涉及的所有场景中,延迟作业都不会对实时作业产生影响,符合预期。

Kafka在美团数据平台承担统一的数据缓存和分发的角色,针对目前由于PageCache互相污染、进而引发PageCache竞争导致实时作业被延迟作业影响的痛点,我们基于SSD自研了Kafka的应用层缓存架构。本文主要介绍Kafka新架构的设计思路以及与其他开源方案的对比。与普通集群相比,新缓存架构具备非常明显的优势:

  1. 降低读写耗时:比起普通集群,新架构集群读写耗时降低80%。
  2. 实时消费不受延迟消费的影响:比起普通集群,新架构集群实时读写性能稳定,不受延时消费的影响。

目前,这套缓存架构优已经验证完成,正在灰度阶段,未来也优先部署到高优集群。其中涉及的代码也将提交给Kafka社区,作为对社区的回馈,也欢迎大家跟我们一起交流。

世吉,仕禄,均为美团数据平台工程师。

本文由B站微服务技术团队资深开发工程师周佳辉原创分享。

如果你在 2015 年就使用 B 站,那么你一定不会忘记那一年 B 站工作日选择性崩溃,周末必然性崩溃的一段时间。

也是那一年 B 站投稿量激增,访问量随之成倍上升,而过去的 PHP 全家桶也开始逐渐展露出颓势,运维难、监控难、排查故障难、调用路径深不见底。

也就是在这一年,B 站开始正式用 Go 重构 B 站,从此B站的API网关技术子开始了从0到1的持续演进。。。

* 补充说明:本次 API 网关演进也以开源形式进行了开发,源码详见本文“”。

PS:本文分享的API网关涉及到的主要是HTTP短连接,虽然跟长连接技术有些差异,但从架构设计思路和实践上是一脉相承的,所以也就收录到了本《》系列文章中。

本文已同步发布于:

周佳辉:哔哩哔哩资深开发工程师。始终以简单为核心的技术设计理念,追求极致简单有效的后端架构。

2017 年加入 B 站,先后从事账号、网关、基础库等开发工作。编码 C/V 技能传授者,技术文档背诵者。开源社区爱好者,安全技术爱好者,云计算行业活跃用户,网络工程熟练工。史诗级 bug 生产者,熟练掌握 bug 产生的各类场景。

本文是专题系列文章的第8篇,总目录如下:

鉴于引言中所列举的各种技术问题,也是在2015年,财队开始正式用 Go 重构 B 站。

B站第一个 Go 项目——bilizone,由冠冠老师(郝冠伟)花了一个周末时间编码完成。

让开发者专注于应用逻辑的开发,底层复杂的即时通讯算法交由SDK开发人员,从而解偶即时通讯应用开发的复杂性。

  1. iOS客户端SDK:用于开发iOS版即时通讯客户端,支持iOS 8.0及以上,;
  2. Java客户端SDK:用于开发跨平台的PC端即时通讯客户端,支持Java 1.6及以上,;
  3. H5客户端SDK:资料整理中,不日正式发布;
  4. 服务端SDK:用于开发即时通讯服务端,支持Java 1.7及以上版本,。
  1. 支持多端互踢功能(可应对复杂的移动端网络变动逻辑对多端互踢算法的影响);
  1. [服务端] 解决了UDP协议下,重连情况下的被踢者已被服务端注销会话后,客户端才发回登陆响应ACK应答,导致服务端错误地向未被踢者发出已登陆者重复登陆响应的问题;
  1. [iOS] 解决了iOS端SDK工程中两处因类名重构导致的在XCode12.5.1上编译出错。
  2. [服务端] 将服务端Demo中的Log4j日志框架升级为最新的Log4j2;
  3. [服务端] 服务端可控制是否为每条消息生成发送时间戳(可辅助用于客户端的消息排序逻辑等)。

本文作者潘唐磊,腾讯WXG(微信事业群)开发工程师,毕业于中山大学。内容有修订。1、内容概述本文总结了企业微信的IM消息系统架构设计,阐述了企业业务给IM架构设计带来的技术难点和挑战,以及技术方案的对比与分析。同时总结了IM后台开发的一些常用手段,适用于IM消息系统。* 推荐阅读:企业微信团队分享的另一篇《企业微信客户端中组织架构数据的同步更新方案优化实战》也值得一读。学习交流:-

本文由喜马拉雅技术团队李乾坤原创,原题《推送系统实践》,感谢作者的无私分享。

1.1 什么是离线消息推送

对于IM的开发者来说,离线消息推送是再熟悉不过的需求了,比如下图就是典型的IM离线消息通知效果。

移动端离线消息推送涉及的端无非就是两个——iOS端和Andriod端,iOS端没什么好说的,是唯一选项。

Andriod端比较奇葩(主要指国内的手机),为了实现离线推送,各种保活黑科技层出不穷,随着保活难度的不断升级,可以使用的保活手段也是越来越少,有兴趣可以读一读我整理的下面这些文章,感受一下(文章是按时间顺序,随着Andriod系统保活难度的提升,不断进阶的)。

上面这几篇只是我整理的这方面的文章中的一部分,特别注意这最后一篇《》。是的,当前Andriod系统对APP自已保活的容忍度几乎为0,所以那些曾今的保活手段在新版本系统里,几乎统统都失效了。

自已做保活已经没戏了,保离线消息推送总归是还得做。怎么办?按照现时的最佳实践,那就是对接种手机厂商的ROOM级推送通道。具体我就不在这里展开,有兴趣的地可以详读《》。

自已做保活、自建推送通道的时代(这里当然指的是Andriod端啦),离线消息推送这种系统的架构设计相对简单,无非就是每台终端计算出一个deviceID,服务端通过自建通道进行消息透传,就这么点事。

而在自建通道死翘翘,只能依赖厂商推送通道的如今,、、、、(这只是主流的几家)等等,手机型号太多,各家的推送API、设计规范各不相同(别跟我提什么统一推送联盟,那玩意儿我等他3年了——详见《),这也直接导致先前的离线消息推送系统架构设计必须重新设计,以适应新时代的推送技术要求。

1.3 怎么设计合理呢

那么,针对不同厂商的ROOM级推送通道,我们的后台推送架构到底该怎么设计合理呢?

本文分享的离线消息推送系统设计并非专门针对IM产品,但无论业务层的差别有多少,大致的技术思路上都是相通的,希望借喜马拉雅的这篇分享能给正在设计大用户量的离线消息推送的你带来些许启发。

* 推荐阅读:喜马拉雅技术团队分享的另一篇《》,有兴趣也可以一并阅读。

首先介绍下在喜马拉雅APP中推送系统的作用,如下图就是一个新闻业务的推送/通知。

离线推送主要就是在用户不打开APP的时候有一个手段触达用户,保持APP的存在感,提高APP的日活。

我们目前主要用推送的业务包括:

  • 1)主播开播:公司有直播业务,主播在开直播的时候会给这个主播的所有粉丝发一个推送开播提醒
  • 2)专辑更新:平台上有非常多的专辑,专辑下面是一系列具体的声音,比如一本儿小说是一个专辑,小说有很多章节,那么当小说更新章节的时候给所有订阅这个专辑的用户发一个更新的提醒:
  • 3)个性化、新闻业务等。

既然想给一个用户发离线推送,系统就要跟这个用户设备之间有一个联系的通道。

做过这个的都知道:自建推送通道需要App常驻后台(就是引言里提到的应用“保活”),而手机厂商因为省电等原因普遍采取“激进”的后台进程管理策略,导致自建通道质量较差。目前通道一般是由“推送服务商”去维护,也就是说公司内的推送系统并不直接给用户发推送(就是上节内容的这篇里提到的情况:《)。

这种情况下的离线推送流转流程如下:

国内的几大厂商(、、、、等)都有自己官方的推送通道,但是每一家接口都不一样,所以一些厂商比如小米、个推提供集成接口。发送时推送系统发给集成商,然后集成商根据具体的设备,发给具体的厂商推送通道,最终发给用户。

给设备发推送的时候,必须说清楚你要发的是什么内容:即title、message/body,还要指定给哪个设备发推送。

我们以token来标识一个设备, 在不同的场景下token的含义是不一样的,公司内部一般用uid或者deviceId标识一个设备,对于集成商、不同的厂商也有自己对设备的唯一“编号”,所以公司内部的推送服务,要负责进行uid、deviceId到集成商token 的转换。

如上图所示,推送系统整体上是一个基于队列的流式处理系统。

上图右侧:是主链路,各个业务方通过推送接口给推送系统发推送,推送接口会把数据发到一个队列,由转换和过滤服务消费。转换就是上文说的uid/deviceId到token的转换,过滤下文专门讲,转换过滤处理后发给发送模块,最终给到集成商接口。

App 启动时:会向服务端发送绑定请求,上报uid/deviceId与token的绑定关系。当卸载/重装App等导致token失效时,集成商通过http回调告知推送系统。各个组件都会通过kafka 发送流水到公司的xstream 实时流处理集群,聚合数据并落盘到mysql,最终由grafana提供各种报表展示。

各个业务方可以无脑给用户发推送,但推送系统要有节制,因此要对业务消息有选择的过滤。

过滤机制的设计包括以下几点(按支持的先后顺序):

  • 1)用户开关:App支持配置用户开关,若用户关闭了推送,则不向用户设备发推送;
  • 2)文案排重:一个用户不能收到重复的文案,用于防止上游业务方发送逻辑出错;
  • 3)频率控制:每一个业务对应一个msg_type,设定xx时间内最多发xx条推送;
  • 4)静默时间:每天xx点到xx点不给用户发推送,以免打扰用户休息。
  • 5)分级管理:从用户和消息两维度进行分级控制。

针对第5点,具体来说就是:

  • 2)当用户一天收到xx条推送时,不是重要的消息就不再发给这些用户。

很多时候,设计都是基于理论和经验,但实操时,总会遇到各种具体的问题。

喜马拉雅现在已经有6亿+用户,对应的推送系统的设备表(记录uid/deviceId到token的映射)也有类似的量级,所以对设备表进行了分库分表,以 deviceId 为分表列。

因为每天会进行一两次全局推,且针对沉默用户(即不常使用APP的用户)也有专门的推送,存储方面实际上不存在“热点”,虽然使用了缓存,但作用很有限,且占用空间巨大。

多分表以及缓存导致数据存在三四个副本,不同逻辑使用不同副本,经常出现不一致问题(追求一致则影响性能), 查询代码非常复杂且性能较低。

最终我们选择了将设备数据存储在tidb上,在性能够用的前提下,大大简化了代码。

推送系统是基于队列的,“先到先推”。大部分业务不要求很高的实时性,但直播业务要求半个小时送达,新闻业务更是“欲求不满”,越快越好。

若进行新闻推送时:队列中有巨量的“专辑更新”推送等待处理,则专辑更新业务会严重干扰新闻业务的送达。

6.2 这是隔离问题?

一开始我们认为这是一个隔离问题:比如10个消费节点,3个专门负责高时效性业务、7个节点负责一般业务。当时队列用的是,为此改造了 spring-rabbit 支持根据msytype将消息路由到特定节点。

  • 1)总有一些机器很忙的时候,另一些机器在“袖手旁观”;
  • 2)新增业务时,需要额外配置msgType到消费节点的映射关系,维护成本较高;
  • 3)rabbitmq基于内存实现,推送瞬时高峰时占用内存较大,进而引发rabbitmq 不稳定。

6.3 其实是个优先级问题

后来我们觉察到这是一个优先级问题:高优先级业务/消息可以插队,于是封装kafka支持优先级,比较好的解决了隔离性方案带来的问题。具体实现是建立多个topic,一个topic代表一个优先级,封装kafka主要是封装消费端的逻辑(即构造一个PriorityConsumer)。

  • 1.1)如果使用有界队列,队列打满后,后面的消息优先级再高也put 不进去,失去“插队”效果;
  • 1.2)如果使用无界队列,本来应堆在kafka上的消息都会堆到内存里,OOM的风险很大。

2)先拉取高优先级topic的数据:只要有就一直消费,直到没有数据再消费低一级topic。消费低一级topic的过程中,如果发现有高一级topic消息到来,则转向消费高优先级消息。

该方案实现较为复杂,且在晚高峰等推送密集的时间段,可能会导致低优先级业务完全失去推送机会。

3)优先级从高到低,循环拉取数据:

都会被消费,通过一次消费数量的多少来变相实现“插队效果”。具体细节上还借鉴了“滑动窗口”策略来优化某个优先级的topic 长期没有消息时总的消费性能。

从中我们可以看到,时效问题先是被理解为一个隔离问题,后被视为优先级问题,最终转化为了一个权重问题。

在我们的架构中,影响推送发送速度的主要就是tidb查询和过滤逻辑,过滤机制又分为存储和性能两个问题。

这里我们以xx业务频控限制“一个小时最多发送一条”为例来进行分析。

  • 1)发送时,incr key,发送次数加1;
  • 2)如果超限(incr命令返回值>发送次数上限),则不推送;
  • 3)若未超限且返回值为1,说明在msgtype频控周期内第一次向该deviceId发消息,需expire key设置过期时间(等于频控周期)。
  • 1)目前公司有60+推送业务, 6亿+ deviceId,一共6亿*60个key ,占用空间巨大;

为此,我们的解决方法是:

  • 1)使用pika(基于磁盘的redis)替换redis,磁盘空间可以满足存储需求;
  • 2)委托系统架构组扩充了redis协议,支持新结构ehash。
  • 1)当field未设置有效期时,则为其设置有效期;
  • 2)当field还未过期时,则忽略有效期参数。

因为推送系统重度使用 incr 指令,可以视为一条写指令,大部分场景还用了pipeline 来实现批量写的效果,我们委托系统架构组小伙伴专门优化了 的写入性能,支持“写模式”(优化了写场景下的相关参数),qps达到10w以上。

ehash结构在流水记录时也发挥了重要作用,比如<deviceId,msgId,>,其中  是我们约定的一个数据格式示例值,前中后三个部分(每个部分占3位)分别表示了某个消息(msgId)针对deviceId的发送、接收和点击详情,比如头3位“100”表示因发送时处于静默时间段所以发送失败。

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:。同步发布链接是:

本文由阿里闲鱼技术团队祈晴分享,本次有修订和改动,感谢作者的技术分享。

本文总结了阿里闲鱼技术团队使用Flutter在对闲鱼IM进行移动端跨端改造过程中的技术实践等,文中对比了传统Native与现在大热的Flutter跨端方案在一些主要技术实现上的差异,以及针对Flutter技术特点的具体技术实现,值得同样准备使用Flutter开发IM的技术同行们借鉴和参考。

闲鱼IM的移动端框架构建于2016至2017年间,期间经过多次迭代升级导致历史包袱累积多,后面又经历IM界面的Flutter化,从而造成了客户端架构愈加复杂。

从开发层面总结闲鱼IM移动端当前架构主要存在如下几个问题:

  • 1)研发效率较低:当前架构涉及到Android/iOS双端的逻辑代码以及Flutter的UI代码,定位问题往往只能从Flutter UI表相倒查到Native逻辑层;
  • 2)架构层次较差:架构设计上分层不清晰,业务逻辑夹杂在核心的逻辑层致使代码变更风险大;
  • 3)性能测试略差:核心数据源存储Native内存,需经Flutter Plugin将数据源序列化上抛Flutter侧,在大批量数据源情况下性能表现较差。

从产品层面总结闲鱼IM移动端当前架构的主要问题如下:

  • 1)定位问题困难:线上舆情反馈千奇百怪,测试始终无法复现相关场景,因此很多时候只能靠现象猜测本质;
  • 2)疑难杂症较多:架构的不稳定性造成出现的问题反复出现,当前疑难杂症主要包括未读红点计数、iPhone5C低端机以及多媒体发送等多个问题;
  • 3)问题差异性大:Android和iOS两端逻辑代码差异大,包括埋点逻辑都不尽相同,排查问题根源时双端都会有不同根因,解决方案也不相同。

为解决当前IM的技术痛点,闲鱼今年特起关于IM架构升级项目,重在解决客户端中Andriod和iOS双端一致性的痛点,初步设想方案就是实现跨端统一的Android/iOS逻辑架构。

在当前行业内跨端方案可初步归类如下图架构:

在GUI层面的跨端方案有、、H5、Uni-APP等,其内存模型大多需要通过桥接到Native模式存储。

在逻辑层面的跨端方案大致有C/C++等与虚拟机无关语言实现跨端,当然汇编语言也可行。

此外有两个独立于上述体系之外的架构就是Flutter和KMM(谷歌基于Kotlin实现类似Flutter架构),其中Flutter运行特定DartVM,将内存数据挂载其自身的isolate中。

考虑闲鱼是Flutter的前沿探索者,方案上优先使用Flutter。然而Flutter的isolate更像一个进程的概念(底层实现非使用进程模式),相比Android,同一进程场景中,Android的Dalvik虚拟机多个线程运行共享一个内存Heap,而DartVM的Isolate运行隔离各自的Heap,因而isolate之间通讯方式比较繁琐(需经过序列化反序列化过程)。

如下图所示:是一个老架构方案,其核心问题主要集中于Native逻辑抽象差,其中逻辑层面还设计到多线程并发使得问题倍增,Android/iOS/Flutter交互繁杂,开发维护成本高,核心层耦合较为严重,无插拔式概念.

考虑到历史架构的问题,演进如下新架构设计:

如上图所示,架构从上至下依次为:

数据源层来源于推送或网络请求,其封装于Native层,通过Flutter插件将消息协议数据上抛到Flutter侧的核心逻辑层,处理完成后变成Flutter DB的Enitity实体,实体中挂载一些消息协议实体。

核心逻辑层将繁杂数据扁平化打包挂载到分发层中的会话内存模型数据或消息内存模型数据,最后通过观察者模式的订阅分发到业务逻辑中。

Flutter IM重点集中改造逻辑层和分发层,将IM核心逻辑和业务层面数据模型进行封装隔离,核心逻辑层和数据库交互后将数据封装到分发层的moduleData中,通过订阅方式分发到业务层数据模型中。

此外在IM模型中DB也是重点依赖的,个人对DB数据库管理进行全面封装解,实现一种轻量级,性能佳的Flutter DB管理框架。

Flutter IM架构的DB存储依赖数据库插件,目前主流插件是。

依据上图Sqflite插件的DB存储模型会有2个等待队列:

  • 一个是Flutter层同步执行队列;
  • 一个是Native层的线程执行队列。

其Android实现机制是HandlerThread,因此Query/Save读写在会同一线程队列中,导致响应速度慢,容易造成DB SQL堆积,此外缺失缓存模型。

于是个人定制如下改进方案:

Flutter侧通过表的主键设计查询时候会优先从Entity Cache层去获取,若缓存不存在,则通过Sqflite插件查询。

同时改造Sqflite插件成支持sync/Async同步异步两种方式操作,对应到Native侧也会有同步线程队列和异步线程队列,保证数据吞吐率。但是这里建议查询使用异步,存储使用同步更稳妥,主要怕出现多个相同的数据元model同一时间进入异步线程池中,存储先后顺序无法有效的保证。

IM架构重度依赖DB数据库,而当前业界还没有一个完备的数据库ORM管理方案,参考了Android的OrmLite/GreenDao,个人自行设计一套Flutter ORM数据库管理方案。

由于Flutter不支持反射,因此无法直接像Android的开源数据库方式操作,但可通过APT方式,将Entity和Orm Entity绑定于一身,操作OrmEntity即操作Entity,整个代码风格设计也和OrmLite极其相似。

基于Flutter的IM移动端架构在内存数据模型主要划分为会话和消息两个颗粒度:

这种做法的好处是各地去拿会话数据元时候都是缓存中同一个对象,容易保证多次重复读写的数据一致性。而PSessionMessageNotice考虑到其数量可以无限多的特殊性,因此这里将其挂载到MessageContainer的内存管理中,在退出会话的时机会校验容器中PMessage集合的数量,适当缩容可以减少内存开销。

基于Flutter的IM移动端架构状态管理方案比较简单,对数据源Session/Message维度使用观察者模式的订阅分发方式实现,架构类似于EventBus模式,页面级的状态管理无论使用、scopeModel或者provider几乎影响面不大,核心还是需保留一种插拔式抽象更重要。

当前现状的消息同步模型:

native的推送和网络请求同步的隔离方案通过Lock的锁机制,并且通过队列降频等方式处理,流程繁琐且易出错。整体通过Region Version Gap去判断是否有域空洞,进而执行域同步补充数据。

如上图所示,在Flutter侧天然没多线程场景,通过一种标记位的转化同步异步实现类似Handler消息队列,架构清晰简约了很多,避免锁带来的开销以及同步问题。

在基于Flutter的IM架构中,重点将双端逻辑差异性统一成同一份Dart代码,完全磨平Android/iOS的代码差异性带来的问题。

  • 1)降低开发维护、测试回归、视觉验收的一半成本,极大提高研发效率;
  • 2)架构上进行重构分层,实现一种解耦合,插拔式的IM架构;
  • 3)同时Native到Flutter侧的大量数据上抛序列化过程改造程Flutter引用传递,解决极限测试场景下的私聊卡顿问题。
  • 1)补齐UT和TLog的集团日志方式做到可追踪,可排查;
  • 2)针对于很多现存的疑难杂症重点集中专项解决,比如iphone5C的架构在Flutter侧统一规划;
  • 3)未读红点计数等问题也在架构模型升级中修复;
  • 4)此外多媒体音视频发送模块进行改造升级。

当IM架构的逻辑层和UI层都切换成Flutter后,和原先架构模式初步对比,整体内存水位持平。

  • 1)私聊场景下小米9测试结构内存下降40M,功耗降低4mah,CPU降低1%;
  • 2)极限测试场景下新架构内存数据相比于旧架构有一个较为明显的改观(主要由于两个界面都使用Flutter场景下,页面切换的开销降低很多)。

JS跨端不安全,C++跨端成本有点高,Flutter会是一个较好选择。彼时闲鱼FlutterIM架构升级根本目的从来不是因Flutter而Flutter,是由于历史包袱的繁重,代码层面的维护成本高,新业务的扩展性差,人力配比不协调以及疑难杂症的舆情持续反馈等等因素造成我们不得不去探索新方案。

经过闲鱼IM超复杂业务场景验证Flutter模式的逻辑跨端可行性,闲鱼在Flutter路上会一直保持前沿探索,最后能反馈到生态圈。

总结一句话,探索过程在于你勇于迈出第一步,后面才会不断惊喜发现。

(原文链接:,本次有修订和改动)

[1] 更多阿里团队的文章分享:

[2] 更多IM开发综合文章:

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:。同步发布链接是:

     摘要: 本文作者张彦飞,原题“127.0.0.1 之本机网络通信过程知多少 ”,首次发布于“开发内功修炼”,转载请联系作者。本次有改动。1、引言继《你真的了解127.0.0.1和0.0.0.0的区别?》之后,这是我整理的第2篇有关本机网络方面的网络编程基础文章。这次的文章由作者张彦飞原创分享,写作本文的原因是现在本机网络 IO

本文由微信开发团队工程师“virwu”分享。1、引言近期,微信小游戏支持了视频号一键开播,将微信升级到最新版本,打开腾讯系小游戏(如跳一跳、欢乐斗地主等),在右上角菜单就可以看到发起直播的按钮一键成为游戏主播了(如下图所示)。然而微信小游戏出于性能和安全等一系列考虑,运行在一个独立的进程中,在该环境中不会初始化视频号直播相关的模块。这就意味着小游戏的音视频数据必须跨进程传输...  

本文引用了“拍乐云Pano”的“深入浅出理解视频编解码技术”和“揭秘视频千倍压缩背后的技术原理之预测技术”文章部分内容,感谢原作者的分享。

从 20 世纪 90 年代以来,数字音视频编解码技术迅速发展,一直是国内外研究的热点领域。随着5G的成熟和广泛商用,带宽已经越来越高,传输音视频变得更加容易。视频直播、视频聊天,已经完全融入了每个人的生活。

视频为何如此普及呢?是因为通过视频能方便快捷地获取到大量信息。但视频数据量非常巨大,视频的网络传输也面临着巨大的挑战。于是视频编解码技术就出场了。

具体到实时视频场景,不仅仅是数据量的问题,实时通信对时延要求、设备适配、带宽适应的要求也非常高,要解决这些问题,始终离不开视频编解码技术的范畴。

本文将从视频编解码技术的基础知识入手,引出视频编解码技术中非常基础且重要的预测技术,学习帧内预测和帧间预测的技术原理。

本文已同步发布于:

如果你是音视频技术初学者,以下3篇入门级干货非常推荐一读:

首先,来复习一下视频编解码方面的理论常识。

视频是由一系列图片按照时间顺序排列而成:

  • 1)每一张图片为一帧;
  • 2)每一帧可以理解为一个二维矩阵;
  • 3)矩阵的每个元素为一个像素。

一个像素通常由三个颜色进行表达,例如用RGB颜色空间表示时,每一个像素由三个颜色分量组成。每一个颜色分量用1个字节来表达,其取值范围就是0~255。编码中常用的YUV格式与之类似,这里不作展开。

fps的视频序列为例,十秒钟的视频有:*60*10 = 1.6GB

如此大量的数据,无论是存储还是传输,都面临巨大的挑战。视频压缩或者编码的目的,也是为了保证视频质量的前提下,将视频减小,以利于传输和存储。同时,为了能正确还原视频,需要将其解码。

PS:限于篇幅,视频编解码方面的技术原理就不在此展开,有兴趣强烈推荐从这篇深入学习:《》。

总之,视频编解码技术的主要作用就是:在可用的计算资源内,追求尽可能高的视频重建质量和尽可能高的压缩比,以达到带宽和存储容量的要求。

为何突出“重建质量”?

因为视频编码是个有损的过程,用户只能从收到的视频流中解析出“重建”画面,它与原始的画面已经不同,例如观看低质量视频时经常会碰到的“块”效应。

如何在一定的带宽占用下,尽可能地保持视频的质量,或者在保持质量情况下,尽可能地减少带宽利用率,是视频编码的基本目标。

用专业术语来说,即视频编解码标准的“率失真”性能:

  • 1)“率”是指码率或者带宽占用;
  • 2)“失真”是用来描述重建视频的质量。

与编码相对应的是解码或者解压缩过程,是将接收到的或者已经存储在介质上的压缩码流重建成视频信号,然后在各种设备上进行显示。

视频编解码标准,通常只定义上述的解码过程。

例如 H.264 / AVC 标准,它定义了什么是符合标准的视频流,对每一个比特的顺序和意义都进行了严格地定义,对如何使用每个比特或者几个比特表达的信息也有精确的定义。

正是这样的严格和精确,保证了不同厂商的视频相关服务,可以很方便地兼容在一起,例如用 iPhone、Android Phone 或者 windows PC 都可以观看同一在线视频网站的同一视频。

世界上有多个组织进行视频编码标准的制定工作,国际标准组织 ISO 的 MPEG 小组、国际电信联盟 ITU-T 的 VCEG 小组、中国的 AVS 工作组、Google 及各大厂商组成的开放媒体联盟等。

视频编码标准及发展历史:

Google、思科、微软、苹果等公司组成的开放媒体联盟(AOM)制定的 AV1。

这里特别提一下H.264/AVC:H.264/AVC虽有近20年历史,但它优秀的压缩性能、适当的运算复杂度、优秀的开源社区支持、友好的专利政策、强大的生态圈等多个方面的因素,依旧让它保持着强大的生命力,特别是在实时通信领域。像 ZOOM、思科 Webex 等视频会议产品和基于 WebRTC SDK 的视频服务,大多数主流场景都采用

有关视频编解码标准,这里就不深入展开。更多详细资料,可以读一下下面这些精选文章:

纵观视频编解码标准历史,每一代视频标准都在率失真性能上有着显著的提升,他们都有一个核心的框架,就是基于块的混合编码框架(如下图所示)。它是由J. R. Jain 和A. K. Jain在1979年的国际图像编码学会(PCS 1979)上提出了基于块运动补偿和变换编码的混合编码框架。

我们一起来对该框架进行拆解和分析。

从摄像头采集到的一帧视频:通常是 YUV 格式的原始数据,我们将它划分成多个方形的像素块依次进行处理(例如 H.264/AVC 中以16x16像素为基本单元),进行帧内/帧间预测、正变换、量化、反量化、反变换、环路滤波、熵编码,最后得到视频码流。从视频第一帧的第一个块开始进行空间预测,因当前正在进行编码处理的图像块和其周围的图像块有相似性,我们可以用周围的像素来预测当前的像素。我们将原始像素减去预测像素得到预测残差,再将预测残差进行变换、量化,得到变换系数,然后将其进行熵编码后得到视频码流。

接下来:为了可以使后续的图像块可以使用已经编码过的块进行预测,我们还要对变换系统进行反量化、反变换,得到重建残差,再与预测值进行求合,得到重建图像。最后我们对重建图像进行环路滤波、去除块效应等,这样得到的重建图像,就可以用来对后续图像块进行预测了。按照以上步骤,我们依次对后续图像块进行处理。

对于视频而言:视频帧与帧的间隔大约只有十到几十毫秒,通常拍摄的内容不会发生剧烈变化,它们之间存在非常强的相关性。

如下图所示,将视频图像分割成块,在时间相邻的图像之间进行匹配,然后将匹配之后的残差部分进行编码,这样可以较好地去除视频信号中的视频帧与帧之间的冗余,达到视频压缩的目的。这就是运动补偿技术,直到今天它仍然是视频编解码的核心技术之一。

变换编码的核心思想:是把视频数据分割成块,利用正交变换将数据的能量集中到较少几个变换系数上。结合量化和熵编码,我们可以获得更有效的压缩。视频编码中信息的损失和压缩比的获得,很大程度上来源于量化模块,就是将源信号中的单一样本映射到某一固定值,形成多到少的映射,从而达到压缩的目的,当然在压缩的过程中就引入了损失。量化后的信号再进行无损的熵编码,消除信号中的统计冗余。熵编码的研究最早可以追溯到 年代,经过几十年的发展,熵编码在视频编码中的应用更加成熟、更加精巧,充分利用视频数据中的上下文信息,将概率模型估计得更加准确,从而提高了熵编码的效率。例如H.264/AVC中的Cavlc(基于上下文的变长编码)、Cabac(基于上下文的二进制算术编码)。算术编码技术在后续的视频编码标准,如AV1、HEVC/H.265、VVC/H.266

视频编码发展至今,VVC/H.266 作为最新制定的标准,采纳了一系列先进的技术,对混合编码框架的各个部分都进行了优化和改进,使得其率失真性能相比前一代标准,又提高了一倍。

例如:VVC/H.266 采用了128x128大小的基本编码单元,并且可以继续进行四叉树划分,支持对一个划分进行二分、三分;色度分量独立于亮度分量,支持单独进行划分;更多更精细的帧内预测方向、帧间预测模式;支持多种尺寸和形式的变换、环内滤波等。

VVC/H.266 的制定,目标是对多种视频内容有更好支持,例如屏幕共享内容、游戏、动漫、虚拟现实内容(VR、AR)等。其中也有特定的技术被采纳进标准,例如调色板模式、帧内运动补偿、仿射变换、跳过变换、自适应颜色变换等。   

回到本文的正题,接下来的内容,我们着重介绍视频编解码中的预测技术。

视频数据被划分成方块之后,相邻的方块的像素,以及方块内的像素,颜色往往是逐渐变化的,他们之间有比较强的有相似性。这种相似性,就是空间冗余。既然存在冗余,就可以用更少的数据量来表达这样的特征。

比如:先传输第一个像素的值,再传输第二个像素相对于第一个像素的变化值,这个变化值往往取值范围变小了许多,原来要8个bit来表达的像素值,可能只需要少于8个bit就足够了。

同样的道理,以像素块为基本单位,也可以进行类似的“差分”操作。我们从示例图中,来更加直观地感受一下这样的相似性。

如上图中所标出的两个8x8的块:其亮度分量(Y)沿着“左上到右下”的方向,具有连续性,变化不大。

假如:我们设计某种特定的“模式”,使其利用左边的块来“预测”右边的块,那么“原始像素”减去“预测像素”就可以减少传输所需要的数据量,同时将该“模式”写入最终的码流,解码器便可以利用左侧的块来“重建”右侧的块。

极端一点讲:假如左侧的块的像素值经过一定的运算可以完全和右侧的块相同,那么编码器只要用一个“模式”的代价,传输右侧的块。

当然,视频中的纹理多种多样,单一的模式很难对所有的纹理都适用,因此标准中也设计了多种多样的帧内预测模式,以充分利用像素间的相关性,达到压缩的目的。

例如下图所示的H.264中9种帧内预测方向:以模式0(竖直预测)为例,上方块的每个像素值(重建)各复制一列,得到帧内预测值。其它各种模式也采用类似的方法,不过,生成预测值的方式稍有不同。有这么多的模式,就产生了一个问题,对于一个块而言,我们应该采用哪种模式来进行编码呢?最佳的选择方式,就是遍历所有的模式进行尝试,计算其编码的所需的比特数和产生的质量损失,即率失真优化,这样明显非常复杂,因而也有很多种其它的方式来推断哪种模式更好,例如基于SATD或者边缘检测等。

从H.264的9种预测模式,到AV1的56种帧内方向预测模式,越来越多的模式也是为了更加精准地预测未编码的块,但是模式的增加,一方面增加了传输模式的码率开销,另一方面,从如此重多的模式中选一个最优的模式来编码,使其能达到更高的压缩比,这对编码器的设计和实现也提出了更高的要求。

以下5张图片是一段视频的前5帧:可以看出,图片中只有Mario和砖块在运动,其余的场景大多是相似的,这种相似性就称之为时间冗余。编码的时候,我们先将第一帧图片通过前文所述的帧内预测方式进行编码传输,再将后续帧的Mario、砖块的运动方向进行传输,解码的时候,就可以将运动信息和第一帧一起来合成后续的帧,这样就大大减少了传输所需的bit数。这种利用时间冗余来进行压缩的技术,就是运动补偿技术。该技术早在H.261标准中,就已经被采用。

细心地读者可能已经发现:Mario和砖块这样的物体怎么描述,才能让它仅凭运动信息就能完整地呈现出来?

其实视频编码中并不需要知道运动的物体的形状,而是将整帧图像划分成像素块,每个像素块使用一个运动信息。即基于块的运动补偿。

下图中红色圈出的白色箭头即编码砖块和Mario时的运动信息,它们都指向了前一帧中所在的位置。Mario和砖块都有两个箭头,说明它们都被划分在了两个块中,每一个块都有单独的运动信息。这些运动信息就是运动矢量。运动矢量有水平和竖直两个分量,代表是的一个块相对于其参考帧的位置变化。参考帧就是已经编码过的某一(多)个帧。

当然:传输运动矢量本身就要占用很多 bit。为了提高运动矢量的传输效率,主要有以下措施。

一方面:可以尽可能得将块划分变大,共用一个运动矢量,因为平坦区域或者较大的物体,他们的运动可能是比较一致的。从 H.264 开始,可变块大小的运动补偿技术被广泛采用。

另一方面:相邻的块之间的运动往往也有比较高的相似性,其运动矢量也有较高的相似性,运动矢量本身也可以根据相邻的块运动矢量来进行预测,即运动矢量预测技术;

最后:运动矢量在表达物体运动的时候,有精度的取舍。像素是离散化的表达,现实中物体的运动显然不是以像素为单位进行运动的,为了精确地表达物体的运动,需要选择合适的精度来定义运动矢量。各视频编解码标准都定义了运动矢量的精度,运动矢量精度越高,越能精确地表达运动,但是代价就是传输运动矢量需要花费更多的bit。

H.261中运动矢量是以整像素为精度的,H.264中运动矢量是以四分之一像素为精度的,AV1中还增加了八分之一精度。一般情况,时间上越近的帧,它们之间的相似性越高,也有例外,例如往复运动的场景等,可能相隔几帧,甚至更远的帧,会有更高的相似度。

为了充分利用已经编码过的帧来提高运动补偿的准确度,从H.264开始引入了多参考帧技术。

即:一个块可以从已经编码过的很多个参考帧中进行运动匹配,将匹配的帧索引和运动矢量信息都进行传输。

那么如何得到一个块的运动信息呢?最朴素的想法就是,将一个块,在其参考帧中,逐个位置进行匹配检查,匹配度最高的,就是最终的运动矢量。

Difference)等。逐个位置进行匹配度检查,即常说的全搜索运动估计,其计算复杂度可想而知是非常高的。为了加快运动估计,我们可以减少搜索的位置数,类似的有很多算法,常用的如钻石搜索、六边形搜索、非对称十字型多层次六边形格点搜索算法等。

以钻石搜索为例,如下图所示,以起始的蓝色点为中心的9个匹配位置,分别计算这9个位置的SAD,如果SAD最小的是中心位置,下一步搜索中心点更近的周围4个绿色点的SAD,选择其中SAD最小的位置,继续缩小范围进行搜索;如果第一步中SAD最小的点不在中心,那么以该位置为中心,增加褐色的5或者3个点,继续计算SAD,如此迭代,直到找到最佳匹配位置。

编码器在实现时,可根据实际的应用场景,对搜索算法进行选择。

例如:在实时音视频场景下,计算复杂度是相对有限的,运动估计模块要选择计算量较小的算法,以平衡复杂度和编码效率。当然,运动估计与运动补偿的复杂度还与块的大小,参考帧的个数,亚像素的计算等有关,在此不再深入展开。

更多预测技术方面的原理这里就不再赘述。如果你对上面所述的预测技术理解上感到力不从心,这里有篇入门级的文章,可以先读读这篇《》。

音视频编解码技术,归根结底就是在有限的资源下(网络带宽、计算资源等),让音质更清晰、视频更高质。

这其中,对于视频来说,质量的提升仍然有很多可以深入研究的热点问题。

比如:基于人眼的主观质量优化,主要利用人眼的视觉特性,将掩蔽效应、对比度灵敏度、注意力模型等与编码相结合,合理分配码率、减少编码损失引起的视觉不适。

AI在视频编解码领域的应用:包括将多种人工智能算法,如分类器、支持向量机、CNN等对编码参数进行快速选择,也可以使用深度学习对视频进行编码环外与编码环内的处理,如视频超分辨率、去噪、去雾、自适应动态范围调整等编码环外处理,达到提升视频质量的目的。

此外还有打破传统混合编码框架的深度神经网络编码,如Nvidia的Maxine视频会议服务,利用深度学习来提取特征,然后对特征进行传输以节省带宽。

[1] 实时音视频开发的其它精华资料:

[2] 开源实时音视频技术WebRTC的文章:

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:。同步发布链接是:

本文作者“商文默”,有修订和改动。

即时通讯网整理的大量IM技术文章中(见本文末“参考资料”一节),有关消息可靠性和一致性问题的文章占了很大比重,原因是IM这类系统抛开各种眼花缭乱的产品功能和技术特性,保证消息的可靠性和一致性几乎是IM产品必需的素质。

试想如果一个IM连发出的消息都不知道对方到底能不能收到、发出的聊天内容对方看到的到底是不是“胡言乱语”(严重乱序问题),这样的APP用户肯定不会让他在手机上过夜(肯定第一时间卸载了),因为最基本的聊天逻辑都无法实现,它已经失去了IM软件本身的意义。

不过,另一个方面来讲,IM系统是不标准的(虽然曾经这种协议试图解决这个问题,但事实证明那根本不现实),各家几乎都是自已的私有协议、不同的实现逻辑,这也决定了即使同一个技术问题,对于IM来说很难有固定的实现套路和标准的解决方案。

所以,对于本文来说,文中作者虽然提供了有关IM消息“可靠性”与“一致性”问题的解决方案,但方案到底合不合理、适不适合你,这就是仁者见仁、智者见智的事了。用人话说就是:本文内容仅供参考,具体的解决方案请务结合自已的系统构架和实现情况,多阅读几篇即时通讯网上有关这个技术话题的文章,取其精华,找到适合自已的技术方案和思路才是最明智的。

丛所周之,即时通讯聊天(IM)系统必需要解决消息可靠性及消息一致性问题(PS:如果具体IM系统是什么你都还没弄明白,先读这篇《)。

这两个问题,通俗来说就是:

  • 1)消息可靠性:简单来说就是不丢消息,会话一方发送消息,消息成功到达对方并正确显示;
  • 2)消息一致性:包括发送一方消息一致及会话双方消息一致,要求消息不重复,不乱序。

本文会从典型的IM消息发送逻辑开始,简单易懂地阐明消息可靠性、一致性问题的原理及可参考的技术解决方法,或许技术方案并不完美,但希望能为你的IM技术问题解决带来启发。

IM的消息发送一般的实现过程可以分为两个阶段:

  • 1)发送方发送消息、服务端接收、返回消息 ACK 给发送方;
  • 2)服务端将消息推送到接收方。

判断消息发送是否成功主要依据第一阶段——即服务器是否接受到消息。

对于消息发送者来说,消息状态可以分为三类:

具体来说,这三类状态的具体意义是:

  • 1)正在发送:发送方触发发送事件开始,到收到服务端返回消息对应 ACK 之前;
  • 2)发送成功:发送方收到消息对应 ACK 回复;
  • 3)发送失败:超过一定重发次数,未收到消息对应 ACK 回复。

对应的消息发送流程如下图所示:

限于篇幅,对于IM消息可靠性的基本概念和详细原理建议阅读《》,本文着重谈谈解决思路。

保证消息发送第一阶段(见本文“”一节)消息成功发送的方法是设立重发机制:

  • 1)依据一定时长内是否收到消息对应 ACK,判断消息是否要重发;
  • 2)如果超过预设时长,就重新发送;
  • 3)当重发次数超过预设次数,就不再重发,判定该消息发送失败,修改消息发送状态。

PS:具体的完整方案级代码实现,可以参考  中有关QoS机制的代码实现。

消息发送第二阶段(见本文“”一节)服务端推送消息到接收方,如果连接断开,会丢失消息。

所以要保证消息完整,就需要在建立连接后,根据上一条消息(已经 ACK)时间戳,获取会话记录,一次返回一段时间内所有消息(PS:中大型应用中,消息的拉取也不是个简单事情,详情可以阅读《)。

另一种保证方法是加入定时轮询,检查消息完整性,具体的思路如下图所示。

4.3 需要考虑的两个问题

消息重发、会话记录检查需要考虑两个问题:

  • 1)消息是否会重复发送;
  • 2)消息顺序是否会被打乱。
  • 1)如果丢消息的点在消息达到服务端之前,服务端并没有收到消息,发送方重新发送丢失消息,服务端接收成功,不会产生两条相同消息;
  • 2)而如果服务端接收到消息,返回 ACK 丢失,这时再发送一次相同消息,就可能造成消息重复。
  • 1)如果发送方连发三条消息,第一、第三条成功被服务端接收,第二条丢了,那第三条消息是否会被记录?
  • 2)如果这时第二条消息达到服务端,其顺序是在第三条时间之前还是之后(服务端一般都会给记录打一个时间戳)?

同上节一样,对于IM消息一致性的基本概念和详细原理建议阅读《》。

对于消息重发问题,可以给每条消息增加属性 uuid 作为消息唯一标识,重发消息 uuid 不变,前端根据 uuid 去重。大致思路就是这样。

PS:对于IM来说,消息ID也是个很大的技术话题,有兴趣可以读下面这个系列:

5.2 使用向量时钟进行消息排序

对于消息排序问题:因为在聊天中,消息的顺序对于发送方的表述有重要的影响,消息不完整或顺序颠倒都可能造成语意不连贯,甚至曲解。所以需要保证发送方发送消息顺序,而会话双方消息排序需要考虑实际情况。

在一般的认知里:状态是正在发送的消息,应该还没有被对方看到,只有发送成功的消息,才会被对方看到。但在实现中,消息发送成功是以服务器接收消息并返回 ACK 成功为判断依据,而不是被对方接收到。

那么就会出现这样一个问题:如果一条消息状态是正在发送,此时收到一条消息,那么收到的消息是在正在发送的消息之前还是之后?

这是一个上下文关系,关键问题是:发送方是以哪条所见消息为依据发送消息的。

这里提供一种思路:借鉴分布式系统中的向量时钟算法(见《)。

先简单描述向量时钟算法:

向量时钟算法用于在分布式系统中生成事件偏序关系,并纠正因果关系。一个系统包含 N 个节点,每个节点产生的消息体中包含该节点的逻辑时钟,整体系统的向量时钟由 N 维逻辑时钟组成,并在每个节点产生的消息体中传递。

简单来说,向量时钟算法的实现原理如下:

  • 1)初始状态,向量值为 0;
  • 2)每次节点处理完节点事件,该节点时钟+1;
  • 3)每次节点发送消息,将包含自身时钟的系统向量时钟一起发送;
  • 4)每次节点收到消息,更新向量时钟,该节点时钟+1,其他节点对比每个节点本地保留的向量时钟值和消息体中向量时钟值,取最大值;
  • 5)节点同时收到多条消息,判断接收消息的向量时钟之间是否存在偏序关系。
  • 1)如果存在偏序关系,则合并向量时钟,取偏序较大的向量时钟;
  • 2)如果不存在偏序关系,则不能合并。

偏序关系:如果 A 向量中的每一维都大于等于 B 向量,则 A、B 之间存在偏序关系,否则不存在偏序关系。

对于IM为聊天消息排序来说,其实就是处理聊天消息的上下文语境,决定消息之间的因果关系。

参考向量时钟算法:假设有 N 个消息会话方,系统的向量时钟由 N 维时钟组成,向量时钟在各方发送的消息体中传递,并依据向量时钟排序。

  • 2)节点发送消息,更新系统向量时钟,该节点时钟加一,其他节点不变;
  • 3)节点接收消息,更新系统向量时钟,该节点时钟加一;其他节点对比每个节点本地保留的向量时钟的值和消息中向量时钟的值,取最大值;
  • 4)依据消息体内系统向量时钟的偏序关系决定消息顺序。
  • 1)如果可以确定偏序关系,则根据偏序关系由小到大显示;
  • 2)如果多条消息不能确定偏序关系,则按照自然顺序(接收到的顺序)显示。

向量时钟在理论上可以解决大部分消息一致性的问题,但在实现中还需要考虑实际使用时的体验。

这其中最需要关注的问题是:是否要强制排序,或者说,如果实际显示顺序和向量时钟之间的偏序关系不一致,是否要移动消息之间的顺序。

举个例子:在一个有多人的会话中,如果有一方网速特别慢,收不到消息,也发不出消息。在他看到的最后的消息之后,其他人已经开始新的话题,这时他关于上一个话题的消息终于发送成功,并被其他人收到。

此时就存在这样一个问题:这条关于上一个话题的消息是显示在最后,还是移到较早时间?

  • 1)如果显示在最后,但消息内容和目前的话题不相关,其他人可能会感到莫名其妙;
  • 2)如果把消息移到较早时间,那么这条消息可能不会被其他人看到,或者看到前面多了一条消息,会有种突兀的感觉。

IM 的场景很多,也很复杂,更多的时候需要从产品角度考虑问题。

对于消息是否需要排序的问题,这里只提出一个比较通用的方案:建议会话中不强制排序,会话历史记录中按照向量时钟的偏序关系进行排序。

对于 IM 系统消息可靠性及一致性问题,通过消息重发机制保证消息成功被服务端接收,通过会话记录检查保证收取消息完整,从而保证整个消息发送过程的可靠性。使用 uuid 消息去重,参考向量时钟算法进行消息排序,为保证消息一致性提供一种解决方案。

总之,IM这类系统看似简单,实则水深似海,如果你是IM开发新手,可以从《》这篇入手系统学习。如果你自认为已是IM老手,这里整理的  方面的文章或许可以参考一下。

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:。同步发布链接是:

本文由喜马拉雅技术团队原创分享,原题《喜马拉雅自研网关架构实践》,有改动。1、引言网关是一个比较成熟的产品,基本上各大互联网公司都会有网关这个中间件,来解决一些公有业务的上浮,而且能快速的更新迭代。如果没有网关,要更新一个公有特性,就要推动所有业务方都更新和发布,那是效率极低的事,有网关后,这一切都变得不是问题。喜马拉雅也是一样,用户数增长达到 6 亿多的级别,Web

本文来自“糊糊糊糊糊了”的分享,原题《实时消息推送整理》,有优化和改动。1、写在前面对Web端即时通讯技术熟悉的开发者来说,我们回顾网页端IM的底层通信技术,从短轮询、长轮询,到后来的SSE以及WebSocket,使用门槛越来越低(早期的长轮询Comet这类技术实际属于hack手段,使用门槛并不低),技术手段越来越先进,网页端即时通讯技术的体验也因此越来越好。但上周在编辑《...  

本文由爱奇艺技术团队原创分享,原题《构建通用WebSocket推送网关的设计与实践》,有优化和改动。

丛所周之,HTTP协议是一种无状态、基于TCP的请求/响应模式的协议,即请求只能由客户端发起、由服务端进行响应。在大多数场景,这种请求/响应的Pull模式可以满足需求。但在某些情形:例如消息推送(IM中最为常见,比如IM的离线消息推送)、实时通知等应用场景,需要实时将数据同步到客户端,这就要求服务端支持主动Push数据的能力。

传统的Web服务端推送技术历史悠久,经历了短轮询、长轮询等阶段的发展(见《),一定程度上能够解决问题,但也存在着不足,例如时效性、资源浪费等。HTML5标准带来的WebSocket规范基本结束了这一局面,成为目前服务端消息推送技术的主流方案。

在系统中集成WebSocket十分简单,相关讨论与资料很丰富。但如何实现一个通用的WebSocket推送网关尚未有成熟的方案。目前的云服务厂商主要关注iOS和安卓等移动端推送,也缺少对WebSocket的支持。本文分享了爱奇艺基于Netty实现WebSocket长连接实时推送网关时的实践经验总结。

本文是系列文章的第4篇,总目录如下:

爱奇艺技术团队分享的其它文章:

爱奇艺号是我们内容生态的重要组成,作为前台系统,对用户体验有较高要求,直接影响着创作者的创作热情。

目前,爱奇艺号多个业务场景中用到了WebSocket实时推送技术,包括:

  • 1)用户评论:实时的将评论消息推送到浏览器;
  • 2)实名认证:合同签署前需要对用户进行实名认证,用户扫描二维码后进入第三方的认证页面,认证完成后异步通知浏览器认证的状态;
  • 3)活体识别:类似实名认证,当活体识别完成后,异步将结果通知浏览器。

在实际的业务开发中,我们发现,WebSocket实时推送技术在使用中存在一些问题。

  • 1)首先:WebSocket技术栈不统一,既有基于Netty实现的,也有基于Web容器实现的,给开发和维护带来困难;
  • 2)其次:WebSocket实现分散在在各个工程中,与业务系统强耦合,如果有其他业务需要集成WebSocket,面临着重复开发的窘境,浪费成本、效率低下;
  • 3)第三:WebSocket是有状态协议的,客户端连接服务器时只和集群中一个节点连接,数据传输过程中也只与这一节点通信。WebSocket集群需要解决会话共享的问题。如果只采用单节点部署,虽然可以避免这一问题,但无法水平扩展支撑更高负载,有单点的风险;
  • 4)最后:缺乏监控与报警,虽然可以通过Linux的Socket连接数大致评估WebSocket长连接数,但数字并不准确,也无法得知用户数等具有业务含义的指标数据;无法与现有的微服务监控整合,实现统一监控和报警。

PS:限于篇幅本文不详细介绍WebSocket技术本身,有兴趣可以详读《》。

如上节所示,为了解决旧方案中存在的问题,我们需要实现统一的WebSocket长连接实时推送网关。

这套新的网关需要具备如下特点:

  • 1)集中实现长连接管理和推送能力:统一技术栈,将长连接作为基础能力沉淀,便于功能迭代和升级维护;
  • 2)与业务解耦:将业务逻辑与长连接通信分离,使业务系统不再关心通信细节,也避免了重复开发,浪费研发成本;
  • 3)使用简单:提供HTTP推送通道,方便各种开发语言的接入。业务系统只需要简单的调用,就可以实现数据推送,提升研发效率;
  • 4)分布式架构:实现多节点的集群,支持水平扩展应对业务增长带来的挑战;节点宕机不影响服务整体可用性,保证高可靠;
  • 5)多端消息同步:允许用户使用多个浏览器或标签页同时登陆在线,保证消息同步发送;
  • 6)多维度监控与报警:自定义监控指标与现有微服务监控系统打通,出现问题时可及时报警,保证服务的稳定性。

在众多的WebSocket实现中,从性能、扩展性、社区支持等方面考虑,最终选择了Netty。Netty是一个高性能、事件驱动、异步非阻塞的网络通信框架,在许多知名的开源软件中被广泛使用。

PS:如果你对Netty知之甚少,可以详读以下两篇:

WebSocket是有状态的,无法像直接HTTP以集群方式实现负载均衡,长连接建立后即与服务端某个节点保持着会话,因此集群下想要得知会话属于哪个节点有点困难。

解决以上问题一般有两种技术方案:

  • 1)一种是使用类似微服务的注册中心来维护全局的会话映射关系;
  • 2)一种是使用事件广播由各节点自行判断是否持有会话,两种方案对比如下表所示。

综合考虑实现成本与集群规模,选择了轻量级的事件广播方案。

实现广播可以选择基于RocketMQ的消息广播、基于Redis的Publish/Subscribe、基于ZooKeeper的通知等方案,其优缺点对比如下表所示。从吞吐量、实时性、持久化、实现难易等方面考虑,最终选择了RocketMQ。

网关的整体架构如下图所示:

1)客户端与网关任一节点握手建立起长连接,节点将其加入到内存维护的长连接队列。客户端定时向服务端发送心跳消息,如果超过设定的时间仍没有收到心跳,则认为客户端与服务端的长连接已断开,服务端会关闭连接,清理内存中的会话。

2)当业务系统需要向客户端推送数据时,通过网关提供的HTTP接口将数据发向网关。

3)网关在接收到推送请求后,将消息写入RocketMQ。

4)网关作为消费者,以广播模式消费消息,所有节点都会接收到消息。

5)节点接收到消息后判断推送的消息目标是否在自己内存中维护的长连接队列里,如果存在则通过长连接推送数据,否则直接忽略。

网关以多节点方式构成集群,每节点负责一部分长连接,可实现负载均衡,当面对海量连接时,也可以通过增加节点的方式分担压力,实现水平扩展。

同时,当节点出现宕机时,客户端会尝试重新与其他节点握手建立长连接,保证服务整体的可用性。

WebSocket长连接建立起来后,会话维护在各节点的内存中。SessionManager组件负责管理会话,内部使用了哈希表维护了UID与UserSession的关系。

UserSession代表用户维度的会话,一个用户可能会同时建立多个长连接,因此UserSession内部同样使用了一个哈希表维护与ChannelSession的关系。

为了了解集群建立了多少长连接、包含了多少用户,网关提供了基本的监控与报警能力。

网关接入了,将连接数与用户数作为自定义指标暴露,供进行采集,实现了与现有的微服务监控系统打通。

在中方便地查看连接数、用户数、JVM、CPU、内存等指标数据,了解网关当前的服务能力与压力。报警规则也可以在Grafana中配置,当数据异常时触发奇信(内部报警平台)报警。

  • 1)压测选择两台配置为4核16G的虚拟机,分别作为服务器和客户端;
  • 2)压测时选择为网关开放了20个端口,同时建立20个客户端;
  • 3)每个客户端使用一个服务端端口建立起5万连接,可以同时创建百万个连接。

连接数(百万级)与内存使用情况如下图所示:

给百万个长连接同时发送一条消息,采用单线程发送,服务器发送完成的平均耗时在10s左右,如下图所示。

服务器推送耗时: 

一般同一用户同时建立的长连接都在个位数。以10个长连接为例,在并发数600、持续时间120s条件下压测,推送接口的TPS大约在1600+,如下图所示。

长连接10、并发600、持续时间120s的压测数据:

当前的性能指标已满足我们的实际业务场景,可支持未来的业务增长。

为了更生动的说明优化效果,文章最后,我们也以封面图添加滤镜效果为例,介绍一个爱奇艺号使用新WebSocket网关方案的案例。

爱奇艺号自媒体发表视频时,可选择为封面图添加滤镜效果,引导用户提供提供更优质的封面。

当用户选择一个封面图后,会提交异步的后台处理任务。当异步任务处理完成后,通过WebSocket将不同滤镜效果处理后的图片返回给浏览器,业务场景如下图所示。

从研发效率方面考虑,如果在业务系统中集成WebSocket,至少需要1-2天的开发时间。

如果直接使用新的WebSocket网关的推送能力,只需要简单的接口调用就实现了数据推送,开发时间降低到分钟级别,研发效率大大提高。

从运维成本方面考虑,业务系统不再含有与业务逻辑无关的通信细节,代码的可维护性更强,系统架构变得更加简单,运维成本大大降低。

WebSocket是目前实现服务端推送的主流技术,恰当使用能够有效提供系统响应能力,提升用户体验。通过WebSocket长连接网关可以快速为系统增加数据推送能力,有效减少运维成本,提高开发效率。

长连接网关的价值在于:

  • 1)它封装了WebSocket通信细节,与业务系统解耦,使得长连接网关与业务系统可独立优化迭代,避免重复开发,便于开发与维护;
  • 2)网关提供了简单易用的HTTP推送通道,支持多种开发语言接入,便于系统集成和使用;
  • 3)网关采用了分布式架构,可以实现服务的水平扩容、负载均衡与高可用;
  • 4)网关集成了监控与报警,当系统异常时能及时预警,保证服务的健康和稳定。

目前,新的WebSocket长连接实时网关已在爱奇艺号图片滤镜结果通知、MCN电子签章等多个业务场景中得到应用。

未来还有许多方面需要探索,例如消息的重发与ACK、WebSocket二进制数据的支持、多租户的支持等。

[1] 有关WEB端即时通讯开发:

[2] 有关推送技术的文章:

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:。同步发布链接是:

     摘要: 本文引用了作者“大古同学”的“二维码扫码登录是什么原理”一文的主要内容,为了更好的理解和阅读,即时通讯网收录时有修订和改动,感谢原作者的分享。1、引言自从微信的PC端使用扫码登陆认证逻辑后,这种方式似乎在越来越多的IM中看到(虽然我个人认为这种登录方式很酷,但并不方便,尤其手机不大身边的时候)。 ▲

我要回帖

更多关于 为什么手机限速了还是很快 的文章

 

随机推荐