image

go-zero微服务框架学习笔记

  • WORDS 37840

安装

goctl

goctlgo-zero内置的脚手架工具

# 安装最新版本
$ go install github.com/zeromicro/go-zero/tools/goctl@latest

goctl api

goctl的核心模块之一,可以通过 .api文件快速生成一个 api服务。

TODO

go env

# 查看一些环境变量
$ goctl env

# 检查 goctl 所依赖的环境是否安装完全,如果缺少依赖项会提示
# 有三个可选参数
# -i, --install 如果有缺少的组件 那么直接安装
# -f, --force 静默安装组件,不传递的话安装前需要手动确认
# -v, --verbose 是否输出执行日志
$ goctl env check

# 安装goctl需要的依赖 其作用和 goctl env check --install一致
# 也有 goctl env check 的 -f 和 -v 参数
$ gocel env install

protoc安装

protoc是用于 proto文件的代码生成工具,可以生成多种语言的代码。golanggrpc代码生成依赖于 protoc-gen-goprotoc-gen-go-grpc

可以通过 goctl工具一键安装相关组件

$ goctl env check --install --verbose --force

go-zero安装

在项目根目录执行 go mod init初始化后,将 go-zero假如当前项目依赖。

$ go get -u github.com/zeromicro/go-zero@latest

DSL 语法

api 语法

syntax 语句

syntax语句用于标记 api语言的版本,当前版本为 v1

syntax = "v1"

info语句

info语句是 api语言的 meta信息,仅用于到当前文件进行描述,目前不参与代码生成

// 包含key, value 的info语句块
info(
    name: "demo",
    summay: 
)

import 语句

import语句用于在当前 api文件中引入其它 api文件的语句块,支持相对路径和绝对路径。

// 单个import
import "/path/api"

// 多个import
import (
    "base",
    "/path/api"
)

数据类型

api中的数据类型基本沿用了 golang的数据类型,用于对服务/接口的请求/响应体结构进行描述。

int | bool | string | byte | float32 | float64 这些基本类型,以及 [4]int 这样的数组和切片类型以及 map 都是可用的。

同时和 go一样,也支持自定义结构体,如若需要使用结构体接收参数,api 中也同样和 go 使用 tag 接收参数

规则 说明 应用 tag示例 请求示例
json json序列化 请求体和响应体 json:"name" {"name": "aa"}
path 路由参数 请求体 path:"id" /info/:id
form post请求的表单和 get请求的 query参数 请求体 form:"name" /query?name=aa
header 接收请求头 请求体 header:"User-Agent"

go-zero 不支持多个 tag 来接收参数,一个结构体字段只能有一个 tag

**同时还可以通过 **tag 来进行接收参数的校验,校验规则进队请求体有效

规则 说明 示例
optional 可选参数,允许为零值 json:"name,optional"
options 当前参数仅可接收的枚举值 json:"type,options=1
default 当前参数默认值 json:"sort,default=1"
range 当前参数值有效范围 json:"age,range=[0:100]"

range表达式规则

左开右闭区间:(min:max],表示大于 min 小于等于 maxminmax 可以缺省,缺省时分别表示 0 和 无穷大,但是不能同时缺省

左开右闭区间:[min:max),表示大于等于 min 小于 max,缺省分别表示无穷小和 0

闭区间:[min:max],表示大于等于 min 小于等于 max ,缺省和左开右闭相同

开区间:(min:max),表示大于 min 小于 max,缺省和闭区间相同

// 单个结构体
type Student {
    StudentId uint64 `json:"studentId"`
    Name string  `json:"name"`
    Tags []string  `json:"tags"` // 切片
    Unknown map[string]string `json:"unknown"` // map
}

// 嵌套结构体
type Class {
    ClassId uint `json:"classId"`
    ClassName string `json:"className"`
    Students []*Student `json: "students"` // 指针类型
    Major {
        MajorId uint `json:"majorId"`
        MajorName string `json:"majorName"`
    }
}

// 结构体组
type (
    Integer int
    Long int64
    Query {
        Name string `json:"name"`
        Num Integer `json:"num"`
    }
)

service 语句

service语句是对 http服务的直观描述,包含请求的 handler、请求方法、请求路由、请求和响应体、鉴权、中间件等定义。

@server 语句是对服务语句的 meta 信息描述,其特性包括但不限于

  • jwt开关
  • 中间件
  • 路由分组
  • 路由前缀
@server (
    // jwt声明,value为生成的Go代码中配置文件的结构体名称 (会把jwt的token信息方式到Auth字段中)
    jwt: Auth
    // 路由前缀,字符串必须以 / 开头
    prefix: /v1
    // 路由分组,value为具体的分组名称,会按此值进行文件夹分组
    group: Base
    // 中间件声明,value为具体的中间件函数名称。可以声明多个中间件,使用 , 号分隔
    // AuthMiddleware,LogMiddleware 会被生成为两个同名的 go 结构体,结构体有一个 Handle 函数,用于实现中间件的逻辑
    middleware: AuthMiddleware
    // 超时控制,value为具体的超时时间
    timeout: 5s
    // 除了内置的 key-value 外,其它 key-value 也可做为 annotation 信息传递给goctl及其插件
    source: demo
)

@doc 语句用于对单个路由的 meta 信息进行描述,一般为 key-value 值,可以传递给 goctl及其插件进行扩展生成。

// 单行 doc
@doc "demo"

// doc 组
@doc (
    source: "demo"
)

@handler 语句用于对单个路由的 handler 信息控制,主要用于生成 http.HandleFunc 的实现转换方法。

@handler demo

路由语句是单词 HTTP请求的具体描述,包括请求方法,请求路径,请求体,响应体信息。

路由规则

**路由必须以 **/ 开头,节点之间必须以 / 分隔

**路由节点可以包含 **: 用于接收路径参数,: 必须是路由节点的第一个字符,并且节点值中必须要在请求结构体中以 path tag声明接收路由参数

// 没有请求体和响应体的方法
// /hello 为路由路径
get /hello

// 只有响应体的方法
get /hello (Query)

// 包含请求体和响应体的方法
get /hello (Query) returns (Student)

// type UserInfoReq {
//     UserId uint64 `path:"id"`
// }
// 路由节点的 id 参数必须在请求结构体中以 path tag 接收 
get /user/:id (UserInfoReq) returns (UserInfoResp)

完整示例

// 有 @server 的写法
@server (
	// 路由前缀和中间件
    prefix: /v1
    middleware: AuthMiddleware
)
service user {
    @doc "登录"
    @handler login
    post /user/login (LoginReq) returns (LoginResp)

    @doc "用户信息"
    @handler userInfo
    get /user/info () returns (UserInfoResp)
}


