动态挂载文件存储NAS

使用动态挂载文件存储NAS功能,函数实例的每个用户会话可动态挂载到文件存储NAS中专属的子目录,实现多租户环境下持久化数据的安全隔离。

背景介绍

随着 AI Agent 应用的快速发展,越来越多的场景需要在安全沙箱(Sandbox)中执行用户自定义代码,并持久化保存会话期间生成的文件、项目代码、数据集等关键资产。这类应用通常以多租户 SaaS 形式部署,成千上万的用户会话并发运行,每个会话都需独立、隔离的存储空间。

在过去的方案中,开发者常将所有会话挂载到同一个共享的NAS根目录下。然而,这种做法存在严重的数据越权访问风险:一个恶意或异常会话可能通过路径遍历、权限提升等方式访问甚至篡改其他租户的数据,无法满足企业级安全与合规要求。

为应对这一挑战,阿里云函数计算(FC)在支持会话亲和与会话隔离的基础上,进一步将隔离能力延伸至持久化存储层,推出 “动态挂载文件存储NAS” 功能。该功能允许每个 AI Sandbox 会话在启动时,自动、安全地挂载到专属的 NAS 子目录,实现从计算到存储的端到端多租户隔离。

功能说明

核心能力

函数计算通过会话动态挂载 NAS功能,允许您在创建会话(Session)时,按需为该会话挂载专属的 NAS 目录,并指定独立的用户身份(POSIX UID/GID)。其核心特性包括:

  • 动态与按需:存储的挂载与会话生命周期绑定,仅在需要时分配,会话销毁后自动解绑。

  • 强隔离性:通过为每个会话指定独立的 User ID/Group ID,从文件系统层面彻底隔离不同租户的数据访问权限。

  • 高灵活性:可为不同租户挂载到 NAS 文件系统的不同子目录,轻松实现数据分区。

  • 性能优化:结合会话预热,可在实例启动时预先挂载好数据目录,加速模型或数据的加载。

工作原理

该功能将 NAS 挂载操作与 CreateSession API 调用深度集成。以 HeaderField 亲和为例,整体流程如下:

image

准备阶段:创建并配置带有存储的会话

此阶段的目标是创建一个与特定存储绑定的、隔离的会话环境,以便后续使用。

  1. 发起创建请求:后端管理服务调用 CreateSession API,并在请求中指定NAS挂载配置以及用于隔离的用户身份(UID/GID)。

  2. 准备实例与挂载:函数计算(FC)平台接收请求后,准备一个函数实例,该函数实例会根据配置执行NAS挂载操作,将专属目录挂载到实例的指定路径(例如 /mnt/data)。

  3. 绑定并返回ID:挂载成功后,平台将此实例与一个新生成的唯一SessionID进行绑定,并将该ID返回给后端服务。

调用阶段:使用已配置的会话

此阶段使用已准备好的会话环境来执行实际的业务逻辑。

  1. 发起调用请求:后端管理服务在调用 InvokeFunction API 时,通过一个特定的HTTP请求头(如 x-affinity-header-v1)传入之前获取的 SessionID

  2. 路由请求:函数计算平台的网关与调度器根据此SessionID,将请求精确地路由到已绑定的特定函数实例上。

  3. 执行代码:函数代码在该实例中执行。此时,由于环境和权限都已预先配置好,代码可以直接读写已挂载的NAS目录。

  4. 返回结果:函数执行完毕后,将结果逐层返回给最初的调用方。

适用范围

  • 功能邀测:会话生命周期管理功能处于公测阶段,使用前请提交工单申请;

  • 地域限制:不支持在华北5(呼和浩特)地域使用,动态挂载NAS功能仅限美国(硅谷)地域使用,如需支持其他地域,在提交工单时补充说明。

  • 文件存储NAS:您已经创建了文件系统NAS,并规划好了用于租户数据隔离的目录结构;

  • 会话亲和类型:适用于HeaderFieldCookie亲和类型。

操作步骤

以下将以 Python SDK 为例,完整演示如何为一个函数配置并使用动态 NAS 挂载。

步骤一:配置函数(前置设定)

