微服务架构,客户端如何catch服务端的异常?

这篇具有很好参考价值的文章主要介绍了微服务架构,客户端如何catch服务端的异常?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

在微服务架构或者分布式系统中,客户端如何捕捉服务端的异常?

这里说的客户端指调用方、服务端指被调用方,它们通常运行在不同的进程之中,这些进程可能运行在同一台服务器,也可能运行在不同的服务器,甚至不同的数据机房;其使用的技术栈可能相同,也可能存在很大的差异。

为什么

在Java、C#等高级语言中,程序遇到无法处理的情况,或者不满足运行条件时,比如除数是0的情况,底层代码通常会通过抛出异常(Exception)的方式向上层传递问题,上层代码通过 try-catch 的方式捕捉异常并进行处理,不过这种方式一般只能在同一个进程中使用,如果跨进程就没办法直接使用了。

有的同学可能会问:为什么要跨进程传递异常呢?

大家调用远程接口的时候可能有过这样的体验:

  • 首先远程接口可能会返回一些提前定义好的错误码,此时我们需要从返回数据中提取这些错误码,然后再根据不同的值进行相应的业务处理;
  • 其次我们还需要处理一些未知的错误,它们可能来源于服务端未注意到的地方,比如空指针问题,也可能是底层框架、操作系统或者硬件等抛出的一些问题,比如请求或者返回格式不匹配、网络中断、磁盘故障、内存溢出、文件系统损坏等各种技术问题。

如此我们实际上需要面对两种错误,而且需要采用不同的方式在不同的地方处理它们,这相当繁琐,心智负担比较大。从Java、C#等转Go的同学可能对此也深有体会,随处可见的error判断,还要留心panic的问题,当然Go有自己的意图和坚持,只是写起来真的很糟心。

那我们有什么办法来处理这个问题呢?我的选择是全部统一为处理异常(Exception),异常中可以包含错误码、错误描述,完全可以覆盖错误码的处理方式;而且异常不可避免,错误码则都是上层应用自己定义的。

基本原理

异常信息也是一种数据,所以传递异常也是传输数据。我们想要把数据从一个进程传递给另一个进程有很多种方法,在微服务架构或者分布式系统中,服务之间就是各种远程网络调用,服务的具体实现可能是基于Http协议的Restful、gRPC,也可能是基于TCP的Dubbo等等,我们的异常信息传递也要基于这些框架的约定和底层通信协议。

以Restful为例,当服务端产生异常时,我们通过拦截器或者程序内部的中间件捕捉到这个异常,提取出其中的异常信息,并中断这个异常的继续抛出,然后把拿到的异常信息写到HTTP Header中,返回到客户端。客户端的HTTP请求程序则从HTTP响应的Header中读取到这些异常信息,然后再把他们包装成异常(Exception),throw 出来。最后客户端中的业务代码就可以使用 try-catch 捕捉到这个异常,并根据错误码进行相应的处理。

微服务架构,客户端如何catch服务端的异常?

使用WCF、gPRC和Dubbo等框架时也是类似的方法,只是传递异常时其写入和读取的位置不同。比如Dubbo可以在其数据包的消息头中声明这是一个错误相应,并在消息体中包含详细的异常信息;gPRC则可以利用它提供的Status来传递错误码、错误描述和一些额外的参考信息。

使用Restful、gRPC等协议或者技术还有一个好处,那就是这些技术使用的协议是跨平台的,你用Java开发,他用Go开发,你的程序跑在Windows上,他的程序跑在Linux上,这些都没有问题,都可以按照一套规则正常通信,传递异常也完全没有问题。

有的同学可能会担心性能的问题,因为抛出异常时,程序通常要把整个调用堆栈回溯一遍,这个过程可能会消耗一些计算资源,特别是当异常频繁发生或堆栈层次很深时。不过正常情况下,各种防护到位时,异常应该很少发生;而且现代编译器和运行时环境也会对异常处理进行优化,以减少性能开销。最后,异常处理机制的设计初衷是为了提高代码的健壮性和可维护性,在大多数情况下,异常处理所带来的性能开销是可以接受的。

最佳实践

接下来聊一些具体实现、遇到的问题和应对方法。

抛出业务异常

服务在改变数据状态之前,通常需要对数据进行一些验证,比如必填验证、格式验证、数据一致性验证等等,如果验证不通过,就要返回错误信息。

在传统的方案中,我们可能会定义一个通用的消息格式,其中包含错误码、错误描述,以及正常的业务字段,如下这样:

public class Response{
  // 处理状态:错误码、错误描述
  public int ErrCode{get;set;}
  public string ErrMsg{get;set;}

  // 处理成功时返回的业务数据
  public string UserId{get;set;}
  public string UserName{get;set;}
  ...
}

需要返回错误时,我们就会创建一个Response的实例,然后返回它,就像下边这样:

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

