Spring Cloud Gateway 流量复制

1530 字
8 分钟
Spring Cloud Gateway 流量复制
📌

某些业务合作方,只支持配置一个回调地址,针对开发、测试、生产等多套环境,需要用到流量复制功能,这里记录一个通过 Spring Cloud Gateway 的流量复制功能实现(实际上基于 nginx 或其他轮子也能实现目的)

功能逻辑#

异步

流式

请求到达

有请求体?

立即异步镜像

装饰请求流

继续主请求

主请求完成

镜像执行

边读边镜像

异步

流式

请求到达

有请求体?

立即异步镜像

装饰请求流

继续主请求

主请求完成

镜像执行

边读边镜像

配置示例#

spring:
cloud:
gateway:
routes:
- id: example-route
uri: http://main-service
filters:
- name: TrafficMirroring
args:
mirroredRoutes:
- http://mirror-service-1
- http://mirror-service-2
timeoutSeconds: 3

功能特色#

  • 立即返回:主请求不等待镜像完成
  • 独立线程池:镜像使用专用线程,不占用网关核心线程
  • 流式处理:大请求体不需要完整加载到内存
  • 错误隔离:镜像失败绝不影响主业务
  • 资源限制:配置超时和大小限制防止资源泄露
  • 这样的实现确保了 镜像流量零阻塞主链路请求,同时保持了镜像功能的完整性和可靠性。

核心代码#

