魅族 C++ 微服务框架技术内幕揭秘

分享嘉宾简介:魅族科技平台事业部于洋子,专一于高并发高性能服务端架构设计与开发,参与过flyme通信、推送平台、实时大数据统计等项目。前端

kiev,是魅族科技推送平台目前使用的C++后台开发框架。2012年,魅族的推送业务刚刚有一点从传统架构向微服务架构转型的意识萌芽,为了在拆分系统的同时提升开发效率,决定作一个C++开发框架,这就是最先期Kiev的由来。在不断的演变中,框架也通过了屡次调整升级,在此一一进行讲述和揭秘。框架是为架构在作服务,因此整篇内容会在架构演进和框架演进两条线之间交错展开。mysql

初版:没有开发框架git

首个版本的架构很是简单粗暴,首先开一个WEB接口,接入PUSH,再开一个TCP长链接的接口,让手机连上。这么作的目的就是为了能快速上线。不过快是快了,问题也很严重。这个版本没有开发框架,彻底从socket写起,不只难写,并且不能水平扩展,承载能力也很是有限。github

第二版:框架首次出现redis

随着魅族用户量级的快速提高,很快迭代了第二个版本。第二版首次出现了开发框架,命名为“Kiev”。这个版本对手机链接的部分进行了拆分,拆出接入层和路由层,业务层支持水平扩展,这样重构之后抗住了百万级的用户量。不过一样存在很多问题,由于仍是在用普通的HASH算法在作均衡负载,扩容很是不平滑,容易影响用户体验。并且随着用户量的增加,日志量变的很是多,甚至都要把磁盘刷爆。此外,因为使用的文本协议很臃肿,当某一天中午12点推送高峰期的时候,整个公司的机房带宽都被吃完,其余业务受到了不一样程度的干扰。算法

这个版本的框架以下,左上角是Kiev协议,左下角是使用到的一些开源的第三方库 ,包括谷歌开源的Protobuf、用于加解密的Openssl、 用于支持HTTP的Curl、优化内存分配的Tcmalloc等。右上角是Kiev框架的功能组件,包括提供HTTP接口的FastCGI、一些经常使用的算法和数据结构、日志模块、编码经常使用的定时器以及一个自研的单连接能达到10W+QPS的Redis Client。sql

第三版:增长限速、业务流程优化、日志切割和压缩后端

考虑到前面说到的带宽撑爆问题,第三版增长了限速模块。此外还作了一个业务流程上的优化,使用redis存储离线消息,用户上线时再推送出去。负载均衡上,改用一致性HASH算法,这样作的好处是每次扩容受到影响的只有迁移的那一部分用户,另外一部分用户则不会受任何影响,扩容变得平滑了不少。针对日志刷爆磁盘的问题,作了一个天天定时切割和压缩日志的脚本。服务器

看看这个版本在框架上作的一些修改,图中深色部分为新增的东西:网络

第四版:全面重构

为了完全解决第二版的一些问题,花了半年多的时间对框架进行全面重构。重构主要针对如下几点:

一是将限速、接入层、路由层、逻辑层等都作成了无状态服务,这样的话在整个扩容的过程当中能够作到彻底平滑;

二是对协议进行优化,将本来臃肿的文本协议改成二进制协议,协议头从700字节降到6个字节,大幅度下降了流量;

三是流程上的优化,这个仍是趋于流量的考量。你们都知道移动互联网有个很显著的特色,就是手机网络特别不稳定,可能这一秒在线,下一秒走进电梯就失去信号,这个时候若是直接进行消息推送的话,既浪费机房带宽,又没效果,并且还可能会出现重复推送的问题。因此针对这种状况,魅族的作法是每次先推一个很小的只有几个字节的消息过去,若是手机端的网络稳定,它会回复一个一样很小的消息,这时候再真正进行消息推送,这样能够有效利用带宽资源。并且给每一条消息打上惟一的序号,当手机端每次收到消息时,会将序号储存起来,下次拉取消息的时候再带上来,好比某用户已收到一、二、3的消息,拉取的时候把3带上来,服务端就知道一、二、3都已经推过了,直接推送4以后的消息便可,避免消息重复。

这个版本的框架改进比较小,在上个版本的基础上引入MongoDBClient,对序号进行索引。

业务越作越大,发现新问题1

随着业务越作越大,业务流程也变得愈来愈复杂。举个栗子,魅族有一个业务流程中,请求过来时,会先和Redis来回交互几回,而后才访问MongoDB,最后还要和Redis交互几回才能返回结果。

这种时候若是按早期的异步模式去写代码,会很难看。能够看到整个业务流程被切割的支离破碎,写代码的和看代码的人都会以为这种方式很不舒服,也容易出错。

针对这种复杂的问题,魅族引入了“协程”,用仿造Golang的方式本身作了一套协程框架Libgo。重构后的代码变成以下图左侧的方式,整个业务流程是顺序编写的,不只没有损失运行的效率,同时还提升了开发的效率。

