广告投放系统的高并发Redis架构复盘:从单集群200万QPS到分片集群的性能演进

广告投放系统的高并发Redis架构复盘:从单集群200万QPS到分片集群的性能演进 广告投放系统的高并发Redis架构复盘从单集群200万QPS到分片集群的性能演进一、背景与问题广告投放系统的核心业务链路包括用户画像匹配、广告排序、频次控制与实时计费四个环节每个环节都依赖Redis作为实时状态存储。在2024年的架构中系统采用单Redis集群6主6从每节点128GB内存支撑约200万QPS的读写请求——该架构在业务增长到350万QPS时出现了严重瓶颈单节点内存压力用户画像数据持续膨胀单节点内存使用率达到85%触发频繁的key淘汰与swap操作P99延迟从2ms飙升至50ms网络带宽瓶颈6主节点的总入流量达到1.2Gbps接近单节点网络卡的上限高峰期出现TCP重传率超过5%的异常热点key问题部分高频广告位的频次控制key集中访问导致单主节点负载不均衡最繁忙节点的QPS是平均值的三倍这些问题的根源在于单集群架构的扩展性受限于单节点的内存上限和网络带宽上限且Redis的哈希槽分配无法自动感知业务热点分布。二、架构演进方案从单集群到分片集群的演进不是简单的加机器而是需要在数据分片策略、读写路由逻辑、跨分片事务一致性三个维度上重新设计。2.1 逻辑分片策略按业务域拆分将单集群拆分为三个逻辑分片集群每个集群承载一个业务域的数据。分片策略的核心考量是各业务域的读写模式差异显著——画像数据以读为主、频次控制以写为主、计费数据读写均衡。import hashlib import logging logger logging.getLogger(redis-shard-router) class RedisShardRouter: Redis分片路由中间件按业务域路由到对应分片集群 # 业务域与分片集群映射 DOMAIN_CLUSTER_MAP { user_profile: profile-cluster, # 画像集群读多写少 freq_control: freq-cluster, # 频次集群写多读少 realtime_billing: billing-cluster, # 计费集群读写均衡 } # key前缀与业务域映射规则 KEY_PREFIX_RULES { up:: user_profile, # up:* → 画像域 fc:: freq_control, # fc:* → 频次域 rb:: realtime_billing, # rb:* → 计费域 } def __init__(self, cluster_connections: dict): # cluster_connections: {profile-cluster: RedisConn, ...} self.connections cluster_connections def route(self, key: str) - str: 根据key前缀路由到对应分片集群 返回: 集群名称 try: # 提取key前缀格式: 前缀:业务ID prefix key.split(:)[0] : domain self.KEY_PREFIX_RULES.get(prefix) if not domain: # 无匹配前缀时使用哈希兜底路由 domain self._hash_fallback(key) logger.debug(fkey前缀未匹配哈希兜底: key{key}, domain{domain}) cluster self.DOMAIN_CLUSTER_MAP[domain] return cluster except Exception as e: logger.error(f分片路由失败: key{key}, error{e}) # 路由失败时降级到画像集群读多写少相对安全 return profile-cluster def _hash_fallback(self, key: str) - str: 哈希兜底路由用于无前缀规则的key hash_val int(hashlib.md5(key.encode()).hexdigest(), 16) domains list(self.DOMAIN_CLUSTER_MAP.keys()) return domains[hash_val % len(domains)]2.2 热点key的本地缓存层频次控制的key如fc:ad_slot_1234:user_5678是系统最高频的访问对象单key峰值QPS可达5万。引入Caffeine本地缓存作为L1层将70%的频次查询命中在本地穿透到Redis的请求量从350万降至105万。from caffeine import Cache import time import logging logger logging.getLogger(freq-local-cache) class FrequencyControlCache: 频次控制本地缓存层Caffeine L1 Redis L2 def __init__(self, redis_client, max_size: int 100000, expire_seconds: int 60): # Caffeine本地缓存配置 self.local_cache Cache( maximum_sizemax_size, expire_after_writeexpire_seconds, # 频次控制60秒过期 ) self.redis_client redis_client async def check_frequency(self, key: str, limit: int) - bool: 检查用户对某广告位的频次是否超限 L1本地缓存命中 → 直接返回无需穿透Redis L1未命中 → 穿透Redis获取并回填L1 try: # L1层查询 cached_count self.local_cache.get(key) if cached_count is not None: if cached_count limit: logger.debug(fL1命中频次超限: key{key}, count{cached_count}) return False # 未超限时仍需Redis确认最新值本地缓存可能有延迟 # 但对于已超限的判断L1的过期时间内的结果可信 # L2层穿透Redis redis_count await self.redis_client.get(key) if redis_count is None: redis_count 0 else: redis_count int(redis_count) # 回填L1缓存 self.local_cache.put(key, redis_count) if redis_count limit: return False # Redis侧原子递增INCR命令 new_count await self.redis_client.incr(key) # 更新L1缓存 self.local_cache.put(key, new_count) return new_count limit except Exception as e: logger.error(f频次检查失败: key{key}, error{e}) # 缓存层异常时降级为直接Redis操作 try: new_count await self.redis_client.incr(key) return new_count limit except Exception as redis_err: logger.critical(fRedis降级也失败: {redis_err}) # 最终降级允许展示宁可多展示也不阻断业务 return True三、性能演进的关键数据三个阶段架构的性能实测对比指标阶段一单集群阶段二逻辑分片阶段三分片L1缓存总QPS承载200万350万500万P99延迟2ms常态/50ms高峰3ms常态/8ms高峰1ms常态/3ms高峰单节点内存使用率85%45%40%网络带宽利用率80%瓶颈35%15%L1拦截70%请求热点key最大QPS5万单节点过载5万独立分片5万L1命中70%故障影响范围全业务域单业务域单业务域L1容灾四、运维自动化与容灾设计4.1 Redis集群健康巡检# Redis集群健康巡检脚本定时检测各分片集群的关键指标 #!/bin/bash set -e CLUSTERS(profile-cluster freq-cluster billing-cluster) ALERT_THRESHOLD_MEMORY80 # 内存使用率告警阈值 ALERT_THRESHOLD_QPS_PER_NODE50000 # 单节点QPS告警阈值 LOG_FILE/var/log/redis-health-check.log logger 开始Redis集群健康巡检... for cluster in ${CLUSTERS[]}; do # 获取集群各主节点的关键指标 nodes_info$(redis-cli --cluster-info $cluster 2/dev/null || { logger 获取${cluster}集群信息失败 continue }) # 解析内存使用率 memory_usage$(echo $nodes_info | grep memory_used_percent | awk {print $2}) if [ -n $memory_usage ] [ $memory_usage -gt $ALERT_THRESHOLD_MEMORY ]; then logger 告警: ${cluster}内存使用率${memory_usage}%超过阈值${ALERT_THRESHOLD_MEMORY}% # 触发Prometheus告警 curl -s -X POST http://alertmanager:9093/api/v1/alerts \ -d [{labels:{cluster:$cluster,alertname:RedisMemoryHigh}}] fi # 解析单节点QPS for node in $(echo $nodes_info | grep master | awk {print $1}); do node_qps$(redis-cli -h $node info stats 2/dev/null | \ grep instantaneous_ops_per_sec | awk -F: {print $2}) if [ -n $node_qps ] [ $node_qps -gt $ALERT_THRESHOLD_QPS_PER_NODE ]; then logger 告警: ${cluster}节点${node} QPS${node_qps}超过阈值 fi done done logger Redis集群健康巡检完成4.2 跨分片数据迁移工具class RedisCrossShardMigrator: 跨分片数据迁移工具用于key前缀规则调整时的数据搬迁 def __init__(self, source_client, target_client, batch_size: int 1000): self.source source_client self.target target_client self.batch_size batch_size async def migrate_keys(self, key_pattern: str) - dict: 按pattern批量迁移key到目标分片 返回: {migrated: count, failed: count} migrated 0 failed 0 try: # SCAN遍历匹配的key cursor 0 while True: keys await self.source.scan( cursorcursor, matchkey_pattern, countself.batch_size ) cursor keys[0] key_list keys[1] if not key_list: if cursor 0: break continue # 批量获取值并写入目标集群 for key in key_list: try: value await self.source.get(key) ttl await self.source.ttl(key) if value is not None: await self.target.set(key, value, exmax(ttl, 0)) # 原集群删除已迁移的key await self.source.delete(key) migrated 1 except Exception as e: logger.error(f迁移key失败: key{key}, error{e}) failed 1 if cursor 0: break logger.info(f迁移完成: migrated{migrated}, failed{failed}) return {migrated: migrated, failed: failed} except Exception as e: logger.error(f迁移整体失败: {e}) return {migrated: migrated, failed: failed, error: str(e)}五、总结从单集群200万QPS到分片集群本地缓存500万QPS的演进核心思路是三层解耦业务域解耦按画像、频次、计费三个读写模式不同的域拆分物理集群避免热点key对全局的拖累缓存层解耦Caffeine本地缓存拦截70%的高频读请求将Redis的负载从350万降至105万延迟从8ms降至1ms路由层解耦分片路由中间件基于key前缀规则精确路由哈希兜底确保无规则key仍可正常分发性能演进的关键经验单集群架构的瓶颈不在QPS上限而在热点分布不均衡——一个5万QPS的热点key足以让整个集群的P99延迟劣化。逻辑分片解决热点隔离本地缓存解决穿透量控制两者结合才是在成本可控前提下的最优扩展路径。Redis架构的演进不是追求无限QPS而是在业务增长曲线中找到每个阶段的最优性价比方案——单集群到分片集群的迁移成本约为两周但带来的性能提升是3倍以上的线性增益。