• 电商秒杀系统:单机服务器性能压测+链路监控+优化(数据库连接池、缓存(字典、内存)、消息队列)


    说明

    使用压测工具,配合链路监控,来查看程序的性能瓶颈,然后继续优化,继续压测。

    因为本机硬件配置一般,压测工具、数据库、SkyWalking、Elasticsearch对性能影响大,后面的测试都是在阿里云服务上完成。

    常见压测工具

    • Apache JMeter:java 开发,使用比较简单
    • ApacheBench (ab): c语言
    • Gatling:scala 语言,使用命令脚本
    • k6: js开发
    • Locust:python开发 单台测试 使用命令脚本
    • Netling: c#开发, 测试起来比较简单
    • Vegeta: go语言 ,命令行工具和一个开发库

    备注:如果要非常精准的压测,建议在服务器使用命令行的压测工具来压测

    服务器配置

    阿里云

    1、cpu 8核

    2、内存 16G

    3、硬盘 100G

    4、带宽 10M

    环境准备

    链路监控 SkyWalking

    项目中添加使用SkyWalking

    安装SkyWalking

    安装Elasticsearch 

    压测工具 JMeter

    参考:JMeter使用教程

    需要依赖Java

    • 安装java jdk1.8及以上版本
    • 配置java环境变量

    JMeter

    • 官网下载后,打开bin文件夹下的jmeter.bat
    • 线程组:添加=》线程(用户)=》线程组
      • HTTP请求:添加=》取样器=》HTTP请求
      • 聚合报告:添加=》监听器=》去和报告
      • 察看结果树:添加=》监听器=》察看结果树
      • 图形结果树:添加=》监听器=》图形结果树
      • HTTP消息头管理器:添加=》配置元件=》HTTP消息头管理器,然后添加UserId和UserName来配置token和用户,避免登录认证
        • 也可以另外使【用户参数】来登录,不过每次都需要登录会造成压测不准确,所以建议用【HTTP消息头管理器】

    第一阶段优化:consul服务发现使用缓存字典+数据库连接池AddDbContextPool-->每秒170

    优化前

    • 多次压测和链路监控发现,1s能处理100个请求左右。
    • 通过链路监控发现【秒杀聚合服务】耗时多,通过排查是consul注册时耗时非常多。

    优化后

    • 结果:压测后发现1s能处理170个请求

    数据库连接池优化

    • 用到AddDbContext的服务都可以使用连接池,如OrderServices服务和Seckill服务的Startup类的AddDbContext改为AddDbContextPool

    consul服务发现缓存优化

    consul服务发现组件位置:Projects.CoresRegistryConsulConsulServiceDiscovery.cs

    优化前代码:是继承IServiceDiscovery类

    using Consul;
    using Microsoft.Extensions.Options;
    using RuanMou.Projects.Commons.Exceptions;
    using RuanMou.Projects.Cores.Registry.Options;
    using System;
    using System.Collections.Generic;
    using System.Net;
    
    namespace RuanMou.Projects.Cores.Registry
    {
        /// <summary>
        /// consul服务发现实现
        /// </summary>
        public class ConsulServiceDiscovery : IServiceDiscovery
        {
            private readonly ServiceDiscoveryOptions serviceDiscoveryOptions;
            public ConsulServiceDiscovery(IOptions<ServiceDiscoveryOptions> options)
            {
                this.serviceDiscoveryOptions = options.Value;
            }
    
            public List<ServiceNode> Discovery(string serviceName)
            {
                // 1.2、从远程服务器取
                CatalogService[] queryResult = RemoteDiscovery(serviceName);
    
                var list = new List<ServiceNode>();
                foreach (var service in queryResult)
                {
                    list.Add(new ServiceNode { Url = service.ServiceAddress + ":" + service.ServicePort });
                }
    
                return list;
            }
    
            private CatalogService[] RemoteDiscovery(string serviceName)
            {
                // 1、创建consul客户端连接
                var consulClient = new ConsulClient(configuration =>
                {
                    //1.1 建立客户端和服务端连接
                    configuration.Address = new Uri(serviceDiscoveryOptions.DiscoveryAddress);
                });
    
                // 2、consul查询服务,根据具体的服务名称查询
                var queryResult = consulClient.Catalog.Service(serviceName).Result;
                // 3、判断请求是否失败
                if (!queryResult.StatusCode.Equals(HttpStatusCode.OK))
                {
                    throw new FrameException($"consul连接失败:{queryResult.StatusCode}");
                }
    
                return queryResult.Response;
            }
        }
    }
    View Code
    using System.Collections.Generic;
    using System.Threading.Tasks;
    
    namespace RuanMou.Projects.Cores.Registry
    {
        /// <summary>
        /// 服务发现
        /// </summary>
        public interface IServiceDiscovery
        {
            /// <summary>
            /// 服务发现
            /// </summary>
            /// <param name="serviceName">服务名称</param>
            /// <returns></returns>
            List<ServiceNode> Discovery(string serviceName);
        }
    }
    View Code

    优化后代码:继承缓存类AbstractServiceDiscovery类,且把Discovery方法放到里面,把从远程获取改为从缓存获取

    using Consul;
    using Microsoft.Extensions.Options;
    using RuanMou.Projects.Commons.Exceptions;
    using RuanMou.Projects.Cores.Registry.Options;
    using System;
    using System.Collections.Generic;
    using System.Net;
    
    namespace RuanMou.Projects.Cores.Registry
    {
        /// <summary>
        /// consul服务发现实现
        /// </summary>
        public class ConsulServiceDiscovery : AbstractServiceDiscovery
        {
            public ConsulServiceDiscovery(IOptions<ServiceDiscoveryOptions> options) : base(options)
            {
            }
    
            protected override CatalogService[] RemoteDiscovery(string serviceName)
            {
                // 1、创建consul客户端连接 2s 1、使用单例全局共享 2、使用数据缓存(进程:字典,集合) 3、使用连接池
                var consulClient = new ConsulClient(configuration =>
                {
                    //1.1 建立客户端和服务端连接
                    configuration.Address = new Uri(serviceDiscoveryOptions.DiscoveryAddress);
                });
    
                // 2、consul查询服务,根据具体的服务名称查询
                var queryResult = consulClient.Catalog.Service(serviceName).Result;
                // 3、判断请求是否失败
                if (!queryResult.StatusCode.Equals(HttpStatusCode.OK))
                {
                    throw new FrameException($"consul连接失败:{queryResult.StatusCode}");
                }
    
                return queryResult.Response;
            }
    
        }
    }
    View Code

    其实就是一个字典类型的字段,服务发现时如果字典中存在就直接从字典中获取,没有时再从远程获取,获取后再存到字典中,以达到缓存重用的效果

    AbstractServiceDiscovery类代码如下:

    using Consul;
    using Microsoft.Extensions.Options;
    using RuanMou.Projects.Commons.Exceptions;
    using RuanMou.Projects.Cores.Registry.Options;
    using System;
    using System.Collections.Generic;
    using System.Net;
    using System.Threading.Tasks;
    
    namespace RuanMou.Projects.Cores.Registry
    {
        /// <summary>
        /// 抽象服务发现,主要是缓存功能
        /// </summary>
        public abstract class AbstractServiceDiscovery : IServiceDiscovery
        {
            // 字典缓存
            private readonly Dictionary<string, List<ServiceNode>> CacheConsulResult = new Dictionary<string, List<ServiceNode>>();
            protected readonly ServiceDiscoveryOptions serviceDiscoveryOptions;
            public AbstractServiceDiscovery(IOptions<ServiceDiscoveryOptions> options)
            {
                this.serviceDiscoveryOptions = options.Value;
    
                // 1、创建consul客户端连接
                var consulClient = new ConsulClient(configuration =>
                {
                    //1.1 建立客户端和服务端连接
                    configuration.Address = new Uri(serviceDiscoveryOptions.DiscoveryAddress);
                });
    
                // 2、consul 先查询服务
                var queryResult = consulClient.Catalog.Services().Result;
                if (!queryResult.StatusCode.Equals(HttpStatusCode.OK))
                {
                    throw new FrameException($"consul连接失败:{queryResult.StatusCode}");
                }
    
                // 3、获取服务下的所有实例
                foreach (var item in queryResult.Response)
                {
                    QueryResult<CatalogService[]> result = consulClient.Catalog.Service(item.Key).Result;
                    if (!queryResult.StatusCode.Equals(HttpStatusCode.OK))
                    {
                        throw new FrameException($"consul连接失败:{queryResult.StatusCode}");
                    }
                    var list = new List<ServiceNode>();
                    foreach (var service in result.Response)
                    {
                        list.Add(new ServiceNode { Url = service.ServiceAddress + ":" + service.ServicePort });
                    }
                    CacheConsulResult.Add(item.Key, list);
                }
            }
    
    
            public List<ServiceNode> Discovery(string serviceName)
            {
                // 1、从缓存中查询consulj结果
                if (CacheConsulResult.ContainsKey(serviceName))
                {
                    return CacheConsulResult[serviceName];
                }
                else
                {
                    // 1.2、从远程服务器取
                    CatalogService[] queryResult = RemoteDiscovery(serviceName);
    
                    var list = new List<ServiceNode>();
                    foreach (var service in queryResult)
                    {
                        list.Add(new ServiceNode { Url = service.ServiceAddress + ":" + service.ServicePort });
                    }
    
                    // 1.3 将结果添加到缓存
                    CacheConsulResult.Add(serviceName, list);
    
                    return list;
                }
            }
    
            /// <summary>
            /// 远程服务发现
            /// </summary>
            /// <param name="serviceName"></param>
            /// <returns></returns>
            protected abstract CatalogService[] RemoteDiscovery(string serviceName);
        }
    }
    View Code 

    时序图

    第二阶段优化:秒杀库内存存缓存优化-->每秒340

    参考:ASP.NET Core 中的缓存内存

    减少网络IO:秒杀库存本来是在秒杀服务中的,但是为了减少网络IO,然后直接把放到秒杀聚合服务中的缓存中

    使用内存缓存 MemoryCache ,减少操作数据库,把秒杀库存缓存放在秒杀聚合服务中:SeckillAggregateServices Caches  SeckillStock  SeckillStockCache.cs,代码如下:

    using Microsoft.Extensions.Caching.Memory;
    using RuanMou.Projects.SeckillAggregateServices.Models.SeckillService;
    using RuanMou.Projects.SeckillAggregateServices.Services;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace RuanMou.Projects.SeckillAggregateServices.Caches.SeckillStock
    {
        /// <summary>
        /// 秒杀库存缓存
        /// </summary>
        public class SeckillStockCache : ISeckillStockCache
        {
            /// <summary>
            /// 秒杀微服务客户端
            /// </summary>
            private readonly ISeckillsClient seckillsClient;
            /// <summary>
            /// 内存缓存
            /// </summary>
            private readonly IMemoryCache memoryCache;
    
            public SeckillStockCache(ISeckillsClient seckillsClient, IMemoryCache memoryCache)
            {
                this.seckillsClient = seckillsClient;
                this.memoryCache = memoryCache;
            }
    
            public int GetSeckillStocks(int ProductId)
            {
                Seckill seckillStock = memoryCache.Get<Seckill>(ProductId);
                return seckillStock.SeckillStock;
            }
    
            /// <summary>
            /// 秒杀库存加载到MemoryCache中
            /// </summary>
            public void SkillStockToCache()
            {
                // 1、查询所有秒杀活动
                List<Seckill> seckills = seckillsClient.GetSeckills();
    
                // 2、存储秒杀库存到缓存
                foreach (var seckill in seckills)
                {
                    // 2.1 将所有秒杀活动存储到缓存中
                    memoryCache.Set<Seckill>(seckill.ProductId, seckill);
                }
            }
    
            public void SubtractSeckillStock(int ProductId, int ProductCount)
            {
                // 1、获取秒杀活动信息
                Seckill seckill = memoryCache.Get<Seckill>(ProductId);
    
                // 2、扣减库存
                int SeckillStock = seckill.SeckillStock;
                SeckillStock = seckill.SeckillStock - ProductCount;
                seckill.SeckillStock = SeckillStock;
    
                // 3、更新库存
                memoryCache.Set<Seckill>(seckill.ProductId, seckill);
    
                Seckill seckill2 = memoryCache.Get<Seckill>(ProductId);
            }
        }
    }
    View Code

    SeckillStockCacheHostedService:服务启动时,加载秒杀库存到缓存

    using Microsoft.Extensions.Hosting;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace RuanMou.Projects.SeckillAggregateServices.Caches.SeckillStock
    {
        /// <summary>
        /// 服务启动时,加载秒杀库存到缓存
        /// </summary>
        public class SeckillStockCacheHostedService : IHostedService
        {
            private readonly ISeckillStockCache seckillStockCache;
    
            public SeckillStockCacheHostedService(ISeckillStockCache seckillStockCache)
            {
                this.seckillStockCache = seckillStockCache;
            }
    
            /// <summary>
            /// 加载秒杀库存缓存
            /// </summary>
            /// <param name="cancellationToken"></param>
            /// <returns></returns>
            public Task StartAsync(CancellationToken cancellationToken)
            {
                Console.WriteLine("加载秒杀库存到缓存中");
                return Task.Run(() => seckillStockCache.SkillStockToCache());
            }
    
            public Task StopAsync(CancellationToken cancellationToken)
            {
                return Task.CompletedTask;
            }
        }
    }
    View Code

    在秒杀聚合服务的订单控制器OrderController中调用秒杀缓存

    时序图:

    第三阶段:上云服务器+流量削峰(使用消息队列CAP. RabbitMQ生成订单)-->每秒3000

    使用CAP框架的消息队列

    就是把超过服务器承受的请求存放在消息队列中,当前先处理一部分,然后再分批处理消息队列中的请求

    异步:使用消息队列Rabbitmq发送消息优化,不直接操作数据库。 在聚合服务上,订单控制器直接把请求发送给Rabbitmq

    位置:SeckillAggregateServices  Controllers  OrderController.cs,代码如下:

    RabbitMQ发布订阅模式:聚合服务发送消息给RabbitMQ直接返回,RabbitMQ发送消息给订单服务服务,聚合服务和订单服务通过RabbitMQ解耦,不直接调用

    • 发布:秒杀聚合服务的订单控制器中发布消息给RabbitMQ后直接返回,不用再等请求执行完毕后再返回。对应数据库发布表:Published
      //在Startup中添加CAP
                  services.AddCap(x =>
                  {
                      // 8.1 使用内存存储消息(消息发送失败处理)
                      x.UseInMemoryStorage();
      
                      // 8.4 使用RabbitMQ进行事件中心处理
                      x.UseRabbitMQ(rb =>
                      {
                          rb.HostName = "10.96.0.3";// K8s集群service
                          rb.UserName = "guest";
                          rb.Password = "guest";
                          rb.Port = 5672;
                          rb.VirtualHost = "/";
                      });
      
                      // 8.5添加cap后台监控页面(人工处理)
                      x.UseDashboard();
                  });
      
      
      //在订单控制器中发布消息给RabbitMQ
      using DotNetCore.CAP;
      
              private readonly ICapPublisher capPublisher;
      
              /// <summary>
              /// 3.1 发送创建订单消息
              /// </summary>
              /// <param name="ProductId"></param>
              /// <param name="ProductCount"></param>
              private void SendOrderCreateMessage(int userId, string orderSn, OrderPo orderPo)
              {
                  var configuration = new MapperConfiguration(cfg =>
                  {
                      cfg.CreateMap<OrderPo, Order>();
                  });
      
                  IMapper mapper = configuration.CreateMapper();
      
                  // 2、设置订单
                  Order order = mapper.Map<OrderPo, Order>(orderPo);
                  order.OrderSn = orderSn;
                  order.OrderType = "1";// 订单类型(1、为秒杀订单)
                  order.UserId = userId;
      
                  // 3、设置订单项
                  OrderItem orderItem = new OrderItem();
                  orderItem.ItemCount = orderPo.ProductCount;
                  orderItem.ItemPrice = orderPo.OrderTotalPrice;
                  orderItem.ItemTotalPrice = orderPo.OrderTotalPrice;
                  orderItem.ProductUrl = orderPo.ProductUrl;
                  orderItem.ProductId = orderPo.ProductId;
                  orderItem.OrderSn = orderSn;
      
                  List<OrderItem> orderItems = new List<OrderItem>();
                  orderItems.Add(orderItem);
                  order.OrderItems = orderItems;
      
                  // 4、发送订单消息
                  capPublisher.Publish<Order>("seckill.order", order);
              }
      View Code
    • 订阅:订单服务中的订单控制器中的方法头上通过过滤器订阅,然后是RabbitMQ调用订单服务。对应数据库订阅表:Received
      //在Stratup中添加cap
                  services.AddCap(x =>
                  {
                      // 7.1 使用EntityFramework进行存储操作
                      x.UseEntityFramework<OrderContext>();
                      // 7.2 使用sqlserver进行事务处理
                      x.UseMySql(Configuration.GetConnectionString("DefaultConnection"));
      
                      // 7.3 使用RabbitMQ进行事件中心处理
                      x.UseRabbitMQ(rb =>
                      {
                          rb.HostName = "localhost"; // 本地主机
                          rb.HostName = "10.96.0.3";// docker集群service
                          rb.UserName = "guest";
                          rb.Password = "guest";
                          rb.Port = 5672;
                          rb.VirtualHost = "/";
                      });
      
      
      //在订单控制器中订阅RabbitMQ消息
      using DotNetCore.CAP;
      
              private readonly IOrderService OrderService;
      
              /// <summary>
              /// 创建订单
              /// </summary>
              /// <param name="Order"></param>
              /// <returns></returns>
              [NonAction]
              [CapSubscribe("seckill.order")]
              public ActionResult<Order> CapPostOrder(Order Order)
              {
                  // 1、创建订单
                  Order.Createtime = new DateTime();
                  OrderService.Create(Order);
                  return CreatedAtAction("GetOrder", new { id = Order.Id }, Order);
              }
      View Code

    问题:发布和订阅的地址是一样,但是数据库不一样,难道两个数据库?  

    消息队列存储在内存中

    消息队列存在数据库中,消息队列不会丢失,但是性能不够好,存在数据库是是使用异步吗?

    消息队列存在内存中性能好,但是如果断机,会总成消息丢失,可以使用分布式事务保证数据完整,还可以使用后面说到的溟等

    在聚合服务的Startup.cs中使用内存存储消息队列,代码是:x.UseInMemoryStorage();

    时序图

  • 相关阅读:
    不同等级数据源的多级缓冲环的生成
    从DataTable到List<Model>(C#.net)
    string与char之间的转换以及如何获得string所占位数
    AndroidKotlin
    Android高级POS开发
    IOS 应用开发
    Android切屏的处理
    Android POS开发
    Android深入理解JNI
    Android的IPC机制Binder
  • 原文地址:https://www.cnblogs.com/qingyunye/p/14249014.html
Copyright © 2020-2023  润新知