城市供水管网智能监控系统------演示程序详细需求规格说明书
文档信息
| 项目 | 内容 |
|---|---|
| 文档名称 | 城市供水管网智能监控系统------演示程序详细需求规格说明书 |
| 版本 | V1.0 |
| 编制日期 | 2026年7月 |
| 编制依据 | 《传统Hadoop大数据技术入门:从数据源到用户界面的全链路实践指南》及《补充与进阶》两书所述技术架构 |
| 文档性质 | 演示程序(Demo)需求规格说明书 |
1. 引言
1.1 项目背景
城市供水管网是城市生命线工程的核心组成部分。传统供水管网管理依赖人工巡检和SCADA系统,存在数据孤岛严重、实时监控能力弱、故障发现滞后等痛点。
本演示程序旨在模拟 城市供水系统大数据采集、ETL加工到大屏展示的完整技术链路,验证两本《传统Hadoop大数据技术入门》图书中阐述的技术架构在智慧水务场景下的可行性。
说明 :本系统为演示程序(Demo) ,所有数据均为程序模拟生成,不对接真实硬件设备。
1.2 项目目标
- 技术验证:验证从数据采集(Kafka)→ 流式入湖(Spark Streaming)→ 离线ETL(Spark SQL)→ 服务导出(HBase/ClickHouse)→ 大屏展示(React/Vue)的全链路可行性。
- 场景模拟 :模拟城市供水管网中管网节点(阀门/泵站) 的实时状态采集、状态展示、故障标识与规则预警。
- 知识呈现:以可视化方式呈现大数据技术栈各组件在真实业务场景中的协作关系。
1.3 适用范围
本说明书适用于演示程序的需求分析、系统设计、编码实现与测试验收全流程。
1.4 参考文献
| 编号 | 文献名称 |
|---|---|
| 1 | 《传统Hadoop大数据技术入门:从数据源到用户界面的全链路实践指南》 |
| 2 | 《传统Hadoop大数据技术入门:补充与进阶------从数据格式到智能决策的问答实录》 |
| 3 | GB/T 国家标准------城镇供水管网智能化总体架构 |
2. 总体描述
2.1 系统全景架构图
text
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 城市供水管网智能监控系统 ------ 全链路架构 │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 【展示层】大屏 UI │ │
│ │ React/Vue 大屏应用 │ │
│ │ • 管网拓扑图(模拟GIS,显示各节点状态) │ │
│ │ • 实时数据面板(压力/流量/流速) │ │
│ │ • 故障列表与预警弹窗 │ │
│ └─────────────────────────────────┬───────────────────────────────────────┘ │
│ │ HTTP / WebSocket │
│ ┌─────────────────────────────────┴───────────────────────────────────────┐ │
│ │ 【服务层】Spring Boot API │ │
│ │ • 大屏聚合数据接口(查ClickHouse) │ │
│ │ • 节点详情点查接口(查HBase) │ │
│ │ • WebSocket实时推送接口 │ │
│ └───────────────┬─────────────────────────┬───────────────────────────────┘ │
│ │ │ │
│ ┌───────────────┴───────────┐ ┌──────────┴──────────────────────────────┐ │
│ │ ClickHouse │ │ HBase │ │
│ │ (DWS/ADS聚合查询) │ │ (DWD明细点查) │ │
│ │ • 按小时/区域聚合统计 │ │ • RowKey = 节点ID+时间戳 │ │
│ │ • 故障率/平均流速趋势 │ │ • 存储节点详细状态历史 │ │
│ └───────────────┬───────────┘ └──────────┬──────────────────────────────┘ │
│ │ │ │
│ └─────────────┬───────────┘ │
│ │ │
│ ┌─────────────────────────────┴───────────────────────────────────────────┐ │
│ │ 【计算层】Spark SQL (离线ETL) │ │
│ │ • 调度器(Airflow)每日凌晨触发 │ │
│ │ • ODS → DWD(清洗:过滤异常值、类型转换) │ │
│ │ • DWD → DWS(聚合:按小时/区域统计平均压力、流量、故障次数) │ │
│ │ • DWS → ADS(导出到HBase/ClickHouse) │ │
│ └─────────────────────────────┬───────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────┴───────────────────────────────────────────┐ │
│ │ 【元数据层】Hive Metastore │ │
│ │ • 外部表: ods_pipe_raw(映射HDFS原始数据) │ │
│ │ • 内部表: dwd_pipe_detail, dws_pipe_hourly │ │
│ └─────────────────────────────┬───────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────┴───────────────────────────────────────────┐ │
│ │ 【存储层】HDFS (数据湖) │ │
│ │ /data/raw/pipe/dt=2026-07-16/hour=14/ (Parquet格式) │ │
│ │ /warehouse/dwd_pipe_detail/dt=.../ (Parquet格式) │ │
│ │ /warehouse/dws_pipe_hourly/dt=.../ (Parquet格式) │ │
│ └─────────────────────────────┬───────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────┴───────────────────────────────────────────┐ │
│ │ 【流式写入层】Spark Structured Streaming │ │
│ │ • 从Kafka实时消费传感器数据 │ │
│ │ • 解析JSON,提取dt/hour分区字段 │ │
│ │ • partitionBy("dt","hour")写入HDFS │ │
│ │ • 每5分钟一个微批次 │ │
│ └─────────────────────────────┬───────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────┴───────────────────────────────────────────┐ │
│ │ 【缓冲层】Kafka │ │
│ │ Topic: pipe-sensor-topic (模拟传感器数据) │ │
│ └─────────────────────────────┬───────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────┴───────────────────────────────────────────┐ │
│ │ 【数据源】模拟传感器数据生成器 (Java Producer) │ │
│ │ • 模拟100个管网节点(阀门/泵站/监测点) │ │
│ │ • 每秒生成一条JSON格式的状态数据 │ │
│ │ • 字段:节点ID、压力(MPa)、流量(m³/h)、流速(m/s)、阀门开度(%)、状态 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
2.2 数据流全景图
text
【全链路数据流】传感器模拟 → Kafka → HDFS → ETL → 服务层 → 大屏
时间轴:实时流 + 每日批处理
═══════════════════════════════════════════════════════════════════════════════════
【实时流】每5分钟一个微批次
T0 (每秒) 模拟传感器生成JSON → Kafka (pipe-sensor-topic)
│
│ 数据示例:
│ {"nodeId":"N001","pressure":0.45,"flow":120.5,
│ "velocity":1.2,"valveOpen":85,"status":"OPEN",
│ "timestamp":1721123456789}
│
T0+5min Spark Streaming 拉取微批次
│
│ ① 解析JSON,提取 dt='2026-07-16', hour='14'
│ ② 写入HDFS:
│ /data/raw/pipe/dt=2026-07-16/hour=14/part-0000.parquet
│
│ ③ 同时,向Hive Metastore注册新分区(或由调度器统一处理)
│
═══════════════════════════════════════════════════════════════════════════════════
【离线批处理】每日凌晨触发
次日 01:00 Airflow 触发 Task 1: ODS → DWD (清洗)
│
│ Spark SQL 读取昨日所有小时分区
│ ① 过滤:pressure < 0 或 > 1.5 标记为异常
│ ② 类型转换:字符串 → Double/Timestamp
│ ③ 写入 Parquet: /warehouse/dwd_pipe_detail/dt=2026-07-16/
│
次日 01:30 Airflow 触发 Task 2: DWD → DWS (聚合)
│
│ Spark SQL 读取 DWD 明细
│ ① 按 hour, region 聚合:
│ AVG(pressure), AVG(flow), COUNT(异常事件)
│ ② 写入 Parquet: /warehouse/dws_pipe_hourly/dt=2026-07-16/
│
次日 02:00 Airflow 触发 Task 3: 导出到 HBase + ClickHouse
│
│ ① 将 DWD 明细(含故障标识)导出到 HBase
│ RowKey = nodeId + 时间戳
│ ② 将 DWS 聚合结果导出到 ClickHouse
│
次日 02:30 Airflow 触发 Task 4: 规则预警检测 (独立DQC任务)
│
│ ① 检测规则1:某节点连续30分钟无数据 → 判定"离线"
│ ② 检测规则2:某节点压力波动 > 30% → 判定"压力异常"
│ ③ 检测规则3:某区域故障率 > 5% → 判定"区域预警"
│ ④ 将预警结果写入预警表,推送到大屏
│
═══════════════════════════════════════════════════════════════════════════════════
【查询服务】用户请求触发
用户打开大屏 前端 → Spring Boot API
│
│ ① 大屏聚合数据 → 查 ClickHouse (亚秒级)
│ SELECT hour, AVG(pressure), COUNT(fault)
│ FROM dws_pipe_hourly WHERE dt=today()
│
用户点击节点 前端 → Spring Boot API
│
│ ② 节点详情 → 查 HBase (毫秒级)
│ Get rowKey = "N001_2026-07-16_14"
│
用户看到预警 前端 WebSocket 实时推送
│
│ ③ 预警事件 → 从预警表推送至大屏弹窗
│
═══════════════════════════════════════════════════════════════════════════════════
2.3 用户角色
| 角色 | 描述 | 主要操作 |
|---|---|---|
| 运维值班员 | 监控中心值班人员 | 查看大屏、接收预警、查看节点详情 |
| 系统管理员 | 大数据平台运维 | 监控数据链路、管理调度任务 |
| 数据分析师 | (演示程序暂不涉及) | --- |
2.4 业务场景
| 场景编号 | 场景名称 | 描述 |
|---|---|---|
| S01 | 管网全景监控 | 大屏展示所有管网节点状态(开启/关闭/流速),模拟管网拓扑图 |
| S02 | 实时数据查看 | 查看各节点的实时压力、流量、流速数据 |
| S03 | 故障标识 | 节点出现异常(压力过高/过低、断连)时,在大屏上以红色/闪烁标识 |
| S04 | 规则预警 | 基于预设规则自动检测异常并触发预警弹窗 |
| S05 | 历史趋势查看 | 查看某节点/区域的历史数据趋势(近24小时/近7天) |
3. 具体需求
3.1 数据源层需求
3.1.1 模拟传感器数据生成器
需求描述 :系统应包含一个模拟数据生成器,模拟城市供水管网中100个节点的传感器数据,持续写入Kafka。
数据格式(JSON):
json
{
"nodeId": "N001",
"nodeName": "人民路-解放路交叉口阀门",
"nodeType": "VALVE", // VALVE | PUMP | MONITOR
"region": "华东区",
"pressure": 0.45, // 单位: MPa (兆帕)
"flow": 120.5, // 单位: m³/h (立方米/小时)
"velocity": 1.2, // 单位: m/s (米/秒)
"valveOpen": 85, // 单位: % (百分比), 仅阀门有效
"status": "OPEN", // OPEN | CLOSED | FAULT
"temperature": 18.5, // 单位: °C
"timestamp": 1721123456789 // 毫秒级时间戳
}
生成逻辑:
- 100个节点中,80个为阀门(VALVE),10个为泵站(PUMP),10个为监测点(MONITOR)。
- 正常数据:压力 0.3~0.6 MPa 正态分布,流速 0.5~2.0 m/s。
- 异常注入(演示预警能力):随机以5%概率注入异常值(压力突降到0.05或飙升到1.2,流速归零等)。
- 每秒生成100条记录(每个节点1条/秒)。
输出目标 :Kafka Topic pipe-sensor-topic,3个分区。
伪代码:
java
// 模拟传感器数据生成器
public class PipeSensorSimulator {
public static void main(String[] args) {
Properties props = new Properties();
props.put("bootstrap.servers", "kafka:9092");
props.put("key.serializer", "StringSerializer");
props.put("value.serializer", "StringSerializer");
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
List<Node> nodes = generateNodes(100); // 生成100个节点
while (true) {
for (Node node : nodes) {
String data = generateSensorData(node); // 含5%异常注入
producer.send(new ProducerRecord<>("pipe-sensor-topic", data));
}
Thread.sleep(1000); // 每秒一轮
}
}
}
3.2 数据采集与缓冲层需求
3.2.1 Kafka 消息队列
| 配置项 | 值 | 说明 |
|---|---|---|
| Topic名称 | pipe-sensor-topic |
传感器原始数据 |
| 分区数 | 3 | 按 nodeId hash 分区 |
| 副本数 | 1 | 演示环境单副本 |
| 保留时间 | 7天 | --- |
| 消息格式 | JSON | 见3.1.1 |
3.3 流式写入层需求
3.3.1 Spark Structured Streaming 作业
需求描述 :从Kafka实时消费传感器数据,解析后写入HDFS原始层(ODS),按dt/hour分区存储为Parquet格式【参见第一本书第2章】。
核心配置:
| 配置项 | 值 | 说明 |
|---|---|---|
| 触发间隔 | 5分钟 | Trigger.ProcessingTime("5 minutes") |
| 存储格式 | Parquet | 列式存储 |
| 分区字段 | dt, hour | 自动创建子目录 |
| 文件大小控制 | maxRecordsPerFile=100000 |
防止小文件 |
| Checkpoint路径 | /checkpoint/pipe_streaming/ |
Exactly-Once语义保障 |
伪代码:
java
// Spark Structured Streaming: Kafka → HDFS
public class PipeStreamingToHdfs {
public static void main(String[] args) throws Exception {
SparkSession spark = SparkSession.builder()
.appName("PipeStreaming")
.master("yarn")
.getOrCreate();
Dataset<Row> df = spark.readStream()
.format("kafka")
.option("kafka.bootstrap.servers", "kafka:9092")
.option("subscribe", "pipe-sensor-topic")
.option("startingOffsets", "latest")
.load();
Dataset<Row> pipeDF = df
.selectExpr("CAST(value AS STRING) as json_str")
.selectExpr(
"json_str",
"get_json_object(json_str, '$.nodeId') as node_id",
"CAST(get_json_object(json_str, '$.pressure') AS DOUBLE) as pressure",
"CAST(get_json_object(json_str, '$.flow') AS DOUBLE) as flow",
"CAST(get_json_object(json_str, '$.velocity') AS DOUBLE) as velocity",
"get_json_object(json_str, '$.status') as status",
"CAST(get_json_object(json_str, '$.timestamp') AS BIGINT) as ts",
"from_unixtime(cast(get_json_object(json_str, '$.timestamp') as bigint)/1000, 'yyyy-MM-dd') as dt",
"from_unixtime(cast(get_json_object(json_str, '$.timestamp') as bigint)/1000, 'HH') as hour"
);
StreamingQuery query = pipeDF
.writeStream()
.outputMode(OutputMode.Append())
.format("parquet")
.partitionBy("dt", "hour")
.option("path", "hdfs://namenode:8020/data/raw/pipe/")
.option("maxRecordsPerFile", 100000)
.option("checkpointLocation", "hdfs://namenode:8020/checkpoint/pipe_streaming/")
.trigger(Trigger.ProcessingTime("5 minutes"))
.start();
query.awaitTermination();
}
}
3.4 元数据管理层需求
3.4.1 Hive Metastore 表定义
ODS层(外部表) :映射HDFS原始数据目录。
sql
CREATE EXTERNAL TABLE IF NOT EXISTS ods_pipe_raw (
json_str STRING,
node_id STRING,
pressure DOUBLE,
flow DOUBLE,
velocity DOUBLE,
status STRING,
ts BIGINT
)
PARTITIONED BY (dt STRING, hour STRING)
STORED AS PARQUET
LOCATION 'hdfs://namenode:8020/data/raw/pipe/';
DWD层(明细表) :清洗后的明细数据。
sql
CREATE TABLE IF NOT EXISTS dwd_pipe_detail (
node_id STRING,
node_name STRING,
node_type STRING,
region STRING,
pressure DOUBLE,
flow DOUBLE,
velocity DOUBLE,
valve_open INT,
status STRING,
temperature DOUBLE,
ts TIMESTAMP,
is_fault BOOLEAN, -- 是否异常(由ETL规则判定)
fault_type STRING -- 异常类型:PRESSURE_HIGH/LOW/OFFLINE等
)
PARTITIONED BY (dt STRING, hour STRING)
STORED AS PARQUET;
DWS层(汇总表) :按小时聚合的统计数据。
sql
CREATE TABLE IF NOT EXISTS dws_pipe_hourly (
region STRING,
hour INT,
avg_pressure DOUBLE,
max_pressure DOUBLE,
min_pressure DOUBLE,
avg_flow DOUBLE,
total_flow DOUBLE,
avg_velocity DOUBLE,
fault_count INT,
total_count INT,
fault_rate DOUBLE
)
PARTITIONED BY (dt STRING)
STORED AS PARQUET;
预警结果表:独立存储预警事件。
sql
CREATE TABLE IF NOT EXISTS dwd_alert_events (
alert_id STRING,
node_id STRING,
node_name STRING,
alert_type STRING, -- OFFLINE | PRESSURE_ABNORMAL | FLOW_ABNORMAL | REGION_FAULT
alert_level STRING, -- INFO | WARNING | CRITICAL
alert_desc STRING,
trigger_time TIMESTAMP,
is_resolved BOOLEAN
)
PARTITIONED BY (dt STRING)
STORED AS PARQUET;
3.5 计算层(ETL)需求
3.5.1 调度任务定义
采用 Apache Airflow 编排以下任务【参见第一本书第6章】:
| 任务ID | 任务名称 | 触发条件 | 预计耗时 |
|---|---|---|---|
task_ods_to_dwd |
ODS → DWD 清洗 | 每日01:00 | 10分钟 |
task_dwd_to_dws |
DWD → DWS 聚合 | task_ods_to_dwd 成功 | 10分钟 |
task_export_hbase |
导出到HBase | task_dwd_to_dws 成功 | 15分钟 |
task_export_clickhouse |
导出到ClickHouse | task_dwd_to_dws 成功 | 5分钟 |
task_rule_alert |
规则预警检测 | task_dwd_to_dws 成功 | 5分钟 |
Airflow DAG 伪代码:
python
from airflow import DAG
from airflow.operators.bash import BashOperator
from datetime import datetime, timedelta
default_args = {
'owner': 'data-team',
'retries': 3,
'retry_delay': timedelta(minutes=5),
}
dag = DAG(
'pipe_daily_pipeline',
default_args=default_args,
description='供水管网每日ETL流水线',
schedule_interval='0 1 * * *',
start_date=datetime(2026, 7, 1),
catchup=False,
)
task_ods_to_dwd = BashOperator(
task_id='ods_to_dwd',
bash_command='spark-submit --class OdsToDwdJob /jars/pipe_etl.jar --dt {{ ds }}',
dag=dag,
)
task_dwd_to_dws = BashOperator(
task_id='dwd_to_dws',
bash_command='spark-submit --class DwdToDwsJob /jars/pipe_etl.jar --dt {{ ds }}',
dag=dag,
)
task_export_hbase = BashOperator(
task_id='export_hbase',
bash_command='spark-submit --class ExportToHBaseJob /jars/pipe_export.jar --dt {{ ds }}',
dag=dag,
)
task_export_clickhouse = BashOperator(
task_id='export_clickhouse',
bash_command='spark-submit --class ExportToCKJob /jars/pipe_export.jar --dt {{ ds }}',
dag=dag,
)
task_rule_alert = BashOperator(
task_id='rule_alert',
bash_command='spark-submit --class RuleAlertJob /jars/pipe_alert.jar --dt {{ ds }}',
dag=dag,
)
task_ods_to_dwd >> task_dwd_to_dws >> [task_export_hbase, task_export_clickhouse, task_rule_alert]
3.5.2 ODS → DWD 清洗逻辑
需求描述:读取ODS层原始数据,执行以下清洗操作:
| 清洗规则 | 处理方式 |
|---|---|
| pressure < 0 或 > 1.5 | 标记为 is_fault=true, fault_type='PRESSURE_ABNORMAL' |
| velocity < 0 | 标记为 is_fault=true, fault_type='VELOCITY_ABNORMAL' |
| status = 'FAULT' | 标记为 is_fault=true, fault_type='STATUS_FAULT' |
| 连续心跳丢失(由后续规则检测) | 标记为 is_fault=true, fault_type='OFFLINE' |
伪代码:
java
public class OdsToDwdJob {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder()
.appName("OdsToDwd")
.enableHiveSupport()
.getOrCreate();
String dt = args[0]; // 例如: 2026-07-16
String sql =
"SELECT " +
" node_id, node_name, node_type, region, " +
" pressure, flow, velocity, valve_open, status, temperature, " +
" FROM_UNIXTIME(ts/1000) as ts, " +
" dt, hour, " +
" CASE " +
" WHEN pressure < 0 OR pressure > 1.5 THEN true " +
" WHEN velocity < 0 THEN true " +
" WHEN status = 'FAULT' THEN true " +
" ELSE false " +
" END as is_fault, " +
" CASE " +
" WHEN pressure < 0 OR pressure > 1.5 THEN 'PRESSURE_ABNORMAL' " +
" WHEN velocity < 0 THEN 'VELOCITY_ABNORMAL' " +
" WHEN status = 'FAULT' THEN 'STATUS_FAULT' " +
" ELSE NULL " +
" END as fault_type " +
"FROM ods_pipe_raw " +
"WHERE dt = '" + dt + "'";
Dataset<Row> dwdDF = spark.sql(sql);
dwdDF.write()
.mode(SaveMode.Overwrite)
.format("parquet")
.partitionBy("dt", "hour")
.saveAsTable("dwd_pipe_detail");
}
}
3.5.3 DWD → DWS 聚合逻辑
需求描述:按区域和小时聚合DWD明细数据。
java
public class DwdToDwsJob {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder()
.appName("DwdToDws")
.enableHiveSupport()
.getOrCreate();
String dt = args[0];
String sql =
"SELECT " +
" region, " +
" CAST(hour AS INT) as hour, " +
" AVG(pressure) as avg_pressure, " +
" MAX(pressure) as max_pressure, " +
" MIN(pressure) as min_pressure, " +
" AVG(flow) as avg_flow, " +
" SUM(flow) as total_flow, " +
" AVG(velocity) as avg_velocity, " +
" SUM(CASE WHEN is_fault = true THEN 1 ELSE 0 END) as fault_count, " +
" COUNT(1) as total_count, " +
" CAST(SUM(CASE WHEN is_fault = true THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(1) as fault_rate " +
"FROM dwd_pipe_detail " +
"WHERE dt = '" + dt + "' " +
"GROUP BY region, hour";
Dataset<Row> dwsDF = spark.sql(sql);
dwsDF.write()
.mode(SaveMode.Overwrite)
.format("parquet")
.partitionBy("dt")
.saveAsTable("dws_pipe_hourly");
}
}
3.6 服务导出层需求
3.6.1 导出到 HBase
需求描述:将DWD明细数据导出到HBase,供前端"节点详情点查"使用【参见第二本书问题7】。
HBase表设计:
| 配置项 | 值 |
|---|---|
| 表名 | pipe_detail |
| RowKey | {nodeId}_{dt}_{hour} (如 N001_2026-07-16_14) |
| 列族 | info |
| 列 | pressure, flow, velocity, status, is_fault, fault_type, ts |
伪代码:
java
public class ExportToHBaseJob {
public static void main(String[] args) throws IOException {
SparkSession spark = SparkSession.builder()
.appName("ExportToHBase")
.enableHiveSupport()
.getOrCreate();
String dt = args[0];
Dataset<Row> df = spark.sql(
"SELECT node_id, dt, hour, pressure, flow, velocity, status, is_fault, fault_type, ts " +
"FROM dwd_pipe_detail WHERE dt = '" + dt + "'"
);
df.foreachPartition((Iterator<Row> rows) -> {
Connection hbConn = HBaseConnectionPool.getConnection();
Table table = hbConn.getTable(TableName.valueOf("pipe_detail"));
List<Put> puts = new ArrayList<>();
while (rows.hasNext()) {
Row row = rows.next();
String rowKey = row.getString(0) + "_" + row.getString(1) + "_" + row.getString(2);
Put put = new Put(Bytes.toBytes(rowKey));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("pressure"),
Bytes.toBytes(row.getDouble(3).toString()));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("flow"),
Bytes.toBytes(row.getDouble(4).toString()));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("velocity"),
Bytes.toBytes(row.getDouble(5).toString()));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("status"),
Bytes.toBytes(row.getString(6)));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("is_fault"),
Bytes.toBytes(String.valueOf(row.getBoolean(7))));
if (row.getString(8) != null) {
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("fault_type"),
Bytes.toBytes(row.getString(8)));
}
puts.add(put);
if (puts.size() >= 1000) {
table.put(puts);
puts.clear();
}
}
if (!puts.isEmpty()) table.put(puts);
table.close();
});
}
}
3.6.2 导出到 ClickHouse
需求描述:将DWS聚合数据导出到ClickHouse,供大屏聚合查询使用【参见第一本书第5章】。
ClickHouse表设计:
sql
CREATE TABLE dws_pipe_hourly_local ON CLUSTER default (
dt Date,
region String,
hour UInt8,
avg_pressure Float32,
max_pressure Float32,
min_pressure Float32,
avg_flow Float32,
total_flow Float32,
avg_velocity Float32,
fault_count UInt32,
total_count UInt32,
fault_rate Float32
) ENGINE = MergeTree()
PARTITION BY dt
ORDER BY (dt, region, hour);
伪代码(JDBC批量导出):
java
public class ExportToCKJob {
public static void main(String[] args) throws SQLException {
SparkSession spark = SparkSession.builder()
.appName("ExportToCK")
.enableHiveSupport()
.getOrCreate();
String dt = args[0];
Dataset<Row> df = spark.sql(
"SELECT dt, region, hour, avg_pressure, max_pressure, min_pressure, " +
"avg_flow, total_flow, avg_velocity, fault_count, total_count, fault_rate " +
"FROM dws_pipe_hourly WHERE dt = '" + dt + "'"
);
// 使用 foreachPartition 批量 JDBC 写入 ClickHouse
df.foreachPartition((Iterator<Row> rows) -> {
try (Connection conn = DriverManager.getConnection(
"jdbc:clickhouse://ck-host:8123/default")) {
String insertSQL = "INSERT INTO dws_pipe_hourly_local VALUES (?,?,?,?,?,?,?,?,?,?,?,?)";
PreparedStatement ps = conn.prepareStatement(insertSQL);
while (rows.hasNext()) {
Row row = rows.next();
ps.setDate(1, Date.valueOf(row.getString(0)));
ps.setString(2, row.getString(1));
ps.setInt(3, row.getInt(2));
ps.setDouble(4, row.getDouble(3));
// ... 设置所有字段
ps.addBatch();
}
ps.executeBatch();
}
});
}
}
3.7 规则预警层需求
3.7.1 预警规则定义
需求描述 :系统应支持以下预设规则的自动检测与预警【参见第二本书问题10】。
| 规则ID | 规则名称 | 触发条件 | 预警级别 | 检测频率 |
|---|---|---|---|---|
| R01 | 节点离线 | 某节点连续30分钟无数据上报 | CRITICAL | 每10分钟 |
| R02 | 压力异常 | 某节点压力 < 0.2 或 > 0.8 MPa | WARNING | 每小时 |
| R03 | 压力骤变 | 某节点压力较前一小时变化 > 30% | WARNING | 每小时 |
| R04 | 流速异常 | 某节点流速 > 3.0 m/s | INFO | 每小时 |
| R05 | 区域故障率过高 | 某区域故障率 > 5% | CRITICAL | 每小时 |
| R06 | 阀门状态异常 | 阀门状态=CLOSED但流速>0.5 m/s | CRITICAL | 每小时 |
伪代码(独立DQC任务,与ETL解耦):
java
public class RuleAlertJob {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder()
.appName("RuleAlert")
.enableHiveSupport()
.getOrCreate();
String dt = args[0];
// R01: 检测离线节点(最近30分钟无数据)
Dataset<Row> offlineNodes = spark.sql(
"SELECT node_id, MAX(ts) as last_ts " +
"FROM dwd_pipe_detail WHERE dt = '" + dt + "' " +
"GROUP BY node_id " +
"HAVING (UNIX_TIMESTAMP() - MAX(ts)) > 1800"
);
// R02: 检测压力异常
Dataset<Row> pressureAbnormal = spark.sql(
"SELECT node_id, AVG(pressure) as avg_pressure " +
"FROM dwd_pipe_detail WHERE dt = '" + dt + "' " +
"GROUP BY node_id " +
"HAVING avg_pressure < 0.2 OR avg_pressure > 0.8"
);
// R05: 检测区域故障率
Dataset<Row> regionFault = spark.sql(
"SELECT region, fault_rate " +
"FROM dws_pipe_hourly WHERE dt = '" + dt + "' " +
"AND fault_rate > 0.05"
);
// 汇总所有预警结果,写入预警表
// 同时通过 API 推送到大屏(WebSocket)
}
}
3.8 大屏展示层需求
3.8.1 大屏布局
大屏采用 1920×1080 分辨率,布局如下:
text
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 🏙️ 城市供水管网智能监控系统 2026-07-16 14:32:05 │
├─────────────────────────────────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────────────────────────────┐ │
│ │ 管网拓扑图(模拟GIS) │ │
│ │ │ │
│ │ [N001] ─── [N002] ─── [N003] │ │
│ │ ● 0.45MPa ● 0.38MPa ● 0.52MPa │ │
│ │ 1.2m/s 0.9m/s 1.5m/s │ │
│ │ │ │
│ │ [N004] ─── [N005] ─── [N006] │ │
│ │ ● 0.28MPa 🔴 ● 0.41MPa ● 0.33MPa │ │
│ │ 0.5m/s 1.1m/s 0.8m/s │ │
│ │ │ │
│ │ ● 正常节点 🔴 故障节点 ⚠️ 预警节点 ● 关闭节点 │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────────────────────────┐ │
│ │ 📊 实时统计面板 │ │ 🚨 实时预警列表 │ │
│ │ 总节点: 100 │ │ 🔴 N005 压力异常 14:30 │ │
│ │ 正常: 92 │ │ ⚠️ N012 流速过高 14:28 │ │
│ │ 故障: 5 │ │ 🔴 华东区 故障率6.2% 14:25 │ │
│ │ 离线: 3 │ │ ... │ │
│ │ 今日故障率: 4.2% │ └─────────────────────────────────────────────┘ │
│ └─────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────────┐ │
│ │ 📈 今日压力/流量趋势图 (折线图) │ │
│ │ 压力(MPa) 流量(m³/h) │ │
│ │ 0.6 ────╮ 120 ────╮ │ │
│ │ 0.4 ────┼─────╮ 100 ────┼─────╮ │ │
│ │ 0.2 ────┴─────┴─── 80 ────┴─────┴─── │ │
│ │ 00 04 08 12 16 20 00 04 08 12 16 20 │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
3.8.2 功能需求明细
| 功能模块 | 需求描述 | 数据来源 | 刷新方式 |
|---|---|---|---|
| 管网拓扑图 | 以节点连线图展示100个管网节点,用颜色标识状态(正常/故障/离线/关闭),显示实时压力和流速 | HBase (点查) + WebSocket推送 | 实时推送 |
| 实时统计面板 | 显示总节点数、正常数、故障数、离线数、今日故障率 | ClickHouse (聚合查询) | 每5秒轮询 |
| 实时预警列表 | 滚动显示最新预警事件,按级别(CRITICAL/WARNING/INFO)着色 | 预警结果表 | WebSocket推送 |
| 趋势图 | 显示今日24小时的压力/流量/故障率趋势 | ClickHouse (DWS表) | 每5分钟刷新 |
| 节点详情弹窗 | 点击节点弹出详情:该节点最近24小时的压力/流速曲线及故障记录 | HBase (RowKey点查) | 点击时触发 |
3.8.3 前端技术选型
| 技术 | 用途 |
|---|---|
| React / Vue | 大屏应用框架 |
| ECharts / D3.js | 图表渲染(折线图、拓扑图) |
| Axios | HTTP API调用 |
| WebSocket | 实时预警推送 |
| 模拟GIS(Canvas/SVG) | 管网拓扑图绘制 |
3.8.4 后端API设计
| API端点 | 方法 | 用途 | 数据源 |
|---|---|---|---|
/api/dashboard/overview |
GET | 获取大屏概览统计数据 | ClickHouse |
/api/dashboard/trend |
GET | 获取今日趋势数据 | ClickHouse |
/api/node/{nodeId}/detail |
GET | 获取某节点详情及24小时历史 | HBase |
/api/node/{nodeId}/history |
GET | 获取某节点历史趋势 | HBase / ClickHouse |
/api/alert/list |
GET | 获取预警列表 | 预警结果表 |
/ws/alert |
WebSocket | 实时预警推送 | --- |
Spring Boot 伪代码:
java
@RestController
@RequestMapping("/api")
public class PipeDashboardController {
// 大屏概览统计 → ClickHouse
@GetMapping("/dashboard/overview")
public OverviewVO getOverview() throws SQLException {
String sql = "SELECT " +
"COUNT(DISTINCT node_id) as total_nodes, " +
"SUM(CASE WHEN is_fault=false THEN 1 ELSE 0 END) as normal_nodes, " +
"SUM(CASE WHEN is_fault=true THEN 1 ELSE 0 END) as fault_nodes " +
"FROM dwd_pipe_detail WHERE dt = today()";
// 执行查询并返回
}
// 节点详情 → HBase
@GetMapping("/node/{nodeId}/detail")
public NodeDetailVO getNodeDetail(@PathVariable String nodeId) throws IOException {
String rowKey = nodeId + "_" + getToday() + "_" + getCurrentHour();
Get get = new Get(Bytes.toBytes(rowKey));
Table table = hbaseConn.getTable(TableName.valueOf("pipe_detail"));
Result result = table.get(get);
// 解析并返回
}
}
3.9 业务场景实现矩阵
| 场景编号 | 场景名称 | 涉及组件 | 实现方式 |
|---|---|---|---|
| S01 | 管网全景监控 | HBase + ClickHouse + WebSocket | 拓扑图实时渲染 + 状态轮询 |
| S02 | 实时数据查看 | HBase | RowKey点查 |
| S03 | 故障标识 | HBase + 前端CSS | 状态字段驱动颜色/闪烁 |
| S04 | 规则预警 | 独立DQC任务 + WebSocket | 定时检测 + 实时推送 |
| S05 | 历史趋势查看 | ClickHouse | 聚合查询 |
4. 非功能需求
4.1 性能需求
| 指标 | 要求 | 说明 |
|---|---|---|
| 数据采集吞吐量 | ≥ 100 条/秒 | 100个节点,每秒1条 |
| 流式写入延迟 | ≤ 5分钟 | 微批次间隔 |
| 离线ETL完成时间 | ≤ 1小时 | 每日1亿条数据量级 |
| 大屏聚合查询响应 | ≤ 1秒 | ClickHouse查询 |
| 节点详情点查响应 | ≤ 50ms | HBase RowKey查询 |
| 预警检测延迟 | ≤ 10分钟 | 从数据产生到预警触发 |
4.2 可用性需求
| 指标 | 要求 |
|---|---|
| 系统可用性 | ≥ 99%(演示环境) |
| 数据丢失容忍 | Exactly-Once(Checkpoint机制保障)【参见第一本书第2章】 |
| 故障恢复 | 任务失败自动重试3次 |
4.3 可扩展性需求
- 节点数量可配置(演示默认100个,可通过配置文件调整)
- 预警规则支持通过配置文件新增/修改(无需重新编译代码)
- 数据保留时间可配置(默认7天)
4.4 安全需求
- 演示程序不涉及生产数据,使用模拟数据
- 各组件使用独立账号,遵循最小权限原则
- ClickHouse连接使用只读账号(大屏查询)【参见第二本书问题11】
5. 技术栈总览
| 层级 | 组件 | 版本 | 用途 |
|---|---|---|---|
| 数据源 | Java Kafka Producer | --- | 模拟传感器数据生成 |
| 缓冲层 | Apache Kafka | 3.x | 消息队列,削峰填谷 |
| 流式写入 | Spark Structured Streaming | 3.x | 实时入湖 |
| 存储层 | HDFS | 3.x | 数据湖存储 |
| 元数据层 | Hive Metastore | 3.x | 元数据管理 |
| 计算层 | Spark SQL | 3.x | 离线ETL |
| 调度层 | Apache Airflow | 2.x | 工作流调度 |
| 服务层-点查 | HBase | 2.x | 明细数据点查 |
| 服务层-聚合 | ClickHouse | 23.x | 聚合查询 |
| 后端API | Spring Boot | 3.x | RESTful API + WebSocket |
| 前端大屏 | React / Vue + ECharts | --- | 大屏展示 |
6. 演示数据说明
6.1 模拟节点列表(100个)
| 节点ID范围 | 数量 | 类型 | 区域 | 备注 |
|---|---|---|---|---|
| N001--N080 | 80 | VALVE(阀门) | 按区域均分 | 可调节开度 |
| N081--N090 | 10 | PUMP(泵站) | 按区域均分 | 压力输出端 |
| N091--N100 | 10 | MONITOR(监测点) | 按区域均分 | 纯监测无控制 |
6.2 模拟数据特征
| 参数 | 正常范围 | 异常注入条件 | 异常值范围 |
|---|---|---|---|
| pressure | 0.3--0.6 MPa | 5%概率 | <0.1 或 >1.0 |
| flow | 50--200 m³/h | 5%概率 | 0 或 >300 |
| velocity | 0.5--2.0 m/s | 5%概率 | <0.1 或 >3.5 |
| status | OPEN (85%) / CLOSED (15%) | --- | FAULT(5%概率) |
7. 验收标准
| 编号 | 验收项 | 验收标准 |
|---|---|---|
| AC-01 | 数据生成 | 模拟生成器每秒产生100条有效JSON数据写入Kafka |
| AC-02 | 流式入湖 | HDFS上按dt/hour分区产生Parquet文件,每5分钟一批 |
| AC-03 | ETL清洗 | DWD表数据量 = ODS表有效数据量 - 异常标记数据 |
| AC-04 | ETL聚合 | DWS表按区域+小时产生24×N条汇总记录 |
| AC-05 | HBase导出 | 可通过RowKey {nodeId}_{dt}_{hour} 在50ms内查到明细 |
| AC-06 | ClickHouse导出 | 大屏聚合查询在1秒内返回 |
| AC-07 | 规则预警 | 离线节点、压力异常等在10分钟内触发预警并推送大屏 |
| AC-08 | 大屏展示 | 拓扑图正确显示所有节点状态,颜色标识符合需求 |
8. 附录
附录A:两本书与本系统各模块对应关系
| 本书章节 | 对应第一本书章节 | 对应第二本书问题 |
|---|---|---|
| 3.1 数据源 | 第1章 | 问题1、问题2 |
| 3.2--3.3 采集与流式写入 | 第2章 | 问题3 |
| 3.4 元数据管理 | 第3章 | --- |
| 3.5 计算层ETL | 第4章 | 问题6、问题7 |
| 3.6 服务导出 | 第5章 | 问题7、问题8 |
| 3.7 规则预警 | --- | 问题10 |
| 3.8 大屏展示 | 第7章 | 问题9 |
| 整体调度 | 第6章 | --- |
附录B:术语表
| 术语 | 全称 | 说明 |
|---|---|---|
| ODS | Operational Data Store | 操作数据存储层(贴源层) |
| DWD | Data Warehouse Detail | 数据仓库明细层 |
| DWS | Data Warehouse Summary | 数据仓库汇总层 |
| ADS | Application Data Service | 应用数据服务层 |
| ETL | Extract-Transform-Load | 抽取-转换-加载 |
| DQC | Data Quality Control | 数据质量控制 |
------文档结束------