最近接了某需求,是需要把文字转换成语音MP3文件,存入fastdfs,然后把文件路径存入数据库。

我们fastdfs是5.05版本,fastdfs工具类已经改成支持并发

public class FastClient<main> {


    private static Logger logger = Logger.getLogger(FastClient.class);

    /**
     * 只加载一次.
     */
    static {
        try {
            ClientGlobal.init("fdfs_client.properties");
            TrackerClient trackerClient = new TrackerClient(ClientGlobal.getGtrackerGroup());
            TrackerServer trackerServer = trackerClient.getConnection();
            if (trackerServer == null) {
                logger.error("getConnection return null");
            }
            StorageServer storageServer = trackerClient.getStoreStorage(trackerServer);
            if (storageServer == null) {
                logger.error("getStoreStorage return null");
            }
            // 这里有坑,具体参考文章:http://www.ityouknow.com//fastdfs/2017/12/26/fastdfs-concurrent.html
            // 根本原因:storageServer在高并发使用时有可能被置空,storageClient1使用时报NPE
            // 解决办法:放弃共享 storageClient1方式,每次调用时自己new出来
            // 所以注掉
//			storageClient1 = new StorageClientExtend(trackerServer, storageServer);

        } catch (Exception e) {
            logger.error(e);
        }
    }


    /**
     * description 获取StorageClient 实例,并发可用
     * param
     * return 
     * author 
     * createTime 2019/4/27 13:12
     **/
    private static StorageClientExtend getStorageClientExtend() throws IOException {
        TrackerClient trackerClient = new TrackerClient(ClientGlobal.getGtrackerGroup());
        TrackerServer trackerServer = trackerClient.getConnection();
        if (trackerServer == null) {
            logger.error("getConnection return null");
        }
        logger.info( "获取socket连接===》"+trackerServer.getSocket() );
        //给dfs发送一个消息
        ProtoCommon.activeTest(trackerServer.getSocket());

        StorageServer storageServer = trackerClient.getStoreStorage(trackerServer);
        return  new StorageClientExtend(trackerServer, storageServer);
    }


  /**
     *
     * @param fis
     *            文件
     * @param fileName
     *            文件名
     * @return 返回Null则为失败
     */
    public static synchronized String uploadFile(InputStream fis, String fileName) {
        try {

            NameValuePair[] metaList = null;
            byte[] fileBuff = null;
            if (fis != null) {
                int len = fis.available();
                fileBuff = new byte[len];
                int i = fis.read(fileBuff);
            }

            StorageClientExtend storageClientExtend = getStorageClientExtend();
            String uploadFilepath = storageClientExtend.uploadFile1(fileBuff, getFileExt(fileName), metaList);
            logger.info("uploadFile 文件名称 uploadFilepath : " + uploadFilepath);
            return uploadFilepath;
        } catch (Exception ex) {
            logger.warn("uploadFile上传文件异常 Exception:{}",ex);
            logger.error(ex);
            return null;
        }finally{
            if (fis != null){
                try {
                    fis.close();
                } catch (IOException e) {
                    logger.warn("uploadFile IOException 上传文件异常 , Exception:{}",e);
                    logger.error(e);
                }
            }
        }
    }

 

贴一下业务代码大家看看

public void generateVoice(MessageNoticeDTO messageNoticeDTO, Integer type)throws NoSuchAlgorithmException, InvalidKeyException, MalformedURLException {
        Long messageNoticeId = messageNoticeDTO.getId();
        String content = messageNoticeDTO.getContent();
        Long messageNoticeCreateTime = messageNoticeDTO.getCreateTime();
        OkHttpClient client = new OkHttpClient.Builder()
                //超时时间
                .connectTimeout(60, TimeUnit.SECONDS)
                //读取时间
                .readTimeout(60, TimeUnit.SECONDS)
                .build();

        //科大讯飞构建鉴权
        Request request = authentication();
        // 开启webSocket
        client.newWebSocket(request, new WebSocketListener() {

            //调用科大接口
            @Override
            public void onOpen(WebSocket webSocket, Response response) {
                super.onOpen(webSocket, response);
                //发送数据
                JSONObject frame = organizationRequestData(content);
                webSocket.send(frame.toString());
            }

            //获取语音流
            @SneakyThrows
            @Override
            public void onMessage(WebSocket webSocket, String text) {
                super.onMessage(webSocket, text);
                JSONObject object = JSONObject.parseObject(text);
                //处理返回数据
                ResponseData resp = JSONObject.toJavaObject(object, ResponseData.class);

                if (null == resp || resp.getCode() != 0 || resp.getData() == null) {
                    log.error("获取音频数据异常,向重试队列发送消息,快讯id:{},响应:{}", messageNoticeId, resp);
                    throw new NullPointerException("获取音频数据异常");
                }

                //resp.data.status ==2 说明数据全部返回完毕,可以关闭连接,释放资源
                if (resp.getData().getStatus() == 2) {
                    //上传到fastfds
                    String uploadFilepath = uploadFile(resp);
                    if (StringUtils.isBlank(uploadFilepath)) {
                        log.error("上传到文件服务器异常,向重试队列发送消息,快讯id:{}", messageNoticeId);
                        throw new NullPointerException("上传到文件服务器异常 获取音频数据url异常");
                    }
                    log.info("快讯语音数据url生成,id:{},voiceUrl:{}", messageNoticeId, uploadFilepath);
                    //更新快讯数据信息
                    modifyMessageNotice(messageNoticeDTO, uploadFilepath);
                    log.info("快讯语音数据更新完成,id:{},voiceUrl:{}", messageNoticeId, uploadFilepath);
                    webSocket.close(1000, "获取音频完成");

                }
            }

            //websocker异常回调
            @Override
            public void onFailure(WebSocket webSocket, Throwable t, Response response) {
                super.onFailure(webSocket, t, response);
                webSocket.close(1000, "获取音频完成");
                //异常情况处理
                log.error("科大异常,快讯id入参:{},Throwable:{},Response:{}", messageNoticeId, t, response);
                voiceMessageNoticeAo.handleInfoMessageVoiceFail(messageNoticeId, messageNoticeCreateTime, type);
            }
        });

    }

