您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
网易云信 IM 推送保障及网络优化实践
 
 作者:周江华 来源:极客头条 发布于: 2016-11-9
   次浏览      
 

网易云信Android端高级技术架构师 周江华

大家好,我是来自网易云信的周江华,从 2009年毕业至今,我一直在从事 IM 客户端的研发工作,最早是 Windows 客户端开发,直至 2011 年开始转向移动端,先是 iOS,到 2012 年开始进行 Android 端的开发,从 2014 年到现在,一直负责云信 Android SDK 的开发工作。今天,我想和大家分享的是云信在保障 IM 推送和移动网络优化方面的一些实践经验。

对于移动 App 来说,除了简单的工具类 App 外,IM 功能可谓非常重要,它能够创建起人与人之间的连接。社交类产品中,用户与用户之间的沟通可以产生出更好的用户粘性。电商类产品,用户与商家的连接能极大的促进沟通效率,降低沟通成本。教育类产品,学生与老师的连接让在线教育犹如课堂。医疗类 App,医生与患者的沟通让医生把脉更加准确。

IM 应用如此之广泛,那么,IM 是什么,有些什么要求呢?IM 由两个字组成:Instant,Messaging。即时性要求有新消息时能够立即收到,如果程序在后台,则要能立即收到推送通知。而通信则要求稳定可靠,系统不宕机,程序不崩溃,安全,传递消息时不会被拦截监听,消息不丢,顺序不乱,不重复,如果包含音视频聊天,则要求延迟低,流畅不卡顿。这两点说起来容易,但是,要真正做出一套稳定可靠的商用级IM系统,挑战非常之多。下面,我会挑选影响 IM 即时性和消息稳定性最核心的两个问题,来看看都有哪些障碍,以及云信是如何去实践克服这些障碍的。

首先第一个问题是消息推送,在 iOS 端有 APNS 做推送,相当稳定。Android 本身也有 GCM 可以用,但是在国内,我们有一个叫作“墙”的东西,直接就把 GCM 等 Google 服务全部挡在外面了,导致国内根本无法运用。对于 IM,当 App 退到后台,是必须还能够收到新消息提醒的,没有 GCM,怎么办?唯一能做的,就是后台运行了。Android 从设计上,就是支持真后台运行的,后台运行的特性也是 Android 现在能如此成功的原因之一。但另一面,Android 长久以来一直摆脱不了的卡顿、耗电等坏名声,后台运行也拖不了干系。因此,系统对于后台运行也不会放任自流。App 想要在后台运行,需要面对不少的障碍。

第一个障碍是 Android 的 Low Memory Killer 机制。手机的内存毕竟是有限的,当后台运行的进程越来越多,内存剩余量也就随之减少。当有一个新的 App 想要启动,如果此时内存不够了,LMK 机制就会启动,从正在运行的进程中挑选一个清理掉,释放出空间,然后新的 App 就可以云信了。这个挑选过程显然不会随机拼人品,LMK 有两个尺度去评判。一个是进程优先级,优先级越低,被清理的可能性越大,另一个是内存占用,占的内存越多,被清理的权重自然也越大。

因为 LMK 机制的存在,虽然 App 允许在后台运行,但同样也面临随时被清理的风险。因此,我们需要在被清理后及时的重新启动。常规的,有 4 种方式能够做到:

1.sticky service,就是在 Service 的 onStartCommand 中返回 sticky flag,这样当 service 被 kill 掉后,系统会将它加入重启的 pending 列表,在后面合适的时机再把 service 重启;

2.alarm,闹钟,有循环闹钟和一次性闹钟两种,在闹钟触发后启动对应的组件;

3.在 Manifest 文件中静态注册的 Receiver,通过监听各种系统事件,比如开机、网络变化、mount/unmounts 等。在这些事件发生时启动组件,因为这种方式会造成在这些事件发生时系统容易卡顿,在7.0里面,Android增加了限制;

4.JobScheduler,这是在 5.0 里面新增的,允许 App 在特定事件发生时做一些动作,比如充电、切换到 Wi-Fi 等。

虽说无论怎么做,App 终究免不了一死,但通过对照 LMK 的评判准则,我们还是可以降低 App 被清理的概率的。第一个就是降低进程的内存占用。如果采用单进程的模式,由于进程中包含了 UI、Webview、各种图片缓存等内容,内存必然会居高不下,降不下来。IM 软件一般都会采用双进程甚至多进程的策略,将 push 进程独立出来,在 push 进程里只处理网络连接和 push 业务,不参与任何其他业务逻辑,更不包含任何 UI。

