vLLM 四卡部署 Embedding 模型实战:离线部署、Nginx 负载均衡、FastAPI 256D 网关与 systemd 自启

最近把一套 Embedding 服务完整落地了一遍:4 张显卡分别启动 vLLM 实例,用 Nginx 做统一入口和故障切换,再在上层挂一个 FastAPI 网关,把原始向量统一裁剪成 256 维并归一化,最终形成一套比较完整、可直接对外提供服务的 Embedding 架构。

这篇文章把整个过程完整整理一下,包含环境准备、离线模型部署、多卡启动、Nginx 配置、systemd 开机自启,以及业务网关设计。

整套架构的目标很明确:

  • 提供标准 HTTP Embeddings API
  • 支持四卡并行
  • 支持统一入口与负载均衡
  • 单实例故障时自动 failover
  • 支持开机自启
  • 保留日志,便于运维与统计

一、整体架构

先看整体结构:

Client
  │
  ▼
FastAPI Gateway(8681)   ← 推荐对外入口
  │
  ▼
Nginx(8670)            ← 统一入口 / 负载均衡 / failover
  │
  ▼
vLLM ×4(8666~8669)     ← 每张 GPU 一个实例

这一套里面,各层职责很清晰:

  • vLLM 层:真正负责 Embedding 推理
  • Nginx 层:做统一入口、负载均衡和故障切换
  • FastAPI 层:做业务语义统一,例如把原始 768 维向量转换成 256 维并归一化
  • systemd 层:负责服务托管与开机自启

这里选择 vLLM 而不是 sentence-transformers,核心原因是 vLLM 在服务端会自动调度和合批,更适合高并发服务化场景。业务侧主要按“单条请求并发调用”来用就可以,不需要自己强行在客户端凑大 batch。


二、部署前提与基本约定

这套方案基于以下约定:

  • 机器上有 4 张显卡
  • 每张卡启动一个实例
  • 一共暴露 4 个端口:8666866786688669
  • Nginx 使用 8670 作为统一入口
  • FastAPI 网关使用 8681 作为推荐对外入口
  • 模型是本地离线准备好的
  • 推理使用 bfloat16

本地模型目录为:

/home/<USER>/.cache/huggingface/hub/models--google--embeddinggemma-300m

由于采用离线部署,建议固定开启以下环境变量:

export HF_HUB_OFFLINE=1
export TRANSFORMERS_OFFLINE=1

这样可以避免运行时去联网拉取依赖或模型。


三、Conda 环境与依赖安装

先创建一个独立环境:

conda create -n vllm-embed python=3.10 -y
conda activate vllm-embed

安装 vLLM:

pip install -U pip
pip install vllm

这里有一个很关键的坑:transformers 版本要求 huggingface-hub < 1.0,所以要手动把它装回 0.x 版本区间。

pip uninstall -y huggingface-hub
pip install "huggingface-hub>=0.34.0,<1.0"
pip install -U transformers tokenizers safetensors

最后建议做一次版本验证:

python -c "import vllm, transformers, huggingface_hub; print(vllm.__version__, transformers.__version__, huggingface_hub.__version__)"

这一步别省。很多部署问题不是 vLLM 本身出错,而是依赖版本不兼容。


四、先做单卡验证

别一上来就四卡全起。先单卡验证,确认模型能正常返回向量。

CUDA_VISIBLE_DEVICES=0 HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 \
vllm serve /home/<USER>/.cache/huggingface/hub/models--google--embeddinggemma-300m \
  --port 8666 \
  --dtype bfloat16 \
  --runner pooling \
  --convert embed \
  --hf_overrides '{"matryoshka_dimensions":[128,256,512,768]}'

测试请求:

curl http://127.0.0.1:8666/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
    "model": "/home/<USER>/.cache/huggingface/hub/models--google--embeddinggemma-300m",
    "input": "hello"
}'

这里有个重要注意点:这个模型架构 不支持 float16,必须使用 bfloat16float32。这一点如果忽略,会在运行时直接踩坑。


五、四卡启动脚本

单卡验证通过后,就可以正式上四卡。

脚本建议放在:

/home/<USER>/vllm_embed/start_vllm_4gpu.sh

完整脚本如下:

#!/usr/bin/env bash
set -euo pipefail

