10(下) 消息队列/Docker/Zookeeper/Nginx-面试题

10.3 消息队列

10.3.1 消息队列设计

什么是消息队列?

消息队列(Message Queue) 是一种在分布式系统或应用程序之间进行异步通信的机制。它基于“生产者-消费者”模型,主要作用是在不同的服务或组件之间传递消息(通常是数据或指令),以实现解耦、异步处理和流量削峰等功能。

简单来说:

  • 生产者(Producer):负责创建并发送消息到消息队列。

  • 消息队列(Message Queue):是一个中间缓冲区,用于暂存消息,确保消息不会丢失,并按一定顺序被处理。

  • 消费者(Consumer):从消息队列中获取消息并进行处理。

消息队列充当了生产者和消费者之间的中介,两者不需要直接通信或相互知晓对方的存在,从而实现系统解耦。

消息队列有哪些应用场景?

  1. 异步处理(Async Processing)

    1. 场景:用户注册后需要发送邮件、短信通知,但不想让用户等待这些操作完成。

    2. 解决方案:将发送邮件/短信的任务放入消息队列,由后台消费者异步处理。

  2. 应用解耦

    1. 场景:订单系统和库存系统是两个独立服务,订单创建后需要扣减库存。

    2. 解决方案:订单服务将“扣减库存”消息发送到消息队列,库存服务自行消费,彼此不直接调用。

  3. 流量削峰 / 请求缓冲

    1. 场景:电商秒杀活动中,大量用户同时下单,系统可能瞬间扛不住。

    2. 解决方案:将用户请求放入消息队列中排队处理,后端按处理能力逐步消费,避免系统崩溃。

  4. 日志收集与处理

    1. 场景:分布式系统中多个服务产生大量日志,需要统一收集和分析。

    2. 解决方案:各服务将日志消息发送到消息队列(如Kafka),再由日志处理服务统一消费。

  5. 任务调度与分发

    1. 场景:有多个 worker 节点处理任务,需要公平高效地分配任务。

    2. 解决方案:使用消息队列作为任务队列,worker 从队列中获取任务执行。

  6. 微服务通信

    1. 场景:微服务架构中,服务之间需要相互通信,但不希望强依赖。

    2. 解决方案:通过消息队列进行事件驱动的通信,提升系统的弹性和可维护性。

【必问】消息队列的数据结构是什么?topic、broker是什么?

  1. 队列(Queue)

    1. 最基础的数据结构,遵循 FIFO(先进先出) 原则。

    2. 生产者(Producer) 往队列里 发送消息(enqueue)。

    3. 消费者(Consumer) 从队列里 接收消息(dequeue)。

  2. 主题(Topic)

    1. 消息按主题分类,生产者发送消息到某个 Topic,消费者 订阅(Subscribe)感兴趣的 Topic。

  3. 分区(Partition)

    1. Topic 可以分成多个 Partition(分区),提高 并行处理能力。

    2. 每个 Partition 是一个独立的队列,消息按顺序存储。

    3. 生产者发送消息时可以指定 Partition,或者由 MQ 自动分配。

    4. 消费者可以按 Partition 并行消费。

  4. Broker 的定义

    1. Broker 是消息队列的服务器(Server),负责:

      • 接收生产者发送的消息。

      • 存储消息(持久化或内存)。

      • 把消息分发给订阅的消费者。

    2. 多个 Broker 可以组成集群,提高 可用性(高可用)和吞吐量(高并发)。

消息队列如何保证消息不丢失?

生产者发送阶段 —— 保证消息成功到达消息队列

  1. 消息确认机制(ACK / Confirm机制)

  2. 失败重试机制

  3. 本地消息表(最终一致性方案,适用于业务层)

    1. 在发送消息前,先将消息保存到本地数据库(状态为待发送),然后异步发送到消息队列。

    2. 若发送成功,则更新本地状态;若失败则定时任务重试,确保最终消息进入队列。

消息队列存储阶段 —— 保证消息在 Broker 中不丢失

  1. 消息持久化(Persistence)

  2. 高可用集群 / 副本机制

消费者处理阶段 —— 保证消息被成功消费

  1. 消费者确认机制(ACK)

  2. 幂等性设计:例如通过业务唯一ID、去重表等方式避免重复操作。

  3. 死信队列(DLQ):对于多次重试仍失败的消息,可以转入死信队列进行人工干预或后续处理,避免消息被无限重试而丢失。

