分类 我的实验室,记录一些探索经验。 下的文章

bjmayor发布于2025-11-10

IO 多路复用技术演进与原理深度解析

本文档是 IO 多路复用深度剖析 项目的理论部分

项目地址

GitHub: https://github.com/d60-Lab/io-multiplexing-deep-dive

# 克隆项目并运行测试 git clone git@github.com:d60-Lab/io-multiplexing-deep-dive.git cd io-multiplexing-deep-dive ulimit -n 10240 cargo build --release cd scripts && ./benchmark.sh

本文档结合实际代码和测试数据,深入剖析五种 IO 多路复用技术的原理、性能和适用场景。


目录

  1. 问题背景:C10K 问题
  2. 技术演进路线
  3. 五种模型深度对比
  4. 性能瓶颈分析
  5. 实际测试结果
  6. 结论与建议

问题背景:C10K 问题

什么是 C10K 问题?

C10K 指的是服务器如何支持 10,000 个并发连接 (Concurrent 10 Thousand)。在 2000 年左右,这是一个巨大的技术挑战。

为什么会有这个问题?

1. 传统模型的资源限制

每连接一个线程模型

  • 每个线程栈空间:2-8 MB
  • 10,000 个线程 = 20-80 GB 内存(仅栈空间!)
  • 线程上下文切换开销:O(n)
  • 系统调度开销巨大

2. 具体瓶颈

资源消耗示例:
- 1 个连接 = 1 个线程
- 1 个线程 = 2 MB 栈 + 内核数据结构
- 10,000 连接 = 20 GB + 大量上下文切换

3. 为什么不能简单增加线程?

  • 内存墙:物理内存有限
  • 调度墙:CPU 时间片轮转,上下文切换成本高
  • 锁竞争:大量线程导致锁争用

技术演进路线

时间线 | 技术      | 复杂度 | 并发能力  | 备注
-------|----------|--------|----------|------------------
1980s  | Blocking | O(1)   | < 100    | 每连接一个线程
1990s  | select   | O(n)   | < 1024   | FD_SETSIZE 限制
1997   | poll     | O(n)   | > 1024   | 突破 fd 限制
1999   | epoll    | O(1)   | > 100K   | Linux 2.6+
2000   | kqueue   | O(1)   | > 100K   | FreeBSD/macOS
2019   | io_uring | O(1)   | > 1M     | Linux 5.1+
2020s  | async    | O(1)   | > 1M     | 协程 + 事件循环

阅读全文»

bjmayor发布于2025-11-10

从理论到实践:用代码验证李智慧的短URL系统设计

作者:bjmayor
时间:2025年11月
目标:用简化的本地实现来验证百亿级短URL系统的核心技术点

引言

李智慧老师在《高并发架构实战课》中设计了一个名为Fuxi的短URL系统,目标是处理144亿条短URL4万QPS并发、12TB存储。这是一个典型的高并发+海量数据场景。

但作为学习者,我们遇到几个问题:

  1. 无法复现环境:家用电脑没有几百G内存、几十T硬盘
  2. 概念难以理解:HDFS、HBase、Redis集群配置复杂
  3. 缺少直观验证:为什么要预生成?缓存策略真的有效吗?

本文的目标:用简化的本地实现,通过代码和测试数据,验证李智慧设计中的核心技术点。


一、为什么不用Hash?不用自增?

问题分析

生成短URL有三种常见方案:

方案 实现方式 优点 缺点
Hash截断 MD5/SHA256 → Base64 → 截断前6位 实现简单 冲突率高
自增序列 数字自增 → Base64编码 无冲突,最快 可预测 ⚠️
随机预生成 随机生成 → 布隆过滤器去重 无冲突,不可预测 需要预处理

代码验证

我实现了三种算法并进行对比测试:

// 方法1:Hash截断 func GenerateWithHash(input string) string { hash := simpleHash(input) result := make([]byte, 6) for i := 0; i < 6; i++ { result[i] = charset[hash%64] hash = hash / 64 } return string(result) } // 方法2:自增序列 func GenerateWithSequence(seq int64) string { result := make([]byte, 6) for i := 0; i < 6; i++ { result[5-i] = charset[seq%64] seq = seq / 64 } return string(result) } // 方法3:随机生成(带布隆过滤器) func (g *Generator) Generate(count int) ([]string, error) { urls := make([]string, 0, count) for generated < count { code := g.generateRandom() if !g.bloomFilter.Contains(code) { g.bloomFilter.Add(code) urls = append(urls, code) generated++ } } return urls, nil }

阅读全文»

bjmayor发布于2025-11-07

分库分表最佳实践

引言

随着业务的快速增长,单一数据库面临的性能瓶颈和存储限制问题日益突出。分库分表作为一种经典的数据库扩展方案,已经成为大规模互联网应用的标准实践。本文将从实际场景出发,探讨分库分表的演进过程、面临的挑战以及应对方案。

⚠️ 核心认知:分库分表的本质

分库分表不是性能优化的银弹,而是用"系统复杂度"换"单机性能突破"的妥协方案。

分库分表解决了什么?

解决单机硬件瓶颈

  • CPU 瓶颈:单机 CPU 核心数有限,分库后多机并行处理
  • 内存瓶颈:单机内存有限,分库后每个实例的工作集更小
  • 磁盘瓶颈:单机 IOPS 有限,分库后 IO 分散到多个磁盘
  • 网络瓶颈:单机网卡带宽有限(如 10Gbps),分库后总带宽提升

