高併發解決方案orleans實踐

星仔007發表於2023-01-10

開具一張圖,展開來聊天。有從單個服務、consul叢集和orleans來展開高併發測試一個小小資料庫併發例項。

首先介紹下場景,建立一個order,同時去product表裡面減掉一個庫存。很簡單的業務但是遇到併發問題在專案中就很頭痛。

由於內容比較多,簡單介紹了。

對外的介面很簡單,客戶端程式碼如下,透過不同的方法去控制併發問題,當然者都是在單個服務跑起來的時候。下面介紹下怎麼去測試。

 [Route("api/[controller]/[action]")]
    [ApiController]
    public class OrderController : ControllerBase
    {
        private readonly IClusterClient orderService;
        public OrderController(IClusterClient orderService)
        {
            this.orderService = orderService;//500請求 併發50 . 100庫存
        }

        [HttpPost]
        public async Task Create([FromServices] Channel<CreateOrderDto> channel, string sku, int count)
        {
            await channel.Writer.WriteAsync(new CreateOrderDto(sku, count));   //高併發高效解決方案  併發測試工具postjson_windows 10s
        }

        [HttpPost]
        public async Task CreateTestLock(string sku, int count)//非阻塞鎖
        {
            await orderService.GetGrain<IOrderGrains>(Random.Shared.Next()).CreateTestLock(sku, count); //執行時間快,庫存少量扣減 10s
        }
        [HttpPost]
        public async Task CreateBlockingLock(string sku, int count)//阻塞鎖
        {
            await orderService.GetGrain<IOrderGrains>(Random.Shared.Next()).CreateBlockingLock(sku, count); //賣不完,時間長 50s
        }
        [HttpPost]
        public async Task CreateDistLock(string sku, int count) //colder元件 分散式鎖
        {
            await orderService.GetGrain<IOrderGrains>(Random.Shared.Next()).CreateDistLock(sku, count); //庫存扣完,時間長 50s
        }

        [HttpPost]
        public async Task CreateNetLock(string sku, int count)   //netlock.net鎖 
        {
            await orderService.GetGrain<IOrderGrains>(Random.Shared.Next()).CreateNetLock(sku, count); //庫存扣完,時間長 50s
        }

        static System.Threading.SpinLock semaphore = new SpinLock(false);
        [HttpPost]
        public async Task CreateLock(string sku, int count)   //賣不完
        {
            bool lockTaken = false;
            try
            {
                semaphore.Enter(ref lockTaken);
                await orderService.GetGrain<IOrderGrains>(0).CreateLock(sku, count);
            }
            finally
            {
                if (lockTaken)
                    semaphore.Exit();
            }
        }

        [HttpPost]
        public  void CreateLocalLock(string sku, int count)  //能賣完
        {
             orderService.GetGrain<IOrderGrains>(Random.Shared.Next()).CreateLocalLock(sku, count); //
        }

        [HttpPost]
        public async Task CreateNoLock(string sku, int count)
        {
           await orderService.GetGrain<IOrderGrains>(Random.Shared.Next()).CreateNoLock(sku, count); //亂的
        }
        [HttpGet]
        public async Task ChangeOrderStatus(int orderId, OrderStatus status)
        {
            switch (status)
            {
                case OrderStatus.Shipment:
                    await orderService.GetGrain<IOrderGrains>(0).Shipment(orderId);
                    break;
                case OrderStatus.Completed:
                    await orderService.GetGrain<IOrderGrains>(0).Completed(orderId);
                    break;
                case OrderStatus.Rejected:
                    await orderService.GetGrain<IOrderGrains>(0).Rejected(orderId);
                    break;
                default:
                    break;
            }
        }
    }

redis必不可少,有用到分散式鎖。

高併發測試工具我用的是postjson_windows,自行百度。很方便。

下面簡單說一下單機測試吧,上面的程式碼註釋不多但是簡單總結了結果。透過測試我個人認為channel是最理想的方式,速度快,而且結果很完美,lock鎖其次。其他結果或多或少有瑕疵。

但是如果部署成叢集會怎樣呢,因為lock鎖和channel是程式內安全的,多開幾個就以為這多開了幾個程式,這樣的話分散式鎖是大家最先想到的,但是結果呢

 

下面簡單介紹下consul叢集,以前有寫過例子,這次拿來真的是很快就跑起來了。第一個就封裝的consul註冊中心和心跳檢查的類庫,第二個是我們的服務,第三個是我們的閘道器。具體程式碼後面有連結。consul自行安裝,配置什麼的預設。

這裡部署了三個服務,把生成的程式碼複製了三份,分別執行。再啟動閘道器。然後按照對應的方法名呼叫併發測試。(這裡複製三份有點麻煩,後面orleans再看新的執行方法)

多開服務

dotnet eapi.dll --urls="http://*:5007" --ip="127.0.0.1" --port="5007" --weight=1
dotnet eapi.dll --urls="http://*:5008" --ip="127.0.0.1" --port="5008" --weight=2
dotnet eapi.dll --urls="http://*:5009" --ip="127.0.0.1" --port="5009" --weight=5


consul啟動走命令,應對閃退
Consul.exe agent -dev

dotnet eapi.gateway.dll --urls="https://*:5000" --ip="127.0.0.1" --port="5000"

