移动无线网络长连接 推送技术 已经服务器连接数多时解决策略

来源:互联网 发布:网络选修课怎么上 编辑:程序博客网 时间:2024/05/07 04:37

推送技术原理:移动互联网长连接

本文转自极光推送技术博客。

原文链接:http://blog.jpush.cn/index.php/jpush_wireless_push_principle/

原文标题是“极光推送技术原理”,说极光推送是如何在 Android 上实现移动 Push 技术的。但实际上是在讲个普遍的问题,对理解 Apple APNs, Google GCM 原理有帮助。

移动互联网应用现状

因为手机平台本身、电量、网络流量的限制,移动互联网应用在设计上跟传统 PC 上的应用很大不一样,需要根据手机本身的特点,尽量的节省电量和流量,同时又要尽可能的保证数据能及时到达客户端。

为了解决数据同步的问题,在手机平台上,常用的方法有2种。一种是定时去服务器上查询数据,也叫Polling,还有一种手机跟服务器之间维护一个 TCP 长连接,当服务器有数据时,实时推送到客户端,也就是我们说的 Push。

从耗费的电量、流量和数据送达的及时性来说,Push 都会有明显的优势,但 Push 的实现和维护成本相对较高。在移动无线网络下维护长连接,相对也有一些技术上的难度。本文试图给大家介绍一下我们极光推送在 Android 平台上是如何维护长连接。

移动无线网络的特点

因为 IP v4 的 IP 量有限,运营商分配给手机终端的 IP 是运营商内网的 IP,手机要连接 Internet,就需要通过运营商的网关做一个网络地址转换(Network Address Translation,NAT)。简单的说运营商的网关需要维护一个外网 IP、端口到内网 IP、端口的对应关系,以确保内网的手机可以跟 Internet 的服务器通讯。



NAT 功能由图中的 GGSN 模块实现。

大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断。

Android 平台上长连接的实现

为了不让 NAT 表失效,我们需要定时的发心跳,以刷新 NAT 表项,避免被淘汰。

Android 上定时运行任务常用的方法有2种,一种方法用 Timer,另一种是AlarmManager。

Timer

Android 的 Timer 类可以用来计划需要循环执行的任务,Timer 的问题是它需要用 WakeLock 让 CPU 保持唤醒状态,这样会大量消耗手机电量,大大减短手机待机时间。这种方式不能满足我们的需求。

AlarmManager

AlarmManager 是 Android 系统封装的用于管理 RTC 的模块,RTC (Real Time Clock) 是一个独立的硬件时钟,可以在 CPU 休眠时正常运行,在预设的时间到达时,通过中断唤醒 CPU。

这意味着,如果我们用 AlarmManager 来定时执行任务,CPU 可以正常的休眠,只有在需要运行任务时醒来一段很短的时间。极光推送的 Android SDK 就是基于这种技术实现的。

服务器设计

当有大量的手机终端需要与服务器维持长连接时,对服务器的设计会是一个很大的挑战。

假设一台服务器维护10万个长连接,当有1000万用户量时,需要有多达100台的服务器来维护这些用户的长连接,这里还不算用于做备份的服务器,这将会是一个巨大的成本问题。那就需要我们尽可能提高单台服务器接入用户的量,也就是业界已经讨论很久了的 C10K 问题。

C2000K

针对这个问题,我们专门成立了一个项目,命名为C2000K,顾名思义,我们的目标是单机维持200万个长连接。最终我们采用了多消息循环、异步非阻塞的模型,在一台双核、24G内存的服务器上,实现峰值维持超过300万个长连接。




关于C10问题的经典描述可以查看这个网页 

具体来说就是服务器如何处理10k个客户端的并发连接,即 concurrent 10,000 connection 。如果在很早以前互联网还不普及的时候,一个服务器很少会同时出现有10k的连接,但是现在互联网高速发展,这种规模的连接可能随处可见,所以如何来解决C10k的问题对于服务提供者来说是一个最先需要解决的问题。有人可以说现在硬件成本很低,连接增多可能会消耗很大的内存那么我扩充内存就可以了,cpu的负载很高,那么我提高cpu的性能就可以了。

OK,这种解决方案类似于兵来将挡,水来土掩,感觉很easy,但不幸的是,连接的客户端数超过一定的规模之后对服务器的资源的要求往往是不是线性的,大多数是O(n^2)的需求,所以你再任性,再有钱,传统的解决方案也是解决不了的。

同时,服务商一般都会从经济的角度去考虑问题,而对于互联网公司来说,很大的一笔成本就是硬件的投入和消耗,当然电费也在里面。所以如果相同的硬件成本和电费可以处理更多的请求完成更多的事情,我想他们何乐不为?


