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@Componentpublic 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-流量复制/相关文章智能推荐
1
Spring Cloud Gateway GlobalFilter 整理
技术分享记录 spring cloud gateway 自带 GlobalFilter 方便查询
2
JMM
技术分享Java 内存模型详解。包含主内存/工作内存、happens-before、内存屏障、volatile 原理等。
3
JVM GC
技术分享JVM 垃圾回收机制详解。包含 GC 算法、垃圾回收器、内存区域、GC 日志分析等。
4
Playwright 使用心得
2025-08-08
5
Mybatis 基础字段自动维护
技术分享感觉 mybatis plus 中的自动填充机制不够灵活优雅, 所以有了这篇文章
随机文章随机推荐