我们来看一下云信 Demo 的进程内存占用情况。上面一个是主进程,看第四列 PSS 的数据,内存占用是 50M 左右,下面一个是 push 进程,内存占用只有 10M 左右。当处于后台时,push 进程被清理概率比 UI 主进程低很多。

降低被清理概率的第二个手段是提升进程优先级。我们先看这个例子,这是绿色守护的一个截图,我们评价其他 App 的行为,也不打广告,所以这里对 icon 和名字做了模糊处理。看最上面,这一组是“暂不自动休眠”,因为这里列出的两个 App 的状态都是工作中,对应的进程优先级是“可视进程”。但这两个 App 并没有提供桌面小部门在运行,也没有指示前台服务的常驻通知栏提醒,事实上,它们就只是在后台运而已。通常进程退到后台后,其进程优先级类型就变成了较低的后台进程,而不是这样的“可视进程”,它们是通过什么方法来提升优先级,降低被清理概率的呢?

Android 在设计前台服务上有一个漏洞,通过两个服务配合,我们就能创建一个隐形的前台服务。这里有两个已经启动的 service: A 和 B。先在 A 中调用 startForeground,提供一个 NOTIFY_ID, 然后 A 就变成前台服务了,同时有了一个 ID 为 NOTIFY_ID 的常驻通知栏提醒,然后我们在 B 中也调用 startForeground,提供相同的 NOTIFY_ID, B 也变成了前台服务,因为两个通知 ID 相同,因此这一次就不会创建新的通知栏提醒了。然后再在 A 中调用 stopForeground,A 的前台属性被取消,同时,常驻通知栏提醒也会被移除,但是,service B 并不会受到任何影响,还是前台服务,这是再把 A 停掉,进程就只剩下前台服务 B 了,进程也变成了前台进程,但用户不会有任何感知。

正常来说,做了上面三步之后,我们的进程就能够比较稳定地在后台运行了。但是后来发现,在有些情况下,我们的推送进程却永远起不来。跟踪之后发现,除了系统能够杀掉后台运行的进程外,用户也一样是可以杀死进程的。用户杀掉进程的方式有两种,一种是在最近任务列表中将 App 划掉,这种方式和系统杀掉进程效果相同。另外一种就是通过这里的 force stop,这种方式比系统清理更加彻底。不但 App 正在运行的进程会被清理,App 当前在重启列表中的待重启服务,注册的各种闹钟、事件监听组件等都会被移除,除非用户在主动点击或者系统重启等外力,App 没法再自己重新爬起来了。

我们后来还发现,在有些国内的像 MIUI 一类的 ROM 上,用户从最近任务列表中将 App 移除,效果竟然也是 force stop。正常来说,如果是用户主动操作,我们 App 本身也不应该再重启了。但有些时候这个并不是用户本意。况且,对于 IM 软件来说,消息提送是一定要得到保障的,否则不明正确的吃瓜群众们会觉得是我们软件不行,连消息推送都做不好。因此,这时候还是应该想办法继续维持后台运行。那么,又有哪些办法呢?

第一个是通过两次 fork 加上 exec 的方式。两个 fork 后,第一次 fork 的进程退出,第二次 fork 出来的进程就会被 init 进程领养。用户此时再 force stop,因为这个进程父进程是 init,而不是 Zygote,因此不会被清理。由于这个进程还是从 Android 进程 fork 出来的,带有 Android 运行时环境以及父进程的资源,所以内存会比较大。这里可以再通过 exec 命令,打开一个纯 Linux 的可执行文件,开启一个 daemon 进程,其内存占用大概只有100K+,对用户也就完全无感了。利用这个后台进程,可以定时地将 push 进程拉起来。此种方式只在 5.0 以下的系统中有效,在 4.4 及以上系统中,SELinux 特性是强制开启的,exec 没有权限执行,同时在 5.0 之后,ActivityManager 在做 force stop 以及移除任务时,只要是具有相同的 uid 的进程,就会全部清理掉,不再漏掉没有虚拟机环境的进程。

最后一个后台保活的手段是一个大杀器,也是带有强烈的中国特色。因为前面所列的所有保活手段都不是那么保险,因此想出来这么一个互相保活的方式。当一个 App 进程起来后,它就去扫描已安装的应用列表,看看有没有自己的兄弟姐妹。比如说同一个长的 App,或者是集成了同一个 SDK 的 App,如果有,就把这些 App 都拉起来。这也就是现在比较出名的“全家桶”方案。虽说这种方法确实能够带来较高的后台存活率,特别是那些大厂和应用广泛的 SDK,但是这种方式对于用户的伤害也非常大,如果有后台推送的必要性,且不会对用户体验造成太大伤害时,此方式还可以使用,但如果只是为了推广告,则会对用户造成伤害,反过来,也可能会导致用户直接卸载 App。

