最近做了一个小项目,尝试用fastapi框架异步方式开发了接口。
开发过程中【通义千问帮了很大的忙,很感谢这个免费的GPT,我用的国产产品中助力最大的GPT!】
从掘金看到很多帮助很大的文章,不需要关注作者,不需要点击广告,不需要花钱就看到学到,很感激。 所以想略尽薄力,填充下掘金的笔记库。
在fastapi开发异步hello world
from fastapi import FastAPI, status
async def lifespan(app: FastAPI):
# startup
print('lifespan startup....')
yield
# stopup
print('lifespan stopup....')
app = FastAPI(title="my api 2024-01", docs_url=None, redoc_url=None, lifespan=lifespan)
@app.get("/")
async def root():
return {"message": "Hello World."}
if __name__ == "__main__":
# Use this for debugging purposes only
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=23400)
异步操作redis
pip install redis
直接贴我用的代码,功能如下
- 可以异步读写redis
- 主库挂掉自动切换备库,不包含自动切回主库
- 通义千问提供代码结构,我进行debug。
from redis.asyncio import StrictRedis
from app.core.config import settings
from app.logger import logger
async def create_redis_connection(host: str):
redis_conn = await StrictRedis(
host=host,
password=settings.REDIS_PASSWORD,
port=6379,
encoding="utf8",
socket_connect_timeout=1,
decode_responses=True,
db=0,
)
return redis_conn
is_try_slave = False
class RedisStore:
def __init__(self):
self.redis_conn = None # 不应在初始化时直接调用异步方法
async def init(self, is_master=True):
if is_master:
tmp_conn = await create_redis_connection(settings.REDIS_HOST)
else:
tmp_conn = await create_redis_connection(settings.REDIS_HOST_SLAVE)
try:
await tmp_conn.ping()
self.redis_conn = tmp_conn
if is_master:
logger.info('master redis connect success.')
else:
logger.info('slave redis connect success.')
except Exception as e:
if is_master:
logger.info('master redis connect failed, ready to connect slave redis')
await self.init(is_master=False)
else:
logger.info('slave redis connect failed')
async def set(self, key, value):
await self.redis_conn.set(key, value)
await self.redis_conn.expire(key, 60 * 60 * 12) # 12 hours
async def get(self, key):
res = await self.redis_conn.get(key)
return res
redis_store = RedisStore()
初始化redis
# main.py
async def lifespan(app: FastAPI):
logger.info('lifespan startup....')
await check_mysql_connection()
# startup
await redis_store.init()
if redis_store.redis_conn is not None:
logger.info('redis init done....')
else:
logger.info('redis init failed')
yield
# stopup
logger.info('lifespan stopup....')
异步操作mysql
这部分查了很久的资料,有好的知识不知道怎么整理,直接贴代码吧~
功能如下:
- 异步对MySQL读写
- 主库挂掉自动切换备库,不包含自动切回主库
- 通义千问提供代码结构,我进行debug。
pip install SQLAlchemy cryptography asyncmy
class Settings():
MYSQL_ENGIN: str = 'asyncmy' # pymysql, asyncmy
SQLALCHEMY_DATABASE_URI: str = f"mysql+{MYSQL_ENGIN}://root:123456@localhost_master/demo"
SQLALCHEMY_DATABASE_URI_SLAVE: str = f"mysql+{MYSQL_ENGIN}://root:123456@localhost_slave/demo"
# app/db/session.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
from sqlalchemy import select
from app.models.user import User
from app.logger import logger
# 使用元组存储引擎和会话创建器
db_dict = {
'engine': create_async_engine(settings.SQLALCHEMY_DATABASE_URI),
}
db_dict['session_local'] = sessionmaker(bind=db_dict['engine'], autocommit=False, autoflush=False, class_=AsyncSession)
engines = {
'master': create_async_engine(settings.SQLALCHEMY_DATABASE_URI),
'slave': None,
}
session_locals = {
'master': sessionmaker(bind=engines['master'], autocommit=False, autoflush=False, class_=AsyncSession),
'slave': None,
}
session_key = 'master'
engine = None
SessionLocal = None
async def check_mysql_connection(is_master=True):
async with db_dict['session_local']() as db:
try:
await db.execute(select(User))
if is_master:
logger.info(f"master MySQL connect success.")
else:
logger.info(f"slave MySQL connect success.")
except Exception as e:
# 如果当前是主库且连接失败,则尝试连接从库
if is_master:
logger.info('Master MySQL connect failed, ready to connect slave MySQL.')
# logger.info(f'{settings.SQLALCHEMY_DATABASE_URI_SLAVE}')
db_dict['engine'] = create_async_engine(settings.SQLALCHEMY_DATABASE_URI_SLAVE)
db_dict['session_local'] = sessionmaker(bind=db_dict['engine'], autocommit=False, autoflush=False, class_=AsyncSession)
await check_mysql_connection(is_master=False)
else:
logger.info(f"Slave MySQL connect failed.")
获取session的函数
# app/api/deps.py
from app.db.session import db_dict
from sqlalchemy.ext.asyncio import AsyncSession
async def get_db() -> AsyncSession:
async with db_dict['session_local']() as session:
yield session
在接口获取数据库session
# app/api/endpoints/user.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
import app.crud as crud
from app.api import deps
router = APIRouter()
@router.get("/users")
async def get_all_users(db: AsyncSession = Depends(deps.get_db)):
users = await crud.user.get_users(db=db)
return { 'code': 200, 'user_list': users, 'total_count': len(users) }
crud
# app/crud/crud_user.py
from fastapi.encoders import jsonable_encoder
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.crud.base import CRUDBase
from app.models.user import User
import app.schemas as schemas
class CRUDUser(CRUDBase):
async def create(self, db: AsyncSession, *, obj_in: schemas.CreateUserType) -> None:
db_obj = User(**jsonable_encoder(obj_in))
db.add(db_obj)
await db.commit()
async def get_users(self, db: AsyncSession):
r = await db.execute(select(User.name, User.uuid, User.phone).filter(User.is_del == False))
users = r.all()
# 需要转成dict,否则无法json dumps
return [dict(user._mapping) for user in users]
user = CRUDUser(User)
jMeter
截止目前发现jmeter很方便使用。我是在mac使用,直接brew install jmeter就完事~
当然电脑要装好Java,具体怎么装网上搜索或者使用GPT工具,比如阿里的通义千问就挺好用。
安装插件
直接可以在菜单上打开插件管理器进行安装,不需要像以前一样手动安装了。
我装了这两个插件,用于生成图表。
开始测试
-
输入计划名称,保存为文件
-
点击计划右键->创建线程组
-
线程组点击右键->创建http header
3.1. http heade点击左键 -> 点击最下方Add按钮 -> 在表格新增内容
根据实际情况设置接口通用头部信息
-
线程组点击右键 -> 创建http request
-
点击http request进行编辑
-
线程组右键 -> 创建两个数据收集监视器 View Results Tree: 可以查看每个请求具体信息,还要将数据导出的指定文件,用于后续生成图表。
Summary Report: 可以查看综合信息,我主要是看Throughput,可以简单理解为业务QPS
6.1. 设置请求信息保存路径
-
线程组右键 -> 新增Transactions per Second 要记得设置步骤6.1中的结果路径,会自动读取这个结果生成图表
-
线程组设置 我喜欢设置的是:线程数量设置100个,线程生成时间设置3秒,循环100次。总计会请求100 x 100 = 10000次。一发入魂。
-
然后在Summary Report和Transactions per Second查看请求速度。
-
报错信息可以在View Results Tree中查看,比如下面这个图红了一片有报错。
以上, 只是小白,对这个工具不熟。简单使用记录结束。
以下是优秀文章链接:
Docker相关常识
避免每次都运行pip安装依赖
栗子A:
FROM python:3.10.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --upgrade pip --index-url https://mirrors.cloud.tencent.com/pypi/simple --trusted-host mirrors.cloud.tencent.com
RUN pip install --no-cache-dir -r requirements.txt --index-url https://mirrors.cloud.tencent.com/pypi/simple --trusted-host mirrors.cloud.tencent.com
COPY . .
栗子B:
FROM python:3.10.13-slim
WORKDIR /app
COPY . .
RUN pip install --upgrade pip --index-url https://mirrors.cloud.tencent.com/pypi/simple --trusted-host mirrors.cloud.tencent.com
RUN pip install --no-cache-dir -r requirements.txt --index-url https://mirrors.cloud.tencent.com/pypi/simple --trusted-host mirrors.cloud.tencent.com
两个栗子的区别是一个是A一个是B。
还有就是栗子A在只是修改接口代码,不修改requirements.txt的时候不会触发重新pip安装。
栗子B是不修改requirements.txt,只是修改接口代码的时候都会触发pip安装。
原理网上很多,反正我就记住这样了~
通过本次吸取经验是:可以基于基础镜像制作一份包含pip依赖库的环境镜像,然后制作接口镜像时候引用环境镜像。 比如我的环境镜像配置文件是:
# 使用官方Python基础镜像
FROM python:3.10.13-slim
# 设置工作目录
WORKDIR /app
# 复制仅包含requirements.txt的文件夹
COPY requirements.txt .
# 安装依赖,这里分开两步是为了充分利用缓存
# 若requirements.txt未变,则只会使用缓存
RUN pip install --upgrade pip --index-url https://mirrors.cloud.tencent.com/pypi/simple --trusted-host mirrors.cloud.tencent.com
RUN pip install --no-cache-dir -r requirements.txt --index-url https://mirrors.cloud.tencent.com/pypi/simple --trusted-host mirrors.cloud.tencent.com
以上, 网上文章很多很杂,先GTP一下然后再搜索会学到新知识效率杠杠的~
ps: 掘金网页编辑器吃字的bug什么时候能修好?每次写完都要先换行改字。