1. 程式人生 > >【.net core】電商平臺升級之微服務架構應用實戰(core-grpc)

【.net core】電商平臺升級之微服務架構應用實戰(core-grpc)

## 一、前言 這篇文章本來是繼續分享`IdentityServer4` 的相關文章,由於之前有博友問我關於`微服務`相關的問題,我就先跳過`IdentityServer4`的分享,進行`微服務`相關的技術學習和分享。`微服務`在我的分享目錄裡面是放到四月份開始系列文章分享的,這裡就先穿越下,提前安排`微服務`應用的開篇文章 `電商系統升級之微服務架構的應用`。 本部落格以及公眾號**堅持以架構的思維來分享技術,不僅僅是單純的分享怎麼使用的Demo**。 ## 二、場景 先來回顧下我上篇文章 [Asp.Net Core 中IdentityServer4 授權中心之應用實戰](https://www.cnblogs.com/jlion/p/12447081.html) 中,電商架構由`單體式架構`拆分升級到`多閘道器架構` #### 升級之前 ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200310110017008-1660735876.png) #### 升級之後: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200310111100212-1434441217.png) 然而升級之後問題又來了,由於之前增加了代理商業務並且把`授權中心`和`支付閘道器`單獨拆出來了,這使得公司的業務訂單量翻了幾十倍,這個時候整個電商系統達到了瓶頸,如果再不找解決方案系統又得宕機了。 ### 2.1 問題及解決方案 經過技術的調研及問題分析,導致這個瓶頸的問題主要有以下幾個原因,只需要把下面問題解決就可以得到很大的效能提升 - 每天的訂單量暴增,導致訂單資料太大,然而整個電商系統資料儲存在一個數據庫中,並且是`單表`、`單資料庫`(未進行讀寫分離),以致於訂單資料持續暴增。 - 相關業務需要依賴訂單查詢,訂單資料查詢慢以至於拖垮資料庫 - 整個電商系統連線數達到瓶頸(已經分散式部署,在多加伺服器會損耗更多的經費而達不到最佳價效比) 為了一勞永逸的解決以上問題,經過技術的調研,決定對訂單業務做如下升級改造: - 拆分獨立的訂單微服務(本章節著重分享) - 使用`ES`進行資料遷移(按年進行劃分,並且進行讀寫分離,這裡就不著重講,下次來跟大家一起學習和分享) - 增加`分散式快取` (也不是本次的重點,後續再來跟大家學習和分享) 經過升級後的架構圖如下: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200314122846964-397634622.jpg) 架構圖說明: - 右邊同一顏色的代表還是原先電商系統的`單體式架構`,為拆分的單體架構業務,其中在業務處理上夾雜了一層分散式快取的處理 - 左邊的是微服務的架構,是這次升級拆分後的架構,其中資料庫也已經從原有的資料庫拆分並且資料遷移到了ES叢集中,並進行了讀寫分離。 - 訂單服務可以隨意擴容成分散式服務,通過一些工具動態擴充套件服務及伺服器的支援。 - 右邊的業務後續也可以進行拆分,拆分成不同的業務服務。 - 後續升級還可以考慮訊息佇列等相關方面,架構圖中未構思(後續再來分享升級用到的相關技術,這裡還是迴歸到本文的核心`微服務`) ## 三、微服務概述 `微服務`的相關概念我就不多說了,以下就先簡單概況下微服務帶來的利和弊。 ### 3.1 微服務的優勢 - 使大型的複雜應用程式可以持續交付和持續部署:持續交付和持續部署是DevOps的一部分,DevOps是一套快速、頻繁、可靠的軟體交付實踐。高效的DevOps組織通常將軟體部署到生產環境時面臨更少的問題和故障。DevOps工具有`Docker`、`Kubernets`、`Jenkins`、`Git`等。 - 每個服務相對較小並容易維護:微服務架構相比單體應用要小的多,開發者理解服務中的邏輯程式碼更容易。程式碼庫小,打包,啟動服務速度也快。 - 服務可以獨立部署:每個服務都可以獨立於其他服務進行部署 - 服務可以獨立擴充套件:服務可以獨立擴充套件,不論是採用X軸擴充套件的例項克隆,還是Z軸的流量分割槽方式。此外每個服務都可以部署到適合它們需求的硬體之上 - 微服務架構可以實現團隊的自治:可以根據服務來把開發團隊拆分。每個團隊都有自己負責的微服務,而不用關心不屬於他們負責的服務。 - 更容易實驗和採納新的技術:最後,微服務可以消除對某個技術棧的長期依賴。因為服務更小,使用更換的程式語言和技術來重寫一項服務變得有可能,這也意味著,對一項新技術嘗試失敗後,可以直接丟棄這部分工作而不至於給整個應用帶來失敗的風險。 - 更好的容錯性:微服務架構也可以實現更換的故障隔離。例如,某個服務引發的致命錯誤,不會影響其他服務。其他服務仍然正常執行。 - 服務可以獨立擴容:對於整個架構來說,可以隨意選擇相關業務進行擴容和負載,通過相關技術工具動態進行隨意擴容 ### 3.2 微服務的劣勢 - 服務拆分和定義是一項挑戰:採用微服務架構首當其衝的問題,就是根本沒有一個具體的、良好定義的演算法可以完成服務的拆分工作。與軟體開發一樣,服務的拆分和定義更像一門藝術。更糟糕的是,如果對系統的服務拆分出現了偏差,很有可能會構建出一個分散式的單體應用;一個包含了一大堆互相之間緊耦合的服務,卻又必須部署在一起的所謂分散式系統。這將會把單體架構和微服務架構兩者的弊端集於一身。 - 分散式系統帶來的各種複雜性、使開發、測試和部署變得更困難:使用微服務架構的另一個問題是開發人員必須處理建立分散式系統的額外複雜性。服務必須是程序間通訊。這比簡單的方法呼叫要複雜的多。 - 當部署跨越多個服務的功能時需要謹慎地協調更多的開發團隊:使用微服務架構的另外一項挑戰在於當部署跨越多個服務的功能時需要謹慎地協調更多開發團隊。必須制定一個釋出計劃,把服務按照依賴關係進行排序。這跟單體架構下部署多個元件的方式截然不同。 - 開發者需要思考到底應該在應用的什麼階段使用微服務架構:使用微服務架構的另一個問題是決定在應用程式生命週期的哪個階段開始使用這種架構。 - 跨服務資料的問題:在單體應用中,所有的資料都在一個數據庫中,而在微服務架構中,每個服務都有自己的資料庫,想要獲取,操作其他服務的資料,只能通過該服務提供API進行呼叫,這樣就帶來一個問題,程序通訊的問題,如果涉及到事務,那麼還需要使用Saga來管理事務,增加了開發的難度。 ### 3.3 微服務拆分原則 說到`單體架構`拆分,那也不是隨意拆分,是要有一定的原則,拆分的好是優勢,拆分的不好是混亂。以下是我查閱資料以及我的經驗總結出來的拆分原則 - 1、單一職責、高內聚低耦合 - 2、微服務粒度適中 - 3、考慮團隊結構 - 4、以業務模型切入 - 5、演進式拆分 - 6、避免環形依賴與雙向依賴 - 7、DDD(可以考慮使用`領域驅動設計`去進行底層服務的設計,後續會單獨分析該設計的相關文章) ## 四、微服務實戰 好了,到這裡大家已經對微服務有了一定的理解,就不繼續詳細概述相關理念的東西,下面來直接擼程式碼,讓大家熟悉微服務的應用。這裡我使用 `莫堇蕈` 在github 上開源的**微服務框架**,框架原始碼地址 :https://github.com/overtly/core-grpc (**我這裡強烈推薦該框架,目前已經比較成熟的用於公司生產環境**) **為了更好的維護開源專案以及技術交流,特意建立了一個交流群,群號:1083147206 有興趣者開源加入交流** ### 4.1 `core-grpc` 微服務框架的優勢: - 整合Consul 實現服務發現和註冊以及健康檢查等機制 - 實時監聽服務狀態 - 多節點 輪詢機制 - 故障轉移,拉入黑名單 - 支援.Net Core 和Framework 兩種框架 - 實現基於Grpc的微服務 - 部署支援環境變數 ### 4.2 實戰 #### 建立`Jlion.NetCore.OrderService` 訂單微服務 我們用`vs2019` 建立控制檯應用程式 選擇框架.Net Core 3.1 命名為`Jlion.NetCore.OrderService` 後面簡稱`訂單服務`,建立完後我們通過`nuget`包引入 `core-grpc`微服務框架,如下圖: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200314140932240-924518816.png) 目前`core-grpc`微服務框架,最新正式釋出版本是 1.0.3 引用了`core-grpc` 後我們還需要安裝一個工具`VS RPC Menu`,這個工具也是大神免費提供的,圖片如下: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200314142038399-880772553.png) 由於微軟官方下載比較慢,我這裡共享到 百度網盤,百度網盤下載地址如下: > 連結: https://pan.baidu.com/s/1twpmA4_aErrsg-m0ICmOPw 提取碼: cshs **如果通過下載後安裝不是vs 整合安裝方式,下載完成後需要關閉vs 2019相關才能正常安裝。** > VS RPC Menu 工具說明如下: - 用於客戶端程式碼生成 支援Grpc 和Thrift 我們再在 `訂單服務`專案 中建立`OrderRequest.proto`檔案,這個是`Grpc` 的語法,不瞭解該語法的同學可以 點選 [gRPC 官方文件中文版_V1.0](http://doc.oschina.net/grpc?t=56831) 進行學習,地址:http://doc.oschina.net/grpc?t=56831 `OrderRequest.proto`程式碼如下: ``` syntax = "proto3"; package Jlion.NetCore.OrderService.Service.Grpc; //定義訂單查詢引數實體 message OrderSearchRequest{ string OrderId = 1; //定義訂單ID string Name = 2; } //定義訂單實體 message OrderRepsonse{ string OrderId = 1; string Name = 2; double Amount = 3; int32 Count = 4; string Time = 5; } //定義訂單查詢列表 message OrderSearchResponse{ bool Success = 1; string ErrorMsg = 2; repeated OrderRepsonse Data = 3; } ``` 上面主要是定義了幾個訊息實體, 我們再建立`JlionOrderService.proto`,程式碼如下: ``` syntax = "proto3"; package Jlion.NetCore.OrderService.Service.Grpc; import "OrderRequest.proto"; service JlionOrderService{ rpc Order_Search(OrderSearchRequest) returns (OrderSearchResponse){} } ``` 上面的程式碼中都可以看到最上面有 `package Jlion.NetCore.OrderService.Service.Grpc` 程式碼,這是宣告包名也就是後面生成程式碼後的名稱空間,**這個很重要**。 同時定義了`JlionOrderService`服務入口,並且定義了一個訂單搜尋的方法`Order_Search`,到這裡我們已經完成了一小部分了。 #### 生成客戶端程式碼 再在`JlionOrderService.proto`檔案裡面右鍵 》選擇Grpc程式碼生成》Grpc 程式碼 會自動生存微服務客戶端程式碼 。 生存工具中具有如下功能: - 生存Grpc客戶端程式碼 - Grpc 編譯(不常用) - Grpc 打包(常用,用來把客戶端dll釋出到nuget伺服器上) - 還可以對Thrift 程式碼進行生成和打包 #### 建立`Jlion.NetCore.OrderService.Grpc` 類庫 把剛剛通過工具生成的`Grpc`客戶端程式碼直接copy到 `Jlion.NetCore.OrderService.Grpc`這個類庫中(必須和上面Grpc 的程式碼宣告的package 一致)以下簡稱`訂單服務客戶端`,並且需要通過`Nuget`包新增`Overt.Core.Grpc` 的依賴,程式碼結構如下: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200314154132290-1968561026.png) `Jlion.NetCore.OrderService.Grpc`類庫已經構建完成,現在讓 `Jlion.NetCore.OrderService` 服務引用`Jlion.NetCore.OrderService.Grpc` 類庫 #### `訂單服務`中 實現自己的`IHostedService` 建立`HostService`類,繼承`IHostedService`程式碼如下: ``` public class HostedService : IHostedService { readonly ILogger _logger; readonly JlionOrderServiceBase _grpcServImpl; public HostedService( ILogger logger, JlionOrderServiceBase grpcService) { _logger = logger; _grpcServImpl = grpcService; } //服務的啟動機相關配置 public Task StartAsync(CancellationToken cancellationToken) { return Task.Factory.StartNew(() => { var channelOptions = new List() { new ChannelOption(ChannelOptions.MaxReceiveMessageLength, int.MaxValue), new ChannelOption(ChannelOptions.MaxSendMessageLength, int.MaxValue), }; GrpcServiceManager.Start(BindService(_grpcServImpl), channelOptions: channelOptions, whenException: (ex) => { _logger.LogError(ex, $"{typeof(HostedService).Namespace.Replace(".", "")}開啟失敗"); throw ex; }); System.Console.WriteLine("服務已經啟動"); _logger.LogInformation($"{nameof(Jlion.NetCore.OrderService.Service).Replace(".", "")}開啟成功"); }, cancellationToken); } //服務的停止 public Task StopAsync(CancellationToken cancellationToken) { return Task.Factory.StartNew(() => { GrpcServiceManager.Stop(); _logger.LogInformation($"{typeof(HostedService).Namespace.Replace(".", "")}停止成功"); }, cancellationToken); } } ``` 上面程式碼主要是建立宿主機並且實現了`StartAsync` 服務啟動及`StopAsync` 服務停止方法。 我們建立完`HostedServicce`程式碼再來建立之前定義的`Grpc`服務的方法實現類`JlionOrderServiceImpl`,程式碼如下: ``` public partial class JlionOrderServiceImpl : JlionOrderServiceBase { private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; public JlionOrderServiceImpl(ILogger logger, IServiceProvider provider) { _logger = logger; _serviceProvider = provider; } public override async Task Order_Search(OrderSearchRequest request, ServerCallContext context) { //TODO 從底層ES中查詢訂單資料, //可以設計成DDD 方式來進行ES的操作,這裡我就為了演示直接硬編碼了 var response = new OrderSearchResponse(); try { response.Data.Add(new OrderRepsonse() { Amount = 100.00, Count = 10, Name = "訂單名稱測試", OrderId = DateTime.Now.ToString("yyyyMMddHHmmss"), Time = DateTime.Now.ToString() }); response.Data.Add(new OrderRepsonse() { Amount = 200.00, Count = 10, Name = "訂單名稱測試2", OrderId = DateTime.Now.ToString("yyyyMMddHHmmss"), Time = DateTime.Now.ToString() }); response.Data.Add(new OrderRepsonse() { Amount = 300.00, Count = 10, Name = "訂單名稱測試2", OrderId = DateTime.Now.ToString("yyyyMMddHHmmss"), Time = DateTime.Now.ToString() }); response.Success = true; } catch (Exception ex) { response.ErrorMsg = ex.Message; _logger.LogWarning("異常"); } return response; } } ``` 再修改`Program`程式碼,並把`HostedService`和`JlionOrderServiceImpl` 注入到容器中,程式碼如下: ``` class Program { static void Main(string[] args) { var host = new HostBuilder() .UseConsoleLifetime() //使用控制檯生命週期 .ConfigureAppConfiguration((context, configuration) => { configuration .AddJsonFile("appsettings.json", optional: true) .AddEnvironmentVariables(); }) .ConfigureLogging(logger => { logger.AddFilter("Microsoft", LogLevel.Critical) .AddFilter("System", LogLevel.Critical); }) .ConfigureServices(ConfigureServices) .Build(); AppDomain.CurrentDomain.UnhandledException += (sender, e) => { var logFactory = host.Services.GetService(); var logger = logFactory.CreateLogger(); logger.LogError(e.ExceptionObject as Exception, $"UnhandledException"); }; host.Run(); } /// /// 通用DI注入 ///
/// /// private static void ConfigureServices(HostBuilderContext context, IServiceCollection services) { //HostedService 單例注入到DI 中 services.AddSingleton(); services.AddTransient(); } } ``` 到了這裡簡單的`微服務`已經編碼完成,但是還缺少兩個配置檔案,我們建立`appsettings.json`配置檔案和`consulsettings.json` 服務註冊發現的配置檔案 `consulsettings.json`配置檔案如下: ``` { "ConsulServer": { "Service": { "Address": "127.0.0.1:8500"// 你的Consul 服務註冊及發現配置地址 } } } ``` 上面的地址配置只是簡單的例子,我這裡假定我的`Consul`服務地址是 127.0.0.1:8500 等下服務啟動是會通過這個地址進行註冊。 `appsettings.json`配置檔案如下: ``` { "GrpcServer": { "Service": { "Name": "JlionOrderService", "Port": 10001, "HostEnv": "serviceaddress", "Consul": { "Path": "dllconfigs/consulsettings.json" } } } } ``` 我這裡服務監聽了10001 埠,後面註冊到`Consul`中也會看到該埠 官方完整的配置檔案如下: ``` { "GrpcServer": { "Service": { "Name": "OvertGrpcServiceApp", // 服務名稱使用服務名稱去除點:OvertGrpcServiceApp "Host": "service.g.lan", // 專用註冊的域名 (可選)格式:ip[:port=default] "HostEnv": "serviceaddress", // 獲取註冊地址的環境變數名字(可選,優先)環境變數值格式:ip[:port=default] "Port": 10001, // 埠自定義 "Consul": { "Path": "dllconfigs/consulsettings.json" // Consul路徑 } } } } ``` 好了,`訂單服務`已經全部完成了,`訂單服務`服務整體結構圖如下: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200314164656785-2129915807.png) 好了,我們這裡通過命令列啟動下`JlionOrderService`服務,生產環境你們可以搭建在`Docker` 容器裡面 ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200314165357311-817378551.png) 我們可以來看下我之前搭建好的`Consul`服務 ,開啟管理介面,如圖: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200314165513459-1651269728.png) 圖片中可以發現剛剛啟動的服務已經註冊進去了,但是裡面有一個健康檢查未通過,主要是由於服務端不能訪問我本地的`訂單服務`,所有健康檢查不能通過。你可以在你本地搭建 `Consul`服務用於測試。 我本地再來開啟一個服務,配置中的的埠號由10001 改成10002,再檢視下`Consul`的管理介面,如下圖: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200314165736341-1838405733.png) 發現已經註冊了兩個服務,埠號分別是10001 和10002,這樣可以通過自定化工具自動新增服務及下架服務,分散式服務也即完成。 到這裡`訂單服務`的啟動已經完全成功了,我們接下來是需要客戶端也就是上面架構圖中的`電商業務閘道器`或者`支付閘道器`等等要跟`訂單服務`進行通訊了。 #### 建立訂單閘道器(跟訂單服務進行通訊) 建立訂單閘道器之前我先把上面的 `訂單服務客戶端` 類庫釋出到我的nuget包上,這裡就不演示了。我釋出的測試包名稱`JlionOrderServiceDemo` nuget官方可以搜尋找到。你們也可以直接搜尋新增到你們的Demo中進行測試。 我通過VS 2019 建立Asp.Net Core 3.1 框架的`WebApi` 取名為`Jlion.NetCore.OrderApiService` 下面簡稱`訂單閘道器服務` 現在我把前面釋出的`微服務`客戶端依賴包 `JlionOrderServiceDemo` 新增到`訂單閘道器服務`中,如下圖: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200314172605904-911890551.png) 現在在`訂單閘道器服務`中新增`OrderController` api控制器,程式碼如下: ``` namespace Jlion.NetCore.OrderApiService.Controllers { [Route("[controller]")] [ApiController] public class OrderController : ControllerBase { private readonly IGrpcClient _orderService; public OrderController (IGrpcClient orderService) { _orderService = orderService; } [HttpGet("getlist")] public async Task> GetList() { var respData =await _orderService.Client.Order_SearchAsync(new OrderService.Service.Grpc.OrderSearchRequest() { Name = "test", OrderId = "", }); if ((respData?.Data?.Count ?? 0) <= 0) { return new List(); } return respData.Data.ToList(); } } } ``` 程式碼中通過建構函式注入 `OrderService` 並且提供了一個`GetList`的介面方法。接下來我們還需要把`OrderService.Service.Grpc.JlionOrderService`注入到容器中,程式碼如下: ``` public void ConfigureServices(IServiceCollection services) { services.AddControllers(); //註冊Grpc 客戶端,具體可以檢視原始碼 services.AddGrpcClient(); } ``` 現在整個`訂單閘道器服務`專案結構如下圖: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200314175223455-775032078.png) 專案中有兩個最重要的配置`dllconfig//Jlion.NetCore.OrderService.Grpc.dll.json` 和`consulsettings.json` 他們分別是幹什麼的呢?我們先分別來看我本地這兩個配置的內容 `Jlion.NetCore.OrderService.Grpc.dll.json` 配置如下: ``` { "GrpcClient": { "Service": { "Name": "JlionOrderService", // 服務名稱與服務端保持一致 "MaxRetry": 0, // 最大可重試次數,預設不重試 "Discovery": { "Consul": { // Consul叢集,叢集優先原則 "Path": "dllconfigs/consulsettings.json" }, "EndPoints": [ // 單點模式 { "Host": "127.0.0.1", "Port": 10001 }] } } } } ``` `Jlion.NetCore.OrderService.Grpc.dll.json` 配置主要是告訴`訂單閘道器服務`和`訂單服務`應該怎樣進行通訊,以及通訊當中的一些引數配置。我為了測試,本地使用單點模式,不使用Consul模式 `consulsettings.json` 配置如下: ``` { "ConsulServer": { "Service": { "Address": "127.0.0.1:8500" } } } ``` 有沒有發現這個配置和之前服務端的配置一樣,主要是告訴`訂單閘道器服務`(客戶端呼叫者)和`訂單服務`服務端服務發現的叢集地址,如果上面的配置是單點模式則這個配置不會起作用。 到這裡`訂單閘道器服務` (客戶呼叫端)編碼完成,我們開始啟動它: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200314180253929-956598381.png) 我這裡固定5003埠,現在完美的啟動了,我們訪問下訂單介面,看下是否成功。訪問結果如下圖: ![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200314181438804-166900734.png) 微服務完美的執行成功。 上面的構建微服務還是比較麻煩,官方提供了比較快速構建你需要的微服務方式,不需要寫上面的那些程式碼,那些程式碼全部通過模板的方式進行構建你的微服務,有需要學習的可以到點選 [微服務專案構建模板使用教程](https://www.cnblogs.com/jlion/p/12494525.html) 教程地址:https://www.cnblogs.com/jlion/p/12494525.html 文章中的Demo 程式碼已經提交到github 上,程式碼地址:https://github.com/a312586670/IdentityServerDemo 微服務框架開源專案地址:[https://github.com/overtly/core-grpc](https://github.com/overtly/core-grpc )