消息队列如何保证消息幂等性,即不被重复消费?

  1. 唯一 ID + 去重表(最常用)

核心思想:每条消息带一个 唯一 ID(如 UUID、业务订单 ID),消费者在处理前先检查该 ID 是否已经处理过。

  1. 业务状态机(适用于有明确状态的业务)

核心思想:业务数据本身有 状态流转,只有当前状态允许时才处理消息,否则忽略。

  1. 消息唯一键 + 本地缓存(适用于单机/小规模)

核心思想:消费者在 内存(如 std::unordered_set) 或 本地文件 记录已处理的消息 ID,避免重复处理。

  1. 消息队列本身的幂等机制(如 Kafka、RocketMQ)

某些 MQ 原生支持幂等性,例如:

  • Kafka:可以通过 enable.idempotence=true 开启 生产者幂等(避免重复发送)。

  • RocketMQ:支持 消息去重(Message Deduplication),消费者可以配置 MessageListenerOrderly 保证顺序消费 + 去重。

  • RabbitMQ:通常需要 业务层自己处理幂等性(如上述方法)。

消息队列如何处理/防止消息堆积?

消息队列中的消息堆积(Message Backlog / Queue Build-up)是指:生产者持续向队列发送消息,但消费者处理速度跟不上,导致消息在队列中不断积压,未被及时消费。

消息堆积如果不及时处理,可能会带来严重问题,比如:

  • 消息延迟变高,影响业务实时性

  • 消息队列存储空间被占满,导致新消息被拒绝或服务崩溃

  • 系统负载升高,影响整体稳定性与性能

防止

  1. 保证消费者处理能力 >= 生产者发送能力

  2. 水平扩展消费者(增加消费者实例)

  3. 优化消费者处理逻辑

  4. 使用批量消费

  5. 流量控制 / 生产限流

处理

  1. 紧急扩容消费者

  2. 跳过非关键逻辑 / 降级处理

  3. 使用多线程 / 并发消费

  4. 临时增加分区

  5. 死信队列 + 人工干预

消息队列如何保证消息的有序性?

Kafka 的有序性机制:

  • Kafka 的消息是按照 Topic 下的 Partition(分区) 进行存储和消费的。

  • 在同一个 Partition 内,消息是严格有序的(FIFO),并且消费者也是按顺序拉取的。

  • 不同 Partition 之间的消息顺序是无法保证的!

RabbitMQ 的有序性机制:

  • RabbitMQ 本身不保证消息的顺序性,尤其是在使用多个消费者、多个线程、Prefetch、重试等机制时。

  • 如果你将消息发送到同一个队列,并且只使用一个消费者(单线程)去消费,那么消息基本上是按 FIFO 顺序处理的。

消息队列设计成推消息还是拉消息?推拉模式的各自优劣?

推模式(Push Model):

  • 消息到达 Broker 后,Broker 主动将消息推送给已订阅的消费者。

  • 消费者无需主动请求,而是被动接收 Broker 推送过来的消息。

推模式优点:

  1. 实时性高

  2. 消费者无需轮询

  3. 简化消费者逻辑

推模式缺点:

  1. 难以控制消费速率(消费者压力大)

  2. 难以适应不同消费者的处理能力

  3. 消息确认(ACK)机制复杂

  4. 不适合高吞吐批量消费场景

拉模式(Pull Model):

  • 消费者主动向 Broker 发起请求,拉取最新的消息。

  • 消费者可以控制拉取的时机、频率和数量。

拉模式优点:

  1. 消费者可控性强

  2. 批量拉取,提高吞吐

  3. 消息处理更灵活可靠

  4. 适应性强

拉模式缺点:

  1. 实时性相对较低

  2. 消费者负担增加

  3. 空轮询问题

