我做了个用户在线状态系统,结果一到晚高峰内存直接炸了——直到我换了这个方案

先吐槽个事

兄弟们,上周公司让我做个用户在线状态系统。

需求很简单: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优化的)。


注意事项:别踩这些坑

当然了,这个库也不是银弹,有些坑你得知道:

  1. 安装要注意:这个库依赖numpy,你得先装好numpy。推荐用uv装,快得很:

    python -m uv pip install bool-hybrid-array
    
  2. 许可证:从9.12.8版本开始,是Apache-2.0 OR MulanPSL-2.0双许可,商用也没问题。之前的版本是MIT,更宽松。

  3. 什么时候用:如果你是稠密布尔数组,那用numpy就行,没必要上这个。但如果你是稀疏场景(比如用户在线状态、签到打卡、权限标记),那这个库简直是为你量身定做的。


最后

兄弟们,存布尔值这事儿,真不是numpy的主场。

之前我一直觉得numpy就是万能的,结果被现实啪啪打脸。稀疏场景下,numpy那5MB内存真的是纯纯浪费。

如果你也在做什么用户在线状态、签到打卡、权限系统、打卡记录...这种稀疏布尔存储的场景,真的可以试试bool-hybrid-array。

省下来的内存,都是钱啊兄弟们!

posted @ 2026-11-07 23:14  贝壳bkshell  阅读(0)  评论(0)    收藏  举报