// 可以不包含 @server
service user {
    @doc "登录"
    @handler login
    post /user/login (LoginReq) returns (LoginResp)

    @doc "用户信息"
    @handler userInfo
    get /user/info () returns (UserInfoResp)
}

proto 语法

参考 grpc

goctl 生成 grpc 代码时所有的 message 必须在主 proto 中声明,不支持从包外包 importmessage

服务分组

当服务不分组时,一个 proto文件中的 service所生成的代码都会在一个目录中,并且不分组的情况下,不支持在 proto文件中定义多个 service

服务分组后,会按照 service名称创建文件夹分别存放,目录结构更清晰。

syntax = "proto3";

package user;

// 在定义 message 后紧跟定义 service ,就是分组写法
message UserLoginReq {}
message UserLoginResp {}
message UserInfoReq {}
message UserInfoResp {}
service UserService {
  rpc Login(UserInfoReq) returns (UserLoginResp);
  rpc UserInfo(UserInfoReq) returns (UserInfoResp);
}

message UserRoleListReq {}
message UserRoleListResp {}
message UserRoleCreateReq {}
message UserRoleUpdateReq {}
service UserRoleService {
  rpc UserRoleList(UserRoleListReq) returns (UserRoleListResp);
  rpc UserRoleCreate(UserRoleCreateReq) returns (UserRoleListResp);
}

配置

go-zero 提供了 conf 包用于加载配置,支持 yaml | json | toml 三种格式的配置文件。

配置的数据绑定均通过结构体 tagjson 字段,tag 支持数据类型的参数校验规则且大小写不敏感。此外,在 1.4.3版本以上还可以通过 env tag 来支持环境变量配置的加载。

配置加载的优先级:env > 配置文件 > tag 中的 default 定义

// 配置结构体
type Config struct {
	Host string `json:",default=0.0.0.0"`
    // tag 支持数据类型的参数校验
    Port uint   `json:",default=8080,range(1024,65535)"`
}

func main() {
	var config Config
    // MustLoad 加载配置不会返回 error,加载错误会直接 fatal 掉程序
    // Load() 函数加载会返回错误,不会 fatal 程序
	conf.MustLoad(".\\configs\\config.yaml", &config)
	fmt.Printf("%s:%d", config.Host, config.Port)
}

使用环境变量加载配置,env 后面添加环境变量的名称即可

type Config struct {
	Host string `json:",default=0.0.0.0,env=SERVER_HOST"`
    Port uint   `json:",default=8080,range(1024,65535),env=SERVER_PORT"`
}

**此外,为了省略很多重复的配置,配置加载还支持继承策略,往 **tag 添加了 inherit 关键字之后,会自动向上一层寻找配置。

**在不使用继承的情况下,需要编写很多重复的 **etcd 配置。

type Config struct {
	Etcd    discov.EtcdConf   `json:""`
	UserRpc UserServiceConfig `json:""`
}
type UserServiceConfig struct {
	Etcd discov.EtcdConf `json:""`
}
Etcd:
  Key: rpcServer
  Hosts:
    - '127.0.0.1:6377'
    - '127.0.0.1:6376'
    - '127.0.0.1:6375'
UserRpc:
  Etcd:
    Key: userService
    Hosts:
      - '127.0.0.1:6377'
      - '127.0.0.1:6376'
      - '127.0.0.1:6375'

使用继承之后,可以省略公共配置

type UserServiceConfig struct {
    // tag 添加继承关键字
	Etcd discov.EtcdConf `json:",inherit"`
}
Etcd:
  Key: rpcServer
  Hosts:
    - '127.0.0.1:6377'
    - '127.0.0.1:6376'
    - '127.0.0.1:6375'
UserRpc:
  # 会自动在上一级寻找 etcd 的配置
  Etcd:
    Key: userService

服务配置

service.ServiceConf 配置用来表示一个独立的服务,例如 jobsearch 等,可以被 restzrpc 等引用。也可以简单定义为自己的服务。

type MqConfig struct {
	service.ServiceConf
	StoreRpc zrpc.RpcClientConf
}

func main() {
	var config MqConfig
	conf.MustLoad("config.yaml", &config)
    // 提供基础的服务配置
	config.MustSetUp()
}

**这样就相当于启动了一个服务 **service ,并依赖 ServiceConf 自动集成了 Metrics、Trace、DevServer、Log 等能力。

type ServiceConf struct {
    Name       string  // 服务名称 会显示在 log 和 trace 中
    Log        logx.LogConf // 日志配置
    Mode       string `json:",default=pro,options=dev|test|rt|pre|pro"`// 服务所处的环境
    MetricsUrl string `json:",optional"` // 打点上报的url,如果为空则不会上传
    Prometheus prometheus.Config `json:",optional"`
    Telemetry  trace.Config      `json:",optional"`
    DevServer  DevServerConfig   `json:",optional"`
}

日志配置

logx.LogConf用于 go-zero 中的日志配置,logx.MustSetUp() 会提供基础的日志配置。

func main() {
	var logConfig logx.LogConf
    // 使用纯文本格式输出
	logConfig.Encoding = "plain"
	logx.MustSetup(logConfig)
	logx.Info(context.Background(), "hello go-zero")
}
// 日志配置定义
type LogConf struct {
    // 服务名称
	ServiceName string `json:",optional"`
    // 日志打印模式 console 控制台,file 输出到文件
	Mode string `json:",default=console,options=[console,file,volume]"`
    // 日志格式化 json 或 plain 纯文本
	Encoding string `json:",default=json,options=[json,plain]"`
    // 日期格式化
	TimeFormat string `json:",optional"`
    // 日志在文件模式下的输出路径
	Path string `json:",default=logs"`
    // 日志的输出级别
	Level string `json:",default=info,options=[debug,info,error,severe]"`
    // 日志 content 内容的长度限制 超出会裁剪
	MaxContentLength uint32 `json:",optional"`
    // 是否压缩日志
	Compress bool `json:",optional"`
    // 是否开启 stat 日志
	Stat bool `json:",default=true"`
    // 日志保留天数 文件模式生效
	KeepDays int `json:",optional"`
    // 堆栈打印冷却时间
	StackCooldownMillis int `json:",default=100"`
    // 文件模式按照大小分割时的保留文件个数
	MaxBackups int `json:",default=0"`
    // 文件模式按照大小分割时单个文件的最大大小
	MaxSize int `json:",default=0"`
    // 文件模式日志分割的模式 daily 日期,size 大小
	Rotation string `json:",default=daily,options=[daily,size]"`
    // 文件名日期格式
	FileTimeFormat string `json:",optional"`
}

HTTPServer

服务配置

针对 HTTP服务主机、端口、证书等进行配置

