基于 Go 的协同系统深度实践

mob604756f0bbf4 · · 737 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

导读

本文介绍的是基于网页端的操作协同,常见于教学、游戏等场景。由 TutorABC 架构师张新宇在Gopher Meetup 上海站上进行的演讲整理完成。

内容结构:

协同概念及应用

Go 服务端设计

主要技术点及解决方案

(编辑整理:李平平)

01

协同概念及应用

各位下午好,我是TutorABC音视频的架构师,主要是跟大家分享一下我们在TutorABC产品实践当中的心得体会。开始之前我跟大家交流一下什么是协同?这个是百度上面的说明,和传统意义的协同不一样,我今天演讲的是一种多人交互式的,在同一个黑板上或者是在一个房间里面交互式的协同。
简单描述就是这样的,比如说大家在一个IDE里面编写代码,你的编写的内容会同步传输到另外一个人的网页端,并且你可以使用编译结果同时广播到两个地方,大家都可以看到你的编译的结果,那这个是最基本的协同方式,原理上也很简单,传递的只是在你的浏览器中改动,这个是最基本的应用。

基于 Go 的协同系统深度实践

稍微高级一点的就是我们需要捕捉不但是传字符还需要捕捉你的鼠标在dom上面的事件,点击,拖动,双击会有捕捉,把这些事件转化成文本的内容传递给对方,在dom上面进行汇总。

基于 Go 的协同系统深度实践

这个是我们ABC的产品,在一个桌面上面进行基本操作,比如:绘画、文字、放大缩小等,这些东西会在远端比如说学生的pad上面在另外手机上面会有同步的显示,这个是最基本的协同。当然,我们还有编程模式,这个是高级的协同,大概是这样的。我们看到白板上显示的并不是像刚刚看到的是一个图片,可以在上面画,这个上面实际上是一个远程桌面,远程桌面的引入就可以让老师可以去教一些更高级的东西,比如说PS,CAD,还有C++,Golang等等带GUI特性的东西,可以引入到网页端。

02

go服务端设计

基于 Go 的协同系统深度实践

我现在分享一下我们在上面的一些实现,讲一下我们的技术栈,是通过websocket,服务器的后端都是通过Tcp,传输协议有protobuf和fjs,还有rfb、rdp是远端桌面协议,这个是我们处理的协议,开发语言是GO,主要是GO然后一些关键的地方是用的C++,前端是用的js,依赖的库有websocket,Freerdp,libvnc还有cairo的库。
这个图我刚刚说的最简单的不带远程桌面的实现,实际上最核心的技术是劫持操作者在网页上面所有的动作(DOM变化),我们会注入代码把你的动作捕获到,然后把它编译成我们的代码,然后传输通过websocket传输到网关上面,网关就是负责将所有的传入数据收集到HUB上面进行转发,这样代码可以在远端进行重汇,再基于这个之上是基于远程桌面的协同,我们的远程桌面是通过HUB建立了连接器,这个connector会做虚拟机的连接或者是vm容器的连接,并且把对方发过来的这种RFB,RDP的协议进行转码,在connector的内存里面保留一个内存进行图形的渲染,并且根据每个客户端的请求,比如说他是到了哪一祯了进行请求差异的传输,gateway传输到不同的浏览器上面进行绘画。

基于 Go 的协同系统深度实践


架构图

基于 Go 的协同系统深度实践

