
我与DeepSeek读《大型网站技术架构》(4)
本章聚焦于如何通过系统性优化实现网站快速响应,从多层级、多维度剖析了高性能架构的核心策略。
瞬时响应:网站的高性能架构
章节要点
本章聚焦于如何通过系统性优化实现网站快速响应,从多层级、多维度剖析了高性能架构的核心策略。
1. 高性能架构的优化层级
(1) 前端性能优化
- 目标:减少用户端请求延迟,提升页面加载速度。
- 核心策略:
- 减少 HTTP 请求:合并 CSS/JS 文件、使用 CSS Sprites(雪碧图)。
- 压缩资源:Gzip 压缩文本文件(HTML/CSS/JS)、图片优化(WebP 格式)。
- CDN 加速:静态资源(图片、视频)分发至边缘节点,缩短物理距离。
- 浏览器缓存:通过
Cache-Control
和ETag
控制缓存策略。
- 典型案例:
- 淘宝首页通过 CDN 和资源合并将加载时间从 2s 缩短至 500ms。
(2) 服务端性能优化
- 目标:降低服务处理耗时,提升吞吐量。
- 核心策略:
- 缓存技术:
- 本地缓存(如 Caffeine)减少远程调用。
- 分布式缓存(如 Redis)缓存数据库查询结果。
- 异步处理:
- 消息队列(如 Kafka)解耦耗时操作(如日志写入、邮件发送)。
- 异步线程池处理非实时任务。
- 负载均衡:
- 硬件负载(F5)或软件负载(Nginx、LVS)分流请求。
- 算法选择:轮询、加权轮询、一致性哈希(保障会话粘性)。
- 代码优化:
- 多线程: 启动线程数=[任务执行时间/(任务执行时间-IO等待时间)] * CPU核数。
- 解决线程安全:将对象设计为无状态对象;使用局部对象;并发访问资源时使用锁 。
- 资源复用:单例模式+对象池(数据库连接池、线程池)。
- 数据结构:如HashCode 越随机散列,Hash表的冲突就越少,读写性能就越高。
- 垃圾回收:合理设置年轻代和老年代大小,尽量减少full gc次数。
- 缓存技术:
- 典型案例:
- 秒杀场景通过 Redis 预减库存 + 消息队列异步生成订单。
(3) 数据库性能优化
- 目标:降低数据库查询延迟,避免成为性能瓶颈。
- 核心策略:
- 读写分离:主库写,从库读(如 MySQL 主从同步 + MyCAT 路由)。
- 分库分表:
- 水平分表(按用户 ID 分片)解决单表数据量过大问题。
- 垂直分库(按业务模块拆分)降低耦合。
- 索引优化:避免全表扫描,联合索引遵循最左前缀原则。
- 冷热分离:历史数据归档至 OLAP 数据库(如 HBase)。
- 典型案例:
- 微信朋友圈通过分库分表支持千亿级数据存储。
(4) 架构设计优化
- 目标:通过分布式架构设计提升整体性能。
- 核心策略:
- 反向代理与动静分离:Nginx 处理静态请求,Tomcat 处理动态请求。
- 分布式计算:MapReduce 或 Spark 并行处理大数据任务。
- 无状态化设计:会话状态存储至 Redis,服务节点可水平扩展。
- 典型案例:
- 12306 使用内存计算(如 Redis + 分布式锁)解决高并发余票查询。
2. 高性能的关键技术实践
(1) 缓存设计
- 多级缓存:浏览器缓存 → CDN → 反向代理缓存 → 应用本地缓存 → 分布式缓存。
- 缓存策略:
- 缓存穿透:空值缓存 + 布隆过滤器(如 RedisBloom)。
- 缓存雪崩:随机过期时间 + 热点数据永不过期。
- 缓存一致性:延时双删策略(先删缓存→更新数据库→延时再删缓存)。
(2) 异步化设计
- 场景:高并发写入(如微博发帖)通过消息队列异步同步至从库或搜索引擎。
- 技术选型:
- Kafka:高吞吐量,适合日志、监控数据流。
- RocketMQ:事务消息,适合金融场景。
(3) 数据库扩展
- 读写分离:基于 Binlog 同步(如 MySQL Replication)。
- 分库分表工具:
- 客户端分片(Sharding-JDBC)。
- 代理层分片(MyCAT、ProxySQL)。
3. 性能监控与调优
(1) 性能指标监控
- 核心指标:
- 前端:页面加载时间、首屏时间。
- 服务端:QPS、RT、错误率、线程池状态。
- 数据库:慢查询率、连接数、锁等待时间。
- 工具链:
- 前端:Google Lighthouse、WebPageTest。
- 服务端:Prometheus + Grafana、SkyWalking。
- 数据库:Percona Monitoring、慢查询日志分析。
(2) 压测与调优
- 压力测试工具:JMeter、wrk、LoadRunner。
- 调优步骤:
- 基准测试:确定系统瓶颈(如 CPU、IO、网络)。
- 针对性优化:例如调整 JVM 内存参数、优化 SQL 执行计划。
- 全链路压测:模拟真实流量验证优化效果。
4. 总结
高性能架构需通过前端→服务端→数据库的全链路优化,结合缓存、异步、分片等核心技术,将系统瓶颈逐层击破。关键设计原则包括:
- 减少等待:通过异步化缩短请求链路。
- 并行处理:利用分布式计算提升吞吐量。
- 空间换时间:缓存高频数据减少重复计算。
- 持续监控:通过数据驱动性能迭代优化。
LRU 算法
LRU(Least Recently Used)算法是一种常用的缓存淘汰策略,其核心思想是“淘汰最久未被访问的数据”。当缓存空间不足时,优先移除最近最少被使用的数据。
LRU 算法核心逻辑
- 访问顺序管理:每次访问数据(读/写)时,将该数据标记为“最新使用”。
- 淘汰机制:当缓存满时,删除最久未被访问的数据。
实现方式
通常使用 双向链表 + 哈希表 实现:
- 双向链表:维护数据的访问顺序(链表头部是最久未访问,尾部是最近访问)。
- 哈希表:快速定位数据在链表中的位置,实现 O(1) 时间复杂度访问。
Python 代码示例
class ListNode:
def __init__(self, key=None, value=None):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.hashmap = {} # 哈希表存储 key -> ListNode 映射
# 初始化双向链表头尾哨兵节点
self.head = ListNode()
self.tail = ListNode()
self.head.next = self.tail
self.tail.prev = self.head
def _remove_node(self, node: ListNode):
"""从链表中删除指定节点"""
node.prev.next = node.next
node.next.prev = node.prev
def _add_to_tail(self, node: ListNode):
"""将节点添加到链表尾部(标记为最近使用)"""
node.prev = self.tail.prev
node.next = self.tail
self.tail.prev.next = node
self.tail.prev = node
def get(self, key: int) -> int:
"""获取缓存数据"""
if key not in self.hashmap:
return -1
node = self.hashmap[key]
self._remove_node(node) # 从原位置删除
self._add_to_tail(node) # 移动到尾部(最新)
return node.value
def put(self, key: int, value: int) -> None:
"""插入/更新缓存数据"""
if key in self.hashmap:
node = self.hashmap[key]
node.value = value # 更新值
self._remove_node(node)
self._add_to_tail(node)
else:
if len(self.hashmap) >= self.capacity:
# 删除链表头部节点(最久未使用)
lru_node = self.head.next
self._remove_node(lru_node)
del self.hashmap[lru_node.key]
# 创建新节点并添加到尾部
new_node = ListNode(key, value)
self.hashmap[key] = new_node
self._add_to_tail(new_node)
# 测试示例
cache = LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
print(cache.get(1)) # 输出 1(此时 key=1 变为最近使用)
cache.put(3, 3) # 容量已满,淘汰 key=2
print(cache.get(2)) # 输出 -1(已被淘汰)
代码解析
- 双向链表操作:
_remove_node
:删除节点。_add_to_tail
:将节点插入链表尾部(表示最近使用)。
- 核心方法:
get
:若 key 存在,将节点移动到链表尾部。put
:若 key 存在则更新并移动;否则新增节点,若容量满则删除头节点。
LRU 应用场景
- 数据库查询缓存(如 MySQL 的 Buffer Pool)。
- 浏览器缓存最近访问的页面。
- 操作系统的页面置换算法。
分布式缓存 Memcached
Memcached 是一个高性能的分布式内存缓存系统,主要用于缓存数据库查询结果、API响应等数据,以减轻后端数据库压力,提升应用响应速度。以下是它的核心功能、用法以及与 Redis 的对比:
Memcached 核心功能
- 分布式内存缓存:
- 数据分片:通过客户端哈希算法(如一致性哈希)将数据分布到多个节点,支持横向扩展。
- 无主从设计:节点间完全独立,无数据同步逻辑,依赖客户端路由。
- 简单键值存储:
- 仅支持字符串(String)类型,通过 Key-Value 形式存储数据。
- 支持设置过期时间(TTL),自动清理过期数据。
- 高性能:
- 基于内存操作,单节点吞吐量可达数十万 QPS。
- 多线程架构,充分利用多核 CPU。
- 轻量级协议:
- 使用文本协议(也支持二进制协议),简单高效,适合快速读写场景。
Memcached 典型用法
1. 安装与启动
- 安装:
# Ubuntu apt-get install memcached # CentOS yum install memcached
- 启动服务(默认端口 11211):
memcached -d -m 1024 -p 11211 -u nobody -l 127.0.0.1
2. 客户端操作示例(Python)
import memcache
# 连接 Memcached 集群(多个节点逗号分隔)
mc = memcache.Client(['127.0.0.1:11211', '127.0.0.1:11212'], debug=0)
# 写入数据(过期时间 60 秒)
mc.set("user:1001", {"name": "Alice", "age": 30}, time=60)
# 读取数据
data = mc.get("user:1001")
print(data) # 输出:{'name': 'Alice', 'age': 30}
# 删除数据
mc.delete("user:1001")
3. 适用场景
- 数据库查询缓存:缓存高频 SQL 查询结果。
- 会话存储:存储用户 Session 实现无状态服务。
- HTML 片段缓存:缓存动态页面的渲染结果。
Memcached vs Redis
特性 | Memcached | Redis |
---|---|---|
数据结构 | 仅支持字符串(String) | 支持字符串、哈希、列表、集合、有序集合等 |
持久化 | 不支持,重启数据丢失 | 支持 RDB 快照和 AOF 日志 |
分布式实现 | 客户端分片(如一致性哈希) | 服务端分片(Redis Cluster) |
线程模型 | 多线程(高并发场景性能更优) | 单线程(避免锁竞争,原子性更强) |
内存管理 | 预分配内存池,碎片少 | 动态分配,支持内存淘汰策略 |
适用场景 | 简单键值缓存(高并发读) | 缓存、消息队列、实时排行榜等复杂场景 |
选型建议
- 选 Memcached:
- 需要简单键值缓存且无需复杂操作。
- 对内存利用率要求高(如缓存大量小对象)。
- 多线程高并发场景优先(如纯读密集型应用)。
- 选 Redis:
- 需要持久化或复杂数据结构(如哈希、有序集合)。
- 需要事务、发布订阅、Lua 脚本等高级功能。
- 对数据一致性和高可用性要求较高(如金融场景)。
总结
Memcached 是轻量级缓存解决方案,适合简单键值缓存和高并发读场景;Redis 功能更全面,适合需要持久化和复杂操作的场景。实际选型需结合业务需求(数据结构、持久化、性能)综合评估。
Redis 的分布式存储方案和集群分片方式是实现高可用性和水平扩展的核心技术,以下从 分布式方案 和 分片方式 两个维度详细说明:
Redis 分布式存储常见方案
1. 主从复制(Replication)
- 核心原理:主节点(Master)负责写操作,从节点(Slave)异步复制主节点数据,提供读服务。
- 特点:
- 读写分离:通过多个从节点分担读请求压力。
- 数据冗余:主从数据异步同步,主节点故障时需手动切换。
- 缺点:无法自动故障转移,写操作仍受限于单主节点性能。
- 适用场景:中小规模系统,需要读写分离但无需自动容灾。
2. 哨兵模式(Sentinel)
- 核心原理:通过哨兵节点监控主从集群,实现自动故障检测与主从切换。
- 特点:
- 高可用:主节点故障时,哨兵投票选举新主节点。
- 客户端透明:客户端通过哨兵获取最新主节点地址。
- 缺点:数据分片需客户端或代理层实现,无法横向扩展写性能。
- 适用场景:对高可用性要求高,但数据量未达到分片需求的场景。
3. Redis Cluster(官方集群方案)
- 核心原理:数据分片存储在多个主节点,每个主节点有从节点备份,支持自动分片和故障转移。
- 特点:
- 去中心化:节点间通过 Gossip 协议通信,无中心协调者。
- 高扩展性:支持动态增删节点,数据自动迁移。
- 高可用:主节点故障时,从节点自动晋升为主节点。
- 缺点:跨槽事务支持有限(需
MULTI
+EXEC
在同一节点执行)。 - 适用场景:大规模数据场景,需同时满足高可用和水平扩展。
Redis 集群分片方式
一、数据分布策略
1. 哈希槽分片(Redis Cluster 采用)
- 核心原理:
- 将整个 Key 空间划分为 16384 个哈希槽,每个节点负责部分槽。
- 计算 Key 的哈希槽:
CRC16(key) 16384
。
- 优点:
- 动态扩缩容:槽迁移时仅影响部分数据,无需全局重哈希。
- 负载均衡:槽均匀分布,避免数据倾斜。
- 示例:
# 查看 Key 所属槽位 redis-cli -c CLUSTER KEYSLOT "user:1001" # 手动迁移槽(将槽 1000 从节点 A 迁移到节点 B) redis-cli --cluster reshard <host:port> --cluster-from <node-A-id> --cluster-to <node-B-id> --cluster-slots 1000
2. 一致性哈希分片
- 核心原理:
- 将节点和 Key 映射到哈希环,Key 按顺时针找到最近的节点。
- 通过虚拟节点(Virtual Nodes)解决数据分布不均问题。
- 优点:
- 扩容影响小:仅迁移相邻节点间的数据。
- 缺点:Redis Cluster 未采用此方案,需客户端或代理层实现。
- 适用场景:Memcached 或自定义分片场景。
3. 范围分片(Range Sharding)
- 核心原理:按 Key 的范围划分数据(如
user:0001~user:1000
归节点 A)。 - 优点:适合范围查询(如
KEYS user:1*
)。 - 缺点:易导致数据倾斜,扩容需手动调整范围。
- 适用场景:Key 有明显范围特征且查询模式固定的场景。
4. 预分片(Pre-sharding)
- 核心原理:提前创建大量虚拟节点(如 1024 个),每个物理节点托管多个虚拟节点。
- 优点:扩容时仅需迁移部分虚拟节点,减少数据迁移量。
- 缺点:管理复杂度高,需工具支持。
- 适用场景:超大规模集群的平滑扩容。
二、集群切面方式
1. 客户端分片(Client Sharding)
- 核心原理:客户端通过一致性哈希或固定规则(如取模)将数据分布到多个独立 Redis 实例。
- 特点:
- 无中心化依赖:客户端直接管理分片逻辑。
- 灵活性高:可自定义分片策略。
- 缺点:扩容需手动迁移数据,客户端逻辑复杂。
- 适用场景:已有分片框架(如 Sharding-JDBC)或简单分片需求。
2. 代理分片(Proxy-based Sharding)
- 核心原理:通过代理层(如 Twemproxy、Codis)统一接收请求并路由到后端 Redis 节点。
- 特点:
- 客户端透明:客户端无需感知分片逻辑。
- 集中管理:代理层统一处理路由、负载均衡。
- 缺点:代理层可能成为性能瓶颈,Codis 依赖 ZooKeeper 管理元数据。
- 适用场景:需简化客户端逻辑的中大规模集群。
Redis 的主从模式(Replication)和哨兵模式(Sentinel)本身并不提供数据分片(Sharding)功能,它们的设计目标主要是实现数据冗余、读写分离和高可用性。如果要实现数据分片(即分布式存储),需要结合其他分片策略或工具。以下是详细说明:
Redis 哨兵/主从模式
一、主从模式与哨兵模式的核心定位
-
主从模式:
- 功能:主节点(Master)负责写入,从节点(Slave)异步复制主节点数据,提供读服务。
- 数据分布:所有节点(主和从)存储全量数据,无分片逻辑。
- 适用场景:读多写少,需要读写分离但数据量未超出单节点内存容量。
-
哨兵模式:
- 功能:监控主从集群,自动实现故障转移(主节点宕机时选举新主)。
- 数据分布:与主从模式一致,所有节点存储全量数据,无分片。
- 适用场景:在读写分离基础上,增强高可用性。
二、主从/哨兵模式与分片的关系
- 无分片策略:主从和哨兵模式中,所有节点保存相同的数据副本,无法横向扩展数据容量。
- 容量瓶颈:数据量受限于单节点内存大小,无法通过主从或哨兵解决大数据存储问题。
- 分片需求:若数据量超过单节点内存,需额外引入分片策略(如 Redis Cluster、客户端分片等)。
三、如何为主从/哨兵模式添加分片?
如果需要在主从或哨兵架构下实现分片,需借助外部方案,常见方法如下:
1. 客户端分片(Client Sharding)
- 原理:客户端通过哈希算法(如一致性哈希、取模)将数据分布到多个独立的主从集群。
- 示例:
# 客户端代码示例(伪代码) shard_nodes = [ {"master": "redis-node1:6379", "slaves": ["redis-node1-slave:6379"]}, {"master": "redis-node2:6379", "slaves": ["redis-node2-slave:6379"]}, ] def get_shard(key): hash_value = crc32(key) % len(shard_nodes) return shard_nodes[hash_value] # 写入数据时选择对应的主节点 shard = get_shard("user:1001") redis_client = Redis(shard["master"]) redis_client.set("user:1001", "Alice")
- 优点:灵活可控,无需额外组件。
- 缺点:
- 扩容时需手动迁移数据。
- 客户端逻辑复杂,需处理分片路由、故障转移。
2. 代理分片(Proxy-based Sharding)
- 原理:通过代理层(如 Twemproxy、Codis)统一接收请求,路由到后端多个主从集群。
- 架构示例:
客户端 → Twemproxy → [主从集群1, 主从集群2, ...] ↓ Sentinel 监控每个主从集群
- 优点:客户端无感知,分片逻辑由代理统一管理。
- 缺点:
- 代理层可能成为性能瓶颈。
- Codis 依赖 ZooKeeper 管理元数据,架构复杂度高。
四、主从/哨兵模式 vs Redis Cluster
若需同时实现分片和高可用,应直接使用 Redis Cluster(官方集群方案):
- 分片方式:采用哈希槽分片(16384 个槽),支持自动数据迁移。
- 高可用:每个分片(槽组)包含主从节点,主故障时从节点自动晋升。
- 对比优势:
- 无需客户端或代理分片,由集群自动管理。
- 支持动态扩缩容,数据迁移对业务透明。
五、总结
- 主从/哨兵模式无分片:仅用于数据冗余和读写分离,无法解决单节点数据容量瓶颈。
- 分片需额外方案:
- 客户端分片:适合简单场景,需自定义路由逻辑。
- 代理分片:适合希望客户端无感知的场景,但需维护代理层。
- Redis Cluster:官方推荐方案,集成分片与高可用,适合大规模场景。
建议:若数据量超出单节点内存,直接使用 Redis Cluster,避免在主从/哨兵模式上自行实现分片。
更多推荐
所有评论(0)