
先交代一个场景你搭了一套带登录态的Jmeter压测脚本第一个线程组专门跑登录第二个线程组跑业务接口。单跑登录线程组全部成功单跑业务线程组各种报错把两个线程组一起跑登录是绿的业务请求却成片地返回401或者302跳登录页。这种“登录成功但业务拿不到身份”的经典问题十有八九就是卡在Cookie没跨线程组传过去。网上搜“Jmeter 多线程组共享Cookie”能翻出好几页帖子但大部分只给你一段BeanShell代码不解释为什么抄完还跑不通。这篇我把原理、脚本、踩坑和排查链路一次性说清楚覆盖单用户共享和多用户各自传Cookie两种场景照着改就能用。1. 为什么两个线程组之间登录Cookie就是传不过去1.1 先看一个最常见的翻车现场典型的脚本结构是这样的线程组A登录里放一个HTTP登录请求线程组B业务里放业务接口请求。两个线程组下面都加了HTTP Cookie管理器。跑起来之后A组登录全部成功B组业务请求全部401。很多人第一反应是“那我把Cookie管理器拖到测试计划根节点下让两个线程组共用不就行了”实际上没用。JMeter的Cookie管理器虽然是配置元件但它收集到的Cookie是存到当前线程的变量空间里的不是存到元件本身。就算你把这个管理器放到测试计划层每个线程仍然各自维护一份Cookie数据A组线程保存的Cookie不会凭空跑到B组线程里。还有个更隐蔽的误解有些人以为在A组登录接口上加了“正则表达式提取器”提取Cookie再在B组请求里用${loginCookie}引用就能拿到值。这同样行不通。JMeter变量vars的作用域只限定在当前线程内线程A的第1号线程里存的变量线程B的第1号线程根本看不见。这一点不搞清楚后面所有方案都会跑偏。1.2 问题本质线程组变量隔离与Cookie的默认管理方式JMeter里有两套“键值对”容器搞混了就会踩坑一套是JMeter变量vars对应你平时用的${变量名}。它每个线程独占一份线程内可见线程间完全隔离。所以你在A组用“用户自定义变量”或“正则提取器”存了东西B组默认读不到。另一套是JMeter属性props对应${__P(属性名,)}这种引用方式。它是整个JMeter进程级别的全局键值存储所有线程组、所有线程都能读写正好可以用来跨线程组搬运数据。Cookie本身是HTTP协议里的东西服务端通过Set-Cookie响应头把会话标识种到客户端客户端后续请求在请求头里加一个Cookie字段带回去。Jmeter不是浏览器不会自动维护“登录后带上Cookie”这个闭环它需要你显式配置Cookie管理器或者手动在请求头里塞Cookie。Cookie管理器默认在每个线程内独立工作第一个请求收到了Set-Cookie存到当前线程的Cookie存储区当前线程后续请求会自动带上。但它管不到别的线程更管不到别的线程组。这就是为什么“把登录和业务放两个线程组”之后业务线程组拿不到登录态。1.3 动手之前先判断你是“单用户共享”还是“多用户各自传递”网上很多教程只讲一个场景所有线程共用同一个账号、同一个Cookie。这种最简单登录一次全局共用。但很多压测场景是每个线程模拟一个用户A组用不同的账号并发登录B组每个线程要带着自己的Cookie去压业务接口。这两种场景的脚本写法差别很大不能混用。如果是单用户共享核心思路就是把登录线程组拿到的Cookie字符串存进props业务线程组从props里读出来塞进请求头。如果是多用户各自传递核心思路是按线程编号分别存储比如cookie_1、cookie_2业务线程组的第N号线程只读cookie_N。判断清楚再往下看别拿多用户方案去套单用户场景也别拿共享方案去做多用户压测。2. 最稳妥的跨线程组传递props全局属性 JSR223脚本2.1 为什么是props而不是vars或者BeanShell先说结论跨线程组传数据只用props最稳妥。vars.put()是每个线程自己的小本子写完别的线程看不到。props.put()是挂在JMeter进程上的公告栏谁都能看。JMeter里Beanshell、JSR223脚本可以直接操作props对象不需要额外声明。为什么不推荐用BeanshellJMeter老版本只有Beanshell可用但它每次运行都要启动脚本解释器高并发下性能很差。从JMeter 3.1开始JSR223配Groovy是官方推荐方案脚本引擎复用、执行效率高。你如果拿着一个老帖子的BeanShell代码能用就先别动但新写的脚本我建议直接用JSR223 Groovy别给自己埋性能雷。另外还有一种做法是用${__setProperty(login_cookie, ${loginCookie},)}函数直接设置属性。它能用但有两个问题一是这个函数写在请求参数里会返回被设置的值容易污染请求参数二是它只能处理“单值字符串”遇到多个Cookie要拼接、要判断、要清洗的时候函数表达式写起来非常痛苦。脚本方式可控性更好。2.2 登录线程组把Set-Cookie完整提取出来给登录请求添加一个“正则表达式提取器”配置如下字段响应头Response Headers引用名称loginCookie正则表达式(?i)set-cookie: ([^;])模板$1$匹配数字1缺省值NOT_FOUND这个正则的作用是在响应头里找到Set-Cookie:这一行把第一个分号之前的内容截出来。比如响应头里有Set-Cookie: JSESSIONID8F2A...; Path/; HttpOnly提取到的就是JSESSIONID8F2A...。注意(?i)表示忽略大小写因为HTTP头字段名不区分大小写有的服务端返回set-cookie全小写有的返回Set-Cookie不忽略大小写就提取不到。如果服务端一次回了多个Set-Cookie比如同时种了JSESSIONID和AUTH_TOKEN上面的正则只会提取第一个。这时候改用JSR223后置处理器更省事直接在脚本里循环匹配所有CookieString headers prev.getResponseHeaders() def matcher (headers ~ /(?i)set-cookie:\s*([^;][^;]*)/) def cookies [] while (matcher.find()) { cookies matcher.group(1) } props.put(login_cookie, cookies.join(; ))这段脚本放在“JSR223后置处理器”里运行完会把所有Cookie拼成一个用分号分隔的字符串存进login_cookie属性。登录接口一次性种多个Cookie的场景下这个写法比正则提取器靠谱得多。然后在登录请求下加一个“JSR223后置处理器”把提取到的值写入propsif (!NOT_FOUND.equals(vars.get(loginCookie))) { props.put(login_cookie, vars.get(loginCookie)) log.info(cookie saved: props.get(login_cookie)) }log.info这行不是必须的但建议保留。排查问题的时候它能在JMeter日志里告诉你到底存了什么东西。2.3 业务线程组用__P函数把Cookie注入请求头在业务线程组里给需要携带Cookie的HTTP请求添加一个“HTTP头管理器”添加一行名称Cookie值${__P(login_cookie,)}__P函数是从JMeter属性里取值。第二个参数是默认值留空表示取不到就返回空字符串这样属性还没写入时请求头不会出现奇怪的内容。用这个方案所有业务线程组的所有线程在每次请求时都会带上同一个Cookie头。只要服务器认这份登录态业务请求就不会再401。这里要特别提醒**在这个方案里业务线程组不需要再额外放HTTP Cookie管理器。**Cookie管理器的自动管理逻辑可能会和你手动塞的Cookie头混在一起反而搞出问题。手动塞了Cookie头就让浏览器式的自动管理靠边站。2.4 如果登录接口返回的是一段JSON而不是Set-Cookie很多系统现在不走Set-Cookie登录接口直接返回JSON里面带一个token字段前端再手动把token放进后续请求。这种情况原理一样只是提取方式从正则提取器换成JSON提取器。登录请求上加“JSON提取器”变量名称tokenJSONPath表达式$.data.token缺省值NOT_FOUND然后在“JSR223后置处理器”里拼Cookieif (!NOT_FOUND.equals(vars.get(token))) { props.put(login_cookie, token vars.get(token)) }后面业务线程组的引用方式完全一样${__P(login_cookie,)}。这套“提取-存props-注入”的思路换成任何字段都成立不只是Cookie。3. setUp线程组集中登录更优雅、更省事的替代做法3.1 setUp线程组的执行时机和适用边界如果你整个测试计划只要一份登录态不涉及多账号那可以不写单独的登录线程组改用“setUp线程组”。setUp线程组有个天然优势它在所有普通线程组执行之前启动。你不需要关心两个线程组谁先谁后、要不要勾选“独立运行每个线程组”setUp先跑完普通线程组才开始。等于把“先登录、再压业务”这个顺序从机制上固定下来。在setUp线程组里放登录请求按第2章的方式提取Cookie存入props然后在普通线程组里用${__P(login_cookie,)}引用。逻辑完全一样但脚本结构比“两个普通线程组”更清晰谁负责登录、谁负责业务一眼就懂。适用边界也要说明setUp线程组只在整个测试开始时执行一次。如果你的压测要跑很久服务端会话中途过期了setUp里拿到的Cookie早就失效了后面的请求会全部报错。长压测场景下要么把Cookie的有效期拉长要么在业务线程组里做“响应检测到401就重新登录”的逻辑这两个都超出本文范围但你要知道setUp方案有这个先天限制。3.2 两种注入方式怎么选Cookie管理器还是Header管理器有了全局Cookie之后业务线程组往请求里塞Cookie有两条路。一条是手动Header管理器值写${__P(login_cookie,)}。优点是简单直接、完全可控缺点是不会按域名区分所有请求都会带上这份Cookie。如果业务线程组里的接口都是同一个域名完全没问题如果混了别的域名的请求多余Cookie会被一起发送。另一条是HTTP Cookie管理器在“用户定义的Cookie”里填名称: Cookie值: ${__P(login_cookie,)}。它按域名规则发送理论上更“标准”但实际使用中容易踩坑Cookie管理器还有“每次迭代清除Cookie”之类的选项一个不小心就把你的配置清了或者合并出意外结果。我的建议很直接**用Header管理器。**压测脚本要的是确定性和可排查性手动塞Cookie头出问题一眼就能在“查看结果树”里看到真实请求头发了什么。Cookie管理器那一堆选项处理浏览器场景还行压测场景里徒增变数。3.3 那些把Cookie“弄丢”的隐藏开关有一种情况特别坑脚本结构完全正确Cookie也存进props了业务请求却时而正常时而不正常。最后查到原因是测试计划根节点上那个“独立运行每个线程组”的选项被取消了。JMeter新版本支持线程组并行执行。如果取消勾选“Run Thread Groups consecutively (i.e. one at a time)”setUp线程组虽然先启动但普通线程组可能等不及它跑完就开始发请求了。结果就是部分线程读props时Cookie还没写进去拿到空值。另外一个隐藏开关在HTTP Cookie管理器里“Clear cookies each iteration?”。如果业务线程组里放了Cookie管理器且勾了这个选项每次迭代都会清空它自己维护的Cookie存储。如果你同时用了Manual Header和Cookie管理器这时候Cookie管理器清不清空不影响手动Header但如果你把Cookie写在Cookie管理器的“用户定义的Cookie”里这个选项的行为在不同版本里有差异很容易翻车。所以排查Cookie问题时第一件事不是怀疑脚本而是先确认测试计划是不是顺序执行、业务线程组里有没有多余的Cookie管理器。4. 多用户压测多个Cookie按线程编号一一对应地传递4.1 先想清楚所有用户共用一个Cookie有意义吗单用户共享方案虽然简单但压测结果经常被质疑所有线程顶着同一个用户身份打业务接口后端缓存、限流、锁粒度全都不一样测出来的数据根本不代表真实场景。多用户压测的正确做法是登录线程组用CSV或函数生成不同账号每个线程登录自己的账号拿到各自的Cookie业务线程组里第N号线程只用第N号账号的Cookie。但这里有个执行顺序前提登录线程组和业务线程组如果同时跑业务线程组发起请求时对应账号的Cookie可能还没生成。所以要么保证两个线程组顺序执行要么用setUp线程组专门做多账号登录。4.2 按线程编号存取Cookie的完整做法登录线程组假设线程数为5里登录请求提取Cookie之后用JSR223后置处理器按线程编号存入propsint idx ctx.getThreadNum() 1 props.put(cookie_ idx, vars.get(loginCookie)) log.info(saved cookie_ idx props.get(cookie_ idx))ctx.getThreadNum()返回当前线程在线程组内的编号从0开始。加1存成cookie_1到cookie_5方便和JMeter的__threadNum函数对齐。业务线程组线程数同样为5里HTTP头管理器的值写成${__P(cookie_${__threadNum},)}${__threadNum}返回当前线程的编号从1开始。JMeter支持函数嵌套内层的__threadNum会先被解析成数字然后外层的__P去读对应属性。比如业务线程组的第3号线程读到的就是cookie_3。这个方案的好处是业务线程组完全不需要写脚本只靠一个函数表达式就能按线程号精准取Cookie。4.3 线程数不一致、执行顺序错位、覆盖写入这三个坑第一个坑两个线程组线程数不一致。登录线程组5个业务线程组10个那第6到第10号业务线程读不到cookie_6请求直接失败。要么保证线程数一致要么在业务线程组里加一个JSR223预处理程序读不到对应Cookie时取一个兜底值或者直接抛错方便快速发现问题。第二个坑线程组执行顺序错位。如果测试计划是并行执行登录线程组还没跑完业务线程组就开始跑前几个线程只能拿到空Cookie。解决办法是勾选“独立运行每个线程组”或者用setUp线程组承载登录逻辑。第三个坑多线程并发写同一个props键。如果你图省事让登录线程组的所有线程都写login_cookie这个key后登录的会覆盖先登录的最后业务线程组所有线程都拿到同一个“最后登录用户”的Cookie等于又退回单用户方案了。更隐蔽的是如果登录接口响应有快有慢谁先写谁后写完全不可控你甚至无法预测最后留下来的是哪个用户的身份。所以多用户场景一定要按线程编号分开存。5. 排查实录Cookie明明存进去了业务请求还是4015.1 第一板斧先确定服务器真的种了Cookie很多人在“查看结果树”里看到登录请求响应体是成功JSON就以为登录没问题。但Cookie是放在响应头里的不在响应体里。你要点开“查看结果树”的“响应头”标签确认真的有Set-Cookie字段。如果响应头里没有Set-Cookie说明登录态根本不是通过Cookie下发的。这时候去看响应体里有没有token字段有token就走JSON提取器第2.4节的方案。什么都没有那说明登录本身就是失败的先排查登录请求的参数和鉴权逻辑。如果响应头里有Set-Cookie但值特别长注意检查是不是带了expires、path、HttpOnly这些属性。你提取正则只截第一个分号之前的部分是正确的要的就是KEYVALUE这一段后面那些属性字段不要带上。5.2 第二板斧查看业务请求实际发送的请求头这一步最关键。在“查看结果树”里点开业务请求的“请求头”标签看有没有Cookie这一行值是什么。如果根本没有Cookie这一行说明Header管理器的引用没生效。检查一下值是不是写成了${__P(login_cookie,)}注意函数名的大小写__P是大写P。JMeter函数解析失败时会原样输出字符串请求头里会看到一串${__P(login_cookie,)}那就说明函数写错了或者JMeter版本不支持。如果Cookie头存在但值为空说明读取的时候login_cookie属性还没写入。去日志里查之前加的log.info输出确认登录线程组到底有没有执行完。再回去检查测试计划里线程组的执行顺序。如果Cookie头有值但业务请求还是401那就进入第三板斧。5.3 第三板斧并发覆盖、特殊字符、过期Cookie的隐蔽原因并行执行导致读了别人的Cookie是很常见的。现象是业务请求里面返回的身份时对时错错误集中在某些固定线程号上。解决办法就是第4章的按线程编号存储方案别让所有线程抢同一个key。特殊字符和编码问题也很坑。有些服务端返回的Cookie值里带中文或URL编码字符存进props再通过__P函数取出来在个别JMeter版本里可能出现截断或转义问题。排查方法是在业务请求上临时加一个JSR223预处理程序把实际取到的值打印到日志log.info(final cookie [ props.get(login_cookie) ])如果打印出来的值和登录响应头的原始值对不上多半是编码问题。可以把Cookie值在保存时做一次URL编码或者在读取后做解码按你服务端的实际规则来。Cookie过期这个坑常见于长时间压测。测试跑了十几分钟Cookie在第三分钟就过期了后面全是401。页面看起来就是“一开始正常某段时间后突然全挂”。这时候先看服务端会话超时配置再看脚本里有没有定时器导致请求间隔过长。如果必须长跑就得做“401时自动重新登录”的循环逻辑。5.4 一个完整排查案例从响应头到请求头的链路有次帮人排查一个脚本现象是两个线程组登录成功业务请求全部401。第一板斧看响应头Set-Cookie存在但有两个一个是JSESSIONID一个是AUTH_TOKEN。他只用正则提取器取了第一个JSESSIONID业务请求原样带上了但系统校验身份用的是AUTH_TOKEN所以一直401。处理方法很简单改成JSR223后置处理器循环匹配所有Set-Cookie把JSESSIONIDxxx; AUTH_TOKENyyy拼好存入props再让业务Header管理器引用。改完立刻通了。这个案例想说明一件事Cookie跨线程组传递的链路其实不长——服务端下发、脚本提取、存入props、Header引用。任何一环出问题都按这个顺序查比瞎猜快得多。如果你刚开始改造这类脚本我建议不要一上来就写完整的多用户方案。先在登录线程组加一个JSR223后置处理器打印提取结果再在业务线程组加一个调试用的JSR223预处理程序打印读取结果把两端都确认了再往正式逻辑上靠。我自己调试跨线程组Cookie时这个“打印两端”的习惯帮我省了大量时间。最后再分享一个小技巧把存Cookie的key固定命名为login_cookie、cookie_1这类见名知意的格式不要用cc、ck这种缩写脚本文件过两周再看你还是能一眼看懂它在干什么。