4. 扫描器架构与调度

DAST 扫描器不是“输入 URL 就出报告”的黑盒。当扫描目标从几个站点扩展到数百个域名、上千条业务路径时,真正决定扫描效率与稳定性的,是扫描引擎内部的组件划分和调度策略。本节把扫描器拆成可观察、可配置、可调度的部件,并说明任务队列、并发控制、分布式节点、资源配额与优先级如何协同工作。

一、扫描引擎的组件拆解

一个可调度的 DAST 引擎通常由六类组件构成,它们各自承担明确职责,并通过队列解耦:

  • 任务接入层(Scheduler API):接收扫描请求,校验目标归属、认证凭据、扫描策略模板,生成一条扫描任务记录,写入任务队列。它不执行实际扫描,只负责“登记与排队”。
  • 任务队列(Task Queue):持久化待执行任务,支持优先级、延迟重试、去重。常用 Redis Stream、RabbitMQ 或数据库表实现。
  • 调度器(Dispatcher):从队列取任务,按配额与节点负载选择执行节点,下发任务并跟踪心跳。
  • 扫描工作节点(Scanner Worker):真正执行爬虫、被动代理、主动 fuzz、漏洞验证的进程或容器。
  • 结果汇聚与去重(Result Aggregator):收集各节点发现,按“目标+参数+漏洞类型+证据指纹”去重,避免同一问题重复上报。
  • 状态与配额存储(State Store):记录任务状态、节点心跳、租户配额消耗、限速令牌。

这六类组件的连接关系与数据流向如下图所示。图中同时标出了任务从接入到结果汇聚的主链路,以及配额与心跳两条控制链路。

任务接入层 Scheduler API 任务队列 优先级 / 重试 / 去重 调度器 Dispatcher 扫描工作节点 爬虫 / 被动代理 主动 fuzz / 漏洞验证 Worker × N 结果汇聚与去重 Aggregator 状态与配额存储 State Store 发现结果 配额 / 限速 心跳 / 负载 图例 任务主链路 结果汇聚链路 配额控制链路 心跳与负载链路 说明:调度器依据配额与节点负载决定任务下发目标;工作节点通过心跳上报存活与并发占用。

二、任务队列与优先级设计

任务队列的核心不是“先进先出”,而是“按业务价值排序”。实践中通常把扫描任务划分为若干优先级,并映射到队列的不同消费通道:

优先级典型场景队列通道并发上限超时
P0 紧急生产环境高危漏洞复测、上线前阻断项queue:critical高短(如 30 min)
P1 高核心业务域名日常扫描queue:high中高中(如 2 h)
P2 中一般业务系统周期扫描queue:normal中长(如 6 h)
P3 低全量资产普查、历史遗留系统queue:low低最长(如 24 h)

队列实现上,建议为每个优先级设置独立队列,调度器按权重轮询消费,避免低优先级任务被“饿死”。同时,任务入队时需做幂等去重:以“目标 URL + 扫描策略 ID + 时间窗口”作为去重键,防止同一目标在短时间内被重复扫描,浪费节点资源。

三、并发控制与资源配额

并发控制要回答两个问题:单个目标允许多少并发请求,以及整个扫描集群允许多少并发任务。前者防止把目标打挂,后者防止集群过载。

对单个目标,常用令牌桶或漏桶限制请求速率。例如某业务系统约定“每秒不超过 20 个请求,单 IP 并发连接不超过 10”,则工作节点在发起请求前需从该目标的令牌桶取令牌,取不到则等待。对集群,调度器维护每个租户的配额,例如“租户 A 最多同时运行 5 个扫描任务,总并发请求数不超过 200”。配额消耗记录在状态存储中,任务结束或超时后释放。

配额设计建议:配额应同时覆盖“任务数”和“请求速率”两个维度。只限制任务数,可能出现单个任务疯狂发包拖垮目标;只限制速率,可能出现大量任务同时运行拖垮集群。

四、分布式扫描节点与调度策略

当单机无法承载扫描量时,需要部署多个扫描工作节点。节点可以是物理机、虚拟机或容器,每个节点注册自身能力标签,例如:

node_id: scanner-03
region: cn-east
capabilities: [crawler, passive-proxy, active-fuzz, auth-scan]
max_concurrency: 40
current_load: 12
heartbeat_at: 2025-01-15T10:23:41Z

调度器选择节点时,通常按以下顺序过滤:能力匹配(节点是否支持该扫描策略所需能力)→ 区域匹配(目标是否要求从特定区域发起)→ 负载最低(current_load / max_concurrency 最小)→ 配额允许(租户配额未超限)。若所有节点均不满足,任务重新入队并延迟重试,而不是直接失败。

节点故障处理依赖心跳机制:节点每 10~15 秒上报一次心跳,调度器若连续 3 个周期未收到心跳,则将该节点标记为不可用,并把其上运行的任务重新入队。为避免重复扫描,重新入队的任务需携带“已扫描路径”的断点信息,或由结果汇聚层按证据指纹去重。

五、一个可参考的调度参数示例

以下是一份示例配置,用于说明各参数如何协同。参数值属于合理假设,实际部署需根据目标承载能力和集群规模调整。

scheduler:
  poll_interval: 2s
  max_retry: 3
  retry_backoff: [30s, 2m, 10m]

queues:
  critical: { weight: 8, max_concurrency: 20 }
  high:     { weight: 4, max_concurrency: 15 }
  normal:   { weight: 2, max_concurrency: 10 }
  low:      { weight: 1, max_concurrency: 5  }

tenant_quota:
  default:
    max_running_tasks: 5
    max_requests_per_second: 200
    max_scan_duration: 6h

target_rate_limit:
  default:
    requests_per_second: 20
    max_connections: 10

按此配置,若租户同时提交 3 个 P0、5 个 P1、10 个 P2 任务,调度器会优先消费 critical 队列,但受租户 max_running_tasks=5 限制,最多同时运行 5 个任务;其余任务在队列中等待。每个运行中的任务对单目标限速 20 req/s,租户总请求速率上限 200 req/s,两者共同约束实际发包量。

六、结果观察与调度效果判断

调度是否合理,可以通过几个可观测指标判断:队列等待时间(任务从入队到开始执行的时长)、节点利用率(current_load / max_concurrency 的分布)、任务失败率与重试次数、单目标请求速率是否触发限速。若 P0 任务等待时间持续超过阈值,说明 critical 队列并发不足或租户配额过紧;若节点利用率长期低于 30%,说明调度权重或节点数量需要调整;若某目标频繁出现连接超时或 429 响应,说明目标限速参数过于激进,应下调 requests_per_second 或 max_connections。

常见误区:把“并发数”直接等同于“扫描速度”。并发过高会导致目标响应变慢、误报增多、甚至触发 WAF 封禁;并发过低则扫描周期过长。合理的做法是先做小流量探测,观察目标响应时间和错误率,再逐步提高并发,直到找到稳定拐点。

综上,DAST 扫描器的调度设计可以概括为:组件解耦、队列分级、配额双维、节点标签化、故障可重入。把这五件事落实到位,扫描集群才能在业务增长时保持可控、可观测、可扩展。