消息队列的模型有哪些?

  1. 点对点模型(Point-to-Point,简称 P2P 模型 / 队列模型)

    1. 消息只有一个消费者能收到(即一条消息只会被一个消费者处理)。

    2. 消息通过队列(Queue)进行传递。

    3. 消息被消费后通常会从队列中移除(除非配置了特殊机制,如死信队列)。

  2. 发布/订阅模型(Publish/Subscribe,简称 Pub/Sub 模型)

    1. 生产者发布消息到一个主题(Topic),多个订阅了该主题的消费者都会收到该消息的副本。

    2. 消息会被广播给所有订阅者,每个订阅者都能独立消费该消息。

    3. 消息的生命周期通常不由单个消费者控制。

  3. 消息队列模型的组合与扩展

    1. RabbitMQ 的模型(灵活路由模型)

      • RabbitMQ 不仅支持经典的 P2P(Queue) 和 Pub/Sub(Exchange + Fanout),还支持更复杂的路由模型,通过 Exchange(交换机) 实现

    2. 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)中的物理位置,从而提高消息的读写效率。

  1. 偏移量索引(Offset Index)

  • 作用:记录消息的 逻辑偏移量(Offset) 与 物理文件中的相对位置(Position) 的映射关系。

  • 存储内容:每条索引项通常包含一个偏移量(offset)和对应的日志文件中的物理位置(position,即文件中的字节偏移)。

  • 索引特点:

    • 是 稀疏索引(Sparse Index),即不是每条消息都建立索引,而是每隔一定数量的消息建立一条索引项,以节省空间并保持高效。

    • 通过二分查找快速定位到目标偏移量所在的日志段(Log Segment),再进一步找到具体的消息。

  1. 时间戳索引(Timestamp Index,可选,Kafka 0.10.0+ 引入)

  • 作用:用于根据 消息的时间戳 快速定位到对应的日志位置。

  • 存储内容:记录时间戳和对应的偏移量。可以用来实现基于时间的日志保留策略或者按时间范围查找消息。

  • 主要用于:

    • 按时间范围查询消息(如用于日志检索、审计等场景)。

    • 实现基于时间的日志清理策略(如保留最近 N 天的消息)。

亮点:

  • 稀疏索引:减少索引大小,提升效率,配合二分查找快速定位

  • Log Segment 机制:每个日志段独立管理,索引与日志文件一一对应,便于维护和清理

  • 索引常驻内存:加快查找速度,减少磁盘 IO,提升吞吐量

  • 高效查找流程:先通过索引定位大致位置,再顺序扫描找到确切消息,平衡了速度与资源消耗

Kafka的事务机制是什么?如何实现?

Kafka 的 事务机制(Transaction Mechanism) 是为了支持 跨分区、跨会话或跨生产者的原子性写入 而引入的一项重要功能,它使得 Kafka 不仅可以作为一个高吞吐的消息队列,还能支持类似数据库的事务语义,特别是在需要 “精确一次”(Exactly Once Semantics, EOS) 消息处理的场景中至关重要。

  • 调用 commitTransaction() 提交事务,Kafka 会通过 两阶段提交协议 确保所有消息 原子性地对消费者可见。

    • 准备阶段:事务协调器确保所有相关分区可以接受该事务的消息。

    • 提交/中止阶段:根据生产者的指令提交或回滚事务。

  • 事务协调器将状态写入 __transaction_state Topic,用于故障恢复,确保即使生产者崩溃也能正确恢复或中止事务。

Kafka为什么性能高?

  1. 顺序 I/O(Sequential Disk Access):Kafka 将消息 持久化到磁盘上,并且采用顺序写入(append-only)的方式,而不是随机写入。

  2. 零拷贝(Zero-Copy)技术:传统消息传递需要多次内核态与用户态拷贝:而 Kafka 使用 FileChannel.transferTo() 实现 零拷贝,直接将磁盘文件数据通过内核态发送到网卡,无需经过用户态。

  3. 批处理(Batching):Kafka 的 生产者和消费者都支持消息的批量发送与拉取,将多条消息打包成一个 Batch 一起处理。

  4. 页缓存(Page Cache)优化:Kafka 直接利用操作系统的页缓存(Page Cache),而不是自己维护一套内存缓存。

  5. 索引机制(稀疏索引 + 顺序查找):Kafka 为每个日志段维护了 稀疏的偏移量索引和时间戳索引,但 不是为每条消息都建索引,而是定期记录,占用空间小,且能快速定位到消息所在的大致位置。

  6. 异步非阻塞设计 & 高并发模型

Kafka中Zookeeper的作用?(已过时)

  1. 存储和管理 Kafka 的元数据(Metadata)

  2. Kafka Controller 的选举

  3. Broker 的注册与状态监控

  4. 分布式协调与 Watcher 机制

