如何避免批量发送带工资单附件邮件时SendGrid被限流?
解决方案建议
针对你遇到的SendGrid限流和Azure Function超时问题,结合带唯一附件的场景,给出以下实用方案:
1. 改用SendGrid批量发送接口
放弃循环中单封发送的方式,改用SendGrid批量邮件接口,将多封邮件打包成一个API请求发送,大幅减少调用次数,从根源上降低触发限流的概率。
实现示例:
var sendGridMsg = new SendGridMessage(); sendGridMsg.SetFrom(new EmailAddress("your-sender@domain.com", "公司名称")); sendGridMsg.SetTemplateId("你的邮件模板ID"); foreach (var employee in employeeList) { // 为每个员工创建独立的个性化配置 var personalization = new Personalization(); personalization.AddTo(new EmailAddress(employee.Email, employee.Name)); // 注入模板所需的个性化数据(比如员工姓名、工号等) personalization.SetDynamicTemplateData("employeeName", employee.Name); // 添加该员工的唯一工资单附件 var payslipAttachment = new Attachment { Content = Convert.ToBase64String(employee.PayslipFileContent), Filename = $"工资单_{employee.Id}_{DateTime.Now:yyyyMMdd}.pdf", Type = "application/pdf", Disposition = "attachment" }; personalization.AddAttachment(payslipAttachment); sendGridMsg.AddPersonalization(personalization); } // 一次请求发送所有邮件 await sendGridClient.SendEmailAsync(sendGridMsg);
2. 实现智能限流重试机制
即使使用批量接口,仍可能遇到SendGrid的限流(429状态码),需添加重试逻辑并遵守SendGrid的限流头部信息:
- 用Polly库实现指数退避重试,避免频繁重试加剧限流
- 解析响应头中的
X-RateLimit-Reset值,精准等待到限流重置时间后再重试
示例代码:
var retryPolicy = Policy .Handle<SendGridException>(ex => ex.StatusCode == HttpStatusCode.TooManyRequests) .WaitAndRetryAsync(3, (retryCount, context) => { // 尝试从异常中获取限流重置时间,没有则用指数退避 if (context.Exception is SendGridException sgEx && sgEx.ResponseHeaders.TryGetValue("X-RateLimit-Reset", out var resetTimeStr) && long.TryParse(resetTimeStr, out var resetEpoch)) { var resetTime = DateTimeOffset.FromUnixTimeSeconds(resetEpoch).UtcDateTime; return resetTime - DateTime.UtcNow; } return TimeSpan.FromSeconds(Math.Pow(2, retryCount)); }); await retryPolicy.ExecuteAsync(async () => { await sendGridClient.SendEmailAsync(sendGridMsg); });
3. 用Azure Durable Functions解决超时问题
普通Azure Function有严格的超时限制(消费计划默认5分钟,最大10分钟),改用Durable Functions的Fan-Out/Fan-In模式拆分任务:
- 创建Orchestrator函数,将90名员工分成多个批次(比如每15个一批)
- 每个批次调用独立的Activity函数发送邮件,Orchestrator负责协调所有批次的执行
- Durable Functions支持长时间运行的工作流,不会因为单个Activity超时导致整个任务终止
核心逻辑示例:
// Orchestrator函数 [FunctionName("PayslipEmailOrchestrator")] public static async Task RunOrchestrator( [OrchestrationTrigger] IDurableOrchestrationContext context) { var employeeList = context.GetInput<List<Employee>>(); // 拆分成每15个员工一批 var batches = employeeList.Chunk(15); var tasks = new List<Task>(); foreach (var batch in batches) { tasks.Add(context.CallActivityAsync("SendPayslipBatch", batch)); } // 等待所有批次完成 await Task.WhenAll(tasks); } // Activity函数 [FunctionName("SendPayslipBatch")] public static async Task SendPayslipBatch( [ActivityTrigger] List<Employee> batch, ILogger log) { // 这里复用前面的批量发送逻辑发送当前批次的邮件 var sendGridClient = new SendGridClient(Environment.GetEnvironmentVariable("SendGridApiKey")); // ... 构造批量邮件并发送 }
4. 优化附件预处理逻辑
避免在发送循环中实时生成或读取附件,提前完成工资单的生成和存储:
- 提前将所有员工的工资单PDF生成并上传到Azure Blob Storage
- 为每个Blob生成临时SAS链接,在邮件中直接引用链接(或在批量请求中嵌入Base64编码的内容)
- 这样能减少发送逻辑中的IO操作,提升整体处理效率
5. 临时调整Azure Function配置(可选)
如果暂时无法切换到Durable Functions,可以尝试调整普通Function的超时时间:
- 在
host.json中设置functionTimeout为消费计划允许的最大值(00:10:00) - 注意这只是临时方案,长期来看Durable Functions更可靠
内容的提问来源于stack exchange,提问作者Tho-venaar
相关产品推荐
相关产品推荐

