cover_image

政通多媒体存储服务的进阶之路

皮卡丘 政通技术团队
2021年12月24日 07:16

1.背景

多媒体存储是每个业务系统搭建时必备功能,政通的业务系统中多媒体存储部分经历了几个阶段的演进,其结构如图所示:

图片
在传统单体服务的框架下,“多媒体权限管理”和“业务数据关联”部分集成到了业务系统中,作为业务系统中的多媒体管理模块来实现;多媒体管理模块通过相关协议来调用后端的管理服务进行多媒体存取操作,其中最重要的几个功能是文件上传、文件下载和文件删除。管理模块所依赖的操作协议经历了几次更改:
  • 服务器消息块(SMB)协议,即沿用自Windows部署时代的方案,同时可以在Linux上使用samba服务提供文件共享。

  • 文件传输协议(FTP),搭建FTP服务提供文件服务。

  • 超文本传输协议(HTTP),自建HttpFileServer,配合Nginx发布静态文件,通过Http协议上传和下载文件。

另外还有一些项目提供了对象存储服务,也可以归为使用HTTP协议。目前大部分项目都使用了基于Spring框架的HttpFileServer作为文件存储服务,好处在于部署简单、端口开放要求少(只需要一个端口),客户端开发简单(只需要实现几个REST接口的请求)。但在项目运行过程中,自建的HttpFileServer也暴露出了一些功能和性能上的问题,本文将从这些问题出发,探讨问题出现原因引出选择Netty的原因,并简述开发过程及最终成果。

2 相关问题

2.1 断点下载的缺失

起因是和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配置调小,让客户端下的快一些。更深层次的分析可以单独写一篇文章了,各位同学可以自行查找资料,本文就不展开说明。
抛开业务端转发的逻辑,这次也暴露出了HttpFileServer的不足:竟然没有实现文件下载的断点续传。

2.2 文件服务性能

DevOps的同学在验证多媒体相关接口性能的时候,给出的分析结果是:通过HttpFileServer发布的多媒体服务,多媒体获取性能只有Nginx的25%左右(调整了部署参数后)。

同时给了相关建议:

  • 提升HttpFileServer性能;

  • 不需要权限控制的情况下,使用Nginx发布多媒体目录。

其中第二点修改方式不满足一些项目的需求,原因有两点:一是业务产品的多媒体访问权限管理是在业务系统实现,即业务系统不修改的情况下,多媒体的使用必须通过HttpFileServer;二是在复杂网络条件下,为了减少实施运维的配置量,前端访问多媒体是经过了业务系统接口进行数据流转发,需要根据多媒体标识从多媒体服务获取对应文件。
既然存在必须使用多媒体服务的场景,就需要想办法提升HttpFIleServer的性能。

3 选择Netty的原因

考虑新版本HttpFileServer开发需要满足如下条件:

  • 文件服务的基本功能:上传、下载、删除、重命名等;

  • 兼容已有项目的部署和使用;

  • 能够提供较高的并发性能。

Netty框架无疑是最佳的选择,其具有如下特性:

1.设计好

统一的API,支持各种协议实现。基于可扩展的事件驱动模型,方便开发。高度可定制的线程模型。可靠的无连接数据socket支持(UDP)。

2.并发高

Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),其并发性能得到了很大提高。

图片
3.传输快
Netty建立在NIO基础上,在NIO之上又提供了高层次的抽象,提供了最基础的并发性能保障。同时借助NIO的零拷贝特性,通过ByteBuf直接对数据进行操作,加快传输速度。
4.封装好
提供各种类型消息的Encoder、Decoder、Handler(例如HttpResponseEncoder、HttpRequestDecoder、ChunkedWriteHandler),通过Pipeline整合后进行消息处理。高度灵活的接口定义,可以实现自己的消息转换类,自行解析协议。

许多有名的软件或者框架都使用到了Netty,例如阿里的Dubbo默认使用Netty作为基础通信组件。

4 开发过程

4.1 开发思路

HttpFileServer的应包含请求鉴权、文件读写。服务核心是文件读写,鉴权部分兼容原应用的鉴权方式即可,文件读写部分自行开发代码实现。
官方示例中有简单的文件服务实现,但未考虑文件大小、异常处理等问题,有兴趣的同学可以去GitHub自行查看。

4.2 应用入口

Netty仅是一套开发框架,需要有其他web容器支撑来启用服务。为方便项目启动和后续扩展,选用了Springboot为启动器(也仅仅是作为启动器和Bean管理),整个项目显式引入的依赖仅两个:spring-boot-starter 和 netty-all 。其中netty-all版本为4.1.67.Final(截止发稿时,Netty5已经发布的最新版本为5.0.0.FINAL-SNAPSHOT,Netty4最新版本为 4.1.72.FINAL)。

Netty服务启动后可以直接处理网络消息,不再需要SpringBoot启用Web服务,此时可以关闭SpringBoot的web服务功能。

