自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

微服務(wù)架構(gòu)中,客戶端如何捕捉服務(wù)端的異常?

開發(fā) 架構(gòu)
在Java、C#等高級語言中,程序遇到無法處理的情況,或者不滿足運行條件時,比如除數(shù)是0的情況,底層代碼通常會通過拋出異常(Exception)的方式向上層傳遞問題,上層代碼通過 try-catch 的方式捕捉異常并進(jìn)行處理,不過這種方式一般只能在同一個進(jìn)程中使用,如果跨進(jìn)程就沒辦法直接使用了。

在微服務(wù)架構(gòu)或者分布式系統(tǒng)中,客戶端如何捕捉服務(wù)端的異常?

這里說的客戶端指調(diào)用方、服務(wù)端指被調(diào)用方,它們通常運行在不同的進(jìn)程之中,這些進(jìn)程可能運行在同一臺服務(wù)器,也可能運行在不同的服務(wù)器,甚至不同的數(shù)據(jù)機(jī)房;其使用的技術(shù)??赡芟嗤部赡艽嬖诤艽蟮牟町?。

為什么

在Java、C#等高級語言中,程序遇到無法處理的情況,或者不滿足運行條件時,比如除數(shù)是0的情況,底層代碼通常會通過拋出異常(Exception)的方式向上層傳遞問題,上層代碼通過 try-catch 的方式捕捉異常并進(jìn)行處理,不過這種方式一般只能在同一個進(jìn)程中使用,如果跨進(jìn)程就沒辦法直接使用了。

有的同學(xué)可能會問:為什么要跨進(jìn)程傳遞異常呢?

大家調(diào)用遠(yuǎn)程接口的時候可能有過這樣的體驗:

  • 首先遠(yuǎn)程接口可能會返回一些提前定義好的錯誤碼,此時我們需要從返回數(shù)據(jù)中提取這些錯誤碼,然后再根據(jù)不同的值進(jìn)行相應(yīng)的業(yè)務(wù)處理;
  • 其次我們還需要處理一些未知的錯誤,它們可能來源于服務(wù)端未注意到的地方,比如空指針問題,也可能是底層框架、操作系統(tǒng)或者硬件等拋出的一些問題,比如請求或者返回格式不匹配、網(wǎng)絡(luò)中斷、磁盤故障、內(nèi)存溢出、文件系統(tǒng)損壞等各種技術(shù)問題。

如此我們實際上需要面對兩種錯誤,而且需要采用不同的方式在不同的地方處理它們,這相當(dāng)繁瑣,心智負(fù)擔(dān)比較大。從Java、C#等轉(zhuǎn)Go的同學(xué)可能對此也深有體會,隨處可見的error判斷,還要留心panic的問題,當(dāng)然Go有自己的意圖和堅持,只是寫起來真的很糟心。

那我們有什么辦法來處理這個問題呢?我的選擇是全部統(tǒng)一為處理異常(Exception),異常中可以包含錯誤碼、錯誤描述,完全可以覆蓋錯誤碼的處理方式;而且異常不可避免,錯誤碼則都是上層應(yīng)用自己定義的。

基本原理

異常信息也是一種數(shù)據(jù),所以傳遞異常也是傳輸數(shù)據(jù)。我們想要把數(shù)據(jù)從一個進(jìn)程傳遞給另一個進(jìn)程有很多種方法,在微服務(wù)架構(gòu)或者分布式系統(tǒng)中,服務(wù)之間就是各種遠(yuǎn)程網(wǎng)絡(luò)調(diào)用,服務(wù)的具體實現(xiàn)可能是基于Http協(xié)議的Restful、gRPC,也可能是基于TCP的Dubbo等等,我們的異常信息傳遞也要基于這些框架的約定和底層通信協(xié)議。

以Restful為例,當(dāng)服務(wù)端產(chǎn)生異常時,我們通過攔截器或者程序內(nèi)部的中間件捕捉到這個異常,提取出其中的異常信息,并中斷這個異常的繼續(xù)拋出,然后把拿到的異常信息寫到HTTP Header中,返回到客戶端??蛻舳说腍TTP請求程序則從HTTP響應(yīng)的Header中讀取到這些異常信息,然后再把他們包裝成異常(Exception),throw 出來。最后客戶端中的業(yè)務(wù)代碼就可以使用 try-catch 捕捉到這個異常,并根據(jù)錯誤碼進(jìn)行相應(yīng)的處理。

