本文为《FastAPI Web开发入门、进阶与实战/钟远晓著》的读书笔记。
作者书籍源码 https://gitee.com/xiaozhong1988/fastapi_tutorial
移至github存储 https://github.com/Gitxieada/fastapi_tutorial
初识FastAPI
FastAPI框架概述
在Python中,早期并发模式的实现主要依赖多线程或者多进程,但是因为历史遗留的GIL(全局解释器锁)问题,Python多线程未能把多核的优势发挥得淋漓尽致。FastAPI框架不仅具有Flask或Django的Web核心功能,还兼具异步特性,可以同时兼容同步和异步这两种模式的运行。框架对于应用开发本身只是一个辅助实现业务逻辑的工具。对于FastAPI框架的详细说明,读者可以自行查阅,网址为https://FastAPI.tiangolo.com/alternatives/ 。
FastAPI的特性主要有:
❑是一个支持ASGI(Asynchronous Server Gateway Interface)协议的Web应用框架,也就是说,它同时兼容ASGI和WSGI的应用。
❑天然支持异步协程处理,能快速处理更多的HTTP请求。
❑使用了Pydantic类型提示的特性,可以更加高效、快速地进行接口数据类型校验及模型响应等处理。
❑基于Pydantic模型,它还可以自动对响应数据进行格式化和序列化处理。
❑提供依赖注入系统的实现,它可以让用户更高效地进行代码复用。
❑它支持WebSocket、GraphQL等。
❑支持异步后台任务,可以方便地对耗时的任务进行异步处理。
❑支持服务进程启动和关闭事件回调监听,可以方便地进行一些插件的扩展初始化。
❑支持跨域请求CORS、压缩Gzip请求、静态文件、流式响应。
❑支持自定义相关中间件来处理请求及响应。
❑支持开箱即用OpenAPI(以前被称为Swagger)和JSON Schema,可以自动生成交互式文档。
❑使用uvloop模块,让原生标准的asyncio内置的事件循环更快。
ASGI是异步服务器网关接口,和WSGI一样,都是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。但是ASGI是WSGI的一种扩展的实现,并且提供异步特性和WebSocket等的支持。同时ASGI也是兼容WSGI的,在某种程度上可以理解为ASGI是WSGI的超集,所以ASGI可以支持同步和异步同时运行,内部可以直接通过一个转换装饰器进行相互切换。
异步编程基础
在同步IO编程中,由于CPU处理任务计算的速度远高于内存执行任务的速度,所以会遇到IO阻塞引发的执行效率低的问题。
引入多线程和多进程方式在某种程度上可以实现多任务并发执行。线程相互之间独立执行,互不影响。对于IO型任务,通常通过多线程调度来实现表面上的并发;对于计算密集型任务,则使用多进程来实现并发。其实,无论是多线程还是多进程或协程都无法实现真正的并行。
❑Python多进程并发缺点:
❍进程的创建和销毁代价非常高。
❍需要开辟更多的虚拟空间。
❍多进程之间上下文的切换时间长。
❍需要考虑多进程之间的同步问题。
❑Python多线程并发缺点:
❍每一个线程都包含一个内核调用栈(Kenerl Stack)和CPU寄存器上下文表(该表列出了CPU中的寄存器以及它们的名称、大小、功能和对应的指令等信息)。
❍共享同一个进程空间会涉及同步问题。
❍线程之间上下文的切换需要消耗时间。
❍受限于GIL,在Python进程中只允许一个线程处于运行状态,多线程无法充分利用CPU多核。
❍受OS调度管制,线程是抢占式多任务并发的(需要关心同步问题)。
异步IO的本质是基于事件触发机制来实现异步回调。
并发通常是指在单核CPU情况下可以同时运行多个应用程序。
本质上说,多任务其实是交叉执行的,并发只是一种“假象”。并行包含了并发,并发是并行的一种特殊表现。
❑并发通常是对单核CPU任务执行过程的一种组织结构描述的说明,并行是对程序执行过程中一种状态的描述,其主要目的是充分利用多核CPU加速任务执行。
❑由于一个系统运行的任务数量远超过CPU数量,所以在现在的操作系统中没有绝对的真正并行的任务。
同步(Synchronous),其实是在强调多个任务执行的一个完整过程,其中的某个任务在执行过程中不允许被中断。
相对于同步来说,异步(Asynchronous)强调多个任务可以分开执行,彼此之间互不影响,某一个任务遇到阻塞,其他任务不需要等待,但是任务执行的结果依然是保持一致的。
阻塞(Blocking)和非阻塞(Nonblocking)都是针对CPU对线程的调度来说的。当调用的函数(任务)遇到IO时会进行线程挂起的操作,此时就需要等待返回IO执行结果,在等待的过程中无法处理其他任务,称此时的任务执行操作处于阻塞状态。
当调用的函数遇到IO时,若不会进行线程挂起的操作,则不需要等待返回IO执行结果,此时可以去做其他任务,称此时的任务执行操作处于非阻塞状态。
相对于线程来说,协程不存在于操作系统中,它只是一种程序级别上的IO调度。读者可以将它理解为对现有线程进行的一次分片任务处理,线程可以在代码块之间来回切换执行,而非逐行执行,因此能够支持更快的上下文切换,减少线程的创建开销和切换开销,从而大大提高了系统性能。
asyncio是Python官方提供的用于构建协程的并发应用库,是FastAPI实现异步特性的重要组成部分。asyncio的核心是Eventloop(事件循环),它以Eventloop为核心来实现协程函数结果的回调。它提供了相关协程任务的注册、取消、执行及回调等方法来实现并发。定义一个协程函数(Coroutine)的核心是async关键词。
初试FastAPI
本书所用的Python开发包基于Python 3.9.10,因为FastAPI要求的最低版本是Python 3.6+,且Python 3.9.10支持任意表达式均可以做装饰器等新特性。Python官网的下载界面(https://www.Python.org/downloads/), PyCharm官网下载界面 (https://www.jetbrains.com/pycharm/) PyCharm官网提供了专业版和社区版两个版本。和社区版相比,专业版可以提供更丰富的功能,如远程连接和实用模板等。社区版是免费的,而专业版需要付费,但有一个月的免费试用期。读者可以根据自己的需要来权衡使用哪一个版本。这里选择的是社区版。
由于本地环境可能安装了很多版本的Python,所以需要为开发的项目分配指定的解析器,这样有利于在多项目下实现解析器的环境隔离,避免项目之间产生冲突。Virtualenv主要用于构建一个独立、虚拟的Python环境,这样可以为不同的项目分配一个专属的Python环境,使各个项目彼此独立,从而避免混合项目依赖包的污染。
国内可以使用的PIP安装源有以下几个。
❑清华:https://pypi.tuna.tsinghua.edu.cn/simple 。
❑阿里云:http://mirrors.aliyun.com/pypi/simple/ 。
❑豆瓣:http://pypi.douban.com/simple/ 。
❑中国科学技术大学:https://pypi.mirrors.ustc.edu.cn/simple/ 。
❑山东理工大学:http://pypi.sdutlinux.org/ 。
FastAPI框架常用的主要依赖库如下:
❑email.validator:主要用于邮件格式校验处理。
❑requests:使用单例测试TestClient或请求第三方接口时需要使用该依赖库。
❑aiofiles:主要用于异步处理文件读写操作。
❑jinja2:主要供用户渲染静态文件模板时使用,当项目要使用后端渲染模板时安装该依赖库。
❑Python-multipart:当需要获取From表单数据时,只有通过这个库才可以提取表单的数据并进行解析。
❑itsdangerous:用于在SessionMiddleware中间件中生成Session临时身份令牌。
❑graphene:需要GraphQLApp支持时安装这个依赖库。
❑orjson:主要用于JSON序列化和反序列化,如使用FastAPI提供的ORJSONR-esponse响应体处理时,则需要安装这个依赖库。
❑ujson:主要用于JSON序列化和反序列化,如需要使用FastAPI提供的UJSONResponse响应体时就需要安装该依赖库。
❑uvicorn:主要用于运行和加载服务应用程序Web服务。
路由注册与Flask框架所提供的路由注册的本质是一样的。所谓路由注册,就是提供一个对应的URL地址来关联或绑定定义的函数。在常见的Web框架中,通常使用装饰器的形式对需要绑定的函数进行映射绑定,这个过程就是路由注册,也可以理解为创建视图的过程。
被@app.get()装饰的函数可以有两种,一种是使用def定义的同步函数,另一种是使用async def定义的协程函数。同步函数会运行于外部的线程池中,协程函数会运行于异步事件循环中。简单地说,同步函数是基于多线程并发模式进行相关处理的,而协程函数是基于单线程内的异步并发模式进行相关处理的。
在本地开发环境中,使用uvicorn启动服务分两种方式:
- 使用命令行方式启动服务
调用uvicorn库并传入指定的参数“main:app–reload”来启动对应main模块的app对象。--reload
命令主要用于热启动,这个参数的主要作用是在编写代码阶段,若需要对当前代码进行改动,那么代码改动完后程序会自动重启服务的进程,使修改即时生效,这样可以提高编码效率。注意,–reload仅用于项目编码调试阶段,正式部署到生产环境后应尽量避免使用该参数。如果希望对外允许访问服务,那么可以将–host参数设置为0.0.0.0。如果希望自定义监听端口,那么可以添加–port参数。
下面对几个IP地址相关的概念进行简单介绍。
❑公网IP地址:互联网上的所有人都可以通过这个IP地址访问到服务。公网IP地址一般需要向服务提供商申请获得,如在阿里云购买云服务器时通常会配备一个公网IP地址。
❑内网IP地址:主要是指个人计算机内的局域网地址。如果服务是通过内网IP开启的,则通常只能使用本机上或所属局域网内其他内网的IP地址进行访问,外部人员是无法通过内网IP地址进行访问的。
❑0.0.0.0:代表本机上所有的IP地址,也包括了回环地址127.0.0.1。如果本机存在多网卡的情况,则0.0.0.0是所有网卡IP的集合。如果使用host=0.0.0.0绑定了IP地址,那么表示服务进程会接收来自所有网卡上的IP地址的请求。 - 使用代码方式启动服务
在main.py中导入uvicorn模块,然后使用uvicorn.run()函数来启动服务。
uvicorn提供的参数比较多,这里不可能逐一展开介绍,更多内容可以访问uvicorn的官网来学习:https://www.uvicorn.org/ 。
FastAPI提供了两种可视化交互式API文档模式——Swagger UI、ReDoc。
Swagger UI模式下,服务启动之后,通过浏览器访问http://127.0.0.1:8000/docs#/
ReDoc模式下,服务启动之后,通过浏览器访问http://127.0.0.1:8000/redoc
FastAPI基础入门
FastAPI类中内置了一个Debug(调试)功能,通过它,人们可以实现类似使用Flask框架时在网页中看到错误堆栈信息明细的功能。
在项目的本地开发调试阶段开启Debug模式非常有用,但需要注意,在线上生产环境中应避免开启这个功能。另外,如果定义了全局异常处理,则不建议同时开启Debug模式,否则全局异常处理会失效。
自动生成API可视化交互式文档虽然方便,但是在生产环境中开启此文档访问会带来安全隐患。为了解决此类安全问题,通常会使用某种策略机制限制访问,如启动相关的身份认证、IP白名单等。如果项目不允许访问API文档,那么最直接的做法就是直接禁用,关闭访问,设置了docs_url=None和redoc_url=None,此时访问 http://127.0.0.1:8000/docs 的请求都会返回“Not Found”的提示,表示找不到该路由的访问地址。使用这种方式可达到关闭API交互式文档访问的目的。另外,还可以通过设置openapi_url=None或禁用OpenAPI Schema来关闭API交互式文档访问。
在对FastAPI类进行实例化时,有一个预设的routes路由列表参数,它保存了app中所有API端点的路由信息。
exception_handlers参数主要用于捕获在执行业务逻辑处理过程中抛出的各种异常。
FastAPI还提供了@app.api_route()来支持配置路由函数使用不同的HTTP请求方法;另外,还可以直接使用FastAPI提供的app.add_api_route()方法来实现类似@app.api_route()的功能。app.add_api_route()需要传入一个endpoint(端点)参数(endpoint参数可以理解为需要绑定关联的同步函数或协程函数,也就是视图函数)。
API端点路由注册方式大致可以分为3种:
❑基于app实例对象提供的装饰器或函数进行注册。
❑基于FastAPI提供的APIRouter类的实例对象提供的装饰器或函数进行注册。
❑通过直接实例化APIRoute对象且添加的方式进行注册。
APIRouter主要用于定义路由组,可以理解为一个路由组的根路由,而APIRoute则表示具体路由节点对象。
在大型的项目开发中,通常需要针对不同业务进行不同API的分组,此时如果单纯基于app实例对象来实现分组,那么相对来说比较麻烦,所以引入APIRouter类来实现路由嵌套和分组。
当完成路由分组和对应视图函数绑定后,还需要使用app.include_router()把APIRouter对象添加到app路由列表里面。同步路由的主要表现形式为:当使用URL地址绑定的关联视图函数是一个同步函数(使用def定义的函数)时,就把这个绑定过程理解为一个同步路由注册的过程。同步路由的并发处理机制是基于多线程方式来实现的。异步路由的主要表现形式为:当使用URL地址绑定的视图函数是一个协程函数(使用async def定义的函数)时,就把这个绑定过程理解为一个异步路由注册的过程,异步路由的并发处理机制是基于单线程方式运行的。
注意通常不建议在异步操作中使用同步函数,因为在一个线程中执行同步函数时肯定会引发阻塞。比如,一般不会在异步协程函数中使用time.sleep(),而是使用await asyncio.sleep()。
- 多应用挂载
在实际项目开发过程中,如果项目比较庞大,则除了使用APIRouter进行模块划分之外,还可以使用主应用挂载子应用的方式来进行划分。主应用挂载子应用的好处有:app独立管理;各自所属的API交互式文档是独立的,可以分开访问。
如果已开发好了WSGI应用程序(如Flask或Django等应用),也可以通过FastAPI无缝进行挂载关联,这样就可以通过FastAPI部署来启动WSGI的应用程序。FastAPI提供了一个名为WSGI Middleware的中间件,通过它可以挂载WSGI应用程序。 - 应用配置信息读取
一般做法是把配置参数写入外部文件或环境变量中。在大型微服务式应用中,还可能把参数写入线上的配置中心(如nacos、etcd等)进行统一管理,通过这些配置中心可以做到不重新打包、不发布版本就变更参数项。
- 基于文件读取配置参数
如果配置参数不是写入环境变量中,而是写入类似Windows的*.ini文件中,那么可以通过Python自带的configparser来解析读取配置参数。 - 基于Pydantic和.env环境变量读取配置参数
通过环境变量的方式来读取配置参数是FastAPI官方推荐的方式,通过Pydantic可以直接解析出对应的配置参数项。Pydantic还提供了参数类型校验的功能。 - 给配置读取加上缓存
对于非单例模式的实例对象,可以通过添加缓存的方式来避免多次实例化,进而提高整体性能。
参数的校验库比较多,此处仅列举几个常用的。
❑WTForms:支持多个Web框架的Form组件,主要用于对用户请求数据进行验证。
❑valideer:轻量级、可扩展的数据验证和适配库。
❑validators:验证库。
❑cerberus:用于Python的轻量级和可扩展的数据验证库。
❑colander:用于对XML、JSON、HTML以及其他同样简单的序列化数据进行校验和反序列化的库。
❑jsonschema:用来标记和校验JSON数据,可在自动化测试中验证JSON的整体结构和字段类型。
❑schematics:一个Python库,用于将类型组合到结构中并验证它们,然后根据简单的描述转换数据的形状。
❑voluptuous:主要用于验证以JSON、YAML等形式传入Python的数据。
通过Pydantic库,用户可以直接定义数据接口schema,并进行数据校验。在一个API中,请求参数的提交有多种方式,使用Pydantic可以统一进行参数绑定解析。
如果需要对单独的一个参数进行多维度校验,则单纯采用上文的声明方式无法完成。FastAPI框架还提供了一个专门用于多维度条件校验的Path类来声明参数
参数声明顺序问题:在视图函数中,声明的参数分为有默认值和无默认值两种。如果把有默认值参数放在无默认值参数的前面,那么IDE会提示告警,并且无法启动服务。如果坚持把有默认值的参数放在最前面,则需要在第一个参数前面加一个,此时,FastAPI框架会将之后的参数自动识别为关键字参数(键值对),也就是**kwargs,并且它不会在意之后的参数是否有默认值。这里建议把无默认值的参数放在最前面。和Path参数一样,FastAPI框架还提供了一个专门用于在查询参数中进行多维度条件限制的Query类,Query类中的参数和Path中的大部分参数是相同的。
对int类型数据校验时涉及的参数说明如下:
❑gt:表示大于。
❑lt:表示小于。
❑ge:表示大于或等于。
❑le:表示小于或等于。
FastAPI框架提供了如下3种方式来接收Body参数,并自动把JSON格式的字符串转换为dict格式。
❑引入Pydantic模型来声明请求体并进行绑定。
❑直接通过Request对象获取Body的函数。
❑使用Body类来定义。
模型中的Field字段主要应用于Pydantic模型内部,所以和Query、Path有所不同,这主要表现在Field仅可在模型内部定义,不可直接在视图函数中声明。
- Form数据和文件处理
对于表单数据的传输,通常要求提交请求头字段Content-Type的方式是application/x-www-form-urlencoded,这也是默认的方式。FastAPI表单的处理需要借助第三方库python-multipart。
FastAPI中提供了专门用于接收处理文件的File类,它是基于Form模块扩展而来的,所以它的用法和Form一样,可以直接声明在视图函数中。需要注意,File参数接收到的内容是文件字节,它会将整个内容读取并存储到内存中,所以它主要用在上传小文件的场景中。
在异步视图函数中,通过引入aiofiles异步库来异步写入文件。直接使用File对象来接收处理上传的文件,但它接收的是字节数据,而且缺少相关文件元数据信息,如文件名称、文件大小、文件格式类型等。如果需要获取更多关于文件的元信息,则需要从请求头来截取。
直接声明接收文件为UploadFile类型可获取更多的文件信息。
与bytes对比,UploadFile具有以下几点优势。
❑使用UploadFile进行文件读取时,所获得的数据存储在内存中,当占用的内存达到阈值后将被保存在磁盘中。这种读取方式更适用于大图片、视频等大文件的上传处理。
❑UploadFile对象包含很多文件元数据,如文件名、文件类型等。
❑有文件对象的异步接口,可以对文件对象进行write()、read()、seek()和close()等操作。
UploadFile提供的方法都是协程方法,所以它仅适用于异步协程函数。
FastAPI提供了Header类用于请求头参数解析读取,这里要强调一点,在Python中,如果参数变量使用带横杠(即连字符)的方式定义,如“User-Agent”,则会认定该变量是一个非法变量名称。而默认情况下,浏览器传递请求头参数时使用的都是带横杠的。此时,为了在代码中正常获取请求头参数,需要借助Header模块进行转换处理。
定义的Header对象中的convert_underscores参数表示的意思是:如果在视图函数中声明的请求头参数使用了下画线命名法,那么是否对下画线进行转换。convert_underscores=True表示转换,False表示不转换。
Cookie是用于客户端保持用户信息状态的方案,但是任何放置在客户端的信息都会存在被恶意篡改或伪造的风险,所以使用Cookie进行认证时,要注意对敏感内容进行加密处理。
Response响应报文处理模块,Response主要用于与响应报文相关的封装处理模块,它可以直接写入对应的Cookie。Response对象提供了一个set_Cookie()方法来设置Cookie键值对信息。
在应用开发过程中,有时会遇到这样的需求:当后端接收到前端的请求之后,需要处理比较耗时的任务,为了更快地使前端响应数据,不需要等待该任务处理完成再进行响应,如请求第三方接口发送短信、发送邮件、异步日志处理、大数据量汇总统计等操作。FastAPI提供了一种称为BackgroundTasks
的后台任务机制来解决此类需求。
startup和shutdown事件是FastAPI提供的进行服务启动和关闭时执行的事件回调通知处理机制。注意基于startup和shutdown事件回调处理业务逻辑时,不建议进行中间件添加注册处理,官方在文档中建议使用基于lifespan的生命周期的方式进行处理。
如果需要基于Pydantic的BaseSettings类来读取环境变量,那么还需要安装dotenv库。通常,在安装FastAPI框架时,默认已经安装了对应的依赖库。
- FastAPI依赖注入机制详解
依赖注入(Dependency Injection,DI)是编程模式中的一种依赖倒置原则的范式应用实现。对于依赖注入,可以理解为,是一种能让一个对象接收来自其所依赖的其他对象的一种模式,是一种能通过某种依赖机制自动对依赖对象进行依赖处理,并对依赖对象执行具体实例化的操作。FastAPI框架中存在一个依赖树机制,依赖树在某种程度上扮演了整个IOC容器的角色,它可自动解析并处理每层中所有依赖项的注册和执行。
依赖注入的实现其实是FastAPI框架的另一个特性,它在框架中的应用场景主要有:
❑业务逻辑共享:使用依赖注入机制来统一定义业务逻辑处理,可以避免在每个函数中重复创建,快速共享一段业务逻辑处理。
❑数据库连接:使用依赖注入机制来管理数据库连接,可以避免在每个函数中重复创建连接,共享同一个上下文中的连接对象。
❑认证和授权:使用依赖注入机制来管理用户认证和授权,可以避免在每个函数中重复编写认证和授权代码。
❑缓存管理:使用依赖注入机制来管理缓存,可以避免在每个函数中重复编写缓存管理代码。
❑外部API调用:使用依赖注入机制来管理外部API调用,可以避免在每个函数中重复编写API调用代码。
❑参数校验和转换:使用依赖注入机制来管理参数校验和转换,可以避免在每个函数中重复编写参数校验和转换代码。
Python中目前有比较多的开源依赖注入框架,如python-dependency-injector(GitHub地址:https://github.com/ets-labs/python-dependency-injector )、injector( GitHub地址:https://github.com/alecthomas/injector )等。python-dependency-injector库是基于BSD-3-Clause license开源协议方式发布的。
在Web开发过程中,通常需要一种机制处理请求前和响应后的一些钩子函数,通常把这类函数称为中间件。FastAPI的中间件是在应用程序处理HTTP请求和响应之前或之后执行功能的一个组件。中间件通常具有轻量、低级别、可拔插化的特点,它可以在全局范围内处理对客户端的请求和响应拦截。
FastAPI框架提供了非常多的内置中间件,其中大部分的中间件都直接来自于Starlette框架。
中间件的适用场景:请求跨域处理、API限流限速、对接口进行监控、日志请求记录、IP白名单限制处理、请求权限的校验、请求缓存处理、认证和授权、压缩响应内容等。
数据库的应用
SQL是进行关系型数据库操作时通用的查询语言。SQL通常由DDL(数据定义语言)和DML(数据操作语言)两个部分组成。DML其实就是人们常说的CRUD(Create,Read,Update,Delete)。常见的ORM库主要有SQLAlchemy、Tortoise-orm、SQLModel、peewee、Database、piccolo、ormar。
在Python领域,对一些库进行异步支持时,通常会在前面加上一个“aio×××”,表示当前这个数据库是异步类型的数据库或模块。在数据库连接驱动库中,常见的同步库有PyMySQL、Redis、psycopg2、psycopg3、SQLite3等,对应的异步库主要有aiomysql、aiomongo、aioredis、asyncpg aiosqlite等。
SQLite是一种基于文件类型的数据库,它不像MySQL或PostgreSQL等数据库那样,需要设置URL中的hostname和username:password等信息,只需要设置一个保存数据库文件的路径即可。
SQLAlchemy提供了一个ORM模型类的基类,这个基类通过declarative_base()函数返回,用户自定义的扩展模型类都需要基于这个基类来实现。
和FastAPI一样,SQLModel整合了Pydantic和SQLAlchemy,这样可以减少代码的重复量。SQLModel也要求使用Python 3.6及以上版本。
NoSQL主要存储一些半结构化的数据,比较常见的键值对数据库Redis就是NoSQL数据库。Redis是一个基于内存的、高性能的键值对数据库,支持多种数据类型,如String、Hash、List、Set、Zset等,同时支持数据持久化。Redis采用的是单进程、单线程模式(这里所说的单线程模式强调的是对命令的执行是单线程的,在最新的Redis 6.0版本中,虽然推出了多线程模式,但是这个多线程模式主要是指网络模型中I/O的多线程,并非是命令执行的多线程),所以在执行数据命令时需要通过队列模式把所有的并发请求命令变成串行之后再处理。异步库aioredis,该库是支持asyncio的Redis客户端库,通过该库可以基于asyncio为Redis提供简单易用的接口。
安全认证机制
OpenAPI规范(OAS)是一种和语言无关的RESTful API规范。基于FastAPI框架,可以生成对应的OpenAPI规范,还可以通过访问openapi_url地址来获取OpenAPI规范,默认的openapi_url地址为http://ip:port/openapi.json 。
OpenAPI规范除了提供文档方案外,还内置了多种解决安全问题的方案,主要有如下几个:
❑基于APIKey的特定密钥方案。
❑基于标准HTTP的身份验证方案,包括HTTPBearer、HTTPBasic基本认证和HTTPDigest摘要认证等。
❑基于OAuth 2的授权机制颁发令牌(token)方案。
❑openIdConnect自动发现OAuth 2身份验证数据方案。
对于上述方案,FastAPI框架进行了提供,这些解决方案存在于fastapi.security模块中,导入后就可以直接使用。
OAuth是一个开放授权的开发标准,是一种授权协议,它是Open Authorization的缩写,其中Open表示开发,而Authorization表示授权,而OAuth 2中的2表示版本号。OAuth 2与OAuth 1不兼容。
注意,OAuth 2是授权协议,而非认证协议,它没有提供完善的身份认证功能。其中,认证指对用户名和密码等用户身份信息进行验证;授权可在系统认证身份后判断用户可访问的资源范围。
在FastAPI框架中,官方推荐使用JOSE规范集进行JWT生成。其中,python-jose
是JOSE中的Python实现库。
python-jose默认方式:使用python-rsa和python-ecdsa进行加密和解密。
OAuth 2的授权处理机制有以下几个优势:
❑避免在授权给第三方应用的过程中泄露用户信息。
❑结合token认证机制,可设置允许授权访问的第三方应用资源范围和token有效期。
预约挂号系统实战
消息队列(Message Queue,MQ)
消息队列(Message Queue,MQ)是一种用于处理异步通信的协议方法,它允许应用程序在同一个进程内或跨进程之间发送和接收消息。消息队列可以帮助解耦应用程序组件之间的通信,并提高系统的可伸缩性和可靠性。“消息队列”中的“消息”通常是指应用程序需要处理的传递消息的抽象封装,这些消息通常被推送到一个待处理的队列里面进行排队,等待被取出来进行消费。
使用消息队列的好处有:
❑消息的异步通信处理,允许应用程序在处理消息时不必等待响应,加速响应,提供Web吞吐量。
❑应用和业务解耦。因为消息的传递没有直接调用关系,都是依赖中间件来处理的,因此系统侵入性不强,耦合度低。
❑进行流量控制,有效地削峰填谷,避免流量突刺造成系统负载过高。
❑多种消息通信可以为点对点的通信,也可以为聊天时的通信,基于此特性可更容易地扩展应用程序,而无须更改现有代码。
❑消息队列可以提供持久性和可靠性,确保消息在传递过程中不会丢失或重复。
RabbitMQ是当前比较流行的开源消息队列系统之一,是基于Erlang语言开发的,它遵循AMQP(高级消息队列协议),是AMQP的标准实现。
由于RabbitMQ是基于Erlang编写的,所以在安装RabbitMQ之前需要先安装好Erlang。首先访问RabbitMQ官网地址 https://www.rabbitmq.com/download.html ,单击“Erlang Versions”链接,查看具体的版本信息,知道需要安装的具体Erlang版本之后,访问Erlang官方,进行安装包下载即可。Erlang官方地址为http://www.erlang.org/downloads 。安装成功后,需要配置当前的Erlang环境变量。
安装好RabbitMQ后,即可启动RabbitMQ服务。在启动RabbitMQ服务之前,需要安装RabbitMQ图形界面管理插件。插件安装完成后,启动RabbitMQ(或直接进入安装路径的sbin中,双击rabbitmqserver.bat),服务启动后打开浏览器访问http://localhost:15672/ ,即可看到RabbitMQ图形界面管理首页(如图12-32所示),然后输入RabbitMQ默认账号及密码信息。需要注意,RabbitMQ系统默认账号及密码都是guest。
客户端pika库,它是当前比较常用的使用同步方式进行RabbitMQ连接和操作的库,如果使用异步方式,则需要安装aio-pika。
RabbitMQ提供了6种消息类型:
❑生产者/消费者模式。
❑工作队列模式。
❑发布/订阅(Pub/Sub)模式。
❑路由模式。
❑主题通配符模式。
❑请求/回复模式。
- RabbitMQ死信队列
在整个系统的运行过程中,生产者将待处理的消息正常投递到队列后,消费者由于某些因素或特定设置而引发了队列中的某些消息无法被正常进行消费的情况,这种类型的消息一直没处理,就会成为死信(Dead Letter),此时死信会转移到新的队列进行保存,这个新队列就是死信队列。
通常,消息转换为死信有以下几种情况:
❑消息被消费者拒绝。使用channel.basicNack或channel.basicReject,并且此时的requeue属性需设置为False。
❑消息在队列中的时间超过设置的TTL时间(一个队列中消息的TTL对其他队列中同一条消息的TTL没有影响)。默认情况下,RabbitMQ中的消息不会过期,但可以人为地设置队列的过期时间及消息的过期时间。
❑消息队列的消息数量已经超过最大队列长度,无法继续新增消息到MQ。
RabbitMQ中对于死信消息的处理,会依据是否配置了死信队列信息来决定消息的去留。如果开启了配置死信队列信息,则消息会被转移到这个死信队列(DLX)中;如果没有配置,则此消息会被丢弃。
RabbitMQ本身没有直接支持延迟队列的功能,但是综合死信队列和过期时间的使用可实现延迟队列。延迟队列指某个消息在某个固定的时间失效后,进入死信队列,其他死信的消费者实时处理这些过期的消息,就可以起到延迟处理的效果。
asgiref库是一个用于ASGI应用程序的通用辅助库,提供了一些常用的功能和工具。它提供了两个非常方便的转换函数。使用asgiref可以把异步或同步函数进行相互的转换。
生产环境部署详解
Python版本管理
pyenv是Python版本管理的常用工具之一,它可以在当前系统中进行全局Python版本的切换,或者进行局部设置,如为单个项目提供对应的Python版本。
基于pyenv-installer自动化脚本安装,一键安装的优点是方便升级维护,它的安装是基于git的方式实现的,所以可以保证当前安装的是最新版本,对应的Python版本也是比较新的。
基于pipenv管理虚拟环境
在生产环境中进行对应依赖库安装时,可以直接通过requirements.txt来安装。
基于Gunicorn+Uvicorn的服务部署
Gunicorn是UNIX下的WSGI HTTP服务器(所以它不支持在Windows系统下使用)。因为Gunicorn具有轻量级的资源消耗、高稳定性和高性能等特性,所以通过它可以大幅度地提高WSGI App运行时的性能。它还可以与Nginx和Apache等Web服务器配合使用,将请求从Web服务器转发给Gunicorn进程,提供更好的性能和可靠性。
在之前的本地开始阶段,运行服务器都是基于Uvicorn实现的。相对于WSGI服务器,Uvicorn是一个基于asyncio、高效、轻量级的Python ASGI(Asynchronous Server Gateway Interface)服务器。Gunicorn可以作为反向代理服务器来接收客户端请求,并将请求转发给Uvicorn服务器处理。而Uvicorn则是一个异步框架,可以处理高并发的请求。人们可以综合Gunicorn+Uvicorn的方式来进行服务部署,利用Gunicorn的预生成工作模式和Uvicorn的异步能力等各自的优势,实现高效且稳定的FastAPI服务部署。这样既可以拥有Uvicorn异步高性能的特性,又可以基于Gunicorn对worker工作进程进行监控管理。
基于Supervisor的服务进程管理
常见的进程管理器有PM2和Supervisor等,这里选择的是Supervisor。Supervisor本身是基于Python编写的进程管理应用,在生产环境中,用它可以很方便地启动、重启、关闭服务进程,并且当系统出现错误或重启时,可以自动重启相关服务进程。
Supervisor是基于CS架构实现的,主要有以下两个组成部分:
❑supervisord:supervisord是Supervisor的服务端程序。
❑supervisorctl:客户端命令行工具,可以连接supervisord服务器端进行进程管理,如进程启动、进程关闭、进程重启、进程配置更新、查看进程状态等。
supervisord.conf的文件内容过多,这里不展开,读者可以查阅官网进行了解,Supervisor的官网地址为http://supervisord.org/configuration.html。
注意 日志文件的目录/data/logs/supervisord需要提前创建好,不然容易出现问题。
基于OpenResty的反向代理
通常,为了提供更多系统负载能力,在部署服务时启动多个服务实例。所谓“负载均衡”就是将客户端请求分发到多个服务实例或多个服务器上,以此来横向扩展服务性能。业界常用的工具是Nginx,它是一个开源的Web服务器。
OpenResty,它是基于Nginx与Lua的高性能Web开源服务器。使用它可以实现Nginx所具备的功能,也可以方便地搭建处理超高并发和扩展性能的动态Web应用、Web服务等,如通过OpenResty与Lua来进行网关接口API限流等。注意
Nginx主要负责监听外部流量,以及把服务暴露给外网,而Gunicorn启动的服务都在内网中,此时Gunicorn不对外网进行开放,而是将Nginx作为入口,进而转发到Gunicorn内网的服务端口上。
基于SVN自动化部署
关于代码的版本控制方式,目前常用的主要是SVN和Git等。其中,Git是分布式的,SVN是集中式的。SVN上手相对简单,相比于Git复杂的命令来说,SVN也方便进行代码管理。当然,SVN也有缺点,就是需要中央服务器和用户客户端。虽然SVN有缺点,但是对于个人或小企业来说,使用此方式可以满足基本的代码版本管理需求。
创建post-commit钩子文件(post-commit文件没有扩展名),用于SVN更新成功后自动同步到对应的项目目录和执行相关的重启服务命令。
基于Docker进行服务部署
Dockerfile是由一系列的命令和参数构成的脚本文件,在该文件中构建镜像需要执行UNIX命令,比如使用PIP安装第三方依赖库等,最终使用docker build执行Dockerfile脚本来构建镜像。
在FastAPI框架中读取环境变量的常用方式有3种:
❑基于OS标准库读取。
❑基于Pydantic中的BaseSettings自动绑定并解析环境变量。
❑结合Docker下的环境变量读取。
通常,在Linux系统上设置环境变量的方式是执行export命令。
需要注意,基于Pydantic读取环境变量遵循的优先级是:先读取环境变量,再读取本地变量。
在Docker中设置环境变量的方法主要有以下几种:
❑在Dockerfile文件内使用ENV指令设置环境变量,对应的格式有如下两种:
❍ENV key value
❍ENV key1=value1 key2=value2
❑通过命令行参数–env或-e来设置环境变量。
❑通过命令行参数–env-file来指定某.env文件作为环境变量配置文件并完成解析和读取。
基于Docker Compose进行服务编排
通常,目录挂载是为了解决容器内部文件和外部文件的互通问题。通过它可以解决同步相关代码配置修改,以及日志双向的数据同步等问题。
注意 大多数有状态的服务组件,如数据库服务MySQL以及postgres等,不建议运行在容器内(不是完全不能运行,主要是看对数据的敏感程度),因为这会带来如下问题:
❑容器自身具有停止或者删除等操作,当容器被删除时,容器内的数据会丢失(虽然可以通过目录挂载方式进行存储,但是容器本身的一些特性有可能会造成数据损坏。)
❑通过容器运行数据服务程序,会存在IO读写性能问题。
Docker Compose是基于Python开发的用于Docker服务编排的工具,在构建基于Docker的复杂应用时通过Docker Compose编写docker-compose.yml配置文件来管理多个Docker容器,或对容器集群进行管理和编排。Docker Compose主要用于封装并执行一些docker run命令。
基于Gogs+Drone进行可持续集成
持续集成是一种软件应用开发交付方式,通过持续集成可以让软件应用更快、更高质量地进行迭代发布。
- 通过Gogs搭建自助Git服务
Gogs是一个类似SVN的代码管理系统,基于Gogs可以实现类似GitHub的相关功能。使用Gogs可以轻松、方便、快捷地构建一个自助Git服务。它具有易安装、跨平台、轻量级等特性。相对其他的自助Git服务(如Gitlab、Gitea等),Gogs对机器硬件的要求较低。由于这里的项目比较小,所以选择Gogs来进行项目代码的管理。 - Drone
Drone是一个新一代CICD(持续集成和持续交付)的平台,相对于Jenkins、Gitlab CICD之类的工具来说比较轻量,它可以与Docker和K8s无缝衔接。通过Drone结合各种主流的Git平台,可以实现对应Git Push自动化的打包和部署。它使用简单的YAML配置文件就可以完成比较复杂的自动化构建、测试和发布等一系列的工作流程。
需注意,在Drone中,Trusted特权容器是一种对宿主机资源具有特殊访问权限的容器,它能够为Drone构建过程中使用到的Docker容器提供相关必要的权限。在使用Drone构建和部署应用程序时,需要在Drone服务器上配置Trusted容器映像,并在Drone Agent使用Docker执行器时开启Trusted特权容器支持,以便其能够获取对应权限,不然执行流水线任务时会出现问题。
Drone提供了Secrets配置密钥项,用户可以把一些关键敏感的配置信息写入Secrets中进行保存。基于Sentry实现错误信息收集
Sentry是一个基于Diango的实时事件日志和错误收集聚合平台,它专注于应用程序的Error、Exception、Crash等错误和异常,可以查看具体的错误信息和调用栈,能快速定位问题代码。当出现异常时,还可以及时通知开发者,以便尽早发现隐蔽的问题,改善产品质量和研发效率。Sentry分为客户端和服务端,服务端主要负责提供错误消息和日志信息等,以及相关的展示和查询。客户端负责收集程序异常信息并发送到服务端。官网也提供了基于docker-compose安装Sentry的脚本。安装脚本的下载地址为 https://github.com/getsentry/self-hosted 。注意,Sentry的安装对服务器配置有一定的要求:Docker 17.05.0+,Compose 1.23.0+,两核及以上处理器,3800MB(最好不低于8GB)内存。
Sentry服务默认使用的端口是9000,通过浏览器访问 http://192.168.126.130:9000/auth/login/sentry/ 地址即可访问Sentry管理系统后台。