转载

在kubernetes上运行Spring Cloud Gateway

在kubernetes上运行Spring Cloud Gateway

作者:青木,工程师,DevOps践行者,微服务化,容器化业务实践者。

前言

https://github.com/spring-cloud/spring-cloud-gateway是Spring Cloud官方推出的一个网关项目,主要是基于reactor-netty实现。网关在微服务系统主要充当了一个入口”门”的作用,所有的IN/OUT都需要经过这一道门,才能访问到微服务池子中的功能api。

这样的设计方便了我们对业务功能的保护api资源的保护,我们可以在这里灵活的控制对外开放的API集合,而这些API集合就构成了我们的"系统"。

我们还可以方便的在这里完成鉴权,如果是非法用户之间在这里干掉,从而避免了对业务的调用。

还可以对参数进行转换,比如从调用者带过来的是一个JWT我们可以解析这个Token,得到诸如currentUserId,租户id,username,等等一些列参数后,再往后传递到具体的微服务上。

这里只是一些常用功能的列举,本质上来说呢,他就是一组Filters,我们可以通过扩展它的Filter,快速完成业务所需要的功能效果。

依赖

首先肯定是需要导入gateway


 

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-gateway</artifactId>

</dependency>

我们还需要服务发现eureka,这里直接排除掉jersey的相关依赖,能少点jar就少点吧。


 

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>

<exclusions>

<exclusion>

<groupId>com.sun.jersey</groupId>

<artifactId>jersey-client</artifactId>

</exclusion>

<exclusion>

<groupId>com.sun.jersey</groupId>

<artifactId>jersey-core</artifactId>

</exclusion>

<exclusion>

<groupId>com.sun.jersey.contribs</groupId>

<artifactId>jersey-apache-client4</artifactId>

</exclusion>

</exclusions>

</dependency>

网关也是需要对服务进行LoadBalance,这里导入ribbon的依赖。这里他也依赖了jersey相关的东西,排除掉好了。


 

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>

<exclusions>

<exclusion>

<artifactId>jersey-client</artifactId>

<groupId>com.sun.jersey</groupId>

</exclusion>

<exclusion>

<artifactId>jersey-apache-client4</artifactId>

<groupId>com.sun.jersey.contribs</groupId>

</exclusion>

</exclusions>

</dependency>

我们可以启用ribbon中的okhttpclent,需要加入okhttp的依赖


 

<dependency>

<groupId>com.squareup.okhttp3</groupId>

<artifactId>okhttp</artifactId>

<version>4.1.0</version>

</dependency>

需要分布式追踪功能加入zipkin和sleuth的依赖。


 

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-sleuth</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-zipkin</artifactId>

</dependency>

pom.xml

完整的pom长这样


 

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.1.7.RELEASE</version>

<relativePath/> <!-- lookup parent from repository -->

</parent>

<groupId>io.qingmu</groupId>

<artifactId>demo-gateway</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>demo-gateway</name>

<description>Demo project for Spring Boot</description>


<properties>

<java.version>1.8</java.version>

<spring-cloud.version>Greenwich.SR2</spring-cloud.version>

</properties>


<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-config</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-gateway</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>

<exclusions>

<exclusion>

<groupId>com.sun.jersey</groupId>

<artifactId>jersey-client</artifactId>

</exclusion>

<exclusion>

<groupId>com.sun.jersey</groupId>

<artifactId>jersey-core</artifactId>

</exclusion>

<exclusion>

<groupId>com.sun.jersey.contribs</groupId>

<artifactId>jersey-apache-client4</artifactId>

</exclusion>

</exclusions>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>

<exclusions>

<exclusion>

<artifactId>jersey-client</artifactId>

<groupId>com.sun.jersey</groupId>

</exclusion>

<exclusion>

<artifactId>jersey-apache-client4</artifactId>

<groupId>com.sun.jersey.contribs</groupId>

</exclusion>

</exclusions>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-sleuth</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-zipkin</artifactId>

</dependency>


<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>com.squareup.okhttp3</groupId>

<artifactId>okhttp</artifactId>

<version>4.1.0</version>

</dependency>

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

<version>1.18.4</version>

<scope>provided</scope>

</dependency>

</dependencies>


<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-dependencies</artifactId>

<version>${spring-cloud.version}</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>


<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<groupId>com.spotify</groupId>

<artifactId>docker-maven-plugin</artifactId>

<configuration>

<imageName>

freemanliu/demo-gatway:v1.0.0

</imageName>

<registryUrl></registryUrl>

<workdir>/work</workdir>

<rm>true</rm>

<env>

<TZ>Asia/Shanghai</TZ>

<JAVA_OPTS>

-XX:+UnlockExperimentalVMOptions /

-XX:+UseCGroupMemoryLimitForHeap /