ENV_NAME="vllm-embed"
MODEL_DIR="/home/<USER>/.cache/huggingface/hub/models--google--embeddinggemma-300m"
BASE_PORT=8666
DTYPE="bfloat16"
HF_OVERRIDES='{"matryoshka_dimensions":[128,256,512,768]}'

export HF_HUB_OFFLINE=1
export TRANSFORMERS_OFFLINE=1

LOG_DIR="$HOME/vllm_embed/vllm_logs"
mkdir -p "$LOG_DIR"

source "$HOME/miniconda3/etc/profile.d/conda.sh"
conda activate "$ENV_NAME"

echo "Starting 4 vLLM servers (High Performance Mode + CPU Affinity)..."

for GPU in 0 1 2 3; do
  PORT=$((BASE_PORT + GPU))
  LOG_FILE="$LOG_DIR/vllm_gpu${GPU}.log"

  echo "GPU=$GPU PORT=$PORT LOG=$LOG_FILE"

  export OMP_NUM_THREADS=8
  export MKL_NUM_THREADS=8
  export CUDA_VISIBLE_DEVICES=$GPU

  nohup vllm serve "$MODEL_DIR" \
    --port "$PORT" \
    --dtype "$DTYPE" \
    --runner pooling \
    --convert embed \
    --hf_overrides "$HF_OVERRIDES" \
    --max-num-seqs 2048 \
    --max-model-len 1024 \
    --gpu-memory-utilization 0.5 \
    --disable-log-requests \
    > "$LOG_FILE" 2>&1 &
done

echo "All started. Logs in: $LOG_DIR"
echo "Check ports: ss -lntp | grep 866"
echo "Check process: ps -ef | grep vllm"

wait

建议目录结构这样放:

/home/<USER>/vllm_embed/
├── start_vllm_4gpu.sh
└── vllm_logs/
    ├── vllm_gpu0.log
    ├── vllm_gpu1.log
    ├── vllm_gpu2.log
    └── vllm_gpu3.log

这里有几个点很重要。

1)一张卡一个实例

不是单进程吃四卡,而是:

1 GPU = 1 vLLM 进程 = 1 端口

这种做法更直观,也更适合后面挂到 Nginx 上做负载均衡。

2)限制 CPU 线程数

脚本里专门加了:

export OMP_NUM_THREADS=8
export MKL_NUM_THREADS=8

原因很现实:4 个实例同时跑,如果不限制 CPU 线程数,就容易互相争抢 CPU,最后整体效率下降。
如果机器 CPU 核心不多,可以把 8 调成 4

3)脚本最后必须有 wait

这一点非常关键。因为后面要交给 systemd 托管,脚本必须前台挂住,否则 service 会误判进程已经结束。


六、Nginx 统一入口与日志

为了不让业务方自己感知 4 个端口,最简单的方式就是在前面挂一层 Nginx。

建议目录结构:

/home/<USER>/nginx/
├── docker-compose.yml
├── nginx.conf
└── logs/

1)docker-compose.yml

这里用 Docker 跑 Nginx,并使用 host 网络模式,这样容器就能直接访问宿主机上的 127.0.0.1:8666~8669

version: "3.8"

services:
  nginx:
    image: nginx:1.26
    container_name: vllm-nginx
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./logs:/var/log/nginx

启动方式:

cd /home/<USER>/nginx
mkdir -p logs
docker compose up -d

2)nginx.conf

完整配置如下:

worker_processes auto;

events {
    worker_connections 65535;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    client_max_body_size 50m;

    log_format embed_stats
        '$time_iso8601 '
        'client=$remote_addr '
        'method=$request_method uri=$uri '
        'status=$status '
        'rt=$request_time '
        'ua="$upstream_addr" '
        'ust="$upstream_status" '
        'urt="$upstream_response_time" '
        'req_len=$request_length resp_bytes=$body_bytes_sent';

    access_log /var/log/nginx/access.log embed_stats;
    error_log /var/log/nginx/error.log warn;

    upstream vllm_embed {
        least_conn;
        server 127.0.0.1:8666 max_fails=3 fail_timeout=30s;
        server 127.0.0.1:8667 max_fails=3 fail_timeout=30s;
        server 127.0.0.1:8668 max_fails=3 fail_timeout=30s;
        server 127.0.0.1:8669 max_fails=3 fail_timeout=30s;
        keepalive 64;
    }

    server {
        listen 8670;

        location / {
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            proxy_connect_timeout 3s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;

            proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
            proxy_next_upstream_tries 3;

            proxy_pass http://vllm_embed;
        }

        location /health {
            return 200 "ok\n";
        }
    }
}