// 配置定义
type RestConf struct {
    service.ServiceConf
    // 服务监听的地址
    Host     string `json:",default=0.0.0.0"`
    // 服务绑定的端口
    Port     int
    // https 的证书文件
    CertFile string `json:",optional"`
    // https 私钥文件
    KeyFile  string `json:",optional"`
    // 是否打印详细日志
    Verbose  bool   `json:",optional"`
    // 最大连接数
    MaxConns int    `json:",default=10000"`
    // ContentLength 的最大长度
    MaxBytes int64  `json:",default=1048576"`
    // 超时时间 单位:ms
    Timeout      int64         `json:",default=3000"`
    CpuThreshold int64         `json:",default=900,range=[0:1000)"`
    Signature    SignatureConf `json:",optional"`
    // 需要使用的中间件
    Middlewares MiddlewaresConf
    TraceIgnorePaths []string `json:",optional"`
}

请求参数

go-zero 中,支持 http.RequestBodyFormquerypath 和请求头的参数获取,这些都是通过 httpx.Parse 函数将数据解析到结构体中。结构体的 tag 定义和数据类型中相同.

响应参数

go-zero 中,依然使用 http 库原生的 http.ResponseWriter 来返回响应数据和响应头。httpx 包内包含一个扩展函数用于便捷的返回数据。例如 httpx.OkJson(w http.ResponseWriter, result any)

type RequestData struct {
	Name string `path:"name"`
}

// 简单的 httpServer 示例
func main() {
    // 配置 httpServer
	restServer := rest.MustNewServer(rest.RestConf{
		Host: "127.0.0.1",
		Port: 4001,
	})
    // 添加路由
	restServer.AddRoute(rest.Route{
		Method: http.MethodGet,
		Path:   "/:name",
        // 路由的处理函数
		Handler: func(writer http.ResponseWriter, request *http.Request) {
			data := &RequestData{}
            // 获取请求参数
			if err := httpx.Parse(request, data); err != nil {
                // 响应错误信息
				httpx.Error(writer, errors.New("request params not"))
				return
			}
            // 响应数据
			httpx.OkJson(writer, map[string]string{"name": data.Name})
		},
	})
    // 启动httpServer
	restServer.Start()
}

中间件

go-zero 内置支持多种功能的中间件,其列表为:

  • AuthorizeHandler :鉴权管理中间件
  • BreakerHandler:熔断中间件
  • ContentSecurityHandler:内容安全中间件
  • CryptionHandler:解密中间件
  • GunzipHandler:压缩管理中间件
  • LogHandler:日志中间件,每次 http 请求都会输出对应的请求日志,默认会在日志 content 中显示 statuc_codeurihost:portuser-agent 等信息。
  • MaxBytesHandlerContentLength 管理中间件
  • MaxConnsHandler:限流中间件,限制 http 最大请求并发数,当并发请求超过指定数值时会返回 http.StatusServiceUnavailable 错误码。
  • MetricHandler:指标统计中间件
  • RecoverHandlerpanic 恢复中间件
  • SheddingHandler:负载监控中间件
  • TimeoutHandler:超时中间件
  • TraceHandler:链路追踪中间件,默认集成了 Opentelemetry 进行链路追踪,如果想把追踪信息上报到 jaeger ,只需要修改配置即可。链路请求信息默认会携带 hostmethodroutestatus_code 等信息。需要提前启动 jaeger 服务
    Name: hello
    Host: 127.0.0.1
    Port: 4001
    Middlewares:
      Metrics: true
    Telemetry:
      Name: hello
      # jaeger 上报地址
      Endpoint: http://127.0.0.1:14268/api/traces
      Batcher: jaeger
      Sampler: 1.0
    

以上中间件默认启用

// 中间件配置
type MiddlewaresConf struct {
    Trace      bool `json:",default=true"` // 是否启用链路追踪中间件
    Log        bool `json:",default=true"` // ...日志中间件
    Prometheus bool `json:",default=true"` // ...普罗米修斯指标中间件
    MaxConns   bool `json:",default=true"` // ...限流中间件
    Breaker    bool `json:",default=true"` // ...熔断中间件
    Shedding   bool `json:",default=true"` // ...负载监控中间件
    Timeout    bool `json:",default=true"` // ...超时中间件
    Recover    bool `json:",default=true"` // ...panic恢复中间件
    Metrics    bool `json:",default=true"` // ...指标统计中间件
    MaxBytes   bool `json:",default=true"` // ...ContentLength 管理中间件
    Gunzip     bool `json:",default=true"` // ...压缩管理中间件
}

**如果需要加载自定义中间件,只需要创建一个 **Middleware 类型的函数并传递给 ServerUse 函数即可。

// 一个简单的基础认证中间件
func BasicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(writer http.ResponseWriter, request *http.Request) {
		username, password, ok := request.BasicAuth()
		if !ok {
			writer.WriteHeader(http.StatusUnauthorized)
			return
		}
		if username != "admin" || password != "password" {
			writer.WriteHeader(http.StatusUnauthorized)
			_, _ = writer.Write([]byte("用户名或密码错误"))
		}
		next(writer, request)
	}
}

func main() {
	restServer := rest.MustNewServer(rest.RestConf{
		Host: "127.0.0.1",
		Port: 4001,
	})
    // 通过 use 函数使用中间件
	restServer.Use(BasicAuthMiddleware)
}

JWT 认证

go-zero 简单化了 jwt 在项目中的引用,内置了其解密和验证功能 ,只需要通过可选参数控制是否启用 JWT 即可。

func main() {
	restServer := rest.MustNewServer(rest.RestConf{
		Host: "127.0.0.1",
		Port: 4001,
	})
    // 通过 rest.WithJwt 为当前地址开启 jwt 验证,在验证时使用传入的字符串做为 secret
	restServer.AddRoute(rest.Route{
		Method: http.MethodGet,
		Path:   "/:name",
		Handler: func(writer http.ResponseWriter, request *http.Request) {
			data := &RequestData{}
			if err := httpx.Parse(request, data); err != nil {
				httpx.Error(writer, errors.New("request params not"))
				return
			}
			httpx.OkJson(writer, map[string]string{"name": data.Name})
		},
	}, rest.WithJwt("hello world"))
	defer restServer.Stop()
	restServer.Start()
}

**默认情况下 **JWT 验证失败会返回 401 状态码和错误信息,如果需要自定义处理 JWT 验证失败的逻辑,添加一个自定义的处理函数即可。

func main() {
    // rest.WithUnauthorizedCallback 用户处理 jwt 验证失败
	restServer := rest.MustNewServer(rest.RestConf{
		Host: "127.0.0.1",
		Port: 4001,
	}, rest.WithUnauthorizedCallback(func(w http.ResponseWriter, r *http.Request, err error) {
		w.WriteHeader(http.StatusUnauthorized)
        // 自定义验证失败的返回信息
	}))
}

错误处理

**处理 **HTTP 的错误,根据不同的错误类型响应不同的状态码和错误消息

type RequestData struct {
	Name string `path:"name"`
}

