Java微服務概念 · 應用 · 通訊 · 授權 · 跨域 · 限流
Java微服務概念 · 應用 · 通訊 · 授權 · 跨域 · 限流
微服務的概念
微服務是一種開發(fā)軟件的架構和組織方法,其中軟件由通過明確定義的 API 進行通信的小型獨立服務組成。這些服務由各個小型獨立團隊負責。
微服務架構使應用程序更易于擴展和更快地開發(fā),從而加速創(chuàng)新并縮短新功能的發(fā)布時間。
整體式架構 與 微服務架構 的比較
通過整體式架構
所有進程緊密耦合,并可作為單項服務運行。這意味著,如果應用程序的一個進程遇到需求峰值,則必須擴展整個架構。隨著代碼庫的增長,添加或改進整體式應用程序的功能變得更加復雜。這種復雜性限制了試驗的可行性,并使實施新概念變得困難。整體式架構增加了應用程序可用性的風險,因為許多依賴且緊密耦合的進程會擴大單個進程故障的影響。
使用微服務架構
將應用程序構建為獨立的組件,并將每個應用程序進程作為一項服務運行。這些服務使用輕量級 API 通過明確定義的接口進行通信。這些服務是圍繞業(yè)務功能構建的,每項服務執(zhí)行一項功能。由于它們是獨立運行的,因此可以針對各項服務進行更新、部署和擴展,以滿足對應用程序特定功能的需求。
微服務的特性
自主性
可以對微服務架構中的每個組件服務進行開發(fā)、部署、運營和擴展,而不影響其他服務的功能。這些服務不需要與其他服務共享任何代碼或實施。各個組件之間的任何通信都是通過明確定義的 API 進行的。
專用性
每項服務都是針對一組功能而設計的,并專注于解決特定的問題。如果開發(fā)人員逐漸將更多代碼增加到一項服務中并且這項服務變得復雜,那么可以將其拆分成多項更小的服務。
單一職責
每個微服務都需要滿足單一職責原則,微服務本身是內聚的,因此微服務通常比較小。每個微服務按業(yè)務邏輯劃分,每個微服務僅負責自己歸屬于自己業(yè)務領域的功能。
微服務的優(yōu)勢
敏捷性
微服務促進若干小型獨立團隊形成一個組織,這些團隊負責自己的服務。各團隊在小型且易于理解的環(huán)境中行事,并且可以更獨立、更快速地工作。這縮短了開發(fā)周期時間。您可以從組織的總吞吐量中顯著獲益。
靈活擴展
通過微服務,您可以獨立擴展各項服務以滿足其支持的應用程序功能的需求。這使團隊能夠適當調整基礎設施需求,準確衡量功能成本,并在服務需求激增時保持可用性。
輕松部署
微服務支持持續(xù)集成和持續(xù)交付,可以輕松嘗試新想法,并可以在無法正常運行時回滾。由于故障成本較低,因此可以大膽試驗,更輕松地更新代碼,并縮短新功能的上市時間。
技術自由
微服務架構不遵循“一刀切”的方法。團隊可以自由選擇最佳工具來解決他們的具體問題。因此,構建微服務的團隊可以為每項作業(yè)選擇最佳工具。
可重復使用的代碼:將軟件劃分為小型且明確定義的模塊,讓團隊可以將功能用于多種目的。專為某項功能編寫的服務可以用作另一項功能的構建塊。這樣應用程序就可以自行引導,因為開發(fā)人員可以創(chuàng)建新功能,而無需從頭開始編寫代碼。
彈性
服務獨立性增加了應用程序應對故障的彈性。在整體式架構中,如果一個組件出現故障,可能導致整個應用程序無法運行。通過微服務,應用程序可以通過降低功能而不導致整個應用程序崩潰來處理總體服務故障。
微服務的缺點
當微服務過多時,服務間的通信變得錯綜復雜,比如:A服務 -> E服務 -> B服務 … 甚至更多的分支串聯,形成一張莫大的蜘蛛網,若要追蹤一筆數據… 這對未來的工作變得更加復雜。
認證授權
參考以往文章:
《IdentityServer4 – v4.x 概念理解及運行過程》
《IdentityServer4 – v4.x .Net中的實踐應用》
服務限流
為什么要限流。。。削峰,減輕壓力,為了確保服務器能夠正常持續(xù)的平穩(wěn)運行。
當訪問量大于服務器的承載量,我們不希望有服務器的災難發(fā)生;在接收請求的初期,適當的過濾一些請求,或延時處理或忽略掉。
有第三方工具如hystrix、有分布式網關限流如Nginx、未來的.NET7自帶限流中間件AspNetCoreRateLimit等。
以下按限流算法的理解做一些分享。
限流方式
計數方式、固定窗口方式、滑動窗口方式、令牌桶方式、漏桶方式等。
滑動窗口方式
隨著時間的流逝,窗口逐步向前移動;窗口有寬度,也就是時長;窗口內處理的量,也就是量有上限。
數組存放每個請求的時間點;數組首尾時間差不超過定義時長;定義時長可接收的量。
運行示例圖:
實現過程:
- 準備一個數組,存儲每次請求的時間點;定義時長1s;定義單位時長內可接收請求數量的上限
- 本次請求的當前時間點,與數組中最早的請求時間點 比對(數組首尾比對)
- 比對差值(秒)在定義的時間內 & 在上限數量的范圍內,當前時間點記錄到數組,被視為可接收的請求
- 比對差值(秒)超過定義時長(1s)或超出上限的請求,被限制/忽略;不加入數組,設置Response后返回
- 每次記得移除超出時長的記錄,以確保持續(xù)接收合規(guī)的新請求
限流中間件案例:
非完整版 看懂就行
public class RequestLimitingMiddleware{ // 單位時間內,可接收的請求數量 private int _qps = 6; // 定義單位時長(秒) private readonly int _unit_seconds = 1; // 集合存放已接收的請求 private ConcurrentQueue<DateTime> _backlog_request = new ConcurrentQueue<DateTime>(); /// <summary> /// 限流方法 - 時間滑動窗口算法,是否限流 /// </summary> /// <returns></returns> private bool Limiting() { // 比對的結果差值 double _diff_sec = 0; // 本次請求時間 DateTime _curr_req_now = DateTime.Now; #region 1、每次先消除已過期的請求(超出時間范圍的請求,被定義為系統(tǒng)已處理) // 遍歷整個集合 DateTime _disused_req = new DateTime(); while (_backlog_request.TryPeek(out _disused_req)) { // 超出定義時長的 if (_curr_req_now.Subtract(_disused_req).TotalSeconds > _unit_seconds) { // 移除 _backlog_request.TryDequeue(out _disused_req); } else break; } #endregion #region 2、有積壓的請求,取最早的那個請求時間,與本次時間比對,并計算出差值 DateTime _first_req_now = new DateTime(); if (_backlog_request.TryPeek(out _first_req_now)) { // 當前請求的時間 與 最早的請求時間 跨度 _diff_sec = _curr_req_now.Subtract(_first_req_now).TotalSeconds; } #endregion #region 3、是否限制的請求 // 集合的首尾不能超過單位時長,及數量上限 if (_diff_sec < _unit_seconds && _backlog_request.Count < _qps) { // 可接收的新請求 記錄到集合 _backlog_request.Enqueue(_curr_req_now); return true; } // 被視為限制的請求 return false; #endregion } public Task Invoke(HttpContext context) { #region 限流方法的應用 if (!this.Limiting()) { _logger.LogWarning($" ! 被限制的請求,忽略"); context.Response.StatusCode = (Int16)HttpStatusCode.TooManyRequests; context.Response.ContentType = "text/json;charset=utf-8;"; return context.Response.WriteAsync("抱歉,限流了,請稍后再試。"); } _logger.LogInformation($" 新增的請求,當前積壓 {_backlog_request.Count} req."); #endregion // 模擬運行消耗時間 Thread.Sleep(300); _next(context); return Task.CompletedTask; }}
滑動窗口限流測試
由于設置的1s/6次請求,所以手動可以測試;瀏覽器快速的敲擊F5請求API接口,測試效果如下圖:
漏桶方式
看桶內容量,溢出就拒絕;(累加的請求數是否小于上限)
實現邏輯:
有上限數量的桶,接收任意請求
隨著時間的流逝,上次請求時間到現在,通過速率,計算出桶內應有的量
此量超過上限,拒絕新的請求
直到消耗出空余數量后,再接收新的請求
以上僅通過計算出的剩余的數字,決定是否接收新請求
比如:每秒10個請求上線,還沒到下一秒,進來的第11個請求被拒絕
令牌方式
看令牌數量,用完就拒絕;(累減的令牌是否大于0)
假如以秒為單位發(fā)放令牌,每秒發(fā)10個令牌,當這一秒還沒過完,收到了第11個請求,此時令牌干枯了,那就拒絕此請求;
所以每次請求看有沒有令牌可用。
實現邏輯:
按速率,兩次請求的時間差,計算出可生成的令牌數;每個請求減一個令牌
相同時間進來的請求,時間差值為0,所以每次沒能生成新的令牌,此請求也消耗一個令牌
直到令牌數等于0,拒絕新請求
跨域
為什么有跨域
源自于瀏覽器;出于安全的考慮,瀏覽器默認限制不同站點域名間的通訊,所以 JS/Cookie 只能訪問本站點下的內容;叫 同源策略。
跨域的原理及策略
瀏覽器默認是限制跨域的,當然也可以告訴瀏覽器,怎樣的站點間通訊可以取消限制。
Request 或 Response 中追加 Header 的設定:允許的請求源頭,允許的請求動作,允許的Header方式等。
如:Access-Control-Allow-Origin:{目標域名Url}
可以用不受限的*,允許所有的跨域請求,這樣的安全性低;
也可以指定一個二級域名,域名下所有的Url不受限;
也可以僅指定一個固定的Url;
也可以指定請求動作 GET/PUT;
以上設定都稱為跨域的策略,按實際情況自定義策略。
.NET跨域的實現
Request / Response 的 Header 設定方式:
Response.Headers["Access-Control-Allow-Origin"] = "{域名地址}";Response.Headers["Access-Control-Allow-Credentials"] = "true";Response.Headers["Access-Control-Allow-Headers"] = "x-requested-with,content-type";
中間件定義策略方式:
.NET默認提供了跨域的中間件UseCors,同樣可以在中間件中設定 源頭/動作/Header 等。
全局策略案例:
// 設定跨域策略builder.Services.AddCors(options =>{ options.AddPolicy(name: "策略名稱1", policy => { // 允許的域名 policy.WithOrigins("http://contoso.com", "http://*.sol.com") // 允許的請求動作 .WithMethods("GET", "POST", "PUT", "DELETE") // 允許的 Header .AllowAnyHeader(); });});// ... 最后啟用跨域中間件app.UseCors("{策略名稱}");
Action單獨設定跨域:
啟用:[EnableCors]
指定:[EnableCors("策略名稱")]
詳細:[EnableCors(origins: "http://Sol.com:8013/", headers: "*", methods: "GET,PUT")]
排除:[DisableCors]
服務間的通信
Remote Procedure Call – RPC
Remote Procedure Call,遠程過程調用。通常,RPC要求在調用方中放置被調用的方法的接口。調用方只要調用了這些接口,就相當于調用了被調用方的實際方法,十分易用。于是,調用方可以像調用內部接口一樣調用遠程的方法,而不用封裝參數名和參數值等操作。傳輸速度快,效率高的特點,常用于服務間的通信。
整體運行過程:
.NET服務被調方集成 gRPC
1、NuGet 安裝 Grpc.AspNetCore
2、編寫 Proto 文件(為生成C#代碼)
syntax = "proto3";// 生成代碼后的命名空間option csharp_namespace = "GrpcService";// 包名(不是必須)package product;// 定義一個服務service Producter{ // 定義一個方法(請求參數類,返回參數類) rpc Add(CreateProductRequest) returns (CreateProductResponse); rpc Query(QueryProductRequest) returns (QueryProductResponse);}// 為上述服務 定義 請求參數類message QueryProductRequest{ // 類型、名稱、唯一標識 string name = 1; string code = 2;}// 為上述服務 定義 返回參數類message QueryProductResponse{ // 定義為集合類型 repeated Product products = 1;}message CreateProductRequest{ string name = 1; string code = 2; string color = 3; string size = 4; string manufacturing = 5;}message CreateProductResponse{ ResultType result = 1;}// 定義(以上用到的)枚舉enum ResultType{ success=0; fail=1;}message Product{ int32 id = 1; string name = 2; string code = 3; string color = 4; string size = 5;}
3、項目屬性文件配置編譯包含項
4、Build 項目;通過 proto 文件自動生成C#代碼(于obj目錄中)
5、編寫對應的Service 繼承于自動生成的抽象類,并實現其中抽象方法
public class ProductService : Producter.ProducterBase
6、注冊到容器
// 注冊builder.Services.AddGrpc();// 到容器app.MapGrpcService<ProductService>();
7、appsettings.json 配置啟用RPC所需的HTTP2協議
"Kestrel": { "EndpointDefaults": { "Protocols": "Http2" }}
8、最終目錄效果圖
.NET服務調用方集成 gRPC
1、NuGet 安裝 Grpc.AspNetCore、Grpc.Net.Client
2、Cope 服務端 Proto 文件于目錄
3、項目屬性文件配置編譯包含項
<ItemGroup> <Protobuf Include="Protosproduct.proto" GrpcServices="Client" /></ItemGroup>
4、Build 項目;通過 proto 文件自動生成C#代碼(于obj目錄中)
5、使用生成的客戶端代碼請求服務端
// 建立連接var channel = GrpcChannel.ForAddress("https://localhost:7068");// 創(chuàng)建客戶端對象var client = new Producter.ProducterClient(channel);// 調用服務端方法(及參數)QueryProductResponse resp = client.Query(new QueryProductRequest { Code = "1", Name = "1" });// 返回的數據集合foreach (var item in resp.Products)