解决单表数据量问题

  • 索引深度降低(B+Tree 层级减少,查询更快)
  • 单表数据量小,全表扫描更快
  • 备份恢复时间缩短

分库分表带来了什么问题?

性能不一定提升,某些场景反而降低

场景 单库性能 分库性能 说明
按主键查询 1ms 1-2ms 分库增加路由开销
带分片键的查询 5ms 3-5ms 只查一个分片,性能相当
不带分片键的查询 10ms 80ms 需要查 8 个分片并合并
跨表 JOIN 5ms 禁止 无法跨库 JOIN
分布式事务 1ms 50-500ms 2PC/TCC 性能极差
分页查询(全局) 10ms 80ms+ 需要从每个分片取数据合并

网络开销示例

单库查询:
应用 -> MySQL (1次网络往返, ~1ms)

分库查询(不带分片键):
应用 -> 8个 MySQL (8次网络往返并发, ~2-5ms)
应用层聚合 (CPU 排序、去重, ~5ms)
总耗时: ~10ms (比单库慢 10 倍)

维护成本指数级增长

运维复杂度

单库:
- 监控: 1 个实例
- 备份: 1 个库,1TB 数据,1 小时
- 恢复: 1 个库,1 小时
- 扩容: 垂直扩容(加 CPU/内存)
- 故障处理: 切换 1 个主从

分库分表 (8 库):
- 监控: 8 个实例 × 64 张表 = 512 个对象
- 备份: 8 个库,8TB 数据,需要 8 小时(串行)或复杂的并行方案
- 恢复: 需要恢复 8 个库,协调一致性
- 扩容: 数据迁移(75% 数据需要移动,停服或双写)
- 故障处理: 可能需要切换多个实例,影响面更大

开发复杂度

// 单库: 简单直接 order := db.Query("SELECT * FROM orders WHERE id = ?", orderID) // 分库: 需要路由逻辑 shard := shardRouter.GetShard(orderID) // 路由 order := shard.Query("SELECT * FROM orders_" + getTableSuffix(orderID) + " WHERE id = ?", orderID) // 单库: 支持事务 db.Transaction(func(tx) { tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE user_id = ?", 1) tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE user_id = ?", 2) }) // 分库: 需要分布式事务或最终一致性方案(代码量 10 倍) saga := NewSaga() saga.AddStep(step1, compensate1) saga.AddStep(step2, compensate2) saga.Execute() // 100+ 行代码

灵活性大幅降低

  • 不能随意加字段做查询(必须考虑是否包含分片键)
  • 不能随意 JOIN(需要提前设计好数据冗余)
  • 业务迭代困难(表结构变更需要改 64 张表)
  • A/B 测试困难(数据隔离复杂)

什么时候才应该分库分表?

单库能撑就别分!先尝试这些优化

阶段 1: SQL 优化 (成本: 0, 收益: 10-100x)
├── 添加索引
├── 优化慢查询
└── 去除不必要的 SELECT *

阶段 2: 读写分离 (成本: 低, 收益: 3-5x)
├── 1 主 3 从
├── 读请求走从库
└── 缓存热点数据 (Redis)

阶段 3: 垂直拆分 (成本: 中, 收益: 2-3x)
├── 按业务模块拆分数据库
├── 大字段拆分到独立表
└── 冷热数据分离

阶段 4: 分库分表 (成本: 高, 收益: 5-10x,但复杂度 +100x)
├── 单表 > 2000 万
├── 单库 QPS > 5000
└── 单库存储 > 1TB

真实成本对比

项目 单库 分库分表 (8 库 64 表)
开发成本 1 个月 3-6 个月(路由、聚合、迁移)
硬件成本 1 台 (16C64G) = $1000/月 8 台 × $1000 = $8000/月
运维人力 1 人 2-3 人(监控、巡检、应急)
故障恢复时间 1 小时 4-8 小时(多库协调)
新人上手时间 1 周 1-2 个月(理解路由逻辑)

正确的心态

正确

  • 分库分表是不得已的选择,不是炫技
  • 数据量没到千万级别,不要考虑分库
  • 先优化 SQL、加缓存、读写分离,能撑 90% 的业务
  • 分库分表后团队要有专门的 DBA 和架构师

错误

  • “微服务时代,每个服务都要分库分表”
  • “大厂都在用,我们也要用”
  • “提前设计好分库分表,以后就不用改了”(扩容仍然是大工程)

阅读全文»

bjmayor发布于2025-11-07

如何正确使用缓存:常见陷阱与最佳实践

前言

缓存(Redis/Memcache)是提升系统性能的利器,但错误的使用方式不仅无法带来性能提升,反而会引入更多问题。本文通过真实案例和性能基准测试,帮助初级开发者理解缓存使用的常见误区和正确姿势。

阅读全文»

bjmayor发布于2025-11-06

亿级信息发布与订阅系统架构设计:从发布到时间线

引言:一条内容如何抵达千万时间线

当大V发布一条内容,需要在数秒内出现在千万粉丝的时间线里;普通用户发布,也要在好友的时间线中有良好曝光。这背后是“高吞吐写入 + 扇出分发 + 低延迟读取 + 排序推荐”的系统工程。

本文延续关系链的思路,给出信息发布(Feed/Timeline)在亿级规模下的可落地架构:选型、数据模型、分片与热点、写读路径、缓存与排序、最终一致性与修复,以及 PostgreSQL 的实践要点与本地可验证路径。

阅读全文»