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

欢迎使用 go-zerostar 支持我们!

微信交流群

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

go-zero微服务实战系列(四、CRUD热热身)的更多相关文章

  1. go-zero微服务实战系列(十一、大结局)

    本篇是整个系列的最后一篇了,本来打算在系列的最后一两篇写一下关于k8s部署相关的内容,在构思的过程中觉得自己对k8s知识的掌握还很不足,在自己没有理解掌握的前提下我觉得也很难写出自己满意的文章,大家看 ...

  2. 微服务实战系列--Nginx官网发布(转)

    这是Nginx官网写的一个系列,共七篇文章,如下 Introduction to Microservices (this article) Building Microservices: Using ...

  3. 微服务实战(四):服务发现的可行方案以及实践案例 - DockOne.io

    原文:微服务实战(四):服务发现的可行方案以及实践案例 - DockOne.io 这是关于使用微服务架构创建应用系列的第四篇文章.第一篇介绍了微服务架构的模式,讨论了使用微服务架构的优缺点.第二和第三 ...

  4. go-zero微服务实战系列(三、API定义和表结构设计)

    前两篇文章分别介绍了本系列文章的背景以及根据业务职能对商城系统做了服务的拆分,其中每个服务又可分为如下三类: api服务 - BFF层,对外提供HTTP接口 rpc服务 - 内部依赖的微服务,实现单一 ...

  5. Chris Richardson微服务实战系列

    微服务实战(一):微服务架构的优势与不足 微服务实战(二):使用API Gateway 微服务实战(三):深入微服务架构的进程间通信 微服务实战(四):服务发现的可行方案以及实践案例 微服务实践(五) ...

  6. ASP.NET Core微服务实战系列

    希望给你3-5分钟的碎片化学习,可能是坐地铁.等公交,积少成多,水滴石穿,码字辛苦,如果你吃了蛋觉得味道不错,希望点个赞,谢谢关注. 前言 这里记录的是个人奋斗和成长的地方,该篇只是一个系列目录和构想 ...

  7. 微服务实战系列(四)-注册中心springcloud alibaba nacos

    1.场景描述 因要用到微服务,关于注册中心这块,与同事在技术原型上做了讨论,初步定的方案是使用:阿里巴巴的nacos+springcloud gateway,下面表格是同事整理的注册中心对比,以前用的 ...

  8. go-zero 微服务实战系列(一、开篇)

    前言 在社区中经常看到有人问有没有基于 go-zero 的比较完整的项目参考,该类问题本质上是想知道基于 go-zero 的项目的最佳实践.完整的项目应该是一个完整的产品功能,包含产品需求.架构设计. ...

  9. Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第一篇(内附开发 demo)

    简介 小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系. 系列 云原生 API 网关,gRPC-Gateway V2 初探 业务流程 官方开发接入文档 ...

随机推荐

  1. c++对c的拓展_指针的引用

    套用引用公式:Type & ref =val; 假设:type 类型为int * 由公式得 int * & ref = val; // int * *const ref=&va ...

  2. JDBC 4.0 开始Java操作数据库不用再使用 Class.forName加载驱动类了

    JDBC 4.0 开始Java操作数据库不用再使用 Class.forName加载驱动类了 代码示例 转自 https://docs.oracle.com/javase/tutorial/jdbc/o ...

  3. 从实例学习 Go 语言、"基础与进阶" 学习笔记及心得体会、Go指南

    第一轮学习 golang "基础与进阶"学习笔记,Go指南练习题目解析.使用学习资料 <Go-zh/tour tour>.记录我认为会比较容易忘记的知识点,进行补充,整 ...

  4. 图数据库|基于 Nebula Graph 的 BetweennessCentrality 算法

    本文首发于 Nebula Graph Community 公众号 ​在图论中,介数(Betweenness)反应节点在整个网络中的作用和影响力.而本文主要介绍如何基于 Nebula Graph 图数据 ...

  5. 网络协议自动化逆向工具开山鼻祖discoverer 分析

    本文系原创,转载请说明出处:信安科研人 也可关注微信公众号:信安科研人 原论文发表在2007年的USENIX上,链接如下:https://www.usenix.org/legacy/event/sec ...

  6. Java语言学习day32--8月07日

    ###07正则表达式邮箱地址验证 * A: 正则表达式邮箱地址验证 * a: 案例代码 public class RegexDemo2 { public static void main(String ...

  7. 【FAQ】接入HMS Core地图服务过程中常见问题总结

    HMS Core地图服务(Map Kit)给开发者提供一套地图开发调用的SDK,助力全球开发者实现个性化地图呈现与交互,方便轻松地在应用中集成地图相关的功能,全方位提升用户体验. 在日常工作中,我们会 ...

  8. Halo 开源项目学习(四):发布文章与页面

    基本介绍 博客最基本的功能就是让作者能够自由发布自己的文章,分享自己观点,记录学习的过程.Halo 为用户提供了发布文章和展示自定义页面的功能,下面我们分析一下这些功能的实现过程. 管理员发布文章 H ...

  9. ghostnet论文解析:ghost

    创建日期: 2020-03-02 17:02:54 简介: GhostNet是2020CVPR录用的一篇对卷积操作进行改进的论文.文章的核心内容是Ghost模块(Ghost Module),可以用来替 ...

  10. 五四青年节,今天要学习。汇总5道难度不高但可能遇到的JS手写编程题

    壹 ❀ 引 时间一晃,今天已是五一假期最后一天了,没有出门,没有太多惊喜与意外.今天五四青年节,脑子里突然想起鲁迅先生以及悲欢并不相通的话,我的五一经历了什么呢,忍不住想说那大概是,父母教育孩子大声嚷 ...