Java开发中使用 HTTP 的注意要点
2022/6/3 1:22:49
本文主要是介绍Java开发中使用 HTTP 的注意要点,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
若察觉文中知识要点有误,请私信及时指正
本篇内容总结于极客时间——《Java开发常见错误 100 例》
配置连接超时和读取超时的学问
虽然 HTTP 是应用层的协议,但本质上还是执行的网络层的 TCP/IP 协议,TCP/IP 协议是面向连接的协议,在传输数据前需要建立连接,而基本每个网络框架都会给予两个配置参数:
- 连接超时参数,用户等待连接建立的耗时;
- 读取超时参数,读取数据的最长等待时间;
连接超时参数和连接超时的误区
将连接超时参数设置的特别长,比如 60s
,就像上面提到的,HTTP 本质还是 TCP/IP 协议的三次握手,这套动作的耗时一般都是在毫秒级别最多几秒,所以当出现十几秒甚至几十秒的情况,一般都是网络问题或者防火墙问题,基本上可以认定连接不上。因此,设置特别长的连接超时参数是没有什么意义的,将其配置的短一些是没有问题的(一般是 1-5s ),如果是内网就可以更短。没有抓住是谁导致连接超时
,有些服务端具有负载均衡的能力,所以可以跟客户端直连。但是一般的情况下,可能都是通过像 Nginx 这种通过反向代理支持负载均衡能力来连接服务端的,而如果服务端通过类似 Nginx 的反向代理来负载均衡,客户端连接的其实是 Nginx,而不是服务端,此时出现连接超时应该排查 Nginx。
读取超时参数和读取超时的误区:
读取超时后,服务器的执行就中断了
,这个是我初入职场时犯的错误,当时我需要执行一个耗时 5s 的统计任务,但框架设定的读取超时参数是在 3s ,也就是我这样我永远也拿不到返回值,在不了解公司读取超时参数的情况下,我反复的执行那条复杂的 sql,最终在 mysql 警报的情况下被我的组长给拦了下来。
来还原当时的场景,定义一个 client 接口,内部通过 HttpClient 调用服务端接口 server,客户端读取超时 2s,服务端接口执行耗时 5s
@RestController @RequestMapping("clientreadtimeout") @Slf4j public class ClientReadTimeoutController { private String getResponse(String url, int connectTimeout, int readTimeout) throws IOException { return Request.Get("http://localhost:45678/clientreadtimeout" + url) .connectTimeout(connectTimeout) .socketTimeout(readTimeout) .execute() .returnContent() .asString(); } @GetMapping("client") public String client() throws IOException { log.info("client1 called"); //服务端5s超时,客户端读取超时2秒 return getResponse("/server?timeout=5000", 1000, 2000); } @GetMapping("server") public void server(@RequestParam("timeout") int timeout) throws InterruptedException { log.info("server called"); TimeUnit.MILLISECONDS.sleep(timeout); log.info("Done"); } }
调用 client 接口后,从日志中可以看到,客户端 2 秒后出现了 SocketTimeoutException,原因是读取超时,服务端却丝毫没受影响在 3 秒后执行完成。
[11:35:11.943] [http-nio-45678-exec-1] [INFO ] [.t.c.c.d.ClientReadTimeoutController:29 ] - client1 called [11:35:12.032] [http-nio-45678-exec-2] [INFO ] [.t.c.c.d.ClientReadTimeoutController:36 ] - server called [11:35:14.042] [http-nio-45678-exec-1] [ERROR] [.a.c.c.C.[.[.[/].[dispatcherServlet]:175 ] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception java.net.SocketTimeoutException: Read timed out at java.net.SocketInputStream.socketRead0(Native Method) ... [11:35:17.036] [http-nio-45678-exec-2] [INFO ] [.t.c.c.d.ClientReadTimeoutController:38 ] - Done
我们都知道,类似 Tomcat 的服务器都是把请求提交到线程池来做处理,只要服务端收到了请求,网络层面的超时和断开便不会影响服务端的执行。因此,出现读取超时不能随意假设服务端的处理情况,需要根据业务状态考虑如何进行后续处理。
认为超时时间越长任务接口成功率就越高,将读取超时参数配置得太长
。HTTP 请求需要等待结果的返回,那就是同步调用,当你把超时时间设置的过于长,我们的程序运行在 Tomcat 这样的线程池中(现在问题可能就变成了线程池的问题),客户端线程也在等待的情况下,当下游服务出现大量超时的时候,程序可能也会受到拖累创建大量线程,最终崩溃。
对于异步任务或者定时任务来说,你设置一个较长的超时时间是没什么问题的。但是对于面向用户和一些短平快的微服务时,就要调整一下策略,一般不会超过30s(一般就是在 3s-6s )
Feign 和 Ribbon 配合使用,你知道怎么配置超时吗?
对 feign 的配置感到棘手,一方面是网上的资料五花八门,另一方面则是因为 feign 和它的组件 ribbon 都有对应的配置参数。那么,这些配置的优先级是怎样的,又哪些什么坑呢?接下来,我们做一些实验吧。
首先查看一下 feign 的默认读取超时参数是多少,假设有这么一个服务端接口,什么都不干只休眠 10 分钟
@PostMapping("/server") public void server() throws InterruptedException { TimeUnit.MINUTES.sleep(10); } // 定义一个 Feign 来调用这个接口 @FeignClient(name = "clientsdk") public interface Client { @PostMapping("/feignandribbon/server") void server(); } // 通过 Feign Client 进行接口调用 @GetMapping("client") public void timeout() { long begin=System.currentTimeMillis(); try{ client.server(); }catch (Exception ex){ log.warn("执行耗时:{}ms 错误:{}", System.currentTimeMillis() - begin, ex.getMessage()); } }
得到以下输出:
[15:40:16.094] [http-nio-45678-exec-3] [WARN ] [o.g.t.c.h.f.FeignAndRibbonController :26 ] - 执行耗时:1007ms 错误:Read timed out executing POST http://clientsdk/feignandribbon/server
从这个输出中,我们可以得到结论一,默认情况下 Feign 的读取超时是 1 秒,如此短的读取超时算是坑点一。
我们来分析一下源码。打开 RibbonClientConfiguration 类后,会看到 DefaultClientConfigImpl 被创建出来之后,ReadTimeout 和 ConnectTimeout 被设置为 1s:
/** * Ribbon client default connect timeout. */ public static final int DEFAULT_CONNECT_TIMEOUT = 1000; /** * Ribbon client default read timeout. */ public static final int DEFAULT_READ_TIMEOUT = 1000; @Bean @ConditionalOnMissingBean public IClientConfig ribbonClientConfig() { DefaultClientConfigImpl config = new DefaultClientConfigImpl(); config.loadProperties(this.name); config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT); config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT); config.set(CommonClientConfigKey.GZipPayload, DEFAULT_GZIP_PAYLOAD); return config; }
那么你想修改参数,就需要自己进行配置:
feign.client.config.default.readTimeout=3000 feign.client.config.default.connectTimeout=3000
但!这里也有一个坑点,也就是结论二 feign 配置参数是需要上面两个参数都配置,否则只配一个读取超时是会失败的,原因是这段代码:
if (config.getConnectTimeout() != null && config.getReadTimeout() != null) { builder.options(new Request.Options(config.getConnectTimeout(), config.getReadTimeout())); }
上面也提到 ribbon 也可以配置相关的参数,但它的坑点也就是结论三是配置参数需要大写
ribbon.ReadTimeout=4000 ribbon.ConnectTimeout=4000
那么回到重点,feign 和 ribbon 都配置了参数的情况下谁会生效,答案是结论四,feign 的会生效。 在 LoadBalancerFeignClient 源码中可以看到,如果 Request.Options 不是默认值,就会创建一个 FeignOptionsClientConfig 代替原来 Ribbon 的 DefaultClientConfigImpl,导致 Ribbon 的配置被 Feign 覆盖:
IClientConfig getClientConfig(Request.Options options, String clientName) { IClientConfig requestConfig; if (options == DEFAULT_OPTIONS) { requestConfig = this.clientFactory.getClientConfig(clientName); } else { requestConfig = new FeignOptionsClientConfig(options); } return requestConfig; }
但注意 feign 的配置如果只配置了一条那么还是 ribbon 的生效,原因是结论二。
Ribbon 的重试
这个问题是当时我们开发了一个短信发送的系统,但是偶尔会出现重复发送的情况,但我们反复确定了代码内没有重复发送的逻辑,最后确定问题出现在 ribbon,我们翻看它的源码,MaxAutoRetriesNextServer 参数默认为 1,也就是 Get 请求在某个服务端节点出现问题(比如读取超时)时,Ribbon 会自动重试一次:
// DefaultClientConfigImpl public static final int DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER = 1; public static final int DEFAULT_MAX_AUTO_RETRIES = 0; // RibbonLoadBalancedRetryPolicy public boolean canRetry(LoadBalancedRetryContext context) { HttpMethod method = context.getRequest().getMethod(); return HttpMethod.GET == method || lbContext.isOkToRetryOnAllOperations(); } @Override public boolean canRetrySameServer(LoadBalancedRetryContext context) { return sameServerCount < lbContext.getRetryHandler().getMaxRetriesOnSameServer() && canRetry(context); } @Override public boolean canRetryNextServer(LoadBalancedRetryContext context) { // this will be called after a failure occurs and we increment the counter // so we check that the count is less than or equals to too make sure // we try the next server the right number of times return nextServerCount <= lbContext.getRetryHandler().getMaxRetriesOnNextServer() && canRetry(context); }
解决办法有两个:
- 一是,把发短信接口从 Get 改为 Post。其实,这里还有一个 API 设计问题,有状态的 API 接口不应该定义为 Get。根据 HTTP 协议的规范,Get 请求用于数据查询,而 Post 才是把数据提交到服务端用于修改或新增。选择 Get 还是 Post 的依据,应该是 API 的行为,而不是参数大小。这里的一个误区是,Get 请求的参数包含在 Url QueryString 中,会受浏览器长度限制,所以一些同学会选择使用 JSON 以 Post 提交大参数,使用 Get 提交小参数
- 二是,将 MaxAutoRetriesNextServer 参数配置为 0,禁用服务调用失败后在下一个服务端节点的自动重试。在配置文件中添加一行即可:
ribbon.MaxAutoRetriesNextServer=0
其实这里是双方都没有做好,就像之前说的,Get 请求应该是无状态或者幂等的,短信服务商的短信接口可以设计为支持幂等调用的。
HTTP 协议的并发数限制
这里我们可以直接看 PoolingHttpClientConnectionManager 的源码,这里有 2 个参数:
public PoolingHttpClientConnectionManager( final HttpClientConnectionOperator httpClientConnectionOperator, final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory, final long timeToLive, final TimeUnit timeUnit) { ... this.pool = new CPool(new InternalConnectionFactory( this.configData, connFactory), 2, 20, timeToLive, timeUnit); ... } public CPool( final ConnFactory<HttpRoute, ManagedHttpClientConnection> connFactory, final int defaultMaxPerRoute, final int maxTotal, final long timeToLive, final TimeUnit timeUnit) { ... }}
- defaultMaxPerRoute=2,也就是同一个主机 / 域名的最大并发请求数为 2。如果你使用爬虫需要 10 个并发,则默认值太小会限制爬虫的效率
- maxTotal=20,也就是所有主机整体最大并发为 20,这也是 HttpClient 整体的并发度
HttpClient 是 Java 非常常用的 HTTP 客户端,这个问题经常出现。你可能会问,为什么默认值限制得这么小。其实,这不能完全怪 HttpClient,很多早期的浏览器也限制了同一个域名两个并发请求。对于同一个域名并发连接的限制,其实是 HTTP 1.1 协议要求的:
Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy. A proxy SHOULD use up to 2*N connections to another server or proxy, where N is the number of simultaneously active users. These guidelines are intended to improve HTTP response times and avoid congestion.
HTTP 1.1 协议是 20 年前制定的,现在 HTTP 服务器的能力强很多了,所以有些新的浏览器没有完全遵从 2 并发这个限制,放开并发数到了 8 甚至更大。如果需要通过 HTTP 客户端发起大量并发请求,不管使用什么客户端,请务必确认客户端的实现默认的并发度是否满足需求。
这篇关于Java开发中使用 HTTP 的注意要点的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南