找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2385

积分

0

好友

341

主题
发表于 前天 01:30 | 查看: 7| 回复: 0

凌晨两点,生产环境突然报警:数据库连接池耗尽
我打开监控,看到几千个TCP连接卡在 TIME_WAIT 状态,新请求全部被拒绝。
“不就是个数据库连接吗?”我一边重启应用一边嘀咕。

三小时后,连接池再次耗尽。这次我学乖了,抓了个包。

tcpdump显示: 每个查询都建立了新连接,用完就扔,操作系统端口很快被耗尽。

今天,我不讲Spring Boot,不讲微服务,就讲讲网络层那些事——这些学校不教、文档不说、只有踩过坑才懂的知识。

一、TCP/IP:你以为懂了,其实不懂

1.1 三次握手:不是礼貌,是必要

错误认知: “TCP握手就是客气一下,说三句话”

现实: 三次握手解决了一个致命问题——历史连接幽灵

// 模拟场景:客户端发送SYN,但网络延迟了
// 旧SYN(序列号=100)还在路上
// 客户端超时重发新SYN(序列号=200)

// 服务器收到旧SYN(序列号=100)
// 如果只有两次握手:
// 服务器:SYN-ACK(确认号=101)
// 客户端看到确认号不对(期待201),直接RST
// 但服务器已经建立了连接,开始等数据...

// 三次握手解决了这个问题:
// 客户端发送SYN(seq=200)
// 服务器回复SYN-ACK(ack=201)
// 客户端再发ACK(ack=服务器的seq+1)
// 这样双方都确认对方活着,且序列号同步

代码中的体现:

// Java Socket默认行为
Socket socket = new Socket();
socket.setTcpNoDelay(true); // 禁用Nagle算法(小数据立即发)
socket.setSoTimeout(5000);   // 读取超时5秒
socket.setKeepAlive(true);   // 启用TCP保活

// 连接时的三次握手是自动的
socket.connect(new InetSocketAddress("db-host", 3306), 3000);

1.2 TIME_WAIT:不是bug,是feature

我踩过的坑:

# 查看TIME_WAIT连接
netstat -nat | grep TIME_WAIT | wc -l
# 输出:28000 (2.8万个!)

# 结果:新连接失败
java.net.BindException: Address already in use

为什么要有TIME_WAIT?

  1. 防止旧连接的数据包:确保迟到的数据包不会影响新连接
  2. 保证可靠关闭:让被动关闭方收到最后的ACK

解决方案:

# Linux内核参数调整(/etc/sysctl.conf)
# 1. 快速回收TIME_WAIT(谨慎使用)
net.ipv4.tcp_tw_reuse = 1      # 允许将TIME-WAIT sockets重新用于新的TCP连接
net.ipv4.tcp_tw_recycle = 0    # 不建议开启,在NAT环境下有问题

# 2. 调整TIME_WAIT超时时间
net.ipv4.tcp_fin_timeout = 30  # 从默认60秒改为30秒

# 3. 增加可用端口范围
net.ipv4.ip_local_port_range = 10000 65000

# 4. 增大连接跟踪表
net.ipv4.tcp_max_tw_buckets = 20000

# 应用配置
sysctl -p

应用层优化:

// 使用连接池,避免短连接
@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        config.setUsername("root");
        config.setPassword("password");

        // 关键配置
        config.setMaximumPoolSize(20);          // 最大连接数
        config.setMinimumIdle(10);              // 最小空闲连接
        config.setConnectionTimeout(30000);     // 连接超时30秒
        config.setIdleTimeout(600000);          // 空闲连接超时10分钟
        config.setMaxLifetime(1800000);         // 连接最大生命周期30分钟
        config.setConnectionTestQuery("SELECT 1");

        return new HikariDataSource(config);
    }
}

二、HTTP层:不只是发请求收响应

2.1 Keep-Alive:连接复用不是魔法

错误做法: 每个请求都新建连接

// ❌ 每次都新建连接(性能杀手)
for (int i = 0; i < 1000; i++) {
    CloseableHttpClient client = HttpClients.createDefault();
    HttpGet request = new HttpGet("http://api.example.com/data");
    CloseableHttpResponse response = client.execute(request);
    // 处理响应
    response.close();
    client.close(); // 连接关闭,进入TIME_WAIT
}

正确做法: 使用连接池

