记一次上传文件超时问题的排查过程

背景:

报错nginx 504 timeout,上传请求的链路如下:
页面请求->nginx->nodejs服务->网关->后端java服务,如果是nginx超时,则nodejs服务、网关、java服务都有嫌疑。

解决:

1、尝试用curl命令进行文件上传模拟:

curl -F "name=abc" http://10.24.238.76:8715/api/v1/stream/kafka/entity/schema/import

可以直接看到,后端有日志打出,说明正常的post请求可以走到后端,初步排除后端代码错误。


2、联系了nodejs服务团队和网关团队,都没有进行版本的变更,解决过程陷入僵局。


3、再次将焦点放在了java服务,会不会是上传文件的逻辑中有过于冗余的逻辑导致超时?于是在页面进行尝试,上传一个只有3字节的空excel文件,后端传来了java报错,证明确实走到了后端。定位到后端逻辑,有这样一段代码:

public class RpcSchemaManagerServiceImpl implements RpcSchemaManagerService {

    @Autowired
    private SchemaServiceBo schemaServiceBo;
    //D0CF11E0->.xls,504B0304->.xlsx
    private static final List<String> EXCELFILEHEADERS = Arrays.asList("D0CF11E0", "504B0304");

    public DubboResult<SchemaUploadResultDTO> uploadSchema(MultipartFormDataInput formDataInput) {
        try {
            Map<String, InputPart> uploadForm = formDataInput.getFormData();
            InputPart inputSourceTypePart = uploadForm.get("inputSourceType");
            String inputSourceType = inputSourceTypePart.getBody(String.class, null);
            InputPart entityPart = uploadForm.get("entity");
            String entity = entityPart.getBody(String.class, null);
            InputPart srcPart = uploadForm.get("src");
            InputStream inputStream = srcPart.getBody(InputStream.class, null);
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);

            //复制inputstream
            ByteArrayOutputStream bos = SchemaUtil.cloneInputStream(inputStream);
            //根据文件头判断是否是excel文件
            byte[] ioBuffer = new byte[4];
            new ByteArrayInputStream(bos.toByteArray()).read(ioBuffer, 0, ioBuffer.length);
            String value = SchemaUtil.bytesToHexString(ioBuffer);
            if (!EXCELFILEHEADERS.contains(value)) {
                int result;
                StringBuffer stringBuffer = new StringBuffer("");
                char[] array = new char[1024];
                while ((result = inputStreamReader.read(array)) != -1) {
                    stringBuffer.append(new String(array, 0, result));
                }
                try {
                    JSONObject.parse(stringBuffer.toString());
                    new Schema.Parser().parse(stringBuffer.toString());
                } catch (SchemaParseException sp) {
                    throw new BizException("解析文件avro-schema内容失败", sp);
                }
                SchemaUploadResultDTO schemaUploadResultDTO = new SchemaUploadResultDTO();
                schemaUploadResultDTO.setEntity(entity);
                schemaUploadResultDTO.setSchemaText(stringBuffer.toString());
                return DubboResult.buildSuccessResult(schemaUploadResultDTO);
            } else {
                xxx
            }
        }
    }

该逻辑是先复制一个字节流,取字节流的前四个字节得到文件头,判断是否为excel文件。之所以要先复制一个字节流,是因为流一旦被读取,哪怕只是读取一个字节,流中的指针就会向后移动,要想再次读取整段流则不再可能,这符合流的设计原则。其中,复制流的逻辑如下:

    /**
     * @Description: Inputstream以ByteArrayOutputStream的形式进行存储,防止pos指针不可逆前进
     * @param: [input]
     * @return: java.io.ByteArrayOutputStream
     * @throws:
     */
    public static ByteArrayOutputStream cloneInputStream(InputStream input) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = input.read(buffer)) > -1) {
                baos.write(buffer, 0, len);
            }
            baos.flush();
            return baos;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

复制流的过程相当于重新复制了一份excel文件,此过程比较耗时。但是奇怪的是,开发环境和提测环境都有这段逻辑,开发环境可以正常上传,而提测环境则会超时。


4、查看提测环境的硬盘、内存、cpu等使用情况,通过

df -h

命令发现,某个挂载路径下,磁盘使用率达到了百分之百,这样上传失败就能解释通了。再通过

du -hsx * | sort -rh | head -10

命令去定位具体是哪个目录或文件撑爆了磁盘。磁盘、内存、cpu任何一方出问题,都会拖慢进程或线程的处理速度,加上java服务确实有那么一段耗时逻辑,最终导致超过nginx默认超时时间1min。

结论:

服务出了问题,要有查看外部环境的意识,而不光是考虑代码逻辑。磁盘、内存、cpu、full gc、young gc都可以看一看,各个角度去找原因。

参考:
http://how2j.cn/k/io/io-bufferedstream/342.html

你可能感兴趣的:(Java,Linux)