圖片圖片

使用WCF、gPRC和Dubbo等框架時也是類似的方法,只是傳遞異常時其寫入和讀取的位置不同。比如Dubbo可以在其數(shù)據(jù)包的消息頭中聲明這是一個錯誤相應(yīng),并在消息體中包含詳細(xì)的異常信息;gPRC則可以利用它提供的Status來傳遞錯誤碼、錯誤描述和一些額外的參考信息。

使用Restful、gRPC等協(xié)議或者技術(shù)還有一個好處,那就是這些技術(shù)使用的協(xié)議是跨平臺的,你用Java開發(fā),他用Go開發(fā),你的程序跑在Windows上,他的程序跑在Linux上,這些都沒有問題,都可以按照一套規(guī)則正常通信,傳遞異常也完全沒有問題。

有的同學(xué)可能會擔(dān)心性能的問題,因為拋出異常時,程序通常要把整個調(diào)用堆棧回溯一遍,這個過程可能會消耗一些計算資源,特別是當(dāng)異常頻繁發(fā)生或堆棧層次很深時。不過正常情況下,各種防護(hù)到位時,異常應(yīng)該很少發(fā)生;而且現(xiàn)代編譯器和運行時環(huán)境也會對異常處理進(jìn)行優(yōu)化,以減少性能開銷。最后,異常處理機(jī)制的設(shè)計初衷是為了提高代碼的健壯性和可維護(hù)性,在大多數(shù)情況下,異常處理所帶來的性能開銷是可以接受的。

最佳實踐

接下來聊一些具體實現(xiàn)、遇到的問題和應(yīng)對方法。

拋出業(yè)務(wù)異常

服務(wù)在改變數(shù)據(jù)狀態(tài)之前,通常需要對數(shù)據(jù)進(jìn)行一些驗證,比如必填驗證、格式驗證、數(shù)據(jù)一致性驗證等等,如果驗證不通過,就要返回錯誤信息。

在傳統(tǒng)的方案中,我們可能會定義一個通用的消息格式,其中包含錯誤碼、錯誤描述,以及正常的業(yè)務(wù)字段,如下這樣:

public class Response{
  // 處理狀態(tài):錯誤碼、錯誤描述
  public int ErrCode{get;set;}
  public string ErrMsg{get;set;}

  // 處理成功時返回的業(yè)務(wù)數(shù)據(jù)
  public string UserId{get;set;}
  public string UserName{get;set;}
  ...
}

需要返回錯誤時,我們就會創(chuàng)建一個Response的實例,然后返回它,就像下邊這樣:

if(stirng.IsNullOrEmpty(id)){
  return new Response(100,"Id為空");
}

為了實現(xiàn)更為統(tǒng)一的錯誤處理方式,我們這里可以把返回Response實例的方式改為拋出異常。

if(stirng.IsNullOrEmpty(id)){
  throw new FireflySoftException(100,"Id為空");
}

如此,我們只需要在攔截器或者中間件中捕捉異常,并進(jìn)行相應(yīng)的處理就可以了,不管它是一個業(yè)務(wù)上的驗證錯誤,還是底層框架中的某種未知異常。

比如在ASP.NET Core的異常攔截器中可以這樣統(tǒng)一處理:

/// <summary>
/// WebAPI異常過濾器
/// </summary>
internal class WebAPIAsyncExceptionFilter : IAsyncExceptionFilter
{
    /// <summary>
    /// 異步異常處理
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task OnExceptionAsync(ExceptionContext context)
    {
          // 將自定義的異?;蛳到y(tǒng)自帶異常都轉(zhuǎn)換為一種異常
          FireflySoftException ex;
          if(context.Exception is FireflySoftException){
            ex = (FireflySoftException)context.Exception;
          }else{
            ex = ConvertToFireflySoftException(context.Exception);
          }

          // 將異常信息寫到 Http Header 中
          context.HttpContext.Response.StatusCode = 500;
          context.HttpContext.Response.Headers.Add("errcode", ex.Code.ToString());
          context.HttpContext.Response.Headers.Add("errmsg", System.Web.HttpUtility.UrlEncode(ex.Message));
          // 異常描述也寫到 Http Body 中,方便人看
          var bodyContent = Encoding.UTF8.GetBytes(ex.Message);
          await context.HttpContext.Response.Body.WriteAsync(bodyContent, 0, bodyContent.Length).ConfigureAwait(false);
          
          context.ExceptionHandled = true;
    }
}

