Go 分布式实时服务架构

张新宇 / 目前担任TM+系统架构师,主要负责TM+服务架构设计及开发。

15+年的软件开发经验,8年互联网架构经验,曾任职于盛大游戏、沪江网、途牛旅游等企业,参与架构设计、开发了多个公司级核心项目。

前言

很荣幸展示一下我们的架构,包括分享一下我在GO方面的一些实践,希望能给大家带来收获。我今天将通过这四个部分来给大家介绍Go分布式实时服务架构:

一、实时系统介绍

二、服务架构设计

三、经验总结及收获

四、 未来展望

实时系统介绍

TutorMeet+介绍

首先是实时系统的介绍,TutorABC是在线音视频实时的系统,就是直接通过网络可以跟外籍的老师进行面对面的沟通的系统,可以和说当地正宗语言的外籍人士进行交流,在这过程中我们的英语包括其他技能能得到有效提高,跟着专业的外籍人士学英语,能解决很多我们跟着中国人学英语而遇到的因文化背景差异导致英语教学上出现的尴尬。下面是我们TutorMeet的一些数据:服务全球135个国家,包括100多个城市的外籍顾问和学习者;最关键的我们是7×24小时,365天全年无休提供服务;每年提供超过千万级别,现在可能是更大了,现在我们每天并发数有几万的在线课堂,这个课堂并不是一对一的,最大的达到1对6,1对8都有,我们的云讲堂是1对几万,一个讲堂里面几万个学生,一个老师这样的互动。

实时系统特点

高性能

低延迟

前面已经讲到了低延迟,我们的音视频已经做到了低延迟,所以指令也要与之同步,假如指令响应过高或者是反馈过高就会出现前面讲的现象,多人音视频的实时同步,比如说1对6,1对8,大家同时在一个房间一起在说话,这时实时同步的音频,视频结构要满足我们的需求,不然就会出现你一言我一语,完全不知道讲什么,延迟带来的问题就会很严重。

稳定性

目前,我们是多个异地机房灾备,服务稳定性基本达到了4倍多,由于快速的故障切换,比如某个机房断了或者是哪个光缆断了,我们会在几分钟之内做快速两个机房的切换,这个是满足了稳定性的需求。

监控性

我们的音视频是采取直接在网页上,让用户无安装无成本的直接打开网页就接连上的,所以设备会给我们造成很多的困扰,例如用户的摄像头、麦克风、操作系统以及现在有一些用户还在用着XP,这时候就需要我们的客服人员或者是技术人员实时的能够隐身进入这堂课里面,实时对用户线下的捕获,做用户数据的采集,能够针对用户设备提供智能的检测这样系统的需求。

TM+业务发展历程

TM(旧时代)解决方案(< 2016)

Tutormeet的发展不是一次性成功的,它已经运行了将近10年时间,都是硅谷团队基于Flash的技术站,目前Flash已经被各大厂商抛弃了,以后的设备会越来越少用flash,但是实现的是挺好的,技术本身的特点也带来了很多的局限性,2016年以前Tutormeet基本上是flash的方案。

TM+1.0解决方案(2016-2017)  

Tutormeet的技术原型阶段,在2016-2017年基本上采用了Webrtc做的,因为那时我们对整个音视频的支持包括中国复杂网络的适配是有一个原型测试的阶段,这个时段也是跟flash并行跑的。原型阶段由各个团队提供的技术是非常复杂的,有nodjs前端的,他们做前端的打包,包括浇水层等,还有python是语言数据的传输,还有Vuejs,有C++,还有有部分GO的。主要的问题是技术栈过多,导致了成本过高,人员维护的成本也很高,因为每个技术栈都有自己的库包括代码结构,然后连接复杂,这个版本是前端通过web连接指令,连接媒体,因为webrtc要传输一些FDD打隧道的指令,还有前端的数据打点、监控等很多的连接,一个前端可能连接了N多的。

当一个系统的输入量多了以后带来的问题会非常多就导致整个系统混乱的根源,它的音视频的引擎,我们基于开源的是Kurento,这个已经被一个公司收购了,我们通过自身研究的代码有一些性能问题,比如基于Kms,是会带来很重的内存不释放的问题,这个是我们2016年到2017年的原型阶段,对我们的最大的好处是把整个的流程全部跑通了,包括整个的运维,包括整个的思路已经确定好了,最后,由于在原型阶段我们经历过多次失败,最终确定了以GO为技术上核心的主要思路,GO+C++这样的主要思路,提供了2.0的方案,目前今年的2.0的方案。

