拓冰建站拓冰建站
首页 / 资讯中心 / 正文

压力测试调优实战:从1000到90000连接的Linux内核参数与自动化测试

简介一份测试开发岗位的实习报告完整记录了实习生参与运输网站安全项目上线的全过程。报告从实习目的与背景讲起详细展开需求评审、需求分析、测试计划、用例设计、测试环境搭建、执行测试、BUG跟踪、测试报告八个核心环节并梳理了白盒/黑盒/灰盒、静态/动态、单元/集成/系统等测试方法分类以及BUG提交单的构成要素和实操压力测试案例。适合软件测试初学者、计算机专业学生及准备测试开发实习的人群参考。资源为单个doc文档共1个文件压缩包大小24KB目前已有442人学习下载。内容结构完整包含真实项目测试流程、压力测试中Linux文件句柄限制的排错思路等能为读者提供测试开发岗位的实际工作认知和实习报告撰写参考。1. 压力测试从 1000 到 90000测试开发实习里最能打的一段调优经历第一天上手压力测试我以为把客户端连接数开到 10 万就能测出服务器上限。结果客户端跑起来不到两秒连接数停在 1000 出头不再上涨——不是服务器扛不住并发而是 Linux 默认只给当前用户开了 1024 个文件描述符扣掉标准输入输出和服务端监听 socket 占用的那十来个真正能分给客户端连接的只剩 1014 个。测试开发岗位的第一个完整任务就是把这 1014 个连接一路调到 9 万多。这篇文章把这段经历完整拆开从测试全流程、Bug 单怎么写到压力测试里文件描述符、端口范围、TIME_WAIT 三层限制的逐级突破最后落到 pytest 自动化登录测试的落地方式。想做测试开发的新手可以跟着走一遍写过一段时间用例的人也能在参数细节里翻到一些值得对照的东西。2. 测试开发日常工作流需求评审、用例设计到 Bug 单落地的关键细节2.1 测试流程八阶段每个阶段回答什么问题产出什么正式接手压测之前先跟着测试主管完整走了一遍运输网站上线前的测试流程。看起来是八个阶段但每个阶段其实都在回答一个具体问题产物也不只是文档而是下一阶段的输入。把这个问题和产出物对齐流程才不会变成形式。阶段要回答的问题核心产出需求评审需求文档是否自洽有没有描述不清或互相矛盾的地方评审结论与问题清单需求分析目标系统必须完成哪些工作范围边界在哪功能清单、需求分析说明测试计划需要多少人力、设备时间怎么排功能点如何划分测试计划、资源排期用例设计哪些输入组合能覆盖功能点和边界条件测试用例集测试环境软件、硬件、网络、测试数据、工具是否满足测试要求环境检查单、环境配置基线执行测试按用例跑程序实际结果和预期是否一致缺陷记录、测试执行结果Bug 跟踪提交的缺陷是否修复修复有没有引入新问题回归结果、Bug 关闭记录测试报告当前版本质量是否达到上线标准测试报告、缺陷分析这个表格里的顺序不能乱尤其是需求评审和需求分析很多人会把它们合并成“看一下需求”但评审的核心是“找问题”分析的核心是“定范围”。当时在运输网站这个项目上需求评审阶段就发现一处业务规则描述前后矛盾——用户修改密码后旧 token 是否立即失效文档里写了两种结论。如果这个矛盾没有在评审阶段暴露后面用例设计、执行、回归全部都会跟着返工。每个阶段把上阶段的产出当作输入问题越早发现返工成本越低。2.2 测试方法选型白盒、黑盒、灰盒与阶段划分怎么对应实践测试方法从不同视角有不同的分类方式但落到日常测试开发工作里选型逻辑比记忆分类本身更重要。从是否关心软件内部结构来看白盒测试要读代码、分析逻辑路径一般由开发人员自己做的单元测试覆盖黑盒测试完全不看内部实现只验证输入输出是否符合需求是功能测试和系统测试最常用的方式灰盒测试介于两者之间关注接口报文、数据流不关心内部具体实现——接口自动化测试基本都属于这一类。从是否执行代码的角度静态测试靠代码评审和静态扫描工具做动态测试靠真正运行程序做。按开发阶段划分的单元、集成、确认、系统、验收、回归这六类日常工作里接触最多的是系统测试和回归测试。在运输网站这个项目里主营业务的登录、订单、运单查询走的是黑盒功能测试接口自动化接近灰盒视角安全相关专项测试则要结合系统配置、权限矩阵做系统级验证。选型的关键不是哪种方法更高级而是被测对象处于什么阶段、缺陷最容易藏在哪一层。一般情况下业务功能层面的缺陷用黑盒用例就能暴露接口层的字段校验和异常处理要靠灰盒接口用例兜底涉及性能和安全的部分才需要专门的压测与安全测试介入。2.3 用例设计实操从需求拆功能点到边界数据用例设计是测试执行前最重要的手工活。当时在主管指导下做登录模块用例时用的是一套很朴素的步骤后来发现这套步骤放到任何业务模块都通用。第一从需求文档里提取功能点。登录模块的功能点是正确用户名和密码可以登录、错误密码拒绝登录、不存在用户拒绝登录、空用户名和空密码提示必填。第二按功能点拆正常场景和异常场景。正常场景走通主流程异常场景覆盖提示信息是否准确、系统是否报错而不是崩溃。第三补充边界和权限场景。比如密码长度上下限、用户名是否区分大小写、连续登录失败后是否有锁定策略。第四把环境相关的前提写进前置条件。比如本地数据库是否存在对应测试账号、是否需要先清空登录锁定记录。这套步骤的关键在于“先列功能点再设计数据”而不是拿着需求文本直接写用例。功能点列的越全测试数据的针对性越强。用例设计完一定要评审评审时开发会从实现角度指出一些不可能出现的输入产品会从业务角度补充一些真实用户会做的操作两轮评审下来遗漏率会明显下降。2.4 一份定位不靠问的 Bug 单要素与判断标准实习期间最受用的一句话是优秀的 Bug 单开发不用跑来问测试就能知道怎么重现。判断一个 Bug 单是否合格不是看描述写了多少字而是看开发拿到单子之后能否不依赖任何口述独立复现问题并理解影响范围。一份完整的 Bug 单至少要覆盖 12 类信息所属系统、发现的版本、所属模块、提交人、错误类型、重现概率、严重级别、优先级、标题、Bug 单号、Bug 内容、附件。其中最容易敷衍的是标题和重现步骤常见错误是直接把测试用例的名字复制一遍比如“登录模块测试001失败”这种标题对定位没有任何帮助。合格的标题应该一句话说清“什么功能在什么条件下出了什么问题”例如“修改密码后旧 token 仍可访问订单接口”。下面是一个可以直接套用的示例字段示例内容所属系统运输网站-客户管理模块发现的版本V1.2.3所属模块登录认证错误类型代码错误重现概率必现严重级别严重优先级高标题修改密码后旧 token 仍可请求订单接口重现步骤1. 登录获取 token 2. 修改密码 3. 携带旧 token 请求订单列表预期结果返回 401要求重新登录实际结果返回 200 和订单列表数据附件旧 token 请求日志、接口响应截图提交 Bug 时尽量附上截图并做标注操作过程说明清楚。字不如图描述半天的文本信息量可能不如一张带红框标注的截图。重现概率、严重级别、优先级三个字段要分开填重现概率描述稳定性严重级别描述对用户的影响程度优先级描述修复的紧迫性。比如一个只在特定浏览器出现的页面布局错位严重级别是“一般”优先级可以是“低”但一个会导致用户订单数据错乱的场景即使出现概率是“小概率”严重级别也必须是“严重”优先级要提到“高”。填这三个字段时想的不是“这个 bug 多严重”而是“如果我是项目负责人看到这张单子会先安排谁处理”。3. TCP 连接压力测试的三层限制ulimit、端口范围与 TIME_WAIT 内核参数3.1 压测目标与初始现象默认只能建立 1014 个连接压力测试是进入公司后独立完成的第一个完整测试任务。被测系统是基于 TCP/IP 的 C/S 架构服务端监听固定端口客户端通过循环不断发起连接请求目标是验证服务器最多能响应多少个并发客户端连接。每成功建立一个客户端连接服务端就维护一个连接计数。用客户端脚本把目标并发数开到 10 万后连接数到 1000 出头就停住了。用ulimit -n查看发现当前用户最多同时打开 1024 个文件。Linux 下一切皆文件socket 也是文件描述符的一种每建立一个 TCP 连接就要消耗一个文件描述符1024 个文件描述符扣掉标准输入、标准输出、标准错误、服务端监听 socket 等固有占用可用的连接名额只剩 1014 个。这就是压测上不去的第一层瓶颈。3.2 第一层限制文件描述符与 limits.conf文件描述符限制分软限制和硬限制两层。软限制是当前会话内可以随时调低或调高的值硬限制是软限制可以到达的上限。直接执行ulimit -n 100000会失败因为修改后的数值超过了硬限制这是 Linux 防止普通用户无限消耗系统资源的一道闸门。正确的修改方式是编辑/etc/security/limits.conf给指定用户配置 nofile 的软硬限制# 编辑 /etc/security/limits.conf追加以下两行 # 第一列是用户名第二列是限制类型第三列是资源项目第四列是数值 speng soft nofile 100000 speng hard nofile 100000软限制和硬限制要一起放开只调 soft 不调 hard后续进程运行中如果尝试再次提升软限制仍然会被硬限制挡住。修改后需要重新登录会话同时要确认/etc/pam.d/login里有session required pam_limits.so这一行否则登录会话不会去读取 limits.conf 的配置。这个文件决定的是用户在登录阶段能不能获得配置的限额很多人改完 limits.conf 发现ulimit -n没变就是漏了 pam 模块。3.3 第二层限制fs.file-max 与本地端口范围文件描述符放开之后继续压测仍然远远达不到 10 万而且结果不稳定。此时用cat /proc/sys/fs/file-max查看系统级最大文件数发现数值只有一万多。这个值是根据内核参数和硬件资源计算出来的系统级硬上限是所有进程加起来的文件描述符总量不是单个用户的限制。它受内存影响同一个环境下不同机器的值可能差很多不能用机器 A 的数值去倒推机器 B 的容量。继续往上走还会遇到端口范围限制。TCP 连接的四元组是“源 IP、源端口、目标 IP、目标端口”压测客户端作为主动连接方每建立一个连接都要占用一个本地端口。端口号只有 16 位最大只能是 65535Linux 默认的本地端口范围通常是 32768 到 60999可用端口数有限连接数自然上不去。修改/etc/sysctl.conf扩大本地端口范围# 编辑 /etc/sysctl.conf追加 net.ipv4.ip_local_port_range 1024 65535 # 执行 sysctl -p 使配置生效1024 以下为特权端口一般不改动65535 是 16 位端口号的上限所以最大只能配置到这个值。修改后理论上能承担的并发连接从两三万扩展到六万多但实测仍然达不到 10 万而且结果时高时低。到这里已经能明确判断问题不在连接数量的初始配额而在连接的生命周期管理上。3.4 第三层限制TIME_WAIT 堆积与内核参数用 netstat 统计当前 TCP/IP 连接状态分布命令是netstat -an | awk /^tcp/{s[$NF]} END {for(a in s) print a, s[a]}输出里 TIME_WAIT 数量非常多。TIME_WAIT 是主动关闭连接的一方在收到对端 FIN 后进入的状态默认要等待 2 个 MSL最大报文段生存时间通常是 60 秒左右。压测场景下客户端每建一个连接就断开大量连接短时间进入 TIME_WAIT旧的连接还没释放完新的连接已经占不到端口和连接表项了。这才是连接数上不去的根本原因。在 /etc/sysctl.conf 里追加一组连接回收相关的内核参数# 开启 TIME_WAIT socket 重用允许将其用于新的 TCP 连接 net.ipv4.tcp_tw_reuse 1 # 开启 TIME_WAIT socket 快速回收内核 4.12 后已移除新环境勿用 net.ipv4.tcp_tw_recycle 0 # 缩短 TIME_WAIT 等待时间这里设置为 5 秒 net.ipv4.tcp_fin_timeout 5 # 加大 SYN 队列长度容纳更多等待连接的请求 net.ipv4.tcp_max_syn_backlog 8192这里有一个容易踩的坑原文资料里写的net.ipv4.tcp_tw_fin_timewait参数名是错的内核里对应的是net.ipv4.tcp_fin_timeout直接照抄旧参数名sysctl -p会报“没有这个键”。另一个坑是tcp_tw_recycle它在 Linux 4.12 版本已经被移除而且它在 NAT 环境下开启会导致随机丢包让部分用户连接异常。现在的新环境不要开 recycle用tcp_tw_reuse配合缩短tcp_fin_timeout就够用。当年在实习环境里开启后确实大幅回收了 TIME_WAIT但那是旧内核下的行为换成生产环境不能照搬。调优之后重新执行压测连接数从一万多稳步上升到 9 万多接近预设的 10 万目标。整个链路整理下来是这样一个递进关系限制层级关键配置生效方式常见坑文件描述符ulimit -n / limits.conf重新登录、pam_limits.so只改 soft 不改 hard或漏掉 pam 模块系统文件数fs.file-max内核自动计算系统级上限受硬件资源影响端口范围ip_local_port_rangesysctl -p16 位上限最大只能 65535连接回收tcp_tw_reuse / tcp_fin_timeoutsysctl -ptw fin 参数名易写错recycle 新内核已移除3.5 客户端压测脚本循环建连与并发控制调优过程中用到的压测脚本并不复杂核心就是循环建连加并发控制。用 Python 的 socket 和 threading 就可以完成# load_test.py import socket import threading import time target_ip 192.168.1.100 # 被测服务端地址 target_port 8080 # 被测服务端端口 target_count 100000 # 目标连接总数 success 0 # 成功连接计数 lock threading.Lock() # 线程安全计数器 def connect_once(): global success try: s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(3) s.connect((target_ip, target_port)) with lock: success 1 time.sleep(600) # 保持连接让服务端维持并发数量 except Exception as e: print(str(e)[:80]) # 只打印错误片段避免刷屏 threads [] for i in range(target_count): t threading.Thread(targetconnect_once) t.start() threads.append(t) time.sleep(0.001) # 减缓建连速率避免客户端句柄瞬间耗尽 for t in threads: t.join() print(success:, success)connect_once里每个线程建立一个 socket 连接成功后保持连接不释放这样服务端的并发连接数才会持续累计。time.sleep(0.001)控制建连速率毫秒级间隔可以让连接请求更接近生产环境的真实到达模式避免瞬间打满客户端自身资源导致误判。计数用 lock 保护因为多线程下对 success 的并发写会导致计数丢失。注意客户端自身也有文件描述符上限如果目标是 10 万连接单机客户端不一定能扛住常见做法是拆成多台压测机分布式执行或者用协程替代线程降低客户端资源占用。4. pytest 自动化测试实现fixture 前置处理与 parametrize 参数化登录用例4.1 为什么是 pytestfixture、参数化和断言机制压力测试解决的是“服务器能扛多少”的问题自动化测试解决的是“每次改版后这些功能还能不能跑通”的问题。公司自动化测试选了 pytest 框架原因集中在三点fixture 可以复用测试前置逻辑parametrize 可以天然生成用例组合断言直接使用 Python 原生 assert不需要额外学习一套断言 API。比起 unittest 的类继承结构pytest 函数式的组织方式对测试开发更友好写起来也短很多。4.2 fixture执行前的准备和执行后的清理fixture 用来处理“执行测试前需要做的事”和“执行测试后需要收尾的事”。登录测试场景里每个用例执行前需要清理掉上次登录失败留下的锁定记录用例执行后需要把环境复位避免影响下一个用例。这两件事在 pytest 里用一个 fixture 就能完成# test_login.py 中的 fixture 定义 import pytest from login import login # 被测函数实际项目中对应登录接口封装 pytest.fixture def clean_login_lock(): # 用例执行前清空登录失败锁定记录 print(clean login lock records) yield # yield 之前是前置yield 之后是清理 # 用例执行后复位环境 print(reset environment)yield 之前的代码在用例执行前运行yield 之后的代码在用例结束后运行不管用例通过还是失败清理代码都会执行。这一点比在用例里手动写 try/finally 干净很多。如果多个测试文件都需要这个 fixture就把它放到 conftest.py 里pytest 会自动发现不需要显式 import。4.3 parametrize 参数化登录用例组合与 ids 可读化登录测试的特点是同一个入口、多组输入数据。用 mark.parametrize 把用例数据抽出来一组参数就是一条用例不需要为每个输入组合写一个测试函数。参数化配合 ids 可以让每条用例在测试报告中显示为可读的名称方便定位是哪组数据挂在哪个用例上# test_login.py pytest.mark.parametrize( username, password, expected_code, [ (admin, admin123, 0), # 正确凭证预期成功 (admin, wrong, 1), # 密码错误预期拒绝 (guest, guest123, 1), # 无权限用户预期拒绝 (, , 1), # 空用户名和密码预期提示必填 ], ids[correct, wrong-password, no-permission, empty-fields] ) def test_login(clean_login_lock, username, password, expected_code): result login(username, password) assert result[code] expected_codeparametrize 第一个参数是参数字段名第二个参数是数据列表ids 负责给每个组合起名。执行时 pytest 会把 username、password、expected_code 三组值注入到测试函数里每组数据生成一条独立用例。fixture 参数放在 parametrize 参数之前pytest 会自动先执行 fixture 再执行用例。实际项目中 login 函数通常替换为 requests.post 调登录接口返回值从字典变成了接口响应但测试结构完全一致。4.4 运行与结果解读在项目目录下执行pytest test_login.py -v-v 参数输出每条用例的详细执行结果可以看到类似下面的输出test_login.py::test_login[correct] PASSED test_login.py::test_login[wrong-password] PASSED test_login.py::test_login[no-permission] PASSED test_login.py::test_login[empty-fields] PASSED方括号里的内容就是 ids 设置的名字。如果某条用例失败pytest 会把 assert 前后的实际值和预期值直接对比展示出来不用加额外的日志就能看到是哪里不匹配。这里有个细节parametrize 里如果数据量很大比如要跑几十组账号密码组合ids 不写也可以pytest 会自动用参数值做标识但参数值包含特殊字符时会很难读所以 ids 建议在用例矩阵固定后统一补充。5. 压测与回归的工程化技巧环境体检脚本、JVM 内存防 OOM 与 Jenkins 接入5.1 压测前环境体检脚本压测调优踩过的坑第二次就不该用试错方式再走一遍。把三类限制和连接状态统计写成一个体检脚本压测前先跑一遍确认每一项都处于放开状态效率会比边压测边查高很多#!/bin/bash # 压测前体检逐项打印资源限制和连接状态 echo 当前进程文件描述符上限 ulimit -n echo 系统级文件描述符上限 cat /proc/sys/fs/file-max echo 本地端口范围 cat /proc/sys/net/ipv4/ip_local_port_range echo 连接回收相关内核参数 sysctl net.ipv4.tcp_tw_reuse net.ipv4.tcp_fin_timeout net.ipv4.tcp_max_syn_backlog 2/dev/null echo 目标端口连接状态分布 netstat -an | grep :8080 | awk {print $6} | sort | uniq -c最后一行统计目标端口当前 TCP 状态分布用sort | uniq -c做分组计数可以直接看到 ESTABLISHED 和 TIME_WAIT 的数量级。判断逻辑很简单TIME_WAIT 数量持续增长说明回收参数没生效ulimit 值低于预期说明 limits.conf 或 pam 配置不对端口范围最大值小于 65535说明 sysctl.conf 没有加载成功。5.2 压测客户端的 JVM 内存配置与 OOM压测过程中服务端会不断优化但客户端自身也会成为瓶颈。如果压测工具是基于 JVM 的比如 Gatling 或自研的 Java 压测客户端高并发下最容易出现的是堆内存溢出和频繁 Full GC。每个 socket 连接在 JVM 内部对应着对象和缓冲区连接数上来之后堆内存占用会快速膨胀。开发测试阶段就合理设置 JVM 运行内存能提前避免这类 OOM 问题java -Xms2g -Xmx4g -XX:UseG1GC -XX:MaxDirectMemorySize1g -jar load-client.jar-Xms 和 -Xmx 控制堆内存的初始值和最大值压测工具建议把两者设置为相同值避免运行中动态扩容引发停顿。-XX:MaxDirectMemorySize 限制堆外直接内存NIO 场景下 socket 读写缓冲走的是直接内存这个值太小同样会抛 OOM。还要注意给操作系统预留余量堆内存加直接内存不要超过物理内存的 70%否则内存换出会直接拉高压测延迟测出来的数据失真。5.3 把 pytest 用例接进 Jenkins 做发版回归压测确认容量自动化测试守住功能两者最终要在持续集成里汇合。Jenkins 的构建任务里加一个执行 shell 的构建步骤跑pytest test_login.py -v --junitxmlreport.xmlpytest 会把结果输出成 JUnit 格式的 XMLJenkins 再配一个 JUnit 插件把 XML 解析成趋势图每次发版前自动跑一遍登录用例。用例少的时候这个流程看不出优势但用例集到几百条之后手动回归一次要一两个小时Jenkins 自动跑只需要几分钟而且每次代码提交都能触发不用等人想起来才执行。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门