总体的层次架构图就是这样,我们有一个ResourceManage服务,这个是管理所有的虚拟机,我们桌面也是装在容器里面,启动的时候会对资源进行注册,如果挂掉之后会进行反注册,然后manage会通知所有服务,这个远程桌面不能用了,调回其他的桌面用。
所有的请求会集中到HUB上面,HUB进行协议的转发和流的控制,在connector渲染出来的通道会有一个旁路的录像方便客户回看.还有一个是服务于大讲堂的推流模块,比如说上千人,上万人大会堂的模式会做RTMP的回流,在讲堂里面把你操作的东西分享出去,这样就形成了远近场,我们看到Gateway连的是近场的所有的老师操作是实时的,远场看的是视频。
主要是前端会有一个协议的解析,就是在于remote frame buffer的解析,我们可以看到刚刚的地方在connector将远端的数据进行解码传到gateway上面是要适应这个协议的,为什么前端JS需要解析这个东西呢?正常来说一个图片非常大,尽可能传递越小的数据是越好的,第一种是原始的数据,就是没有办法,比如说在远端桌面放视频,是没有办法进行优化,只是按照视频是什么进行传递,这个是Raw的模式性能较差。copyrect是整块数据的传递,在远端的RTP等等,检测到你的网页是滚动,IDE我们打开一个vscode也好,或者是sublime进行代码的滚动或者是看网页会有一个检测,如果是滚动的话,会把一大块数据做标记,告诉你标记变动了就不需要再传这种重复的数据过来,然后RRE就是相同RGB数据的合并,就比如说我们要向远端传递PPT这张图什么地方可以压缩呢?很明显是黑的地方,黑的地方用不用传?实际上不用的,我标记一整个数据,到标号那个地方就 回放这个区域全部是标号填充同样的RGB就不需要传那么多了
Hextile是RRE的升级版就是有一大块数据不好标会划成一小块一小块的16×16的大小,就不是纯色的RGB,就是文字里面有相同的地方,16×16这俩相同这个数据也可以进行压缩,最后一个ZRLE的方式,就是在前面的这种tell上面进行zlib的压缩,就是减少你的传递,提高整个的可用性和弱网情况下面的性能。

基于 Go 的协同系统深度实践

03

主要技术点及解决方案

1:流量

基于 Go 的协同系统深度实践

在GO技术上有一些分享,第一点分享我们面临的挑战是数据量大,我们一天就几万堂课,包括一些大会堂的广播这个数据量会非常大。如果我们有一些操作是比较重的话,比如说我们有一些画曲线,画很复杂的鼠标动的情况下面,原来是用json封装,大家都知道websocket一般是json做数据传递,这个是比较通行的办法。我们当初也是,我举个例子画线的会有6K的数据进行传递。如果是多个房间多个老师同时进行这样操作,很明显流量就往上飙了,这时你的血压可能也要往上飙了。
解决办法:我们原来用的io是基于socket之上的,后来换成websocket,我们把json换成binary,大家知道socketio不是传二进制设计的,而且base64远远不如二进制的更优秀。前端的js功能,已经可以解码二进制的位操作了,也可以custom你私有协议的解码和编码,当然我们出于扩展的考虑,更多希望用成熟的框架,比如说pb、thrift这种成熟的框架,这个在前端的js使用上面也是毫无压力很完美。

基于 Go 的协同系统深度实践

大家看一下,这个是前端的Js进行pb封包,比如说你定义了一个model一个command,定义的message,所有的包都得实现,不必一一实现,不需要你做拆解封装,最后serializebinary把你的整个packet封装成了二进制,把这些发送出去就可以了,解封包也是,从websocketdata传进来,然后把serializebinary把解封包传进来就行,然后进行这个地方都是pb已经做好编译的。

基于 Go 的协同系统深度实践

效果怎么样呢?是很明显的,刚刚600k的,我们直接压缩到了388个字节,IO提高了近20倍,服务器也提高了,为什么服务器也提高了?大家知道GO的json的序列化反序列化是有一些消耗的,你单个的json感觉不到,可能压力上来了上万个并发上十万个并发就感觉到json序列化反序列化消耗资源比较大,开源的库也是解决了这个痛点,我们用了pb以后整个序列化和反序列化的性能是好的,大家看图表上面第一个size是直接压缩的,ptime处理时间也进行了压缩,吞吐量我们用pb提高了近2倍的性能。用了pbjs以后唯一不好了就是调试上面比较坑,你必须通过你在前端埋点进行输出,不像原来直接打开开发者工具直接可以看到你传递的json和返回的json,必须通过埋点进行调试。
2:性能

基于 Go 的协同系统深度实践

