4. 扫描器架构与调度
DAST 扫描器不是“输入 URL 就出报告”的黑盒。当扫描目标从几个站点扩展到数百个域名、上千条业务路径时,真正决定扫描效率与稳定性的,是扫描引擎内部的组件划分和调度策略。本节把扫描器拆成可观察、可配置、可调度的部件,并说明任务队列、并发控制、分布式节点、资源配额与优先级如何协同工作。
一、扫描引擎的组件拆解
一个可调度的 DAST 引擎通常由六类组件构成,它们各自承担明确职责,并通过队列解耦:
- 任务接入层(Scheduler API):接收扫描请求,校验目标归属、认证凭据、扫描策略模板,生成一条扫描任务记录,写入任务队列。它不执行实际扫描,只负责“登记与排队”。
- 任务队列(Task Queue):持久化待执行任务,支持优先级、延迟重试、去重。常用 Redis Stream、RabbitMQ 或数据库表实现。
- 调度器(Dispatcher):从队列取任务,按配额与节点负载选择执行节点,下发任务并跟踪心跳。
- 扫描工作节点(Scanner Worker):真正执行爬虫、被动代理、主动 fuzz、漏洞验证的进程或容器。
- 结果汇聚与去重(Result 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。
综上,DAST 扫描器的调度设计可以概括为:组件解耦、队列分级、配额双维、节点标签化、故障可重入。把这五件事落实到位,扫描集群才能在业务增长时保持可控、可观测、可扩展。