跳转至

配置运行与部署

本篇文档将介绍如何配置和运行 UtilMeta 项目,以及在生产环境中的部署

服务初始化参数

from utilmeta import UtilMeta

service = UtilMeta(__name__, ...)

UtilMeta 服务的第一个参数接收当前模块的名称(__name__),此外支持的参数有

  • backend:传入运行时框架的模块或者名称,如 backend='flask'
  • name:服务的名称,应该反映着服务的领域和业务,之后会用户服务的注册,发现与检测等功能
  • description:服务的描述
  • production:是否处于生产状态,默认为 False,在部署的生产环境中应该置为 True,这个参数会影响底层框架的运行配置,比如 djangoPRODUCTION 设置,与 flask / starlette / fastapi / sanic 的 debug 参数

  • host:服务运行时监听的主机 IP,默认为 127.0.0.1,也可以设置为运行主机的 IP 或者 0.0.0.0 (公开访问)

  • port:服务运行时监听的端口号,默认取决于运行时框架,比如 flask 使用的是 5000,其他框架一般使用的是 8000

  • version:指定服务当前的版本号,你可以传入一个字符串,如 '1.0.2',也可以传入一个元组,比如 (0, 1, 0),版本号的规范建议遵守 语义版本标准

  • asynchronous:强制指定服务是否提供异步接口,默认由运行时框架的特性决定
  • api:传入 UtilMeta 的根 API 类或它的引用字符串
  • route:传入 UtilMeta 的根 API 挂载的路径字符串,默认为 '/',即挂载到根路径

当你初始化 UtilMeta 后,除了直接导入外,你还可以用这种方式导入当前进程的 UtilMeta 服务实例

from utilmeta import service

Warning

一个进程中只能定义一个 UtilMeta 服务实例

选择 backend 框架

目前 UtilMeta 内置支持的运行时框架有

  • django
  • flask
  • starlette
  • fastapi
  • sanic
  • tornado

Tip

如果你还希望支持更多的运行时框架实现,请在 UtilMeta 框架的 Issues 中提出

你使用 backend 参数指定的框架需要先安装到你的 Python 环境中,然后你可以使用类似如下的方式将包导入后传入 backend 参数

import django
from utilmeta import UtilMeta

service = UtilMeta(
    __name__, 
    name='demo',
    backend=django
)
import flask
from utilmeta import UtilMeta

service = UtilMeta(
    __name__, 
    name='demo',
    backend=flask
)
import starlette
from utilmeta import UtilMeta

service = UtilMeta(
    __name__, 
    name='demo',
    backend=starlette
)
import sanic
from utilmeta import UtilMeta

service = UtilMeta(
    __name__, 
    name='demo',
    backend=sanic
)

注入自定义应用

一些运行时框架往往会提供开发者一个同名的应用类,比如 Flask, FastAPI, Sanic,其中可以定义一些初始化参数,如果你需要对其中的参数进行配置,则可以把定义出的应用实例传入 backend 参数,比如

from fastapi import FastAPI

fastapi_app = FastAPI(debug=False)

service = UtilMeta(
    __name__,
    name='demo',
    backend=fastapi_app,
)

Tip

你还可以使用这种方式在 UtilMeta 框架项目中接入你选择的运行时框架的原生接口,详细的用法可以参考 从现有项目迁移 这篇文档

异步服务

不同的运行时框架对于异步的支持程度不同,如果没有显式指定 asynchronous 参数,服务接口是否为异步取决于各自的特性,如

  • Django:同时支持 WSGI 与 ASGI,但是 asynchronous 默认为 False

Tip

对于使用 Django 作为 backend 的服务,如果开启 asynchronous=True 则会得到一个 ASGI 应用,否则会得到一个 WSGI 应用

  • Flask:支持 WSGI,处理异步函数需要将其先转化为同步函数,asynchronous 默认为 False
  • Sanic:支持 ASGI,asynchronous 默认为 True
  • Tornado:自行基于 asyncio 实现了 HTTP Server,asynchronous 默认为 True
  • Starlette/FastAPI:支持 ASGI,asynchronous 默认为 True

