凌晨两点,生产环境突然报警:数据库连接池耗尽。
我打开监控,看到几千个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?
- 防止旧连接的数据包:确保迟到的数据包不会影响新连接
- 保证可靠关闭:让被动关闭方收到最后的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的响应先返回
解决方案:
-
域名分片:把资源分散到多个域名
<!-- 浏览器对同一域名有并发限制(通常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">
-
升级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请求慢,不知道时间花在哪了
步骤:
- 抓包:
tcpdump -i any port 8080 -w http.pcap
- 用Wireshark打开,过滤:
http
- 分析时间线:
- 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 必须掌握的技能
- 会抓包:tcpdump/Wireshark是必备技能
- 懂TCP状态:知道TIME_WAIT、CLOSE_WAIT的意思
- 会看监控:能看懂连接数、延迟、丢包率
- 懂基本调优:知道怎么调内核参数、连接池参数
7.2 避坑指南
- 不要小看DNS:DNS问题能让你debug一整天
- 连接池不是越大越好:要考虑下游服务的承受能力
- 超时设置要合理:太短了误杀,太长了拖垮系统
- 监控要分层:从应用层到网络层都要监控
7.3 学习路径
第一阶段:会用
- ping/telnet/nslookup
- netstat/ss
- 基本的tcpdump
第二阶段:会调
- TCP内核参数
- 连接池配置
- 超时重试策略
第三阶段:会防
- 网络拓扑设计
- 容灾方案
- 全链路监控
网络问题就像冰山——你看到的表面问题,下面往往藏着更深层的原因。
会写代码只是开始,懂网络才是进阶。
如果你对更多系统设计和高可用性架构的实践感兴趣,可以到云栈社区与其他开发者交流探讨。