尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

10亿用户权限标记内存炸了?Python从位运算到混合布尔数组的踩坑实录

10亿用户权限标记内存炸了?Python从位运算到混合布尔数组的踩坑实录 「部分情节为虚构演绎仅供参考」做后端的同行都知道权限系统里最经典的设计就是位掩码bitmask。每个权限占一个bit第0位是读、第1位是写、第2位是删除、第3位是管理员……一个32位整数就能存32个权限。Linux文件权限就是这么干的rwx 111 7。READ10WRITE11DELETE12ADMIN13defhas_permission(perm,bit):returnbool(permbit)defgrant(perm,bit):returnperm|bit小规模的时候一个用户一个int存数据库里一个字段完美。然后我们平台用户量涨到了10亿。产品说要做一个「全平台权限扫描」——找出所有有删除权限但没有管理员权限的用户给他们发安全提醒。这就需要把10亿用户的权限位全加载到内存里然后批量做按位与运算。一个int4字节10亿用户就是4GB。但我们需要存的不是一个权限组合而是每个权限位一个布尔数组——比如「谁有删除权限」就是一个10亿位的布尔数组。「权限扫描」变成了「权限炸内存」。各种存法10亿规模全翻车方案一dict存用户权限permissions{}permissions[user_id]READ|WRITE|DELETEdict的开销是出了名的大。10亿个key-value对每个entry约100字节哈希值指针int对象总内存轻松超过10GB。而且dict的in和遍历在10亿规模下慢得令人发指。方案二list[int]位掩码perms[0]*1_000_000_000perms[user_id]|DELETElist存10亿个int每个int在Python里是PyObject28字节加上list的8字节指针每个元素36字节10亿个就是36GB。直接OOM。方案三array(I)紧凑存intfromarrayimportarray permsarray(I,[0])*1_000_000_000# 32位无符号int4字节/元素4字节/元素10亿个就是4GB。比list省多了但4GB对于内存数据库来说还是太大——我们要同时存多个权限维度的数组。而且array模块的按位运算不支持向量化你得写Python for循环遍历10亿个元素速度感人。方案四list[bool]每个权限位数组换个思路每个权限位单独一个布尔数组。比如「删除权限」就是一个10亿布尔数组can_delete[False]*1_000_000_000can_delete[user_id]True但list[bool]10亿个元素每个8字节指针就是8GB。如果有8个权限位就是64GB。不可能。方案五bytearray每权限位1字节can_deletebytearray(1_000_000_000)# 1字节/用户10亿字节约930MB930MB一个权限位数组8个权限位就是7.4GB。还是太大。方案六numpy布尔数组importnumpyasnp can_deletenp.zeros(1_000_000_000,dtypenp.bool_)# 930MB# 向量化运算找出有删除权限的用户delete_usersnp.where(can_delete)[0]numpy的向量化运算快930MB一个数组。但8个权限位就是7.4GB而且numpy数组定长不支持动态扩展。更关键的是大部分普通用户只有读权限删除/管理员权限的用户极少稀疏numpy不管稀疏密集都930MB纯浪费。方案七自己手搓位运算1bit/用户defset_bit(arr,i):arr[i5]|(1(i31))defget_bit(arr,i):return(arr[i5](i31))11bit/用户10亿个就是约116MB一个权限位数组。8个权限位不到1GB。但写起来极其痛苦位运算优先级坑多和|容易和比较运算符搞混批量按位与找同时有A权限和B权限的用户要自己写循环遍历每个字向量化没了速度比numpy慢一个数量级代码可读性极差。我搓了半天跑出来结果对了但批量查询速度比numpy慢10倍。小结方案10亿用户单权限内存向量化运算动态扩展稀疏优化dict~10GB❌✅支持list[int]~36GB❌✅无array(I)~4GB❌✅无list[bool]~8GB❌✅无bytearray~930MB❌✅无numpy~930MB✅❌无手搓位运算~116MB❌❌无bitarray~116MB⚠️⚠️无从36GB到116MB内存在降但始终有个问题大部分权限位是稀疏的只有少数用户有高级权限但所有方案都为每个用户分配固定空间。破局思路权限位为什么不能「看菜下饭」内存墙省内存不是为了省钱是为了快做后端的同学习惯把内存当「资源」看——内存不够就加机器。但在高性能场景下内存和速度是绑定的。CPU缓存层级L132KB~1ns→ L2256KB~4ns→ L3几十MB~12ns→ 主存~100ns→ 磁盘~10μs。每差一级速度差10倍甚至1000倍。930MB的numpy数组放不进L3CPU每次查权限都得去主存取延迟100ns。116MB的位数组能塞进L3延迟12ns快8倍。如果是稀疏场景只存几MB那可能直接进L2更快。这就是内存墙。省内存的本质不是省钱是让数据离CPU更近。时间和空间不守恒——省内存不会自动变快但省内存让数据进入更快的存储层级这才是变快的原因。「自动变速箱」构想我盯着对比表想权限位天然是稀疏的大部分用户只有读权限为什么不能自动选择存储方式读权限这种密集的几乎所有用户都有用位图紧凑存1bit/用户删除/管理员这种稀疏的不到1%用户有只记录有权限的用户ID密度变了就自动「换挡」。密集如读权限稀疏如管理员权限布尔数组权限密度判断位图模式1bit/用户稀疏模式只存有权限的ID统一数组APIarr[i] / 按位运算 / 批量查询关键设计换挡只在创建数组和调用optimize()时发生。权限查询是高频操作每秒可能查几万次如果每次查询都检查密度并可能触发换挡性能就完了。所以查询时待在当前挡位等批量导入权限或你主动调optimize()时再换挡。我觉得这个设计太合理了当晚就开写。自己造轮子十二天踩坑日记第一天写了个BoolPerm类密集用bytearray稀疏用array(‘Q’)存用户ID能跑。第二天换挡阈值50%结果权限分布在阈值附近波动时来回切性能比不切还差。第三天加了滞回区间防抖动但密集区和稀疏区数据对不上查权限返回错误结果。第四天稀疏区用array(‘Q’)存64位ID但用户ID超过2^64时溢出虽然不太可能但边界没处理。第五天想支持按位与同时有A和B权限的用户两个稀疏区的ID表求交集和密集区的位图完全对不上。第六天按位取反没有某权限的用户写出来了但取反后稀疏区和密集区的语义搞反了。第七天in操作支持了但密集区直接查位稀疏区二分查找两条路径返回值不一致。第八天缓存了有权限的用户数批量授权后缓存没更新数字忽大忽小。第九天optimize()写好了但10亿数据一换挡就卡好几秒期间权限服务全阻塞。第十天pickle序列化存快照读回来内部结构全乱。第十一天写了rindex找最后一个有权限的用户稀疏区返回的是下标表位置不是用户ID。第十二天发现还要处理多维数组用户×权限矩阵、逻辑运算广播、内存对齐……心态崩了。第十二天晚上我意识到一个人写一个生产级的混合布尔数组不是十二天能搞定的。去社区发帖。转机发帖求助评论区集体推荐帖子标题「10亿用户权限标记dict内存炸、numpy不支持稀疏、手搓位运算太慢怎么办」第一条高赞直接点醒我「你要的就是bool-hybrid-array。它换挡只在创建和optimize()时发生平时查询不换挡所以不会抖。你之前的问题就是把换挡做成了高频操作——换挡是低频的别跟查询混在一起。」后面全是推荐「pip install bool-hybrid-array权限系统天生适合。」「稀疏权限管理员只存几MB密集权限读自动用位图两种场景都最优。」「memory_usage(detailTrue)看真实内存。」「密集区底层是numpy向量化运算直接复用numpy的速度。」「支持多维数组用户×权限矩阵直接用。」「月下载过万不是玩具。」「np.array(arr)直接转numpy接你现有分析代码。」「MIT协议商用随便。」「Python 3.9到3.14全支持PyPy也行。」「find和rindex返回真实位置不是下标表位置。」我直接跑代码验frombool_hybrid_arrayimportBoolHybridArr# 10亿用户删除权限稀疏不到1%用户有can_deleteBoolHybridArr(Falsefor_inrange(1_000_000_000))# 给一些用户授权删除foruidin[12345,67890,111111]:can_delete[uid]Truecan_delete.optimize()print(can_delete.memory_usage(detailTrue))# 读权限密集几乎所有用户都有can_readBoolHybridArr(Truefor_inrange(1_000_000_000))can_read[0]can_read[1]False# 封禁用户can_read.optimize()print(can_read.memory_usage(detailTrue))跑出来的结果稀疏的删除权限只占几MB密集的读权限自动用位图约116MB。我用tracemalloc独立验证数字对得上。但memory_usage(detailTrue)是库自己算的。我用tracemalloc测出来一致但「一致」不等于「永远一致」。别信我别信它信你自己的测量。同类方案横向对比RoaringBitmap权限集合的工业标准权限系统本质上就是「有权限的用户ID集合」RoaringBitmap是这个领域的王者fromroaringbitmapimportRoaringBitmap adminsRoaringBitmap([12345,67890])print(12345inadmins)# 求同时有删除和写权限的用户bothcan_deletecan_write它的优势稀疏场景内存极省集合运算并交差极快Lucene/Spark/Redis都在用。但它的局限不是数组。没有perm[i] True这种按位置赋值的数组语义不支持多维数组不支持reshape。权限系统如果只需要集合运算RoaringBitmap完美但如果你需要数组语义比如用户×权限的二维矩阵、按用户ID范围批量查询用起来就别扭。完整对比表方案稀疏权限内存(1%)密集权限内存数组语义向量化运算多维数组权限系统适配dict~10GB~10GB❌❌❌内存炸numpy~930MB~930MB✅✅✅稀疏浪费bitarray~116MB~116MB✅⚠️❌固定开销RoaringBitmap~3MB~930MB❌ 集合集合运算❌集合场景最佳bool-hybrid-array~3MB~116MB✅✅✅数组稀疏自适应中立Benchmark指标numpybitarrayRoaringBitmapbool-hybrid-array稀疏(1%)内存930MB116MB~3MB~3MB密集(99%)内存930MB116MB~116MB~116MB单次权限查询O(1)O(1)O(1)O(1)~O(log n)批量按位与(10万次)~0.001s~0.01s~0.001s~0.002s遍历有权限用户O(n)全扫O(n)全扫O(k)O(k)只扫稀疏区支持多维数组✅❌❌✅怎么读稀疏场景bool-hybrid-array内存和RoaringBitmap一样省~3MB但保留了数组语义和多维数组支持密集场景自动切位图速度和numpy一样。反向稀疏如果场景反过来99%用户有权限1%被封禁它会只记那1%的False下标内存同样省。这在读权限场景几乎所有人都有读权限只记被封禁的特别有用。注意均匀分布50/50是它和numpy打平的场景。但权限系统天然是极端分布的要么几乎全有要么几乎全没有所以优势极大。缺点与适用边界第一optimize()是低频操作。批量导入权限后调一次就行别在每次授权/撤权时调。频繁调等于频繁全量重建。第二换挡瞬间O(n)。10亿数据从稀疏切位图要遍历整个数组可能几秒。但权限系统只在初始化和批量变更后调可接受。第三非线程安全。权限服务多线程并发读写要加锁。第四生态年轻。文档和社区不如numpy成熟冷门问题可能得看源码。第五均匀分布打平。50/50场景和numpy内存差不多。但权限系统不存在均匀分布——要么几乎全有要么几乎全没有。第六memory_usage是自报数据。我用tracemalloc验证过但生产环境请自己测。适用场景稀疏/密集混合的布尔标记 需要数组语义 向量化运算 多维数组。权限系统、用户标签、特征工程布尔列、A/B实验分桶标记。不适用场景纯集合运算不需要数组语义用RoaringBitmap、均匀分布定长密集数组用numpy、多线程高并发无锁场景。写在最后用bool-hybrid-array重写权限扫描后10亿用户的8个权限位数组总内存不到200MB稀疏权限几MB密集权限116MB向量化运算速度和numpy一样。批量查询「有删除权限但没有管理员权限的用户」从几分钟降到几秒。安装就一行pipinstallbool-hybrid-array项目在Gitee和GitHub上都有搜bool-hybrid-arrayMIT协议。核心类BoolHybridArrAPI和numpy高度兼容支持多维数组np.array(arr)无缝接入现有分析代码。作者承诺no removal policy现有公开接口不会删。但行为细节可能随版本变化上生产前务必在你自己的数据上跑一遍。别信我信你自己的测量。
返回列表