「码动四季·开源同行」go语言:微服务网关如何作为服务端统一入口点?

在单体架构中,客户端在向服务端发起请求时,会通过类似Nginx 的负载均衡组件获取到多个相同的应用程序实例中的一个。请求由该服务实例进行处理,服务端处理完之后返回响应给客户端。

而在微服务架构下,原来的单体应用拆分成了多个业务微服务。此时,直接对外暴露这些业务微服务,必然会存在一些问题。客户端直接向每个微服务发送请求,其问题主要如下:

  • API粒度的问题,客户端需求和每个微服务暴露的细粒度可能存在 API不匹配的情况。
  • 微服务之间的调用可能不仅仅基于 HTTP的方式,还有可能使用 Thrift、gRPC 和 AMQP 消息传递协议,这些API无法暴露出去。
  • 直接对外暴露接口,使得微服务难以重构,特别是服务数量达到一个量级,这类重构就非常困难了。

如上问题,解决的方案是使用微服务网关。网关在一个API架构中的作用是保护、增强和控制外部请求对于API服务的访问。

什么是微服务网关

在微服务架构中,网关位于接入层之下和业务服务层之上。微服务网关是微服务架构中的一个基础服务,从面向对象设计的角度看,它与外观模式类似。

微服务架构图

微服务网关封装了系统内部架构,为每个客户端提供一个定制的AP,用来保护、增强和控制对于微服务的访问。换句话来讲,微服务网关就是一个处于应用程序或服务之前的系统,用来管理授权、访问控制和流量限制等,这样微服务就会被微服务网关保护起来,对所有的调用者透明。因此,隐藏在微服务网关后面的业务系统就可以更加专注于业务本身。

微服务网关的功能特性

作为连接服务消费方和服务提供方的中间件系统,微服务网关将各自业务系统的演进和发展做了天然的隔离,使业务系统更加专注于业务服务本身,同时微服务网关还可以为服务提供和沉淀更多附加功能。
微服务网关的主要功能特性如下图所示:

结合该图,我们就来具体介绍下这四类功能

  • **请求接入。**管理所有接入请求,作为所有API接口的请求入口。在生产环境中,为了保护内部系统的安全性,往往内网与外网都是隔离的,服务端应用都是运行在内网环境中,为了安全,一般不允许外部直接访问。网关可以通过校验规则和配置白名单,对外部请求进行初步过滤,这种方式更加动态灵活。
  • **统一管理。**可以提供统一的监控工具、配置管理和接口的API文档管理等基础设施。例如,统一配置日志切面,并记录对应的日志文件。
  • **解耦。**可以使得微服务系统的各方能够独立、自由、高效、灵活地调整,而不用担心给其他方面带来影响。软件系统的整个过程中包括不同的角色,有服务的开发提供方、服务的用户、运维人员、安全管理人员等,每个角色的职责和关注点都不同。微服务网关可以很好地解耦各方的相互依赖关系,让各个角色的用户更加专注自己的目标。
  • **拦截插件。**服务网关层除了处理请求的路由转发外,还需要负责认证鉴权、限流熔断、监控和安全防范等,这些功能的实现方式,往往随着业务的变化不断调整。这就要求网关层提供一套机制,可以很好地支持这种动态扩展。拦截策略提供了一个扩展点,方便通过扩展机制对请求进行一系列加工和处理。同时还可以提供统一的安全、路由和流控等公共服务组件。

实战案例:自己动手实现一个网关

API网关最基础的功能是对请求进行路由转发,根据配置的转发规则将请求动态地转发到指定的服务实例。动态是指与服务发现结合,如Consul、ZooKeeper等组件,我们在前面的"服务注册与发现"模块已详细讲解。本课时我们将会使用 Go 实现一个简易的API网关。

API网关根据客户端HTTP请求,动态查询注册中心的服务实例,通过反向代理实现对后台服务的调用。

API网关将符合规则的请求路由调用对应的后端服务。这里的规则可以有很多种,如HTTP 请求的资源路径、方法、头部和参数等。这里我们以最简单的请求路径为例,规则为:/{serviceName}/#。即:路径第一部分为注册中心服务实例名称,其余部分为服务实例的 REST路径。如:

bash 复制代码
/cargo-service/cargos/
/cargo-service/locations

其中:

  • /cargo-service为服务名称;
  • /ocations 为 cargo-service 服务提供的接口。

1.实现思路

客户端向网关发起请求,网关解析请求资源路径中的信息,根据服务名称查询注册中心的服务实例;然后使用反向代理技术把客户端请求转发至后端真实的服务实例,请求执行完毕后,再把响应信息返回客户端。

