MassTransit OutBox 不发送消息问题

12 阅读5分钟

MassTransit UseBusOutbox() + EF Core + MySQL 场景下消息偶发不投递,OutboxMessage/OutboxState 已插入但未进入 SendObserver

问题概述

我在一个 ASP.NET Core + MassTransit + RabbitMQ + EF Core + MySQL 的项目中,使用了:

  • MassTransit.EntityFrameworkCore
  • MassTransit.RabbitMQ
  • UseBusOutbox()

遇到一个比较诡异的问题:

  • 业务代码里 publishEndpoint.Publish(...) 能正常返回
  • EF Core 日志能看到 OutboxState / OutboxMessage 已成功插入
  • CommitAsync 没有异常
  • 但消息偶发不会真正发到 RabbitMQ
  • 此时我挂的 SendObserver / ReceiveObserver 都没有任何日志

如果去掉 UseBusOutbox(),同样的消息链路连续测试很多次都正常,没有这个现象。

所以我目前基本确认问题只出现在 MassTransit EF Outbox 这一层。


环境信息

  • .NET: net10.0
  • EF Core: 9.0.12
  • Pomelo.EntityFrameworkCore.MySql: 9.0.0
  • MassTransit.EntityFrameworkCore: 8.5.5
  • MassTransit.RabbitMQ: 8.5.5
  • RabbitMQ: 使用 RabbitMQ transport
  • 数据库: MySQL

相关包版本:

<PackageReference Include="MassTransit.EntityFrameworkCore" Version="8.5.5" />
<PackageReference Include="MassTransit.RabbitMQ" Version="8.5.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.12" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />

Outbox 配置

配置方式如下:

services.AddMassTransit(x => {
    x.SetEndpointNameFormatter(
        new KebabCaseEndpointNameFormatter(prefix: endpointNamePrefix, includeNamespace: true)
    );

    x.AddConsumers(consumerAssemblies);

    x.AddEntityFrameworkOutbox<TDbContext>(o => {
        o.UseMySql();
        o.UseBusOutbox();
        // 我后续也尝试过调小 QueryDelay,例如 500ms
        // o.QueryDelay = TimeSpan.FromMilliseconds(500);
    });

    x.UsingRabbitMq((context, cfg) => {
        cfg.UsePublishFilter(typeof(TenantPublishFilter<>), context);
        cfg.UseConsumeFilter(typeof(TenantConsumeFilter<>), context);

        cfg.ConnectPublishObserver(new TrackingPublishObserver(...));
        cfg.ConnectSendObserver(new TrackingSendObserver(...));
        cfg.ConnectReceiveObserver(new TrackingReceiveObserver(...));

        cfg.ConfigureEndpoints(context);
    });
});

业务场景

消息类型是一个普通 contract:

public record ContractReviewAnalyzeRequestedContract(long FileId);

发布方式有两种:

1. HTTP 测试接口内直接 Publish

[HttpPost("{id:long}/test")]
public async Task<IActionResult> Test(long id) {
    await unitOfWork.ExecuteAsync(async () => {
        await publishEndpoint.Publish(new ContractReviewAnalyzeRequestedContract(id));
    });
    return Ok();
}

2. 领域事件处理器内 Publish

public async override Task<ContractReviewFileUploadedDomainEvent> HandleAsync(
    ContractReviewFileUploadedDomainEvent command,
    CancellationToken cancellationToken = default
) {
    var fileId = command.File.Id;
    var messageId = NewId.NextGuid();

    logger.LogInformation(
        "准备发布合同分析请求。fileId={FileId}, messageId={MessageId}",
        fileId,
        messageId
    );

    await publishEndpoint.Publish(
        new ContractReviewAnalyzeRequestedContract(fileId),
        publishContext => {
            publishContext.MessageId = messageId;
            publishContext.CorrelationId = messageId;
        },
        cancellationToken
    );

    logger.LogInformation(
        "合同分析请求 Publish 调用完成。fileId={FileId}, messageId={MessageId}",
        fileId,
        messageId
    );

    return await base.HandleAsync(command, cancellationToken);
}

现象

现象 1:偶发不发

同样一条消息,连续发多次时:

  • 大概 10 次里有 4 次正常
  • 大概 10 次里有 6 次“看起来没发出去”

所谓“没发出去”指的是:

  • 发布前后业务日志都有
  • EF Core 能看到 Outbox 插入 SQL
  • TrackingSendObserver 没有 PreSend/PostSend
  • TrackingReceiveObserver 也没有 PreReceive/PostReceive
  • 消费者没有收到消息

现象 2:去掉 UseBusOutbox() 后稳定正常

我把 UseBusOutbox() 去掉后,连续测试很多次:

  • SendObserver
  • ReceiveObserver
  • Consumer

都稳定触发,没有复现问题。


已确认的事实

1. 消息确实写入了 Outbox

EF Core SQL 日志能明确看到:

INSERT INTO `OutboxState` ...
INSERT INTO `OutboxMessage` ...