10.3.3 RabbitMQ

RabbitMQ的基本架构是什么?包括哪些核心组件?

  1. 生产者(Producer)

  2. 消费者(Consumer)

  3. 队列(Queue)

  4. 交换机(Exchange)

    1. 定义:接收生产者发送的消息,并根据一定的规则(Binding)将消息路由到一个或多个队列。

    2. 作用:是消息的“分拣中心”,不存储消息,只负责将消息路由到正确的队列。

    3. 类型(重要!)RabbitMQ 支持多种 Exchange 类型,常用的有:

      • Direct:根据 routing key 精确匹配绑定键,将消息路由到对应的队列。

      • Fanout:广播模式,将消息发送到所有绑定到该交换机的队列,忽略 routing key。

      • Topic:基于通配符匹配 routing key,如 logs.*logs.error,灵活路由。

      • Headers:基于消息头(headers)中的键值对进行匹配,较复杂,使用较少。

  5. 绑定(Binding)

    1. 定义:连接 交换机(Exchange) 和 队列(Queue) 的规则。

    2. 作用:定义了消息如何从交换机路由到队列,通常会指定一个 routing key(路由键)。

    3. 特点:通过绑定,RabbitMQ 知道某个交换机收到消息后应该发给哪些队列。

  6. 路由键(Routing Key)

    1. 定义:一个字符串,用于在某些 Exchange 类型(如 Direct 和 Topic)中决定消息应该被路由到哪个队列。

    2. 作用:作为消息发送时的一个标识,与 Binding 中的规则相匹配,决定消息去向。

  7. Broker(代理服务器)

RabbitMQ如何保证消息的可靠性?RabbitMQ能做消息持久化吗?

  1. 消息生产者发送阶段:确保消息成功到达 Broker(RabbitMQ 服务端)

    1. 生产者发送消息后,RabbitMQ 处理成功后会异步发送一个 Basic.Ack 给生产者;如果失败(比如队列不存在、磁盘满等),则返回 Basic.Nack。

  2. 消息 Broker 存储阶段:确保消息成功存储(不因宕机等原因丢失)

    1. 队列持久化(Durable Queue):声明队列时设置 durable=True,表示即使 RabbitMQ 重启,队列也依然存在。

    2. 消息持久化(Persistent Message):发送消息时,设置消息的 delivery_mode=2(2 表示持久化消息)。表示该消息会被写入磁盘,RabbitMQ 重启后仍然存在。

  3. 消息路由阶段:确保消息正确路由到目标队列

    1. 确保 Exchange 和 Queue 之间存在正确的 Binding,并且使用合适的 Routing Key。

    2. 对于无法路由的消息(比如 Fanout 以外的 Exchange 找不到匹配队列),可以通过设置 Alternate Exchange(备用交换机) 来接收这些“无路可去”的消息,避免丢失。

  4. 消息消费者处理阶段:确保消息被成功消费(并得到确认)

    1. 为保证可靠性,应设置 auto_ack=False,并在业务逻辑 处理成功后手动发送 Ack,处理失败可发送 Nack 或 Reject。

RabbitMQ中无法路由的消息会去到哪里?

对于无法路由的消息(比如 Fanout 以外的 Exchange 找不到匹配队列),可以通过设置 Alternate Exchange(备用交换机) 来接收这些“无路可去”的消息,避免丢失。

AMQP协议是什么?

AMQP(Advanced Message Queuing Protocol,即 高级消息队列协议)是一个应用层的网络协议,用于在分布式系统中实现消息的可靠、异步通信。它为应用程序之间传递消息提供了一种标准化、跨平台、跨语言的通信方式,是现代消息中间件(如 RabbitMQ、Apache Qpid 等)的核心协议之一。

AMQP 的工作流程 = RabbitMQ的工作流程

  1. 生产者 将消息发送到 Exchange,并指定一个 Routing Key。

  2. Exchange 根据其类型和 Binding 规则,将消息路由到一个或多个 Queue。

  3. Queue 存储消息,等待 消费者 来获取并处理。

  4. 消费者 从 Queue 中接收消息,并可向 Broker 发送确认(ACK)表示已成功处理。

RabbitMQ怎么实现延迟队列?

