一、前言
這篇文章本來是繼續分享IdentityServer4
的相關文章,由於之前有博友問我關於微服務
相關的問題,我就先跳過IdentityServer4
的分享,進行微服務
相關的技術學習和分享。微服務
在我的分享目錄里面是放到四月份開始系列文章分享的,這里就先穿越下,提前安排微服務
應用的開篇文章 電商系統升級之微服務架構的應用
。
本博客以及公眾號堅持以架構的思維來分享技術,不僅僅是單純的分享怎么使用的Demo。
二、場景
先來回顧下我上篇文章 Asp.Net Core 中IdentityServer4 授權中心之應用實戰 中,電商架構由單體式架構
拆分升級到多網關架構
升級之前
升級之后:
然而升級之后問題又來了,由於之前增加了代理商業務並且把授權中心
和支付網關
單獨拆出來了,這使得公司的業務訂單量翻了幾十倍,這個時候整個電商系統達到了瓶頸,如果再不找解決方案系統又得宕機了。
2.1 問題及解決方案
經過技術的調研及問題分析,導致這個瓶頸的問題主要有以下幾個原因,只需要把下面問題解決就可以得到很大的性能提升
- 每天的訂單量暴增,導致訂單數據太大,然而整個電商系統數據存儲在一個數據庫中,並且是
單表
、單數據庫
(未進行讀寫分離),以致於訂單數據持續暴增。 - 相關業務需要依賴訂單查詢,訂單數據查詢慢以至於拖垮數據庫
- 整個電商系統連接數達到瓶頸(已經分布式部署,在多加服務器會損耗更多的經費而達不到最佳性價比)
為了一勞永逸的解決以上問題,經過技術的調研,決定對訂單業務做如下升級改造:
- 拆分獨立的訂單微服務(本章節着重分享)
- 使用
ES
進行數據遷移(按年進行划分,並且進行讀寫分離,這里就不着重講,下次來跟大家一起學習和分享) - 增加
分布式緩存
(也不是本次的重點,后續再來跟大家學習和分享)
經過升級后的架構圖如下:
架構圖說明:
- 右邊同一顏色的代表還是原先電商系統的
單體式架構
,為拆分的單體架構業務,其中在業務處理上夾雜了一層分布式緩存的處理 - 左邊的是微服務的架構,是這次升級拆分后的架構,其中數據庫也已經從原有的數據庫拆分並且數據遷移到了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
微服務框架,如下圖:
目前core-grpc
微服務框架,最新正式發布版本是 1.0.3
引用了core-grpc
后我們還需要安裝一個工具VS RPC Menu
,這個工具也是大神免費提供的,圖片如下:
由於微軟官方下載比較慢,我這里共享到 百度網盤,百度網盤下載地址如下:
鏈接: 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
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
的依賴,代碼結構如下:
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<HostedService> logger,
JlionOrderServiceBase grpcService)
{
_logger = logger;
_grpcServImpl = grpcService;
}
//服務的啟動機相關配置
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
var channelOptions = new List<ChannelOption>()
{
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<JlionOrderServiceImpl> logger, IServiceProvider provider)
{
_logger = logger;
_serviceProvider = provider;
}
public override async Task<OrderSearchResponse> 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<ILoggerFactory>();
var logger = logFactory.CreateLogger<Program>();
logger.LogError(e.ExceptionObject as Exception, $"UnhandledException");
};
host.Run();
}
/// <summary>
/// 通用DI注入
/// </summary>
/// <param name="context"></param>
/// <param name="services"></param>
private static void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
//HostedService 單例注入到DI 中
services.AddSingleton<IHostedService, HostedService>();
services.AddTransient<JlionOrderServiceBase, JlionOrderServiceImpl>();
}
}
到了這里簡單的微服務
已經編碼完成,但是還缺少兩個配置文件,我們創建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路徑
}
}
}
}
好了,訂單服務
已經全部完成了,訂單服務
服務整體結構圖如下:
好了,我們這里通過命令行啟動下JlionOrderService
服務,生產環境你們可以搭建在Docker
容器里面
我們可以來看下我之前搭建好的Consul
服務 ,打開管理界面,如圖:
圖片中可以發現剛剛啟動的服務已經注冊進去了,但是里面有一個健康檢查未通過,主要是由於服務端不能訪問我本地的訂單服務
,所有健康檢查不能通過。你可以在你本地搭建 Consul
服務用於測試。
我本地再來開啟一個服務,配置中的的端口號由10001 改成10002,再查看下Consul
的管理界面,如下圖:
發現已經注冊了兩個服務,端口號分別是10001 和10002,這樣可以通過自定化工具自動添加服務及下架服務,分布式服務也即完成。
到這里訂單服務
的啟動已經完全成功了,我們接下來是需要客戶端也就是上面架構圖中的電商業務網關
或者支付網關
等等要跟訂單服務
進行通訊了。
創建訂單網關(跟訂單服務進行通信)
創建訂單網關之前我先把上面的 訂單服務客戶端
類庫發布到我的nuget包上,這里就不演示了。我發布的測試包名稱JlionOrderServiceDemo
nuget官方可以搜索找到。你們也可以直接搜索添加到你們的Demo中進行測試。
我通過VS 2019 創建Asp.Net Core 3.1 框架的WebApi
取名為Jlion.NetCore.OrderApiService
下面簡稱訂單網關服務
現在我把前面發布的微服務
客戶端依賴包 JlionOrderServiceDemo
添加到訂單網關服務
中,如下圖:
現在在訂單網關服務
中添加OrderController
api控制器,代碼如下:
namespace Jlion.NetCore.OrderApiService.Controllers
{
[Route("[controller]")]
[ApiController]
public class OrderController : ControllerBase
{
private readonly IGrpcClient<OrderService.Service.Grpc.JlionOrderService.JlionOrderServiceClient> _orderService;
public OrderController (IGrpcClient<OrderService.Service.Grpc.JlionOrderService.JlionOrderServiceClient> orderService)
{
_orderService = orderService;
}
[HttpGet("getlist")]
public async Task<List<OrderRepsonse>> 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<OrderRepsonse>();
}
return respData.Data.ToList();
}
}
}
代碼中通過構造函數注入 OrderService
並且提供了一個GetList
的接口方法。接下來我們還需要把OrderService.Service.Grpc.JlionOrderService
注入到容器中,代碼如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//注冊Grpc 客戶端,具體可以查看源代碼
services.AddGrpcClient();
}
現在整個訂單網關服務
項目結構如下圖:
項目中有兩個最重要的配置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"
}
}
}
有沒有發現這個配置和之前服務端的配置一樣,主要是告訴訂單網關服務
(客戶端調用者)和訂單服務
服務端服務發現的集群地址,如果上面的配置是單點模式則這個配置不會起作用。
到這里訂單網關服務
(客戶調用端)編碼完成,我們開始啟動它:
我這里固定5003端口,現在完美的啟動了,我們訪問下訂單接口,看下是否成功。訪問結果如下圖:
微服務完美的運行成功。
上面的構建微服務還是比較麻煩,官方提供了比較快速構建你需要的微服務方式,不需要寫上面的那些代碼,那些代碼全部通過模板的方式進行構建你的微服務,有需要學習的可以到點擊
微服務項目構建模板使用教程
教程地址:https://www.cnblogs.com/jlion/p/12494525.html
文章中的Demo 代碼已經提交到github 上,代碼地址:https://github.com/a312586670/NetCoreDemo
微服務框架開源項目地址:https://github.com/overtly/core-grpc