上一篇文章咱们把整个项目的架子建立完结,服务在本地也已经能运行起来了,顺利成章的接下来咱们就应该开始写事务逻辑代码了,可是单纯的写事务逻辑代码是比较单调的,事务逻辑的代码我会不断地补充到 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

并行调用

在高并发的体系中,接口耗时是咱们十分重视的点,接口快速呼应能够提高用户体会,长时刻的等待会让用户体会很差,用户也就会渐渐的脱离咱们。这儿咱们介绍简单但很有用的提高接口呼应时刻的办法,那就是并行的依靠调用。

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

go-zero微服务实战系列(四、CRUD热热身)

在获取产品概况的接口中,参数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

go-zero微服务实战系列(四、CRUD热热身)

登录阿里云目标存储查看已上传的图片

go-zero微服务实战系列(四、CRUD热热身)

结束语

本篇文章经过日志界说和服务依靠介绍了服务构建中常见的一些装备,这儿并没有把一切装备一一列举而是举例说明了社区中经常有人问到的场景,后边的文章还会持续不断完善服务的相关装备。接着又经过服务依靠的并行调用和图片上传两个案例展现了常见功用的优化手段以及编码办法。

这儿并没有把一切的功用都列出来,也是想起个头,大家能够把项目down下来自己去完善这个项目,纸上得来终觉浅,绝知此事要躬行,当然我也会持续完善项目代码和大家一起学习前进。

期望本篇文章对你有所协助,谢谢。

每周一、周四更新

代码库房

项目地址

github.com/zeromicro/g…

欢迎运用 go-zerostar 支持咱们!

微信交流群

重视『微服务实践』公众号并点击 交流群 获取社区群二维码。