当前位置: 首页 > news >正文

Python 全栈系列271 微服务踩坑记

说明

这个坑花了10个小时才爬出来

碰到一个现象:将微服务改造为并发后,请求最初很快,然后就出现大量的失败,然后过一会又能用。

过去从来没有碰到这个问题,要么是一些比较明显的资源,或者逻辑bug,总之跑断了就明白,非常显眼。然后从接口传入的并发量并不是很大,而且即使初期传入的量大也没见崩。

内容

1 分析

我把日志贴给了chatgpt,然后得到了一些信息。

在压测过程中,出现 “Cannot assign requested address” 错误,通常是由于系统的网络端口资源耗尽或请求连接过于频繁,导致无法分配新的端口。这类问题可能与以下几个因素有关:

  1. 端口耗尽:系统的可用端口数有限,尤其是在并发请求非常高时,操作系统可能耗尽可用的端口资源。这通常会导致类似 “Cannot assign requested address” 的错误。

  2. TIME_WAIT 状态:HTTP 请求结束后,系统会将使用的端口暂时保留在 TIME_WAIT 状态,导致端口无法立即释放和重复使用。如果并发请求较多,TIME_WAIT 状态的端口过多,会导致端口耗尽。

  3. 系统参数限制:一些系统网络参数设置可能过低,例如 net.ipv4.ip_local_port_range(本地可用端口范围)或 net.ipv4.tcp_tw_reuse(TIME_WAIT 状态端口重用),可能会影响并发连接的处理能力。

解决方案建议:

  1. 调整系统端口范围
    你可以通过 sysctl 修改系统可用的端口范围:

    sysctl -w net.ipv4.ip_local_port_range="1024 65535"
    
  2. 启用 TIME_WAIT 重用
    开启 tcp_tw_reuse 允许系统重用 TIME_WAIT 状态的端口:

    sysctl -w net.ipv4.tcp_tw_reuse=1
    
  3. 增加文件句柄和连接数
    提升系统最大文件句柄和连接数限制:

    ulimit -n 65536
    sysctl -w fs.file-max=1000000
    
  4. 合理配置负载均衡
    如果你使用了 Nginx 作为负载均衡器,确保 Nginx 配置了足够的连接处理能力,例如增加 worker_connections

这些设置可以帮助你在高并发压测环境中优化连接资源,减少端口耗尽的情况。

文件句柄数我先看了,非常大100多万。然后我看到fastapi在疯狂的刷端口,一个容器内所有的端口都给这个服务,所以说端口不够用也不对。

看到TIME_WAIT我突然有感觉了,应该是这个问题。

由于这个微服务实际上是第二级的,负责处理大量的规则。每一个entity都会执行一次规则。没执行一次都发起一次调用。然后按照TCP的规范,握手释放的时间至少要10秒,保守是30秒。默认情况下是2Min。

在这里插入图片描述
从这里也可以推算,假设有5万个端口分配给微服务,每120秒的冷却时间,那么TPS大约是400,如果改为30秒,那么是1200。还有就是通过长连接 + 批量发送来实质性的扩大TPS。

最初我的主web框架是flask和tornado。

本来这次没有意外的话,我应该使用Tornado搭建服务的,也许也就不会有这个问题。最初是看到FastAPI的一些优点:例如更高的json序列化效率,结合pydantic的写法,的确是更好的,所以也冒了一点险来使用。

然后在优化的过程中了解到了Gunicorn和Uvicorn的差别,也更明确了FastAPI适用的场景。例如,使用FastAPI进行异步数据库查询应该会比Tornado更合适 。反正想要做的更好就必须去冒险,踩坑可能是一种无法避免的事,当我再从坑里爬出来,就离目标更近了一点。

我也和Chat确认了一下:

是的,TornadoFastAPI 确实有不同之处,尤其是在长连接(如 WebSocket、长轮询)方面。

1. Tornado 的长连接能力

Tornado 是一个异步的 Python web 框架,最早是为实时 Web 应用设计的。它天然支持长连接,适合处理WebSocket长轮询、**服务器推送(Server-Sent Events, SSE)**等应用场景。它的异步 IO 模型基于 tornado.ioloop.IOLoop,让 Tornado 可以高效地处理并发连接和长时间保持的连接。

2. FastAPI 与长连接

FastAPI 虽然不是为长连接特别设计的,但它基于 Starlette 框架,同样支持 WebSocket、SSE 等长连接类型的通信。FastAPI 的优势在于它的性能和现代设计,包括使用异步支持(async/await),并且在许多情况下性能可以媲美 Tornado。

