乐趣区

关于go:gozero微服务实战系列四CRUD热身

上一篇文章咱们把整个我的项目的架子搭建实现,服务在本地也曾经能运行起来了,顺利成章的接下来咱们就应该开始写业务逻辑代码了,然而单纯的写业务逻辑代码是比拟干燥的,业务逻辑的代码我会一直地补充到 lerbon 我的项目中去,要害局部我也会加上正文。

那么本篇文章我次要想和大家分享下服务的根本配置和几个典型的代码示例。

日志定义

go-zero 的 logx 包提供了日志性能,默认不须要做任何配置就能够在 stdout 中输入日志。当咱们申请 /v1/order/list 接口的时候输入日志如下,默认是 json 格局输入,包含工夫戳,http 申请的根本信息,接口耗时,以及链路追踪的 span 和 trace 信息。

{"@timestamp":"2022-06-11T08:23:36.342+08:00","caller":"handler/loghandler.go:197","content":"[HTTP] 200 - GET /v1/order/list?uid=123 - 127.0.0.1:59998 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36","duration":"21.2ms","level":"info","span":"23c4deaa3432fd03","trace":"091ffcb0eafe7818b294e4d8122cf8a1"}

程序启动后,框架会默认输入 level 为 stat 的统计日志,用于输入以后资源的应用状况,次要为 cpu 和内存,内容如下:

{"@timestamp":"2022-06-11T08:34:58.402+08:00","caller":"stat/usage.go:61","content":"CPU: 0m, MEMORY: Alloc=3.3Mi, TotalAlloc=7.0Mi, Sys=16.3Mi, NumGC=8","level":"stat"}

当咱们不须要这类日志的时候,咱们能够通过如下形式敞开该类日志的输入:

logx.DisableStat()

有的时候咱们只须要记录谬误日志,能够通过设置日志等级来勾销 level 为 info 级别日志的输入:

logx.SetLevel(logx.ErrorLevel)

能够扩大日志输入的字段,增加了 uid 字段记录申请的用户的 uid,日志打印内容如下:

logx.Infow("order list", logx.Field("uid",req.UID))
{"@timestamp":"2022-06-11T08:53:50.609+08:00","caller":"logic/orderlistlogic.go:31","content":"order list","level":"info","uid":123}

咱们还能够扩大其余第三方日志库,通过 logx.SetWriter 来进行设置

writer := logrusx.NewLogrusWriter(func(logger *logrus.Logger) {logger.SetFormatter(&logrus.JSONFormatter{})
})
logx.SetWriter(writer)

同时 logx 还提供了丰盛的配置,能够配置日志输入模式,工夫格局,输入门路,是否压缩,日志保留工夫等

type LogConf struct {
    ServiceName         string `json:",optional"`
    Mode                string `json:",default=console,options=[console,file,volume]"`
    Encoding            string `json:",default=json,options=[json,plain]"`
    TimeFormat          string `json:",optional"`
    Path                string `json:",default=logs"`
    Level               string `json:",default=info,options=[info,error,severe]"`
    Compress            bool   `json:",optional"`
    KeepDays            int    `json:",optional"`
    StackCooldownMillis int    `json:",default=100"`
}

能够看到 logx 提供的日志性能还是十分丰盛的,同时反对了各种自定义的形式。日志是咱们排查线上问题十分重要的依赖,咱们还会依据日志做各种告警,所以这里咱们先做了一些日志应用的介绍。

服务依赖

在 BFF 服务中会依赖多个 RPC 服务,默认状况下,如果依赖的 RPC 服务没有启动,BFF 服务也会启动异样,报错如下,通过日志能够晓得是因为 order.rpc 没有启动,因为 order.rpc 是整个商城零碎的外围服务,BFF 对 order.rpc 是强依赖,在强依赖的状况下如果被依赖服务异样,那么依赖服务也无奈失常启动。

