23 KiB
数据采集系统 (Collectors)
一、系统架构
┌─────────────────────────────────────────────────────────────────┐
│ 数据采集系统架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ TOP500 │ │ Epoch AI │ │ HuggingFace │ │
│ │ 采集器 │ │ 采集器 │ │ 采集器 │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └───────────────────┼───────────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ BaseCollector │◄── 基类 (统一处理) │
│ │ run() 方法 │ │
│ └─────────┬───────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ fetch() │ │transform()│ │ _save_data│ │
│ │ 获取原始数据 │ │ 数据转换 │ │ 保存到DB │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ CollectedData 表 │◄── 统一存储 │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Scheduler (APScheduler) │ │
│ │ 定时任务调度: 每4小时/6小时/12小时/1天 自动执行 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
二、工作流程 (Pipeline)
# 1. Scheduler 触发 (定时 或 手动触发)
# ↓
# 2. run() 方法执行完整流水线
async def run(self, db):
# 2.1 检查采集器是否启用
if not collector_registry.is_active(self.name):
return {"status": "skipped"}
# 2.2 记录任务开始
task = CollectionTask(status="running")
db.add(task)
await db.commit()
# 2.3 FETCH - 获取原始数据 (由子类实现)
raw_data = await self.fetch()
# 2.4 TRANSFORM - 转换为统一格式
data = self.transform(raw_data)
# 2.5 SAVE - 保存到数据库
records_count = await self._save_data(db, data)
# 2.6 记录任务完成
task.status = "success"
task.records_processed = records_count
await db.commit()
核心文件: backend/app/services/collectors/base.py
三、采集器列表
| 采集器 | 数据类型 | 数据内容 | 采集频率 |
|---|---|---|---|
| TOP500 | supercomputer | 全球超级计算机排名 (算力、性能) | 4小时 |
| Epoch AI | gpu_cluster | GPU算力集群信息 | 6小时 |
| HuggingFace Models | model | AI模型信息 | 12小时 |
| HuggingFace Datasets | dataset | 数据集信息 | 12小时 |
| HuggingFace Spaces | space | Demo应用 | 1天 |
| PeeringDB | ixp/network/facility | 互联网交换点/网络/机房 | 1-2天 |
| TeleGeography | submarine_cable | 海底光缆信息 | 7天 |
| Space-Track TLE | satellite_tle | 卫星轨道 TLE 数据 | 依采集器配置 |
| BarentsWatch AIS | vessel | 船只位置、航速、航向、MMSI 等 AIS 数据 | 依采集器配置 |
| AISStream Vessels | vessel_ais | AIS WebSocket 实时流,写入原始观测层并由聚合接口展示 | 依采集器配置 |
AIS 船只类采集器和其它 CollectedData 采集器的落库路径不同。BarentsWatch、AISStream 和自定义 vessel_ais 源都会进入 AIS 原始观测层,随后由聚合服务合并成 Earth 船只图层使用的 GeoJSON 和详情数据。这样做可以保留来源、传输方式、字段冲突和观测时间,避免某个实时源直接覆盖最终展示表。
Earth 国界不再属于采集器体系。它是 Earth 静态渲染资产,由控制台 运维与配置 -> Earth 内容 -> 国界精度 维护源配置,并由 /api/v1/earth/boundaries/* 构建 frontend/public/earth/data/boundaries/earth-boundaries-china-pov-v1.pmtiles。本地没有高精 PMTiles 时,前端会使用仓库内置的低精度 GeoJSON 作为 fallback,不会向 CollectedData 写入国界记录。
TOP500 和 Epoch AI 算力数据的公开源不总是提供可用经纬度。Earth 统一算力中心接口在主地图启动链路中只使用源数据自带坐标或 compute_center_locations 维表坐标;缺少坐标的记录会进入 unresolved,不会通过本地注册表、国家质心或猜测城市自动渲染。用户手动采集候选时,后端会用源字段调用 ROR 组织注册 API 和 Nominatim/OpenStreetMap 在线搜索;候选经前端保存后写入 compute_center_locations,后续地图刷新再从维表渲染。
四、数据格式 (统一存储到 CollectedData 表)
# 每个采集器 parse_response() 返回格式
{
"source_id": "top500_1", # 原始系统ID (必填)
"name": "El Capitan", # 名称 (必填)
"description": "系统描述...", # 描述
"country": "United States", # 国家
"city": "Livermore, CA", # 城市
"latitude": "37.6819", # 纬度 (字符串)
"longitude": "-121.7681", # 经度 (字符串)
"value": "1742.00", # 性能值 (如算力)
"unit": "PFlop/s", # 单位
"metadata": { # 额外数据 (JSON)
"rank": 1,
"r_peak": 2746.38,
"cores": 11039616
},
"reference_date": "2025-11-01" # 数据参考日期
}
五、数据库表结构
CollectedData 表 (collected_data)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | SERIAL | 主键 |
| source | VARCHAR(100) | 数据源名称 (top500, huggingface等) |
| source_id | VARCHAR(100) | 原始数据ID |
| data_type | VARCHAR(50) | 数据类型 (supercomputer, model等) |
| name | VARCHAR(500) | 名称 |
| title | VARCHAR(500) | 标题 |
| description | TEXT | 描述 |
| country | VARCHAR(100) | 国家 |
| city | VARCHAR(100) | 城市 |
| latitude | VARCHAR(50) | 纬度 |
| longitude | VARCHAR(50) | 经度 |
| value | VARCHAR(100) | 性能值 |
| unit | VARCHAR(20) | 单位 |
| metadata | JSONB | 额外元数据 |
| collected_at | TIMESTAMP | 采集时间 |
| reference_date | TIMESTAMP | 数据参考日期 |
| is_valid | INTEGER | 是否有效 |
核心文件: backend/app/models/collected_data.py
六、TOP500 采集器示例 (完整流程)
# 1. fetch() - 从网页获取HTML
async def fetch(self):
url = "https://top500.org/lists/top500/list/2025/11/"
response = await client.get(url)
return response.text # 返回HTML
# 2. parse_response() - 解析HTML为统一格式
def parse_response(self, html):
soup = BeautifulSoup(html, "html.parser")
table = soup.find("table")
for row in table.find_all("tr")[1:]: # 跳过表头
cells = row.find_all("td")
entry = {
"source_id": f"top500_{cells[0].text}", # "top500_1"
"name": cells[1].text.strip(), # "El Capitan"
"country": cells[2].text.strip(), # "United States"
"city": "", # 城市
"latitude": "", # 需进一步解析
"longitude": "",
"value": "1742.00", # Rmax
"unit": "PFlop/s",
"metadata": {
"rank": 1,
"cores": "11340000"
},
"reference_date": "2025-11-01"
}
data.append(entry)
return data
# 3. run() 自动调用 _save_data() 保存到数据库
核心文件: backend/app/services/collectors/top500.py
七、调度机制
# 启动时注册所有采集器到定时任务
def start_scheduler():
for name, collector in collectors.items():
if collector_registry.is_active(name):
scheduler.add_job(
run_collector_task,
trigger=IntervalTrigger(hours=collector.frequency_hours),
id=name,
name=name
)
| 采集器 | 采集频率 |
|---|---|
| TOP500 | 每4小时 |
| Epoch AI | 每6小时 |
| HuggingFace | 每12小时 |
| PeeringDB | 每1-2天 |
| TeleGeography | 每7天 |
核心文件: backend/app/services/scheduler.py
成功采集与连接状态
内置采集器成功采集后,调度器会记录当前有效配置已通过连接验证:
if datasource.last_status == "success":
effective_candidate = await get_builtin_effective_candidate(db, datasource.source)
checksum, _ = await build_builtin_connectivity_checksum(...)
await save_connectivity_success(
db,
datasource.source,
checksum,
{"status_code": None},
connected_by="collection",
)
这个记录用于控制台“采集管理 -> 采集器”中的连接状态判断:如果当前配置和成功采集时的 checksum 一致,就视为已连接,不要求用户再手动点击连接按钮。只有 endpoint、请求头、基础配置或凭证指纹变化时,才需要重新验证。
采集管理与快照
Admin Next 的采集管理入口按业务层级组织:
采集器:配置 endpoint、认证方式、请求头、基础参数、启用状态和凭证教程。采集调度:查看和调整调度状态,触发、停止或刷新采集任务。采集历史 / 快照:按采集器聚合展示历史,详情中用快照选择器查看同一采集器的不同版本。
快照列表不应把同一个采集器的每次快照都拍平成独立主列表项。主列表负责选择采集器,详情区负责时间版本切换。
凭证教程由 backend/app/services/credential_guides.py 维护,控制台通过 read / generate / reset 三个动作读取或生成 Markdown 教程。教程面向运维配置人员,前端应渲染 Markdown 正文,而不是展示生成 prompt 或原始元数据。
相关实现见:
八、相关代码文件
backend/app/services/collectors/
├── base.py # 基类: run() 流水线, _save_data() 保存
├── registry.py # 采集器注册表
├── scheduler.py # 定时任务调度 (APScheduler)
├── top500.py # TOP500采集器
├── epoch_ai.py # Epoch AI采集器
├── huggingface.py # HuggingFace采集器
├── peeringdb.py # PeeringDB采集器
├── telegeraphy.py # TeleGeography海底光缆采集器
├── vessel_ais.py # BarentsWatch AIS 船只采集器
├── aisstream.py # AISStream WebSocket 船只采集器
└── earth_boundaries.py # Earth 国界源校验和静态瓦片 artifact 采集器
backend/app/services/
├── custom_datasource_runtime.py # 自定义 REST / WebSocket 映射运行时
├── datasource_mapping.py # 确定性字段映射与目标写入
├── vessel_ais_aggregation.py # AIS 原始观测写入与聚合读取
├── vessel_aggregation_strategy.py # 多源字段选择、freshness fallback 和冲突记录
└── vessel_enrichment.py # 船舶资料富化缓存
backend/app/models/
├── collected_data.py # 统一数据模型
└── vessel_enrichment.py # 船舶富化结果缓存
九、凭证型采集器
部分采集器需要外部服务凭证,例如:
| 采集器 | credential provider | 凭证来源 |
|---|---|---|
barentswatch_vessels |
barentswatch |
控制台“采集管理 -> 采集器”、环境变量、~/.zshrc |
aisstream_vessels |
aisstream |
控制台“采集管理 -> 采集器”、环境变量、~/.zshrc(连接验证可读;正式采集建议保存到“采集管理 -> 采集器”或注入后端环境) |
spacetrack_tle |
spacetrack |
环境变量、~/.zshrc |
BarentsWatch AIS
BarentsWatch AIS 的凭证解析统一在:
VesselAISCollector 只负责采集和转换 AIS 数据,不再自己读取环境变量或拼 token 请求。它通过:
resolve_barentswatch_config()fetch_barentswatch_access_token()
获取运行时配置。
解析优先级:
DataSourceConfig.auth_configDataSourceConfig.config- 环境变量
~/.zshrc
支持变量:
export BARENTSWATCH_CLIENT_ID="..."
export BARENTSWATCH_CLIENT_SECRET="..."
并兼容历史拼写:
export BARRENTSWATCH_CLIENT_ID="..."
export BARRENTSWATCH_CLIENT_SECRET="..."
连接验证会先请求 https://id.barentswatch.no/connect/token 获取 scope=ais 的 access token,再用 Authorization: Bearer <token> 请求 AIS endpoint。
AISStream 实时船舶
AISStream 使用 wss://stream.aisstream.io/v0/stream WebSocket endpoint。默认运行方式是长连接实时采集,而不是传统 REST collector 的“请求一次、进度到 100%、完成”模型。
运行时配置:
api_key:优先从DataSourceConfig.auth_config.api_key或config.api_key读取;也可由后端进程环境变量AISSTREAM_API_KEY提供。bounding_boxes:AISStream 订阅范围,默认示例为全球[[[-90, -180], [90, 180]]],生产或演示建议先缩小区域。message_types:默认PositionReport和ShipStaticData。streaming_enabled:默认启用长连接;关闭后回退到批次式fetch -> transform -> save。streaming_max_messages:测试用上限,非 0 时收到指定消息数后停止。reconnect_delay_seconds、receive_timeout_seconds:控制断线重连和空闲等待。
状态语义:
connecting:正在连接 AISStream。streaming:持续接收实时消息,records_processed表示已见消息数,通常没有固定总量和百分比。reconnecting:上游断开或网络异常,采集器记录AISSourceHealth后等待重连。stopped/cancelled:任务被测试上限或用户停止。
AISStream 连接验证会通过 datasource_connectivity.py 读取保存的采集器配置、环境变量和 ~/.zshrc 中的 AISSTREAM_API_KEY。正式采集时,最稳妥的方式是把 API Key 保存到“采集管理 -> 采集器 -> AISStream 实时船舶”;如果只放在 ~/.zshrc,需要确认后端进程实际继承到了该环境变量。
控制台通过 /datasources -> 实时流 管理 AISStream,而不是把它放进普通有限采集任务的进度条。实时流 API 会聚合运行态、健康状态、配置摘要和 raw observation 计数:
GET /api/v1/realtime-sources
POST /api/v1/realtime-sources/{source}/start
POST /api/v1/realtime-sources/{source}/stop
POST /api/v1/realtime-sources/{source}/restart
aisstream_vessels 和自定义 source_type=websocket 数据源会出现在该接口中。它们不参与一键采集百分比;前端按长连接服务展示消息计数、延迟、最近成功和最近错误。
AIS 原始观测与聚合
AIS 观测写入后不会直接替换最终船只记录,而是先保存为 raw observation:
source记录来源,例如barentswatch_vessels、aisstream_vessels或自定义源名称。delivery_mode表达实时性,realtime_stream优先于polling。transport记录websocket或http。- 位置、速度、航向等动态字段会按 freshness 和来源优先级选择。
- 静态字段优先保留非空值;冲突候选会记录到详情接口,便于排查多源差异。
Earth 船只展示现在使用受控快照接口和实时增量通道:
GET /api/v1/vessels/snapshot?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000
GET /api/v1/visualization/vessels/{mmsi}
GET /api/v1/visualization/vessels/{mmsi}/track
GET /api/v1/visualization/vessels/{mmsi}/conflicts
/api/v1/vessels/snapshot 必须携带 bbox 和 zoom,默认 limit=1000,最大 limit=5000。它优先消费 ais_raw_observations 聚合结果;当当前 raw 窗口为空时,会受控回退到 legacy vessel_position / vessel_static 最新点,并在 diagnostics.legacy_fallback_used 中标明。旧 /api/v1/visualization/geo/vessels 路由已移除。
实时增量通过 /ws 的 vessels channel 推送。客户端订阅时必须带当前视口:
{
"type": "subscribe",
"data": {
"channel": "vessels",
"bbox": [120.8, 30.7, 122.1, 31.8],
"zoom": 12,
"limit": 1000
}
}
后端按连接保存轻量订阅条件,只向 bbox 命中的连接发送船只更新。collector 广播会先进入 1 秒节流队列,同一 MMSI 在一个 flush 周期内只保留最新位置,避免高频实时流拖垮 WebSocket。
图层接口与全量统计分离
Earth 后续迁移到两类接口:
GET /api/v1/data-products
GET /api/v1/data-products/{product_id}/status
GET /api/v1/layers/vessels/snapshot?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000
GET /api/v1/layers/cables?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000
GET /api/v1/layers/landing-points?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000
GET /api/v1/layers/satellites?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000
GET /api/v1/layers/bgp/anomalies?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000
GET /api/v1/layers/bgp/incidents?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000
GET /api/v1/layers/bgp/collectors?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000
/api/v1/data-products/* 面向聚合面板,统计口径是全量/全局,不受地图 bbox 影响。/api/v1/layers/* 面向地图渲染,必须携带 bbox 和 zoom,默认 limit=1000,最大 limit=5000;低 zoom 会降级到更小的返回上限,并在 diagnostics 中暴露 degraded、truncated、limit_clamped 和 stats_scope=viewport。当前版本的非船只图层先复用已有 GeoJSON 转换再做保护层,后续可继续把 bbox 下推到各产品专用查询。
十、采集器设置与连接验证
控制台的“采集管理 -> 采集器”页提供所有内置采集器的 endpoint、请求头、超时、重试和凭证配置。连接验证不是只看前端按钮状态,而是由后端计算 checksum:
- endpoint
- auth type
- headers
- config
- credential provider
- 凭证指纹
相关 API:
GET /api/v1/datasources/configs/all
POST /api/v1/datasources/configs/builtin/connection-status
POST /api/v1/datasources/configs/builtin/connect
POST /api/v1/settings/integrations/barentswatch/connect
GET /api/v1/settings/credential-guides/{provider}
POST /api/v1/settings/credential-guides/{provider}/generate
POST /api/v1/settings/credential-guides/{provider}/reset
更多细节见:
十一、数据使用场景
采集的数据最终会:
- 可视化展示 - 在UE5大屏上显示超级计算机、GPU集群、海底光缆的地理位置
- 态势分析 - 统计全球算力分布、增长趋势
- 告警系统 - 检测重要节点变化
十二、采集器注册机制
采集器在应用启动时自动注册:
# backend/app/services/collectors/__init__.py
collector_registry.register(TOP500Collector())
collector_registry.register(EpochAIGPUCollector())
collector_registry.register(HuggingFaceModelCollector())
collector_registry.register(HuggingFaceDatasetCollector())
collector_registry.register(HuggingFaceSpacesCollector())
collector_registry.register(PeeringDBIXPCollector())
collector_registry.register(PeeringDBNetworkCollector())
collector_registry.register(PeeringDBFacilityCollector())
collector_registry.register(TeleGeographyCableCollector())
collector_registry.register(TeleGeographyLandingPointCollector())
collector_registry.register(TeleGeographyCableSystemCollector())
核心文件: backend/app/services/collectors/registry.py
十三、触发采集
方式一:定时触发
系统启动时,APScheduler会自动根据各采集器的frequency_hours设置定时任务。
方式二:手动触发 API
# 触发TOP500采集
curl -X POST http://localhost:8000/api/v1/datasources/1/trigger \
-H "Authorization: Bearer <token>"
批量采集使用:
POST /api/v1/datasources/trigger-batch
请求体可以传 source_ids 精确触发选中项;如果不传 source_ids,后端按 product、module、is_active、run_status、collected、credential_status 和 q 过滤后触发。接口会跳过禁用源、正在运行且未 force 的源,以及未到频率窗口的源,并返回 triggered、skipped、failed 三组结果。
核心文件: backend/app/api/v1/datasources.py