Spring Boot 内置反向代理(Undertow Proxy)高可用配置

708 阅读6分钟

引言

在微服务架构中,反向代理是一个不可或缺的组件,它负责请求转发、负载均衡、安全过滤等关键功能。

通常我们会选择 Nginx、HAProxy 等专业反向代理组件,但在某些场景下,使用 Spring Boot 内置的反向代理功能可以简化架构,减少运维复杂度。

本文将介绍如何利用 Undertow 服务器的反向代理能力,实现高可用的反向代理配置。

Undertow 简介

Undertow 是一个采用 Java 开发的灵活的高性能 Web 服务器,提供基于 NIO 的阻塞和非阻塞 API。

作为 Spring Boot 支持的内嵌式服务器之一,它具有以下特点:

轻量级:核心仅依赖于 JBoss Logging 和 xnio
高性能:在多核系统上表现优异
内置反向代理:支持 HTTP、HTTPS、HTTP/2 代理
可扩展:通过 Handler 链模式支持灵活扩展

为什么选择 Undertow 内置反向代理

在某些场景下,使用 Undertow 内置的反向代理功能比独立部署 Nginx 等代理服务器更有优势:

1. 简化架构:减少额外组件,降低部署复杂度
2. 统一技术栈:全 Java 技术栈,便于开发团队维护
3. 配置灵活:可通过代码动态调整代理规则
4. 节约资源:适合资源有限的环境,如边缘计算场景
5. 集成监控:与 Spring Boot 的监控体系无缝集成

基础配置

步骤 1:添加 Undertow 依赖

首先,确保 Spring Boot 项目使用 Undertow 作为嵌入式服务器:

<?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>3.4.5</version>
        <relativePath/>
    </parent>
    <groupId>demo</groupId>
    <artifactId>springboot-undertow-proxy</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.32</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>21</source>
                    <target>21</target>
                    <encoding>utf-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>3.2.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

步骤 2:创建 Undertow 配置类

package com.example.config;

import io.undertow.Handlers;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.PathHandler;
import io.undertow.server.handlers.RequestLimitingHandler;
import io.undertow.server.handlers.ResponseCodeHandler;
import io.undertow.server.handlers.proxy.LoadBalancingProxyClient;
import io.undertow.server.handlers.proxy.ProxyHandler;
import io.undertow.util.HeaderMap;
import io.undertow.util.HttpString;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.xnio.OptionMap;

import java.net.URI;

@Configuration
public class UndertowProxyConfig {

    @Bean
    @ConditionalOnProperty(name = "user.enabled", havingValue = "false", matchIfMissing = true)
    public WebServerFactoryCustomizer<UndertowServletWebServerFactory> undertowProxyCustomizer() {
        return factory -> factory.addDeploymentInfoCustomizers(deploymentInfo -> {
            deploymentInfo.addInitialHandlerChainWrapper(handler -> {
                PathHandler pathHandler = Handlers.path(handler);

                // 配置代理路由
                HttpHandler handler1 = createProxyClient("http://127.0.0.1:8081/user");
                HttpHandler handler2 = createProxyClient("http://127.0.0.2:8081/user/users2");

                handler1 = secureProxyHandler(handler1);
                handler1 = createRateLimitingHandler(handler1);

                // 添加路由规则
                pathHandler.addPrefixPath("/user", handler1);
                pathHandler.addPrefixPath("/user/users2", handler2);

                return pathHandler;
            });
        });
    }
    
    private HttpHandler createProxyClient(String targetUrl) {
        try {
            URI uri = new URI(targetUrl);
            LoadBalancingProxyClient proxyClient = new LoadBalancingProxyClient();
            proxyClient.addHost(uri);
            proxyClient
                    .setConnectionsPerThread(20)
                    .setMaxQueueSize(10)
                    .setSoftMaxConnectionsPerThread(20)
                    .setProblemServerRetry(5)
                    .setTtl(30000);

            return ProxyHandler.builder()
                    .setProxyClient(proxyClient)
                    .setMaxRequestTime(30000)
                    .setRewriteHostHeader(false)
                    .setReuseXForwarded(true)
                    .build();

        } catch (Exception e) {
            throw new RuntimeException("创建代理客户端失败", e);
        }
    }

