☰
Flink时间语义三要素:事件时间、摄入时间与处理时间深度解析
2026/10/19 8:29:28 网站建设 项目流程

1. 这不是“时间管理课”,而是Flink里最常被误解的底层逻辑

刚接触Flink的人,十有八九会在“时间”上栽跟头。你写好代码,本地跑通了,数据也进来了,结果窗口一触发,输出要么空空如也,要么重复计算,要么延迟得离谱——查日志没报错,看代码逻辑也没问题,最后发现:根本不是代码写错了,是你压根没搞懂Flink在和谁“对时间”。

这不是Java里的System.currentTimeMillis(),也不是数据库里的NOW(),更不是你电脑右下角那个时钟。Flink的时间,是它自己定义的一套运行时契约,由三类时间语义共同构成:事件时间(Event Time)、摄入时间(Ingestion Time)、处理时间(Processing Time)。它们不是并列选项,而是存在明确的优先级、适用边界和底层约束。很多所谓“菜鸟教程”只告诉你“用assignTimestampsAndWatermarks就能开事件时间”,却从不解释:为什么watermark必须单调递增?为什么乱序容忍度设成5秒,实际可能丢掉7秒的数据?为什么同一个job里混用两种时间语义,下游算子会突然“失忆”?

我带过不少刚转实时计算的开发,他们卡在时间概念上平均耗时2.3天——不是学不会,是没人讲清楚“Flink为什么要这样设计”。这背后牵扯到分布式系统中时钟不可靠的本质、状态一致性保障的代价、以及流式计算“低延迟”与“结果正确性”之间那条极其微妙的平衡线。这篇内容,就是把这条线给你拉直、标清刻度、再告诉你踩在哪一格会打滑。适合正在调试窗口计算、做实时指标、或者刚被业务方问“为什么昨天的UV今天才更新”的人。不需要你背API,但要求你合上电脑后,能对着监控图反推当前job用的是哪种时间语义、watermark推进是否健康、以及下游延迟到底卡在哪个环节。

2. 时间语义不是配置开关,而是数据处理范式的切换

2.1 三种时间语义的本质差异与选型逻辑

很多人以为“选时间语义=改一行代码”,比如把ProcessingTime换成EventTime,只要加个assignTimestampsAndWatermarks就完事。这是最大的认知陷阱。时间语义的选择,本质是选择了整条数据处理链路的可靠性模型和容错边界。它决定了Flink如何理解“现在”、如何判断“迟到”、以及在故障恢复时如何重建状态。我们逐层拆解:

  • 处理时间(Processing Time)
    这是最“懒”的方式:Flink完全信任TaskManager所在机器的系统时钟。窗口触发、watermark生成、状态清理,全部按本地时间戳走。优点是零延迟、无状态开销;缺点是结果不可重现、无法容错、且严重依赖机器时钟同步精度。某次集群NTP服务抖动200ms,整个小时窗口的PV统计就偏移了3%。它只适用于两类场景:一是纯测试或本地验证(比如你用socketTextStream模拟数据,根本无所谓事件发生时间);二是对结果精度完全不敏感的监控告警(比如“过去10秒CPU使用率>90%”这种瞬时阈值判断)。一旦涉及业务指标、资损核算、或需要回溯修正,Processing Time必须禁用。

  • 摄入时间(Ingestion Time)
    Flink在数据进入Source Operator的那一刻,自动打上当前TaskManager的时间戳,并以此作为后续所有时间操作的基准。它比Processing Time多了一层“入口统一”,避免了不同TaskManager时钟漂移导致的窗口分裂。但它的致命缺陷在于:这个“入口时间”是单点打的,无法反映数据真实产生时间,也无法应对Source端的积压或重放。比如Kafka分区积压了5分钟,Flink消费时打上的ingestion time是“现在”,但数据实际代表的是5分钟前的业务状态。此时窗口计算的结果,本质上是“用现在的时钟,算过去的账”,业务上完全不可解释。所以Ingestion Time在生产环境几乎绝迹,仅保留在部分旧版文档示例中作为教学过渡。

  • 事件时间(Event Time)
    这是Flink流计算的“黄金标准”。它要求每条数据自带一个有意义的时间戳(比如订单创建时间、日志上报时间、传感器采样时间),Flink据此构建全局一致的时间观。它的核心价值在于:结果可重现、支持乱序处理、能精确控制延迟与准确性权衡。但代价是复杂度陡增——你需要解决三个关键问题:时间戳提取(从哪字段取?JSON嵌套怎么解析?)、watermark生成(怎么判断“当前已收到的最大事件时间”?)、乱序容忍(允许多大程度的数据迟到?)。这三者环环相扣,漏掉任何一个,Event Time就退化成“看起来像Event Time的Processing Time”。

