No more cluster attempts left 折腾半天没解决

举报
赵KK日常技术记录 发表于 2023/06/30 16:15:36 2023/06/30
【摘要】 对redis自动生成数据接口进行压测,压测使用ApiPost进行,并发数50,轮次2000。图片图片起初数据执行是正常的,先是出现了redis集群错误redis.clients.jedis.exceptions.JedisClusterMaxAttemptsException: No more cluster attempts left.at redis.clients.jedis.Jedi...

对redis自动生成数据接口进行压测,压测使用ApiPost进行,并发数50,轮次2000。

图片

图片

起初数据执行是正常的,先是出现了redis集群错误

redis.clients.jedis.exceptions.JedisClusterMaxAttemptsException: No more cluster attempts left.
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:86) ~[jedis-3.0.1.jar:na]
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:124) ~[jedis-3.0.1.jar:na]
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:124) ~[jedis-3.0.1.jar:na]
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:124) ~[jedis-3.0.1.jar:na]
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:124) ~[jedis-3.0.1.jar:na]
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:124) ~[jedis-3.0.1.jar:na]
at redis.clients.jedis.JedisClusterCommand.run(JedisClusterCommand.java:25) ~[jedis-3.0.1.jar:na]
at redis.clients.jedis.JedisCluster.exists(JedisCluster.java:142) ~[jedis-3.0.1.jar:na
查看源码报错位置如下

private T runWithRetries(final int slot, int attempts, boolean tryRandomNode, JedisRedirectionException redirect) {
if (attempts <= 0) {
throw new JedisClusterMaxAttemptsException(“No more cluster attempts left.”);
}
attempts小于等于0时报错,根据日志向上追踪至124行

 // release current connection before recursion
  releaseConnection(connection);
  connection = null;

  if (attempts <= 1) {
    //We need this because if node is not reachable anymore - we need to finally initiate slots
    //renewing, or we can stuck with cluster state without one node in opposite case.
    //But now if maxAttempts = [1 or 2] we will do it too often.
    //TODO make tracking of successful/unsuccessful operations for node - do renewing only
    //if there were no successful responses from this node last few seconds
    this.connectionHandler.renewSlotCache();
  }
  //124行
  return runWithRetries(slot, attempts - 1, tryRandomNode, redirect);

入参之前对attempts 进行-1,也就是说attempts 想要小于等于0,attempts 入参就是小于等于1,根据注释来说当尝试次数是1-2之间时,将会对不成功响应的节点进行初始化。根据压测数量来看,最多数据为2000*50即10万条,排除数据量过大或者版本问题,开发环境redis为集群模式。

最终也没有得到合适的解决方案,当然在压测过程中除了偶发失败之外,用户是无感知的,节点失败重试即可。

getOutputStream() has already been called for this response

response.getOutputStream() 已经用过了不能再次使用。
java.lang.IllegalStateException: getOutputStream() has already been called for this response
at org.apache.catalina.connector.Response.getWriter(Response.java:582) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
at org.apache.catalina.connector.ResponseFacade.getWriter(ResponseFacade.java:227) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
在get请求中,对于token验证和跨域处理,通过实现Filter接口实现,报错位置如下

private void handleException(ServletResponse response, CommonResponse commonResponse) throws IOException {
response.setContentType(“application/json;charset=UTF-8”);
PrintWriter writer = response.getWriter();//此行
writer.write(JSONUtil.tJSON(commonResponse));
writer.flush();
writer.close();
}
图片

图片

针对filter的代码非源码部分,stackoverflow针对此问题有解答

图片

建议执行flush方法后执行close方法,可见在代码中是没有区别的。

知识点

SpringMVC中所有Controller接口进行返回时底层都是用response.getOutputStream() 或 response.getWriter()进行输出的;Tomcat的ServletRequest中, getParameter()方法与getInputStream()/getReader()不兼容, 只能选择一方.调用了一方, 另一方就会是空的(前提:表单的POST请求).

private void handleException(ServletResponse response, CommonResponse commonResponse) throws IOException {
response.setContentType(“application/json;charset=UTF-8”);
response.reset();
PrintWriter writer = response.getWriter();
writer.write(JSONUtil.tJSON(commonResponse));
writer.flush();
writer.close();
}
但是仍没有解决,依旧报错。在HttpServletRequestWrapper中重写getreader和getInputStream方法。

图片这个jodd的StreamUtil真是难找

图片

public class ParameterRequestWrapper extends HttpServletRequestWrapper {

private Map<String,Object> params;

private byte[] body;

public ParameterRequestWrapper(HttpServletRequest request, Map<String,Object> newParams) throws IOException {
super(request);
this.params = newParams;
body = StreamUtil.readBytes(request.getReader(), “UTF-8”);
}

@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}

@SuppressWarnings(“unchecked”)
public Map getParameterMap() {
return params;
}

@SuppressWarnings(“unchecked”)
public Enumeration getParameterNames() {
Vector l = new Vector(params.keySet());
return l.elements();
}

public String[] getParameterValues(String name) {
Object v = params.get(name);
if (v == null) {
return null;
} else if (v instanceof String[]) {
return (String[]) v;
} else if (v instanceof String) {
return new String[] { (String) v };
} else {
return new String[] { v.toString() };
}
}

public String getParameter(String name) {
Object v = params.get(name);
if (v == null) {
return null;
} else if (v instanceof String[]) {
String[] strArr = (String[]) v;
if (strArr.length > 0) {
return strArr[0];
} else {
return null;
}
} else if (v instanceof String) {
return (String) v;
} else {
return v.toString();
}
}

@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {

  @Override
  public boolean isFinished() {
    return bais.available() == 0;
  }

  @Override
  public boolean isReady() {
    return true;
  }

  @Override
  public void setReadListener(ReadListener readListener) {

  }

  @Override
  public int read() throws IOException {
    return bais.read();
  }
};

}

}
仍旧报错图片