    private HttpHandler secureProxyHandler(HttpHandler proxyHandler) {
        return exchange -> {
            // 移除敏感头部
            HeaderMap headers = exchange.getRequestHeaders();
            headers.remove("X-Forwarded-Server");

            // 添加安全头部
            exchange.getResponseHeaders().add(new HttpString("X-XSS-Protection"), "1; mode=block");
            exchange.getResponseHeaders().add(new HttpString("X-Content-Type-Options"), "nosniff");
            exchange.getResponseHeaders().add(new HttpString("X-Frame-Options"), "DENY");

            // 添加代理信息
            headers.add(new HttpString("X-Forwarded-For"), exchange.getSourceAddress().getAddress().getHostAddress());
            headers.add(new HttpString("X-Forwarded-Proto"), exchange.getRequestScheme());
            headers.add(new HttpString("X-Forwarded-Host"), exchange.getHostName());

            proxyHandler.handleRequest(exchange);
        };
    }

    private HttpHandler createRateLimitingHandler(HttpHandler next) {
        // 根据实际情况调整
        return new RequestLimitingHandler(1,1,next);
    }

}

高可用配置

要实现真正的高可用反向代理,需要考虑以下几个关键方面:

1. 负载均衡策略

Undertow 提供多种负载均衡策略,可以根据需求选择:

@Bean
public LoadBalancingProxyClient loadBalancingProxyClient() {
    LoadBalancingProxyClient loadBalancer = new LoadBalancingProxyClient();
    
    // 配置负载均衡策略
    loadBalancer.setRouteParsingStrategy(RouteParsingStrategy.RANKED);
    loadBalancer.setConnectionsPerThread(20);

    // 添加后端服务器
    loadBalancer.addHost(new URI("http://backend1:8080"));
    loadBalancer.addHost(new URI("http://backend2:8080"));
    loadBalancer.addHost(new URI("http://backend3:8080"));
    
    // 设置会话亲和性(可选)
    loadBalancer.addSessionCookieName("JSESSIONID");
    
    return loadBalancer;
}

2. 健康检查与自动故障转移

实现定期健康检查,自动剔除不健康节点:

package com.example.config;

import io.undertow.server.handlers.proxy.LoadBalancingProxyClient;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Component
@ConditionalOnProperty(name = "user.enabled", havingValue = "false", matchIfMissing = true)
@Slf4j
public class BackendHealthMonitor {
    
    private final LoadBalancingProxyClient loadBalancer;
    private final List<URI> backendServers;
    private final RestTemplate restTemplate;
    
    public BackendHealthMonitor(@Value("#{'${user.backends}'.split(',')}") String[] backends,
            LoadBalancingProxyClient loadBalancer) throws URISyntaxException {
        this.loadBalancer = loadBalancer;
        this.restTemplate = new RestTemplate();
        this.backendServers = Arrays.stream(backends)
                .map(url -> {
                    try {
                        return new URI(url);
                    } catch (URISyntaxException e) {
                        throw new RuntimeException(e);
                    }
                })
                .collect(Collectors.toList());
    }
    
    @Scheduled(fixedDelay = 10000) // 每10秒检查一次
    public void checkBackendHealth() {
        for (URI server : backendServers) {
            try {
                String healthUrl = server.getScheme() + "://" + server.getHost() + ":" + server.getPort() + "/health";
                ResponseEntity<String> response = restTemplate.getForEntity(healthUrl, String.class);
                
                if (response.getStatusCode().is2xxSuccessful()) {
                    loadBalancer.addHost(server);
                    log.info("后端服务 {} 状态正常,已添加到负载均衡", server);
                } else {
                    // 服务不健康,从负载均衡器中移除
                    loadBalancer.removeHost(server);
                    log.warn("后端服务 {} 状态异常,已从负载均衡中移除", server);
                }
            } catch (Exception e) {
                // 连接异常,从负载均衡器中移除
                loadBalancer.removeHost(server);
                log.error("后端服务 {} 连接异常: {}", server, e.getMessage());
            }
        }
    }
}

3. 集群高可用

为确保被代理服务的高可用,可配置多个代理实例:

user:
  backends: "http://127.0.0.1:8081,http://127.0.0.2:8081"

性能优化

要获得最佳性能,需要调整 Undertow 的相关参数(需要根据项目实际情况进行测试调整):

server:
  undertow:
    threads: 
      io: 8                      # IO线程数,建议设置为CPU核心数
      worker: 64                 # 工作线程数,IO线程数的8倍
    buffer-size: 16384           # 缓冲区大小
    direct-buffers: true         # 使用直接缓冲区
    max-http-post-size: 10485760 # 最大POST大小
    max-parameters: 2000         # 最大参数数量
    max-headers: 200             # 最大请求头数量
    max-cookies: 200             # 最大Cookie数量

连接池优化

