IM.md
IM
简单介绍一下你的这个 DDD 六边形和微服务架构的 IM 项目
我的这个 IM 项目是按 DDD 六边形和微服务架构落地的即时通讯系统,是我在 GitHub 上找到的一个开源项目,它原本使用的是很经典的 MVC 架构,我为了把写入和投递解耦、业务更清晰并按流量独立扩缩容,就利用 DDD 和微服务来把它重构了一下,也刚好是学习一下这两个架构,我之前是没有接触过它们的
整体来说,项目对外只有一个网关,负责 HTTPS、RESTful、WebSocket、鉴权、心跳和限流,内部服务则用 gRPC 通信,核心服务共七个,分别是 identity(负责管理用户账号、联系人、鉴权和黑白名单等)、conversation(负责聊天会话与成员)、message(负责消息写入、历史、生成会话内自增 seq 并幂等)、delivery(负责从队列消费并做在线推送和离线入库)、presence(负责用户在哪个网关节点的在线路由)、media-signal(负责音视频的 WebRTC 信令)和 file(负责文件管理)
消息链路的话,客户端经过网关的 WebSocket 发送,message 服务同事务写库并记录到 Outbox,Outbox relay 把消息按照按会话 id 作为 key 投到 Kafka,天然保证了会话内有序和至少一次可达,delivery 消费后,如果用户在线就通过 presence 服务找到节点把消息推下去,如果离线就只更新进度,待用户上线了就按照 seq 回放
存储方面的话是用 MySQL 和 GORM 来做的持久化,用 Redis 做缓存和在线状态管理,未读和最近会话是走缓存,热点会话限速与退避,文件用预签名直传,服务端只存对象键和元数据
音视频方面是用 WebRTC 的信令单独做服务,负责发起、接受、拒绝、挂断以及 SDP 和 ICE 交换,如果是弱网环境或 NAT 就直接回退到 TURN,呼叫事件也写入消息流,保障离线可见
最后就是用 Docker 来一键部署,用 k8s 管理,CI/CD 用的是 GitHub Actions,日志主要是结合 zap 库二次封装的,实现链路追踪与指标覆盖写入、投递和推送等关键路径,并配置告警
可以讲一下你在这个项目里是怎么用到 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
-
项目的代码量大概是多少?是你自己一个人在写吗,项目是怎么来的