ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Java开发中使用 HTTP 的注意要点

2022-06-02 13:02:35  阅读:124  来源: 互联网

标签:Java 读取 server 参数 要点 HTTP 超时 public 服务端


若察觉文中知识要点有误,请私信及时指正
本篇内容总结于极客时间——《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,读取,server,参数,要点,HTTP,超时,public,服务端
来源: https://www.cnblogs.com/lastboss/p/16337214.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有