如果你希望编写异步(async def )接口,请选择一个默认支持异步的运行时框架,这样可以发挥出你的异步接口的性能,如果你选择的运行时框架默认不启用异步(如 django / flask),则需要开启 asynchronous=True 选项,否则将无法执行其中的异步函数

设置服务的访问地址

UtilMeta 服务还有一个 origin 参数可以指定这个服务提供公开访问的源地址,比如 `

from utilmeta import UtilMeta
import django

service = UtilMeta(
    __name__,
    name='blog',
    backend=django,
    api='service.api.RootAPI',
    route='/api',
    origin='https://blog.mysite.com',
)

由于一个请求在抵达后端服务前可能经过很多网关或负载均衡,所以后端服务部署的地址往往不是互联网能直接访问到的地址,服务的 origin 参数让我们可以设置服务提供访问的地址

比如例子中的 blog 服务的基准 URL 就是 https://blog.mysite.com/api,也会体现在生成的 OpenAPI 文档中

Tip

origin 参数提供的地址需要满足 “源(Origin)” 的定义,即包含网络协议,主机,端口号(如果有),但不包含 URL 路径,根路径应该体现在 route 参数中

服务的方法与钩子

实例化的 UtilMeta 服务还有一些方法或钩子可以使用

use(config) 注入配置

服务实例的 use 方法可以用于注入配置,例如

from utilmeta import UtilMeta
from config.env import env

def configure(service: UtilMeta):
    from utilmeta.core.server.backends.django import DjangoSettings
    from utilmeta.core.orm import DatabaseConnections, Database
    from utilmeta.conf.time import Time

    service.use(DjangoSettings(
        apps_package='domain',
        secret_key=env.DJANGO_SECRET_KEY
    ))
    service.use(DatabaseConnections({
        'default': Database(
            name='db',
            engine='sqlite3',
        )
    }))
    service.use(Time(
        time_zone='UTC',
        use_tz=True,
        datetime_format="%Y-%m-%dT%H:%M:%S.%fZ"
    ))

UtilMeta 内置的常用配置有

  • utilmeta.core.server.backends.django.DjangoSettings:配置 Django 项目,如果你的服务以 Django 作为运行时框架,或者需要使用 Django 模型的话就需要使用这个配置项
  • utilmeta.core.orm.DatabaseConnections:配置数据库连接
  • utilmeta.core.cache.CacheConnections:配置缓存连接
  • utilmeta.conf.time.Time:配置服务的时区与接口中的时间格式

Warning

一个类型的配置只能使用 use 注入一次

UtilMeta 提供了一个 DjangoSettings 配置,能够方便地对 Django 项目和需要使用 Django ORM 的项目进行 Django 配置

setup() 配置的安装

一些服务的配置项需要在服务启动前进行安装与准备,比如对于使用 Django 模型的服务,setup() 会调用 django.setup 函数完成模型的发现,你需要在调用这个方法之后才能导入你定义的 Django 模型以及依赖这些模型的接口与 Schema 类,比如

from utilmeta import UtilMeta
from config.conf import configure 
import django

service = UtilMeta(__name__, name='demo', backend=django)
configure(service)

service.setup()

from user.models import *

否则会出现类似如下的错误

django.core.exceptions.ImproperlyConfigured: 
Requested setting INSTALLED_APPS, but settings are not configured, ...  

当然对于使用 Django 的项目,最佳实践是使用引用字符串来指定根 API,这样在服务配置文件中就不需要包含对 Django 模型的导入了,例如

from utilmeta import UtilMeta
import django

service = UtilMeta(
    __name__,
    name='demo',
    backend=django,
    api='service.api.RootAPI',
    route='/api',
)
from utilmeta.core import api

class RootAPI(api.API):
    @api.get
    def hello(self):
        return 'world'

application() 获取 WSGI/ASGI 应用

你可以通过调用服务的 application() 方法返回它生成的 WSGI / ASGI 应用,比如在 Hello World 示例中

from utilmeta import UtilMeta
from utilmeta.core import api
import django

class RootAPI(api.API):
    @api.get
    def hello(self):
        return 'world'

service = UtilMeta(
    __name__,
    name='demo',
    backend=django,
    api=RootAPI,
    route='/api'
)

app = service.application()

if __name__ == '__main__':
    service.run()

这个生成的 app 的类型取决于服务实例指定的 backend ,如

  • flask: 返回一个 Flask 实例
  • starlette:返回一个 Starlette 实例
  • fastapi:返回一个 FastAPI 实例
  • sanic:返回一个 Sanic 实例
  • django:默认返回一个 WSGIHandler,如果指定了 asynchronous=True,则会返回一个 ASGIHandler
  • tornado:返回一个 tornado.web.Application 实例

如果你使用 uwsgi 和 gunicorn 等 WSGI 服务器部署 API 服务的话,其中都需要指定一个 WSGI 应用,你只需要将对应的配置项设为 app 的引用即可,比如

wsgi_app = 'server:app'
[uwsgi]
module = server.app

使用 sanic

当你使用 sanic 作为运行时框架时,即使不使用 WSGI 服务器,也需要在启动服务的文件中声明 app = service.application(),因为 sanic 会启动新的进程来处理请求,如果没有 application() 对接口的加载,新的进程将检测不到任何路由

@on_startup 启动钩子

你可以使用服务实例的 @on_startup 装饰器装饰一个启动钩子函数,在服务进程启动前调用,可以用于进行一些服务的初始化操作,如

from utilmeta import UtilMeta
import starlette

service = UtilMeta(
    __name__,
    name='demo',
    backend=starlette,
)

@service.on_startup
async def on_start():
    import asyncio
    print('prepare')
    await asyncio.sleep(0.5)
    print('done')

对于支持异步的 backend 框架,如 Starlette / FastAPI / Sanic / Tornado,你可以使用异步函数作为启动钩子函数,否则你需要使用同步函数,比如 Django / Flask

@on_shutdown 终止钩子

你可以使用服务实例的 @on_shutdown 装饰器装饰一个终止钩子函数,在服务进程结束前调用,可以用于进行一些服务进程的清理操作,如

from utilmeta import UtilMeta
import starlette

service = UtilMeta(
    __name__,
    name='demo',
    backend=starlette,
)

@service.on_shutdown
def clean_up():
    # clean up
    print('done!')

环境变量管理

在实际后端开发中,你往往需要使用很多密钥,比如数据库密码,第三方应用密钥,JWT 密钥等等,这些信息如果硬编码到代码中有泄露的风险,很不安全,并且这些密钥信息在开发,测试和生产环境中也往往有着不同的配置,所以更适合使用环境变量来管理

UtilMeta 提供了一个内置环境变量组件 utilmeta.conf.Env 便于你管理这些变量与密钥信息,使用的方式如下

from utilmeta.conf import Env

class ServiceEnvironment(Env):
    PRODUCTION: bool = False
    DJANGO_SECRET_KEY: str = ''
    COOKIE_AGE: int = 7 * 24 * 3600

    # cache -----
    REDIS_DB: int = 0
    REDIS_PORT: int = 6379
    REDIS_PASSWORD: str

    # databases ---------
    DB_HOST: str = ''
    DB_USER: str  
    DB_PASSWORD: str
    DB_PORT: int = 5432

env = ServiceEnvironment(sys_env='DEMO_')

通过继承 utilmeta.conf.Env 类,你可以在其中声明服务需要的环境变量,环境变量在解析时会忽略大小写,但我们建议使用大写的方式与其他属性进行区分,你可以为每个变量声明类型与默认值,Env 会将变量转化为你声明的类型,在获取不到对应的变量时使用你指定的默认值

Env 子类在实例化时可以指定环境变量的来源,如

sys_env 系统环境变量

你可以指定一个前缀,从而会在系统环境变量中拾取 前缀+名称 的变量,比如例子中指定的前缀为 'DEMO_',那么系统环境变量中的 DEMO_DB_PASSWORD 将会被解析为环境变量数据中的 DB_PASSWORD 属性

如果你不需要指定任何前缀,可以使用 sys_env=True

file 配置文件

除了从系统环境变量中拾取外,你还可以使用 file 参数指定一个 JSON 或 INI 格式的配置文件

from utilmeta.conf import Env

class ServiceEnvironment(Env):
    pass

env = ServiceEnvironment(file='/path/to/config.json')

在文件中声明对应的环境变量,如

{
    "DB_USER": "my_user",
    "DB_PASSWORD": "my_password"
}
DB_USER=my_user
DB_PASSWORD=my_password

Warning

如果你使用的是配置文件,请把配置文件放在项目目录之外,或者在 .gitignore 中把它从版本管理中排除

常用配置

DjangoSettings 配置

UtilMeta 提供了一个 DjangoSettings 配置,可以为所有使用 Django 作为 backend 的项目和使用 django ORM 的项目提供声明式的 django 配置, DjangoSettings 的常用配置参数如下

  • secret_key:指定 Django 的项目密钥,推荐在环境变量中生成一个长的随机密钥
  • apps:用于指定 Django 的 INSTALLED_APPS
  • apps_package:这是一个便捷配置项,如果你的已安装 app 都放在一个包中,你可以使用 apps_package 指定这个包的路径,UtilMeta 会读取其中的所有子文件夹查找 django app
  • middleware:可以传入一个 django 中间件的列表
  • module_name:指定 django 的配置文件引用
  • extra:可以传入一个字典指定额外的 Django 配置

另外,如果你没有为 DjangoSettings 指定 module_name,它将默认使用 UtilMeta 服务所在的模块作为配置,所以你也可以在服务 setup() 之前 直接在这个文件中声明 django 配置,用法与原生 django 配置一样,例如

from utilmeta import UtilMeta
from config.conf import configure 
import django

DATA_UPLOAD_MAX_NUMBER_FILES = 1000

service = UtilMeta(__name__, name='demo', backend=django)
configure(service)

service.setup()

Warning

service.setup() 也会同步触发 django 的 setup() 从而加载所有 django 设置,所以在它之后进行的 django 配置无法被加载

DatabaseConnections 数据库配置

在 UtilMeta 中,DatabaseConnections 用于配置数据库连接,它接受一个字典参数,字典的键是连接的名称,值是一个 Database 实例,用于配置数据库的地址和连接信息,在 UtilMeta 的 ORM 中,如果没有显式指定,会默认使用名称为 'default' 的数据库连接

from utilmeta.core.orm import DatabaseConnections, Database
from config.env import env

service.use(DatabaseConnections({
    'default': Database(
        name='blog',
        engine='postgresql',
        host=env.DB_HOST,
        user=env.DB_USER,
        password=env.DB_PASSWORD,
        port=env.DB_PORT,
    )
}))

Note

如果你使用过 Django,应该对这种配置方式很属性,在 UtilMeta 中使用 django ORM 时,DatabaseConnections 也会生成 django 的 DATABASES 配置

你可以在 ORM 配置数据库连接 中查看详细的用法

CacheConnections 缓存配置

CacheConnections 用于配置缓存连接,语法与 DatabaseConnections 类似,连接字典的值指定一个缓存实例,其中可以配置缓存的地址与连接信息,例如

from utilmeta.core.cache import CacheConnections, Cache
from utilmeta.core.cache.backends.redis import RedisCache
from config.env import env

service.use(CacheConnections({
    'default': RedisCache(
        port=env.REDIS_PORT,
        db=env.REDIS_DB,
        password=env.REDIS_PASSWORD
    ),
    'fallback': Cache(engine='django.core.cache.backends.locmem.LocMemCache')
}))

目前 UtilMeta 支持两种缓存配置

  • DjangoCache:默认的缓存配置,将会使用 Django 的缓存进行实现,其中 engine 参数可以传入 Django 的缓存类
  • RedisCache:Redis 缓存配置,同时支持同步与异步用法,同步的用法由 Django 实现,异步的用法使用 aioredis 实现(若需使用请先安装 aioredis

Time 时间与时区配置

Time 用于配置项目的时间与时区设置,影响 API 接口的时间序列化与数据库中的时间存储

from utilmeta.conf import Time

service.use(Time(
    time_zone='UTC',
    use_tz=True,
    datetime_format="%Y-%m-%dT%H:%M:%SZ"
))

其中的参数包括 * time_zone:指定时间的时区,默认为本机的时区,可以使用 'UTC' 来指定 UTC 时区 * use_tz:是否为项目的所有 datetime 时间开启时区(timezone aware),默认为 True,会同步 Django 的 USE_TZ 配置 * date_formatdate 类型的序列化格式,默认为 %Y-%m-%d * time_formattime 类型的序列化格式,默认为 %H:%M:%S * datetime_formatdatetime 类型的序列化格式,默认为 %Y-%m-%d %H:%M:%S

运行服务

UtilMeta 服务实例提供了一个 run() 方法用于运行服务,我们已经看到过它的用法了

from utilmeta import UtilMeta
from utilmeta.core import api
import django

class RootAPI(api.API):
    @api.get
    def hello(self):
        return 'world'

service = UtilMeta(
    __name__,
    name='demo',
    backend=django,
    api=RootAPI,
    route='/api'
)

app = service.application()

if __name__ == '__main__':
    service.run()

我们一般在 __name__ == '__main__' 的版块内调用 service.run(),这样你就可以通过使用 python 执行这一文件从而运行服务,例如

python server.py

run() 方法会根据服务实例的 backend 执行对应的运行策略,比如

  • Django:通过调用 runserver 命令运行服务,不建议在生产环境中使用
  • Flask:直接调用 Flask 应用的 run() 方法运行服务
  • Starlette/FastAPI:将使用 uvicorn 运行服务
  • Sanic:直接调用 Sanic 应用的 run() 方法运行服务
  • Tornado:使用 asyncio.run 运行服务

另外如果你在 meta.ini 中使用 main 指定了包含 service.run() 的入口文件

[service]
main = server

你也可以通过运行

meta run

来启动服务,在 Linux 系统中,还可以加上 -d 参数指定为 nohup 运行,不被当前终端的关闭影响

自定义运行

对于 Flask, FastAPI, Sanic 等框架,你可以通过 service.application() 获取到生成的 Flask, FastAPI Sanic 应用,所以你也可以直接调用它们的 run() 方法从而传入对应框架支持的参数

from utilmeta import UtilMeta
from utilmeta.core import api
import flask

class RootAPI(api.API):
    @api.get
    def hello(self):
        return 'world'

service = UtilMeta(
    __name__,
    name='demo',
    backend=flask,
    api=RootAPI,
    route='/api'
)

app = service.application()

if __name__ == '__main__':
    app.run(
        debug=False,
        port=8000
    )

部署服务

通过 run() 方法我们可以在调试时快速运行一个可访问服务实例,但是在实际部署时,我们往往需要额外的配置从而使得服务稳定地运行在我们期望的地址,并且充分发挥运行环境的性能

Python 开发的 API 服务常用的一种部署架构为

 BMI API Doc

将你开发的 API 服务使用 uwsgi / gunicorn 等 WSGI 服务器或 Daphne 等 ASGI 服务器运行,它们会更高效地进行多进程(worker)管理和请求分发

然后使用一个反向代理服务(如 Nginx)将 API 的根路由解析到 WSGI/ASGI 服务器提供的端口,同时可以代理图片或 HTML 等静态文件,然后根据需要对外提供 80 端口的 HTTP 服务或 443 端口的 HTTPS 服务

但是由于不同的运行时框架的特性不同,我们建议根据你使用的 backend 选择部署策略

  • Django:默认生成 WSGI 应用,可以使用 uWSGI / Gunicorn 部署,如果是异步服务生成的 ASGI 应用,也可以使用 Daphne ASGI 服务器部署
  • Flask:生成 WSGI 应用,可以使用 uWSGI / Gunicorn 部署
  • Sanic:自身就是一个多进程服务应用,可以直接使用 Nginx 代理
  • Starlette/FastAPI:可以使用搭载了 Uvicorn worker 的 Gunicorn 部署
  • Tornado:自身就是一个异步服务应用,可以直接使用 Nginx 代理

Tip

有些框架如 Sanic 和 Tornado,本身就可以直接运行可靠的高性能服务,所以无需使用 WSGI / ASGI 服务器,可以直接运行并接入 Nginx 代理

uWSGI

使用 uWSGI 之前需要先安装

pip install uwsgi

之后可以使用 ini 文件编写 uwsgi 的配置文件,如

[uwsgi]
module = server:app
chdir = /path/to/your/project
daemonize = /path/to/your/log
workers = 5
socket=127.0.0.1:8000

其中重要的参数包括

  • chdir:指定服务的运行目录,一般是项目的根目录
  • module:指定你服务的 WSGI 应用,相对服务的运行目录,假设你的 WSGI 应用位于 server.py 中的 app 属性,就可以使用 server:app 定义
  • daemonize:设置日志文件地址
  • workers:设置服务运行的进程数量,一般可以设为服务器的 CPU 数 x 2 + 1
  • socket:uwsgi 服务监听的 socket 地址,用于与前置的 Nginx 等代理服务器通信

uwsgi 服务器的运行命令如下

uwsgi --ini /path/to/your/uwsgi.ini

Gunicorn

使用 Gunicorn 之前需要先安装

pip install gunicorn

之后可以直接使用 Python 文件编写 Gunicorn 的配置文件,如

wsgi_app = 'server:app'
bind = '127.0.0.1:8000'
workers = 5
accesslog = '/path/to/your/log/access.log'
errorlog = '/path/to/your/log/error.log'

其中主要的参数有

  • wsgi_app:指定你服务的 WSGI 应用的引用,假设你的 WSGI 应用位于 server.py 中的 app 属性,就可以使用 server:app 定义
  • bind:服务监听的地址,用于与前置的 Nginx 等代理服务器通信
  • workers:设置服务运行的进程数量,一般可以设为服务器的 CPU 数 x 2 + 1
  • accesslog:设置服务的访问日志地址
  • errorlog:设置服务的运行于错误日志地址

此外你还可以使用 worder_class 属性指定工作进程的实现,可以根据接口的类型优化运行效率,如

  • 'uvicorn.workers.UvicornWorker':适合异步接口的,如 Starlette / FastAPI,需要先安装 uvicorn
  • 'gevent':适合同步接口,如 Django / Flask,会使用 gevent 库中的协程(green thread)提高接口的并发性能,需要先安装 gevent

gunicorn 服务器的运行命令如下

gunicorn -c /path/to/gunicorn.py

Nginx

对于使用 uWSGI 的 API 服务,假设运行并监听 8000 端口,Nginx 的配置大致为

server{
    listen 80;
    server_name example.com;
    charset utf-8;
    include /etc/nginx/proxy_params;

    location /api/{
        include /etc/nginx/uwsgi_params;
        uwsgi_pass 127.0.0.1:8000;
    }
}

对于 Gunicorn 或者其他直接运行的服务,假设运行在 8000 端口,Nginx 的配置大致为

server{
    listen 80;
    server_name example.com;
    charset utf-8;
    include /etc/nginx/proxy_params;

    location /api/{
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header REMOTE_ADDR $remote_addr;
    }
}

Tip

为了使得 API 服务能够获取到请求的来源 IP(而不是反向代理服务的 IP),我们需要使用 proxy_set_header 将对应的请求头参数传递过去

你应该把配置好的 nginx 文件放置在 ·/etc/nginx/sites-enabled/ 中从而启用对应的配置,可以使用如下命令检测配置是否有问题

nginx -t

如果没有问题,就可以使用如下命令重启 nginx 服务使得更新的配置生效

nginx -s reload

服务观测与运维管理

UtilMeta 框架内置了一个 API 服务管理系统,可以方便地观测与管理对本地与线上的 API 服务,提供了数据,接口,日志,监控,测试,报警等一系列运维管理功能,在 Operations 运维管理系统配置 中有这个系统的详细介绍与配置方式

你也可以直接进入 UtilMeta API 服务管理平台 进行体验