Tips: SpringBoot项目关闭web服务可以在main方法中处理,示例:
@SpringBootApplication
public class HttpFileServerNetty {
    public static void main(String[] args) {
        new SpringApplicationBuilder(HttpFileServerNetty.class)
                .web(WebApplicationType.NONE)
                .run(args)
;
    }   
}

4.3 Pipeline

在Nio网络编程模型中, 服务端和客户端进行IO数据交互(得到彼此推送的信息)的媒介称作channel,channel是一个用于连接字节缓冲区和另一端的实例(例如Socket或者文件)。Netty通过对JDK原生的ServerSocketChannel进行封装增强,形成NioxxxxChannel,从而能够提供更多能力。Pipeline就是其中的核心部分,通过采用责任链模式,实现请求发起与请求处理的解耦,责任链上的处理器负责处理请求,客户端只需将请求发送到责任链上即可,每个消息通道Channel都有自己的ChannelPipeline,入站和出站消息的操作都会在Pipeline定义的处理器中进行。
HttpFileServer的Pipeline设置如图:
图片
  • IdleStateHandler,用于配置相关超时事件参数;
  • StateHandler,自定义实现,用于处理部分IdleStateHandler检查超时后触发的事件;
  • HttpRequestDecoder,Http请求解码;
  • HttpResponseEncoder,Http请求编码,可以使用 HttpServerCodec 替代HttpRequestDecoder和HttpResponseEncoder;
  • HttpObjectAggregator,用于Http消息聚合,在Netty中,一个Http请求最少也会在HttpRequestDecoder里分两次往后传递,第一次是消息行和消息头,第二次是消息体。如果消息体比较大,可能还会分成多次传递,这就需要HttpObjectAggregator来将消息聚合为一个完整的Http请求;
  • FileHandler,自定义业务处理器,即HttpFileServer的核心处理逻辑,这里直接以文件处理器命名(FileHandler)。

4.4 业务处理器

核心业务处理器实现了SimpleChannelInboundHandler<FullHttpRequerst>,处理器做的事情为:
1)请求解析:处理经过HttpObjectAggregator聚合后的完整Http请求,提取uri、参数等信息。
2)路由匹配:解析资源路径,将请求匹配到正确的方法中。即匹配到对外提供的最主要方法:
    • 文件上传: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 {
         // 异常处理
     }
}

4.5 需要注意的问题

文件下载

1)Netty中需要自行解析请求头来实现断点下载。

TIPS:断点下载请求头,如下表示请求0-99字节的数据:

Range: bytes=0-99
断点下载返回头,表示总大小1000字节的数据,这次返回0-99字节:
Status Code: 206 Partial Content

Accept-Ranges: bytes
Content-Range: bytes 0-99/1000
content-length: 1000
2)Netty提供的零拷贝的文件传输实现,但在启用SSL的时候不能使用,此时可以使用 ChunkedFile。
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));
}
DefaultFileRegion内部也是使用 FileChannel 的 transferTo 方法来实现零拷贝传输的。
文件上传
文件上传需要注意的是大文件的处理,Netty自带的HttpObjectAggregator只能配置为支持 Integer.MAX 大小的消息。另外在消息过大的时候,HttpObjectAggregator进行消息合并时有可能出现"failed to allocate XXXX byte(s) of memory (used: XXX,max: XXX...)"的不能再分配内存的异常。
一个小插曲:由于一开始没实现Handler的exceptionCaught方法,导致异常信息被隐藏,一直到逐行调试时才发现问题原因。
在参考了其他框架的文件上传处理逻辑后,最终使用的方案为继承HttpObjectAggregator,添加自定义处理逻辑,当从HttpRequestDecoder发送的消息内容实时写入硬盘,不做额外的合并工作。具体流程为:
图片
其中缓存文件位置既支持统一设置缓存文件夹,也支持在真实目标目录下创建临时文件,上传完成后重命名为正式文件名称。

5 成果

断点下载

配合业务系统的请求头信息传递功能实现后,完美实现断点下载的支持。

性能提升

条件限制,这里直接给个文件下载单机压测结果数据(受JMeter资源消耗影响)。

图片

6 总结

技术的发展与更新离不开业务的需求,在无论是并发量还是文件大小都在逐步增长的趋势下,有一个能够提供高并发、高可用、高性能的多媒体服务是系统发展的必然。本文从多媒体服务当前的功能、性能问题出发,分析了问题出现的原因,说明了选择Netty的原因,简述了基于Netty的HttpFileServer的开发过程并展示了最终提升的结果。总体来说Netty本质上就是对于Reactor响应式模型的实现,通过使用Pipeline责任链模式并基于NIO模型实现了一种更优性能的多媒体服务。

Netty作为一套优秀的框架,也是程序员更深入了解服务开发,技术进阶的必经门槛。文章所述仅仅是入门级别的知识,更多应用形式还需要各位同学动手敲敲代码展现出来。

修改于2021年12月24日
继续滑动看下一个
政通技术团队
向上滑动看下一个