zoukankan      html  css  js  c++  java
  • 【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注册的定时任务如果执行报错,捕获不到异常信息
    

    解决办法可见 分离定时任务

  • 相关阅读:
    Jquery 图片预览插件 imgPreview
    对request.getSession(false)的理解(附程序员常疏忽的一个漏洞)
    JavaScript拖拽实现(附注释),最经典!最简单!短小精悍!
    如何使用VC++写一个小程序来检测.NetFrameWork版本
    利用TreeView实现C#工具箱效果
    JavaScript中json对象和string对象之间的转化
    Ubuntu Server上搭建可用于生产环境的ASP.NET服务器
    winexec()函数的参数说明(c++)
    C#对文件夹的判断、创建、移动、删除
    C#程序不用安装.NET环境运行(让C#程序脱离.net框架)
  • 原文地址:https://www.cnblogs.com/zhangliang91/p/13388200.html
Copyright © 2011-2022 走看看