From 996bb9481a1aba08ce20b3ad16a155825201a4ea Mon Sep 17 00:00:00 2001 From: wenjinbo <599483010@qq.com> Date: Sun, 28 Sep 2025 17:30:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E7=AE=A1=E7=90=86http?= =?UTF-8?q?=E8=B5=84=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat-server/pom.xml | 17 +- .../com/bjtds/brichat/config/EsConfig.java | 53 ++++- .../brichat/config/HttpClientConfig.java | 96 +++++++- .../brichat/config/RestTemplateConfig.java | 107 ++++++++- .../util/HttpConnectionPoolTestUtil.java | 212 ++++++++++++++++++ .../src/main/resources/application-wuhan.yml | 2 +- .../src/main/resources/application.yml | 35 +++ 7 files changed, 509 insertions(+), 13 deletions(-) create mode 100644 chat-server/src/main/java/com/bjtds/brichat/util/HttpConnectionPoolTestUtil.java diff --git a/chat-server/pom.xml b/chat-server/pom.xml index 10c48d8..60863ec 100644 --- a/chat-server/pom.xml +++ b/chat-server/pom.xml @@ -309,13 +309,26 @@ org.apache.httpcomponents httpclient - 4.5 + 4.5.13 org.apache.httpcomponents httpcore - 4.4.5 + 4.4.15 + + + + + + + + + + + + + diff --git a/chat-server/src/main/java/com/bjtds/brichat/config/EsConfig.java b/chat-server/src/main/java/com/bjtds/brichat/config/EsConfig.java index 85f1d02..80cbb9d 100644 --- a/chat-server/src/main/java/com/bjtds/brichat/config/EsConfig.java +++ b/chat-server/src/main/java/com/bjtds/brichat/config/EsConfig.java @@ -5,13 +5,18 @@ import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.rest_client.RestClientTransport; import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; import org.elasticsearch.client.RestClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Configuration public class EsConfig { + + private static final Logger logger = LoggerFactory.getLogger(EsConfig.class); @Value("${elasticsearch.host}") private String host; @@ -21,15 +26,61 @@ public class EsConfig { @Value("${elasticsearch.scheme}") private String scheme; + + // 复用HttpClientConfig中的连接池配置参数 + @Value("${http.pool.max-total:200}") + private int maxTotal; + + @Value("${http.pool.max-per-route:50}") + private int maxPerRoute; + + @Value("${http.timeout.connect:10000}") + private int connectTimeout; + + @Value("${http.timeout.socket:30000}") + private int socketTimeout; + + @Value("${http.timeout.request:5000}") + private int connectionRequestTimeout; @Bean public ElasticsearchClient elasticsearchClient() { + // 为Elasticsearch配置独立的RestClient,但使用相同的连接池参数 RestClient restClient = RestClient.builder( new HttpHost(host, port, scheme) - ).build(); + ) + .setRequestConfigCallback(requestConfigBuilder -> { + return requestConfigBuilder + .setConnectTimeout(connectTimeout) + .setSocketTimeout(socketTimeout) + .setConnectionRequestTimeout(connectionRequestTimeout); + }) + .setHttpClientConfigCallback(httpClientBuilder -> { + return httpClientBuilder + .setMaxConnTotal(maxTotal) + .setMaxConnPerRoute(maxPerRoute); + }) + .build(); + ElasticsearchTransport transport = new RestClientTransport( restClient, new JacksonJsonpMapper() ); + + logger.info("Elasticsearch客户端已配置独立连接池 - 主机: {}:{}, 最大连接数: {}, 每路由最大连接数: {}", + host, port, maxTotal, maxPerRoute); + return new ElasticsearchClient(transport); } + + +// @Bean +// public ElasticsearchClient elasticsearchClient() { +// RestClient restClient = RestClient.builder( +// new HttpHost(host, port, scheme) +// ).build(); +// ElasticsearchTransport transport = new RestClientTransport( +// restClient, new JacksonJsonpMapper() +// ); +// return new ElasticsearchClient(transport); +// } } diff --git a/chat-server/src/main/java/com/bjtds/brichat/config/HttpClientConfig.java b/chat-server/src/main/java/com/bjtds/brichat/config/HttpClientConfig.java index b0eb164..f20fb76 100644 --- a/chat-server/src/main/java/com/bjtds/brichat/config/HttpClientConfig.java +++ b/chat-server/src/main/java/com/bjtds/brichat/config/HttpClientConfig.java @@ -6,8 +6,13 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.PreDestroy; +import java.util.concurrent.TimeUnit; /** * HTTP客户端连接池配置 @@ -15,9 +20,30 @@ import javax.annotation.PreDestroy; */ @Configuration public class HttpClientConfig { + + private static final Logger logger = LoggerFactory.getLogger(HttpClientConfig.class); private PoolingHttpClientConnectionManager connectionManager; private CloseableHttpClient httpClient; + + // 连接池配置参数 + @Value("${http.pool.max-total:200}") + private int maxTotal; + + @Value("${http.pool.max-per-route:50}") + private int maxPerRoute; + + @Value("${http.timeout.connect:10000}") + private int connectTimeout; + + @Value("${http.timeout.socket:30000}") + private int socketTimeout; + + @Value("${http.timeout.request:5000}") + private int connectionRequestTimeout; + + @Value("${http.pool.validate-after-inactivity:30000}") + private int validateAfterInactivity; @Bean public CloseableHttpClient httpClient() { @@ -25,27 +51,82 @@ public class HttpClientConfig { connectionManager = new PoolingHttpClientConnectionManager(); // 设置最大连接数 - connectionManager.setMaxTotal(200); + connectionManager.setMaxTotal(maxTotal); // 设置每个路由的最大连接数 - connectionManager.setDefaultMaxPerRoute(50); + connectionManager.setDefaultMaxPerRoute(maxPerRoute); + + // 设置连接在池中的最大空闲时间 + connectionManager.setValidateAfterInactivity(validateAfterInactivity); // 设置连接超时、读取超时等配置 RequestConfig requestConfig = RequestConfig.custom() - .setConnectTimeout(10000) // 连接超时10秒 - .setSocketTimeout(30000) // 读取超时30秒 - .setConnectionRequestTimeout(5000) // 从连接池获取连接超时5秒 + .setConnectTimeout(connectTimeout) // 连接超时 + .setSocketTimeout(socketTimeout) // 读取超时 + .setConnectionRequestTimeout(connectionRequestTimeout) // 从连接池获取连接超时 .build(); // 创建HttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfig) + .setConnectionManagerShared(false) + .evictExpiredConnections() // 自动清理过期连接 + .evictIdleConnections(60, TimeUnit.SECONDS) // 清理60秒空闲连接 .build(); + logger.info("HttpClient连接池配置完成 - 最大连接数: {}, 每路由最大连接数: {}, 连接超时: {}ms, 读取超时: {}ms", + maxTotal, maxPerRoute, connectTimeout, socketTimeout); + return httpClient; } + /** + * 定期清理空闲和过期连接 + */ + @Scheduled(fixedRate = 30000) // 每30秒执行一次 + public void cleanupConnections() { + if (connectionManager != null) { + // 清理过期连接 + connectionManager.closeExpiredConnections(); + // 清理空闲超过60秒的连接 + connectionManager.closeIdleConnections(60, TimeUnit.SECONDS); + + // 记录连接池状态 + logConnectionPoolStats(); + } + } + + /** + * 记录连接池状态 + */ + private void logConnectionPoolStats() { + if (connectionManager != null) { + int totalStats = connectionManager.getTotalStats().getAvailable() + + connectionManager.getTotalStats().getLeased(); + int available = connectionManager.getTotalStats().getAvailable(); + int leased = connectionManager.getTotalStats().getLeased(); + int pending = connectionManager.getTotalStats().getPending(); + + logger.debug("HTTP连接池状态 - 总连接数: {}, 可用: {}, 已使用: {}, 等待: {}", + totalStats, available, leased, pending); + } + } + + /** + * 获取连接池状态信息 + */ + public String getConnectionPoolStatus() { + if (connectionManager != null) { + return String.format("总连接数: %d, 可用: %d, 已使用: %d, 等待: %d", + connectionManager.getTotalStats().getAvailable() + connectionManager.getTotalStats().getLeased(), + connectionManager.getTotalStats().getAvailable(), + connectionManager.getTotalStats().getLeased(), + connectionManager.getTotalStats().getPending()); + } + return "连接池未初始化"; + } + /** * 应用关闭时清理资源 */ @@ -53,13 +134,16 @@ public class HttpClientConfig { public void destroy() { try { if (httpClient != null) { + logger.info("正在关闭HttpClient连接池..."); httpClient.close(); + logger.info("HttpClient连接池已关闭"); } if (connectionManager != null) { connectionManager.close(); + logger.info("HTTP连接管理器已关闭"); } } catch (Exception e) { - // 忽略关闭异常 + logger.error("关闭HTTP连接池时发生异常", e); } } } \ No newline at end of file diff --git a/chat-server/src/main/java/com/bjtds/brichat/config/RestTemplateConfig.java b/chat-server/src/main/java/com/bjtds/brichat/config/RestTemplateConfig.java index 96bf869..f9106bf 100644 --- a/chat-server/src/main/java/com/bjtds/brichat/config/RestTemplateConfig.java +++ b/chat-server/src/main/java/com/bjtds/brichat/config/RestTemplateConfig.java @@ -1,18 +1,119 @@ package com.bjtds.brichat.config; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; +import org.springframework.beans.factory.annotation.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.PreDestroy; +import java.util.concurrent.TimeUnit; @Configuration public class RestTemplateConfig { + + private static final Logger logger = LoggerFactory.getLogger(RestTemplateConfig.class); + + private PoolingHttpClientConnectionManager connectionManager; + private CloseableHttpClient httpClient; + + // RestTemplate连接池配置参数 + @Value("${rest-template.pool.max-total:300}") + private int maxTotal; + + @Value("${rest-template.pool.max-per-route:100}") + private int maxPerRoute; + + @Value("${rest-template.timeout.connect:15000}") + private int connectTimeout; + + @Value("${rest-template.timeout.socket:60000}") + private int socketTimeout; + + @Value("${rest-template.timeout.request:10000}") + private int connectionRequestTimeout; @Bean public RestTemplate restTemplate() { - return new RestTemplate(); + // 创建连接池管理器 + connectionManager = new PoolingHttpClientConnectionManager(); + + // 设置最大连接数 + connectionManager.setMaxTotal(maxTotal); + + // 设置每个路由的最大连接数 + connectionManager.setDefaultMaxPerRoute(maxPerRoute); + + // 设置连接验证间隔 + connectionManager.setValidateAfterInactivity(30000); + + // 设置连接超时、读取超时等配置 + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(connectTimeout) // 连接超时 + .setSocketTimeout(socketTimeout) // 读取超时 + .setConnectionRequestTimeout(connectionRequestTimeout) // 从连接池获取连接超时 + .build(); + + // 创建HttpClient + httpClient = HttpClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + .setConnectionManagerShared(false) + .evictExpiredConnections() // 自动清理过期连接 + .evictIdleConnections(60, TimeUnit.SECONDS) // 清理60秒空闲连接 + .build(); + + // 创建HttpComponentsClientHttpRequestFactory + HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); + + // 创建RestTemplate + RestTemplate restTemplate = new RestTemplate(factory); + + logger.info("RestTemplate连接池配置完成 - 最大连接数: {}, 每路由最大连接数: {}, 连接超时: {}ms, 读取超时: {}ms", + maxTotal, maxPerRoute, connectTimeout, socketTimeout); + + return restTemplate; + } + + /** + * 获取RestTemplate连接池状态 + */ + public String getRestTemplateConnectionPoolStatus() { + if (connectionManager != null) { + return String.format("RestTemplate连接池 - 总连接数: %d, 可用: %d, 已使用: %d, 等待: %d", + connectionManager.getTotalStats().getAvailable() + connectionManager.getTotalStats().getLeased(), + connectionManager.getTotalStats().getAvailable(), + connectionManager.getTotalStats().getLeased(), + connectionManager.getTotalStats().getPending()); + } + return "RestTemplate连接池未初始化"; + } + + /** + * 应用关闭时清理资源 + */ + @PreDestroy + public void destroy() { + try { + if (httpClient != null) { + logger.info("正在关闭RestTemplate连接池..."); + httpClient.close(); + logger.info("RestTemplate连接池已关闭"); + } + if (connectionManager != null) { + connectionManager.close(); + logger.info("RestTemplate连接管理器已关闭"); + } + } catch (Exception e) { + logger.error("关闭RestTemplate连接池时发生异常", e); + } } - } diff --git a/chat-server/src/main/java/com/bjtds/brichat/util/HttpConnectionPoolTestUtil.java b/chat-server/src/main/java/com/bjtds/brichat/util/HttpConnectionPoolTestUtil.java new file mode 100644 index 0000000..7d1e2a0 --- /dev/null +++ b/chat-server/src/main/java/com/bjtds/brichat/util/HttpConnectionPoolTestUtil.java @@ -0,0 +1,212 @@ +package com.bjtds.brichat.util; + +import org.apache.http.impl.client.CloseableHttpClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +/** + * HTTP连接池性能测试工具类 + * 用于验证连接池优化效果 + */ +@Component +public class HttpConnectionPoolTestUtil { + + private static final Logger logger = LoggerFactory.getLogger(HttpConnectionPoolTestUtil.class); + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private CloseableHttpClient httpClient; + + /** + * 并发测试HTTP连接池性能 + * + * @param targetUrl 测试目标URL + * @param concurrency 并发数 + * @param totalRequests 总请求数 + * @return 测试结果 + */ + public TestResult performConcurrencyTest(String targetUrl, int concurrency, int totalRequests) { + logger.info("开始HTTP连接池并发测试 - URL: {}, 并发数: {}, 总请求数: {}", + targetUrl, concurrency, totalRequests); + + ExecutorService executor = Executors.newFixedThreadPool(concurrency); + CountDownLatch latch = new CountDownLatch(totalRequests); + List> futures = new ArrayList<>(); + + long startTime = System.currentTimeMillis(); + + // 提交并发请求任务 + for (int i = 0; i < totalRequests; i++) { + Future future = executor.submit(() -> { + try { + long requestStart = System.currentTimeMillis(); + + // 使用RestTemplate发送请求(可以根据需要切换为HttpClient) + String response = restTemplate.getForObject(targetUrl, String.class); + + long requestEnd = System.currentTimeMillis(); + long responseTime = requestEnd - requestStart; + + return new RequestResult(true, responseTime, null); + } catch (Exception e) { + logger.warn("请求失败: {}", e.getMessage()); + return new RequestResult(false, 0, e.getMessage()); + } finally { + latch.countDown(); + } + }); + futures.add(future); + } + + try { + // 等待所有请求完成 + latch.await(120, TimeUnit.SECONDS); // 最多等待2分钟 + } catch (InterruptedException e) { + logger.error("等待测试完成时被中断", e); + } + + long endTime = System.currentTimeMillis(); + long totalTime = endTime - startTime; + + // 收集结果 + TestResult result = collectResults(futures, totalTime, concurrency, totalRequests); + + executor.shutdown(); + + logger.info("HTTP连接池并发测试完成 - 总耗时: {}ms, 成功率: {}%, 平均响应时间: {}ms", + result.getTotalTime(), result.getSuccessRate(), result.getAverageResponseTime()); + + return result; + } + + /** + * 收集测试结果 + */ + private TestResult collectResults(List> futures, long totalTime, + int concurrency, int totalRequests) { + int successCount = 0; + int failureCount = 0; + long totalResponseTime = 0; + long maxResponseTime = 0; + long minResponseTime = Long.MAX_VALUE; + List errorMessages = new ArrayList<>(); + + for (Future future : futures) { + try { + RequestResult result = future.get(1, TimeUnit.SECONDS); + if (result.isSuccess()) { + successCount++; + totalResponseTime += result.getResponseTime(); + maxResponseTime = Math.max(maxResponseTime, result.getResponseTime()); + minResponseTime = Math.min(minResponseTime, result.getResponseTime()); + } else { + failureCount++; + if (result.getErrorMessage() != null) { + errorMessages.add(result.getErrorMessage()); + } + } + } catch (Exception e) { + failureCount++; + errorMessages.add("获取结果超时: " + e.getMessage()); + } + } + + double successRate = (double) successCount / totalRequests * 100; + long averageResponseTime = successCount > 0 ? totalResponseTime / successCount : 0; + double qps = (double) successCount / (totalTime / 1000.0); + + return TestResult.builder() + .totalRequests(totalRequests) + .successCount(successCount) + .failureCount(failureCount) + .successRate(successRate) + .totalTime(totalTime) + .averageResponseTime(averageResponseTime) + .maxResponseTime(maxResponseTime) + .minResponseTime(minResponseTime == Long.MAX_VALUE ? 0 : minResponseTime) + .qps(qps) + .concurrency(concurrency) + .errorMessages(errorMessages) + .build(); + } + + /** + * 请求结果 + */ + public static class RequestResult { + private final boolean success; + private final long responseTime; + private final String errorMessage; + + public RequestResult(boolean success, long responseTime, String errorMessage) { + this.success = success; + this.responseTime = responseTime; + this.errorMessage = errorMessage; + } + + public boolean isSuccess() { return success; } + public long getResponseTime() { return responseTime; } + public String getErrorMessage() { return errorMessage; } + } + + /** + * 测试结果 + */ + public static class TestResult { + private int totalRequests; + private int successCount; + private int failureCount; + private double successRate; + private long totalTime; + private long averageResponseTime; + private long maxResponseTime; + private long minResponseTime; + private double qps; + private int concurrency; + private List errorMessages; + + public static Builder builder() { + return new Builder(); + } + + // Getters + public int getTotalRequests() { return totalRequests; } + public int getSuccessCount() { return successCount; } + public int getFailureCount() { return failureCount; } + public double getSuccessRate() { return successRate; } + public long getTotalTime() { return totalTime; } + public long getAverageResponseTime() { return averageResponseTime; } + public long getMaxResponseTime() { return maxResponseTime; } + public long getMinResponseTime() { return minResponseTime; } + public double getQps() { return qps; } + public int getConcurrency() { return concurrency; } + public List getErrorMessages() { return errorMessages; } + + public static class Builder { + private TestResult result = new TestResult(); + + public Builder totalRequests(int totalRequests) { result.totalRequests = totalRequests; return this; } + public Builder successCount(int successCount) { result.successCount = successCount; return this; } + public Builder failureCount(int failureCount) { result.failureCount = failureCount; return this; } + public Builder successRate(double successRate) { result.successRate = successRate; return this; } + public Builder totalTime(long totalTime) { result.totalTime = totalTime; return this; } + public Builder averageResponseTime(long averageResponseTime) { result.averageResponseTime = averageResponseTime; return this; } + public Builder maxResponseTime(long maxResponseTime) { result.maxResponseTime = maxResponseTime; return this; } + public Builder minResponseTime(long minResponseTime) { result.minResponseTime = minResponseTime; return this; } + public Builder qps(double qps) { result.qps = qps; return this; } + public Builder concurrency(int concurrency) { result.concurrency = concurrency; return this; } + public Builder errorMessages(List errorMessages) { result.errorMessages = errorMessages; return this; } + + public TestResult build() { return result; } + } + } +} \ No newline at end of file diff --git a/chat-server/src/main/resources/application-wuhan.yml b/chat-server/src/main/resources/application-wuhan.yml index 315a260..3bd1d62 100644 --- a/chat-server/src/main/resources/application-wuhan.yml +++ b/chat-server/src/main/resources/application-wuhan.yml @@ -52,7 +52,7 @@ dify: email: bjtds@bjtds.com # 请替换为实际的 Dify 服务邮箱,若不需要调用 server相关接口可不填 password: 123456Aa # 请替换为实际的 Dify 服务密码,若不需要调用 server相关接口可不填 dataset: - api-key: ${dify-dataset-api-key:dataset-0Hij9IwoWYbJe1vvwVh8y7DS} # 请替换为实际的知识库api-key, 若不需要调用知识库可不填 + api-key: ${dify-dataset-api-key:dataset-fSaqYAlxDdkyyFRWA9zg1Jaw} # 请替换为实际的知识库api-key, 若不需要调用知识库可不填 # PDF转换服务配置 ocr: diff --git a/chat-server/src/main/resources/application.yml b/chat-server/src/main/resources/application.yml index fa7f0fa..9234dc6 100644 --- a/chat-server/src/main/resources/application.yml +++ b/chat-server/src/main/resources/application.yml @@ -42,6 +42,41 @@ spring: jackson: time-zone: GMT+8 +# HTTP连接池配置 +http: + pool: + max-total: 300 # 连接池最大连接数 + max-per-route: 100 # 每个路由的最大连接数 + validate-after-inactivity: 30000 # 连接验证间隔(毫秒) + timeout: + connect: 15000 # 连接超时(毫秒) + socket: 60000 # 读取超时(毫秒) + request: 10000 # 从连接池获取连接超时(毫秒) + +# RestTemplate连接池配置 +rest-template: + pool: + max-total: 300 # RestTemplate连接池最大连接数 + max-per-route: 100 # 每个路由的最大连接数 + timeout: + connect: 15000 # 连接超时(毫秒) + socket: 60000 # 读取超时(毫秒) + request: 10000 # 从连接池获取连接超时(毫秒) + +# Feign客户端连接池配置 +feign: + httpclient: + enabled: true + max-connections: 400 # Feign最大连接数 + max-connections-per-route: 150 # 每个路由的最大连接数 + connection-timeout: 20000 # 连接超时(毫秒) + connection-timer-repeat: 15000 # 连接请求超时(毫秒) + client: + config: + default: + connectTimeout: 20000 + readTimeout: 90000 + # Redis配置已移至对应的环境配置文件 redis: database: 0