Libgo的简介和开源地址以下:

  • 提供CSP模型的协程功能
  • Hook阻塞的系统调用,IO等待时自动切换协程
  • 无缝集成使用同步网络模型的第三方库 (mysqlclient/CURL)
  • 完善的功能体系:Channel / 协程锁 / 定时器 / 线程池等等

开源地址:https://github.com/yyzybb537/libgo

业务越作越大,发现新问题2

在这个时期,在运营过程当中有遇到一个问题,天天早上9点钟,手机端会向服务端发一个小小的订阅请求,这个请求一旦超时会再来一遍,不断重试。当某天用户量增加到1300万左右的时候,服务器雪崩了!

雪崩的缘由是由于过载产生的,经过分析发现过载是在流程中的两个服务器间产生的。服务器A出现了大量的请求超时的log,服务器B出现接收队列已满的log,此时会将新请求进行丢弃。此时发现,在服务器B的接收队列中积压了大量请求,而这些请求又都是已经超时的请求,手机端已经在重试第二次,因此当服务器拿起以前这些请求来处理,也是在作无用功,正由于服务器一直在作无用功,手机端就会一直重试,所以在外部看来整个服务是处于不可用状态,也就造成了雪崩效应。

当时的紧急处理方式是先对接收队列的容量进行缩小,提供有损服务。所谓的有损服务就是当服务器收到1000个请求但只能处理200个请求时,就会直接丢弃剩下的800个请求,而不是让他们排队等待,这样就能避免大量超时请求的问题。

那紧急处理后,要怎么样根治这个问题呢?首先对这个过载问题产生的过程进行分析,发现是在接收队列堵塞,因此对接收点进行改造,从原来的单队列变为多队列,按优先级进行划分。核心级业务会赋予最高级的优先处理队列,当高优先级的请求处理完后才会处理低优先级的请求。这样作的就能保证核心业务不会由于过载问题而受到影响。

还有一点是使用固定数量的工做协程处理请求,这样作的好处是能够控制整个系统的并发量,防止请求积压过多,拖慢系统响应速度。

业务越作越大,发现新问题3

在最先的时候,这一块是没有灰度发布机制的,全部发布都是直接发全网,一直到机器量涨到上百台时依然是用这种方式,若是没问题固然皆大欢喜,有问题则全部一块儿死。这种方式确定是没法长远进行,须要灰度和分组。但因为服务是基于TCP长链接的,在业内目前没有成熟的解决方案,因此只能本身摸索。

当时的第一个想法是进行分组,分为组1和组2,全部的请求过来前都加上中间层。这样作的好处是能够分流用户,当某一组出现故障时,不会影响到所有,也能够导到另一组去,并且在发布的时候也能够只发其中一组。

那中间层这一块要怎么作呢?在参考了不少业界的成熟方案,但大可能是基于HTTP协议的,不多有基于TCP长链接的方案,最终决定作一个反向代理。它的灵感是来源于Nginx反向代理,Nginx反向代理你们知道是针对HTTP协议,而这个是要针对框架的Kiev协议,刚好魅族在使用ProtoBuf在作协议解析,具备动态解析的功能,所以基于这样一个功能作了Kiev反向代理的组件。这个组件在启动时会向后端查询提供哪些服务、每一个服务有哪些接口、每一个接口要什么样的请求、回复什么样的数据等等。将这些请求存储在反向代理组件中,组成一张路由表。接收到前端的请求时,对请求的数据进行动态解析,在路由表中找到能够处理的后端服务并转发过去。

第五版:针对问题,解决问题

有了上述这些规则后,第五版也就是目前使用的版本部署以下图。对逻辑层进行了分组,分流用户。在实际使用过程当中精准调控用户分流规则,慢慢进行迁移,一旦发现有问题,当即往回倒。此外,还精简了存储层,把性价比不高的MongoDB砍掉,下降了70%的存储成本。

不少项目特别是互联网项目,在刚刚上线的时候都有个美好的开始,美好之处在于最初全部服务的协议版本号都是同样的。就好比说A服务、B服务、C服务刚开始的时候全都是1.0,彻底不用去考虑兼容性问题。当有一天,你须要升级了,要把这三个服务都变成2.0的时候,若是想平滑的去升级就只能一个一个来。而在这个升级的过程当中,会出现低版本调用高版本,也会出现高版本调用低版本的状况,特别蛋疼,这就要求选择的通信协议支持双向兼容,这也是魅族使用Protobuf的缘由。

最终,完整的框架生态以下。虚线框内为后续将加入的服务。

魅族消息推送服务的现状

该服务在过去的4年多来一直只是默默的为魅族的100多个项目提供,前段时间,正式向社区全部的开发者开放了这种推送能力,接入的交流群:QQ488591713。目前有3000万的长链接用户,为100多个项目提供服务。集群中有20多个微服务和数百个服务进程,有100多台服务器,天天的推送量在2亿左右。

(文章内容由开源中国整理自2016年9月10日的【OSC源创会】珠海站,转载请注明出处。)

相关文章
相关标签/搜索