func main() {
	restServer := rest.MustNewServer(rest.RestConf{
		Host: "127.0.0.1",
		Port: 4001,
	}, rest.WithUnauthorizedCallback(func(w http.ResponseWriter, r *http.Request, err error) {
		w.WriteHeader(http.StatusUnauthorized)

	}))
    // 自定义错误函数,此处理仅在使用 httpx.Error 时有效
	httpx.SetErrorHandler(func(err error) (int, any) {
		switch e := err.(type) {
		case *errors.CodeMsg:
			return e.Code, map[string]any{
				"code":    e.Code,
				"message": e.Msg,
			}
		default:
			return http.StatusInternalServerError, map[string]any{
				"code":    http.StatusInternalServerError,
				"message": "服务器内部错误",
			}
		}
	})
	restServer.AddRoute(rest.Route{
		Method: http.MethodGet,
		Path:   "/:name",
		Handler: func(writer http.ResponseWriter, request *http.Request) {
			data := &RequestData{}
			if err := httpx.Parse(request, data); err != nil {
                // 为请求端返回错误状态码和错误消息
				httpx.Error(writer, errors.New(http.StatusBadRequest, "request params not"))
				return
			}
			httpx.OkJson(writer, map[string]string{"name": data.Name})
		},
	}, rest.WithJwt("hello world"))
	defer restServer.Stop()
	restServer.Start()
}

gRPC

Server

go-zero 提供了 gRPC Server 能力,内置了多种功能支持:

  • 服务发现(etcd 作为注册中心)
  • 负载均衡(p2c 算法)
  • 节点亲和性处理
  • 多节点直连支持
  • 超时处理
  • 限流、熔断
  • 鉴权
  • 异常捕获

gRPC server 使用 zrpc.RpcServerConf 作为通用的服务端配置,用于控制 rpc 的监听地址、服务注册发现、链路追踪、日志和中间件能功能。

type RpcServerConf struct {
    // 基础服务配置
    service.ServiceConf
    // 监听地址
    ListenOn      string
    Etcd          discov.EtcdConf    `json:",optional,inherit"`
    // 是否开启Auth
    Auth          bool               `json:",optional"`
    Redis         redis.RedisKeyConf `json:",optional"`
    // 是否Strict模式,Strict模式下错误都被认为是Auth失败
    StrictControl bool               `json:",optional"`
    // 超时时间 ms
    Timeout      int64 `json:",default=2000"`
    // 降载阈值 可设置范围 0-1000
    CpuThreshold int64 `json:",default=900,range=[0:1000)"`
    // 是否开启健康检查
    Health      bool `json:",default=true"`
    Middlewares ServerMiddlewaresConf
    MethodTimeouts []MethodTimeoutConf `json:",optional"`
}

// Etcd 配置
type EtcdConf struct {
	Hosts              []string // etcd集群地址
	Key                string // 服务的唯一 key,用于服务注册发现,不同服务不可重复
	ID                 int64  `json:",optional"` 
	User               string `json:",optional"` // etcd 用户
	Pass               string `json:",optional"` // etcd 密码
	CertFile           string `json:",optional"` // 证书文件
	CertKeyFile        string `json:",optional=CertFile"` // 私钥文件
	CACertFile         string `json:",optional=CertFile"` // CA证书文件
	InsecureSkipVerify bool   `json:",optional"`
}

// 当使用 redis 管理 rpc 认证时需要配置的参数,如果不使用redis进行rpc认证,可以不配置
type RedisConf struct {
    Host     string // redis地址
    Type     string `json:",default=node,options=node|cluster"` // 类型 node:单机 cluster 集群
    Pass     string `json:",optional"` // 密码
    Tls      bool   `json:",optional"` // 使用使用tls
    Key 	 string  // redis key
}

// 中间件配置
type ServerMiddlewaresConf struct {
    Trace      bool     `json:",default=true"` // 是否启用链路追踪中间件
    Recover    bool     `json:",default=true"` // ...异常捕获中间件
    Stat       bool     `json:",default=true"` // ...统计中间件
    StatConf   StatConf `json:",optional"` // 统计配置
    Prometheus bool     `json:",default=true"` // ...Prometheus中间件
    Breaker    bool     `json:",default=true"` // ...熔断中间件
}

使用示例

**创建一个 **proto 文件

syntax = "proto3";

package book;
// pb.go 和 grpc_bp.go 存放的目录
option go_package=".\\book";

message Empty {}

message Book {
  uint64 bookId = 1;
  string bookName = 2;
  double price = 3;
  string summary = 4;
}

message BookInfoReq {
  uint64 bookId = 1;
}

message BookListResp {
  repeated Book books = 1;
}

service BookService {
  rpc BookInfo(BookInfoReq) returns (Book);

  rpc BookList(Empty) returns (BookListResp);
}

**使用 **goctl 工具生成代码,生成完毕后,client 客户端代码存放在当前目录的 <serviceName> 目录下,server 端代码存放在 internal/server 目录下。

$ goctl rpc protoc greet.proto --go_out=.  --go-grpc_out=.  --zrpc_out=.

对于微服务的服务间调用,go-zero 支持 etcd 服务注册和直连模式,go-zero 会自动识别。如果需要使用 etcd 服务注册,需要将 etcd 的配置添加到 etc 目录中当前服务的配置文件中

Name: book.rpc
ListenOn: 0.0.0.0:8080
Etcd:
  Hosts:
  - 127.0.0.1:2379
  # 当前rpc服务的key,和name保持相同
  Key: book.rpc

**待实现的业务代码位于 **logic 目录下,在没有服务分组的情况下,每个 rpc 服务都会创建一个同名文件,以 BookInfo 为例,生成的文件名为 bookinflogic.go

type BookInfoLogic struct {
	ctx    context.Context
	svcCtx *svc.ServiceContext
	logx.Logger
}

func NewBookInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BookInfoLogic {
	return &BookInfoLogic{
		ctx:    ctx,
		svcCtx: svcCtx,
		Logger: logx.WithContext(ctx),
	}
}
// 实现业务处理逻辑
func (l *BookInfoLogic) BookInfo(in *book.BookInfoReq) (*book.Book, error) {
	// todo: add your logic here and delete this line

	return &book.Book{}, nil
}

**当需要在 **logic 层调用其它层或者需要创建对象时,所依赖的对象必须在 svc/servicecontext 中显式注入,所有的 rpc 服务都依赖于这一个 context

// 一个简单的 book data层
type BookRepo struct {
	state map[uint64]book.Book
}
func NewBookRepo() *BookRepo {
	return &BookRepo{
		state: map[uint64]book.Book{
			1: {
				BookId:   1,
				BookName: "Go从入门到实践",
				Price:    10.11,
				Summary:  "Go入门书籍",
			},
		},
	}
}
func (self *BookRepo) GetBookById(bookId uint64) *book.Book {
	item, ok := self.state[bookId]
	if ok {
		return &item
	}
	return nil
}
func (self *BookRepo) ListBook() []book.Book {
	books := make([]book.Book, len(self.state))
	for _, value := range self.state {
		books = append(books, value)
	}
	return books
}

