📋 Research Summary
研究发现,2024-2025 年 Feed 流系统架构的核心在于写扩散(推模式)与 读扩散(拉模式)的权衡选择。阿里云、腾讯云等厂商的最新方案普遍采用推拉结合策略,根据粉丝数量动态选择推或拉模式。微博、Twitter 等千万级用户系统通过分片策略、多级缓存和智能推荐应对 100W QPS 级别的访问压力。
🌱 逻辑原点
如果一个用户有 1000 万粉丝,他发一条微博,需要给每个粉丝的 Feed 流都推送吗?
Feed 流系统的根本矛盾:**写时扩散(推)与读时扩散(拉)**无法兼得。推模式在大 V 场景下写入爆炸,拉模式在普通用户场景下读取缓慢。如何在千万级用户规模下,既保证大 V 发推不卡顿,又保证普通用户刷 Feed 流流畅?
🧠 苏格拉底式对话
1️⃣ 现状:最原始的解法是什么?
Q: 如果不用 Feed 流架构,最简单的"关注"功能怎么做?
最原始的数据库查询方案:
sql
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50)
);
-- 关注关系表
CREATE TABLE follow_relations (
follower_id BIGINT, -- 粉丝
followee_id BIGINT, -- 关注对象
created_at TIMESTAMP,
PRIMARY KEY (follower_id, followee_id)
);
-- 内容表
CREATE TABLE posts (
id BIGINT PRIMARY KEY,
user_id BIGINT,
content TEXT,
created_at TIMESTAMP
);
查询我的 Feed 流:
sql
-- 方式 1:直接 JOIN(性能灾难)
SELECT p.* FROM posts p
JOIN follow_relations f ON f.followee_id = p.user_id
WHERE f.follower_id = 123
ORDER BY p.created_at DESC
LIMIT 20;
-- 问题:关注 1000 人,需要扫描 1000 个用户的帖子
问题:
- ✅ 数据结构简单,容易理解
- ❌ 查询需要 JOIN 大量数据,性能极差
- ❌ 无法分页(每次都要重新排序)
- ❌ 无法支持个性化推荐
这就像是每次要看朋友圈,都要给所有好友打电话问"你最近发了什么"。
2️⃣ 瓶颈:规模扩大 100 倍时会在哪里崩溃?
Q: 当用户从 1 万增长到 100 万,大 V 粉丝从 1 万增长到 1000 万时,简单方案会在哪里崩溃?
崩溃点 1:查询性能瓶颈
普通用户关注 100 人:
- 扫描 100 个用户的帖子
- 假设每个用户 100 条帖子,需要扫描 10,000 条
- 查询时间:1 秒
大 V 粉丝 1000 万:
- 大 V 发 1 条微博
- 1000 万粉丝刷新 Feed 流
- 数据库连接数瞬间爆炸
崩溃点 2:JOIN 操作的 O(N) 复杂度
- 关注人数从 100 → 10,000
- 查询时间从 100ms → 10,000ms(10 秒)
- 数据库 CPU 跑满,所有查询阻塞
崩溃点 3:无法支持实时更新
- 用户 A 刚发了内容
- 粉丝 B 刷新 Feed 流,却看不到新内容
- 因为缓存未失效,或者索引未更新
这就像是让邮递员每次送信前,都要先跑遍整个城市问"谁有信要收"。
3️⃣ 突破:必须引入什么新维度?
Q: 如何避免"写时复制"和"读时聚合"的双重性能陷阱?
引入新维度:预计算 + 异步分发
核心思想:
- 推模式(写扩散):写时将内容推送到粉丝收件箱
- 拉模式(读扩散):读时从关注对象拉取内容
- 推拉结合:根据粉丝数量动态选择策略
架构对比:
传统方案:
用户刷新 → JOIN 查询 → 实时聚合 → 返回结果
(每次都要扫描所有关注对象的内容)
推模式:
博主发帖 → 推送到所有粉丝收件箱 → 用户刷新 → 直接读取
(写时复制,读时简单)
拉模式:
用户刷新 → 聚合关注对象内容 → 排序返回
(写时简单,读时聚合)
推拉结合:
小 V(<5000 粉丝)→ 推模式
大 V(>5000 粉丝)→ 拉模式
这就像是报纸订阅:小众刊物直接投递到户,大众刊物到报刊亭自取。
📊 视觉骨架
推拉结合 Hybrid
小V <5000
大V >5000
判断粉丝数 Check Followers
推模式 Push
拉模式 Pull
拉模式 Pull Model
写入自己的发件箱 Outbox
聚合关注对象内容 Aggregate
拉取 Pull
拉取 Pull
排序 Sort
博主发帖 Post
博主发件箱 Outbox
用户刷新 Refresh
关注对象2发件箱 Outbox2
关注对象N发件箱 OutboxN
返回结果 Result
推模式 Push Model
复制到粉丝收件箱 Fanout
复制 Copy
复制 Copy
直接读取 Read
直接读取 Read
博主发帖 Post
粉丝1收件箱 Inbox1
粉丝2收件箱 Inbox2
粉丝N收件箱 InboxN
用户刷新 Refresh
用户刷新 Refresh
核心组件说明:
- 推模式:写时扩散,每个粉丝维护一份 Feed 流
- 拉模式:读时聚合,从关注对象拉取内容
- 推拉结合:根据粉丝数量动态选择策略
⚖️ 权衡模型
公式:
Feed 流架构 = 解决了读写性能冲突 + 牺牲了存储空间 + 增加了系统复杂度
代价分析:
推模式(写扩散)
-
✅ 解决:
- 读取极快:直接读取自己的收件箱
- 时效性好:新内容立即可见
- 支持实时更新
-
❌ 牺牲:
- 存储爆炸:1000 万粉丝 × 每人 100 条 = 10 亿条记录
- 写放大:大 V 发 1 条,需要写 1000 万次
- 无法快速取消关注:需要删除大量历史数据
-
⚠️ 增加:
- 粉丝收件箱维护:每个用户一个 Feed 流队列
- 写限流机制:防止大 V 发帖时系统雪崩
拉模式(读扩散)
-
✅ 解决:
- 写操作简单:只需写自己的发件箱
- 存储节省:不需要为每个粉丝复制内容
- 适合大 V 场景
-
❌ 牺牲:
- 读取慢:需要聚合多个关注对象的内容
- 缓存依赖:严重依赖多级缓存架构
- 无法支持复杂排序
-
⚠️ 增加:
- 内容聚合服务:实时拉取并合并内容
- 多级缓存:Redis、本地缓存、CDN
推拉结合(推荐)
-
✅ 解决:
- 兼顾读写性能
- 灵活适应不同场景
- 成本可控
-
❌ 牺牲:
- 架构复杂:需要同时维护两套逻辑
- 阈值选择:粉丝数阈值需要动态调整
-
⚠️ 增加:
- 动态路由:根据粉丝数选择策略
- 监控告警:监控推拉模式的效果
🗂️ 数据分片策略:应对千万级用户规模
Q: 当用户规模达到千万级,如何保证数据均匀分布,避免热点问题?
Feed 流系统的数据分片是架构设计的核心挑战。收件箱和发件箱需要不同的分片策略,既要保证查询性能,又要避免热点数据。
🎯 分片挑战
分片挑战
收件箱分片
发件箱分片
热点数据隔离
每个用户一个收件箱
查询模式:按用户ID读取
每个用户一个发件箱
查询模式:按用户ID写入
大V收件箱访问热点
大V发件箱写入热点
1️⃣ Hash 分片(推荐)
核心思想:使用用户 ID 的 Hash 值决定数据存储的分片。
python
class HashShardingStrategy:
"""
Hash 分片策略
"""
def __init__(self, shard_count):
self.shard_count = shard_count
def get_shard_id(self, user_id):
"""
根据 user_id 计算分片 ID
"""
# 使用 MurmurHash 或 SHA256
hash_value = hash(str(user_id))
return hash_value % self.shard_count
def get_feed_key(self, user_id):
"""
获取用户的 Feed 流存储 Key
"""
shard_id = self.get_shard_id(user_id)
return f"feed:shard{shard_id}:{user_id}"
def get_outbox_key(self, user_id):
"""
获取用户的发件箱存储 Key
"""
shard_id = self.get_shard_id(user_id)
return f"outbox:shard{shard_id}:{user_id}"
# 使用示例
sharding = HashShardingStrategy(shard_count=64)
# 用户 123 的 Feed 流存储在 shard 15
feed_key = sharding.get_feed_key(123) # "feed:shard15:123"
# 用户 456 的发件箱存储在 shard 42
outbox_key = sharding.get_outbox_key(456) # "outbox:shard42:456"
优势:
- ✅ 数据均匀分布
- ✅ 查询简单:通过 user_id 直接定位分片
- ✅ 扩展性好:增加分片时只需重新计算
劣势:
- ❌ 跨分片查询困难(如查询多个用户的 Feed)
- ❌ 分片扩容时需要迁移数据
2️⃣ 一致性哈希(进阶)
核心思想:使用环形哈希,减少分片扩容时的数据迁移量。
python
import hashlib
class ConsistentHashSharding:
"""
一致性哈希分片策略
"""
def __init__(self, shard_count, virtual_nodes=160):
self.shard_count = shard_count
self.virtual_nodes = virtual_nodes # 虚拟节点数
self.ring = {} # 哈希环
self.sorted_keys = [] # 排序后的哈希值
# 初始化哈希环
self._init_ring()
def _init_ring(self):
"""初始化哈希环"""
for shard_id in range(self.shard_count):
# 为每个分片创建多个虚拟节点
for i in range(self.virtual_nodes):
# 计算虚拟节点的哈希值
virtual_key = f"shard{shard_id}:virtual{i}"
hash_value = self._hash(virtual_key)
# 添加到哈希环
self.ring[hash_value] = shard_id
self.sorted_keys.append(hash_value)
# 排序哈希值
self.sorted_keys.sort()
def _hash(self, key):
"""计算哈希值"""
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def get_shard_id(self, user_id):
"""
根据 user_id 获取分片 ID
"""
hash_value = self._hash(str(user_id))
# 在哈希环上找到第一个大于等于 hash_value 的节点
for key in self.sorted_keys:
if key >= hash_value:
return self.ring[key]
# 如果没找到,返回第一个节点(环形)
return self.ring[self.sorted_keys[0]]
def add_shard(self, new_shard_id):
"""
动态添加分片
"""
# 为新分片创建虚拟节点
for i in range(self.virtual_nodes):
virtual_key = f"shard{new_shard_id}:virtual{i}"
hash_value = self._hash(virtual_key)
self.ring[hash_value] = new_shard_id
self.sorted_keys.append(hash_value)
self.sorted_keys.sort()
def remove_shard(self, shard_id):
"""
移除分片
"""
# 删除该分片的所有虚拟节点
keys_to_remove = []
for key in self.sorted_keys:
if self.ring[key] == shard_id:
keys_to_remove.append(key)
for key in keys_to_remove:
del self.ring[key]
self.sorted_keys.remove(key)
# 使用示例
sharding = ConsistentHashSharding(shard_count=10, virtual_nodes=160)
# 用户 123 的分片
shard_id = sharding.get_shard_id(123)
# 动态扩容:添加新分片
sharding.add_shard(10)
# 用户 456 的分片(可能在新分片上)
shard_id = sharding.get_shard_id(456)
优势:
- ✅ 分片扩容时数据迁移量小
- ✅ 负载均衡更好
- ✅ 支持动态增删分片
劣势:
- ❌ 实现复杂
- ❌ 需要维护虚拟节点
3️⃣ 热点数据隔离
问题场景:大 V(1000 万粉丝)的收件箱访问量极高,导致单个分片成为热点。
解决方案:将热点用户的数据独立存储。
python
class HotUserIsolation:
"""
热点用户隔离策略
"""
def __init__(self, hot_user_threshold=100000):
self.hot_user_threshold = hot_user_threshold
self.hot_users = set() # 热点用户集合
self.hot_shards = {} # 热点用户到独立分片的映射
def is_hot_user(self, user_id, follower_count):
"""
判断是否为热点用户
"""
return follower_count >= self.hot_user_threshold
def mark_hot_user(self, user_id, dedicated_shard_id):
"""
标记为热点用户,分配独立分片
"""
self.hot_users.add(user_id)
self.hot_shards[user_id] = dedicated_shard_id
def get_shard_id(self, user_id, follower_count=0):
"""
获取分片 ID
"""
# 热点用户使用独立分片
if user_id in self.hot_users:
return self.hot_shards[user_id]
# 检查是否成为新的热点用户
if follower_count >= self.hot_user_threshold:
# 分配新的独立分片
dedicated_shard_id = len(self.hot_shards) + 1000 # 从 1000 开始编号
self.mark_hot_user(user_id, dedicated_shard_id)
return dedicated_shard_id
# 普通用户使用 Hash 分片
return hash(str(user_id)) % 64
# 使用示例
isolation = HotUserIsolation(hot_user_threshold=100000)
# 普通用户:使用 Hash 分片
shard_id = isolation.get_shard_id(123, follower_count=1000) # shard_id = 15
# 大 V:使用独立分片
shard_id = isolation.get_shard_id(456, follower_count=1000000) # shard_id = 1000
架构图:
┌─────────────────────────────────────────────────────────┐
│ Feed 流存储层 │
├─────────────────────────────────────────────────────────┤
│ 普通用户分片(Hash 分片) │
│ Shard 0 │ Shard 1 │ ... │ Shard 63 │
│ [用户1] │ [用户2] │ │ [用户N] │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 热点用户独立分片 │
├─────────────────────────────────────────────────────────┤
│ Shard 1000 │ Shard 1001 │ Shard 1002 │ ... │
│ [大V1] │ [大V2] │ [大V3] │ │
│ 收件箱 │ 收件箱 │ 收件箱 │ │
└─────────────────────────────────────────────────────────┘
4️⃣ 混合分片策略
核心思想:收件箱和发件箱使用不同的分片策略。
python
class HybridShardingStrategy:
"""
混合分片策略
"""
def __init__(self):
# 收件箱:按用户 ID Hash 分片(查询模式:按用户读取)
self.inbox_sharding = HashShardingStrategy(shard_count=64)
# 发件箱:按用户 ID Hash 分片(查询模式:按用户写入)
self.outbox_sharding = HashShardingStrategy(shard_count=64)
# 关注关系:按用户 ID 范围分片(查询模式:按用户查询关注列表)
self.follow_sharding = RangeShardingStrategy(shard_count=32)
def get_inbox_shard(self, user_id):
"""获取收件箱分片"""
return self.inbox_sharding.get_shard_id(user_id)
def get_outbox_shard(self, user_id):
"""获取发件箱分片"""
return self.outbox_sharding.get_shard_id(user_id)
def get_follow_shard(self, user_id):
"""获取关注关系分片"""
return self.follow_sharding.get_shard_id(user_id)
class RangeShardingStrategy:
"""
范围分片策略
"""
def __init__(self, shard_count):
self.shard_count = shard_count
def get_shard_id(self, user_id):
"""
根据 user_id 范围计算分片 ID
"""
return (user_id // 1000000) % self.shard_count
# 使用示例
sharding = HybridShardingStrategy()
# 用户 123 的收件箱在 shard 15
inbox_shard = sharding.get_inbox_shard(123)
# 用户 123 的发件箱在 shard 15
outbox_shard = sharding.get_outbox_shard(123)
# 用户 123 的关注关系在 shard 0
follow_shard = sharding.get_follow_shard(123)
📊 分片策略对比
| 策略 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Hash 分片 | 数据均匀、查询简单 | 跨分片查询困难 | 收件箱、发件箱 |
| 一致性哈希 | 扩容迁移量小 | 实现复杂 | 需要动态扩容的场景 |
| 范围分片 | 支持范围查询 | 数据不均匀 | 关注关系、时间线 |
| 热点隔离 | 避免热点问题 | 需要识别热点 | 大 V 收件箱 |
🎯 分片选择建议
收件箱(Feed 流):
- 推荐:Hash 分片 + 热点隔离
- 理由:查询模式是按用户读取,Hash 分片可以快速定位
发件箱(用户内容):
- 推荐:Hash 分片
- 理由:查询模式是按用户写入,Hash 分片可以快速定位
关注关系:
- 推荐:范围分片
- 理由:查询模式是按用户查询关注列表,范围分片支持批量查询
时间线(按时间查询):
- 推荐:时间分片(按天/周/月)
- 理由:查询模式是按时间范围查询,时间分片可以快速定位
💾 多级缓存架构:提升读取性能
Q: 拉模式需要实时聚合多个关注对象的内容,如何保证读取性能?
拉模式严重依赖缓存,多级缓存架构是提升性能的关键。通过本地缓存、分布式缓存、CDN 的层次设计,可以将 Feed 流的读取延迟降低到毫秒级。
🎯 缓存层次架构
未命中
未命中
命中
命中
命中
用户请求
本地缓存 L1
Caffeine/Guava Cache
分布式缓存 L2
Redis Cluster
数据库 L3
MySQL/PostgreSQL
返回结果
1ms
返回结果
10ms
返回结果
100ms
1️⃣ L1 缓存:本地缓存
目标:减少网络开销,提供微秒级响应。
python
from caffeine import Cache
import time
class LocalFeedCache:
"""
本地 Feed 流缓存
"""
def __init__(self, max_size=10000, expire_after_write=300):
"""
:param max_size: 最大缓存条目数
:param expire_after_write: 写入后过期时间(秒)
"""
self.cache = Cache.builder() \
.maximum_size(max_size) \
.expire_after_write(expire_after_write) \
.build()
def get(self, user_id):
"""
获取用户的 Feed 流
"""
key = f"feed:{user_id}"
return self.cache.get(key)
def put(self, user_id, feed_items):
"""
缓存用户的 Feed 流
"""
key = f"feed:{user_id}"
self.cache.put(key, feed_items)
def invalidate(self, user_id):
"""
失效缓存
"""
key = f"feed:{user_id}"
self.cache.invalidate(key)
def get_stats(self):
"""
获取缓存统计信息
"""
return {
'hit_rate': self.cache.stats().hit_rate(),
'hit_count': self.cache.stats().hit_count(),
'miss_count': self.cache.stats().miss_count(),
'size': self.cache.estimated_size()
}
# 使用示例
local_cache = LocalFeedCache(max_size=10000, expire_after_write=300)
# 查询 Feed 流(先查本地缓存)
feed_items = local_cache.get(123)
if feed_items is None:
# 本地缓存未命中,查询 Redis
feed_items = redis_cache.get(123)
if feed_items:
# 写入本地缓存
local_cache.put(123, feed_items)
print(f"缓存命中率: {local_cache.get_stats()['hit_rate']:.2%}")
优势:
- ✅ 响应极快(微秒级)
- ✅ 无网络开销
- ✅ 减轻 Redis 压力
劣势:
- ❌ 内存占用高
- ❌ 多实例间数据不一致
- ❌ 需要手动失效
2️⃣ L2 缓存:分布式缓存
目标:提供全局共享缓存,支持高并发访问。
python
import redis
import json
from typing import List, Optional
class RedisFeedCache:
"""
Redis Feed 流缓存
"""
def __init__(self, redis_client, expire_seconds=3600):
"""
:param redis_client: Redis 客户端
:param expire_seconds: 过期时间(秒)
"""
self.redis = redis_client
self.expire_seconds = expire_seconds
def get(self, user_id: int) -> Optional[List[dict]]:
"""
获取用户的 Feed 流
"""
key = f"feed:{user_id}"
data = self.redis.get(key)
if data:
return json.loads(data)
return None
def put(self, user_id: int, feed_items: List[dict]):
"""
缓存用户的 Feed 流
"""
key = f"feed:{user_id}"
data = json.dumps(feed_items, ensure_ascii=False)
# 使用 Pipeline 批量执行
pipeline = self.redis.pipeline()
pipeline.set(key, data)
pipeline.expire(key, self.expire_seconds)
pipeline.execute()
def invalidate(self, user_id: int):
"""
失效缓存
"""
key = f"feed:{user_id}"
self.redis.delete(key)
def get_outbox(self, user_id: int, limit: int = 100) -> List[dict]:
"""
获取用户的发件箱(使用 Sorted Set)
"""
key = f"outbox:{user_id}"
# 使用 ZREVRANGE 获取最新的帖子
post_ids = self.redis.zrevrange(key, 0, limit - 1, withscores=True)
# 批量获取帖子详情
if post_ids:
post_ids_str = [str(post_id) for post_id, score in post_ids]
posts = self.redis.mget([f"post:{post_id}" for post_id in post_ids_str])
return [json.loads(post) for post in posts if post]
return []
def add_to_outbox(self, user_id: int, post_id: int, timestamp: float):
"""
添加帖子到发件箱
"""
key = f"outbox:{user_id}"
# 使用 ZADD 添加到 Sorted Set
self.redis.zadd(key, {str(post_id): timestamp})
# 设置过期时间
self.redis.expire(key, self.expire_seconds)
# 使用示例
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
redis_cache = RedisFeedCache(redis_client, expire_seconds=3600)
# 查询 Feed 流
feed_items = redis_cache.get(123)
if feed_items:
print(f"Feed 流缓存命中: {len(feed_items)} 条")
else:
print("Feed 流缓存未命中,需要查询数据库")
# 获取发件箱
outbox_items = redis_cache.get_outbox(123, limit=20)
print(f"发件箱: {len(outbox_items)} 条")
优势:
- ✅ 全局共享,多实例一致
- ✅ 支持高并发
- ✅ 数据结构丰富(List、Set、Sorted Set)
劣势:
- ❌ 网络开销(毫秒级)
- ❌ 需要维护 Redis 集群
3️⃣ 缓存预热
目标:提前加载热点数据,避免缓存雪崩。
python
class CacheWarmer:
"""
缓存预热器
"""
def __init__(self, redis_cache, local_cache):
self.redis_cache = redis_cache
self.local_cache = local_cache
def warm_up_hot_users(self, hot_user_ids: List[int]):
"""
预热热点用户的 Feed 流
"""
for user_id in hot_user_ids:
# 从数据库加载 Feed 流
feed_items = self.load_feed_from_db(user_id)
# 写入 Redis 缓存
self.redis_cache.put(user_id, feed_items)
# 写入本地缓存
self.local_cache.put(user_id, feed_items)
print(f"预热用户 {user_id} 的 Feed 流: {len(feed_items)} 条")
def warm_up_outbox(self, user_ids: List[int]):
"""
预热用户的发件箱
"""
for user_id in user_ids:
# 从数据库加载发件箱
outbox_items = self.load_outbox_from_db(user_id)
# 批量写入 Redis
for item in outbox_items:
self.redis_cache.add_to_outbox(
user_id,
item['post_id'],
item['timestamp']
)
print(f"预热用户 {user_id} 的发件箱: {len(outbox_items)} 条")
def load_feed_from_db(self, user_id: int) -> List[dict]:
"""从数据库加载 Feed 流"""
# 实现数据库查询逻辑
pass
def load_outbox_from_db(self, user_id: int) -> List[dict]:
"""从数据库加载发件箱"""
# 实现数据库查询逻辑
pass
# 使用示例
warmer = CacheWarmer(redis_cache, local_cache)
# 预热热点用户(粉丝数 > 10 万)
hot_users = get_hot_users(min_followers=100000)
warmer.warm_up_hot_users(hot_users)
# 预热活跃用户的发件箱
active_users = get_active_users(days=7)
warmer.warm_up_outbox(active_users)
4️⃣ 缓存一致性保证
问题:如何保证缓存和数据库的一致性?
策略 1:Cache Aside(推荐)
python
class CacheAsidePattern:
"""
Cache Aside 模式
"""
def get_feed(self, user_id: int) -> List[dict]:
"""
获取 Feed 流(先查缓存,未命中再查数据库)
"""
# 1. 查询缓存
feed_items = redis_cache.get(user_id)
if feed_items is not None:
# 缓存命中,直接返回
return feed_items
# 2. 缓存未命中,查询数据库
feed_items = self.load_feed_from_db(user_id)
# 3. 写入缓存
redis_cache.put(user_id, feed_items)
return feed_items
def update_feed(self, user_id: int, new_items: List[dict]):
"""
更新 Feed 流(先更新数据库,再失效缓存)
"""
# 1. 更新数据库
self.save_feed_to_db(user_id, new_items)
# 2. 失效缓存(而不是更新缓存)
redis_cache.invalidate(user_id)
# 3. 失效本地缓存
local_cache.invalidate(user_id)
策略 2:Write Through
python
class WriteThroughPattern:
"""
Write Through 模式
"""
def update_feed(self, user_id: int, new_items: List[dict]):
"""
更新 Feed 流(同时更新数据库和缓存)
"""
# 1. 更新数据库
self.save_feed_to_db(user_id, new_items)
# 2. 更新缓存
redis_cache.put(user_id, new_items)
# 3. 更新本地缓存
local_cache.put(user_id, new_items)
策略 3:Write Behind
python
import threading
from queue import Queue
class WriteBehindPattern:
"""
Write Behind 模式(异步写入)
"""
def __init__(self):
self.write_queue = Queue()
self.worker_thread = threading.Thread(target=self._write_worker)
self.worker_thread.daemon = True
self.worker_thread.start()
def update_feed(self, user_id: int, new_items: List[dict]):
"""
更新 Feed 流(异步写入数据库)
"""
# 1. 更新缓存(立即)
redis_cache.put(user_id, new_items)
local_cache.put(user_id, new_items)
# 2. 加入写入队列(异步)
self.write_queue.put({
'user_id': user_id,
'items': new_items
})
def _write_worker(self):
"""后台写入线程"""
while True:
task = self.write_queue.get()
try:
# 异步写入数据库
self.save_feed_to_db(task['user_id'], task['items'])
except Exception as e:
logger.error(f"写入数据库失败: {e}")
# 可以加入重试队列
finally:
self.write_queue.task_done()
5️⃣ 缓存击穿防护
问题:热点 Key 失效时,大量请求同时击穿到数据库。
解决方案:互斥锁 + 缓存预热。
python
import threading
class CacheBreakdownProtection:
"""
缓存击穿防护
"""
def __init__(self):
self.locks = {} # 用户 ID 到锁的映射
def get_feed(self, user_id: int) -> List[dict]:
"""
获取 Feed 流(带缓存击穿防护)
"""
# 1. 查询本地缓存
feed_items = local_cache.get(user_id)
if feed_items is not None:
return feed_items
# 2. 查询 Redis 缓存
feed_items = redis_cache.get(user_id)
if feed_items is not None:
# 写入本地缓存
local_cache.put(user_id, feed_items)
return feed_items
# 3. 缓存未命中,使用互斥锁防止击穿
lock = self._get_lock(user_id)
with lock:
# 双重检查:可能其他线程已经加载了缓存
feed_items = redis_cache.get(user_id)
if feed_items is not None:
local_cache.put(user_id, feed_items)
return feed_items
# 4. 从数据库加载
feed_items = self.load_feed_from_db(user_id)
# 5. 写入缓存
redis_cache.put(user_id, feed_items)
local_cache.put(user_id, feed_items)
return feed_items
def _get_lock(self, user_id: int) -> threading.Lock:
"""获取或创建锁"""
if user_id not in self.locks:
self.locks[user_id] = threading.Lock()
return self.locks[user_id]
6️⃣ 缓存雪崩防护
问题:大量 Key 同时失效,导致数据库压力激增。
解决方案:随机过期时间 + 缓存预热。
python
import random
class CacheAvalancheProtection:
"""
缓存雪崩防护
"""
def put(self, user_id: int, feed_items: List[dict]):
"""
写入缓存(带随机过期时间)
"""
# 基础过期时间:3600 秒
base_expire = 3600
# 随机偏移:±600 秒(10 分钟)
random_offset = random.randint(-600, 600)
# 实际过期时间
actual_expire = base_expire + random_offset
# 写入 Redis
key = f"feed:{user_id}"
data = json.dumps(feed_items, ensure_ascii=False)
pipeline = redis_client.pipeline()
pipeline.set(key, data)
pipeline.expire(key, actual_expire)
pipeline.execute()
logger.info(f"写入缓存: user_id={user_id}, expire={actual_expire}s")
📊 缓存策略对比
| 策略 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Cache Aside | 实现简单、一致性好 | 缓存未命中时性能下降 | 读多写少 |
| Write Through | 数据一致性好 | 写入性能差 | 写多读少 |
| Write Behind | 写入性能高 | 数据可能丢失 | 写入频繁、允许短暂不一致 |
🎯 缓存最佳实践
- 多级缓存:本地缓存 + Redis 缓存 + 数据库
- 缓存预热:提前加载热点数据
- 随机过期:避免缓存雪崩
- 互斥锁:防止缓存击穿
- 监控指标:缓存命中率、响应时间、错误率
- 降级策略:缓存不可用时直接查询数据库
⚡ 性能优化实践:应对百万级 QPS
Q: 当 QPS 达到百万级,如何保证 Feed 流的读写性能?
Feed 流系统的性能优化需要从多个维度入手:批量推送、异步解耦、并行拉取、索引优化等。通过这些优化,可以将系统吞吐量提升 10-100 倍。
1️⃣ 批量推送优化
问题:大 V 发 1 条微博,需要推送到 1000 万个粉丝收件箱,如何避免循环超时?
解决方案:批量写入 + Redis Pipeline。
python
import redis
from typing import List, Dict
import time
class BatchPushService:
"""
批量推送服务
"""
def __init__(self, redis_client, batch_size=1000, max_retries=3):
self.redis = redis_client
self.batch_size = batch_size
self.max_retries = max_retries
def push_to_followers(self, post: Dict, followers: List[int]):
"""
批量推送到粉丝收件箱
:param post: 帖子数据
:param followers: 粉丝 ID 列表
"""
total_followers = len(followers)
logger.info(f"开始推送: post_id={post['id']}, 粉丝数={total_followers}")
# 分批处理
for i in range(0, total_followers, self.batch_size):
batch = followers[i:i + self.batch_size]
# 使用 Redis Pipeline 批量写入
self._push_batch(post, batch)
# 记录进度
progress = min(i + self.batch_size, total_followers)
logger.info(f"推送进度: {progress}/{total_followers}")
logger.info(f"推送完成: post_id={post['id']}")
def _push_batch(self, post: Dict, followers: List[int]):
"""
推送一批粉丝
"""
for attempt in range(self.max_retries):
try:
# 创建 Pipeline
pipeline = self.redis.pipeline()
# 批量添加到 Pipeline
for follower_id in followers:
feed_key = f"feed:{follower_id}"
# 使用 ZADD 添加到 Sorted Set(按时间排序)
pipeline.zadd(
feed_key,
{str(post['id']): post['timestamp']}
)
# 限制 Feed 流长度(只保留最新 100 条)
pipeline.zremrangebyrank(feed_key, 0, -101)
# 设置过期时间
pipeline.expire(feed_key, 86400) # 24 小时
# 执行 Pipeline
results = pipeline.execute()
# 检查结果
if all(results):
return # 成功
else:
raise Exception("Pipeline 执行失败")
except Exception as e:
logger.error(f"推送失败,重试 {attempt + 1}/{self.max_retries}: {e}")
if attempt < self.max_retries - 1:
time.sleep(2 ** attempt) # 指数退避
else:
logger.error(f"推送最终失败: {e}")
raise
# 使用示例
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
push_service = BatchPushService(redis_client, batch_size=1000)
# 大 V 发帖
post = {
'id': 12345,
'user_id': 789,
'content': '这是一条微博',
'timestamp': time.time()
}
# 获取粉丝列表(1000 万)
followers = get_followers(user_id=789)
# 批量推送
push_service.push_to_followers(post, followers)
性能对比:
| 方式 | 1000 万粉丝推送时间 | QPS |
|---|---|---|
| 单条循环 | ~1000 秒 | 10,000 |
| 批量推送(1000) | ~100 秒 | 100,000 |
| 批量推送 + Pipeline | ~10 秒 | 1,000,000 |
2️⃣ 异步队列解耦
问题:如何避免推送操作阻塞主流程?
解决方案:使用消息队列异步推送。
python
from kafka import KafkaProducer
import json
class AsyncPushService:
"""
异步推送服务
"""
def __init__(self, kafka_servers):
self.producer = KafkaProducer(
bootstrap_servers=kafka_servers,
value_serializer=lambda v: json.dumps(v).encode('utf-8')
)
def push_to_followers_async(self, post: Dict, followers: List[int]):
"""
异步推送到粉丝收件箱
"""
# 将推送任务发送到 Kafka
message = {
'post': post,
'followers': followers,
'task_id': generate_task_id(),
'timestamp': time.time()
}
# 根据粉丝数选择分区(负载均衡)
partition = hash(str(post['user_id'])) % 10
self.producer.send(
'feed-push',
value=message,
partition=partition
)
logger.info(f"推送任务已发送: task_id={message['task_id']}, 粉丝数={len(followers)}")
# Kafka 消费者
from kafka import KafkaConsumer
class PushConsumer:
"""
推送消费者
"""
def __init__(self, kafka_servers, group_id):
self.consumer = KafkaConsumer(
'feed-push',
bootstrap_servers=kafka_servers,
group_id=group_id,
value_deserializer=lambda m: json.loads(m.decode('utf-8'))
)
self.push_service = BatchPushService(redis_client)
def start(self):
"""启动消费者"""
logger.info("推送消费者启动")
for message in self.consumer:
try:
task = message.value
self._process_task(task)
except Exception as e:
logger.error(f"处理任务失败: {e}")
def _process_task(self, task: Dict):
"""处理推送任务"""
post = task['post']
followers = task['followers']
logger.info(f"开始处理任务: task_id={task['task_id']}")
# 批量推送
self.push_service.push_to_followers(post, followers)
logger.info(f"任务完成: task_id={task['task_id']}")
# 使用示例
# 生产者:发送推送任务
async_push = AsyncPushService(kafka_servers='localhost:9092')
async_push.push_to_followers_async(post, followers)
# 消费者:处理推送任务(独立进程)
consumer = PushConsumer(kafka_servers='localhost:9092', group_id='feed-push-group')
consumer.start()
架构图:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 用户发帖 │────▶│ Kafka │────▶│ 推送消费者 │
│ (主流程) │ │ 消息队列 │ │ (异步) │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ Redis │
│ 批量写入 │
└─────────────┘
3️⃣ 并行拉取优化
问题:拉模式需要聚合多个关注对象的内容,如何提升拉取速度?
解决方案:并行拉取 + 归并排序。
python
import concurrent.futures
from typing import List, Dict
import heapq
class ParallelPullService:
"""
并行拉取服务
"""
def __init__(self, max_workers=10):
self.max_workers = max_workers
def pull_feed(self, user_id: int, followees: List[int], limit: int = 20) -> List[Dict]:
"""
并行拉取 Feed 流
:param user_id: 用户 ID
:param followees: 关注对象列表
:param limit: 返回数量
"""
logger.info(f"开始拉取 Feed: user_id={user_id}, 关注数={len(followees)}")
# 并行拉取关注对象的内容
with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
# 提交拉取任务
futures = {
executor.submit(self._pull_outbox, followee): followee
for followee in followees
}
# 收集结果
all_posts = []
for future in concurrent.futures.as_completed(futures):
followee = futures[future]
try:
posts = future.result()
all_posts.extend(posts)
logger.info(f"拉取完成: followee={followee}, 帖子数={len(posts)}")
except Exception as e:
logger.error(f"拉取失败: followee={followee}, error={e}")
# 按时间排序
all_posts.sort(key=lambda x: x['timestamp'], reverse=True)
# 返回最新的 N 条
result = all_posts[:limit]
logger.info(f"拉取完成: user_id={user_id}, 返回={len(result)} 条")
return result
def _pull_outbox(self, followee_id: int) -> List[Dict]:
"""
拉取单个关注对象的发件箱
"""
# 从 Redis 拉取
outbox_items = redis_cache.get_outbox(followee_id, limit=100)
return outbox_items
def pull_feed_merge(self, user_id: int, followees: List[int], limit: int = 20) -> List[Dict]:
"""
使用归并排序优化拉取(避免全量排序)
"""
logger.info(f"开始拉取 Feed(归并排序): user_id={user_id}, 关注数={len(followees)}")
# 并行拉取关注对象的内容
with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
futures = {
executor.submit(self._pull_outbox, followee): followee
for followee in followees
}
# 使用最小堆进行归并排序
heap = []
for future in concurrent.futures.as_completed(futures):
followee = futures[future]
try:
posts = future.result()
if posts:
# 将第一个元素加入堆
heapq.heappush(heap, (-posts[0]['timestamp'], 0, posts, followee))
except Exception as e:
logger.error(f"拉取失败: followee={followee}, error={e}")
# 归并排序
result = []
while heap and len(result) < limit:
neg_timestamp, idx, posts, followee = heapq.heappop(heap)
result.append(posts[idx])
# 将该列表的下一个元素加入堆
if idx + 1 < len(posts):
heapq.heappush(heap, (-posts[idx + 1]['timestamp'], idx + 1, posts, followee))
logger.info(f"拉取完成: user_id={user_id}, 返回={len(result)} 条")
return result
# 使用示例
pull_service = ParallelPullService(max_workers=10)
# 获取关注列表
followees = get_followees(user_id=123)
# 并行拉取 Feed 流
feed_items = pull_service.pull_feed(user_id=123, followees=followees, limit=20)
# 使用归并排序优化
feed_items = pull_service.pull_feed_merge(user_id=123, followees=followees, limit=20)
性能对比:
| 方式 | 关注 100 人耗时 | 关注 1000 人耗时 |
|---|---|---|
| 串行拉取 | ~1000ms | ~10000ms |
| 并行拉取(10 线程) | ~100ms | ~1000ms |
| 并行拉取 + 归并排序 | ~50ms | ~500ms |
4️⃣ 分页优化
问题:如何避免深分页的性能问题?
解决方案:游标分页 + 限制分页深度。
python
class PaginationService:
"""
分页服务
"""
def __init__(self):
self.max_page_size = 100 # 最大每页数量
self.max_page_depth = 10 # 最大分页深度
def get_feed_by_offset(self, user_id: int, page: int, page_size: int) -> Dict:
"""
偏移量分页(传统方式,性能较差)
"""
# 限制分页参数
page_size = min(page_size, self.max_page_size)
page = min(page, self.max_page_depth)
offset = (page - 1) * page_size
# 使用 ZREVRANGE 获取分页数据
feed_key = f"feed:{user_id}"
start = offset
end = offset + page_size - 1
post_ids = redis_client.zrevrange(feed_key, start, end, withscores=True)
# 获取帖子详情
posts = self._get_posts_details(post_ids)
return {
'posts': posts,
'page': page,
'page_size': page_size,
'total': redis_client.zcard(feed_key),
'has_more': offset + page_size < redis_client.zcard(feed_key)
}
def get_feed_by_cursor(self, user_id: int, cursor: str = None, limit: int = 20) -> Dict:
"""
游标分页(推荐方式,性能好)
"""
limit = min(limit, self.max_page_size)
feed_key = f"feed:{user_id}"
if cursor:
# 从游标位置开始
cursor_timestamp = float(cursor)
# 使用 ZREVRANGEBYSCORE 获取小于游标时间戳的数据
post_ids = redis_client.zrevrangebyscore(
feed_key,
cursor_timestamp,
'-inf',
start=0,
num=limit + 1, # 多取一条,用于判断是否有更多数据
withscores=True
)
else:
# 从最新数据开始
post_ids = redis_client.zrevrange(
feed_key,
0,
limit,
withscores=True
)
# 判断是否有更多数据
has_more = len(post_ids) > limit
if has_more:
post_ids = post_ids[:limit]
# 获取帖子详情
posts = self._get_posts_details(post_ids)
# 计算下一页游标
next_cursor = None
if has_more and post_ids:
next_cursor = str(post_ids[-1][1]) # 最后一条的时间戳
return {
'posts': posts,
'cursor': next_cursor,
'has_more': has_more,
'limit': limit
}
def _get_posts_details(self, post_ids: List[tuple]) -> List[Dict]:
"""获取帖子详情"""
if not post_ids:
return []
# 批量获取帖子详情
post_ids_str = [str(post_id) for post_id, score in post_ids]
posts_data = redis_client.mget([f"post:{post_id}" for post_id in post_ids_str])
posts = []
for i, post_data in enumerate(posts_data):
if post_data:
post = json.loads(post_data)
post['timestamp'] = post_ids[i][1]
posts.append(post)
return posts
# 使用示例
pagination = PaginationService()
# 传统分页(性能较差)
result = pagination.get_feed_by_offset(user_id=123, page=1, page_size=20)
# 游标分页(推荐)
result = pagination.get_feed_by_cursor(user_id=123, cursor=None, limit=20)
next_cursor = result['cursor']
# 下一页
result = pagination.get_feed_by_cursor(user_id=123, cursor=next_cursor, limit=20)
5️⃣ 写入限流
问题:如何防止大 V 频繁发帖导致系统雪崩?
解决方案:令牌桶算法 + 优先级队列。
python
import time
from collections import deque
class RateLimiter:
"""
令牌桶限流器
"""
def __init__(self, capacity: int, refill_rate: float):
"""
:param capacity: 桶容量(最大令牌数)
:param refill_rate: 令牌补充速率(令牌/秒)
"""
self.capacity = capacity
self.refill_rate = refill_rate
self.tokens = capacity
self.last_refill_time = time.time()
def allow(self, tokens_needed: int = 1) -> bool:
"""
检查是否允许请求
"""
# 补充令牌
self._refill()
# 检查令牌是否足够
if self.tokens >= tokens_needed:
self.tokens -= tokens_needed
return True
return False
def _refill(self):
"""补充令牌"""
now = time.time()
elapsed = now - self.last_refill_time
tokens_to_add = elapsed * self.refill_rate
self.tokens = min(self.capacity, self.tokens + tokens_to_add)
self.last_refill_time = now
class PostRateLimiter:
"""
发帖限流器
"""
def __init__(self):
# 普通用户:每小时 10 条
self.normal_limiter = RateLimiter(capacity=10, refill_rate=10/3600)
# 大 V:每小时 100 条
self.influencer_limiter = RateLimiter(capacity=100, refill_rate=100/3600)
# 超级大 V:不限流(但需要异步处理)
self.super_influencer_threshold = 1000000 # 100 万粉丝
def allow_post(self, user_id: int, follower_count: int) -> bool:
"""
检查是否允许发帖
"""
if follower_count >= self.super_influencer_threshold:
# 超级大 V:不限流,但使用异步推送
return True
if follower_count >= 10000:
# 大 V:使用大 V 限流器
return self.influencer_limiter.allow()
else:
# 普通用户:使用普通限流器
return self.normal_limiter.allow()
# 使用示例
post_limiter = PostRateLimiter()
# 普通用户发帖
if post_limiter.allow_post(user_id=123, follower_count=100):
print("允许发帖")
else:
print("发帖频率过高,请稍后再试")
# 大 V 发帖
if post_limiter.allow_post(user_id=789, follower_count=50000):
print("允许发帖")
else:
print("发帖频率过高,请稍后再试")
📊 性能优化总结
| 优化项 | 优化前 | 优化后 | 提升倍数 |
|---|---|---|---|
| 批量推送 | 1000 秒 | 10 秒 | 100x |
| 异步解耦 | 同步阻塞 | 异步非阻塞 | 10x |
| 并行拉取 | 1000ms | 50ms | 20x |
| 游标分页 | 深分页慢 | 恒定速度 | 100x |
| 写入限流 | 系统雪崩 | 平滑流量 | N/A |
📡 实时性保证:毫秒级内容触达
Q: 如何保证用户发帖后,粉丝能在毫秒级内看到新内容?
Feed 流的实时性是用户体验的核心。通过 WebSocket 推送、消息队列优化、延迟监控,可以实现毫秒级的内容触达。
1️⃣ WebSocket 实时推送
架构图:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 用户 A 发帖 │────▶│ Kafka │────▶│ 推送服务 │
│ │ │ 消息队列 │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ WebSocket │
│ 服务器 │
└─────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 粉丝 B │ │ 粉丝 C │ │ 粉丝 D │
│ WebSocket│ │ WebSocket│ │ WebSocket│
└─────────┘ └─────────┘ └─────────┘
实现代码:
python
import asyncio
import json
from websockets.server import serve
from typing import Set
class WebSocketFeedServer:
"""
WebSocket Feed 流服务器
"""
def __init__(self):
self.connections: Dict[int, Set] = {} # user_id -> WebSocket 连接集合
async def register(self, user_id: int, websocket):
"""注册用户连接"""
if user_id not in self.connections:
self.connections[user_id] = set()
self.connections[user_id].add(websocket)
logger.info(f"用户 {user_id} 连接成功")
async def unregister(self, user_id: int, websocket):
"""注销用户连接"""
if user_id in self.connections:
self.connections[user_id].discard(websocket)
if not self.connections[user_id]:
del self.connections[user_id]
logger.info(f"用户 {user_id} 断开连接")
async def broadcast_to_followers(self, post: Dict, followers: List[int]):
"""
向粉丝广播新帖子
"""
message = {
'type': 'new_post',
'post': post,
'timestamp': time.time()
}
# 并行推送给所有在线粉丝
tasks = []
for follower_id in followers:
if follower_id in self.connections:
for websocket in self.connections[follower_id]:
tasks.append(self._send_to_client(websocket, message))
# 并行发送
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
logger.info(f"推送完成: post_id={post['id']}, 在线粉丝={len(tasks)}")
async def _send_to_client(self, websocket, message):
"""发送消息给客户端"""
try:
await websocket.send(json.dumps(message, ensure_ascii=False))
except Exception as e:
logger.error(f"发送消息失败: {e}")
async def handle_client(self, websocket, path):
"""处理客户端连接"""
# 获取用户 ID(从 URL 参数或认证 token)
user_id = await self._authenticate(websocket)
# 注册连接
await self.register(user_id, websocket)
try:
# 保持连接,处理客户端消息
async for message in websocket:
data = json.loads(message)
await self._handle_client_message(user_id, data)
finally:
# 注销连接
await self.unregister(user_id, websocket)
async def _authenticate(self, websocket) -> int:
"""认证用户"""
# 实现认证逻辑
pass
async def _handle_client_message(self, user_id: int, data: Dict):
"""处理客户端消息"""
# 实现消息处理逻辑
pass
# 启动 WebSocket 服务器
async def start_websocket_server():
server = WebSocketFeedServer()
async with serve(server.handle_client, "localhost", 8765):
await asyncio.Future() # 永久运行
# 使用示例
# 在推送服务中集成 WebSocket
async def push_to_followers_realtime(post: Dict, followers: List[int]):
"""实时推送给粉丝"""
# 1. 推送到 Redis(用于离线用户)
push_service.push_to_followers(post, followers)
# 2. 推送到 WebSocket(用于在线用户)
await ws_server.broadcast_to_followers(post, followers)
2️⃣ 消息队列延迟监控
目标:监控推送延迟,及时发现性能问题。
python
import time
from prometheus_client import Counter, Histogram, Gauge
# Prometheus 指标
push_latency = Histogram(
'feed_push_latency_seconds',
'Feed 推送延迟',
['user_type'] # 普通用户、大 V、超级大 V
)
push_success = Counter(
'feed_push_success_total',
'Feed 推送成功次数',
['user_type']
)
push_failure = Counter(
'feed_push_failure_total',
'Feed 推送失败次数',
['user_type', 'error_type']
)
push_queue_size = Gauge(
'feed_push_queue_size',
'Feed 推送队列大小'
)
class MonitoredPushService:
"""
带监控的推送服务
"""
def __init__(self, push_service):
self.push_service = push_service
def push_to_followers(self, post: Dict, followers: List[int]):
"""
推送(带监控)
"""
start_time = time.time()
# 判断用户类型
user_type = self._get_user_type(post['user_id'], len(followers))
try:
# 执行推送
self.push_service.push_to_followers(post, followers)
# 记录成功
push_success.labels(user_type=user_type).inc()
# 记录延迟
latency = time.time() - start_time
push_latency.labels(user_type=user_type).observe(latency)
logger.info(f"推送成功: post_id={post['id']}, 延迟={latency:.3f}s")
except Exception as e:
# 记录失败
push_failure.labels(user_type=user_type, error_type=type(e).__name__).inc()
logger.error(f"推送失败: {e}")
raise
def _get_user_type(self, user_id: int, follower_count: int) -> str:
"""获取用户类型"""
if follower_count >= 1000000:
return 'super_influencer'
elif follower_count >= 10000:
return 'influencer'
else:
return 'normal'
# Kafka 消费者延迟监控
class MonitoredPushConsumer:
"""
带监控的推送消费者
"""
def __init__(self, kafka_consumer, push_service):
self.consumer = kafka_consumer
self.push_service = MonitoredPushService(push_service)
def start(self):
"""启动消费者"""
for message in self.consumer:
try:
# 计算消息延迟(从发送到消费的时间)
task = message.value
message_latency = time.time() - task['timestamp']
# 记录队列大小
push_queue_size.set(self.consumer._consumer._fetcher._subscriptions.subscription.assignment.lag())
# 处理任务
self.push_service.push_to_followers(task['post'], task['followers'])
logger.info(f"消息处理完成: task_id={task['task_id']}, 消息延迟={message_latency:.3f}s")
except Exception as e:
logger.error(f"处理任务失败: {e}")
3️⃣ 推送进度追踪
目标:追踪推送进度,确保所有粉丝都能收到新内容。
python
from typing import Dict, List
import redis
class PushProgressTracker:
"""
推送进度追踪器
"""
def __init__(self, redis_client):
self.redis = redis_client
def start_push_task(self, post_id: int, total_followers: int) -> str:
"""
开始推送任务
"""
task_id = generate_task_id()
# 初始化任务状态
task_key = f"push_task:{task_id}"
self.redis.hset(task_key, mapping={
'post_id': post_id,
'total_followers': total_followers,
'pushed_count': 0,
'failed_count': 0,
'status': 'running',
'start_time': time.time()
})
# 设置过期时间(24 小时)
self.redis.expire(task_key, 86400)
logger.info(f"推送任务开始: task_id={task_id}, post_id={post_id}, 粉丝数={total_followers}")
return task_id
def update_progress(self, task_id: str, pushed_count: int, failed_count: int):
"""
更新推送进度
"""
task_key = f"push_task:{task_id}"
self.redis.hset(task_key, mapping={
'pushed_count': pushed_count,
'failed_count': failed_count,
'update_time': time.time()
})
def complete_push_task(self, task_id: str, status: str = 'completed'):
"""
完成推送任务
"""
task_key = f"push_task:{task_id}"
self.redis.hset(task_key, mapping={
'status': status,
'end_time': time.time()
})
logger.info(f"推送任务完成: task_id={task_id}, status={status}")
def get_push_progress(self, task_id: str) -> Dict:
"""
获取推送进度
"""
task_key = f"push_task:{task_id}"
task_data = self.redis.hgetall(task_key)
if task_data:
return {
'post_id': int(task_data.get('post_id', 0)),
'total_followers': int(task_data.get('total_followers', 0)),
'pushed_count': int(task_data.get('pushed_count', 0)),
'failed_count': int(task_data.get('failed_count', 0)),
'status': task_data.get('status', 'unknown'),
'start_time': float(task_data.get('start_time', 0)),
'end_time': float(task_data.get('end_time', 0))
}
return None
# 使用示例
tracker = PushProgressTracker(redis_client)
# 开始推送任务
task_id = tracker.start_push_task(post_id=12345, total_followers=10000000)
# 更新进度
tracker.update_progress(task_id, pushed_count=5000000, failed_count=100)
# 完成任务
tracker.complete_push_task(task_id, status='completed')
# 查询进度
progress = tracker.get_push_progress(task_id)
print(f"推送进度: {progress}")
4️⃣ 实时性优化总结
| 优化项 | 实现方式 | 效果 |
|---|---|---|
| WebSocket 推送 | 在线用户实时推送 | 毫秒级触达 |
| 消息队列监控 | Prometheus + Grafana | 实时监控延迟 |
| 进度追踪 | Redis 存储任务状态 | 确保推送完成 |
| 批量推送 | Redis Pipeline | 提升吞吐量 |
🔒 一致性保证:分布式环境下的数据一致性
Q: 如何保证推送过程中的一致性?如果推送失败,如何恢复?
分布式环境下的一致性是 Feed 流系统的核心挑战。通过幂等性保证、事务性消息、最终一致性监控,可以确保数据的准确性和可靠性。
1️⃣ 幂等性保证
问题:消息可能重复投递,如何避免重复推送?
解决方案:使用唯一 ID + 去重表。
sql
-- 推送记录表
CREATE TABLE push_records (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
post_id BIGINT NOT NULL,
follower_id BIGINT NOT NULL,
task_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_post_follower (post_id, follower_id),
INDEX idx_task_id (task_id)
);
python
import redis
class IdempotentPushService:
"""
幂等性推送服务
"""
def __init__(self, redis_client, db):
self.redis = redis_client
self.db = db
def push_to_follower(self, post_id: int, follower_id: int) -> bool:
"""
推送给单个粉丝(幂等)
"""
# 1. 检查是否已推送(Redis 快速检查)
push_key = f"pushed:{post_id}:{follower_id}"
if self.redis.exists(push_key):
logger.info(f"已推送,跳过: post_id={post_id}, follower_id={follower_id}")
return True
# 2. 检查数据库(确保幂等)
result = self.db.query(
"SELECT id FROM push_records WHERE post_id = ? AND follower_id = ?",
[post_id, follower_id]
)
if result:
# 已推送,写入 Redis 缓存
self.redis.setex(push_key, 86400, 1) # 缓存 24 小时
return True
# 3. 执行推送
try:
# 推送到 Redis Feed 流
feed_key = f"feed:{follower_id}"
post_data = self._get_post_data(post_id)
self.redis.zadd(feed_key, {str(post_id): post_data['timestamp']})
self.redis.zremrangebyrank(feed_key, 0, -101) # 保留最新 100 条
self.redis.expire(feed_key, 86400)
# 4. 记录推送记录
self.db.execute(
"INSERT INTO push_records (post_id, follower_id, task_id) VALUES (?, ?, ?)",
[post_id, follower_id, generate_task_id()]
)
# 5. 写入 Redis 缓存
self.redis.setex(push_key, 86400, 1)
logger.info(f"推送成功: post_id={post_id}, follower_id={follower_id}")
return True
except Exception as e:
logger.error(f"推送失败: {e}")
return False
def _get_post_data(self, post_id: int) -> Dict:
"""获取帖子数据"""
# 从数据库或缓存获取帖子数据
pass
# 使用示例
push_service = IdempotentPushService(redis_client, db)
# 推送给粉丝(即使重复调用也只推送一次)
push_service.push_to_follower(post_id=12345, follower_id=678)
push_service.push_to_follower(post_id=12345, follower_id=678) # 重复调用,自动跳过
2️⃣ 事务性消息
问题:如何保证发帖和推送的原子性?
解决方案:使用 Kafka 事务性消息。
python
from kafka import KafkaProducer
import json
class TransactionalPushService:
"""
事务性推送服务
"""
def __init__(self, kafka_servers):
self.producer = KafkaProducer(
bootstrap_servers=kafka_servers,
value_serializer=lambda v: json.dumps(v).encode('utf-8'),
transactional_id='feed-push-transaction' # 事务 ID
)
self.producer.init_transactions()
def create_post_with_push(self, user_id: int, content: str, followers: List[int]):
"""
创建帖子并推送(事务性)
"""
# 开始事务
self.producer.begin_transaction()
try:
# 1. 创建帖子(写入数据库)
post_id = self._create_post_in_db(user_id, content)
# 2. 发送帖子创建事件
post_event = {
'type': 'post_created',
'post_id': post_id,
'user_id': user_id,
'content': content,
'timestamp': time.time()
}
self.producer.send(
'feed-events',
value=post_event,
key=str(user_id)
)
# 3. 发送推送任务
push_task = {
'post_id': post_id,
'followers': followers,
'task_id': generate_task_id(),
'timestamp': time.time()
}
self.producer.send(
'feed-push',
value=push_task,
key=str(post_id)
)
# 提交事务
self.producer.commit_transaction()
logger.info(f"创建帖子并推送成功: post_id={post_id}")
return post_id
except Exception as e:
# 回滚事务
self.producer.abort_transaction()
logger.error(f"创建帖子失败,事务已回滚: {e}")
raise
def _create_post_in_db(self, user_id: int, content: str) -> int:
"""在数据库中创建帖子"""
# 实现数据库插入逻辑
pass
# 使用示例
transactional_service = TransactionalPushService(kafka_servers='localhost:9092')
# 创建帖子并推送(事务性)
post_id = transactional_service.create_post_with_push(
user_id=123,
content='这是一条微博',
followers=get_followers(123)
)
3️⃣ 最终一致性监控
目标:监控推送完成率,及时发现不一致问题。
python
from typing import Dict, List
import time
class ConsistencyMonitor:
"""
一致性监控器
"""
def __init__(self, db, redis_client):
self.db = db
self.redis = redis_client
def check_push_completeness(self, post_id: int) -> Dict:
"""
检查推送完成度
"""
# 1. 获取帖子信息
post = self.db.query(
"SELECT user_id, created_at FROM posts WHERE id = ?",
[post_id]
)
if not post:
return {'error': '帖子不存在'}
# 2. 获取粉丝数量
follower_count = self.db.query(
"SELECT COUNT(*) as count FROM follow_relations WHERE followee_id = ?",
[post['user_id']]
)['count']
# 3. 获取已推送数量
pushed_count = self.db.query(
"SELECT COUNT(*) as count FROM push_records WHERE post_id = ?",
[post_id]
)['count']
# 4. 计算完成率
completeness = (pushed_count / follower_count * 100) if follower_count > 0 else 0
return {
'post_id': post_id,
'follower_count': follower_count,
'pushed_count': pushed_count,
'completeness': completeness,
'status': 'completed' if completeness >= 99.9 else 'incomplete'
}
def repair_incomplete_push(self, post_id: int):
"""
修复未完成的推送
"""
# 1. 获取帖子信息
post = self.db.query(
"SELECT user_id FROM posts WHERE id = ?",
[post_id]
)
# 2. 获取所有粉丝
all_followers = self.db.query(
"SELECT follower_id FROM follow_relations WHERE followee_id = ?",
[post['user_id']]
)
all_follower_ids = [f['follower_id'] for f in all_followers]
# 3. 获取已推送的粉丝
pushed_followers = self.db.query(
"SELECT follower_id FROM push_records WHERE post_id = ?",
[post_id]
)
pushed_follower_ids = [f['follower_id'] for f in pushed_followers]
# 4. 找出未推送的粉丝
missing_followers = set(all_follower_ids) - set(pushed_follower_ids)
if missing_followers:
logger.info(f"发现未推送粉丝: post_id={post_id}, 数量={len(missing_followers)}")
# 5. 补充推送
for follower_id in missing_followers:
push_service.push_to_follower(post_id, follower_id)
logger.info(f"补充推送完成: post_id={post_id}, 数量={len(missing_followers)}")
else:
logger.info(f"推送完整,无需修复: post_id={post_id}")
def monitor_all_posts(self, hours=24):
"""
监控最近 N 小时的帖子
"""
# 获取最近 N 小时的帖子
posts = self.db.query(
"""
SELECT id FROM posts
WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? HOUR)
ORDER BY created_at DESC
""",
[hours]
)
incomplete_posts = []
for post in posts:
result = self.check_push_completeness(post['id'])
if result['status'] == 'incomplete':
incomplete_posts.append(result)
logger.warning(
f"推送不完整: post_id={result['post_id']}, "
f"完成率={result['completeness']:.2f}%"
)
return incomplete_posts
# 使用示例
monitor = ConsistencyMonitor(db, redis_client)
# 检查单个帖子的推送完成度
result = monitor.check_push_completeness(post_id=12345)
print(f"推送完成度: {result}")
# 修复未完成的推送
monitor.repair_incomplete_push(post_id=12345)
# 监控最近 24 小时的帖子
incomplete_posts = monitor.monitor_all_posts(hours=24)
print(f"发现 {len(incomplete_posts)} 个未完成的推送")
4️⃣ 数据校验
目标:定期校验数据一致性。
python
class DataValidator:
"""
数据校验器
"""
def __init__(self, db, redis_client):
self.db = db
self.redis = redis_client
def validate_feed_consistency(self, user_id: int) -> Dict:
"""
校验用户 Feed 流的一致性
"""
# 1. 获取关注列表
followees = self.db.query(
"SELECT followee_id FROM follow_relations WHERE follower_id = ?",
[user_id]
)
followee_ids = [f['followee_id'] for f in followees]
if not followee_ids:
return {'status': 'no_followees'}
# 2. 从 Redis 获取 Feed 流
feed_key = f"feed:{user_id}"
redis_feed = self.redis.zrevrange(feed_key, 0, -1, withscores=True)
redis_post_ids = [int(post_id) for post_id, score in redis_feed]
# 3. 从数据库计算期望的 Feed 流
expected_post_ids = self._calculate_expected_feed(user_id, followee_ids)
# 4. 对比差异
missing_posts = set(expected_post_ids[:100]) - set(redis_post_ids)
extra_posts = set(redis_post_ids) - set(expected_post_ids)
is_consistent = len(missing_posts) == 0 and len(extra_posts) == 0
return {
'user_id': user_id,
'is_consistent': is_consistent,
'missing_posts': list(missing_posts),
'extra_posts': list(extra_posts),
'redis_count': len(redis_post_ids),
'expected_count': len(expected_post_ids)
}
def _calculate_expected_feed(self, user_id: int, followee_ids: List[int]) -> List[int]:
"""计算期望的 Feed 流"""
# 从数据库获取所有关注对象的帖子
posts = self.db.query(
"""
SELECT id FROM posts
WHERE user_id IN ({})
ORDER BY created_at DESC
LIMIT 100
""".format(','.join(map(str, followee_ids)))
)
return [p['id'] for p in posts]
def repair_feed(self, user_id: int):
"""
修复不一致的 Feed 流
"""
validation_result = self.validate_feed_consistency(user_id)
if validation_result['is_consistent']:
logger.info(f"Feed 流一致,无需修复: user_id={user_id}")
return
logger.warning(
f"Feed 流不一致: user_id={user_id}, "
f"缺失={len(validation_result['missing_posts'])}, "
f"多余={len(validation_result['extra_posts'])}"
)
# 1. 清空当前 Feed 流
feed_key = f"feed:{user_id}"
self.redis.delete(feed_key)
# 2. 重新构建 Feed 流
followees = self.db.query(
"SELECT followee_id FROM follow_relations WHERE follower_id = ?",
[user_id]
)
followee_ids = [f['followee_id'] for f in followees]
expected_post_ids = self._calculate_expected_feed(user_id, followee_ids)
# 3. 批量写入 Redis
for post_id in expected_post_ids:
post_data = self._get_post_data(post_id)
self.redis.zadd(feed_key, {str(post_id): post_data['timestamp']})
logger.info(f"Feed 流修复完成: user_id={user_id}")
# 使用示例
validator = DataValidator(db, redis_client)
# 校验用户 Feed 流一致性
result = validator.validate_feed_consistency(user_id=123)
print(f"一致性检查结果: {result}")
# 修复不一致的 Feed 流
validator.repair_feed(user_id=123)
📊 一致性保证总结
| 机制 | 实现方式 | 效果 |
|---|---|---|
| 幂等性 | 唯一 ID + 去重表 | 避免重复推送 |
| 事务性消息 | Kafka 事务 | 保证原子性 |
| 一致性监控 | 定期检查推送完成率 | 及时发现问题 |
| 数据校验 | 对比 Redis 和数据库 | 确保数据准确 |
📊 监控与运维:保障系统稳定运行
Q: 如何监控系统运行状态,及时发现和解决问题?
完善的监控和运维体系是 Feed 流系统稳定运行的保障。通过关键指标监控、告警策略、故障排查,可以快速定位和解决问题。
1️⃣ 关键监控指标
python
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time
# Feed 流相关指标
feed_read_total = Counter(
'feed_read_total',
'Feed 流读取总次数',
['user_type', 'strategy'] # user_type: normal/influencer, strategy: push/pull/hybrid
)
feed_read_latency = Histogram(
'feed_read_latency_seconds',
'Feed 流读取延迟',
['user_type', 'strategy'],
buckets=[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0]
)
feed_cache_hit_rate = Gauge(
'feed_cache_hit_rate',
'Feed 流缓存命中率',
['cache_level'] # cache_level: local/redis
)
feed_push_total = Counter(
'feed_push_total',
'Feed 流推送总次数',
['user_type']
)
feed_push_latency = Histogram(
'feed_push_latency_seconds',
'Feed 流推送延迟',
['user_type'],
buckets=[0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0, 300.0]
)
feed_push_queue_size = Gauge(
'feed_push_queue_size',
'Feed 推送队列大小'
)
feed_push_failure_rate = Gauge(
'feed_push_failure_rate',
'Feed 推送失败率',
['user_type']
)
# 数据库相关指标
db_query_total = Counter(
'db_query_total',
'数据库查询总次数',
['query_type']
)
db_query_latency = Histogram(
'db_query_latency_seconds',
'数据库查询延迟',
['query_type']
)
# Redis 相关指标
redis_command_total = Counter(
'redis_command_total',
'Redis 命令总次数',
['command']
)
redis_command_latency = Histogram(
'redis_command_latency_seconds',
'Redis 命令延迟',
['command']
)
# WebSocket 相关指标
websocket_connections = Gauge(
'websocket_connections',
'WebSocket 连接数'
)
websocket_messages_sent = Counter(
'websocket_messages_sent',
'WebSocket 发送消息数'
)
# 系统资源指标
system_cpu_usage = Gauge(
'system_cpu_usage',
'系统 CPU 使用率'
)
system_memory_usage = Gauge(
'system_memory_usage',
'系统内存使用率'
)
system_disk_usage = Gauge(
'system_disk_usage',
'系统磁盘使用率'
)
class FeedMetricsCollector:
"""
Feed 流指标收集器
"""
def record_feed_read(self, user_id: int, strategy: str, latency: float, cache_hit: bool):
"""
记录 Feed 流读取
"""
# 判断用户类型
user_type = self._get_user_type(user_id)
# 记录读取次数
feed_read_total.labels(user_type=user_type, strategy=strategy).inc()
# 记录延迟
feed_read_latency.labels(user_type=user_type, strategy=strategy).observe(latency)
# 记录缓存命中
cache_level = 'local' if cache_hit else 'redis'
feed_cache_hit_rate.labels(cache_level=cache_level).set(1 if cache_hit else 0)
def record_feed_push(self, user_id: int, followers_count: int, latency: float, success: bool):
"""
记录 Feed 流推送
"""
user_type = self._get_user_type_by_followers(followers_count)
# 记录推送次数
feed_push_total.labels(user_type=user_type).inc()
# 记录延迟
feed_push_latency.labels(user_type=user_type).observe(latency)
# 记录失败率
if not success:
feed_push_failure_rate.labels(user_type=user_type).inc()
def _get_user_type(self, user_id: int) -> str:
"""获取用户类型"""
follower_count = get_follower_count(user_id)
return self._get_user_type_by_followers(follower_count)
def _get_user_type_by_followers(self, follower_count: int) -> str:
"""根据粉丝数获取用户类型"""
if follower_count >= 1000000:
return 'super_influencer'
elif follower_count >= 10000:
return 'influencer'
else:
return 'normal'
# 启动 Prometheus 指标服务器
start_http_server(8000)
2️⃣ Grafana 仪表盘配置
yaml
# Grafana Dashboard: Feed 流系统监控
dashboard:
title: "Feed 流系统监控"
panels:
- title: "Feed 流读取 QPS"
type: graph
targets:
- expr: rate(feed_read_total[5m])
legendFormat: "{{user_type}} - {{strategy}}"
- title: "Feed 流读取延迟 (P95)"
type: graph
targets:
- expr: histogram_quantile(0.95, rate(feed_read_latency_seconds_bucket[5m]))
legendFormat: "{{user_type}} - {{strategy}}"
- title: "Feed 流缓存命中率"
type: graph
targets:
- expr: feed_cache_hit_rate
legendFormat: "{{cache_level}}"
- title: "Feed 流推送 QPS"
type: graph
targets:
- expr: rate(feed_push_total[5m])
legendFormat: "{{user_type}}"
- title: "Feed 流推送延迟 (P95)"
type: graph
targets:
- expr: histogram_quantile(0.95, rate(feed_push_latency_seconds_bucket[5m]))
legendFormat: "{{user_type}}"
- title: "Feed 流推送队列大小"
type: graph
targets:
- expr: feed_push_queue_size
- title: "Feed 流推送失败率"
type: graph
targets:
- expr: rate(feed_push_failure_rate[5m])
legendFormat: "{{user_type}}"
- title: "WebSocket 连接数"
type: graph
targets:
- expr: websocket_connections
- title: "数据库查询延迟"
type: graph
targets:
- expr: histogram_quantile(0.95, rate(db_query_latency_seconds_bucket[5m]))
legendFormat: "{{query_type}}"
- title: "Redis 命令延迟"
type: graph
targets:
- expr: histogram_quantile(0.95, rate(redis_command_latency_seconds_bucket[5m]))
legendFormat: "{{command}}"
- title: "系统资源使用率"
type: graph
targets:
- expr: system_cpu_usage
legendFormat: "CPU"
- expr: system_memory_usage
legendFormat: "Memory"
- expr: system_disk_usage
legendFormat: "Disk"
3️⃣ 告警规则
yaml
# Prometheus Alert Rules
groups:
- name: feed_system_alerts
rules:
# Feed 流读取延迟告警
- alert: HighFeedReadLatency
expr: histogram_quantile(0.95, rate(feed_read_latency_seconds_bucket[5m])) > 1.0
for: 5m
labels:
severity: warning
annotations:
summary: "Feed 流读取延迟过高"
description: "Feed 流读取 P95 延迟超过 1 秒: {{ $value }}s"
# Feed 流推送延迟告警
- alert: HighFeedPushLatency
expr: histogram_quantile(0.95, rate(feed_push_latency_seconds_bucket[5m])) > 60.0
for: 5m
labels:
severity: warning
annotations:
summary: "Feed 流推送延迟过高"
description: "Feed 流推送 P95 延迟超过 60 秒: {{ $value }}s"
# Feed 流推送队列积压告警
- alert: FeedPushQueueBacklog
expr: feed_push_queue_size > 100000
for: 5m
labels:
severity: critical
annotations:
summary: "Feed 流推送队列积压"
description: "推送队列积压超过 10 万条: {{ $value }}"
# Feed 流推送失败率告警
- alert: HighFeedPushFailureRate
expr: rate(feed_push_failure_rate[5m]) > 0.01
for: 5m
labels:
severity: critical
annotations:
summary: "Feed 流推送失败率过高"
description: "Feed 流推送失败率超过 1%: {{ $value }}"
# 缓存命中率告警
- alert: LowCacheHitRate
expr: feed_cache_hit_rate{cache_level="redis"} < 0.8
for: 10m
labels:
severity: warning
annotations:
summary: "Redis 缓存命中率过低"
description: "Redis 缓存命中率低于 80%: {{ $value }}"
# WebSocket 连接数告警
- alert: HighWebSocketConnections
expr: websocket_connections > 100000
for: 5m
labels:
severity: warning
annotations:
summary: "WebSocket 连接数过高"
description: "WebSocket 连接数超过 10 万: {{ $value }}"
# 数据库查询延迟告警
- alert: HighDBQueryLatency
expr: histogram_quantile(0.95, rate(db_query_latency_seconds_bucket[5m])) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "数据库查询延迟过高"
description: "数据库查询 P95 延迟超过 500ms: {{ $value }}s"
# Redis 命令延迟告警
- alert: HighRedisCommandLatency
expr: histogram_quantile(0.95, rate(redis_command_latency_seconds_bucket[5m])) > 0.01
for: 5m
labels:
severity: warning
annotations:
summary: "Redis 命令延迟过高"
description: "Redis 命令 P95 延迟超过 10ms: {{ $value }}s"
4️⃣ 故障排查
问题 1:Feed 流读取延迟过高
排查步骤:
python
def diagnose_feed_read_latency(user_id: int):
"""
诊断 Feed 流读取延迟
"""
print(f"=== 诊断 Feed 流读取延迟: user_id={user_id} ===")
# 1. 检查本地缓存
start = time.time()
local_cache_hit = local_cache.get(user_id) is not None
local_cache_latency = (time.time() - start) * 1000
print(f"本地缓存: 命中={local_cache_hit}, 延迟={local_cache_latency:.2f}ms")
# 2. 检查 Redis 缓存
start = time.time()
redis_cache_hit = redis_cache.get(user_id) is not None
redis_cache_latency = (time.time() - start) * 1000
print(f"Redis 缓存: 命中={redis_cache_hit}, 延迟={redis_cache_latency:.2f}ms")
# 3. 检查数据库查询
if not redis_cache_hit:
start = time.time()
followees = get_followees(user_id)
db_query_latency = (time.time() - start) * 1000
print(f"数据库查询: 关注数={len(followees)}, 延迟={db_query_latency:.2f}ms")
# 4. 检查并行拉取
start = time.time()
feed_items = pull_service.pull_feed(user_id, followees, limit=20)
pull_latency = (time.time() - start) * 1000
print(f"并行拉取: 帖子数={len(feed_items)}, 延迟={pull_latency:.2f}ms")
# 5. 检查网络延迟
import subprocess
result = subprocess.run(['ping', '-c', '3', 'redis-server'], capture_output=True)
print(f"Redis 网络延迟: {result.stdout.decode()}")
# 使用示例
diagnose_feed_read_latency(user_id=123)
问题 2:推送队列积压
排查步骤:
python
def diagnose_push_queue_backlog():
"""
诊断推送队列积压
"""
print("=== 诊断推送队列积压 ===")
# 1. 检查队列大小
queue_size = feed_push_queue_size._value.get()
print(f"队列大小: {queue_size}")
# 2. 检查消费者数量
consumer_count = get_kafka_consumer_count('feed-push')
print(f"消费者数量: {consumer_count}")
# 3. 检查消费者延迟
consumer_lag = get_kafka_consumer_lag('feed-push')
print(f"消费者延迟: {consumer_lag}")
# 4. 检查推送失败率
failure_rate = feed_push_failure_rate._value.get()
print(f"推送失败率: {failure_rate}")
# 5. 检查 Redis 性能
redis_info = redis_client.info()
print(f"Redis CPU 使用率: {redis_info['used_cpu_sys']}")
print(f"Redis 内存使用: {redis_info['used_memory_human']}")
# 6. 检查数据库性能
db_stats = db.query("SHOW STATUS LIKE 'Threads_connected'")
print(f"数据库连接数: {db_stats}")
# 使用示例
diagnose_push_queue_backlog()
5️⃣ 日志追踪
python
import logging
from opentelemetry import trace
from opentelemetry.instrumentation.logging import LoggingInstrumentor
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# 启用 OpenTelemetry 日志追踪
LoggingInstrumentor().instrument()
class FeedService:
"""
Feed 流服务(带日志追踪)
"""
def __init__(self):
self.logger = logging.getLogger(__name__)
self.tracer = trace.get_tracer(__name__)
def get_feed(self, user_id: int) -> List[Dict]:
"""
获取 Feed 流(带日志追踪)
"""
with self.tracer.start_as_current_span("get_feed") as span:
span.set_attribute("user_id", user_id)
self.logger.info(f"开始获取 Feed 流: user_id={user_id}")
# 检查缓存
with self.tracer.start_as_current_span("check_cache"):
feed_items = local_cache.get(user_id)
if feed_items:
self.logger.info(f"本地缓存命中: user_id={user_id}")
return feed_items
feed_items = redis_cache.get(user_id)
if feed_items:
self.logger.info(f"Redis 缓存命中: user_id={user_id}")
local_cache.put(user_id, feed_items)
return feed_items
# 从数据库加载
with self.tracer.start_as_current_span("load_from_db"):
self.logger.info(f"缓存未命中,从数据库加载: user_id={user_id}")
followees = get_followees(user_id)
feed_items = pull_service.pull_feed(user_id, followees)
# 写入缓存
redis_cache.put(user_id, feed_items)
local_cache.put(user_id, feed_items)
self.logger.info(f"Feed 流获取完成: user_id={user_id}, 帖子数={len(feed_items)}")
return feed_items
# 使用示例
feed_service = FeedService()
feed_items = feed_service.get_feed(user_id=123)
📊 监控运维总结
| 维度 | 监控指标 | 告警阈值 | 处理建议 |
|---|---|---|---|
| 读取性能 | P95 延迟 | > 1s | 检查缓存命中率、数据库性能 |
| 推送性能 | P95 延迟 | > 60s | 检查队列积压、消费者数量 |
| 推送队列 | 队列大小 | > 10 万 | 增加消费者、优化推送逻辑 |
| 缓存 | 命中率 | < 80% | 检查缓存配置、预热策略 |
| WebSocket | 连接数 | > 10 万 | 检查连接管理、负载均衡 |
🔁 记忆锚点
python
def feed_strategy(follower_count):
"""
Feed 流策略选择(生产级)
小 V(<5000 粉丝)→ 推模式:写时扩散,读时直接
大 V(>5000 粉丝)→ 拉模式:写时简单,读时聚合
超级大 V(>100 万粉丝)→ 混合模式:推送给活跃粉丝,拉取给普通粉丝
"""
if follower_count < 5000:
return "push" # 推模式:写扩散
elif follower_count < 1000000:
return "pull" # 拉模式:读扩散
else:
return "hybrid" # 混合模式
def push_mode(post, followers, batch_size=1000, max_retries=3):
"""
推模式:写时扩散(生产级)
写入放大:1 条帖子 → N 个粉丝收件箱
:param post: 帖子数据
:param followers: 粉丝 ID 列表
:param batch_size: 批量大小
:param max_retries: 最大重试次数
"""
total_followers = len(followers)
logger.info(f"开始推送: post_id={post.id}, 粉丝数={total_followers}")
# 分批处理
for i in range(0, total_followers, batch_size):
batch = followers[i:i + batch_size]
# 使用 Redis Pipeline 批量写入
for attempt in range(max_retries):
try:
pipeline = redis_client.pipeline()
for follower_id in batch:
feed_key = f"feed:{follower_id}"
pipeline.zadd(feed_key, {str(post.id): post.timestamp})
pipeline.zremrangebyrank(feed_key, 0, -101) # 保留最新 100 条
pipeline.expire(feed_key, 86400)
results = pipeline.execute()
if all(results):
break # 成功
else:
raise Exception("Pipeline 执行失败")
except Exception as e:
if attempt < max_retries - 1:
logger.warning(f"推送失败,重试 {attempt + 1}/{max_retries}: {e}")
time.sleep(2 ** attempt) # 指数退避
else:
logger.error(f"推送最终失败: {e}")
raise
progress = min(i + batch_size, total_followers)
logger.info(f"推送进度: {progress}/{total_followers}")
logger.info(f"推送完成: post_id={post.id}")
def pull_mode(user, followees, limit=20, max_workers=10):
"""
拉模式:读时聚合(生产级)
读取延迟:需要聚合 N 个关注对象的内容
:param user: 用户 ID
:param followees: 关注对象列表
:param limit: 返回数量
:param max_workers: 最大并行数
"""
logger.info(f"开始拉取 Feed: user_id={user}, 关注数={len(followees)}")
# 并行拉取关注对象的内容
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(redis_cache.get_outbox, followee, limit=100): followee
for followee in followees
}
# 使用最小堆进行归并排序
heap = []
for future in concurrent.futures.as_completed(futures):
followee = futures[future]
try:
posts = future.result(timeout=5) # 5 秒超时
if posts:
heapq.heappush(heap, (-posts[0]['timestamp'], 0, posts, followee))
except Exception as e:
logger.error(f"拉取失败: followee={followee}, error={e}")
# 归并排序
result = []
while heap and len(result) < limit:
neg_timestamp, idx, posts, followee = heapq.heappop(heap)
result.append(posts[idx])
if idx + 1 < len(posts):
heapq.heappush(heap, (-posts[idx + 1]['timestamp'], idx + 1, posts, followee))
logger.info(f"拉取完成: user_id={user}, 返回={len(result)} 条")
return result
def hybrid_mode(post, followers, active_followers, batch_size=1000):
"""
混合模式:推拉结合(生产级)
- 活跃粉丝:推模式
- 普通粉丝:拉模式
:param post: 帖子数据
:param followers: 所有粉丝
:param active_followers: 活跃粉丝(最近 7 天登录)
:param batch_size: 批量大小
"""
logger.info(f"混合模式: post_id={post.id}, 总粉丝={len(followers)}, 活跃粉丝={len(active_followers)}")
# 推送给活跃粉丝
if active_followers:
logger.info(f"推送给活跃粉丝: {len(active_followers)}")
push_mode(post, active_followers, batch_size)
# 普通粉丝使用拉模式
inactive_followers = set(followers) - set(active_followers)
logger.info(f"普通粉丝使用拉模式: {len(inactive_followers)}")
logger.info(f"混合模式完成: post_id={post.id}")
一句话本质:Feed 流架构通过"写时复制"换"读时快速",或"写时简单"换"读时聚合",推拉结合是在两者间的动态权衡,混合模式则根据用户活跃度进一步优化。