微服务从代码到k8s部署应有尽有系列(十、错误处理)

简介: 微服务从代码到k8s部署应有尽有系列(十、错误处理)

我们用一个系列来讲解从需求到上线、从代码到k8s部署、从日志到监控等各个方面的微服务完整实践。

整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中间件,所用到的技术栈基本是go-zero项目组的自研组件,基本是go-zero全家桶了。

实战项目地址:https://github.com/Mikaelemmmm/go-zero-looklook

1、概述

我们在平时开发时候,程序在出错时,希望可以通过错误日志能快速定位问题(那么传递进来的参数、包括堆栈信息肯定就要都要打印到日志),但同时又想返回给前端用户比较友善、能看得懂的错误提示,那这两点如果只通过一个fmt.Error、errors.new等返回一个错误信息肯定是无法做到的,除非在返回前端错误提示的地方同时在记录log,这样的话日志满天飞,代码难看不说,日志到时候也会很难看。

那么我们想一下,如果有一个统一的地方记录日志,同时在业务代码中只需要一个return err 就能将返回给前端的错误提示信息、日志记录相信信息分开提示跟记录,如果按照这个思路实现,那简直不要太爽,是的 go-zero-looklook就是这么处理的,接下来我们看下。

2、rpc错误处理

按照正常情况下,go-zero的rpc服务是基于grpc的,默认返回的错误是grpc的status.Error 没法给我们自定义的错误合并,并且也不适合我们自定义的错误,它的错误码、错误类型都是定义死在grpc包中的,ok ,如果我们在rpc中能用自定义错误返回,然后在拦截器统一返回时候转成grpc的status.Error  , 那么我们rpc的err跟api的err是不是可以统一管理我们自己的错误了呢?

我们看一下grpc的status.Error的code里面是什么

package codes // import "google.golang.org/grpc/codes"
import (
    "fmt"
    "strconv"
)
// A Code is an unsigned 32-bit error code as defined in the gRPC spec.
type Code uint32
.......

grpc的err对应的错误码其实就是一个uint32 , 我们自己定义错误用uint32然后在rpc的全局拦截器返回时候转成grpc的err,就可以了

所以我们自己定义全局错误码在app/common/xerr

errCode.go

package xerr
// 成功返回
const OK uint32 = 200
// 前3位代表业务,后三位代表具体功能
// 全局错误码
const SERVER_COMMON_ERROR uint32 = 100001
const REUQES_PARAM_ERROR uint32 = 100002
const TOKEN_EXPIRE_ERROR uint32 = 100003
const TOKEN_GENERATE_ERROR uint32 = 100004
const DB_ERROR uint32 = 100005
// 用户模块

errMsg.go

package xerr
var message map[uint32]string
func init() {
   message = make(map[uint32]string)
   message[OK] = "SUCCESS"
   message[SERVER_COMMON_ERROR] = "服务器开小差啦,稍后再来试一试"
   message[REUQES_PARAM_ERROR] = "参数错误"
   message[TOKEN_EXPIRE_ERROR] = "token失效,请重新登陆"
   message[TOKEN_GENERATE_ERROR] = "生成token失败"
   message[DB_ERROR] = "数据库繁忙,请稍后再试"
}
func MapErrMsg(errcode uint32) string {
   if msg, ok := message[errcode]; ok {
      return msg
   } else {
      return "服务器开小差啦,稍后再来试一试"
   }
}
func IsCodeErr(errcode uint32) bool {
   if _, ok := message[errcode]; ok {
      return true
   } else {
      return false
   }
}

errors.go

package xerr
import "fmt"
// 常用通用固定错误
type CodeError struct {
   errCode uint32
   errMsg  string
}
// 返回给前端的错误码
func (e *CodeError) GetErrCode() uint32 {
   return e.errCode
}
// 返回给前端显示端错误信息
func (e *CodeError) GetErrMsg() string {
   return e.errMsg
}
func (e *CodeError) Error() string {
   return fmt.Sprintf("ErrCode:%d,ErrMsg:%s", e.errCode, e.errMsg)
}
func NewErrCodeMsg(errCode uint32, errMsg string) *CodeError {
   return &CodeError{errCode: errCode, errMsg: errMsg}
}
func NewErrCode(errCode uint32) *CodeError {
   return &CodeError{errCode: errCode, errMsg: MapErrMsg(errCode)}
}
func NewErrMsg(errMsg string) *CodeError {
   return &CodeError{errCode: SERVER_COMMON_ERROR, errMsg: errMsg}
}