**在 **ServiceContext 中以结构体参数的形式显式注入

type ServiceContext struct {
	Config   config.Config
	BookRepo *data.BookRepo
}

func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config:   c,
		BookRepo: data.NewBookRepo(),
	}
}

**然后,便可在 **logic 层使用注入的依赖来实现具体的业务逻辑

func NewBookInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BookInfoLogic {
	return &BookInfoLogic{
		ctx:    ctx,
		svcCtx: svcCtx,
		Logger: logx.WithContext(ctx),
	}
}

func (self *BookInfoLogic) BookInfo(in *book.BookInfoReq) (*book.Book, error) {
    // 调用 svcContext 中注入的依赖
	return self.svcCtx.BookRepo.GetBookById(in.BookId), nil
}

中间件

go-zerozrpc 包中内置了很多常用的中间件,包括

  • 鉴权中间件:StreamAuthorizeInterceptor|UnaryAuthorizeInterceptor
  • 熔断中间件:StreamBreakerInterceptor|UnaryBreakerInterceptor
  • 指标统计中间件:UnaryPrometheusInterceptor
  • 异常捕获中间件:StreamRecoverInterceptor|UnaryRecoverInterceptor
  • 服务降级中间件:UnarySheddingInterceptor
  • 时长统计中间件:UnaryStatInterceptor
  • 请求超时中间件:UnaryTimeoutInterceptor
  • 链路追踪中间件:StreamTraceInterceptor|UnaryTraceInterceptor

除了链路追踪、指标统计、时长统计、异常捕获、熔断中间件可以通过配置来开启或关闭,其余中间件都默认开启。

如果想要添加自定义的请求拦截逻辑,也可以添加自定义中间件

// DemoUnaryInterceptor 普通请求中间件
func DemoUnaryInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
	// 自定义的中间件处理逻辑
	logx.Infof("Method: %s", info.FullMethod)
	return handler(ctx, req)
}

// DemoStreamInterceptor 流式请求中间件
func DemoStreamInterceptor(srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
	// 自定义的中间件处理逻辑
	logx.Info(info.IsServerStream, info.IsClientStream)
	return handler(srv, stream)
}

func main() {
	// ...
	ctx := svc.NewServiceContext(c)
	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
		book.RegisterBookServiceServer(grpcServer, server.NewBookServiceServer(ctx))

		if c.Mode == service.DevMode || c.Mode == service.TestMode {
			reflection.Register(grpcServer)
		}
	})
    // 添加流式请求中间件
	s.AddStreamInterceptors(DemoStreamInterceptor)
    // 添加普通请求中间件
	s.AddUnaryInterceptors(DemoUnaryInterceptor)
	// ...
}

Client

gRPC client 的开发主要是修改配置文件,具体的客户端代码已经由 gRPC 生成好了。

type RpcClientConf struct {
    // Etcd 注册中心配置 和 server 一致
    Etcd          discov.EtcdConf `json:",optional,inherit"`
    // RPC server地址列表,直连模式下配置
    Endpoints     []string        `json:",optional"`
    // 域名解析地址
    Target        string          `json:",optional"`
    // rpc 认证的app名称,仅在rpc server开启认证时使用
    App           string          `json:",optional"`
    // rpc认证的token,同上
    Token         string          `json:",optional"`
    // 是否阻塞模式
    NonBlock      bool            `json:",optional"`
    // 超时时间
    Timeout       int64           `json:",default=2000"`
    // 保活时间
    KeepaliveTime time.Duration   `json:",default=20s"`
    // 中间件配置
    Middlewares   ClientMiddlewaresConf
}

type ClientMiddlewaresConf struct {
    // 链路追踪
    Trace      bool `json:",default=true"`
    // 异常恢复
    Recover    bool     `json:",default=true"`
    // 统计
    Stat       bool     `json:",default=true"`
    // 统计配置
    StatConf   StatConf `json:",optional"`
    Prometheus bool     `json:",default=true"`
    // 熔断
    Breaker    bool     `json:",default=true"`
}
// 直连模式,通过地址访问
conn := zrpc.MustNewClient(zrpc.RpcClientConf{
    Endpoints: []string{"127.0.0.1:8080"},
})
// 创建 rpc client 并调用对应的接口
client := book.NewBookServiceClient(conn.Conn())
bookInfo, err := client.BookInfo(context.Background(), &book.BookInfoReq{
    BookId: 1,
})
// 通过 etcd 服务注册调用
conn := zrpc.MustNewClient(zrpc.RpcClientConf{
    Etcd: discov.EtcdConf{
        Hosts: []string{"127.0.0.1:2379"},
        Key:   "book.rpc",
    },
})
client := book.NewBookServiceClient(conn.Conn())

消息队列

go-zerokafka-go 的基础上封装了 go-queue ,对 kafka 消息队列提供了更加便捷的支持。

$ go get github.com/zeromicro/go-queue@latest

生产者

**创建 **kafka 的生产者 pusher 主要的参数需要 kafka 的地址列表和 topic 的分区名称。此外,NewPusher 的第三个参数是几个可选参数:

  • chunkSize kq client 在提交消息时是批量提交,此参数指定批量块的大小
  • flushInterval:提交的间隔时间,间隔时间触发后即使未达到 chunkSize 的大小也会提交消息
// addrs kafka 的集群地址 topic 需要发送的 topic 名称
func NewPusher(addrs []string, topic string, opts ...PushOption) *Pusher {}

**创建成功后便可通过 **KPush , Push, PushWithKey 这三个函数向 kafka 发送消息

// 发送消息 需要传递 cxt, key, 和消息值
// kafka接收到消息后会通过key将消息存入对应的分区
func (p *Pusher) KPush(ctx context.Context, k, v string) error {}

// 发送消息 无需 key,默认的key为时间戳
func (p *Pusher) Push(ctx context.Context, v string) error {}

// 发送消息 和 KPush 同理
func (p *Pusher) PushWithKey(ctx context.Context, key, v string) error {}
// 最简单的消息发送示例
if err := pusher.Push(context.Background(), message); err != nil {
    logx.Errorf("kafka 消息发送失败,error: %s, message: %s", err, message)
}

消费者

**创建消费者依赖于 **kq.KqConf 配置

