• 简体中文
  • Jupyter

    /v1/jupyter 在真正的 IPython kernel 里执行 Python。它覆盖原生 Python REPL 做不到的场景,代价是进程开销更大:

    • 富输出(display_data mime bundle,比如 matplotlib 的 PNG)
    • IPython magic 命令
    • 在已装的多个 Python 版本之间选择

    aiod 自己实现这一层:启动 ipykernel,直接用 Jupyter kernel 协议与它通信。不涉及 Jupyter server,也不需要额外起任何东西。

    运行要求

    aiod 选用的 Python 解释器必须能 import ipykernel。如果没有安装,请先执行 pip install ipykernel,再启动 daemon。

    daemon 只在启动时探测一次。安装完成后,重启 daemon,再检查 GET /v1/capabilitiescode_interpreter.jupyter_backend 应为 "kernel"python_kernels 会列出可用的 kernel。

    未安装时,/v1/jupyter/* 路由不可用,并会说明修复方式;/v1/code/info 也不会声明 jupyter 这一 kind。

    如果设置了 AIO_JUPYTER_ENDPOINT 且可以连接,外部 Jupyter server 会优先于内嵌 kernel,daemon 会将 /v1/jupyter 原样代理到该服务。

    执行代码单元

    提交这一格的源码,响应中包含 notebook 输出:

    curl -X POST "$BASE_URL/v1/jupyter/execute" \
      -H "Content-Type: application/json" \
      -d '{"code": "from IPython.display import HTML\nHTML(\"<b>hi</b>\")"}'

    请求体示例:

    {
      "code": "from IPython.display import HTML\nHTML(\"<b>hi</b>\")"
    }

    data 是:

    • code —— 实际运行的代码
    • status —— okerrortimeout
    • execution_count —— kernel 的 cell 计数器
    • outputs —— notebook 输出对象(streamexecute_resultdisplay_dataerror
    • session_id —— 运行它的会话;一次性执行时是 null
    • kernel_name —— 运行它的 kernel
    • msg_id —— 该请求的 Jupyter 消息 id

    每个输出对象上所有键都在,用不到的是 null,所以客户端可以直接读 outputs[i].textoutputs[i].data,不必先按类型分支。

    请求体字段:

    字段取值含义
    coderequiredcell 源码;空 cell 也会执行并返回 ok
    session_idoptional复用时在同一命名空间里继续
    statefultrue为 true 时返回会话 id
    kernel_nameavailable_kernels 之一,默认取第一个用哪个已装的 kernel 运行
    timeout1–900 秒(默认 30)超时的 cell 会被中断
    cwdoptional在 kernel 启动时固定;传新值会启动新 kernel

    daemon 不支持的 kernel_name 返回 422,例如:unknown kernel 'ir'; available: python3, python3.14

    cwd 不是目录不算请求错误。该 cell 返回 200successfalse,并带一条写明路径的 DirectoryError 输出。

    查看 kernel 信息

    curl "$BASE_URL/v1/jupyter/info"

    available_kernels 是探测出来的,不是声明的:里面的每个名字都可以作为 kernel_name 传进来。返回里还有 descriptionkernel_detection 两行,给直接看原始 JSON 的人。

    输出格式

    一个输出对象是四种形态之一:

    output_type携带由什么产生
    streamnamestdoutstderr)和 textprint,以及 %time 这类 magic
    execute_resultdata mime bundle 和 execution_countcell 的最后一个表达式
    display_datadata mime bundle,含 image/pngdisplay(),以及 %matplotlib inline 的图
    errorenameevaluetraceback抛出的异常;status 变成 error

    matplotlib 要返回 PNG,需要使用 %matplotlib inline。只使用 Agg 后端时,图会写入文件,该 cell 不返回 display_data

    client = Sandbox(base_url=BASE_URL)
    
    # 1. Open a session and load the data in it.
    client.jupyter.execute_code(
        session_id="report",
        code="import pandas as pd\ndf = pd.DataFrame({'v': [120, 135, 150]})",
    )
    
    # 2. A chart comes back as a PNG in display_data.
    run = client.jupyter.execute_code(
        session_id="report",
        code="%matplotlib inline\ndf.plot()",
        timeout=120,
    ).data
    for out in run.outputs:
        print(out.output_type, sorted((out.data or {}).keys()))
    
    # 3. End the session; its kernel goes with it.
    client.jupyter.delete_session("report")

    image/png 是 base64,这张图大约 27 KB。cell 写入的文件也是普通文件:通过 文件 API 读取。

    一次性执行与会话

    不传 session_idstateful: true 时,请求只执行一次。daemon 会从预热池取一个 kernel;没有可用 kernel 时,再为这次请求启动一个。执行结束后,kernel 也会关闭。

    因此,一次性执行中定义的变量不会保留到下一次请求。再次使用时会得到 NameError

    stateful: true 后,响应会返回一个会话 id;也可以自行指定 session_id。后续请求带上同一个 id,就会在同一个命名空间中继续执行。

    一个会话在整个生命周期内独占一个 kernel,execution_count 会在多次调用之间连续累加。

    路由作用说明
    POST /v1/jupyter/sessions/create提前开一个会话接受 kernel_namecwdsession_id
    GET /v1/jupyter/sessions列出全部每条带 kernel_namecwdstate
    GET /v1/jupyter/sessions/{id}读取一个未知 id 返回 404
    DELETE /v1/jupyter/sessions/{id}结束一个未知 id 返回 200successfalse
    DELETE /v1/jupyter/sessions结束全部返回 cleaned_sessions

    会话没有 interrupt 和 restart:超过 timeout 的 cell 会被自动中断,结束会话用 DELETE。这两个路径返回 501 并说明这一点。

    会话由 kernel 和 id 共同标识。因此,默认 kernel 上的 s1 与另一个 kernel 上的 s1 属于两个不同的命名空间。

    后者会以组合键 python3.14:s1 列出。如果该 kernel 同时支撑 Code 路由,在那里传相同的 id 也会访问该会话的命名空间。

    kernel 的工作目录在启动时固定。请求的 cwd 与池里所有 kernel 都不同时,会启动一个新 kernel;池里的 kernel 不会被移动。

    超时与输出上限

    一个 cell 超过 timeout 后,daemon 会先通过 control 通道发送 interrupt_request,再等待 2 秒。

    如果 kernel 及时停止,会话仍然保留,cell 的 status"timeout"。输出中会先出现 KeyboardInterrupt,再出现 execution timed out after 2000ms and was interrupted

    忽略中断的 kernel 会被终止,会话随之丢弃。

    每个 cell 最多返回 2,000,000 个字符。流式文本超过上限时,保留开头部分,并追加一条 stderr[output truncated at 2000000 characters]

    同一个 cell 后续产生的输出仍会返回,额外保留 64 KiB。单条结果或图片超过上限时无法截断,会直接丢弃,并只保留这条提示。

    最多同时运行 5 个会话(AIO_KERNEL_MAX_SESSIONS),每个会话空闲 300 秒后回收(AIO_KERNEL_SESSION_TIMEOUT_SECS)。

    第 6 个会话请求返回 429,消息为 Maximum number of kernel sessions (5) reached。已有会话不会被清除。

    一个 kernel 占用约 60 MB 内存。这两个上限比原生 REPL 的 20 个会话 / 1800 秒更紧。

    AIO_KERNEL_PREWARM(默认 0)保持这么多个空闲 kernel 预热。启动后的第一个 cell 因此是约 90 ms 而不是 1.4 秒。池在启动时填满,每次用掉后在后台补充。

    内存占用

    相同限制下(0.5 CPU、0.5 GiB)对比 aiod 和 1.x 镜像:aiod 运行 nginx 和一个预热 kernel,1.x 镜像开启 Jupyter、关闭浏览器和 VNC。

    kernel 本身占用相近,差异来自外围服务:空闲时 aiod 占 69.8 MB,1.x 镜像占 248.4 MB。

    相关页面

    • Code —— 统一的代码执行路由,以及 Python 后端的选择方式
    • Node.js —— JavaScript REPL