这个配置里最值钱的地方有两个:

第一,least_conn 让请求优先分发给当前连接数更少的后端。

第二,proxy_next_upstream 配合 max_fails / fail_timeout,可以让某个实例挂掉时自动绕开,继续转发到其他后端。也就是说,单个 GPU 或单个 vLLM 实例故障时,整体服务仍然可用,只是吞吐下降。

3)统一入口测试

curl http://127.0.0.1:8670/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
    "model": "/home/<USER>/.cache/huggingface/hub/models--google--embeddinggemma-300m",
    "input": "nginx route test"
}'

4)统计每天成功请求数

Nginx 配了访问日志后,可以直接统计当天成功的 Embedding 请求数:

grep ' uri=/v1/embeddings ' /home/<USER>/nginx/logs/access.log \
| grep ' status=200 ' \
| grep "^$(date +%F)" \
| wc -l

这个方式很土,但非常实用。
而且由于日志里保留了 ua="$upstream_addr",还能观察请求究竟落到了哪个后端端口。


七、用 systemd 托管 vLLM 服务

为了让这 4 个实例具备真正服务化的能力,应该交给 systemd 托管。

服务文件路径:

/home/<USER>/.config/systemd/user/vllm-embed.service

内容如下:

[Unit]
Description=vLLM EmbeddingGemma (4 GPUs)
After=network.target

[Service]
Type=simple
WorkingDirectory=/home/<USER>/vllm_embed
Environment=HF_HUB_OFFLINE=1
Environment=TRANSFORMERS_OFFLINE=1
ExecStart=/home/<USER>/vllm_embed/start_vllm_4gpu.sh
KillMode=control-group
KillSignal=SIGTERM
TimeoutStopSec=30
Restart=on-failure
RestartSec=3
LimitNOFILE=65535

[Install]
WantedBy=default.target

其中有几个配置值得特别说明。

KillMode=control-group

这个很关键。因为 start_vllm_4gpu.sh 会拉起 4 个子进程,停服务时不能只杀掉父 shell,必须把整组子进程一起干净结束。
KillMode=control-group 就是为了解决这个问题。

Restart=on-failure

实例异常退出时自动重启。

LimitNOFILE=65535

把文件句柄上限拉高,避免高并发时被系统默认值卡住。

启用方式

systemctl --user daemon-reload
systemctl --user enable --now vllm-embed.service

开机不登录也能自动拉起

如果你用的是 user service,还需要做这一步:

loginctl enable-linger <USER>

否则机器虽然开机了,但你没登录 SSH 时,这个用户级服务不一定会自动起来。
这一步只需要做一次。


八、常用运维命令

vLLM 服务状态

systemctl --user status vllm-embed.service

启动 / 停止 / 重启

systemctl --user start vllm-embed.service
systemctl --user stop vllm-embed.service
systemctl --user restart vllm-embed.service

查看 systemd 日志

journalctl --user -u vllm-embed.service -f

查看端口监听

ss -lntp | egrep '8666|8667|8668|8669'

查看各实例日志

tail -n 50 /home/<USER>/vllm_embed/vllm_logs/vllm_gpu0.log
tail -n 50 /home/<USER>/vllm_embed/vllm_logs/vllm_gpu1.log
tail -n 50 /home/<USER>/vllm_embed/vllm_logs/vllm_gpu2.log
tail -n 50 /home/<USER>/vllm_embed/vllm_logs/vllm_gpu3.log

Nginx 容器状态

docker ps | grep vllm-nginx
docker logs -f vllm-nginx
docker restart vllm-nginx

PDF 里还给了状态截图,实际效果就是:vllm-embed.service 正常运行,8666~8669 全部处于监听状态。


九、四个原生端口与统一入口的测试命令

逐个测试四个后端端口:

curl http://127.0.0.1:8666/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
    "model": "/home/<USER>/.cache/huggingface/hub/models--google--embeddinggemma-300m",
    "input": ["你好", "测试 embedding"]
}'

curl http://127.0.0.1:8667/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
    "model": "/home/<USER>/.cache/huggingface/hub/models--google--embeddinggemma-300m",
    "input": ["你好", "测试 embedding"]
}'

curl http://127.0.0.1:8668/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
    "model": "/home/<USER>/.cache/huggingface/hub/models--google--embeddinggemma-300m",
    "input": ["你好", "测试 embedding"]
}'

