1. 为什么微服务架构下传统 Session 鉴权会“突然失灵”去年在参与某高校实验室的模拟项目X——一个面向教学资源调度的跨平台系统时我第一次被“用户登录后点开另一个服务就提示未登录”这个问题卡了整整三天。当时整个系统已拆分为课程管理、作业分发、成绩录入、通知推送四个独立服务前端用 Vue 做聚合页后端全部 Spring Boot 微服务数据库各自隔离。我们沿用了单体时代最熟悉的方案Spring Security HttpSession。登录成功后把用户信息塞进 session后续请求靠 Cookie 携带 JSESSIONID 来识别身份。结果一上微服务就崩了。不是偶尔失效而是每次跳转到新服务必然 401。排查过程像在迷宫里摸墙先查 Nginx 转发是否丢了 Cookie —— 没丢再看各服务 session 是否共享 —— 试过 Redis 共享 session但发现通知服务压根不走登录流程它只接收内部事件却也要校验权限最后发现更致命的问题前端调用成绩录入服务时用的是 axios 的默认配置而课程管理服务用的是 fetch两者对 Cookie 的携带策略默认就不一致。更讽刺的是当我们在网关层加了负载均衡用户请求被随机打到不同实例Redis 共享 session 看似解决了但通知服务因无登录态每次调用都触发鉴权拦截器抛异常日志里全是No authentication header found。这根本不是 Bug是架构范式切换时的认知断层。单体应用里Session 是“本地内存里的熟人”所有模块共享同一块内存地址微服务里Session 变成了“寄往不同城市的挂号信”每封信要单独封装、单独投递、单独签收而邮局网关和收件人各服务之间没有统一的邮编规则。JWT 就是为解决这个“邮编混乱”而生的标准化信封它把用户身份、权限、有效期等关键信息全部加密打包成一段 Base64 字符串附在 HTTP Header 的 Authorization 字段里随请求一起流转。收件人服务不用查数据库、不依赖外部存储自己解密就能验证真伪、提取权限。网关作为第一道门只负责拆信封、验印章、拒收假信然后把干净的请求放行下游服务拿到请求连印章都不用再验——因为网关已经盖过“已验真”章它只需专注读信内容即解析 JWT payload 中的 roles 字段做细粒度控制。所以“Gateway JWT”不是两个技术名词的简单拼接而是一套职责分离的协作协议网关做“身份初筛”服务做“权限精判”。前者解决“你是谁”后者解决“你能做什么”。这种分工让每个服务彻底摆脱了对 session 存储的依赖也消除了跨服务传递认证状态的耦合。我后来在模拟项目X的重构中把原来散落在四个服务里的鉴权逻辑全部抽离网关统一校验 JWT 签名与过期时间服务层只保留PreAuthorize(hasRole(TEACHER))这类声明式注解。上线后服务启停、扩容缩容再也不影响用户登录态运维同学说“终于不用半夜起来手动刷 Redis 了。”提示JWT 不是银弹。它的 payload 是 Base64 编码而非加密敏感信息如手机号、身份证号绝不能放进 token。我见过有团队把用户完整档案塞进 JWT结果前端 localStorage 里明文躺着所有隐私数据这是典型的“把保险箱当透明玻璃柜用”。2. Gateway 层 JWT 校验的三个硬性门槛与绕不开的细节很多教程一上来就贴JwtAuthenticationFilter代码仿佛只要把过滤器往网关里一塞鉴权就自动生效。我在某公司带新人做模拟项目X时发现超过七成的失败案例根源不在代码而在三个被严重低估的前置条件没达标。它们像三把锁缺一把JWT 校验这扇门就永远打不开。2.1 第一把锁密钥管理必须脱离代码硬编码JWT 的签名算法如 HS256、RS256本质是“数字印章”而密钥就是刻章的模具。如果模具直接写死在 Java 代码里比如String secret my_super_secret_key_123;那等于把银行金库的钥匙焊死在大门上。任何能反编译 jar 包的人都能拿到这把钥匙伪造任意用户的 token。更危险的是当密钥需要轮换安全规范要求定期更换你得重新编译、打包、发布整个网关服务停机时间无法避免。正确做法是密钥外置化。我们采用 Spring Cloud Config Vault 的组合Vault 作为密钥中心存储加密后的密钥值Config Server 从 Vault 拉取密钥注入到网关的配置中心网关启动时通过Value(${jwt.secret})动态加载。这样密钥变更只需在 Vault 后台操作Config Server 推送新配置网关通过 Actuator 的/actuator/refresh端点热刷新即可生效全程零停机。实测下来一次密钥轮换从原来的 20 分钟缩短到 47 秒。关键参数如下表参数项推荐值说明密钥长度HS256≥32 字节RFC 7518 建议最小长度短于 32 字节易受暴力破解签名算法RS256非对称更安全私钥仅网关持有公钥可分发给所有服务用于验签密钥存储位置Vault / AWS KMS / Azure Key Vault绝对禁止存于 Git 仓库、配置文件、环境变量注意如果你的团队暂无 Vault退而求其次的方案是使用 Kubernetes Secret 挂载密钥文件。我试过把密钥存在 configmap 里结果被安全审计直接打回——configmap 是明文存储等同于把钥匙贴在玻璃窗上。2.2 第二把锁Token 解析必须覆盖所有合法入口路径网关是流量入口但“入口”不止一个。新手常犯的错误是只拦截/api/**却忘了静态资源、健康检查、Swagger 文档这些路径也需要鉴权放行或特殊处理。比如模拟项目X的前端页面/index.html本身不需要鉴权但页面加载后发起的/api/course/list请求必须校验。如果网关过滤器配置为antMatchers(/api/**).authenticated()那/index.html会直接 401用户连登录页都打不开。我们最终采用三级路径白名单机制L1 免鉴权路径/actuator/**,/swagger-ui/**,/webjars/**,/login,/register—— 这些是网关自身功能或用户注册入口完全放行L2 公共接口路径/api/public/**—— 如获取验证码、查询公开课列表无需登录即可访问L3 鉴权路径/api/**除 L1、L2 外的所有/api/开头路径—— 必须携带有效 JWT。这个逻辑不能靠 if-else 硬写而是用 Spring Security 的RequestMatcher链式构建。核心代码片段如下以 Spring Cloud Gateway 为例Bean public SecurityWebFilterChain webFilterChain(ServerHttpSecurity http) { return http .authorizeExchange(exchange - exchange // L1免鉴权 .pathMatchers(/actuator/**, /swagger-ui/**, /login, /register).permitAll() // L2公共接口 .pathMatchers(/api/public/**).permitAll() // L3默认所有 /api/ 下路径需鉴权 .pathMatchers(/api/**).authenticated() .anyExchange().denyAll() // 兜底其他路径一律拒绝 ) .addFilterAt(jwtAuthenticationFilter(), SecurityWebFiltersOrder.AUTHENTICATION) .build(); }这段代码的关键在于anyExchange().denyAll()的兜底策略。它像一道防火墙确保任何未被明确放行的路径都不会意外透传。我曾在线上环境见过因漏配/.well-known/jwks.json用于 RS256 公钥发现导致所有服务验签失败的事故就是少了这道兜底。2.3 第三把锁Header 传递必须穿透所有中间件JWT 通常放在Authorization: Bearer token头里。网关校验完必须把这个头原样传递给下游服务。看似简单但实际部署中Nginx、Kubernetes Ingress、甚至某些 CDN 都可能“吃掉”或“改写”这个头。最典型的是 Nginx 默认不转发带下划线的 Header而有些旧版客户端会把 token 放在X-Auth-Token这样的头里。我们的解决方案是双保险网关层显式透传在 Spring Cloud Gateway 的路由配置中强制设置filtersspring: cloud: gateway: routes: - id: course-service uri: lb://course-service predicates: - Path/api/course/** filters: - StripPrefix1 # 关键确保 Authorization 头必传 - SetRequestHeaderAuthorization, #{request.headers[Authorization]}基础设施层全局配置在 Kubernetes Ingress 的 annotation 中添加nginx.ingress.kubernetes.io/configuration-snippet: | proxy_set_header Authorization $http_authorization; proxy_pass_request_headers on;这两步做完我们用curl -H Authorization: Bearer xxx直接调用网关再在下游服务日志里打印request.getHeader(Authorization)确认值完全一致。这是上线前必做的“连通性测试”比任何单元测试都管用。3. JWT Payload 设计别把权限字段当成“万能收纳盒”JWT 的 payload载荷是 JSON 对象里面可以塞任何字段。很多团队一上来就往里堆userId,username,email,avatarUrl,departmentId,roleId,permissions……恨不得把用户表所有字段都复制一份进去。结果呢token 体积暴涨一个普通用户 token 从 300 字节膨胀到 1.2KB。当这个 token 被前端存在 localStorage 里每次请求都得完整传输HTTP Header 体积过大还可能触发某些代理服务器的截断如 Nginx 默认large_client_header_buffers是 4KB超了就 400 错误。我在模拟项目X初期也犯过这个错。当时把教师的全部课程权限列表含课程名、学期、班级号全塞进permissions数组一个资深教师的 token 达到 1.8KB。压力测试时发现当并发请求超过 200 QPS网关 CPU 使用率飙升至 95%排查发现是 JWT 解析耗时激增——不是验签慢是 JSON 解析大字符串太费 CPU。于是我们做了三重瘦身3.1 权限表达用“角色资源动作”替代“全量权限列表”原始设计permissions: [ {courseId: C001, courseName: 高等数学, semester: 2023-2, class: A班, actions: [view, edit, delete]}, {courseId: C002, courseName: 线性代数, semester: 2023-2, class: B班, actions: [view, edit]} ]优化后roles: [TEACHER], resource_actions: { course:C001: [view, edit, delete], course:C002: [view, edit] }关键变化roles字段只存角色标识字符串数组由网关和各服务共享一套角色定义字典resource_actions是对象key 是资源 ID如course:C001value 是该资源允许的动作数组所有课程名称、学期、班级号等展示信息下游服务需要时自行查数据库获取绝不塞进 token。这样一个教师的 token 体积从 1.8KB 降到 420 字节CPU 解析耗时下降 76%。3.2 敏感信息绝对禁止放入 payload再次强调JWT payload 是 Base64 编码不是加密任何能拿到 token 的人都能用在线工具如 jwt.io秒解出全部内容。所以以下信息严禁放入手机号、身份证号、邮箱除非是公开邮箱且业务允许密码哈希值哪怕加盐了也不行会话密钥、临时令牌用户住址、银行卡号等 PII个人身份信息我们曾有个实习生把用户加密后的手机号AES 加密塞进 token自以为很安全。结果被安全组指出AES 密钥如果也存在服务端攻击者一旦拿下服务就能解密所有手机号。正确做法是手机号这类信息服务端需要时用用户 ID 去数据库查查完立刻丢弃绝不缓存、不透传。3.3 过期与刷新用双 Token 机制解决体验与安全的矛盾JWT 的exp过期时间设太短如 15 分钟用户频繁被踢出设太长如 7 天一旦 token 泄露攻击者有长达 7 天的作恶窗口。标准解法是 Access Token Refresh Token 双机制。Access Token短时效15-30 分钟存于前端内存如 Vue 的 pinia store不持久化用完即焚Refresh Token长时效7-30 天存于 HttpOnly Cookie防 XSS 窃取且绑定设备指纹如 User-Agent IP 前缀网关校验 Access Token 过期时不直接返回 401而是检查请求头是否携带有效的 Refresh Token。若有效则生成新的 Access Token 返回若无效或缺失则返回 401 强制重新登录。这个逻辑必须由网关统一实现不能让每个服务自己搞。我们把 Refresh Token 的校验与发放逻辑封装成一个独立的RefreshTokenService网关调用它服务层完全无感。实测下来用户无感知续期成功率 99.2%主动登出率下降 40%。提示Refresh Token 的存储必须 HttpOnly Secure SameSiteStrict。我见过有团队把它存 localStorage结果 XSS 攻击脚本一行document.cookie就全拿走了。4. 服务层权限精控从“能进门”到“能动哪把锁”网关的 JWT 校验只回答了一个问题“这个人有没有资格进这栋楼”但它不管这个人进了楼之后能不能进财务室、能不能碰服务器机柜、能不能删数据库。这才是服务层真正的价值基于 JWT payload 中的结构化权限数据做细粒度的访问控制RBAC 或 ABAC。4.1 声明式注解用PreAuthorize实现零侵入权限判断Spring Security 的PreAuthorize是最优雅的方案。它让你在 Controller 方法上用 SpEL 表达式直接写权限逻辑完全不污染业务代码。比如课程管理服务的删除接口DeleteMapping(/courses/{id}) PreAuthorize(permissionService.canDeleteCourse(#id, principal)) public ResponseEntityVoid deleteCourse(PathVariable String id, Authentication principal) { courseService.delete(id); return ResponseEntity.noContent().build(); }这里principal是 Spring Security 封装的认证对象它内部已解析好 JWT 的 payload。permissionService.canDeleteCourse()是我们自定义的服务方法它从principal中提取resource_actions检查course:id是否包含delete动作。整个过程对 Controller 来说是透明的——它只管删权限校验由 AOP 在方法执行前自动完成。这种写法的好处是权限逻辑集中、可测试、可复用。我们把PermissionService单独抽成模块所有服务都引用它保证权限规则全局一致。当某天教务处要求“只有开课院系的负责人能删除课程”我们只需修改canDeleteCourse方法的判断逻辑所有服务立即生效不用改一行 Controller 代码。4.2 方法级动态权限当角色不够用时用属性驱动决策纯角色ROLE_TEACHER有时太粗。比如“教师”角色下张老师只能删自己教的课李老师能删全院的课。这时就需要 ABAC基于属性的访问控制。JWT payload 里可以加入自定义属性attributes: { department: math, is_head: true, teaching_courses: [C001, C002] }然后在PreAuthorize里用 SpEL 访问PreAuthorize(#id in principal.attributes[teaching_courses] or principal.attributes[is_head]) public void updateCourse(PathVariable String id) { ... }这个表达式的意思是“当前课程 ID 必须在老师所教课程列表里或者该老师是院系负责人”。SpEL 引擎会自动从principal中提取attributes并计算布尔值。我们实测过这种动态判断的性能损耗几乎为零平均 0.3ms远低于一次数据库查询平均 12ms。4.3 数据级权限用 MyBatis 拦截器自动注入租户/数据范围最狠的权限控制是让 SQL 查询自动带上数据过滤条件。比如成绩录入服务教师只能看到自己班级的成绩。传统做法是在每个 DAO 方法里手动加WHERE class_id ?极易遗漏。我们用 MyBatis 的Executor拦截器在 SQL 执行前自动注入Component public class DataScopeInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { Object[] args invocation.getArgs(); MappedStatement ms (MappedStatement) args[0]; Object parameter args[1]; // 从 ThreadLocal 获取当前用户权限上下文 PermissionContext context PermissionContextHolder.getContext(); if (context ! null context.isClassRestricted()) { BoundSql boundSql ms.getBoundSql(parameter); String sql boundSql.getSql(); // 自动在 SELECT 语句末尾追加 WHERE 条件 if (sql.trim().toLowerCase().startsWith(select)) { sql sql AND class_id IN ( String.join(,, context.getClassIds()) ); BoundSql newBoundSql new BoundSql(ms.getConfiguration(), sql, boundSql.getParameterMappings(), boundSql.getParameterObject()); MappedStatement newMs copyFromMappedStatement(ms, new BoundSqlSupplier(newBoundSql)); args[0] newMs; } } return invocation.proceed(); } }这个拦截器像一个隐形的 SQL 编辑器所有查询都会被它“悄悄加料”。教师 A 查询成绩SQL 自动变成SELECT * FROM score WHERE class_id IN (101,102)教师 B 查询自动变成SELECT * FROM score WHERE class_id IN (201,202)。业务代码里完全看不到class_id权限控制下沉到数据访问层安全水位拉到最高。注意这种拦截器必须配合严格的PermissionContext管理。我们用ThreadLocalPermissionContext存储上下文并在网关解析 JWT 后通过RequestContextHolder注入到每个请求线程。每次请求结束必须remove()否则线程复用时会污染下一个请求。5. 真实踩坑记录那些文档里不会写的“血泪教训”理论讲完现在来点硬货。以下是我在模拟项目X和某公司实际项目中亲手踩过、反复验证过的五个致命坑。它们不涉及高深算法但每一个都足以让线上服务瘫痪数小时。5.1 坑一时钟漂移导致 token 频繁过期JWT 的nbfNot Before和expExpiration Time都是 Unix 时间戳秒级。如果网关服务器和下游服务服务器的系统时间不一致就会出问题。我们曾遇到网关时间比服务快 3 分钟结果网关签发的 token下游服务一收到就发现nbf还没到直接拒绝。日志里全是JWT must not be used before。排查链路第一步在网关和所有服务节点执行date -u对比输出第二步用ntpq -p检查 NTP 服务是否同步第三步发现某台服务节点的 NTP 服务异常ntpq -p显示*号消失说明未连接到时间源。修复方案所有节点强制启用 NTP并配置内网时间源如server 192.168.1.10 iburst在网关 JWT 解析时增加leeway宽容时间Spring Security JWT 库支持setAllowedClockSkewSeconds(60)允许最多 60 秒的时钟偏差最终我们把所有节点的时钟偏差控制在 ±500ms 内leeway设为 2 秒彻底解决。5.2 坑二Base64 URL 安全编码的“”号丢失JWT 的三段header.payload.signature都是 Base64Url 编码它和标准 Base64 的区别在于用-替代用_替代/并省略末尾的填充符。很多前端库尤其是老版本 axios在拼接 Authorization 头时会把当作非法字符自动截断。结果 token 变成eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.xxxxxx少两个网关解析时报Invalid JWT signature。定位方法在网关JwtAuthenticationFilter的doFilter方法开头加一行日志log.info(Raw token: {}, token);把日志里的 token 复制到 jwt.io 解析如果报Invalid signature但手动补上后能正常解析就是此问题。终极解法前端统一使用js-cookiejwt-decode库它们默认处理 Base64Url网关层加一层“容错解析”捕获SignatureException后尝试在 token 末尾补直到长度能被 4 整除再重试解析。代码片段private String fixJwtPadding(String token) { int padLength (4 - token.length() % 4) % 4; StringBuilder sb new StringBuilder(token); for (int i 0; i padLength; i) { sb.append(); } return sb.toString(); }5.3 坑三跨域请求时Authorization 头被浏览器静默丢弃前端用 Vue 开发调用网关 API 时Chrome 控制台 Network 面板显示请求头里有Authorization: Bearer xxx但网关日志里request.getHeader(Authorization)却是 null。抓包发现浏览器发出了两个请求第一个是OPTIONS预检请求第二个才是真正的POST。而OPTIONS请求里没有Authorization头网关对OPTIONS也做了鉴权拦截直接返回 401导致浏览器终止后续请求。根因浏览器跨域预检CORS Preflight时Authorization是“非简单请求头”必须在服务端显式声明允许。修复步骤网关的 CORS 配置中allowedHeaders必须包含AuthorizationSpring Cloud Gateway 配置示例spring: cloud: gateway: globalcors: cors-configurations: [/**]: allowed-origins: https://your-frontend.com allowed-methods: GET, POST, PUT, DELETE, OPTIONS allowed-headers: Authorization, Content-Type, X-Requested-With expose-headers: X-Total-Count, X-Content-Range allow-credentials: true关键是allowed-headers里必须有Authorization且allow-credentials: true否则带 cookie 的请求会被拒。5.4 坑四JWT 黑名单失效Redis 过期时间与 token exp 不同步为了支持用户主动登出使 token 失效我们用 Redis 存黑名单key 是blacklist:{token-jti}value 是空字符串TTL 设为 token 的剩余过期时间。但上线后发现用户登出后token 还能用 5 分钟。查 Rediskey 确实存在但 TTL 显示-1永不过期。原因我们用redisTemplate.opsForValue().set(key, , Duration.ofSeconds(expireSeconds))设置但expireSeconds是 token 的总过期时间如 1800 秒而用户登出时token 已用了 1700 秒剩余只有 100 秒。我们却按 1800 秒设 TTL导致 key 实际过期时间比 token 晚 1700 秒。正确做法登出时从 JWT 中解析出exp字段减去当前时间戳得到剩余秒数再设为 TTLlong expireAt jwt.getExpiresAt().getTime(); // token 的过期毫秒时间戳 long remainingSeconds (expireAt - System.currentTimeMillis()) / 1000; redisTemplate.opsForValue().set(blacklist: jti, , Duration.ofSeconds(remainingSeconds));5.5 坑五网关熔断时JWT 校验被跳过导致未授权访问这是最危险的坑。当网关因下游服务雪崩而触发 Hystrix 熔断时部分请求会走 fallback 逻辑。我们最初 fallback 返回一个默认 HTML 页面但没意识到fallback 是在网关线程里执行的它绕过了所有过滤器链包括 JWT 认证过滤器。结果一个未登录用户只要故意触发熔断如连续刷/api/course/list接口就能直接访问 fallback 页面而页面里可能嵌着未鉴权的 API 调用。解决方案所有 fallback 方法必须手动校验ServerWebExchange中的Authorization头更彻底的做法把 JWT 校验逻辑下沉到网关的GlobalFilter并在熔断 fallback 前强制调用该 filter我们最终选择后者写了一个FallbackAuthenticationFilter在fallback执行前用exchange.getRequest().getHeaders().getFirst(Authorization)提取 token 并校验校验失败则直接返回 401绝不进入 fallback 页面。6. 性能压测实录从 500 QPS 到 5000 QPS 的调优全过程理论和避坑说完最后来点硬核数据。模拟项目X上线前我们对网关鉴权模块做了全链路压测。目标支撑 5000 QPS 的并发请求平均响应时间 100ms错误率 0.1%。初始版本未优化在 500 QPS 就开始抖动P95 响应时间飙到 1200ms错误率 12%。以下是我们的调优步骤和实测数据。6.1 阶段一JVM 与线程池调优提升 3.2 倍吞吐初始配置Spring Cloud Gateway 默认 Netty 线程数 CPU 核数 × 2JVM 堆内存 1G。问题定位jstat -gc显示 Young GC 频繁每 2 秒一次Full GC 每 5 分钟一次top -H发现大量线程处于RUNNABLE状态但 CPU 利用率仅 40%说明线程在等 I/O调优动作Netty 线程池将reactor.netty.http.server.HttpServer的wiretap关闭减少日志开销maxConnections从默认 1000 提升到 5000JVM 参数-Xms4g -Xmx4g -XX:UseG1GC -XX:MaxGCPauseMillis100 -XX:UseStringDeduplication连接池网关到下游服务的 WebClient 连接池maxIdleTime从 30s 改为 60smaxLifeTime从 5min 改为 10min效果QPS 从 500 提升到 1600P95 响应时间从 1200ms 降至 320ms。6.2 阶段二JWT 解析与验签优化提升 2.1 倍吞吐瓶颈转移jstack发现大量线程阻塞在io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws()验签耗时占总耗时 65%。优化方案算法升级从 HS256对称加密切换到 RS256非对称网关用私钥签名服务用公钥验签。虽然单次验签慢 20%但公钥可缓存避免每次从 Vault 拉取公钥缓存将公钥加载到ConcurrentHashMapString, PublicKeykey 为kid密钥 ID首次加载后永久缓存Payload 解析懒加载不立即解析整个 payload只解析exp,nbf,jti等必要字段roles和resource_actions等业务字段等到服务层真正需要时再解析效果QPS 从 1600 提升到 3350P95 响应时间降至 140ms。6.3 阶段三Redis 黑名单查询优化提升 1.5 倍吞吐瓶颈再转移redis-cli --latency显示平均延迟 8msslowlog get发现GET blacklist:xxx命令频繁出现在慢日志里。优化动作Pipeline 批量查询网关不再对每个请求单独查一次 Redis而是将一批请求的jti收集起来用Pipeline一次性发送MGET命令本地缓存在网关 JVM 内存中用 Caffeine 缓存最近 1000 个jti的黑名单状态过期时间 1 分钟远小于 token 过期时间命中率 92%Redis 集群分片将黑名单 key 按jti的 hash 值分到 8 个 Redis 节点避免单点压力效果QPS 从 3350 提升到 5050P95 响应时间稳定在 85ms错误率 0.03%。6.4 最终压测报告5000 QPS 持续 30 分钟指标初始版本优化后提升倍数平均响应时间842ms78ms10.8xP95 响应时间1200ms85ms14.1xQPS500505010.1x错误率12.3%0.03%—网关 CPU 使用率95%42%—内存占用1.8GB2.1GB—最后分享一个小技巧压测时用wrk -t12 -c400 -d30s --latency http://gateway/api/course/list比 JMeter 更轻量--latency参数能直接输出详细的延迟分布比看平均值有用得多。我们就是靠它精准定位到MGET的延迟毛刺才决定上 Pipeline 的。
阅读完成 · 觉得有帮助?