比如我们在用户注册时候的rpc代码

package logic
import (
    "context"
    "looklook/app/identity/cmd/rpc/identity"
    "looklook/app/usercenter/cmd/rpc/internal/svc"
    "looklook/app/usercenter/cmd/rpc/usercenter"
    "looklook/app/usercenter/model"
    "looklook/common/xerr"
    "github.com/pkg/errors"
    "github.com/tal-tech/go-zero/core/logx"
    "github.com/tal-tech/go-zero/core/stores/sqlx"
)
var ErrUserAlreadyRegisterError = xerr.NewErrMsg("该用户已被注册")
type RegisterLogic struct {
    ctx    context.Context
    svcCtx *svc.ServiceContext
    logx.Logger
}
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
    return &RegisterLogic{
        ctx:    ctx,
        svcCtx: svcCtx,
        Logger: logx.WithContext(ctx),
    }
}
func (l *RegisterLogic) Register(in *usercenter.RegisterReq) (*usercenter.RegisterResp, error) {
    user, err := l.svcCtx.UserModel.FindOneByMobile(in.Mobile)
    if err != nil && err != model.ErrNotFound {
        return nil, errors.Wrapf(xerr.ErrDBError, "mobile:%s,err:%v", in.Mobile, err)
    }
    if user != nil {
        return nil, errors.Wrapf(ErrUserAlreadyRegisterError, "用户已经存在 mobile:%s,err:%v", in.Mobile, err)
    }
    var userId int64
    if err := l.svcCtx.UserModel.Trans(func(session sqlx.Session) error {
        user := new(model.User)
        user.Mobile = in.Mobile
        user.Nickname = in.Nickname
        insertResult, err := l.svcCtx.UserModel.Insert(session, user)
        if err != nil {
            return errors.Wrapf(xerr.ErrDBError, "err:%v,user:%+v", err, user)
        }
        lastId, err := insertResult.LastInsertId()
        if err != nil {
            return errors.Wrapf(xerr.ErrDBError, "insertResult.LastInsertId err:%v,user:%+v", err, user)
        }        userId = lastId
        userAuth := new(model.UserAuth)
        userAuth.UserId = lastId
        userAuth.AuthKey = in.AuthKey
        userAuth.AuthType = in.AuthType
        if _, err := l.svcCtx.UserAuthModel.Insert(session, userAuth); err != nil {
            return errors.Wrapf(xerr.ErrDBError, "err:%v,userAuth:%v", err, userAuth)
        }
        return nil
    }); err != nil {
        return nil, err
    }
    // 2、生成token.
    resp, err := l.svcCtx.IdentityRpc.GenerateToken(l.ctx, &identity.GenerateTokenReq{
        UserId: userId,
    })
    if err != nil {
        return nil, errors.Wrapf(ErrGenerateTokenError, "IdentityRpc.GenerateToken userId : %d , err:%+v", userId, err)
    }
    return &usercenter.RegisterResp{
        AccessToken:  resp.AccessToken,
        AccessExpire: resp.AccessExpire,
        RefreshAfter: resp.RefreshAfter,
    }, nil
}
errors.Wrapf(ErrUserAlreadyRegisterError, "用户已经存在 mobile:%s,err:%v", in.Mobile, err)

这里我们使用go默认的errors的包的errors.Wrapf ( 如果这里不明白就去查一下go的errors包下的Wrap、 Wrapf等)

第一个参数, ErrUserAlreadyRegisterError 定义在上方 就是使用xerr.NewErrMsg("该用户已被注册") , 返回给前端友好的提示,要记住这里用的是我们xerr包下的方法

第二个参数,就是记录在服务器日志,可以写详细一点都没关系只会记录在服务器不会被返回给前端

