You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Docker部署的FastAPI应用运行一段时间后无法接收请求

问题描述

通过Portainer部署Docker中的FastAPI应用,启动初期运行正常,但一段时间后突然无法接收任何请求。日志无请求记录,使用curl访问Docker网桥端口时请求会无限挂起。Portainer采用基础配置,仅设置了随机端口映射。

Dockerfile配置

FROM python:3.10-slim

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV POETRY_VERSION=1.8.2

WORKDIR /app

RUN apt-get update && apt-get install -y \
    curl \
    build-essential \
    postgresql-client \
    libpq-dev \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

RUN curl -sSL https://install.python-poetry.org | python3 -

ENV PATH="${PATH}:/root/.local/bin"

RUN mkdir -p $HOME/.postgresql

RUN curl --create-dirs -o $HOME/.postgresql/root.crt 'https://cockroachlabs.cloud/clusters/.../cert'

COPY pyproject.toml poetry.lock* ./

RUN poetry config virtualenvs.create false \
    && poetry install --no-interaction --no-ansi

COPY . .

EXPOSE 8121

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8121"]

main.py 文件内容

import secrets
from datetime import datetime
from typing import List

import fastapi
import uvicorn
from fastapi import Depends, HTTPException, Security
from fastapi.security import APIKeyHeader

from gpt_proxy.utils import mask_token

from .config import ADMIN_KEY, log
from .db import USERS, add_user, db_close, db_init, del_user
from .firebase_manager import Firebase
from .models import TokenCreate, TokenResponse, UserToken
from .openai_forward import OpenAiForward

app = fastapi.FastAPI()
forwarder = OpenAiForward()
api_key_header = APIKeyHeader(name="X-Admin-Key", auto_error=True)


def verify_admin_key(api_key: str = Security(api_key_header)):
    if api_key != ADMIN_KEY:
        raise HTTPException(status_code=403, detail="Invalid admin key")
    return api_key


@app.on_event("startup")
async def startup():
    log.info("Starting up OpenAI Forward application")
    await db_init()
    log.info("Application startup complete")


@app.on_event("shutdown")
async def shutdown():
    log.info("Shutting down OpenAI Forward application")
    if forwarder.client:
        await forwarder.client.close()
    await db_close()
    log.info("Application shutdown complete")


@app.post("/tokens", response_model=TokenResponse)
async def create_token(
    token_request: TokenCreate, api_key: str = Depends(verify_admin_key)
):
    new_token = f"mn-{secrets.token_urlsafe(32)}"
    return await add_user(username=token_request.username, token=new_token)


@app.delete("/tokens/{username}")
async def delete_token(username: str, api_key: str = Depends(verify_admin_key)):
    await del_user(username)
    return {"message": f"Token for user {username} has been deleted"}


@app.get("/tokens", response_model=List[UserToken])
async def list_users(api_key: str = Depends(verify_admin_key)):
    users = []
    for user in USERS:
        user["token"] = mask_token(user["token"])
        users.append(user)
    return users


@app.route(
    "/{api_path:path}",
    methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"],
)
async def _handle_openai_request(request: fastapi.Request):
    return await forwarder.reverse_proxy(request)


if __name__ == "__main__":
    log.info("Starting OpenAI Forward server")
    uvicorn.run(app, host="0.0.0.0", port=8010)

数据库初始化代码

import time

from tortoise import Tortoise

from ..config import log
from .models import FirebaseToken, User

USERS = []


async def _do_migration() -> None:
    old_users = {
        ....
    }

    for k, v in old_users.items():
        _ = await User.get_or_create(username=k, token=v, created_at=time.time())


async def _get_all_users() -> None:
    users = await User.all()
    if not users:
        await _do_migration()
        await _get_all_users()
    for user in users:
        USERS.append(dict(user))


async def db_init() -> None:
    await Tortoise.init(db_url=DB_URI, modules={"models": ["gpt_proxy.db.models"]})

    await Tortoise.generate_schemas()
    await _get_all_users()

OpenAI转发类代码

import asyncio
import time
from functools import wraps
from typing import Any, AsyncGenerator, Callable, Tuple, Type, TypeVar

import aiohttp
import anyio
import fastapi
from fastapi import HTTPException
from starlette.responses import BackgroundTask, StreamingResponse

from .config import log
from .firebase_manager import Firebase
from .models import ClientConfig
from .utils import get_token_user, header_cloudflare_safe, mask_token

T = TypeVar("T")


