python3写一个http接口服务(url, get, post),接口限流、拒绝访问

python3写一个http接口服务(url, get, post),接口限流、拒绝访问

http(url)接口限流/拒绝访问/限制队列

接口限流(rate-limit),笔者最近工作中,遇到提供给客户的算法微服务,遇到大量请求挂掉了,除了扩容负载均衡外,也采取了限流的方式。
一般来说,常见的接口限流,我们可以采用

  • a. (看门和安保)WEB服务器/反向代理服务器(c语言), Nginx/Apache等支持 高并发负载均衡、拦截静态请求、SSL支持、保留80端口;
  • b. (运行和管理)WEB服务器(wsgi, http, python), Gunicorn/uWsgi等支持 多进程多线程、失败重启、接收请求分发(调用服务);
  • c. (工作和业务)WEB服务框架(轻量级、python), Fastapi/flask/Tornado等支持 后端应用、单个服务、轻量级并发;

一般架构

(nginx) -> (gunicorn) -> fastapi

限流方式

  • a. Nginx配置限流, 其内置漏桶算法、令牌桶算法, 可配置 limit_req_zone/limit_req/limit_rate(burst, nodelay)等模块限流;
  • b. Gunicorn配置限流, 其内置计数器, 可配置队列queue长度等拒绝访问, 可采用gunicorn-proxy请求缓冲、分流;
  • c. FastAPI配置限流, 可配置计数器(内存缓存)、信号量(redis缓存)等, 计数(时间、IP)限制访问;

fastapi + slowapi 限流

# !/usr/bin/python
# -*- coding: utf-8 -*-
# @time    : 2023/02/01 21:07
# @author  : Mo
# @function: 限流(slowapi), http of fastapi


import time

from fastapi import FastAPI, Request, Response, File, UploadFile
from starlette.responses import JSONResponse
from pydantic import BaseModel
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address
from slowapi import Limiter


def _rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded) -> Response:
    """
    Build a simple JSON response that includes the details of the rate limit
    that was hit. If no limit is hit, the countdown is added to headers.
    """
    response = JSONResponse({"error": f"Rate limit exceeded: {exc.detail}; status_code: {ERROR_CODE}; "
                                      f"request: {request.base_url}"}, status_code=ERROR_CODE)
    response = request.app.state.limiter._inject_headers(response, request.state.view_rate_limit)
    return response


LIMIT = "{}/second".format(5*2)  #  ("5/day") ("5/hour") (5/minute) ("5/second")  # 需放在@app.后
ERROR_CODE = 429

limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)


class Item(BaseModel):
    a: int = None
    b: int = None


@app.post("/calculate")
@limiter.limit(LIMIT)  #  ("5/day") ("5/hour") (5/minute) ("5/second")  # 需放在@app.后
async def calculate(item: Item, request: Request):  # Request必须配置
    a = item.a
    b = item.b
    c = a + b
    res = {"code": 200, "res": c}
    time.sleep(0.05)
    return res


if __name__ == '__main__':
    import uvicorn

    uvicorn.run(app=app,
                host="0.0.0.0",
                port=8832,
                workers=1)

测试

# !/usr/bin/python
# -*- coding: utf-8 -*-
# @time    : 2020/7/9 14:14
# @author  : Mo
# @function: test 


import requests, time, json, threading, random
import pandas as pd


def get_variable_name():
    """ 获取 string 变量的变量名字, variable_name: str """
    import inspect
    variable_name_dict = dict(inspect.currentframe().f_locals.items())
    res = None
    for k, v in variable_name_dict.items():
        if "frame" in str(type(v)):
            res = k
            break
    return res


class PressTet(object):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36',
        'Content-Type': 'application/json; charset=UTF-8',
        'Connection': 'close',
    }

    def __init__(self, press_url, data_json):
        self.press_url = press_url
        self.session = requests.Session()
        self.session.headers = self.headers
        self.data_json = data_json

    def work(self):
        '''压测接口'''
        time_str = str(time.time()) + str(random.random())
        self.data_json["time"] = time_str  # 加个参数使得每次传参不一样
        global ERROR_NUM
        try:
            html = self.session.post(self.press_url, data=json.dumps(self.data_json))
            html_json = html.json()
            # 错误判断
            if html_json.get("code", 0) != 200:
                print(html.json())
                ERROR_NUM += 1
            html.close()
        except Exception as e:
            print(str(e))
            ERROR_NUM += 1

    def tet_onework(self):
        '''一次并发处理单个任务'''
        i = 0
        while i < ONE_WORKER_NUM:
            i += 1
            self.work()
        time.sleep(LOOP_SLEEP)

    def run(self):
        '''使用多线程进程并发测试'''
        t1 = time.time()
        Threads = []

        for i in range(THREAD_NUM):
            t = threading.Thread(target=self.tet_onework, name="T" + str(i))
            t.setDaemon(True)
            Threads.append(t)

        for t in Threads:
            t.start()
        for t in Threads:
            t.join()
        t2 = time.time()

        print("===============压测结果===================")
        print("URL:", self.press_url)
        print("任务数量:", THREAD_NUM, "*", ONE_WORKER_NUM, "=", THREAD_NUM * ONE_WORKER_NUM)
        print("总耗时(秒):", t2 - t1)
        print("每次请求耗时(秒):", (t2 - t1) / (THREAD_NUM * ONE_WORKER_NUM))
        print("每秒承载请求数:", 1 / ((t2 - t1) / (THREAD_NUM * ONE_WORKER_NUM)))
        print("错误数量:", ERROR_NUM)


if __name__ == '__main__':


    # 在work里边设置一下错误判断, work
    press_url = 'http://localhost:8832/calculate'
    data_sample = {"a": 12, "b": 32}

    THREAD_NUM = 50  # 并发线程总数, 可设置 10, 20, 30, 50...
    ONE_WORKER_NUM = 10  # 每个线程的循环次数,  可设置 10, 20, 30, 50...
    LOOP_SLEEP = 0.01  # 每次请求时间间隔(秒), 可设置 0.1, 0.01, 0.001, 0.0001
    ERROR_NUM = 0  # 出错数

    obj = PressTet(press_url=press_url, data_json=data_sample)
    obj.run()
    
"""
===============压测结果===================
URL: http://localhost:8832/calculate
任务数量: 50 * 10 = 500
总耗时(秒): 21.13799810409546
每次请求耗时(秒): 0.04227599620819092
每秒承载请求数: 23.654084816249732
错误数量: 400

tcp 默认有心跳确保长连接,http1.1 无限制,nginx 默认一分半,gunicorn 默认 1 分钟,uvicorn 和 starlette 默认无限制,
所以要确保连接需要修改 nginx 和 gunicorn 的默认设置,另外浏览器也有限制但是比较长。

"""

参考

猜你喜欢

转载自blog.csdn.net/rensihui/article/details/129267528