读后笔记 -- 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)
main.py 挂载子模块

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")
step1: main.py 跳转主端点
from fastapi import APIRouter

router = APIRouter()


@router.get("/university/{portal_id}")
def access_portal(portal_id: int):
    return {"message": "University ERP Systems"}
step2: controller\university.py 定义主端点
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
step3: gateway\api_router.py 定义通用网关+自定义异常
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)
step4: main.py 定义异常处理器,重定向到各微服务

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
main.py 中通过 loguru 实现集中日志记录
注意: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()
\student_mgmg\controllers\assignment.py (客户端)
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()
\faculty_mgmt\controllers\assignments.py (服务端)

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()
faculty_mgmt.controllers.book.py (客户端)
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()
library_mgmt.controllers.admin.py (服务端)

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  
微服务 faculty 项目结构

 


4.16 微服务的配置

两种实现方式:

4.16.1 设置存储为类属性 

class FacultySettings(BaseSettings):
    application: str = 'Faculty Management System'
    webmaster: str = 'sjctrags@university.com'
    created: date = '2021-11-10'
方式一:将设置存储为类属性(configuarion\config.py)
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)
方式一的调用 (main.py)

 

4.16.2 在属性文件中存储设置

production_server = prodserver100
prod_port = 9000
development_server = devserver200
dev_port = 10000
方式二:step1 属性文件中定义设置 (configuration\erp_settings.properties)
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")
方式二:step2 继承 BaseSettings 的类 + Config 内部类及 env_file 属性读取配置文件 (configuration\config.py)
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)
方式二:step3 调用(main.py)

 

posted on 2024-05-19 11:47  bruce_he  阅读(259)  评论(0编辑  收藏  举报