那我们来看看为什么第一个参数就能是返回给前端的,第二个参数就是记录日志的

⚠️【注】我们在rpc的启动文件main方法中,加了grpc的全局拦截器,这个很重要 ,如果不加这个没办法实现

package main
......
func main() {
    ........
    //rpc log,grpc的全局拦截器
    s.AddUnaryInterceptors(rpcserver.LoggerInterceptor)
    .......
}

我们看看rpcserver.LoggerInterceptor的具体实现

import (
    ...
    "github.com/pkg/errors"
)
func LoggerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
   resp, err = handler(ctx, req)
   if err != nil {
      causeErr := errors.Cause(err)                // err类型
      if e, ok := causeErr.(*xerr.CodeError); ok { //自定义错误类型
         logx.WithContext(ctx).Errorf("【RPC-SRV-ERR】 %+v", err)
         //转成grpc err
         err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg())
      } else {
         logx.WithContext(ctx).Errorf("【RPC-SRV-ERR】 %+v", err)
      }
   }
   return resp, err
}

当有请求进入到rpc服务时候,先进入拦截器然后就是执行handler方法,如果你想在进入之前处理某些事情就可以写在handler方法之前,那我们想处理的是返回结果如果有错误的情况,所以我们在handler下方使用了github.com/pkg/errors这个包,这个包处理错误是go中经常用到的这不是官方的errors包,但是设计的很好,go官方的Wrap、Wrapf等就是借鉴了这个包的思路。

因为我们grpc内部业务在返回错误时候

1)如果是我们自己业务错误,我们会统一用xerr生成错误,这样就可以拿到我们定义的错误信息,因为前面我们自己错误也是用的uint32,所以在这里统一转成  grpc错误err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg()),那这里获取到的,e.GetErrCode()就是我们定义的code,e.GetErrMsg() 就是我们之前定义返回的错误第二个参数

2)但是还有一种情况是rpc服务异常了底部抛出来的错误,本身就是grpc错误了,那这种的我们直接就记录异常就好了

3、api错误

当我们api在logic中调用rpc的Register时候,rpc返回了上面第2步的错误信息 代码如下

......
func (l *RegisterLogic) Register(req types.RegisterReq) (*types.RegisterResp, error) {
    registerResp, err := l.svcCtx.UsercenterRpc.Register(l.ctx, &usercenter.RegisterReq{
        Mobile:   req.Mobile,
        Nickname: req.Nickname,
        AuthKey:  req.Mobile,
        AuthType: model.UserAuthTypeSystem,
    })
    if err != nil {
        return nil, errors.Wrapf(err, "req: %+v", req)
    }
    var resp types.RegisterResp
    _ = copier.Copy(&resp, registerResp)
    return &resp, nil
}

这里同样是使用标准包的errors.Wrapf , 也就是说所有我们业务中返回错误都适用标准包的errors,但是内部参数要使用我们xerr定义的错误

这里有2个注意点

1)api服务想把rpc返回给前端友好的错误提示信息,我们想直接返回给前端不做任何处理(比如rpc已经返回了“用户已存在”,api不想做什么处理,就想把这个错误信息直接返回给前端)

针对这种情况,直接就像上图这种写就可以了,将rpc调用处的err直接作为errors.Wrapf 第一个参数扔出去,但是第二个参数最好记录一下自己需要的详细日志方便后续在api log里查看

2)api服务不管rpc返回的是什么错误信息,我就想自己再重新定义给前端返回错误信息(比如rpc已经返回了“用户已存在”,api想调用rpc时只要有错误我就返回给前端“用户注册失败”)

针对这种情况,如下这样写即可(当然你可以将xerr.NewErrMsg("用户注册失败") 放到代码上方使用一个变量,这里放变量也可以)

func (l *RegisterLogic) Register(req types.RegisterReq) (*types.RegisterResp, error) {
    .......
    if err != nil {
        return nil, errors.Wrapf(xerr.NewErrMsg("用户注册失败"), "req: %+v,rpc err:%+v", req,err)
    }
    .....
}