在本filter执行dofilterchain之前应将request转为HttpServletRequest然后再进行wrapper初始化。

更改代码如下

HttpServletRequest req = (HttpServletRequest) request;
Map<String,Object> map=new HashMap(request.getParameterMap());
String[] keySet = map.keySet().toArray(new String[0]);

        for(String key : keySet){
            Object value = map.get(key);
            if (value instanceof String[]) {
                String[] valueArray = (String[]) value;
                for (int i = 0; i < valueArray.length; i++) {
                    if(org.apache.commons.lang.StringUtils.isNotEmpty(valueArray[i]) && !"null".equals(valueArray[i])){
                        valueArray[i] = valueArray[i].trim();
                    }else{
                        map.remove(key);
                    }
                }
            }else{
                if(value==null){
                    map.remove(key);
                }
            }
        }
        ParameterRequestWrapper wrapRequest=new ParameterRequestWrapper(req,map);

        chain.doFilter(wrapRequest, response); 

报错仍旧产生,但细心又发现在我这个filter之前还有一个上游组件的filter已经执行了dofilter,但是在上游的dofilter并未执行response的getwriter方法。

在报错的源码中最终定位到org.apache.catalina.connector.Response#getWriter方法,可以看到在if判断中应该是为了保证调用的resposne写方法一致,要不都用getWriter(),要不都用getOutputStream()。而我并没有使用getOutputStream(),那就只能说SpringBoot中底层代码用的getOutputStream。

@Override
public PrintWriter getWriter()
throws IOException {

    if (usingOutputStream) {
        throw new IllegalStateException
            (sm.getString("coyoteResponse.getWriter.ise"));
    }

    if (ENFORCE_ENCODING_IN_GET_WRITER) {
        /*
         * If the response's character encoding has not been specified as
         * described in <code>getCharacterEncoding</code> (i.e., the method
         * just returns the default value <code>ISO-8859-1</code>),
         * <code>getWriter</code> updates it to <code>ISO-8859-1</code>
         * (with the effect that a subsequent call to getContentType() will
         * include a charset=ISO-8859-1 component which will also be
         * reflected in the Content-Type response header, thereby satisfying
         * the Servlet spec requirement that containers must communicate the
         * character encoding used for the servlet response's writer to the
         * client).
         */
        setCharacterEncoding(getCharacterEncoding());
    }

    usingWriter = true;
    outputBuffer.checkConverter();
    if (writer == null) {
        writer = new CoyoteWriter(outputBuffer);
    }
    return writer;
}

遂改造如下

ServletOutputStream outputStream = response.getOutputStream();
outputStream.flush();
outputStream.close();
,尽管如此,我仍觉得流的关闭节点有问题,但我又没什么证据。第一个报错用户虽无法感知,但面临丢数据和偶发报错的情况,如有好的解决方案会及时更新。

强烈推荐参考博文
https://www.cnblogs.com/bencakes/p/14433332.html
https://blog.csdn.net/u012977486/article/details/88821887
https://blog.csdn.net/TimerBin/article/details/90295451

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。