type KqConf struct {
	service.ServiceConf
	Brokers       []string // kafka集群地址
	Group         string // 消费者群组名称
	Topic         string // 需要消费的topic名称
	CaFile        string `json:",optional"` // ca证书
    Offset        string `json:",options=first|last,default=last"` // 读取时的偏移量设置 first:头 last:尾
	Conns         int    `json:",default=1"` // 消费者数量
	Consumers     int    `json:",default=8"` // 从kafka获取信息写入进程的channel数量
	Processors    int    `json:",default=8"` // 消费消息的goroutine数量
	MinBytes      int    `json:",default=10240"`    // 一次返回的最小字节数,如果不够就等待
	MaxBytes      int    `json:",default=10485760"` // 一次返回的最大字节数
	Username      string `json:",optional"` // kafka的用户
	Password      string `json:",optional"` // kafka的密码
	ForceCommit   bool   `json:",default=true"`
	CommitInOrder bool   `json:",default=false"`
}

**实现消费者依赖于 **ConsumeHandler 接口,需要实现该接口以读取消息并自定义消费逻辑

type DemoConsumeHandler struct{}

func NewDemoConsumeHandler() kq.ConsumeHandler {
	return &DemoConsumeHandler{}
}

// 消费消息,会传入ctx和消息的key,value
func (self *DemoConsumeHandler) Consume(ctx context.Context, key, value string) error {
	logx.Infof("接收 kafka 消息成功,key: %s, value: %s", key, value)
	return nil
}

**实现了消息的消费逻辑之后,需要将消费者添加到 **ServiceGroup 中并启动等待消息消费。

// 创建消费者
queue := kq.MustNewQueue(kq.KqConf{
    Brokers:    brokers,
    Group:      "consumer",
    Topic:      topic,
    Offset:     "last",
    Consumers:  1,
    Processors: 1,
}, NewDemoConsumeHandler())
// 创建一个服务组
serviceGroup := service.NewServiceGroup()
defer serviceGroup.Stop()
// 将消费者添加到服务组中,可以添加多个
serviceGroup.Add(queue)
// 启动,等待消息并进行消费
serviceGroup.Start()

MustNewQueue 在初始化时也有几个可选参数:

  • commitInterval:提交给 kafka borker 的间隔时间,默认 1s
  • queueCapacitykafka 内部队列长度
  • maxWait:从 kafka 批量获取数据时,当数据量不够等待新数据到来的最大时间
  • metrics:上报每个消息的消费时间,默认会初始化,一般不需要指定

延时队列

延时队列在一些超时的场景会经常使用,例如订单未支付关闭等。go-queue 也实现了延时队列 dq ,其底层使用 beanstalkd

生产者

type (
    // beanstalkd 节点配置
	Beanstalk struct {
		Endpoint string
		Tube     string
	}

	DqConf struct {
        // 多个 beanstalkd 节点配置
		Beanstalks []Beanstalk
        // redis配置,本身不需要依赖redis 主要使用redis的Setnx进行去重 防止重复消费
		Redis      redis.RedisConf
	}
)

**创建一个生产者,需要传入 **beanstalkd 的节点配置

// 实际使用中 此配置应该读取配置文件
beanstalks := []dq.Beanstalk{
		{
			Endpoint: "127.0.0.1:7771",
			Tube:     "tube1",
		},
	}
pusher := dq.NewProducer(beanstalks)

而后便可发送延迟消息或者在指定时间消费的消息

// 指定延迟时间的消息,在 1 分钟后执行
delay, err := pusher.Delay([]byte("timeout message"), 1*time.Minute)
if err != nil {
    logx.Errorf("延迟消息发送失败,error: %s", err)
}
logx.Infof("resp: %s", delay)

// 发送在指定时间执行的消息,传入具体的 time 对象
resp, err := pusher.At([]byte("message"), time.Now().Add(24*time.Hour))

消费者

**消费者和生产者的配置相同,但是需要传入 **redis 配置,同样也是先初始化一个消费者

// 配置使用完整的 dqConf Beanstalks和生产者相同 但是需要传入redis配置
consumer := dq.NewConsumer(dq.DqConf{
    Redis: redis.RedisConf{
        Host: "127.0.0.1:6379",
        Type: "node",
    },
    Beanstalks: beanstalks,
})

**调用消费者的 **Consume 方法即可定义消息的消费逻辑,非常方便

consumer.Consume(func(body []byte) {
    logx.Infof("接收到 beanstalkd 消息,message:%s", body)
})

Redis

go-zero 提供了 redis 组件,其底层基于 redis-go 。但是没有提供 db选择,默认使用 db0 ,主要因为 redis cluster 也仅支持 db0

conf := redis.RedisConf{
    Host: "127.0.0.1:6379",
    Type: "node",
}
// 创建一个redisClient
redisClient, err := redis.NewRedis(conf)
if err != nil {
    log.Fatalf("创建 redis 客户端失败, error: %s", err)
}
// 最简单的 set 操作
if err = redisClient.Set("demo", "hello go-zero"); err != nil {
    logx.Errorf("添加redis key失败,error:%s", err)
}
// 最简单的 get 操作
value, err := redisClient.Get("demo")
if err != nil {
    logx.Errorf("获取redis key失败,error:%s", err)
} else {
    logx.Info("redis key获取成功,value:", value)
}

**除了最简单的 **get|set 操作,go-zeroredis 组件提供了绝大部分 redis 操作的封装,更便于使用。

Redis 锁

go-zeroredis 组件集成了 redis 锁的操作,可以非常方便的使用 redis 创建分布式锁。

redisClient, err := redis.NewRedis(conf)
if err != nil {
    log.Fatalf("创建 redis 客户端失败, error: %s", err)
}
// 先创建 redis 客户端,再通过客户端和锁的key创建一个锁
redisLock := redis.NewRedisLock(redisClient, "lock")

redis.RedisLock 包含 5 个常用方法:

  • SetExpire(expire):设置锁的过期时间,单位: 秒
  • Acquire:获取锁,会返回 bool, errbool 表示锁是否成功获取
  • Release:释放锁,同样会返回 bool, err
  • AcquireCtx:可以传递 context 的获取锁操作
  • ReleaseCtx:可以传递 context 的释放锁操作
redisLock := redis.NewRedisLock(redisClient, "lock")
// 设置过期时间 60 秒
redisLock.SetExpire(60)
// 尝试获取锁
isAcquire, err := redisLock.Acquire()
switch {
case err != nil:
    logx.Errorf("获取 redis 锁异常,error:%s", err)
    return
case !isAcquire:
    logx.Errorf("获取 redis 锁失败,锁已被占用")
    return
case isAcquire:
    // 业务处理完成后释放锁
    defer redisLock.Release()
    logx.Info("获取 redis 锁成功")
    // 业务逻辑
}

Redis 监控

