上一篇文章我们用etcd做为服务发现组件,替换了micro默认的基于mnds的服务发现,并简单通过跟踪源码了解了服务注册以及发现的原理。这篇文章,我们来认识微服务架构中另外一个很常见的东东:API Gateway。
我们把一个应用拆分成了一个一个的微服务后,客户端如何调用就是个问题,因为服务是部署在不同的机器上面,这样客户端(比如iOS,android,web)势必将使用很多不同的URL,API网关最主要的作用实际上就是作为一个统一的入口,所有的请求都指向网关,再由网关负责协议转换,并将请求路由到后端服务上,以下是micro github主页上关于api gateway的一张图:
除了做为统一的请求入口外,API网关还具备安全防护、限流、熔断等功能。通常大厂都有自己自研的一套网关用来接入其内部服务,比如腾讯的TGW。
micro框架也实现了基于HTTP协议的网关,用户通过http协议向网关发送请求,再由网关将请求转发给后端服务。
从安全性来讲,micro实现的这个网关支持ACME和TLS。
至于网关的性能,目前还没做过测试,不得而知,因为网关并不做特别重的逻辑,主要是负责转发请求,因此猜测中小规模的APP应该是可以支持到。
现在我们就继续开撕代码,给我们的示例程序加个网关玩玩。
先来看看micro api的基本用法,我们再命令行输入:micro api --help,可以看到以下输出:
我们用到以下几个选项:
--address:指定网关地址,默认实在8080端口
--namespace:可以通过namespace将服务归类,例如对外的公共服务和内部服务,注意这里的域名要跟代码对应起来,比如域名为com.test.api,那么代码中的服务名就要以com.test.api开头。
--handler:网关用到的http handler,不同的handler处理不同的请求,对请求的处理方式也有所不同,micro支持以下几种handler:
api handler:可以处理任意http请求,并以RPC的方式将请求转发给后端服务,url path为/service/method,例如http://localhost:8080/greeter/hello,则网关会在注册中心中寻找endpoints为Greeter.Hello的服务并转发;
rpc handler:处理http post请求,http body为json或者protobuf格式(Content-Type为application/json或者application/protobuf),url path的要求同api handler;
event handler:将请求做为消息发送到消息中间件上;
proxy handler:用于http反向代理;
具体用哪种handler需要根据自己业务的需求来定,我们就用rpc handler来实操一下。
我们先来启动网关:
注意在启动网关时的一些参数:
MICRO_REGISTRY:用来指定注册中心,我们用etcd做注册中心,所以这个值写etcd;
MICRO_REGISTRY_ADDRESS:指定注册中心的地址,本地测试,就写本机地址(etcd默认端口是2379)
--handler:我们采用rpc handler,网关将会根据服务的EndPoint找到服务并转发请求;
--namespace:指定命名空间
--address:网关地址
服务端的代码与之前一样,我们稍作修改,改一下服务名:
package main
import (
"context"
"fmt"
"github.com/micro/go-micro/v2"
"micro/proto/pb"
"micro/registry/etcd"
)
type Greeter struct {
}
func (g *Greeter) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
//把客户端的请求回射给客户端
rsp.Msg = req.Name
return nil
}
func main() {
// 新创建一个服务,服务名为greeter,服务注册中心会用这个名字来发现服务
service := micro.NewService(
micro.Name("svr.greeter"),
micro.Registry(etcd.NewRegistry()),
)
// 初始化
service.Init()
// 注册处理器
pb.RegisterGreeterHandler(service.Server(), new(Greeter))
// 启动服务运行
if err := service.Run(); err != nil {
fmt.Println(err)
}
}
我们只是在创建服务的时候将服务名改成了svr.greeter,其他没有任何变化。
然后在终端启动运行服务即可。
下面要修改客户端,客户端实际上是服务端的一个代理,从设计模式的角度来看实际上就是个代理模式,因此客户端和服务端需要实现相同的接口,在客户端可以做一些额外的操作(比如参数安全校验之类的),然后通过rpc调用服务端。
package main
import (
"context"
"fmt"
"github.com/micro/go-micro/v2"
"micro/proto/pb"
"micro/registry/etcd"
)
const (
apiNameSpace = "com.jupiter.api"
service = "greeter"
backendService = "svr.greeter" //真正做事情的后端服务
)
/**
* url path:/greeter/hello
* 接收网关转发的请求,通过rpc将请求转发给后端服务
* 实际上就是个后端服务的代理(客户端),实现和服务端相同的接口
*/
type Greeter struct {
Client pb.GreeterService
}
func (g *Greeter) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
//通过rpc调用服务端
response, e := g.Client.Hello(ctx, &pb.Request{Name: "Hello Micro"})
if e != nil {
return e
}
rsp.Msg = response.Msg
return nil
}
func main() {
// 创建一个服务
service := micro.NewService(
micro.Name(apiNameSpace + "." + service), //注意这里服务的名称:namespace + service,这样网关才能找的到
micro.Registry(etcd.NewRegistry()))
// 初始化
service.Init()
pb.RegisterGreeterHandler(service.Server(), &Greeter{
Client: pb.NewGreeterService(backendService, service.Client()),
})
if err := service.Run(); err != nil {
fmt.Println(err)
}
}
以上代码中关键的地方都做了注释,在强调一下:
代理服务的名称必须是namespace.service,namespace是api网关的namespace,service是在注册中心的EndPoint的服务名,这个名称一般就是.proto协议文件中定义的service的名字,例如我们的.proto定义:
// 定义微服务对外提供的接口
service Greeter {
rpc Hello(Request) returns (Response) {}
}
那么service就是Greeter,完整的名称就是com.jupiter.api.greeter。
现在我们再终端启动客户端运行。
现在来测试一下加了网关后的服务,我们再终端用curl请求一下:
输出了我们想要的结果!
这篇文章我们进一步扩展了微服务示例代码,在应用中加入了API网关,统一请求入口,现在我们这个极简单的微服务已经是“麻雀虽小五脏俱全了”