Azure WebJob(TimerTrigger)长时长任务故障排查与配置优化技术问询
解决Azure WebJob长时长任务莫名失败与重启重复执行的问题
针对你遇到的两个核心痛点——长时任务无异常中断、重启后重复执行,我来一步步拆解和解决:
一、选对Azure App Service计划,解决无异常失败问题
你的WebJob跑4-5小时偶尔莫名挂掉,大概率是资源不够或平台进程回收导致的,按以下步骤排查和选型:
1. 先排查当前计划的瓶颈
先确认你用的是哪一层计划:
- Free/Shared层:绝对不适合长时任务,每天只有60分钟CPU时长,还会被强制回收,直接排除
- Basic/Standard/Premium层:支持Always On,资源配额更高,是长时任务的基础
2. 监控资源使用,找准问题
去Azure Portal的App Service资源页,打开Metrics面板:
- 盯紧CPU、内存使用率:如果任务跑的时候CPU持续飘在80%以上,或者内存快摸到计划的上限,那就是资源不够了
- 用Diagnose and solve problems工具:搜“WebJob failures”或“App Service crashes”,看看有没有平台级的回收日志(比如内存过载导致进程被kill)
3. 选合适的计划规格
你的任务要处理17500条Parquet记录,还要发大量SOAP请求、写存储,属于IO+CPU混合密集型:
- 起步选Basic B2或Standard S2:2核CPU+3.5GB内存,足够支撑4-5小时的长时任务
- 如果CPU占用一直很高,直接升级到Standard S3或Premium层,给足资源
- 你已经开了64位模式,Basic及以上都支持,没问题
4. 额外优化小技巧
- 你代码里
RestClient.Timeout = -1太激进了,建议设个合理值(比如300秒),避免单个请求挂死整个任务 - 考虑分批处理:把17500条记录拆成小批次,每处理一批就提交存储,减少内存压力
- 开WebJob详细日志:在Portal的WebJob页开启Logging,说不定能挖到隐藏的平台日志(比如进程被强制终止的原因)
二、实现任务幂等性,避免重启重复执行
TimerTrigger默认会在重启后重新触发最近未完成的任务,要解决这个问题,核心是让任务能判断当前调度周期的活儿已经干过了,用你现有的存储表就能实现:
1. 新增任务执行状态表
用你已经在用的IStorageTableService,加个JobExecution表,记录每个调度周期的任务状态:
public class JobExecution : TableEntity { // 分区键用调度时间格式化字符串(比如"2024-05-20-10-00-00"),保证每个调度周期唯一 // 行键用任务标识,比如"FeedJob" public DateTime ExecutionTime { get; set; } public JobStatus Status { get; set; } public DateTime? CompletedTime { get; set; } } public enum JobStatus { Pending, Running, Completed, Failed }
2. 在任务开头加检查逻辑
修改你的ProcessTimerJob方法,先检查当前调度的任务是否已经执行过,再决定要不要继续:
public async Task ProcessTimerJob([TimerTrigger("%RunEvery%")] TimerInfo myTimer, ILogger log) { logJob = log; // 用调度时间作为唯一标识,避免重复执行 var scheduledTimeStr = myTimer.ScheduleStatus.Last.ToString("yyyy-MM-dd-HH-mm-ss"); var executionRecord = new JobExecution { PartitionKey = scheduledTimeStr, RowKey = "FeedJob", ExecutionTime = myTimer.ScheduleStatus.Last, Status = JobStatus.Running }; try { // 先查这个调度周期的任务是否已经完成 var existingRecord = await _storageTableService.GetEntity<JobExecution>(scheduledTimeStr, "FeedJob"); if (existingRecord != null && existingRecord.Status == JobStatus.Completed) { logJob.LogInformation($"OP- =========== 调度任务[{scheduledTimeStr}]已完成,跳过执行 ==========="); return; } // 如果任务还在跑(比如之前的实例没挂完),也跳过 if (existingRecord != null && existingRecord.Status == JobStatus.Running) { logJob.LogInformation($"OP- =========== 调度任务[{scheduledTimeStr}]正在执行中,跳过执行 ==========="); return; } // 标记任务为运行中 await _storageTableService.InsertOrMergeEntity(executionRecord); // --- 以下是你原来的核心任务逻辑 --- _cptTotal = _cptErrors = _cptNominals = 0; _dtStart = DateTime.UtcNow; var dtStartForReconstruction = _dtStart.AddHours(-1); logJob.LogInformation($"OP- =========== Job - - =========== Started at : {_dtStart}"); // ... 你的原有代码 ... // 任务完成后标记为已完成 executionRecord.Status = JobStatus.Completed; executionRecord.CompletedTime = DateTime.UtcNow; await _storageTableService.InsertOrMergeEntity(executionRecord); } catch (Exception e) { // 异常时标记为失败 executionRecord.Status = JobStatus.Failed; executionRecord.CompletedTime = DateTime.UtcNow; await _storageTableService.InsertOrMergeEntity(executionRecord); // 原有异常日志逻辑 logJob.LogInformation($@"OP- {_prefix} {_dtStart} =========== Job - - =========== Exception generale at : {DateTime.UtcNow}"); if (e.InnerException != null) { logJob.LogError($@"OP- {_prefix} {_dtStart} =========== Job - - =========== Exception generale Message : {e.Message}"); logJob.LogError($@"OP- {_prefix} {_dtStart} =========== Job - - =========== Exception generale Inner Message : {e.InnerException.Message}"); } else { logJob.LogError($@"OP- {_prefix} {_dtStart} =========== Job - - =========== Exception generale Message : {e.Message}"); } } }
3. 注意事项
- 确保
IStorageTableService的InsertOrMergeEntity方法支持Table Storage的Merge操作(更新现有实体) - 如果你的Timer是固定间隔(比如每天一次),
myTimer.ScheduleStatus.Last能准确拿到调度时间,作为唯一标识很靠谱 - 定期清理表中的旧记录,避免数据量过大影响性能
内容的提问来源于stack exchange,提问作者fguigui
相关产品推荐
相关产品推荐