// ✅ 使用连接池
@Bean
public CloseableHttpClient httpClient() {
    PoolingHttpClientConnectionManager connectionManager =
        new PoolingHttpClientConnectionManager();

    // 关键配置
    connectionManager.setMaxTotal(200);          // 最大连接数
    connectionManager.setDefaultMaxPerRoute(50); // 每个路由最大连接数

    // 连接存活时间(避免长连接占用)
    connectionManager.setValidateAfterInactivity(30000); // 30秒后验证

    return HttpClients.custom()
            .setConnectionManager(connectionManager)
            .setDefaultRequestConfig(RequestConfig.custom()
                .setConnectTimeout(5000)    // 连接超时5秒
                .setSocketTimeout(10000)    // 读取超时10秒
                .setConnectionRequestTimeout(2000) // 从池中获取连接超时
                .build())
            // 禁用Expect: 100-continue(某些服务器不支持)
            .disableContentCompression()
            .build();
}

2.2 队头阻塞:HTTP/1.1的先天缺陷

问题: 一个慢请求阻塞后面所有请求

HTTP/1.1管道化(默认关闭):
请求1 → 请求2 → 请求3
响应1 ← 响应2 ← 响应3  # 必须按顺序返回

即使请求2先处理完,也要等请求1的响应先返回

解决方案:

  1. 域名分片:把资源分散到多个域名

    <!-- 浏览器对同一域名有并发限制(通常6个) -->
    <img src="https://static1.example.com/image1.jpg">
    <img src="https://static2.example.com/image2.jpg">
    <img src="https://static3.example.com/image3.jpg">
  2. 升级HTTP/2:多路复用解决队头阻塞

    # Nginx配置HTTP/2
    server {
        listen 443 ssl http2;  # 关键:http2
        server_name api.example.com;
    
        ssl_certificate /path/to/cert.pem;
        ssl_certificate_key /path/to/key.pem;
    
        location / {
            proxy_pass http://backend;
            # HTTP/2会自动多路复用
        }
    }

三、DNS:被忽视的性能杀手

3.1 DNS解析的隐藏成本

一次HTTP请求的时间线:

0ms: 浏览器解析URL
5ms: DNS查询开始
50ms: DNS响应返回(如果缓存未命中)
55ms: TCP三次握手
80ms: TLS握手(HTTPS)
100ms: 发送HTTP请求
120ms: 收到第一个字节

DNS可能占用了近一半的时间!

3.2 Java中的DNS优化

// 1. 设置JVM DNS缓存
java -jar yourapp.jar \
  -Dsun.net.inetaddr.ttl=60 \           # 缓存60秒
  -Dsun.net.inetaddr.negative.ttl=10 \   # 失败缓存10秒
  -Dnetworkaddress.cache.ttl=60 \        # 安全管理器缓存
  -Dnetworkaddress.cache.negative.ttl=10

// 2. 使用连接池避免重复解析
PoolingHttpClientConnectionManager connManager =
    new PoolingHttpClientConnectionManager(
        // 使用自定义的DNS解析器
        new SystemDefaultDnsResolver() {
            @Override
            public InetAddress[] resolve(String host) throws UnknownHostException {
                // 可以在这里加入自定义逻辑
                // 比如:内部域名直接返回IP
                if ("internal-service".equals(host)) {
                    return new InetAddress[]{
                        InetAddress.getByName("10.0.0.1")
                    };
                }
                return super.resolve(host);
            }
        }
    );

// 3. 预解析重要域名
@PostConstruct
public void warmUpDns() {
    CompletableFuture.runAsync(() -> {
        String[] importantHosts = {"api.payment.com", "api.notification.com"};
        for (String host : importantHosts) {
            try {
                InetAddress.getAllByName(host);
                log.info("DNS预热: {} -> {}",
                        Arrays.toString(InetAddress.getAllByName(host)));
            } catch (UnknownHostException e) {
                log.warn("DNS预热失败: {}", host);
            }
        }
    });
}

3.3 /etc/hosts的妙用

# 开发环境加速(/etc/hosts)
10.0.0.1   mysql-primary
10.0.0.2   mysql-replica
10.0.0.3   redis-master
10.0.0.4   elasticsearch
10.0.0.5   kafka-broker1

# 生产环境慎用,但可以:
# 1. 核心服务固定IP,避免DNS故障
# 2. 灾备时快速切换

四、网络调试:从新手到专家

4.1 必备工具集

# 1. tcpdump:网络抓包
tcpdump -i any port 3306 -w mysql.pcap  # 抓取MySQL流量
tcpdump -i any port 8080 -A              # 抓取HTTP流量并显示ASCII

# 2. netstat:连接状态
netstat -nat | awk '{print $6}' | sort | uniq -c  # 统计各种状态数量
netstat -tnp | grep ESTABLISHED | wc -l           # 查看活跃连接数

# 3. ss:netstat的现代替代品
ss -tn sport = :8080  # 查看8080端口的TCP连接
ss -s                 # 查看统计信息

# 4. traceroute/mtr:路由追踪
mtr -r api.example.com  # 持续追踪,显示丢包率