curl http://127.0.0.1:8669/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
    "model": "/home/<USER>/.cache/huggingface/hub/models--google--embeddinggemma-300m",
    "input": ["你好", "测试 embedding"]
}'

再测试总入口:

curl http://127.0.0.1:8670/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
    "model": "/home/<USER>/.cache/huggingface/hub/models--google--embeddinggemma-300m",
    "input": "nginx test"
}'

十、为什么还要再加一层 FastAPI 网关

到这里,四卡 vLLM + Nginx 其实已经能提供标准的 /v1/embeddings 服务了。
但业务侧还有一个额外需求:最终只存 256 维向量,并且语义上与之前 SentenceTransformer(truncate_dim=256, normalize_embeddings=True) 对齐。

而 vLLM 原生返回通常是 768 维向量。
所以这里又加了一层 FastAPI 网关,负责做两件事:

  1. 截断:768 -> 256
  2. 归一化:L2 normalize

网关的上游地址是:

http://127.0.0.1:8670/v1/embeddings

模型字段仍然使用本地模型路径。

推荐业务统一调用这个网关,而不是直接调 8670
也就是说:

  • 8670 /v1/embeddings:内部上游依赖
  • 8681 /embed:真正推荐的业务入口


十一、FastAPI 网关接口设计

1)标准接口

请求:

{"texts":["文本1","文本2"]}

响应:

{"vectors":[[...256 floats...],[...]], "dim":256, "model":"...", "normalized": true}

实际处理逻辑非常简单:

vec256 = vec768[:256]
vec256 = vec256 / (||vec256|| + 1e-12)

这一点与 SentenceTransformertruncate_dim=256 + normalize_embeddings=True 保持语义一致,但不追求 bit-level 完全一致。


十二、FastAPI 网关完整代码

文件路径:

/home/<USER>/vllm_embed/embed_gateway.py

整理后的代码如下:

from __future__ import annotations
from typing import List
import os
import json
import numpy as np
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import asyncio
from contextlib import asynccontextmanager

DIM = int(os.getenv("EMBED_DIM", "256"))
VLLM_EMBEDDINGS_URL = os.getenv(
    "VLLM_EMBEDDINGS_URL",
    "http://127.0.0.1:8670/v1/embeddings"
)
MODEL = os.getenv(
    "VLLM_MODEL",
    "/home/<USER>/.cache/huggingface/hub/models--google--embeddinggemma-300m"
)

http_client = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    global http_client
    limits = httpx.Limits(max_keepalive_connections=200, max_connections=500)
    http_client = httpx.AsyncClient(limits=limits, timeout=120.0)
    yield
    await http_client.aclose()

app = FastAPI(title="Embedding Gateway", version="5.0", lifespan=lifespan)

class EmbedRequest(BaseModel):
    texts: List[str] = Field(..., description="List of raw texts")

class EmbedResponse(BaseModel):
    vectors: List[str]
    dim: int
    model: str
    normalized: bool

class EmbedRowRequest(BaseModel):
    row: List[str]

class EmbedRowResponse(BaseModel):
    result_row: List[str]

def process_vectors_numpy(embeddings_list: List[List[float]]) -> List[str]:
    if not embeddings_list:
        return []

    mat = np.array(embeddings_list, dtype=np.float32)

    if mat.shape[1] > DIM:
        mat = mat[:, :DIM]

    norms = np.linalg.norm(mat, axis=1, keepdims=True)
    norms[norms == 0] = 1e-12
    mat = mat / norms

    return [json.dumps(vec.tolist()) for vec in mat]

async def _get_vectors_internal(texts: List[str]) -> List[str]:
    if not texts:
        return []

    try:
        resp = await http_client.post(
            VLLM_EMBEDDINGS_URL,
            json={"model": MODEL, "input": texts},
        )
        resp.raise_for_status()
    except Exception as e:
        raise HTTPException(status_code=502, detail=f"vLLM error: {str(e)}")

    payload = resp.json()
    raw_vectors = [item["embedding"] for item in payload.get("data", [])]

    loop = asyncio.get_running_loop()
    try:
        json_vectors = await loop.run_in_executor(
            None,
            process_vectors_numpy,
            raw_vectors
        )
        return json_vectors
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Numpy processing error: {e}")

@app.get("/health")
async def health():
    return {"ok": True}