接下来我们看最终返回给前端怎么处理的,我们接着看app/usercenter/cmd/api/internal/handler/user/registerHandler.go

func RegisterHandler(ctx *svc.ServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var req types.RegisterReq
        if err := httpx.Parse(r, &req); err != nil {
            httpx.Error(w, err)
            return
        }
        l := user.NewRegisterLogic(r.Context(), ctx)
        resp, err := l.Register(req)
        result.HttpResult(r, w, resp, err)
    }
}

这里可以看到,go-zero-looklook生成的handler代码 有2个地方跟默认官方的goctl生成的代码不一样,就是在处理错误处理的时候,这里替换成我们自己的错误处理了,在common/result/httpResult.go

【注】有人会说,每次使用goctl都要过来手动改,那不是要麻烦死了,这里我们使用go-zero给我们提供的template模版功能(还不知道这个的就要去官方文档学习一下了),修改一下handler生成模版即可,整个项目的模版文件放在deploy/goctl下,这里hanlder修改的模版在deploy/goctl/1.2.3-cli/api/handler.tpl

ParamErrorResult很简单,专门处理参数错误的

// http 参数错误返回
func ParamErrorResult(r *http.Request, w http.ResponseWriter, err error) {
   errMsg := fmt.Sprintf("%s ,%s", xerr.MapErrMsg(xerr.REUQES_PARAM_ERROR), err.Error())
   httpx.WriteJson(w, http.StatusBadRequest, Error(xerr.REUQES_PARAM_ERROR, errMsg))
}

我们主要来看HttpResult , 业务返回的错误处理的

// http返回
func HttpResult(r *http.Request, w http.ResponseWriter, resp interface{}, err error) {
    if err == nil {
        // 成功返回
        r := Success(resp)
        httpx.WriteJson(w, http.StatusOK, r)
    } else {
        // 错误返回
        errcode := xerr.SERVER_COMMON_ERROR
        errmsg := "服务器开小差啦,稍后再来试一试"
        causeErr := errors.Cause(err) // err类型
        if e, ok := causeErr.(*xerr.CodeError); ok {
            // 自定义错误类型
            // 自定义CodeError
            errcode = e.GetErrCode()
            errmsg = e.GetErrMsg()
        } else {
            if gstatus, ok := status.FromError(causeErr); ok {
                // grpc err错误
                grpcCode := uint32(gstatus.Code())
                if xerr.IsCodeErr(grpcCode) {
                    // 区分自定义错误跟系统底层、db等错误,底层、db错误不能返回给前端
                    errcode = grpcCode
                    errmsg = gstatus.Message()
                }
            }
        }
        logx.WithContext(r.Context()).Errorf("【API-ERR】 : %+v ", err)
        httpx.WriteJson(w, http.StatusBadRequest, Error(errcode, errmsg))
    }
}

err :要记录的日志错误

errcode :返回给前端的错误码

errmsg :返回给前端的友好的错误提示信息

成功直接返回,如果遇到错误了,也是使用github.com/pkg/errors这个包来判断错误,是不是我们自己定义的错误(api中定义的错误直接使用我们自己定义的xerr),还是grpc错误(rpc业务抛出来的),如果是grpc错误在通过uint32转成我们自己错误码,根据错误码再去我们自己定义错误信息中找到定义的错误信息返回给前端,如果是api错误直接返回给前端我们自己定义的错误信息,都找不到那就返回默认错误“服务器开小差了” ,

4、结尾

到这里错误处理已经消息描述清楚了,接下来我们要看打印了服务端的错误日志,我们该如何收集查看,就涉及到日志收集系统。

项目地址

https://github.com/zeromicro/go-zero