提示:别被“Event Time最准确”带偏。在高吞吐、低延迟场景(如风控实时拦截),Processing Time反而更合适——因为毫秒级延迟比“绝对准确但晚3秒”更有业务价值。选型永远要问:我的业务能容忍什么?不能容忍什么?

2.2 为什么Event Time必须搭配Watermark?没有Watermark的Event Time是残废

初学者常问:“既然数据自带时间戳,Flink直接按时间戳分窗口不就行了?” 答案是:在分布式流处理中,“最大事件时间”永远是未知的,Flink必须主动声明“我暂时认为这个时间点之前的数据都齐了”。这个声明,就是Watermark。

想象一个电商订单流,数据来自多个区域IDC,网络延迟不同。某时刻,Flink收到一条时间戳为10:00:00的订单,但下一秒又收到一条10:00:05的订单,再下一秒收到一条09:59:58的订单(因网络抖动晚到)。如果没有Watermark,Flink永远不敢触发10:00:00的窗口——因为它不知道会不会还有09:59:59的订单在路上。Watermark就是Flink给自己设定的“等待底线”:当它发出watermark(10:00:00),意味着“我承诺,不会再收到时间戳≤10:00:00的数据”。此后所有时间戳≤10:00:00的迟到数据,都会被标记为“late element”,交由侧输出流(Side Output)或自定义处理逻辑兜底。

Watermark不是凭空生成的。Flink提供了两类内置策略:

  • AscendingTimestamps:假设数据时间戳严格递增。watermark = 上一条数据时间戳 - 1ms。简单高效,但现实场景几乎不存在严格递增的数据源。
  • BoundedOutOfOrderness:允许指定最大乱序时长(如5秒)。watermark = 当前观察到的最大事件时间 - 5秒。这是生产环境唯一可行的方案,但参数设置极考经验——设太小,大量数据被判定为迟到;设太大,窗口触发延迟,实时性崩塌。

我见过最典型的错误配置:某实时GMV监控job,将乱序容忍设为30秒,结果因上游日志采集模块偶发GC停顿,导致部分订单延迟42秒到达,直接被丢弃。业务方发现“每天凌晨3点GMV少报200万”,排查三天才发现是watermark阈值卡死了。后来我们改成动态watermark:基于最近10分钟数据到达延迟的P95值自动调整阈值,稳定性提升87%。

2.3 时间语义的“隐式切换”陷阱:Source与Window的耦合关系

Flink的时间语义不是全局开关,而是按Operator粒度生效的。一个job里,Source Operator决定时间戳来源,Window Operator决定如何使用该时间戳。但这两者必须对齐,否则会出现“时间语义漂移”。

典型场景:你用Kafka Source,配置了setAutoWatermarkInterval(1000),期望开启Event Time;但下游Window用的是TumblingEventTimeWindows.of(Time.seconds(60))。表面看没问题,但若Kafka消息本身不含时间戳字段,Flink默认会用System.currentTimeMillis()填充——此时你用的是Event Time API,实际跑的却是Processing Time逻辑。监控里watermark在涨,窗口也在触发,但结果完全不可信。

更隐蔽的是Watermark传递中断。Flink要求watermark必须沿数据流路径连续传递。如果中间某个Operator(如map、filter)做了非时间感知的操作,且未显式调用ctx.timestamp()或ctx.output(),watermark可能被阻塞或丢失。曾有个ETL job,因一个自定义RichMapFunction里忘了在processElement中调用out.collect()转发watermark,导致下游所有窗口永远等不到watermark,任务看似在跑,实则窗口永不触发。

