微服務

2023-04-06 21:00:34

微服務的概念

微服務是一種開發軟體的架構和組織方法,其中軟體由通過明確定義的 API 進行通訊的小型獨立服務組成。這些服務由各個小型獨立團隊負責。
微服務架構使應用程式更易於擴充套件和更快地開發,從而加速創新並縮短新功能的釋出時間。

整體式架構 與 微服務架構 的比較

通過整體式架構

所有程序緊密耦合,並可作為單項服務執行。這意味著,如果應用程式的一個程序遇到需求峰值,則必須擴充套件整個架構。隨著程式碼庫的增長,新增或改進整體式應用程式的功能變得更加複雜。這種複雜性限制了試驗的可行性,並使實施新概念變得困難。整體式架構增加了應用程式可用性的風險,因為許多依賴且緊密耦合的程序會擴大單個程序故障的影響。

使用微服務架構

將應用程式構建為獨立的元件,並將每個應用程式程序作為一項服務執行。這些服務使用輕量級 API 通過明確定義的介面進行通訊。這些服務是圍繞業務功能構建的,每項服務執行一項功能。由於它們是獨立執行的,因此可以針對各項服務進行更新、部署和擴充套件,以滿足對應用程式特定功能的需求。

微服務的特性

自主性

可以對微服務架構中的每個元件服務進行開發、部署、運營和擴充套件,而不影響其他服務的功能。這些服務不需要與其他服務共用任何程式碼或實施。各個元件之間的任何通訊都是通過明確定義的 API 進行的。

專用性

每項服務都是針對一組功能而設計的,並專注於解決特定的問題。如果開發人員逐漸將更多程式碼增加到一項服務中並且這項服務變得複雜,那麼可以將其拆分成多項更小的服務。

單一職責

每個微服務都需要滿足單一職責原則,微服務本身是內聚的,因此微服務通常比較小。每個微服務按業務邏輯劃分,每個微服務僅負責自己歸屬於自己業務領域的功能。

微服務的優勢

敏捷性

微服務促進若干小型獨立團隊形成一個組織,這些團隊負責自己的服務。各團隊在小型且易於理解的環境中行事,並且可以更獨立、更快速地工作。這縮短了開發週期時間。您可以從組織的總吞吐量中顯著獲益。

靈活擴充套件

通過微服務,您可以獨立擴充套件各項服務以滿足其支援的應用程式功能的需求。這使團隊能夠適當調整基礎設施需求,準確衡量功能成本,並在服務需求激增時保持可用性。

輕鬆部署

微服務支援持續整合和持續交付,可以輕鬆嘗試新想法,並可以在無法正常執行時回滾。由於故障成本較低,因此可以大膽試驗,更輕鬆地更新程式碼,並縮短新功能的上市時間。

技術自由

微服務架構不遵循「一刀切」的方法。團隊可以自由選擇最佳工具來解決他們的具體問題。因此,構建微服務的團隊可以為每項作業選擇最佳工具。

可重複使用的程式碼:將軟體劃分為小型且明確定義的模組,讓團隊可以將功能用於多種目的。專為某項功能編寫的服務可以用作另一項功能的構建塊。這樣應用程式就可以自行引導,因為開發人員可以建立新功能,而無需從頭開始編寫程式碼。

彈性

服務獨立性增加了應用程式應對故障的彈性。在整體式架構中,如果一個元件出現故障,可能導致整個應用程式無法執行。通過微服務,應用程式可以通過降低功能而不導致整個應用程式崩潰來處理總體服務故障。

微服務的缺點

當微服務過多時,服務間的通訊變得錯綜複雜,比如:A服務 -> E服務 -> B服務 ... 甚至更多的分支串聯,形成一張莫大的蜘蛛網,若要追蹤一筆資料... 這對未來的工作變得更加複雜。

作者:[Sol·wang] - 部落格園,原文出處:https://www.cnblogs.com/Sol-wang/p/17293829.html

認證授權

參考以往文章:

IdentityServer4 - v4.x 概念理解及執行過程

IdentityServer4 - v4.x .Net中的實踐應用

服務限流

為什麼要限流。。。削峰,減輕壓力,為了確保伺服器能夠正常持續的平穩執行。
當存取量大於伺服器的承載量,我們不希望有伺服器的災難發生;在接收請求的初期,適當的過濾一些請求,或延時處理或忽略掉。
有第三方工具如hystrix、有分散式閘道器限流如Nginx、未來的.NET7自帶限流中介軟體AspNetCoreRateLimit等。
以下按限流演演算法的理解做一些分享。

限流方式

計數方式、固定視窗方式、滑動視窗方式、令牌桶方式、漏桶方式等。

滑動視窗方式

隨著時間的流逝,視窗逐步向前移動;視窗有寬度,也就是時長;視窗內處理的量,也就是量有上限。

陣列存放每個請求的時間點;陣列首尾時間差不超過定義時長;定義時長可接收的量。

執行範例圖

限流滑動視窗示意圖

實現過程

  1. 準備一個陣列,儲存每次請求的時間點;定義時長1s;定義單位時長內可接收請求數量的上限
  2. 本次請求的當前時間點,與陣列中最早的請求時間點 比對(陣列首尾比對)
  3. 比對差值(秒)在定義的時間內 & 在上限數量的範圍內,當前時間點記錄到陣列,被視為可接收的請求
  4. 比對差值(秒)超過定義時長(1s)或超出上限的請求,被限制/忽略;不加入陣列,設定Response後返回
  5. 每次記得移除超出時長的記錄,以確保持續接收合規的新請求

限流中介軟體案例

非完整版 看懂就行

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、每次先消除已過期的請求(超出時間範圍的請求,被定義為系統已處理)
        // 遍歷整個集合
        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)

假如以秒為單位發放令牌,每秒發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="Protos\product.proto" GrpcServices="Client" />
</ItemGroup>

4、Build 專案;通過 proto 檔案自動生成C#程式碼(於obj目錄中)

5、使用生成的使用者端程式碼請求伺服器端

// 建立連線
var channel = GrpcChannel.ForAddress("https://localhost:7068");
// 建立使用者端物件
var client = new Producter.ProducterClient(channel);
// 呼叫伺服器端方法(及引數)
QueryProductResponse resp = client.Query(new QueryProductRequest { Code = "1", Name = "1" });
// 返回的資料集合
foreach (var item in resp.Products)