異常的正確使用在微服務架構中的重要性排前三,沒什麼意見吧

異常的正確使用在微服務架構中的重要性排前三,沒什麼意見吧php

Curdboy 們很久不見,先祝你們端午節快樂。最近想說說異常,個人思考儼然造成了閉環,但願這套組合拳能對你的業務代碼有所幫助。前端

下面只討論世界上最好的語言和生態最完整的語言,沒什麼意見吧。java

異常的異同

PHP 在 PHP7 異常的設計和 Java 保持一致了 Exception extends Throwable ,不過在歷史緣由和設計理念上仍是有一些細微的差異。好比 PHP 中的異常是有 code 屬性的,這樣就存在多種異常聚類爲同一個異常,而後在catch 區塊里根據 code 寫不一樣的業務邏輯代碼。json

而 Java 異常則沒有code ,不能這樣設計,只能針對不一樣的狀況使用不一樣的異常。因此咱們習慣服務對外暴露的經過包裝類來封裝,而不是直接依賴異常的透傳。架構

統一異常的處理

在 Java 代碼裏,最讓人詬病的就是漫山遍野的try catch ,沒什麼意見吧。隨便抓一段代碼分佈式

@Override
public DataResult<List<AdsDTO>> getAds(Integer liveId) {
    
    try {
        List<AdsDTO> adsDTO = new ArrayList<>();
        //...業務邏輯省略
        DataResult.success(adsDTO);
    } catch (Exception e) {
        log.error("getAds has Exception:{}", e.getMessage(), e);
        DataResult.failure(ResultCode.CODE_INTERNAL_ERROR, e.getMessage()); // 將異常信息返回給服務端調用方
    }
    
    return dataResult;
}

不少時候都是無腦上來就先寫個 try catch 再說,無論裏面是否會有非運行時異常。比較好的方式是使用 aop 的方式來攔截全部的服務方法的調用,統一接管異常而後作處理。ide

@Around("recordLog()")
public Object record(ProceedingJoinPoint joinPoint) throws Throwable {
  //... 請求調用來源記錄
  
  Object result;

  try {
    result = joinPoint.proceed(joinPoint.getArgs());
  } catch (Exception e) {
    //... 記錄異常日誌
    
    DataResult<Object> res = DataResult.failure(ResultCode.CODE_INTERNAL_ERROR, e.getMessage());
    result = res;
  }

    //... 返回值日誌記錄
  
  return result;
}

有一點小問題,若是直接將 A 服務的異常信息直接返回給調用者 B,可能存在一些潛在的風險,永遠不能相信調用者,即便他根正苗紅三代貧農也不行。由於不能肯定調用者會將該錯誤信息做何處理,可能就直接做爲 json 返回給了前端。函數

RuntimeException

在 Java 中異常能夠分爲運行時異常和非運行時異常,運行時異常是不須要捕獲的,在方法上也不須要標註 throw Exception,好比咱們在方法裏使用 guava 包裏的Preconditions工具類,拋出的IllegalArgumentException也是運行時異常。微服務

@Override
public DataResult<List<AdsDTO>> getAds(Integer liveId) {
  Preconditions.checkArgument(null != liveId, "liveIds not be null");
  
  List<AdsDTO> adsDTOS = new ArrayList<>();
  //...業務邏輯省略
  return DataResult.success(adsDTOS);
}

咱們也可使用該特性,自定義本身的業務異常類繼承RuntimeException工具

XXServiceRuntimeException extends RuntimeException

對於不符合業務邏輯狀況則直接拋出 XXServiceRuntimeException

@Override
public DataResult<List<AdsDTO>> getAds(Integer liveId) {

  if (null == liveId) {
    throw new XXServiceRuntimeException("liveId can't be null");
  }
  
  List<AdsDTO> adsDTOS = new ArrayList<>();
  //...業務邏輯省略
  return DataResult.success(adsDTOS);
}

而後在 aop 作統一處理作相應的優化,對於前面比較粗暴的作法,應該將除了XXServiceRuntimeExceptionIllegalArgumentException以外的異常內部記錄,再也不對外暴露,可是必定要記得經過requestId將分佈式鏈路串起來,在DataResult中返回,方便問題的排查。

@Around("recordLog()")
public Object record(ProceedingJoinPoint joinPoint) throws Throwable {
  //... 請求調用來源記錄
  
  Object result;

  try {
    result = joinPoint.proceed(joinPoint.getArgs());
  } catch (Exception e) {
    //... 記錄異常日誌①
    log.error("{}#{}, exception:{}:", clazzSimpleName, methodName, e.getClass().getSimpleName(), e);
    
    DataResult<Object> res = DataResult.failure(ResultCode.CODE_INTERNAL_ERROR);
    if (e instanceof XXServiceRuntimeException || e instanceof IllegalArgumentException) {
       res.setMessage(e.getMessage());
    }
 
    result = res;
  }

  if (result instanceof DataResult) {
      ((DataResult) result).setRequestId(EagleEye.getTraceId()); // DMC 
  }

    //... 返回值日誌記錄
  
  return result;
}