TM+2.0解决方案(2017 - 2018)

这个阶段是产品阶段,目前已经并发团队。以GO为核心,前端的reactjs,C++的核心技术栈。完全是自己控制,并且合并连接,优化了传输,比如说我们国外的媒体网关会把国外的音视频流通过SFU数据流控过来到我们的主机房,然后视频流的处理,我们提供了自建的MCU,提供了一些全功能媒体处理能力,包括音视频混流还有转码,我们还提供了大会堂的承载能力,然后提供了全链路监控,可以实时进入教室处理。这个是目前为止2.0的优化的成果,基本上已经完全替代了以前的flash和1.0的版本,最基本的是GO。GO带来高性能的同时,并且带来高的开发效率。

TutorMeet+界面的介绍

这个是我们的整个上课界面,大家可以看一下,实际上结构还是蛮复杂的,主要是除了音视频的处理,有消息聊天,还有实时的白板互动,稍有一些延迟或者是丢包就会很明显,学生的界面还有后台监控的数据,提供了在线实时的数据,并且提供报警的功能,TutorMeet+还有自建的监控的全链路的平台。

TM+相关的开源项目

目前我们用到的相关的技术有这些,主要是以容器为核心,几乎已经达到99%的服务都在容器中跑了,然后注册与发现都是通过consul进行的,整个服务的开发源是GO,音视频的C++,我们在实时跑的数据里面拿Redis当数据库用,mysql是源数据保存。我们的旁路有日志服务,包括kafka,kibana等等,包括监控有grafanalabs等等,还有一些网关,比如说一些caddy做了二次的开发。

目前有95%主要的开发语言是GOlang和C++,还有一些其他的相关的系统是其他语言的实现,我们整体的音视频是抗丢包率是25%在很差的情况下25%感觉不到音视频有延迟,全端覆盖,PC端安卓、IOS,包括浏览器。100毫秒的端对端延迟,包括支持全高清模式的传输。

服务架构设计

TM+ 2.0系统布局

接下来是介绍服务架构的设计,大家可以看到我们整个的界面上有很多的功能,每一个功能对应的后台都是一些服务,前端方面,当你访问我们的Url的地址他会根据你的历史信息包括目前提供的可用的情况会分配你,我们后台分布两层,一个是指令层,另外一层是媒体层,有一层是走UTP的因为追求高性能,这边是走的TCP的包,gateway是保持连接,无论是手机4G网络等等,然后把连接从国外引入到我们的roomservice,然后再进行分发传递给gateway返回到前端,当然今天我重点会介绍我们前面的指令层。

包括MCU,包括分析服务,推流包括录制服务,包括媒体控制等等这些在音视频当中会提供。我主要介绍的是我们指令层,包括了白板、一些技术服务、状态等,然后我们的跨平台的支持有服务发现,我们提供的服务发现包括了数据存储,包括第三方的,第三方指的是还有会员和还有排课等很多其他的系统接口,包括对微信的一些接口都是在技术上提供的。然后我们还提供了监控服务,比如说各个服务的健康状况,一个指令的延迟状况,包括我们的学生端接收到SRUTPRPT的状态参数,包括丢包,延迟,包括采集音视频数据的实时的状态会反馈,这个就是我们大致的系统的功能划分。

Zone说明   

具体到某一块是这样的,我们的用户通过先访问路由,会分配他可用的网关,比如说国外老师就会访问国外的网关,例如你来自于欧洲我们反馈你德国或者是法国的网关,然后你来自中国比如说是广州,我们会广州机房的网关分配给你,保持连接,并且把客户端的指令实时发送到主机房的roomserver上面来,这里有一个问题,当你课还没有开的时候,老师和学生同时登陆进来的时候会有一个冲突,因为没有开的时候我不知道你的roomserver在哪?开了以后第一个人进来我可以分配,如果是两个人同时进来有可能会分错,所以说这个地方采用了一个一致性的算法,就是当两个人同时进来,我们的服务缓存里面并没有,我们会两边投票出一个roomserver把指令丢给他,所有的服务我们的都是无状态的,这台机器挂掉也不影响,你到其他的机器上面可以接着上,因为状态都是保证在集群里面,只要发现我们的数据这个房间里面没有数据,我们都会到集群里面把集群恢复回来,由roomserver再进行就近的服务把指令丢给他们,因为是实时系统,所以说主流层都是基于PCT的长连接。