-XX:MaxRAMFraction=2 /

-XX:CICompilerCount=8 /

-XX:ActiveProcessorCount=8 /

-XX:+UseG1GC /

-XX:+AggressiveOpts /

-XX:+UseFastAccessorMethods /

-XX:+UseStringDeduplication /

-XX:+UseCompressedOops /

-XX:+OptimizeStringConcat

</JAVA_OPTS>

</env>

<baseImage>freemanliu/openjre:8.212</baseImage>

<cmd>

/sbin/tini java ${JAVA_OPTS} -jar ${project.build.finalName}.jar

</cmd>

<!--是否推送image-->

<pushImage>true</pushImage>

<resources>

<resource>

<directory>${project.build.directory}</directory>

<include>${project.build.finalName}.jar</include>

</resource>

</resources>

<serverId>docker-hub</serverId>

</configuration>

<executions>

<execution>

<phase>package</phase>

<goals>

<goal>build</goal>

</goals>

</execution>

</executions>

</plugin>

</plugins>

</build>

</project>

异常处理器

为什么要有异常处理?

  1. 需要将异常信息处理成我们所需要的格式。

  2. 统一处理自定义filter抛出的异常。

在不处理异常信息时,gateway默认返回的信息格式如下


 

{

"timestamp":"2019-08-21T06:46:19.819+0000"

,"path":"/demo1-service/hello"

,"status":504

,"error":"Gateway Timeout"

,"message":"Response took longer than timeout: PT20S"

}

这显然不太和我们一般定义的返回结构不同,一般常见的返回结构如下:


 

{

"code": 0

,"data"": {}

,"message": ""

}

CustomExceptionHandler.java


 

package io.qingmu.demogateway.advice;


import lombok.Setter;

import lombok.extern.slf4j.Slf4j;

import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;

import org.springframework.http.HttpStatus;

import org.springframework.http.MediaType;

import org.springframework.http.server.reactive.ServerHttpRequest;

import org.springframework.http.server.reactive.ServerHttpResponse;

import org.springframework.web.reactive.function.client.WebClientResponseException;

import org.springframework.web.server.ResponseStatusException;

import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Mono;


import javax.validation.ValidationException;

import java.nio.charset.Charset;


@Setter

@Slf4j

public class CustomExceptionHandler implements ErrorWebExceptionHandler {



@Override

public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {

// 按照异常类型进行处理

final ServerHttpRequest request = exchange.getRequest();

HttpStatus httpStatus;

String body;

int code = 500;

if (ex instanceof ResponseStatusException) {

ResponseStatusException responseStatusException = (ResponseStatusException) ex;

httpStatus = responseStatusException.getStatus();

if (httpStatus == HttpStatus.NOT_FOUND) {

body = "服务接口未找到-404,path:" + request.getPath().value();

} else

body = responseStatusException.getMessage();

} else if (ex instanceof CustomException) {

body = ((CustomException) ex).getMessage();

code = ((CustomException) ex).getCode();

} else if (ex instanceof WebClientResponseException) {

final Response result = JsonUtils.fromJson(((WebClientResponseException) ex).getResponseBodyAsString(), Response.class);

body = result.getMessage();

code = result.getCode();

} else if (ex instanceof ValidationException) {

body = ex.getMessage();

code = 400;

} else {

log.error(ex.getMessage(), ex);

body = "服务器繁忙-请稍后重试。";

}

log.error("[全局异常处理]异常请求路径:{},记录异常信息:{}", request.getPath(), ex.getMessage());

final ServerHttpResponse response = exchange.getResponse();

if (response.isCommitted()) {

return Mono.error(ex);

}

response.getHeaders()

.setContentType(MediaType

.APPLICATION_JSON_UTF8);

response.setStatusCode(HttpStatus

.INTERNAL_SERVER_ERROR);

return response

.writeWith(Mono

.just(response

.bufferFactory()

.wrap(JsonUtils

.toJson(Response

.builder()

.code(code)

.message(body)

.build())

.getBytes(Charset

.forName("UTF-8")))));

}

}

Response.java


 

package io.qingmu.demogateway.advice;


import lombok.*;

import java.io.Serializable;


@Getter

@Setter

@NoArgsConstructor

@Builder

@AllArgsConstructor

public class Response<T> implements Serializable {


protected int code;


protected String message;


protected T data;

}

DemoGatewayApplication


 

package io.qingmu.demogateway;


import io.qingmu.demogateway.advice.CustomExceptionHandler;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Primary;

import org.springframework.core.Ordered;

import org.springframework.core.annotation.Order;


@SpringBootApplication

@EnableDiscoveryClient

