概述
- 本文使用最简单和快速的方式基于Gin框架搭建一个微服务的网关调用微服务的场景
- 网关作为客户端基于RPC调用某一服务端的服务并接入熔断和限流以及链路追踪
- 具体场景:通过网关API查询购物车里的数据
- 在最后,会贴上网关和购物车服务的代码仓库
服务端搭建
1 )目录结构
cart
├── domain
│ ├── model
│ │ └── cart.go
│ ├── repository
│ │ └── cart_repository.go
│ ├── service
│ │ └── cart_data_service.go
├── handler
│ └── cart.go
├── proto
│ ├── cart
│ ├── cart.proto
│ ├── cart.pb.go # 待生成
│ └── cart.pb.micro.go # 待生成
├── go.mod
├── main.go
└── Makefile
- 可以看到这是基于领域模型搭建的框架,默认是通过 go-micro生成的模板,之后进行修改的
- proto 下的 pb.go 和 pb.micro.go 是通过 Makefile 中配置的 protoc 命令生成的
- $
protoc --proto_path=. --micro_out=. --go_out=:. proto/cart/*.proto
- $
2 ) 核心 main.go
package main
import (
"fmt"
"log"
"strconv"
"github.com/go-micro/plugins/v4/registry/consul"
opentracingTool "github.com/go-micro/plugins/v4/wrapper/trace/opentracing"
"github.com/go-micro/plugins/v4/wrapper/ratelimiter/ratelimit"
jujuratelimit "github.com/juju/ratelimit"
"github.com/opentracing/opentracing-go"
"go-micro.dev/v4"
"go-micro.dev/v4/registry"
"gitee.com/go-micro-services/cart/domain/repository"
"gitee.com/go-micro-services/cart/domain/service"
"gitee.com/go-micro-services/cart/handler"
pbcart "gitee.com/go-micro-services/cart/proto/cart"
"gitee.com/go-micro-services/common"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
var (
serviceName = "go.micro.service.cart"
version = "latest"
host = "127.0.0.1"
port = 8500
address = host + ":" + strconv.Itoa(port)
mysqlConfigPath = "/micro/config/mysql"
)
func main() {
// 1. 指定注册中心
consulReg := consul.NewRegistry(
registry.Addrs(address),
)
// 2. 从配置中心获取mysql配置并创建处理
mysqlConfig, consulConfigErr := common.GetConsulMysqlConfig(address, mysqlConfigPath)
if consulConfigErr != nil {
log.Fatal(consulConfigErr)
}
mysqlUrl := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", mysqlConfig.User, mysqlConfig.Pwd, mysqlConfig.Host, mysqlConfig.Port, mysqlConfig.Database)
db, dbErr := gorm.Open("mysql", mysqlUrl)
if dbErr != nil {
log.Fatal(dbErr)
}
defer db.Close()
rp := repository.NewCartRepository(db)
// 数据库表初始化,只执行一次, 如果本来就设计好了,则无需下面
// rp.InitTable()
// db.SingularTable(false) // true 则 表就是单数
// 3. 链路追踪配置
tracer, closer, tracerErr := common.NewTracer("go.micro.service.cart", "localhost:6831")
if tracerErr != nil {
log.Fatal(tracerErr)
}
defer closer.Close()
opentracing.SetGlobalTracer(tracer)
// 4. 创建服务实例
cartDataService := service.NewCartDataService(rp)
// 5. 创建服务和初始化
srv := micro.NewService()
srv.Init(
micro.Name(serviceName),
micro.Version(version),
micro.Registry(consulReg),
// micro.Address("0.0.0.0:8087"), // 暴露的服务地址 这里可用可不用指定
micro.WrapHandler(opentracingTool.NewHandlerWrapper(opentracing.GlobalTracer())), // 绑定链路追踪
micro.WrapHandler(ratelimit.NewHandlerWrapper(jujuratelimit.NewBucket(50, 100), true)), // 添加限流
)
// 6. 注册 handler
if handlerErr := pbcart.RegisterCartHandler(srv.Server(), &handler.Cart{CartDataService: cartDataService}); handlerErr != nil {
log.Fatal(handlerErr)
}
// 7. 运行服务
if runErr := srv.Run(); runErr != nil {
log.Fatal(runErr)
}
}
- 以下是一些注意事项
- 服务初始化需要借助mysql,mysql的一些连接信息是从consul配置中心获取的
- 在初始化的时候,需要先运行一次
rp.InitTable()
和db.SingularTable(false)
- 上面
jujuratelimit.NewBucket(50, 100)
中的参数50, 100可以写入环境变量来获取,这个可以根据具体资源情况进行调节
- 这里,购物车服务已经搭建完成,可以进行启动了
- $
sudo go run main.go
- $
- 启动后,在consul中查看,发现已经注册成功了
网关搭建
1 )目录结构
api
├── conf
│ ├── app.ini # 通用配置文件
│ ├── env.local.ini # 本地配置文件
│ ├── env.test.ini # 测试环境配置
│ ├── env.uat.ini # uat环境配置
│ ├── env.prod.ini # prod环境配置
├── controllers
│ ├── api
│ └── api.go
├── middlewares
│ └── tracer.go # 链路追踪中间件
├── routers
│ └── router.go # 路由配置
├── utils
│ ├── common.go # 工具包通用工具
│ ├── conf.go # 读取配置工具
│ └── micro.go # 微服务工具
├── go.mod
├── main.go
└── Makefile
2 )通用配置信息
[app]
appName = go.micro.api
appVersion = latest
appZeroHost = 0.0.0.0
appAddr = 0.0.0.0:8080
[consul]
address = 127.0.0.1:8500
[jaeger]
tracerAddr = 127.0.0.1:6831
[hystrix]
hystrixPort = 9096
3 ) main.go 核心代码
package main
import (
"gitee.com/go-micro-services/api/middlewares"
"gitee.com/go-micro-services/api/routers"
"gitee.com/go-micro-services/api/utils"
"github.com/gin-gonic/gin"
"go-micro.dev/v4/web"
)
func main() {
// 1. 创建一个默认的路由引擎
ginRouter := gin.Default()
ginRouter.Use(middlewares.Trace()) // 加入 tracing 中间件
routers.RoutersInit(ginRouter)
// 2. web网关服务开启
server := web.NewService(
web.Name(utils.AppName), // 服务名称
web.Address(utils.AppAddr), // 服务端口
web.Handler(ginRouter), // 服务路由
web.Registry(utils.ConsulReg), // 注册中心
)
// 3. 启动
server.Run()
}
- 这里,web模块是go-micro中用于构建Web服务的部分
- 它允许你使用标准的HTTP协议来暴露和调用微服务
- web.NewService 函数的作用
- 用于创建一个新的Web服务实例
- 该函数接受一系列的配置参数
- 用于定义服务的名称、地址、处理程序(即路由引擎)以及注册中心等信息
- 下面是web.NewService函数中各个参数的意义
web.Name(utils.AppName)
: 设置服务的名称, 这通常用于服务发现和注册中心中的服务标识web.Address(utils.AppAddr)
: 设置服务的监听地址和端口号, 这决定了服务应该在哪里监听传入的HTTP请求web.Handler(ginRouter)
: 设置服务的路由处理程序, 这里传入的是基于gin框架的路由引擎实例,它定义了如何处理传入的HTTP请求web.Registry(utils.ConsulReg)
: 设置服务的注册中心。这允许服务在启动时将自己注册到指定的注册中心(例如Consul),以便其他服务可以发现和调用它
4 )utils 包
-
utils.common.go
package utils import ( "fmt" "os" "gopkg.in/ini.v1" ) // 读取通用配置 func getConfig() *ini.File { appConfig, iniErr := ini.Load("conf/app.ini") if iniErr != nil { fmt.Printf("Fail to read file: %v", iniErr) os.Exit(1) } return appConfig } func init() { // 1. 读取配置文件 appConfig := getConfig() // 2. 获取配置 initConfig(appConfig) // 3. 初始化微服务 initMicro(appConfig) }
-
utils/conf.go
package utils import ( "gopkg.in/ini.v1" ) // 通用配置 var AppName string var AppVersion string var AppAddr string var AppZeroHost string // 读取通用配置 func initConfig(appConfig *ini.File) { AppName = appConfig.Section("app").Key("appName").String() // 应用名称 AppVersion = appConfig.Section("app").Key("appVersion").String() // 应用版本 AppAddr = appConfig.Section("app").Key("appAddr").String() // 应用地址 AppZeroHost = appConfig.Section("app").Key("appZeroHost").String() // 零地址 }
-
utils/micro.go
package utils import ( "context" "fmt" "net" "net/http" "go-micro.dev/v4" "go-micro.dev/v4/client" "gopkg.in/ini.v1" "gitee.com/go-micro-services/common" "github.com/go-micro/plugins/v4/registry/consul" opentracingTool "github.com/go-micro/plugins/v4/wrapper/trace/opentracing" "github.com/opentracing/opentracing-go" log "go-micro.dev/v4/logger" "go-micro.dev/v4/registry" "github.com/afex/hystrix-go/hystrix" "github.com/go-micro/plugins/v4/wrapper/select/roundrobin" ) // 微服务micro客户端 var SrvClient client.Client var ConsulAddr string var ConsulReg registry.Registry var TracerAddr string var HystrixPort string // 读取通用配置 func initMicro(appConfig *ini.File) { // 1. 配置注册中心 ConsulAddr = appConfig.Section("consul").Key("address").String() ConsulReg := consul.NewRegistry( registry.Addrs(ConsulAddr), ) // 2. 配置链路追踪 jaeger TracerAddr = appConfig.Section("jaeger").Key("tracerAddr").String() // 3. 配置熔断 HystrixPort = appConfig.Section("hystrix").Key("hystrixPort").String() setHystrix() // 4. 创建服务 srv := micro.NewService() srv.Init( micro.Name(AppName), micro.Version(AppVersion), micro.Registry(ConsulReg), // 绑定链路追踪 micro.WrapHandler(opentracingTool.NewHandlerWrapper(opentracing.GlobalTracer())), // 添加熔断 micro.WrapClient(NewClientHystrixWrapper()), // 添加负载均衡 micro.WrapClient(roundrobin.NewClientWrapper()), ) SrvClient = srv.Client() // 对外暴露 } // 链路追踪配置 func SetTracer() (opentracing.Tracer, error) { tracer, closer, err := common.NewTracer(AppName, TracerAddr) if err != nil { log.Fatal(err) } defer closer.Close() return tracer, err } // 熔断器 设置 func setHystrix() { hystrixStreamHandler := hystrix.NewStreamHandler() hystrixStreamHandler.Start() go func() { // 启动端口 err := http.ListenAndServe(net.JoinHostPort(AppZeroHost, HystrixPort), hystrixStreamHandler) if err != nil { log.Error(err) } }() } // 熔断器 type type clientWrapper struct { client.Client } // 熔断器 Call func (c *clientWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { return hystrix.Do(req.Service()+"."+req.Endpoint(), func() error { // run 正常执行 fmt.Println(req.Service() + "." + req.Endpoint()) return c.Client.Call(ctx, req, rsp, opts...) }, func(err error) error { fmt.Println(err) return err }) } // 熔断器 Wrapper func NewClientHystrixWrapper() client.Wrapper { return func(i client.Client) client.Client { return &clientWrapper{i} } }
5 )路由
package routers
import (
"gitee.com/go-micro-services/api/controllers/api"
"github.com/gin-gonic/gin"
)
func RoutersInit(r *gin.Engine) {
rr := r.Group("/api")
{
rr.GET("/findAll", api.ApiController{}.FindAll)
}
}
6 ) 控制器
package api
import (
"context"
"fmt"
"strconv"
"gitee.com/go-micro-services/api/utils"
cart "gitee.com/go-micro-services/cart/proto/cart"
"github.com/gin-gonic/gin"
"github.com/prometheus/common/log"
)
type ApiController struct{}
func (con ApiController) FindAll(c *gin.Context) {
log.Info("接受到 /api/findAll 访问请求")
// 1. 获取参数
user_id_str := c.Query("user_id")
userId, err := strconv.ParseInt(user_id_str, 10, 64)
if err != nil {
c.JSON(200, gin.H{
"message": "参数异常",
"success": false,
})
return
}
fmt.Println(userId)
// 2. rpc 远程调用:获取购物车所有商品
cartClient := cart.NewCartService("go.micro.service.cart", utils.SrvClient)
cartAll, err := cartClient.GetAll(context.TODO(), &cart.CartFindAll{UserId: userId})
fmt.Println(cartAll)
fmt.Println("-----")
c.JSON(200, gin.H{
"data": cartAll,
"success": true,
})
}
7 ) 服务启动准备
- 这里可以看到,基于gin框架来搭建了一个网关
- 这里可以访问 /api/findAll?user_id=xxx 来调用购物车的微服务
- 现在我们准备下数据,在数据库中初始化一条数据
- 现在就可以通过:网关ip:8080/api/findAll?user_id=1 来调用了
8 )服务启动
- $
sudo go run main.go
- 可以看到,服务已经启动了
服务调用与测试
1 ) 调用
- 访问网关ip:8080/api/findAll?user_id=1
- 可以看到,调用成功
2 )查看链路追踪
- 在 JAEGER UI 上, 比如本机访问:http://127.0.0.1:16686/search
- 可以看到链路追踪上多了2个服务
- 我们可以看下 api 的相关调用
- 再看下 cart 的服务调用
- 当然以上都是通常的调用,如果存在复杂的调用关系或出错信息,也可以从这里看出来
3 )测试限流
- 在已搭建好的熔断面板上可以查看熔断功能:http://127.0.0.1:9002/hystrix
- 因为熔断器是在客户端,也就是网关层接入的,所以,上面填入 网关ip:/hystrix.stream,比如上面的:192.168.1.7:9096/hystrix.stream
- 好,现在我们频繁访问,刷新API,来测试下
- 可见峰值不超过 60%,限流成功
总结
- 从上面可见,我们基于gin框架搭建网关服务和微服务基本已经调通了
- 相关原理和更多细节参考
- 1 ) jaeger 链路追踪
- https://blog.csdn.net/Tyro_java/article/details/137754812
- 2 )hystrix 熔断
- https://blog.csdn.net/Tyro_java/article/details/137777246
- 3 ) ratelimit 限流
- https://blog.csdn.net/Tyro_java/article/details/137780273
- 1 ) jaeger 链路追踪
- 源码
- 服务端 cart 服务:https://gitee.com/go-micro-services/cart
- 客户端 网关api 服务: https://gitee.com/go-micro-services/api