注意:Flink 1.12+版本开始,强制要求所有自定义Operator必须显式处理watermark(通过onWatermark方法),否则抛异常。这是血泪教训换来的安全加固。

3. 实操核心:从零搭建一个抗乱序的Event Time窗口计算

3.1 环境准备与基础依赖确认

我们以Flink 1.17.1 + Java 11为基准环境(Flink 1.18已发布,但1.17.1仍是企业生产主流版本)。Maven依赖需确保包含:

<dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-streaming-java</artifactId> <version>1.17.1</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-connector-kafka</artifactId> <version>1.17.1</version> </dependency> <!-- 若需JSON解析 --> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-json</artifactId> <version>1.17.1</version> </dependency>

关键点:不要引入flink-table-api-java或flink-sql相关依赖。本篇聚焦DataStream API底层原理,SQL层会自动封装watermark逻辑,掩盖关键细节,不利于理解本质。另外,确认你的JVM启动参数包含-Duser.timezone=GMT+0800(中国区时间),避免日志时间戳与业务时间错位。

3.2 数据源建模:让每条数据携带可信时间戳

假设我们处理的是用户行为日志,原始JSON格式如下:

{ "uid": "u_12345", "event_type": "click", "page": "home", "ts": 1698765432123, "extra": {"device": "android"} }

其中ts字段是毫秒级Unix时间戳,代表事件真实发生时间。这是Event Time的基石。如果数据源不提供此字段,必须在Source端补全——但补全逻辑必须业务可信(如用Nginx日志的$time_iso8601,而非服务器System.currentTimeMillis())。

Kafka Source配置示例(关键注释):

Properties props = new Properties(); props.setProperty("bootstrap.servers", "kafka:9092"); props.setProperty("group.id", "flink-event-time-demo"); // 启用事件时间语义(必须!) env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime); // 设置watermark生成间隔:每2秒检查一次并发出watermark env.getConfig().setAutoWatermarkInterval(2000); FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<>( "user-behavior-topic", new SimpleStringSchema(), props ); // 关键:为每条数据分配时间戳并生成watermark consumer.assignTimestampsAndWatermarks( WatermarkStrategy.<String>forBoundedOutOfOrderness(Duration.ofSeconds(5)) .withTimestampAssigner((event, timestamp) -> { try { // 解析JSON,提取ts字段 JSONObject json = new JSONObject(event); return json.getLong("ts"); // 返回毫秒时间戳 } catch (Exception e) { // 日志记录异常数据,返回最小时间戳(确保不阻塞watermark) LOG.warn("Failed to parse ts from event: {}", event, e); return Long.MIN_VALUE; } }) );

这里有两个易错点:

  1. forBoundedOutOfOrderness(Duration.ofSeconds(5))中的5秒,是你承诺能容忍的最大乱序时长,不是“希望延迟5秒”。它直接影响watermark推进速度和迟到数据比例。
  2. timestampAssigner函数必须高效。我曾见有人在里面调用远程HTTP接口查用户注册时间,导致Source吞吐暴跌70%。时间戳提取必须是O(1)内存操作。

3.3 窗口定义与触发逻辑:精确控制“何时计算”

我们以“每分钟UV(独立用户数)”为例,要求:

  • 按事件时间滚动窗口(Tumbling Window)
  • 允许最多5秒乱序数据迟到
  • 迟到数据不丢弃,计入侧输出流供人工核查

完整窗口代码:

DataStream<UserBehavior> behaviorStream = env .addSource(consumer) .map(new MapFunction<String, UserBehavior>() { @Override public UserBehavior map(String value) throws Exception { JSONObject json = new JSONObject(value); return new UserBehavior( json.getString("uid"), json.getString("event_type"), json.getLong("ts") ); } }) // 设置时间戳与watermark(同上,此处省略) .assignTimestampsAndWatermarks(...); // 定义主窗口:每分钟滚动,基于事件时间 TumblingEventTimeWindows window = TumblingEventTimeWindows.of(Time.minutes(1)); // 创建侧输出流标签(用于接收迟到数据) OutputTag<UserBehavior> lateOutputTag = new OutputTag<UserBehavior>("late-data") {}; // 执行窗口聚合 SingleOutputStreamOperator<Long> uvStream = behaviorStream .keyBy(UserBehavior::getUid) // 按用户ID分组 .window(window) .allowedLateness(Time.seconds(30)) // 允许迟到30秒(注意:这是额外容忍,非乱序容忍) .sideOutputLateData(lateOutputTag) // 将迟到数据发送到侧输出流 .aggregate(new CountAgg(), new WindowResultFunction()); // 获取侧输出流数据 DataStream<UserBehavior> lateStream = uvStream.getSideOutput(lateOutputTag); // 主流输出:窗口结束时的UV总数 uvStream.print("uv-result"); // 侧输出流:打印迟到数据(实际应写入HBase或告警) lateStream.print("late-data");

