dotnet core使用IO合并技巧轻松实现千万级消息推送

之前讲述过多路复用实现单服百万级别RPS吞吐,但在文中有一点是没有说的就是消息IO合并,如果缺少了消息IO合并即使怎样多路复用也很难达到百万级别的请求响毕竟所有应用层面的网络IO读写都是非常损耗性能的(需要硬件配置很高的服务器)。这一章主要讲述的是IO合并的应用,并通过这个特性实现普通单服务千万级别的消息推送测试。

什么是消息IO合并

所谓的消息IO合并即是由原来一个消息对应一个网络读写设计成多个消息共享一个网络读写。那这样的设计到底会带来多大的性能提升,最简单的对比场就是每次执行1条SQL执行1万次和直接批执行1万条SQL的差别,相信做过的朋友一定非常清楚其性能提升的幅度。那在网络通讯中如何设计才能让多个消息进行IO合并呢?作者在实际实践中的方式有两种:1)通过定时器把队列中的所有消息定期合并发送,2)通过一个状态机归递消息队列,一旦队列存在消息一次过合并发送。定时器这种比较损耗性能,在连接量大的情况存在延时间相互影响;对于后者则比较好控制很多也不存在延时性,原理发送消息进队列后和网络发送完成再回到状态机检测消息队列状态即可。 

消息推送相对于请求响应来说相还是简单很多的,毕竟消息推送是单向并不需要有高效的响应机制。不过对于普通服务器间实现千万级的消息推送还是需要做些规划,毕竟是需要在有限的IO读写量的情况来达到这么大规模的消息处理。还有这么大量的消息序列化和反序列化也是一非常损耗性能的事情,所以这次实践并没有使用Protobuf,而是采用自定义序列化。测试的通讯组件选择Beetlex因为它具备了自动消息合并能力,并配合高效的多复路用机制在服务之间进行千万级别的消息推变得简单。

测试简述

这一次测试主要是向服务端推着一个简单的订单信息,由客户每次生成不同的订单信息推送给服务端,服务器接收订单消息后进行统计,并计算每秒接收的订单数量。

消息结构

    public class Order
    {
        public long ID;
        public string Product;
        public int Quantity;
        public double Price;
        public double Total;
    }

创建订单

private static long mId;
private static string[] mProducts = new string[] { "Apple", "Orange", "Banana", "Citrus", "Mango" };
private static int[] mQuantity = new int[] { 3, 10, 20, 23, 6, 9, 21 };
private static double[] mPrice = new double[] { 2.3, 1.6, 3.2, 4.6, 20, 4 };
public static Order CreateOrder()
{
      Order order = new Order();
      order.ID = System.Threading.Interlocked.Increment(ref mId);
      order.Product = mProducts[order.ID % mPrice.Length];
      order.Quantity = mQuantity[order.ID % mQuantity.Length];
      order.Price = mPrice[order.ID % mPrice.Length];
      order.Total = order.Quantity * order.Price;
      return order;
 }

基于测试资源有限,这次的测试并没像之前跑PRS那样采用Protobuf,因为这量的对象处理量实在太大,测试的硬件环境不变所以采了自定义的序列化方式,具体可以参考源代码。

接收端代码

        public override void SessionPacketDecodeCompleted(IServer server, PacketDecodeCompletedEventArgs e)
        {
            PushMessages.Order order = (PushMessages.Order)e.Message;
            if (order.ID > MaxOrerID)
                MaxOrerID = order.ID;
            System.Threading.Interlocked.Increment(ref Count);
        }

由于是接收推送的消息,服务端接收消息后统计相关数量即可完成,对于之前的RPS测试所需处理的东西就少很多了。

推送端压测代码

        public void Run()
        {
            foreach (var item in mClients)
                System.Threading.ThreadPool.QueueUserWorkItem(OnRun, item);
        }
        private void OnRun(object state)
        {
            AsyncTcpClient item = (AsyncTcpClient)state;
            while (true)
            {
                Order order = OrderFactory.CreateOrder();
                item.Send(order);
                if (order.ID > MaxOrerID)
                    MaxOrerID = order.ID;
                System.Threading.Interlocked.Increment(ref Count);
                if (item.Count > 2000)
                    System.Threading.Thread.Sleep(1);
            }
        }

为了防止压爆连接内部的消息队列,压测端当连接队列超过2000个消息的时候停止一下。由于采用了消息合并机制所以并不需要太多连接,在整个测试过程中开启了三个压测实例,每个实例使用5个连接,换句话说BeetleX通过15个连接,实现千万级消息的推送能力。

测试服务器资源

这次测试使用了两家云服务器,第一家名字就不说了,开启了V16核的虚拟服务器,内部带宽6G和100万pps,结果实际压测2G带宽就压不上去了,刚开始以为是linux系统要配置问题,换了windows系统试一下还是不行……,最终还是换回了阿里云测,在v12核的虚拟服务器上顺利完成了这一次测试。

服务端: v12核,24G内存,操作系统ubuntu16.04 一台,内网最大带宽4Gb.

压测端: v12核,24G内存,操作系统ubuntu16.04,内网最大带宽4Gb, 两台(主要测试方式有些暴力一台无法达到压测目标)

测试结果

二台压测机共开启了3个实例,每个实例5个连接,每个连接应用层处理的buffer 32k;整个测结果消息推送量达到了1000万个/秒。服务端记录接收IO每秒15000次,平均每次receive得到的消息大概在600个左右。以下是测试情况的截图:

服务程序统计情况

服务端CPU情况

网络使用情况

下功测试代码

posted @ 2018-10-29 10:54 smark 阅读(...) 评论(...) 编辑 收藏