FastAPI 使用 WebSocket 的例子:

from fastapi import FastAPI, WebSocketapp = FastAPI()@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):await websocket.accept()while True:data = await websocket.receive_text()await websocket.send_text(f"Message received: {data}")

3. Tornado 和 FastAPI 的区别

  • 长连接支持:Tornado 原生支持长连接,适合实时通讯类应用,例如聊天服务器、实时数据流等。FastAPI 也支持长连接,但需要通过 WebSocket 等机制来实现,底层是通过 Starlette 提供的异步支持。

  • 并发模型:Tornado 使用自己的事件循环 (IOLoop) 和异步编程模型,比较轻量且高效。而 FastAPI 基于 Python 的 asyncio 事件循环,允许更现代的异步编程(async/await),并且和 Starlette、Uvicorn 结合提供高性能的异步 API。

  • 实时性和延迟:Tornado 被设计为应对高并发的长连接场景,在处理实时应用时,性能和低延迟方面表现突出。FastAPI 也表现不错,但对于长连接,可能在某些特定情况下性能不如 Tornado 稳定。

4. 长连接适用场景

  • Tornado:适合构建实时交互应用,如聊天系统、股票行情推送系统、游戏服务器等需要保持长时间的连接或高频率通讯的场景。

  • FastAPI:适合 RESTful API 服务,同时也可以处理长连接需求,如 WebSocket,但它的设计初衷是高性能的 API,而非长连接为主的应用场景。

总结:

  • Tornado:更适合专注于长连接实时通信的场景,原生支持这些特性。
  • FastAPI:更适合快速构建高性能API,也可以支持长连接,但需要依赖底层的异步框架。

小结论

到这里可以有一些小结论:

  • 1 在大量的密集处理任务上,tornado可能会更好一些;而fastapi更适合零散的异步任务。400TPS-1200TPS是一个天然限制。
  • 2 大量的规则不可以作为API,而是应该封装为函数式。事实上,我估算了一下产生的二级请求是一个比较夸张的量。能勉强顶住,FastAPI已经算不错的了。
  • 3 度量是否要API也要看计算/传输比。在简单规则中,这个值太低了,完全划不来。

2 解决

既然是“端口相对不足”的问题,那么就做相应的调整。

the_port=34009
docker run -d \--name=short_name_query_server_${the_port} \-v /etc/localtime:/etc/localtime \-v /etc/timezone:/etc/timezone \-v /etc/hostname:/etc/hostname \-p ${the_port}:8000 \-e "LANG=C.UTF-8" \--sysctl net.ipv4.tcp_fin_timeout=30 \--sysctl net.ipv4.tcp_tw_reuse=1 \--sysctl net.ipv4.ip_local_port_range="10000 65535" \-w /workspace \IMAGE \sh -c "uvicorn fast_server:app --host 0.0.0.0 --port 8000 --workers=5 

在docker启动时增加配置项,反正顾名思义吧

然后可以切入容器检查

cat /proc/sys/net/ipv4/tcp_fin_timeout
cat /proc/sys/net/ipv4/tcp_tw_reuse
cat /proc/sys/net/ipv4/ip_local_port_range

改完后实测下来是在足够大的批量里跑数都是0错误了,当然只能是可怜的并发2,而且服务内部我还不敢去并发执行规则。

3 Next

规则执行只会有 get、pass、reject、error四个状态

目前规则的样式

# reject
@app.post("/r000/")
async def r000(justent:JustEnt):the_ent = justent.some_entthe_result = RuleResult()try:if judge_existence(the_ent, word_list=r0_exe_clude_list):the_result.status = 'reject'else:the_result.status = 'pass'return the_result.dict()except Exception as e:raise HTTPException(status_code=400, detail=str(e))

在上层调用的样式

# 接口返回数据模型  v {status: pass/reject/get , data:None 或者匹配全称}
# mapping_list 仅用于本次,不是通用设计
# raw 也是如此
import time 
def waterfall_api_mode(last_fall, rule_name ,reject_list = None, get_list = None, mappling_list = None, raw = None , base_url = None):next_fall = []last_ent_list = last_fall pure_rule_url = rule_name + '/'if len(last_ent_list):rule_url = base_url + pure_rule_url# api modetick1 = time.time()task_list = []for ent in last_ent_list:tem_dict = {}tem_dict['task_id'] =  ent tem_dict['url'] = rule_urlif raw is None :tem_dict['json_params'] = {'some_ent':ent}else:tem_dict['json_params'] = {'some_ent':ent,'raw':raw}task_list.append(tem_dict)rule_res = asyncio.run(json_player(task_list, concurrent = 10))# 解析结果,保留passfor tem_res in rule_res:for k,v in tem_res.items():# print(k,v)if v['status'] == 'pass':next_fall.append(k)elif v['status'] == 'get':if get_list is not None :get_list.append(v['data'])if mappling_list is not None :mappling_list.append({'ent':k,'mapping_ent': v['data']})elif v['status'] == 'reject':if reject_list is not None :reject_list.append(k)tick2 = time.time()print('takes %.2f ' %(tick2-tick1))return next_fall