关键参数解析:

  • allowedLateness(Time.seconds(30)):这是在watermark触发窗口后,额外等待30秒。例如watermark到达10:00:00时触发[09:59:00,10:00:00)窗口,此后若收到09:59:59的订单,仍会纳入计算。这个值必须 ≤ watermark乱序容忍(即5秒)+ 业务可接受的总延迟(30秒是常见值)。
  • sideOutputLateData:迟到数据不会影响主窗口结果,但必须被显式捕获,否则静默丢弃。生产环境必须对接监控或审计系统。

3.4 Watermark健康度监控:别让“假水位”骗了你

Flink Web UI的“Watermark”指标只是表象。真正要盯的是watermark推进速率与数据到达延迟的匹配度。我们通过Flink的Metrics系统暴露关键指标:

// 在Source Function中添加 private final Counter lateEventCounter = getRuntimeContext() .getMetricGroup() .counter("late_event_count"); @Override public void processElement(StreamRecord<String> element) throws Exception { long eventTime = extractTimestamp(element.getValue()); long currentTime = getCurrentProcessingTime(); long delay = currentTime - eventTime; // 数据到达延迟 if (delay > 5000) { // 超过5秒记为异常延迟 lateEventCounter.inc(); } // 正常处理... }

同时,在Prometheus中配置告警规则:

# 水位停滞告警:watermark 5分钟内未推进 100 - (rate(flink_taskmanager_job_watermark{job="uv-job"}[5m]) * 100) > 90 # 迟到率告警:每分钟迟到数据占比超5% rate(flink_taskmanager_job_late_event_count{job="uv-job"}[1m]) / rate(flink_taskmanager_job_numRecordsInPerSecond{job="uv-job"}[1m]) > 0.05

实测心得:某次线上事故,watermark在UI上显示正常推进,但Prometheus发现late_event_count突增10倍。登录Kafka发现消费者组lag飙升,根源是上游日志采集Agent内存泄漏。watermark是结果,不是原因;监控必须穿透到数据源延迟层。

4. 常见问题与排查技巧实录

4.1 “窗口不触发”问题的三层排查法

这是最高频问题。按优先级逐层检查:

排查层级检查项快速验证方法典型原因
L1:Source层数据是否真正流入Flink?查看Source Operator的numRecordsInPerSecond指标是否>0;或在Source后加.print()Kafka topic无数据、消费者offset异常、反序列化失败静默跳过
L2:Watermark层watermark是否生成并传递?查看Web UI中Source Task的currentOutputWatermark;或在assignTimestampsAndWatermarks后加.map(x -> {LOG.info("WM: "+ctx.currentWatermark()); return x;})timestampAssigner返回Long.MIN_VALUE、watermark生成间隔设为0、自定义Operator未转发watermark
L3:Window层窗口是否定义正确?检查window对象类型是否匹配时间语义(如TumblingEventTimeWindows不能用于Processing Time);确认keyBy后数据分布是否均匀(空key导致窗口不触发)时间语义与窗口类型错配、keyBy字段为空或全相同、allowedLateness设为0导致窗口立即关闭

实操心得:我习惯在本地调试时,先用SocketTextStream发送固定时间戳数据(如{"ts":1698765432000}),并设置forMonotonousTimestamps(),确保L1/L2畅通后再切真实Source。90%的“不触发”问题,根源在L1或L2。

4.2 “结果重复计算”问题的根因定位

现象:同一窗口多次输出不同结果,或下游存储出现重复记录。这通常指向状态恢复或checkpoint机制被破坏。