我们设计实现的网关的功能主要包含如下几点:

  • HTTP请求的规则遵循 /{serviceName}/#,否则不予通过。
  • 使用 Go 提供的反向代理包 httputil.ReverseProxy 实现一个简单的反向代理,它能够对请求实现负载均衡,随机地把请求发送给服务实例。
  • 使用Consul客户端API动态查询服务实例。

2.编写反向代理方法

创建目录 gateway,然后新建main.go 文件。NewReverseProxy 方法接受两个参数:Consul 客户端对象 api.Client 和日志记录工具log.Logger,返回反向代理对象。该方法的实现过程如下:

  • 获取请求路径,检查是否符合规则,不符合规则直接返回;
  • 解析请求路径,获取服务名称(请求路径的第一部分);
  • 使用Consul客户端查询服务实例,若查询到结果,则随机选择一个作为目标实例;
  • 根据选定的目标实例,设置反向代理参数 Schema、Host 和 Path。
Go 复制代码
// 位于 section19/gateway/main.go
// NewReverseProxy 创建反向代理处理方法
func NewReverseProxy(client *api.CTient, Togger Tog.Logger) *httputil.ReverseProxy {
    // 创建 Director
    director := func(req *http.Request) {
        // 查询原始请求路径,如:/arithmetic/calculate
        reqPath := req.URL.Path
        if reqPath == "" {
            return
        }

        // 按照分隔符'/'对路径进行分解,获取服务名称serviceName
        pathArray := strings.Split(reqPath, "/")
        serviceName := pathArray[1]
        // 调用consulapi查询serviceName的服务实例列表
        result, −, err := client.Catalog().Service(serviceName, "", nil)
        if err != nil {
            logger.Log("ReverseProxy failed", "query service instace error"
err.Error())
            return
        }
        if len(result) == 0 {
            logger.Log("Reverseproxy failed", "no such service instance",
serviceName)
            return
        }
        // 重新组织请求路径,去掉服务名称部分
        destPath := strings.Join(pathArray[2:], "/")
        // 随机选择一个服务实例
        tgt := result[rand.IntO%len(result)]
        logger.Log("'service id", tgt.ServiceID)
        // 设置代理服务地址信息
        req.URL.Scheme = "http"
        req.URL.Host = fmt.Sprintf("%s:%d", tgt.ServiceAddress, tgt.ServicePort)
        req.URL.Path = "/" + destPath
    }
    return &httputil.ReverseProxy{Director: director}
}

在反向转发处理的时候,我们只是根据请求中的服务名直接转发,如果需要对外屏蔽服务名的话,这样的路由转发规则显然是不够的。为了增加路由配置的多样性,我们可以抽出路由配置层,根据指定的规则进行路由转发,如根据配置名称、头部的信息、请求的参数、请求的body等规则转发到指定的服务。

3.编写入口方法

main方法的主要任务是创建 Consul连接对象、创建日志记录对象和开启反向代理HTTP服务。整个过程与前面课时创建用户服务类似,代码如下(为了测试方便,直接指定了Consu服务地址信息):

