概述
Feign 的接口调用写起来很舒服,但默认配置下它并不快:底层用的 HttpURLConnection 没有连接池,每个请求都要重新建 TCP 连接。这篇把 Feign 的性能开销拆成四个可控因素,逐个给出配置写法、生效验证方式和踩坑点,核心是"换带连接池的客户端"这一步。
纲要
- Feign 的性能开销来自哪里
- 底层 HTTP 客户端(默认
HttpURLConnection,无连接池) - 日志级别(
FULL打印请求体,有 IO 与字符串拼接开销) - 传输体积(大 payload 未压缩)
- 连接数与超时设置
- 底层 HTTP 客户端(默认
- 默认客户端的真实代价
- 每次请求新建连接:三次握手 + 四次挥手
- 高并发下大量
TIME_WAIT HttpURLConnection/HttpClient/OkHttp三者对比
- 优化点一:换用带连接池的客户端(重点)
- 引依赖
io.github.openfeign:feign-httpclient - 打开开关
feign.httpclient.enabled - 连接池参数
max-connections/max-connections-per-route - 参数设太小会排队------"越优化越慢"
- 用 Debug 断点验证底层客户端确实换了
- 引依赖
- 优化点二:日志级别降到
BASIC - 优化点三:GZIP 压缩(只对大报文有意义)
- 优化点四:超时与重试,Feign 与 Ribbon 是两层
- 怎么验证优化生效:压测耗时 +
netstat连接数 - 实战坑清单
- 工程结构 / API 速览 / 官方文档 / 总结
Feign 的性能开销来自哪里
聊优化之前先定位开销。Feign 本身只是一层"接口转 HTTP 请求"的封装,真正发请求、建连接、传字节的是它背后的东西。所以性能问题基本落在四个地方:
#mermaid-svg-MlDdwCnKJHTj309E{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MlDdwCnKJHTj309E .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MlDdwCnKJHTj309E .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MlDdwCnKJHTj309E .error-icon{fill:#552222;}#mermaid-svg-MlDdwCnKJHTj309E .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MlDdwCnKJHTj309E .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MlDdwCnKJHTj309E .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MlDdwCnKJHTj309E .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MlDdwCnKJHTj309E .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MlDdwCnKJHTj309E .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MlDdwCnKJHTj309E .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MlDdwCnKJHTj309E .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MlDdwCnKJHTj309E .marker.cross{stroke:#333333;}#mermaid-svg-MlDdwCnKJHTj309E svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MlDdwCnKJHTj309E p{margin:0;}#mermaid-svg-MlDdwCnKJHTj309E .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MlDdwCnKJHTj309E .cluster-label text{fill:#333;}#mermaid-svg-MlDdwCnKJHTj309E .cluster-label span{color:#333;}#mermaid-svg-MlDdwCnKJHTj309E .cluster-label span p{background-color:transparent;}#mermaid-svg-MlDdwCnKJHTj309E .label text,#mermaid-svg-MlDdwCnKJHTj309E span{fill:#333;color:#333;}#mermaid-svg-MlDdwCnKJHTj309E .node rect,#mermaid-svg-MlDdwCnKJHTj309E .node circle,#mermaid-svg-MlDdwCnKJHTj309E .node ellipse,#mermaid-svg-MlDdwCnKJHTj309E .node polygon,#mermaid-svg-MlDdwCnKJHTj309E .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MlDdwCnKJHTj309E .rough-node .label text,#mermaid-svg-MlDdwCnKJHTj309E .node .label text,#mermaid-svg-MlDdwCnKJHTj309E .image-shape .label,#mermaid-svg-MlDdwCnKJHTj309E .icon-shape .label{text-anchor:middle;}#mermaid-svg-MlDdwCnKJHTj309E .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MlDdwCnKJHTj309E .rough-node .label,#mermaid-svg-MlDdwCnKJHTj309E .node .label,#mermaid-svg-MlDdwCnKJHTj309E .image-shape .label,#mermaid-svg-MlDdwCnKJHTj309E .icon-shape .label{text-align:center;}#mermaid-svg-MlDdwCnKJHTj309E .node.clickable{cursor:pointer;}#mermaid-svg-MlDdwCnKJHTj309E .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MlDdwCnKJHTj309E .arrowheadPath{fill:#333333;}#mermaid-svg-MlDdwCnKJHTj309E .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MlDdwCnKJHTj309E .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MlDdwCnKJHTj309E .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MlDdwCnKJHTj309E .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MlDdwCnKJHTj309E .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MlDdwCnKJHTj309E .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MlDdwCnKJHTj309E .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MlDdwCnKJHTj309E .cluster text{fill:#333;}#mermaid-svg-MlDdwCnKJHTj309E .cluster span{color:#333;}#mermaid-svg-MlDdwCnKJHTj309E div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MlDdwCnKJHTj309E .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MlDdwCnKJHTj309E rect.text{fill:none;stroke-width:0;}#mermaid-svg-MlDdwCnKJHTj309E .icon-shape,#mermaid-svg-MlDdwCnKJHTj309E .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MlDdwCnKJHTj309E .icon-shape p,#mermaid-svg-MlDdwCnKJHTj309E .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MlDdwCnKJHTj309E .icon-shape .label rect,#mermaid-svg-MlDdwCnKJHTj309E .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MlDdwCnKJHTj309E .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MlDdwCnKJHTj309E .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MlDdwCnKJHTj309E :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Feign 远程调用
底层 HTTP 客户端
日志级别
传输体积
连接数与超时
默认 HttpURLConnection
无连接池,每次新建连接
FULL 级别打印请求体
IO + 字符串拼接开销
大 payload 不压缩
占用带宽与序列化时间
max-connections 过小
超时不对齐
换 HttpClient / OkHttp
连接复用,收益最大
降到 BASIC / NONE
收益中等
开启 GZIP
仅大报文有收益
对齐两层超时
避免重试放大
| 优化点 | 具体动作 | 收益量级 | 适用条件 |
|---|---|---|---|
| 底层客户端 | HttpClient/OkHttp 替换 HttpURLConnection |
最大 | 所有场景,尤其高并发 |
| 日志级别 | FULL → BASIC 或 NONE |
中等 | 生产环境必做 |
| 压缩 | 开启请求/响应 GZIP | 视报文大小 | 单次报文较大时 |
| 超时与重试 | 对齐 Feign 与 Ribbon 两层超时 | 防恶化 | 存在慢调用或重试时 |
排在最前面的永远是底层客户端,因为它是"每个请求都要付一次"的固定成本,其他三项都是特定条件下才省下来的。
默认客户端的真实代价
Feign 在默认情况下依赖一个叫 HttpURLConnection 的客户端(JDK 自带)。它的关键缺陷是没有连接池:
- 每调一次远程接口,就新建一个 TCP 连接
- 业务执行完,连接立即关闭
- 建连接要三次握手,断连接要四次挥手
单次调用的几毫秒握手机制开销看着不多,但 Feign 调用的单位是 QPS,不是次数。一个订单服务每秒调 200 次用户服务,就等于每秒抛掉 200 个连接,内核里堆出一批处于 TIME_WAIT 状态的套接字,端口和文件描述符都在被消耗。
UserService HttpURLConnection OrderService UserService HttpURLConnection OrderService #mermaid-svg-t1X2Ylc1o8fFHwCS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-t1X2Ylc1o8fFHwCS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-t1X2Ylc1o8fFHwCS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-t1X2Ylc1o8fFHwCS .error-icon{fill:#552222;}#mermaid-svg-t1X2Ylc1o8fFHwCS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-t1X2Ylc1o8fFHwCS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-t1X2Ylc1o8fFHwCS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-t1X2Ylc1o8fFHwCS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-t1X2Ylc1o8fFHwCS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-t1X2Ylc1o8fFHwCS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-t1X2Ylc1o8fFHwCS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-t1X2Ylc1o8fFHwCS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-t1X2Ylc1o8fFHwCS .marker.cross{stroke:#333333;}#mermaid-svg-t1X2Ylc1o8fFHwCS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-t1X2Ylc1o8fFHwCS p{margin:0;}#mermaid-svg-t1X2Ylc1o8fFHwCS .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-t1X2Ylc1o8fFHwCS text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-t1X2Ylc1o8fFHwCS .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-t1X2Ylc1o8fFHwCS .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-t1X2Ylc1o8fFHwCS .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-t1X2Ylc1o8fFHwCS .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-t1X2Ylc1o8fFHwCS #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-t1X2Ylc1o8fFHwCS .sequenceNumber{fill:white;}#mermaid-svg-t1X2Ylc1o8fFHwCS #sequencenumber{fill:#333;}#mermaid-svg-t1X2Ylc1o8fFHwCS #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-t1X2Ylc1o8fFHwCS .messageText{fill:#333;stroke:none;}#mermaid-svg-t1X2Ylc1o8fFHwCS .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-t1X2Ylc1o8fFHwCS .labelText,#mermaid-svg-t1X2Ylc1o8fFHwCS .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-t1X2Ylc1o8fFHwCS .loopText,#mermaid-svg-t1X2Ylc1o8fFHwCS .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-t1X2Ylc1o8fFHwCS .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-t1X2Ylc1o8fFHwCS .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-t1X2Ylc1o8fFHwCS .noteText,#mermaid-svg-t1X2Ylc1o8fFHwCS .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-t1X2Ylc1o8fFHwCS .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-t1X2Ylc1o8fFHwCS .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-t1X2Ylc1o8fFHwCS .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-t1X2Ylc1o8fFHwCS .actorPopupMenu{position:absolute;}#mermaid-svg-t1X2Ylc1o8fFHwCS .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-t1X2Ylc1o8fFHwCS .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-t1X2Ylc1o8fFHwCS .actor-man circle,#mermaid-svg-t1X2Ylc1o8fFHwCS line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-t1X2Ylc1o8fFHwCS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 默认实现:每次调用独立连接 第 1 次调用 TCP 三次握手 建立连接 HTTP 请求 HTTP 响应 TCP 四次挥手(连接销毁) 第 2 次调用 TCP 三次握手(再来一遍) 建立连接 HTTP 请求 HTTP 响应 TCP 四次挥手
Feign 的可选客户端实现有三种,讲义 2.3 节列的就是这张表:
| 客户端 | 是否支持连接池 | 说明 |
|---|---|---|
URLConnection |
否 | Feign 默认实现,每次新建/关闭连接 |
Apache HttpClient |
是 | Apache 出品,本篇演示用这个 |
OKHttp |
是 | 安卓开发用得很多,移动端常见 |
两个支持连接池的客户端任选其一,都能把"每次握手"变成"第一次握手,之后复用"。连接复用之后,一次远程调用的耗时能降到接近纯网络传输 + 服务端处理的时间。
优化点一:换用带连接池的客户端
整个优化只有两步,但两步必须都做,缺一步不生效。
第一步:引入依赖
打开 order-service/pom.xml,在 <dependencies> 里加入 OpenFeign 与 Apache HttpClient 的整合依赖:
xml
<!-- openfeign 与 Apache HttpClient 的整合依赖,版本由 Spring Cloud 统一管理,不用写版本号 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
注意 artifactId 是 feign-httpclient,不是 Apache 原生的 httpclient,也不是 feign-okhttp。这个包是 OpenFeign 官方做的适配层,引入后靠自动装配就能把底层客户端切过去,不用自己写 HttpClient 的构造代码。
工程里 Feign 的完整依赖集合是这样的(终态工程 order-service/pom.xml 中的实际内容):
xml
<!--feign客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--引入HttpClient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<!--引入feign的统一api-->
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
第二步:打开开关并配置连接池
只引依赖是不够的。feign-httpclient 在类路径上只代表"能力可用",是否启用由配置开关决定。在 order-service/src/main/resources/application.yml 里加:
yaml
feign:
httpclient:
enabled: true # 开启 feign 对 HttpClient 的支持,默认是 false
max-connections: 200 # 连接池中的最大连接数
max-connections-per-route: 50 # 单个路径(单个服务)的最大连接数
这三行就是终态工程里的原始内容,一个字没改。enabled: true 是那个最容易漏的开关,漏了它,依赖引了、代码编译过了、服务也起来了,但底层还是 HttpURLConnection,性能一点没变。
两个连接数参数的含义要分清:
| 参数 | 含义 | 类比 |
|---|---|---|
max-connections |
整个连接池允许的最大连接总数 | 停车场总车位数 |
max-connections-per-route |
每个路由(每个被调服务)允许的最大连接数 | 分配给某家公司的固定车位 |
"路由"就是路径。订单服务会调用户服务、商品服务、库存服务,每个服务地址不同,就是不同的路由。如果只设总数不设单路由上限,会出现一种情况:调用户服务时把 200 个连接全占了,转去调商品服务时一个连接都拿不到,只能干等。
参数设太小反而更慢
这是最反直觉的一点。max-connections 和 max-connections-per-route 不是越大越好,但设小了会直接排队。
假设 max-connections-per-route: 50,同一时刻有 200 个线程要调用户服务。多出来的 150 个线程拿不到连接,只能阻塞等待池子里有连接被归还。表现上就是接口 RT 飙升,但 CPU、带宽、下游服务都不忙------瓶颈在本地连接池。也就是说,配置写错方向时,"优化"会变成"限流"。
那设多少合适?靠算,不靠抄:
- 先估算峰值 QPS,乘以单次调用平均耗时,得到并发请求数(利特尔法则:并发 ≈ QPS × 平均耗时)
- 再按"要调用几个下游服务"分摊
- 部署后用压测验证,看不同取值下的 RT 与吞吐变化
比如峰值 500 QPS、单次调用平均 50ms,并发约 25;同时调用 5 个下游服务,单路由 50 已经绰绰有余。反过来,如果下游某个服务本身慢(RT 500ms),同样 QPS 下并发会被放大十倍,这时候 50 就不够了。所以这两个值要跟着自己的 QPS、实例数和下游响应时间走。
怎么确认客户端真的换了
配完了不算数,得看到证据。Feign 客户端是个接口,OrderService 里却直接 @Autowired 注入就能用,说明底层一定是动态代理在创建对象:
#mermaid-svg-vNk688RzomhkKHbU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vNk688RzomhkKHbU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vNk688RzomhkKHbU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vNk688RzomhkKHbU .error-icon{fill:#552222;}#mermaid-svg-vNk688RzomhkKHbU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vNk688RzomhkKHbU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vNk688RzomhkKHbU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vNk688RzomhkKHbU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vNk688RzomhkKHbU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vNk688RzomhkKHbU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vNk688RzomhkKHbU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vNk688RzomhkKHbU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vNk688RzomhkKHbU .marker.cross{stroke:#333333;}#mermaid-svg-vNk688RzomhkKHbU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vNk688RzomhkKHbU p{margin:0;}#mermaid-svg-vNk688RzomhkKHbU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vNk688RzomhkKHbU .cluster-label text{fill:#333;}#mermaid-svg-vNk688RzomhkKHbU .cluster-label span{color:#333;}#mermaid-svg-vNk688RzomhkKHbU .cluster-label span p{background-color:transparent;}#mermaid-svg-vNk688RzomhkKHbU .label text,#mermaid-svg-vNk688RzomhkKHbU span{fill:#333;color:#333;}#mermaid-svg-vNk688RzomhkKHbU .node rect,#mermaid-svg-vNk688RzomhkKHbU .node circle,#mermaid-svg-vNk688RzomhkKHbU .node ellipse,#mermaid-svg-vNk688RzomhkKHbU .node polygon,#mermaid-svg-vNk688RzomhkKHbU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vNk688RzomhkKHbU .rough-node .label text,#mermaid-svg-vNk688RzomhkKHbU .node .label text,#mermaid-svg-vNk688RzomhkKHbU .image-shape .label,#mermaid-svg-vNk688RzomhkKHbU .icon-shape .label{text-anchor:middle;}#mermaid-svg-vNk688RzomhkKHbU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vNk688RzomhkKHbU .rough-node .label,#mermaid-svg-vNk688RzomhkKHbU .node .label,#mermaid-svg-vNk688RzomhkKHbU .image-shape .label,#mermaid-svg-vNk688RzomhkKHbU .icon-shape .label{text-align:center;}#mermaid-svg-vNk688RzomhkKHbU .node.clickable{cursor:pointer;}#mermaid-svg-vNk688RzomhkKHbU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vNk688RzomhkKHbU .arrowheadPath{fill:#333333;}#mermaid-svg-vNk688RzomhkKHbU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vNk688RzomhkKHbU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vNk688RzomhkKHbU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vNk688RzomhkKHbU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vNk688RzomhkKHbU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vNk688RzomhkKHbU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vNk688RzomhkKHbU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vNk688RzomhkKHbU .cluster text{fill:#333;}#mermaid-svg-vNk688RzomhkKHbU .cluster span{color:#333;}#mermaid-svg-vNk688RzomhkKHbU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vNk688RzomhkKHbU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vNk688RzomhkKHbU rect.text{fill:none;stroke-width:0;}#mermaid-svg-vNk688RzomhkKHbU .icon-shape,#mermaid-svg-vNk688RzomhkKHbU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vNk688RzomhkKHbU .icon-shape p,#mermaid-svg-vNk688RzomhkKHbU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vNk688RzomhkKHbU .icon-shape .label rect,#mermaid-svg-vNk688RzomhkKHbU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vNk688RzomhkKHbU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vNk688RzomhkKHbU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vNk688RzomhkKHbU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} @Autowired UserClient userClient
动态代理生成的 Proxy 对象
FeignInvocationHandler.invoke()
构建 RequestTemplate 请求模板
execute 真正发起请求
LoadBalancerFeignClient
装饰 Apache HttpClient
连接池复用连接,发起真实 HTTP 调用
验证方法:在 FeignClientFactoryBean 的 loadBalance 方法里打断点,Debug 启动 order-service,浏览器刷新触发调用。进断点后看 client 字段,如果底层是 Apache 的 HttpClient(被 LoadBalancerFeignClient 装饰了一层做负载均衡),说明配置生效;如果还是 HttpURLConnection 相关实现,说明开关没打开。讲义 2.3 节演示的就是这个断点位置。
优化点二:日志级别降到 BASIC
Feign 的日志级别四档里,FULL 会把请求头、请求体、响应体全打出来。请求体是被序列化过的 JSON 字符串,打印它意味着一次额外的字符串拼接和一次磁盘/网络 IO。QPS 一高,这部分开销就不可忽略;而且请求体里往往带着手机号、身份证这类敏感字段,写进日志本身就是风险。
| 级别 | 记录内容 | 建议 |
|---|---|---|
NONE |
什么都不记 | 默认值,生产可用 |
BASIC |
请求方法、URL、响应状态码、执行时间 | 生产推荐,排障够用 |
HEADERS |
BASIC + 请求/响应头 |
需要时临时开 |
FULL |
BASIC + 头 + 请求体 + 响应体 + 元数据 |
只用于本地排障,禁止留在生产 |
配置写法:
yaml
feign:
client:
config:
default: # default 代表全局,写具体服务名则只对该服务生效
loggerLevel: BASIC # 日志级别,只记基本的请求与响应信息
这里有个容易忽略的联动:Feign 的 Logger.Level 决定"生成不生成日志",日志框架的 logging.level 决定"输出不输出日志",两道闸门要一起开。终态工程里配的是:
yaml
logging:
level:
cn.itcast: debug # Feign 日志走 debug 级别输出
生产环境把 cn.itcast 从 debug 调到 info,再配合 loggerLevel: NONE,才算真正关掉。
优化点三:GZIP 压缩
这个优化有明确的适用边界:单次报文大才有收益。
Feign 的压缩配置在 Spring Cloud OpenFeign 2.2.x(Hoxton 线,也就是本工程的 Hoxton.SR10)里是 feign.compression.* 前缀:
yaml
feign:
compression:
request:
enabled: true # 请求体压缩
min-request-size: 2048 # 超过该字节数才压缩,避免小报文白耗 CPU
response:
enabled: true # 声明接受 gzip 响应并自动解压
写这段配置前注意两点。一是键名与所属版本强相关 :Spring Cloud 2022 之后整个前缀挪到了 spring.cloud.openfeign.compression.*,升级版本时这段配置会直接失效,具体键名以所用版本的官方文档为准。二是压缩有 CPU 成本:几百字节的 JSON 压不缩多少,反而多了一次压缩和解压。真正该开的是批量接口这种一次返回几百条记录、报文几 MB 的场景。
如果只是想统一减小响应体积,也可以在服务端侧开启容器压缩(对所有接口生效,不用改 Feign):
yaml
server:
compression:
enabled: true
mime-types: application/json # 只压缩指定类型
min-response-size: 2048
优化点四:超时与重试
性能问题常常表现为"慢"而不是"错",所以超时设置是性能优化的一部分。漏配超时的后果是请求一直挂着、线程一直占着,连接池明明够用却被打满。
Feign 的超时配置:
yaml
feign:
client:
config:
default:
connectTimeout: 5000 # 建立连接的超时,单位毫秒
readTimeout: 5000 # 读取响应数据的超时,单位毫秒
loggerLevel: BASIC
关键认知在这里:Feign 的超时和 Ribbon 的超时是两层,不是一回事 。Feign 的 connectTimeout/readTimeout 是传给底层 HTTP 客户端的参数;而调用链上还套着 Ribbon,Ribbon 自己也有一组 ConnectTimeout/ReadTimeout。当 Ribbon 层的超时比 Feign 层先到,外层就先抛异常并触发重试,Feign 那组配置根本没机会生效。
#mermaid-svg-amot9ygzUuhMRcVz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-amot9ygzUuhMRcVz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-amot9ygzUuhMRcVz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-amot9ygzUuhMRcVz .error-icon{fill:#552222;}#mermaid-svg-amot9ygzUuhMRcVz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-amot9ygzUuhMRcVz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-amot9ygzUuhMRcVz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-amot9ygzUuhMRcVz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-amot9ygzUuhMRcVz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-amot9ygzUuhMRcVz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-amot9ygzUuhMRcVz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-amot9ygzUuhMRcVz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-amot9ygzUuhMRcVz .marker.cross{stroke:#333333;}#mermaid-svg-amot9ygzUuhMRcVz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-amot9ygzUuhMRcVz p{margin:0;}#mermaid-svg-amot9ygzUuhMRcVz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-amot9ygzUuhMRcVz .cluster-label text{fill:#333;}#mermaid-svg-amot9ygzUuhMRcVz .cluster-label span{color:#333;}#mermaid-svg-amot9ygzUuhMRcVz .cluster-label span p{background-color:transparent;}#mermaid-svg-amot9ygzUuhMRcVz .label text,#mermaid-svg-amot9ygzUuhMRcVz span{fill:#333;color:#333;}#mermaid-svg-amot9ygzUuhMRcVz .node rect,#mermaid-svg-amot9ygzUuhMRcVz .node circle,#mermaid-svg-amot9ygzUuhMRcVz .node ellipse,#mermaid-svg-amot9ygzUuhMRcVz .node polygon,#mermaid-svg-amot9ygzUuhMRcVz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-amot9ygzUuhMRcVz .rough-node .label text,#mermaid-svg-amot9ygzUuhMRcVz .node .label text,#mermaid-svg-amot9ygzUuhMRcVz .image-shape .label,#mermaid-svg-amot9ygzUuhMRcVz .icon-shape .label{text-anchor:middle;}#mermaid-svg-amot9ygzUuhMRcVz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-amot9ygzUuhMRcVz .rough-node .label,#mermaid-svg-amot9ygzUuhMRcVz .node .label,#mermaid-svg-amot9ygzUuhMRcVz .image-shape .label,#mermaid-svg-amot9ygzUuhMRcVz .icon-shape .label{text-align:center;}#mermaid-svg-amot9ygzUuhMRcVz .node.clickable{cursor:pointer;}#mermaid-svg-amot9ygzUuhMRcVz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-amot9ygzUuhMRcVz .arrowheadPath{fill:#333333;}#mermaid-svg-amot9ygzUuhMRcVz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-amot9ygzUuhMRcVz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-amot9ygzUuhMRcVz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-amot9ygzUuhMRcVz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-amot9ygzUuhMRcVz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-amot9ygzUuhMRcVz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-amot9ygzUuhMRcVz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-amot9ygzUuhMRcVz .cluster text{fill:#333;}#mermaid-svg-amot9ygzUuhMRcVz .cluster span{color:#333;}#mermaid-svg-amot9ygzUuhMRcVz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-amot9ygzUuhMRcVz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-amot9ygzUuhMRcVz rect.text{fill:none;stroke-width:0;}#mermaid-svg-amot9ygzUuhMRcVz .icon-shape,#mermaid-svg-amot9ygzUuhMRcVz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-amot9ygzUuhMRcVz .icon-shape p,#mermaid-svg-amot9ygzUuhMRcVz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-amot9ygzUuhMRcVz .icon-shape .label rect,#mermaid-svg-amot9ygzUuhMRcVz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-amot9ygzUuhMRcVz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-amot9ygzUuhMRcVz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-amot9ygzUuhMRcVz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Ribbon 先超时
重试放大
Feign 发起调用
Ribbon 层超时
ConnectTimeout / ReadTimeout
底层 HTTP 客户端超时
feign.client.config.default.*Timeout
真实 HTTP 请求
抛异常 + 重试
Ribbon 重试其他实例
MaxAutoRetries / MaxAutoRetriesNextServer
下游收到成倍请求
Ribbon 侧的配置要写在被调服务名下(终态工程里已经有一份针对 userservice 的 Ribbon 配置):
yaml
userservice:
ribbon:
ConnectTimeout: 5000 # 与 Feign 的 connectTimeout 对齐或略大
ReadTimeout: 5000 # 与 Feign 的 readTimeout 对齐或略大
MaxAutoRetries: 0 # 同一实例的重试次数
MaxAutoRetriesNextServer: 1 # 换实例重试的次数
对齐原则:Ribbon 层的超时不应小于 Feign 层的超时 ,否则里层还没走完,外层已经判失败并重试了。至于两层的默认值,不同版本口径有差异(Feign 的 readTimeout 默认偏大,Ribbon 的默认值偏小、毫秒级),别靠记忆,直接以所用版本的文档和实际断点为准。重试次数也要克制:MaxAutoRetriesNextServer 每加 1,最坏情况下游收到的请求就翻一倍,配合超时错配就是妥妥的流量放大。
怎么验证优化生效
改完配置要给出前后对比,不能只凭感觉。
压测或循环调用,对比优化前后的平均耗时与 P95:
bash
# 用 ab 对订单接口压测,观察 RT 与吞吐变化
ab -n 5000 -c 200 http://localhost:8088/order/101
# 循环调用,粗略看单次耗时
for i in $(seq 1 100); do curl -s -o /dev/null -w "%{time_total}\n" http://localhost:8088/order/101; done
再看连接数 的变化,这是最能说明问题的指标。优化前每次调用都新建并销毁连接,TIME_WAIT 数量会随 QPS 线性增长;优化后连接被池子复用,ESTABLISHED 数量稳定在一个接近 max-connections 的水平,TIME_WAIT 基本不涨:
bash
# 统计与用户服务(8081)相关的连接状态
netstat -an | grep 8081 | awk '{print $6}' | sort | uniq -c
# 只看 TIME_WAIT 总数
netstat -an | grep TIME_WAIT | wc -l
判定标准很直接:同样压测参数下,TIME_WAIT 数量大幅下降、RT 下降,就说明连接池生效了。如果 TIME_WAIT 没变,回到"开关是不是真开了"那一步查。
工程结构
终态工程是多模块结构,Feign 的接口被抽到了独立的 feign-api 模块,order-service 只依赖它:
tree
cloud-demo
├── pom.xml # 父工程,管理 Spring Cloud 版本
├── feign-api # Feign 客户端与实体抽取模块
│ └── src/main/java/cn/itcast/feign
│ ├── clients/UserClient.java # @FeignClient 接口
│ ├── config/DefaultFeignConfiguration.java # 日志级别等默认配置
│ └── pojo/User.java
├── order-service # 调用方,本篇优化落在这里
│ ├── pom.xml # 引入 feign-httpclient
│ └── src/main
│ ├── java/cn/itcast/order
│ │ ├── OrderApplication.java # @EnableFeignClients
│ │ ├── service/OrderService.java # 注入 UserClient
│ │ └── web/OrderController.java
│ └── resources/application.yml # feign.httpclient 配置段
├── user-service # 被调方,端口 8081
├── eureka-server # 已切 Nacos,保留模块
└── gateway # 网关,端口 10010
Feign 接口的定义长这样,@FeignClient 的 value 就是服务名,也就是前面说的"路由":
java
package cn.itcast.feign.clients;
import cn.itcast.feign.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
启动类上挂全局 Feign 配置,clients 显式指定要扫描的客户端(因为 feign-api 的包路径不在启动类所在包下):
java
package cn.itcast.order;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class, defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
默认配置类,把日志级别降到 BASIC:
java
package cn.itcast.feign.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfiguration {
@Bean
public Logger.Level logLevel() {
return Logger.Level.BASIC; // 只记请求方法、URL、状态码与耗时
}
}
调用方 OrderService 通过 Feign 接口完成远程调用,注意它是注入接口、不是实现类:
java
package cn.itcast.order.service;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1. 查询订单
Order order = orderMapper.findById(orderId);
// 2. 通过 Feign 远程调用用户服务,底层走连接池复用的 HttpClient
User user = userClient.findById(order.getUserId());
// 3. 封装 user 到 Order
order.setUser(user);
// 4. 返回
return order;
}
}
实战坑清单
| 现象 | 真实原因 | 处理方式 |
|---|---|---|
引了 feign-httpclient 但性能没变化 |
没打开 feign.httpclient.enabled,开关默认 false |
补上 enabled: true 并重启,用断点确认底层客户端 |
| 高并发下接口 RT 飙升,下游却不忙 | max-connections / max-connections-per-route 设太小,请求在本地排队等连接 |
按 QPS × RT 估算并发后调大,压测验证 |
| 下游收到的请求量是预期的一倍多 | Feign 与 Ribbon 超时没对齐,外层先超时触发重试 | 让 Ribbon 超时 ≥ Feign 超时,收敛 MaxAutoRetries* |
| 生产日志里出现完整请求体 | loggerLevel: FULL 忘改回去 |
降到 BASIC,同时把 logging.level 调到 info |
| 开了 GZIP 反而更慢 | 报文太小,压缩收益抵不过 CPU 开销 | 用 min-request-size / min-response-size 设阈值 |
| 连接池参数照抄别人的值 | 别人的 QPS、实例数、下游 RT 和你不一致 | 按自己的指标算,压测后定值 |
feign.compression 配置升级后失效 |
2022 版起前缀改为 spring.cloud.openfeign.* |
升级时同步核对文档键名 |
升级 Spring Cloud 后 feign-httpclient 行为变了 |
新版对连接池客户端的默认装配策略有调整 | 以所用版本文档为准,重新做一次压测确认 |
API 速览
| 配置项 | 位置 | 作用 | 说明 |
|---|---|---|---|
io.github.openfeign:feign-httpclient |
order-service/pom.xml |
引入带连接池的客户端 | 版本由 Spring Cloud 管理,不写版本号 |
feign.httpclient.enabled |
application.yml |
启用 HttpClient | 默认 false,引依赖不够,必须显式打开 |
feign.httpclient.max-connections |
application.yml |
连接池最大连接数 | 全局上限,终态工程为 200 |
feign.httpclient.max-connections-per-route |
application.yml |
单个服务最大连接数 | 防单一路由占满全池,终态工程为 50 |
feign.client.config.default.loggerLevel |
application.yml |
Feign 日志级别 | 生产用 BASIC 或 NONE |
feign.client.config.default.connectTimeout / readTimeout |
application.yml |
连接/读取超时 | 单位毫秒,注意与 Ribbon 层对齐 |
feign.compression.request.enabled / response.enabled |
application.yml |
请求/响应压缩 | 键名随版本变化,2022 版起前缀为 spring.cloud.openfeign.* |
<服务名>.ribbon.*Timeout |
application.yml |
Ribbon 层超时 | 两层超时里先触发的那一层 |
官方文档
- Spring Cloud OpenFeign --- Feign 使用优化(讲义 2.3 节对应内容)
- Spring Cloud OpenFeign --- Configuration properties(httpclient / compression / client.config)
- Apache HttpComponents HttpClient --- Connection management
总结
- Feign 慢的根子在底层客户端:默认的
HttpURLConnection没有连接池,每次调用都建一次 TCP 连接再关掉。 - 换
HttpClient或OkHttp是收益最大的一步,做法就两件事:引feign-httpclient依赖、打开feign.httpclient.enabled。只做第一件等于没做。 max-connections管总池、max-connections-per-route管单个服务。设小了不是省资源,是让请求排队,性能反而下降,取值要按自己的 QPS 和下游 RT 算完后压测验证。- 日志级别降到
BASIC,FULL只留给本地排障;Feign 的Logger.Level和logging.level两道闸门要一起调。 - GZIP 只对大报文划算,小报文开启是净亏 CPU,且配置键名随版本变化。
- 超时要记住是两层:Ribbon 在外面、Feign 在里层,外层的值不能比里层小,否则超时错配会触发重试,把下游请求量放大。
- 验证别只看感觉:压测看 RT 与吞吐,
netstat看TIME_WAIT和ESTABLISHED的数量变化。