Java框架 SpringCloud 快速入门: Feign 的性能优化

概述

Feign 的接口调用写起来很舒服,但默认配置下它并不快:底层用的 HttpURLConnection 没有连接池,每个请求都要重新建 TCP 连接。这篇把 Feign 的性能开销拆成四个可控因素,逐个给出配置写法、生效验证方式和踩坑点,核心是"换带连接池的客户端"这一步。

纲要

  • Feign 的性能开销来自哪里
    • 底层 HTTP 客户端(默认 HttpURLConnection,无连接池)
    • 日志级别(FULL 打印请求体,有 IO 与字符串拼接开销)
    • 传输体积(大 payload 未压缩)
    • 连接数与超时设置
  • 默认客户端的真实代价
    • 每次请求新建连接:三次握手 + 四次挥手
    • 高并发下大量 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、带宽、下游服务都不忙------瓶颈在本地连接池。也就是说,配置写错方向时,"优化"会变成"限流"。

那设多少合适?靠算,不靠抄:

  1. 先估算峰值 QPS,乘以单次调用平均耗时,得到并发请求数(利特尔法则:并发 ≈ QPS × 平均耗时)
  2. 再按"要调用几个下游服务"分摊
  3. 部署后用压测验证,看不同取值下的 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 层超时 两层超时里先触发的那一层

官方文档

总结

  • 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 的数量变化。
相关推荐
蛋蛋的就会顺顺的1 小时前
hot100——二叉树
java·数据结构·算法·leetcode·力扣
打工仔折腾 AI2 小时前
System Prompt 替代 Few-shot:用规则约束大模型输出的省钱实践
java·人工智能·python·spring·langchain·prompt·ai agent 实战
打工仔折腾 AI2 小时前
从零写一个CAD 05:以鼠标为中心的滚轮缩放,招法能复用但顺序不能反
开发语言·后端·python·性能优化·计算机外设·swift·ai agent 实战
CarIise2 小时前
Java实训阶段查漏补缺复习笔记1
java·开发语言·笔记
vx-程序开发3 小时前
【计算机毕设】51300童装超市管理系统的设计与实现
java·vue.js·spring boot·源码·课程设计·毕设·大作业
ym hyd 1113 小时前
题库建立与考试系统源码 Java+SpringBoot+Vue3 前后分离
java·开发语言·vue.js·spring boot·毕设
risc1234563 小时前
什么是码点?
java
JavaPub-rodert4 小时前
公共 Docker 镜像源总失效?用 Harbor Proxy Cache 自建 Docker Hub 镜像缓存
java·缓存·docker
智鸟科技GemeOpen开发者智能设备4 小时前
平安校园 AI 智能实时告警系统 智鸟科技·GemeOpen + 谷华科技·融合创新方案白皮书
java·开发语言·python·物联网·智能家居