在我印象中,似乎很少有关于文件操作的面试题,而大多数面试题都围绕着多线程、网络编程、RPC、数据库,但其实掌握文件操作也同等重要。只是我们很少会碰到需要操作文件的需求,毕竟百分之九十的工作都是依靠操作数据库、网络通信完成,而存储都被各类关系型数据库、分布式数据库、缓存、搜索引擎、甚至云存储替代了。
虽然偶尔我们也需要实现文件上传的接口,但文件上传一般都会选择存储到云端,顶多就临时转存一下,相信很多人会选择直接百度拷贝一份代码完成文件存储了事,甚至于都不关心是如何实现的。而多数的表格导出操作也都依赖一些现成的框架,以致于面向文件编程的重要性被弱化了。
如果我们去研究一些框架的底层源码,我们就能发现掌握文件操作其实也很重要。以RocketMQ
为例,RocketMQ
的消息存储并没有借用数据库,也没有借用其它第三方框架,仅仅是用文件存储。我很好奇,为什么没有面试题问RocketMQ
的消息存储实现原理。
我自己也开发过一些组件/框架/中间件,但由于文件操作这块知识太欠缺,首先想到的都是依赖一些第三方存储中间件/库实现,如Redis
、Mysql
、LevelDB
,这直接提升了框架的使用成本。所以我也一直知道掌握文件操作的重要性。
有时候我也在想,为什么部署Kafka
(旧版本)要部署一个Zookeeper
,而部署Zookeeper
的作用只是用于管理节点、消费者、实现Leader
选举。部署Zookeeper
为了保证Zookeeper
的可用性又要部署几个节点,这无疑增加了Kafka
的使用成本。所以当我看到Alibaba Sentinel
实现集群限流功能提供嵌入式模式时就很理解为什么要同时提供嵌入式部署和独立部署两种模式。
我去年开始着手自研一个分布式延迟调度中间件,其实核心功能早就实现了,也以嵌入式部署的方式在项目中支撑业务功能。但为了去掉依赖Redis
实现存储功能、第三方框架实现RPC
功能、广播机制实现Leader
选举功能,我才决定重新写一个。因此我用Raft
共识算法+LevelDB
(Key-Value存储库
)替代Redis
实现存储、基于Netty
自己封装RPC
框架、基于Raft
算法替代广播实现Leader
选举。这直接就降低了这款自研中间件的使用成本。而在实现Raft
算法的日记Appender
时,我又遇到了同样的槛,但这次我选择跨过去。
阿里开源的众多项目中,除RocketMQ
的消息存储使用文件存储外,Sentinel
存储资源指标数据统计也是使用文件存储,这两个框架在实现存储上都使用了同一种设计思想,即数据文件+索引文件。我在自研分布式延迟调度中间件中就借鉴了RocketMQ
与Sentinel
中的文件存储索引设计,数据文件存储日记,而索引文件存储日记ID
与日记在数据文件中的物理偏移量。
Sentinel
按精确到秒的时间戳存储索引,和时间戳是有序增长的,而且时间戳是long
类型占8个
字节,根据单个指标文件的最大大小,物理偏移量也正好可以是long
类型,因此每个索引占16
个字节。资源指标数据可能由于某段时间没有请求或者应用重启导致某些时间戳没有记录,但至少时间戳是单调递增的,因此我们只需要采用简单的折半查找就能快速定位到索引。由于Sentinel
资源指标数据收集不需要考虑高并发,这样的设计足以满足需求。
RocketMQ
需要提供可以通过key
或时间区间来查询消息的功能,因此RocketMQ
的索引存储实现相对Sentinel
较难。单个索引文件固定的文件大小约为400M
,一个索引文件可以保存2000W
个索引,索引文件的底层存储设计相当于是在文件系统中实现HashMap
结构,每个文件头存储了此文件存储的消息的最小时间戳和最大时间戳,这用于实现按时间区间搜索消息记录。消息key
的hash
值则作为索引项存放在索引文件中的物理偏移量,当然,还要加上文件头的大小,以及乘以单项索引占用的字节数。
你或许觉得,RocketMQ
与Sentinel
实现索引难在算法,的确,算法是灵魂。但软件的强大依然需要依赖硬件的支持,你是否考虑到,如何跳转到文件中的某个位置读取指定字节的数据,又如何改写文件中指定位置的数据?如何考虑并发读写问题,如何调优性能?是实时写文件呢,还是参考Mysql
的Binlog
、Redis
的RDB
刷盘策略?文件的NIO
如何理解、如何使用MappedByteBuffer
提升性能以及原理是什么?
解读这几个问题是我学习文件读写的目的,也体现了掌握面向文件编程的重要性。
最后,虽然为了提升工作效率以及降低犯错率我们并不需要重复造轮子,但重复造轮子无疑是提升自身能力最高效的学习方法。写出来的东西并不一定就要用,但为了写出一款好的开源作品,我会不断的尝试。