Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

对象存储 #7

Open
renyh opened this issue May 22, 2018 · 2 comments
Open

对象存储 #7

renyh opened this issue May 22, 2018 · 2 comments

Comments

@renyh
Copy link
Collaborator

renyh commented May 22, 2018

书目记录及下级对象资源的文档结构

类型 备注
_id ObjectId dp3成员,代替dp2的id。由MongoDB自动生成的文档唯一标识,例如 ObjectId("5aaceadd3b683f1598ceb894"), 12字节,每个字节两位16进制数字。
range String 资源的字节范围,资源不完整时值形态为#0~(当前长度-1)多个range以逗号分隔,完整时值为#在dp2系统中,资源可以分块多次保存,一块的尺寸前端自己定义,目前一块尺寸是500KB(500*1024),超过这个尺寸会分成多次保存,该字段主要用于存储资源未完整时的字节范围。
dptimestamp String 资源的时间戳,形态为十六进制文本字符串,从左到右每二个字符表示一个byte的值。例如568c4a13a4b8d5080000000000000018,当资源不完整时值为null
newdptimestamp String 资源的临时时间戳,当资源不完整时有值,完整后为null,在dp2系统中,主要用于大的资源分块多次保存时,每次写后的时间戳。
metadata String 资源元数据内容,资源未完整时为null。对于书目记录,元数据格式为<file size="4145" lastmodified="2018/5/13 15:58:54" />;对于对象资源,元数据格式一般为。<file mimetype="application/octet-stream" localpath="C:\0-d\0-测试\大备份与恢复测试\l1.dp2bak" size="3298463" lastmodified="2018/5/13 15:35:18" />,dp2是这样设计的,在想dp3对于书目记录,是否就分成两个成员size和lastmodified;对于对象资源元数据字段多时再用xml表示或者也分成字段。
filename String 对于MySQL、SQLite数据库,资源本身存储在本地文件,当资源完整时,该字段存储文件名。如0000000\001。如果资源不完整值为null。
newfilename String 当资源不完整时,该字段存储临时文件名。如0000000\001_0.temp。资源完整值为null。
files Collection dp3新增的在员,用于存储对象资源,待讨论确认。

问题一:在MongoDB中,书目下的对象资源,是否用内嵌的文档来存储?
在dp2系统,书目记录与其下级的对象资源是作为1+n条记录存储在同一个数据表中,他们记录路径id有内在关联,例如书目记录的id是0000000001,它的第1个对象的路径id为0000000001_0,第2个对象的路径id为0000000001_1

外部访问资源的路径也有规则,书目记录路径为:数据库名/书目记录id,例如中文图书/1
书目下的对象路径格式为数据库名/书目记录id/object/对象id,例如中文图书/1/object/0

在MongoDB中,书目记录与其下级的对象,可以考虑用一条文档存储,书目文档中有个成员是files,files是个集合,用于存储下级对象。

目前觉得对象的结构与书目记录结构类似。

问题二:关于像书目Xml这类资源的结构,是否有些字段可以不要
如果将书目与对象资源分成不同的结构,对于书目记录结构,不知是否会超过500KB(一般不会吧),如果不超过就可以一次性保存,那么还需要这些range,newdptimestamp,newfilename字段吗?

另外考虑数据库中第一层的结构,可能会存除了书目xml之外的资源,比如新闻一类其它信息,尺寸比较大的情况。

问题三:在MongoDB中,考虑是用二进制类型,还是用本地文件
MongoDB的二进制类型,最大尺寸限制是16MB,对于XML一类是够了。
对于大的对象,有可能超过这个尺寸。MongoDB有关于大文件存储的GridFs,还没有学习。是否先考虑参考dp2用本地文件存储大对象。

@renyh
Copy link
Collaborator Author

renyh commented May 22, 2018

GridFS实现原理