我们在心跳检测断的时候都会两边再进行投票,投票出来公共决策的,所以说这就保证了高可用。roomserver当前端有webrtc的请求协议或者是打洞的信息传递过来,我们的roomserver会通过媒体控制单元去分配它的gateway和防火墙的信息,生成sttp的返回给前端,然后就通过gateway广播的数据传递,这个模型在原来是很困难的,并没有提供SFU的功能。然后我们所有的这种服务器的健康数据都会采集到一个收集器里面,同样会影响到我们的,如果哪一块服务挂掉了我们会有一个闭环控制到前端进行再分配。

这部分就是我说的分配算法,包括了一致性hash,大家可以看到前面这么大一块,只是一个zone就是可工作区,就像一个工作盒子一样,整个的服务都合成在一块了,每一个服务都是做容器化的管理,那这个zone有什么用处呢?也是我们通过很久摸索出来的一个经验,这个就是标准的打包方式,我们通过服务推到线上。

Zone优势

Zone的优势有这几点:1、进行服务管理,可以实时的调度上架下架;2、可以进行负载分流,如果销售团队做了一次推广活动一下引来了几十万的流量,大家都来开体验课,我们会实时开工作区进行负载分流;3、故障转移,假如我们哪一个Zone出现故障了,比如说是有程序或者是由线路导致的故障,我们会动态的调到其他的可用区里面;4、动态扩展,例如老师数量增加了,我们会进行动态扩展增加资源的部署;5、蓝绿发布,我们的各个版本发布上去以后可以在线做一个实时的调整,如果发现2.0版本出现的问题的话,我可以实时回转到1.0版本,首先是把流导走,导到其他的稳定的区去。

TM+ 2.0系统布局

我们整个的架构是这样的,我们分成各个工作中心,这个工作中心可以理解成可用机房,但是一个异地的机房,机房里面分出N多的可用区,红线的地方是新版本发布了,但是现在是0%的状态,所有的压力都承载在这些黑的可用区里面,然后可以分1%的流量然后我们再跑,如果发现这个上面跑了以后没问题,我们再分5%,10%,50%,100%,如果说是发现有问题以后,我们实时把这个流量通过前面的控制指令导走,导到其他的稳定区里面,这样整个容器就下载下来了供我们使用。

整个的工作中心,就是可以理解成是各地的机房,我们机房之间也是做了异地的,如果整个的机房挂掉了,可以迁到阿里机房或者是腾讯机房这样不同的方式,除了这种切分的方式以外,还有一些统一的共用的数据,因为数据库要存储一些开课信息,用户信息,学生信息,我们的路由包括控制器,就是我们的服务发现的一些控制,一些是日志和性能收集的MQ,还有一些监控的东西会放统一的地方,但是我们会用异地的载备,这样整体上保证了我们所有的服务可用区里面,无论挂掉哪个,包括数据中心都会有一个高可用的方案存在,就不会存在在单点和并发压力。有时会想,我们的架构在设计的时候有存在着很多的权衡,实际上都可以,总之是实时做各种各样的权衡。

RPC? HTTP vs TCP

我们在做一些服务时,会考虑到底是用HTTP还是TCP?基于 HTTP 的 RPC 我们做了一些尝试,实际上达不到实时的需求,为什么呢?首先是是第七层的协议,有额外的头部信息包括了一些传输的控制,还有很多大家了解到 http 是基于 ask 码的信息,当然这样会带来更多的工作和问题。当然,最好是有很成熟的负载均衡的方案,它的 rest 比较清楚,服务提供的比较清楚,它的Keep alive的模式也可以进行数据长流的推送,甚至你可以通过代码实现了双向的同步,但是有一个最大的问题是一个请求和响应是同步的,你一个请求过去响应回来才能发下一个请求再响应。

RPC via http2

这个就是我们当初做Http做RPC的限制,导致了我们只是做一些旁路的服务,所谓旁路的服务就不会是主流的服务,我们最终选择的是通过tcp原始的处理,然后我们的封包的指令是选择的protobuf,轻易突破了10W的QPS的指标。

我们当初也参考了gRPC,也打算在一些后续的领域里面继续在这方面进行gRPC的尝试,据我们了解下来成熟度很高了,也避免了一些http1.0带来的问题,包括前面说的你的请求和响应必须是同步的方式,并且也支持了双向的通讯,但是怎么说?对于一个音视频指令来说还是过重,我们还是选择了在其他的旁路的服务里面使用了gRPC,包括QUIC协议,因为我们有大量的从国外传递的数据,包括这样的一些指令的传递,QUIC基于UTP的协议来说更于整个的连接和响应速度有效,但是很遗憾国内大部分的厂商和设备对这个支持还是不好,我们期待下一步的发展。

