1. 需求背景与挑战
在 Kubernetes (K8S) 环境中,为满足临时需求而新增一个服务,传统流程通常需要:
- 编写服务代码。
- 创建 Dockerfile。
- 构建并推送 Docker 镜像到仓库。
- 编写或修改 K8S 部署清单(Deployment/Service)。
- 应用清单,等待 Pod 启动。
这个过程耗时较长,尤其在快速验证、临时调试或部署一次性任务时显得笨重。用户希望找到一种更简便的方法:避开创建 Docker 镜像的步骤,直接通过一个 API 上传文件(如可执行文件、模型文件、配置文件),就能在 K8S 的某个容器(Container)上动态创建并运行服务,例如快速部署一个 Ollama 服务来加载上传的模型。
2. 核心思路:利用 Init Container 与共享 Volume
K8S 本身不直接支持"上传文件即服务",但我们可以通过组合现有资源来实现类似效果。核心思路如下:
- 设计文件上传 API:该 API 运行在主容器或集群内另一个服务中,负责接收文件并存储到某个持久化存储(如 PVC、ConfigMap、或云存储)。
- 主容器启动服务:主容器启动时,从共享 Volume 读取文件,并执行相应命令来启动目标服务(如 Ollama)。
这样,更新服务只需通过 API 上传新文件,然后重启服务,而无需重建 Docker 镜像。
3. 架构设计与组件
下图展示了该方案的组件交互流程:
cs
flowchart TD
A[用户/客户端] -->|1. 上传文件| B[文件上传 API]
B -->|2. 存储文件| C[(持久化存储
如 PVC/MinIO)]
D -->|3. 读取文件| E
F -->|4. 启动服务| G[服务端点]
A -->|5. 访问服务| G
关键组件说明:
- 文件上传 API :一个简单的 Web 服务,提供
POST /upload接口,将接收到的文件写入后端存储。可以用 Python Flask、Go Gin 或 Node.js Express 快速实现。 - 持久化存储:选择取决于文件大小和访问模式。对于模型等大文件,推荐使用与云平台集成的对象存储(如 AWS S3、MinIO)或 ReadWriteMany 类型的 PVC。
- 主容器:基础镜像包含目标服务的运行时环境(如ASP.NET 镜像)。启动脚本文件并启动服务。
4. 实战示例:动态部署 Ollama 服务
假设我们已有 K8S 集群,并希望动态部署 Ollama 来加载用户上传的 GGUF 模型文件。
4.1 准备文件上传 API(ASP.NET 示例)
Program.cs
cs
using Microsoft.AspNetCore.Http.Features;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = null;
});
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = long.MaxValue;
options.ValueLengthLimit = int.MaxValue;
options.MultipartHeadersLengthLimit = int.MaxValue;
});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "LargeFileUploadApi",
Version = "v1"
});
});
var app = builder.Build();
Directory.CreateDirectory(Path.Combine(app.Environment.ContentRootPath, "uploads"));
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "uploads")),
RequestPath = "/uploads"
});
app.MapControllers();
app.MapGet("/", () => Results.Ok(new
{
service = "LargeFileUploadApi",
swagger = "/swagger",
upload = "/api/files/upload",
health = "/health"
}));
app.Run();
FilesController.cs
cs
using Microsoft.AspNetCore.Mvc;
namespace LargeFileUploadApi.Controllers;
[ApiController]
[Route("api/files")]
public sealed class FilesController : ControllerBase
{
private readonly IWebHostEnvironment _environment;
public FilesController(IWebHostEnvironment environment)
{
_environment = environment;
}
[HttpPost("upload")]
public async Task<ActionResult<UploadResult>> Upload(IFormFile file, CancellationToken cancellationToken)
{
if (file is null || file.Length == 0)
{
return BadRequest(new { error = "No file was provided." });
}
var uploadRoot = Path.Combine(_environment.ContentRootPath, "uploads");
Directory.CreateDirectory(uploadRoot);
var fileName = Path.GetFileName(file.FileName);
var filePath = Path.Combine(uploadRoot, fileName);
await using var targetStream = System.IO.File.Create(filePath);
await file.CopyToAsync(targetStream, cancellationToken);
var fileInfo = new FileInfo(filePath);
return Ok(new UploadResult(
fileName,
fileInfo.Length,
$"/uploads/{Uri.EscapeDataString(fileName)}"));
}
[HttpGet("health")]
public IActionResult Health()
{
return Ok(new { status = "ok" });
}
}
public sealed record UploadResult(string FileName, long SizeInBytes, string DownloadUrl);
将此 API 部署为 K8S Service。
4.2 打包 Docker Image
比如我在本地执行:
4.2.1 发布API,并复制发布文件到Dockerfile同目录的publish中
cs
dotnet publish -c Release -o publish -r linux-x64
4.2.2 编写Dockerfile
Dockerfile
cs
FROM harbor.xxx.com/base_image/aspnet:10.0
ENV ASPNETCORE_ENVIRONMENT=Development
ENV TZ=Asia/Shanghai
ENV LANG=en_US.UTF-8
# 1. 创建用户
RUN adduser --disabled-password myuser
# 2. 创建应用目录 (此时还是 root 权限)
RUN mkdir -p /app
# 3. 将目录所有权赋予 myuser
RUN chown -R myuser:myuser /app
# 4. 设置工作目录
WORKDIR /app
# 5. 复制文件 (此时还是 root 权限,可以写入 /app)
COPY publish/ .
# 6. 再次确保所有复制进来的文件归属权正确 (可选,但推荐)
RUN chown -R myuser:myuser /app
# 7. 切换到非 root 用户
# USER myuser
# 8. 启动命令
EXPOSE 80 11434 5000 5001
ENV ASPNETCORE_URLS http://0.0.0.0:80
ENTRYPOINT ["dotnet", "LargeFileUploadApi.dll"]
4.2.3 docker build
docker_build.sh
cs
export HARBOR_URL=harbor.xxx.com
export HARBOR_PROJECT=k8sdev
export HARBOR_USER=robot\$k8sdev+test
export HARBOR_PASSWORD=**************
export DOCKER_IMAGE=dev/docker-debug:1.0.0
# delete docker image
docker rmi "$HARBOR_URL/$HARBOR_PROJECT/$DOCKER_IMAGE"
# docker build
docker build --no-cache -t "$HARBOR_URL/$HARBOR_PROJECT/$DOCKER_IMAGE" .
# run docker image
# docker run -it --rm --name docker-debug -p 801:80 -p 802:11434 -p 803:5000 -p 804:5001 "$HARBOR_URL/$HARBOR_PROJECT/$DOCKER_IMAGE"
# docker exec -it docker-debug /bin/bash
#psuh docker image to Harbor
#docker tag mcr.microsoft.com/dotnet/sdk:9.0 "$HARBOR_URL/$HARBOR_PROJECT/dotnetsdk:9.0.202"
##docker login -u $HARBOR_USER -p $HARBOR_PASSWORD $HARBOR_URL
##docker push "$HARBOR_URL/$HARBOR_PROJECT/$DOCKER_IMAGE"
##docker logout "$HARBOR_URL"
4.2 创建 K8S Deployment 清单
cs
略...
4.3 操作流程
-
部署基础设施:部署文件上传 API。
-
编写Modelfile :
cs# 指向容器内模型文件的路径 FROM /root/.ollama/models/Qwen3.5-4B-Q4_K_M.gguf # 设置模型参数(可选) PARAMETER temperature 0.7 PARAMETER top_p 0.9 PARAMETER num_ctx 4096 # 设置系统提示词(可选) SYSTEM "You are a helpful assistant." -
编写 entrypoint.sh : 启动 ollama。
cs#!/bin/bash set -e # 1. 设置环境变量,允许所有网络接口访问 export OLLAMA_HOST=0.0.0.0:11434 # 定义模型名称 MODEL_NAME="Qwen3.5-4B-Q4_K_M" MODELFILE_PATH="/root/.ollama/models/Modelfile" # 1. 启动 Ollama 后台服务 # 使用 & 让 serve 在后台运行,并将输出重定向到日志文件以便调试 echo ">>> 正在启动 Ollama 后台服务..." nohup ollama serve > /tmp/ollama_server.log 2>&1 & SERVE_PID=$! disown $SERVE_PID # <--- 关键添加:将进程从当前 shell 会话中分离 # 2. 等待服务完全启动并监听端口 # Ollama 启动通常需要几秒钟,这里通过循环检查端口或健康接口来确保服务可用 echo ">>> 等待 Ollama 服务就绪..." MAX_RETRIES=30 RETRY_COUNT=0 while ! curl -s http://localhost:11434/ > /dev/null 2>&1; do RETRY_COUNT=$((RETRY_COUNT + 1)) if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then echo "!!! 错误: Ollama 服务启动超时,请检查日志 /tmp/ollama_server.log" kill $SERVE_PID exit 1 fi sleep 1 done echo ">>> Ollama 服务已就绪。" # 3. 检查模型是否存在 # 此时服务已启动,ollama list 可以正常工作 if ! ollama list | grep -q "$MODEL_NAME"; then echo ">>> 模型未找到,正在注册模型: $MODEL_NAME ..." # 检查 Modelfile 是否存在 if [ ! -f "$MODELFILE_PATH" ]; then echo "!!! 错误: 找不到 Modelfile: $MODELFILE_PATH" kill $SERVE_PID exit 1 fi # 创建模型 if ollama create "$MODEL_NAME" -f "$MODELFILE_PATH"; then echo ">>> 模型注册成功!" else echo "!!! 错误: 模型注册失败" kill $SERVE_PID exit 1 fi else echo ">>> 模型 $MODEL_NAME 已存在,跳过注册步骤。" fi echo ">>> 初始化完成。Ollama 服务正在后台运行 (PID: $SERVE_PID)" echo ">>> 提示: 当前脚本即将结束,如果需要保持服务长期运行,请不要关闭此终端,或使用 nohup/systemd 管理。" # 4. 保持脚本运行,防止后台服务随脚本退出而被杀死(可选) # 如果这是启动脚本的最后一步,通常我们希望服务继续运行。 # wait $SERVE_PID 会让脚本挂起,直到 serve 进程结束。 wait $SERVE_PID -
上传文件 :用户通过 API 上传
Ollama package, Model, Modelfile, entrypoint文件。cs# 1.上传文件 on your PC (Ollama package, Model, Modelfile, entrypoint) cd /Docker/ollama/ && \ zstd -d /model/ollama-linux-amd64.tar.zst -o ./ollama-linux-amd64.tar && \ curl -X POST -F "file=@./ollama-linux-amd64.tar" http://localhost:801/api/files/upload && \ rm ./ollama-linux-amd64.tar && \ curl -X POST -F "file=@/models/Qwen3.5-4B-Q4_K_M.gguf" http://localhost:801/api/files/upload && \ curl -X POST -F "file=@/Docker/ollama/Modelfile" http://localhost:801/api/files/upload && \ curl -X POST -F "file=@/Docker/ollama/entrypoint.sh" http://localhost:801/api/files/upload -
安装并启动 Ollama :Ollama manual install: https://docs.ollama.com/linux#manual-install
cs# 2.安裝Ollama on Docker-debug container curl -fsSL http://localhost/uploads/ollama-linux-amd64.tar \ | tar x -C /usr mkdir -p /root/.ollama/models && \ mv /app/uploads/Qwen3.5-4B-Q4_K_M.gguf /root/.ollama/models/Qwen3.5-4B-Q4_K_M.gguf && \ mv /app/uploads/Modelfile /root/.ollama/models/Modelfile chmod +x /app/uploads/entrypoint.sh && \ /app/uploads/entrypoint.sh -
Ollama 加载模型:主容器启动,检测到文件后启动 Ollama 并加载模型。
-
访问服务 :通过 Service
ollama-service:11434访问 Ollama API。
5. 方案优缺点与注意事项
优点
- 快速迭代:无需每次修改都构建镜像,只需上传文件就可启动新增的服务。
- 镜像通用 :基础容器镜像(如
aspnet)可复用,通过外部文件改变其行为。 - 适合临时需求:完美应对临时调试、一次性任务或快速原型验证。
缺点与注意事项
- 文件管理:需要自行管理存储中的文件版本、清理策略。
- 安全性:上传 API 需做好认证、授权和文件类型校验,防止恶意文件上传。
7. 总结
通过"文件上传 API"的模式,我们可以在 K8S 上实现一种近似"免镜像"的动态服务部署。虽然仍需基础容器镜像,但将易变的业务文件(模型、脚本、配置)外置,大大提升了临时需求的处理效率。此方案特别适合 AI 模型部署、临时数据处理任务等场景。在实际应用中,请根据安全、性能和运维复杂度权衡,选择最合适的存储方案和文件同步机制。