GrudFS是一种将大型文件(超过16MB)存储在MongoDB数据库中的文件规范。MongoDB各个语言驱动均实现了GridFS规范,都可以实现将大型文件存储到MongoDB数据库中。

GridFS的原理就是将大文件分割成许多小块,然后将这些小块封装成BSON对象,存放到特意为GridFS准备的集合。大文件分成小块后,每块作为一个单独的文档存储,然后用一个特别的文档记录来存储分块的信息和文件的元数据,也就是记录这些小块装的是哪一段信息,先后顺序是怎样的,等到用的时候就能拼接起来返回一个完整的大文件。

默认情况下MongoDB为GridFS准备的集合是fs.filesfs.chunks

  • fs.files:用来存储文件元数据信息
  • fs.chunks:用来存储二进制数据块

fs.files中的每个文档代表一个文件,结构如下

{
  "_id" : <ObjectId>,   //文件唯一的id,在fs.chunks作为“files_id”键的值
  "length" : <num>,   // 文件内容总字节数
  "chunkSize" : <num>,  //每块的大小,以字节为单位。默认是256KB,必要时可以调整。
  "uploadDate" : <timestamp>,  // 上传时间,The date the document was first stored by GridFS. This value has the Date type.
  "md5" : <hash>,     //文件内容的md5校验和,An MD5 hash of the complete file returned by the filemd5 command. This value has the String type.
  "filename" : <string>,    //Optional. A human-readable name for the GridFS file.
  "contentType" : <string>,  //Optional. A valid MIME type for the GridFS file.
  "aliases" : <string array>,  //Optional. An array of alias strings.
  "metadata" : <any>,  //Optional. The metadata field may be of any data type and can hold any additional information you want to store.
}

fs.chunks用于存储数据块,结构如下:

{
  "_id" : <ObjectId>, // 块自己唯一的id
  "files_id" : <ObjectId>,  //包含这个块元数据的文档的_id,对应fs.files集合中文档的_id
  "n" : <num>,  //表示块编号,也就是这个块在原文件中的顺序编号
  "data" : <binary> //文件块的二进制数据
}

看到下面这条注意事项,需要测试下:
因为GridFS在上传文件过程中是先把文件数据保存到fs.chunks,最后再把文件信息保存到fs.files中,所以如果在上传文件过程中失败,有可能在fs.chunks中出现垃圾数据。这些垃圾数据可以定期清理掉。

@renyh
Copy link
Collaborator Author

renyh commented May 22, 2018

dp2kernel 有一个对象管理子系统。可以尝试用一下 mongodb 的 GridFS 试试:
https://stackoverflow.com/questions/4988436/mongodb-gridfs-with-c-how-to-store-files-such-as-images

试验了给GridFS写入文件和从GridFS读取文件的操作,下面是代码:

        // 给MongoDB的GridFS中写入文件
        public static ObjectId UploadFile()
        {
            string conn = "mongodb://localhost:27017";
            MongoClient client = new MongoClient(conn);
            var database = client.GetDatabase("TestDB");
            var fs = new GridFSBucket(database);

            using (var s = File.OpenRead(@"C:\0-d\\myimg.png"))
            {
                var t = Task.Run<ObjectId>(() => {
                    return  fs.UploadFromStreamAsync("test.txt", s);
                });
                return t.Result;
            }
        }

        // 从MongoDB的GridFS中读取文件
        public static void DownloadFile()
        {
            string conn = "mongodb://localhost:27017";
            MongoClient client = new MongoClient(conn);
            var database = client.GetDatabase("TestDB");
            var fs = new GridFSBucket(database);

            var t = fs.DownloadAsBytesByNameAsync("test.txt");
            Task.WaitAll(t);
            var bytes = t.Result;
        }