可以把输入的实体列表作为一个series,然后去apply就好了。根据每次apply的结果,分为四个类型:

  • 1 get : 附加到返回部分
  • 2 reject : 目前可以直接扔掉(如果是学习和分析)
  • 3 pass : 没有获取也没有抛弃,传入下一步处理。如果没有pass,那么处理结束。
  • 4 error:发生错误的部分,可以发往kafka

然后做一个简单的程序流就可以取代目前的微服务了。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 什么是 SMB 服务器以及它如何工作?
  • AI论文精读笔记-Generative Adversarial Nets(GAN)
  • YOLOv5改进 | 模块缝合 | C3 融合RFAConv和CBAM注意力机制 【二次融合 小白必备】
  • GANs-生成对抗网络
  • 第二期: 第二节 , 裸机编程 , gpio
  • 基于python+django+vue的鲜花商城系统
  • kafka mirror maker之实现两个kafka集群之间的数据同步
  • freeRTOS 任务的创建、挂起、恢复
  • 使用PyTorch进行图像风格迁移:基于VGG19实现
  • 多目标优化算法求解LSMOP(Large-Scale Multi-Objective Optimization Problem)测试集,MATLAB代码
  • Windows和Mac命令窗快速打开文件夹
  • vue 项目自适应 配置 px转rem 的插件postcss-pxtorem
  • 数据中台建设(六)—— 数据开发-提取数据价值
  • Java实现建造者模式和源码中的应用
  • 大棚分割数据集,40765对影像,16.9g数据量,0.8米高分二,纯手工标注(arcgis标注)的大规模农业大棚分割数据集。
  • JavaScript 如何正确处理 Unicode 编码问题!
  • [微信小程序] 使用ES6特性Class后出现编译异常
  • 「面试题」如何实现一个圣杯布局?
  • IIS 10 PHP CGI 设置 PHP_INI_SCAN_DIR
  • JavaScript工作原理(五):深入了解WebSockets,HTTP/2和SSE,以及如何选择
  • Java教程_软件开发基础
  • Mysql优化
  • React组件设计模式(一)
  • Service Worker
  • tensorflow学习笔记3——MNIST应用篇
  • zookeeper系列(七)实战分布式命名服务
  • 从0搭建SpringBoot的HelloWorld -- Java版本
  • 分享一个自己写的基于canvas的原生js图片爆炸插件
  • 前端工程化(Gulp、Webpack)-webpack
  • 浅析微信支付:申请退款、退款回调接口、查询退款
  • Prometheus VS InfluxDB
  • #android不同版本废弃api,新api。
  • #宝哥教你#查看jquery绑定的事件函数
  • $L^p$ 调和函数恒为零
  • (9)STL算法之逆转旋转
  • (附源码)ssm高校志愿者服务系统 毕业设计 011648
  • (七)c52学习之旅-中断
  • (四) Graphivz 颜色选择
  • (四)图像的%2线性拉伸
  • (转载)虚函数剖析
  • .htaccess配置重写url引擎
  • .NET Core WebAPI中使用Log4net 日志级别分类并记录到数据库
  • .Net Memory Profiler的使用举例
  • .net redis定时_一场由fork引发的超时,让我们重新探讨了Redis的抖动问题
  • .NET 中使用 Mutex 进行跨越进程边界的同步
  • .NET6实现破解Modbus poll点表配置文件
  • .NETCORE 开发登录接口MFA谷歌多因子身份验证
  • .NetCore 如何动态路由
  • .Net中间语言BeforeFieldInit
  • /etc/shadow字段详解
  • @SuppressWarnings注解
  • [ vulhub漏洞复现篇 ] ThinkPHP 5.0.23-Rce
  • [ 云计算 | Azure 实践 ] 在 Azure 门户中创建 VM 虚拟机并进行验证
  • [AIGC] Java List接口详解
  • [android学习笔记]学习jni编程