IM

4,16514 分钟interview-questions

IM

简单介绍一下你的这个 DDD 六边形和微服务架构的 IM 项目

这个项目是一个基于 Go 和 DDD 领域驱动设计的分布式即时通讯系统,主要是面向在保证消息可靠性的前提下的高并发实时通信场景

架构方面我根据领域边界将系统拆分为了一个网关和六个微服务,分别是用户、会话、消息、投递、在线状态和文件,服务间通过 gRPC 同步通信,对外暴露 HTTP 接口,使用 Kafka 统一处理消息队列和吞吐管理,每个服务采用的是六边形架构,端口层定义入站出站接口,适配器层实现具体的技术细节,领域层划分核心业务规则,应用层编排具体的业务流程,这样就实现了业务逻辑和技术细节的解耦,便于测试、技术栈替换和不同团队协作

消息分发策略采用的主要是读写扩散的混合模型,在单聊和 500 人以下的小群主要使用写扩散,在写入时将消息分发到每个成员的 Inbox 收件箱,读取时就可以 O(1) 直接获取,大群则使用读扩散,只写一份 Timeline 时间线,读取时再根据游标计算未读消息和最新消息

可靠性方面主要是使用先落库后投递的策略,通过 Outbox 发件箱模式实现最终一致性,消费端通过指数退避、幂等重试和死信队列兜底,保证至少一次可达,客户端则通过 ACK 机制确认消息收到,未确认的消息会在 30 秒后重传

技术选型上由于 Kafka 的分区机制可以保证同一会话的消息有序,吞吐量也更高,就没有使用 RabbitMQ,而是通过 Conversation ID 作为分区键,将同一会话的消息路由到同一分区,这样可以天然保证顺序;另外在生成全局唯一递增 ID 时需要高并发,于是采用了 Lua 脚本在 Redis 单线程模型下天然保证了原子性递增,不需要分布式锁,获得更好的性能

通过这个项目我主要是深入理解了分布式系统的一致性、可用性和分区容错这三个核心问题之间的取舍,最后也是选择了最终一致性作为实现目标,在保证消息不丢失的前提下,再去追求更高的可用性和性能

什么是 DDD?为什么你的项目要用 DDD?

DDD 是 Domain Driven Design 的缩写,也就是领域驱动设计,它的核心思想是将业务逻辑与技术实现彻底解耦,它不像传统的 MVC 那样自顶向下的 Web 层 Controller —— 业务层 Service —— 数据访问层 DAO 或 Model —— 数据库的依赖方向,而是强调由外向内,最外层的适配器层分为入站和出站两类,入站是驱动应用的外部输入,出站则是被应用驱动的外部依赖,会涉及底层的技术细节,端口层则往往是边界,入站和出站端口分别定义了应用能做什么和应用需要什么外部能力,领域层就是最内层最纯粹的业务逻辑

在我的项目中除了适配器、端口和领域以外,还有一层应用层,负责编排业务逻辑,这样领域层就只需要划分实体、值对象和领域服务,我可以在还没有写好外部依赖的情况下就通过 mock interface 直接对核心业务逻辑进行单元测试,

可以讲一下你在这个项目里是怎么用到 DDD 架构的吗,以及微服务架构

我先把这个项目拆分成了七个微服务和一个网关,分别是 identity、conversation、message、delivery、presence、media-signal 和 file,然后每个微服务进行六边形分层,分别是领域层(domain)、应用层(application)、端口层(port)和适配器层(adapter)

其中端口层和适配器层分为入站和出站,对内就是 http、WebSocket 和 gRPC,对外就是各种中间件什么的(MySQL、Kafka、redis),领域层则主要分为实体、值对象和领域服务,适配器层主要实现中间件相关接口,应用层则是用例的编排以及调用领域服务

每个服务拥有自己的数据和模型:

  • identity:用户/联系人表,令牌签发与黑白名单

  • conversation:会话与成员表

  • message:消息表、inbox、outbox;对外暴露“写入/历史/已读”

  • delivery:不持久化消息本体,消费 Kafka,按在线/离线分别推送/更新进度

  • presence:用 Redis 维护 user_id -> node_id + last_seen 的在线路由

  • media-signal:呼叫/接受/拒绝/挂断,做 SDP/ICE 交换

  • file:预签名直传/回调,只存对象键与元数据

  • 对外只有 api-gateway:握手鉴权、心跳、限流、WS 收发与转发

服务间读小写少走 gRPC(网关→各服务,delivery→网关),Kafka 的 key 取 conversation_id,保证同会话同分区;clientMsgId + 唯一约束去重;投递按 user_id, seq 记已送进度,写 DB 成功即返回;投递失败由重试/死信兜底,恢复后补送

介绍一下你从 MVC 转型到 DDD 的构思

MVC 里一个接口经常串很多表和外部动作(写消息→推送→更新已读),业务规则散在 Controller/Service/DAO 各处,重复且难以复用;IM 强依赖会话内有序 + 幂等 + 最少一次,MVC 里常见“写库成功后再调用推送”,中间失败就乱序或丢投递,单体里写入与投递绑在一起,高峰时无法只扩“投递”,长连接治理也分散到各处。

Conversation(含 Participants):成员不能重复;拉人/踢人按角色校验;单聊通过业务键保证唯一会话

Message:会话内 seq 自增且唯一;写入必须幂等