RabbitMQ 本身并不直接提供“延迟队列”(Delayed Queue)的功能,也就是说,它没有像某些消息队列(如 RocketMQ、RabbitMQ 插件、AWS SQS Delay Queue 等)那样内置一个可以直接设置消息延迟投递的队列类型。

但是,RabbitMQ 可以通过一些巧妙的机制或插件实现延迟队列的功能,最常见的有以下 两种主流方案:

方案一:TTL + 死信队列(DLX)

  1. 生产者 发送消息到一个 中间队列(Queue A),并设置该消息的 TTL(Time To Live,存活时间);

  2. 当消息在 Queue A 中过期后,如果没有被消费,就会变成 “死信”(Dead Letter);

  3. 通过配置,让这个 死信自动路由到另一个队列(Queue B,即实际的延迟目标队列);

  4. 消费者 监听 Queue B,即可获取到“延迟到期”的消息,进行消费。

方案二:使用 RabbitMQ 官方延迟消息插件

  1. 安装这个插件

  2. 使用一个特殊的 Exchange 类型:x-delayed-message

  3. 发送消息时指定 x-delay 参数(单位:毫秒)

RabbitMQ的事务机制是什么?如何实现?

RabbitMQ 的 事务机制(Transaction) 是一种用于保证消息 可靠性投递 的机制,主要用来确保 生产者发送的消息能够成功到达 RabbitMQ 服务端,否则可以进行回滚,避免消息丢失或误认为发送成功。

不过需要注意的是,RabbitMQ 的事务机制 性能较低,通常 不推荐在高并发生产环境中使用,而是推荐使用更高效的 Publisher Confirm(消息确认)机制。

RabbitMQ 的事务机制基于 AMQP 协议的事务模型,核心命令包括三个:

  1. txSelect:开启事务模式。

  2. txCommit:提交事务,确认之前发送的消息正式发送到 RabbitMQ。

  3. 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是做什么的?

  1. 自动化部署与回滚

  2. 服务发现与负载均衡

  3. 自动扩缩容 (Auto Scaling)

  4. 自我修复能力

  5. 配置管理与密钥管理

K8s和Docker的联系?

Docker 是容器运行时(Runtime),K8s 是容器编排器(Orchestrator)

  • Docker 负责 创建和运行单个容器(比如你在本地用 docker run 启动一个 Nginx)。

  • K8s 负责 管理成百上千个容器(Pod),比如:

  • K8s 不直接管理 Docker,而是管理容器,而 Docker 是最常见的容器运行时之一。

Istio是做什么的?

在 Kubernetes (K8s) 中,Istio 是一个开源的 服务网格(Service Mesh) 解决方案,主要用于 管理、观察和控制微服务之间的网络通信,提供 流量管理、安全策略、可观测性 等核心功能,而无需修改应用程序代码。

Istio 主要提供以下三大核心能力:

  1. 流量管理(Traffic Management)

  2. 安全(Security)

  3. 可观测性(Observability)

微服务是什么?

微服务(Microservices) 是一种软件架构风格,它将一个大型、复杂的应用程序拆分成一组小而专一、松耦合、独立部署的服务,每个服务专注于完成一个特定的业务功能,并且可以独立开发、测试、部署和扩展。

每个服务运行在自己的进程中,通过轻量级通信机制(通常是 HTTP/REST 或 gRPC)相互协作,共同构成完整的应用。

微服务的特点是什么?

  • 单一职责:每个微服务只关注一个业务功能,比如“用户管理”、“订单处理”。

  • 独立部署:每个服务可以单独开发、测试、打包和部署,无需影响其他服务。

  • 松耦合:服务之间通过 API(如 REST/gRPC)通信,内部实现互不依赖。

  • 小而专注:每个服务代码量小,功能明确,便于理解和维护。

微服务的优势是什么?

  1. 弹性扩展:按需扩展特定服务,资源利用更高效。

  2. 独立部署:每个服务可单独开发、测试、部署,提高迭代效率。

  3. 松耦合、小而专注:单个服务故障不会导致整个系统崩溃。

  4. 易于维护:代码和服务职责单一,更易理解与维护。

