只有累積,沒有奇蹟

2022年5月9日 星期一

[NETCore] ASP.NET Core 限流框架 AspNetCoreRateLimit

前言
開發者都知道系統上線後才是挑戰的開始,舉例來說像是每天不斷的有爬蟲程式來抓取網站資料,或是對外開放的 API 服務遭到攻擊事件,若沒有良好的防範機制很有可能造成 Server 因為攻擊無法正常服務,甚至引起雪崩效應影響到其他系統服務,在 ASP.NET Core 中可以透過  AspNetCoreRateLimit  框架根據 Request 的 IP 或是 ClientID 來達到限制流量的效果,這篇就來簡單分享一下有關 AspNetCoreRateLimit 的安裝與基本使用,若有問題或是錯誤的地方歡迎網路的高手大大給予指導

安裝
首先,為了方便大家更容易了解,建立一個 ASP.NET Core Web Application 應用程式來作範例,接著開啟 Nuget Package Mnage 輸入 AspNetCoreRateLimit 搜尋,安裝目前最新版的 AspNetCoreRateLimit 套件
  或是透過 Nuget Package Console 輸入下列指令
Install-Package AspNetCoreRateLimit -Version 3.0.5
安裝完畢之後到專案檔底下確認是否有安裝成功

使用與設定 
AspNetCoreRateLimit 可以根據 IP 或是客戶 ID 來作為限速依據,也可以指定 API 中某一個對外接口或是 Http Method 分別設定其限制,兩者皆是在中間件 ( Middleware) 判斷其請求率是否達到設定的上限,分別是  IpRateLimitMiddleware  與  ClientRateLimitMiddleware ,再來決定是否要讓此 Request 繼續下一步或是返回的動作,這裡以限制 IP 為範例,安裝完後接下來就是到 start.cs 中加入下列代碼
    public void ConfigureServices(IServiceCollection services)
    {
        // 將速限計數器資料儲存在 Memory 中
        services.AddMemoryCache();

        // 從 appsettings.json 讀取 IpRateLimiting 設定 
        services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));

        // 從 appsettings.json 讀取 Ip Rule 設定
        services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));

        // 注入 counter and IP Rules 
        services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
        services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();

        // Add framework services.
        services.AddMvc();

        // the clientId/clientIp resolvers use it.
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

        // Rate Limit configuration 設定
        services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        app.UseIpRateLimiting();
        app.UseMvc();
    }
代碼中的 Code 都有加上註解跟說明,這裡就不在詳細說明,在上述代碼中可以看到 Policy 與 IP Rule 都是從 appSettings.json 設定檔中取得,因此下一步就是在設定檔中加上限流相關的設定資訊

IpRateLimiting
"IpRateLimiting": {
    "EnableEndpointRateLimiting": false,
    "StackBlockedRequests": false,
    "RealIpHeader": "X-Real-IP",
    "ClientIdHeader": "X-ClientId",
    "HttpStatusCode": 429,
    "IpWhitelist": [ "127.0.0.1", "::1/10", "192.168.0.0/24" ],
    "EndpointWhitelist": [ "get:/api/license", "*:/api/status" ],
    "ClientWhitelist": [ "dev-id-1", "dev-id-2" ],
    "GeneralRules": [
      {
        "Endpoint": "*",
        "Period": "1s",
        "Limit": 2
      },
      {
        "Endpoint": "*",
        "Period": "15m",
        "Limit": 100
      },
      {
        "Endpoint": "*",
        "Period": "12h",
        "Limit": 1000
      },
      {
        "Endpoint": "*",
        "Period": "7d",
        "Limit": 10000
      }
    ]
  }