为使函数支持会话动态挂载NAS,需要在创建函数或更新函数配置时,完成三项关键设置:

  1. 开启会话亲和:

    1. 登录函数计算控制台,点击函数管理>函数列表

    2. 函数列表页,点击需要配置的函数名称,进入函数详情页

    3. 配置页,找到高级配置,点击image

    4. 打开会话亲和开关,选择HeaderField 亲和并配置Header Name,例如:x-affinity-header-v1

      说明

      不能以 x-fc- 前缀开头,以字母开头,非首字符可包含数字、中划线、下划线、字母,长度大于等于5个字符并且不超过40个字符。

    5. 点击部署完成配置更新。

  2. 开启实例隔离:开启配置>高级配置>实例隔离,并选择会话隔离

  3. 允许访问 VPC配置

    • 开启配置>高级配置>网络>允许访问 VPC

    • 配置方式选择自定义配置

    • 专有网络选择挂载点所在的 VPC。

步骤二:创建带 NAS 配置的会话

  1. 安装依赖

    macOS / Linux

    # 使用 pip3 安装
    pip3 install alibabacloud_fc20230330 alibabacloud_credentials alibabacloud_tea_openapi alibabacloud_tea_util
    
    # 如遇权限问题,使用 --user 参数
    pip3 install --user alibabacloud_fc20230330 alibabacloud_credentials alibabacloud_tea_openapi alibabacloud_tea_util
    
    # macOS Homebrew Python 环境需使用 --break-system-packages
    pip3 install --break-system-packages alibabacloud_fc20230330 alibabacloud_credentials alibabacloud_tea_openapi alibabacloud_tea_util

    Windows

    # 使用 pip 安装
    pip install alibabacloud_fc20230330 alibabacloud_credentials alibabacloud_tea_openapi alibabacloud_tea_util
    
    # 或使用 Python 3 指定
    py -3 -m pip install alibabacloud_fc20230330 alibabacloud_credentials alibabacloud_tea_openapi alibabacloud_tea_util
  2. 编写创建会话的代码

    创建python代码文件(如:creatSession.py),将下列代码复制到文件中并替换核心参数。示例演示了如何为“租户 A”创建一个会话,并将其专属的 NAS 目录 (<YOUR-NAS-SERVER-ADDR>:/tenant-a-data) 挂载到实例的 /mnt/data 路径,同时指定用户身份为 UID=1001 和 GID=1001

    核心方法及核心参数说明:

    重要

    会话动态挂载NAS和在函数配置>高级配置>存储中挂载NAS可以同时配置,但是需要注意:

    • 下文NASConfig 中定义的 User ID/Group ID必须与函数挂载配置中使用的用户(User ID)/用户组(Group ID)保持一致

    • 同一个挂载路径 (mount_dir) 不能同时用于会话动态挂载和函数挂载

    • config.endpoint

    • CreateSessionInput

      • session_ttlin_seconds:会话总生命周期(单位:秒);

      • session_idle_timeout_in_seconds:会话闲置过期时间(单位:秒);

    • client.create_session_with_options:将<函数名称>替换为创建Session的函数名称;

      • NASMountConfig:NAS 挂载配置

        • mount_dir:实例内的挂载路径,如:/home/test

        • server_addr:NAS 文件系统地址及租户专属子目录

      • user_id:为此会话指定独立的 POSIX User ID

      • group_id:为此会话指定独立的 POSIX Group ID

    # -*- coding: utf-8 -*-
    from alibabacloud_fc20230330.client import Client as FC20230330Client
    from alibabacloud_credentials.client import Client as CredentialClient
    from alibabacloud_tea_openapi import models as open_api_models
    from alibabacloud_fc20230330 import models as fc20230330_models
    from alibabacloud_tea_util import models as util_models
    
    # 1. 创建账号Client
    credential = CredentialClient()
    config = open_api_models.Config(credential=credential)
    config.endpoint = f'<账号ID>.<Endpoint>'
    client = FC20230330Client(config)
    
    # 2. 准备 NAS 挂载配置
    nas_mount_config = fc20230330_models.NASMountConfig(
        mount_dir='/mnt/data',  # 实例内的挂载路径
        server_addr='<YOUR-NAS-SERVER-ADDR>:/<tenant-a-path>'  # NAS 文件系统地址及租户专属子目录
    )
    
    # 3. 配置 NAS 和用户身份(为租户A分配独立UID/GID)
    nas_config = fc20230330_models.NASConfig(
        mount_points=[nas_mount_config],
        user_id=1001,  # 为此会话指定独立的 POSIX User ID
        group_id=1001  # 为此会话指定独立的 POSIX Group ID
    )
    
    # 4. 构造 CreateSession 请求
    create_session_input = fc20230330_models.CreateSessionInput(
        nas_config=nas_config,
        session_ttlin_seconds=3600,
        session_idle_timeout_in_seconds=600
    )
    create_session_request = fc20230330_models.CreateSessionRequest(
        body=create_session_input
    )
    
    # 5. 发起请求
    runtime = util_models.RuntimeOptions()
    response = client.create_session_with_options('<函数名称>', create_session_request, {}, runtime)
    
    # 6. 从响应中获取 sessionId
    print(response.body.to_map())
    session_id = response.body.session_id
    print(f"Session created successfully. Session ID: {session_id}")
    
  3. 运行代码

    export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI****************
    export ALIBABA_CLOUD_ACCESS_KEY_SECRET=<yourAccessKeySecret>
    python3 creatSession.py

    参数说明:

    • ALIBABA_CLOUD_ACCESS_KEY_ID:阿里云账号或 RAM 用户的AccessKey ID

    • ALIBABA_CLOUD_ACCESS_KEY_SECRET:阿里云账号或 RAM 用户的AccessKey Secret

  4. 返回结果示例

    {
      'containerId': 'c-********-********-************', 
      'createdTime': '2025-10-30T06:38:10Z', 
      'functionName': '****', 
      'lastModifiedTime': '2025-10-30T06:38:10Z', 
      'nasConfig': 
        {
          'groupId': 1001, 
          'mountPoints': [
            {
              'enableTLS': False, 
              'mountDir': '/home/test', 
              'serverAddr': '*-*.*.nas.aliyuncs.com:/test'
               }
                 ], 
                 'userId': 1001
                 }, 
       'qualifier': 'LATEST', 
       'sessionAffinityType': 'HEADER_FIELD', 
       'sessionId': '******************', 
       'sessionIdleTimeoutInSeconds': 600, 
       'sessionStatus': 'Active', 
       'sessionTTLInSeconds': 3600
       }
    Session created successfully. Session ID: ************