你需要去了解一下mongodb 的这个 GridFS。我可以说一些自己的体会和需求,如果 GridFS 能直接满足最好了,如果不能直接满足,也有可能通过基于它包裹一层处理来间接满足。
dp2kernel 在设计这个对象管理系统的时候,是允许对象尽可能大的,即操作系统文件允许多大,一个对象就可以多大。所以在上传和下载对象的时候,肯定是分片进行的也可以提供类似 Stream 的 API,模仿文件指针,文件读写操作。比如前一段刚开始设计的从 pdf 文件中预览的功能,有关开源模块就要用到 Stream 接口,以便从对象中获取一部分内容创建 .jpg 文件。

GridFS有Stream相关api
image


在上传和下载对象内容的过程中,支持多前端并发操作。并发下载没有太大问题,比较简单。但并发上传就比较麻烦了,首先是不同的前端分别独立地上传去覆盖修改同一个位置的对象,这个需要建立不同的会话,而且最好要有时间戳保护,避免交替写入混乱的内容。
dp2kernel 的一个对象记录,**利用了两套时间戳字段,意图是让这两个字段轮转,实现一边允许上传内容到临时区,一边同时允许下载进行。**比如一个大的对象需要上传一天,这一天中你不可能禁止别人下载。那样系统就没法实用了。这也是初步的会话的概念,但不完满。如果这次 dp3 要做这样的功能,可以允许多个会话,每个会话都有独立的存储区,等对象上传完成后瞬间突然切换到正式位置。

现在还不知道GridFS的fs.fiels和fs.chunks是否有这些机制,看到fs.files结构中有uploadDate与md5这两个字段,还不清楚是怎么用的?


**dp2kernel 也利用了对象存储机制来存储 XML 记录。**XML 记录可以理解为元数据 ,一般不会有那么大,但从存储角度来说和对象数据其实没有什么区别,所以 dp2kernel 就用同一套设施了。从长远来看,你也确实不清楚前端是不是要用一个一百兆的 XML 文件,对不对。XML 也不总是用 XmlDocument 处理,还可以用 XmlReader 处理,reader 就是允许文件无限大的。比如 dp2 系统里面的发票库的发票记录。很难说一张发票里面总共要包括多少条册记录详情。可能会很大。
dp3 设计时候的中立格式,是为了方便处理的一个中间格式,是可以限制它的大小的。但原始数据不好做出尺寸限制。**所以原始数据存储问题必须得到彻底解决。**这是不同的问题。

那就是说书目xml也可能很大,也需要使用对象资源一样的存储办法?


dp2kernel 直接用一个一个文件来存储对象数据,一个文件对应一个对象。这是过分使用了文件系统的能力。也不是没有缺点。缺点就是拷贝的时候,成千山万个文件会有某些问题。另外初始化数据库的时候,要一瞬间删除掉成千上万的物理文件,耗时很长。* 如果是用一个大文件,在里面划分小块,构成链表来存储对象信息,则可能更体面合理一些。如果 GridFS 能用,也可以看看它是怎么实现的,效果如何。*

GridFS本质上还是建立在MongoDB的基本功能上,每个文件元数据信息放在fs.files集合里,每个大文件分成了许多小块作为单独的文档存放的数据块集合fs.chunks,两个集合关联起来,用的时候再拼接成完整 大文件。


在开发和探索的时候,可以分阶段实现不同的模块。* 刚开始为了集中精力解决对象存储以外的问题,可以把对象存储用一些简单的机制实现,先不考虑并发上载等复杂问题。* 等后面有精力了再过来替换模块或者增补开发。对象存储这部分接口可以早期制定好,保持稳定,不会影响到其他模块的开发。
以后的正式产品中,对象存储引擎这个部分也可以提供不同的版本,让用户选用。在开发阶段多实现几个不同的版本,也有利于考验接口的合理性。
考虑到联合编目的情形,可能同一条 MARC 记录,要允许保存所有历史版本。所以存储这个模块还需要比 dp2kernel 更进步。

目前卡在底层存储这里,不知道怎么突破。

@renyh renyh changed the title GridFS 对象存储 May 22, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant