2027秋招-腾讯TEG存储一面
场景题:一个进程内最多能开多少线程?线程数量受什么限制?线程中默认内存用多少,虚拟内存不用的话会请求吗?默认请求多少内存?
单个进程能开多少线程,本质上受三类资源约束:内核给系统的全局配额、用户给账号的进程配额、进程自身的虚拟地址空间和每线程栈大小。
第一层是内核级(系统全局)。/proc/sys/kernel/threads-max 决定整台机器上所有进程的线程总数上限,其默认值由物理内存换算而来(如 16GB 内存通常对应几十万量级)。/proc/sys/kernel/pid_max 限制 PID/TID 的分配池,每个线程都要占用一个 TID,默认 32768,最大可调到 4194304,间接封顶了线程数。还有 /proc/sys/vm/max_map_count(默认 65530),它限制单个进程可拥有的内存映射区域数量——每个线程栈都对应一个独立的 vma,线程数也会被它卡住。
第二层是用户级(资源配额)。ulimit -u(RLIMIT_NPROC)限制单个用户下所有进程加线程的总数,Linux 把线程按轻量级进程计数,所以线程也计入这个配额。普通用户默认常见为 1024 或 4096,root 通常无限制;这意味着即使内存充足,普通用户开到 4000 多个线程就会得到 EAGAIN。容器环境还有额外一层 cgroup 的 pids.max,Docker/K8s 里经常是它先爆而不是内核参数。
第三层是进程级(地址空间与栈)。这是最容易被低估的一层。每个线程都要独占一块栈空间,Linux 默认 8MB(ulimit -s 可查),Windows 默认 1MB,Java 的 -Xss 默认 1MB。线程数被”可用虚拟内存 ÷ 栈大小”封顶。
其中最容易被忽略的是: 线程栈虽然不一定一开始就实际占满物理内存,但虚拟地址空间会为它预留空间。 比如 pthread 默认栈在某些 Linux/glibc 环境下常见是 8 MB。这并不意味着真的马上消耗 8 GB RAM,但会显著影响地址空间和后续内存使用。32 位的虚拟地址空间有限最多表示4GB会是瓶颈,64 位的虚拟地址空间能表示的内存大小很大通常不会是瓶颈。
32 位进程是栈大小硬卡死的典型:Linux 32 位用户空间约 3GB、默认 8MB 栈,实测 pthread_create 大约在 381~382 个线程时返回 EAGAIN/ENOMEM,与 3072MB ÷ 8MB ≈ 384 的理论值吻合。
64 位进程地址空间不再是瓶颈,限制转移到内核参数和配额。SUSE 官方文档给出的实测数据是单进程可以创建 120,000 以上线程,最终上限取决于内存、RLIMIT_STACK、threads-max 等参数的合力。
场景题:int64变为string类型的key,怎么保证变换后的string按字典序排序和int64排序一样
第一:定长编码,高位补0
第二:int64转为16进制,注意负数由于符号位与整数不同为1所以直接转出来的负数字典序比正数高,不合理【解决方法:把最高位符号位翻转】
第三:11位Base64可以表示int64,8位char,8个字节能表示int64。字符前面先加上后续string的长度例如“04”与“11”【把int64映射到string字符后多加一个长度的定长编码,保证长的string字典序一定比短的string字典序大,然后同长度就可以直接转换比较了】
第四:二进制分层(UTF-8 式)取二进制最前面几位数字表示编码是变长的几个字节,这样的话一个字节的符号位可以表示1-256的长度,“04”和“11”这样表示需要两个字节,其实只用一个字节就可以了,一个字节char能表示256种可能,所以可以对应表示可变编码可以跟着1-256的字节数,int128用16个字节也绰绰有余
场景题:设计一个数据结构,insert时若key存在则报错,remove时若key不存在则报错,还要能随机get一个有效值,所有操作都要是O(1)的
随机读取要求存数据的结构得是数组
Bitmap/Hashmap保证insert/remove的查找复杂度为O(1),Bitmap才能严格O(1),Hashmap可能退化为O(n),他们里面存数组的位置,数组里存具体的值
纯 map:无法 O(1) 按下标随机取(可能有空桶或者冲突链/红黑树很大)
数组删除要为O(1),可以把要删的值和末尾的值互换然后删掉末尾的值,这样就能保证数组内连续没有空洞,但是顺序无法保证,不过也不需要保证
场景题:内存有限,三个大文件,每一行为一个string,每个文件都无法单独的放入内存,处理一下,得到三个文件中的所有的string分别在几个文件中出现过?归并需要排序吗?
外部排序 = 内存排序小块 + 多路归并 核心洞察一句话:排序需要“看到全部数据”,但归并只需要看到每个有序段的开头一个元素。 所以把大数据切成内存能装下的小块各自排好(初始归并段 / run),再用小顶堆做 k 路归并——全程内存只握着“每段一个当前元素 + 各自的缓冲区”。
输入 100GB,内存 1GB:
阶段一:生成初始归并段 读 1GB → 内存内快排 → 写出 run1(内部有序) 读 1GB → 排序 → run2 …… 共 100 个 run,每个 1GB、内部有序
阶段二:k 路归并 100 个 run 同时打开,每个配 8MB 读缓冲 100 个“当前头元素”进小顶堆 弹堆顶 → 写输出 → 从该 run 补充下一个 → 入堆 → 得到全局有序的输出
run1 ──[8MB缓冲]──► 头元素 ┐ run2 ──[8MB缓冲]──► 头元素 ┤ run3 ──[8MB缓冲]──► 头元素 ┼──► 小顶堆(k=100 个元素) … │ │ run100 ─[8MB缓冲]──► 头元素┘ ▼ 弹最小 输出缓冲(满 8MB 刷盘) └→ 从弹出元素所属的 run 补充下一个
路线一:打标签 + 一次外排 + 线性扫描 三个文件拼在一起过一遍外排(比“三个文件各自外排再三路归并”少做两次外排,同 key 天然相邻还顺手去重):
路线二:哈希分片 + 桶内聚合(推荐,I/O 最省) 核心性质:hash(s) % S 保证同一个 string 的所有出现(不管来自哪个文件)都落进同一个桶。 桶足够小就能整体进内存,内存里随便你怎么聚合。 A ─┐ B ─┼─ hash(s)%S ──→ shard_0 … shard_{S-1} 同 string 必同桶【算hash的时候尽量一起去重】 C ─┘ │ ▼ 每桶 ≤ 内存/4 桶内聚合:map[string]bitmask(免排序) │ ▼ (string, 出现于哪几个文件) → popcount 得“几个” 桶内如果连 map 都装不下 加大 S:桶更小,map 自然装得下(最简单)

