随着微服务时代的到来,关于服务的访问也变得尤其重要,而微服务网关作为流量和服务之间的桥梁,也占据着举足轻重的地位。近年来伴随着微服务架构的盛行,微服务网关的技术也是日新月异,今天小编就选取几个最常用的几个网关选型,从部署、使用到特性讲解,一步步剖析。
1、网关的概念
传统的单体架构中只需要开放一个服务给客户端调用,但是微服务架构中是将一个系统拆分成多个微服务,如果没有网关,客户端只能在本地记录每个微服务的调用地址,当需要调用的微服务数量很多时,它需要了解每个服务的接口,这个工作量很大。那有了网关之后,网关作为系统的唯一流量入口,封装内部系统的架构,所有请求都先经过网关,由网关将请求路由到合适的微服务,所以,使用网关的好处在于:
-
简化客户端的工作。网关将微服务封装起来后,客户端只需同网关交互,而不必调用各个不同服务;
-
降低函数间的耦合度。一旦服务接口修改,只需修改网关的路由策略,不必修改每个调用该函数的客户端,从而减少了程序间的耦合性
-
解放开发人员把精力专注于业务逻辑的实现。由网关统一实现服务路由(灰度与ABTest)、负载均衡、访问控制、流控熔断降级等非业务相关功能,而不需要每个服务 API 实现时都去考虑
2、流量网关和业务网关
有的小伙伴们可能在工作过程中听到过网关的这种区分,流量网关和业务网关这两个概念其实也并没有严格意义上的区分。从概念上来讲,流量网关是承接系统的一手流量,位于最前端,主要工作通常是转发请求,还可以做一些流量控制、鉴权、黑白名单等操作,流量网关作为访问流程的第一道屏障需要非常高的性能,比较常用的就是Nginx。业务网关更多的是跟后端微服务领域相关的,部署在流量网关的下一层,在这里会做协议转换、服务发现、负载均衡、服务治理(限流、容错、降级)等操作。现在很多企业的架构也经常把两者合二为一,并不一定要严格的区分部署分开部署。
外部流量转发流程图:
3、常用网关选型
目前市面上开源的网关实现有很多种,功能上几乎都差不多,实现语言上有Java、GO、C等,今天我们主要选取三种比较常用的网关技术,具体分析下实现步骤。
Spring Cloud Gateway
Spring Cloud Gateway 是 Spring Cloud的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,可以集成Eureka等注册中心使用。
Spring Cloud Gateway有三大组件:
Route(路由):这是网关的基本模块。它由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配。
Predicate(断言):这是一个 Java 8 的 Predicate。输入类型是一个 ServerWebExchange。我们可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。
Filter(过滤器):这是org.springframework.cloud.gateway.fifilter.GatewayFilter的实例,我们可以使用它修改请求和响应。
下面我们从一个真实案例出发,看看Spring Cloud Gateway如何在项目中使用:
-
创建一个Spring Cloud Gateway项目:
引入依赖:Spring Cloud的一员,直接项目中添加依赖即可
//引入gateway <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> //引入eureka注册中心 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency>
启动项目:
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient // 开启Eureka客户端发现服务 public class DemoGatewayApplication { public static void main(String[] args) { SpringApplication.run(DemoGatewayApplication.class, args); } }
开启配置:application.yml
# 端口号 server.port: 10010 # 应用名 spring.application.name: api-gateway # 注册中心地址 eureka.client.service-url.defaultZone: http://10.106.19.13:10086/eureka/
实现上面几步,我们就已经搭建好了一个Spring Cloud Gateway项目,接下面就看下如何进行一些网关的配置。
- Preicate(断言)
Predicate(断言)断言就是转发条件的配置,满足这个条件才会进入后续的转发流程。
Path规则:路径匹配,比如/user/**,请求URL前缀为user的请求predicates: - Path=/user/** //前缀为user开头的路径会被转发
Query:参数校验,可以校验URL中带指定的参数
predicates: - Query=abc //请求中包含参数abc
Header:请求的Header中必须包含某个参数以及对应的值符合要求
predicates: - Header=Connection,keep-alive - Header=Cache-Control,max-age=0
Method:指定的请求方式才会被转发
predicates: - Method=GET
RemoteAddr:允许访问的客户端IP地址
predicates: - RemoteAddr=127.0.0.1
-
URI配置
URI(转发路径):URI和Predicate是对应的,指定的Predicate校验规则满足条件之后跳转到对应的URI,URI的配置方式也有多种,具体如下:
直接 URI:指定一个完整的目标 URI,请求将直接转发到该 URI。spring: cloud: gateway: routes: - id: direct-route uri: http://example.com //转发到example.com这个地址
Forward Prefix:使用 forward: 前缀来将请求转发到内部资源。
spring: cloud: gateway: routes: - id: forward-route uri: forward:/internal-resource //通过forward转发到内部地址
Websocket URI:用于将请求转发到支持 Websocket 的目标 URI。
spring: cloud: gateway: routes: - id: websocket-route uri: ws://websocket-service //转发到一个websocket服务
不同的 URI 配置方式适用于不同的使用场景。选择合适的配置方式取决于你的需求和后端服务的架构。
-
Filter
Filter(过滤器):过滤器就是在请求的前后做一些操作,比如常用的鉴权、限流等,都是通过过滤器实现,Spring Cloud Gateway的过滤器有"pre"和"post"两个顺序,分别会在请求执行前和执行后调用。
Gateway自带有几十种过滤器,可以直接在配置文件中声明使用:
针对自带的过滤器我们都可以采用声明式的方式在yml配置文件中直接使用,gateway已经帮我们做好了具体的实现,下面介绍几个常用的自带过滤器:
AddRequestHeader 在请求的Header中添加数据 AddRequestParameters 添加请求参数 AddResponseHeader 对从网关返回的响应添加Header StripPrefix 对匹配上的请求路径去除前缀 PrefixPath 对匹配上的请求路径添加前缀 spring: cloud: gateway: # 全局过滤器配置 default-filters: # 往响应过滤器中加入信息 - AddResponseHeader: i-love,itheima - AddRequestParameter:name,bjsxt: - AddRequestHeader: MyHeader,jqk - StripPrefix: 1 - PrefixPath: /user
除了使用Gateway自带的Filter,我们也可以自己自定义Fiilter,下面是一个自定义Filter示例判断请求参数Token:
@Configuration public class MyGlobalFileter implements GlobalFilter, Ordered { /** * 自定义过滤器规则 */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getQueryParams().getFirst("token"); if (StringUtils.isBlank(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } /** * 定义过滤器执行顺序 * 返回值越小,越靠前执行 * @return */ @Override public int getOrder() { return 0; } }
Gateway限流,这里单独提一下Gateway限流,其实也是用Filter进行的实现,不过Gateway为我们自带了限流的功能,是基于redis的RateLimter实现,只需要引入即可使用。
引入依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
定义KeyResolver:KeyResolver⽤于计算某⼀个类型的限流的KEY也就是说,可以通过KeyResolver来指定限流的Key。
@Bean public KeyResolver ipKeyResolver() { return new KeyResolver() { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } }; }
RateLimit也是通过过滤器的方式来实现的,所以定义好过滤的key,还需要在配置文件中配置对应的限流策略:
filters: - PrefixPath=/test - name: RequestRateLimiter args: //ipKeyResolver是通过spel表达式获取对应的Bean key-resolver: "#{@ipKeyResolver}" //令牌桶每秒填充速率 redis-rate-limiter.replenishRate: 1 //令牌桶容量 redis-rate-limiter.burstCapacity: 1
到这里关于Spring Cloud Gateway大部分的功能都介绍完毕了,下面给一个完整的配置示例:
spring: application: name: sysgateway cloud: gateway: globalcors: cors-configurations: '[/**]': # 匹配所有请求 allowedOrigins: "*" #跨域处理 允许所有的域 allowedMethods: # ⽀持的⽅法 - GET - POST - PUT - DELETE routes: - id: goods uri: lb://goods predicates: - Path=/goods/** filters: - StripPrefix= 1 - name: RequestRateLimiter #请求数限流 名字不能随便写 args: key-resolver: "#{@ipKeyResolver}" redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 1 - id: system uri: lb://system predicates: - Path=/system/** filters: - StripPrefix= 1 # 配置Redis 127.0.0.1可以省略配置 redis: host: 192.168.200.128 port: 6379 server: port: 9101 eureka: client: service-url: defaultZone: http://127.0.0.1:6868/eureka instance: prefer-ip-address: true
Zuul
Zuul是Netflix开源的微服务网关,可以和Eureka、Ribbon、Hystrix等组件配合使用,Spring Cloud对Zuul进行了整合与增强,Zuul的主要功能是路由转发和过滤器。路由转发功能是将外部请求转发到具体的我们的微服务上,是实现外部访问统一入口的基础,因为之后我们所有访问都要从网关走,我们会通过配置忽略掉其他的访问。而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验,服务聚合等功能的基础。目前Spring Cloud对Zuul做了整合,所以使用起来也是相当的方便,跟Gateway差别不大。
- 创建一个Zuul应用项目:
引入依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
启动项目:@EnableZuulProxy
@SpringBootApplication @EnableZuulProxy //@EnableZuulProxy - 开启Zuul网关 public class ZuulApplication { public static void main(String[] args) { SpringApplication.run(ZuulApplication.class, args); } }
基础配置application.yml
server: # 本服务的端口号 port: 9100 spring: application: # 服务名或服务ID name: online-taxi-zuul #注册中心 #当前应用是一个Zuul微服务网关。会在Eureka注册中心中注册当前服务。并发现其他的服务。 eureka: client: #设置服务注册中心的URL service-url: defaultZone: http://root:root@eureka-7900:7900/eureka/ instance: # 对应调用本服务的主机名或主机地址 hostname: localhost # 向eureka注册的服务名或服务ID instance-id: online-taxi-zuul
zuul核心配置
转发配置:zuul转发路径配置相对来说比较简单,在application.yml文件中以zuul.routes开头。
采用URL的转发方式:zuul: routes: custom-zuul-name: path: /zuul-custom-name/** url: http://localhost:8080/custom //转发到指定路径
指定service-name的转发方式:
zuul: routes: custom-zuul-name: path: /zuul-custom-name/** service-id: service-custom //转发到指定的服务
对于转发到指定的服务,zuul会从配置中心拉取对应的服务,然后内部是采用Ribbon做的负载均衡。
Filter
Filter(过滤器)是Zuul的最核心的部分,核心逻辑就是通过过滤器链来完成的,类似于权限校验、特殊路径处理、限流以及错误处理等都是通过Filter实现。
定义Zuul里面的Filter需要实现ZuulFilter接口,接口有以下几部分:
filterType 设定过滤器的类型(4种选择) filterOrder 该过滤器在这种类型过滤器中的执行排序 shouldFilter 是否执行该过滤器的业务逻辑 run 该过滤器的业务逻辑
过滤器的类型有四种:
PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
ERROR:在其他阶段发生错误时执行该过滤器。
过滤器内部状态流转:
下面自定义一个限流的过滤器:
@Component public class LimitFilter extends ZuulFilter { //声明类型 @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { // 限流应该是所有过滤器的最前面,最为第一道工具,所以该值越小越好 return -10; } @Override public boolean shouldFilter() { return true; } // gova的东西 相当于限流中的令牌生成器 // create(50) 表示每秒生成50个令牌 // create(0.1) 表示10秒生成1个(1秒生成0.1个) // 2 qps(1秒 2个 请求 Query Per Second 每秒查询量) private static final RateLimiter RATE_LIMITER = RateLimiter.create(50); @Override public Object run() throws ZuulException { RequestContext currentContext = RequestContext.getCurrentContext(); if (RATE_LIMITER.tryAcquire()) { // RATE_LIMITER.tryAcquire(3); // 1下拿几个令牌 // RATE_LIMITER.acquire();// acquire()属于阻塞,就是一直在这里等待着,tryAcquire()非阻塞,但可以设置等待时间,请自行尝试。 System.out.println("通过"); return null; } else { // 被流控的逻辑 System.out.println("被限流了"); // 可以设置limit值,后面所有的过滤器的shouldFilter中都监控该值,如过为false,就都不执行 currentContext.set("limit", false); /** * 所有其他过滤器中shouldFilter中添加的Limit的判断 * Object limit = RequestContext.getCurrentContext().get("limit"); * if (null != limit && false == (Boolean) limit) { * return false; * } */ currentContext.setSendZuulResponse(false); currentContext.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value()); } return null; } }
Traefik
Traefix是一个开源的云原生网关,主要用于云原生场景,可自动识别Kubernetes、Docker、Docker Swarm等等技术下部署的服务,通过label标签自动生成反向代理路由规则。近年来云原生发展迅速,kubernetes几乎是企业项目必备的基础设置,所以Traefix也得到了很好的应用,目前小编所在的公司就是使用的Traefik做的网关,给我的感觉就是为kubernetes而生。
了解Traefix之前我们必须要先了解一下Kubernetes的Ingress, Ingress是Kubernetes的一个插件,用于管理集群中的HTTP和HTTPS流量路由。它允许我们通过一个统一的入口点来管理多个服务的流量,同时提供负载均衡、SSL终止等功能。Ingress只是k8s定义的一种标准并没有具体的实现,而Traefix就是这种实现,同时还有Nginx Ingress Controller和HAProxy。在学习Traefik之前需要对Kubernetes有一定的了解,熟悉里面的资源对象和控制对象,可以参考小编之前发布的关于Kubernetes相关文章: kubernetes基础概念篇 kubernetes核心组件篇
下面是官网上的一张图,描述了一个外部请求是经过哪些流程到我们的k8s服务:
从中可以看到,Traefix所包含的几个核心组件:
-
Providers
Providers是基础组件,Traefik的配置发现是通过它来实现的,它可以是协调器,容器引擎,云提供商或者键值存储。Traefik通过查询Providers的API来查询路由的相关信息,一旦检测到变化,就会动态的更新路由。 -
Entrypoints
Entrypoints是Traefik的网络入口,它定义接收请求的接口,以及是否监听TCP或者UDP,外部的请求首先就是到Entrypoint -
Routers
Routers是路由处理,里面包含了请求的路由规则Rule以及对请求的特殊处理Middware -
Services
Services负责配置如何到达最终将处理传入请求的实际服务 -
Middlewares
Middlewares用来修改请求或者根据请求来做出一些判断(authentication, rate limiting, headers, …),可以理解为上面两种网关的Filter,中间件被附加到路由上,是一种在请求发送到你的服务之前(或者在服务的响应发送到客户端之前)调整请求的一种方法。
下面通过配置一个完整的示例来一步步剖析每个流程,整个过程都是基于k8s的声明方式来进行部署,和之前介绍的两个网关有很大的差别。
- 部署Traefik(通过Helm部署)
下载Traefik镜像:$ helm search repo traefik NAME CHART VERSION APP VERSION DESCRIPTION traefik/traefik 10.6.0 2.5.3 A Traefik based Kubernetes ingress controller $ helm pull traefik/traefik
配置文件:
service: type: NodePort ingressRoute: dashboard: enabled: false ports: traefik: port: 9000 expose: true web: port: 8000 expose: true websecure: port: 8443 expose: true persistence: enabled: true name: data accessMode: ReadWriteOnce size: 5G storageClass: "openebs-hostpath" path: /data additionalArguments: - "--serversTransport.insecureSkipVerify=true" - "--api.insecure=true" - "--api.dashboard=true"
部署命令:
kubectl create ns traefik-ingress helm install traefik -n traefik-ingress -f my-value.yaml .
部署成功后可以登录Traefik的Dashboard:
- 路由配置
上面已经说到了kubernetes路面服务路由是根据Ingress进行配置,Traefik就是Ingress的一种具体实现,这里我使用CRD IngressRoute方式来配置,可以更好的配置对k8s服务的访问。
配置Route:
Match:路径匹配规则
Services:对应kubernetes上面的servicespec: entryPoints: - web - websecure routes: - kind: Rule match: PathPrefix(`/api/com-qt-app-ids/ids-detect`) //匹配规则 services: - kind: Service name: svc-ids-detect //匹配上转发到k8s的这个service上 namespace: qt-system //服务对应的命名空间 passHostHeader: true //将请求头传递给后端服务 port: 80 //服务监听的端口
部署路由规则:
kubectl apply -f detect-ingressRoute.yaml -n qt-system
当我们访问的路径为/api/com-qt-app-ids/ids-detect开头的,就会被转发到svc-ids-detect这个服务的80端口进行处理。
- Middleware配置
Middleware中文解释是中间件,这里我们可以理解为就是网关中的Filter,可以在请求转发前后做一些操作,一个请求匹配Rules后,会经过一系列的Middleware,再到具体的Services上。先创建一个Middleware:将Http请求转发到Https:
apiVersion: traefik.containo.us/v1alpha1 kind: Middleware metadata: name: redirect-https-middleware spec: redirectScheme: scheme: http
在路由规则中配置Middleware:
apiVersion: traefik.containo.us/v1alpha1 kind: IngressRoute metadata: name: ir-service-ids-detect namespace: qt-system spec: entryPoints: - web - websecure routes: - kind: Rule match: PathPrefix(`/api/com-qt-app-ids/ids-detect`) services: - kind: Service name: svc-ids-detect namespace: qt-system passHostHeader: true port: 80 middlewares: - name: redirect-https-middleware //配置之前创建的Middleware namespace: qt-syste
除了Traefik自带的Middleware,我们也可以自己基于GO语音实现不同功能的Middleware,主要是需要编写下述几点:
中间件配置:代码中表现为一个结构体,可以映射到配置文件中。
中间件Handler:作为实现中间件逻辑功能的结构体,实现 http.Handler 接口。
中间件的初始化代码:需要定义中间件的“构造函数”,用于读取配置并实例化中间件的Handler。这里是一个模板示例,插件如果需要被Traefik使用,还需要在Traefik对应的地方加载插件,即调用New方法:
package main import ( "net/http" "strings" "github.com/traefik/traefik/v2/pkg/middlewares" ) type CustomHeader struct { next http.Handler } func NewCustomHeader(next http.Handler) (*CustomHeader, error) { return &CustomHeader{next: next}, nil } func (ch *CustomHeader) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // 在这里添加自定义的头信息 rw.Header().Add("X-Custom-Header", "Hello from Custom Header Middleware") // 继续处理下一个中间件或者后端服务 ch.next.ServeHTTP(rw, req) } func New(ctx middlewares.RequestCtx, next http.Handler, config *middlewares.HandlerInfo) (http.Handler, error) { return NewCustomHeader(next) }
关于网关的介绍到这里也要结束了,上面介绍了几种常用网关的使用方法,配置方面的应该有更丰富的配置,由于篇幅的原因这里只列举了一些常用的,在小编看来选取那种网关并没有好坏之分,需要看各自系统的环境以及技术栈。
学习更多微服务架构相关技术关注作者个人技术公众号:八阿哥技术圈,干货满满!!!