步骤三:在函数中使用挂载的 NAS

现在,调用函数时携带上一步获取的 SessionID,函数实例内部即可访问到已挂载的 /mnt/data 目录。下面以Web函数为例:

  1. 修改函数内代码并重新部署

    import os
    from flask import Flask, request, jsonify
    
    # ... (app setup) ...
    
    # 假设 NAS 挂载点在 /mnt/data
    NAS_MOUNT_PATH = '/mnt/data'
    app = Flask(__name__)
    
    
    @app.route('/<path:path>', methods=['GET', 'POST'])
    def handle_nas_request(path):
        rid = request.headers.get('x-fc-request-id')
        print(f"FC Invoke Start RequestId: {rid}")
    
        # 构造文件在 NAS 上的完整路径
        # 注意:需要防止路径穿越攻击
        safe_path = os.path.normpath(os.path.join(NAS_MOUNT_PATH, path))
        if not safe_path.startswith(NAS_MOUNT_PATH):
            return "Path traversal attempt detected!", 400
    
        response_data = {}
    
        if request.method == 'POST':
            # 写入文件
            body = request.data.decode('utf-8')
            try:
                with open(safe_path, 'w') as f:
                    f.write(body)
                response_data['message'] = f"Successfully wrote to {safe_path}"
                print(f"Wrote to {safe_path}")
            except Exception as e:
                return str(e), 500
    
        elif request.method == 'GET':
            # 读取文件
            try:
                if os.path.exists(safe_path) and os.path.isfile(safe_path):
                    with open(safe_path, 'r') as f:
                        content = f.read()
                    response_data['content'] = content
                    print(f"Read from {safe_path}")
                else:
                    return f"File not found: {safe_path}", 404
            except Exception as e:
                return str(e), 500
    
        print(f"FC Invoke End RequestId: {rid}")
        return jsonify(response_data)  # 使用 jsonify 返回 JSON 格式的响应
    
    # ... (if __name__ == '__main__': block) ...
    
  2. 调用函数并验证

    调用API InvokeFunction- 调用函数,使用会话调用函数。

    核心代码示例及说明:

    InvokeFunctionHeaders:构造请求头,在请求Header中携带上一步返回的sessionIdHeader Key的值与开启会话亲和设置的值保持一致(如:x-affinity-header-v1),实现会话绑定路由。

    # -*- coding: utf-8 -*-
    from alibabacloud_fc20230330.client import Client as FC20230330Client
    from alibabacloud_credentials.client import Client as CredentialClient
    from alibabacloud_tea_openapi import models as open_api_models
    from alibabacloud_fc20230330 import models as fc20230330_models
    from alibabacloud_tea_util import models as util_models
    
    session_id = '************'
    function_name = 'my-session-nas'
    # 1. 创建账号Client
    credential = CredentialClient()
    config = open_api_models.Config(credential=credential)
    config.endpoint = f'<账号ID>.<Endpoint>'
    client = FC20230330Client(config)
    
    # 2. 构造请求头。Header Key(x-affinity-header-v1)必须与函数配置的会话亲和 Key 一致。
    headers = fc20230330_models.InvokeFunctionHeaders(
        common_headers={
            "x-affinity-header-v1": session_id
        }
    )
    
    # 3. 构造调用请求(可根据需要传入 body)
    invoke_request = fc20230330_models.InvokeFunctionRequest(
        body='your_request_payload'.encode('utf-8')  # 示例 payload
    )
    
    runtime = util_models.RuntimeOptions()
    
    try:
        # 4. 发起调用
        invoke_response = client.invoke_function_with_options(
            function_name,
            invoke_request,
            headers,
            runtime
        )
        # 5. 处理响应
        print(f"Status Code: {invoke_response.status_code}")
        print(f"Response Body: {invoke_response.body.decode('utf-8')}")
    except Exception as error:
        print(error.message)
    