@app.post("/embed", response_model=EmbedResponse)
async def embed(req: EmbedRequest):
    vectors = await _get_vectors_internal(req.texts)
    return EmbedResponse(vectors=vectors, dim=DIM, model=MODEL, normalized=True)

@app.post("/embed_row", response_model=EmbedRowResponse)
async def embed_row(req: EmbedRowRequest):
    if not req.row:
        return EmbedRowResponse(result_row=[])

    keyword = req.row[0] if len(req.row) > 0 else ""
    questions = req.row[1:]

    indices_to_embed = []
    texts_to_send = []

    for i, q in enumerate(questions):
        if q and q.strip():
            if keyword and keyword.strip():
                combined = f"{keyword} : {q}"
            else:
                combined = q
            texts_to_send.append(combined)
            indices_to_embed.append(i)

    valid_vec_strs = []
    if texts_to_send:
        valid_vec_strs = await _get_vectors_internal(texts_to_send)

    final_row = []
    final_row.append(keyword)

    vec_ptr = 0
    for i, q in enumerate(questions):
        final_row.append(q)
        if i in indices_to_embed:
            if vec_ptr < len(valid_vec_strs):
                final_row.append(valid_vec_strs[vec_ptr])
                vec_ptr += 1
            else:
                final_row.append("")
        else:
            final_row.append("")

    return EmbedRowResponse(result_row=final_row)

这个实现里有两个很好的点。

1)连接池做了并发优化

httpx.AsyncClient 使用了:

limits = httpx.Limits(max_keepalive_connections=200, max_connections=500)

这对高并发场景很有帮助。

2)IO 和 CPU 分离

请求 vLLM 是 IO;Numpy 做截断和归一化是 CPU。
代码里用了:

loop.run_in_executor(None, process_vectors_numpy, raw_vectors)

把 CPU 密集型计算丢到线程池去做,避免阻塞 FastAPI 的事件循环。
这点写得是比较专业的。


十三、embed_row 业务接口

除了标准的 /embed,这套网关里还做了一个很实用的业务接口:/embed_row

它专门处理类似这样的行数据:

[keyword, q1, q2, q3, q4, q5, q6]

内部逻辑是把每个问题自动拼成:

keyword : question

然后只对非空问题计算 embedding,最终输出:

[keyword, q1, vec1, q2, vec2, ...]

这种设计的好处是:

  • 自动跳过空值
  • 保证结果顺序稳定
  • 对行式数据处理非常方便
  • 适合后续直接喂给表格、数据库或业务流水线

如果你的数据本来就是“关键词 + 多个问题”的结构,这个接口会比标准的批量 /embed 更顺手。


十四、用 systemd 托管 FastAPI 网关

启动前先安装依赖:

pip install gunicorn uvicorn httpx numpy

网关的 systemd 文件路径:

/home/<USER>/.config/systemd/user/embed-gw.service

内容如下:

[Unit]
Description=Embedding Gateway (vLLM -> 256D)
After=network.target

[Service]
Type=simple
WorkingDirectory=/home/<USER>/vllm_embed
Environment=VLLM_EMBEDDINGS_URL=http://127.0.0.1:8670/v1/embeddings
Environment=VLLM_MODEL=/home/<USER>/.cache/huggingface/hub/models--google--embeddinggemma-300m
Environment=EMBED_DIM=256
ExecStart=/home/<USER>/miniconda3/envs/vllm-embed/bin/gunicorn embed_gateway:app --workers 16 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8681 --timeout 300
Restart=on-failure
RestartSec=3
LimitNOFILE=65535

[Install]
WantedBy=default.target

启用方式:

systemctl --user daemon-reload
systemctl --user enable --now embed-gw.service

还是那句话,如果想开机不登录也能启动,别忘了:

loginctl enable-linger <USER>


十五、FastAPI 网关测试

健康检查:

curl http://127.0.0.1:8681/health

单条测试:

curl http://127.0.0.1:8681/embed \
  -H "Content-Type: application/json" \
  -d '{"texts":["hello"]}'

维度校验:

curl -s http://127.0.0.1:8681/embed \
  -H "Content-Type: application/json" \
  -d '{"texts":["hello"]}' \
| python -c "import sys,json; d=json.load(sys.stdin); print(d['dim'], len(d['vectors'][0]))"

如果一切正常,输出应该能看到:

  • dim = 256
  • 向量长度也是 256


十六、这套方案的价值

到这里,这套架构就完整了。