一个典型的微服务系统需要哪些配套组件?

  1. 网关:Nginx

  2. 注册中心、配置中心:nacos

  3. 通信:gRPC、Kafka

  4. 日志:ELK

  5. 监控: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) 是一个集中管理分布式系统中配置信息的服务,主要作用包括:

  • 统一管理配置(如数据库连接、服务地址、功能开关等)。

  • 动态更新配置(无需重启服务,实时生效)。

  • 配置版本管理(支持回滚、灰度发布)。

  • 配置权限控制(不同环境、团队访问不同配置)。

  1. Zookeeper 的配置通常存储在 持久节点(Persistent ZNode) 中。

    1. /config 是根节点,存放所有配置。

    2. 每个配置项是一个 持久节点(Persistent),存储键值对(如 db_url = "mysql://...")。

    3. 可以按 应用/模块 划分子节点(如 /app1/timeout)。

  2. 配置读取流程(C++ 客户端)

    1. 服务启动时,从 Zookeeper 读取配置(如 /config/db_url)。

    2. 将配置加载到内存(如 std::map<std::string, std::string>)。

    3. 业务代码使用内存中的配置,避免频繁访问 Zookeeper。

  3. 配置动态更新(Watcher 机制)

    1. 客户端监听配置节点(如 /config/db_url)。

    2. 当配置被修改时,Zookeeper 会通知客户端(触发 Watcher 回调)。

    3. 客户端重新读取最新配置,并更新内存中的值。

如何用Zookeeper做一个注册中心?

注册中心(Service Registry) 是分布式系统中的服务发现组件,主要作用是:

  • 服务注册(Service Registration):服务启动时,将自己的网络地址(IP:Port)、元数据(如服务名、版本) 注册到注册中心。

  • 服务发现(Service Discovery):其他服务或客户端可以查询可用的服务实例,并动态获取其地址。

  • 健康检查(Health Check):注册中心可以检测服务是否存活,自动剔除不可用的节点。

  • 负载均衡(Load Balancing):客户端可以从多个服务实例中选择一个进行调用。

  1. 服务注册的存储结构(ZNode 设计)

    1. /services 是根节点,存放所有服务。

    2. /service1/service2 是具体服务的节点。

    3. /node1/node2 是临时节点(Ephemeral),存储服务实例的 IP:Port,服务宕机时自动删除。

  2. 服务注册流程(C++ 服务端)

    1. 服务启动时,向 Zookeeper 注册自己(创建临时节点)。

    2. 服务运行期间,保持 Zookeeper 连接(否则临时节点会被删除)。

    3. 服务停止时,Zookeeper 自动删除临时节点(或手动注销)。

  3. 服务发现流程(C++ 客户端)

    1. 客户端查询可用服务(如 /services/service1)。

    2. 获取所有子节点(即所有服务实例的 IP:Port)。

    3. 监听服务节点变化(Watcher 机制,当服务上下线时收到通知)。

    4. 选择其中一个实例进行调用(如随机、轮询、负载均衡)。

【必问】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能干什么?

  1. 作为web服务器:解析http协议

  2. 反向代理服务器

  3. 邮件服务器:解析邮件相关的协议,pop3/smtp/imap

【必问】Nginx的优势?

  1. 更快:高峰期(数以万计的并发时)nginx可以比其它web服务器更快的响应请求

  2. 高扩展:低耦合设计的模块组成,丰富的第三方模块支持

  3. 高可靠:每个worker进程相对独立,出错之后可以快速开启新的worker

  4. 低内存消耗:一般情况下,10000个非活跃的HTTP Keep-Alive连接在nginx中仅消耗2.5M内存

  5. 高并发:单机支持10万以上的并发连接

  6. 热部署(重新部署无需关机):master和worker的分离设计,可实现7x24小时不间断服务的前提下升级nginx可执行文件

Nginx的events配置项,有什么内容?

  • use epoll:多路IO转接模型使用epoll

  • worker_connections 1024:每个工作的进程的最大连接数