现在,因为“全家桶”实在是太令人讨厌,现在各种手机管理软件都会对这种唤醒方式做限制,特别是在 Root 过的机器上,可以做到完全切断这些唤醒路径。同时,很多 ROM 也会自带管理软件,限制后台运行和后台唤醒,以便给设备换取更长的续航。在目前国内的 Android 生态环境中,无论采用什么方式,想要一直在后台运行时越来越难了,我们需要重新想另外的办法来保障消息推送。另一方面,我们作为开发者,也有义务为用户提供更好体验的软件,而不是无休止的在后台浪费用户的资源。

其实,对于 IM 来说,及时的消息推送和较低的电量消耗也并非不可兼得。在传统上,每个 IM 客户端都会各自维护一条与服务器的长连接,自己的消息和信令都在这条长连接上传递,每个 App 也独自去心跳,断线重连等事情。这种模式比较简单,不同的 App 也是完全隔离的,不会互相影响。但它的缺点也非常明显,首先是做了很多重复的事情,造成了流量和电量的无谓消耗,第二是要保证所有的进程都能在后台运行很难。优化的方向也就非常明显了,那就是共享连接,现在绝大部分推送 SDK 也是这么做的。

从这些 App 里选出一个当前正在运行的,或者是被杀概率最低的 App 作为总代理,只由这个代理和服务器建立连接,一个手机上的所有其他 App 都通过这个代理中转与服务器通信。但是,IM有一个很基本的要求在这种模式下无法得到满足:安全。所有 App 的消息都经过代理中转,代理到服务器的连接是加密、安全的,但到了代理这里,消息都被解开了,因此代理理论上可以看到其他所有 App 的来往消息。因此,这种共享长连接的方式并不适用于 IM。

长连接+推送

虽然共享长连接方式不合适,但仍然给我们提供了一个优化的思路。在此基础上,我们想到了另外一个可以脱敏共享连接的方式:安全长连接加推送连接模式。每个 App 在使用和真正传递数据时,仍然独立使用自己的安全长连接。而当 App 退到后台一段时间之后,则断开长连接,然后每个 App 开启一个推送代理,并选择其中一个和云信的推送服务器建立连接,之后当 App 有新消息时,就通过这个推送连接传递。 App 可以自己控制发出的推送消息的安全级别,可以是包含说话人和消息内容,可以只包含说话人,或者只是一条简单的有新消息到达的提醒文案。推送到达后,如果是代理 App 自己的消息,直接传递给代理 App 即可。如果是其他 App 消息,前面说到过,直接唤醒可能会失败,而且会导致无谓的电量消耗,所以这里并不直接将提醒传递给目标 App ,而是由带来发出一条通知栏提醒。等用户去点击通知栏提醒后,才会把目标 App 唤醒。

系统推送

现在国内的ROM中,华为和小米的系统本来是带有推送系统,且开放给了第三方 App 的。在这两个系统上,使用系统的推送通道明显会更加稳定,也更加节省资源。因此在MIUI上,从长连接到推送通道的切换流程仍然和前面的一样,只是不再使用自己的推送连接,而是将消息转发到MIUI的推送服务器,然后转给MIUI系统的推送代理,然后传递给云信的 App 。华为的推送系统流程也是一样。不过现在华为和MIUI在推送实现上有一些区别,例如MIUI的通知栏提醒是在自己的推送代理里完成的,而华为却是将提醒通知交给 App 自己去完成的,另外,他们的通知栏提醒的管理接口也有很多区别。在 App 没有被禁用的情况下,两者都可以收到推送,而如果 App 已经被禁用了,MIUI的通知栏提醒方式还可以将推送送达,而其他的推送方式则不能送达了。

以上就是在保障消息推送方面我们所能够做的所有事情了。如果以后有更多的系统开放自己的推送系统,我们也可以选择逐步接入,以提高推送到达即时性,减少资源消耗。不过相应的,我们也要承受不断加入各种系统的推送SDK,增大发布包体积的缺点。期望Android拥有统一推送平台的那一天早点到来吧。

相对于PC的网络环境,我总结的手机网络有三个特点:

第一个是慢,尤其是2G,3G网络,慢的令人发指。当我们收发图片视频这类比较大的文件时,就会看到蛋疼的菊花一圈一圈不停的转。

