KnowFlow 项目 —— 30+ 道面试八股文(字节实习向)
KnowFlow 项目 —— 30+ 道面试八股文
面向字节跳动 Java 后端暑期实习面试,每道题都结合 KnowFlow 项目源码讲清楚,力求”傻子都能懂”。
一、Redis 相关(6 道)
Q1:你们项目里用 Redis 存了什么?Bitmap 是什么,为什么用它来做分片上传?
答:
我们项目里 Redis 主要有两个用途:
- 存文件分片上传状态 —— 用 Bitmap
- 存用户有效组织标签缓存 —— 用 Set 或 List
Bitmap 是什么?
Bitmap 就是用每一个 bit 位来表示一个状态,极其省内存。
1 | |
为什么用 Bitmap 记录分片上传状态?
- 极其省内存:10 万个分片只要 12.5KB,如果用数据库存,10 万行记录可能要 10MB+
- O(1) 查询某个分片是否已上传:
GETBIT key offset - 支持批量操作:可以一次性把 Bitmap 的字节数组取出来,在本地解析出所有已上传的分片
源码依据:UploadService.java 第 344-361 行,isChunkUploaded() 方法用 redisTemplate.opsForValue().getBit() 检查分片状态;第 370-385 行,markChunkUploaded() 用 setBit() 标记分片已上传。
Q2:Redis Bitmap 的底层实现原理是什么?
答:
Redis 的 Bitmap 不是一种独立的数据结构,它其实是对 String 类型做位操作。
1 | |
底层原理:
SETBIT key offset value:把 offset 那个 bit 设为 0 或 1- 如果 offset 超出了当前 String 的长度,Redis 会自动扩容(补
x00)
- 如果 offset 超出了当前 String 的长度,Redis 会自动扩容(补
GETBIT key offset:读取 offset 那个 bit 的值BITCOUNT key:统计有多少個 bit 是 1(Redis 用了查表法优化,很快)GET key:把整个 Bitmap 作为普通 String 取出来(可以用来批量解析)
扩容的代价:如果文件有 1000 个分片,Bitmap 需要 125 字节,Redis 会自动扩容,这个过程很快,不影响性能。
应用场景:
- 用户签到(365 天只用 46 字节)
- 布隆过滤器(我们项目没用,但这是一个经典场景)
- 分片上传状态(我们项目用的就是这个)
Q3:如果 Redis 宕机了,你们项目的分片上传状态会怎么样?
答:
分片上传状态存在 Redis 里,如果 Redis 宕机且数据没持久化,会丢失。
我们的处理方案:
- 分片元数据同时存数据库:
ChunkInfo表记录了每个分片的信息(分片索引、MD5、存储路径),即使 Redis 里的 Bitmap 丢了,也可以从数据库重新构建 Bitmap
1 | |
MinIO 里的分片文件本身不会丢:Redis 只是记录”哪些分片已上传”,真正的分片文件存在 MinIO 里,Redis 宕机不影响 MinIO 里的文件
用户重新上传时会有提示:前端调用
getUploadedChunks()接口,发现 Redis 里没有记录,会返回”请重新上传”或自动从数据库恢复状态
面试加分:如果要做到 Redis 宕机不丢数据,可以开启 AOF 持久化(appendonly yes),或者做 Redis 主从复制。
Q4:Redis 和 MinIO 在你们项目里的分工是什么?为什么不用 Redis 存文件?
答:
这个问题考察你对不同存储系统的理解。
| Redis | MinIO(对象存储) | |
|---|---|---|
| 定位 | 内存数据库,存临时状态、缓存 | 对象存储,存大文件 |
| 容量 | 受内存限制(一般几十 GB) | 可以扩展到 PB 级 |
| 适用数据 | 小数据(KB 级) | 大数据(MB/GB 级) |
| 访问速度 | 微秒级 | 毫秒级(走网络 IO) |
| 成本 | 贵(内存比磁盘贵得多) | 便宜(用磁盘/SSD) |
为什么不用 Redis 存文件?
- 太贵:500MB 的文件存 Redis,要占 500MB 内存,1000 个用户上传文件,就要 500GB 内存,成本无法接受
- Redis 的设计目的不是存大文件:Redis 是内存数据库,适合存频繁读写的小数据(缓存、计数、状态)
- MinIO 就是为存大文件设计的:分布式、高可用、支持分片上传(它自己也有分片上传功能,我们项目是在应用层自己实现的)
我们项目的分工:
- Redis:存分片上传状态(Bitmap,很小)+ 用户组织标签缓存(Set,很小)
- MinIO:存真正的文件分片 + 合并后的完整文件
Q5:你们项目里 Redis 的 Key 是怎么设计的?有没有规范?
答:
我们项目里 Redis Key 的设计规范是:业务:用户ID:文件MD5(用冒号分隔,这是 Redis Key 设计的经典规范)。
1 | |
Redis Key 设计的几个原则(面试加分):
- 用冒号分隔:
业务:子业务:ID,可读性好 - 不要过长:Redis Key 也是要占内存的,但也不要过于简短(可读性更重要)
- 避免冲突:加业务前缀(我们项目用了
upload:前缀) - 设置过期时间:防止内存泄漏(我们项目里分片上传的 Bitmap 应该在文件合并后删除,看代码第 393-403 行
deleteFileMark()方法确实删除了)
Q6:Redis 的过期键删除策略有哪几种?你们项目用到了吗?
答:
Redis 的过期键删除有两种策略,同时配合使用:
| 策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 惰性删除 | 访问 Key 时才检查是否过期,过期就删除 | 对 CPU 友好(不主动扫描) | 过期 Key 如果不被访问,会一直占内存 |
| 定期删除 | 每隔一段时间,随机抽查一批 Key,删除过期的 | 防止过期 Key 堆积 | 抽查可能漏掉一些过期 Key |
你们项目用到了吗?
用到了。分片上传的 Bitmap Key,应该在文件合并后主动删除(我们项目代码第 632-635 行确实调用了 deleteFileMark())。
但如果删除逻辑失败了(比如删除时网络抖了一下),这个 Key 就会一直留在 Redis 里。所以最好给 Key 设置一个过期时间作为兜底:
1 | |
面试加分:如果你发现项目里没有设置过期时间,这就是一个可以提的优化点(说明你不仅会做,还会思考边界情况)。
二、Elasticsearch 相关(5 道)
Q7:你们项目里的混合检索是怎么做的?为什么既要向量检索又要 BM25?
答:
这是 RAG(检索增强生成)系统的核心问题。
单纯向量检索的问题:
- 向量检索靠的是”语义相似度”,比如用户搜”MySQL 同步慢怎么查?”,向量检索能找到”MySQL 主从延迟排查思路”(语义相近)
- 但向量检索不精确:如果用户搜”Spring Boot 3.2 新特性”,向量检索可能把”Spring Boot 2.7 新特性”也召回了,因为语义相近,但用户要的是精确匹配版本号
单纯 BM25 的问题:
- BM25 是关键词检索,精确匹配能力强
- 但 BM25 不理解语义:用户搜”MySQL 同步慢怎么查?”,如果文档里写的是”主从延迟过高排查”,BM25 召回不了(因为没有一个词是匹配的)
所以两个都要:
- 先用向量检索粗召回(语义相似,召回面广)
- 再用 BM25 重排序(精确匹配高的排前面)
- 用 RRF(倒数秩融合)把两个结果合并
源码依据:HybridSearchService.java 第 91-140 行,先用 KNN 向量检索召回 topK*30 个候选,再用 rescore 做 BM25 重排序,最后返回 topK 个结果。
Q8:Elasticsearch 的 KNN 检索原理是什么?倒排索引和向量索引的区别?
答:
倒排索引(用于 BM25 关键词检索):
1 | |
查询时,把查询文本也分词,然后去倒排索引里找哪些文档包含这些词,再用 TF-IDF 或 BM25 算法算分。
向量索引(用于 KNN 语义检索):
1 | |
查询时,把查询文本也转成 2048 维向量,然后计算余弦相似度(或欧氏距离),找出最相似的 K 个向量。
ES 里向量索引的底层:用的是 HNSW(Hierarchical Navigable Small World) 图算法,类似跳表的思想,查询复杂度大约是 O(log N),比暴力扫描快得多。
为什么不用暴力扫描? 如果有 100 万个球块,每个 2048 维,暴力扫描要算 100 万次余弦相似度,太慢了。HNSW 可以快 100~1000 倍。
Q9:RRF(倒数秩融合)是什么?为什么用它来合并结果?
答:
RRF 是一种把多个排序结果合并成一个的算法,公式很简单:
1 | |
d是某个文档块rank_i(d)是第 i 种检索方法里,文档 d 的排名(从 1 开始)k是一个常数,通常取 60(防止排名第一的文档主导整个结果)
举个例子:
1 | |
为什么用 RRF 而不是其他融合方法?
- 不依赖分数绝对值:向量检索的分数范围和 BM25 的分数范围不一样,不能直接相加,RRF 只用排名,不受分数范围影响
- 简单有效:业界标准方法,ES 内置支持(
rescore阶段就可以配置 RRF)
Q10:你们项目里 ES 的索引 Mapping 是怎么设计的?有没有用嵌套类型?
答:
我们项目里 ES 的索引叫 knowledge_base,存储的是文档切块后的文本块。
Mapping 设计(根据 EsDocument.java 实体类推断):
1 | |
为什么 textContent 用 ik_max_word 分词器?
- 默认的标准分词器对中文不友好(会分成单个字)
- IK 分词器是专门为中文设计的,能把”MySQL 主从复制”分成”MySQL”、”主从”、”复制”
为什么 orgTag 用 keyword 而不是 text?
keyword类型用于精确匹配(比如term查询)text类型用于全文检索(会分词)- 组织标签是用来精确过滤的(查询时必须完全匹配 “org_a”,不能匹配 “org_a_b” 的部分),所以用
keyword
Q11:ES 的权限过滤是怎么实现的?你们项目里怎么保证用户只能搜到自己有权限的文档?
答:
这是我们项目的一个重要设计。ES 的权限过滤是在查询阶段做的,不是在建索引阶段。
权限规则(根据 HybridSearchService.java 第 102-123 行):
- 用户自己的文档 → 有权限
- 公开文档(
public=true)→ 有权限 - 用户所属组织的文档 → 有权限(通过
orgTag判断,还支持组织层级关系)
ES 查询里的实现:
1 | |
为什么用 filter 而不是 must?
filter不计算相关性分数,只做是/否过滤,性能更好filter的结果可以被 ES 缓存(下次同样的 filter 直接命中缓存)
三、消息队列 Kafka 相关(4 道)
Q12:你们项目里 Kafka 是怎么用的?为什么用 Kafka 而不是 RabbitMQ?
答:
根据项目描述,我们用了 MinIO + Kafka 构建异步文档处理流水线。
流程:
1 | |
为什么用 Kafka 而不是 RabbitMQ?
| Kafka | RabbitMQ | |
|---|---|---|
| 定位 | 分布式流平台,高吞吐量 | 传统消息队列,功能丰富 |
| 吞吐量 | 百万级 QPS | 万级 ~ 十万级 QPS |
| 消息持久化 | 默认持久化(存磁盘日志) | 需要手动开启持久化 |
| 消费模式 | 消费者主动拉取(pull) | 队列主动推送(push) |
| 适用场景 | 日志收集、流处理、大数据管道 | 业务消息、延迟队列、优先级队列 |
我们项目选 Kafka 的原因:
- 文档解析 + 向量化是 CPU 密集型任务,处理时间长(一个大文件可能要几分钟),Kafka 的高吞吐量适合这种场景
- Kafka 的消息可以重复消费(只要 offset 不提交),如果某次向量化失败了,可以重新消费处理
- 文档处理顺序是重要的(同一个文件的分片要按照顺序合并),Kafka 的分区有序性可以保证同一个文件的处理顺序
Q13:Kafka 的消息丢失和重复消费怎么处理?
答:
消息丢失的三个阶段:
阶段一:生产者 → Kafka(消息没到 Kafka)
- 解决方案:开启 ACK=all(所有副本都收到才算成功)
- 开启 重试机制(
retries=Integer.MAX_VALUE)
阶段二:Kafka 自身(Kafka 宕机丢消息)
- 解决方案:设置 副本数 >= 3(
replication.factor=3) - 设置 min.insync.replicas=2(至少 2 个副本收到才算成功)
阶段三:Kafka → 消费者(消费者处理失败)
- 解决方案:手动提交 offset(处理成功才提交)
- 如果处理失败,不提交 offset,Kafka 会重新推送这条消息
重复消费的问题(Kafka 的 at-least-once 语义):
- 消费者处理了消息,但提交 offset 之前宕机了 → Kafka 会重新推送这条消息 → 消费者又处理一次
- 解决方案:幂等性设计
- 每条消息带一个唯一业务 ID(比如
fileMd5) - 消费者处理前,先查数据库”这个文件是否已经处理过了”
- 如果处理过了,直接跳过(不重复处理)
- 每条消息带一个唯一业务 ID(比如
Q14:Kafka 的分区(Partition)是什么?你们项目里怎么用的?
答:
分区是什么?
- 一个 Kafka Topic 可以分成多个 Partition(分区)
- 每个 Partition 是一个有序的、不可变的消息序列
- 不同 Partition 之间无顺序保证(Partition A 的消息和 Partition B 的消息顺序无关)
为什么要分区?
- 并行处理:不同 Partition 可以由不同消费者并行消费(提高吞吐量)
- 水平扩展:Partition 可以分布在不同 Broker 上(突破单机限制)
你们项目里怎么用分区的?
- 以
fileMd5作为分区键(Partition Key) - 这样同一个文件的所有消息都会分到同一个 Partition(保证顺序处理)
- 不同文件的消息可以并行处理(提高吞吐量)
1 | |
分区数量怎么设置?
- 分区数 < 消费者数:有些消费者会空闲(浪费资源)
- 分区数 > 消费者数:有些消费者要处理多个分区(可能瓶颈)
- 最佳实践:分区数 = 消费者数 × (1~2)(充分利用并行性)
Q15:Kafka 的消费者组(Consumer Group)是什么?
答:
消费者组是 Kafka 的广播/单播机制:
| 场景 | 配置 |
|---|---|
| 单播(一条消息只被一个消费者处理) | 所有消费者在同一个组里 |
| 广播(一条消息被所有消费者处理) | 每个消费者在不同的组里 |
1 | |
你们项目里的用法:
- 文档处理消费者 → 组名:
file-processor-group(单播,保证每条消息只被处理一次) - 通知消费者 → 组名:
notification-group(广播,和文档处理并行)
四、Spring Security + JWT 相关(5 道)
Q16:你们项目里 JWT 双令牌机制是怎么实现的?为什么需要双令牌?
答:
双令牌机制:Access Token + Refresh Token
| Access Token | Refresh Token | |
|---|---|---|
| 用途 | 访问业务接口 | 换取新的 Access Token |
| 有效期 | 短(15 分钟 ~ 1 小时) | 长(7 天 ~ 30 天) |
| 存储位置 | 内存(前端)或 HttpOnly Cookie | HttpOnly Cookie(防 XSS) |
| 如果泄露 | 危害较小(很快就过期了) | 危害较大(需要支持吊销) |
流程:
1 | |
为什么需要双令牌?(面试高频)
- 安全性:如果只有一个长期有效的 Token,一旦泄露,攻击者可以一直用。双令牌机制下,即使 Access Token 泄露了,也只在 15 分钟内有效。
- 用户体验:用户不用每次 15 分钟就重新登录,Refresh Token 有效期内自动刷新 Access Token,用户无感知。
- 可吊销性:Refresh Token 可以存在数据库里,如果发现账号异常,可以把 Refresh Token 删掉,强制用户重新登录。
源码依据:SecurityConfig.java 第 75 行设置了 sessionCreationPolicy(STATELESS)(无状态,不用 Session),说明用的是 Token 机制。
Q17:JWT 的结构是什么?你们项目里 JWT 存了什么信息?
答:
JWT(JSON Web Token)的结构:Header.Payload.Signature
1 | |
你们项目里 JWT 存了什么?(根据 JwtAuthenticationFilter.java 推断)
- 用户 ID(
sub) - 用户名(
username) - 角色(
roles:USER或ADMIN) - 过期时间(
exp)
安全风险:
- Payload 可以被解码:不要存敏感信息(比如密码、身份证号)
- 签名密钥泄露:攻击者可以伪造任意 Token(所以要保管好
secretKey) - JWT 无法主动吊销(因为是无状态的):解决方案是把有效期设短,或者用黑名单机制(Redis 存已吊销的 Token ID)
Q18:Spring Security 的过滤器链是什么?你们项目里加了哪些过滤器?
答:
Spring Security 是一个过滤器链(类似流水线,每个过滤器做一件事)。
你们项目里的过滤器链(根据 SecurityConfig.java 第 40-91 行):
1 | |
JwtAuthenticationFilter 做了什么?(根据文件名推断)
- 从请求 Header 里取
Authorization: Bearer <token> - 解析 JWT,验证签名是否有效、有没有过期
- 从 JWT 里取出用户 ID、用户名、角色
- 创建一个
Authentication对象,放进SecurityContextHolder(这样后续控制器里可以通过SecurityContextHolder.getContext().getAuthentication()获取当前登录用户)
OrgTagAuthorizationFilter 做了什么?(根据文件名推断)
- 获取当前登录用户的组织标签(
orgTag) - 检查用户要访问的资源(比如某个文档)的组织标签
- 如果用户有权限(自己的、公开的、或同组织的),放行;否则返回 403
Q19:你们项目里的 RBAC(角色权限控制)是怎么设计的?
答:
RBAC(Role-Based Access Control)的核心是:用户 → 角色 → 权限,而不是”用户 → 权限”(这样权限管理会非常混乱)。
我们项目里的 RBAC(根据 SecurityConfig.java 第 59-69 行):
1 | |
多租户隔离(防止用户 A 看到用户 B 的私有文档):
- 每个文件有一个
orgTag(组织标签) - 搜索时,ES 查询会自动加上权限过滤条件(只能搜到自己有权限的文档)
- 详见 Q11 的回答
源码依据:SecurityConfig.java 第 59 行 .requestMatchers("/api/v1/upload/**", ...).hasAnyRole("USER", "ADMIN"),说明普通用户和管理员都能访问上传接口。
Q20:如果 JWT 泄露了,你们项目里有什么应对措施?
答:
JWT 泄露是非常严重的安全事故,我们项目里有这几层防护:
第一层:缩短 Access Token 有效期
- Access Token 只有 15 分钟有效期,即使泄露了,最多也被利用 15 分钟
第二层:Refresh Token 吊销机制
- Refresh Token 存在数据库里(或者 Redis 里)
- 如果发现账号异常(比如用户举报、IP 异常),可以把 Refresh Token 删掉
- 这样即使 Access Token 还没过期,等它过期后,攻击者也无法换取新的 Access Token
第三层:用户主动登出
- 登出时,后端把 Refresh Token 从数据库/Redis 里删掉
- 这样攻击者手里的 Refresh Token 就失效了
第四层:HttpOnly Cookie 防 XSS
- Refresh Token 存在 HttpOnly Cookie 里,JavaScript 无法读取(防止 XSS 攻击偷走 Token)
如果 JWT 还是泄露了怎么办?
- 立即吊销:在数据库里把这个用户的
refresh_token字段置空,强制用户重新登录 - 通知用户:发送邮件/短信,告知账号可能异常,建议修改密码
五、MinIO 相关(3 道)
Q21:MinIO 是什么?和阿里云 OSS、AWS S3 有什么区别?
答:
MinIO 是一个开源的、兼容 S3 协议的对象存储系统,可以自己搭建(私有云),也可以用在公有云。
| MinIO(自建) | 阿里云 OSS / AWS S3(公有云) | |
|---|---|---|
| 部署方式 | 自己部署(物理机/虚拟机/K8s) | 直接用云服务商的 |
| 成本 | 只有服务器成本(便宜) | 按存储量 + 流量收费(贵) |
| 数据控制权 | 数据在自己服务器上(安全) | 数据在云服务商那里 |
| 维护成本 | 需要自己维护(运维成本) | 云服务商维护(省心) |
| 适用场景 | 企业内部系统(数据不能出内网) | 面向公网的应用 |
我们项目为什么用 MinIO?
- 企业知识库系统,文档可能包含敏感信息(不能传到公有云)
- 用 MinIO 可以把文件存在企业自己的服务器上(数据不出内网)
- 兼容 S3 SDK(
minio-javaSDK),以后如果要迁移到阿里云 OSS,代码不用改太多
MinIO 的核心概念:
- Bucket(桶):类似文件夹,用来组织对象
- Object(对象):存储的文件,每个对象有一个唯一的键(Key)
- 我们项目里:Bucket =
uploads,对象键 =chunks/{fileMd5}/{chunkIndex}(分片)或merged/{fileName}(合并后的文件)
Q22:你们项目里 MinIO 的分片合并是怎么做的?合并后分片会删除吗?
答:
分片合并(根据 UploadService.java 第 541-676 行):
1 | |
为什么合并后要删除分片?
- 节省存储空间:分片占了
5MB * N,合并后的文件也是这么大,不删的话占双倍空间 - 避免脏数据:如果分片不删,下次上传同一个文件(相同 MD5)时会误以为分片还在
合并失败怎么办?
- 如果合并过程中失败了(比如某个分片读不出来),会抛异常,前端可以重试合并操作
- MinIO 的分片文件不会被自动删除(因为只有合并成功才执行删除),所以重试时可以重新合并
Q23:MinIO 怎么保证文件不丢?你们项目里做了什么配置?
答:
MinIO 本身支持纠删码(Erasure Code),可以在丢失 N/2 个磁盘的情况下仍然恢复数据。
纠删码原理(类似 RAID 5/6):
1 | |
我们项目里的配置(根据 application.yml 或 application.properties 推断):
- MinIO 部署时启用了纠删码模式(
minio server /data{1...6}启动 6 个磁盘的纠删码模式) - 或者用了多副本模式(最少 2 个副本,数据同时在两个磁盘上)
应用层的保障:
- 文件合并后,可以选择保留原始分片一段时间(比如 7 天),防止合并后的文件损坏后可以恢复
- 定期备份 MinIO 数据到冷存储(比如每小时同步一次到远程 MinIO 或公有云 OSS)
六、可观测性(Prometheus)相关(3 道)
Q24:你们项目里 Prometheus 是怎么配置的?采集了哪些指标?
答:
Prometheus 是一个监控系统,负责:采集指标 → 存储 → 查询 → 告警。
我们项目里采集的指标(根据 MetricsConfig.java 和项目描述):
JVM 指标(自动采集,通过
micrometer-registry-prometheus):- 堆内存使用量
- GC 次数/时间
- 线程数
业务指标(自定义):
- 文件上传计数器(
fileUploadCounter,代码第 53 行) - 搜索耗时定时器(
searchTimer,HybridSearchService.java第 68 行) - 文档处理成功率
- 文件上传计数器(
HTTP 请求指标(通过
micrometer-spring-web自动采集):- 每个接口的请求次数、耗时、成功率
配置方式:
1 | |
Prometheus 怎么采集?
- Spring Boot 应用暴露
/actuator/prometheus端点(我们项目里放行了这个端点,见SecurityConfig.java第 51 行) - Prometheus 服务端定期(比如每 15 秒)来拉取这个端点的指标数据
Q25:有了 Prometheus,为什么还要用 Grafana?
答:
Prometheus 负责”存数据”,Grafana 负责”展示数据”,它们是配合使用的。
| Prometheus | Grafana | |
|---|---|---|
| 功能 | 时间序列数据库(TSDB) | 可视化面板(Dashboard) |
| 查询 | 自带 PromQL 查询语言 | 支持多种数据源(Prometheus、ES、MySQL…) |
| 告警 | 自带告警规则引擎 | 告警通知可以接钉钉/企微/邮件 |
| 界面 | 很丑(自带一个简单的图表) | 很漂亮(各种图表、面板) |
典型的监控架构:
1 | |
我们项目里的 Grafana 面板(推断):
- 系统监控:CPU、内存、磁盘、网络
- JVM 监控:堆内存、GC、线程数
- 业务监控:文件上传量(QPS)、搜索耗时(P99)、文档处理成功率
Q26:如果你们项目线上出问题了,你怎么排查?
答:
这是我们项目里可观测性的价值所在。排查流程:
第一步:看监控面板(Grafana)
- 是不是 Full GC 太频繁了?(看 JVM 面板)
- 是不是某个接口耗时突然变长了?(看 HTTP 请求面板)
- 是不是文件上传量突然暴涨了?(看业务指标面板)
第二步:看日志
- 我们项目里用了
SLF4J + Logback,关键操作都打了日志(UploadService.java里几乎每行都有logger.info()) - 用 ELK(Elasticsearch + Logstash + Kibana) 或 Grafana Loki 聚合所有实例的日志,按关键字搜索
第三步:看链路追踪(如果有接入)
- 用 SkyWalking 或 Zipkin,可以看到一个请求经过了哪些服务、每个服务耗时多少
- 比如”用户上传文件 → 合并 → 解析 → 向量化”这个流程,哪一步最慢
第四步:上机器排查
top:看哪个进程占 CPU 高jstack <pid>:看线程栈(有没有死锁、大量线程阻塞)jmap -histo:live <pid>:看堆里对象分布(哪个类的对象最多,可能有内存泄漏)
七、云原生化 / K8s 相关(4 道)
Q27:你们项目里 K8s 的 Deployment 和 HPA 是怎么配置的?
答:
Deployment 是 K8s 里用来部署无状态应用的资源对象。
我们项目里的 Deployment 配置(推断):
1 | |
HPA(Horizontal Pod Autoscaler) 是 K8s 里用来自动扩缩容的。
1 | |
扩容流程:
1 | |
Q28:你们项目里的多阶段 Dockerfile 是怎么写的?有什么好处?
答:
多阶段构建(Multi-stage Build) 是 Docker 17.05+ 的功能,可以把构建环境和运行环境分开,最终镜像只包含运行时需要的文件(体积小、安全)。
我们项目里的 Dockerfile(根据项目描述推断):
1 | |
好处:
- 镜像体积小:最终镜像只有 JRE + JAR 包(大约 200~300MB),如果用 JDK 镜像要 500MB+
- 安全:构建工具(Maven、源码)不会出现在最终镜像里(减少攻击面)
- 分层构建快:Maven 依赖(基本不变)和源代码(经常变)分开 COPY,利用 Docker 的层缓存,重新构建时不用重新下载依赖
Q29:K8s 里 Service 和 Ingress 的区别是什么?你们项目里怎么用的?
答:
Service 是 K8s 里用来暴露服务的(给集群内的其他 Pod 访问)。
Ingress 是 K8s 里用来暴露服务给集群外的(外部用户通过域名访问)。
1 | |
我们项目里的配置(推断):
1 | |
Q30:你们项目里怎么做滚动更新的?更新时用户会不会感觉到服务中断?
答:
滚动更新(Rolling Update) 是 K8s Deployment 的默认更新策略,保证更新过程中服务不中断。
流程:
1 | |
关键参数(Deployment 的 spec.strategy):
1 | |
maxSurge=1, maxUnavailable=0 的效果:
- 更新时,先启动 1 个新 Pod,新 Pod 就绪后,再删 1 个旧 Pod
- 全程可用 Pod 数 >= replicas(用户完全感觉不到中断)
我们项目里:maxUnavailable 应该设为 0(因为是企业内部系统,用户体验很重要)。
八、开放性问题(3 道)
Q31:如果你们项目的文档量涨到 1000 万块,ES 还能撑住吗?怎么优化?
答:
1000 万块(文本块)对 ES 来说完全没问题(ES 设计就是处理亿级文档的)。
但如果文档量继续涨(1 亿+),可以这样优化:
- ES 集群扩容:加数据节点(Data Node),把 1 亿文档分散到多个节点上
- 按组织分索引:不同组织的文档存在不同 ES 索引里(
knowledge_base_org_a,knowledge_base_org_b…),缩小单个索引的大小 - 热温架构:最近 30 天的文档放”热节点”(SSD),更早的放”温节点”(HDD),降低存储成本
- 向量索引优化:2048 维向量太高了,可以用 PCA 降维到 512 维或 768 维(牺牲一点精度,换来查询速度快几倍)
Q32:如果让你重新设计这个项目,你会改什么?
答:
(这是开放性问题,没有标准答案,以下是可以说的点)
分片上传的并发控制:目前代码里没有对”同一文件同时被多个用户上传”做并发控制,可以加一个分布式锁(Redis
SETNX)向量化失败的重试机制:目前如果某块向量化失败了,没有看到明确的重试逻辑,可以加一个死信队列(失败 3 次后人工介入)
ES 权限过滤的性能优化:目前每次搜索都要带上
orgTag过滤条件,如果用户属于 100 个组织,查询条件会很长,可以改为在应用层做权限过滤(先搜,再过滤)MinIO 分片合并的原子性:目前合并和删除分片不是原子操作(合并成功、但删除分片失败了,会留垃圾数据),可以用 MinIO 的 WORM(Write Once Read Many) 特性
Q33:你在做这个项目时遇到的最大挑战是什么?怎么解决的?
答:
(结合你自己的实际经历说,以下是一个参考模板)
我遇到的最大技术挑战是”如何实现混合检索的高准确率”。
背景:企业专有词汇(比如”XX 系统”、”YY 平台”)在 Embedding 模型的训练数据里没有,向量检索召回率很低。
我做的优化:
- 调整 ES 的搜索策略:不仅限于 KNN + BM25,还加了按组织标签加权(用户所属组织的文档排名靠前)
- 优化文本切块策略:不是固定 500 字一切,而是按段落/章节切(保留语义完整性)
- 调整 Embedding 模型:从通用的
text-embedding-ada-002换成支持微调的模型,用企业自己的文档微调了一下
效果:检索准确率从 65% 提升到了 87%。
总结:面试前必看
- Redis:Bitmap、持久化、过期策略、Key 设计 —— 必问
- ES:KNN 原理、BM25、RRF、权限过滤、Mapping 设计 —— 必问
- Kafka:消息可靠性、重复消费、分区、消费者组 —— 高频
- Spring Security:JWT 双令牌、过滤器链、RBAC —— 必问
- MinIO:分片合并、纠删码、为什么不用公有云 —— 中频
- K8s:Deployment、HPA、滚动更新、Service vs Ingress —— 中频
最后提醒:面试官可能会让你现场看一段代码,指出问题。一定要把项目里的核心代码(UploadService.java、HybridSearchService.java、SecurityConfig.java)过一遍,确保能讲清楚每一行在干什么。
加油!祝你面试顺利!🎉