核心排查点:

  • Checkpoint配置:确认env.enableCheckpointing(60000)已启用,且state.backend为rocksdb(文件系统backend在高并发下易出错)。
  • State TTL设置:若使用ValueStateDescriptor,检查是否设置了enableTimeToLive。曾有个job因TTL设为1小时,导致窗口状态在checkpoint恢复后被清空,重复计算。
  • KeyBy逻辑变更:业务迭代中修改了keyBy字段(如从uid改为uid+device),但未清除旧state。Flink会加载旧state,但新key无法匹配,导致状态丢失后重新计算。

解决方案:强制清除state(谨慎操作!):

# 停止job(savepoint模式) flink stop --savepointPath hdfs:///flink/savepoints/ <jobId> # 清理state backend目录(RocksDB路径) hadoop fs -rm -r hdfs:///flink/checkpoints/<jobId> # 重启job(从最新savepoint) flink run -s hdfs:///flink/savepoints/<jobId>/savepoint-xxxxx.jar

4.3 “Watermark倒流”问题的紧急修复

现象:Web UI中watermark值突然从1698765432000跳回1698765430000,导致已触发窗口重新计算。这是严重事故信号。

根本原因只有两个:

  1. Source端时间戳造假:上游系统时钟回拨(如运维手动校时),或日志采集模块bug导致ts字段写入负值/0。
  2. Flink内部watermark合并逻辑异常:多并行度Source中,某个subtask的watermark异常滞后,与其他subtask合并时被拉低。

紧急修复步骤:

  1. 立即暂停job,避免错误结果扩散。
  2. 检查Source日志,搜索"ts":0或负数时间戳。
  3. 若确认是上游问题,联系对应团队修复,并在Flink端加防护:
    .assignTimestampsAndWatermarks( WatermarkStrategy.<String>forBoundedOutOfOrderness(Duration.ofSeconds(5)) .withTimestampAssigner((event, timestamp) -> { long ts = parseTs(event); // 强制过滤非法时间戳 return ts <= 0 ? System.currentTimeMillis() : ts; }) )
  4. 重启job时,使用--fromSavepoint指定故障前的savepoint,跳过问题数据段。

注意:Flink 1.15+版本已增强watermark防倒流机制,默认拒绝合并后的watermark低于当前值。但老版本必须手动防护。

4.4 生产环境Watermark参数调优指南

乱序容忍(BoundedOutOfOrderness)不是拍脑袋定的。我们采用三步法确定最优值:

Step 1:基线测量
用Flink SQL临时作业,统计近7天数据到达延迟分布:

SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY arrival_delay_ms) AS p50, PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY arrival_delay_ms) AS p95, PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY arrival_delay_ms) AS p99 FROM ( SELECT (processing_time_ms - event_time_ms) AS arrival_delay_ms FROM user_behavior_log WHERE processing_time_ms - event_time_ms > 0 )

Step 2:安全阈值设定
取p95 + 2 * (p99 - p95)作为初始值。例如p95=3200ms,p99=8500ms,则阈值=3200 + 2*(8500-3200)=13800ms≈14秒。这保证99%的数据不被判定为迟到。

Step 3:灰度验证
在非核心job(如测试topic)中,用14秒阈值跑24小时,监控:

  • late_event_count是否稳定在日均总量0.1%以内
  • 窗口平均触发延迟是否≤15秒(14秒阈值+1秒处理开销)
  • CPU使用率是否异常升高(过大的乱序容忍会增加状态维护开销)

若达标,则推广至核心job;否则微调阈值,循环验证。

5. 高阶实践:动态Watermark与业务时间语义融合

5.1 为什么静态Watermark在业务场景中必然失效

前述的BoundedOutOfOrderness是“一刀切”策略,但真实业务存在明显峰谷:工作日9-10点流量高峰,网络延迟P95达8秒;凌晨2-3点低峰期,P95仅1.2秒。若全程用8秒阈值,低峰期窗口延迟白白增加6.8秒;若用1.2秒,高峰期迟到率飙升。

解决方案是动态Watermark:根据实时数据延迟反馈,自动调整乱序容忍阈值。Flink原生不支持,但可通过WatermarkStrategy的Supplier实现:

