面试追问地图
| 主问题 | 必讲关键点 | 下一层追问 |
|---|---|---|
| 秒杀 | 限流、库存、异步、幂等、防刷 | Redis 扣减成功但 MQ 失败、如何对账 |
| 库存防超卖 | DB 乐观锁、Redis Lua、分段库存 | 三种方案怎么选、超卖 vs 少卖优先级 |
| Feed 流 | 推/拉/推拉结合、写扩散 vs 读扩散 | 大 V 阈值、收件箱只存 ID、归并排序 |
| 数据一致性 | 事实源、缓存、消息、补偿 | 重复/乱序消息、补偿失败 |
| LBS 查询 | 空间索引粗筛、精确计算 | 边界、坐标系、热点区域和分片 |
| 实时轨迹 | 接入、队列、流处理、存储 | 乱序、重复、迟到数据、轨迹压缩 |
| 分布式事务 | 2PC/TCC/Saga/消息 | 业务侵入、悬挂、空回滚、幂等 |
| 限流 | 容量基线、令牌桶、分层限流 | 固定窗口临界问题、Redis+Lua 滑动窗口、RateLimiter 预热 |
| 熔断降级 | 慢调用/异常、状态转换、兜底 | 半开探测、兜底数据是否安全 |
| RPC | 代理、序列化、协议、连接、治理 | 超时、重试、服务发现、流控 |
| 短链 | ID、编码、存储、重定向 | 冲突、热点、过期、风控 |
| 分布式 ID | 唯一、趋势递增、高可用 | 时钟回拨、号段用尽、机房隔离 |
| QPS 提升 | 找瓶颈、缓存、异步、扩容 | 容量估算、压测模型、数据一致性 |
| 微服务拆分 | 业务边界、数据归属、渐进迁移 | 跨服务事务、回滚、组织成本 |
系统设计题开场先澄清用户量、QPS、数据量、一致性和延迟目标;没有规模假设的架构图没有判断依据。
一、高并发系统设计
核心设计原则
应对高并发的三板斧:
1. 缓存(Cache)- 减少数据库访问
2. 异步(Async)- 削峰填谷
3. 分布式(Distributed)- 水平扩展
如何设计一个高并发秒杀系统?
难度 🔴
关键挑战: 瞬时大流量,库存有限
流程图:
流量 → 限流(令牌桶) → 请求队列 → 处理
↓
缓存检查(库存)
↓
写消息队列(异步)
↓
返回成功/失败
↓
异步线程处理订单、扣库存
关键实现:
@Service
public class SecKillService {
private static final RateLimiter LIMITER = RateLimiter.create(1000);
public SecKillResult secKill(int productId, int userId) {
// 1. 限流
if (!LIMITER.tryAcquire()) {
return SecKillResult.fail("系统繁忙,请稍后重试");
}
// 2. 防刷检查
String userBuyKey = "seckill:bought:" + productId + ":" + userId;
if (redis.hasKey(userBuyKey)) {
return SecKillResult.fail("您已参加过本次秒杀");
}
// 3. 检查库存(从缓存)
String stockKey = "seckill:stock:" + productId;
Long stock = (Long) redis.opsForValue().get(stockKey);
if (stock == null || stock <= 0) {
return SecKillResult.fail("库存已售罄");
}
// 4. 原子地减库存
Long remaining = redis.opsForValue().decrement(stockKey);
if (remaining < 0) {
redis.opsForValue().increment(stockKey);
return SecKillResult.fail("库存已售罄");
}
// 5. 异步处理订单
mqProducer.send("seckill-topic", new SecKillEvent(productId, userId));
// 6. 标记用户已购买
redis.opsForValue().set(userBuyKey, "1", 24, TimeUnit.HOURS);
return SecKillResult.success("抢购成功,请等待订单确认");
}
}关键优化点:
- 缓存库存:使用 Redis 而不是数据库
- 原子操作:Redis DECR 是原子的,避免超卖
- 异步处理:消息队列异步处理订单,快速返回
- 防刷机制:限流 + 用户购买记录
库存扣减怎么防超卖?三种方案怎么选?
是什么:秒杀示例用的是”Redis DECR 原子递减 + 负数回滚”,这是最简单的一种;完整答题要覆盖三种方案的适用场景和边界。
| 方案 | 做法 | 优点 | 风险 |
|---|---|---|---|
| DB 乐观锁 | UPDATE stock SET count=count-1 WHERE id=? AND count>0 | 不引入额外组件,天然强一致 | 高并发下大量请求打到同一行,行锁排队,DB 成为瓶颈 |
| Redis 预扣减 + Lua | 库存预热进 Redis,扣减和判断写成一个 Lua 脚本原子执行,再异步落库 | 抗并发能力最强,DB 只做异步兜底 | Redis 和 DB 库存短暂不一致;Redis 数据丢失需要有恢复机制(如定时对账) |
| 分段库存 | 总库存拆成 N 份(如 10 份),请求打散到不同分段各自扣减 | 把一行热点行拆成 N 行,从根上消除行锁竞争 | 某段提前扣完但其他段还有余量时,需要”退化到剩余段合并”兜底,实现复杂度最高 |
为什么 Redis 用 Lua 而不是”先 GET 判断再 DECR”:GET+判断+DECR 是三步,中间可能被其他请求插入(先检查后操作的竞态);Lua 脚本在 Redis 里单线程原子执行,把”判断库存是否够 + 扣减”打包成一步,杜绝了这个窗口。
超卖 vs 少卖的边界:
- 超卖(卖出数量 > 库存):致命,必须杜绝——本质是”读后写”没做原子化或没加锁
- 少卖(库存还有但显示已售罄):分段库存最容易出现(某一段先扣完,其他段还有余量却提前拒绝),代价远小于超卖,工程上优先保证不超卖,容忍轻微少卖,实在要压榨到零少卖再做”段间借调”
答题要点:先给结论——DB 乐观锁适合中低并发或没有 Redis 基础设施的场景;秒杀级流量要 Redis Lua 预扣减;如果 QPS 高到单 key 也扛不住(大促核心商品)才上分段库存。再主动提”超卖优先级远高于少卖”这句话,体现工程取舍判断。
高并发下如何保证数据一致性?
难度 🔴
三种一致性级别:
强一致性 → 弱一致性 → 最终一致性
具体方案:
1. 数据库层面:事务保证强一致性,主从复制存在延迟
2. 缓存层面:Cache-Aside 旁路缓存
3. 分布式层面:消息队列保证最终一致性,定时任务、人工兜底
二、LBS 地理信息系统
核心技术栈
| 技术 | 用途 | 选型 |
|---|---|---|
| 地理索引 | 快速查询周边点 | R 树、Geohash、Grid |
| 存储 | 持久化地点数据 | MySQL + Redis |
| 实时处理 | 轨迹更新、围栏 | Kafka + Flink |
| 地理库 | 地址解析、坐标转换 | PostGIS、GDAL |
如何实现亿级别地点库的快速查询?
难度 🔴
三种索引方案:
Geohash 方案(Redis 实现):
// 添加地点
redis.opsForGeo().add(key, new Point(longitude, latitude), locationName);
// 查询周围地点
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redis.opsForGeo().radius(key, circle);- 优点:简单,Redis 原生支持,查询快
- 缺点:精度有限,不支持复杂查询
四叉树方案:
- 优点:精度高,支持自适应细分,支持复杂查询
- 缺点:需要自实现,动态更新时需要重新组织树结构
R 树方案(PostGIS):
CREATE INDEX idx_location_geo ON locations USING GIST(location);
SELECT id, name FROM locations
WHERE ST_DWithin(location, ST_Point(116.3972, 39.9075)::geography, 5000);- 优点:性能最好,支持复杂地理运算
- 缺点:需要 PostgreSQL + PostGIS
选型建议:
- 初期:用 Redis GEO(快速上线)
- 小规模(< 百万):用四叉树
- 大规模(> 百万):用 PostGIS + Redis 缓存
如何实现实时轨迹处理和地理围栏?
难度 🔴
架构:
GPS 设备 → 数据采集 → 消息队列 → 实时处理 → 围栏判断 → 告警/存储
(LBS 服务) (Kafka) (Flink) (Redis) (DB)
轨迹存储优化:
常见的轨迹压缩算法:
1. DP 算法(Douglas-Peucker):移除不重要的点
2. 时间合并:相近时间的连续点合并
3. 空间量化:按网格划分,相同网格的点合并
优化后,存储量减少 80-90%
三、分布式事务
分布式事务的解决方案?
难度 🔴
| 方案 | 原理 | 一致性 | 性能 | 复杂度 |
|---|---|---|---|---|
| 两阶段提交(2PC) | 协调器控制 | 强一致 | 低 | 低 |
| Saga | 编排补偿 | 最终一致 | 高 | 高 |
| TCC | Try-Confirm-Cancel | 强一致 | 低 | 很高 |
| 本地消息表 | 本地事务 + 消息 | 最终一致 | 高 | 中等 |
Saga(最终一致,推荐):
原理:业务分为多个步骤,每个步骤是本地事务,失败时执行补偿操作
示例:订单 → 库存 → 支付 → 发货
失败时:取消订单 ← 恢复库存 ← 退款 ← 取消发货
TCC(强一致,最复杂):
Try 阶段:预留资源,不提交
Confirm 阶段:提交事务
Cancel 阶段:回滚,释放资源
本地消息表(最终一致,实用):
1. 业务操作和消息操作在同一个本地事务中
2. 发送消息给消费者
3. 定时扫描未完成的消息,重试
Seata AT 模式深度解析
频次 ★★★ · 难度 🔴 · 高频:阿里
Seata AT(Automatic Transaction)是阿里开源分布式事务框架的核心模式,自动补偿,业务侵入最低。
AT 模式执行流程:
阶段一(Branch Register):
- 业务 SQL 前,Seata 拦截器解析 SQL,生成 undo log:「执行前数据快照」
- 执行业务 SQL(本地事务提交)
- 提交前再次生成「执行后数据快照」,将回滚日志写入 undo_log 表(同一本地事务)
- 注册分支事务到 TC(Transaction Coordinator)
阶段二(全局提交 / 回滚):
- 提交:TC 通知各分支提交,RM(资源管理器)异步删除 undo log
- 回滚:TC 通知各分支回滚,RM 根据 undo log 做反向 SQL 恢复数据
// undo log 示例(伪 JSON)
before: {id:1, name:"旧值", version:1}
after: {id:1, name:"新值", version:2}
→ 回滚时执行: UPDATE SET name="旧值", version=1 WHERE id=1AT 的关键问题:
| 问题 | 说明 | 解决方案 |
|---|---|---|
| 脏写 | T1 回滚恢复旧值时覆盖 T2 写入的新值 | 全局锁(TC 协调,行级别写锁) |
| 全局锁死锁 | 跨服务事务互相等锁 | 锁超时 + 重试 + 超时回滚 |
| 隔离性 | AT 默认读未提交 | 读操作加 SELECT FOR UPDATE 走全局锁 |
| 大事务 | undo log 膨胀 | 拆小事务、调大 maxCommitRetry |
AT vs TCC 选型对比:
| 维度 | AT | TCC |
|---|---|---|
| 业务侵入 | 低(自动拦截 SQL) | 高(需实现 Try/Confirm/Cancel 三段) |
| 一致性 | 最终一致 | 最终一致 |
| 性能 | 中等(全局锁 + undo log) | 高(无全局锁,资源预扣) |
| 适用场景 | 旧项目改造、SQL 简单 | 新项目设计、需要预留资源 |
| 底层原理 | 反向 SQL 补偿 | 业务补偿逻辑 |
关联知识: 数据库中「事务回滚」也是靠 undo log——InnoDB 的 MVCC 读视图依赖 undo log 构建旧版本,与 Seata AT 的补偿思路异曲同工。
如何实现服务限流?
频次 ★★★★ · 难度 🟡 · 高频:字节/美团
| 算法 | 原理 | 突发流量 | 缺陷 |
|---|---|---|---|
| 固定窗口计数 | 每窗口一个计数器,到点清零 | ❌ | 窗口边界可透 2 倍流量 |
| 滑动窗口计数 | 窗口切小格,随时间滑动统计 | ❌ | 格子越细越准,内存越大 |
| 漏桶 | 恒定速率流出,满则拒绝 | ❌ 削平一切尖峰 | 无法利用闲置容量 |
| 令牌桶 | 定速造令牌,桶存量=可透支的突发额度 | ✅ 支持 | 冷启动需配预热 |
固定窗口的临界问题(先点破这个才算理解):incr + expire 的计数在窗口边界会漏——限 100/s 时,前一秒最后 100ms 进 100 个、后一秒前 100ms 再进 100 个,都合法,但 200ms 内实际透过 200 个,瞬时流量翻倍。
滑动窗口计数(单机版,把 1s 切成 10 格的环形数组):
public class SlidingWindowLimiter {
private final long[] buckets = new long[10]; // 每格 100ms 的计数
private final long[] bucketTime = new long[10]; // 该格对应的时间片
private final int limit;
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis(), slice = now / 100;
int idx = (int) (slice % 10);
if (bucketTime[idx] != slice) { // 环形复用:过期格子清零再用
bucketTime[idx] = slice;
buckets[idx] = 0;
}
long sum = 0; // 只统计最近 1s 内有效的格子
for (int i = 0; i < 10; i++)
if (slice - bucketTime[i] < 10) sum += buckets[i];
if (sum >= limit) return false;
buckets[idx]++;
return true;
}
}分布式限流(Redis + Lua,ZSET 滑动窗口):member 记请求、score 记时间戳,一次原子完成”清过期 → 计数 → 判定 → 记录”:
-- KEYS[1]=限流key ARGV[1]=窗口ms ARGV[2]=阈值 ARGV[3]=当前ms ARGV[4]=请求id
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[3] - ARGV[1]) -- 移出窗口外的
local count = redis.call('ZCARD', KEYS[1])
if count >= tonumber(ARGV[2]) then return 0 end
redis.call('ZADD', KEYS[1], ARGV[3], ARGV[4])
redis.call('PEXPIRE', KEYS[1], ARGV[1])
return 1必须用 Lua 的原因:读计数和写记录分两条命令的话,并发下会一起超发——原子性诉求与Redis”批量操作怎么选”一节的 Lua 场景相同。ZSET 精确但费内存(每请求一个 member),高 QPS 下可退化为”Lua 里维护分格 Hash”省空间。
令牌桶与 Guava RateLimiter 预热:
// 平滑突发:桶里最多攒 1s 的令牌,闲时攒、忙时透支
RateLimiter limiter = RateLimiter.create(100.0);
// 平滑预热:启动后 3s 内发放速率从慢爬升到 100/s
RateLimiter warm = RateLimiter.create(100.0, 3, TimeUnit.SECONDS);
if (!limiter.tryAcquire()) return TOO_MANY_REQUESTS;预热(SmoothWarmingUp)解决的问题:下游刚启动时缓存是冷的、连接池是空的、JIT 没编译热点,真实容量远低于稳态值——限流器如果一上来就放稳态流量,等于亲手把下游打挂。这和Redis”缓存预热”是同一个思想:别让系统在零状态下承接满负载。实现上,Guava 用”存储令牌越多(说明越闲/越冷)、发放间隔越长”的斜坡函数爬升。
常见追问:
- 限流阈值怎么定?→ 压测得出单实例容量基线,留 20%~30% 余量;网关层按全局容量、服务层按单机容量分层限流
- 被限流的请求返回什么?→ HTTP 429 + Retry-After,RPC 返回特定错误码让调用方走降级,别让客户端盲目重试形成重试风暴(见API设计与接口治理)
- 单机限流和分布式限流怎么配合?→ 分布式限流保护共享资源(DB、下游总容量),单机限流保护自身(CPU/线程池);Redis 挂了要降级为单机限流兜底,而不是放开不限
通用概念:令牌桶本质是用”存量额度”把平均速率约束和突发容忍解耦——平均由造令牌速率控制,突发上限由桶容量控制。同类:TCP 拥塞窗口(cwnd 就是动态额度,见网络)、线程池的队列+maxPoolSize(稳态容量与突发缓冲分开配,见并发编程)。
服务熔断 vs 服务降级
| 熔断 | 降级 | |
|---|---|---|
| 目的 | 保护链路,防止雪崩 | 保护核心服务 |
| 触发条件 | 下游服务超时/不可用 | 系统负载过高 |
| 行为 | 停止调用,快速返回错误 | 非核心功能不处理或简化处理 |
熔断的三个状态:
Closed(正常)→ 请求失败率 > 50% → Open(打开)
↓
等待超时时间(30s)
↓
Half-Open(半开)
↓
请求成功 → Closed
请求失败 → Open
组件侧的落地(Hystrix 阈值、Sentinel 流量治理)见 SpringCloud微服务。
五、CAP/BASE 理论
CAP、BASE 与「分布式 vs 微服务」的完整解释统一收录在分布式系统,此处只留设计题答题速记:
- CAP:分区容错(P)是前提,发生分区时在一致性(C)和可用性(A)之间按具体操作取舍
- BASE:基本可用 + 软状态 + 最终一致,是对强一致性的降级妥协
- 设计题用法:先声明业务偏 CP 还是 AP(支付/库存偏 CP,点赞/浏览数偏 AP),再给对应手段(同步复制、多数派 vs 可靠消息 + 补偿对账)
六、系统设计 Q&A
让你设计一个 RPC 框架,怎么设计?
核心目标:调用远端服务时像调用本地方法一样简单——回答时按”动态代理拦截调用 → 序列化协议打包 → 网络通信收发 → 服务注册发现定位实例 → 负载均衡与容错处理失败”这条主链展开。每个环节的具体实现(动态代理源码、序列化方式对比、Netty 线程模型与 TCP 边界处理、注册中心选型、重试与幂等)已在专题篇有源码级展开,设计题的加分点是主动串联全链路而不是逐一背模块名,深挖见 Netty与RPC”RPC 框架”一章。
让你设计一个短链系统,怎么设计?
原理: HTTP 302 重定向(不用 301,因为 301 会被浏览器缓存,无法统计点击量)
生成短链: 分布式 ID + Base62 转换
1. 用分布式 ID 为每个长链分配唯一递增数字 ID
2. 将十进制 ID 转换为 62 进制(0-9、a-z、A-Z)
3. 6 位短链可存 62^6 ≈ 568 亿个链接
存储与抗并发:
- MySQL 存储映射关系,给短串加唯一索引
- 引入 Redis 做缓存
- 布隆过滤器防范缓存穿透
如何设计一个点赞系统?
核心思路:核心读写完全交给 Redis,异步同步到 MySQL。
Redis 存储设计:
- 用 Redis Set 记录谁点过赞,如
article:likes:10086 - 点赞:
sadd,取消:srem,判断:sismember,总数:scard
异步持久化:
- Redis 操作成功后往 MQ 发消息,消费者攒批批量写入 MySQL
大 V 热点防范:
- 加一层本地缓存(Caffeine/Guava Cache),定时汇总更新 Redis
Feed 流(关注列表/朋友圈)怎么设计?推模式还是拉模式?
核心问题:用户发一条动态,关注者刷新时要看到——数据在”发布时”还是”读取时”完成分发,是设计的分水岭。
| 模式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 推(Push,写扩散) | 发布时立即把动态 ID 写入每个粉丝的收件箱(Timeline 表/Redis List) | 读取只需查自己收件箱,读性能极高 | 大 V 发一条要写千万份收件箱,写放大;风暴式写入拖垮系统 |
| 拉(Pull,读扩散) | 只存一份到发布者的发件箱,粉丝刷新时实时拉取所有关注对象的发件箱再合并排序 | 发布只写一次,无写放大 | 关注数多时读要扇出查询 N 个人的发件箱再归并,读性能差、DB 压力大 |
| 推拉结合(主流方案) | 普通用户走推模式;大 V(粉丝数超阈值)改成拉模式,粉丝刷新时单独合并大V的动态 | 兼顾两者:多数写扩散享受读性能,少数大 V 不引发写风暴 | 实现复杂度高,合并逻辑要处理”关注了多个大 V”的归并排序 |
收件箱存储选型:
- Redis List/ZSet:
LPUSH写入、LRANGE分页读,ZSet 可用发布时间戳做 score 天然有序;量大时定期归档旧数据到 MySQL/HBase - 只存 ID 不存内容:收件箱只放动态 ID,详情单独查(内容表可缓存、可去重展示),避免同一条动态在千万粉丝收件箱里重复存储大字段
答题要点:先给出推/拉两个模式的本质(写扩散 vs 读扩散),再说明大厂都用推拉结合(新浪微博经典方案),最后主动提”收件箱只存 ID”这个减少存储放大的细节——这是简历上有社交类项目经历常被追问到的点。发布瞬时峰值的应对可衔接本篇「服务限流」一节。
让你设计一个分布式 ID 发号器,怎么设计?
四种方案对比与雪花算法时钟回拨的处理详见分布式系统,此处给设计题答法。
方案一:雪花算法(Snowflake)
64 位 = 1 位空 + 41 位时间戳 + 10 位机器 ID + 12 位流水号
优势:纯本地计算,无网络 IO,速度极快
隐患:时钟回拨
方案二:数据库号段模式(美团 Leaf)
一次从数据库取一个号段(如 1~1000),缓存在本地内存
双 Buffer 机制:第一批用到 20% 时,异步加载下一批
优势:即使数据库宕机,靠本地缓存的号码仍能运转
订单半小时未支付自动取消怎么实现?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 定时轮询 | 实现简单 | 数据库压力大 |
| JDK DelayQueue | 高效 | 无持久化,崩溃丢数据 |
| Redis Sorted Set | 分布式友好 | 依赖定时任务频率 |
| MQ 延迟队列(推荐) | 处理高效,异步 | 依赖 MQ 服务 |
| MQ 死信队列(推荐) | 隔离异常消息 | 处理不够精确 |
推荐方案: 下单时发送延迟消息,TTL 设为 30 分钟,消费者检查订单状态,未支付则取消。
追问链常问到”延迟任务底层怎么实现”——时间轮(Netty/Kafka)与堆(DelayQueue)的对比见数据结构与算法。
如果系统的 QPS 突然提升 10 倍,怎么设计应对?
紧急止血: 限流降级、关闭非核心业务、弹性扩容
架构演进:
- 读请求:多级缓存(本地 Caffeine + Redis 双层)
- 写请求:消息队列排队削峰
- 数据库:读写分离 + 分库分表
大流量网站,单 Redis 无法承压如何解决?
读写分离: 主节点负责写,从节点负责读
Redis Cluster 集群:
- 16384 个槽,
CRC16(key) % 16384映射到具体节点 - 数据分片分散请求,自动处理故障转移
如何设计一个可重入的分布式锁?
核心思路:锁状态用 Hash 存({owner: 线程标识, count: 重入次数}),获取锁时 owner 相同则 count++,释放时 count— 到 0 才真正删除 key;再叠加自动续期(看门狗)解决业务未跑完锁先过期的问题。这正是 Redisson 的生产级实现——不需要自己写,完整的 Lua 脚本、Hash 结构设计、看门狗续期机制、以及”看门狗能保证锁绝不失效吗”等追问,见分布式系统”Redisson 看门狗(Watchdog)如何实现自动续期?“一节。
有哪些负载均衡方案?
| 方案 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| 硬件负载均衡 | F5 设备 | 性能强大 | 成本极高 |
| 软件负载均衡 | Nginx | 灵活性高,成本低 | 性能不如硬件 |
| DNS 负载均衡 | DNS 分配 IP | 简单易用 | 无法感知实时状态 |
| CDN | 就近节点 | 减少延迟 | 主要适用于静态内容 |
微服务架构是什么?
核心:把大应用拆分成小的独立服务,各自负责具体业务,可独立开发、部署、扩展。
核心组件:
- 服务注册发现:Nacos/Eureka
- 服务调用:RESTful HTTP 或 RPC(Dubbo/gRPC)
- 链路追踪:Skywalking/Zipkin
- 配置管理:配置中心统一管理
- 网关:Spring Cloud Gateway/Nginx
- 异步解耦:消息队列
- 数据管理:每个服务独立数据库,分布式事务用 Seata
适用场景:业务复杂、团队规模大、需要快速迭代。小项目单体更合适。
服务网格(Service Mesh)
频次 ★★★ · 难度 🟡 · 高频:字节/阿里/腾讯
服务网格是微服务架构的演进——将服务间通信(负载均衡、熔断、重试、认证、监控)从业务代码中剥离到基础设施层,由 Sidecar 代理统一处理。
核心架构:
┌──────────────────────┐
│ 控制平面 │ ← istiod(Pilot/Citadel/Galley 合并)
│ 配置/证书/服务发现 │
└──────┬───────────────┘
│ 下发 xDS 配置
▼
┌──────────────────────┐
│ 数据平面 │ ← Envoy Sidecar 代理
│ 拦截所有进出流量 │
│ 实现流量管理/安全/观测 │
└──────────────────────┘Istio 核心组件:
| 组件 | 功能 |
|---|---|
| Envoy(数据面) | L4/L7 代理,拦截所有服务流量,实现熔断、重试、负载均衡、流量分割和故障注入 |
| Pilot(控制面) | 服务发现和流量管理,将路由规则转换为 Envoy 可理解的 xDS 配置下发 |
| Citadel(控制面) | 证书签发和 mTLS 管理,服务间自动加密通信 |
| Galley(控制面) | 配置验证和分发,Istio 1.5+ 后与 Pilot/Citadel 合并为单一 istiod |
解决了什么问题:
- 代码无侵入:熔断/重试/超时/限流不再需要每个微服务自己实现,Sidecar 统一处理
- mTLS 自动化:服务间加密、身份认证零代码改动
- 灰度发布:通过 VirtualService + DestinationRule 按百分比/Header/Cookie 分流,支持金丝雀/A-B 测试
- 可观测性:自动收集 Metrics/Logs/Traces,无需手动埋点
面试追问:
- 服务网格一定比传统微服务好吗?→ 不一定。小团队/简单服务引入 Istio 会增加运维复杂度(Envoy 额外资源消耗约 5%-15%、Istio 本身需要专人维护)。服务网格在大规模、多语言、频繁灰度、强安全的场景下优势明显。
- Sidecar 模式与传统的 SDK 模式对比?→ SDK 模式(如原始 Dubbo/Spring Cloud)在代码层集成,性能好、延迟低,但升级 SDK 需要全量服务重新发布;Sidecar 升级只换代理配置不影响业务,但多一跳延迟(Envoy 本地转发通常在 2-5ms)。
- Linkerd vs Istio?→ Linkerd 更轻量(控制面只一个组件,无 Envoy 换用 Rust 写的微代理),适合中规模集群;Istio 功能更全面,适合大型企业级场景。
分布式链路追踪(Distributed Tracing)
频次 ★★★ · 难度 🟡 · 高频:阿里/腾讯/字节
为什么需要链路追踪: 微服务中一次请求可能跨越数十个服务,传统日志无法将分散的调用串联成完整路径。链路追踪通过 TraceID 贯穿整条调用链,还原请求的完整拓扑。
核心数据模型(源自 Google Dapper 论文):
| 概念 | 说明 | 示例 |
|---|---|---|
| Trace | 一次请求的完整调用链路,由多个 Span 组成 DAG | TraceID = abc123 |
| Span | 一个服务/组件内的一个操作单元 | Span:OrderService.createOrder,耗时 45ms |
| SpanContext | 跨服务传递的追踪元数据(TraceID + SpanID + 采样标识) | HTTP Header:traceparent: 00-abc123-def456-01 |
全链路追踪流程:
客户端请求 → 网关(Gateway)生成 TraceID
→ 服务 A(生成 SpanA,透传 TraceID 到 HTTP Header)
→ 服务 B(解析 Header 得到 TraceID,生成 SpanB,记录耗时和状态)
→ 服务 C(同上)
← 返回
← 返回
← 返回
所有 Span 上报到 Collector → 存储(ES/Cassandra)→ UI 展示
主流实现方案对比:
| 方案 | 埋点方式 | 存储 | 特点 |
|---|---|---|---|
| SkyWalking | Java Agent 自动埋点(字节码增强),无代码侵入 | H2/ES/BanyanDB | 国产,自动感知能力强,国内大厂广泛使用 |
| Jaeger | 应用 SDK 或 OTel SDK 手动/半自动埋点 | ES/Cassandra/Kafka | CNCF 毕业,与 OTel 生态集成最佳 |
| Zipkin | SDK 埋点 | ES/Cassandra | Twitter 开源,轻量简单 |
| OpenTelemetry | 统一 SDK + Collector(不包含 UI 存储) | 后端无关(Jaeger/Zipkin/Prometheus) | 可观测性标准,不是追踪系统而是采集层 |
关键设计考量:
- 采样策略:全量采样性能成本太高(尤其高 QPS 服务),需要分层采样:错误请求 100% 采样 + 高流量接口按 1/100 采样 + 低频接口全局采样
- TraceID 传递:HTTP 用
traceparentHeader(W3C TraceContext 标准),RPC 用 metadata,MQ 用消息属性——所有跨服务调用都必须透传 TraceContext - 采样率的计算:通过 Head-based(请求入口决策是否采样)或 Tail-based(服务结束时根据结果决策),Head-based 更常见但可能漏采错误,Tail-based 准确但成本高
常见追问:
- 接入链路追踪后性能影响多大?→ 主要开销在 TraceID 生成和 Span 序列化上报。采样率 1% 时通常 <1% 的 CPU 开销;Agent 模式(SkyWalking)采用异步批量上报,对业务线程影响极小
- 如何排查 Trace 中没有的错误?→ Trace 没记录通常是采样率导致的漏采(错误请求概率低赶上不采),或 Span 上报失败。应配合 error log(自带 TraceID)兜底——日志和 Trace 相互补充见生产排障
- OpenTelemetry 为什么成为标准?→ 统一了 OpenTracing(API)+ OpenCensus(采集)分裂局面,单 SDKG 即可输出 Trace/Metrics/Logs 三类数据,支持多后端导出,是 CNCF 可观测性事实标准
单体项目怎么拆分成微服务架构?
1. 梳理依赖:画模块依赖图
2. 划分服务边界:遵循领域驱动设计(DDD)
3. 优先拆分:业务独立、变更频繁的模块
4. 绞杀者模式:新功能用微服务,老功能逐步迁移
5. 数据库拆分:先逻辑拆分,再物理拆分
6. 服务间调用:改为 Dubbo/Feign,加超时控制、熔断降级
7. 公共代码:抽成 SDK 或做成基础服务
8. 监控升级:ELK 日志收集 + 链路追踪
核心:业务驱动,不为拆而拆。
对外提供 API 服务,客户说请求接口超时了,怎么排查?
核心思路:先分清”偶发还是持续、所有客户还是个别”判断范围,再从全局监控(延迟分位数/错误率/流量)到 Trace 拆分逐段耗时,最后落到慢 SQL/连接池/下游依赖等具体环节——完整的排查步骤、判断优先级表和”下一条命令看什么”,见生产排障”接口突然变慢怎么排查?“一节,这里不重复展开。