Skip to content

介绍

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

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

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

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

网关(Gateway)这个词在计算机科学中,尤其是在计算机网络中很常见,用于表示位于内部区域边缘,与外界进行交互的某个物理或逻辑设备,譬如你家里的路由器就属于家庭内网与互联网之间的网关。

什么是微服务网关

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

微服务架构图
微服务架构图

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

随着微服务架构的流行,API网关逐渐进入人们的视野,并且越来越受到欢迎。
在微服务体系架构中,我们将应用程序划分为多个低耦合的服务。每个服务都具有特定的功能,并交给不同的团队维护。
尽管微服务具有许多优势,比如程序易于开发、维护和部署,将大团队拆分成小团队利于敏捷实践落地等,但是也带来一些问题,最为直观的就是由于接口过于繁杂,客户端难以快速、安全地访问到所需的信息。

API网关的出现解决了上述问题,它可以充当调用这些微服务的客户端的中央入口。
客户端统一发送请求到网关层,再由网关层进行路由转发,使客户端访问接口的复杂度大大降低。
当然现代API网关的作用已不仅仅局限于此,更多高度抽象的通用功能都由网关层进行统一处理。
网关层随着系统架构升级逐步演化,在整个系统架构中的位置也变得愈发重要。

注意:
我们在网络上还能搜到关于网关的更多定义,比如网关(Gateway)又称网间连接器、协议转换器。
网关默认在网络层以上实现网络互联,是最复杂的网络互联设备,仅用于两个高层协议不同的网络互联。网关既可以用于广域网互联,也可以用于局域网互联。

此处定义的网关更接近于底层,偏向网络基础协议,而我们讨论的网关特指 API 网关,是软件架构中的中间层,偏向于应用和业务需求。

网关的由来

API网关层的兴起离不开微服务。微服务的概念最早在2012年提出。在Martin Fowler等人的大力推广下,微服务在2014年后得到了大力发展。
在微服务架构中,有一个组件可以说是必不可少的,那就是微服务网关。微服务网关具有负载均衡、缓存、路由、访问控制、服务代理、监控、日志等多项功能。
API网关在微服务架构中正是以微服务网关的身份存在。

同时,由于企业间信息交流和共享变得日益频繁,企业需要将自身数据、能力等向外开放,通常以接口的方式向外提供,如淘宝开放平台、腾讯的QQ开放平台和微信开放平台。
开放平台的引入必然涉及客户应用接入、API权限管理、调用次数管理等多项功能的完善,此时需要有一个统一的入口对它们进行管理,这也正是API网关出现的缘由。

网关的职责

在单体架构下,我们一般不太强调“网关”这个概念,为各个单体系统的副本分发流量的负载均衡器实质上扮演了内部服务与外部请求之间的网关角色。
在微服务环境中,网关的存在感就极大地增强了,甚至成为微服务集群中必不可少的设施之一。
其中原因并不难理解:微服务架构下,每个服务节点都可能由不同团队负责,都有着自己独立的、互不相同的接口,
如果服务集群缺少一个统一对外交互的代理人角色,那外部的服务消费者就必须知道所有微服务节点在集群中的精确坐标,
这样,消费者会受到服务集群的网络限制(不能确保集群中每个节点都有外网连接)、安全限制(不仅是服务节点的安全,外部自身也会受到如浏览器同源策略的约束)、依赖限制(服务坐标这类信息不属于对外接口承诺的内容,随时可能变动,不应该依赖它),
就算是调用服务的程序员,也不会愿意记住每一个服务的坐标位置来编写代码。

由此可见,微服务中网关的首要职责就是作为统一的出口对外提供服务,将外部访问网关地址的流量,根据适当的规则路由到内部集群中正确的服务节点之上,
因此,微服务中的网关,也常被称为“服务网关”或者“API网关”,微服务中的网关首先应该是个路由器,在满足此前提的基础上,还可以根据需要作为流量过滤器来使用,以提供某些额外的可选职能,譬如安全、认证、授权、限流、监控、缓存,等等

简言之: 网关 = 路由器(基础职能) + 过滤器(可选职能)

