ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python算法:5亿社交好友申请标记,list爆内存numpy定长灾难,bool-hybrid-array混合存储方案

Python算法:5亿社交好友申请标记,list爆内存numpy定长灾难,bool-hybrid-array混合存储方案 部分情节为虚构演绎仅供参考说实话我做的是一个社交平台的好友系统。平台有5亿注册用户每个用户对其他用户都有好友申请状态已申请是True、未申请是False、已接受是另一个True、已拒绝是False。好友推荐服务要实时判断「这个用户有没有给那个人发过申请」还要支持用户随时发送申请、撤回申请、接受/拒绝。说白了好友申请状态就是海量的布尔标记——5亿用户的社交关系矩阵就是几十亿个True和False。听着挺简单对吧不就是一堆True和False嘛用set存申请关系不就行了但你猜怎么着现实啪啪打脸就是这一堆True和False差点把我给整「申请」成「深请」了——不是申请的「申」是深坑的「深」5亿布尔标记把好友服务拖进深渊OOM之后好友列表全空用户以为自己被删好友了客服电话被打爆。越想精确标记每对用户的好友申请状态越把好友服务拖进深坑。我认为这大概是我做社交系统以来最反直觉的经历明明每一步都在往「更省内存、更快判定」的方向走结果却是一步一个坑。直到我放弃自己造轮子才真正找到解药。1. 从list到各种主流方案数据一涨全线OOM/TLE1.1 list[bool]内存黑洞requested[False]*500_000_000# 5亿用户5亿元素每个指针8字节光指针4GB加上对象头突破5GB。好友服务器一共128GB光这一个数组干掉4%再加上关系链、消息、缓存内存直接告急。1.2 array(‘b’)省内存但慢fromarrayimportarray requestedarray(b,[0])*500_000_000内存降到500MB但每次索引访问要装箱拆箱好友推荐每秒查几十万次申请状态P99延迟翻倍。1.3 numpy.ndarray判定快但申请撤回灾难importnumpyasnp requestednp.zeros(500_000_000,dtypenp.bool_)向量化筛选快但发送/撤回申请是高频操作numpy定长数组insert/append全量拷贝5亿元素拷贝一次500MB高峰期直接卡死。1.4 scipy.sparse语义错位存非零坐标和值布尔值冗余int64坐标8字节API是矩阵那套不是数组操作。1.5 小结方案内存5亿bool申请判定发送/撤回稀疏场景list[bool]~4GB慢快浪费array(‘b’)~500MB慢慢浪费numpy.ndarray500MB快灾难浪费scipy.sparse看稀疏度慢慢语义错位2. 破局思路混合存储2.1 内存墙CPU每秒几十亿次运算内存每秒几GB读写差两个数量级。数据塞不进缓存就得跑远路去主存慢100倍再大就Swap慢100万倍。时间和空间是独立维度省内存的本质是让数据离CPU更近。2.2 构想自动变速箱好友申请有个特点活跃用户申请了很多人密集不活跃用户几乎没发过申请稀疏新用户刚注册没人可申请极稀疏。不同用户密度天差地别但单个用户的申请密度变化是缓慢的。高密度低密度申请数据申请密度有多高三档位图紧凑存储一档只存申请对象下标自动换挡器对外统一接口换挡应该是低频的创建时定挡用户活跃度发生大变化时手动调一次optimize()。同事问「来回抖怎么办」我又噎住了。3. 自己做十几天疼到怀疑人生第一天写了混合申请数组稀疏场景几MB觉得自己天才。第二天阈值写死50%活跃度在阈值附近抖动疯狂来回切。第三天滞回区间和实际存储对不上申请状态错乱没发过申请的显示已申请。第四天稀疏区array(‘I’)存用户ID越界不报错静默写错位置。第五天批量申请接口把「按位置标记」和「按值过滤」写串了。第六天撤回申请后count(True)对不上稀疏区删除后索引没压缩。第七天in运算符每次全量扫描5亿元素查一次好几秒。第八天统计申请数的方法缓存没失效数字忽大忽小。第九天换挡瞬间重建内部结构好友推荐高峰期卡了几百毫秒。第十天pickle序列化存进去读出来全乱了。第十一天查找第一个申请对象稀疏区返回下标表位置不是真实位置。第十二天2000多行代码一堆边界条件没处理心态崩了。第十三天我意识到换挡不该是高频动作。从零锤生产可用的混合布尔数组不是一个人两个月的事。4. 转机被一句话点醒发帖后评论区全在安利bool-hybrid-array。一条评论点醒我「换挡只在创建时和调用optimize()时发生平时insert、pop、赋值都不换挡根本不会来回抖。你把换挡时机搞错了——换挡是低频动作不是高频动作。」对啊用户活跃度变化时调一次optimize()就够了。frombool_hybrid_arrayimportBoolHybridArr# 5亿用户只有0.01%发过好友申请requestedBoolHybridArr(i%100000foriinrange(500_000_000))print(repr(requested))print(requested.memory_usage(detailTrue))100万元素10%密度场景下list约1MBBoolHybridArray约100KB省约90%。我用tracemalloc验证过。memory_usage(detailTrue)是库自己算的别信我也别信它信你自己的测量。BoolHybridArr是工厂函数转成BoolHybridArray实例。内部索引小的位置用numpy.ndarray密集存储索引大的位置用array.array稀疏存储split_index决定分界点。设计源于作者做线性筛时的真实痛点。5. 同类开源方案横向对比5.1 RoaringBitmap申请者集合的工业标配fromroaringbitmapimportRoaringBitmap requestersRoaringBitmap()requesters.add(123456)print(123456inrequesters)优势稀疏极省空间并交差运算极强共同好友、可能认识的人极快。局限不是数组没有arr[i]语义不支持动态append/pop不保留长度。5.2 bitarray和pyarrowbitarray每值1bit5亿元素62.5MB保留数组语义但定长无稀疏优化。pyarrow.BooleanArray位压缩强在列式跨语言但不可变每次修改重建。5.3 对比表方案5亿bool内存0.01%申请数组语义动态追加稀疏自适应集合运算典型场景list[bool]~4GB有有无无小规模numpy.ndarray500MB有无无有密集定长bitarray62.5MB有麻烦无有密集位压缩pyarrow.BooleanArray62.5MB有无无有列式存储scipy.sparse看稀疏度无无有弱数值矩阵RoaringBitmap~5MB无add/remove有极强申请者集合、共同好友bool-hybrid-array~5MB有有有有布尔数组、动态增删5.4 两种思路RoaringBitmap适合「集合」数据本质是「一堆申请者ID」整天问「这个ID在不在集合里」还要做交集差集共同好友、可能认识的人。选RoaringBitmap。bool-hybrid-array适合「数组」数据本质是「一个很长的布尔序列」总在关心「第i个用户申请没」序列要动态增删。选bool-hybrid-array。前者是集合后者是数组。认清边界比会用工具更重要。6. 缺点与适用边界第一optimize()是低频操作用户活跃度大变化时调一次别每次发送撤回都调。第二换挡瞬间O(n)全量拷贝5亿规模可能秒级别在好友推荐高峰期调。第三不是线程安全的申请线程和推荐线程并发要加锁。第四生态年轻196个版本迭代快没有RoaringBitmap十年验证。第五密集场景反向稀疏——大部分为True时只记少数False下标空间反而比numpy省。均匀分布才打平。第六memory_usage(detailTrue)数字是库自己算的生产前自己验证。适用稀疏动态更新单线程数组语义。纯集合运算用RoaringBitmap均匀定长用numpy。pip install bool-hybrid-arrayMIT协议核心类BoolHybridArray工厂函数BoolHybridArr依赖numpy。别信我信你自己的测量。
返回列表