美文网首页
埋点日志服务502异常

埋点日志服务502异常

作者: liujianhuiouc | 来源:发表于2017-10-15 00:54 被阅读0次

背景介绍

好久没有时间写文章了,今天跟大家分享最近在维护的老系统中的遇到的一个问题;由于老系统从公司创建初期就一直存在,笔者不会对现有系统不合理处进行分析,仅仅描述502异常的定位处理过程。

首先介绍下我们的业务场景,为了分析用户的行为轨迹,通过都需要将用户在客户端的行为日志上传到后端服务器上,消息流转图如下所示:

消息流转图
  1. 用户行为日志存储到本地系统中,或者是直接通过message-api请求公网的消息上报接口
  2. 用户行为日志累计到一定的大小后者超过一定时间的时候通过http upload接口将文件上传到埋点服务上
  3. Nginx将请求分发到后端的埋点服务集群上,埋点服务将文件下载在本地磁盘上
  4. 埋点服务中解析已经上传完毕的文件,通过解密、解析、转化为一条条的消息后发往Kafka系统中

问题描述

上述是整个埋点服务的消息流转和业务处理流程,从某一天开发,监控经常会出现某台机器流量突然下跌的情况,图中某一台机器的流量下跌了差不多30%(纵坐标不是从0开始),该问题差不多每天会出现好几次。

数据量监控图

问题追踪

首先怀疑是GC导致系统的抖动,登录到机器上查看了下GC日志,显示一切正常,接下来找运维同学一起查看了下Nginx上的access日志,发现出现了大量的502异常,具体消息为

502异常

nginx配置的策略是发现两次502响应后就认为是失败,在接下来的一段时间内(一般设置为30s)不会转发请求到该机器上;502异常一般是服务不可用,继续查看另外一台nginx上的日志,发现在这一刻有些访问请求是200的;说明服务不是完全不可用的,只是出现有些请求返回502。

知识回顾

由于笔者曾经有过相关的经验,怀疑是TCP请求队列满了,导致有些请求TCP请求无法建立,接下来先跟大家回顾下TCP三次握手的过程。

TCP传输过程

相信大家对TCP连接应该有相应的理解,笔者在此不做过多的介绍,有疑问的同学可以自行百度。

操作系统连接处理

接收端的操作系统内核收到客户端发来的SYNC请求后,会创建一个连接放入到SYNC队列中,当连接从SYNC_RECV变为ESTABLISHED状态时,请求从SYNC队列移交到ESTABLISH队列中。在这个过程中一定会出现队列满的状况,当ESTABLISH满的时候,连接就只能一直处于SYNC_RECV状态,一般称之为半连接状态;当SYNC队列满的时候则会拒绝连接。那这些队列长度如何控制呢,我们来看几个参数
net.ipv4.tcp_max_syn_backlog 处于半连接状态的队列长度
net.core.somaxconn 处于连接状态的队列长度

系统参数

查看了下我们的系统,两个参数都很大,按照我们的流量评估来算,不可能会达到这个阈值,由此排除是系统参数设置不合理导致的原因。

问题再分析

经过前面阶段分析后,笔者陷入了迷雾中,又重新回顾了下系统的架构,同时再仔细的分析了下nginx日志,发送502请求的耗时和正常建立连接的耗时差不多,开始怀疑是不是tomcat自身进行了connection refused,查阅了相关资料和代码,在tomcat的类NioEndpoint中发现了如下代码

