Docker容器中Discord.NET调用DeferAsync后斜杠命令全部超时无响应
问题现象
基于discord.net开发的Discord机器人托管在Docker容器中运行,初始版本/getmoney斜杠命令可正常响应,对应实现代码如下:
[SlashCommand("getmoney", "get current total of currency for given user")] [DefaultPermission(true)] public async Task GetMoney(Discord.IUser user) { var userHelper = new UserHelper(_dbContext); var userRecord = userHelper.FindOrAddUser(user.Id.ToString()); await ReplyAsync($"{user.Username} has {userRecord.Funds} Dollars!"); }
引入DeferAsync()与FollowupAsync()改造命令逻辑后,本地调试环境运行完全正常,对应改造后代码如下:
[SlashCommand("getmoney", "get current total of currency for given user")] [DefaultPermission(true)] public async Task GetMoney(Discord.IUser user) { await DeferAsync(); var userHelper = new UserHelper(_dbContext); var userRecord = userHelper.FindOrAddUser(user.Id.ToString()); await FollowupAsync($"{user.Username} has {userRecord.Funds} Dollars!"); }
但将改造后的代码构建镜像部署到容器后,所有斜杠命令均出现超时问题,包括其他独立模块的命令也无法正常响应;触发命令时日志无任何错误输出。回退到改造前的代码重新构建部署后服务恢复正常,可确认代码修改是问题诱因,但无对应错误日志可追溯。
根因
该问题是典型的Discord.NET网关线程死锁问题:
- Discord.NET默认斜杠命令执行模式为
RunMode.Sync,即命令处理逻辑直接在网关WebSocket事件循环线程上同步执行 - 调用
DeferAsync()时,方法会向Discord API发送交互延迟确认请求,同时需要等待网关线程接收API返回的ACK响应才能继续往下执行 - 此时网关线程正被阻塞在等待
DeferAsync()完成的逻辑上,无法从WebSocket连接读取返回的ACK消息,直接造成整个事件循环卡死 - 本地调试环境运行时,Debug编译配置会附带额外的线程调度兜底逻辑,加上桌面系统的线程池调度策略和Linux容器环境存在差异,因此不会触发死锁;Release编译后在Linux容器中运行时无兜底逻辑,死锁稳定复现,且因为线程直接卡死,不会抛出任何异常,因此没有错误日志。
修复方案
按优先级选择以下配置即可解决问题:
- 全局配置交互服务默认异步执行命令,从根源避免网关线程阻塞。在初始化
InteractionService时传入配置:var interactionService = new InteractionService(client, new InteractionServiceConfig { DefaultRunMode = RunMode.Async }); - 若不想全局修改默认执行模式,可单独给存在长耗时操作、需要调用
DeferAsync的命令添加特性,标记为异步执行:[SlashCommand("getmoney", "get current total of currency for given user")] [DefaultPermission(true)] [RunMode(RunMode.Async)] public async Task GetMoney(Discord.IUser user) { await DeferAsync(); var userHelper = new UserHelper(_dbContext); var userRecord = userHelper.FindOrAddUser(user.Id.ToString()); await FollowupAsync($"{user.Username} has {userRecord.Funds} Dollars!"); } - Docker部署额外优化:
- 发布dotnet项目时不要使用过度激进的程序集裁剪配置,避免异步调度相关依赖被误裁剪
- 启动容器时可添加环境变量
DOTNET_ThreadPoolMinThreads=200,调高Linux容器下dotnet线程池最小线程数,避免线程池饥饿加剧阻塞问题
内容的提问来源于stack exchange,提问作者Rob Gardner
相关产品推荐
相关产品推荐

