第一章:高并发系统设计的核心原则
1.1 系统瓶颈定位方法论
1.1.1 响应时间金字塔模型
响应时间金字塔模型是一种用于剖析系统性能瓶颈的层次化方法,它将系统响应时间按照从高到低的层级进行划分,具体如下:
- 应用层:这一层级主要聚焦于代码层面的性能问题。可以借助
JProfiler
或者Arthas
等工具来找出代码中的热点方法。 - 服务层:该层级关注服务之间的调用关系,特别是远程调用所带来的延迟情况。
- 中间件层:中间件层涉及缓存、消息队列等组件的性能表现。例如,需要检查 Redis 的
slowlog
,以此来确定是否存在慢查询。 - 数据库层:数据库层的性能问题主要通过分析 SQL 执行计划来发现。可以使用
EXPLAIN ANALYZE
命令来优化查询。 - 基础设施层:基础设施层包括 CPU、内存、磁盘 I/O 以及网络等方面的性能瓶颈。可以使用
vmstat
、iostat
等工具进行监控。
下面通过一个实际案例来展示如何运用该模型: 假设一个接口的响应时间为 200ms,通过逐层分析发现:
- 应用层代码的执行时间为 30ms。
- 数据库查询的时间为 150ms,其中索引缺失导致全表扫描。
- 网络传输的时间为 20ms。
1.1.2 火焰图分析实战(附 perf 工具演示)
1. 安装 perf 工具
在 Ubuntu 系统中,可以使用以下命令安装 perf 工具:
`sudo apt-get install linux-tools-generic`
复制代码
2. 生成火焰图
执行以下命令生成火焰图:
sudo perf record -g -p <PID> -- sleep 10
sudo perf script | flamegraph.pl > flamegraph.svg
复制代码
3. 分析火焰图
火焰图的可视化规则如下:
- X 轴表示函数调用栈,每个方块代表一个函数。
- Y 轴表示调用深度。
- 方块的宽度表示该函数占用的 CPU 时间。 当发现某个函数的方块特别宽时,就需要对其进行优化。例如,如果
java.lang.String.replace
函数占用了大量时间,就可以考虑使用StringBuilder
来替代。
1.1.3 分布式追踪系统选型对比(Zipkin vs Jaeger)
对比项 | Zipkin | Jaeger |
---|---|---|
存储 | 支持 MySQL、Cassandra 等多种存储方式 | 采用自研的存储方案,支持 Elasticsearch |
采样率 | 支持动态采样 | 支持概率采样和基于速率的采样 |
UI 功能 | 提供基础的追踪查询功能 | 支持服务依赖分析和性能指标可视化 |
生态集成 | 与 Spring Cloud Sleuth 集成较为方便 | 与 OpenTelemetry 集成紧密 |
适用场景 | 适合中小规模的微服务架构 | 适用于大规模的分布式系统 |
选型建议:
- 如果是中小规模的系统,并且需要快速集成,那么可以选择 Zipkin。
- 如果是大规模的分布式系统,需要进行深度分析,建议选择 Jaeger。
1.2 水平扩展的 3 种常见模式
1.2.1 无状态服务扩展(Tomcat 集群配置示例)
1. 环境准备
- 安装两台 Tomcat 服务器,端口分别设置为 8080 和 8081。
- 配置 Nginx 作为负载均衡器,其配置文件如下:
upstream tomcat_cluster {
server 192.168.1.100:8080;
server 192.168.1.101:8081;
}
server {
listen 80;
location / {
proxy_pass http://tomcat_cluster;
}
}
2. 会话管理
为了实现话共享,可以采用以下两种方式:
- 粘性会话:在 Nginx 中配置
ip_hash
,确保同一客户端的请求始终转发到同一台 Tomcat 服务器。 - Redis 共享会话:在 Tomcat 中集成 Redis,将会话数据存储到 Redis 中。
1.2.2 有状态服务分片(MySQL 分库分表实战)
1. 分片策略
- 范围分片:例如,按照用户 ID 的范围进行分片,如
user_id < 10000
的数据存储到库 A,user_id >= 10000
的数据存储到库 B。 - 哈希分片:通过
user_id % 4
的方式将数据均匀分布到 4 个分片。
2. 实战步骤
- 创建逻辑库:
CREATE DATABASE user_db_0;
CREATE DATABASE user_db_1;
复制代码
- 创建分片表:
CREATE TABLE user_0 (
id BIGINT PRIMARY KEY,
name VARCHAR(100)
) PARTITION BY HASH(id) PARTITIONS 2;
复制代码
- 使用 ShardingSphere 进行路由配置:
dataSources:
ds_0:
url: jdbc:mysql://localhost:3306/user_db_0
ds_1:
url: jdbc:mysql://localhost:3306/user_db_1
rules:
- sharding:
tables:
user:
actualDataNodes: ds_${0..1}.user_${0..1}
databaseStrategy:
inline:
shardingColumn: id
algorithmExpression: ds_${id % 2}
tableStrategy:
inline:
shardingColumn: id
algorithmExpression: user_${id % 2}
复制代码
1.2.3 消息队列削峰填谷(Kafka 分区策略详解)
1. 分区策略
- Round Robin:按照轮询的方式将消息分配到各个分区,这种策略可以保证消息在各个分区之间均匀分布。
- Range:根据消息的键的范围将消息分配到特定的分区,这种策略可能会导致某些分区的数据量较大。
- Custom:用户可以自定义分区策略,例如根据地理位置信息将消息分配到不同的分区。
2. 配置示例
在 Kafka 的生产者配置中,可以设置分区策略:
Properties props = new Properties();
props.put("partitioner.class", "com.example.CustomPartitioner");
复制代码
3. 分区数计算
可以使用以下公式计算合适的分区数:
`分区数 = (预计吞吐量 / 单分区吞吐量) * 冗余因子`
复制代码
其中,单分区吞吐量通常为 1000 - 5000 TPS,冗余因子建议设置为 2 - 3。
注意事项:
- 在进行系统扩展时,要确保各个服务之间的接口是幂等的,以避免重复操作带来的问题。
- 对于有状态服务分片,需要处理跨分片的事务问题,可以采用 Saga 模式或者 TCC 模式来实现分布式事务。
- 在使用消息队列时,要注意消息的顺序性问题,确保关键业务的消息按照正确的顺序进行处理。# 第一章:高并发系统设计的核心原则
回复