# 5. tc:模拟网络问题(测试用)
tc qdisc add dev eth0 root netem delay 100ms  # 增加100ms延迟
tc qdisc add dev eth0 root netem loss 10%     # 10%丢包率
tc qdisc del dev eth0 root                    # 恢复

4.2 Wireshark实战分析

场景: HTTP请求慢,不知道时间花在哪了

步骤:

  1. 抓包:tcpdump -i any port 8080 -w http.pcap
  2. 用Wireshark打开,过滤:http
  3. 分析时间线:
    • DNS查询耗时?
    • TCP握手耗时?
    • TLS握手耗时?
    • 服务器处理耗时?
    • 网络传输耗时?

常见问题识别:

# TCP重传(网络不稳定)
[TCP Retransmission] Seq=100 Len=100

# 零窗口(接收方处理不过来)
[TCP ZeroWindow] Win=0

# 连接重置(对方强制关闭)
[TCP RST] Flags=R

4.3 Java网络调试技巧

// 1. 开启Socket调试
java -Djava.net.debug=all YourApp  # 输出所有网络调试信息
java -Djava.net.debug=ssl YourApp  # 只输出SSL调试信息

// 2. 自定义Socket工厂(记录所有请求)
public class LoggingSocketFactory extends SocketFactory {

    private final SocketFactory delegate;
    private static final Logger log = LoggerFactory.getLogger(LoggingSocketFactory.class);

    @Override
    public Socket createSocket(String host, int port) throws IOException {
        long start = System.currentTimeMillis();
        Socket socket = delegate.createSocket(host, port);
        long cost = System.currentTimeMillis() - start;

        log.info("Socket连接: {}:{}, 耗时: {}ms", host, port, cost);
        return new LoggingSocket(socket);
    }
}

// 3. 监控连接池状态
@Scheduled(fixedRate = 60000)
public void logConnectionPoolStats() {
    PoolStats stats = dataSource.getHikariPoolMXBean().getPoolStats();

    log.info("连接池状态: 活跃={}, 空闲={}, 等待={}, 总数={}",
             stats.getActiveConnections(),
             stats.getIdleConnections(),
             stats.getThreadsAwaitingConnection(),
             stats.getTotalConnections());

    // 预警:如果等待线程过多,说明连接池不够用
    if (stats.getThreadsAwaitingConnection() > 10) {
        log.warn("连接池等待线程过多,考虑扩容");
    }
}

五、生产环境网络架构

5.1 机房内网优化

# 理想的内网架构
网络分层:
1. 接入层: Nginx/HAProxy (南北流量)
2. 服务层: 微服务集群 (东西流量)
3. 数据层: 数据库/缓存 (专线连接)

优化点:
- 服务间通信: 使用内网域名,避免IP直连
- 数据库连接: 使用连接池,配置合理的超时
- 缓存访问: 使用连接池,配置合理的重试策略
- 消息队列: 使用持久连接,配置心跳检测

5.2 跨机房部署

挑战: 延迟增加,网络不稳定

解决方案:

// 1. 数据库读写分离(写主库,读本地从库)
@Configuration
public class DataSourceRoutingConfig {

    @Bean
    @Primary
    public DataSource dataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource());
        targetDataSources.put("slave", slaveDataSource());

        RoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDefaultTargetDataSource(masterDataSource());

        return routingDataSource;
    }

    // 根据上下文选择数据源
    public static class RoutingDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            // 读操作走从库,写操作走主库
            boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
            return isReadOnly ? "slave" : "master";
        }
    }
}

// 2. 缓存多级架构(本地缓存 + 中心缓存)
@Component
public class MultiLevelCache {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private final Cache<String, Object> localCache = Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build();

    public Object get(String key) {
        // 先查本地缓存
        Object value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }

        // 再查Redis(跨机房)
        value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            localCache.put(key, value);
        }

        return value;
    }
}

// 3. 服务调用超时优化(跨机房增加超时时间)
@Configuration
public class FeignConfig {

    @Bean
    public Request.Options feignOptions() {
        // 同机房:连接1秒,读取3秒
        // 跨机房:连接3秒,读取10秒
        boolean crossIdc = isCrossIdcCall();

        int connectTimeout = crossIdc ? 3000 : 1000;
        int readTimeout = crossIdc ? 10000 : 3000;

        return new Request.Options(connectTimeout, TimeUnit.MILLISECONDS,
                                  readTimeout, TimeUnit.MILLISECONDS,
                                  true);
    }
}

5.3 网络质量监控

@Component
@Slf4j
public class NetworkQualityMonitor {