    /**
     * @param resp:
     * @Description:
     * @Author:
     * @Date: 2021/3/25 11:11
     * @return: java.lang.String
     **/
    private String uploadFile(ResponseData resp) {
        String result = resp.getData().getAudio();
        byte[] audio = Base64.getDecoder().decode(result);
        //上传到fastfds
        return FastClient.uploadFile(new ByteArrayInputStream(audio), ".mp3");
    }

 /**
     * @param messageNoticeDTO: 快讯dto对象
     * @Description: 更新短讯表文件路径
     * @Author:
     * @Date: 2021/3/12 11:12
     * @return: void
     **/
    private void modifyMessageNotice(MessageNoticeDTO messageNoticeDTO, String uploadFile) throws MessageNoticeException {
        messageNoticeDTO.setVoiceUrl(FAST_DFS_DOMAIN + uploadFile);
        messageNoticeService.modifyMessageNotice(messageNoticeDTO);
    }

这是获取科大讯飞然后转码上传fastdfs代码,一开始在测试的时候并发达到200,500左右都是很正常的。但是到了线上环境,峰值达到每秒几百个,就受不了,在上传的时候出现各种异常

java.net,SocketException:Connection closed by remote host,Response:null
java.net.SocketException:timeout,Response:null
java.io.EOFException,Response:null
javax.net.ssl.SSLException:SSL peer shut down incorrectly,Response:null
java.io.IOException:recv package size -1 ! = 10

 

NFS大量并发 fastdfs高并发处理_NFS大量并发

NFS大量并发 fastdfs高并发处理_NFS大量并发_02

NFS大量并发 fastdfs高并发处理_NFS大量并发_03

会抛出各种上传异常或者sockertExecption,但是这些异常都是偶发性的。比如上传1000次,可能有十几次失败,也可能百十次的失败。

刚开始以为是配置的通讯时间问题,修改了参数,改的大了一点,发现确实管点事,但是不能从根本解决问题。

#连接超时时间,针对socket套接字函数connect
connect_timeout = 300
#网络通讯超时时间,默认是60秒
network_timeout = 600

从网上查了一下,都说是客户端和服务端连接断开了,需要重试。可以服务端主动给客户端发送消息,保持连接

ProtoCommon.activeTest(trackerServer.getSocket());

在测试发现后,加这个也不管事,此时已经有点束手无策,肝源码看看吧

在  ProtoCommon.activeTest(trackerServer.getSocket());方法中,会发送一个空包给服务端,让服务端不断开连接,但是这个请求发出去就抛异常了。

public static RecvHeaderInfo recvHeader(InputStream in, byte expect_cmd, long expect_body_len) throws IOException
	{
		byte[] header;
		int bytes;
		long pkg_len;
		
		header = new byte[FDFS_PROTO_PKG_LEN_SIZE + 2];
		
		if ((bytes=in.read(header)) != header.length)
		{
			throw new IOException("recv package size " + bytes + " != " + header.length);
		}
		
		if (header[PROTO_HEADER_CMD_INDEX] != expect_cmd)
		{
			throw new IOException("recv cmd: " + header[PROTO_HEADER_CMD_INDEX] + " is not correct, expect cmd: " + expect_cmd);
		}
		
		if (header[PROTO_HEADER_STATUS_INDEX] != 0)
		{
			return new RecvHeaderInfo(header[PROTO_HEADER_STATUS_INDEX], 0);
		}
		
		pkg_len = ProtoCommon.buff2long(header, 0);
		if (pkg_len < 0)
		{
			throw new IOException("recv body length: " + pkg_len + " < 0!");
		}
		
		if (expect_body_len >= 0 && pkg_len != expect_body_len)
		{
			throw new IOException("recv body length: " + pkg_len + " is not correct, expect length: " + expect_body_len);
		}
		
		return new RecvHeaderInfo((byte)0, pkg_len);
	}

在上传的代码里也会调用这个方法,一上传包就挂了

NFS大量并发 fastdfs高并发处理_spring_04

解放方案:

高并发情况下扩大fastdfs最大连接数,根据服务器情况酌情扩大。

fastdfs5.05版本最大连接数256,科大讯飞语音接口,异步方法接收响应,然后在直接处理上传,相当于来了多少请求就会开多少连接,很快就会打满最大连接数,造成后面的上传异常,只有前面的上传完成后,放开了才能获取到服务端的连接,但是异步处理方法不是获取不到就阻塞住的,他是直接new了去上传,然后服务端没有连接线程来处理这个,就会返回一个空的路径或者是连接异常这种错误。