我下一个分享的点是关于内存使用的,也是一个性能上面的优化,大家知道在我们写大量的网络程序的时候是有很多的内存是被New出来的,比如说Golang里面有很多的,大量的被New出来以后被回收带来一些GC的负担,你自己写一个缓冲区做读写是没有问题的,但是你要考虑到跨P,大家知道Golang的GMP的模型跨P的内存的共享,如果你要跨P有大量的锁,比如说这个P在读取你内存的时候锁起来,然后写入的时候锁起来,等写完了以后放开让其他的Goroutine进行访问。
sync.Pool的引入,有几个好处,第一个大小无限的,因为GC的时候会被清除掉。不需要担心会产生内存泄露或者是其他的问题,然后这个是自己跨P的内存的共享,如果说是你用了这个以后,提高了很多的对象的复用率。在1.13以前是使用的有限制的,第一个是高并发的情况下是最合适使用为什么这么说呢?就是说你不是高并发实际上是有损耗的,比我们直接New一个对象是有一些损耗的。第二个是有一个限制,GC的时间要长,为什么GC的时间要长呢?如果是你的大量的内存分配的时候,GC了以后会整个的sync.pool会被清理的,大量以前缓存的都会清理掉,这个时候如果说你是大量依赖于缓存你会产生一个缓存雪崩。和我们使用的自己写的redis不一样的,1.13以后有两个性能的提高,第一个Partially release不是一下子清理掉,它是分带的,这次GC不清理,下下次的GC再清理这样的话缓存的东西就不会一下子全部丢掉,最多丢掉一半,如果这个可以接受的话,我们觉得是可以有一个很深入的使用。第二个是自己实现了一个内核级别的lock-free的缓冲,这个是无锁的跨P的访问,这个东西就是在你跨P的时候会带来很高性能的提升,比你自己写一个要好用了多了,自己写的话带来的复杂度可能还写的不够那么专业。

基于 Go 的协同系统深度实践

这个是我们1.13以后使用的Sync.Pool的例子,比如说我们有一个地方需要创建一个连接缓冲池,实际情况下大家不要这么用,http包里面自带的里面是有连接缓冲池的,上面的ConnPoolnew有方法。如果说Pool里面没有东西会调New,把连接创建出来,如果有的话就直接返回连接给调用方。底下的Test就是去连接池里面取coon,去调用请求,把得到的值打印出来把连接归还掉,比你每次都去New一个返回一个连接Dial好的多,这个是复用的,那什么时候回收呢?GC的时候会被回收的,所以也不用太考虑到你的连接是不是泄露掉或者是什么样的,这个只是举例的应用,实际大家不要这么用,自带的Golang的HPPT库里面是有的。

基于 Go 的协同系统深度实践

还有我们现在的协议是有很多种,第一种最简单的是分成一个包头,有一些Flag这些东西,中间是包体的体长,后面的payload是多长,还有整个包的长度放在最前面,中间是包头,最后是payload这样的结构。原来我们写分配的时候很简单就是从这个读出来,先把包头的长度4个字节读出来,根据后面的字节把后面的用一个payload长度的数组出来从conn把数组读出来,然后再写结构体,这个是原来的一种通用的做法,但是可以看到payload数组完了以后下次又会回收掉,当你的数据量并发量大的时候,这些数据会累计越来越多最后会触发GC。
解决办法:我们改进的方式就是引入了一个sync.pool的组,什么意思呢?我把你的内存的池画成了不同大小的slod,8K一个,16K一个,64K一个,到前面的分配字节的时候根据我要的字节的大小落到什么区间内就分配合理的sync.pool,使用完再归还给他,下次被其他的Goroutine复用,这样就有效减少了GC的时间,提高了整个程序的性能。

基于 Go 的协同系统深度实践