public class DemoGatewayApplication {


public static void main(String[] args) {

SpringApplication.run(DemoGatewayApplication.class, args);

}


@Primary

@Bean

@Order(Ordered.HIGHEST_PRECEDENCE)

public ErrorWebExceptionHandler errorWebExceptionHandler() {

return new CustomExceptionHandler();

}


}

静态路由

服务发现动态路由

配置zipkin的采集率,1.0 表示100%,0.1表示采集10%。


 

spring:

zipkin:

base-url: ${ZIPKIN:http://10.96.0.13:9411/}

sleuth:

sampler:

probability: ${SAMPLER_PROBABILITY:1.0}


启用服务发现,自动配置路由。

比如我们有服务user-service中有api资源获取单个用户信息接口 /get

我们直接访问user-service的http接口为: http://ip:port/get

通过网关服务发现后的访问http接口为: http://gatewayip:gatwayport/user-service/get


 

spring:

cloud:

gateway:

discovery:

locator:

lowerCaseServiceId: true

enabled: true

配置gateway的http client的相关参数


 

spring:

cloud:

gateway:

httpclient:

pool:

max-connections: ${MAX_CONNECTIONS:300}

connect-timeout: ${CONNECT_TIMEOUT:10000}

response-timeout: ${RESPONSE_TIMEOUT:5s}


配置全局默认filters,这里我们可以激活retry filter。


 

spring:

cloud:

gateway:

default-filters:

- StripPrefix=1

- name: Retry

args:

retries: 3

series:

- SERVER_ERROR

- CLIENT_ERROR

statuses:

- INTERNAL_SERVER_ERROR

methods:

- GET

- POST

exceptions:

- java.io.IOException

- java.util.concurrent.TimeoutException


完成的 application.yml 如下


 

server:

port: 8084

spring:

zipkin:

base-url: ${ZIPKIN:http://10.96.0.13:9411/}

sleuth:

sampler:

probability: ${SAMPLER_PROBABILITY:1.0}

cloud:

gateway:

discovery:

locator:

lowerCaseServiceId: true

enabled: true

httpclient:

pool:

max-connections: ${MAX_CONNECTIONS:300}

connect-timeout: ${CONNECT_TIMEOUT:10000}

response-timeout: ${RESPONSE_TIMEOUT:5s}

metrics:

enabled: true

default-filters:

- StripPrefix=1

- name: Retry

args:

retries: 3

series:

- SERVER_ERROR

- CLIENT_ERROR

statuses:

- INTERNAL_SERVER_ERROR

methods:

- GET

- POST

exceptions:

- java.io.IOException

- java.util.concurrent.TimeoutException

application:

name: demo-gateway

ribbon:

okhttp:

enabled: true

logging:

logPath: /var/log/${spring.application.name}

level:

com.netflix.discovery.shared.resolver.aws: ERROR

management:

endpoints:

web:

exposure:

include: "*"

DemoGatewayApplication

启动类如下


 

package io.qingmu.demogateway;


import io.qingmu.demogateway.advice.CustomExceptionHandler;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Primary;

import org.springframework.core.Ordered;

import org.springframework.core.annotation.Order;


@SpringBootApplication

@EnableDiscoveryClient

public class DemoGatewayApplication {

public static void main(String[] args) {

SpringApplication.run(DemoGatewayApplication.class, args);

}

@Primary

@Bean

@Order(Ordered.HIGHEST_PRECEDENCE)

public ErrorWebExceptionHandler errorWebExceptionHandler() {

return new CustomExceptionHandler();

}

}

启动我们网关服务,等待他启动完成,我们就可以通过网关来统一业务服务的访问。

启动我们的之前的业务服务demo1-serivce和demo2-service,通过如下url即可访问到。

访问demo1服务的hello接口


 

$ curl -i http://127.0.0.1:8084/demo1-service/hello

HTTP/1.1 200 OK

Content-Type: text/plain;charset=UTF-8

Content-Length: 5

Date: Wed, 21 Aug 2019 10:41:45 GMT


hello

此时的调用链条,我们可以从zipkin中看到,如下图:

在kubernetes上运行Spring Cloud Gateway

gateway1

访问demo2服务的hello接口


 

$ curl -i http://127.0.0.1:8084/demo2-service/world

HTTP/1.1 200 OK

Content-Type: text/plain;charset=UTF-8

Content-Length: 5

Date: Wed, 21 Aug 2019 10:47:05 GMT


world

这样我们就完成了网关的动态映射。

END

在kubernetes上运行Spring Cloud Gateway

在kubernetes上运行Spring Cloud Gateway

在kubernetes上运行Spring Cloud Gateway

原文  http://mp.weixin.qq.com/s?__biz=MzI5ODQ2MzI3NQ==&mid=2247487934&idx=1&sn=53dea254b45012f895b5e69a39e2385f
正文到此结束
Loading...