第二个是断,手机跟着人不停的移动,网络也不停的在切换,从 Wi-Fi 到移动网络,从一个基站到另一个基站,从有信号到没信号,都可能导致网络中断。有些制式的网络,接打电话也会导致数据网络断开。另外,移动基站还有 NAT 超时,到一个连接上长时间空闲后,基站就会默默的将连接断开,没有任何通知。

第三个是贵,这个就不用多说,看中国移动每天净赚一个亿就知道了。

在云信整个通信系统中,我们有三种类型的连接:TCP,UDP,HTTP。虽说这三个并不是同一层的协议,不过毕竟都在我们的应用的更下层,因此这么划分也无妨。3种类型的协议对应了不同的业务应用。TCP主要是用户长连接,也就是普通IM消息和信令的传输,UDP用于传输实时音视频数据流,而HTTP则主要用在音频,图片等文件的上传下载上。对于不同的业务,我们的优化的关注点会有一些不相同。

长连接是云信所有业务的基础,使用量也是最大的,因此优化也是从基础开始。 在这里我们举两个例子。

第一个是协议的选择。前面说,长连接的使用量是最大,选择一个合适的协议至关重要。如果是刚开始接触IM开发,一般会选择一些开源的协议,比如XMPP,SIP等。这是XMPP协议的一个请求样例,可以看到是一段XML格式的文本数据。这是基于SIP的SIMPLE协议的一个请求样例,可以看到是一段类似HTTP协议的文本数据。这些协议的优势在于开源,有成熟的解决方案可以使用,扩展性好,甚至还可以和其他系统互联互通,协议的可读性也非常好。但是在普遍比较臃肿,冗余字段很多,在昂贵的移动网络里面用起来会让人觉肉疼。云信采用的是私有的二进制协议,这是一个请求的数据样例,这里是把二进制数据转为了16进制显示出来,每个字节这里显示为两个字符。可以看到二进制协议的特点在于完全失去了可读性,但是,却带来极高的表达效率,相对于文本协议,可以节省非常多的数据流量。

另一个例子是登录的优化。由于移动网络经常断开,所以登录常常是心跳之外交互最多的协议了。使用量越大,优化就越有意义。一般而言,登录会经过这么几步。

第一步是LBS。这里的LBS不是经常说的基于地址位置的服务,在不同的厂商可能也有不同的叫法,反正作用都是获取服务器的IP地址。像云信这种需要提供全球服务的系统,在世界各地都要部署服务器,用户登录时,肯定要选择一台最优的服务器接入服务。通过lbs,客户端可以获取离自己最近,连通性最好的服务器连接机IP地址,服务器也可以据此做负载均衡。

拿到服务器连接机IP后,客户端就去连接该服务器。

连接成功,需要有一次握手。这个握手不是TCP的三次握手,而是为了建立安全连接,同服务器协商加密算法和加密密钥。

然后就发送登录请求,这里会带上用户认证信息,本机设备信息等数据。

登录成功之后,就是同步数据,包括离线消息,用户信息,群组信息等。一般而言,这里不会去做全量同步,而是采用基于时间戳的增量同步。

在移动网络上,每一次交互都需要比较长的时间,同时,每一次网络请求电量消耗也是很大的。所以,优化的方向就是尽量减少交互次数,而方法则是合并请求,并行操作以及省略请求。

LBS和连接这两个步骤是可以并行完成的。如果前面已经获取过LBS,这里可以有之前的缓存地址,如果没有,可以先连一个默认地址。

其次是握手和登录也可以并行操作。在握手包中,就可以把加密后的登录包直接带上去了。如果是断线重连,我们还可以简化登录,直接带上上一次登录的会话ID,一来减少服务器鉴权压力,二则可以直接带回在断线期间是否有未读消息等数据,如果没有,则能直接将同步这一步省略掉。如果有,同步也可以只做部分同步,只去拉去离线消息即可。等到 App 切换到前台,才去同步其他的信息。

通过这些优化,登录时间可以降为原来的1/2到1/3,登录的流量消耗也可以节省30%左右。

实时音视频对实时性要求很高,但可以容忍一定的丢包,所以我们选择 UDP 私有协议来作为底层的传输协议。如果只是普通的IM消息,对网络情况其实不是太敏感,最多也就是慢一点,菊花转得久一点。但对于这种视频电话,如果网络差了,发生了经常性卡顿,或者是延迟很高,图像出现花屏,音视频不同步了,这个功能其实也就相当于废弃了。而且,音视频数据量本身也比较大,在弱网环境下发生问题的概率就更大了。

