执行引擎与 Notebook 内核:Spark 提交监控、Flink/Hive 对照与 kernel 生命周期
U85 讲清了"谁来触发、触发是否只发生一次、执行器崩了谁收敛"。这一页往下走一层:触发之后,任务交给谁执行、执行状态在哪里看、调度侧看到的"成功"和引擎产出的"结果"什么时候会不一致,以及 Notebook 的 kernel 作为另一种执行载体,它的生命周期与 Spark/Flink/Hive 有什么本质差别。
可运行结论来自 labs/data-engine:真实 Spark 4.0.1(local[2]) 的 4 个任务(成功闭环、结果丢失反例、先标记后执行的失败反例、事件日志观测)与 真实 Jupyter kernel(jupyter_client 8.6.3 + ipykernel 6.31.0)的 2 组生命周期实验,共 6 组场景一次完整运行 exit 0。Flink 与 Hive 未在本机运行(无集群、无 HiveServer2/Metastore),其提交与监控差异按固定版本官方文档与源码对照说明,并在每节显式标注;不把对照说明写成实测结论。
本页是 U86(#123)的 D-DATA 方向正文。调度器的 DAG、重试、misfire、丢失判定仍归 DAG 调度、持久状态、重试回填与恢复;任务幂等与重复消费归重复消费、幂等与顺序消息;单机定时器语义归调度执行:@Scheduled。租户/配额、所有权代际与 checkpoint 和业务提交的区别归 U87。
术语与四种执行载体
同一个"任务"在数据平台上会以四种完全不同的形态存在,先固定术语:
| 载体 | 提交单位 | 状态源 | "结果"在哪里 |
|---|---|---|---|
| 调度器(U85 xxl-job) | 任务 ID + 一次触发 = 一条日志行 | xxl_job_log 的 trigger/handle 码 | 不存在;调度器不懂数据 |
| Spark | 一次 spark-submit = 一个 application | Spark UI / REST / 事件日志 | 任务自己写的外部存储(表/文件) |
| Flink | 一次提交 = 一个常驻 Job(流)或批作业 | Flink REST /jobs/:jobid、Web UI | 外部存储 + checkpoint/savepoint |
| Hive | 一条 SQL = 一个 Operation(同步或异步) | HiveServer2 TCLI GetOperationStatus | 表(Metastore 记录位置) |
| Notebook(Jupyter) | 一个 kernel 进程 + 若干 cell 执行 | 内核状态(idle/busy)+ 会话记录 | kernel 内存与 cell 输出,不默认落盘 |
这张表是整页的主线:"提交成功"、"状态成功"、"结果存在且正确"是三件不同的事。调度器、引擎、Notebook 各自只能回答其中一部分,任何一层把另外两层的事当作自己已经保证,都会产出"元数据说成功、结果不在"的错位。
版本、工程与观察入口
固定版本(2026-10-28 查询):
| 组件 | 版本 | 证据 |
|---|---|---|
| Spark | 4.0.1(spark-4.0.1-bin-hadoop3.tgz,SHA-512 9198602c…baef41) | 官方 archive 发行包,运行前校验 |
| MySQL 客户端库 | pymysql 1.4.6(PySpark driver 进程内使用) | PyPI;版本记录在运行证据 |
| Jupyter 协议实现 | jupyter_client 8.6.3、ipykernel 6.31.0 | PyPI;Python 3.9 venv |
| 元数据存储 | MySQL 8.4.7(本机 DBngin) | 独有库 study_u86_<runid> |
| 对照:Flink | flink-core 2.3(release-2.3 分支 JobStatus.java) | 官方源码,未运行 |
| 对照:Hive | 4.2.1(release-4.2.1 的 TCLIService.thrift) | 官方源码,未运行 |
运行时是 BellSoft Liberica JDK 21.0.11、macOS 27.2 arm64。Spark 以 local[2] 运行——driver 与 executor 在同一个 JVM 里,没有 YARN/K8s 的资源协商与跨进程调度,这一点后文所有 Spark 结论都受它约束。
公开入口:
python3 labs/data-engine/run.py首次运行会下载 Spark 发行包(约 550MB,SHA-512 校验)并自建 venv;之后走 ~/.cache/software-engineering-study/java-depth-impl/u86/ 缓存。运行时创建独有 MySQL 库、独有临时目录,结束 DROP 数据库、停内核、清理孤儿库。完整场景观察与清理结果见运行证据,工程边界见实验说明。
Spark:提交、状态机与事件日志
一次提交发生了什么
spark-submit 是一个壳脚本:拼 classpath、把 --jars 分发给 driver 与 executor、用 spark-submit 的 launcher 起 driver 进程(cluster 模式下由集群管理器起,client 模式下就是当前进程),driver 里创建 SparkContext,再向集群管理器申请 executor。local 模式下这三者是同一个 JVM:driver 线程 + executor 线程池,所以"申请资源失败"这类集群侧故障在本实验里不会发生,也不该被外推。
任务脚本里 SparkSession.builder.master('local[2]') 创建的是同一层抽象:Spark 4 的 SparkSession 取代了 SparkContext 作为入口,但底层仍是 SparkContext + DAGScheduler + TaskScheduler。本实验的每个任务都显式 spark.stop()。实测一个去掉 stop 的同构脚本在本地也会在约 4 秒内由 JVM shutdown hook 收敛并 exit 0;但显式 stop 能让"释放 executor/UI 线程"的时机可预期,长驻或嵌入式场景不能依赖进程退出时的被动收敛。
job / stage / task:三层状态与触发时机
Spark 的计算是惰性的:createDataFrame、groupBy、select 只构建逻辑计划,只有 action(count、collect、write)才触发 job。一个 job 按 shuffle 边界切成 stage,stage 里是并行的 task。这三层状态不是"日志文本",而是有正式事件模型的。
本实验开 spark.eventLog.enabled=true 后拿到的真实事件日志(Spark 4 默认是 v2 滚动格式:目录 eventlog_v2_<app>/ 下若干 events_<n>_<app>.zstd,zstd 压缩的 JSON lines):
U86 sparkEventLog exit=0 file=events_1_local-1793178575522.zstd events=43
U86 sparkEventLog jobs=4 jobResults=['JobSucceeded']
U86 sparkEventLog stages=4 stageStates=['COMPLETED']
U86 sparkEventLog tasks=5 taskStatuses=['SUCCESS']
U86 sparkEventLog appEnd=True sqlEvents=1一次 groupBy().count().collect() 产生了 4 个 job 与 4 个 stage——4 个 job 来自 AQE 的多次 shuffle 重优化,不是"SQL 解析本身",解析只产生 SQLExecutionStart 事件(本实验 1 条);task 结束事件里没有 Status 字段,状态要从 Task Info 的 Failed/Killed 布尔判断——这是读事件日志时最容易踩的字段陷阱,本实验在第一次解析时就因此失败并修正。
为什么这很重要:调度侧只看到"进程退出码"。同一个退出码 0,可能对应 0 个 job(脚本什么都没做就退出),也可能对应 4 个 job 全部成功。要判断"这次执行到底做了什么",必须看引擎自己的状态源,不能只信退出码。
元数据、状态、结果:三件事的错位
本实验用 MySQL 两张表模拟真实平台的登记结构:job_meta(调度侧写的任务状态)与 job_results(引擎产物表)。三组真实观察:
(1)成功闭环:任务跑真实聚合(3 行、2 个 distinct),driver 进程经 pymysql 写结果行,再把元表置 SUCCESS(这是任务代码自己的写回,不是 Spark JDBC 数据源写入):
U86 sparkOk exit=0 meta=SUCCESS result=success-payload rows=3 distinct=2(2)元数据成功、结果丢失:任务先写结果、更新元表,随后删除结果行(模拟"结果被清理作业误删 / 写到了别处"):
U86 metaResultMissing exit=0 meta=SUCCESS resultRows=0调度侧如果只看 job_meta,这个任务是成功的;实际结果已经不存在。这就是"元数据说成功、结果不在"的最小可运行形态——它不是某个产品的 bug,而是登记与产物没有同一事务边界的必然窗口。
(3)先标记后执行:任务在真正计算之前就把元表置 SUCCESS,然后计算抛异常,spark-submit 以退出码 1 结束:
U86 failedMeta exit=1 metaDuringRun=SUCCESS resultRows=0 metaAfterReconcile=FAILED这是很多自研任务模板的真实写法("先把状态置成功,再执行"),故障时会把失败永久伪装成成功。实验里的对账步骤以 进程退出码 + 结果存在性 为准修正元表;现实中对账还应加上引擎侧状态(Spark REST/事件日志的 application 状态)与产物校验。
从这三组观察可以提炼一条可执行的不变量:
元数据状态必须由"退出码 + 结果存在性(+ 引擎状态)"三者共同推导,不能由任务自己写的某一个字段单独代表。
Spark 的提交监控:你能从哪里看
| 观测源 | 内容 | 什么时候用 | 本实验状态 |
|---|---|---|---|
| 进程退出码 | 0 = driver 正常结束;非 0 = 失败/被杀 | 最粗的一层,调度器默认只信它 | 已运行(exit 0/1) |
spark-submit 日志 | driver 与 executor 的 stderr/stdout | 排错第一现场 | 已运行(写入 runtime 目录) |
| 事件日志 | 全部 job/stage/task/application 事件,可离线重放 | 事后审计、与调度记录对齐 | 已运行(v2 zstd,43 事件) |
Spark REST API(/api/v1/applications) | 运行中/历史 application、job、stage、executor | 平台侧轮询 | 未运行(本实验未起 History Server / REST 网关) |
| Spark UI | 同上,可视化 | 人工排障 | 未运行(无浏览器访问,仅事件日志) |
需要诚实说明的边界:事件日志与 REST 是同一个信息模型的两个出口(REST 是 History Server 解析事件日志后暴露的 API),本实验只验证了事件日志一侧;把 REST 当作"另一套真相"是常见误解,它更慢、更全,但不比事件日志多出信息。
Flink:与 Spark 的提交监控差异(对照,未运行)
Flink 2.3 的作业状态在 flink-core 的 JobStatus 枚举里定义,11 个状态,其中终态有三个:FAILED、CANCELED、FINISHED;SUSPENDED 是局部状态(TerminalState.LOCALLY),RESTARTING/RECONCILING 非终态:
/** Possible states of a job once it has been accepted by the dispatcher. */
@PublicEvolving
public enum JobStatus {
/**
* The job has been received by the Dispatcher, and is waiting for the job manager to receive
* leadership and to be created.
*/
INITIALIZING(TerminalState.NON_TERMINAL),
/** Job is newly created, no task has started to run. */
CREATED(TerminalState.NON_TERMINAL),
/** Some tasks are scheduled or running, some may be pending, some may be finished. */
RUNNING(TerminalState.NON_TERMINAL),
/** The job has failed and is currently waiting for the cleanup to complete. */
FAILING(TerminalState.NON_TERMINAL),
/** The job has failed with a non-recoverable task failure. */
FAILED(TerminalState.GLOBALLY),
/** Job is being cancelled. */
CANCELLING(TerminalState.NON_TERMINAL),
/** Job has been cancelled. */
CANCELED(TerminalState.GLOBALLY),
/** All of the job's tasks have successfully finished. */
FINISHED(TerminalState.GLOBALLY),
/** The job is currently undergoing a reset and total restart. */
RESTARTING(TerminalState.NON_TERMINAL),
/**
* The job has been suspended which means that it has been stopped but not been removed from a
* potential HA job store.
*/
SUSPENDED(TerminalState.LOCALLY),
/** The job is currently reconciling and waits for task execution report to recover state. */
RECONCILING(TerminalState.NON_TERMINAL);与 Spark 的关键差异:
| 维度 | Spark | Flink |
|---|---|---|
| 执行形状 | 批/微批;application 跑完即退出 | 流作业常驻;同一个 Job 在 RUNNING 状态停留直到被取消或失败 |
| 状态源 | application 级(退出码/事件日志/REST) | job 级 REST /jobs/:jobid,含 11 个状态的完整转换 |
| 重启语义 | 失败即结束,由调度器重试(U85 的失败重试层) | 作业内部按 checkpoint/restart strategy 自动重启;外部调度器看到的是"还在 RUNNING",而不是"失败了一次" |
| 监控盲区 | 退出码掩盖"0 个 job" | RUNNING 状态掩盖"重启了很多次"——必须另看 JobManager 的 Job 级指标(重启次数)与 checkpoint 统计;JobDetailsInfo(GET /jobs/:jobid)本身没有 numRestarts 字段 |
这条差异直接决定监控设计:对 Spark,"成功"是终点;对 Flink,"运行中"才是常态。用同一套"看退出码/看是否 FINISHED"的探针去监控 Flink 流作业,会把持续重启的作业当成健康。Flink 的 checkpoint 与业务提交的区别("checkpoint 成功不等于业务结果已提交")归 U87。
本机没有 Flink 集群(无 JobManager/TaskManager 进程,16GB 内存与 Spark 同跑风险高),以上差异来自 release-2.3 的 JobStatus.java 源码与官方 REST 文档,未运行验证;不对 Flink 的 checkpoint 行为做任何实测声明。
Hive:SQL 提交与 Operation 状态(对照,未运行)
Hive 的提交单位是一条 SQL 语句到 HiveServer2 的一个 Operation。TCLI 协议(TCLIService.thrift,Hive 4.2.1)定义的状态机是:
enum TOperationState {
// The operation has been initialized
INITIALIZED_STATE,
// The operation is running. In this state the result
// set is not available.
RUNNING_STATE,
// The operation has completed. When an operation is in
// this state its result set may be fetched.
FINISHED_STATE,
// The operation was canceled by a client
CANCELED_STATE,
// The operation was closed by a client
CLOSED_STATE,
// The operation failed due to an error
ERROR_STATE,
// The operation is in an unrecognized state
UKNOWN_STATE,
// The operation is in an pending state
PENDING_STATE,
// The operation is in an timedout state
TIMEDOUT_STATE,
}与 Spark/Flink 的差异:
| 维度 | HiveServer2 | Spark | Flink |
|---|---|---|---|
| 提交 API | ExecuteStatement → OperationHandle | spark-submit 起进程 | flink run 提交 JobGraph |
| 状态查询 | GetOperationStatus 轮询 | 退出码 / 事件日志 | REST /jobs/:jobid |
| 取消 | CancelOperation(协作) | kill 进程 | PATCH /jobs/:jobid?mode=cancel |
| 结果 | FetchResults(分页取行) | 任务自己写存储 | 外部 sink |
| 典型失败形态 | Operation 停在 ERROR_STATE,客户端要显式取错误 | driver 退出码非 0 | Job 进入 FAILED 后按策略重启 |
状态查询的请求本体很短——客户端拿 OperationHandle 去问,服务端答状态:
// GetOperationStatus()
//
// Get the status of an operation running on the server.
struct TGetOperationStatusReq {
// Session to run this request against
1: required TOperationHandle operationHandle
// optional arguments to get progress information
2: optional bool getProgressUpdate
}Hive 特有的一点是同步与异步的分界:ExecuteStatement 的 runAsync 参数决定调用是否阻塞到完成;同步模式下客户端只拿到"调用返回",异步模式下必须自己轮询 GetOperationStatus 直到终态。两种模式都可能出现"客户端以为成功"的错位:同步模式网络断开会丢失结果,异步模式忘轮询则状态永远停在 RUNNING。这与本实验 Spark 场景 2/3 的错位是同一类问题(状态与产物没有共同裁决点),只是协议不同。
本机没有 HiveServer2 与 Metastore,以上来自 release-4.2.1 的 TCLIService.thrift 源码,未运行;不对 Hive 的 ACID 表、Tez 执行等做任何实测声明。
Notebook:kernel 是另一种"执行器"
kernel 不是进程内的函数调用
Jupyter 的执行模型:一个 kernel 进程(本实验是 ipykernel)通过 ZMQ 五通道(shell、iopub、stdin、control、hb)与客户端通信。客户端发 execute_request,kernel 回 execute_reply(状态 ok/error/aborted),同时通过 iopub 广播 status(busy/idle)、execute_result、stream、error 等消息。
这与调度器执行器的差别是本质的:
- kernel 是有状态的:变量、导入、定义跨 cell 存活,执行顺序由用户决定,没有"一次任务一次执行"的边界;
- kernel 是交互式的:没有"退出码",一次执行失败不会让 kernel 退出,只会返回
error状态; - kernel 的"结果"默认不落盘:
execute_result只是消息,刷新页面/换客户端就没了;要持久化必须自己在 cell 里写文件或数据库。
本实验用 jupyter_client 的真实 kernel 管理协议(不是 mock)跑了完整生命周期:
U86 kernelLifecycle kernel_pid_alive=True
U86 kernelLifecycle exec_result=42
U86 kernelLifecycle exec_status=ok
U86 kernelLifecycle interrupt_status=error
U86 kernelLifecycle interrupt_seconds=0.1
U86 kernelLifecycle state_kept=True
U86 kernelLifecycle kernel_exitcode_after_shutdown=0interrupt_status=error 不是失败——被 SIGINT 中断的 time.sleep(60) 返回的错误状态正是"中断生效"的证据,0.1 秒返回说明中断信号直达内核进程组。state_kept=True state_stdout=kept 验证了同一 kernel 内变量跨 cell 保留(按 print(mark) 的真实输出与 reply 状态判定,不是常量)。
生命周期状态机
jupyter_client 的 KernelManager(manager.py,v8.6.3)把生命周期封装为 start → (busy/idle 交替) → interrupt / shutdown。interrupt_kernel 发送 SIGINT(或 message 模式):
async def _async_interrupt_kernel(self) -> None:
"""Interrupts the kernel by sending it a signal.
Unlike ``signal_kernel``, this operation is well supported on all
platforms.
"""
if not self.has_kernel and self._ready is not None:
if isinstance(self._ready, CFuture):
ready = asyncio.ensure_future(t.cast(Future[t.Any], self._ready))
else:
ready = self._ready
# Wait for a shutdown if one is in progress.
if self.shutting_down:
await ready
# Wait for a startup.
await ready
if self.has_kernel:
assert self.kernel_spec is not None
interrupt_mode = self.kernel_spec.interrupt_mode
if interrupt_mode == "signal":
await self._async_signal_kernel(signal.SIGINT)
elif interrupt_mode == "message":
msg = self.session.msg("interrupt_request", content={})
self._connect_control_socket()
self.session.send(self._control_socket, msg)
else:
msg = "Cannot interrupt kernel. No kernel is running!"
raise RuntimeError(msg)
interrupt_kernel = run_sync(_async_interrupt_kernel)shutdown_kernel 是两段式:先 interrupt,再发 shutdown_request,按 shutdown_wait_time 等待(默认 5.0 秒,源码注释里"1s"是上游过时的注释),最后强制清理:
async def _async_shutdown_kernel(self, now: bool = False, restart: bool = False) -> None:
"""Attempts to stop the kernel process cleanly.
This attempts to shutdown the kernels cleanly by:
1. Sending it a shutdown message over the control channel.
2. If that fails, the kernel is shutdown forcibly by sending it
a signal.
Parameters
----------
now : bool
Should the kernel be forcible killed *now*. This skips the
first, nice shutdown attempt.
restart: bool
Will this kernel be restarted after it is shutdown. When this
is True, connection files will not be cleaned up.
"""
if not self.owns_kernel:
return
self.shutting_down = True # Used by restarter to prevent race condition
# Stop monitoring for restarting while we shutdown.
self.stop_restarter()
if self.has_kernel:
await self._async_interrupt_kernel()
if now:
await self._async_kill_kernel()
else:
await self._async_request_shutdown(restart=restart)
# Don't send any additional kernel kill messages immediately, to give
# the kernel a chance to properly execute shutdown actions. Wait for at
# most 1s, checking every 0.1s.
await self._async_finish_shutdown(restart=restart)
await self._async_cleanup_resources(restart=restart)
shutdown_kernel = run_sync(_async_shutdown_kernel)这两段源码解释了三个操作细节:
- interrupt 与 shutdown 不是一回事:interrupt 只打断当前执行,kernel 继续可用(实验里中断后变量仍在);shutdown 才终止进程。
- shutdown 有优雅窗口:
_async_finish_shutdown按shutdown_wait_time(默认 5.0 秒;实现上在 1/2 处发 SIGTERM、全额处 SIGKILL)让 kernel 执行清理。对本实验的轻量 kernel 来说,kernel_exitcode_after_shutdown=0说明它走完了优雅路径。 is_alive()依赖 provisioner:jupyter_client 8.x 用provisioner.poll()判断存活,而不是直接暴露km.kernel进程对象——本实验第一次写脚本时按旧版 API 取km.kernel.pid直接失败,这是版本差异的真实教训。
崩溃:会话记录还在,状态没了
SIGKILL 模拟 kernel 崩溃(不是优雅 shutdown):
U86 kernelCrash last_result=42
U86 kernelCrash exit_after_kill=-9
U86 kernelCrash is_alive_after_kill=False
U86 kernelCrash session_record_still_says_success=True
U86 kernelCrash new_exec_status=error: Empty四个观察对应四个事实:崩溃前最后一次执行确实成功(42);SIGKILL 让进程以 -9 退出;is_alive() 如实变 false;但本实验预先落盘的那份"会话执行记录"文件里,这条 cell 仍是 ok——如果审计只读会话记录,会以为 kernel 还在正常工作;同一 channel 上的下一次执行返回 Empty 错误,说明连接已断。(Jupyter 本身不维护这种成功记录,记录来自实验脚本写出的文件;要区分的是"记录层"与"执行层"两套事实,不是 Jupyter 的官方特性。)
这就是 Notebook 的"元数据/结果不一致":与 Spark 场景 2/3 同构——记录层(会话/元表)与执行层(kernel 进程/结果文件)是两套独立的事实,任何一方都不能单独代表"成功"。平台要做好 Notebook 的多租户,必须额外维护 kernel 注册表与心跳(JupyterHub 的 gateway 就是干这个的),单靠 notebook 文件里的输出是不可信的。
Notebook 与调度器执行器的对照
| 维度 | 调度器执行器(U85) | Notebook kernel |
|---|---|---|
| 生命周期 | 随任务触发/队列消费,执行完回到空闲 | 常驻,跨多次执行存活,直到显式 shutdown |
| 状态源 | 回调的 handle_code | 无统一状态;kernel 存活 + 会话记录 |
| 故障语义 | 进程崩溃 = 回调丢失 = 待收敛的 UNKNOWN | 进程崩溃 = 变量全丢,会话记录还可能显示"成功" |
| 结果持久化 | 业务自己写 | 默认不写,必须显式落盘 |
| 多用户隔离 | 执行器按 appname 分组 | 需要 gateway/代理层做 kernel 路由 |
观测、排障与恢复
一个"任务跑完了但结果不对"的排障顺序(把本页六组观察串起来):
- 调度层:
xxl_job_log的 trigger/handle 码(U85)——触发成功了吗?执行回调是 200 还是 500? - 引擎层:Spark 退出码与事件日志 / Flink REST job 状态与重启数 / Hive Operation 状态——引擎认为自己做了什么?
- 产物层:结果表/文件是否存在、行数/内容是否符合预期——这是唯一不可替代的真相。
- 登记层:元表状态是否与前三层一致;不一致就以产物为准修正,并记录修正事件(谁、何时、依据什么)。
本实验的 failedMeta 场景演示了第 4 步:以退出码 + 结果存在性把误标的 SUCCESS 改为 FAILED。
恢复责任要写清楚:调度器保证"到点触发一次"(U85),引擎保证"本次提交按自己的语义执行/重试","结果正确且唯一"只能由业务方用自己的幂等键与对账来保证。三层谁都不能替别人签字。
容量与选择
至少两种合理方案,取决于任务形状:
| 方案 | 提交/监控形态 | 成本 | 适用 |
|---|---|---|---|
| Spark on YARN/K8s | 每次提交一个 application,退出即结束;监控走事件日志/History Server | 每次提交有冷启动(秒级到分钟级) | 批量 ETL、按小时/天调度 |
| Flink on YARN/K8s | 常驻 Job,主动/被动重启,checkpoint 保状态 | 常驻资源,运维复杂 | 流式管道、低延迟 |
| Hive on Tez/LLAP | SQL 即 Operation,Metastore 管元数据 | SQL 生态成熟,延迟高 | 数仓 SQL、临时分析 |
| Notebook + 调度 | kernel 常驻 + 定时触发 cell | 交互友好,但状态不可审计 | 探索、报表原型,不适合无人值守生产 |
单位与预算:Spark application 的冷启动时间是"按次"成本;Flink 的 checkpoint 间隔是"按流"成本;Hive Operation 的队列等待是"按查询"成本。三者不能用同一个 SLO 衡量——把 Spark 的"日批 30 分钟"套到 Flink 流作业上会得出"太慢"的错误结论。
演进方向:任务数量增长 → Spark 需要提交网关与事件日志集中存储;从批转流 → 迁移到 Flink 时注意"运行中≠健康",要把重启数纳入告警;Notebook 从个人使用到平台化 → 需要 gateway 与 kernel 注册表,不能靠 notebook 文件。
实验读法
labs/data-engine 的 6 组场景,建议按下面的观察行复读(全部来自同一次 exit 0 运行):
U86 sparkOk exit=0 meta=SUCCESS result=... rows=3 distinct=2:Spark 真实聚合,driver 经 pymysql 写回结果与元表。U86 metaResultMissing exit=0 meta=SUCCESS resultRows=0:元表成功、结果丢失。U86 failedMeta exit=1 metaDuringRun=SUCCESS ... metaAfterReconcile=FAILED:先标记后执行的失败反例与对账。U86 sparkEventLog jobs=4 ... tasks=5 taskStatuses=['SUCCESS'] appEnd=True sqlEvents=1:事件日志里的 job/stage/task 状态。U86 kernelLifecycle ... interrupt_status=error interrupt_seconds=0.1 state_kept=True:kernel 生命周期与中断。U86 kernelCrash ... session_record_still_says_success=True new_exec_status=error: Empty:崩溃后磁盘上的会话记录仍为 ok,而真实执行已断连。
开发中真实踩到的坑都留在 runtime/ 的失败运行里:Spark 4 的 eventLog 是 zstd v2 格式(第一次按纯文本读失败)、TaskEnd 没有 Status 字段、jupyter_client 8.x 不再暴露 km.kernel、Spark 任务的 Python 需要把 venv 的 site-packages 通过 PYTHONPATH 提供——没有一个是"命令写好就算过"。
条件变化与未运行边界
- 未运行:Spark 集群模式(YARN/K8s/Standalone)、Spark REST/History Server、Flink 任何形态、Hive/HiveServer2/Metastore、Notebook 多用户(JupyterHub)、kernel 的 message 模式 interrupt、Windows 平台。
- 受控构造:
job_meta/job_results不一致是实验构造;kernel 崩溃是 SIGKILL 注入。都不是生产故障复现。 - local 模式的局限:driver 与 executor 同 JVM,资源协商、executor 丢失、driver 与集群失联等集群模式特有的故障不在本实验覆盖范围。
- 版本差异:Spark 4 的事件日志格式(v2 zstd)与 3.x 不兼容,读取工具要按版本处理;jupyter_client 8.x 的 KernelManager API 与 7.x 不兼容。
- "调度成功"、"引擎退出码 0"、"结果正确"三者互不蕴含,本页的六组观察只证明它们在实验构造下可以互相错位。
闭卷验收
本页 20 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。
来源与版本
| 来源 | 版本 | 用途 |
|---|---|---|
| Spark 4.0.1 官方发行包 | 4.0.1 | 真实运行(spark-submit、SparkSession、事件日志) |
Flink JobStatus.java | release-2.3 | 状态机对照(未运行) |
| Flink REST API 文档 | 2.3 | /jobs/:jobid 等端点(未运行) |
Hive TCLIService.thrift | 4.2.1 | Operation 状态机(未运行) |
jupyter_client manager.py | 8.6.3 | kernel 生命周期源码与真实运行 |
访问与运行日期:2026-10-28。源码片段为固定 tag 的连续行段,整文件 SHA-256 随每段元数据登记;Spark/Flink/Hive/jupyter_client 的版本以运行证据或固定 tag 为准。产品文档、源码与实验是三类证据,不能互相替代;Flink/Hive 只做源码与文档对照,不是运行结论。