序列化选择 protobuf vs json vs gob

序列化选择,方案里面这三种我们都有,最主要的是PCB+PB,前面过来的是一些json的数据,网关会做一些协议的转化,我们采用了golang内置的gob,但是有一个好处不需要额外的描述,并且支持的数据结构都是最丰富的,也是侵入性最小的一个,我们用它来做一些数据的register化,上课的回放,还有kafka的存储等等,因为我们都是golang的,所以gob也做了使用,基本上性能是从高到低的分布。还有一个很多的时候我们是有动态类型的,比如说类型里面有很多的案例,我们不关心会直接透传或者是广播会定义很多的bytes和any,对于json对应的就是rawmessage。

容错机制 failfast vs failover vs F&F

在容错机制方面,我们也是纠结了很久,当遇到指令发送到下一级处理服务的时候,如果失败了,正常的按照我们的微服务的概念来说,应该是重试,并且对次数进行控制,实际上重试完了之后可能是在学生端前端用户的地方人家已经翻页了,那这个指令就是无效的,并且会带来错乱,所以我们提供了快速失败,如果说这条指令没有传递过去,我们会有一个备选的线路直接发备选线路和你这条指令会做重试,接收端会做趋同和广播,如果备选的也挂了的时候,这个就报警了,这时这个指令就会主动的丢包。

对于旁路的指令会进行正常的重新重试,重新分配下一个指令的数字常识以后,重新分配下一个服务链路。参数收集是最不用担心的,RTCP的参数,包括解码的参数收集我们使用的是fire覆盖,分级报警,如果有这些东西失败了以后,一次性报警会把你的邮箱甚至是微信都弄崩溃,所以我们在报警的时候做了很多的趋同,包括了关联的判断,比如说这个报警是由于某一个已报过的故障引起,所以我们做了这样很多的优化的工作。然后分组处理,就是说我们针对已知的问题的情况下会有一些自动的处理,一些补偿,如果是不在操作库里面补偿的时候,会转为人工干预整个的服务。

经验总结

Goroutine vs map

我们使用GO的时间不是很长,从2016年开始到现在,因为以前我们是从 C++ 和 JAVA 转过来的,所以有一些问题和大家共同的探讨。首先值得分享的是golang的map,在1.9以前是不提供现场安全的map,我们在线上使用的时候大量碰到了同步的问题,因为我们一个房间一个用户就可能有成千上百个状态修改,如果map不强壮会导致很多的问题。

第一版本我们简单做了一下,就用了sync.Mutex锁住,后面用了读写锁,带来的性能的损耗还是挺大的,基于这种情况下我们自己提供了 concurrentHashmap,第一个就是最小化的锁,优化了rehash算法,经过我们测下来的速度远比它高很多,这个只是第一步,在后面的几步,map上线了以后就发现了内存比较平缓的直接上去,为什么呢?

通过检查以后发现了很多的历史数据里面会存于 hashmap 里面,下课的时候会做轻地由于运维的工具和其他的工具会重新的访问这些服务,会读取当时的状态导致这些数据又从reads回来了,我们基于hashmap做了二次的优化,我们又提供了LRU的算法包装,截断很多后体现不是很明显了,那就继续优化,LRU和LFU的算法提供下我们又对它进行了再装饰,提供了一个主动过期的时间,不允许数据在本地缓存里面停留太长时间,如果停留太长时间,我认为会对整个的程序带来一些影响,所以从一个map,一个runtime发展到map,也是经历了很多的工作。

DNS

第二个问题是在大量的线上的并发情况下会出现这样的问题,在做旁路的请求的时候,偶尔会发生DNS不多了的情况,和运维找了很久,后来发现 golang 会提供两种方式,一个是 Cgo,另一个是纯GO,这个是基于文件在下面有一个Edc的文件,如果出现了不支持的关键字就会起用CGO,因为我们的DNS运维会根据实时的动态配置,所以很难对他进行要求,说不能加这个外面的关键字这个很难,然后我们就强制让他在运行的时候加入这个变量,让他强制使用GO,纯GO的实现方式。

我们在运行我们的服务的时候,会发现某一些服务器上面的连接会迅速递增,连接数会迅速的递增,然后爆出文件耗死,耗干的情况。

http issue