相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。     相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
5月前
|
Kubernetes Java 调度
无需接入执行器,0 代码改造实现微服务任务调度
本文提出了一种基于云原生的任务调度新方案,不需要依赖SDK,不依赖语言,实现定时调度和分布式跑批。
316 45
|
5月前
|
Kubernetes 调度 微服务
无需接入执行器,0代码改造实现微服务任务调度
本文提出了一种基于云原生的任务调度新方案,不需要依赖SDK,不依赖语言,实现定时调度和分布式跑批
276 1
|
6月前
|
jenkins Java 持续交付
使用 Jenkins 和 Spring Cloud 自动化微服务部署
随着单体应用逐渐被微服务架构取代,企业对快速发布、可扩展性和高可用性的需求日益增长。Jenkins 作为领先的持续集成与部署工具,结合 Spring Cloud 提供的云原生解决方案,能够有效简化微服务的开发、测试与部署流程。本文介绍了如何通过 Jenkins 实现微服务的自动化构建与部署,并结合 Spring Cloud 的配置管理、服务发现等功能,打造高效、稳定的微服务交付流程。
738 0
使用 Jenkins 和 Spring Cloud 自动化微服务部署
|
12月前
|
存储 Kubernetes 开发工具
使用ArgoCD管理Kubernetes部署指南
ArgoCD 是一款基于 Kubernetes 的声明式 GitOps 持续交付工具,通过自动同步 Git 存储库中的配置与 Kubernetes 集群状态,确保一致性与可靠性。它支持实时同步、声明式设置、自动修复和丰富的用户界面,极大简化了复杂应用的部署管理。结合 Helm Charts,ArgoCD 提供模块化、可重用的部署流程,显著减少人工开销和配置错误。对于云原生企业,ArgoCD 能优化部署策略,提升效率与安全性,是实现自动化与一致性的理想选择。
776 0
|
8月前
|
存储 监控 Shell
SkyWalking微服务监控部署与优化全攻略
综上所述,虽然SkyWalking的初始部署流程相对复杂,但通过一步步的准备和配置,可以充分发挥其作为可观测平台的强大功能,实现对微服务架构的高效监控和治理。尽管未亲临,心已向往。将一件事做到极致,便是天分的展现。
|
11月前
|
存储 Kubernetes 异构计算
Qwen3 大模型在阿里云容器服务上的极简部署教程
通义千问 Qwen3 是 Qwen 系列最新推出的首个混合推理模型,其在代码、数学、通用能力等基准测试中,与 DeepSeek-R1、o1、o3-mini、Grok-3 和 Gemini-2.5-Pro 等顶级模型相比,表现出极具竞争力的结果。
|
10月前
|
人工智能 搜索推荐 前端开发
从代码到心灵对话:我的CodeBuddy升级体验之旅(个性化推荐微服务系统)
本文分享了使用CodeBuddy最新版本的深度体验,重点探讨了Craft智能体、MCP协议和DeepSeek V3三大功能。Craft实现从对话到代码的无缝转化,大幅提升开发效率;MCP协议打通全流程开发,促进团队协作;DeepSeek V3则将代码补全提升至新境界,显著减少Bug并优化跨语言开发。这些功能共同塑造了AI与程序员共生的未来模式,让编程更高效、自然。
908 15
|
12月前
|
存储 Kubernetes 监控
K8s集群实战:使用kubeadm和kuboard部署Kubernetes集群
总之,使用kubeadm和kuboard部署K8s集群就像回归童年一样,简单又有趣。不要忘记,技术是为人服务的,用K8s集群操控云端资源,我们不过是想在复杂的世界找寻简单。尽管部署过程可能遇到困难,但朝着简化复杂的目标,我们就能找到意义和乐趣。希望你也能利用这些工具,找到你的乐趣,满足你的需求。
1045 33
|
12月前
|
Kubernetes 开发者 Docker
集群部署:使用Rancher部署Kubernetes集群。
以上就是使用 Rancher 部署 Kubernetes 集群的流程。使用 Rancher 和 Kubernetes,开发者可以受益于灵活性和可扩展性,允许他们在多种环境中运行多种应用,同时利用自动化工具使工作负载更加高效。
648 19
|
12月前
|
存储 测试技术 对象存储
使用容器服务ACK快速部署QwQ-32B模型并实现推理智能路由
阿里云最新发布的QwQ-32B模型,通过强化学习大幅度提升了模型推理能力。QwQ-32B模型拥有320亿参数,其性能可以与DeepSeek-R1 671B媲美。

推荐镜像

更多