底层是四个 vLLM 实例,吃满四张卡;中间用 Nginx 做统一入口和故障切换;最上面再用 FastAPI 做语义层网关,统一向量维度和输出格式。
同时 systemd 负责托管,保证开机自启与异常重启。

整套方案有几个很实际的优点:

  • 吞吐高:vLLM 自动调度和合批,比传统 ST 服务化更合适
  • 架构清晰:一张卡一个实例,排障简单
  • 高可用更强:单实例故障时 Nginx 自动绕过
  • 业务统一:通过 FastAPI 固定输出 256D + normalize
  • 部署友好:支持离线模型、本地路径、systemd 托管
  • 可运维:日志、端口、服务状态都很明确

如果只是做实验,这套会显得有点重。
但如果要稳定对外提供 Embedding 服务,这套架构就很合理了。它已经不再是“能跑就行”的脚本堆叠,而是一套比较完整的服务化部署方案。

Read more

传统 SaaS 转向 AI 时代,我目前的一点理解:先把数据能力变成 Agent 可调用的基础设施

最近我一直在思考一个问题:传统 SaaS 到底应该怎么转向 AI? 一开始很容易想到的方向是:给原来的系统加一个 AI 助手。 比如在页面右下角放一个聊天框,让用户可以问数据、生成报告、总结内容、解释指标。这个当然有价值,但我现在越来越觉得,这只是比较表层的一种转型。 真正的变化,可能不是“在 SaaS 里面加 AI”,而是 SaaS 本身的能力形态发生变化。 过去的 SaaS,核心是给人使用。 人登录系统,看页面、点按钮、筛选数据、导出报表、判断问题,然后再去做决策。数据库是给 Web 页面供数的,后端 API 是给前端页面服务的,整个产品的中心是“人如何操作软件”。 但 AI 时代,尤其是 Agent 逐渐发展之后,

By ladydd

对 Python 应用场景的一次重新思考:FastAPI、协程、线程、数据库与任务系统边界

最近在重新设计一个任务系统时,我顺便把自己对 Python,尤其是 CPython 应用场景的理解重新梳理了一遍。 这次讨论的背景是一个典型的异步任务服务: 上游提交任务 API 立即返回 task_id 后台 worker 慢慢执行 用户通过 task_id 查询任务状态 任务主要是 LLM 调用、图片下载、外部 HTTP 请求这类 I/O 型工作。 一开始关注的是队列、Redis、PostgreSQL、worker 并发控制这些问题。但聊到后面,其实更核心的问题变成了: Python 到底应该放在什么位置? 哪些并发适合 Python? 哪些并发不要硬塞给 Python? FastAPI、协程、线程、数据库之间应该怎么分工? 这篇文章就是这次思考的整理。 一、我不想抛弃 Python,

By ladydd

Go 和 Python 的并发模型对比:进程、线程、协程、并发和并行到底怎么理解?

最近我在写 worker 任务系统的时候,重新理解了一遍 Python 和 Go 的并发差异。 以前写 Python,多 worker 经常要考虑: 多进程怎么管理? 日志会不会串? 一个 worker 崩了怎么办? 怎么吃满多核心? 后来换成 Go,发现一个进程里开多个 goroutine worker 就很自然: go worker(1) go worker(2) go worker(3) go worker(4) 日志也好管,状态也好管,而且单进程还能利用多个 CPU 核心。 一开始很容易误会成: Python 不行,Go 行 但更准确的理解应该是: Python 和

By ladydd

Python 进程和 Go 进程的区别:为什么 Go 单进程多 worker 用起来更爽?

最近我在做 worker 任务系统的时候,突然意识到一个很关键的问题: 以前写 Python,多 worker 的时候经常要小心日志串、文件切割乱、时间不好管理。 但是换成 Go 以后,一个进程里开多个 goroutine worker,反而可以比较自然地写到同一个日志文件里。 一开始我以为这是“Python 和 Go 写日志能力不一样”,后来想明白了,核心不是日志本身,而是: Python 常见 worker 模型:多进程 Go 常见 worker 模型:单进程 + 多 goroutine 这背后其实是两个语言在并发模型上的巨大差异。 一、进程、线程、goroutine 先分清楚 先把几个概念捋一下。 进程:操作系统分配资源的单位 线程:CPU 调度执行的基本单位

By ladydd
陕公网安备61011302002223号 | 陕ICP备2025083092号