该图片来自微软开源的一个容器商城项目:eShopOnContainers,其中红框部分就是各个微服务模块。
每一个微服务都是一个独立的生态,比较直观的一点是各自拥有独立的数据库。
微服务化后,带来了诸多好处,比如弹性,敏捷,灵活扩展,易于部署,可重用代码等。
但也带来了复杂性,让整个架构变得不易于维护,所以诞生出来很多组件用于辅助,这些组件同样成为了微服务架构中的一员:
综上,容器时微服务架构的绝佳示例,现代云原生应用使用容器来构建微服务。
go-zero 是一个集成了各种工程实践的包含 web 和 rpc 框架,有如下主要特点:
创建user目录
编写proto文件:
syntax = "proto3";
package user;
option go_package = "./user";
message IdRequest {
string id = 1;
}
message UserResponse {
// 用户id
string id = 1;
// 用户名称
string name = 2;
// 用户性别
string gender = 3;
}
service User {
rpc getUser(IdRequest) returns(UserResponse);
}
使用goctl生成go-zero模板和pb文件:goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.
实现查询用户的功能:
func (l *GetUserLogic) GetUser(in *user.IdRequest) (*user.UserResponse, error) {
return &user.UserResponse{
Id: in.Id,
Name: "generalzy",
Gender: "武装直升机",
}, nil
}
go build编译
创建order目录
编写api文件:
type (
OrderReq {
Id string `path:"id"`
}
OrderReply {
Id string `json:"id"`
Name string `json:"name"`
}
)
service order {
@handler getOrder
get /api/order/get/:id (OrderReq) returns (OrderReply)
}
使用goctl生成api:goctl api go -api order.api -dir .
配置user config:
type Config struct {
rest.RestConf
UserRpc zrpc.RpcClientConf
}
配置yaml:
Name: order
Host: 0.0.0.0
Port: 8888
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
完善服务依赖(将user放入到order的上下文)
type ServiceContext struct {
Config config.Config
UserRpc user.User
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserRpc: user.NewUser(zrpc.MustNewClient(c.UserRpc)),
}
}
编写order业务逻辑
func (l *GetOrderLogic) GetOrder(req *types.OrderReq) (resp *types.OrderReply, err error) {
u, err := l.svcCtx.UserRpc.GetUser(l.ctx, &user.IdRequest{
Id: "1",
})
if err != nil {
return nil, err
}
if u.Name != "generalzy" {
return nil, errors.New("用户不存在")
}
return &types.OrderReply{
Id: req.Id,
Name: "test order",
}, nil
}
go build编译
etcd.exe
user.exe -f ./etc/user.yaml
order.exe -f ./etc/order.yaml
http://localhost:8888/api/order/get/1
返回:{"id":"1","name":"test order"}
goctl是go-zero微服务框架下的代码生成工具。使用 goctl 可显著提升开发效率,让开发人员将时间重点放在业务开发上,其功能有:
go install github.com/zeromicro/go-zero/tools/goctl@latest
.
├── etc
│ └── greet-api.yaml // 配置文件
├── go.mod // mod文件
├── greet.api // api描述文件
├── greet.go // main函数入口
└── internal
├── config
│ └── config.go // 配置声明type
├── handler // 路由及handler转发
│ ├── greethandler.go
│ └── routes.go
├── logic // 业务逻辑
│ └── greetlogic.go
├── middleware // 中间件文件
│ └── greetmiddleware.go
├── svc // logic所依赖的资源池
│ └── servicecontext.go
└── types // request、response的struct,根据api自动生成,不建议编辑
└── types.go
.
├── etc
│ └── greet.yaml
├── go.mod
├── go.sum
├── greet // [1]
│ ├── greet.pb.go
│ └── greet_grpc.pb.go
├── greet.go
├── greet.proto
├── internal
│ ├── config
│ │ └── config.go
│ ├── logic
│ │ └── greetlogic.go
│ ├── server
│ │ └── streamgreeterserver.go
│ └── svc
│ └── servicecontext.go
└── streamgreeter
└── streamgreeter.go
一个api语法文件只能有0或者1个syntax语法声明,如果没有syntax,则默认为v1版本
// api语法版本
syntax = "v1"
import语法块可以导入拆分的api文件, 不同的api文件按照一定规则声明,可以降低阅读难度和维护难度。
import "foo.api"
import "foo/bar.api"
import(
"bar.api"
"foo/bar/foo.api"
)
info语法块是一个包含了多个键值对的语法体,其作用相当于一个api服务的描述
info(
foo: "foo value"
bar: "bar value"
desc: "long long long long long long text"
)
在api服务中,需要用到一个结构体(类)来作为请求体,响应体的载体,因此需要声明一些结构体来完成这件事情, type语法块由golang的type演变而来,保留着一些golang type的特性,沿用golang特性有:
type Foo{
Id int `path:"id"`
Foo int `json:"foo"`
}
type Bar{
Bar int `form:"bar"`
}
type(
FooBar{
FooBar int `json:"fooBar"`
}
)
tag定义和golang中json tag语法一样,除了json tag外,go-zero还提供了另外一些tag来实现对字段的描述, 详情见下表。(tag修饰符需要在tag value后以英文逗号,隔开)
tag表
绑定参数时,以下四个tag只能选择其中一个
tag key | 描述 | 提供方 | 有效范围 | 示例 |
json | json序列化tag | golang | request、response | json:"fooo" |
path | 路由path,如/foo/:id |
go-zero | request | path:"id" |
form | 标志请求体是一个form(POST方法时)或者一个query(GET方法时/search?name=keyword ) |
go-zero | request | form:"name" |
header | HTTP header,如 Name: value |
go-zero | request | header:"name" |
tag修饰符
常见参数校验描述
tag key | 描述 | 提供方 | 有效范围 | 示例 |
optional | 定义当前字段为可选参数 | go-zero | request | json:"name,optional" |
options | 定义当前字段的枚举值,多个以竖线|隔开 | go-zero | request | json:"gender,options=male" |
default | 定义当前字段默认值 | go-zero | request | json:"gender,default=male" |
range | 定义当前字段数值范围 | go-zero | request | json:"age,range=[0:120]" |
service语法块用于定义api服务,包含服务名称,服务metadata,中间件声明,路由,handler等。
语法:
serviceSpec:包含了一个可选语法块atServer和serviceApi语法块,其遵循序列模式(编写service必须要按照顺序,否则会解析出错)
atServer: 可选语法块,定义key-value结构的server metadata,‘@server’ 表示这一个server语法块的开始,其可以用于描述serviceApi或者route语法块,其用于描述不同语法块时有一些特殊关键key 需要值得注意,见 atServer关键key描述说明。
serviceApi:包含了1到多个serviceRoute语法块
serviceRoute:按照序列模式包含了atDoc,handler和route
atDoc:可选语法块,一个路由的key-value描述,其在解析后会传递到spec.Spec结构体,如果不关心传递到spec.Spec, 推荐用单行注释替代。
handler:是对路由的handler层描述,可以通过atServer指定handler key来指定handler名称, 也可以直接用atHandler语法块来定义handler名称
atHandler:‘@handler’ 固定token,后接一个遵循正则[a-zA-Z][a-zA-Z-]*)的值,用于声明一个handler名称
route:路由,有httpMethod、path、可选request、可选response组成,httpMethod是必须是小写。
body:api请求体语法定义,必须要由()包裹的可选的ID值
replyBody:api响应体语法定义,必须由()包裹的struct、array(向前兼容处理,后续可能会废弃,强烈推荐以struct包裹,不要直接用array作为响应体)
kvLit: 同info key-value
serviceName: 可以有多个’-'join的ID值
path:api请求路径,必须以’/‘或者’/:‘开头,切不能以’/‘结尾,中间可包含ID或者多个以’-'join的ID字符串
atServer关键key:
修饰service时
key | 描述 | 示例 |
jwt | 声明当前service下所有路由需要jwt鉴权,且会自动生成包含jwt逻辑的代码 | jwt: Auth |
group | 声明当前service或者路由文件分组 | group: login |
middleware | 声明当前service需要开启中间件 | middleware: AuthMiddleware |
prefix | 添加路由分组 | prefix: api |
修饰route时
key | 描述 | 示例 |
handler | 声明一个handler | - |
@server(
jwt: Auth
group: foo
middleware: AuthMiddleware
prefix: api
)
service foo-api{
@doc "foo"
@handler foo
post /foo/:id (Foo) returns (Bar)
}
service foo-api{
@handler ping
get /ping
@doc "foo"
@handler bar
post /bar/:id (Foo)
}
// doc
// comment
goctl api是goctl中的核心模块之一,其可以通过.api文件一键快速生成一个api服务.
api命令说明:goctl api -h
NAME:
goctl api - generate api related files
USAGE:
goctl api command [command options] [arguments...]
COMMANDS:
new fast create api service
format format api files
validate validate api file
doc generate doc files
go generate go files for provided api in yaml file
java generate java files for provided api in api file
ts generate ts files for provided api in api file
dart generate dart files for provided api in api file
kt generate kotlin code for provided api file
plugin custom file generator
OPTIONS:
-o value the output api file
--help, -h show help
重点是 go子命令,其功能是生成golang api服务,通过goctl api go -h看一下使用帮助:
goctl api go -h
NAME:
goctl api go - generate go files for provided api in yaml file
USAGE:
goctl api go [command options] [arguments...]
OPTIONS:
--dir value the target dir
--api value the api file
--style value the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]
//
--dir 代码输出目录
--api 指定api源文件
--style 指定生成代码文件的文件名称风格,详情见文件名称命名style说明
使用示例:
goctl api go -api user.api -dir . -style gozero
Goctl Rpc是goctl脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码开发者只需要关注业务逻辑编写而不用去编写一些重复性的代码。
通过指定proto生成rpc服务:
生成proto模板
goctl rpc template -o=user.proto
syntax = "proto3";
package user;
option go_package="./user";
message Request {
string ping = 1;
}
message Response {
string pong = 1;
}
service User {
rpc Ping(Request) returns(Response);
}
生成rpc服务代码:
goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=.
rpc服务生成用法:
goctl rpc protoc -h
NAME:
goctl rpc protoc - generate grpc code
USAGE:
example: goctl rpc protoc xx.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=.
DESCRIPTION:
for details, see https://go-zero.dev/cn/goctl-rpc.html
OPTIONS:
--zrpc_out value the zrpc output directory
--style value the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]
--home value the goctl home path of the template
--remote value the remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority
The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure
--branch value the branch of the remote repo, it does work with --remote
参数说明:
可以理解为 zrpc 代码生成是用 goctl rpc $protoc_command --zrpc_out=${output}
模板,如原来生成 grpc 代码指令为
$ protoc user.proto --go_out=. --go-grpc_out=.
则生成 zrpc 代码指令就为
$ goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=.
错误的import:
syntax = "proto3";
package greet;
option go_package = "./greet";
import "base/common.proto";
message Request {
string ping = 1;
}
message Response {
string pong = 1;
}
service Greet {
rpc Ping(base.In) returns(base.Out);// request和return 不支持import
}
正确import:
syntax = "proto3";
package greet;
option go_package = "./greet";
import "base/common.proto";
message Request {
base.In in = 1;// 支持import
}
message Response {
base.Out out = 2;// 支持import
}
service Greet {
rpc Ping(Request) returns(Response);
}
goctl model 为go-zero下的工具模块中的组件之一,目前支持识别mysql ddl进行model层代码生成,通过命令行或者idea插件(即将支持)可以有选择地生成带redis cache或者不带redis cache的代码逻辑。
如果使用Gorm
或MongoDB
可以不适用model命令,框架过于大而全反而显得累赘啰嗦。
配套目录介绍:
mongodb集成应该在微服务逻辑模块:
在user/internal/config
下配置:
package config
import "github.com/zeromicro/go-zero/zrpc"
type Config struct {
zrpc.RpcServerConf
// 总配置
MongoUri MongoUri
}
type MongoUri struct {
Uri string
}
修改etc/user.yaml
Name: user.rpc
ListenOn: 0.0.0.0:8080
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
MongoUri:
Uri: mongodb://localhost:27017
在svc/servicecontext
中初始化mongo client:
package svc
import (
"context"
"github.com/generalzy/zeropan/user/internal/config"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type ServiceContext struct {
Config config.Config
MongoClient *mongo.Client
}
func NewServiceContext(c config.Config) *ServiceContext {
clientOptions := options.Client().ApplyURI(c.MongoUri.Uri)
clientOptions.SetMaxPoolSize(100)
client, _ := mongo.Connect(context.TODO(), clientOptions)
return &ServiceContext{
Config: c,
MongoClient: client,
}
}
在logic
下对应实现部分编写业务逻辑代码,因为mongo client已经由配置——》初始化
加入到了上下文中,所以可以l.svcCtx.MongoClient
取到mongo client。
package logic
import (
"context"
"github.com/generalzy/zeropan/models"
"github.com/generalzy/zeropan/user/internal/svc"
"github.com/generalzy/zeropan/user/types/user"
"github.com/zeromicro/go-zero/core/logx"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type RetrieveUserLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewRetrieveUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RetrieveUserLogic {
return &RetrieveUserLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *RetrieveUserLogic) RetrieveUser(in *user.RetrieveUserRequest) (*user.RetrieveUserResponse, error) {
objectID, err := primitive.ObjectIDFromHex(in.GetId())
if err != nil {
return nil, err
}
result := l.svcCtx.MongoClient.Database("zeropan").Collection("user").FindOne(
context.TODO(), bson.M{"_id": objectID})
if result.Err() != nil {
return nil, result.Err()
}
u := new(models.User)
if err := result.Decode(u); err != nil {
return nil, err
}
return &user.RetrieveUserResponse{
Id: in.Id,
Name: u.Name,
Email: u.Email,
CreateAt: u.CreatedAt.Format("2006-01-02 15-04-05"),
UpdateAt: u.UpdatedAt.Format("2006-01-02 15-04-05"),
}, nil
}
集成redis同理
go-zero已经集成了jwt库,所以直接在网关调用即可:
编写api网关文件:
syntax = "v1"
info(
title: "user svc gateway"
desc: "user svc gateway"
author: "generalzy"
email: "-"
version: "v1"
)
type RetrieveUserRequest {
ID string `json:"id"`
}
type RetrieveUserResponse {
Name string `bson:"name" json:"name"`
Password string `bson:"password" json:"password"`
Email string `bson:"email" json:"email"`
CreatedAt string `bson:"created_at" json:"createdAt"`
UpdatedAt string `bson:"updated_at" json:"updatedAt"`
DeletedAt string `bson:"deleted_at" json:"deletedAt"`
}
type CreateUserRequest {
Name string `bson:"name" json:"name"`
Password string `bson:"password" json:"password"`
Email string `bson:"email" json:"email""`
}
type CreateUserResponse {
ID string `json:"id"`
Name string `bson:"name" json:"name"`
Email string `bson:"email" json:"email"`
CreatedAt string `bson:"created_at" json:"createdAt"`
UpdatedAt string `bson:"updated_at" json:"updatedAt"`
}
type UserLoginRequest {
Name string `bson:"name" json:"name"`
Password string `bson:"password" json:"password"`
}
type UserLoginResponse {
Token string `bson:"token"`
}
@server (
// 写了jwt认证代表下边的接口需要jwt认证
jwt: Auth
middleware: AuthMiddleware
)
service user-api{
@handler CreateUserHandler
post /api/v1/user/create(CreateUserRequest)returns(CreateUserResponse)
}
service user-api{
@handler RetrieveUserHandler
get /api/v1/user/retrieve (RetrieveUserRequest) returns (RetrieveUserResponse)
@handler UserLoginHandler
post /api/v1/user/login(UserLoginRequest)returns (UserLoginResponse)
}
使用命令生成代码:goctl api go -api user.api -dir . -style gozero
config配置Auth:
package config
import (
"github.com/generalzy/zeropan/gateway/internal/middleware"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/zrpc"
)
type Config struct {
rest.RestConf
// 映射的是配置文件
UserRpc zrpc.RpcClientConf
AuthMiddleware middleware.AuthMiddleware
// 校验
Auth Auth
}
type Auth struct {
AccessSecret string
AccessExpire int64
}
修改yaml文件:
Name: user-api
Host: 0.0.0.0
Port: 8888
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
Auth:
AccessSecret: 31*&@(*&YUUYO*Q!PP()_@_@_+=
AccessExpire: 36000000
观察routers,发现go-zero在接口上新增了一些关于jwt和middleware的东西:
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AuthMiddleware.Handle},
[]rest.Route{
{
Method: http.MethodPost,
Path: "/api/v1/user/create",
Handler: CreateUserHandler(serverCtx),
},
}...,
),
// 代表create接口需要认证
rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
)
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodGet,
Path: "/api/v1/user/retrieve",
Handler: RetrieveUserHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/api/v1/user/login",
Handler: UserLoginHandler(serverCtx),
},
},
)
}
在login接口中编写签发token的逻辑(原本应该在微服务中编写,然后在login api中写调用微服务的代码,此处图简便,直接写)
func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.UserLoginResponse, err error) {
// 简单生成token
claims := &jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 5)), // 过期时间
Issuer: "generalzy", // 签发人
}
// 生成token对象
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(l.svcCtx.Config.Auth.AccessSecret))
if err != nil {
return nil, err
}
return &types.UserLoginResponse{Token: token}, nil
}
中间件需要到context上下文中初始化:
import (
"github.com/generalzy/zeropan/gateway/internal/config"
"github.com/generalzy/zeropan/gateway/internal/middleware"
"github.com/generalzy/zeropan/user/userclient"
"github.com/zeromicro/go-zero/zrpc"
)
type ServiceContext struct {
Config config.Config
// 此处初始化配置
// UserRpc写的是userclient的User
// go-zero对client封装了一次
UserRpc userclient.User
// 中间件
AuthMiddleware middleware.AuthMiddleware
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)),
AuthMiddleware: *middleware.NewAuthMiddleware(),
}
}
具体逻辑需要到middleware中编写:
type AuthMiddleware struct {
}
func NewAuthMiddleware() *AuthMiddleware {
return &AuthMiddleware{}
}
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 前
next(w, r)
// 后
}
}
由此可见,api网关也就是一个web框架,选择高度集成的框架,还是kong+grpc,或gin+grpc都可以完成微服务开发。
package errorx
type ApiError struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func NewApiError(code int, msg string) *ApiError {
return &ApiError{
Code: code,
Msg: msg,
}
}
// Error 实现error接口
func (e *ApiError) Error() string {
return e.Msg
}
// ParamsError 110代表用户系统 1101代表错误码 微服务的错误可以体现系统
var ParamsError = NewApiError(1101, "参数错误")
type ApiErrorResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
// Data ApiErrorResponse打印详细的信息 否则.Error()会被调用只打印一个msg
func (e *ApiError) Data() *ApiErrorResponse {
return &ApiErrorResponse{
Code: e.Code,
Msg: e.Msg,
}
}
使用paramError:
func (l *RetrieveUserLogic) RetrieveUser(req *types.RetrieveUserRequest) (resp *types.RetrieveUserResponse, err error) {
userID := req.ID
if userID == "" {
return nil, errorx.ParamsError
}
// 获取用户信息
result, e := l.svcCtx.UserRpc.RetrieveUser(context.TODO(), &user.RetrieveUserRequest{
Id: userID,
})
if e != nil {
return nil, e
}
return &types.RetrieveUserResponse{
Name: result.GetName(),
Password: "",
Email: result.GetEmail(),
CreatedAt: result.GetCreateAt(),
UpdatedAt: result.GetUpdateAt(),
DeletedAt: "",
}, nil
}
set自定义的error
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
// 设置自定义error
httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, any) {
switch e := err.(type) {
case *errorx.ApiError:
return http.StatusOK, e.Data()
default:
return http.StatusBadRequest, nil
}
})
httpx.SetErrorHandler(func(err error) (int, any) {
switch e := err.(type) {
case *errorx.ApiError:
return http.StatusOK, e.Data()
default:
return http.StatusBadRequest, nil
}
})
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
如果想要自定义模板,可以执行:goctl template init
,获取模板位置,通过修改模板实现自定制。
使用zap实现logx接口
package zapx
import (
"fmt"
"github.com/zeromicro/go-zero/core/logx"
"go.uber.org/zap"
)
const callerSkipOffset = 3
type ZapWriter struct {
logger *zap.Logger
}
func NewZapWriter(opts ...zap.Option) (logx.Writer, error) {
opts = append(opts, zap.AddCallerSkip(callerSkipOffset))
logger, err := zap.NewProduction(opts...)
if err != nil {
return nil, err
}
return &ZapWriter{
logger: logger,
}, nil
}
func (w *ZapWriter) Alert(v interface{}) {
w.logger.Error(fmt.Sprint(v))
}
func (w *ZapWriter) Close() error {
return w.logger.Sync()
}
func (w *ZapWriter) Debug(v interface{}, fields ...logx.LogField) {
w.logger.Debug(fmt.Sprint(v), toZapFields(fields...)...)
}
func (w *ZapWriter) Error(v interface{}, fields ...logx.LogField) {
w.logger.Error(fmt.Sprint(v), toZapFields(fields...)...)
}
func (w *ZapWriter) Info(v interface{}, fields ...logx.LogField) {
w.logger.Info(fmt.Sprint(v), toZapFields(fields...)...)
}
func (w *ZapWriter) Severe(v interface{}) {
w.logger.Fatal(fmt.Sprint(v))
}
func (w *ZapWriter) Slow(v interface{}, fields ...logx.LogField) {
w.logger.Warn(fmt.Sprint(v), toZapFields(fields...)...)
}
func (w *ZapWriter) Stack(v interface{}) {
w.logger.Error(fmt.Sprint(v), zap.Stack("stack"))
}
func (w *ZapWriter) Stat(v interface{}, fields ...logx.LogField) {
w.logger.Info(fmt.Sprint(v), toZapFields(fields...)...)
}
func toZapFields(fields ...logx.LogField) []zap.Field {
zapFields := make([]zap.Field, 0, len(fields))
for _, f := range fields {
zapFields = append(zapFields, zap.Any(f.Key, f.Value))
}
return zapFields
}
设置日志writer为zap:
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
// 设置自定义error
httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, any) {
switch e := err.(type) {
case *errorx.ApiError:
return http.StatusOK, e.Data()
default:
return http.StatusBadRequest, nil
}
})
httpx.SetErrorHandler(func(err error) (int, any) {
switch e := err.(type) {
case *errorx.ApiError:
return http.StatusOK, e.Data()
default:
return http.StatusBadRequest, nil
}
})
// 替换日志
writer, err := zapx.NewZapWriter()
logx.Must(err)
logx.SetWriter(writer)
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
windows下载链接:https://prometheus.io/download/
编辑prometheus的配置文件prometheus.yml,添加如下配置,并创建targets.json
# my global config
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=` to any timeseries scraped from this config.
# - job_name: "prometheus"
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
# static_configs:
# - targets: ["localhost:9090"]
# job_name可以随便写
- job_name: "user-rpc"
file_sd_configs:
- files:
- targets.json
targets.json
[
{
"targets": ["127.0.0.1:9091"],
"labels": {
"job": "user-rpc",
"app": "user-rpc",
"env": "test",
"instance": "127.0.0.1:8888"
}
}
]
在网关配置:
Name: user-api
Host: 0.0.0.0
Port: 8888
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
Auth:
AccessSecret: 31*&@(*&YUUYO*Q!PP()_@_@_+=
AccessExpire: 36000000
Prometheus:
Host: 127.0.0.1
Port: 9091
Path: /metrics
docker安装docker run -di --rm --name jaeger -p6831:6831/udp -p16686:16686 -p6832:6832/udp -p5778:5778 -p4317:4317 -p4318:4318 -p14250:14250 -p14268:14268 -p14269:14269 -p9411:9411 jaegertracing/all-in-one:latest
go-zero自己做了链路追踪,但jaeger比较知名
网关处配置jaeger:
Name: user-api
Host: 0.0.0.0
Port: 8888
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
Auth:
AccessSecret: 31*&@(*&YUUYO*Q!PP()_@_@_+=
AccessExpire: 36000000
Prometheus:
Host: 127.0.0.1
Port: 9091
Path: /metrics
Telemetry:
Name: user-rpc
Endpoint: http://ip:14268/api/traces
Sample: 1.0
Batcher: jaeger
go-zero与dtm强强联合,推出了在go-zero中无缝接入dtm的极简方案,是go生态中首家提供分布式事务能力的微服务框架。该方案具备以下特征:
分布式事务是在分布式系统中实现一致性的一种机制。在分布式系统中,由于各个节点之间的通信和数据交换可能存在延迟、故障、网络分区等问题,因此需要采用一些特殊的机制来保证数据的一致性。
然而,分布式事务的实现一般需要较高的复杂度和成本,例如需要引入分布式锁、两阶段提交等机制,同时也会对系统的性能产生一定的影响。因此,如果可以通过其他方式来保证系统的一致性,就可以避免使用分布式事务,从而提高系统的性能和可靠性。
在实际的系统设计中,可以采用以下方式来避免使用分布式事务:
尽量采用无状态的设计方式:无状态的设计方式可以使系统更容易扩展和维护,从而降低使用分布式事务的需求。
将系统分解成更小的服务:将系统分解成更小的服务可以降低服务之间的依赖关系,从而减少使用分布式事务的需求。
异步消息传递:将数据的传递方式改为异步消息传递可以避免数据的实时性问题,从而降低使用分布式事务的需求。
最终一致性:使用最终一致性的方式来处理数据更新可以降低分布式事务的需求。
需要注意的是,并不是所有的场景都适合避免使用分布式事务,一些需要强一致性保证的场景仍然需要使用分布式事务来保证数据的一致性。因此,在具体的系统设计中,需要根据实际的需求和场景来进行权衡和选择。
在微服务架构中,当我们需要跨服务保证数据一致性时,原先的数据库事务力不从心,无法将跨库、跨服务的多个操作放在一个事务中。这样的应用场景非常多,我们可以列举出很多:
面对这些本地事务无法解决的场景,我们需要分布式事务的解决方案,保证跨服务、跨数据库更新数据的一致性。
dtm不仅可以解决上述的分布式事务场景,还可以解决更多的与数据一致性相关的场景,包括:
场景,api访问user用户服务,返回之后,再次访问score积分服务,即注册完成后给用户增加积分。如果积分增加出错,则用户注册也必须失败。按照正常逻辑是无法完成的,所以需要引入分布式事务。
下载dtm源码:https://github.com/dtm-labs/dtm
建库建表:
create table if not EXISTS dtm_barrier.barrier(
id BIGINT(22) PRIMARY KEY AUTO_INCREMENT,
trans_type varchar(45) DEFAULT "",
gid VARCHAR(128) default "",
branch_id VARCHAR(128) default "",
op varchar(45) default "",
barrier_id varchar(45) DEFAULT "",
reason varchar(45) default "" comment "the branch type who insert this record",
create_time datetime default now(),
update_time datetime default now(),
key(create_time),
key(update_time),
UNIQUE KEY(gid,branch_id,op,barrier_id)
) ENGINE = INNODB DEFAULT CHARSET =utf8mb4;
创建积分服务goctl rpc protoc pb/score.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.
syntax = "proto3";
package score;
option go_package = "./score";
message UserScoreRequest{
string id = 1;
int32 score = 2;
}
message UserScoreResponse{
string id = 1;
int32 score = 2;
}
service User{
rpc saveUserScore(UserScoreRequest)returns(UserScoreResponse);
// 用作回滚回调
rpc saveUserScoreCallback(UserScoreRequest)returns(UserScoreResponse);
}
将ScoreRpc
也注册到网关中:
Name: user-api
Host: 0.0.0.0
Port: 8888
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
ScoreRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: score.rpc
Auth:
AccessSecret: 31*&@(*&YUUYO*Q!PP()_@_@_+=
AccessExpire: 36000000
Prometheus:
Host: 127.0.0.1
Port: 9091
Path: /metrics
Telemetry:
Name: user-rpc
Endpoint: http://8.130.45.145:14268/api/traces
Sample: 1.0
Batcher: jaeger
修改新增逻辑:
func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) (resp *types.CreateUserResponse, err error) {
response, err := l.svcCtx.UserRpc.CreateUser(context.TODO(), &user.CreateUserRequest{
Name: req.Name,
Email: req.Email,
CreateAt: "",
UpdateAt: "",
})
if err != nil {
return nil, err
}
// 走到此处时,user已经创建,如果此处失败,则user还在,不符合我们的需求。
scoreResponse, err := l.svcCtx.ScoreRpc.SaveUserScore(context.TODO(), &score.UserScoreRequest{
Id: response.Id,
Score: 0,
})
if err != nil {
return nil, err
}
return &types.CreateUserResponse{
ID: scoreResponse.Id,
Name: response.Name,
Email: response.Email,
CreatedAt: response.CreateAt,
UpdatedAt: response.UpdateAt,
}, nil
}
在create login处引入dtm:go get github.com/dtm-labs/dtm
package logic
import (
"context"
"github.com/generalzy/zeropan/score/types/score"
"github.com/generalzy/zeropan/user/types/user"
"github.com/generalzy/zeropan/gateway/internal/svc"
"github.com/generalzy/zeropan/gateway/internal/types"
_ "github.com/dtm-labs/dtmdriver-gozero"
"github.com/zeromicro/go-zero/core/logx"
)
var dtmServer = "etcd://localhost:2379/dtmservice"
user的pb文件新增create callback:
service User{
rpc retrieveUser(RetrieveUserRequest)returns(RetrieveUserResponse);
rpc createUser(CreateUserRequest)returns(CreateUserResponse);
rpc deleteUser(DeleteUserRequest)returns(DeleteUserResponse);
rpc createUserCallback(CreateUserRequest)returns(CreateUserResponse);
}
使用dtm操作svc:
func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) (resp *types.CreateUserResponse, err error) {
gid := dtmgrpc.MustGenGid(dtmServer)
sagaGrpc := dtmgrpc.NewSagaGrpc(dtmServer, gid)
userServer, err := l.svcCtx.Config.UserRpc.BuildTarget()
if err != nil {
return nil, err
}
scoreServer, err := l.svcCtx.Config.ScoreRpc.BuildTarget()
if err != nil {
return nil, err
}
userReq := &user.CreateUserRequest{
Name: req.Name,
Email: req.Email,
CreateAt: "",
UpdateAt: "",
}
// dtm添加user
sagaGrpc.Add(userServer+"/user.User/createUser", userServer+"/user.User/createUserCallback", userReq)
// dtm添加score
scoreReq := &score.UserScoreRequest{
// Id可以另写一个获取ID的服务
Id: "0x12231",
Score: 0,
}
sagaGrpc.Add(scoreServer+"/score.UserScore/saveUserScore", scoreServer+"/score.UserScore/saveUserScoreCallback", scoreReq)
sagaGrpc.WaitResult = true
err = sagaGrpc.Submit()
if err != nil {
return nil, err
}
//response, err := l.svcCtx.UserRpc.CreateUser(context.TODO(), &user.CreateUserRequest{
// Name: req.Name,
// Email: req.Email,
// CreateAt: "",
// UpdateAt: "",
//})
//if err != nil {
// return nil, err
//}
//
//scoreResponse, err := l.svcCtx.ScoreRpc.SaveUserScore(context.TODO(), &score.UserScoreRequest{
// Id: response.Id,
// Score: 0,
//})
//if err != nil {
// return nil, err
//}
return &types.CreateUserResponse{
ID: scoreReq.Id,
Name: userReq.Name,
Email: userReq.Email,
CreatedAt: userReq.CreateAt,
UpdatedAt: userReq.UpdateAt,
}, nil
}
遇到cannot use endpoint (variable of type func() string) as type string in array or slice literal
错误直接修改源码dsn := strings.Join([]string{schemeName + ":/", url.Authority, url.Endpoint()}, "/")
增加事务,需要把逻辑写入到事务中,其他同理,DB需要在上下文中加入
func (l *SaveUserScoreLogic) SaveUserScore(in *score.UserScoreRequest) (*score.UserScoreResponse, error) {
barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
if err != nil {
// 重试
return nil, status.Error(codes.Internal, err.Error())
}
err = barrier.CallWithDB(l.svcCtx.DB, func(tx *sql.Tx) error {
// 此处需要用tx或其他链接写实现saveScore的事务逻辑
logx.Info("saveUserScore called>>>>>>>>>>>>>")
return nil
})
if err != nil {
// 回滚状态
return nil, status.Error(codes.Aborted, err.Error())
}
return &score.UserScoreResponse{
Id: in.Id + "100",
Score: in.Score + 100,
}, nil
}
手动报错:
func (l *SaveUserScoreLogic) SaveUserScore(in *score.UserScoreRequest) (*score.UserScoreResponse, error) {
barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
if err != nil {
// 重试
return nil, status.Error(codes.Internal, err.Error())
}
err = barrier.CallWithDB(l.svcCtx.DB, func(tx *sql.Tx) error {
// 此处需要用tx或其他链接写实现saveScore的事务逻辑
logx.Info("saveUserScore called>>>>>>>>>>>>>")
return errors.New("this is a error")
})
if err != nil {
// 回滚状态
return nil, status.Error(codes.Aborted, err.Error())
}
return &score.UserScoreResponse{
Id: in.Id + "100",
Score: in.Score + 100,
}, nil
}
本demo代码存储:https://github.com/Generalzy/go-zero-demo