def async_retry(
    max_retries: int = 3,
    delay: float = 1.0,
    backoff: float = 2.0,
    exceptions: Tuple[Type[Exception], ...] = (Exception,),
) -> Callable:
    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        @wraps(func)
        async def wrapper(*args: Any, **kwargs: Any) -> T | None:
            current_delay = delay
            for attempt in range(max_retries + 1):
                try:
                    if attempt > 0:
                        log.info(
                            f"Retrying {func.__name__}, attempt {attempt}/{max_retries} "
                            f"after {current_delay:.2f}s delay"
                        )
                        await anyio.sleep(current_delay)
                        current_delay *= backoff

                    return await func(*args, **kwargs)

                except exceptions as e:
                    log.warning(
                        f"Attempt {attempt + 1}/{max_retries + 1} failed for {func.__name__}: "
                        f"{type(e).__name__}: {str(e)}"
                    )

                    if attempt == max_retries:
                        log.error(
                            f"All retry attempts failed for {func.__name__}. "
                            f"Final exception: {type(e).__name__}: {str(e)}"
                        )
                        raise

            return None

        return wrapper

    return decorator


class OpenAiForward:
    def __init__(self) -> None:
        log.info("Initializing OpenAI Forward")
        self.base_url = "https://api.openai.com/"
        self.client: aiohttp.ClientSession | None = None
        self.firebase = Firebase()

    async def _init_client(self) -> None:
        if self.client is None:
            log.info("Initializing aiohttp client session")
            tcp_connector = aiohttp.TCPConnector(
                limit=500, limit_per_host=0, force_close=False
            )
            self.client = aiohttp.ClientSession(connector=tcp_connector)
            log.info("aiohttp client session initialized")

    async def _get_token(self, token: str):
        log.info(f"Processing token {mask_token(token)}")
        if token.startswith("mn-"):
            username = await get_token_user(token)
            if not username:
                log.info("Using direct token")
                return None, token
            fb_token = await self.firebase.get_token()
            return username, fb_token
        else:
            log.info("Using direct token")
            return None, token

    async def iter_bytes(
        self, response: aiohttp.ClientResponse, request: fastapi.Request
    ) -> AsyncGenerator[bytes, Any]:
        log.info(f"Streaming response for {request.url.path}")
        async for chunk, _ in response.content.iter_chunks():
            yield chunk

    @async_retry(
        max_retries=3,
        delay=0.2,
        backoff=0.2,
        exceptions=(
            aiohttp.ServerTimeoutError,
            aiohttp.ServerConnectionError,
            aiohttp.ServerDisconnectedError,
            asyncio.TimeoutError,
            anyio.EndOfStream,
            RuntimeError,
        ),
    )
    async def send(
        self, client_config: ClientConfig, data: dict | None = None
    ) -> aiohttp.client.ClientRequest | Any | None:
        if not self.client:
            await self._init_client()

        log.info(f"Sending {client_config.method} request to {client_config.url}")
        if self.client:
            return await self.client.request(
                method=client_config.method,
                url=client_config.url,
                data=data,
                headers=client_config.headers,
            )
        return None

    async def prepare_config(self, request: fastapi.Request) -> ClientConfig:
        headers: dict = header_cloudflare_safe(request)
        original_bearer: str = headers.get(
            "Authorization", headers.get("authorization")
        )

        if original_bearer:
            token: str = original_bearer.split()[-1].strip()
            user, replacement_token = await self._get_token(token)

            if replacement_token is None:
                raise HTTPException(status_code=401, detail="Invalid token")

            auth_header = f"Bearer {replacement_token}"
            if "Authorization" in headers:
                headers["Authorization"] = auth_header
            elif "authorization" in headers:
                headers["authorization"] = auth_header

            log.info(
                f"Token processing: User={user or 'direct'}, "
                f"Using={'Firebase' if user else 'direct'} token"
            )

        url = f"https://api.openai.com/{request.url.path}"
        if request.url.query:
            url = f"{url}?{request.url.query}"

        return ClientConfig(
            headers=headers,
            method=request.method,
            url=url,
        )

    async def reverse_proxy(self, request: fastapi.Request) -> StreamingResponse:
        request_id = str(time.time())
        log.info(
            f"[{request_id}] Incoming request: {request.method} {request.url.path}"
        )

        config = await self.prepare_config(request)
        body = await request.body()
        data = body if body else None

        try:
            log.info(f"[{request_id}] Forwarding request to OpenAI")
            response = await self.send(config, data=data)
            log.info(f"[{request_id}] OpenAI response received: {response.status}")

            return StreamingResponse(
                self.iter_bytes(response, request),
                status_code=response.status,
                media_type=response.headers.get("content-type"),
                background=BackgroundTask(response.release),
            )

        except aiohttp.ClientError as e:
            log.exception(f"[{request_id}] Failed to forward request to OpenAI")
            raise fastapi.HTTPException(
                status_code=fastapi.status.HTTP_502_BAD_GATEWAY,
                detail=f"Failed to forward request: {str(e)}",
            )
        except Exception as e:
            log.exception(f"[{request_id}] Unexpected error during request forwarding")
            raise fastapi.HTTPException(
                status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail=f"Internal server error: {str(e)}",
            )

内容的提问来源于stack exchange,提问作者Marcel Alexandru

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 04:19:53