import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
/**
* 流量镜像网关过滤器工厂
*
* 功能:将请求复制到指定的镜像路由,实现完全非阻塞流量镜像 特性: - 真正的异步非阻塞处理,镜像不影响主请求性能 - 支持流式请求体处理,避免大请求体内存占用 - 支持多个镜像目标 - 自动错误处理和资源清理 - 智能的请求体复用机制
*/
@Slf4j
@Component
public class TrafficMirroringGatewayFilterFactory
extends AbstractGatewayFilterFactory<TrafficMirroringGatewayFilterFactory.Config> implements DisposableBean {
private final WebClient webClient;
private final ExecutorService executorService;
private final Scheduler scheduler;
public TrafficMirroringGatewayFilterFactory() {
super(TrafficMirroringGatewayFilterFactory.Config.class);
this.executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2, r -> {
Thread t = new Thread(r, "traffic-mirror-");
t.setDaemon(true);
return t;
});
this.scheduler = Schedulers.fromExecutorService(executorService);
this.webClient =
WebClient.builder().codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024)) // 1MB
.build();
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
List<URI> mirroredRoutes = config.getMirroredRoutes();
if (CollectionUtils.isEmpty(mirroredRoutes)) {
return chain.filter(exchange);
}
ServerHttpRequest request = exchange.getRequest();
// 对于无请求体的方法,立即镜像并继续主请求
if (isBodylessMethod(request.getMethod())) {
// 完全异步镜像,不阻塞主请求
Mono.fromRunnable(() -> mirrorBodylessRequest(request, mirroredRoutes, config)).subscribeOn(scheduler)
.doOnError(error -> log.warn("Bodyless mirror failed", error)).subscribe();
return chain.filter(exchange);
}
// 对于有请求体的方法,使用流式处理,不等待请求体完全读取
ServerHttpRequest decoratedRequest = decorateRequestForMirroring(request, mirroredRoutes, config);
// 立即继续主请求,镜像在后台异步进行
return chain.filter(exchange.mutate().request(decoratedRequest).build());
};
}
/**
* 装饰请求以支持流式镜像,不阻塞主请求
*/
private ServerHttpRequest decorateRequestForMirroring(ServerHttpRequest originalRequest, List<URI> mirroredRoutes,
Config config) {
return new ServerHttpRequestDecorator(originalRequest) {
@Override
public Flux<DataBuffer> getBody() {
Flux<DataBuffer> originalBody = super.getBody();
// 收集完整的请求体,然后进行镜像
return originalBody.map(dataBuffer -> {
// 克隆DataBuffer用于镜像,保持原始DataBuffer不变
DataBuffer cloned = cloneDataBuffer(dataBuffer);
return new DataBufferWrapper(dataBuffer, cloned);
}).collectList() // 收集所有DataBufferWrapper
.doOnNext(wrappers -> {
// 异步处理镜像,不阻塞主请求
Mono.fromRunnable(() -> {
try {
// 将所有克隆的DataBuffer合并为完整的请求体用于镜像
java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream();
for (DataBufferWrapper wrapper : wrappers) {
byte[] bytes = new byte[wrapper.cloned.readableByteCount()];
wrapper.cloned.read(bytes);
outputStream.write(bytes);
// 释放克隆的DataBuffer
DataBufferUtils.release(wrapper.cloned);
}
byte[] fullBodyBytes = outputStream.toByteArray();
// 发送镜像请求
mirrorRequest(originalRequest, mirroredRoutes, config, fullBodyBytes);
} catch (Exception e) {
log.warn("Failed to mirror request", e);
}
}).subscribeOn(scheduler).doOnError(error -> log.warn("Mirror request failed", error))
.subscribe();
}).doOnError(error -> log.warn("Request body collection error", error))
.flatMapMany(wrappers -> Flux.fromIterable(wrappers).map(wrapper -> wrapper.original));
}
};
}
/**
* 克隆数据缓冲区用于镜像
*/
private DataBuffer cloneDataBuffer(DataBuffer original) {
DataBuffer cloned = original.factory().allocateBuffer(original.readableByteCount());
cloned.write(original.slice(original.readPosition(), original.readableByteCount()));
return cloned;
}
/**
* DataBuffer包装器,用于同时持有原始和克隆的DataBuffer
*/
private static class DataBufferWrapper {
final DataBuffer original;
final DataBuffer cloned;
DataBufferWrapper(DataBuffer original, DataBuffer cloned) {
this.original = original;
this.cloned = cloned;
}
}
/**
* 判断是否为无请求体的HTTP方法
*/
private boolean isBodylessMethod(HttpMethod method) {
return method == HttpMethod.GET || method == HttpMethod.HEAD || method == HttpMethod.OPTIONS
|| method == HttpMethod.TRACE;
}
/**
* 执行镜像请求 - 完全异步,不阻塞调用者
*/
private void mirrorRequest(ServerHttpRequest request, List<URI> mirroredRoutes, Config config, byte[] bodyBytes) {
mirroredRoutes.forEach(mirroredRoute -> {
Mono.fromCallable(() -> {
WebClient.RequestHeadersSpec<?> requestSpec = webClient.method(request.getMethod())
.uri(buildMirrorUri(mirroredRoute, request)).headers(httpHeaders -> {
httpHeaders.addAll(request.getHeaders());
// 移除可能导致问题的头部
httpHeaders.remove("host");
httpHeaders.remove("content-length");
if (bodyBytes != null && bodyBytes.length == 0) {
httpHeaders.remove("content-type");
}
});
if (bodyBytes != null && bodyBytes.length > 0) {
requestSpec = ((WebClient.RequestBodySpec)requestSpec).body(BodyInserters.fromValue(bodyBytes));
}
return requestSpec.retrieve().toBodilessEntity().timeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.doOnSuccess(response -> {
log.info("Mirror request succeeded to {}", mirroredRoute);
// if (log.isDebugEnabled()) {
// log.debug("Mirror request succeeded to {}", mirroredRoute);
// }
}).doOnError(error -> handleMirrorError(mirroredRoute, error, config));
}).subscribeOn(scheduler).flatMap(mono -> mono)
.doOnError(error -> handleMirrorError(mirroredRoute, error, config)).onErrorResume(error -> {
log.error(error.getMessage(), error);
return Mono.empty();
}) // 忽略镜像错误,不影响主流程
.subscribe();
});
}
/**
* 镜像无请求体的请求
*/
private void mirrorBodylessRequest(ServerHttpRequest request, List<URI> mirroredRoutes, Config config) {
mirrorRequest(request, mirroredRoutes, config, null);
}
/**
* 构建镜像URI,保留原始请求的路径和查询参数
*/
private URI buildMirrorUri(URI mirroredRoute, ServerHttpRequest request) {
String query = request.getURI().getQuery();
String uriString = mirroredRoute.toString();
if (query != null && !query.isEmpty() && !isUrlEncoded(query)) {
try {
uriString += "?" + URLEncoder.encode(query, StandardCharsets.UTF_8.toString());
} catch (Exception e) {
// ignore
}
}
return URI.create(uriString);
}
public static boolean isUrlEncoded(String value) {
if (value == null)
return false;
try {
// 解码后与原字符串不同,则可能已编码
String decoded = URLDecoder.decode(value, StandardCharsets.UTF_8.toString());
return !decoded.equals(value);
} catch (Exception e) {
// 解码失败说明可能未编码
return false;
}
}
/**
* 处理镜像请求错误
*/
private void handleMirrorError(URI mirroredRoute, Throwable error, Config config) {
if (config.isEnableErrorLogging()) {
if (error instanceof WebClientResponseException) {
WebClientResponseException webClientError = (WebClientResponseException)error;
// 404 不记录警告,可能是正常情况
if (webClientError.getStatusCode() != HttpStatus.NOT_FOUND) {
log.warn("Mirror request failed to {}: {} - {}", mirroredRoute, webClientError.getStatusCode(),
webClientError.getMessage());
}
} else {
log.warn("Mirror request failed to {}: {}", mirroredRoute, error.getMessage());
}
}
}
@Override
public String name() {
return "TrafficMirroring";
}
/**
* 清理资源
*/
@Override
public void destroy() throws Exception {
log.info("Shutting down traffic mirroring executor service");
scheduler.dispose();
executorService.shutdown();
try {
if (!executorService.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) {
log.warn("Executor service did not terminate gracefully, forcing shutdown");
executorService.shutdownNow();
}
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for executor service termination");
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
/**
* 流量镜像配置类
*/
@Data
public static class Config {
/**
* 镜像目标路由列表
*/
private List<URI> mirroredRoutes;
/**
* 镜像请求超时时间(秒),默认3秒
*/
private int timeoutSeconds = 3;
/**
* 是否启用错误日志,默认启用
*/
private boolean enableErrorLogging = true;
/**
* 是否启用流式镜像,默认启用
*/
private boolean enableStreamMirroring = true;
/**
* 最大镜像请求体大小(字节),默认1MB
*/
private int maxMirrorBodySize = 1024 * 1024;
}
}

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Spring Cloud Gateway 流量复制
https://blog.sephy.top/posts/spring-cloud-gateway-流量复制/
作者
虾米
发布于
2025-07-02
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
虾米
coder
分类
标签
站点统计
文章
61
分类
4
标签
52
总字数
64,663
运行时长
0
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.13.9
文章许可
CC BY-NC-SA 4.0