@Bean
public UndertowServletWebServerFactory undertowFactory() {
    UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
    factory.addBuilderCustomizers(builder -> {
        builder.setSocketOption(Options.KEEP_ALIVE, true)
               .setSocketOption(Options.TCP_NODELAY, true)
               .setSocketOption(Options.REUSE_ADDRESSES, true)
               .setSocketOption(Options.BACKLOG, 10000)
               .setServerOption(UndertowOptions.MAX_ENTITY_SIZE, 16 * 1024 * 1024L)
               .setServerOption(UndertowOptions.IDLE_TIMEOUT, 60 * 1000)
               .setServerOption(UndertowOptions.REQUEST_PARSE_TIMEOUT, 30 * 1000)
               .setServerOption(UndertowOptions.NO_REQUEST_TIMEOUT, 60 * 1000)
               .setServerOption(UndertowOptions.MAX_CONCURRENT_REQUESTS_PER_CONNECTION, 200);
    });
    return factory;
}

安全强化

反向代理需要考虑安全性,可以添加以下配置:

1. 请求头过滤与重写

private HttpHandler secureProxyHandler(HttpHandler proxyHandler) {
    return exchange -> {
        // 移除敏感头部
        HeaderMap headers = exchange.getRequestHeaders();
        headers.remove("X-Forwarded-Server");
        
        // 添加安全头部
        exchange.getResponseHeaders().add(new HttpString("X-XSS-Protection"), "1; mode=block");
        exchange.getResponseHeaders().add(new HttpString("X-Content-Type-Options"), "nosniff");
        exchange.getResponseHeaders().add(new HttpString("X-Frame-Options"), "DENY");
        
        // 添加代理信息
        headers.add(new HttpString("X-Forwarded-For"), exchange.getSourceAddress().getAddress().getHostAddress());
        headers.add(new HttpString("X-Forwarded-Proto"), exchange.getRequestScheme());
        headers.add(new HttpString("X-Forwarded-Host"), exchange.getHostName());
        
        proxyHandler.handleRequest(exchange);
    };
}

2. 请求限流

private HttpHandler createRateLimitingHandler(HttpHandler next) {
    return new RequestLimitingHandler(100,next);
}

实际案例:某系统 API 网关

以一个电商系统为例,展示 Undertow 反向代理的实际应用:

package com.example.config;

import io.undertow.Handlers;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.PathHandler;
import io.undertow.server.handlers.proxy.LoadBalancingProxyClient;
import io.undertow.server.handlers.proxy.ProxyHandler;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.net.URI;

@Configuration
public class EcommerceProxyConfig {

    @Bean
    public WebServerFactoryCustomizer<UndertowServletWebServerFactory> ecommerceProxyCustomizer() {
        return factory -> factory.addDeploymentInfoCustomizers(deploymentInfo -> {
            deploymentInfo.addInitialHandlerChainWrapper(handler -> {
                PathHandler pathHandler = Handlers.path(handler);
                try {
                    // 用户服务代理
                    LoadBalancingProxyClient userServiceClient = new LoadBalancingProxyClient();
                    userServiceClient.addHost(new URI("http://user-service-1:8080/api/users"));
                    userServiceClient.addHost(new URI("http://user-service-2:8080/api/users"));

                    // 商品服务代理
                    LoadBalancingProxyClient productServiceClient = new LoadBalancingProxyClient();
                    productServiceClient.addHost(new URI("http://product-service-1:8080/api/products"));
                    productServiceClient.addHost(new URI("http://product-service-2:8080/api/products"));

                    // 订单服务代理
                    LoadBalancingProxyClient orderServiceClient = new LoadBalancingProxyClient();
                    orderServiceClient.addHost(new URI("http://order-service-1:8080/api/orders"));
                    orderServiceClient.addHost(new URI("http://order-service-2:8080/api/orders"));

                    // 路由规则
                    pathHandler.addPrefixPath("/api/users", createProxyHandler(userServiceClient));
                    pathHandler.addPrefixPath("/api/products", createProxyHandler(productServiceClient));
                    pathHandler.addPrefixPath("/api/orders", createProxyHandler(orderServiceClient));

                    return pathHandler;
                }catch (Exception e){
                    throw new RuntimeException(e);
                }
            });
        });
    }
    
    private HttpHandler createProxyHandler(LoadBalancingProxyClient client) {
        return ProxyHandler.builder()
                .setProxyClient(client)
                .setMaxRequestTime(30000)
                .setRewriteHostHeader(true)
                .build();
    }
}

总结

Spring Boot 内置的 Undertow 反向代理功能为微服务架构提供了一种轻量级的代理解决方案。

虽然功能上可能不如专业的反向代理服务器(如 Nginx)那么丰富,但在特定场景下,尤其是希望简化架构、统一技术栈的情况下,可以作为一种备选方案。