@Override
   public void bind() throws Exception {

       serverSock = ServerSocketChannel.open();
       socketProperties.setProperties(serverSock.socket());
       InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
       serverSock.socket().bind(addr,getBacklog());
       serverSock.configureBlocking(true); //mimic APR behavior
       serverSock.socket().setSoTimeout(getSocketProperties().getSoTimeout());

       // Initialize thread count defaults for acceptor, poller
       if (acceptorThreadCount == 0) {
           // FIXME: Doesn't seem to work that well with multiple accept threads
           acceptorThreadCount = 1;
       }
       if (pollerThreadCount <= 0) {
           //minimum one poller thread
           pollerThreadCount = 1;

在开启服务段监听端口的时候在bind方法中传入了backlog参数,查看下相关的解释

     * @param   backlog         requested maximum length of the queue of
     *                          incoming connections.

猛然一看还是不太明白,是说能够接收的socket数目吗?继续查看相关资料

acceptCount(backlog)
在源码里头是backlog参数,默认值为100。该参数是指当前连接数超过maxConnections的时候,还可接受的连接数,即tcp的完全连接队列(accept队列)的大小。

backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。在内核版本2.2之前的Linux中,backlog参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket的上限。但自内核版本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。

由此可知,其表示处于连接状态的socket数量,默认是100,参数由acceptAccount控制,spring boot中通过server.tomcat.accept-count设置,因此需要加大该参数的配置,按照我们的流量,将其改为300后问题得到解决。

扩展知识

相信大家都接触过tomcat,大家肯定对maxconnections、maxthreads、acceptcount等相关参数区分的不是很清楚,在此一并介绍下,首先看下流转图

tomcat处理机制
  1. acceptor线程通过 serverSocket.accept方法摘取一个socket
  2. poller线程监听一些socket的事件,用来读取和写入
  3. poller线程在socket上读取一些消息完毕后将其转化为相应的对象提交给handler线程池进行处理

maxthreads其实就是handler的最大线程数,那maxconnections是什么呢,其实是最多支持同时从established队列中摘取多少个socket,
即允许同时accept的数量,NIO下默认是10000;

protected class Acceptor extends AbstractEndpoint.Acceptor {

        @Override
        public void run() {

            int errorDelay = 0;

            // Loop until we receive a shutdown command
            while (running) {

                // Loop if endpoint is paused
                while (paused && running) {
                    state = AcceptorState.PAUSED;
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }

                if (!running) {
                    break;
                }
                state = AcceptorState.RUNNING;

                try {
                    //if we have reached max connections, wait
                    //达到maxconnection的上限,等待,然后再accept摘取新的socket
                    countUpOrAwaitConnection();

                    SocketChannel socket = null;
                    try {
                        // Accept the next incoming connection from the server
                        // socket
                        socket = serverSock.accept();
                    } catch (IOException ioe) {
                        //we didn't get a socket
                        countDownConnection();
                        // Introduce delay if necessary
                        errorDelay = handleExceptionWithDelay(errorDelay);
                        // re-throw
                        throw ioe;
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;

                    // setSocketOptions() will add channel to the poller
                    // if successful
                    if (running && !paused) {
                        if (!setSocketOptions(socket)) {
                            countDownConnection();
                            closeSocket(socket);
                        }
                    } else {
                        countDownConnection();
                        closeSocket(socket);
                    }
                } catch (SocketTimeoutException sx) {
                    // Ignore: Normal condition
                } catch (IOException x) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), x);
                    }
                } catch (OutOfMemoryError oom) {
                    try {
                        oomParachuteData = null;
                        releaseCaches();
                        log.error("", oom);
                    }catch ( Throwable oomt ) {
                        try {
                            try {
                                System.err.println(oomParachuteMsg);
                                oomt.printStackTrace();
                            }catch (Throwable letsHopeWeDontGetHere){
                                ExceptionUtils.handleThrowable(letsHopeWeDontGetHere);
                            }
                        }catch (Throwable letsHopeWeDontGetHere){
                            ExceptionUtils.handleThrowable(letsHopeWeDontGetHere);
                        }
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
            }
            state = AcceptorState.ENDED;
        }
    }

参考

https://segmentfault.com/a/1190000008064162

相关文章

网友评论

      本文标题:埋点日志服务502异常

      本文链接:https://www.haomeiwen.com/subject/chtquxtx.html