每天清晨,快递驿站都会涌进成千上万的包裹。你注意过它们是怎么被快速分好的吗?传送带不停滚动,每个工位的工人只干一件事:这个扫码、那个称重、下一个贴单、再下一个分区。包裹一到工位,工人就条件反射地处理它——这就是“事件驱动”。攒够一车才发走,这就是“窗口”。工位之间的传送带会根据下游速度自动调速,防止包裹堆积——这就是“背压”。而 Apache Pulsar 的 Functions 就像一个把整套快递驿站搬进计算机的魔法:让一段段代码变成流水线上的工位,数据像包裹一样源源不断流过,每个工位只处理一条消息,处理完传给下一个,全程无需你管理服务器。这就是 Serverless 流处理——把“流”变成“流水线”,把“处理”变成“干活”。

岗位关联

掌握 Pulsar Functions,等于掌握了构建实时数据处理管道的最小原子能力,是迈向云原生架构师与实时平台工程师的必修课。

目录
  1. 可视化演示
  2. 函数计算入门
  3. 事件触发机制
  4. 源与汇
  5. 窗口聚合
  6. 函数编排
  7. 状态与可靠性
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示输入Topic到函数执行器到输出Topic的实时数据处理流程、Thread Process Kubernetes三种部署模式的隔离与开销对比、以及Broker计算BookKeeper存储Function服务三层分离架构。

本节配有交互动效(进入视口后自动加载,约 3KB)。

输入到输出的函数处理流程 执行器订阅input topic接收消息 process方法处理消息发布到output In→Process→Out 结果经Sink落库或进入下游topic

函数计算入门

走进快递驿站,你最先看到的往往是入口处的扫码工位。工人举起扫码枪,对着包裹上的条形码“嘀”一声,屏幕上就跳出一行记录。这个工位不关心包裹从哪来、要送到哪去,它只做一件事:读取信息,输出结果。在 Pulsar 的世界里,这样的工位就是 Function——一段部署在 Pulsar 上的轻量级处理逻辑。每当一条消息从 Topic(消息队列)中被取出,Function 就会被调用一次,像扫码枪一样“嘀”一声完成处理。

Pulsar Functions 借鉴了 Serverless 的思想:你只管写“扫码逻辑”(处理函数),不用管扫码枪插在哪个电源上(服务器资源)。Pulsar 负责消息的存储、分发、重复处理与故障转移。Function 的输入是消息,输出可以是另一种消息(发往新 Topic),也可以只是打印一条日志或更新一个外部系统。这种“一进一出”的模型,让每个函数都可以独立开发、独立扩展。

从数学视角看,一个函数就是输入到输出的映射:

传统应用是“常驻服务”,像驿站里坐在柜台后等客户上门的店员;而 Pulsar Functions 是“按消息触发”,像流水线上的扫码工位,包裹到了才干活。理解这一点,你就抓住了 Serverless 流处理的核心:计算不再是常开的灯,而是来人即亮的感应灯。

y=f(x)y = f(x)
输入消息 \(x\) 经过函数逻辑 \(f\) 变换,得到输出消息 \(y\)。这是理解一切流处理的基本单元。
  1. 输入消息一条消息作为自变量 \(x\) 进入函数,类似包裹被放到扫码台上
    xx
  2. 函数映射函数 \(f\) 对 \(x\) 执行处理逻辑,输出结果 \(y\)
    y=f(x)y = f(x)

事件触发机制

驿站里最响亮的不是工人的吆喝,而是自动分拣系统的提示音:“滴!包裹到 3 号工位!” 工人不需要站在门口望眼欲穿地等包裹(那是轮询),也不需要每隔几秒跑去看一眼传送带(那是定时轮询)。只要提示音一响,手头的活就开始干。这就是事件驱动:由外部事件(消息到达)触发处理的开始。

Pulsar Functions 的触发机制分几种。最常见的是消息触发:任何一条消息写入输入 Topic,函数立刻被调用。还有定时触发:像驿站每天早上九点固定盘点一样,函数可以定时执行(如每 5 分钟汇总一次指标)。更精细的是条件过滤:函数可以对每条消息先做判断,满足条件的才继续处理,不满足的静默丢弃,就像扫码枪只认带条码的包裹,没条码的直接推下传送带。

用数学表达条件触发逻辑:

事件驱动最大的优点是“零空转”:系统在无消息时不消耗处理资源,消息一到立即响应。相比传统轮询方式——相当于驿站工人每分钟去门口看一眼有没有货车——事件驱动让每一分算力都花在刀刃上,也让响应延迟从“轮询间隔”降为“消息到达即处理”。

G(x)={f(x),消息满足触发条件∅,消息被丢弃G(x) = \begin{cases} f(x), & \text{消息满足触发条件} \\ \varnothing, & \text{消息被丢弃} \end{cases}
每条消息先经过条件闸门:符合条件就交给处理函数,不符合就丢弃。
  1. 条件判断先通过一个谓词检查消息是否满足预设条件,等价于检查包裹是否贴了有效条码
    trigger(x)=true\text{trigger}(x) = \text{true}
  2. 命中分支条件满足时执行主函数,消息被真正加工
    G(x)=f(x)G(x) = f(x)
  3. 未命分支条件不满足时返回空结果,等价于将包裹推入回收筐
    G(x)=∅G(x) = \varnothing

源与汇

驿站的运转离不开两个门:收货口和发货口。清晨,一辆辆货车从收货口卸下包裹;傍晚,分拣完成的包裹从发货口装上另一批车运走。在 Pulsar Functions 中,这两个口子分别叫 Source 和 Sink。Source 负责把外部系统的数据拉进 Pulsar——比如从数据库日志、Kafka、MQTT 传感器接入消息;Sink 则把处理结果推给外部系统——比如写入 Elasticsearch、MySQL 或调用告警平台的 Webhook。

中间连接的 Topic 就像驿站里的传送带,把 Source 进来的原始数据运给函数,再把函数产出的结果运给 Sink。Pulsar 提供了丰富的连接器生态:Debezium 连接器可以把 MySQL 的 binlog 变成消息流,Elasticsearch Sink 可以把处理后的订单数据实时写入索引。你不需要自己写复杂的客户端代码,配置好连接器,数据就像接好了管道一样自动流动。

衡量一个“门”吞吐能力的核心指标是数据速率:

Source 决定了驿站“能不能及时收货”,Sink 决定了驿站“能不能及时发货”。一个健壮的流处理管道,不仅要让函数跑得快,还要让进出的门足够宽。连接器生态的价值正在于此:把全球各地的“货车”和“收货口”都标准化,让你像搭积木一样组合数据管道。

T=BtT = \frac{B}{t}
吞吐量 \(T\) 等于单位时间 \(t\) 内成功搬运的数据总量 \(B\)。
  1. 数据总量\(B\) 表示统计周期内进入或流出系统的消息总字节数,类似一车货的总重量
    BB
  2. 时间跨度\(t\) 表示统计所用的时间长度,比如 1 秒或 1 分钟
    tt
  3. 速率计算总量除以时间,得到每秒搬运多少数据的吞吐速率
    T=BtT = \frac{B}{t}

窗口聚合

驿站每天要统计“今天发了多少车货”,但包裹是源源不断进来的,不可能等所有包裹到齐再统计——那要等到猴年马月。于是站长规定:要么每凑满一百个包裹就发一车(计数窗口),要么每过十分钟就发一车(时间窗口),要么每五分钟统计一次“最近十分钟发出多少件”(滑动窗口)。这些规则在流处理中就叫窗口(Window)。

为什么需要窗口?因为流是无穷无尽的,而聚合计算(求平均、求和、计数)需要一个“边界”。窗口就是给无限数据流切出的一块块有边界的切片。时间窗口按时间跨度切分,适合“每 5 分钟的平均 CPU 使用率”;计数窗口按消息条数切分,适合“每 100 笔订单统计总额”;滑动窗口则让窗口与窗口之间重叠推进,适合“任意时刻看最近十分钟的流量”。

窗口内最常见的操作之一是求平均:

窗口把“永远算不完的流水”变成“一段段可以结算的账本”。它让实时系统不必等待数据终结,而是在数据流动的过程中就能持续产出统计结果。选对窗口类型,就像选对发车规则一样,决定了你的数据是及时但粗略,还是完整但延迟。

xˉ=1n∑i=1nxi\bar{x} = \frac{1}{n} \sum_{i=1}^{n} x_i
窗口内 \(n\) 条消息的数值求和后除以 \(n\),得到窗口均值。这是窗口聚合的典型形态。
  1. 窗口切片从无限流中截取 \(n\) 条消息作为当前窗口
    x1,x2,…,xnx_1, x_2, \ldots, x_n
  2. 逐条累加对窗口内所有消息的数值字段求和
    ∑i=1nxi\sum_{i=1}^{n} x_i
  3. 平均归约总和除以消息条数,得到窗口平均值
    xˉ=1n∑i=1nxi\bar{x} = \frac{1}{n} \sum_{i=1}^{n} x_i

函数编排

单靠一个扫码工位显然撑不起整个驿站。真正的流水线是:扫码 → 称重 → 贴单 → 分区,环环相扣。Pulsar Functions 也支持这种编排:你可以把多个函数串联成一条处理链,前一个函数的输出,就是后一个函数的输入。比如先从日志消息中提取 IP(函数 A),再根据 IP 查询归属地(函数 B),最后把结果写入告警库(Sink)。

除了串联,还有扇入(Fan-in)与扇出(Fan-out):多个 Topic 的消息汇聚到一个函数处理(多个进货口对一条传送带),或者一个函数的结果广播给多个下游 Topic(一条传送带分叉到多个发货口)。函数的并行度(实例数)决定了一个工位同时开几个窗口:并行度越高,单位时间能处理的包裹越多,但也会占用更多资源。每个函数实例独立处理不同消息,由 Pulsar 负责分发。

多个函数串联的数学本质是复合函数:

编排让简单函数组合出复杂能力,但代价是每增加一个环节就增加一跳延迟和故障点。优秀的架构师像经验丰富的驿站站长:既要合理拆分工位(函数粒度),又要控制流水线长度(拓扑深度),还要为热门工位多开窗口(并行度),让整条流水线既高效又可控。

y=g(f(x))y = g(f(x))
消息 \(x\) 先过函数 \(f\),结果再过函数 \(g\),得到最终输出 \(y\)。
  1. 内层函数消息先交给第一个工位 \(f\) 处理,等价于扫码工位先记录包裹
    u=f(x)u = f(x)
  2. 外层函数中间结果 \(u\) 再交给第二个工位 \(g\),等价于称重工位接着干活
    y=g(u)y = g(u)

状态与可靠性

驿站里每个工位都有一本台账:今天处理了多少件、哪些客户是加急件、哪批货存在异常。没有台账,工位就“失忆”了——处理完这条消息就忘记一切。Pulsar Functions 的状态存储(State Store)就是这本电子台账,让函数能跨消息记录信息,比如统计“当前批次累计处理了多少条消息”或“这个设备最近的平均温度”。

当传送带上的包裹突然暴增,下游工位忙不过来怎么办?驿站的办法是让上游减速——这就是背压(Backpressure)。系统检测到下游处理速率跟不上时,会主动限制上游发送速度或暂存数据,避免消息无限堆积导致内存溢出。背压不是故障,而是系统的自我保护本能,就像水管过载时泄压阀自动打开。

用速率关系判断背压:

可靠性还体现在“事后理赔”:如果某个工位在处理包裹时突然死机,Pulsar 会记下这条消息还没处理完成,重启后重新投递(故障恢复);配合幂等输出(同一任务执行多次结果一致),避免重复处理造成账目错乱。台账、减速带和理赔规则三管齐下,驿站在包裹洪峰中也能不崩不乱、不丢不重。

λ>μ⇒积压\lambda > \mu \Rightarrow \text{积压}
当消息到达速率 \(\lambda\) 超过函数处理速率 \(\mu\) 时,系统进入积压状态,触发背压调节。
  1. 到达速率\(\lambda\) 表示单位时间进入系统的消息数,由上游生产者决定
    λ\lambda
  2. 处理速率\(\mu\) 表示函数单位时间能处理的消息数,由函数逻辑和并行度决定
    μ\mu
  3. 比较判定当 \(\lambda\) 大于 \(\mu\) 时,消息会堆积,系统触发背压
    λ>μ⇒积压\lambda > \mu \Rightarrow \text{积压}

AI 应用场景

实时日志清洗与异常告警

阶段:数据接入与预处理

解决的问题:传统做法是写常驻 Consumer 程序轮询拉取日志,既占用服务器资源,又要处理各种非法格式和异常分支,代码臃肿难维护。使用 Pulsar Functions 之后,每个处理逻辑成为一个独立函数:一个函数负责解析 JSON 日志,一个函数负责过滤 ERROR 级别记录,一个 Sink 把结果推送至告警平台,逻辑清晰且自动伸缩。

具体案例:电商大促期间,订单服务每秒产出上万条日志。团队用 Source 连接器将日志接入 Pulsar Topic,部署三个 Functions 分别完成格式解析、敏感词脱敏、异常码提取,最后通过 Sink 将告警写入钉钉机器人。整个管道无一台常驻处理服务器,大促流量翻倍时函数实例自动扩容。

物联网传感器数据窗口聚合

阶段:流式计算与聚合分析

解决的问题:冷链运输中每辆货车上的温度传感器每秒上报一次数据,单看每一条毫无意义,必须按窗口聚合才能发现异常。用 Pulsar Functions 的窗口语义,对每条温度消息做时间窗口平均,窗口结果直接驱动告警。

具体案例:一家生鲜电商在每辆冷藏车上部署温度传感器,通过 MQTT Source 接入 Pulsar。一个 Function 以 60 秒为窗口计算平均温度,另一个 Function 检测“窗口均值超过 8 摄氏度”时触发 Sink 推送到冷链管理大屏。司机和管理员能实时看到温度趋势,在变质前及时干预。

金融交易实时风控串联

阶段:实时特征处理与决策

解决的问题:交易风控需要多步判断:先做黑名单比对(函数 A),再做金额异常检测(函数 B),最后计算窗口内高频交易次数(函数 C)。传统微服务链路复杂、延迟高,而 Functions 串联让每一步都是无状态轻量计算,天然适配流水线。

具体案例:某支付平台将每笔交易消息接入 Topic,函数 A 查询用户黑名单状态,函数 B 检测单笔金额是否超过阈值,函数 C 基于 10 秒滑动窗口统计该用户交易频次,任一环节命中风险即通过 Sink 写入风控决策引擎,处理延迟控制在百毫秒内。

Python 演示

python91 行
import randomimport time  class Message:    """一条待处理的消息,模拟 Pulsar 中的一条数据。"""    def __init__(self, data, topic):        self.data = data        self.topic = topic     def __repr__(self):        return "<Message data=%s topic=%s>" % (self.data, self.topic)  class Function:    """Pulsar Function 的基类:一个只会做一件事的工位。"""    def __init__(self, name):        self.name = name        self.msg_count = 0     def process(self, message):        raise NotImplementedError("子类必须实现 process 方法")     def __call__(self, message):        self.msg_count += 1        return self.process(message)  class UpperFunction(Function):    """工位 A:把消息文本转为大写。"""    def process(self, message):        message.data = message.data.upper()        message.topic = "topic-upper"        return message  class FilterFunction(Function):    """工位 B:只放行包含 ERROR 的消息,其余丢弃。"""    def process(self, message):        if "ERROR" in message.data:            return message        return None  class FunctionPipeline:    """把多个 Function 串联成一条流水线。"""    def __init__(self, functions):        self.functions = functions        self.invoked = 0     def run(self, message):        self.invoked += 1        current = message        for fn in self.functions:            current = fn(current)            if current is None:                return None        return current  def source_messages():    """模拟数据源:源源不断地产生消息。"""    samples = [        "user:123 login ok",        "server:api error timeout",        "pay:order 9988 success",        "cache:error evict expired key",    ]    for _ in range(6):        yield Message(random.choice(samples), "topic-input")        time.sleep(0.2)  def main():    pipeline = FunctionPipeline([        UpperFunction("大写转换"),        FilterFunction("错误过滤"),    ])    seen = 0    passed = 0    for msg in source_messages():        seen += 1        result = pipeline.run(msg)        if result is not None:            passed += 1            print("通过: %s" % result.data)    print("共消费 %d 条消息,通过 %d 条,过滤 %d 条" % (seen, passed, seen - passed))  if __name__ == "__main__":    main()
目的:用纯 Python 模拟 Pulsar Functions 的“源 → 函数链 → 输出”流处理管道,演示函数串联与条件过滤的核心思想。
  1. class Function:定义函数基类,等价于流水线上的通用工位骨架。
  2. self.msg_count += 1每个函数实例累计处理条数,体现函数的轻量状态记录。
  3. def __call__(self, message):让函数对象本身可被调用,等价于工位“接住包裹”的动作。
  4. def process(self, message):子类必须实现的处理逻辑,对应真正干活的内容。
  5. if "ERROR" in message.data:条件过滤的模拟,不满足条件的消息被丢弃。
  6. current = fn(current)消息在流水线上依次传给下一个工位,体现函数串联。
  7. yield Message(random.choice(samples), "topic-input")模拟 Source 不断产出消息。

常见误区

误区我写了一个函数,它会像微服务一样一直运行在服务器上,我可以随时远程调用它。

正解Pulsar Functions 是消息触发的无状态计算单元,不是常驻服务。它只在消息到达时被调用,处理完就结束本轮执行。就像驿站里的扫码枪,有包裹才“嘀”一声,没包裹就安静待着。它没有对外暴露的 IP 和端口,也不支持远程调用。

为何常见:很多人刚从微服务架构转过来,习惯性地把一切代码块都想象成常驻进程。

误区窗口就是一块大内存,数据一直往里放,等到我想查的时候再统一计算。

正解窗口必须有边界,要么按时间(如 5 分钟窗口),要么按条数(如 100 条窗口)。窗口到达边界后会自动触发计算并清理数据,为下一个窗口腾出空间。无限累积会让内存耗尽,系统在真实场景中会通过背压或淘汰策略保护自己。

为何常见:直觉上“等数据齐了再算”很方便,但流数据永远不齐,必须用窗口主动切分。

误区把处理逻辑尽量拆细,拆成十个函数串起来,每个函数都很简单,整体一定更快。

正解每增加一个函数节点,消息就要多一次发布订阅写入和网络传输,延迟线性增加,吞吐反而可能下降。合理做法是在“函数内做多件事”和“拆分函数独立扩展”之间权衡。就像驿站流水线,工位太多,光传包裹就累死人。

为何常见:微服务的“拆得越细越灵活”观念被错误地搬到了函数编排上。

误区既然 Serverless 免运维,那我完全不用管内存、并行度,平台会自动搞定一切。

正解Serverless 免的是服务器管理,不是免配置。并行度(实例数)、内存、触发 Topic 等仍需你根据预期流量设定。并行度设太低会积压,设太高会浪费资源甚至触发背压。

为何常见:平台宣传“无需管理服务器”被过度解读为“无需任何配置”。

误区看到系统出现背压告警,第一反应是哪里故障了,赶紧重启服务。

正解背压是系统在自我保护,它主动让上游放慢速度,避免消息无限堆积导致内存溢出。背压存在说明下游处理速度暂时跟不上,正确的排查是看下游函数是否出现瓶颈、并行度是否需要调整,而不是恐慌式重启。

为何常见:告警信息带着“压”字,天然让人觉得是坏事,其实它是设计良好的系统的正常调节机制。

面试题

Q1. Pulsar Functions 中一条消息触发函数执行的方式是?

单选题 · easy

  • A.函数作为常驻进程持续运行
  • B.消息到达时自动触发函数← 正确答案
  • C.需要手动编写循环拉取消息
  • D.定时批量读取所有待处理消息

答案:B。Pulsar Functions 是事件驱动模型,消息写入输入 Topic 后自动触发函数执行,无需常驻进程或手动轮询。A 错在函数不是常驻服务;C 和 D 是轮询/批量的旧思路,与事件驱动背道而驰。

Q2. Pulsar Functions 默认的计算状态特性是?

单选题 · easy

  • A.默认有状态持久化存储
  • B.默认无状态地进行流式计算← 正确答案
  • C.状态自动存入外部普通数据库
  • D.状态随每条消息自动转发清除

答案:B。Pulsar Functions 默认是无状态的,函数实例之间不共享内存状态,每次调用相互独立。若需跨消息记忆,要显式使用 State Store。A 是错误的反向认知;C 混淆了状态存储与外部数据库;D 的描述不符合事实。

Q3. 复合函数 y = g(f(x)) 的正确执行顺序是?

单选题 · mid

  • A.先执行 g 再执行 f
  • B.先执行 f 再执行 g← 正确答案
  • C.f 与 g 同时并行执行
  • D.执行顺序完全随机确定

答案:B。复合函数从内向外执行,消息 x 先经过第一个工位 f,产生中间结果后交给 g 处理。这对应流水线上消息先过扫码工位再过称重工位。A 顺序颠倒;C 串联关系不是并行;D 随机执行会导致处理混乱。

Q4. 在 Pulsar Functions 中,Source 连接器的角色是?

单选题 · mid

  • A.将外部系统数据接入消息队列← 正确答案
  • B.将消息处理结果输出到外部系统
  • C.只负责消息的排序与持久存储
  • D.只负责消息的压缩与高效传输

答案:A。Source 是数据的进货口,负责从 Kafka、数据库日志、传感器等外部源拉取数据写入 Pulsar Topic。B 描述的是 Sink 的职责;C 和 D 是存储与传输层的功能,不是 Source 的职责。

Q5. 以下哪些属于 Pulsar Functions 中常见的窗口聚合类型?

多选题 · mid

  • A.时间窗口按固定时长切片← 正确答案
  • B.计数窗口按消息条数切分← 正确答案
  • C.滑动窗口重叠推进计算← 正确答案
  • D.排序窗口按值大小分组

答案:ABC。时间窗口、计数窗口、滑动窗口是流处理三大经典窗口类型。时间窗口按时间跨度切分,计数窗口按条数切分,滑动窗口让窗口重叠推进。排序窗口不是标准窗口类型,排序属于函数内逻辑,与窗口无关。

Q6. 背压机制的直观含义是什么?

单选题 · mid

  • A.感知下游变慢并减速上游生产← 正确答案
  • B.直接丢弃积压消息以快速减负
  • C.把消息复制到更多分区去加速
  • D.随机加快消费者的处理节奏

答案:A。背压是自适应保护机制:当下游处理速率跟不上时,上游主动放慢发送速度,避免消息无限堆积。B 丢弃消息会造成数据丢失;C 复制分区不解决单点瓶颈;D 随机加速消费者不切实际且可能加剧混乱。

Q7. 关于 Pulsar Functions 状态管理,正确的说法有哪些?

多选题 · hard

  • A.状态能随函数一起持久保存← 正确答案
  • B.故障之后状态可以进行恢复← 正确答案
  • C.状态只能保存在某个 Topic 中
  • D.状态不参与故障恢复的逻辑

答案:AB。Pulsar Functions 通过 State Store 将状态持久化保存(A 正确),函数重启后能恢复(B 正确)。状态存储是独立的存储层,而非简单放在某个 Topic 中(C 错);状态更新与消息处理协同,参与故障恢复(D 错)。

Q8. 事件驱动相比轮询方式的优势主要在于?

单选题 · hard

  • A.能降低消息积压的可能性
  • B.事件到达即处理且无需空转← 正确答案
  • C.可以扩大单条消息的体积限制
  • D.能够免除消息序列化的开销

答案:B。事件驱动让函数在消息到达时被唤醒,无消息时完全不消耗计算资源,零空转(B)。轮询方式有固定间隔,可能造成延迟和无效空转。积压可能性主要取决于处理速率(A);消息体积和序列化是消息系统固有属性,与驱动方式无关(C、D)。

Q9. 以下哪些场景更适合用窗口聚合处理?

多选题 · extreme

  • A.体温传感器每五分钟取平均温度← 正确答案
  • B.累计每满一百单生成一张报表← 正确答案
  • C.单条消息到达后立刻回显内容
  • D.滑动统计最近十秒内的页面流量← 正确答案

答案:ABD。A 是时间窗口求平均,B 是计数窗口满百触发,D 是滑动窗口统计最近十秒,都符合窗口聚合语义。C 是单条消息的即时处理,不需要窗口边界,直接函数输出即可。窗口的价值在于把多条消息汇成统计结果。

Q10. 要把处理结果写入外部 Elasticsearch,需要配置什么?

单选题 · easy

  • A.需要配置一个 Source 连接器
  • B.需要配置一个 Sink 连接器← 正确答案
  • C.只能改用窗口聚合来实现
  • D.只能依靠状态存储来处理

答案:B。将数据从 Pulsar 输出到外部系统(Elasticsearch、MySQL 等)使用的是 Sink 连接器。Source 负责数据接入(A 方向相反);窗口聚合和状态存储是计算能力,不是数据输出的通道(C、D 概念混淆)。