最近接了某需求,是需要把文字转换成语音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
会抛出各种上传异常或者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);
}
在上传的代码里也会调用这个方法,一上传包就挂了
解放方案:
高并发情况下扩大fastdfs最大连接数,根据服务器情况酌情扩大。
fastdfs5.05版本最大连接数256,科大讯飞语音接口,异步方法接收响应,然后在直接处理上传,相当于来了多少请求就会开多少连接,很快就会打满最大连接数,造成后面的上传异常,只有前面的上传完成后,放开了才能获取到服务端的连接,但是异步处理方法不是获取不到就阻塞住的,他是直接new了去上传,然后服务端没有连接线程来处理这个,就会返回一个空的路径或者是连接异常这种错误。