{"@timestamp":"2022-06-11T10:21:56.711+08:00","caller":"internal/discovbuilder.go:34","content":"bad resolver state","level":"error"}
2022/06/11 10:21:59 rpc dial: discov://127.0.0.1:2379/order.rpc, error: context deadline exceeded, make sure rpc service "order.rpc" is already started
exit status 1

再看如下的场景,BFF 依赖 reply.rpc,因为 reply.rpc 异样导致 BFF 无奈失常启动,因为 reply.rpc 并不是商城零碎的外围依赖,就算 reply.rpc 挂掉也不影响商城的外围流程,所以对于 BFF 来说 reply.rpc 是弱依赖,在弱依赖的状况下不应该影响依赖方的启动。

{"@timestamp":"2022-06-11T11:26:51.711+08:00","caller":"internal/discovbuilder.go:34","content":"bad resolver state","level":"error"}
2022/06/11 11:26:54 rpc dial: discov://127.0.0.1:2379/reply.rpc, error: context deadline exceeded, make sure rpc service "reply.rpc" is already started
exit status 1

在 go-zero 中提供了弱依赖的配置,配置后 BFF 即可失常启动,能够看到 order.rpc 和 product.rpc 都是强依赖,而 reply.rpc 配置了 NonBlock:true 为弱依赖

OrderRPC:
    Etcd:
        Hosts:
          - 127.0.0.1:2379
        Key: order.rpc
ProductRPC:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: product.rpc
ReplyRPC:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: reply.rpc
  NonBlock: true

并行调用

在高并发的零碎中,接口耗时是咱们十分关注的点,接口疾速响应能够晋升用户体验,长时间的期待会让用户体验很差,用户也就会缓缓的来到咱们。这里咱们介绍简略但很实用的晋升接口响应工夫的办法,那就是并行的依赖调用。

下图展现了串行调用和并行调用的区别,串行调用依赖的话,耗时等于所有依赖耗时的和,并行调用依赖的话,耗时等于所有依赖中耗时最大的一个依赖的耗时。

在获取商品详情的接口中,参数 ProductIds 为逗号分隔的多个商品 id,在这里咱们应用 go-zero 提供的 mapreduce 来并行的依据商品 id 获取商品详情,代码如下,具体代码请参考 product-rpc 服务:

func (l *ProductsLogic) Products(in *product.ProductRequest) (*product.ProductResponse, error) {products := make(map[int64]*product.ProductItem)
    pdis := strings.Split(in.ProductIds, ",")
    ps, err := mr.MapReduce(func(source chan<- interface{}) {
        for _, pid := range pdis {source <- pid}
    }, func(item interface{}, writer mr.Writer, cancel func(error)) {pid := item.(int64)
        p, err := l.svcCtx.ProductModel.FindOne(l.ctx, pid)
        if err != nil {cancel(err)
            return
        }
        writer.Write(p)
    }, func(pipe <-chan interface{}, writer mr.Writer, cancel func(error)) {var r []*model.Product
        for p := range pipe {r = append(r, p.(*model.Product))
        }
        writer.Write(r)
    })
    if err != nil {return nil, err}
    for _, p := range ps.([]*model.Product) {products[p.Id] = &product.ProductItem{
            ProductId: p.Id,
            Name:      p.Name,
        }
    }
    return &product.ProductResponse{Products: products}, nil
}

在商品详情页,不仅展现了商品的详情,同时页展现了商品评估的第一页,而后点击评估详情能够跳转到评估详情页,为了防止客户端同时申请多个接口,所以咱们在商品详情页把评论首页的内容一并返回,因为评论内容并不是核心内容所以在这里咱们还做了降级,即申请 reply.rpc 接口报错咱们会疏忽这个谬误,从而能让商品详情失常的展现。因为获取商品详情和商品评估没有前后依赖关系,所以这里咱们应用 mr.Finish 来并行的申请来升高接口的耗时。

