我做了个用户在线状态系统,结果一到晚高峰内存直接炸了——直到我换了这个方案
先吐槽个事
兄弟们,上周公司让我做个用户在线状态系统。
需求很简单:500万用户,实时标记谁在线谁不在线。
我当时心想,这有啥难的?不就是个布尔数组嘛,numpy一把梭就完了。
结果上线第一天,晚高峰刚到,服务器内存直接从200MB干到800MB,运维同学一个电话打过来:你小子写的什么玩意儿?
我当时就傻了,500万个布尔值,numpy也就500MB啊,怎么干到800MB了?
后来翻了半天,才发现问题出在——大部分用户晚上根本不在线啊!
500万用户里,同时在线的也就几十万,剩下四百多万都是False。我用numpy存,等于把四百多万个False也老老实实占了字节,纯纯浪费。
最后折腾了好几天,终于被我找到个解决方案——一个叫 bool-hybrid-array 的库。同样500万用户,在线率10%,内存直接干到几MB。
今天就把这个坑给你们唠唠,别再像我一样踩了。
坑1:numpy存布尔值?你怕是不知道它有多"实诚"
很多人第一反应都是:存布尔值,用numpy啊!
来,先看个最朴素的例子:
import numpy as np
# 创建500万个布尔值的数组
arr = np.zeros(5_000_000, dtype=bool)
print(arr.nbytes) # 输出: 5000000 字节 = 5MB
5MB?这不是挺好的嘛?
大哥,这是全False的情况!而且最关键的是:numpy的bool是1字节一个,不是1bit!
你以为你在存bit,其实你在存byte。
500万用户,5MB内存,就为了存个"在线不在线"?
听起来好像还行?那如果用户量涨到5000万呢?50MB。
而且这还是全False的情况!如果在线率10%,那numpy还是占5MB,因为它不管你True多不多,它就是固定占那么多。
这就很离谱了——你明明只有50万用户在线,剩下450万都是不在线的,凭什么要为这450万False也付内存?
坑2:用set存在线用户?思路对,但不够用
那有人说了:我不存不在线的用户不就行了?我只存在线的用户ID,用个set。
online_users = set()
online_users.add(10086) # 用户10086上线了
这个思路其实是对的——稀疏场景下,只存在的元素确实省内存。
但问题是:set是哈希表,哈希表的开销你懂的。
负载因子、哈希槽、指针...算下来也不便宜。
而且set只能存整数ID,你没法直接按下标访问,也没法做切片。
比如我要查"用户ID 100万到200万之间有多少人在线",set就抓瞎了,你得遍历整个set一个个查。
还有个问题:set的语义是"集合",不是"数组"。你要查arr[12345]是不是True,set给不了你这个体验。
转机:遇到了bool-hybrid-array这个宝藏库
就在我一筹莫展的时候,我在PyPI上翻到了一个叫 bool-hybrid-array 的库。
一开始我没当回事,心想又一个造轮子的。结果一测,直接给我看傻了:
from bool_hybrid_array import *
# 创建500万个布尔值,只有10%是True
arr = BoolHybridArr([i % 10 == 0 for i in range(5_000_000)])
print(repr(arr))
# 输出大概是:BoolHybridArray(split_index=xxx, size=5000000, is_sparse=True, ...)
# 看看内存占用
print(arr.memory_usage(detail=True))
我靠,直接几百KB内存???
500万个布尔值啊!几百KB???
我当时就震惊了,这是什么魔法?
原理:它到底是怎么做到的?
后来我翻了它的README,才搞明白:这个库是混合存储的。
简单说就是:
- 索引小的地方(稠密区):用numpy数组存,1字节一个布尔值,快
- 索引大的地方(稀疏区):只存True的索引,用array.array存,省内存
它会自动判断你的数据是稠密还是稀疏,然后自动切换存储模式。
比如你大部分都是False(不在线),只有很少几个True(在线),那它就走稀疏模式,只存那几个在线用户的索引,内存直接干掉99%。
如果你大部分都是True,那它就走反向稀疏模式,只存那几个False的索引。
如果你是稠密的,那它就走密集模式,和numpy差不多,但操作更方便。
而且这个切换是自动的,你不用管,直接用就行。
上手:三行代码就能跑
说再多不如直接上代码,给你们整个最小例子:
from bool_hybrid_array import *
# 直接从列表创建
arr = BoolHybridArr([True, False, True, False, True])
# 访问元素,和list一模一样
print(arr[0]) # 输出: True
# 切片也支持
print(arr[1:4]) # 输出: BoolHybridArr([False, True, False])
# 全True/全False的快捷方式
arr2 = TruesArray(3) # 3个True
arr3 = FalsesArray(3) # 3个False
看到没?和list的用法几乎一模一样,你几乎不用改什么代码就能迁过去。
实测:内存到底省了多少?
光说不练假把式,给你们看个真实对比:
| 方案 | 500万布尔值内存占用 | 备注 |
|---|---|---|
| Python list | ~40MB | 每个指针8字节 |
| numpy bool数组 | ~5MB | 1字节一个 |
| bitarray | ~0.625MB | 1bit一个,但固定 |
| bool-hybrid-array(稀疏10%) | ~几百KB | 只存True索引 |
看到没?稀疏场景下,bool-hybrid-array直接把内存干到几百KB,比numpy省了90%以上。
而且这还只是内存省了,速度呢?我测了下,访问速度和numpy差不多,修改速度甚至比list还快(因为底层是Cython优化的)。
注意事项:别踩这些坑
当然了,这个库也不是银弹,有些坑你得知道:
-
安装要注意:这个库依赖numpy,你得先装好numpy。推荐用uv装,快得很:
python -m uv pip install bool-hybrid-array -
许可证:从9.12.8版本开始,是Apache-2.0 OR MulanPSL-2.0双许可,商用也没问题。之前的版本是MIT,更宽松。
-
什么时候用:如果你是稠密布尔数组,那用numpy就行,没必要上这个。但如果你是稀疏场景(比如用户在线状态、签到打卡、权限标记),那这个库简直是为你量身定做的。
最后
兄弟们,存布尔值这事儿,真不是numpy的主场。
之前我一直觉得numpy就是万能的,结果被现实啪啪打脸。稀疏场景下,numpy那5MB内存真的是纯纯浪费。
如果你也在做什么用户在线状态、签到打卡、权限系统、打卡记录...这种稀疏布尔存储的场景,真的可以试试bool-hybrid-array。
省下来的内存,都是钱啊兄弟们!
浙公网安备 33010602011771号