redis 组件内置了两个用于监控相关的 metric

  • metricReqDur:用于对 redis 操作命令的耗时监控
    metricReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{
        Namespace: namespace,
        Subsystem: "requests",
        Name:      "duration_ms",
        Help:      "redis client requests duration(ms).",
        Labels:    []string{"command"},
        Buckets:   []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500},
    }
    
  • metricReqErr:用于对 redis 操作的错误监控
    metricReqErr = metric.NewCounterVec(&metric.CounterVecOpts{
        Namespace: namespace,
        Subsystem: "requests",
        Name:      "error_total",
        Help:      "redis client requests error count.",
        Labels:    []string{"command", "error"},
    }
    

gRPC Gateway

go-zerogRPC Geteway 用于将 REST HTTP 请求转换为 gRPC 请求,其大致的工作流程如下:

  • **从 **proto 文件中解析 gRPC 服务的定义
  • **从配置文件中解析出 **gRPC 服务的 HTTP 映射规则
  • **基于服务定义和映射规则,生成 **gRPC 服务的 HTTP 处理器
  • **启动服务器,将 **HTTP 请求转换为 gRPC 请求
  • **将 **gRPC 响应转换为 HTTP 响应并返回

gRPC Gateway 的定义也是基于配置来实现的,可以完全基于配置文件实现请求转换。

type (
	// 网关配置
	GatewayConf struct {
        // http 服务配置
		rest.RestConf
		Upstreams []Upstream
	}

	// http 映射配置
	RouteMapping struct {
		// 映射的HTTP方法,仅限 GET, POST, PUT, DELETE.
		Method string
		// 映射的HTTP请求路径
		Path string
		// 访问的gRPC服务路径,package.service/method
		RpcPath string
	}

	// gRPC服务配置
	Upstream struct {
		// 服务名称
		Name string `json:",optional"`
		// gRPC服务配置
		Grpc zrpc.RpcClientConf
		// proto文件列表
		ProtoSets []string `json:",optional"`
		// 服务映射,不填则默认映射所有gRPC服务
		Mappings []RouteMapping `json:",optional"`
	}
)

**有两种方式可以使用 **gRPC Gateway ,其分别是:

  • protoDescriptor:通过 protoc 解析 proto 生成 pb 文件,然后在 gateway 中使用该 pb 文件做 rest-grpc 的规则映射。 **创建 **proto 文件,定义 service

    syntax = "proto3";
    
    package hello;
    
    option go_package = ".\\hello";
    
    message Request {}
    message Response {
      string message = 1;
    }
    
    service Hello {
      rpc hello(Request) returns (Response);
    }
    

    **在同级目录下创建一个 **gateway 目录,用于存放生成的 pb 文件

    # 通过 proto 生成 pb 文件
    $ protoc --descriptor_set_out=gateway/hello.pb hello.proto
    

    **再通过 **proto 初始化 rpc 服务,完善业务逻辑后修改配置文件并启动

    # 使用 goctl 工具初始化 rpc 服务
    $ goctl rpc protoc hello.proto --go_out=server --go-grpc_out=server --zrpc_out=server
    
    // logic/hellologic.go
    
    // 完成业务逻辑
    func (self *HelloLogic) Hello(in *hello.Request) (*hello.Response, error) {
        return &hello.Response{
    		Message: "hello",
    	}, nil
    }
    

    **进入 **gateway 目录,创建 etc/gateway.yaml 配置文件,在配置文件中定义 HTTPRPC 服务的映射规则

    Name: hello-gateway
    Host: localhost
    Port: 8888
    Upstreams:
      - Grpc:
          # rpc服务的地址
          Target: localhost:8080
        # pb文件列表
        ProtoSets:
          - hello.pb
        # 映射规则列表
        Mappings:
          - Method: get
            Path: /hello
            # 映射的rpc方法
            # 命名规则 <文件名称>./
            RpcPath: hello.Hello/hello
    

    **新建一个 **main 函数文件,启动 gateway 网关访问对应的 http 地址即可

    // gateway.go
    
    var configFile = flag.String("f", "etc/gateway.yaml", "config file")
    func main() {
        flag.Parse()
    
        var c gateway.GatewayConf
        conf.MustLoad(*configFile, &c)
        gw := gateway.MustNewServer(c)
        defer gw.Stop()
        gw.Start()
    }
    
  • grpcReflection:通过 grpc 反射做 rest-grpc 的规则映射,只在 devtest 环境可用。

流量治理

限流

限流用于控制服务的并发调用量,一般有节点限流、集群限流(将限流数量对集群节点数求平均值,本质还是节点限流)和分布式限流。

**默认的限流实现只需要修复服务配置中的 **MaxConns 选项即可。配置完成后,当并发访问数量超过了限流数量时,超过的请求都会被限流,服务器直接返回 503 状态码。

gRPC 服务一般用于内网,默认情况下没有限流配置。如若确实需要限流,可以使用自定义中间件来实现。

// 自定义 grpc 普通请求限流中间件
func NewGrpcUnaryLimitInterceptors(maxCons uint32) grpc.UnaryServerInterceptor {
	var conCount uint32 = 0
	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
        // 当前请求数量小于限制数量
        // 增加请求数量,正常处理请求
		if atomic.LoadUint32(&conCount) < maxCons {
            atomic.AddUint32(&conCount, 1)
			defer atomic.AddUint32(&conCount, -1)
			return handler(ctx, req)
		} else {
            // 如果达到限流阈值 输出错误日志 直接返回错误状态码
			logx.Errorf("concurrent connections over %d, rejected with code %d", atomic.LoadUint32(&conCount), http.StatusServiceUnavailable)
			return nil, status.Error(codes.Unavailable, "concurrent connections over limit")
		}
	}
}

熔断

熔断用于保护服务调用链路中不会压垮。当某个服务出现异常时,熔断器会将拒绝调用该服务的所有请求,从而保护其它服务不被压垮。

go-zero 内置了熔断器组件 breaker.Breaker ,底层基于滑动窗口进行数据采集,默认以 10s 为一个窗口,单个窗口有 40 个桶,采集的数据通过 googlesre 算法判断是否开启熔断。

