三牛平台
 
首页-首页“325棋盘”
作者:admin    发布于:2022-03-07 14:09   

  首页-首页“325棋盘”首页---主管QQ444708---本文介绍了基于 XMLHttpRequest、Promise、async/await 等三种异步网络请求的写法,其中async/await 写法允许我们以类似于同步的方式编写异步程序,摆脱繁琐的回调函数。

  为了应对越来越多的测试需求,减少重复性的工作,有道智能硬件测试组基于 electron 开发了一系列测试提效工具。

  electron 的编程语言是js,因为大家都不是专业的前端,对js不太熟悉,在编写程序时踩了不少坑。尤其是js中的事件和网络请求,这些涉及到异步编程的地方很容易出错。

  随着工具的快速开发迭代,代码中出现了越来越多的嵌套的回调函数,工具崩溃的几率也越来越大。为了解决这些问题,我们用 async/await 对这些回调函数进行了重构,使得代码量下降,代码的可读性和可理解性都有了大幅度提高。

  本文介绍了基于 XMLHttpRequest、Promise、async/await 等三种异步网络请求的写法,其中 async/await 写法允许我们以类似于同步的方式编写异步程序,摆脱繁琐的回调函数。

  在js中如果只是发起单个网络请求还不算复杂,用fetch、axios或者直接用XMLHttpRequest就能满足要求。

  但若是多个请求按顺序拉取数据那写起来就很麻烦了,因为js中的网络请求都是异步的,想要顺序执行最常见写法就是在回调函数中发起下一个请求,如下面这些代码:

  假设我需要经过两步获取一个数据,如从获取一个数据对象data,通过data.id得到我要获取数据的序号,之后再发一次请求得到想要的数据。

  用回调函数的方式就类似于上面这样,太繁琐了,而且容易出错,并且一旦逻辑复杂就不好改啦。

  接下来梳理一下js的几种网络请求方式,摆脱回调地狱,希望对遇到类似问题的小伙伴有所帮助。

  首先是XMLHttpRequest,初学前端时大名鼎鼎的Ajax主要指的就是它。通过XMLHttpRequest对象创建网络请求的套路如下:

  请求发送后,程序会继续执行不会阻塞,这也是异步调用的好处。当浏览器收到响应时就会进入xhr.onreadystatechange的回调函数中去。在整个请求过程中,xhr.onreadystatechange会触发四次,每次readyState都会自增,从1一直到4,只有到了最后阶段也就是readyState为4时才能得到最终的响应数据。到达第四阶段后还要根据status判断响应的状态码是否正常,通常响应码为200说明请求没有遇到问题。这段代码最终会在控制台上会打出YouDao。

  可以看出,通过XMLHttpRequest处理请求的话,首先要针对每个请求创建一个XMLHttpRequest对象,然后还要对每个对象绑定readystatechange事件的回调函数,若是多个请求串起来,想想就很麻烦。

  Promise是在 ECMAScript 2015 引入的,如果一个事件依赖于另一个事件返回的结果,那么使用回调会使代码变得很复杂。Promise对象提供了检查操作失败或成功的一种模式。如果成功,则会返回另一个Promise。这使得回调的书写更加规范。

  上面这段代码把整个处理过程串起来了,首先创建一个Promise对象,它的构造器接收一个函数,函数的第一个参数是没出错时要执行的函数resolve,第二个参数是出错后要执行的函数reject。

  resolve指执行成功后then里面的回调函数,reject指执行失败后catch里执行的回调函数。最后的finally是不论成功失败都会执行的,可以用来做一些收尾清理工作。

  基于Promise的网络请求可以用axios库或浏览器自带的fetch实现。

  我比较喜欢用fetch,fetch是用来代替XMLHttpRequest的浏览器API,它不需要导库,fetch创建请求的方式和axios类似,在开头已经展示过了就不重复写了。

  虽然Promise把回调函数的编写方式简化了一些,但还是没有摆脱回调地狱,多个请求串起来的话就会像我开头写的那样,在then里面创建新的Promise,最终变成Promise地狱。

  async/await是在 ECMAScript 2017 引入的,可以简化Promise的写法,使得代码中的异步函数调用可以按顺序执行,易于理解。

  改写后的代码是不是就很清楚了,没有那么多的then跟在后面了,这样如果有一连串的网络请求也不用怕了。

  当async放在一个函数的声明前时,这个函数就是一个异步函数,调用该函数会返回一个Promise。

  await用于等待一个Promise对象,它只能在异步函数中使用,await表达式会暂停当前异步函数的执行,等待 Promise 处理完成。

  这样如果想让一连串的异步函数调用顺序执行,只要把被调用的这些函数放到一个用async修饰的函数中,调用前加上await就能让这些函数乖乖地顺序执行了。

  通过本文的梳理,相信你已经知道怎样避免回调地狱了。不过需要注意的是 Promise 是2015年加入语言规范的,而 async/await 是2017年才加入到语言规范的,如果你的项目比较老或者是必须要兼容老版本的浏览器(如IE6),那就需要用别的方式来解决回调地狱了。

  对于 electron 只要你用的是近几年的版本都是支持的,electron 可以当成是 chromium 和 node.js 的结合体,特别适合用来写跨平台的工具类桌面应用程序。

  JS的执行通常在单线程的环境中,遇到比较耗时的代码时,我们首先想到的是将任务分割,让它能够被中断,同时在其他任务到来的时候让出执行权,当其他任务执行后,再从之前中断的部分开始异步执行剩下的计算。所以关键是实现一套异步可中断的方案。那么我们将如何实现一种具备任务分割、异步执行、而且还能让出执行权的解决方案呢。React给出了相应的解决方案。

  React起源于 Facebook 的内部项目,用来架设 Instagram 的网站,并于 2013 年 5 月开源。该框架主要是一个用于构建用户界面的 JavaScript 库,主要用于构建 UI,对于当时双向数据绑定的前端世界来说,可谓是独树一帜。更独特的是,他在页面刷新中引入了局部刷新的机制。优点有很多,总结后react的主要特性如下:

  框架认为 UI 只是把数据通过映射关系变换成另一种形式的数据。同样的输入必会有同样的输出。这恰好就是纯函数。

  实际场景中只需要用一个函数来实现复杂的 UI。重要的是,你需要把 UI 抽象成多个隐藏内部细节,还可以使用多个函数。通过在一个函数中调用另一个函数来实现复杂的用户界面,这就是抽象。

  为了达到可重用的特性,那么每一次组合,都只为他们创造一个新的容器是的。你还需要“其他抽象的容器再次进行组合。”就是将两个或者多个容器。不同的抽象合并为一个。

  React 的核心价值会一直围绕着目标来做更新这件事,将更新和极致的用户体验结合起来,就是 React 团队一直在努力的事情。

  随着应用越来越复杂,React15 架构中,dom diff 的时间超过 16.6ms,就可能会让页面卡顿。那么是哪些因素导致了react变慢,并且需要重构呢。

  React15之前的版本中协调过程是同步的,也叫stack reconciler,又因为js的执行是单线程的,这就导致了在更新比较耗时的任务时,不能及时响应一些高优先级的任务,比如用户在处理耗时任务时输入页面会产生卡顿。页面卡顿的原因大概率由CPU占用过高产生,例如:渲染一个 React 组件时、发出网络请求时、执行函数时,都会占用 CPU,而CPU占用率过高就会产生阻塞的感觉。如何解决这个问题呢?

  在我们在日常的开发中,JS的执行通常在单线程的环境中,遇到比较耗时的代码时,我们首先想到的是将任务分割,让它能够被中断,同时在其他任务到来的时候让出执行权,当其他任务执行后,再从之前中断的部分开始异步执行剩下的计算。所以关键是实现一套异步可中断的方案。

  那么我们将如何实现一种具备任务分割、异步执行、而且还能让出执行权的解决方案呢。React给出了相应的解决方案。

  如何单线程的去执行分割后的任务,尤其是在react15中更新的过程是同步的,我们不能将其任意分割,所以react提供了一套数据结构让他既能够映射真实的dom也能作为分割的单元。这样就引出了我们的Fiber。

  Fiber是React的最小工作单元,在React中,一切皆为组件。HTML页面上,将多个DOM元素整合在一起可以称为一个组件,HTML标签可以是组件(HostComponent),普通的文本节点也可以是组件(HostText)。每一个组件就对应着一个fiber节点,许多fiber节点互相嵌套、关联,就组成了fiber树(为什么要使用链表结构:因为链表结构就是为了空间换时间,对于插入删除操作性能非常好),正如下面表示的Fiber树和DOM的关系一样:

  一个 DOM 节点一定要着一个光纤节点节点,但一个光纤节点却非常有匹配的 DOM 节点节点。fiber作为工作单元的结构如下:

  了解完光纤的结构,那么光纤与光纤之间是如何并创建的链表树链接的呢。这里我们引出双缓冲机制

  在页面中被刷新用来渲染用户界面的树,被称为 current,它用来渲染当前用户界面。每当有更新时,Fiber 会建立一个 workInProgress 树(占用内存),它是由 React 元素中已经更新数据创建的。React 在这个 workInProgress 树上执行工作,并在下次渲染时使用这个更新的树。一旦这个 workInProgress 树被渲染到用户界面上,它就成为 current 树。

  我们知道浏览器有一个api叫做requestIdleCallback,它可以在浏览器空闲的时候执行一些任务,我们用这个api执行react的更新,让高优先级的任务优先响应。对于requsetIdleCallback函数,下面是其原理。

  对于普通的用户交互,上一帧的渲染到下一帧的渲染时间是属于系统空闲时间,Input输入,最快的单字符输入时间平均是33ms(通过持续按同一个键来触发),相当于,上一帧到下一帧中间会存在大于16.4ms的空闲时间,就是说任何离散型交互,最小的系统空闲时间也有16.4ms,也就是说,离散型交互的最短帧长一般是33ms。

  requestIdleCallback回调调用时机是在回调注册完成的上一帧渲染到下一帧渲染之间的空闲时间执行

  didTimeout:布尔型,true 表示该帧里面没有执行回调,超时了。

  options 里面有个重要参数 timeout,如果给定 timeout,那到了时间,不管有没有剩余时间,都会立刻执行回调

  但事实是requestIdleCallback存在着浏览器的兼容性和触发不稳定的问题,所以我们需要用js实现一套时间片运行的机制,在react中这部分叫做scheduler。同时React团队也没有看到任何浏览器厂商在正向的推动requestIdleCallback的覆盖进程,所以React只能采用了偏hack的polyfill方案。

  上面说到requestIdleCallback存在的问题,在react中实现的时间片运行机制叫做scheduler,了解时间片的前提是了解通用场景下页面渲染的整个流程被称为一帧,浏览器渲染的一次完整流程大致为

  帧的渲染与帧的更新呈现是异步的过程,因为屏幕刷新频率是一个固定的刷新频率,通常是60次/秒,就是说,渲染一帧的时间要尽可能的低于16.6毫秒,否则在一些高频次交互动作中是会出现丢帧卡顿的情况,这就是因为渲染帧和刷新频率不同步造成的

  用户通常的交互动作,不要求一帧的渲染时间低于16.6毫秒,但也是需要遵循谷歌的RAIL模型的

  那么Polyfill方案是如何在固定帧数内控制任务执行的呢,究其根本是借助requestAnimationFrame让一批扁平的任务恰好控制在一块一块的33ms这样的时间片内执行。

  以上是我们的异步调度策略,但是仅有异步调度,我们怎么确定应该调度什么任务呢,哪些任务应该被先调度,哪些应该被后调度,这就引出了类似于微任务宏任务的Lane

  有了异步调度,我们还需要细粒度的管理各个任务的优先级,让高优先级的任务优先执行,各个Fiber工作单元还能比较优先级,相同优先级的任务可以一起更新

  有了上面所介绍的这样一套异步可中断分配机制,我们就可以实现batchUpdates批量更新等一系列操作:

  以上除了cpu的瓶颈问题,还有一类问题是和副作用相关的问题,比如获取数据、文件操作等。不同设备性能和网络状况都不一样,react怎样去处理这些副作用,让我们在编码时最佳实践,运行应用时表现一致呢,这就需要react有分离副作用的能力。

  我们都写过获取数据的代码,在获取数据前展示loading,数据获取之后取消loading,假设我们的设备性能和网络状况都很好,数据很快就获取到了,那我们还有必要在一开始的时候展示loading吗?如何才能有更好的用户体验呢?

  我们通常可以用async+await的方式获取数据,但是这会导致调用方法变成异步函数,这就是async的特性,无法分离副作用。

  解耦副作用在函数式编程的实践中非常常见,例如redux-saga,将副作用从saga中分离,自己不处理副作用,只负责发起请求。

  严格意义上讲react是不支持Algebraic Effects的,但是借助fiber执行完更新之后交还执行权给浏览器,让浏览器决定后面怎么调度,Suspense也是这种概念的延伸。

  本文作为react16.5+版本后的核心源码内容,浅析了异步调度分配的机制,了解了其中的原理使我们在系统设计以及模型构建的情况下会有较好的大局观。对于较为复杂的业务场景设计也有一定的辅助作用。这只是react源码系列的第一篇,后续会持续更新,希望可以帮到你。

  几年前,很多人对在线网课还非常陌生。随着移动设备的普及和音视频技术的发展,如今在线教育产品百花齐放。而在线教育产品能服务千万学子离不开流媒体分发技术的支撑。本次LiveVideoStackCon

  2021 音视频技术大会北京站邀请到了网易有道研发工程师周晓天,为我们分享网易有道在线教育业务的流媒体分发相关内容。

  大家好,我来自网易有道精品课研发团队。如今音视频被各界广泛关注,“直播+”成为一个热点,大厂也纷纷推出了一系列音视频的相关服务。

  网易有道是一家以成就学习者“高效学习”为使命的智能学习公司,依托强大的互联网AI等技术手段,围绕学习场景,打造了一系列深受用户喜欢的学习产品和服务。除了面向多种场景的在线教育平台,还有有道词典、有道词典笔等领先市场的软硬件学习工具。

  音视频技术内容广、链条长、每个点又会很深。所以今天分享的内容以有道的在线教育业务为主题,聚焦在有道团队流媒体分发服务端的部分。

  今天的内容分为三个部分,分别是有道在线教育业务介绍、分发系统架构的演进和对分发难点的思考与实践。

  不同班型对应着不同需求。2013年左右最先出现的是1V1课程、普通小班课。本质上是借助RTC实时通信模式构建的教育产品。后来游戏直播和娱乐直播被大家熟悉,而这个阶段被熟知的在线学习的主要形式是视频点播模式,比如网易公开课。随着音视频领域技术成熟,以及用户对在线教育需求的升级,直播网课迅速发展。直播课大约出现在2014年,在疫情后得到了空前的关注。

  传统大班直播课是老师的单向推流,在互动大班课中,学生可以和老师进一步互动,获得更好的上课体验。学生连麦、屏幕/白板、老师视频和互动消息构成一节课的主要内容。

  互动小班进一步优化产品的互动性,提升学员课堂参与感、学习体验与学习效果。音视频+H5互动组件+灵活的布局需求也带来额外复杂性。

  面向业务设计服务,需要理解不同业务的差异再去采取相应的技术。这里提供一种思考的方式:以互动大班课为例,一个老师和一个学生正在连麦,再将连麦的过程分发给其他学生。对于流媒体分发,右侧列出一些考虑的要素:需要什么程度的延迟和流畅性?多大的规模?需要多高的媒体质量?当前业务线对方案成本的敏感度?

  进一步可以用这种方式横向对比不同课程形态,通过它们的区别获得更精细的需求。

  比如,对比大班直播课和互动大班课:对于规模为M的会话,大班直播课要把一个人的信息分发给M-1个人,这可以通过基于CDN的视频直播方式做到。如果进一步想要给产品增增加连麦互动性,成为互动大班课。连麦的增加会让简化模型变为两个部分,如何在一个教室内同时满足这两个需求?最简单的思路是在原有CDN分发的基础上,让连麦内容通过RTC方式交换,再将它们的信息通过原有CDN系统分发,但这么做会带来内容延迟和用户切换延迟等问题。

  对比互动大班和(线上、线下)双师班级,虽然模型类似,但具体到场景中双师班级中的一个“学生端”可能对应一个线下教室的全体学生,这会增加单路分发异常的代价,这样的差异也就要求系统能对不同场景配置不同策略。

  除了在线教育,横向对比的思路同样可以用来分析其他场景的业务线,例如普通小班和游戏开黑。开黑看似和只发送语音的普通小班课程类似,但是在性能和网络占用方面要求更严格。在尽量不占用游戏带宽的同时,还需要尽量减少CPU的操作,为游戏提供充足的算力。如果直接用小班课程的RTC接口用于游戏,保证通话质量的同时反而会影响游戏。如果期望使用一套系统支持多种业务,那么在系统设计早期就要明确业务差异和设计需求。

  通过以上的分析,可以列出了在线教育业务对媒体分发系统的一些主要需求点。第一要满足分发低延迟、上麦低延迟。第二点要做大规模分发。相对一些娱乐场景,要做到高稳定以及高可用。第四点要对成本进行控制。最后,不同学生、不同教室对于上课场景的需求是不同的,所以一定要支持多端接入。

  当多个业务线到小班、到大班直播、再到互动大班以及互动小班等课程,这会影响分发系统的演进过程。一种思路是随着业务的演变,分发架构逐渐复杂,不断支持越来越多的特性。有道并没有采用该思路,而是经历了从基于CDN的分发,到全部业务使用实时通信网络(RTN)的切换,没有架构上的中间过渡状态。

  基于CDN网络的直播内容分发的树状架构十分清晰,架构本身决定数据的路由,同时易于维护、风险和成本可控。当一个用户选定一个边缘接入,媒体数据的分发路由就已经规划好了。同时它有自身的缺点,比如:只支持单向分发、协议带来的固定延迟等。

  早期通过CDN模式部署的直播为了增加互动性和降低延迟,在CDN架构的基础上做了两个优化。一方面在边缘拉流节点支持RTC的方式接入(图中也写为RTN边缘节点),从而屏蔽掉媒体封装协议带来的延迟、增加IM互动效果,同时还能增加弱网抗性。另一方面为了进一步增加互动性,增加了RTC旁路系统以支持双向连麦,再将连麦内容转推到CDN网络中完成直播。一些“低延时CDN直播”产品就采用这样的原理。

  刚刚提到用于连麦的旁路RTC系统需要转推内容到CDN分发网络,那是否能让这个系统把CDN大规模分发的任务也一起做了呢?于是就有了纯RTN的架构。该架构不再有鲜明的树状分发结构,而是用一个网状拓扑分发所有内容。任意单向拉流客户端可以随时切换为双向通信,不需要先做系统的切换。

  通过上述的分析,我们可以大致总结出业内直播流媒体分发演进的方向——音视频直播CDN和RTC网络边界模糊,逐步融为一体。直播CDN厂商逐渐从单向大规模分发支持低延迟接入、连麦。之前的RTC产品,从面向小型会议的架构逐步为了能够同时服务千人、万人,也开始将分发网络变复杂。所以现在我们能看到网易的WE-CAN分布式传输网、阿里云GRTN 流媒体总线、以及其它“X-RTN”都是该演进过程的结果。

  刚刚提到的架构主要是ToB厂商的产品,在ToC服务的场景中也会有如上图所示的架构,通过一个媒体服务器融合两个分发网络提供服务,特别是对于同时有自研和三方接入时。该结构在带来新的非功能特性的同时,也有很大的风险。有道没有选择使用类似的架构进行过度,而是直接用RTN分发网络对原有功能进行替代。

  该架构能满足多种场景的需求,也支持多种推拉流客户端接入。例如当同学上公开课时,通过微信小程序或者浏览器直接看是最为便捷的。已经使用课程APP、已经参加系列课程的用户,使用APP接入以获得最优体验。

  相比CDN架构自身的拓扑结构决定了数据分发路由,RTN网状拓扑在带来灵活性的同时也增加复杂性。比如路由无法从拓扑直接获取,而是需要一个额外的调度中心去计算、规划路由,完成对应转发资源的调度,这也凸显了RTN架构下调度中心的重要性。

  图中也有一个CDN旁路的部分,他的主要作用是做一些突发接入量过大的课程的负载均衡,增加系统的弹性。

  有道在设计网络节点拓扑的时候更偏向于灵活性。一方面,分发节点没有分层、分级,采用扁平拓扑。另一方面,通过配置不同的属性、角色可以实现对网络分发特性的改变。

  对于流媒体分发系统有以下四个要点——接入问题、网络连通性、路由建立以及转发。除此之外还想分享一下关于分层设计和通道的概念。

  解决接入问题的核心理念是“就近”接入——网络质量最好的接入为“最近”的接入。(不同类型的业务可能会有不同思路:有道的教学场景中力求现有每个用户体验尽可能最优,类似于贪心算法;但在别的业务中,思路可能会是在达到QoS最低限制的情况下选择全局成本最优的接入、路由方式)最直观的方法是使用基于IP、位置的接入推荐。进一步利用对不同网关网络探测、连接历史数据优化推荐的结果。除了利用线上、线下数据统计获得的先验的知识进行接入推荐,考虑到这样的方法无法涵盖所有特殊形况,有道还引入人工配置的支持。支持手工热配对部分ToC场景非常有效

  右下角是一个大班课老师上行丢包率打点图,可以看到存在有规律的、平均在9%左右的丢包。该老师长期在固定地点使用固定设备进行直播,而且早期还有技术支持同学进行过网络检查,网络一直很好。按照之前的算法,他的位置没有变、网络没有变,使用的推荐数据库也变化不大,所以根据算法每次会给出相同的推荐结果。突然出现的有规律丢包推测是流量行为被运营商识别、分类,并对其进行了策略限制。

  面对这种情况,修改算法是行不通的。通过有道热配置的方式,在发现问题进行上报的同时就可以人工修改配置,下一次老师接入会避开对应接入节点,解决丢包问题。

  我们通过“过滤器”机制实现该操作:假如所有可接入节点构成一个池子,那么最终“过滤”出的结果构成推荐给客户端进行接入的列表。所以把过滤规则的计算过程作为算法写入系统,将算法执行要使用的参数作为可以热更新的数据写在数据库来实现。

  接入只解决了分发网络的入口问题,那么分发网络究竟是怎样的拓扑形态呢?这就涉及到网络节点的连通性设计问题。有道的网络是一个扁平的拓扑,每个机房都是拓扑中扁平的点。理论上可以给所有节点之间都建立连接,成为一个mesh网络,那么这样的网络将会无比灵活,任意一条通路都可以被规划出来,完全依赖算法进行实际路由的选择。有道并没有采用这样的方式。

  我们还是引入了一些人工经验,比如根据经验将一些机房的连通性删除,成为非Full mesh的结构。可以认为是借助人工的方式进行了剪枝、组织。除了连通性,在路由计算时还需要解决权重的获取问题,也就需要对节点连接情况差异进行量化描述。这种量化是基于规律性的QoS探测完成的,类似前面接入选择的问题,算法可能没法精细地满足所有case或者一些特殊情况,那么在量化差异外,我们也通过可配置的属性描述定性的差异来增加拓扑的灵活性。

  之所以这样提高灵活性、支持人工配置,是为了能满足不同业务的差异化需求。同时也有代价,就是复杂性的提高。所以或许没有最好的架构,只有更合适的架构。

  在确定了接入位置(明确了分发的起点和终点)、建立了分发网络的连通性后,要解决的就是路由规划或者说调度问题。这里可以为大家分享的实践和思考有三点:一条路由的规划、多路径还有成本控制。规划单条路由是完成数据分发的基础,我们根据动态探测、刷新的网络QoS量化质量和基于当前节点状况、节点配置共同完成路由权重的计算。有了无向带权图、有了终点和起点,就可以计规划一条最短分发路由。

  解决了接入问题,又完成分发网络连通性定义,现在解决了媒体数据分发路由的规划,看似就可以完成分发任务了。但对于有道的业务要求这还不够,想进一步保障用户体验就需要提升分发网络对抖动、丢包的抗性。多路径分发是一种保障方式。有道分发网络有三种路径——主要路径、备选路径、实时路径。主要路径直接用于业务分发;备选路径是主要路径的备份,在规划主要路径时生成,当主要路径异常时切换。实时路径是在主要路径之外额外建立的多路冗余分发路径,以提供更加强大的分发抖动、丢包抗性,这对一些重点任务、大规模分发任务有很高价值。

  以图上橙色线路为例。边缘是移动、联通和电信三个单线机房,除了主路径之外,可以在两个边缘的联通运营商之间建立实时路径,在实现实时备份的情况下降低备份线路成本。

  控制中心完成数据分发路径的规划后,就需要沿途节点执行转发任务。这涉及到高性能流媒体分发服务器的设计。上图显示了有道的转发服务器线程模型。协议、端口对应不同的线程,从而在有限端口情况下尽可能利用多核资源。

  除了每个协议-端口对会绑定一个IO线程,还有一个core线程,完成来自不同接入的数据包路由。比如一个推流用户从协议A端口A1接入(如使用UDP,从3000端口推流),同会话另一个拉流用户采用协议B端口B1接入(如使用TCP,从4000端口拉流),这两个用户根据IO线程模型不可能分配到同一个线程,所以需要进行跨线程数据转发。此时core线程会根据会话发布订阅的关系,将接收队列的内容向对应IO线程的队列进行转发。

  该线程模型的设计和业务类型、比例也是相关的。当时系统负载以大班课为主,即推流人数大大小于拉流人数。如果业务类型发生变化,例如班型越来越小、课程每个成员都进行推流,而服务器总用户量如果不变,这会让core线程的转发负载相对大班课大大增加。这也是小班课业务带来的一项挑战,需要架构能随业务变化灵活应对。

  除了上面四个关键问题外,借本次机会想额外分享、探讨两个细节:分层设计和通道的概念。

  分层设计相当于转发问题的延伸。服务器拿到来自一个连接的数据以后,通过core线程分发。逻辑结构上可以理解为三层:链接层解决不同协议连入的问题;路由层负责处理数据在内部的分发、转移;会话层维护了发布订阅关系,指导路由进行分发,将数据发到正确的连接。该分层思想不仅用在单机线程模型中,也用在整个分发网络中。

  当业务方接入一个实时通信SDK时,关于“通道”不同ToB厂商会有不同定义,简单理解就是对实时媒体传输资源的一种抽象。比如一些厂商所服务的业务场景的主要数据是人脸和屏幕共享,对应SDK可能就只提供两个通道资源,其中人脸通道支持大小流的同时推送。

  上图以互动大班课为例介绍有道在“通道”设计方面的思考。左下角图片展示了互动大班的典型教师上课效果:右上角是主讲的老师,正在和左边的学生进行连麦,那么如何进一步把当前界面所有信息传递给其它学生?有道实时通信SDK提供了Live、RTC、Group等多个通道资源。SDK向外暴露的通道资源数量可以定义,同时可以差异化配置,虽然名字不同但是底层资源属于同一类。一个通道对应一路同步的音视频的分发能力。

  仍以刚刚的场景为例:示意图左侧是教师,右侧是学生。橙色是RTC通道,这部分完成老师和学生的连麦。随后教师在端上进行混流——将连麦内容、课程白板等内容混为一路音视频通过Live通道向其它听课的学生发送。比如可以通过获取当前屏幕内容来做端上的混流。在互动大班型的业务场景下,所有学生需要获得信息都在这一张图里,都是视频和音频的媒体信息,这样就可以采取两个通道组合的方式,一个连麦、一个直播,从而完成整个业务。

  不同的通道之所以有不同的名字而不是使用一个通道对象数组,是为了进一步降低客户端接入门槛。比如Live通道概念上相比RTC更强调流畅性,这可以对应一个更大的视频最小缓冲区来提升网络抖动抗性。

  业务中发现SDK提供通道这种资源的方式可能会影响业务方的思考方式:如果只有“人脸通道”和“屏幕通道”,这可能会限制业务产品对新课程形式的思考。

  借本次机会可以和大家分享有道关于互动小班的尝试,在以下两个方面和大家交流:小班的“互动”到底是怎样的?以及互动课程的录制问题。

  在小班课中,多位学生和老师全程可以连麦。不同的同学可以随时被拉到台上进行分享、答题。除了音视频、白板这些基本内容之外,我们还加入了一些互动元素:本地媒体元素播放、多人实时互动棋盘等。这样的互动元素带来什么影响呢?

  前面提到的互动大班课可以在端上混再发送到Live通道,这样流既可以省去需要单独服务端混流带来的视频延迟和同步问题,同时完整地传递了所有课程信息。但是对于互动小班课,如果老师端通过这种截取屏幕将内容分发给其他学生的方式,就会丢失互动元素的可互动性、布局也无法改变。当一个学生回头看录播的时候无法进行参与,只能作为旁观者看到别的同学的互动过程。这也是互动小班课第一个难点——互动元素如何处理?如何进行录制?回放的时候如何保持同步?实际中是有很多坑点和挑战。

  这里的部分内容截取自 ToB 厂商对痛点的分析,自研所遇到的问题可以分为以下几点:

  成本:除了人力、资源覆盖、动态扩缩容的运维等,还有与之对应的机会成本。前两点都比较重要。另外不同业务带宽峰值位置不同,复用一套基础设施和带宽资源可以降低资源、能源的消耗。

  边界:比如是否加入特殊配置解决业务问题,团队内做自研对于业务需求的边界如何把握的问题?

  系统优化门槛:当跑通上文提到的所有内容后,业务可以跑起来。但如果想要进一步压缩成本,就需要对更深技术栈的理解,比如数据驱动的全链路传输优化,编解码的优化,难度和所需的人力可能都会更高。

  对音视频基建的理解:音视频逐步成为一种基建,但如果团队只通过三方SDK的方式接入音视频能力可能无法深刻理解音视频技术的难点、无法正确评估风险、无法把握潜在的机会。

  更多原子能力:自研技术可以根据复杂的业务需要按照业务线进行更灵活的配置,用合理的方式暴露更深的接口,这会让业务层获得更大的灵活性。

  对产品、研发、技术支持提供帮助:音视频技术涉及广泛且复杂,让客户端研发同学、技术支持同学对业务出现的异常准确排错、根据埋点数据分析问题原因是很困难的。依赖音视频自研团队对业务中遇到的问题进行积累、理解更深层的原因、排查未来可能出现的隐患是一种行之有效的方法。通过音视频自研团队可以辅助产品进行设计、加速研发对音视频技术的落地,还能辅助技术支持在业务中确定用户问题原因、提早发现更深的隐患。毕竟再快的工单系统可能也无法比隔壁工位的支持来的更快。

  成本控制、面向业务优化:当能操控的技术越底层,针对特定业务能做的优化空间也就越大,进一步优化体验的同时也有更多成本压缩的空间。

  在 code_pc 项目中,前端需要使用 rrweb 对老师教学内容进行录制,学员可以进行录制回放。为减小录制文件体积,当前的录制策略是先录制一次全量快照,后续录制增量快照,录制阶段实际就是通过 MutationObserver 监听 DOM 元素变化,然后将一个个事件 push 到数组中。

  为了进行持久化存储,可以将录制数据压缩后序列化为 JSON 文件。老师会将 JSON 文件放入课件包中,打成压缩包上传到教务系统中。学员回放时,前端会先下载压缩包,通过 JSZip 解压,取到 JSON 文件后,反序列化再解压后,得到原始的录制数据,再传入 rrwebPlayer 实现录制回放。

  在项目开发阶段,测试录制都不会太长,因此录制文件体积不大(在几百 kb),回放比较流畅。但随着项目进入测试阶段,模拟长时间上课场景的录制之后,发现录制文件变得很大,达到 10-20 M,QA 同学反映打开学员回放页面的时候,页面明显卡顿,卡顿时间在 20s 以上,在这段时间内,页面交互事件没有任何响应。

  页面性能是影响用户体验的主要因素,对于如此长时间的页面卡顿,用户显然是无法接受的。

  经过组内沟通后得知,可能导致页面卡顿的主要有两方面因素:前端解压 zip 包,和录制回放文件加载。同事怀疑主要是 zip 包解压的问题,同时希望我尝试将解压过程放到 worker 线程中进行。那么是否确实如同事所说,前端解压 zip 包导致页面卡顿呢?

  对于页面卡顿问题,首先想到肯定是线程阻塞引起的,这就需要排查哪里出现长任务。

  所谓长任务是指执行耗时在 50ms 以上的任务,大家知道 Chrome 浏览器页面渲染和 V8 引擎用的是一个线程,如果 JS 脚本执行耗时太长,就会阻塞渲染线程,进而导致页面卡顿。

  对于 JS 执行耗时分析,这块大家应该都知道使用 performance 面板。在 performance 面板中,通过看火焰图分析 call stack 和执行耗时。火焰图中每一个方块的宽度代表执行耗时,方块叠加的高度代表调用栈的深度。

  可以看到,replayRRweb 显然是一个长任务,耗时接近 18s ,严重阻塞了主线程。

  而 replayRRweb 耗时过长又是因为内部两个调用引起的,分别是左边浅绿色部分和右边深绿色部分。我们来看下调用栈,看看哪里哪里耗时比较严重:

  熟悉 Vue 源码的同学可能已经看出来了,上面这些耗时比较严重的方法,都是 Vue 内部递归响应式的方法(右边显示这些方法来自 vue.runtime.esm.js)。

  为什么这些方法会长时间占用主线程呢?在 Vue 性能优化中有一条:不要将复杂对象丢到 data 里面,否则会 Vue 会深度遍历对象中的属性添加 getter、setter(即使这些数据不需要用于视图渲染),进而导致性能问题。

  在上面的代码中,创建了一个 rrwebPlayer 实例,并赋值给 rrWebplayer 的响应式数据。在创建实例的时候,还接受了一个 eventsRes 数组,这个数组非常大,包含几万条数据。

  数据没有预先定义在 data 选项中,而是在组件实例 created 之后再动态定义 this.rrwebPlayer (没有事先进行依赖收集,不会递归响应式);

  数据预先定义在 data 选项中,但是后续修改状态的时候,对象经过 Object.freeze 处理(让 Vue 忽略该对象的响应式处理);

  数据定义在组件实例之外,以模块私有变量形式定义(这种方式要注意内存泄漏问题,Vue 不会在组件卸载的时候销毁状态);

  重新加载页面,可以看到这时候页面虽然还卡顿,但是卡顿时间明显缩短到5秒内了。观察火焰图可知,replayRRweb 调用栈下,递归响应式的调用栈已经消失不见了:

  可以看到问题还是出在 replayRRweb 这个函数里面,到底是哪一步呢:

  由于 rrweb 录制回放 需要进行 dom 操作,必须在主线程运行,不能使用 worker 线程(获取不到 dom API)。对于主线程中的长任务,很容易想到的就是通过 时间分片,将长任务分割成一个个小任务,通过事件循环进行任务调度,在主线程空闲且当前帧有空闲时间的时候,执行任务,否则就渲染下一帧。方案确定了,下面就是选择哪个 API 和怎么分割任务的问题。

  这里有同学可能会提出疑问,为什么 unpack 过程不能放到 worker 线程执行,worker

  线程中对数据解压之后返回给主线程加载并回放,这样不就可以实现非阻塞了吗?

  如果仔细想一想,当 worker 线程中进行 unpack,主线程必须等待,直到数据解压完成才能进行回放,这跟直接在主线程中 unpack

  没有本质区别。worker 线程只有在有若干并行任务需要执行的时候,才具有性能优势。

  提到时间分片,很多同学可能都会想到 requestIdleCallback 这个 API。requestIdleCallback 可以在浏览器渲染一帧的空闲时间执行任务,从而不阻塞页面渲染、UI 交互事件等。目的是为了解决当任务需要长时间占用主进程,导致更高优先级任务(如动画或事件任务),无法及时响应,而带来的页面丢帧(卡死)情况。因此,requestIdleCallback 的定位是处理不重要且不紧急的任务。

  中渲染任务结束且还有剩余时间,才会执行。这种情况下,下一帧需要在 requestIdleCallback 执行结束才能继续渲染,所以

  30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。

  这样看来 requestIdleCallback 似乎很完美,能否直接用在实际业务场景中呢?答案是不行。我们查阅 MDN 文档就可以发现,requestIdleCallback 还只是一个实验性 API,浏览器兼容性一般:

  查阅 caniuse 也得到类似的结论,所有 IE 浏览器不支持,safari 默认情况下不启用:

  而且还有一个问题,requestIdleCallback 触发频率不稳定,受很多因素影响。经过实际测试,FPS 只有 20ms 左右,正常情况下渲染一帧时长控制在16.67ms 。

  在项目中,考虑到 api fallback 方案、以及支持取消任务功能(上面的代码比较简单,仅仅只有添加任务功能,无法取消任务),最终选用 React 官方源码实现。

  查阅 rrweb 文档得知,rrWebplayer 实例上提供一个 addEvent 方法,用于动态添加回放数据,可用于实时直播等场景。按照这个思路,我们可以将录制回放数据进行分片,分多次调用 addEvent 添加。

  按照上面的方案,我们重新加载学员回放页面看看,现在已经基本察觉不到卡顿了。我们找一个 20M 大文件加载,观察下火焰图可知,录制文件加载任务已经被分割为一条条很细的小任务,每个任务执行的时间在 10-20ms 左右,已经不会明显阻塞主线程了:

  优化后,页面仍有卡顿,这是因为我们拆分任务的粒度是 100 条,这种情况下加载录制回放仍有压力,我们观察 fps 只有十几,会有卡顿感。我们继续将粒度调整到 10 条,这时候页面加载明显流畅了,基本上 fps 能达到 50 以上,但录制回放加载的总时间略微变长了。使用时间分片方式可以避免页面卡死,但是录制回放的加载平均还需要几秒钟时间,部分大文件可能需要十秒左右,我们在这种耗时任务处理的时候加一个 loading 效果,以防用户在录制文件加载完成之前就开始播放。

  有同学可能会问,既然都加 loading 了,为什么还要时间分片呢?假如不进行时间分片,由于 JS 脚本一直占用主线程,阻塞 UI 线程,这个 loading 动画是不会展示的,只有通过时间分片的方式,把主线程让出来,才能让一些优先级更高的任务(例如 UI 渲染、页面交互事件)执行,这样 loading 动画就有机会展示了。

  使用时间分片并不是没有缺点,正如上面提到的,录制回放加载的总时间略微变长了。但是好在 10-20M 录制文件只出现在测试场景中,老师实际上课录制的文件都在 10M 以下,经过测试录制回放可以在 2s 左右就加载完毕,学员不会等待很久。

  假如后续录制文件很大,需要怎么优化呢?之前提到的 unpack 过程,我们没有放到 worker 线程执行,这是因为考虑到放在 worker 线程,主线程还得等待 worker 线程执行完毕,跟放在主线程执行没有区别。但是受到时间分片启发,我们可以将 unpack 的任务也进行分片处理,然后根据 navigator.hardwareConcurrency 这个 API,开启多线程(线程数等于用户 CPU 逻辑内核数),以并行的方式执行 unpack ,由于利用多核 CPU 性能,应该能够显著提升录制文件加载速率。

  这篇文章中,我们通过 performance 面板的火焰图分析了调用栈和执行耗时,进而排查出两个引起性能问题的因素:Vue 复杂对象递归响应式,和录制回放文件加载。

  对于 Vue 复杂对象递归响应式引起的耗时问题,本文提出的解决方案是,将该对象转为非响应式数据。对于录制回放文件加载引起的耗时问题,本文提出的方案是使用时间分片。

  由于 requestIdleCallback API 的兼容性及触发频率不稳定问题,本文参考了 React 17 源码分析了如何实现 requestIdleCallback 调度,并最终采用 React 源码实现了时间分片。经过实际测试,优化前页面卡顿 20s 左右,优化后已经察觉不到卡顿,fps 能达到 50 以上。但是使用时间分片之后,录制文件加载时间略微变长了。后续的优化方向是将 unpack 过程进行分片,开启多线程,以并行方式执行 unpack,充分利用多核 CPU 性能。

  思否技术先锋年度榜单正式发布。网易有道技术团队同时登榜思否年度技术团队榜单和中国技术品牌影响力企业。

  2022年1月13日,SegmentFault 思否作为中国领先的新一代开发者社区,根据社区用户行为大数据(如文章 & 问答发布数量、获得声望 & 点赞量等)综合分析,评选出了 30 个最杰出的年度技术团队。

  本次最终评选出 30 支年度技术团队,有道技术团队入选,登上思否2021中国技术先锋年度榜单,荣获思否年度技术团队称号。

  本文为网易有道企业发展高级效能项目经理张浩然《研发效能实践助力互联网行业项目管理“行之有效”》的演讲内容,围绕研发效能的实践和项目管理两个主题展开。

  我写分享PPT的时候,起初想的是针对于互联网行业的项目管理。但现在不止是互联网,传统行业也在做数字化转型。所以,这个项目管理是全行业都可以一起探讨的。我之前做研发,后面主要做项目管理,过程中做过一段时间的产品管理。目前主要在网易有道企业发展部,做整个研发效能的推广和项目管理的提升。

Copyright © 2019 [三牛娱乐注册]网站版权所有 TXT地图 HTML地图 XML地图