为了实现更为统一的错误处理方式,我们这里可以把返回Response实例的方式改为抛出异常。

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

如此,我们只需要在拦截器或者中间件中捕捉异常,并进行相应的处理就可以了,不管它是一个业务上的验证错误,还是底层框架中的某种未知异常。

比如在ASP.NET Core的异常拦截器中可以这样统一处理:

/// <summary>
/// WebAPI异常过滤器
/// </summary>
internal class WebAPIAsyncExceptionFilter : IAsyncExceptionFilter
{
    /// <summary>
    /// 异步异常处理
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task OnExceptionAsync(ExceptionContext context)
    {
          // 将自定义的异常或系统自带异常都转换为一种异常
          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;
    }
}

在底层处理异常

不应该让业务程序开发者关心异常的传递实现,比如上边编写的拦截器应该内置到团队的开发框架或者规范类库中,业务程序开发者只需要抛出异常或者捕捉异常就够了。

服务端的异常拦截器上边已经给了个例子,对于客户端,我们可以通过包装网络请求方法来达到相同的目的。这里还是用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");

    // 在实际的网络请求外边包一层
    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 是错误码,会进入这里
        // 从 HTTP Header中提取错误码和错误描述
        // 然后可以创建并抛出对应的异常
         if (response.Headers.TryGetValues("errcode", out IEnumerable<string> errcodes))
         {
             var code = errcodes.FirstOrDefault();
             throw new FireflySoftException(code,"xxxxx");
         }
         ...
    }
}

如此,开发者通过Post调用接口时就可以这样写:

// 根据实际情况,可能需要try-catch,也可能不需要
try
{
  PostAsync("localhost:8080","api/getweather",new Request{
    City="帝都"
  })
}
catch(FireflySoftException ex)
{
    // 这里处理可能的业务异常
}

统一记录异常日志

有的同学为了方便跟踪异常信息,喜欢在程序中catch异常,并记录到日志中。

如果使用统一的异常方式来处理错误,则都可以在拦截器或者中间件中来做这件事,只需要在其中加入日志的记录逻辑就可以了。

当然有些异常可能还是要 catch 一下的,比如“添加信息时重复提交”、“给用户发消息时用户已取消授权”等等,这些异常可能都是要被忽略的,catch 住它们之后,程序可以吞掉这些异常,因为服务调用方也不关心这些异常,就没必要再向上抛出。

区分Warn和Error

这里是说要给异常分个等级,有些异常就是个警告级别的,比如用户没有填写某个参数,只要告诉用户就行了,运维或者开发者不太关心这些消息。有些异常则十分严重,比如空指针异常、除0异常等等,这往往说明程序存在BUG,需要反馈给开发者进行修复。

我们可以在自定义的异常构造函数中增加一个异常等级的参数,如下所示:

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

注意也不是所有的警告都无需管理员过问,比如对于一个网络请求库,我们可能只是把请求超时作为一种警告,但是如果超时发生的非常频繁,也需要通知管理员来进行关注。

根据异常级别,我们就可以记录不同级别的日志,然后监控程序就可以根据日志级别和相应的频率为管理员提供相应的处理建议。

返回200还是500

使用HTTP作为服务之间的通信协议时,发生异常时服务端一般会返回500错误,也就是 HTTP StatusCode = 500,这一般是底层通信框架的默认设计。但是这会导致一个监控问题,监控程序会跟踪服务调用之间的HTTP状态,如果遇到500错误,它就会认为程序发生了错误,而这个错误可能只是一个参数验证不通过的情况,管理员不需要关心这个问题。

此时我们可以在拦截器中处理异常的地方稍微改造一下,将所有的HTTP状态码都改为200,或者当错误级别比较轻(ErrorLevel.Light)时设置为200,错误级别比较重(ErrorLevel.Heavy)时设置为500。

context.HttpContext.Response.StatusCode = 200;

这样做并不影响客户端对错误的处理,因为不管HTTP的状态码如何,客户端都可以从HTTP Header中提取处理错误所需的错误码和错误描述。

自动重试

有时服务端的错误可能只是瞬时的,或者只是多个节点中的少数节点不可用,重新发起请求就能成功完成调用。

我们可以把这个重试机制包装到网络请求方法中,减少业务程序中处理重试的代码量,此举也能更好的规范代码,避免BUG或者性能问题。

一种可行的方法是,我们根据异常的类型或者提前约定好的错误码,在包装的网络请求方法中针对这些异常进行特殊处理。具体实现可以参考下边的代码:

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)
      {
           // 遇到某种特定的异常时,我们就进行一次重试
           if (ex is TaskCanceledException)
           {
              if(tryCount<1){
                tryCount++;
                continue;
              }
              throw;
           }
           ...
      }
  }
}

