一、导言
1关于这个话题,
最近,许多从事移动互联网和物联网发展的学生在微博上给我发电子邮件或私人信件,询问与推送服务相关的问题。有许多种问题。在帮助大家回答问题的过程中,我也总结了一些问题,大致可以归纳为以下几类:
奈蒂能成为推送服务器吗?
如果使用Netty开发推送服务,一个服务器最多可以支持多少个客户端?
使用Netty开发推送服务时遇到的技术问题。
因为有很多咨询顾问,他们的关注点都比较集中,希望通过本文的案例分析和推送服务设计要点的总结,能够帮助您在实际工作中少走弯路。
2关于推送服务
在移动互联网时代,推送服务已经成为应用程序不可或缺的重要组成部分,它可以提高用户的活跃度和保留率。我们的手机每天都会收到各种广告和提示信息,其中大部分是通过推送服务实现的。
随着物联网的发展,大多数智能家庭都支持移动推送服务。未来,所有连接到物联网的智能设备都将成为推送服务的客户端,这意味着推送服务未来将面临大量设备和终端。
推送服务的3个特征
移动推送服务的主要特点如下:
使用的网络主要是运营商的无线移动网络,网络质量不稳定。例如,地铁上的信号很差,容易出现网络闪络;
大规模客户端访问,通常使用长连接,无论是客户端还是服务器,资源消耗都非常大;
由于谷歌的推送框架不能在中国使用,安卓的长连接由每个应用程序维护,这意味着每个安卓设备上将有多个长连接。即使没有要推送的消息,长连接本身的心跳消息量也非常大,这将导致流量和功耗的增加;
不稳定:消息丢失、重复推送、延迟传递和过期推送时有发生;
垃圾邮件满天飞,缺乏统一的服务治理能力。
为了解决上述缺点,一些企业也给出了自己的解决方案,比如京东云推出的推送服务,可以实现多应用单服务的单连接模式,并使用AlarmManager在规律的心跳下节省电力和流量。
2.首先看看智能家居领域的真实案例
1问题描述
智能家庭MQTT消息服务中间件使100,000个用户保持在线连接,20,000个用户同时发出消息请求。运行程序一段时间后,发现内存泄漏,怀疑是网虫。其他相关信息如下:
MQTT消息服务中间件服务器内存16G,8核中央处理器;;
在Netty中,boss线程池的大小是1,工作线程池的大小是6,其余的线程被分配给业务使用。分配方法后来被调整为工作线程池大小11,问题仍然存在;
网络版是4.0.8,最终版..
2问题位置
首先,我们需要转储内存堆栈,并分析怀疑内存泄漏的对象和引用关系,如下所示:
绝对干货:基于Netty _1226000.png的大众化推送服务的技术要点
我们发现,Netty的调度未来任务增加了9076%,达到大约110瓦的实例。通过对业务代码的分析,我们发现当链路空闲时,用户使用IdleStateHandler进行业务逻辑处理,但是空闲时间设置为15分钟。
Netty的IdleStateHandler将根据用户的使用场景启动三种类型的调度任务,即:ReaderIdleTimeoutTask、WriterIdleTimeoutTask和AllIdleTimeoutTask,它们将被添加到NioEventLoop的任务队列中进行调度和执行。
由于超时时间太长,10W长的链接将创建10W ScheduledFutureTask对象,并且每个对象还存储业务成员变量,这将消耗大量内存。用户的持久性生成相对较大,一些计划任务被老化为持久性生成,而这并没有被JVM垃圾收集。内存一直在增长,用户错误地认为存在内存泄漏。
事实上,经过进一步分析,我们发现用户的超时非常不合理,15分钟的超时不能达到设计目标。重新设计后,超时设置为45秒,内存可以正常回收,从而解决了问题。
3问题总结
如果有100个长连接,即使是长时间的计划任务也不会有内存泄漏问题。在新一代中,内存可以通过较小的垃圾收集来回收。正是因为100,000级长的连接,小问题被放大,从而导致各种后续问题。
事实上,如果用户确实有长时间运行的计划任务,他们应该做什么?对于具有大量长连接的推送服务,如果代码处理不当,整个游戏将会失败。根据Netty的架构特点,我们将介绍如何使用Netty来实现数百万客户的推送服务。
3.干货启动:网易大规模接入服务的技术要点
作为一个高性能的NIO框架,使用Netty来开发一个高效的推送服务在技术上是可行的。然而,由于推送服务本身的复杂性,开发稳定且高性能的推送服务并不容易。在设计阶段有必要对推送服务进行合理的设计。
1修改了手柄的最大数量
为了访问一个百万长的连接,首先需要优化的是Linux内核参数,其中最大数量的文件句柄是最重要的调优参数之一。单个进程打开的默认最大句柄数是1024。相关参数可通过ulimit -a查看。示例如下:
01
02
03
04
05
06
07
08
09
10
11
[根@李林峰~]# ulimit -a
核心文件大小(块,-c) 0
数据段大小(千字节,-d)无限制
调度优先级(-e) 0
文件大小(块,-f)无限制
待定信号(-i) 256324
最大锁定内存(kb,-l) 64
最大内存大小(kb,-m)无限制
打开文件(-n) 1024
...后续输出被省略
当单次推送服务收到的链接超过上限时,它将报告“打开的文件太多”,并且所有新客户端将无法访问。
通过vi /etc/security/limits.conf添加以下配置参数:修改后保存,注销当前用户,再次登录,并通过ulimit -a a检查修改后的状态是否有效。
1
2
*软nofile 1000000
*硬nofile 1000000
需要指出的是,虽然我们可以将单个进程打开的最大句柄数量修改为非常大,但是当句柄数量达到一定数量级时,处理效率会明显下降,因此需要根据服务器的硬件配置和处理能力进行合理设置。如果单个服务器的性能不好,也可以通过集群来实现。
2小心CLOSE_WAIT
从事移动推送服务开发的学生可能有过移动无线网络可靠性很差的经历,经常会出现客户端复位连接、网络闪烁等情况。
在具有数百万长连接的推送系统中,服务器需要能够正确处理这些网络异常。设计要点如下:
需要合理设置客户端的重新连接间隔,以防止由于连接过于频繁而导致连接失败(例如,端口尚未释放);
客户端重复登录拒绝机制;
服务器正确处理输入/输出异常和解码异常,以防止处理泄漏。
最后但同样重要的是,我们应该注意太多的近距离等待的问题。由于网络不稳定,客户端通常会断开连接。如果服务器未能及时关闭套接字,将导致处于close_wait状态的链接过多。处于close_wait状态的链接不会释放句柄和内存等资源。如果积压太大,系统句柄将会用尽,并且会出现“打开的文件太多”的异常,这样新的客户端将无法访问它,并且涉及创建或打开句柄的操作将会失败。
下面简要介绍关闭等待状态,被动关闭TCP连接状态的迁移图如下:
绝对干货:基于Netty _1226001.png的大众化推送服务的技术要点
关闭等待是通过被动关闭连接形成的。根据TCP状态机,当服务器接收到客户端发送的FIN时,TCP协议栈会自动发送确认消息,链路会进入关闭等待状态。但是,如果服务器端不执行套接字的close()操作,则状态不能从close_wait迁移到last_ack,并且系统中会有许多处于close_wait状态的连接。一般来说,close_wait至少持续2小时(系统默认超时为7200秒,即2小时)。如果服务器程序由于某种原因导致系统消耗大量资源,系统通常会在发布前崩溃。
过度关闭等待的可能原因如下:
该程序处理bug,导致在接收到另一方的fin后无法及时关闭套接字。这可能是Netty的一个缺陷,也可能是业务层的一个缺陷,需要对具体问题进行具体分析;
关闭套接字是不合时宜的:例如,输入/输出线程被意外阻塞,或者由输入/输出线程执行的用户定义的任务的比例太高,导致输入/输出操作的不合时宜的处理和链接的不合时宜的释放。
接下来,我们结合Netty的原理来分析潜在的故障点。
设计要点1:不要在Netty的输入/输出线程上处理业务(除了心跳发送和检测)。为什么?对于Java进程,线程不能无限增长,这意味着Netty的反应器线程数量必须收敛。Netty的默认值是处理器内核* 2。通常,输入/输出密集型应用程序建议将线程数设置得尽可能大,但这主要是针对传统的同步输入/输出。对于非阻塞输入/输出,不建议将线程数设置得太大。虽然没有最佳值,但输入/输出线程的经验值在[中央处理器内核+1,中央处理器内核*2]之间。
如果单个服务器支持100万个长连接,并且服务器核心数量为32,则单个输入/输出线程处理的链路数量为L = 100/(32 * 2) = 15625。如果每5S有一条消息交互(新消息推送、心跳消息和其他管理消息),平均CAPS = 15625/5 = 3125条/秒。与Netty的处理性能相比,这个值压力更小。然而,在实际的业务处理中,经常会有一些额外的复杂逻辑处理,如性能统计、记录接口日志等。,这些业务运营的性能开销也相对较大。如果直接在输入/输出线程上执行业务逻辑处理,输入/输出线程可能会被阻塞,从而影响其他链路上的读写操作,这将导致被动关闭的链路无法及时关闭,从而导致close_wait的累积。
设计要点2:在输入/输出线程上执行定制任务时要小心。内蒂的输入/输出处理线程NioEventLoop支持两种自定义任务的执行:
普通可运行:通过调用NioEventLoop的执行(可运行任务)方法来执行;
调度任务调度任务:通过调用NioEventLoop的调度(可运行命令,长延迟,时间单位)系列接口来执行。
为什么NioEventLoop支持用户定义的Runnable和ScheduledFutureTask的执行并不是本文的重点,稍后将介绍一篇专门的文章。本文着重分析了它们的影响。
在NioEventLoop中执行可运行和可调度的未来任务意味着允许用户在NioEventLoop中执行非I/O操作业务逻辑,这通常与消息处理和协议管理相关。它们的执行将抢占在Nioeventloop中读写输入/输出的CPU时间。如果用户定义的任务太多,或者单个任务的执行周期太长,I/O读写操作将被阻塞,这将间接导致close_wait的累积。
因此,如果用户在代码中使用了Runnable和ScheduledFutureTask,请合理设置优先级的比例,可以通过NioEventLoop的设置优先级(内部优先级)方法来设置。默认值为50,即输入/输出操作与用户自定义任务的执行时间比为1: 1。
我的建议是,当服务器处理来自客户端的大量长连接时,不要在NioEventLoop中执行自定义任务或非心跳计划任务。
设计要点3:使用3:IdleStateHandler时要小心。许多用户将使用IdleStateHandler发送和检测心跳,这是值得推广的。这种方法比通过自启动时间任务发送心跳更有效。但是,在实际开发中,应该注意的是,在心跳的业务逻辑处理中,处理延迟在正常和异常情况下都应该是可控的,以防止NioEventLoop由于不可控的延迟而被意外阻塞。例如,当心跳超时或发生输入/输出异常时,服务调用电子邮件发送接口发出警报,并且电子邮件发送客户端由于电子邮件服务器超时而被阻止。级联导致IdleStateHandler的AllIdleTimeoutTask任务被阻塞,最后NioEventLoop多路复用器上的其他链接的读写被阻塞。
ReadTimeoutHandler和WriteTimeoutHandler也存在约束。
3合理的心跳周期
数以百万计的推送服务意味着将会有数以百万计的长连接,每个长连接都需要依靠应用程序之间的心跳来维持链接。合理设置心跳周期非常重要,在设置推送服务的心跳周期时应考虑移动无线网络的特点。
当智能手机连接到移动网络时,它并没有真正连接到互联网。运营商分配给移动电话的IP实际上是运营商的内部网IP。要将移动电话终端连接到互联网,必须通过运营商的网关转换IP地址。这个网关简称为网络地址转换。简单地说,移动电话终端到互联网的连接实际上是移动内联网、端口和外部网络之间的映射。
GGSN模块实现了网络地址转换功能。因为大多数移动无线网络运营商都希望减少网关NAT映射表的负载,如果一条链路有一段时间没有通信,它会删除相应的表,导致链路中断。正是这种故意缩短空闲连接的释放超时的做法,最初是为了节省信道资源,但没有想到互联网应用程序不应该发送远高于正常频率的心跳信号来维持长时间的推送连接。以中国移动的2.5G网络为例,当基带空闲约5分钟时,连接将被释放。
由于移动无线网络的特点,推送服务的心跳周期不能设置得太长,否则会释放较长的连接,导致客户端频繁重新连接,但也不能设置得太短,否则在没有统一心跳框架的情况下,很容易出现信令风暴(如微信心跳信令风暴问题)。具体的心跳周期没有统一的标准。180年代可能是个不错的选择,微信是300年代。
在Netty中,心跳检测可以通过在通道管道中添加IdleStateHandler,在构造函数中指定链路空闲时间,然后实现空闲回调接口来实现心跳发送和检测。代码如下:
01
02
03
04
05
06
07
08
09
10
11
12
13
公共无效initChannel({@link Channel}频道){
channel.pipeline()。添加最后一个(“idleStateHandler”,新{@link IdleStateHandler}(0,0,180));
channel.pipeline()。添加最后一个(“我的处理程序”,新的我的处理程序());
}
拦截链路空闲事件并处理心跳:
公共类MyHandler扩展了{@link ChannelHandlerAdapter} {
{@code @Override}
公共空用户事件触发({ @ link Channel HandlerContext } CTX,{@link Object} evt)引发{@link Exception} {
if(evt instance of { @ link IdleStateEvent } } {
//心跳处理
}
}
}
4合理设置接收和发送缓冲区的容量
对于长链接,每个链接需要维护自己的消息接收和发送缓冲区。JDK的本地NIO类库使用java.nio.ByteBuffer,这实际上是一个具有固定长度的字节数组。我们都知道数组不能动态扩展,字节缓冲也有这个限制。相关代码如下:
1
2
3
4
5
6
7
公共抽象类ByteBuffer
扩展缓冲区
可比较的工具
{
最终字节[]HB;//仅堆缓冲区为非空
最终int偏移量;
布尔值isReadOnly
无法动态扩展容量会给用户带来一些麻烦。例如,因为无法预测每个消息的长度,所以可能需要预先分配一个更大的字节缓冲区,这通常没有问题。然而,在大规模推送服务系统中,这会给服务器带来沉重的内存负担。假设单个推送消息的最大上限是10K,消息的平均大小是5K。为了满足10K消息的处理,字节缓冲区的容量被设置为10K,这样每个链路实际上会多消耗5K的内存。如果长链接的数量是100万,并且每个链接独立地保存字节缓冲接收缓冲区,则消耗的总内存是总计(M) = 1000000 * 5K = 4882M。过多的内存消耗不仅会增加硬件成本,还会导致长期的完全垃圾回收,这对系统稳定性有很大影响。
事实上,最灵活的处理方法是动态调整内存,也就是说,接收缓冲区可以根据过去收到的消息来计算,动态调整内存,并使用CPU资源来改变内存资源。具体战略如下:
字节缓冲支持容量扩展和收缩,并可根据需要灵活调整以节省内存;
在接收消息时,可以根据指定的算法分析以前接收到的消息的大小,预测未来消息的大小,并根据预测值灵活调整缓冲容量,使最小的资源损失能够满足程序的正常功能。
幸运的是,Netty提供的ByteBuf支持动态容量调整,并且Netty为接收缓冲区提供了两种内存分配器:
固定长度接收缓冲区分配器。由它分配的字节流有一个固定的大小,不会根据实际数据报的大小动态收缩。但是,如果容量不足,则支持动态扩展。动态扩展是Netty ByteBuf的基本功能,与ByteBuf分发器的实现无关;
AdaptiveRecvByteBufAllocator:一种具有动态容量调整的接收缓冲区分配器,它根据前一个通道接收的数据报的大小进行计算,如果接收缓冲区的可写空间持续填满,则动态扩展容量。如果连续两次接收的数据报小于指定值,则当前容量会减少以节省内存。
与FixedRecvByteBufAllocator相比,使用AdaptiveRecvByteBufAllocator更为合理,并且可以在创建客户端或服务器时指定RecvByteBufAllocator。代码如下:
1
2
3
4
5
引导b =新引导();
b .集团(集团)
。信道(NioSocketChannel.class)
。选项(通道开发。TCP_NODELAY,真)
。选项(通道开发。RCVBUF_ALLOCATOR,AdaptiveRecvByteBufAllocator。默认)
如果默认情况下未设置,则使用AdaptiveRecvByteBufAllocator。
此外,值得注意的是,无论是接收缓冲区还是发送缓冲区,建议将缓冲区的大小设置为消息的平均大小,而不是最大消息的上限,这将导致额外的内存浪费。您可以如下设置接收缓冲区的初始大小:
1
2
3
4
5
6
7
8
/**
*使用指定的参数创建新的预测值。
*
* @param最小值预期缓冲区大小的包含下限
* @param initial未收到反馈时的初始缓冲区大小
* @param maximum预期缓冲区大小的包含上限
*/
public adapteriversecvbytebuflocator(int最小值、int初始值、int最大值)
对于消息发送,用户通常需要构造字节流并自己编码,例如,通过以下工具类创建消息发送缓冲区:
绝对干货:基于Netty _1226002.png的大众化推送服务的技术要点
5内存池
推送服务器承载大量长链接,每个长链接实际上是一个会话。如果每个会话都保存心跳数据、接收缓冲区、指令集和其他数据结构,并且这些实例随着消息的处理而逐渐消失,这会给服务器带来沉重的垃圾收集压力并消耗大量内存。
最有效的解决策略是使用内存池,每个NioEventLoop线程处理N个链接。在线程内,链接被串行处理。如果首先处理链接A,它将创建接收缓冲区和其他对象。解码完成后,构建的POJO对象被封装为任务,并在后台交付给线程池执行。然后,接收缓冲区将被释放,并且接收缓冲区的创建和释放将针对每个消息接收和处理被重复。如果使用了内存池,则在A链路接收到新数据报后,它会从NioEventLoop的内存池中申请空闲字节数。解码后,它调用release将字节流释放到内存池中,以便后续的B链接继续使用。
在使用内存池优化后,单个NioEventLoop的字节数应用程序和气相色谱的数量从最初的N = 1000000/64 = 15625倍减少到至少0倍(假设每个应用程序都有可用内存)。
让我们以使用Netty4的PooledByteBufAllocator进行气相色谱优化的推特为例(点击查看本文),并评估内存池的效果。结果如下:
垃圾生成速度是原始速度的1/5,而垃圾清理速度快5倍。有了新的内存池机制,网络带宽几乎可以填满。
以前版本的Netty 4的问题如下:每当收到新信息或用户向远程端发送信息时,Netty 3都会创建一个新的堆缓冲区。这意味着每个新的缓冲区将有一个新的字节[容量]。这些缓冲区会造成气相色谱压力和消耗内存带宽。为了安全起见,新的字节数组在分配时将被填充零,这将消耗内存带宽。但是,用零填充的数组很可能会再次用实际数据填充,这将消耗相同的内存带宽。如果Java虚拟机(JVM)提供了一种不用零填充就能创建新字节数组的方法,我们可以将内存带宽消耗减少50%,但目前还没有这种方法。
在Netty 4中,实现了一个新的ByteBuf内存池,这是一个纯Java版本的jemalloc(也被脸书使用)。现在,Netty将不再因为用零填充缓冲区而浪费内存带宽。然而,由于它不依赖于GC,开发人员需要小心内存泄漏。如果您忘记释放处理程序中的缓冲区,内存使用将会无限增加。
默认情况下,Netty不使用内存池,这需要在创建客户端或服务器时指定。代码如下:
1
2
3
4
5
引导b =新引导();
b .集团(集团)
。信道(NioSocketChannel.class)
。选项(通道开发。TCP_NODELAY,真)
。选项(通道开发。分配器,PooledByteBufAllocator。默认)
使用内存池后,应用和释放内存必须成对出现,即retain()和release()应该成对出现,否则会导致内存泄漏。
值得注意的是,如果使用内存池,接收缓冲区ByteBuf必须显式调用ReferenceCountUtil.release(msg),以便在ByteBuf解码完成后释放内存,否则将被视为仍在使用,这将导致内存泄漏。
6小心“原木隐形杀手”
通常,我们都知道我们不能在奈蒂的输入/输出线程上进行无法控制的操作,比如访问数据库和发送电子邮件。然而,有一个常见但非常危险的操作很容易被忽略,那就是日志记录。
通常,在生产环境中,有必要实时打印界面日志,而其他日志处于错误级别。当推送服务中出现输入/输出异常时,将记录异常日志。如果当前磁盘的WIO值很高,写入日志文件的操作可能会被同步阻止,并且阻止时间是不可预测的。这将导致Netty的NioEventLoop线程被阻塞,套接字链接无法及时关闭,其他链接无法读写。
以最常用的log4j为例。尽管它支持异步异步访问,但当日志队列已满时,它会同步阻止业务线程,直到日志队列有空闲位置可用。相关代码如下:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
同步(this.buffer) {
虽然(真)
int PreviousSize = this . buffer . size();
if(PreviousSize < this . BufferSize){
this.buffer.add(事件);
if (previousSize!= 0)中断;
this . buffer . NotifyAll();休息;
}
布尔丢弃=真;
如果((this . blocking)& & &(!线程中断())&(线程当前线程()!//判断这是一个业务线程
{
尝试
{
this . buffer . wait();//阻塞业务线程
丢弃=假;
}
catch(中断例外e)
{
Thread.currentThread()。中断();
}
}
像这样的虫子是极其隐蔽的,高WIO的时间通常很短或偶尔会持续。在测试环境中很难模拟这样的故障,并且很难定位问题。这就要求读者在编写代码时要小心,并注意那些隐藏的地雷。
7传输控制协议参数优化
常用的传输控制协议参数,如在传输控制协议层次上的接收和发送缓冲区大小设置,对应于网络环境下信道开发的二进制二进制二进制和二进制三进制三进制,需要根据推送消息的大小进行合理设置。对于大量的长连接,32K通常是一个不错的选择。
另一种常用的优化方法是软中断,如图所示:如果所有的软中断都运行在相应的cpu0网卡的硬件中断上,那么CPU0总是在处理软中断,而其他的处理器资源此时被浪费了,因为多个软中断不能并行执行。
绝对干货:基于Netty _1226003.png的大众化推送服务的技术要点
在Linux内核2.6.35或更高版本以及启用RPS的情况下,网络通信性能可以提高20%以上。RPS的基本原理是:根据数据包的源地址、目的地址、目的地址和源端口计算哈希值,然后根据该哈希值选择运行在软中断中的cpu。从上层来说,也就是说,每个连接都绑定到cpu,哈希值用于平衡运行在多个CPU上的软中断,从而提高通信性能。
8JVM参数
有两个最重要的参数调整:
-xmx:JVM的最大内存需要根据内存模型来计算,并得到一个相对合理的值;
与气相色谱相关的参数:如新一代与老一代的比例、永久一代、气相色谱策略和新一代面积的比例等。,需要根据特定的场景进行设置和测试,并不断进行优化,以尽可能减少完全垃圾收集的频率。
哇谷im_im即时通讯_私有云_公有云-哇谷云科技官网-JM沟通
IM下载体验 - 哇谷IM-企业云办公IM即时聊天社交系统-JM 沟通下载
IM功能与价格 - 哇谷IM-提供即时通讯IM开发-APP搭建私有化-公有云-私有化云-海外云搭建
新闻动态 - 哇谷IM-即时通讯热门动态博客聊天JM沟通APP
关于哇谷-哇谷IM-提供企业即时通讯IM开发-语音通话-APP搭建私有化-公有云-私有化云-海外云搭建
联系我们 - 哇谷IM-即时通讯IM私有化搭建提供接口与SDK及哇谷云服务