UDP 协议是不可靠,为了提高弱网下的实时音视频的通话效果,需要使用相关方案来做 QoS 保障:主要包括了基于 UDP 协议的拥塞控制、前向纠错 FEC 技术及相关的重传技术。同时网络层需要能够实时的探测到网络状态,作为底层调整 QoS 策略的依据,同时需要回调上层,来动态调整音视频的码率,做到音视频码率自适应。通过上面的 QoS 保障,我们实际测试在 20% 的随机丢包弱网环境下,音视频通话还能够正常进行。

第二是音频,我们的音频编解码主要以Opus为主,它具备高音质,高压缩率,高抗丢包等特性,非常适合移动网络。我们使用智能的jitterbuffer算法来平滑由于网络抖动引起的声音卡顿和延迟累计问题。配合PLC丢包补偿算法,来降低音频丢包后的爆音。同时,我们使用自研的高性能降噪算法,配合回声消除、自动增益和舒适噪音等音频处理算法来进一步保证音频部分的质量。

对于视频,我们使用时域分层的H264视频编码器,来降低丢包对视频流畅性的影响,同时支持动态帧率和动态分辨率,方便上层根据业务需求进行切换。现在用户对于视频的清晰度要求越来越高,我们的实时通话系统当前能够支持720p。720p下纯软件编解码对CPU开销过大,因此在可以开启硬件编解码的机器上,对于需要720p清晰度的都尽量使用硬件编解码。

由于音视频的网络优化如果全部细说,恐怕再加1个小时也讲不完,所以这里我只提了一些优化的方向供大家参考,就不一一展开了。

下面再来看看对于HTTP的优化。图片语音是IM的必需元素,而且本身数据比较大。在弱网环境下,快速的上传下载,更少的等待时间可以带来更好的用户体验。

断点续传可以减少因网络原因导致的重复传输,减少传输时间,节省流量。

图片预加载技术可以根据不用网络情况,在收到消息后,就加载不同素质的预览图片,甚至直接将原图预加载,做到用户点开即看。

上面两个是比较基础的优化措施,下面两个则比较高级一点。

图片和语音这种文件我们并没有通过长连接收发,而是通过HTTP去做上传下载。传统上通过HTTP上传时,文件会分为一片一片,传完一片,收到回包,才会穿下一个分片,一直到最终传输完成。可以看到,服务器返回ack这段时间,上传通道其实是空闲的,如果把这段时间利用起来,可以节约不少上传时间。Pipeline就是为此而来。通过重叠利用http请求的响应等待时间,加快传输速度。使用pipeline,需要修改HttpClient,同时还需要服务器提供支持。视网络具体情况,使用pipeline后,一次上传可以减少20%至30%的时间。

常规发送语音消息需要这几步,先录音,然后计算hash值,然后上传,上传完毕后,服务器计算一下校验和,通过后语音消息发送成功。在前面录制语音时,网络其实也是空闲的。把这段时间利用起来,则可以减少后面上传步骤的时间。优化后,流程就变成这样。在录制的过程中,每录完一段,就作为一个分片直接上传。直到最后录完,计算好hash,再把最后一个分片带上hash信息上传。这里除了客户端的改动,也是需要服务器支持。服务器在开始接收时,很多信息都不明确,需要开辟缓存来记录整次上传过程。对于比较差的网络,边录边传的效果会更好,毕竟纯语音的比特率并不高,基本都能做到录完就传完。

   
次浏览       
相关文章

企业架构、TOGAF与ArchiMate概览
架构师之路-如何做好业务建模?
大型网站电商网站架构案例和技术架构的示例
完整的Archimate视点指南(包括示例)
相关文档

数据中台技术架构方法论与实践
适用ArchiMate、EA 和 iSpace进行企业架构建模
Zachman企业架构框架简介
企业架构让SOA落地
相关课程

云平台与微服务架构设计
中台战略、中台建设与数字商业
亿级用户高并发、高可用系统架构
高可用分布式架构设计与实践

最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]

相关文章


专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS

相关培训课程


面向应用的架构设计实践
单元测试+重构+设计模式
软件架构师—高级实践
软件架构设计方法、案例与实践
嵌入式软件架构设计—高级实践
SOA体系结构实践

成功案例


锐安科技 软件架构设计方法
成都 嵌入式软件架构设计
上海汽车 嵌入式软件架构设计
北京 软件架构设计
上海 软件架构设计案例与实践
北京 架构设计方法案例与实践
深圳 架构设计方法案例与实践
嵌入式软件架构设计—高级实践
更多...