ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Quartz.NET 基准对比框架:Quartz.Benchmark.Competitors 如何测量调度器的真实执行能力

Quartz.NET 基准对比框架:Quartz.Benchmark.Competitors 如何测量调度器的真实执行能力 任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载Quartz.Benchmark.Competitors 是 Quartz.NET 仓库内一个专门用于横向对比的基准测试工程它把 Quartz.NET 与另外两个 .NET 调度器——TickerQ 10.4.0 与 Hangfire 1.8.25——放在同一套工作负载下只测量调度器真正该做的事执行作业。读完本文你将掌握这套对比框架的定位、五个基准场景S1–S5的完整定义与运行方法、三个引擎的统一设置与各自差异、它如何在作业体内计数以保证可比性以及它是如何回答一次触发在内存、数据库、时延、周期准确性与写入成本上各值多少的。它为什么存在回应一份没有执行任何作业的第三方对比这个工程诞生的直接动因是回应 TickerQ 官方基准套件benchmarks/TickerQ.Benchmarks/Comparisons/中一份被广泛引用的三方对比。该对比存在三方面问题测的不是同一件事。ConcurrentThroughputComparison的基线是FrozenDictionarystring, TickerFunctionDelegate查找后调用一个函数体为Task.CompletedTask的委托旁边的 Hangfire 分支创建后台作业Quartz 分支则构建IJobDetail与ITrigger并调用ScheduleJob。JobCreationComparison的基线干脆是TickerQFrozenDictionary 函数查找。三列混在同一张表里彼此不可比。基准进程行为不可控。各分支跑在Parallel.For配合.GetAwaiter().GetResult()下Quartz 侧通过new StdSchedulerFactory()引用Quartz3.14.*包迭代之间不清空调度器也不清空存储两个存储随运行时长持续增长给作业命名的计数器一路攀升。没有任何作业被执行。整张表测的都是提交调用而不是作业运行一个调度器可以靠把提交路径做快而显得任意快。Quartz.Benchmark.Competitors 的设计目标因此非常明确每个分支都在运行中的作业内部计数——作业体递增一个Interlocked计数器调度前发布绝对目标值等待端用带超时的ManualResetEventSlim把挂死变成异常。一次基准调用就是等待 N 次执行真实发生。Completion.cs 是这套计数的实现Arm在调度前清零并发布目标Await阻塞到目标达成超时 5 分钟即抛异常判为坏基准Record由作业体第一行指令调用Disarm在引擎拆除后丢弃残留目标。计数器必须是进程级静态的因为 Hangfire 通过静态方法的表达式树运行作业、TickerQ 通过源生成委托来 new 出声明类两者的作业体都无法拿到实例引用。项目定位刻意游离在解决方案之外从 Quartz.Benchmark.Competitors.csproj 的注释可以看到它的边界设计不在Quartz.slnx中。Compile、UnitTest、BenchmarkSmoke、ExamplesSmoke、裁剪 canary 和 Sonar 都走解决方案因此本工程对这些流水线完全不可见允许它引用两个未签名的第三方调度器与一套 EF Core 栈而不污染任何受门禁的构建。IsPackablefalse分析关闭SonarQubeExclude、AnalysisLevelnone产物不发布。以ProjectReference引用Quartz.csproj而非已发布包所以它测量的是工作树——这正是文档强调的what it measures is the working tree。这避免了第三方对比因引用旧包而过期的问题。构建与运行必须显式点名该项目例如dotnet build -c Release src/Quartz.Benchmark.Competitors/Quartz.Benchmark.Competitors.csproj。运行方式从单场景到数据库普查README 给出的命令矩阵如下dotnet build -c Release src/Quartz.Benchmark.Competitors/Quartz.Benchmark.Competitors.csproj # 单个场景带测量 dotnet run -c Release --project src/Quartz.Benchmark.Competitors -- --filter *S1* # 一切无需数据库的场景各执行一次不测量任何东西 dotnet run -c Release --project src/Quartz.Benchmark.Competitors -- --smoke # S4 是普通运行器而非基准单独跑 dotnet run -c Release --project src/Quartz.Benchmark.Competitors -- --recurring几点说明Release 不可省略BenchmarkDotNet 拒绝非优化程序集。--smoke是整场运行而非修饰符不能与其他参数混用它通过 Program.cs 的SmokeConfig用 InProcess 无发射工具链执行所有不含RequiresDatabase类别的场景各一次且不强制电源计划——这是改动引擎后手工验证引擎还在执行作业的手段因为一个停止执行作业的引擎看起来就像一个慢引擎直到有东西等待一个永远不会到来的执行。另外三个整场运行是--recurring、--recurring-postgres和--commits均不接受其他参数Program.cs。--help会打印这些运行器的说明。退出码不等于健康BenchmarkDotNet 的 switcher 对失败的 case 只输出 NA 行Program.cs 的Report会把校验错误与失败 case 汇总成非零退出码并让过滤器匹配不到任何基准也失败而不是假装成功。S2 与--commits需要 PostgreSQL由进程外启动、通过环境变量命名。文档给出容器与连接串docker run -d --name quartz-competitors-pg -p 55432:5432 \ -e POSTGRES_DBquartznet -e POSTGRES_USERquartznet -e POSTGRES_PASSWORDquartznet \ postgres:15.1 -c shared_preload_librariespg_stat_statements $env:QUARTZ_BENCHMARK_POSTGRESHostlocalhost;Port55432;Databasequartznet;Usernamequartznet;Passwordquartznet dotnet run -c Release --project src/Quartz.Benchmark.Competitors -- --filter *S2* dotnet run -c Release --project src/Quartz.Benchmark.Competitors -- --commits # S4 对阵 Quartz 的 ADO 存储每次获取一轮对阵自动批处理 dotnet run -c Release --project src/Quartz.Benchmark.Competitors -- --recurring-postgres环境变量名QUARTZ_BENCHMARK_POSTGRES由 PostgresFixture.cs 读取未设置即抛异常。为什么数据库必须在进程外因为 BenchmarkDotNet 每个 case 跑一个进程若容器归基准所有每个 case 都要启动再丢弃一次。shared_preload_libraries参数仅为语句普查服务不带它--commits仍会统计提交数但语句列显示未采集。三个库共享同一个数据库、各自住在自己的默认 schema 下Quartz 的 shipped DDL 创建不带 schema 限定的qrtz_*表落在publicTickerQ 默认tickerHangfire 默认hangfire见 PersistentEngines.cs。ResetQuartz通过重放 database/tables/tables_postgres.sql 重置 Quartz 表该脚本执行两次即等价于 drop 再建ResetSchema对其它库直接DROP SCHEMA ... CASCADE。五个场景S1–S5 各自测量什么测量内容规模S1内存吞吐每次执行的纳秒数与字节数20,000 个同时到期的一次性作业S2同样的工作负载放到 PostgreSQL 上外加每次执行的提交数2,000 个一次性作业S3空闲引擎上的调度到执行时延200 次重复S4周期准确性每秒一次是否真的每秒触发100 个调度 × 60 秒PostgreSQL 上另加 20 的轻载档S5写入一条调度要花多少钱两个仅 Quartz 的分支把简单行拆成构建器与ScheduleJob两半向空存储写入 50,000 条场景实现分别位于 S1_InMemoryThroughput.cs、S2_PostgresThroughput.cs、S3_ScheduleLatency.cs、S4_RecurringAccuracy.cs、S5_ScheduleCost.cs全部经由 IEngine 这一刻意保持单薄的接口驱动——它只提供Start、ScheduleOneOff、ScheduleRecurring、Clear四个能力。接口文档说明得很直白任何更丰富的抽象都会要求三库都能表达一旦某个形状必须在一侧被模拟对比就从关于库变成关于模拟。公平性不在公共抽象里而在设置与计数位置上各实现用自己的官方 API——IScheduler.ScheduleJob、ITimeTickerManager.AddAsync、BackgroundJob.Schedule。设置与它们为什么是这些值三库对齐的核心设置如下QuartzTickerQHangfire工作线程上限MaxConcurrency10其默认值MaxConcurrency10默认ProcessorCount 此处 32WorkerCount10默认ProcessorCount × 5 此处 160轮询 / 批获取MaxBatchSize自动无提前触发窗口默认池大小、无窗口batched池大小 1 秒窗口tunedMinPollingInterval100 ms默认 1 秒SchedulePollingInterval内存50 ms、PostgreSQL100 ms默认 15 秒其余一切shipped 默认shipped 默认shipped 默认10 个 worker 是三库唯一都必须被明确告知的数字也是整个设置文件中影响最大的一个Harness.cs 的注释指出若不设置Hangfire 将以 16 倍于 Quartz 的 worker 跑这份负载——那样的行谈的不是调度器。轮询间隔对 TickerQ 与 Hangfire 都是#3802 规格指定的、有利于各自库的取值README 对此直言不讳。Quartz 的三种配置档为什么它占多行档MaxBatchSize窗口含义Defaults不设置内存为 1PostgreSQL 上自动跟随池#3862 起零AddQuartz直接给你的样子Batched池大小零#3862 在内存中测量后选定该默认前测过的样子Tuned池大小一秒../Quartz.Benchmark/README.md中 2026-09-02 吞吐数字所用的设置从 QuartzEngine.cs 的实现可见非Defaults档设置MaxBatchSize maxConcurrencyTuned额外设置BatchTriggerAcquisitionFireAheadTimeWindow TimeSpan.FromSeconds(1)。为什么一档是读取表格的读者需要知道自己属于哪一档——这是 README 特意发布三个 Quartz 行的原因。一个批次在now 与首个触发器触发时间中较晚者加窗口处结束所以窗口为零时取走所有已到期且仅已到期的触发器任何触发器都不会被提前触发调度器拒绝大于承载它的线程池的批次。窗口的代价在 QuartzSchedulerOptions.cs 的注释里讲得更细批次上限只是上界BatchTriggerAcquisitionFireAheadTimeWindow默认TimeSpan.Zero时批次只含已到期触发器加宽窗口会连首个触发器之后短时间内到期的触发器一起取走并把它们提前最多那么多触发。MaxBatchSize还不能超过ThreadPoolOptions.MaxConcurrency——QuartzOptionsValidators.cs 正是这条校验的实现超池的触发器会被本节点握住、其它节点无法触发直到池排空。整秒对齐这不是装饰Harness.DueAt把到期时间对齐到下一个整秒Harness.cs原因是 Hangfire 把计划作业的到期时间存成整秒 Unix 时间戳——ScheduledState.Handler.Apply把JobHelper.ToTimestamp(EnqueueAt)当分数存储DelayedJobScheduler拿它和ToTimestamp(now)比较——于是一个小数瞬间到期的作业会在所在秒的开始就变得合格最多提前一秒。对齐前Hangfire 在测量窗口开启前就干掉了 20,000 批次中的 4,319 个。TickerQ 也按整秒分桶GetEarliestTimeTickers取最早到期 ticker 的秒并返回该秒内所有条目。因此对齐让此瞬间到期在三库语义一致。配套防线是 Harness 的两个守卫EnsureScheduledBeforeDue在调度耗时超过 lead 导致作业在窗口开启前就到期时抛异常静默发布数字不被允许EnsureNothingRanEarly在窗口开启前执行量超过批次的 1% 时判失败——最初的未守卫 Hangfire 行就是这么跑掉整批的现在只容忍个位数。两秒 lead、逐迭代重建引擎、进程级 Allocated两秒 leadPostgreSQL 上二十秒把工作放在 TickerQ 的即时分发阈值之上——TickerManager.AddTimeTickerAsync会把ExecutionTime落在now.AddSeconds(1)内的 ticker 直接在调用线程上获取并分发绕开调度循环两秒让三个分支都走各自调度器自己的路径。短路路径本身也被测量各有独立行S1 的 HangfireEnqueue与 S3 的 TickerQ 空ExecutionTime。每迭代重建引擎S1、S2、S5 都是如此因为其中两个存储会塞满——TickerQ 保留每个已完成的 ticker 且每次轮询 LINQ 扫描全部持有条目Hangfire 的已完成作业存活到过期清扫。Quartz 是唯一不这样做的完成一次性触发器即删除它且由于作业明细没有其它触发器、又非 durable连明细一并删除。第二轮迭代若对着另两家的残留测量测的就是残留了。[MemoryDiagnoser]在每个类上Allocated是进程级的BenchmarkDotNet 读取GC.GetTotalAllocatedBytes统计所有线程因此该列是一次执行对整个引擎轮询循环、worker 全算上的代价而非某个线程的代价它也是精确的不管机器此刻还在干什么——这正是不稳定的Mean列比不上的。计数在作业第一条指令排空即最后一个作业开始这是三库可比的根基没有任何一方能把队列写入算作一次执行。代价是每个引擎对最后一次执行的簿记落在窗口之外——两万次执行里这是五万分之一的影响被忽略但一次普查窗口里不行所以--commits会在最后一个作业开始后再让计数器多跑 3 秒把最后一次完成的写入收进计数S2_CommitCensus.cs。--commits读取前先清空连接池PostgreSQL 15 在 backend 变为空闲时最多每秒一次发布其提交计数若 backend 在最近一次发布后一秒内空闲则保留 10 秒PGSTAT_IDLE_INTERVAL。backend 退出时也会发布所以普查先NpgsqlConnection.ClearAllPools()并等待服务过的 backend 消失PostgresFixture.Commits。不这么做的话TickerQ 半秒的爆发结束后 3 秒读取计数器显示每次执行 0.05–0.21 次提交而真相是 2.0#3861语句计数因为pg_stat_statements在语句结束时即计数一直是对的。代价是冷连接池每个引擎下次要连接时重开连接PostgreSQL 把连接启动也记为一次事务Npgsql 默认 100 连接一池即每窗口至多约 100 次提交。写入数则来自事务 ID 计数器pg_snapshot_xmax(pg_current_snapshot())事务一写入即移动、无需等待发布——那是付过 flush 代价的提交PostgresFixture.Writes。调度不被测量所以无批量 API 的库并发调度Hangfire 是三库中唯一没有批量创建 API 的两千次顺序往返 PostgreSQL 约需 16 秒——比 harness 需要的 lead 还长。于是用 8 路并发在迭代 setup 中完成HangfireEngine.cs完全处于任何测量窗口之外。Quartz 的批量 API 是ScheduleJobs(schedule, ScheduleJobOptions.Replacing, ...)TickerQ 是AddBatchAsync。PostgreSQL 上迭代之间清空 schema内存引擎每迭代免费得到新存储新引擎即新存储数据库不会——Hangfire 的成功作业活到过期清扫TickerQ 保留全部已完成 ticker。S2 还因为每次迭代都是两千次往返、接近一分钟所以跑 5 次迭代而非 7 次S2_PostgresThroughput.cs 的IterationCleanup里引擎拆除后按分支重置 schema。一次触发内部发生了什么以下均按本 harness 锁定的各库版本源码描述。Quartz.NET调度线程从存储获取一批触发器等到首个触发器的触发时间调用TriggersFired标记它们并读取作业明细然后在JobRunShell内把每个交给线程池。QuartzEngine.cs 的注释给出完整链路shell 创建 DI 作用域、经它构建作业实例、跑中间件管线、通知注册的监听器、执行作业然后调用TriggeredJobComplete把触发器前移——或对一次性作业删除它及其成为孤儿的作业明细——再释放。RAMJobStore上是一个 monitor 加一个有序集合#3802 的剖析把稳态重复触发器路径定为约 2.5 KB 一次触发此处多出的是一次性作业的移除。ADO 存储上约 9.7 条语句、1.24 次提交。调度器由QuartzSchedulerBuilder构建、自建容器所以测的是AddQuartz给应用的布局而不是手工拼装的内部布局。S1 的 Quartz 行有个刻意的形状选择一个作业明细对应一个触发器与另两库的独立单元形状一致QuartzEngine.ScheduleOneOff。另一种布局——一个作业明细挂 20,000 个触发器——对 Quartz 显著更差且被记录为独立发现见下文 Findings不该混进表里冒充调度器测量。TickerQ一个后台服务轮询向持久化提供者询问最早到期的 tickers内存提供者上是对其持有的全部条目做 LINQ 扫描、过滤、排序、两次物化——这正是已完成 ticker 为何要紧的原因把整秒的一批标记Queued睡到它们到期从不短于MinPollingInterval标记InProgress再各自排进自己的任务调度器。运行一次要付出CreateAsyncScope、链接的CancellationTokenSource、TickerFunctionContext、两个Stopwatch、一次静态取消令牌管理器注册以及经提供者的最终状态写——内存与数据库一视同仁。作业实例由源生成代码 new 出来而非从 scope 解析TickerQEngine.cs。一秒内到期的 ticker 根本不进那个循环TickerManager.AddTimeTickerAsync拿ExecutionTime与now.AddSeconds(1)比较落在窗内就在调用线程上获取并分发——S3 的 TickerQ 行测的正是这条最快捷径。发现机制同样依赖源生成生成器向程序集发出带[ModuleInitializer]的TickerQInstanceFactoryExtensions.Initialize模块加载时函数自动注册console 工程无需任何 host 装配该行实现见 TickerQEngine.cs。在 EF Core 存储上c6ed1e7daa数据库侧的动作是步骤语句时机认领每个 tickerQueueTimeTickers每 ticker 一条UPDATE … WHERE Id id循环一看到它即执行无论离到期还有多远标记整批InProgressSetTickersInProgress一条UPDATE … WHERE Id ANY(ids)到期瞬间完成UpdateTimeTicker每次执行一条UPDATE … WHERE Id id函数返回之后每一步都是自己的隐式事务之后跟着 Npgsql 在池化连接下次使用时的DISCARD ALL。没有synchronous_commit设置也没有完成操作的批处理。循环苏醒会晚掉与认领耗时相当的时间GetNextTickers读时钟、算出距离最早 ticker 到期还有多久然后一条条UPDATE认领那一秒的所有 tickerInternalTickerManager.cs:35、:60、:103循环再睡它先前算好的时间TickerQSchedulerBackgroundService.cs:160。这台机器上两千次认领要 6–7 秒所以 S2 的两千次执行全部在到期后 6–7 秒才开始、并在接下来的半秒内跑完——这个迟到就是 S2 TickerQ 行的主体#3861。Hangfire计划作业躺在有序集合里直到DelayedJobScheduler——按SchedulePollingInterval轮询——把它移到Enqueued状态并把 id 放进队列。worker 取到 id 后从存储读回InvocationData、反序列化方法与其参数转到Processing跑过滤器管线经JobActivator激活类型静态方法无需激活反射调用再转Succeeded。每个状态转换都是一次带状态历史条目的存储写。Hangfire 没有单独的调度线程轮询与 worker 都是同一个 server 上的BackgroundProcess。Enqueue创建的作业完全跳过轮询——所以它有独立行HangfireEngine.cs。实现细节上引擎用IBackgroundJobClient与RecurringJobManager而非静态门面BackgroundJob/RecurringJob那些门面是JobStorage.Current查找其 client 缓存于一个把首个见到的存储绑死的Lazy里会让第一轮之后的每一轮都写进上一轮的存储HangfireEngine.cs。S1 的HangfireEnqueued行还刻意在测量窗口内才启动 server——否则已入队的作业会在批次尚未写完时就被 drain 大半S1_InMemoryThroughput.cs。结果在哪里看本 README 刻意不重复数字结果在 ../Quartz.Benchmark/README.md 中按日期归档的Against TickerQ and Hangfire (2026-09-19, AMD Ryzen 9 5950X)一节与仓库其余测量放在一处只有一个地方需要保持更新。该节也说明了测量环境BenchmarkDotNet v0.15.8AMD Ryzen 9 5950X 3.40 GHz、32 逻辑 / 16 物理核.NET 10PostgreSQL 15.1 Dockerloopback、shipped 持久化设置、预加载pg_stat_statements。核心结论如下该文档同样声明读Error列这在工作中的机器上跑安全读法是行与行之间的比值而非任何一行的绝对值Allocated与数据库普查不受负载影响可放心搬用S1 内存吞吐Quartzdefaults3.89–5.54 µs / 3.46 KBQuartztuned5.51–6.57 µs / 3.26 KBTickerQ 8.68–10.25 µs / 4.92–5.39 KBHangfirescheduled14.90–17.12 µs / 24.29–24.39 KBHangfireenqueued10.83–15.07 µs / 19.24 KB。Quartz 在这份负载上最快且分配最少——时间上约为 TickerQ 的 2×、Hangfire 的 3×字节上为 1.5× 与 7×tuned 档反而比 defaults 慢因为 20,000 个同时到期触发器的瓶颈是存储锁的争用而非获取轮数。S2 PostgreSQL 吞吐Quartzdefaults11.30–11.77 ms / 89.9 KBQuartztuned10.18–10.47 ms / 76.5 KBTickerQ 2.91–3.15 ms / 48.7–50.0 KBHangfire 15.78–15.84 ms / 101.5 KB。Quartz 此行输给 TickerQ 3.5–4×但 TickerQ 的行是迟到而非速率普查显示它首个执行在到期后 5.8–7.0 秒才开跑、全部 2,000 次在接下来半秒内完成。这里 tuned 比 defaults 快数据库上获取一轮是往返与提交摊薄到一批 10 个真实划算。S2 数据库普查每执行提交/写入/语句——Quartzdefaults6.00 / 3.00 / 26.00Quartztuned2.80 / 1.40 / 19.59TickerQ 2.01–2.02 / 1.00 / 1.96Hangfire 29.6–29.8 / 11.36 / 62.0–62.3。按作业完整生命周期窗口 ahead合算TickerQ 约 4 条语句、4 次提交、2 次写入Quartz tuned 2.8 次提交、1.4 次写入但语句量是 TickerQ 的 5 倍——Quartz 的语句多是三笔多语句事务 vs TickerQ 的两笔单语句事务。Hangfire 最大且约七分之一来自 server 空闲轮询每秒约 520 条语句。Quartz 的 6.0/3.0/26.0 是一次性作业最贵形态完成即删触发器、简单触发器行与孤儿作业明细#3802 在重复触发器上测的触发路径是 9.7 语句、1.24 提交。S3 调度到执行时延p50QuartzStartNow58–70 µsHangfireEnqueue94–235 µsTickerQ 空ExecutionTime14.7–14.9 msHangfireSchedule(TimeSpan.Zero)30.6–30.8 ms。Quartz 快出一个数量级。TickerQ 的即时路径不慢在分发而慢在被拾取它分发进TickerQTaskScheduler其 worker 在连续三次抢不到活后await Task.Delay(Math.Min(consecutiveStealFailures * 2, 50))退避空闲引擎上每个 worker 都在延迟里。Hangfire 的Schedule行受SchedulePollingInterval50 ms约束读数应理解为最多一个轮询间隔。S4 周期准确性100 个每秒调度 × 60 秒Quartz simple defaults 6,000/6,000、100% 落在 ±50 ms、最大偏差 15–23 msQuartz cron defaults 6,000/6,000、100%、15 msQuartz fire-ahead 1 s 5,999–6,000、98.2%、最大 999 ms#3861 复测后 cron 行为 21.8 msTickerQ 6,000/6,000、73–78% 在 ±50 ms、74–75 msHangfire 5,900–6,000、0–15% 在 ±50 ms、494–497 ms。三库都触发了正确次数差别在何时——fire-ahead 窗口是 Quartz 自己的权衡被明码标价它是吞吐行的正确设置、准时调度的错误设置一个部署不该两者兼得。S5 单条调度写入成本50,000 条逐 APITickerQAddAsync1.15–1.93 µs / 562–666 BICronTickerManager.AddAsync1.90–2.55 µs / 450 BHangfireAddOrUpdate6.88–7.98 µs / 5,158 BSchedule6.99–7.25 µs / 7,416 BQuartzScheduleJobsimple 6.85–8.22 µs / 3,378–3,514 Bcron 9.67–10.45 µs / 4,299–4,497 B。Quartz 此行输给 TickerQ 约 4.5 倍但三个调用存储的不是同一东西——Quartz 写作业明细 触发器Hangfire 写带序列化方法与参数的作业TickerQ 写一条命名编译期注册函数的 ticker没有作业可存且 Quartz 的数字是一次调度而非一次触发一个触发器写好可触发多年S1 才是触发的成本。无法验证的事项该文档的诚实边界pg_stat_statements默认关闭。普通postgres:15.1容器没有shared_preload_libraries--commits会输出未采集而非估算。TickerQ 在 SQLite 上未被测量。#3802 要求若 spike 发现可行才加 SQLite 行spike 未跑因为 PostgreSQL 场景加数据库普查已用尽预算且双写者的 SQLite 行问的是 SQLite 写锁而非调度器。列为后续事项。Hangfire 的周期行测的是与最近整秒的偏差而非与某个计划瞬间的偏差。周期作业被转成普通后台作业来源 occurrence 不传给作业且默认MisfireHandlingMode.Relaxed会在建作业前把 occurrence 改写为当前瞬间RecurringJobEntity.ScheduleNext——没有可迟到的计划瞬间。Quartz 与 TickerQ 的行用引擎交给作业的值IJobExecutionContext.ScheduledFireTimeUtc、TickerFunctionContext.ScheduledFor。S3 的 HangfireSchedule行与轮询相位锁定20 ms 静默对 50 ms 轮询稳定成固定相位p50 接近 30 ms 且几乎无散布——按受SchedulePollingInterval界定读别当精确数字。争用触发被哪个锁挡住在本 harness 中未建立——那是 #3802 D1 剖析为 Quartz 准备的题目本 harness 不剖析三库中的任何一方。这个 harness 暴露的发现记录而非处置Quartz 的非 durable 作业若挂大量触发器排空是二次方级。最初 S1 让一个作业明细挂全部 20,000 个触发器每次完成要移除一个触发器RAMJobStore.RemoveTriggerNoLock对该作业的触发器列表做线性ListTriggerWrapper.Remove再物化剩余触发器键成数组判定是否孤儿——20,000 个触发器时每次触发读 83 KB、二次方运行。场景现在改为独立单元一个作业明细对应一个触发器与另两库同形读 3.3–3.5 KB。README 认为这值得单独研究扇形扩展调度正是这种形状。TickerQ 的生成器编译不过voidticker 函数。[TickerFunction]挂在返回void的方法上会在TickerQInstanceFactory.g.cs产出无 return 的asynclambdaCS1643。所以本 harness 的计数作业返回Task。TickerQ 的 worker 池空闲时退避最多 50 ms。TickerQTaskScheduler的 worker 循环连续三次抢不到活后await Task.Delay(Math.Min(consecutiveStealFailures * 2, 50))。空闲引擎上每个 worker 都在延迟内即时路径分发的工作要等一个 worker 出来——S3 的 TickerQ 行就是这个组成的。TickerQ 在 EF Core 上同秒批次要晚认领耗时那么久才跑。此处约每 ticker 3 ms2,000 个同时到期会晚 6–7 秒20,000 个约晚一分钟。睡眠在认领前算好、认领后不再重算。S2 的 TickerQ 行就是这个等待除以 2,000。pg_stat_database.xact_commit在 PostgreSQL 15 上不是按窗口的仪器。backend 的提交最晚延迟 10 秒到达爆发后立刻读会少计爆发量。首个 S2 普查给出 TickerQ 每次执行 0.21 次提交真相是 2.0。Quartz 的提前触发窗口用准时性换批处理S4 为它定价。一秒窗口允许批次持有晚至一秒到期的触发器它们会在批次最早触发时间触发——所以 tuned 档在一秒周期上的最坏偏差约 1,000 ms而 defaults 档约 15–23 ms。两行都在 S4 表里。与 #3802 D2 规格的偏差SQLite 行没有原定仅当 spike 发现可行才取spike 未跑列为后续事项而非已发布表的缺口。Hangfire 的每调度行调用IBackgroundJobClient与RecurringJobManager而非静态门面门面的JobStorage.Current查找与Lazy缓存会让每轮迭代写进上一轮的存储。S3 同时报告 BenchmarkDotNet 的 P50/P95 与作业内时间戳的 P50/P95/P99BenchmarkDotNet 没有 P99 列发布的分位是作业内的那个——它止于作业开始处而非等待线程醒来的位置。S4 的 Quartz 行用默认档tuned 档是第三个独立行——用一秒窗口测准时性等于测窗口本身。S2 的 lead 是二十秒而非两秒Hangfire 的调度是并发的两千次往返顺序要约 16 秒EnsureScheduledBeforeDue会判失败而非容忍8 路并发把它带进窗内。Quartz 的两千作业明细 两千触发器批量写曾在十秒 lead 时触发一次该守卫所以 lead 定二十。调度属迭代 setup永远在测量窗口之外。S2 跑 5 次迭代而非 7 次每次都是两千次数据库往返、近一分钟。数字取自89fbadcdc2在 #3801 的 cron 快速路径之前S5、S2 普查与 Quartz 的 S4 cron 行已在936bf26e69为 #3861 复测cron 行没有移动。结语这套 harness 的方法论价值无论结果如何Quartz.Benchmark.Competitors最重要的遗产是它的方法论在被比较的作业内部计数、让一次基准调用等于等待 N 次执行、把可比的设置worker、轮询、批获取显式对齐并逐项声明、给 Quartz 发布多个配置档以对应不同部署、用数据库侧计数而非客户端秒表来回答持久化成本、并为每一个无法验证或偏离规格的角落留下书面记录。它回答的第三方对比只发布了作者赢的那一半而 ../Quartz.Benchmark/README.md 的结语强调这两个半场都被放在同一页上是有意的——输的行S2、S5与赢的行S1、S3、S4并列才是读者可以引用的完整证据。赞分享任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载相关推荐终极指南如何为GNOME桌面打造动态视频壁纸体验终极指南如何为GNOME桌面打造动态视频壁纸体验 在静态壁纸统治桌面环境的今天你是否曾幻想过让桌面活起来当传统的静态图片已经无法满足个性化需求动态桌桌面应用音视频SuperAGI性能基准测试与同类框架的执行效率对比SuperAGI性能基准测试与同类框架的执行效率对比 引言AI代理框架的性能瓶颈与突破方向 在AI代理框架快速发展的今天开发者面临一个关键挑战 如何在保AI Agent自主智能体后端RAGPhotino.NET轻量级跨平台桌面应用开发新方案Photino.NET轻量级跨平台桌面应用开发新方案 想象一下你正在构建一个现代化的桌面应用既想利用熟悉的Web技术栈又不希望应用体积臃肿、内存占用过高上一篇cloudflare-os 集成测试完整指南跨进程 Worker 只 mock 出站 HTTP 的 harness 搭建下一篇3分钟终极指南如何为国内开发者免费加速GitHub访问创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表