10.3 消息队列
10.3.1 消息队列设计
什么是消息队列?
消息队列(Message Queue) 是一种在分布式系统或应用程序之间进行异步通信的机制。它基于“生产者-消费者”模型,主要作用是在不同的服务或组件之间传递消息(通常是数据或指令),以实现解耦、异步处理和流量削峰等功能。
简单来说:
-
生产者(Producer):负责创建并发送消息到消息队列。
-
消息队列(Message Queue):是一个中间缓冲区,用于暂存消息,确保消息不会丢失,并按一定顺序被处理。
-
消费者(Consumer):从消息队列中获取消息并进行处理。
消息队列充当了生产者和消费者之间的中介,两者不需要直接通信或相互知晓对方的存在,从而实现系统解耦。
消息队列有哪些应用场景?
-
异步处理(Async Processing)
-
场景:用户注册后需要发送邮件、短信通知,但不想让用户等待这些操作完成。
-
解决方案:将发送邮件/短信的任务放入消息队列,由后台消费者异步处理。
-
-
应用解耦
-
场景:订单系统和库存系统是两个独立服务,订单创建后需要扣减库存。
-
解决方案:订单服务将“扣减库存”消息发送到消息队列,库存服务自行消费,彼此不直接调用。
-
-
流量削峰 / 请求缓冲
-
场景:电商秒杀活动中,大量用户同时下单,系统可能瞬间扛不住。
-
解决方案:将用户请求放入消息队列中排队处理,后端按处理能力逐步消费,避免系统崩溃。
-
-
日志收集与处理
-
场景:分布式系统中多个服务产生大量日志,需要统一收集和分析。
-
解决方案:各服务将日志消息发送到消息队列(如Kafka),再由日志处理服务统一消费。
-
-
任务调度与分发
-
场景:有多个 worker 节点处理任务,需要公平高效地分配任务。
-
解决方案:使用消息队列作为任务队列,worker 从队列中获取任务执行。
-
-
微服务通信
-
场景:微服务架构中,服务之间需要相互通信,但不希望强依赖。
-
解决方案:通过消息队列进行事件驱动的通信,提升系统的弹性和可维护性。
-
【必问】消息队列的数据结构是什么?topic、broker是什么?
-
队列(Queue)
-
最基础的数据结构,遵循 FIFO(先进先出) 原则。
-
生产者(Producer) 往队列里 发送消息(enqueue)。
-
消费者(Consumer) 从队列里 接收消息(dequeue)。
-
-
主题(Topic)
-
消息按主题分类,生产者发送消息到某个 Topic,消费者 订阅(Subscribe)感兴趣的 Topic。
-
-
分区(Partition)
-
Topic 可以分成多个 Partition(分区),提高 并行处理能力。
-
每个 Partition 是一个独立的队列,消息按顺序存储。
-
生产者发送消息时可以指定 Partition,或者由 MQ 自动分配。
-
消费者可以按 Partition 并行消费。
-
-
Broker 的定义
-
Broker 是消息队列的服务器(Server),负责:
-
接收生产者发送的消息。
-
存储消息(持久化或内存)。
-
把消息分发给订阅的消费者。
-
-
多个 Broker 可以组成集群,提高 可用性(高可用)和吞吐量(高并发)。
-
消息队列如何保证消息不丢失?
生产者发送阶段 —— 保证消息成功到达消息队列
-
消息确认机制(ACK / Confirm机制)
-
失败重试机制
-
本地消息表(最终一致性方案,适用于业务层)
-
在发送消息前,先将消息保存到本地数据库(状态为待发送),然后异步发送到消息队列。
-
若发送成功,则更新本地状态;若失败则定时任务重试,确保最终消息进入队列。
-
消息队列存储阶段 —— 保证消息在 Broker 中不丢失
-
消息持久化(Persistence)
-
高可用集群 / 副本机制
消费者处理阶段 —— 保证消息被成功消费
-
消费者确认机制(ACK)
-
幂等性设计:例如通过业务唯一ID、去重表等方式避免重复操作。
-
死信队列(DLQ):对于多次重试仍失败的消息,可以转入死信队列进行人工干预或后续处理,避免消息被无限重试而丢失。
消息队列如何保证消息幂等性,即不被重复消费?
-
唯一 ID + 去重表(最常用)
核心思想:每条消息带一个 唯一 ID(如 UUID、业务订单 ID),消费者在处理前先检查该 ID 是否已经处理过。
-
业务状态机(适用于有明确状态的业务)
核心思想:业务数据本身有 状态流转,只有当前状态允许时才处理消息,否则忽略。
-
消息唯一键 + 本地缓存(适用于单机/小规模)
核心思想:消费者在 内存(如 std::unordered_set) 或 本地文件 记录已处理的消息 ID,避免重复处理。
-
消息队列本身的幂等机制(如 Kafka、RocketMQ)
某些 MQ 原生支持幂等性,例如:
-
Kafka:可以通过
enable.idempotence=true开启 生产者幂等(避免重复发送)。 -
RocketMQ:支持 消息去重(Message Deduplication),消费者可以配置
MessageListenerOrderly保证顺序消费 + 去重。 -
RabbitMQ:通常需要 业务层自己处理幂等性(如上述方法)。
消息队列如何处理/防止消息堆积?
消息队列中的消息堆积(Message Backlog / Queue Build-up)是指:生产者持续向队列发送消息,但消费者处理速度跟不上,导致消息在队列中不断积压,未被及时消费。
消息堆积如果不及时处理,可能会带来严重问题,比如:
-
消息延迟变高,影响业务实时性
-
消息队列存储空间被占满,导致新消息被拒绝或服务崩溃
-
系统负载升高,影响整体稳定性与性能
防止:
-
保证消费者处理能力 >= 生产者发送能力
-
水平扩展消费者(增加消费者实例)
-
优化消费者处理逻辑
-
使用批量消费
-
流量控制 / 生产限流
处理:
-
紧急扩容消费者
-
跳过非关键逻辑 / 降级处理
-
使用多线程 / 并发消费
-
临时增加分区
-
死信队列 + 人工干预
消息队列如何保证消息的有序性?
Kafka 的有序性机制:
-
Kafka 的消息是按照 Topic 下的 Partition(分区) 进行存储和消费的。
-
在同一个 Partition 内,消息是严格有序的(FIFO),并且消费者也是按顺序拉取的。
-
不同 Partition 之间的消息顺序是无法保证的!
RabbitMQ 的有序性机制:
-
RabbitMQ 本身不保证消息的顺序性,尤其是在使用多个消费者、多个线程、Prefetch、重试等机制时。
-
如果你将消息发送到同一个队列,并且只使用一个消费者(单线程)去消费,那么消息基本上是按 FIFO 顺序处理的。
消息队列设计成推消息还是拉消息?推拉模式的各自优劣?
推模式(Push Model):
-
消息到达 Broker 后,Broker 主动将消息推送给已订阅的消费者。
-
消费者无需主动请求,而是被动接收 Broker 推送过来的消息。
推模式优点:
-
实时性高
-
消费者无需轮询
-
简化消费者逻辑
推模式缺点:
-
难以控制消费速率(消费者压力大)
-
难以适应不同消费者的处理能力
-
消息确认(ACK)机制复杂
-
不适合高吞吐批量消费场景
拉模式(Pull Model):
-
消费者主动向 Broker 发起请求,拉取最新的消息。
-
消费者可以控制拉取的时机、频率和数量。
拉模式优点:
-
消费者可控性强
-
批量拉取,提高吞吐
-
消息处理更灵活可靠
-
适应性强
拉模式缺点:
-
实时性相对较低
-
消费者负担增加
-
空轮询问题
消息队列的模型有哪些?
-
点对点模型(Point-to-Point,简称 P2P 模型 / 队列模型)
-
消息只有一个消费者能收到(即一条消息只会被一个消费者处理)。
-
消息通过队列(Queue)进行传递。
-
消息被消费后通常会从队列中移除(除非配置了特殊机制,如死信队列)。
-
-
发布/订阅模型(Publish/Subscribe,简称 Pub/Sub 模型)
-
生产者发布消息到一个主题(Topic),多个订阅了该主题的消费者都会收到该消息的副本。
-
消息会被广播给所有订阅者,每个订阅者都能独立消费该消息。
-
消息的生命周期通常不由单个消费者控制。
-
-
消息队列模型的组合与扩展
-
RabbitMQ 的模型(灵活路由模型)
-
RabbitMQ 不仅支持经典的 P2P(Queue) 和 Pub/Sub(Exchange + Fanout),还支持更复杂的路由模型,通过 Exchange(交换机) 实现
-
-
Kafka 的模型(发布-订阅 + 分区消费)
-
Kafka 的核心是 Topic(主题),每个 Topic 可以分成多个 Partition(分区)
-
生产者向 Topic 发布消息
-
-
消息队列中的死信队列是什么?
死信队列(DLQ) 是消息队列中一种用于处理无法被正常消费的消息的特殊队列。当一条消息由于某些原因(如消费失败、超过重试次数、过期等)不能被正常处理时,可以被转移到死信队列中,以便后续进行分析、处理或人工干预,避免消息丢失或无限重试造成系统问题。
RabbitMQ、Kafka在架构和功能上有什么区别?
RabbitMQ
-
消息模型:基于 队列(Queue)和交换机(Exchange)的路由模型,支持多种路由方式(如 Direct, Topic, Fanout, Headers)
-
消息存储:消息一般存储在内存/磁盘中,消费者取走后默认删除(除非配置持久化+ACK机制)
-
消息消费模式:消费者从队列获取消息,一般消费完就删除
-
吞吐量:一般(万级 ~ 十万级 TPS,视配置而定)
-
消费者模型:Push 或 Pull,通常由 Broker 推送
Kafka:
-
消息模型:基于 Topic + Partition 的发布-订阅模型,天然支持一对多、多订阅者
-
消息存储:消息会 持久化到磁盘并保留一段时间(可配置),可被多个消费者重复消费
-
消息消费模式:消费者从 Partition 拉取消息,消息可保留多天,支持重放与回溯
-
吞吐量:超高吞吐(百万级 TPS,适合大数据场景)
-
消费者模型:Pull 模式,消费者主动拉取,更灵活可控
10.3.2 Kafka
Kafka的索引是什么?其设计有什么亮点?
Apache Kafka 的 索引(Index) 是 Kafka 日志存储机制中的一个重要组成部分,主要用于 快速定位消息在日志文件(Log Segment)中的物理位置,从而提高消息的读写效率。
-
偏移量索引(Offset Index)
-
作用:记录消息的 逻辑偏移量(Offset) 与 物理文件中的相对位置(Position) 的映射关系。
-
存储内容:每条索引项通常包含一个偏移量(offset)和对应的日志文件中的物理位置(position,即文件中的字节偏移)。
-
索引特点:
-
是 稀疏索引(Sparse Index),即不是每条消息都建立索引,而是每隔一定数量的消息建立一条索引项,以节省空间并保持高效。
-
通过二分查找快速定位到目标偏移量所在的日志段(Log Segment),再进一步找到具体的消息。
-
-
时间戳索引(Timestamp Index,可选,Kafka 0.10.0+ 引入)
-
作用:用于根据 消息的时间戳 快速定位到对应的日志位置。
-
存储内容:记录时间戳和对应的偏移量。可以用来实现基于时间的日志保留策略或者按时间范围查找消息。
-
主要用于:
-
按时间范围查询消息(如用于日志检索、审计等场景)。
-
实现基于时间的日志清理策略(如保留最近 N 天的消息)。
-
亮点:
-
稀疏索引:减少索引大小,提升效率,配合二分查找快速定位
-
Log Segment 机制:每个日志段独立管理,索引与日志文件一一对应,便于维护和清理
-
索引常驻内存:加快查找速度,减少磁盘 IO,提升吞吐量
-
高效查找流程:先通过索引定位大致位置,再顺序扫描找到确切消息,平衡了速度与资源消耗
Kafka的事务机制是什么?如何实现?
Kafka 的 事务机制(Transaction Mechanism) 是为了支持 跨分区、跨会话或跨生产者的原子性写入 而引入的一项重要功能,它使得 Kafka 不仅可以作为一个高吞吐的消息队列,还能支持类似数据库的事务语义,特别是在需要 “精确一次”(Exactly Once Semantics, EOS) 消息处理的场景中至关重要。
-
调用
commitTransaction()提交事务,Kafka 会通过 两阶段提交协议 确保所有消息 原子性地对消费者可见。-
准备阶段:事务协调器确保所有相关分区可以接受该事务的消息。
-
提交/中止阶段:根据生产者的指令提交或回滚事务。
-
-
事务协调器将状态写入
__transaction_stateTopic,用于故障恢复,确保即使生产者崩溃也能正确恢复或中止事务。
Kafka为什么性能高?
-
顺序 I/O(Sequential Disk Access):Kafka 将消息 持久化到磁盘上,并且采用顺序写入(append-only)的方式,而不是随机写入。
-
零拷贝(Zero-Copy)技术:传统消息传递需要多次内核态与用户态拷贝:而 Kafka 使用
FileChannel.transferTo()实现 零拷贝,直接将磁盘文件数据通过内核态发送到网卡,无需经过用户态。 -
批处理(Batching):Kafka 的 生产者和消费者都支持消息的批量发送与拉取,将多条消息打包成一个 Batch 一起处理。
-
页缓存(Page Cache)优化:Kafka 直接利用操作系统的页缓存(Page Cache),而不是自己维护一套内存缓存。
-
索引机制(稀疏索引 + 顺序查找):Kafka 为每个日志段维护了 稀疏的偏移量索引和时间戳索引,但 不是为每条消息都建索引,而是定期记录,占用空间小,且能快速定位到消息所在的大致位置。
-
异步非阻塞设计 & 高并发模型
Kafka中Zookeeper的作用?(已过时)
-
存储和管理 Kafka 的元数据(Metadata)
-
Kafka Controller 的选举
-
Broker 的注册与状态监控
-
分布式协调与 Watcher 机制
10.3.3 RabbitMQ
RabbitMQ的基本架构是什么?包括哪些核心组件?
-
生产者(Producer)
-
消费者(Consumer)
-
队列(Queue)
-
交换机(Exchange)
-
定义:接收生产者发送的消息,并根据一定的规则(Binding)将消息路由到一个或多个队列。
-
作用:是消息的“分拣中心”,不存储消息,只负责将消息路由到正确的队列。
-
类型(重要!)RabbitMQ 支持多种 Exchange 类型,常用的有:
-
Direct:根据 routing key 精确匹配绑定键,将消息路由到对应的队列。
-
Fanout:广播模式,将消息发送到所有绑定到该交换机的队列,忽略 routing key。
-
Topic:基于通配符匹配 routing key,如
logs.*或logs.error,灵活路由。 -
Headers:基于消息头(headers)中的键值对进行匹配,较复杂,使用较少。
-
-
-
绑定(Binding)
-
定义:连接 交换机(Exchange) 和 队列(Queue) 的规则。
-
作用:定义了消息如何从交换机路由到队列,通常会指定一个 routing key(路由键)。
-
特点:通过绑定,RabbitMQ 知道某个交换机收到消息后应该发给哪些队列。
-
-
路由键(Routing Key)
-
定义:一个字符串,用于在某些 Exchange 类型(如 Direct 和 Topic)中决定消息应该被路由到哪个队列。
-
作用:作为消息发送时的一个标识,与 Binding 中的规则相匹配,决定消息去向。
-
-
Broker(代理服务器)
RabbitMQ如何保证消息的可靠性?RabbitMQ能做消息持久化吗?
-
消息生产者发送阶段:确保消息成功到达 Broker(RabbitMQ 服务端)
-
生产者发送消息后,RabbitMQ 处理成功后会异步发送一个 Basic.Ack 给生产者;如果失败(比如队列不存在、磁盘满等),则返回 Basic.Nack。
-
-
消息 Broker 存储阶段:确保消息成功存储(不因宕机等原因丢失)
-
队列持久化(Durable Queue):声明队列时设置
durable=True,表示即使 RabbitMQ 重启,队列也依然存在。 -
消息持久化(Persistent Message):发送消息时,设置消息的
delivery_mode=2(2 表示持久化消息)。表示该消息会被写入磁盘,RabbitMQ 重启后仍然存在。
-
-
消息路由阶段:确保消息正确路由到目标队列
-
确保 Exchange 和 Queue 之间存在正确的 Binding,并且使用合适的 Routing Key。
-
对于无法路由的消息(比如 Fanout 以外的 Exchange 找不到匹配队列),可以通过设置 Alternate Exchange(备用交换机) 来接收这些“无路可去”的消息,避免丢失。
-
-
消息消费者处理阶段:确保消息被成功消费(并得到确认)
-
为保证可靠性,应设置 auto_ack=False,并在业务逻辑 处理成功后手动发送 Ack,处理失败可发送 Nack 或 Reject。
-
RabbitMQ中无法路由的消息会去到哪里?
对于无法路由的消息(比如 Fanout 以外的 Exchange 找不到匹配队列),可以通过设置 Alternate Exchange(备用交换机) 来接收这些“无路可去”的消息,避免丢失。
AMQP协议是什么?
AMQP(Advanced Message Queuing Protocol,即 高级消息队列协议)是一个应用层的网络协议,用于在分布式系统中实现消息的可靠、异步通信。它为应用程序之间传递消息提供了一种标准化、跨平台、跨语言的通信方式,是现代消息中间件(如 RabbitMQ、Apache Qpid 等)的核心协议之一。
AMQP 的工作流程 = RabbitMQ的工作流程
-
生产者 将消息发送到 Exchange,并指定一个 Routing Key。
-
Exchange 根据其类型和 Binding 规则,将消息路由到一个或多个 Queue。
-
Queue 存储消息,等待 消费者 来获取并处理。
-
消费者 从 Queue 中接收消息,并可向 Broker 发送确认(ACK)表示已成功处理。
RabbitMQ怎么实现延迟队列?
RabbitMQ 本身并不直接提供“延迟队列”(Delayed Queue)的功能,也就是说,它没有像某些消息队列(如 RocketMQ、RabbitMQ 插件、AWS SQS Delay Queue 等)那样内置一个可以直接设置消息延迟投递的队列类型。
但是,RabbitMQ 可以通过一些巧妙的机制或插件实现延迟队列的功能,最常见的有以下 两种主流方案:
方案一:TTL + 死信队列(DLX)
-
生产者 发送消息到一个 中间队列(Queue A),并设置该消息的 TTL(Time To Live,存活时间);
-
当消息在 Queue A 中过期后,如果没有被消费,就会变成 “死信”(Dead Letter);
-
通过配置,让这个 死信自动路由到另一个队列(Queue B,即实际的延迟目标队列);
-
消费者 监听 Queue B,即可获取到“延迟到期”的消息,进行消费。
方案二:使用 RabbitMQ 官方延迟消息插件
-
安装这个插件
-
使用一个特殊的 Exchange 类型:
x-delayed-message -
发送消息时指定
x-delay参数(单位:毫秒)
RabbitMQ的事务机制是什么?如何实现?
RabbitMQ 的 事务机制(Transaction) 是一种用于保证消息 可靠性投递 的机制,主要用来确保 生产者发送的消息能够成功到达 RabbitMQ 服务端,否则可以进行回滚,避免消息丢失或误认为发送成功。
不过需要注意的是,RabbitMQ 的事务机制 性能较低,通常 不推荐在高并发生产环境中使用,而是推荐使用更高效的 Publisher Confirm(消息确认)机制。
RabbitMQ 的事务机制基于 AMQP 协议的事务模型,核心命令包括三个:
-
txSelect:开启事务模式。
-
txCommit:提交事务,确认之前发送的消息正式发送到 RabbitMQ。
-
txRollback:回滚事务,之前发送的消息会被丢弃,不会到达队列。
RabbitMQ的集群模式是什么?
镜像队列模式(Mirrored Queues,队列高可用)
-
是 RabbitMQ 提供的一种 队列冗余机制,可以将 一个队列镜像(复制)到集群中的多个节点上;
-
每个镜像都是队列的一个完整副本,包括消息、状态等;
-
如果 主节点(Master)宕机,其中一个镜像节点(Mirror)会自动提升为新的主节点,从而 保证队列的高可用性。
RabbitMQ集群中,节点间如何同步数据?
-
元数据
-
RabbitMQ 集群中的各个节点通过 Erlang 的分布式通信机制(基于 EPMD、节点间 TCP 连接)进行 实时通信与状态同步。
-
当你在 一个节点上创建/删除交换机、队列或绑定 时,该操作会通过 Erlang 分布式协议 自动同步到集群中的其他节点。
-
-
消息数据
-
普通模式默认不同步。
-
镜像队列将主节点数据复制到从节点,从节点被动接收主节点的同步数据。
-
10.4 Docker/K8s
10.4.1 容器和镜像
【必问】docker是什么?容器技术是什么?容器技术==docker吗?
容器是一种虚拟化技术,这种技术将操作系统内核虚拟化,可以允许用户空间软件实例(instances)被分割成几个独立的单元,在内核中运行,而不是只有一个单一实例运行。
一个主机可以有多个相似或相同的容器,应用程序不知道自己运行在容器中。
docker!=容器,docker只是容器的一种。docker是当前最主流的容器工具。
docker的底层原理是什么?
docker是一个实现容器技术的软件,用到了linux内核的命名空间原理。
docker是CS架构的软件,命令行敲的命令会发送到一个守护进程docker Daemon执行。
docker镜像和容器的关系是什么?
docker镜像是只读的模板,用于创建容器。
容器是镜像的运行实例,可以启动、停止和删除。
容器也可以固化成镜像。
docker容器 vs 传统虚拟机,有什么区别?
-
docker速度快(基于当前系统创建不同的运行上下文)。
-
docker体量小(镜像文件可以自由定制裁剪)。
-
docker分发容易(dockerhub,Dockerfile)。
-
docker复杂度低(对于操作系统而言只是一个程序)。
-
但是虚拟机独立性相对较高。
10.4.2 docker命令、Dockerfile、docker-compose
说几个你常用的docker命令?
镜像操作:
docker images:显示存在的当前镜像
docker rmi 镜像ID:删除指定的镜像
docker build -t 镜像名称:tag dockerfile所在路径:编译镜像
docker tag 镜像名称:tag 镜像作者/新名称:tag:规范重命名镜像
容器操作:
docker ps -a:显示当前所有容器
docker rm 容器ID:删除指定容器,运行中容器不能删
docker start -ai 容器ID:启动之前退出的容器
docker stop 容器ID:停止指定容器
docker run -d 镜像名 -v 主机绝对路径: 容器内绝对路径 -p 主机端口: 容器内端口 -e 环境变量名=环境变量值 执行程序名:创建并运行容器
使用docker的run命令指定端口,若是和Dockerfile里的不一致,会发生什么?
如果使用 docker run -p 指定的端口与 Dockerfile 中 EXPOSE 的端口不一致,不会报错,但只有 docker run -p 指定的端口映射会生效,用于主机与容器间的通信。 EXPOSE 只是文档化提示,不实际做端口映射。
Dockerfile中有哪些常用的命令?
-
FROM 本地镜像名或dockerhub镜像名
-
WORKDIR 容器内绝对路径
-
RUN 命令
-
COPY 主机文件路径 容器内路径
-
EXPOSE 端口号
-
ENTRYPOINT ["程序"]或脚本
-
CMD ["命令或参数"]
docker-compose vs Dockerfile有什么不同?
Dockerfile 是用来定义单个镜像如何构建的脚本(如基础镜像、代码、依赖等)。
docker-compose.yml 是用来定义和运行多容器应用的工具,通过一个文件管理多个服务(如 Web、数据库等)及其配置(如网络、卷、端口等)。
10.4.3 K8s
【必问】K8s是做什么的?
-
自动化部署与回滚
-
服务发现与负载均衡
-
自动扩缩容 (Auto Scaling)
-
自我修复能力
-
配置管理与密钥管理
K8s和Docker的联系?
Docker 是容器运行时(Runtime),K8s 是容器编排器(Orchestrator)
-
Docker 负责 创建和运行单个容器(比如你在本地用
docker run启动一个 Nginx)。 -
K8s 负责 管理成百上千个容器(Pod),比如:
-
K8s 不直接管理 Docker,而是管理容器,而 Docker 是最常见的容器运行时之一。
Istio是做什么的?
在 Kubernetes (K8s) 中,Istio 是一个开源的 服务网格(Service Mesh) 解决方案,主要用于 管理、观察和控制微服务之间的网络通信,提供 流量管理、安全策略、可观测性 等核心功能,而无需修改应用程序代码。
Istio 主要提供以下三大核心能力:
-
流量管理(Traffic Management)
-
安全(Security)
-
可观测性(Observability)
微服务是什么?
微服务(Microservices) 是一种软件架构风格,它将一个大型、复杂的应用程序拆分成一组小而专一、松耦合、独立部署的服务,每个服务专注于完成一个特定的业务功能,并且可以独立开发、测试、部署和扩展。
每个服务运行在自己的进程中,通过轻量级通信机制(通常是 HTTP/REST 或 gRPC)相互协作,共同构成完整的应用。
微服务的特点是什么?
-
单一职责:每个微服务只关注一个业务功能,比如“用户管理”、“订单处理”。
-
独立部署:每个服务可以单独开发、测试、打包和部署,无需影响其他服务。
-
松耦合:服务之间通过 API(如 REST/gRPC)通信,内部实现互不依赖。
-
小而专注:每个服务代码量小,功能明确,便于理解和维护。
微服务的优势是什么?
-
弹性扩展:按需扩展特定服务,资源利用更高效。
-
独立部署:每个服务可单独开发、测试、部署,提高迭代效率。
-
松耦合、小而专注:单个服务故障不会导致整个系统崩溃。
-
易于维护:代码和服务职责单一,更易理解与维护。
一个典型的微服务系统需要哪些配套组件?
-
网关:Nginx
-
注册中心、配置中心:nacos
-
通信:gRPC、Kafka
-
日志:ELK
-
监控:Prometheus
你来设计一个微服务项目,你是如何设计的?
我一般设计成两层:业务层和能力层(中台),业务层接受用户请求,然后通过调用能力层来完成业务逻辑。
10.5 Zookeeper
【必问】Zookeeper是什么?
Zookeeper 是一个开源的、分布式的、高可用的协调服务(Coordination Service),由 Apache 基金会 开发并维护。它最初是为 Hadoop 生态系统中的分布式应用提供协调服务而设计的,但后来因其简单、可靠和功能强大,被广泛应用于各种分布式系统中。
Zookeeper可以用来做什么?
Zookeeper 主要用于解决分布式系统中多个进程/节点之间的协调问题,比如:
-
配置管理(Configuration Management)
-
命名服务(Naming Service)
-
分布式锁(Distributed Locking)
-
集群管理(Cluster Management)
-
Leader 选举(Leader Election)
-
状态同步(State Synchronization)
Zookeeper是数据结构是什么?
Zookeeper 的数据存储采用一种类似文件系统的树形结构(ZNode Tree),每个节点称为 ZNode,可以存储少量数据(一般不超过 1MB),并且可以有子节点。
ZNode 类型包括:
-
持久节点(Persistent):创建后一直存在,除非显式删除。
-
临时节点(Ephemeral):与客户端会话绑定,会话结束则自动删除。
-
顺序节点(Sequential):Zookeeper 会在节点名后自动追加一个单调递增的序号,常用于实现分布式锁或队列。
每个 ZNode 包含以下几个关键部分:
-
Path(路径):ZNode 的唯一标识,如 /config/app1,采用类似文件系统的层级路径。
-
Data(数据):每个 ZNode 可以存储一段二进制数据(通常是配置信息、状态等),大小限制通常为 1MB。
-
Children(子节点):一个 ZNode 可以有多个子 ZNode,形成树状结构。
-
Metadata(元数据):包括 版本号(version)、创建时间、修改时间、ACL 权限、节点类型 等信息。
Zookeeper的常用命令?
-
zkCli.sh -server 127.0.0.1:2181:启动客户端连接 -
ls:查看指定路径下的子节点(类似ls命令)。ls / -
get:获取指定 znode 节点上存储的 数据内容 以及该节点的 元数据(如版本号、创建时间等)。get /zookeeper -
create:创建一个新的 znode,并可附带存储一段字符串数据。create /my_node "Hello,ZooKeeper"-
创建 临时节点(ephemeral,会话结束自动删除):
create -e /temp_node "temp_data" -
创建 顺序节点(sequence,自动在名字后加序号,如 /node0000000001):
create -s /seq_node "sequence_data"
-
-
set:修改指定 znode 节点上存储的数据内容。set /my_node "Updated data at 2024" -
delete:删除指定的 znode 节点。delete /my_node
如何用Zookeeper做一个配置中心?
配置中心(Configuration Center) 是一个集中管理分布式系统中配置信息的服务,主要作用包括:
-
统一管理配置(如数据库连接、服务地址、功能开关等)。
-
动态更新配置(无需重启服务,实时生效)。
-
配置版本管理(支持回滚、灰度发布)。
-
配置权限控制(不同环境、团队访问不同配置)。
-
Zookeeper 的配置通常存储在 持久节点(Persistent ZNode) 中。
-
/config是根节点,存放所有配置。 -
每个配置项是一个 持久节点(Persistent),存储键值对(如
db_url = "mysql://...")。 -
可以按 应用/模块 划分子节点(如
/app1/timeout)。
-
-
配置读取流程(C++ 客户端)
-
服务启动时,从 Zookeeper 读取配置(如
/config/db_url)。 -
将配置加载到内存(如
std::map<std::string, std::string>)。 -
业务代码使用内存中的配置,避免频繁访问 Zookeeper。
-
-
配置动态更新(Watcher 机制)
-
客户端监听配置节点(如
/config/db_url)。 -
当配置被修改时,Zookeeper 会通知客户端(触发 Watcher 回调)。
-
客户端重新读取最新配置,并更新内存中的值。
-
如何用Zookeeper做一个注册中心?
注册中心(Service Registry) 是分布式系统中的服务发现组件,主要作用是:
-
服务注册(Service Registration):服务启动时,将自己的网络地址(IP:Port)、元数据(如服务名、版本) 注册到注册中心。
-
服务发现(Service Discovery):其他服务或客户端可以查询可用的服务实例,并动态获取其地址。
-
健康检查(Health Check):注册中心可以检测服务是否存活,自动剔除不可用的节点。
-
负载均衡(Load Balancing):客户端可以从多个服务实例中选择一个进行调用。
-
服务注册的存储结构(ZNode 设计)
-
/services是根节点,存放所有服务。 -
/service1、/service2是具体服务的节点。 -
/node1、/node2是临时节点(Ephemeral),存储服务实例的 IP:Port,服务宕机时自动删除。
-
-
服务注册流程(C++ 服务端)
-
服务启动时,向 Zookeeper 注册自己(创建临时节点)。
-
服务运行期间,保持 Zookeeper 连接(否则临时节点会被删除)。
-
服务停止时,Zookeeper 自动删除临时节点(或手动注销)。
-
-
服务发现流程(C++ 客户端)
-
客户端查询可用服务(如
/services/service1)。 -
获取所有子节点(即所有服务实例的 IP:Port)。
-
监听服务节点变化(Watcher 机制,当服务上下线时收到通知)。
-
选择其中一个实例进行调用(如随机、轮询、负载均衡)。
-
【必问】Zookeeper的Watcher机制是什么?
Watcher(监视器/监听器) 是 Zookeeper 提供的一种事件监听机制,允许客户端监听 ZNode(Zookeeper 数据节点)的变化,并在特定事件发生时得到通知。
-
Watcher 是一次性的(One-time Trigger):即一旦触发后,需要重新注册才能继续监听。
-
异步通知:当被监听的 ZNode 发生变化时,Zookeeper 服务器会异步通知客户端。
-
轻量级:Watcher 不会阻塞正常读写操作,适合用于配置变更、服务上下线通知等场景。
10.6 Nginx
10.6.1 Nginx使用
Nginx是什么?
Nginx是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。它由俄罗斯程序员 Igor Sysoev 开发,最早发布于 2004 年。Nginx 以其高并发、低内存消耗、高稳定性和模块化设计而闻名,被广泛应用于 Web 服务、负载均衡、反向代理、静态资源服务等场景。
【必问】Nginx能干什么?
-
作为web服务器:解析http协议
-
反向代理服务器
-
邮件服务器:解析邮件相关的协议,pop3/smtp/imap
【必问】Nginx的优势?
-
更快:高峰期(数以万计的并发时)nginx可以比其它web服务器更快的响应请求
-
高扩展:低耦合设计的模块组成,丰富的第三方模块支持
-
高可靠:每个worker进程相对独立,出错之后可以快速开启新的worker
-
低内存消耗:一般情况下,10000个非活跃的HTTP Keep-Alive连接在nginx中仅消耗2.5M内存
-
高并发:单机支持10万以上的并发连接
-
热部署(重新部署无需关机):master和worker的分离设计,可实现7x24小时不间断服务的前提下升级nginx可执行文件
Nginx的events配置项,有什么内容?
-
use epoll:多路IO转接模型使用epoll -
worker_connections 1024:每个工作的进程的最大连接数
Nginx的server配置项,有什么内容?
-
listen 80:web服务器监听的端口,http协议的默认端口 -
server_namelocalhost:对应一个域名, 客户端通过该域名访问服务器 -
charset utf8:字符串编码 -
location /XX {……}:处理客户端的请求
Nginx的location配置项,如何配置静态网页?
-
location /upload/:请求中的URL的中间段
-
root html:在服务器的哪个文件夹找请求的网页呢?
-
index login.html:当请求是一个目录时,nginx需要找一默认显示的网页
URL vs URI有什么区别?
-
URI 的作用是:标识资源(可以用来找到它,也可以只是命名它)。
-
URL 的作用是:标识资源 + 告诉你怎么访问它(定位资源)。基本格式:
协议://IP地址/路径和文件名。 -
所有 URL 都是 URI,但并非所有 URI 都是 URL。URL 是 URI 的一种,专门用来定位资源;而 URI 是一个更通用的概念,用于标识资源(可能可以访问,也可能只是一个名字)。
DNS是什么?DNS如何查询?
域名系统是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。
-
第一步:检查本地的 DNS 缓存
-
第二步:向 配置的 DNS 服务器 发起查询
-
递归查询(Recursive Query):你的电脑 → 向 本地 DNS 服务器(递归 DNS,如 8.8.8.8) 发出查询请求:
www.example.com的 IP 是什么?。本地 DNS 服务器 如果没有该记录,就会代表你一层层向根 DNS、顶级域 DNS、权威 DNS 查询,直到拿到最终的 IP 地址,然后把结果返回给你。 -
迭代查询(Iterative Query):这是 DNS 服务器之间的查询方式,不是由本地 DNS 直接返回最终答案,而是返回下一个应该去问谁,客户端或 DNS 服务器需要自己去继续询问。
-
【必问】正向代理 vs 反向代理有什么区别?
正向代理是位于客户端与互联网之间的代理服务器,代表客户端去访问外部资源(如网站、API 等)。客户端明确知道代理的存在,并主动将请求发送给代理,由代理去访问目标服务器。
简而言之:我想要访问XX,代理服务器把我的请求转到了XX。它服务的对象是客户端。
反向代理是位于客户端与后端服务器之间的代理,代表服务器接收客户端的请求,并将其转发到内部的一个或多个后端服务,最后将响应返回给客户端。客户端以为自己访问的是反向代理,而不知道后端真实服务器的存在。
简而言之:我想访问XX,反向代理服务器不让你访问,只能让你访问反向代理服务器(只有它的IP是公开的),由它决定你访问哪台背后的服务器。它服务的对象是其他服务器(其他服务器的数量一般>1)。
Nginx的location配置项,如何配置反向代理?
-
location /:请求中的URL的中间段 -
proxy_passhttp://XX:反向代理服务器转发指令,向XX转发
此外在server的外面还需要一个配置项:
-
upstream XX{server 192.168.247.135:80;}:XX背后的IP+端口。
Nginx的location配置项,如何配置负载均衡?
-
upstream XX{server 192.168.247.135:80;server 192.168.247.147:80;}:XX背后的多个IP+端口,访问的概率是均等的。
Nginx的负载均衡如何加权?
增加weight字段:
server 192.168.247.135:80 weight=2; server 192.168.247.147:80 weight=1;
10.6.2 CGI
静态请求 vs 动态请求有什么区别?
静态请求:客户端访问服务器的静态网页,不涉及任何数据的处理。
动态请求:客户端会将数据提交给服务器。
CGI是什么?
通用网关接口(Common Gateway Interface/CGI)描述了客户端和服务器程序之间传输数据的一种标准,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI独立于任何语言的,CGI程序可以用任何脚本语言或者是完全独立编程语言实现,只要这个语言可以在这个系统上运行。
CGI的通用执行流程是什么?
-
用户通过浏览器访问服务器,发送了一个请求
-
服务器接收数据,对接收的数据进行解析
-
nginx对于一些登录数据不知道如何处理,nginx将数据发送给了cgi程序:服务器端会创建一个cgi进程
-
CGI进程执行
-
服务器将cgi处理结果发送给客户端
弊端:在服务器端CGI进程会被频繁的创建销毁,服务器开销大,效率低
fastCGI是什么?
快速通用网关接口(Fast Common Gateway Interface / FastCGI)是通用网关接口(CGI)的改进,描述了客户端和服务器程序之间传输数据的一种标准。FastCGI致力于减少Web服务器与 CGI 程式 之间互动的开销,从而使服务器可以同时处理更多的Web请求 。与为每个请求创建一个新的进程不同,FastCGI使用持续的进程来处理一连串的请求。这些进程由FastCGI进程管理器管理,而不是web服务器。
fastCGI vs CGI有什么区别?
CGI 就是所谓的短生存期应用程序,FastCGI 就是所谓的长生存期应用程序。FastCGI像是一个常驻(long-live)型的 CGI,它可以一直执行着,不会每次都要花费时间去fork一次。
fastCGI的通用执行流程是什么?
-
用户通过浏览器访问服务器,发送了一个请求
-
服务器接收数据,对接收的数据进行解析
-
Nginx对于一些登录数据不知道如何处理,nginx将数据发送给了fastCGI程序:fastCGI不是由web服务器直接启动!而是通过一个fastCGI进程管理器启动
-
fastCGI进程执行
-
服务器有请求 -> 处理:将处理结果发送给服务器(本地套接字、网络通信)
-
没有请求 -> 阻塞
-
-
服务器将cgi处理结果发送给客户端
spawn-fcgi是什么?
它是fastCGI的进程管理器。
用spawn-fgci启动cgi程序。
spawn-fcgi使用pre-fork模型,功能主要是打开监听端口,绑定地址,然后fork-and-exec创建我们编写的fastcgi应用程序进程,退出完成工作。fastcgi应用程序初始化,然后进入死循环侦听socket的连接请求。
Nginx+fastCGI如何组合使用?流程如何?Nginx如何配置?
-
客户端访问,发送请求
-
nginx web服务器,无法处理用户提交的数据
-
spawn-fcgi - 通信过程中的服务器角色
-
被动接收数据
-
在spawn-fcgi启动的时候给其绑定IP和端口
-
-
fastCGI程序
-
程序猿写的 -> login.c -> 可执行程序( login )
-
使用 spawn-fcgi 进程管理器启动 login 程序,得到一个进程
-
-
location /login:请求中的URL的中间段 -
fastcgi_pass 地址信息(IP或域名):端口:需要和spawn-fcgi启动时设置的地址信息+端口一致 -
include fastcgi.conf:这个文件中定义了一些http通信的时候用到环境变量,nginx赋值的
fastCGI如何获得请求头?如何获得请求体?如何发回响应?
-
请求头:读环境变量,以键值对形式存入
fastcgi.conf环境变量 -
请求体:读标准输入,Nginx会发到一个fd的读缓冲区。spawn-fcgi会使标准输入dup2重定向到这个fd,所以在代码里读标准输入即可。
-
发响应:写标准输出,把处理后的数据发回Nginx时,写到标准输出,spawn-fcgi会把标准输出重定向到那个fd的写缓冲区,再发给Nginx。
10.6.3 Nginx源码
Nginx的线程池的结构是什么?
Nginx 默认模型:异步非阻塞 + 事件驱动
-
一个 Master 进程:负责管理配置、启动/停止 Worker 进程,不处理请求。
-
多个 Worker 进程(通常与 CPU 核心数一致):每个 Worker 是一个独立的进程,单线程运行,通过 事件驱动模型(Event Loop) 处理大量并发连接。
-
Worker 进程基于 epoll(Linux)、kqueue(BSD/macOS)、select 等多路复用机制,监听大量 socket 的 I/O 事件(如连接建立、数据可读、可写等)。
-
当某个 socket 有数据可读/可写时,事件循环回调相应的处理函数,非阻塞地处理请求。
-
这种模型使得 单个 Worker 进程可以轻松处理数万甚至数十万的并发连接,而无需创建大量线程。
-
Nginx 的线程池并不是一个通用的、任务类型随意的线程池(不像 C++ 的 std::thread + 任务队列那样灵活),而是一个针对特定阻塞任务(主要是磁盘 I/O)的优化实现,其结构主要包括以下几个部分:
整体结构:
-
由若干个工作线程(Worker Threads)组成
-
每个线程池有一个任务队列(Task Queue)
-
有一个管理者机制,负责将任务派发到线程,并管理线程的生命周期
当某个 Worker 进程遇到一个阻塞型任务时,它不会直接调用阻塞接口,而是:将这个任务封装后,放入线程池的任务队列中,然后立即返回继续处理其他请求。
工作线程运行在一个独立于主事件循环的线程上下文中,它们不断地从任务队列中取出任务并执行,执行完毕后,可能会通过某种方式通知主线程(如回调、事件触发等),但这取决于具体实现。
Nginx哪里用到了内存池?该内存池的作用?
Nginx 在其核心架构中广泛使用了自定义的 内存池(Memory Pool)机制,主要用于管理请求生命周期内的内存分配,比如处理 HTTP 请求、连接、响应等场景。Nginx 内存池的作用是提升内存分配效率、减少碎片、加速分配/释放、并统一管理内存的生命周期,特别适合高并发场景。
HTTP 请求处理(核心使用场景):
-
每当有一个新的 HTTP 请求到来时,Nginx 会为该请求创建一个内存池(request pool)
-
该请求在处理过程中所需的各种临时内存对象(如请求头、响应体、URL 解析、Header、Buffer 等)都从这个内存池中分配
-
当请求处理完成(或连接关闭)时,整个内存池一次性销毁,池中所有内存统一释放
Nginx handler是什么?工作流程是什么?
在 Nginx 中,Handler(处理器) 是 Nginx 模块化架构中的一个关键概念,它指的是 负责处理请求并生成响应的模块。更具体地说,Handler 是 Nginx 模块中实现特定业务逻辑、直接对客户端请求进行处理并返回内容的部分。
你可以将 Handler 理解为:“对请求进行实际处理的模块或函数”,它决定了如何处理一个特定的请求,并生成最终的 HTTP 响应(比如返回静态文件内容、动态内容、或者自定义的响应等)。
-
Nginx 接收客户端请求
-
Nginx 主进程监听端口,接收到用户请求后,通过 Master-Worker 模型 将请求交给某个 Worker 进程处理。
-
-
请求的 Location 匹配
-
Nginx 根据配置文件中的
server和location块,决定如何处理该请求。
-
-
选择 Handler 模块
-
根据配置,Nginx 会确定由哪个 Handler 模块来处理当前请求。
-
一个请求通常只由一个 Handler 模块处理(特别设计的模块可能例外,但一般不常见)。
-
-
Handler 处理请求
-
被选中的 Handler 模块开始工作,它可能会:
-
读取文件并返回(如静态文件模块)
-
执行代码生成动态内容(如 FastCGI、PHP 模块,或自定义模块)
-
返回固定的响应内容(如自定义的 "Hello, World!" 响应)
-
-
Handler 模块生成响应内容(可能包括状态码、响应头、响应体)。
-
-
交给 Filter 模块处理(可选)
-
如果有配置 Filter 模块(比如 gzip、headers_more、代理添加头等),Handler 生成的响应会进一步经过这些 Filter 模块处理和修饰。
-
-
返回响应给客户端
-
最终,经过 Handler 和 Filter 处理后的响应内容,会被发送回客户端。
-
Nginx handler中的ngx_module_t是什么?
ngx_module_t 是 Nginx 源码中定义的一个结构体类型,位于 src/core/ngx_module.h 文件中。它用来描述一个 Nginx 模块的基本信息、功能接口、生命周期回调函数等。
每个 Nginx 模块(无论是内置的核心模块,还是 HTTP 模块,或者是你自定义的模块),最终都要定义一个类型为 ngx_module_t 的变量,这个变量就是该模块的“身份证”,Nginx 启动时会根据这些信息加载并管理模块。
Nginx handler中的ngx_http_module_t是什么?
ngx_http_module_t 是专门为 HTTP 模块 定义的上下文结构体,用于定义该模块如何解析自己的配置项,以及如何介入到 Nginx 的 HTTP 请求处理流程中(比如注册 Handler)。
允许模块在 Nginx 解析 http{}、server{}、location{} 配置块时,进行自定义配置的处理,并可以注册自己的 Handler(处理器)到 Nginx 的请求处理阶段。
ngx_command_t vs nginx.conf有什么关系?
ngx_command_t 是 Nginx 模块用来告诉 Nginx:“我支持哪些配置指令,以及这些指令应该如何被解析和处理”的结构体数组。而 nginx.conf 是用户编写的配置文件,里面包含了实际使用的配置指令。
两者的关系是:
ngx_command_t 定义了模块能识别哪些配置指令以及如何处理它们,而 nginx.conf 是用户具体使用这些指令的地方。Nginx 在启动时,会解析 nginx.conf,并根据模块注册的 ngx_command_t 数组,来正确地识别和处理这些指令。
假设你正在开发一个 Nginx 自定义模块,希望用户可以在 location 块中写一个叫 hello_world 的配置项。
-
定义
ngx_command_t:在你的模块代码中,定义一个ngx_command_t数组,告诉 Nginx 你支持hello_world指令 -
实现 set 函数(处理指令逻辑):
static char* my_hello_world_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {} -
将命令数组挂载到模块中:在模块的
ngx_module_t结构中,将这个commands数组关联起来
Nginx upstream是什么?
Nginx 的 upstream 是一个模块(或称为功能模块集合),它允许 Nginx 将客户端的请求转发(代理)到一组后端服务器(称为“上游服务器”),并支持多种调度算法来实现负载均衡。
-
用户发起请求 到 Nginx(比如访问
http://example.com/api)。 -
Nginx 根据
server块中的location规则,发现要使用proxy_passhttp://backend_servers;。 -
Nginx 根据
upstream backend_servers { ... }中定义的服务器列表和负载均衡策略,选择一个后端服务器。 -
Nginx 作为反向代理,将用户请求转发(代理)到选中的后端服务器。
-
后端服务器处理请求,并将响应返回给 Nginx。
-
Nginx 再将后端响应返回给客户端,客户端只知道是 Nginx 响应的,不知道后面还有多个服务。
Nginx filter是什么?
Nginx 的 Filter(过滤器模块)是用来对 HTTP 响应内容进行二次加工的模块。它不直接处理用户请求,而是在请求已经被某个 Handler 模块处理并生成响应之后,对响应的“头”和“体”进行修改、优化或增强。
1433

被折叠的 条评论
为什么被折叠?