针对“路由器”这个基础职能,服务网关主要考量的是能够支持路由的“网络协议层次”和“性能与可用性”这两方面的因素。
网络协议层次是指负载均衡中介绍过的四层流量转发与七层流量代理。
仅从技术实现角度来看,对于路由这项工作,负载均衡器与服务网关在实现上是没有什么差别的,很多服务网关本身就是基于老牌的负载均衡器来实现的,譬如基于 Nginx、HAProxy 开发的 Ingress Controller,基于 Netty 开发的 Zuul 2.0 等;
但从目的角度来看,负载均衡器与服务网关会有一些区别,具体在于前者是为了根据均衡算法对流量进行平均地路由,后者是为了根据流量中的某种特征进行正确地路由。
网关必须能够识别流量中的特征,这意味着网关能够支持的网络通信协议的层次将会直接限制后端服务节点能够选择的服务通信方式。
如果服务集群只提供像 etcd 这样直接基于 TCP 访问的服务,那只部署四层网关便可满足,网关以 IP 报文中源地址、目标地址为特征进行路由;
如果服务集群要提供 HTTP 服务,那就必须部署一个七层网关,网关以 HTTP 报文中的 URL、Header 等信息为特征进行路由;
如果服务集群还要提供更上层的 WebSocket、SOAP 等服务,那就必须要求网关同样能够支持这些上层协议,才能从中提取到特征。