IpRateLimiting 區塊設定 :  
  • EnableEndpointRateLimiting : 全部套用為 false,設定為 true 則 適用於每個 endPoint 。
  • StackBlockedRequests : false 代表拒絕的呼叫不會加入計數器中,如果希望被拒絕的請求也要包含在計數器中則要設定為 true。  
  • ClientIdHeader : 如果 Request 請求中的 Head 客戶 ID 與 ClientWhitelist 相同的話就不受限流設定影響。
  • GeneralRules : Period 設定可以包含天(d)、小時(h)、分鐘(m) 以及秒(s) 等設定值,舉例來說第一個區段設定限制為 1 秒限定 2 次,超過就會開始限流。

  • IpRateLimitPolicics
    在 appSettings.json 加入下列設定資訊
    "IpRateLimitPolicies": {
        "IpRules": [
          {
            "Ip": "84.247.85.224",
            "Rules": [
              {
                "Endpoint": "*",
                "Period": "1s",
                "Limit": 10
              },
              {
                "Endpoint": "*",
                "Period": "15m",
                "Limit": 200
              }
            ]
          },
          {
            "Ip": "192.168.3.22/25",
            "Rules": [
              {
                "Endpoint": "*",
                "Period": "1s",
                "Limit": 5
              },
              {
                "Endpoint": "*",
                "Period": "15m",
                "Limit": 150
              },
              {
                "Endpoint": "*",
                "Period": "12h",
                "Limit": 500
              }
            ]
          }
        ]
      }
    備註 : 這裡 IP 支援 IPv4 & IPv6 

    限流規則
    使用 Throttling 最重要的是了解限速的規則設定,才可以因應不同的情境調整需要的限速(流)設定,這裡直接來看一下官方說明文件提到的 Throttling 規則與 Rule,有 EndPoint Format、Period 與 Limit 等三種格式
    廢話不多說直接看範例更容易了解
    Sample 1 : 
    {
        "Endpoint": "*",
        "Period": "1s",
        "Limit": 2
    }
    目的 : 所有端點每秒鐘允許 2 次呼叫 
    使用 GET 方法發送 api / Values 一秒三個 Request,前 2 個 Request 會 pass,第 3 個 request 會無法使用,如果同時間使用 PUT 方法呼叫相同 api / values 則會 pass,因為 GET 與 PUT 分別加總不會混和計算。

     Sample 2 : 
    {
        "Endpoint": "*:/api/values",
        "Period": "15m",
        "Limit": 5
    }
    目的 : api / Values 用 HTTP 所有 Method 的設定為每 15 分鐘允許呼叫 5 次 

    Sample 3 : 
    {
        "Endpoint": "get:/api/values",
        "Period": "1h",
        "Limit": 5
    }
    目的 : api / Values 用 HTTP GET Method 的設定為每小時允許呼叫 5 次
    每小時呼叫 5 次,PASS 
    每小時呼叫 6 次,第六次則會正常取得正確資料

    當超過我們定義的次數限制時會顯示
    API calls quota exceeded! maximum admitted 2 per 10s.
    打開 Chrome 瀏覽器透過 Network 可以看到回傳的內容細節
    可以從開發者工具看到 Response 的資訊,在 Response Header 中 StatusCode 為 429,retry-after 為 9,如果不希望在 Response Header 中顯示 retry 次數,可以在 appsettings.json 設定檔中加上 DisableRateLimitHeaders 為 true 隱藏 retry 資訊,如果想要自訂 Response 時的訊息內容可以參考官網的說明 : 傳送門


    感想
    除了在 ASP.NET Core 有限流以外,作者也有提供 .NET Framework 版本的限流框架 WebApiThrottle,可以省下自己開發時間,也可以快速地將限流機制套用在需要的專案上,另外在本篇中提到的都是以 IP 為出發作為限制,在 AspNetCoreRateLimit 也有提供使用 Client 作為設定的機制,如有需要可以參考 ClientRateLimitMiddleware 相關說明,希望這篇介紹可以有幫助到有需要的朋友,謝謝

    參考
    AspNetCoreRateLimit
    Throttling your API in ASP.NET

    2022年5月1日 星期日

    [Architecture] 架構設計 - 限流策略 Rate Limiting Strategies

    前言
    這一篇是紀錄閱讀 超大流量分佈式系統架構解決方案:人人都是架構師2.0 的讀書筆記,筆者在書中有介紹在面對大流量的請求洗禮時,常見的手段有以下幾種方式
    • 擴充
    • 靜態化
    • 限流
    • 緩存
    • 隊列
    這篇文章是介紹上述方案中的限流方案章節筆記,在整理時內容加上自己理解的說明與圖片,其中若是自己有理解不正確的地方是錯誤的部分歡迎各位高手一同討論或給予指導


    為什麼需要限流
    大型電商網站主要技術挑戰可能來自短時間內客戶忽然的大流量與短時間內的高併發請求,像是之前大家在搶購的 Switch 動物之森主機(搶三次都沒搶到 怒),或是最近吵得很紅的 PS4 造型悠遊卡(搶不到again,怒怒),如果系統不對流量進行合理的管制,任意地讓所有的請求流量在短時間內衝擊系統,可能會導致一連串的問題發生,像是原本設定的 cache 機制因為超過負荷被撐爆、主機連線資源被耗盡,資料庫要處理大量的請求而造成回應時間變長,最後系統很有可能發生雪崩效應(Avalanche effect),造成服務的中斷。任何一個系統的容量都存在著上限,一旦用戶請求量過載超過上限,系統的吞吐量便會逐漸開始下降,流量管制的目的是保護系統,讓系統的負載壓力處於一個比較均衡的水位,有效的管理峰值的請求量。 從上圖可以得知,當處理大量的請求時前面可以使用 CDN (靜態資源),FrondEnd 或是 Backend 可以使用擴充的方式來承接更大的流量請求,但資料的異動最後還是會交由資料庫來進行處理,Database 數據庫的連線是一個非常昂貴且數量有限的底層資源,為了避免開發環境下連接數超過數據庫所能乘載的最大上限,合理的運用資料庫的連線池技術,確保系統在高併發時連接數不會超過資源的水位值(資源閥值)。

    常見限流演算法
    書中介紹常見的限流演算有令牌桶算法、漏桶算法以及計數器算法三種,以下就針對這三種作簡單介紹

    令牌桶算法
    令牌桶(Token Bucket)算法在 wiki 說明敘述如下
    每 1/r 秒會新增一個令牌(Token)到桶(bucket)中
    桶(bucket)中最多可容納 b 令牌。如果桶已滿時則丟棄該令牌(Token)
    當傳送 n bytes 封包時,桶(bucket)中如果有 n 個令牌(token >= n),封包將會送至網路
    當可用的 token < n 時,不會從桶(bucket)刪除任何 token,該封包會被視為不合格的
    

    圖片來源 https://gateoverflow.in/39720/gate2016-1-54

    令牌桶算法的原理是會以固定的速度將令牌(token)放入桶(bucket)中,當要處理請求時首先會先到桶(bucket)中取得是否有足夠的令牌(token),當桶(bucket)中的令牌(token)用完時則拒絕服務。

    從上述原理可以得知桶(bucket)的容量大小就是系統的最大併發數;可能的瓶頸是令牌桶算法中會有一個執行緒專門在產生/更新令牌(token)數,如果要在很短的時間內要產生大量的令牌數量的話會消耗系統 CPU 資源,舉例來說如果需求是每 1ms 就產生一次 Token 數量,該執行緒就會頻繁的佔用系統 CPU 資源來產生需要的 Token 數量,關於更多令牌桶算法的討論可以參考此討論串 GATE2016-1-54,裡面有更多詳細關於該算法的速率解說與範例算式說明。


    漏桶算法
    漏桶(Leaky bucket)算法在 wiki 說明敘述如下
    請求進入漏桶(bucket)裡,桶(bucket)容量是固定的
    當桶(bucket)中的水量(請求)為空時,則不在流出水滴
    流出水滴的速率為固定的,桶(bucket)滿了則會溢出
    


    圖片來源 https://developpaper.com/rate-limit-scheme-of-gateway/

    漏桶算法的原理較為簡單,水滴(請求)進入漏水桶(bucker),漏水桶流出水滴的速度為固定的,當水滴進入水桶的速度太快時就直接溢出。漏桶算法提供的機制是限制流量數據的流出速度,且流出速度是持續保持固定的。

    上述是介紹 Leaky bucket 中的 meter 版本,在 wiki 中還有另外一種是 queue 版本
    水滴(請求)進入漏桶(bucket)裡,桶(bucket)容量是固定的
    當超過桶(bucket)容量限制時,排隊或者被丟棄
    桶(bucket)流出水滴的速率為固定的
    



    計數器算法
    計數器算法算式最單純的限流算法,應用層面相當的廣泛,原理如下
    在單位時間之內由一個計數器專門負責計算,當新的請求來的時候會與閥值(水位)進行比較,當請求數達到閥值或水位時,便會觸發限流機制的邏輯。
    
    舉例來說,假設限流策略為每分鐘為 5 個請求數,每請求一次計數器便會遞增加一,單位時間內計數器與限流閥值相等時,後續的請求將會被執行限流處理,當單位時間過後計數器便會進行重置的動作,新的請求才可以繼續訪問,示意圖如下



    漏桶算法 v.s 令牌算法
    從本質上來看兩者算法都可以用於高併發與大流量的場景下進行流量管制,不會因為瞬間的大流量而讓系統被打爆。但需要注意這兩者的現流方向是相反的,令牌桶(Token Bucket)算法限制的是流量的平均流入速率,並允許一定程度上的突發狀況(大流量);而漏桶(Leaky bucket)算法限制的是流量的流出速率,而非流入速度,這種流出速度還是保持固定不變的;令牌桶算法的極限可能會是桶的容量加上生成令牌處理程式的最大併發能力。

    HTTP Status
    上面提到了一些常用的限流算法,接著來看 HTTP 協定中有提到定義當達到限流機制的 Status Code 代碼,在達到閥值(水位)值時,開發者可以使用 429 Too Many Request 告訴呼叫端(用戶)特定時間內發送太多的請求,並在 Response 中說明觸發條件的詳細訊息,以及多久後可以再進行重試(Retry-After),範例如下
    HTTP/1.1 429 Too Many Requests
       Content-Type: text/html
       Retry-After: 3600
       
       Too Many Requests
       I only allow 50 requests per hour to this Web site per
       logged in user.  Try again soon.     
       
    

    ASP.NET Core 實現限流策略
    身為一位不專業的工程師,上面介紹這麼多之後當然也要介紹在 ASP.NET 好用的限速策略 Library 其中比較推薦的是 AspNetCoreRateLimit 套件,使用與設定上相當簡單,也可以根據不同情境像是 Request IP 或是 Client ID 進行限流的策略,Github wiki 介紹頁面如下

    以上若有問題歡迎一起討論,或是有錯誤的地方也請高手大大們給予指導予糾正,謝謝


    參考
    Rate limiting
    Scaling your API with rate limiters
    High-performance rate limiting
    An alternative approach to rate limiting
    Leaky bucket
    Rate limit scheme of gateway
    流量调整和限流技术
    rate limiting 之 leaky bucket
    想通关「限流」?只要这一篇

    2022年4月28日 星期四

    [WEB API] Swagger - 在 Headers 中新增 API Token 驗證

    問題
    在開發 API 時都會在網站加上 API Token 機制,當收到一個 Request 請求時 API 會驗證 Token 的正確性,確認請求參數中的 Token 是否是有效 / 已授權 / 有沒有過期或是用來當 SSO (Single sign-on) 的使用,驗證無誤後才會進入接口的邏輯處理目前公司內部 API 專案也有驗證 Token 的設計之前文章介紹了 Swagger 基本使用 發現在線上文件進行 API 測試沒有 Token 的話根本無法測試,此篇記錄如果遇到此問題時該如何解決

    解決方案
    Swagger 在產生 API 接口會將參數 (Model) 資訊顯示在 html 文件,但如果驗證 Token 機制是在 Header時所生成的 Web API 文件就不會生成在網頁上,寫了一個簡單的範例專案讓大家比較好了解 (僅示意,相信寫法上有更多好的驗證機制)如下圖Code 內容所示,程式碼在進入時就檢查 Request Header 是否有提供 Token 參數,此時使用 Swagger 就是沒有地方可以輸入 Token 參數,因此測試就會因為不符合 Token 驗證會一直爆炸
     
    該如何解決呢? 在 Swashbuckle 是很容易擴充的,可以透過加入 IOperationFilter 達到在線上文件新增驗證 Header 的需求,更符合測試上的情境,以下為研究後的小小心得

    新增類別並實作 IOperationFilter 
    以範例程式為例新增 HeaderTokenOperationFilter 類別並實作 IOperationFilter 介面中的方法 Apply
    public class HeaderTokenOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
        {
     if (operation.parameters == null)
      operation.parameters = new List();
    
     operation.parameters.Add(new Parameter
     {
      name = "Token",
      @in = "header", //query
      type = "string",
      description = "User Token In Header",
      required = true
     });
        }
    }
    
    
    程式說明 : 
    • Line 10 : 要加入的 Parameter 名稱
    • Line 11 : 請求時所要放的位置,此範例是在 Header,也可以放在 QueryString
    • Line 12 : 參數型態
    • Line 13 : Parameter 說明
    • Line 14 : 是否必填
    設定 SwaggerConfig
    開啟 SwaggerConfig.cs 檔案加入下列程式
    驗證
    重新開啟專案,發現 Swagger 已經有 Token 參數可以提供輸入並會顯示為必填欄位
    為了驗證是否真的有在 Request 的 Headers 帶 Token 參數,可以透過 Chrome F12 來觀察是否有帶正確的 Token 參數 : F12 > Network > Headers 
    驗證無誤,打完收工

    完整 Sample
    完整的 Code 有需要請自行服用
        public class HomeController : ApiController
        {
            public string _userSsoToken = "1aa2a116-876e-464b-bdaf-d6d3adaeb4e4";
            
            /// 
            /// 取得使用者金額
            /// 
            /// 
            [HttpPost]
            [Route("GetMoney")]
            [SwaggerRequestExample(typeof(MemberUser), typeof(MemberUserExample))]
            public IHttpActionResult GetMoney(MemberUser input)
            {
                var userInfo = new UserInfo();
                
                // 檢查 Header 是否有 token
                if (Request.Headers.Contains("Token"))
                {
                    string token = Request.Headers.GetValues("Token").First();
    
                    if (token == _userSsoToken)
                    {
                        // 驗證無誤 回傳資料
                        if (input.UserId == 9487)
                        {
                            GetUserInfo(userInfo);
                        }
                    }
                    else
                    {
                        // 驗證錯誤
                        TokenError(userInfo);
                    }
    
                    userInfo.Token = token;
                }
                
                return Ok(userInfo);
            }
    
            private static void TokenError(UserInfo userInfo)
            {
                userInfo.Code = "999";
                userInfo.Message = "Token error";
            }
    
            private static void GetUserInfo(UserInfo userInfo)
            {
                userInfo.Code = "000";
                userInfo.Message = "Success";
                userInfo.UserId = "9487";
                userInfo.Name = "marcus";
                userInfo.Money = "1000";
            }
        }
        
        internal class UserInfo
        {
            public string UserId { get; set; }
            public string Name { get; set; }
            public string Money { get; set; }
            public string Code { get; set; }
            public string Message { get; set; }
            public string Token { get; set; }
        }
    
        public class MemberUser
        {
            public int UserId { get; set; }
        }
    
        public class MemberUserExample : IExamplesProvider
        {
            public object GetExamples()
            {
                return new MemberUser
                {
                    UserId = 9487,
                };
            }
        }
        public class HeaderTokenOperationFilter : IOperationFilter
        {
            public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
            {
                if (operation.parameters == null)
                    operation.parameters = new List();
    
                operation.parameters.Add(new Parameter
                {
                    name = "Token",
                    @in = "header",
                    type = "string",
                    description = "User Token In Header",
                    required = true
                });
            }
        }
    
      參考
      add-an-authorization-header-to-your-swagger-ui-with-swashbuckle
      .NET -Swagger Web API for Basic Authentication

      Copyright © m@rcus 學習筆記 | Powered by Blogger

      Design by Anders Noren | Blogger Theme by NewBloggerThemes.com