1. 程式人生 > 實用技巧 >【FastAPI 】踩坑總結

【FastAPI 】踩坑總結


閱讀目錄

一、部署之殤

二、日誌之殤

三、中介軟體之殤

四、配置檔案之殤

五、其它

一、部署之殤

1 linux後臺啟動

nohup uvicorn main:app --host 0.0.0.0 --port 8080 

2 Docker部署

FROM python:3.7
RUN pip install fastapi uvicorn
EXPOSE 80
COPY ./app /app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

2.1 Docker + gunicorn

gunicorn配置檔案

#!usr/bin/env python
# encoding: utf-8
import multiprocessing

# 監聽埠
bind = '0.0.0.0:8899'
# 工作模式
worker_class = 'uvicorn.workers.UvicornWorker'
# 並行工作程序數
workers = multiprocessing.cpu_count() * 2 + 1
# 設定守護程序
#daemon = True
# 配置檔案方式配置日誌
logconfig = "./logger.ini"

Dockerfile

FROM python:3.7

ENV TZ Asia/Shanghai

#將專案程式碼放入映象
COPY . /app

WORKDIR /app

#安裝第三方模組,更新資料庫
RUN pip install -r requirements.txt -i http://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com \
&& rm -rf configure

ENTRYPOINT ["gunicorn", "-c", "gunicorn.conf.py", "main:app"]

3 k8s部署

3.1 service.yaml

apiVersion: v1
kind: Service
metadata:
  name: project_name   # 專案名稱
spec:
  ports:
  - name: http
    port: 80
    targetPort: 8899
  type: ClusterIP

3.2 deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: project_name   # 專案名稱
spec:
  template:
    spec:
      imagePullSecrets:
      - name: registry-pull-secret
      containers:
        - name: project_name   # 專案名稱
          image: registry-vpc.cn-shanghai.aliyuncs.com/xxx/project_name:lates  # 映象
          imagePullPolicy: Always
          volumeMounts:
            - name: host-time
              mountPath: /etc/localtime
      volumes:
      - hostPath:
          path: /etc/localtime
        name: host-time

二、日誌之殤

1 日誌配置

日誌配置檔案,本地環境、測試環境、生產環境可以配置不同的日誌的列印

[loggers]
;這裡面把uvicorn建立的logger配置都覆蓋了,注意最後一個`,`不能缺少、防止日誌多次列印
keys=root, gunicorn.error, gunicorn.access,uvicorn.error,uvicorn.access,

[handlers]
keys=error_file, access_file

[formatters]
keys=generic, access

[logger_root]
level=DEBUG
handlers=access_file

[logger_]
level=INFO
handlers=access_file
qualname=
propagate=0

[logger_uvicorn.error]
level=INFO
handlers=error_file
qualname=uvicorn.error
propagate=0

[logger_uvicorn.access]
level=INFO
handlers=access_file
qualname=uvicorn.access
propagate=0

[logger_gunicorn.error]
level=INFO
handlers=error_file
propagate=1
qualname=gunicorn.error

[logger_gunicorn.access]
level=INFO
handlers=access_file
propagate=0
qualname=gunicorn.access

;注意日誌配置的地址
[handler_error_file]
class=logging.FileHandler
formatter=generic
args=('/app/log/error.log',)

[handler_access_file]
class=logging.FileHandler
formatter=access
args=('/app/log/access.log',)

[formatter_generic]
format=[%(asctime)s] %(levelname)s in %(module)s: %(message)s
datefmt=%Y-%m-%d %H:%M:%S
class=logging.Formatter

;配置日誌列印的資訊
[formatter_access]
format=[%(asctime)s] %(levelname)s in %(module)s: %(message)s
class=logging.Formatter

2 讀取配置

方案:讀取檔案 or 啟動時設配置

# 環境變數
fast_api_env = os.environ.get('FAST_API_ENV')

# 獲取logger物件 
def get_logger(filename="logger.ini", logger_name='root'):
    logging.config.fileConfig(fname=filename, disable_existing_loggers=False)
    return logging.getLogger(logger_name)