首先操作来说不如直接去mag的,因为有一些逻辑,分配大小落到哪个区间,再到哪个slod里面分配哪些内存,上面是23.7ns,我带来的是GC的好处,就是说我们可以看到上面的做GC分配的时候,正常一次分配了413次GC,下面的我们做内存池的操作只是用了6次GC,同样的处理压力,所以带来的好处很明显,减少了GC的次数。
3:最后一公里
最后一点就是说最后一公里的问题,我们现在是在用的websocket,在信息传递文字传递方面是很不错的,但是我们基于了远程桌面以后,基于大量的动画视频一样的情况下面,TCP在弱网的情况下跟不上,所以我们实验阶段已经是在预发布的环节,我们在测试下面也引用了一些其他的方式,从网页到服务器端不仅是websocket可以做长连接,我们可以用UDP,大家知道websocket是基于TCP的,优势就是很省心不需要做关心底层做重连丢包或者是包序的管理,UDP就是很free的怎么传都行,特别是在于音视频上面是用的非常常见的东西,当然我们远程桌面的东西丢包说穿了也无所谓,丢包了大不了下一祯再补回来而已。
我们实验的就是DataChannel,我们的网页端除了WebRTC进行传递我们还用DataChannel传递,下面是连接,大家有兴趣可以看一下,优势就是在于不但是传递了数据还有反馈,比如说他有一个SCTP的就是流控协议,你可以根据这个协议看到你的这条连接的丢包情况,RTT还有传输的阅点,还有质量。第二个是接口一致性和websocket一致的,编码上面可以很平滑的做切换,第三个是包序可控,DataChannel是可以控制的,不控制包序传输效率会有提升。第四个是支持P2P不需要服务器就可以直接进行DataChannel的数据的传递。
当然也不是全部是优势,也有劣势,第一个是需要其他通道做sdp就是绘画描述协议的穿透需要websocket传递最初始的绘画让它俩之间可以握手,所以一进来是有延迟的。第二个就是需要新的服务器端的技术引入,作为golang有开源的比如说grpc或者是pion有websocket的网关的处理,结构更复杂,就是有更多的控制技巧,就是在要提升性能的前提下面肯定需要更多的代码的投入。
大家可以大概看一看,实际上跟websocket是很类似的,从js端做peerconnection,可以控制包有序还是无序的,我可以控制这个包的最大生命周期,如果超过这个300我认为这个包是无效的,那我用后续的祯来补前面的空缺。实际上跟websocket的是类似的,onmessage,onopen,onclose是类似的,就是在弱网上面给websocket有更加优势的提升,比如说传递一些远程桌面操作,或者是操作比较频繁的时候,还有大量的数据的一些允许丢包的数据。

基于 Go 的协同系统深度实践

Q & A

提问:我有几个问题想了解一下,第一个我们做协同操作一定是有锁的,避免我对一个资源的竞争,那么我想了解一下在你目前的系统里面能达到的上限的并发量是多少,比如说我最多支持多少人一起来修改这一整个coding?
回答:coding是分成几种,第一种是简单的那种coding比如说你写一行我一些行,如果两个同时写一行是不允许的,会有独占的锁定,世界上没有一种协同是可以让你同时写一行数据的。

提问:我的意思目前最多支持的是同时多少个人来一起上课?
回答:现在是1对6,再多就会乱了,整个房间全部是鼠标在晃。

提问:第二个问题之前我有看你们整个的架构,最终是在VM上面,那么你们有没有考虑单点失败的情况?在你们的架构当中很多的节点有状态的,特别是长连接等等,比如说中间某个一节点挂掉对用户的体验是很有影响的,最终的VM,假如说上课上到一半这个VM挂了有没有办法重新的恢复,拉起恢复到同一个状态中?
回答:所以我说我们VM大部分用了doker,无论是物理机挂掉了直接拉过去没有任何的问题。

提问:比如说我现在在上面已经写了很多的代码,但是这个时候我挂的话可能这些数据是保存在当前的container里面的,如果重新启一个doker这些数据会丢失的。
回答:基于运维的管理每5秒进行保存的。

提问:最后一个问题,我看你们有自己管理内存,其实有点像GO的MPG的管理方法,有没有考虑对外的内存,整个内存自己申请自己释放?
回答:实际上对外内存并不是在GO里面实现,是在C++里面实现,我们对外的内存都是在C++实现。

提问:GO没有考虑堆外内存进行优化
回答:对。


有疑问加站长微信联系(非本文作者)

本文来自:51CTO博客

感谢作者:mob604756f0bbf4

查看原文:基于 Go 的协同系统深度实践

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

737 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传