Inbox:每用户每会话一条进度,delivered/read 只单调前进

Outbox:与消息同事务写入,作为对外投递的唯一事实来源

入站端口:gRPC/WS Handler

出站端口:仓储(MySQL/GORM)、队列发布(Kafka)、缓存(Redis)、对象存储

适配器可换,领域层不依赖具体技术

领域边界 = 微服务边界:identity、conversation、message、delivery、presence、media-signal、file;外部统一走网关

MVC 老路:Controller 校验 → Service 写库 → 调 WebSocket 推送 → 出错回滚/重试点很多,顺序与幂等靠代码约定

DDD/六边形新路:

应用层 SendMessage 调领域服务生成 seq;

同事务写 messages + outbox,返回 MessageItem(seq, id);

Outbox-relay 按 conversation_id 作为 key 写 Kafka;

delivery 消费后依据 presence 找节点推送,失败重试/死信;离线只推进 inbox.delivered,上线按 seq 回放 结果:顺序有了抓手(seq + 分区),可靠有了抓手(Outbox + 重放),扩展有了抓手(写入/投递各自扩容)

你在做架构转型时遇到的最麻烦的一个点是什么,有没有遇到比较典型的一些困难

你觉得使用微服务这种方式来部署的优点是什么,当时为什么要用这种方式来部署你的服务

微服务你分了哪几块呢?

你在构建文件处理服务时有没有做一些优化呢?比方说对存储的一些交互

你提到的这个缓存我比较好奇,比如用户上传一个文件,在数据库里有这样的一个文件的信息记录,文件被上传或被下载实际对应的请求是到后端的存储对吧,这个存储你用的是什么类型的?比如说你去申请云厂商,会让你选存储的类型,有没有关注选的是哪一种类型的存储呢?

那你用的是什么协议呢,和文件上传下载相关的一些协议,或者你是用的哪个 sdk 去和文件系统做的交互?(答了 aws,面试官提示说是 s3)

能讲一下音视频服务用到的协议的一些概念吗?就 WebRTC 这一块的概念,以及你在做这个项目里面会遇到一些什么问题

你说有一个专门用作用户管理的微服务,就是用户信息和鉴权,那首先一点,你用的鉴权方式是什么呢?然后和前端那块是怎么交互的?

你刚刚提到了用 jwt 的 token 去做整个系统的鉴权,一个是 auth token,一个是 refresh token,那你是如何去验证这个 token 的有效性的?第二个点是,如果前端发送的请求带的是一个有效的 refresh token,那你是怎样继续去后面的逻辑,让这个请求可以完整的去返回给前端呢?

网关这一块你主要是涉及到哪一些开发?

网关的逻辑是你自己写的吗,还是说延续了之前的

对于 HTTP 请求,这个网关是怎样的逻辑去转发给后端不同的服务的?比方说用户验证,或者说刚刚提到的音视频请求,你是怎样分发给这些服务的,你微服务之间是怎样去做协调的?

它的网络连接是怎么走的?比如现在有一个请求进来,到网关之后被认为请求是有效的,你会将相应的一个 message 发到消息队列中对吧,所有的微服务都在监听着这一个事件中心,或者说是 eventbus,比方说我是属于一个音频服务,我现在需要返回我的服务给前端,它后续的逻辑是怎么走的?完整的请求链是怎么实现的?

  • 你在这个项目中是把网关做成了适配器层吗,还是转发层

  • 你这个网关是业务网关还是流量网关

  • 那假如现在让你把这个项目做成一个高性能的服务,部署在各个国家各个地区的节点,那你会怎么设计这个系统

  • 你的这个项目有什么亮点吗

  • 那你是如何界定多个模块之间的领域边界的呢

  • 项目做了哪些功能?

  • 挑一个你开发的功能模块介绍

  • 为什么要做这个项目?项目规模多大?做之前有调研过其他同类产品吗?

  • 你觉得你的项目相比别人有什么优点?

  • 如果要优化你的项目,可能会有哪些可以优化的点?

  • 回顾整个项目,你最自豪的技术实现点是什么?

  • 如果给你一次重构机会,你最想改进或重写哪部分?为什么?

  • 分析一下你之前项目当中,印象比较深刻的部分(难点,亮点)

  • 项目的高并发是怎么做的?是自己假想了高并发的场景?

  • 项目的同步机制和低延迟通信是怎么做的?

  • 项目里有没有使用过多线程,线程安全问题

  • 为什么项目要用微服务

  • 是如何具体用JWT实现身份认证的?

  • (如果简历里有写压测)10w+ 并发用户是怎么测的,用什么工具?

  • (如果简历里有写部署)项目服务器是怎么部署的?用到了几台服务器?

  • 项目为什么要用 JWT

  • 项目的代码量大概是多少?是你自己一个人在写吗,项目是怎么来的

  • 介绍一下你简历上的几个项目

  • 我刚刚听你有提到你对项目进行了 DDD 的重构,能解释一下 DDD 的概念吗

  • 那假如现在我们有两个不同的领域,但是它们需要共用用户的数据,那你这时候该如何把用户的数据按照领域去拆分呢,你有遇到过这种情况吗

面试时可拓展:

  • CAS/乐观锁与可能存在的ABA问题、Redis Lua 脚本
  • 分布式多节点
  • 多端同步
  • 强一致性保证
  • 已读/未读
  • 撤回、重新编辑

目录