Go 复制代码
// 位于section19/gateway/main.go:65
func main() {
    // 创建环境变量
    var (
        consulHost = flag.String("consul.host", "127.0.0.1", "consul server ip
address")
        consulPort = flag.String("consul.port", "8500", "consul server port")
    )
    flag.Parse()
    // 创建日志组件
    var logger log.Logger
    {
        logger = log.NewLogfmtLogger(os.Stderr)
        logger = log.with(logger, "ts", log.DefaultTimestampuTC)
        logger = log.with(logger, "caller", log.Defaultcaller)
    }
    // 创建consul api客户端
    consulConfig := api.DefaultConfig()
    consulConfig.Address = "http://" + *consulHost + ":" + *consulPort
    consulclient, err := api.Newclient(consulConfig)
    if err != nil {
        logger.Log("err", err)
        os.Exit(1)
    }
    // 创建反向代理
    proxy := NewReverseProxy(consulclient, logger)
    errc := make(chan error)
    go func() {
        C:= make(chan os.signal)
        signal.Notify(C, syscall.SIGINT, syscall.SIGTERM)
        errc <- fmt.Errorf("%s", <-c)
    }()
    // 开始监听
    go func() {
        logger.Log("transport", "HTTP","addr","9099")
        errc <- http.ListenAndserve(":9099", proxy)
    }()
    // 开始运行,等待结束
    logger.Log("exit", <-errc)
}

如上的代码实现,为了创建反向代理,需要先创建日志组件和Consu连接对象。反向代理处理器一般还可以使用装饰者模式封装,如增加中间件Hystrix 断路器、链路追踪Tracer(Zipkin、Jaeger)组件等。

4.运行货运与网关服务

做好如上的准备步骤之后,我们开始运行货运服务。为了测试负载均衡效果,启动两个实例。这里我们是在一台主机上测试,所以需要使用不同的端口。首先编译货运服务:

Go 复制代码
$ go build -o cmd/cargo cmd/main.go

在 cmd目录下生成了cargo可执行文件,下面我们就分别来启动两个货运服务实例:

Go 复制代码
./cargo/cmd/cargo -consul.host localhost -consul.port 8500 -service.host 127.0.0.1
-service.port 8000
-/cargo/cmd/cargo -consul.host localhost -consul.port 8500 -service.host 127.0.0.l -
service.port 8002


##  启动成功并注册到Consu,控制台输出如下:
ts=2020-07-28T10:11:12.974789z transport=http address=8000 msg=1istening
ts=2020-07-28T10:11:13.006241z service=cargo-service tags="[cargo-service aoho]"
address=localhost action=register


##  再切换至目录gateway,执行go build 完成编译,最后启动网关服务。
> ./gateway -consul.host localhost -consul.port 8500
> ts=2020-07-28T10:11:37.662124Z caller=main.go:56 transport=HTTP addr=9099

5.测试

网关服务和两个货运服务实例启动好之后,我们通过命令行请求货运服务的接口/cargos,以获取指定Id 的货运信息,请求如下:

bash 复制代码
$ curl -X POST \
  http://localhost:9099/cargo-service/cargos/  \
  -H  'Content-Type: application/json'  \
  -d  '{
  "Id":"ABC123"
}'
{
    "cargo":{
        "arrival_deadline": "2020-08-11T18:56:44.627+08:00",
        "destination": "CNHKG"
        "misrouted": false,
        "origin": "SESTO"
        "routed": false,
        "tracking_id": "ABC123"
    }
}

同时,在终端可以看到如下输出,说明多次请求访问了不同的服务实例:

bash 复制代码
ts=2020-07-28T10:11:51.108611z ca11er=main.go:96 serviceid=cargo-service64ffdd53-
9c66-43cb-9ada-0d48ebddc632
ts=2020-07-28T10:12:00.215364z ca11er=main.go:96 serviceid=cargo-servicee8c53e6f-
e4ff-4737-a3bd-f1b11b0b2e95

本案例我们使用反向代理技术,并结合注册中心ConsuI实现了简单的API网关。Go提供了反向代理工具包,使得整个实现过程变得比较简单。实际项目中使用的产品,如Zuul、Nginx等,还包含了限流请求过滤、身份认证等功能。该网关虽然仅仅实现了请求的代理,但重点在于帮助你了解了网关实现的基本原理,从而为后续网关功能的扩增打下基础

小结

本文我们首先介绍了微服务网关产生的背景及其相关概念,然后还介绍了微服务网关在微服务架构中的职能。作为服务端的统一入口点,微服务网关主要用来实现接入请求、统一管理、解耦和配置拦截策略等功能。最后,为便于你更加详细地了解网关组件相关功能的实现原理,我们还自己动手实现了一个Go 微服务网关,你可以跟着上手实操下。

相关推荐
提子拌饭1333 小时前
液相色谱质谱联用(LC-MS)数据可视化引擎:基于鸿蒙Flutter的高精度色谱卡与多维峰值拟合架构
flutter·华为·信息可视化·开源·harmonyos·鸿蒙
孤岛站岗3 小时前
WAN:万象视频,开源视频生成的新标杆
开源·音视频
2301_771717213 小时前
登录生成 Token + 网关解析 Token + 微服务透传 userId
微服务·云原生·架构
2301_822703203 小时前
生命科学大分子资产模拟交易系统:基于鸿蒙Flutter跨端架构的高频订单簿与K线图渲染引擎
flutter·华为·架构·开源·harmonyos·鸿蒙
大数据新鸟4 小时前
微服务之Spring Cloud OpenFeign
spring cloud·微服务·架构
大数据新鸟4 小时前
微服务之Spring Cloud LoadBalancer
java·spring cloud·微服务
GitCode官方4 小时前
活动预告|AI × 开源进校园!AtomGit 源启高校・南京大学站
人工智能·开源
qyhua4 小时前
开源推荐 | ModelX RAG:基于 LangChain + Ollama 的企业级知识库系统
python·langchain·开源
handsomestWei4 小时前
【开源】从设计文档到可交付技术交底书:专利.Skill
开源·大模型·agent·skill·clawhub·skillhub