Nginx的server配置项,有什么内容?

  • listen 80:web服务器监听的端口,http协议的默认端口

  • server_name localhost:对应一个域名, 客户端通过该域名访问服务器

  • 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_pass http://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的通用执行流程是什么?

  1. 用户通过浏览器访问服务器,发送了一个请求

  2. 服务器接收数据,对接收的数据进行解析

  3. nginx对于一些登录数据不知道如何处理,nginx将数据发送给了cgi程序:服务器端会创建一个cgi进程

  4. CGI进程执行

  5. 服务器将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的通用执行流程是什么?

  1. 用户通过浏览器访问服务器,发送了一个请求

  2. 服务器接收数据,对接收的数据进行解析

  3. Nginx对于一些登录数据不知道如何处理,nginx将数据发送给了fastCGI程序:fastCGI不是由web服务器直接启动!而是通过一个fastCGI进程管理器启动

  4. fastCGI进程执行

    1. 服务器有请求 -> 处理:将处理结果发送给服务器(本地套接字、网络通信)

    2. 没有请求 -> 阻塞

  5. 服务器将cgi处理结果发送给客户端

spawn-fcgi是什么?

它是fastCGI的进程管理器。

用spawn-fgci启动cgi程序。

spawn-fcgi使用pre-fork模型,功能主要是打开监听端口,绑定地址,然后fork-and-exec创建我们编写的fastcgi应用程序进程,退出完成工作。fastcgi应用程序初始化,然后进入死循环侦听socket的连接请求。

Nginx+fastCGI如何组合使用?流程如何?Nginx如何配置?

  1. 客户端访问,发送请求

  2. nginx web服务器,无法处理用户提交的数据

  3. spawn-fcgi - 通信过程中的服务器角色

    1. 被动接收数据

    2. 在spawn-fcgi启动的时候给其绑定IP和端口

  4. fastCGI程序

    1. 程序猿写的 -> login.c -> 可执行程序( login )

    2. 使用 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 响应(比如返回静态文件内容、动态内容、或者自定义的响应等)。

  1. Nginx 接收客户端请求

    1. Nginx 主进程监听端口,接收到用户请求后,通过 Master-Worker 模型 将请求交给某个 Worker 进程处理。

  2. 请求的 Location 匹配

    1. Nginx 根据配置文件中的 serverlocation 块,决定如何处理该请求。

  3. 选择 Handler 模块

    1. 根据配置,Nginx 会确定由哪个 Handler 模块来处理当前请求。

    2. 一个请求通常只由一个 Handler 模块处理(特别设计的模块可能例外,但一般不常见)。

  4. Handler 处理请求

    1. 被选中的 Handler 模块开始工作,它可能会:

      • 读取文件并返回(如静态文件模块)

      • 执行代码生成动态内容(如 FastCGI、PHP 模块,或自定义模块)

      • 返回固定的响应内容(如自定义的 "Hello, World!" 响应)

    2. Handler 模块生成响应内容(可能包括状态码、响应头、响应体)。

  5. 交给 Filter 模块处理(可选)

    1. 如果有配置 Filter 模块(比如 gzip、headers_more、代理添加头等),Handler 生成的响应会进一步经过这些 Filter 模块处理和修饰。

  6. 返回响应给客户端

    1. 最终,经过 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 的配置项。

  1. 定义 ngx_command_t:在你的模块代码中,定义一个 ngx_command_t 数组,告诉 Nginx 你支持 hello_world 指令

  2. 实现 set 函数(处理指令逻辑):static char* my_hello_world_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {}

  3. 将命令数组挂载到模块中:在模块的 ngx_module_t 结构中,将这个 commands 数组关联起来

Nginx upstream是什么?

Nginx 的 upstream 是一个模块(或称为功能模块集合),它允许 Nginx 将客户端的请求转发(代理)到一组后端服务器(称为“上游服务器”),并支持多种调度算法来实现负载均衡。

  1. 用户发起请求 到 Nginx(比如访问 http://example.com/api)。

  2. Nginx 根据 server 块中的 location 规则,发现要使用 proxy_pass http://backend_servers;

  3. Nginx 根据 upstream backend_servers { ... } 中定义的服务器列表和负载均衡策略,选择一个后端服务器。

  4. Nginx 作为反向代理,将用户请求转发(代理)到选中的后端服务器。

  5. 后端服务器处理请求,并将响应返回给 Nginx。

  6. Nginx 再将后端响应返回给客户端,客户端只知道是 Nginx 响应的,不知道后面还有多个服务。

Nginx filter是什么?

Nginx 的 Filter(过滤器模块)是用来对 HTTP 响应内容进行二次加工的模块。它不直接处理用户请求,而是在请求已经被某个 Handler 模块处理并生成响应之后,对响应的“头”和“体”进行修改、优化或增强。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值