以上就是本文的主要内容,文章虽然描述了微服务架构下异常传递的基本原理,也探讨了一些具体的实践方法,但要完完整整的实现并集成到自己的开发框架中,必然还有很多的工作要做,比如错误码的定义,异常处理与限流、熔断等的整合,等等。

文章难免错漏,如有问题欢迎交流讨论。

关注萤火架构,加速技术提升!文章来源地址https://www.toymoban.com/news/detail-837844.html

到了这里,关于微服务架构,客户端如何catch服务端的异常?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • AI对话交互场景使用WebSocket建立H5客户端和服务端的信息实时双向通信

    WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许 服务端主动向客户端推送数据 。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。 初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HT

    2024年02月03日
    浏览(53)
  • TCP服务器监测客户端异常退出方法

            作为服务器必须得具备监测客户端状态得机制,以保证客户端处于不同的状态,服务器进行不同得状态处理,依次来提高实时性,可控性,并且有利于服务器得内存管理。其中客户端得异常处理就属于其中得一种。         客户端得断开情形无非就两种情况:

    2024年02月09日
    浏览(54)
  • C++项目——集群聊天服务器项目(九)客户端异常退出业务

    服务器端应检测到客户端是否异常退出,因此本节来实现客户端异常退出,项目流程见后文 (1)在业务模块定义处理客户端异常退出的函数 (2)集群聊天服务器项目(八)提到,哈希表_userConnMap存储用户的id和通信连接,若客户端异常退出,表示该客户端登录用户已经下线,

    2024年04月17日
    浏览(48)
  • TCP通信—客户端与客户端的双向通信

    1.实现客户端与客户端之间的TCP双向通信; 2.服务器记录客户端实名连接,并显示客户端数据记录; 3.客户端退出,服务器和另一客户端显示相应提示; 1.定义数组保存客户端文件描述符; 2.主程序负责接收客户端1的数据发送给客户端2; 3.开展一个线程负责接收到客户端2的

    2024年02月15日
    浏览(44)
  • golang kafka客户端 sarama 在 rebalance时异常如何解决

    在使用sarama作为Kafka客户端的过程中,在进行消费者分区的rebalance操作时,可能会发生异常,在解决这些异常一般可以采取以下措施: 1. 异常处理:在consumer rebalance过程中如果发生异常,Sarama库将会发出错误事件(error event)。因此在编写代码时应该注册错误事件处理函数

    2024年02月10日
    浏览(50)
  • ios客户端学习笔记(七):iOS客户端的UI设计

    iOS客户端的UI设计是指在iOS操作系统上开发应用程序时所涉及的用户界面设计,包括应用程序的布局、颜色、字体、图标等元素的设计。良好的UI设计应该能够提高用户体验,使用户能够轻松地使用应用程序。 在iOS客户端的UI设计中,需要考虑以下几个方面: 应用程序布局应

    2023年04月26日
    浏览(48)
  • 【高并发网络通信架构】2.引入多线程实现多客户端连接的tcp服务端

    目录 一,往期文章 二,代码实现 关键代码 完整代码 运行效果 【高并发网络通信架构】1.Linux下实现单客户连接的tcp服务端 因为accept是阻塞等待客户端连接,当客户端连接成功后才会执行accept后面的代码,所以为实现多个客户端连接,第一步是将accept放在master循环里。 rec

    2024年02月13日
    浏览(47)
  • RustDesk自建中转服务器如何自己编译 RustDesk客户端,将企业固定IP/域名写进客户端,客户端安装无需配置直接使用(三)

    Ubuntu20.04.4 LTS Docker Version: 20.10.12 RustDesk1.20 Git-2.39.0-64-bit visual studio 2022 VSCodeUserSetup-x64-1.74.1 RustDesk自建中转服务器如何自己编译 RustDesk客户端,将企业固定IP/域名写进客户端,客户端安装无需配置直接使 最近有粉丝反映,编译最后会提示key不匹配等问题,拉取普通开源版,别

    2024年02月05日
    浏览(170)
  • java-websocket服务端、客户端及如何测试

    1. 导入依赖 2. 服务端实现 (1)基础版 (2)优化版 对String分片转换为Listbyte[] 3. 客户端实现 4. websocket服务端测试方法 (1)自己编写一个客户端 ​ 使用上面的java客户端就可以直接调用测试。 (2)使用postman测试 ​ postman版本需要在v8.0以上才有websocket的接口测试

    2024年02月11日
    浏览(52)
  • 【全方位解析】如何获取客户端/服务端真实 IP

    1.比如在投票系统开发中,为了防止刷票,我们需要限制每个 IP 地址只能投票一次 2.当网站受到诸如 DDoS(Distributed Denial of Service,分布式拒绝服务攻击)等攻击时,我们需要快速定位攻击者 IP 3.在渗透测试过程中,经常会碰到网站有 CDN(Content Distribution Network,内容交付网络

    2024年02月07日
    浏览(58)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包