異常監控

說好的閉環呢,使用了自定義異常類以後,對異常日誌的監控報警的閾值就能夠下降很多,報警更加精準,以阿里雲 SLS 的監控爲例

* and ERROR not XXServiceRuntimeException not IllegalArgumentException|SELECT COUNT(*) AS count
這裏監控的是 記錄異常日誌① 的日誌

PHP 裏的異常

上面 Java 裏說到的問題在 PHP 裏也一樣存在,不用 3 種方法來模擬 aop 都不能體現 PHP 是世界上最好的語言

//1. call_user_func_array
//2. 反射
//3. 直接 new
try {
  $class = new $className();
  $result = $class->$methodName();
} catch (\Throwable $e) {
    //...略
}

相似上面的架構邏輯再也不重複編寫僞代碼,基本保持一致。也是自定義本身的業務異常類繼承RuntimeException,而後作對外輸出處理。

可是PHP 裏有一些歷史包袱,起初設計的時候不少運行時異常都是做爲 NoticeWarning 錯誤輸出的,可是錯誤的輸出缺乏調用棧,不利於問題的排查

function foo(){
  return boo("xxx");
}

function boo($a){
  return explode($a);
}

foo();
Warning: explode() expects at least 2 parameters, 1 given in /Users/mengkang/Downloads/ab.php on line 8

看不到具體的參數,也看不到調用棧。若是使用set_error_handler + ErrorException以後,就很是清晰了。

set_error_handler(function ($severity, $message, $file, $line) {
    throw new ErrorException($message, 10001, $severity, $file, $line);
});

function foo(){
  return boo("xxx");
}

function boo($a){
  return explode($a);
}

try{
  foo();
}catch(Exception $e){
  echo $e->getTraceAsString();
}

最後打印出來的信息就是

Fatal error: Uncaught ErrorException: explode() expects at least 2 parameters, 1 given in /Users/mengkang/Downloads/ab.php:12
Stack trace:
#0 [internal function]: {closure}(2, 'explode() expec...', '/Users/mengkang...', 12, Array)
#1 /Users/mengkang/Downloads/ab.php(12): explode('xxx')
#2 /Users/mengkang/Downloads/ab.php(8): boo('xxx')
#3 /Users/mengkang/Downloads/ab.php(15): foo()
#4 {main}
  thrown in /Users/mengkang/Downloads/ab.php on line 12

修改上面的函數

function boo(array $a){
  return implode(",", $a);
}

則無法捕獲了,由於拋出的是PHP Fatal error: Uncaught TypeError,PHP7 新增了
class Error implements Throwable,則在 PHP 系統錯誤日誌裏會有 Stack,可是不能和整個業務系統串聯起來,這裏就又不得不說日誌的設計,咱們指望像 Java 那樣經過一個 traceId 將全部的日誌串聯起來,從 Nginx 日誌到 PHP 裏的正常 info level 日誌以及這些Uncaught TypeError,因此接管默認輸出到系統錯誤日誌,在 catch 代碼塊中記錄到統一的地方。那麼這裏就簡單修改成

set_error_handler(function ($severity, $message, $file, $line) {
    throw new ErrorException($message, 10001, $severity, $file, $line);
});

function foo(){
  return boo("xxx");
}

function boo(array $a){
  return implode(",", $a);
}

try{
  foo();
}catch(Throwable $e){
  echo $e->getTraceAsString();
}

catch Throwable就能接受ErrorException了。

可是 set_error_handler 沒辦法處理一些錯誤,好比E_PARSE的錯誤,能夠用register_shutdown_function來兜底。

值得注意的是 register_shutdown_function的用意是在腳本正常退出或顯示調用exit時,執行註冊的函數。
是腳本運行(run-time not parse-time)出錯退出時,才能使用。若是在調用 register_shutdown_function的同一文件的裏面有語法錯誤,是沒法註冊的, 可是咱們項目通常都是分多個文件的,這樣就其餘文件裏有語法錯誤,也能捕獲了
register_shutdown_function(function(){
    $e = error_get_last();
    if ($e){
        throw new \ErrorException($e["message"], 10002, E_ERROR, $e["file"], $e["line"]);
    }
});

若是你想直接使用這些代碼(PHP的)直接到項目可能會有不少坑,由於咱們習慣了系統中有不少 notice 了,能夠將 notice 的錯誤轉成異常以後主動記錄,可是不對外拋出異常便可。

今天先到這裏,下次說下日誌應該怎麼記。

雖然 PHP 大環境的發展上彷佛不太明朗,可是由於愛因此持續吧。某些場景下仍是最佳選擇的。

相關文章
相關標籤/搜索