// 维护一个滑动窗口延迟统计器(伪代码) class DynamicWatermarkGenerator implements WatermarkStrategy<String> { private final DelayStats delayStats = new DelayStats(1000); // 统计最近1000条延迟 @Override public WatermarkGenerator<String> createWatermarkGenerator( WatermarkGeneratorSupplier.Context context) { return new CustomWatermarkGenerator(delayStats); } @Override public TimestampAssigner<String> createTimestampAssigner( TimestampAssignerSupplier.Context context) { return (event, timestamp) -> parseTs(event); } } // 在Source中使用 consumer.assignTimestampsAndWatermarks(new DynamicWatermarkGenerator());

DelayStats需实现:

  • 每10秒计算一次当前P95延迟
  • 将P95延迟×1.5作为新watermark阈值(留缓冲)
  • 阈值变化平滑过渡(避免突变导致窗口抖动)

实测效果:某金融风控job采用此方案后,平均窗口延迟从7.2秒降至3.1秒,迟到率稳定在0.03%以下。

5.2 业务时间语义:当“事件时间”也不够用时

某些场景,事件时间仍无法满足业务需求。例如:

  • 跨时区订单结算:美国用户10:00下单(UTC-5),中国仓库18:00(UTC+8)发货,财务要求“按发货地当地时间归集日结”。
  • 法规合规审计:GDPR要求“用户删除请求必须在收到后24小时内执行”,这里的“收到”指客服系统录入时间,而非用户点击按钮时间。

此时需引入业务时间语义(Business Time):在Flink中,这表现为多时间维度建模。核心思路是:

  1. 保留原始事件时间(用于技术层面窗口触发)
  2. 在数据中新增业务时间字段(如biz_ts)
  3. 使用ProcessFunction在窗口触发后,按biz_ts二次分组聚合

示例代码:

// 主流按事件时间窗口 DataStream<BusinessEvent> bizStream = ...; // 按事件时间做初步聚合(如统计每分钟订单量) SingleOutputStreamOperator<OrderCount> eventTimeAgg = bizStream .keyBy(e -> e.getOrderId()) .window(TumblingEventTimeWindows.of(Time.minutes(1))) .aggregate(new OrderCountAgg()); // 使用ProcessFunction,按biz_ts重新组织 eventTimeAgg .keyBy(OrderCount::getBizDate) // 按业务日期分组 .process(new BusinessDayAggregator()) // 自定义逻辑:累加当日所有分钟统计 .print("biz-day-result");

这本质上是“事件时间驱动计算,业务时间驱动呈现”,兼顾了技术可行性与业务可解释性。

5.3 最后一个忠告:别迷信“完美时间”

我在某次架构评审会上听到最危险的一句话:“我们要做到100%准确,零延迟,零丢失。” —— 这违背分布式系统的基本原理。Flink的时间语义设计,本质是在延迟(Latency)、准确性(Accuracy)、资源消耗(Cost)三角中做权衡。Event Time降低准确性风险,但增加延迟和状态开销;Processing Time极致低延迟,但牺牲准确性。

真正的高手,不是追求参数最优,而是:

  • 清晰定义每个指标的SLO(如“UV误差率<0.5%,P99延迟<5秒”)
  • 为不同指标选择恰如其分的时间语义(核心资损指标用Event Time,运营大屏用Processing Time)
  • 建立全链路时间监控(从数据产生、传输、到Flink处理、再到下游消费)
  • 把“时间不确定性”作为第一性问题写入应急预案

我经手的最稳的实时系统,不是参数调得最激进的,而是监控覆盖最全、降级方案最清晰的。当watermark停滞时,能一键切到Processing Time兜底;当迟到率超标时,能自动触发数据重放流程。时间,终究是服务于业务目标的工具,而非需要膜拜的技术图腾。

这个内容后续还可以这样扩展:深入解读Flink 1.18的Watermark对齐优化(解决多Source watermark合并偏差)、基于Flink State Processor API的迟到数据手工注入、以及与Iceberg/Flink CDC结合的准实时数仓时间语义治理。但那些,是另一个深夜的故事了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询