    @Scheduled(fixedRate = 300000) // 每5分钟
    public void monitorNetworkQuality() {
        // 1. Ping关键节点
        Map<String, Long> pingResults = pingCriticalHosts();

        // 2. 测试TCP连接时间
        Map<String, Long> tcpResults = testTcpConnection();

        // 3. 测试HTTP接口
        Map<String, Long> httpResults = testHttpApis();

        // 4. 记录到监控系统
        pingResults.forEach((host, latency) -> {
            Metrics.recordGauge("network.ping.latency",
                               Tags.of("host", host),
                               latency);
        });

        // 5. 告警:延迟过高或丢包
        pingResults.entrySet().stream()
            .filter(entry -> entry.getValue() > 100) // 超过100ms
            .forEach(entry -> {
                alertService.sendAlert("NETWORK_LATENCY_HIGH",
                    Map.of("host", entry.getKey(),
                           "latency", entry.getValue().toString()));
            });
    }

    private Map<String, Long> pingCriticalHosts() {
        List<String> hosts = Arrays.asList(
            "database-primary",
            "redis-master",
            "kafka-broker1",
            "gateway-service"
        );

        Map<String, Long> results = new HashMap<>();
        for (String host : hosts) {
            try {
                long start = System.currentTimeMillis();
                boolean reachable = InetAddress.getByName(host).isReachable(3000);
                long cost = System.currentTimeMillis() - start;

                if (reachable) {
                    results.put(host, cost);
                } else {
                    results.put(host, -1L); // 不可达
                }
            } catch (Exception e) {
                log.error("Ping测试失败: {}", host, e);
            }
        }

        return results;
    }
}

六、云原生时代的网络

6.1 Kubernetes网络模型

# Service的几种类型
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order
  ports:
  - protocol: TCP
    port: 80      # Service端口
    targetPort: 8080  # Pod端口

  # ClusterIP: 集群内访问(默认)
  type: ClusterIP

  # NodePort: 节点端口访问
  # type: NodePort
  # nodePort: 30080

  # LoadBalancer: 云厂商负载均衡器
  # type: LoadBalancer

  # ExternalName: DNS别名
  # type: ExternalName
  # externalName: api.example.com

6.2 Service Mesh(Istio)的网络治理

# Istio VirtualService:细粒度流量控制
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: order-service
spec:
  hosts:
  - order-service
  http:
  - route:
    - destination:
        host: order-service
        subset: v1
      weight: 90  # 90%流量到v1
    - destination:
        host: order-service
        subset: v2
      weight: 10  # 10%流量到v2

  # 超时、重试、熔断
  timeout: 2s
  retries:
    attempts: 3
    perTryTimeout: 1s
  fault:
    delay:
      percentage:
        value: 10.0  # 10%的请求延迟1秒
      fixedDelay: 1s

6.3 eBPF:下一代网络观测

# 使用bpftrace追踪网络事件
# 追踪TCP连接建立
bpftrace -e 'tracepoint:tcp:tcp_connect {
    printf("TCP连接: %s:%d -> %s:%d\n",
           ntop(args->saddr), args->sport,
           ntop(args->daddr), args->dport);
}'

# 追踪TCP重传
bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb {
    printf("TCP重传: %s:%d -> %s:%d, 序列号: %d\n",
           ntop(args->saddr), args->sport,
           ntop(args->daddr), args->dport,
           args->seq);
}'

七、最后的忠告

网络知识不是“遇到问题再学”,而是必须掌握的基础。五年来,我总结了几条血泪经验:

7.1 必须掌握的技能

  1. 会抓包:tcpdump/Wireshark是必备技能
  2. 懂TCP状态:知道TIME_WAIT、CLOSE_WAIT的意思
  3. 会看监控:能看懂连接数、延迟、丢包率
  4. 懂基本调优:知道怎么调内核参数、连接池参数

7.2 避坑指南

  1. 不要小看DNS:DNS问题能让你debug一整天
  2. 连接池不是越大越好:要考虑下游服务的承受能力
  3. 超时设置要合理:太短了误杀,太长了拖垮系统
  4. 监控要分层:从应用层到网络层都要监控

7.3 学习路径

第一阶段:会用
  - ping/telnet/nslookup
  - netstat/ss
  - 基本的tcpdump

第二阶段:会调
  - TCP内核参数
  - 连接池配置
  - 超时重试策略

第三阶段:会防
  - 网络拓扑设计
  - 容灾方案
  - 全链路监控

网络问题就像冰山——你看到的表面问题,下面往往藏着更深层的原因。
会写代码只是开始,懂网络才是进阶。

如果你对更多系统设计和高可用性架构的实践感兴趣,可以到云栈社区与其他开发者交流探讨。




上一篇:Apache SIS CVE-2025-68280 XXE漏洞分析:影响版本与修复指南
下一篇:苹果App Store付费榜现「死了么」应用:8元“健康打卡”工具登顶引热议
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-14 14:15 , Processed in 0.215764 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表