在底層處理異常

不應(yīng)該讓業(yè)務(wù)程序開發(fā)者關(guān)心異常的傳遞實現(xiàn),比如上邊編寫的攔截器應(yīng)該內(nèi)置到團(tuán)隊的開發(fā)框架或者規(guī)范類庫中,業(yè)務(wù)程序開發(fā)者只需要拋出異常或者捕捉異常就夠了。

服務(wù)端的異常攔截器上邊已經(jīng)給了個例子,對于客戶端,我們可以通過包裝網(wǎng)絡(luò)請求方法來達(dá)到相同的目的。這里還是用ASP.NET Core舉個例子:

// 包裝的Post請求方法
public async Task<HttpResponseMessage> PostAsync<TRequest>(string hostAndPort, string resourceUri, TRequest request)
{
    string requestJson = JsonConvert.SerializeObject(request);
    var content = new StringContent(requestJson, Encoding.UTF8, "application/json");

    // 在實際的網(wǎng)絡(luò)請求外邊包一層
    return await DoHttp(async client =>
    {
        var uri = new Uri(client.BaseAddress, resourceUri);
        var requestMessage = new HttpRequestMessage()
        {
            Method = HttpMethod.Post,
            RequestUri = uri,
            Content = content
        };

        return await client.SendAsync(requestMessage).ConfigureAwait(false);
    }, hostAndPort).ConfigureAwait(false);
}

// 攔截HTTP錯誤并包裝為自定義的異常
private async Task<HttpResponseMessage> DoHttp(Func<HttpClient, Task<HttpResponseMessage>> action, string hostAndPort)
{
    HttpResponseMessage response;
    try
    {
        var client = GetHttpClient();
        response = await action(client).ConfigureAwait(false);
        return response.EnsureSuccessStatusCode();
    }
    catch (Exception ex)
    {
        // 如果 HTTP StatusCode 是錯誤碼,會進(jìn)入這里
        // 從 HTTP Header中提取錯誤碼和錯誤描述
        // 然后可以創(chuàng)建并拋出對應(yīng)的異常
         if (response.Headers.TryGetValues("errcode", out IEnumerable<string> errcodes))
         {
             var code = errcodes.FirstOrDefault();
             throw new FireflySoftException(code,"xxxxx");
         }
         ...
    }
}

如此,開發(fā)者通過Post調(diào)用接口時就可以這樣寫:

// 根據(jù)實際情況,可能需要try-catch,也可能不需要
try
{
  PostAsync("localhost:8080","api/getweather",new Request{
    City="帝都"
  })
}
catch(FireflySoftException ex)
{
    // 這里處理可能的業(yè)務(wù)異常
}

統(tǒng)一記錄異常日志

有的同學(xué)為了方便跟蹤異常信息,喜歡在程序中catch異常,并記錄到日志中。

如果使用統(tǒng)一的異常方式來處理錯誤,則都可以在攔截器或者中間件中來做這件事,只需要在其中加入日志的記錄邏輯就可以了。

當(dāng)然有些異常可能還是要 catch 一下的,比如“添加信息時重復(fù)提交”、“給用戶發(fā)消息時用戶已取消授權(quán)”等等,這些異??赡芏际且缓雎缘?,catch 住它們之后,程序可以吞掉這些異常,因為服務(wù)調(diào)用方也不關(guān)心這些異常,就沒必要再向上拋出。

區(qū)分Warn和Error

這里是說要給異常分個等級,有些異常就是個警告級別的,比如用戶沒有填寫某個參數(shù),只要告訴用戶就行了,運維或者開發(fā)者不太關(guān)心這些消息。有些異常則十分嚴(yán)重,比如空指針異常、除0異常等等,這往往說明程序存在BUG,需要反饋給開發(fā)者進(jìn)行修復(fù)。

我們可以在自定義的異常構(gòu)造函數(shù)中增加一個異常等級的參數(shù),如下所示:

if(stirng.IsNullOrEmpty(id)){
  throw new FireflySoftException(100,"Id為空",ErrorLevel.Light);
}

注意也不是所有的警告都無需管理員過問,比如對于一個網(wǎng)絡(luò)請求庫,我們可能只是把請求超時作為一種警告,但是如果超時發(fā)生的非常頻繁,也需要通知管理員來進(jìn)行關(guān)注。

根據(jù)異常級別,我們就可以記錄不同級別的日志,然后監(jiān)控程序就可以根據(jù)日志級別和相應(yīng)的頻率為管理員提供相應(yīng)的處理建議。