实际日志里 OutboxMessage.Body 中能看到完整消息体,例如:

{
  "messageId": "50860000-5f41-aa39-d9b2-08de9b88fe12",
  "destinationAddress": "rabbitmq://.../ContractReview.Domain.Contracts:ContractReviewAnalyzeRequestedContract",
  "messageType": [
    "urn:message:ContractReview.Domain.Contracts:ContractReviewAnalyzeRequestedContract"
  ],
  "message": {
    "fileId": 795229726203973
  }
}

2. 事务提交没有报错

我的 UnitOfWork 大致如下:

public async Task<T> ExecuteAsync<T>(Func<Task<T>> func, CancellationToken cancellationToken = default) {
    await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);

    try {
        var result = await func();

        if (dbContext.ChangeTracker.HasChanges()) {
            logger.LogInformation("发生了变更,进行SaveChanges");
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        await transaction.CommitAsync(cancellationToken);
        return result;
    } catch (Exception ex) {
        logger.LogError(ex, "工作单元异常");
        await transaction.RollbackAsync(cancellationToken);
        throw;
    }
}

出现问题时:

  • 能看到 发生了变更,进行SaveChanges
  • 看不到 工作单元异常

所以至少从应用日志看,SaveChanges/Commit 没有抛异常。

3. Delivery 线程确实在轮询

EF Core SQL 日志还能看到周期性执行:

SELECT * FROM `OutboxState` ORDER BY `Created` LIMIT 1 FOR UPDATE SKIP LOCKED

说明 Outbox Delivery 线程并不是完全没跑。

4. 但有些时候始终不进入 SendObserver

正常时会看到:

[发信探针 - PreSend]
[发信探针 - PostSend]
[收信探针 - PreReceive]
消费者日志

异常时则只有:

  • 业务侧 Publish 前后日志
  • EF Core 的 Outbox 插入日志
  • OutboxState 轮询 SQL

但没有任何 PreSend/PostSend


关键日志片段

异常场景

业务日志:

准备发布合同分析请求。fileId=795269460717637, messageId=08490000-5f41-aa39-d7db-08de9b85beaf
合同分析请求 Publish 调用完成。fileId=795269460717637, messageId=08490000-5f41-aa39-d7db-08de9b85beaf

EF Core SQL:

INSERT INTO `OutboxState` ...
INSERT INTO `OutboxMessage` ...

之后仅看到:

SELECT * FROM `OutboxState` ORDER BY `Created` LIMIT 1 FOR UPDATE SKIP LOCKED

但没有:

TrackingSendObserver.PreSend
TrackingSendObserver.PostSend
TrackingReceiveObserver.PreReceive
Consumer.Consume

正常场景

同样的 Publish 链路,正常时可以看到:

>>> [发信探针 - PreSend]
<<< [发信探针 - PostSend]
>>> [收信探针 - PreReceive]
收到合同分析请求。fileId=...

已排除项

1. RabbitMQ 基础连通性问题

去掉 UseBusOutbox() 后稳定正常,因此不是 RabbitMQ 基础连通性问题。

2. 消费者逻辑异常

有些时候连 ReceiveObserver 都不触发,所以不是消费逻辑导致的“处理失败后看起来像没收到”。

3. CommitAsync 抛异常

没有进入 UnitOfWorkcatch

4. 消息根本没入库

EF Core SQL 明确能看到 OutboxState/OutboxMessage 插入。


额外现象:RabbitMQ 里出现过一个异常队列

我曾在 RabbitMQ 管理页面看到一个队列:

contract-review-contract-review-infrastructure-consumers-contract-review-analyze-test

现象是:

  • 我每次 Publish,这个队列的 Ready 数量也会增加
  • 但当前代码里并没有显式定义这个 ...-test 端点

这让我怀疑是否有历史遗留绑定、旧实例、测试端点绑定未清理等问题。不过即使不考虑这个队列,UseBusOutbox() 偶发不发的问题依然存在。


我目前最想确认的问题

  1. MassTransit 8.5.5 + EntityFrameworkOutbox + MySQL + EF Core 9 是否已知存在类似问题?
  2. 有没有可能是 OutboxState / OutboxMessage 被某种竞争条件处理掉了,但没有真正进入 transport send?
  3. SELECT ... FOR UPDATE SKIP LOCKED 能看到,但没有 SendObserver,这种现象更可能出现在什么执行路径上?
  4. 有没有推荐的源码断点位置,适合直接确认消息是卡在:
    • Outbox delivery 查询后
    • transport send 前
    • 还是某个内部状态更新逻辑中

如果有人建议升级

我目前不能简单升级到 MassTransit.EntityFrameworkCore 8.5.6+,因为它引出的 EF Core 依赖版本在我的项目里暂时无法接受,所以如果有建议,最好是在:

  • MassTransit 8.5.5
  • EF Core 9.0.12
  • Pomelo 9.0.0

这个约束下给出排查方向。


感谢

如果有人遇到过类似问题,或者知道应该优先打哪些源码断点/看哪些内部日志,非常感谢指点。