后续步骤:删除会话

在任务完成后调用 DeleteSession - 删除会话资源,释放会话资源

生产环境建议

  • UID/GID 规划:为确保隔离性,必须为每个租户分配唯一的 POSIX UID;

  • 目录配额:为防止单个租户耗尽共享存储空间,建议在 NAS 侧为每个租户的根目录配置目录配额(Directory Quotas);

  • 数据垃圾回收 (GC)DeleteSession - 删除会话资源 操作不会自动删除 NAS 上的文件数据。需建立配套的异步垃圾回收机制,定期扫描并清理无主的文件目录,以回收存储空间。

计费说明

  • 计费从CreateSession - 创建会话资源成功起开始,按实例运行时长计费。

  • 即使会话处于空闲状态,只要未过期或删除,将持续产生费用。

  • DeleteSession - 删除会话资源后计费行为分以下两种情况:

    • 非隔离模式下,DeleteSession 不终止正在进行的调用,Session 关联的实例资源费用将持续计费至执行结束。

    • 隔离模式下,DeleteSession终止运行中的请求,销毁绑定的实例资源,终止计费。

会话Active期间,如果有请求,需要按照弹性实例(活跃)单价进行计费,如果无请求,则按照弹性实例(闲置)单价进行计费。

相关文档及API说明

功能列表

行为说明

CreateSession - 创建会话资源

  1. 创建一个显式会话资源,系统自动生成唯一 SessionID,并预分配绑定函数实例。

  2. 支持在创建时通过 nas_config 参数为会话动态挂载专属的持久化存储目录,并指定其在文件系统中的用户与组ID(UID/GID)。

  3. 支持指定会话闲置过期时间SessionIdleTimeoutInSeconds和会话生命周期参数SessionTTLInSeconds,未指定时使用函数默认配置。

  4. 适用于HeaderFieldCookie亲和类型,调用后即可用于请求路由。

  5. 暂不支持自定义 SessionID,需由服务端生成。

  6. 若未提前调用 CreateSession,也可在首次 InvokeFunction 时携带自定义 SessionID,服务端将视为新会话处理。

GetSession - 获取会话配置信息

  1. 获取指定会话的详细信息,包括 SessionID、关联函数、亲和类型、生命周期配置、状态及实例信息。

  2. 支持按 functionName 和 qualifier 精确定位。

  3. 返回指定Active会话的完整配置信息,无法获取过期或 DeleteSession 主动删除的会话信息。

ListSessions - 查询会话信息列表

列举指定函数下的会话列表,支持按 qualifier、状态、sessionID 过滤,并支持分页查询。

  1. 单次最多返回 100 条记录,默认 20 条。

  2. 未传 qualifier 或为 LATEST时,返回所有版本下的会话。

  3. 未传状态时,默认返回 Active 与 Expired 两种状态的会话。

UpdateSession - 更新会话配置

更新会话的闲置过期时间SessionIdleTimeoutInSeconds和会话生命周期参数SessionTTLInSeconds,更新后立即生效,lastModifiedTime自动刷新。可用于动态延长或缩短会话有效期。

DeleteSession - 删除会话资源

  1. 删除指定会话,系统清除相关数据,删除后无法通过 GetSession 或 ListSessions 查询。

  2. 删除后,携带相同 SessionID 的请求将被视为新会话。

  3. 删除时若存在正在运行的请求:

    • 会话隔离场景:相关资源立即释放,正在执行的请求被终止。

    • 非会话隔离场景:请求继续执行至完成,实现优雅退出。