读后笔记 -- FastAPI 构建Python微服务 Chapter4:构建微服务应用程序
4.2 应用分解模式
将应用程序分解可以有两种方式:
1)按业务单元分解:基于组织结构、架构组件和结构单元;
根据业务分成3个独立的微服务,每个微服务作为独立的项目打开,并作为独立的微服务运行。另外两个微服务(ch04-library、ch04-student)和 ch04-faculty 具有相同的结构;
以 ch04-faculty 为例(另外两个类似),
- 独立作为一个 project 打开,
- 在 run 中 > uvicorn main:app --port 8001 --reload
- 在 web 中 http://localhost:8001/docs 打开 swagger
2)按子域分解:使用领域模型(domain model)及对应的业务流程作为分解的基础;
该结构具有:
1)faculty-mgt, library-mgt, student-mgt 是3个独立的微服务应用程序,并且在各自的 xx_main.py 中 include_router 其他的两个路由 来实现子模块的相互访问;
2)在 main.py 中 通过 挂载子模块 app.mount() 挂载 3 个微服务;
运行时:uvicorn main:app --port 8001 --reload
访问 student 微服务:http://localhost:8001/ch04/student/docs
访问 faculty 微服务:http://localhost:8001/ch04/faculty/docs
4.3 访问方式一:main.py 挂载子模块 来访问子服务
1)项目结构如下:
│ main.py # main │ READMD.md # readme │ __init__.py ├─configuration # 配置文件 │ │ config.py │ │ erp_settings.properties │ │ __init__.py ├─faculty_mgt # faculty 子模块 │ │ faculty_main.py │ │ __init__.py │ ├─controllers │ │ │ assignments.py │ │ │ books.py │ │ │ __init__.py │ ├─models │ │ │ __init__.py │ │ ├─data │ │ │ │ faculty.py │ │ │ │ facultydb.py │ │ │ │ __init__.py │ │ ├─request │ │ │ │ assignment.py │ │ │ │ __init__.py │ ├─repository │ │ │ assignments.py │ │ │ __init__.py │ ├─services │ │ │ assignments.py │ │ │ __init__.py ├─library_mgt # library 子模块 │ │ library_main.py │ │ __init__.py │ ├─controllers │ │ │ admin.py │ │ │ management.py │ │ │ __init__.py │ ├─models │ │ │ __init__.py │ │ ├─data │ │ │ │ library.py │ │ │ │ librarydb.py │ │ │ │ __init__.py │ ├─repository │ │ │ books.py │ │ │ reservations.py │ │ │ __init__.py │ ├─services │ │ │ books.py │ │ │ reservations.py │ │ │ __init__.py ├─student_mgt # student 子模块 │ │ student_main.py │ │ __init__.py │ ├─controllers │ │ │ assignments.py │ │ │ __init__.py ```
2)main.py 的挂载:
from fastapi import FastAPI from faculty_mgt import faculty_main from library_mgt import library_main from student_mgt import student_main app = FastAPI() app.mount("/ch04/student", student_main.student_app) app.mount("/ch04/faculty", faculty_main.faculty_app) app.mount("/ch04/library", library_main.library_app)
3)实现:通过 "url + 不同端点" 来访问子模块
运行:uvicorn main:app --port 8001 --reload
访问 student 服务:http://localhost:8001/ch04/student/docs
访问 library 服务:http://localhost:8001/ch04/library/docs
访问 faculty 服务:http://localhost:8001/ch04/faculty/docs
4.4 ~ 4.7 访问方式二:主端点+通用网关+利用异常 重定向 具体的微服务 API
1)项目结构
│ main.py # main │ READMD.md # readme │ __init__.py ├─configuration # 配置文件 │ │ config.py │ │ erp_settings.properties │ │ __init__.py ├─controller # 定义主端点 │ │ university.py │ │ __init__.py ├─faculty_mgt │ │ faculty_main.py │ │ __init__.py │ ├─controllers │ │ │ assignments.py │ │ │ books.py │ │ │ __init__.py │ ├─models │ │ │ __init__.py │ │ ├─data │ │ │ │ faculty.py │ │ │ │ facultydb.py │ │ │ │ __init__.py │ │ ├─request │ │ │ │ assignment.py │ │ │ │ __init__.py │ ├─repository │ │ │ assignments.py │ │ │ __init__.py │ ├─services │ │ │ assignments.py │ │ │ __init__.py ├─gateway # 定义通用网关 │ │ api_router.py │ │ __init__.py ├─library_mgt │ │ library_main.py │ │ __init__.py │ ├─controllers │ │ │ admin.py │ │ │ management.py │ │ │ __init__.py │ ├─models │ │ │ __init__.py │ │ ├─data │ │ │ │ library.py │ │ │ │ librarydb.py │ │ │ │ __init__.py │ ├─repository │ │ │ books.py │ │ │ reservations.py │ │ │ __init__.py │ ├─services │ │ │ books.py │ │ │ reservations.py │ │ │ __init__.py ├─student_mgt │ │ student_main.py │ │ __init__.py │ ├─controllers │ │ │ assignments.py │ │ │ __init__.py ```
2)具体实现:
from fastapi import FastAPI, Depends from controller import university from gateway.api_router import call_api_gateway app = FastAPI() # 通过方法 /ch04/university/{portal_id},指向通用的网关 app.include_router(university.router, dependencies=[Depends(call_api_gateway)], prefix="/ch04")
from fastapi import APIRouter router = APIRouter() @router.get("/university/{portal_id}") def access_portal(portal_id: int): return {"message": "University ERP Systems"}
from fastapi import Request def call_api_gateway(request: Request): portal_id = request.path_params['portal_id'] logger.info(request.path_params) if portal_id == str(1): raise RedirectStudentPortalException() elif portal_id == str(2): raise RedirectFacultyPortalException() elif portal_id == str(3): raise RedirectLibraryPortalException() class RedirectStudentPortalException(Exception): pass class RedirectFacultyPortalException(Exception): pass class RedirectLibraryPortalException(Exception): pass
from fastapi import FastAPI, Depends, Request, Response from fastapi.responses import RedirectResponse, JSONResponse from constant import port from controller import university from faculty_mgt import faculty_main from gateway.api_router import call_api_gateway, RedirectStudentPortalException, RedirectLibraryPortalException, \ RedirectFacultyPortalException from library_mgt import library_main from student_mgt import student_main app = FastAPI() # 定义异常处理器,重定向到各微服务 @app.exception_handler(RedirectStudentPortalException) def exception_handler_student(request: Request, exc: RedirectStudentPortalException) -> Response: return RedirectResponse(url=f"http://localhost:{port}/ch04/student/index") @app.exception_handler(RedirectFacultyPortalException) def exception_handler_faculty(request: Request, exc: RedirectFacultyPortalException) -> Response: return RedirectResponse(url=f"http://localhost:{port}/ch04/faculty/docs") @app.exception_handler(RedirectLibraryPortalException) def exception_handler_library(request: Request, exc: RedirectLibraryPortalException) -> Response: return RedirectResponse(url=f"http://localhost:{port}/ch04/library/index") app.mount("/ch04/student", student_main.student_app) app.mount("/ch04/faculty", faculty_main.faculty_app) app.mount("/ch04/library", library_main.library_app)
3)实现:通过 "url + 通用端点 university + portal_id" 来重定向到某个具体的微服务
运行:uvicorn main:app --port 8005 --reload
访问 student 的某个具体微服务:http://localhost:8005/university/1
访问 faculty 的某个具体微服务:http://localhost:8005/unversity/2
访问 library 的某个具体微服务:http://localhost:8005/unveristy/3
实现逻辑:
API 访问 http://localhost:8005/ch04/university/3
1)通过 app.include_router 路由 指向 university.py,
2)根据依赖, gateway.api_router.call_api_gateway,根据后面的 portal_id 指向不同的 Exception
3)main.py 中自定义异常处理,将各异常重定向到 各子服务上 (通过 url 上重定向 跳转到 http://localhost:8005/ch04/library/index)
4.8-4.9 使用 loguru,构建日志中间件实现集中日志记录
Python 自身的 logging 本身是基于同步日志记录,对于 FastAPI 同时支持 同步和异步 API 运行的日志记录,更倾向于异步日志 loguru。
pip 安装: pip install loguru
实现逻辑(主程序或子程序的任何 API 服务都会生成日志, 而且该中间件还有参数 call_next 可以实现 前处理+后处理):
from uuid import uuid4 from loguru import logger # 使用 loguru 适合来处理 FastAPI 这种异步和同步都存在的架构。 配置了一个日志处理器,它会将级别为INFO及以上的日志记录,按照指定的格式, # 异步地写入到名为info.log的文件中,同时包含额外的上下文信息(如log_id),以增强日志的可追踪性和信息丰富度 logger.add("info.log", format="Log: [{extra[log_id]}: {time} - {level} - {message} ", level="INFO", enqueue=True) # 构建日志中间件 @app.middleware("http") async def log_middleware(request: Request, call_next): log_id = str(uuid4()) with logger.contextualize(log_id=log_id): logger.info("Request to access " + request.url.path) try: response = await call_next(request) except Exception as ex: logger.error(f"Request to {request.url.path } failed: {ex}") response = JSONResponse(content={"success": False}, status_code=500) finally: logger.info("Successfully accessed " + request.url.path) return response
注意:request 放在第一个参数,call_next 放在后面
4.10-4.11 两个微服务的交互的两个方式:httpx / requests
4.10 方式一:使用 httpx 模块
1)安装 httpx: pip install httpx
2)实现逻辑:子服务的一端视为客户端,另一子服务的一端视为服务端
3)实现代码:
import httpx from fastapi import APIRouter router = APIRouter() @router.get("/assignment/list") async def list_assignments(): async with httpx.AsyncClient() as client: response = await client.get(f"http://localhost:{port}/ch04/faculty/assignments/list") return response.json()
from fastapi import APIRouter from faculty_mgt.services.assignments import AssignmentService router = APIRouter() @router.get("/assignments/list") async def provide_assignments(): assignment_service: AssignmentService = AssignmentService() return assignment_service.list_assignment()
4)解释:
- url 访问 swagger:http://localhost:8005/ch04/student/docs,
- 请求接口 GET "/assignment/list"
- 通过 httpx.Client() GET 请求 http://localhost:8005/ch04/faculty/assignments/list
- 重定向到 faculty_mgmt.controllers.assignments.py 的 /assignments/list 即 /ch04/faculty/assignments/list
4.11 方式二:使用 requests 模块
1)安装 httpx: pip install requests
2)实现逻辑:子服务的一端视为客户端,另一子服务的一端视为服务端
3)实现代码:
import requests from fastapi import APIRouter from constant import port router = APIRouter() @router.get("/books/access/list") def list_all_books(): with requests.Session() as sess: response = sess.get(f"http://localhost:{port}/ch04/library/book/list") return response.json()
from fastapi import APIRouter from library_mgt.services.books import BookService router = APIRouter() @router.get("/book/list") def view_books(): book_service = BookService() return book_service.list_book()
4)解释:
- url 访问 http://127.0.0.1:8005/ch04/faculty/docs swagger,
- 请求接口 GET "/books/access/list"
- 将在 session 中 GET 请求 http://localhost:8005/ch04/library/book/list
- 重定向到 library_mgmt.controllers.admin.py 的 /book/list 即 /ch04/library/book/list
4.12 应用领域建模方法
Pre. 领域建模:当设计在应用程序中建立和组织结构时,了解应用程序的行为并使用其背后的业务逻辑来派生领域模型。下面是来自于 Chatgpt 的更详细的介绍:
领域建模在设计应用程序时扮演着至关重要的角色,它不仅帮助我们理解应用程序的行为,还指导我们如何根据业务逻辑有效地组织和结构化代码。以下是通过业务逻辑派生领域模型时的一些关键步骤和实践: 1. 需求调研与领域知识获取 访谈与工作坊:与业务专家、最终用户进行深入交流,了解业务流程、规则、术语以及痛点。 文档审查:查阅现有业务文档、流程图、政策手册,提取关键信息。 2. 领域分解与识别 识别子域:将复杂业务领域拆分成若干个小的、可管理的子域,每个子域聚焦特定的业务功能或领域知识。 核心域识别:确定哪些子域对业务竞争力至关重要,这些核心域将得到最多的关注和投资。 3. 领域概念提炼 实体(Entity):识别并定义领域中的关键对象,它们通常是持久化的,并具有唯一标识。 值对象(Value Object):描述实体的属性或描述,它们没有独立的身份,仅通过其属性来区分。 聚合(Aggregate):定义一组相关实体和值对象,作为数据修改的一致性单元。 领域事件(Domain Event):捕捉领域内发生的重大事件,触发其他领域的反应或改变领域状态。 4. 行为建模 领域服务(Domain Service):封装不属于任何特定实体或值对象的业务逻辑。 领域规则(Domain Rules):明确领域内的业务约束和规则,确保业务逻辑的正确性。 用例分析:通过用例描述系统如何响应外部事件,映射到领域模型中的行为。 5. 模型验证与迭代 原型与反馈:创建模型的初步版本,通过原型或简单的实现与业务专家验证。 持续迭代:根据反馈调整模型,直到它准确反映业务需求和逻辑。 6. 编码实现 领域驱动设计(DDD):依据领域模型,采用DDD的战术模式(如Repository、Factory、Service Layer等)来指导代码结构。 分层架构:设计清晰的分层架构,如表现层、应用服务层、领域层、基础设施层,确保领域模型的纯净性。 7. 测试与验证 单元测试:为领域模型的关键行为编写单元测试,确保业务规则的正确实施。 集成测试:验证不同子域之间的交互,确保系统作为一个整体的协调工作。
通过以上过程,我们可以确保应用程序的设计和结构紧密围绕业务逻辑展开,提高软件的可维护性、扩展性和对业务变化的适应能力。
微服务项目结构(以 ch04-faculty 为例)
ch04-faculty ├── configuration/ # 服务配置 │ └── config │ └── erp_settings.properties ├── controllers/ # 控制层 │ └── assignments │ └── ... ├── models/ # 模型层,提供应用程序的上下文框架 │ └── data # 数据模型,在其临时数据存储中捕获和存储数据的模型 │ └── faculty │ └── facultydb │ └── request # 请求模型,在 API 服务中使用的 BaseModel 对象 │ └── faculty ├── repository/ # 存储库层,1)创建管理数据访问的策略;2)为应用程序提供了一个高级抽象 │ └── assignments │ └── ... ├── services/ # 服务层,定义了应用程序的算法、操作和流程。通常而言,与存储库交互,为应用程序的其他组件构建必要的业务逻辑、管理和控制 │ └── assignments │ └── ... └── main.py └── README.md
4.16 微服务的配置
两种实现方式:
4.16.1 设置存储为类属性
class FacultySettings(BaseSettings): application: str = 'Faculty Management System' webmaster: str = 'sjctrags@university.com' created: date = '2021-11-10'
from fastapi import FastAPI, Depends from configuration.config import FacultySettings from controllers import assignments app = FastAPI() app.include_router(assignments.router, prefix="/ch04/faculty") def build_config(): return FacultySettings() @app.get('/index') def index_faculty(config:FacultySettings = Depends(build_config)): return { 'project_name': config.application, 'webmaster': config.webmaster, 'created': config.created } if __name__ == '__main__': import uvicorn uvicorn.run(app, host="127.0.0.1", port=8001)
4.16.2 在属性文件中存储设置
production_server = prodserver100 prod_port = 9000 development_server = devserver200 dev_port = 10000
import os from pydantic_settings import BaseSettings # 方式二: 配置文件 + 继承 BaseSettings 的类 + Config 内部类及 env_file 属性读取配置文件 class ServerSettings(BaseSettings): production_server: str prod_port: int development_server: str dev_port: int # 定义一个内部类 Config 和 env_file 来读取配置文件 class Config: env_file = os.path.join(os.getcwd(), "configuration", "erp_settings.properties")
from fastapi import FastAPI, Depends from configuration.config import ServerSettings from controllers import assignments app = FastAPI() app.include_router(assignments.router, prefix="/ch04/faculty") def fetch_config(): return ServerSettings() @app.get('/findex') def index_faculty(fconfig: ServerSettings = Depends(fetch_config)): return { 'development_server': fconfig.development_server, 'dev_port': fconfig.dev_port } if __name__ == '__main__': import uvicorn uvicorn.run(app, host="127.0.0.1", port=8001)