簡單說下結果吧,叢集下只有分散式鎖能解決問題,但是隻生成了50條資料,扣減庫存也是50。channel和本地鎖結果都跟想的差不多,是不對的。

 

下面說寫orleans下的表現,首先eapi是服務,gateway是閘道器,interfaces就是閘道器和服務僑鏈的介面。這裡程式碼跟上面的有些差別,就是事務實現方式不一樣。因為執行起來的時候發現每次只能成功第一次,後面一直報錯資料庫連結被佔用,所以我改掉了事務實現方式。因為前面的沒這問題所以很納悶,實際專案中也是感覺到orleans對資料庫的連結管理是有點問題的,所以不去深究。

下面跑起來看看orleans怎麼樣,dotnet命令我們是可以指定appsettings.json的配置的,所以配置檔案指定好OrleansOptions,只需要在生產程式碼目錄orleans多例項服務\eapi\bin\Debug\net7.0下面執行者三個服務命令就開了三服務,下面執行下閘道器。

{
  "urls": "https://*:5005",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "distributedLock": {
    "LockType": "Redis",
    "RedisEndPoints": [ "127.0.0.1:6379" ]
  },
  "OrleansOptions": {
    "GatewayPort": 30001,
    "SiloPort": 1112
  }


}
  builder.Host.ConfigureDistributedLockDefaults();
            var gport = int.Parse(builder.Configuration["OrleansOptions:GatewayPort"]);
            var sport = int.Parse(builder.Configuration["OrleansOptions:SiloPort"]);
            builder.Host.UseOrleans(b => b.UseLocalhostClustering(sport, gport));

 

先說下遇到的問題, ReposioryBase<T>下面的程式碼原來返回的是iquerable<T> 這是大佬推薦的做法返回iquerable,結果到多服務就出現佔用,我把事務換成現在的寫法發現還是徒勞,直接查詢出來ToListAsync就可以了。

public async Task<IReadOnlyList<T>> FindByCondition(Expression<Func<T, bool>> expression)
{
return await DbSet.Where(expression).ToListAsync();
}

orderService下面的程式碼

var product = (await _productRepository.FindByCondition(x => x.Sku.Equals(sku))).SingleOrDefault(); //執行一尺order建立後此處連結就不釋放了。

if (product == null || product.Count < count)
{
_logger.LogInformation("庫存不足,稍後重試");
return;
}
else
{
product.Count -= count;
}

下面的問題就是負載的問題,單個服務的話GetGrain<IOrderGrains>(0)沒問題,多個服務這樣的話所有請求都只會落到一個服務上,改成隨機的三個服務就都可以了。

 internal class NotificationDispatcher : BackgroundService
    {
        private readonly ILogger<NotificationDispatcher> logger;
        private readonly Channel<CreateOrderDto> channel;
        private readonly IServiceProvider serviceProvider;
        public NotificationDispatcher( ILogger<NotificationDispatcher> logger, Channel<CreateOrderDto> channel, IServiceProvider serviceProvider)
        {
            this.logger = logger;
            this.channel = channel;
            this.serviceProvider = serviceProvider;
        }

        protected async override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!channel.Reader.Completion.IsCompleted)
            {
                var createOrderDto = await channel.Reader.ReadAsync();
                try
                {
                    using (var scope = serviceProvider.CreateScope())
                    {
                        var client = scope.ServiceProvider.GetRequiredService<IClusterClient>();
                        var orderService = client.GetGrain<IOrderGrains>(Random.Shared.Next()); //設定為0導致指揮獲取一個服務,隨機的話就是多服務負載
                        //var orderService = client.GetGrain<IOrderGrains>(0);
                        await orderService.Create(createOrderDto.sku, createOrderDto.count);
                    }
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, "notification failed");
                }

            }
        }
    }

結果發現channel竟然很迅速的完成500次請求,50併發數,只用了8秒鐘。資料庫庫存扣減完畢,order生成100條。有去了解過orleans,理解它的設計思路。但是說不上來具體的。

這是啟動的三兄弟:

下面只要啟動了閘道器就會連結上服務,這裡需要啟動先後順序,先服務後閘道器要不然閘道器會起不來。啟動後服務掉線或者關掉都不會再影響閘道器,自動註冊和發現。

透過swagger試了一條資料,服務正常,服務被服務埠5008,閘道器埠30002的執行了。

 

併發測試結果,看sql註釋有提前把庫存還原,order清空:

 

看看是哪些服務拿到了請求,因為500次請求只有100個庫存所以後面就是一直庫存不足,而且每個服務都參與了處理。

 

個人推薦用orleans做微服務叢集,channel處理防止併發。其次是分散式鎖redlock.net。

下面redlock.net執行結果,orleans叢集上b表現還不錯。在單機和consul只能完成50條。

consul不知道為什麼特別慢,而且和單機一樣遇到有些鎖完全跑不完。對於channel和本地鎖跑到consul上結果是亂的。

只有orlans表現相當突出。

 


結論,單機用本地鎖或者channel。 consul和orleans我選orleans。 consul下只能分散式鎖。orleans用channel就夠了。

原始碼:liuzhixin405/orleans-consul-cluster: orleans和consul併發測試 (github.com)

 

相關文章