我举出这样的例子,我们有一些同学在写请求的时候写一个 client,因为我们要起用 keepalive,我们在dialcontext进行超时的设置,然后也受到roomserver的注意了,就是关闭client这样的代码,实际上我们测下来并没有真正的关闭,因为底层里面有一个 dofo ,如果设置的时候,把client作为唯一性的或者是把transport弄下来,如果这个池每次创建都会创建一个连接池连接,并且关不了,然后会导致越来越多,最后把资源耗死。

我们主要是执行了这样的一些命令做一个文件发现,第一个解决的方式是还有一种如果你不需要去连接共享,你可以关掉,每次我都连一次完了,还有一种是去做transport的池化存储,每次创建就可以把这个给他,这样就可以共享池。当然了系统层面上也有一些设置,包括ulimit,包括lacal,port,range,包括tcp的关闭时间,尽量的缩短可以减少很多时间损耗。

loop issue  

接下来是我们经常常见的处理方式,我们有一个stream,各个端把数据汇到这里,然后每次读出来一个msg进行处理,整个for的循环进行处理,逻辑就是所有的连接把数据丢进来进行逻辑处理。如果是中小型并发量处理并没有问题,每秒钟几十万的并发量,这是一个关键,假如在平时,几毫秒内处理完这个没有问题,在有异常的情况,假如网络环境也包括内存的导致了handle函数慢了10几毫秒,就会导致stream大量的堆积,延迟会越来越高,很多的时候我们会发现容器刚刚启动的时候所有的性能没有问题,跑了越来越久就发现这个数据通过我们打在包里的时间会看到累计的时间越来越多,这个是累计的问题。

解决方式也很简单,第一个是把它容量扩大,另一个是弄一个批处理,不断的append进去做批次的处理,这个方式是指某一些情况,并不是适合所有的情况,很多的时候有一些依赖关系就很难做。

然后还有一种方式,做的提供一个协商的池化,可以并发16个池,所有的sema创建一个chan,所有的指令读出来以后进入这个池,当它满了16个以后就会锁住,这个go继续执行,当处理完以后就读出来放一个进来,读出来一个放一个进来,它不允许并发太多的无限制的使用。

performance issue   

然后还有说的一点是我们的一些 performance,所有的依赖必须加入 context.WithDeadline (WithTimeout) ,就在本机房,网卡没有问题。无法确定所有的问题,所有的操作都是保证加大延迟的时间,如果不这样的话,我们上次就不会有太多的损耗,但是架不住有一些工具,当然了这个是其他人写的工具,里面用了一些keys等等,然后就崩溃了,监控中心找到,然后堆到下面就产生了雪崩的效应,这个大家应该也清楚,所有的这个failover的操作还是基于合理的控制范围内。

未来规划

GO 发展规划

对于未来的规划,首先我们的升级并不是那么的激进,我们目前还是在1.9的版本,但是我们对最新的1.11版本表示有很大的热情,包括了引入Go Module 的打包都会用于1.1的服务。目前让我们痛苦的是,管理的 GO module 的工具都缺乏版本控制,我们技术进来搭 GO 环境的时候最喜欢拉最新的版本跑,跑完发现很多方面存在问题,需要做统一的包管理,包括版本的控制会对整个的统一化的提高,我个人觉得这个统一的包管理也是golang的一大痛点,希望后续能够把这个包管理得像java能够做大做全。然后我们会去研究 Wasm with go,对它有很浓的兴趣是因为很多时候我们的解码能力,包括三维处理都能靠服务系统,如果是能在浏览器端提供基础不用太多基础的音视频解码能力,或者是三维处理能力,会对整个的服务的架构体系会带来很高的希望。

基于 GO+Docker 组件化平台,支持热部署和热更新,目前我们是整个的按照微服务的框架,但是我们希望以后会把微服务拆的更细,类似于一个组件的格式,提供按需的拉取,按需的发布和更新架构的方式。我们目前正在研究的QUIC也是我们很感兴趣的,包括 service mesh,还有一些lstio的研究,包括我们的 feedback 实时的响应,把用户的参数,性能参数实时的反馈,并且有效的通过代码的形式进行影响的机制。

TM+容器化发展规划

最后是容器化的发展,我们目前是基于一个公有云和私有云的混合模式,希望以后能够发展成基于微服务化混合型和按需分配的模式。

下面是我们音视频方案更好的网络建设,更快更灵活,就是打通中国到国外的线路瓶颈,支持4K,8K高清的音视频,包括今年很热的h265,AV1,以及AI和VR,AR的结合,我们在旁路做数据引流,包括AI分析的同时会提供AI的切入和转化,这一块也是在我们的研究当中,还有一些微服务化的scaleout的更新的响应。

我的演讲就到这里,谢谢大家!

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章