**在 **breaker.Breaker 中,提供了四个方法用于熔断,分别对应不同的场景:

  • Do:按照错误率来判断服务是否可用,不支持指标自定义和错误回调
    // 自定义错误类型
    type BreakerError struct {
    	Status int
    }
    
    func (self *BreakerError) Error() string {
    	return fmt.Sprintf("Error Status: %d", self.Status)
    }
    
    func main() {
    	for i := 0; i <= 100; i++ {
            // 会以传入的字符串做为唯一key创建breaker实例
    		if err := breaker.Do("demo", func() error {
    			if i%4 == 0 {
    				return &BreakerError{
    					Status: 500,
    				}
    			}
    			return nil
    		}); err != nil {
    			fmt.Println(err)
    		}
    	}
    }
    
  • DoWithAcceptable:支持自定义的采集指标,可以自主控制哪种情况需要假如熔断指标采集窗口
    for i := 0; i <= 100; i++ {
        if err := breaker.DoWithAcceptable("demo", func() error {
            if i%4 == 0 {
                return &BreakerError{Status: 500}
            } else if i%5 == 0 {
                return &BreakerError{Status: 400}
            }
            return nil
        }, func(err error) bool {
            // 状态码不等于500都认为正常,不记录
            be, ok := err.(BreakerError)
            if ok {
                return be.Status != 500
            }
            return false
        }); err != nil {
            fmt.Println(err)
        }
    }
    
  • DoWithFallback:支持熔断回调,不支持指标自定义
    for i := 0; i <= 100; i++ {
        if err := breaker.DoWithFallback("demo", func() error {
            if i%4 == 0 {
                return &BreakerError{Status: 500}
            } else if i%5 == 0 {
                return &BreakerError{Status: 400}
            }
            return nil
        }, func(err error) error {
            // 自定义错误转换
            return errors.New(err.Error())
        }); err != nil {
            fmt.Println(err)
        }
    }
    
  • DoWithFallbackAcceptable:支持熔断回调和指标自定义

**在实际的使用中,不需要对请求单独进行熔断处理,功能已经集成到了 **go-zero 中。HTTP 请求以请求方法 + 路由做为统计 key,用 500 状态码做为错误采集指标进行统计;gRPC 客户端以 rpc 方法名做为统计 keygRPC 错误码为 codes.DeadlineExceeded, codes.Internal, codes.Unavailable, codes.DataLoss, codes.Unimplemented,做为错误采集指标进行统计。gRPC 服务端和客户端一致,只是错误采集指标变成了所有 gRPC 错误。

负载均衡

负载均衡用于将请求均匀的分发给集群中的不同节点,常见的算法有:

  • 轮询:循环重复的将请求依次分发给每个节点,适用于节点处理能力相同的情况。
  • 权重:将请求优先分配给权重更高的节点,节点的权重可以单独设置,更高权重的节点处理更多的请求。
  • 最少连接数:优先将请求分配给连接数最少的节点,适用于单次请求连接时间较长的场景。
  • IP散列:对请求的源 IP 继续哈希运算,根据结果将请求分配给对应的节点,可以确保同一源 IP 始终分配给同一节点。
  • P2C:从节点列表中随机选择两个节点,根据负载情况选择负载较小的节点,避免最劣选择。

go-zero 中,负载均衡算法主要应用在 gRPC 客户端,且采用的 P2C 算法。

监控

健康检查

go-zero 启动的服务,默认已经集成了了健康检查,默认端口和 path6060/healthz ,访问健康检查地址会返回 OK

如果需要关闭或者修改端口、地址,可以通过修改配置实现。

srv := rest.MustNewServer(rest.RestConf{
    Port: 8002,
    ServiceConf: service.ServiceConf{
        DevServer: devserver.Config{
            // 开启健康检查
            Enabled:       true,
            // 健康检查服务端口
            Port:          8080,
            // 健康检查服务地址
            HealthPath:    "/ping",
        },
    },
})

也可以使用配置文件

Name: user-api
Host: 0.0.0.0
Port: 8002
DevServer:
    Enabled: true
    Port: 8080
    HealthPath: "/ping"

链路追踪

go-zero 基于 OpenTelemetry 集成了链路追踪,支持 jaeger/zipkin 只需修改配置即可启用。

type Config struct {
    Name     string  `json:",optional"`
    Endpoint string  `json:",optional"` // trace信息上报的url
    Sampler  float64 `json:",default=1.0"`  // 采样率
    Batcher  string  `json:",default=jaeger,options=jaeger|zipkin|otlpgrpc|otlphttp"`
}

api 链路追踪

Name: user-api
Host: 0.0.0.0
Port: 8002
Mode: dev

#链路追踪
Telemetry:
  Name: user-api
  Endpoint: http://jaeger:14268/api/traces
  Sampler: 1.0
  Batcher: jaeger

rpc 链路追踪

Name: user-rpc
ListenOn: 0.0.0.0:9002
Mode: dev

#链路追踪
Telemetry:
  Name: user-rpc
  Endpoint: http://jaeger:14268/api/traces
  Sampler: 1.0
  Batcher: jaeger

指标监控

go-zero 中默认使用 prometheus 进行指标收集,默认端口和 path6470/metrics。可以通过 DevServer 配置指标监控 Path 和是否开启。

DevServer:
    Enabled: true
    Port: 6470
    // 修改指标监控的地址
    MetricsPath: /metrics
    // 是否启用指标监控
    EnableMetrics: false

**默认集成的 **prometheus 指标:

  • RPC Server:| 名称 | 标签 | 说明 | | ----------------------------------- | -------------------- | --------------------------------- | | rpc_server_requests_duration_ms | method | Histogram,耗时统计单位为毫秒 | | rpc_server_requests_code_total | methodcode | Counter,错误码统计 |
  • RPC client:和 RPC Server 一致
  • HTTP Server:| 名称 | 标签 | 说明 | | ------------------------------------ | ------------------ | --------------------------------- | | http_server_requests_duration_ms | path | Histogram,耗时统计单位为毫秒 | | http_server_requests_code_total | pathcode | Counter,错误码统计 |
  • MySQL:| 名称 | 标签 | 说明 | | ----------------------------------- | ---------------------- | --------------------------------- | | sql_client_requests_duration_ms | command | Histogram,耗时统计单位为毫秒 | | sql_client_requests_error_total | commanderror | Counter,错误统计 |
  • Redis:| 名称 | 标签 | 说明 | | ------------------------------------- | ---------------------- | --------------------------------- | | redis_client_requests_duration_ms | command | Histogram,耗时统计单位为毫秒 | | redis_client_requests_error_total | commanderror | Counter,错误统计 |

CLI工具

goctlgo-zero 配套的代码生成脚手架工具,集成了 HTTP 服务、RPC 服务 、数据库 modelk8sdocker 等生成功能。

指令 指令 指令 说明
goctl api dart 生成 dart代码
doc 文档生成
format api文件格式化
go HTTP服务生成
kt 生成 kotlin代码
new 快速创建 HTTP服务
plugin 使用 goctl插件
ts 生成 Typescript代码
validate api文件格式校验
bug Github issue上报
completion bash bash自动补全设置
fish
powershell
zsh
docker Dockerfile生成
env check goctl环境检测
install 安装 goctl依赖环境
kube deploy k8s yaml文件生成
migrate go-zero项目迁移
model mongo MongoDB代码生成
mysql MySQL代码生成
pg PostgreSQL代码生成
quickstart 快速启动并生成服务
rpc new gRPC服务生成
protoc gRPC代码生成
template 创建 proto文件
template clean 模板清理
init 模板文件初始化
revert 模板回滚
update 模板更新
upgrade goctl版本更新

运维部署

关联文章

0 条评论