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