举个例子,以下是一段基于 Spring Cloud 实现的 Fenix’s Bookstore 实例中用到的 Netflix Zuul 网关的配置,Zuul 是 HTTP 网关,/restful/accounts/**/restful/pay/** 是 HTTP 中的 URL 的特征,而配置中的 serviceId 就是路由的目标服务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
routes:
    account:
        path: /restful/accounts/**
        serviceId: account
        stripPrefix: false
        sensitiveHeaders: "*"

    payment:
        path: /restful/pay/**
        serviceId: payment
        stripPrefix: false
        sensitiveHeaders: "*"

无须纠结上面代码清单中配置的确切写法、每个指令的含义。如果你从根本上理解了网关的原理,参考一下技术手册,很容易就能够将上面的信息改写成 Kubernetes Ingress Controller、Istio VirtualServer 或者其他服务网关所需的配置形式。

网关的作用

这里分享一些在真实开发场景中,如果没有统一的网关层,或者网关层不完善带来的严重问题。

  • 网关层在整个系统架构中处于门面位置,是系统中的流量中枢。所有的请求都最先流入网关层。
    网关服务器每天需要面对千万级,甚至亿级的流量冲刷,包括活动期间瞬时爆发的流量洪峰。
    网关层的健壮性与稳定性对于整个系统的重要性不言而喻,如果网关层发生宕机或者服务不可用,后果可想而知。
  • 微服务架构思想在现代系统架构设计中已经全面铺开,不仅大公司已经有了很好的实践方案,中小公司也开始纷纷效仿做架构升级。
    在落地过程中,无论是技术栈升级变迁,如 PHP 迁移到 Node 或 Go,还是引入 BFF(Backend For Frontend)层做服务聚合中间层等,都离不开网关层的支持。
  • 除加解密之外,鉴权、黑白名单管控等一些零散的安全配置定制化需求也非常多,但在实际开发过程中,留给研发人员开发调试的时间极为有限。
    更重要的是,每次更新网关层代码不能影响到生产环境的实时流量,这又给网关层的运维带来相当大的挑战。
  • 微服务架构将大型应用程序拆分成小型的独立功能,极大地提高了研发、部署效率,但带来的问题是每个独立小应用需要各自实现日志管理、限流熔断、服务注册发现等众多通用功能。
    你可能会想到使用统一的底层架构封装来解决问题,但如果微服务架构本身是跨多语言栈的,就需要多套底层框架同时支持,之后的框架迭代也需要同步进行,这带来的研发成本是巨大的。

除上述几点外,缓存、监控、动态路由配置等一系列问题也有待解决,而且这些仅仅是技术层面暴露出来的可预知风险点。
如果我们带着 DevOps 理念重新看待现在的产品研发流程,即技术人员需要参与产品开发、测试、交付、维护等所有环节,一个高效能的网关层无疑极大地解放了技术人员。

微服务网关的功能特性

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

微服务网关的主要功能特性如下图所示:

网关的功能特性示意图
网关的功能特性示意图

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

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

网络 I/O 模型

网关的另一个主要关注点是它的性能与可用性。
由于网关是所有服务对外的总出口,是流量必经之地,所以网关的路由性能将导致全局的、系统性的影响,如果经过网关路由会有 1 毫秒的性能损失,就意味着整个系统所有服务的响应延迟都会增加 1 毫秒。

网关的性能与它的工作模式和自身实现算法都有关系,但毫无疑问工作模式是最关键的因素,如果能够采用 DSR 三角传输模式,在实现原理上就决定了性能一定会比代理模式来的强(DSR、IP Tunnel、NAT、代理等这些都是网络基础知识)

不过,因为今天REST和JSON-RPC等基于HTTP协议的服务接口在对外部提供的服务中占绝对主流的地位,所以我们所讨论的服务网关默认都必须支持七层路由,通常默认无法直接进行流量转发,只能采用代理模式。
在这个前提约束下,网关的性能主要取决于它们如何代理网络请求,也即它们的网络 I/O 模型,正好借这个场景介绍一下网络 I/O 的基础知识。

在套接字接口抽象下,网络 I/O 的出入口就是 Socket 的读和写,Socket 在操作系统接口中被抽象为数据流,而网络 I/O 可以理解为对流的操作。
每一次网络访问,从远程主机返回的数据会先存放到操作系统内核的缓冲区中,然后从内核的缓冲区复制到应用程序的地址空间,所以当发生一次网络请求时,将会按顺序经历“等待数据从远程主机到达缓冲区”和“将数据从缓冲区复制到应用程序地址空间”两个阶段,
根据实现这两个阶段的不同方法,人们把网络 I/O 模型总结为两类、五种模型:两类是指同步 I/O 与异步 I/O,
五种是指在同步 I/O 中又划分出阻塞 I/O、非阻塞 I/O、多路复用 I/O、信号驱动 I/O 四种细分模型以及异步I/O模型。

这里先解释一下同步和异步、阻塞和非阻塞的概念。
同步是指调用端在发出请求之后,得到结果之前必须一直等待,与之相对的就是异步,发出调用请求之后将立即返回,不会马上得到处理结果,结果将通过状态变化和回调来通知调用者。
阻塞和非阻塞是针对请求处理过程而言,指在收到调用请求之后,返回结果之前,当前处理线程是否会被挂起。
这种概念上的叙述可能还是不太好理解,下面以“你如何领到盒饭”为情景,将之类比解释如下。

  • 异步 I/O (Asynchronous I/O):比如你在美团外卖订了个盒饭,付款之后你自己该干嘛干嘛,饭送到时骑手自然会打电话通知你。
    异步I/O中数据到达缓冲区后,不需要由调用进程主动进行从缓冲区复制数据的操作,而是复制完成后由操作系统向线程发送信号,所以它一定是非阻塞的。
  • 同步 I/O (Synchronous I/O):比如你自己去饭堂打饭,这时可能有如下情形发生。
  • 阻塞 I/O (Blocking I/O):你去饭堂打饭,发现饭还没做好,只能等待(线程休眠),直到饭做好,这就是被阻塞了。
    阻塞 I/O 是最直观的 I/O 模型,逻辑清晰,也比较节省 CPU 资源,但缺点是线程休眠所带来的上下文切换,这是一种需要切换到内核态的重负载操作,不应当频繁进行。
  • 非阻塞 I/O (Non-Blocking I/O):你去饭堂,发现饭还没做好,你就回去了,然后每隔 3 分钟来一次饭堂看饭是否做好,一直重复,直到饭做好。
    非阻塞I/O能够避免线程休眠,对于一些很快就能返回结果的请求,非阻塞 I/O 可以节省切换上下文切换的消耗,但是对于较长时间才能返回的请求,非阻塞 I/O 反而白白浪费了 CPU 资源,所以目前并不太常用。
  • 多路复用 I/O (Multiplexing I/O):多路复用 I/O 本质上是阻塞 I/O 的一种,但是它的好处是可以在同一条阻塞线程上处理多个不同端口的监听。
    仍以去食堂打饭为例,比如你代表整个宿舍去饭堂打饭,去到饭堂,发现饭还没做好,还是继续等待,其中某个舍友的饭好了,你就马上把那份饭送回去,然后继续等待其他舍友的饭做好。
    多路复用 I/O 是目前高并发网络应用的主流,它还可以细分为 select、epoll、kqueue 等不同实现,这里就不再展开了。
  • 信号驱动 I/O (Signal-Driven I/O):你去到饭堂,发现饭还没做好,但你跟厨师很熟,跟他说饭做好了叫你,然后你就回去了,等收到厨师通知后,你再去饭堂把饭拿回宿舍。
    这里厨师的通知就是那个“信号”,信号驱动 I/O 与异步 I/O 的区别是“从缓冲区获取数据”这个步骤的处理,前者收到的通知是可以开始进行复制操作了,即要你自己从饭堂拿回宿舍,在复制完成之前线程处于阻塞状态,所以它仍属于同步 I/O 操作,而后者收到的通知是复制操作已经完成,即外卖小哥已经把饭送到了。

显然,异步 I/O 模型是最方便的,但前提是系统支持异步操作。
异步 I/O 受限于操作系统,Windows NT 内核早在 3.5 以后,就通过 IOCP 实现了真正的异步 I/O 模型。
而 Linux 系统是在 Linux Kernel 2.6 才首次引入,目前还不算很完善,因此在 Linux 系统下实现高并发网络编程时仍以多路复用 I/O 模型模式为主。

回到服务网关的话题上,有了网络I/O模型的知识,我们就可以在理论上定性分析不同七层网关的性能差异了。
七层服务网关处理一次请求代理时,包含两组网络操作,分别是作为服务端对外部请求的应答和作为客户端对内部服务的请求,理论上这两组网络操作可以采用不同的模型去完成,但一般没有必要这样做。

以 Zuul 网关为例,在 Zuul 1.0 时,它采用的是阻塞 I/O 模型来进行最经典的“一条线程对应一个连接”(Thread-per-Connection)的方式来代理流量。
采用阻塞 I/O 模型意味着它会有线程休眠,就有上下文切换的成本,所以如果后端服务普遍属于计算密集型(CPU Bound,可以通俗理解为服务耗时比较长,主要消耗在CPU上)时,这种模型能够相对节省网关的CPU资源,但如果后端服务普遍都是I/O密集型(I/O Bound,可以理解为服务都很快返回,主要消耗在I/O上),它就会由于频繁的上下文切换而降低性能。

Zuul 2.0 版本最大的改进就是基于 Netty Server 实现了异步 I/O 模型来处理请求,大幅度减少了线程数,获得了更高的性能和更低的延迟。
根据 Netflix 官方给出的数据,Zuul 2.0 大约要比 Zuul 1.0 快上 20% 左右。还有一些网关甚至支持自行配置,或者根据环境选择不同的网络 I/O 模型,典型代表就是 Nginx,它可以支持在配置文件中指定 select、poll、epoll、kqueue 等并发模型。

网关的性能高低一般只会定性分析,要定量地说哪一种网关性能最高、高多少是很困难的,就像我们都认可 Chrome 要比 IE 快,但脱离了具体场景,快多少就很难说的清楚。
尽管上面引用了 Netflix 官方对 Zuul 两个版本的量化对比,网络上也有不少关于各种网关的性能对比数据,但若脱离具体应用场景去定量地比较不同网关的性能差异还是难以令人信服,不同的测试环境和后端服务都会直接影响结果。

可用性

网关还有最后一点必须关注的是它的可用性问题。
任何系统的网络调用过程中都至少会有一个单点存在,这是由用户只通过唯一的地址去访问系统所决定的。
即使是淘宝、亚马逊这样全球多数据中心部署的大型系统也不例外。
对于更普遍的小型系统(小型是相对淘宝这些而言)来说,作为后端对外服务代理人角色的网关经常被视为整个系统的入口,往往很容易成为网络访问中的单点,这时候它的可用性就尤为重要。
由于网关的地址具有唯一性,所以不能像之前服务发现那些注册中心那样,用集群的方式解决问题。
在网关的可用性方面,我们应该考虑到以下几点。

  • 网关应尽可能轻量,尽管网关作为服务集群统一的出入口,可以很方便地实现安全、认证、授权、限流、监控等功能,但给网关附加这些功能时还是要仔细权衡,取得功能性与可用性之间的平衡,过度增加网关的功能是危险的。
  • 网关选型时,应该尽可能选择较成熟的产品实现,譬如 Nginx Ingress Controller、KONG、Zuul 这些经过长期考验的产品,而不能一味只考虑性能选择最新的产品,性能与可用性之间的平衡也需要权衡。
  • 在需要高可用的生产环境中,应当考虑在网关之前部署负载均衡器或者等价路由器(ECMP),让那些更成熟健壮的设施(往往是硬件物理设备)去充当整个系统的入口地址,这样网关也可以进行扩展。

BFF 网关

提到网关的唯一性、高可用与扩展,顺带说一下近年来随着微服务一起火起来的概念“BFF”(Backend for Frontend),如下图所示。
这个概念目前还没有特别权威的中文翻译,在我们讨论的上下文里,它的意思是:网关不必为所有的前端提供无差别的服务,而是应该针对不同的前端,聚合不同的服务,提供不同的接口和网络访问协议支持。
譬如,运行于浏览器的 Web 程序,由于浏览器一般只支持 HTTP 协议,服务网关就应提供 REST 等基于 HTTP 协议的服务,但同时我们也可以针对运行于桌面系统的程序部署另外一套网关,它能与Web网关有完全不同的技术选型,能提供基于更高性能协议(如 gRPC)的接口以提供更好的体验。
在网关这种边缘节点上,针对同样的后端集群,裁剪、适配、聚合出适应不一样的前端的服务,有助于后端的稳定,也有助于前端的赋能。

BFF网关
BFF网关