暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Connection reset

领创集团Advance Group 2022-05-07
3038


什么是Connection reset

在TCP首部中有6个标志位,其中一个标志位为RST,用于“复位”的。无论何时一个报文 段发往基准的连接( referenced connection)出现错误,TCP都会发出一个复位报文段。如果双方需要继续建立连接,那么需要重新进行三次握手建立连接。


本文作者

导致“Connection reset”的原因是服务器端因为某种原因关闭了Connection,而客户端依然在读写数据,此时服务器会返回复位标志“RST”,然后此时客户端就会提示“java.net.SocketException: Connection reset”


TCP建立连接时需要三次握手,在释放连接需要四次挥手;例如三次握手的过程如下:

1.第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;

2.第二次握手:服务器收到syn包,并会确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

3.第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

可以看到握手时会在客户端和服务器之间传递一些TCP头信息,比如ACK标志、SYN标志以及挥手时的FIN标志等。

除了以上这些常见的标志头信息,还有另外一些标志头信息,比如推标志PSH、复位标志RST等。其中复位标志RST的作用就是“复位相应的TCP连接”。



Connection reset的原因

导致此异常的原因,总结下来有三种情况:

1.服务器端偶尔出现了异常,导致连接关闭

解决方法:采用出错重试机制

2.服务器端和客户端使用的连接方式不一致

解决方法:服务器端和客户端使用相同的连接方式,即同时使用长连接或短连接

3.如果是HTTPS,那么还存在TLS版本不一致

解决方法:服务器端和客户端使用相同的TLS版本


线上问题排查过程

3.1第一阶段的

cash服务上线两天就有一条报警 connection reset的报警,第一反应就是服务重启,看发版记录,确实在报警时间段确实进行了服务发版,但是看项目代码已经做了平滑发布。



平滑发布解决方案

方案一:spring处理(本服务采用)

Spring Boot 需要2.3及以上

    server.shutdown=graceful
    spring.lifecycle.timeout-per-shutdown-phase=20s


    方案二:k8s处理

    https://docs.spring.io/spring-boot/docs/current/reference/html/deployment.html#deployment.cloud.kubernetes.container-lifecycle

      spec:
      containers:
      - name: "example-container"
      image: "example-image"
      lifecycle:
      preStop:
      exec:
      command: ["sh", "-c", "sleep 10"]

      3.2第二阶段

      过了几天又出现了几次,排查继续通过看日志发现Caused by: javax.net.ssl.SSLException: Connection reset,而且目标服务没有收到请求,没有日志输出。

      怀疑是ssl安全协议不一致通过https://www.ssllabs.com/ssltest/analyze.html查询域名支持的ssl版本,项目是jdk1.8默认是sslv1.2通过对比排除是ssl版本不一致导致。


      3.3第三阶段

      上线15天通过日志查询发现一共出现过7次,通过发生频率,确定为网路不稳定导致。所以通过重试解决网络不稳定的问题。


      方案一:
      前提条件被调用接口需要做幂等处理

        @Configuration
        public class FeignConfiguration {
        @Bean
        public Retryer feignRetryer() {
        return new Retryer.Default(100, 100, 2);
        }
        }

        这样不仅仅RST异常会重试,readTimeout等异常也会重试。


        方案二:

        重写Retryer.Default判断报错方法是否是GET,如果是GET进行重试,其他方法不重试。



          @Slf4j
          public class FeignRetryer extends Retryer.Default {
          public FeignRetryer() {
          super(100, 100, 2);
          }


          @Override
          public void continueOrPropagate(RetryableException e) {
          get 方法可以重试,其他方法不重试
          if (e.method() == Request.HttpMethod.GET) {
          super.continueOrPropagate(e);
          } else {
          throw e;
          }
          }




          @Override
          public Retryer clone() {
          return new FeignRetryer();
          }
          }


          @Configuration
          public class FeignConfiguration {
          @Bean
          public Retryer feignRetryer() {
          return new FeignRetryer();
          }
          }

          仅对get接口异常重试,不区分RST异常还是其他异常,post不重试。但是如果使用不规范get也会涉及幂等问题。


          方案三

          这种方式比较稳妥,只对RST重试,而且当报RST后,被请求的接口是没有收到请求的,所以也不涉及幂等问题。通过递归判断异常类型和异常信息中是否包含关键字决策是否重试。

            @Slf4j
            public class FeignRetryer extends Retryer.Default {
            private static final String KEYWORD = "Connection reset";


            **
            * 重试三次
            */
            public FeignRetryer() {
            super(100, 100, 3);
            }


            @Override
            public void continueOrPropagate(RetryableException e) {
            if (ExceptionUtil.isExceptionCauseContainKey(e, SocketException.class,KEYWORD)) {
            super.continueOrPropagate(e);
            } else {
            throw e;
            }
            }




            @Override
            public Retryer clone() {
            return new FeignRetryer();
            }
            }


            @Configuration
            public class FeignConfiguration {
            @Bean
            public Retryer feignRetryer() {
            return new FeignRetryer();
            }
            }






            @Slf4j
            public class ExceptionUtil {
            **
            * cause 是否包含关键字key
            *
            * @param throwable
            * @param key
            * @return
            */
            public static boolean isExceptionCauseContainKey(@NotNull Throwable throwable, @NotNull Class<?> expectedType, String key) {
            while (throwable != null) {
            log.info("#ExceptionUtil.isExceptionCauseContainKey expectedType:{} message:{}", expectedType, throwable.getMessage());
            if (expectedType.isInstance(throwable) && throwable.getMessage().contains(key)) {
            return true;
            }
            throwable = throwable.getCause();
            }
            return false;
            }
            }


            大家在各种异常重试时,是怎么做的呢?

            欢迎在评论区分享你的观点~



            感谢阅读「技术创想」第46期文章

            领创集团正在春季招聘中

            期待你的加入

            点击文末

            阅读原文

            获取更多

            招聘信息


            关于领创集团

            (Advance Intelligence Group)
            领创集团成立于 2016年,致力于通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈。集团旗下包含企业业务和消费者业务两大板块,企业业务包含 ADVANCE.AI 和 Ginee,分别为银行、金融、金融科技、零售和电商行业客户提供基于 AI 技术的数字身份验证、风险管理产品和全渠道电商服务解决方案;消费者业务 Atome Financial 包括亚洲领先的先享后付平台 Atome 和数字金融服务。2021年 9月,领创集团宣布完成超4亿美元 D 轮融资,融资完成后领创集团估值已超 20亿美元,成为新加坡最大的独立科技创业公司之一。
            文章转载自领创集团Advance Group,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

            评论