一、业务场景
客户有一批数据,里面大约有1万7千个mbtiles文件(可以理解为紧凑型数据),本质就是sqlitedb数据库。现在需要去访问mbtiles数据库中的pbf瓦片(二进制),以流的形式返回给前端进行渲染。
二、实现方案
场景如上,可得出三种解决方案
1、直接访问mbtiles,根据一定的规则找到对应的pbf瓦片并返回给前端
优点:不需要将内部的数据还原,直接操作即可
缺点:
- sqlitedb这种嵌入型数据库存在一定的并发问题。
- 更换服务地址需要迁移数据,数据量大迁移慢
因为客户对并发量有一定的要求 于是果断放弃方案1
2、将mbtiles中的文件还原并上传到分布式存储中
基于该方案,我试过操作两种方式,但都存在不同程度的问题
-
先将文件写到服务器磁盘上并将文件上传到分布式存储上
写到磁盘上很快,但是写着写着发现 服务器的inode数满了!
原来这个mbtiles中存在大量的小文件,全部还原后预计有两亿个,使用 df -i 命令查看服务器的inode数发现只有一亿个左右,没办法再继续写入了。inode译成中文就是索引节点,每个存储设备(例如硬盘)或存储设备的分区被格式化为文件系统后,应该有两部份,一部份是inode,另一部份是Block,Block是用来存储数据用的。而inode呢,就是用来存储这些数据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。inode为每个文件进行信息索引,所以就有了inode的数值。操作系统根据指令,能通过inode值最快的找到相对应的文件。
这种情况的原因通常是:尽管那个分区的磁盘占用率未满,但是inode已经用完,应该是该磁盘的某些目录下存在大量的小文件导致。尽管小文件占用的磁盘空间并不大,但是数量太多,inode用尽。 -
将文件直接写到分布式存储的挂载目录上
竟然直接写到磁盘上不行,那试试写到挂载目录上呢,我们选型的分布式存储是 seaweed ,df -i 查看seaweed fuse挂载发现inode数几乎为无限。那就试试吧,但是发现速度非常的慢,可能是seaweed上传的时候还需要对数据进行备份,且小文件让seaweed的速度提升不上来。加上fuse每写入一个文件就要向temp文件夹中写入一个日志,一下子日志就满了,导致seaweed无法再写入文件了。
3、将数据写到mongodb中。
竟然写到磁盘上和挂载上都不行,那能不能写到数据库中呢?答案是当然可以。
那为什么选mongodb呢?分析一下业务场景:
1)读>写:全球矢量基本只会入库一次,而读则是每次加载场景都会产生大量的并发
2)支持二进制类型的数据存储
3)不存在复杂的业务逻辑,只做简单的查询
而MongoDB具有良好的写入性能和读取性能,可以处理大量的数据和高并发请求。这使得MongoDB适合处理需要高速数据访问和响应的应用程序。同时Mongodb也能很好的支持二进制类型的数据,虽然在复杂业务上语句不好写,但是恰恰我们这次只用做简单的查询。
于是就选择了mongodb:
得益于瓦片数据的文件目录的固定规则,我们可以简单的将z层级作为集合,文件夹的名称作为列x,文件的名称作为字段y,而文件的二进制则是单独一个字段 data。
存在数据库中就不用担心inode节点的问题啦,测试发现速度起码提升10倍以上!不过由于文件过多 再快也是需要时间的。
4、选定了方案后,还做了以下的优化
- 还原文件的脚本使用了多线程去处理,向数据库中提交数据的时候使用批量提交 减少与数据库之间的连接消耗
- 将一万七千个Mbtiles 分组 1000个文件一组 同时启动多个脚本进行写入。
- 调整mongodb的内存,让他足以支持并发写入。
- 12-16级的文件较多,其实可以再细分,比如不单按z来划分集合,还可以按 z 和 x % 100 将数据更加分散一点,减少一个集合中的数据量。
解决全球矢量的数据入库问题后,正打算以同样的方式将全球影像一起入库。然而发现影像的大小是矢量的20倍(未解压时 70T)
查看磁盘,发现单节点mongodb所在的磁盘存储完全不足,怎么办呢?
查看mongodb官网文档,mongodb有一个拓展机制,叫做分片集群( Sharding — MongoDB Manual)
当时我采取的部署分布 参考如下:
节点 \ 组件 | mongos 路由组件 | configServer 配置组件 | shard 分片集群1 | shard 分片集群2 | shard 分片集群3 |
---|---|---|---|---|---|
10.0.2.30 | 27017 | 27018 | 27019 主 | 27021 | |
10.0.2.31 | 27017 | 27018 | 27019 | 27020 主 | |
10.0.2.34 | 27017 | 27018 | 27020 | 27021 主 |
通过合理的设置shard key 将数据分布在不同的服务器上,以达到横向拓展的目的。
同时使用nginx对mongos做负载均衡以提高连接数的上限,方便脚本入库。
5、将文件写入到内存再读取,代替seek
然而全球影像和全球矢量的数据的体量完全不是同一个等级,且由于脚本原本使用seek对pyr文件进行读取,在大文件随机寻址时非常的慢。后面采用mmap代替seek优化脚本,将文件映射到内存中访问,以提高读取性能。
#优化前
def getData(filename,index):
file_path = fileMap.get(filename)
fp = open(file_path, "rb")
fp.seek(28 + index * 12) # 将文件指针移动到第k个字节
buf = fp.read(12)
arr = struct.unpack("qi", buf)
offset = arr[0]
size = arr[1]
if offset < 0 or size <= 0 or offset + size > file_length:
return None
fp.seek(offset) # 将文件指针移动到指定偏移量
data = fp.read(size)
fp.close()
return data if data != b'' else None
#优化后
def getData(fp, index):
with mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as mm:
start_offset = 28 + index * 12
buf = mm[start_offset:start_offset + 12]
arr = struct.unpack("qi", buf)
offset = arr[0]
size = arr[1]
# Check for invalid offset or size
if offset < 0 or size <= 0 or offset + size > mm.size():
return None
data = mm[offset:offset + size]
return data if data != b'' else None
完毕