func (l *ProductDetailLogic) ProductDetail(req *types.ProductDetailRequest) (resp *types.ProductDetailResponse, err error) {
    var (
        p *product.ProductItem
        cs *reply.CommentsResponse
    )
    if err := mr.Finish(func() error {
        var err error
        if p, err = l.svcCtx.ProductRPC.Product(l.ctx, &product.ProductItemRequest{ProductId: req.ProductID}); err != nil {return err}
        return nil
    }, func() error {
        var err error
        if cs, err = l.svcCtx.ReplyRPC.Comments(l.ctx, &reply.CommentsRequest{TargetId: req.ProductID}); err != nil {logx.Errorf("get comments error: %v", err)
        }
        return nil
    }); err != nil {return nil, err}
    var comments []*types.Comment
    for _, c := range cs.Comments {
        comments = append(comments, &types.Comment{
            ID: c.Id,
            Content:   c.Content,
        })
    }
    return &types.ProductDetailResponse{
        Product: &types.Product{
            ID:        p.ProductId,
            Name:      p.Name,
        },
        Comments: comments,
    }, nil
}

图片上传

图片上传是十分罕用的性能,咱们在 product-admin 中须要上传商品图片,这里咱们把商品图片上传到阿里云 OSS 中,api 定义如下

syntax = "v1"

type UploadImageResponse {Success bool `json:"success"`}

service admin-api {
    @handler UploadImageHandler
    post /v1/upload/image() returns (UploadImageResponse)
}

在 admin-api.yaml 中增加如下配置

Name: admin-api
Host: 0.0.0.0
Port: 8888
OSSEndpoint: https://oss-cn-hangzhou.aliyuncs.com
AccessKeyID: xxxxxxxxxxxxxxxxxxxxxxxx
AccessKeySecret: xxxxxxxxxxxxxxxxxxxxxxxx

增加 OSS 客户端

type ServiceContext struct {
    Config config.Config
    OssClient *oss.Client
}

func NewServiceContext(c config.Config) *ServiceContext {oc, err := oss.New(c.OSSEndpoint, c.AccessKeyID, c.AccessKeySecret)
    if err != nil {panic(err)
    }
    return &ServiceContext{
        Config: c,
        OssClient: oc,
    }
}

上传逻辑须要先获取 bucket,该 bucket 为事后定义的 bucket,能够通过 api 调用创立,也能够在阿里云工作台手动创立

func (l *UploadImageLogic) UploadImage() (resp *types.UploadImageResponse, err error) {file, header, err := l.r.FormFile(imageFileName)
    if err != nil {return nil, err}
    defer file.Close()
    bucket, err := l.svcCtx.OssClient.Bucket(bucketName)
    if err != nil {return nil, err}
    if err = bucket.PutObject(header.Filename, file); err != nil {return nil, err}
    return &types.UploadImageResponse{Success: true}, nil
}

应用 Postman 上传图片,留神在上传图片前须要先创立 bucket

登录阿里云对象存储查看已上传的图片

结束语

本篇文章通过日志定义和服务依赖介绍了服务构建中常见的一些配置,这里并没有把所有配置一一列举而是举例说明了社区中常常有人问到的场景,前面的文章还会持续不断完善服务的相干配置。接着又通过服务依赖的并行调用和图片上传两个案例展现了常见性能的优化伎俩以及编码方式。

这里并没有把所有的性能都列出来,也是想起个头,大家能够把我的项目 down 下来本人去欠缺这个我的项目,纸上得来终觉浅,绝知此事要躬行,当然我也会持续欠缺我的项目代码和大家一起学习提高。

心愿本篇文章对你有所帮忙,谢谢。

每周一、周四更新

代码仓库: https://github.com/zhoushuguang/lebron

我的项目地址

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

欢送应用 go-zerostar 反对咱们!

微信交换群

关注『微服务实际 』公众号并点击 交换群 获取社区群二维码。

退出移动版