多媒体存储是每个业务系统搭建时必备功能,政通的业务系统中多媒体存储部分经历了几个阶段的演进,其结构如图所示:
服务器消息块(SMB)协议,即沿用自Windows部署时代的方案,同时可以在Linux上使用samba服务提供文件共享。
文件传输协议(FTP),搭建FTP服务提供文件服务。
超文本传输协议(HTTP),自建HttpFileServer,配合Nginx发布静态文件,通过Http协议上传和下载文件。
另外还有一些项目提供了对象存储服务,也可以归为使用HTTP协议。目前大部分项目都使用了基于Spring框架的HttpFileServer作为文件存储服务,好处在于部署简单、端口开放要求少(只需要一个端口),客户端开发简单(只需要实现几个REST接口的请求)。但在项目运行过程中,自建的HttpFileServer也暴露出了一些功能和性能上的问题,本文将从这些问题出发,探讨问题出现原因引出选择Netty的原因,并简述开发过程及最终成果。
起因是和DevOps的同学一起排查现场问题时,发现了Nginx 临时文件大小配置的坑。具体是指Nginx中proxy_max_temp_file_size
配置,默认是1024M。相关场景为:客户端需要根据附件ID请求从服务端下载附件,在这过程中需要进行鉴权、查询相关多媒体存储位置等操作,具体请求路径如下:
此时Nginx配置了缓存,proxy_max_temp_file_size
使用默认配置,由于客户端与服务端之间网络速度比较慢。此时发生的事情:
Nginx缓存文件很快就达到了1024M,Nginx 就停止从后端服务获取数据;
后端服务等待时间超过Socket Timeout配置后(设置为60s),服务端就抛出异常,停止此次下载请求,关闭连接;
客户端把1024M数据下完了,想继续请求后面数据,通过Nginx再请求服务端时,由于服务端不支持断点续传,就导致下载失败。
proxy_max_temp_file_size
配置调小,让客户端下的快一些。更深层次的分析可以单独写一篇文章了,各位同学可以自行查找资料,本文就不展开说明。DevOps的同学在验证多媒体相关接口性能的时候,给出的分析结果是:通过HttpFileServer发布的多媒体服务,多媒体获取性能只有Nginx的25%左右(调整了部署参数后)。
同时给了相关建议:
提升HttpFileServer性能;
不需要权限控制的情况下,使用Nginx发布多媒体目录。
考虑新版本HttpFileServer开发需要满足如下条件:
文件服务的基本功能:上传、下载、删除、重命名等;
兼容已有项目的部署和使用;
能够提供较高的并发性能。
Netty框架无疑是最佳的选择,其具有如下特性:
1.设计好
统一的API,支持各种协议实现。基于可扩展的事件驱动模型,方便开发。高度可定制的线程模型。可靠的无连接数据socket支持(UDP)。
2.并发高
Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),其并发性能得到了很大提高。
许多有名的软件或者框架都使用到了Netty,例如阿里的Dubbo默认使用Netty作为基础通信组件。
Netty服务启动后可以直接处理网络消息,不再需要SpringBoot启用Web服务,此时可以关闭SpringBoot的web服务功能。
@SpringBootApplication
public class HttpFileServerNetty {
public static void main(String[] args) {
new SpringApplicationBuilder(HttpFileServerNetty.class)
.web(WebApplicationType.NONE)
.run(args);
}
}
SimpleChannelInboundHandler<FullHttpRequerst>
,处理器做的事情为:文件上传:POST /home/httpfile/writefile
文件下载:GET /home/httpfile/readfile
文件删除:POST /home/httpfile/deletefile
3)路由正确匹配后,调用对应的方法进行文件处理。
4)文件操作相关异常处理。
业务处理器继承了Netty中常用的SimpleChannelInboundHandler,父类方法中并未实现全局异常处理方法,需要自行实现异常处理,否则异常信息会被“吞掉”,给问题排查带来困难。
实现示例
@Component
@ChannelHandler.Sharable
public class FileHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
// 消息处理方法
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 异常处理
}
}
1)Netty中需要自行解析请求头来实现断点下载。
TIPS:断点下载请求头,如下表示请求0-99字节的数据:
Range: bytes=0-99
Status Code: 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 0-99/1000
content-length: 1000
ChannelFuture sendFileFuture = null;
if (ctx.pipeline().get(SslHandler.class) == null) {
// 传输文件使用了 DefaultFileRegion 进行写入到 NioSocketChannel 中
sendFileFuture = ctx.write(new DefaultFileRegion(raf.getChannel(), startIdx, contentLength));
} else {
// SSL enabled - 不能使用 zero-copy 文件传输
sendFileFuture = ctx.write(new ChunkedFile(raf, startIdx, contentLength, 8192));
}
HttpObjectAggregator
只能配置为支持 Integer.MAX 大小的消息。另外在消息过大的时候,HttpObjectAggregator
进行消息合并时有可能出现"failed to allocate XXXX byte(s) of memory (used: XXX,max: XXX...)"的不能再分配内存的异常。一个小插曲:由于一开始没实现Handler的exceptionCaught方法,导致异常信息被隐藏,一直到逐行调试时才发现问题原因。
HttpObjectAggregator
,添加自定义处理逻辑,当从HttpRequestDecoder发送的消息内容实时写入硬盘,不做额外的合并工作。具体流程为:断点下载
配合业务系统的请求头信息传递功能实现后,完美实现断点下载的支持。
性能提升
条件限制,这里直接给个文件下载单机压测结果数据(受JMeter资源消耗影响)。
技术的发展与更新离不开业务的需求,在无论是并发量还是文件大小都在逐步增长的趋势下,有一个能够提供高并发、高可用、高性能的多媒体服务是系统发展的必然。本文从多媒体服务当前的功能、性能问题出发,分析了问题出现的原因,说明了选择Netty的原因,简述了基于Netty的HttpFileServer的开发过程并展示了最终提升的结果。总体来说Netty本质上就是对于Reactor响应式模型的实现,通过使用Pipeline责任链模式并基于NIO模型实现了一种更优性能的多媒体服务。
Netty作为一套优秀的框架,也是程序员更深入了解服务开发,技术进阶的必经门槛。文章所述仅仅是入门级别的知识,更多应用形式还需要各位同学动手敲敲代码展现出来。