返回200還是500

使用HTTP作為服務(wù)之間的通信協(xié)議時,發(fā)生異常時服務(wù)端一般會返回500錯誤,也就是 HTTP StatusCode = 500,這一般是底層通信框架的默認(rèn)設(shè)計。但是這會導(dǎo)致一個監(jiān)控問題,監(jiān)控程序會跟蹤服務(wù)調(diào)用之間的HTTP狀態(tài),如果遇到500錯誤,它就會認(rèn)為程序發(fā)生了錯誤,而這個錯誤可能只是一個參數(shù)驗證不通過的情況,管理員不需要關(guān)心這個問題。

此時我們可以在攔截器中處理異常的地方稍微改造一下,將所有的HTTP狀態(tài)碼都改為200,或者當(dāng)錯誤級別比較輕(ErrorLevel.Light)時設(shè)置為200,錯誤級別比較重(ErrorLevel.Heavy)時設(shè)置為500。

context.HttpContext.Response.StatusCode = 200;

這樣做并不影響客戶端對錯誤的處理,因為不管HTTP的狀態(tài)碼如何,客戶端都可以從HTTP Header中提取處理錯誤所需的錯誤碼和錯誤描述。

自動重試

有時服務(wù)端的錯誤可能只是瞬時的,或者只是多個節(jié)點中的少數(shù)節(jié)點不可用,重新發(fā)起請求就能成功完成調(diào)用。

我們可以把這個重試機(jī)制包裝到網(wǎng)絡(luò)請求方法中,減少業(yè)務(wù)程序中處理重試的代碼量,此舉也能更好的規(guī)范代碼,避免BUG或者性能問題。

一種可行的方法是,我們根據(jù)異常的類型或者提前約定好的錯誤碼,在包裝的網(wǎng)絡(luò)請求方法中針對這些異常進(jìn)行特殊處理。具體實現(xiàn)可以參考下邊的代碼:

private async Task<HttpResponseMessage> DoHttp(Func<HttpClient, Task<HttpResponseMessage>> action, string hostAndPort)
{
  int tryCount = 0;
  while (true)
  {
      HttpResponseMessage response;
      try
      {
          var client = GetHttpClient();
          response = await action(client).ConfigureAwait(false);
          return response.EnsureSuccessStatusCode();
      }
      catch (Exception ex)
      {
           // 遇到某種特定的異常時,我們就進(jìn)行一次重試
           if (ex is TaskCanceledException)
           {
              if(tryCount<1){
                tryCount++;
                continue;
              }
              throw;
           }
           ...
      }
  }
}

以上就是本文的主要內(nèi)容,文章雖然描述了微服務(wù)架構(gòu)下異常傳遞的基本原理,也探討了一些具體的實踐方法,但要完完整整的實現(xiàn)并集成到自己的開發(fā)框架中,必然還有很多的工作要做,比如錯誤碼的定義,異常處理與限流、熔斷等的整合,等等。

責(zé)任編輯:武曉燕 來源: 螢火架構(gòu)
相關(guān)推薦

2009-08-21 15:59:22

服務(wù)端與客戶端通信

2009-08-21 16:14:52

服務(wù)端與客戶端通信

2011-09-09 09:44:23

WCF

2010-11-19 14:22:04

oracle服務(wù)端

2010-03-18 17:47:07

Java 多客戶端通信

2023-03-06 08:01:56

MySQLCtrl + C

2009-08-21 15:36:41

服務(wù)端與客戶端

2009-08-21 15:54:40

服務(wù)端與客戶端

2021-10-19 08:58:48

Java 語言 Java 基礎(chǔ)

2015-01-13 10:32:23

RestfulWeb框架

2023-04-03 08:13:05

MySQLCtrl + C

2023-10-30 09:06:22

2011-06-09 10:51:26

Qt 服務(wù)器 客戶端

2022-09-05 14:36:26

服務(wù)端TCP連接

2021-06-11 06:54:34

Dubbo客戶端服務(wù)端

2010-05-28 14:11:37

SVN1.6

2018-12-19 10:31:32

客戶端IP服務(wù)器

2009-08-18 12:51:19

服務(wù)器+客戶端

2025-05-12 03:02:00

SpringOAuth2客戶端

2021-07-16 06:56:50

Nacos注冊源碼
點贊
收藏

51CTO技術(shù)棧公眾號