解决方案:

解决C10k问题的总体的思路就是从两个方面来考虑:


1.应用程序以何种方式和操作系统合作来处理I/O的问题,阻塞,非阻塞,还是异步I/O?
2. 应用程序如何处理任务和线程/进程的关系, one task one thread/process, 还是one thread/process more tasks, or thread poll ?


展开来说主要有:

应用程序以何种方式和操作系统合作来处理I/O

 0.  阻塞式的I/O,例如read()函数,这种模型如果想实现并发就必须要使用多线程机制来完成。

 1.  非阻塞I/O,例如I/O多路复用中的select,poll等通过设置句柄的方式在I/o就绪的时候由内核通知应用程序进行读写,epoll则直接通知进行那个文件描述符的读写,当然还有将传统的阻塞式I/O函数,如write()设置为O_NONBLOCK的模式来进行非阻塞的写。当然,这种方法只是适合网络IO,对于磁盘IO这种方法并不合适。

 2.  使用异步IO调用(比如aio_write())来启动IO ,这种I/O由内核线程来完成I/O操作,并在完成之后通知应用程序,由于内核线程的效率较高,所以可以取得很好的效率,但是遗憾的是在*nix中并没有对此种模式直接的支持,而在windows中是可以的。这种方法对于网络IO和磁盘IO都很适合。

应用程序如何处理任务和线程/进程的关系

 以客户端连接服务器的情况来说,,主要有下面的几个模型

1. 为每个客户端分配一个OS Level的线程,因为往往进程消耗的资源较多,并且在创建和切换上比较麻烦,所以后来OS开始支持轻量级的进程,也即线程来为每个连接做服务。但是,后来随着硬件资源越来越便宜,进程模型又变的比线程模型要好,因为进程之间不牵扯到资源的同步问题,关于这些这里不做讨论

2. 一个线程处理多个客户端等。


具体的I/O模型

 

(注:这里的内容来自于网上其他作者的资源,主有:)


这种方式很简单,它将所有的网络文件句柄的工作模式都设置成NON-BLOCKING,通过调用select()方法或者poll()方法来告诉应用层哪些个网络句柄有正在等待着并需要被处理的数据。这是一种非常传统的方法。通过这种机制,内核能够告诉应用层一个文件描述符是否准备好了(这里的准备好有着明确的含义,对于读描述符,准备好了意味着此时该描述符的缓冲区内数据已经准备好,读取该描述符的数据不会发生阻塞,而对于写描述符而言,准备好了意味着另外一层含义,它意味着写缓冲区已经准备好了,此时对该操作符的写操作也将不会导致任何阻塞发生),以及你是否已经利用该文件描述符作了相应的事情。因为这里的就绪通知方式是水平触发,也就说如果内核通知应用层某一个文件描述符已经就绪,而如果应用层此后一直没有完整的处理该描述符(没有读取完相应的数据或者没有写入任何数据),那么内核会不断地通知应用层该文件描述符已经就绪。这就是所谓的水平触发L-T:只要条件满足,那内核就会触发一个事件(只要文件描述符对应的数据没有被读取或者写入,那内核就不断地通知你)。

需要注意的是:内核的就绪通知只是一个提示,提示也就意味着这个通知消息未必是100%准确的,当你读取一个就绪的读文件描述符时,实际上你有可能会发现这个描述符对应的数据并没有准备好。这就是为什么如果使用就绪通知的话一定要将文件描述符的模式设置成NOBLOCK的,因为NOBLOCK模式的读取或者写入在文件描述符没有就绪的时候会直接返回,而不是引起阻塞。如果这里发生了阻塞,那将是非常致命的,因为我们只有一个线程,唯一的线程被阻塞了的话,那我们就玩完了。

这种方式的一个缺陷就是不适用磁盘文件的IO操作。将磁盘文件的操作句柄的工作模式设置成NOBLOCK是无效的,此时对该磁盘文件进行读写依然有可能导致阻塞。对于缺乏AIO(异步IO)支持的系统,将磁盘IO操作委托给worker线程或者进程是一个好方法来绕过这个问题。一个可行的方法是使用memory mapped file,然后调用mincore(),mincore会返回一个向量来表示相应的page是否在ram缓存中,如果page不在ram缓存中,则意味着读取该页面会导致page falut,从而引起阻塞,那么就需要通过委托的worker线程来进行IO操作。这种方式的实现方法在Linux上就是selectpoll这样的系统调用。


0 0