def init_log():
    """初始化日誌"""
    print("載入log檔案...")
    try:
        global common_config
        if fast_api_env == 'local':
            # 本地環境
            LOG_CONFIG_PATH = os.path.join(BASE_DIR, 'conf', 'logger-local.ini')
            # logger = get_logger(os.path.join(BASE_DIR, 'conf', 'logger.ini'))
        else:
            common_config.LOG_CONFIG_PATH = os.path.join(BASE_DIR, 'conf', 'logger-prod.ini')
            # logger = get_logger(common_config.LOG_CONFIG_PATH, logger_name='file')
    except Exception as e:
        raise LogConfigError(e)

3 啟動配置logger.ini

uvicorn.run(app, host='0.0.0.0', port=8899, log_config=common_config.LOG_CONFIG_PATH)

*配置完成後,logging.debug()等使用即可

三、中介軟體之殤(自定義中介軟體)

1 @app.middleware("http")

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time
    # 新增響應頭
    response.headers["X-Process-Time"] = str(process_time)
    return response

2 app.add_middleware

from starlette.datastructures import Headers
from starlette.responses import PlainTextResponse
from starlette.types import ASGIApp, Receive, Scope, Send

class AuthMiddleware:
    def __init__(self, app: ASGIApp) -> None:
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        logging.info(scope.get('path'))
        if scope.get('path'):
            url = URL(scope=scope)
            if url.path not in common_config.WHITE_LIST:   # 設定白名單
                headers = Headers(scope=scope)
                token = headers.get("Token")
                # 自定義訪問攔截
                if not token or not headers.get("username") or not check_devops_auth(token):   # 自定義驗證token,和其他請求資訊作為認證攔截
                    response = PlainTextResponse("未登陸使用者", status_code=401)
                    await response(scope, receive, send)
                    return
        await self.app(scope, receive, send)

app.add_middleware(AuthMiddleware)

四、配置檔案之殤

方案:採用ini配置檔案,讀取後寫入全域性變數

1 配置檔案

[common]
PROJECT_NAME = kk-jira-monitor
BACKEND_CORS_ORIGINS = http://127.0.0.1:8080,
API_V1_STR = /api/v1.0

[mysql]
USERNAME = admin
PASSWORD = 123456
HOST = 
PORT = 3306
DATABASE = 
SQLALCHEMY_DATABASE_URI = mysql+pymysql://%(USERNAME)s:%(PASSWORD)s@%(HOST)s:%(PORT)s/%(DATABASE)s

2 初始化

common_config = None
mysql_config = None

def init_config():
    """初始化配置檔案"""
    global mysql_config, common_config
    print("載入配置檔案...")
    config = ReConfigParser()
    try:
        if fast_api_env == 'local':
            config.read(os.path.join(BASE_DIR, 'conf', 'conf-local.ini'), encoding='utf-8')
        elif fast_api_env == 'dev':
            config.read(os.path.join(BASE_DIR, 'conf', 'conf-dev.ini'), encoding='utf-8')
        else:
            config.read(os.path.join(BASE_DIR, 'conf', 'conf-prod.ini'), encoding='utf-8')

        mysql_config = MySQLConfig(**dict(config.items('mysql')))
        common_config = CommonConfig(**dict(config.items('common')))

    except Exception as e:
        # logger.exception(f"配置檔案初始化失敗,{e.__str__()}")
        raise ConfigError(e)

3 configparse

from configparser import ConfigParser


class ReConfigParser(ConfigParser):
    def __init__(self, defaults=None):
        ConfigParser.__init__(self, defaults=defaults)

    """複寫方法實現key值區分大小寫"""
    def optionxform(self, optionstr):
        return optionstr

4 配置變數驗證

import os
from typing import Optional
from pydantic import BaseModel


class CommonConfig(BaseModel):
    SECRET_KEY: str = os.urandom(32)
    PROJECT_NAME: str
    API_V1_STR: str
    # 允許訪問的origins
    BACKEND_CORS_ORIGINS: str

class MySQLConfig(BaseModel):
    USERNAME: str = None
    PASSWORD: str = None
    HOST: str = None
    PORT: int = None
    DATABASE: str = None
    SQLALCHEMY_DATABASE_URI: str = (
        f"mysql://{USERNAME}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}"
    )

五、其它

1 問題一(中介軟體執行報錯)

ASGI 'lifespan' protocol appears unsupported.

@app.on_event('startup') 將不會執行

2 問題二(定時任務報錯)

藉助的apshechduler註冊的定時任務如果執行報錯,捕獲不到異常資訊

解決辦法可見 分離定時任務