0、前言
有人说:真正的架构师,不是在项目中引入多少模块,而是能干掉多少模块。
如果你的架构师有一天把你喊到小黑屋里,悄悄的和你说:“目前咱们项目先不考虑负载均衡,我发现咱们用 consul 做这个注册中心,好像只用到了服务注册发现的功能,那你说,我们能不能把这个 consul 去掉,因为你 rpc 通信现在不也只是从注册中心拿到 ip 和 port,然后访问么,那我直接写死,理论是可行的吧?”
不要着急反驳,先想想真的不可能吗?
1、前置环境
1.1、环境配置
版本 | 说明 | |
---|---|---|
go | 1.13.6 | 语言 |
go-micro | 1.18.0 | 微服务框架 |
protoc | 3.11.4 | protocol buffer 生成工具 |
1.2、go-micro demo 项目
1.2.1、新建下述目录与文件
1.2.2、编写协议
在 hello.proto 写入下面的 SayHello。
syntax = "proto3";
package proto;
option go_package = "proto/pb";
message HelloReq {
int32 id = 1;
}
message HelloResp {
string msg = 1;
}
service SayHelloHandler {
rpc SayHello(HelloReq) returns(HelloResp){}
}
cd 到项目的 proto 目录,输入 protoc --micro_out=../ --go_out=../ hello.proto
生成对应的协议实现。
1.2.3、服务方(A)
我们将 A 作为服务方,即:我们在 A 实现 SayHello 协议,并且启动服务等待 B 服务调用。
具体的如何实现就不在赘述了,感兴趣的同学请自行学习 grpc 相关知识。
实现如下:
type SayHelloHandler struct {
}
func (shh *SayHelloHandler) SayHello(ctx context.Context, req *pb.HelloReq, resp *pb.HelloResp) error {
fmt.Println(req.Id)
resp.Msg = "hello world"
return nil
}
在这里我们打印一下 req 带来的 Id,并设置 resp 的 Msg 为 hello world,返回给调用者。
OK,剩下的就很简单了,启动 go-micro 服务
A.go 文件完整代码如下:
package main
import (
"context"
"fmt"
"github.com/Mor1aty/we_need_not_consul/proto/pb"
"github.com/micro/go-micro"
"log"
)
func main() {
server := micro.NewService(
micro.Name("A"),
micro.Address(":8002"),
micro.Version("1.0"),
)
shh := new(SayHelloHandler)
if err := pb.RegisterSayHelloHandlerHandler(server.Server(), shh); err != nil {
log.Fatalf("A server register SayHello failed, err: %v", err)
}
server.Init()
if err := server.Run(); err != nil {
log.Fatalf("A server run failed, err: %v", err)
}
}
type SayHelloHandler struct {
}
func (shh *SayHelloHandler) SayHello(ctx context.Context, req *pb.HelloReq, resp *pb.HelloResp) error {
fmt.Println(req.Id)
resp.Msg = "hello world"
return nil
}
这里我们先不把 A 服务注册到某个地方,因为我们的目标毕竟是干掉注册中心。
1.2.4、调用方(B)
调用方我就不过多赘述了,B.go 文件代码如下:
package main
import (
"context"
"fmt"
"github.com/Mor1aty/we_need_not_consul/proto/pb"
"github.com/micro/go-micro"
"log"
)
func main() {
server := micro.NewService()
shhs := pb.NewSayHelloHandlerService("A", server.Client())
resp, err := shhs.SayHello(context.Background(), &pb.HelloReq{
Id: 10,
})
if err != nil {
log.Fatalf("SayHello request failed, err: %v", err)
}
fmt.Println(resp)
}
2、go-micro 插件
首先我们会想到 go-micro 是否已经提供了这样的插件(不需要注册中心的插件)。成为一个合格 gopher 的第一堂课就是认识到其他的都是假的,只有 github 才是真的。
找到 asim 的 go-plugins 项目,我们惊喜的发现,register 目录下有一个 memory 似乎符合我们的要求。
memory,内存。那放在 registry 的目录下的意思是不是指注册到内存里的意思,如果说可以把内存作为注册中心,那岂不是变相的干掉了注册中心!
OK,我们直接引入看看。
A.go
reg := memory.NewRegistry()
server := micro.NewService(
micro.Name("A"),
micro.Address(":8002"),
micro.Version("1.0"),
micro.Registry(reg),
)
B.go
reg := memory.NewRegistry()
server := micro.NewService(
micro.Registry(reg),
)
我们直接 go run 尝试一次
这里可以看到 A 服务被注册到了 memory 中。
然而,我们 go run B 服务就会发现,他找不到 A 服务。
这是为什么呢?
3、为什么找不到?
这一节的标题表明了,这里主要是探究为什么找不到,不感兴趣的同学可以直接翻看下一节。
明明 A 服务已经被注册到 memory 里了,为什么会显示找不到呢?
为了解决这个问题,我翻遍了 go 中文网,go-micro 官网,百度,谷歌,反正是没有找到相关的资料。甚至就连 memory 这个插件也很少有地方提到。如果有找到相关资料的小伙伴,请私聊我,非常感谢。
没有办法,看源码吧。
这里我们可以发现,github.com/micro/go-plugins/registry/memory 这个包是直接调用另一个地方的 memory 来实现功能。继续深入,我们发现这个插件调用的是 go-micro 提供的 memory。
可喜可贺,至少我们不必引入 github.com/micro/go-plugins/registry/memory 了。可以直接调用 go-micro 本身的 registry 了。
继续深入在他的 util 里我们可以发现。go-micro 注册服务到内存,具体是把服务以 record 结构体的形式存储到一个 map 中。
而这个 map 是 Registry 的一个属性。
那事情就明朗了,回去看我们 A.go 和 B.go 的代码。
A 和 B 分别 new 了一个 Registry,连“注册中心”都不是同一个,B 服务是一定访问不到 A 服务的。
那我们能否抽取出一个公共方法,维护同一个 registry,这样可行吗?我们试试
首先,在新建的 common 目录下新建一个 common.go,编写如下代码。
package common
import (
"github.com/micro/go-micro/registry"
"github.com/micro/go-plugins/registry/memory"
)
var reg registry.Registry
func GetReg() registry.Registry {
if reg == nil {
reg = memory.NewRegistry()
return reg
}
return reg
}
修改 A.go 和 B.go
go run 一下
不幸的是,依然不行。
实际上,这个想法在想出来的时候,就应该被否定了。甚至说这个想法都不应该出现。因为 A 和 B 在分开 go run 运行的情况下,他俩明显是两个不同的服务。他们会分别建立一个 common.go,那自然 reg 也不可能是通用的了。
能想到这个操作,可见我当时可能已经“失了智”。
4、那咋办嘛
实际上,到了这个时候,我差不多已经处于放弃的阶段了。在这个问题上,已经花费了一个下午的时间了(大部分用在谷歌百度上)。
今天早晨刷牙的时候,我忽然想到,服务注册只是把自己注册到一个注册中心里,如果说他只是做这个动作的话,那为什么我不能主动去完成这个动作呢?
在服务启动的时候,就把其他的服务注册到 memory 的那个 map 里。
有了思路,我们继续上路。
翻看源码,我们可以发现 memory 的 registry 提供了一个 Register 方法,通过这个方法可以把服务注册到 memory 里。
ok,改造一下我们的代码。
这里注意,go.mod 可以直接去除 github.com/micro/go-plugins/registry/memory 依赖,因为他也是调用 github.com/micro/go-micro/registry/memory 来工作的,我们就不需要 go-plugins 这个中间商了。
改造一下 A.go,服务端相对较为简单。只需要修改为原来的 memory 注册就好。注意上面的引用已经换成了 go-micro 的 registry。
主要是 B.go,这么多属性,我们该写什么呢?
不要慌,go-micro 给我们提供了一个 GetService 方法,帮助我们根据服务名获取服务,我们可以在 A 服务启动之后,调用这个方法来,来查看内存中存储 A 服务的属性。
我这里引入了 gin 框架,提供了一个 GET 服务来查看的。大家可以采用自己合适的方法来查看。
改造一下 A.go。
A.go :
package main
import (
"context"
"fmt"
"github.com/Mor1aty/we_need_not_consul/proto/pb"
"github.com/gin-gonic/gin"
"github.com/micro/go-micro"
"github.com/micro/go-micro/registry/memory"
"github.com/micro/go-micro/web"
"log"
)
func main() {
reg := memory.NewRegistry()
server := micro.NewService(
micro.Name("A"),
micro.Address(":8002"),
micro.Version("1.0"),
micro.Registry(reg),
)
r := gin.Default()
r.GET("/findA", func(c *gin.Context) {
service, err := reg.GetService("A")
if err != nil {
fmt.Println(err)
c.String(200, "success")
return
}
for _, s := range service {
fmt.Printf("%#v\n", s)
for _, n := range s.Nodes {
fmt.Printf("%#v\n", n)
}
}
c.String(200, "success")
})
webServer := web.NewService(
web.Address(":8080"),
web.Name("A"),
web.Handler(r),
web.MicroService(server),
)
shh := new(SayHelloHandler)
if err := pb.RegisterSayHelloHandlerHandler(server.Server(), shh); err != nil {
log.Fatalf("A server register SayHello failed, err: %v", err)
}
webServer.Init()
if err := webServer.Run(); err != nil {
log.Fatalf("A server run failed, err: %v", err)
}
}
type SayHelloHandler struct {
}
func (shh *SayHelloHandler) SayHello(ctx context.Context, req *pb.HelloReq, resp *pb.HelloResp) error {
fmt.Println(req.Id)
resp.Msg = "hello world"
return nil
}
这里改造有些点需要特别注意,请大家详细看一下上面的 A.go 代码。
go run 一下。
这里我们可以发现,当服务注册的时候,Name,Version 是需要指定的,Metadata,Endpoints 可以不指定。
Nodes 只需要指定 Id,Address。Metadata 是可以不指定的。
Name,Version,Address 都好说,关键是这个 Id 怎么办,从打印中我们可以看到,这个 Id 是个随机的,那是不是意味着我们可以不指定,然后让 go-micro 自动生成呢?试一试。
改造一下 B.go
package main
import (
"context"
"fmt"
"github.com/Mor1aty/we_need_not_consul/proto/pb"
"github.com/micro/go-micro"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/registry/memory"
"log"
)
func main() {
reg := memory.NewRegistry()
if err := reg.Register(®istry.Service{
Name: "A",
Version: "1.0",
Nodes: []*registry.Node{
{
Address: "你的 ip:8002",
},
},
}); err != nil {
log.Fatalf("registry failed, err: %v", err)
}
server := micro.NewService(
micro.Registry(reg),
)
shhs := pb.NewSayHelloHandlerService("A", server.Client())
resp, err := shhs.SayHello(context.Background(), &pb.HelloReq{
Id: 10,
})
if err != nil {
log.Fatalf("SayHello request failed, err: %v", err)
}
fmt.Println(resp)
}
注意,打码的部分(Address)请自行替换成自己的ip。
go run 一下。
nice!史诗级突破,我们可以发现,他已经访问到 A 服务,只是被拒绝了而已。
那这里我进行了猜测,是不是 A 服务不仅用到了 go-micro 的 rpc,还引入了 gin 作为 http 服务呢?
毕竟 A 服务启动的是 web 那个 server,而不是 rpc 那个 server。有可能,先试试。
我们把 A 服务引入 gin 去掉,同时换回 rpc server run。
package main
import (
"context"
"fmt"
"github.com/Mor1aty/we_need_not_consul/proto/pb"
"github.com/micro/go-micro"
"github.com/micro/go-micro/registry/memory"
"log"
)
func main() {
reg := memory.NewRegistry()
server := micro.NewService(
micro.Name("A"),
micro.Address(":8002"),
micro.Version("1.0"),
micro.Registry(reg),
)
//r := gin.Default()
//r.GET("/findA", func(c *gin.Context) {
// service, err := reg.GetService("A")
// if err != nil {
// fmt.Println(err)
// c.String(200, "success")
// return
// }
// for _, s := range service {
// fmt.Printf("%#v\n", s)
// for _, n := range s.Nodes {
// fmt.Printf("%#v\n", n)
// }
// }
// c.String(200, "success")
//})
//
//webServer := web.NewService(
// web.Address(":8080"),
// web.Name("A"),
// web.Handler(r),
// web.MicroService(server),
//)
shh := new(SayHelloHandler)
if err := pb.RegisterSayHelloHandlerHandler(server.Server(), shh); err != nil {
log.Fatalf("A server register SayHello failed, err: %v", err)
}
server.Init()
if err := server.Run(); err != nil {
log.Fatalf("A server run failed, err: %v", err)
}
}
type SayHelloHandler struct {
}
func (shh *SayHelloHandler) SayHello(ctx context.Context, req *pb.HelloReq, resp *pb.HelloResp) error {
fmt.Println(req.Id)
resp.Msg = "hello world"
return nil
}
go run 一下:
Nice!
成了。
5、总结
经过将近一个工作日的斗智斗勇,我终于实现了架构师大佬提出的去除注册中心的想法。尽管只是把注册中心从 consul, erueka 换成了内存而已。不过这也无需在引入新的模块了,也算是好事吧。
在这个过程中,我有了两个体会。一个是源码并没有那么可怕,只要细心一点还是能看明白的。另一个是 go 的文章真的好难找啊。跪求各位 gopher 大佬们多分享分享啊,我太难了。
这边文章中使用的 we_need_not_consul 项目,我上传到了自己的 github,大家可以直接下载查看。地址:https://github.com/Mor1aty/we_need_not_consul