Flink端到端延迟测量四大实战方法论

发布时间:2026/10/16 14:57:00
Flink端到端延迟测量四大实战方法论 1. 为什么“端到端延迟”不是看一眼Flink Web UI就能解决的问题在某次线上压测复盘会上A同学指着Flink Web UI里那个醒目的“582ms”平均延迟数字说“看我们的实时链路延迟完全达标。”结果话音刚落下游业务方就发来告警订单履约状态更新平均滞后3.2秒超时订单激增。现场安静了三秒——没人质疑数据本身但所有人都意识到那个UI上显示的根本不是业务真正关心的“端到端延迟”。这就是Flink全链路延迟测量最常被忽视的真相Flink内置的延迟指标如latencymetric只反映算子内部处理耗时它不包含数据从源头写入、网络传输、序列化反序列化、Checkpoint对齐等待、下游消费确认等所有真实业务路径上的时间开销。它更像一个“引擎内部仪表盘”而业务需要的是“从踩下油门到车轮转动”的完整响应时间。我参与过的多个实时数仓和风控项目中有70%以上的性能争议都源于对“延迟”定义的错位。有人把Kafka Producer发送耗时当延迟有人把Flink Source读取间隔当延迟还有人直接拿两个日志时间戳相减——这些做法在特定场景下可能凑合但一旦涉及SLA承诺、故障归因或容量规划就会立刻暴露致命缺陷。所以“Flink全链路端到端延迟测量”本质上不是一个技术配置问题而是一场业务语义对齐工程。它要求你明确回答三个问题延迟的起点在哪里是业务事件生成时刻如MySQL binlog commit时间、还是数据进入Kafka分区的时刻延迟的终点在哪里是Flink JobManager收到结果的时刻、还是结果写入HBase后返回ACK的时刻时间基准是否统一所有组件是否使用同一NTP源校准时钟漂移是否在可接受范围内通常要求10ms提示不要跳过时钟同步验证。我曾在一个金融项目中发现Kafka Broker与Flink TaskManager之间存在47ms系统时钟偏差导致基于时间戳的延迟计算整体偏高排查耗时两天。关键词“Flink 端到端延迟”背后实际承载的是对实时性承诺的兑现能力。它不服务于监控大屏的美观而服务于故障时的分钟级定位、扩容时的精准预估、以及架构演进时的客观评估。接下来我会用真实项目中的四套方法论拆解如何让这个抽象概念变成可采集、可存储、可分析、可归因的确定性数据。2. 方法一基于事件时间戳的被动埋点法最轻量也最容易误用这是团队初期最常用的方法在数据源头打上时间戳在最终输出点再次记录时间戳相减即得延迟。看似简单实则暗坑密布。我们以一个典型的电商用户行为实时分析链路为例MySQL binlog → Kafka → Flink SQLETLJoin→ Redis用户画像更新→ 业务API调用2.1 标准实现流程与关键参数设计第一步必须在源头注入不可篡改的事件时间戳。这里强调“不可篡改”是因为很多团队习惯在Flink Source中用System.currentTimeMillis()补时间戳这会导致同一批数据因TaskManager负载差异产生毫秒级时间偏移Checkpoint恢复时重放数据的时间戳与原始事件严重不符。正确做法是由业务系统在生成事件时将当前精确时间建议纳秒级作为字段写入消息体。例如MySQL表增加event_time_ns BIGINT列Kafka消息value中嵌入{event_time_ns: 1712345678901234567, user_id: U123, ...}。第二步在Flink作业的Sink端本例为Redis写入完成回调捕获处理完成时刻。注意不是processElement结束而是Jedis.set()返回成功之后public class RedisSinkWithLatency extends RichSinkFunctionUserProfile { private transient Jedis jedis; Override public void invoke(UserProfile value, Context context) throws Exception { long startTimeNs value.getEventTimeNs(); // 源头注入的时间戳 long sinkStartTime System.nanoTime(); jedis.set(profile: value.getUserId(), value.toJson()); long sinkEndTime System.nanoTime(); long endToEndLatencyNs sinkEndTime - startTimeNs; // 上报到Metrics系统如Prometheus latencyGauge.set(endToEndLatencyNs / 1_000_000.0); // 转为毫秒 } }第三步设置合理的采样率与聚合策略。全量上报会带来巨大压力我们采用分层采样99%流量仅上报P99延迟值每分钟聚合一次1%流量全量上报原始延迟数据用于深度归因关键用户VIP标识100%全量上报。注意System.nanoTime()比System.currentTimeMillis()更适合测量耗时因为它不受系统时钟调整影响且精度更高通常为纳秒级。但务必确保所有节点JVM版本一致早期JDK8存在nanoTime在某些虚拟化环境下的抖动问题。2.2 为什么这个方法经常失效三个典型误用场景场景一忽略序列化/反序列化耗时很多团队只测了Flink内部处理时间却忘了Kafka Consumer从字节流解析成Java对象的过程。实测显示在使用Avro Schema且消息体较大5KB时反序列化平均耗时可达8~12ms。解决方案是在Kafka Consumer的DeserializationSchema.deserialize()方法内埋点与Flink Source的processElement起始时间做差。场景二混淆“处理延迟”与“排队延迟”Flink Web UI显示的“Source延迟”其实是currentProcessingTime - eventTime它反映的是数据在Source端的积压程度而非处理耗时。曾有团队将此值当作端到端延迟导致误判集群资源充足——实际上数据在Kafka Topic中已堆积数分钟只是Flink消费速度尚可。正确做法是在Kafka Producer端记录send()调用时刻在Flink Source的open()或map()中记录deserialize()完成时刻二者之差才是真实的“摄入延迟”。场景三跨时区时间戳解析错误当源头系统部署在UTC8而Flink集群运行在UTC时区若未显式指定时区解析SimpleDateFormat可能将时间戳误判为本地时间。我们吃过亏某次海外活动期间印尼节点上报的时间戳被解析为北京时间导致延迟虚高2小时。根治方案是所有时间戳统一使用ISO 8601格式字符串含时区信息如2024-04-05T14:30:00.12308:00并用ZonedDateTime.parse()解析。2.3 实战效果与局限性在某千万级DAU的APP行为分析项目中该方法上线后首周即暴露关键瓶颈Redis写入延迟P99达1.8秒远超预期的200ms。进一步分析发现是由于Flink并发度128远高于Redis连接池大小默认32导致大量线程阻塞在连接获取阶段。通过将连接池扩容至256并启用异步Redis客户端LettuceP99降至142ms。但该方法有硬伤它无法测量网络传输耗时、磁盘IO等待、GC暂停等非代码执行时间。当出现偶发性长尾延迟如单次延迟5秒时你只能知道“结果很慢”却无法定位是网络抖动、Redis主从切换还是Flink自身Full GC。这就引出了第二种方法——主动探针法。3. 方法二基于Probe Record的主动探针法精准定位长尾但需改造链路当被动埋点无法满足根因分析需求时我们转向主动注入“探测包”。其核心思想是向生产数据流中周期性注入特殊标记的数据全程跟踪其在各环节的耗时形成一条可审计的黄金路径。这类似于网络领域的Ping机制但针对的是数据处理流水线。3.1 Probe Record的设计原则与生命周期Probe Record不是普通数据它必须满足四个刚性条件可识别性拥有全局唯一ID如UUIDv4且在整条链路中绝不被修改或丢失低侵入性体积极小建议100字节不触发任何业务逻辑分支强隔离性在Kafka中使用独立Topic如probe-events避免与业务流量混杂可终止性到达最终节点后必须生成终结报告并退出链路不进入下游业务系统。我们设计的Probe Record结构如下JSON格式{ probe_id: a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8, inject_time_ns: 1712345678901234567, stage: [kafka_produce, flink_source, flink_process, redis_sink], timestamps: [1712345678901234567, 0, 0, 0], metadata: {env: prod, region: shanghai} }其中timestamps数组按stage顺序记录各环节时间戳初始仅填充inject_time_ns后续环节通过probe_id查找并更新对应位置。3.2 四个关键注入点的实现细节① Kafka Producer端注入在业务应用的Kafka Producer封装层中每10秒生成一个Probe Record并调用producer.send()异步发送。关键点在于必须使用Callback监听发送结果只有onCompletion成功时才认为注入有效。失败时需重试最多3次或丢弃避免污染探测数据。// 注入逻辑伪代码 ScheduledExecutorService probeScheduler Executors.newSingleThreadScheduledExecutor(); probeScheduler.scheduleAtFixedRate(() - { ProbeRecord probe new ProbeRecord(); producer.send(new ProducerRecord(probe-events, probe.toJson()), (metadata, exception) - { if (exception null) { log.info(Probe injected successfully: {}, probe.getProbeId()); } else { log.warn(Probe injection failed, exception); } }); }, 0, 10, TimeUnit.SECONDS);② Flink Source端捕获与透传在自定义SourceFunction中当poll()返回的消息key或value包含probe_id字段时立即记录System.nanoTime()并更新timestamps[1]。注意不能在RichSourceFunction.open()中初始化时间戳因为Source可能在Checkpoint恢复后重复读取同一批数据。必须在每次poll()解析出完整消息体后才触发。③ Flink ProcessFunction中更新处理时间在关键Stateful Operator如KeyedProcessFunction的processElement方法开头检查输入元素是否为Probe Record。若是则调用getRuntimeContext().getMetricGroup().counter(probe_processed).inc()进行计数并更新timestamps[2]。④ Sink端生成终结报告当Probe Record抵达最终Sink如Redis在invoke()方法末尾构造完整的终结报告{ probe_id: a1b2c3d4-..., end_to_end_ms: 1247.3, breakdown: { kafka_produce: 12.5, kafka_network: 8.2, flink_source: 3.1, flink_process: 426.7, redis_network: 15.8, redis_write: 781.0 }, timestamp: 2024-04-05T14:30:00.12308:00 }该报告被发送至专用的probe-reportsTopic供ELK或Prometheus采集。提示Breakdown字段中的kafka_network和redis_network并非直接测量而是通过上下游时间戳相减推算。例如kafka_network timestamps[1] - timestamps[0]。这要求所有组件时间高度同步否则误差会累积。3.3 如何避免Probe Record干扰正常业务这是实施中最常被质疑的问题。我们的实践方案是“三隔离”Topic隔离Probe Record走独立Kafka TopicConsumer Group也独立与业务Consumer无任何交集线程隔离Probe注入、捕获、报告生成全部在独立线程池执行不占用Flink TaskManager的Slot资源数据隔离Probe Record的probe_id采用固定前缀如PROBE_所有业务逻辑的Filter条件均排除该前缀确保100%不进入业务处理流。在某支付风控项目中我们曾担心Probe Record会触发规则引擎的异常检测。解决方案是在规则引擎的最外层添加白名单校验if (event.getId().startsWith(PROBE_)) return;一行代码解决。3.4 主动探针法的价值与代价该方法最大的价值在于它提供了延迟的“分解视图”Breakdown View。当端到端延迟飙升时你不再需要猜测而是直接查看breakdown字段5秒内定位瓶颈环节。在一次线上事故中我们发现redis_write耗时突增至4.2秒而redis_network仅12ms立即锁定为Redis主节点内存不足触发Swap而非网络问题。但代价也很明显需要改造所有中间件客户端Kafka Producer/Consumer、Redis Client等工作量较大Probe Record的注入频率需精细调控太低如30秒则无法捕捉瞬态抖动太高如1秒则增加链路负担我们最终选定10秒平衡了精度与开销无法覆盖“纯离线”环节如Flink写入HDFS后的MapReduce处理因其不接入实时链路。当需要覆盖全链路包括离线环节时就必须升级到第三种方法——混合时间溯源法。4. 方法三混合时间溯源法覆盖实时离线但依赖强治理前述两种方法均聚焦于“实时链路”但现代数据架构往往是Lambda或Kappa架构实时结果需与离线批处理结果对账。此时“端到端延迟”的终点不再是Redis写入而是数据在BI报表中可查询的时刻。这就要求追踪跨越实时与离线两套系统的完整路径。4.1 核心思路用统一Trace ID串联异构系统我们放弃为每个环节单独埋点转而构建一个贯穿始终的Trace上下文TraceContext。其本质是一个轻量级分布式追踪协议但专为数据流水线优化不依赖Zipkin/Jaeger等重型APM。TraceContext包含三个核心字段字段名类型说明trace_idString全局唯一由源头生成如Snowflake IDspan_idString当前环节唯一ID格式为{trace_id}.{seq}parent_span_idString上一环节的span_id源头为null例如一个订单事件的TraceContext流转如下MySQL (source) → trace_id123456, span_id123456.1, parentnull Kafka Producer → trace_id123456, span_id123456.2, parent123456.1 Flink Source → trace_id123456, span_id123456.3, parent123456.2 Flink Process → trace_id123456, span_id123456.4, parent123456.3 Hive Insert (离线) → trace_id123456, span_id123456.5, parent123456.4 BI Dashboard Query → trace_id123456, span_id123456.6, parent123456.54.2 关键环节的TraceContext注入与传递源头系统MySQL在业务写入订单表时应用层生成trace_id并将其作为额外字段存入表中如trace_id VARCHAR(32)。Binlog解析服务如Canal在解析时自动提取该字段并注入到Kafka消息的Header中Kafka支持自定义Headers。Kafka ConsumerFlink Source在自定义DeserializationSchema中从ConsumerRecord.headers()读取trace_id并将其作为Row的第一个字段或ProcessFunction的Context属性透传。public class TraceAwareDeserializationSchema implements DeserializationSchemaRow { Override public Row deserialize(byte[] message, TypeInformationRow typeInfo) { // 解析消息体... Row row parseMessageBody(message); // 从Headers提取trace_id Headers headers ...; // 从ConsumerRecord获取 String traceId headers.lastHeader(trace_id).valueAsString(); // 将trace_id作为Row的第一个字段 return Row.of(traceId, row.getField(0), row.getField(1), ...); } }Flink内部传递利用Flink的RuntimeContext或ProcessFunction.Context将trace_id作为StateDescriptor的key的一部分确保在KeyedStream中不丢失。对于非Keyed操作如map可通过BroadcastState广播trace_id映射表。离线环节Hive/Spark在Flink写入Hive的HiveTableSink中将trace_id作为分区字段如dt20240405/trace_id123456。Spark SQL读取该分区时自动继承trace_id并在写入最终报表表时保留。BI系统Superset/Metabase在BI工具的SQL查询中添加WHERE trace_id 123456条件即可拉取该Trace的全链路耗时数据。我们开发了一个轻量级Dashboard输入trace_id后自动绘制各环节时间轴。4.3 如何保证TraceContext的100%完整性这是混合溯源法成败的关键。我们采用“双保险”机制保险一强制校验Fail-Fast在每个环节的入口处校验trace_id是否存在且符合格式如正则^[0-9a-f]{16}$。若缺失或非法立即打点告警并记录error_reasonmissing_trace_id该数据不进入主链路转至死信队列人工核查。保险二自动补全Best-Effort对于无法改造的遗留系统如第三方SDK我们部署一个“Trace Injector”代理服务。它位于Kafka Consumer与Flink之间当检测到消息无trace_id时自动生成一个并记录inject_sourceauto_repair标签便于后续统计补全率。在某大型电商平台的落地中我们要求trace_id完整率≥99.99%。通过半年治理从初期的92%提升至99.997%主要靠两点对所有上游业务方发布《Trace ID接入规范》明确为强制要求在数据质量平台中将trace_id完整率纳入SLA考核未达标则触发告警并暂停数据接入。4.4 混合时间溯源法的终极价值实现“延迟即服务”当TraceContext覆盖全链路后“端到端延迟”就从一个监控指标升维为一种可编程的服务。我们基于此构建了两个高价值能力能力一延迟SLA自动核验为每个核心业务事件如“支付成功”定义SLAP95 ≤ 2s。系统每分钟扫描probe-reportsTopic对trace_id匹配的报告自动计算end_to_end_ms并与SLA比对。不达标时不仅告警还自动生成根因分析报告指出最耗时的三个环节及优化建议。能力二智能降级决策当实时链路延迟持续超过阈值如P99 5s时系统自动将该trace_id对应的查询路由至离线宽表保障业务可用性。这比传统的“开关降级”更精细——不是整个服务降级而是对高延迟数据流进行动态路由。这种方法的代价是治理成本极高但它带来的确定性是其他方法无法替代的。当你能说出“这笔订单的延迟78%来自Hive Compaction12%来自Flink State Backend IO”你就真正掌控了实时性的命脉。5. 方法四基于Flink Metrics的增强观测法零代码改造但需深刻理解Flink内核最后介绍一种“懒人友好”但极具深度的方法不修改任何业务代码仅通过Flink原生Metrics体系挖掘出接近端到端的延迟视图。这要求你对Flink的Checkpoint、Watermark、Backpressure等机制有肌肉记忆般的理解。5.1 从Flink内置Metrics中榨取延迟信号Flink暴露了数百个Metrics但与延迟强相关的只有五个它们构成了一条隐含的延迟证据链Metric Name所属组件物理含义延迟关联性numBytesInPerSecondSource每秒流入字节数反映源头生产速率骤降可能预示上游阻塞checkpointAlignmentTimeCheckpointBarrier对齐耗时直接贡献端到端延迟1s需警惕watermarkLagWatermark当前Watermark与系统时间差衡量事件时间进度滞后越大延迟感知越强buffers.inputQueueLengthNetwork输入缓冲区队列长度0表明下游处理不及积压开始backPressuredTimeMsPerSecondTaskManager每秒背压时间0表明TaskManager被上游压垮我们不孤立看单个指标而是构建延迟健康度评分卡Latency Health ScoreScore 100 - 20 * (checkpointAlignmentTime 1000 ? 1 : 0) - 30 * (watermarkLag 5000 ? 1 : 0) - 25 * (buffers.inputQueueLength 100 ? 1 : 0) - 25 * (backPressuredTimeMsPerSecond 100 ? 1 : 0)当Score 60时系统自动触发深度诊断脚本。5.2 水印滞后watermarkLag的深度解读与误判规避watermarkLag是最易被误解的指标。很多人以为它就是端到端延迟其实不然。它的计算公式是watermarkLag System.currentTimeMillis() - currentWatermark而currentWatermark是由AssignerWithPeriodicWatermarks生成的其值取决于最大事件时间max(event_time)允许的乱序时间allowedLatenessWatermark生成周期默认200ms。因此watermarkLag高可能有三种原因真延迟数据确实晚到如IoT设备断网后重连假延迟allowedLateness设得太小如设为1秒但业务允许5秒乱序导致Watermark被人为压低配置延迟Watermark生成周期过长如设为5秒导致currentWatermark更新不及时。我们的诊断流程是查看watermarkLag趋势若呈阶梯状上升大概率是allowedLateness过小检查numRecordsInPerSecond若同步下降指向源头问题对比checkpointAlignmentTime若二者同步飙升基本可断定为网络或磁盘IO瓶颈。在某物流轨迹项目中watermarkLag持续在8秒左右团队一度认为链路严重延迟。经排查发现allowedLateness被错误配置为1秒而GPS设备因隧道信号丢失天然存在5秒级乱序。将allowedLateness调至10秒后watermarkLag稳定在1.2秒真实延迟暴露出来。5.3 利用Checkpoint对齐时间checkpointAlignmentTime定位网络瓶颈checkpointAlignmentTime是Flink 1.11引入的关键指标它精确测量了Barrier在Operator间对齐所花的时间。其值高意味着网络带宽不足Barrier传输慢下游Operator处理太慢Barrier被阻塞序列化开销大Barrier本身被序列化。我们通过对比checkpointAlignmentTime与checkpointEndLatency从Barrier发出到Checkpoint完成的总时间可分离出网络因素network_contribution ≈ checkpointAlignmentTime - (checkpointEndLatency - checkpointStartLatency)若network_contribution 500ms则优先检查Kafka与Flink集群是否跨机房部署如Kafka在IDC-AFlink在IDC-B网络QoS策略是否限制了小包传输Barrier是小数据包是否启用了SSL加密显著增加小包延迟。在某金融风控项目中我们将Kafka与Flink从跨机房部署改为同机房checkpointAlignmentTime从平均1.2秒降至180ms端到端延迟P95下降42%。5.4 增强观测法的适用边界与组合策略这种方法的最大优势是零改造、零侵入、实时性强适合快速摸底或作为其他方法的补充验证。但它也有明确边界无法测量业务逻辑耗时它告诉你“系统很忙”但不告诉你“哪段代码最慢”无法区分数据源所有数据流共享同一组Metrics无法按业务维度切分对时钟漂移敏感System.currentTimeMillis()的误差会直接污染watermarkLag。因此我们从不单独使用它。标准组合是日常巡检用增强观测法看大盘故障定位用主动探针法看明细架构评审用混合时间溯源法看全貌快速验证用被动埋点法看基线。在Flink运维的三年中我总结出一条铁律没有银弹只有组合拳。一个成熟的延迟观测体系必然是这四种方法的有机融合根据场景、成本、精度需求动态选用。6. 四种方法的选型决策树与避坑清单面对具体项目如何选择最适合的延迟测量方法我们提炼出一张决策树它不是教科书式的理论框架而是从上百次实战中凝练的血泪经验。6.1 方法选型决策树五问定乾坤第一问你的延迟SLA是多少≤100ms必须用主动探针法。被动埋点的JVM时钟抖动、网络传输不确定性已无法满足亚百毫秒精度要求。100ms~5s被动埋点法是性价比首选配合分层采样可覆盖95%场景。5s增强观测法足够因为业务本身对延迟不敏感重点在稳定性而非精度。第二问你的链路是否包含离线环节是必须上混合时间溯源法否则“端到端”就是伪命题。否前三者任选但需评估改造成本。第三问你的团队是否有中间件改造权限有主动探针法、混合溯源法均可推进。无如使用SaaS版Kafka/Redis只能选被动埋点法或增强观测法。第四问你是否需要分钟级故障归因是主动探针法的Breakdown视图是刚需。否仅需日报/周报被动埋点法的聚合指标足矣。第五问你的数据源头是否可控MySQL/PostgreSQL等自建DB可注入trace_id混合溯源法可行。第三方API/传感器源头不可控被动埋点法是唯一选择需在第一个接收服务如API网关中打时间戳。注意决策树不是单选题。在某广告实时竞价项目中我们同时运行三种方法用增强观测法做大盘监控用被动埋点法测核心竞价路径用主动探针法对Top 10广告主做深度追踪。多管齐下才能应对复杂场景。6.2 血泪避坑清单那些让我们加班到凌晨三点的错误坑一在Flink中用System.currentTimeMillis()打时间戳后果Checkpoint恢复后重放数据的时间戳是恢复时刻而非原始事件时刻导致延迟曲线出现诡异的“断崖式下跌”。正解源头注入时间戳或使用Flink的ProcessingTimeService获取getCurrentProcessingTime()仅适用于Processing Time语义。坑二将Kafka Consumer的fetch.latency.avg当作端到端延迟后果这只是Consumer拉取数据的网络耗时不包含反序列化、Flink处理、Sink写入等。某次误用导致我们花了两天排查不存在的网络问题。正解fetch.latency.avg仅作为网络健康度参考与端到端延迟无直接关系。坑三忽略Flink的minIdleTimeBetweenCheckpoints配置后果当Checkpoint频繁失败时Flink会指数退避导致watermarkLag持续升高被误判为数据延迟。正解设置minIdleTimeBetweenCheckpoints 3000030秒确保Checkpoint有足够喘息时间再结合checkpointAlignmentTime判断真伪。坑四Probe Record的注入频率与业务流量不匹配后果在大促期间10秒一个Probe显得稀疏无法捕捉瞬时抖动而在日常又造成不必要的资源消耗。正解实现动态注入频率基于numRecordsInPerSecond指标自动调节。流量高峰时缩至2秒低谷时扩至30秒。坑五未对延迟数据做异常值过滤后果单次GC Pause如G1 Full GC 8秒导致延迟P99虚高掩盖真实问题。正解在上报前用IQR四分位距算法过滤异常值。公式Q1 - 1.5*IQR至Q3 1.5*IQR之外的数据视为异常不参与聚合。6.3 我的个人经验延迟测量的本质是建立信任最后分享一个感悟。在最初做Flink延迟测量时我执着于技术实现追求毫秒级精度、100%覆盖率。但后来发现延迟测量的终极目标不是得到一个数字而是让业务方、开发方、运维方对“系统是否健康”达成共识。当风控团队说“延迟高”运维说“指标正常”开发说“代码没变”矛盾就产生了。而一套各方认可的延迟测量体系就是这个共识的基石。它不一定是完美的但必须是透明的、可验证的、可归因的。所以我建议每个团队在启动延迟测量项目时先做一件事召集所有相关方共同定义“端到端”的起点、终点、时间基准、误差容忍度。这个过程本身比选择哪种技术方法更重要。毕竟技术只是工具而信任才是实时计算真正的护城河。