[翻译] MaxMind DB 文件格式规范
MaxMind DB 文件格式规范
来源:http://maxmind.github.io/MaxMind-DB/
翻译:御风 2017-03-23
--------------------------------------------------
描述(Description)
MaxMind DB 文件格式是使用高效的二进制搜索树将IPv4和IPv6地址映射到数据记录的数据库格式
--------------------------------------------------
版本(Version)
这个文档是版本2.0的MaxMind DB二进制格式的规范
版本号由单独的主版本和次要版本号组成。它不应被认为是十进制数。也就是说,版本2.10在版本2.9之后
能够读取给定格式的主要版本的代码应适用小版本格式
--------------------------------------------------
概述(Overview)
二进制数据库被分成三个部分:
1、二叉搜索树。每个级别的树对应于一个位在128位表示的IPv6地址
2、数据段。这些值返回到客户端的特定IP地址,如“US”,“New York”,或一个更复杂的Map类型组成的多个字段
3、数据库元数据。数据库本身的信息
--------------------------------------------------
数据库元数据(Database Metadata)
数据库的这一部分存储在文件的结尾。它最先记录,因为了解一些元数据是了解其他部分如何工作的关键
通过查找二进制序列匹配,可以找到此节“\\xab\\xcd\\xefMaxMind.com”。文件中此字符串的最后一次出现标志着数据段的结束和元数据的开始。
由于我们允许任意的二进制数据的数据段,其他一些数据可以包含这些值。这就是为什么你需要找到这个序列最后一次出现位置的原因
对于元数据部分所允许的最大大小,包括标记开始的元数据,是128KB
元数据存储为Map数据结构。这种结构的规范在稍后描述。改变键的数据类型或删除键,将会影响主版本
除非另有规定,数据库所需的每个键都被认为是有效的
添加键构成小版本更改。删除键或更改其类型则构成主版本更改
当前格式版本的已知键列表如下:
node_count
这是一个无符号的32位整数,表示搜索树中的节点数
record_size
这是一个无符号的16位整数。它指示搜索树中记录中的位数。注意每个节点由两个记录组成
ip_version
这是一个无符号的16位整数,总是为4或6。它指示数据库是否包含IPv4或IPv6地址数据
database_type
这是一个字符串,指示与IP地址相关联的每个数据记录的结构。这些结构的实际定义由数据库创建者决定
以“GeoIP”开头的名字是保留给MaxMind使用的标记,“GeoIP”也是一个保留的标记
languages
一个字符串数组,每个字符串都是一个区域代码。给定的记录可能包含已定位到某些或所有这些区域的数据项。记录不应该包含不包含在这个数组中的局部地区数据
这是一个可选的键,因为这可能并不适用于所有类型的数据
binary_format_major_version
这是一个无符号的16位整数,表示数据库二进制格式的主要版本号
binary_format_minor_version
这是一个无符号的16位整数,表示数据库二进制格式的次要版本号
build_epoch
这是一个无符号的64位整数,表示数据库生成时Unix的时间戳
description
这个键始终指向一个Map。Map的键是语言代码,值在对应语言里是UTF-8编码的字符串
代码可能包括脚本或国家标识符等附加信息,像“zh-TW”或“mn-Cyrl-MN”。附加标识符将由破折号字符分隔(“-”)
这是一个可选的键,不过还是建议创建者至少包含一种以上的语言描述
--------------------------------------------------
计算搜索树截面大小(Calculating the Search Tree Section Size)
计算搜索树的字节大小的公式如下:
(($record_size * 2) / 8) * $number_of_nodes
搜索树的结束标志着数据段的开始
--------------------------------------------------
二叉搜索树部分(Binary Search Tree Section)
数据库文件以二进制搜索树开始。在树中的节点的数目取决于有多少唯一的网络块(网段)是特定数据库所需的。例如,城市数据库比国家数据库需要更多小的网络块(网段)
最顶部的节点总是位于搜索树节的地址空间的开始处。
每个节点由两个记录组成,每个记录是指向文件中地址的指针。
指针可以指向三个事物中的一个。
首先,它可能指向搜索树地址空间中的另一个节点。下列指针作为IP地址搜索算法的一部分,如下所述
指针可以指向一个值等于$number_of_nodes的地址。如果是这种情况,则意味着我们正在搜索的IP地址不在数据库中
最后,它可能指向数据段中的一个地址。这是给定的网络块(网段)有关的数据
--------------------------------------------------
节点布局(Node Layout)
搜索树中的每个节点由两个记录组成,每一个都是指针。记录大小因数据库而异,但单个数据库节点记录内的大小始终相同。记录长度可以从24位到128位,依赖于在树中的节点的数目。
这些指针存储在大端(Big Endian)格式(大字节序、高字节序)
下面是一些如何在24、28和32位记录的节点中设置记录的例子。大多数的记录大小遵循同样的模式
24位(小数据库),一个节点是6字节
| <------------- node --------------->|
| 23 .. 0 | 23 .. 0 |
28位(中等数据库),一个节点是7字节
| <------------- node --------------->|
| 23 .. 0 | 27..24 | 27..24 | 23 .. 0 |
注意,每个指针的最后4位被组合到中间字节中
32位(大数据库),一个节点是8字节
| <------------- node --------------->|
| 31 .. 0 | 31 .. 0 |
--------------------------------------------------
搜索查找算法(Search Lookup Algorithm)
首先将IP地址转换成大端二进制数据。对于IPv4地址,就是32位数据。对于IPv6就是128位数据
最左边的点对应于在搜索树的第一个节点。对于每一个比特,值为0表示我们选择节点中的左记录,值为1表示选择节点中的右记录
记录值总是被解释为无符号整数。整数的最大大小取决于记录中的位数(24、28或32)
如果记录值是小于搜索树中节点数(不是字节数,而是实际节点数)的数目(这是存储在数据库中的元数据),那么这个值是一个节点编号。在这种情况下,我们可以从那里重复查找找到搜索树中的节点
如果记录值等于节点数,这意味着我们没有任何IP地址的数据,搜索到此结束
如果记录值大于搜索树中的节点数,则它是指向数据段的实际指针值。指针的值是从数据段的开始计算的,而不是从文件的开始
为了确定我们应该在哪里开始寻找数据段,我们使用下面的公式:
$data_section_offset = ($record_value - $node_count) - 16
16是数据段分隔符的大小(详见下文)
最好是通过一个例子说明我们减去$node_count的原因:
假设我们有一个24位的树,有1000个节点。每个节点包含48位,或6字节。树的大小是6000字节
当树中的记录包含 <1000 的数值时,这是一个节点号,我们查找那个节点。如果记录包含值 >1016,我们知道它是一个数据段值。我们减去节点数量(1000),然后减去数据段分隔符的16,得到的数值0,是数据段的第一个字节
如果记录包含值6000,这个公式将给我们偏移到数据段的4084
为了确定此偏移量真正指向的文件中的位置,我们还需要知道数据段的起始位置。这可以通过确定搜索树的大小以字节计算,然后为数据段分隔符添加一个额外的16字节
因此,最后的公式,以确定在文件中的偏移量是:
$offset_in_file = ($record_value - $node_count) + $search_tree_size_in_bytes + 16
--------------------------------------------------
IPv6树中的IPv4地址(IPv4 addresses in an IPv6 tree)
在IPv6树中存储IPv4地址时,按原样存储,所以它们占据了地址空间的前32位(从0到2^32-1)
创造者的数据库应该决定一个策略来处理各种IPv4和IPv6之间的映射。
MaxMind所使用的策略为其GeoIP数据库包含一个 ::ffff:0:0/96 子网的根节点树中的IPv4地址空间的指针,这是IPv4映射到IPv6地址的描述
MaxMind还包括一个 2002::/16 子网的IPv4地址空间的根节点的树的指针,这是IPv6映射到IPv4地址的描述
建议数据库创建者记录他们是否为他们的数据库做类似的事情
Teredo子网在树中无法解码,相反,代码搜索树可以提供一个Teredo地址IPv4部分解码和查找
--------------------------------------------------
数据段分离器(Data Section Separator)
有16个字节的空值在搜索树和数据段之间。此分隔符存在,以便使验证工具可以区分两个部分
此分隔符不被认为是数据段本身的一部分。换句话说,数据段在文件中的“$size_of_search_tree + 16”字节开始
--------------------------------------------------
输出数据段(Output Data Section)
每个输出数据字段都有一个关联的类型,这类型编码为一个数字开始的数据字段。某些类型的长度是可变的。在这些情况下,类型指示器后跟长度。数据有效载荷总是在字段的结尾
所有的二进制数据存储为大端(Big Endian)格式
注意给定数据类型含义的解释由高级API决定,而不是由二进制格式本身决定
1、指针(pointer - 1)
指向数据段地址空间的另一部分的指针。指针将指向字段的开头。指针指向另一指针是非法的。
指针的值从数据部分开始,不是文件开始
2、UTF-8字符串(UTF-8 string - 2)
包含有效UTF-8可变长度的字节序列。如果长度为零,那么这是一个空字符串
3、双精度浮点型(double - 3)
存储为大端(Big Endian)格式,长度为8个字节
4、字节数组(bytes - 4)
可变长度的字节序列包含任何类型的二进制数据。如果长度为零则这是一个零长度的字节序列
这不是当前使用的但将来可以使用嵌入非文本数据(图片等)
整数格式(integer formats)
整数存储在可变长度二进制字段中
我们支持16位、32位、64位、和128位无符号整数,还支持32位有符号整数
一个128位整数可以使用多达16个字节,但可以使用更少的。同样,一个32位整数,可以用0字节。对使用的字节数由在控制字节长度的说明符确定。详情见下文
总长度为0表示数字0
当存储一个带符号整数,最左边的位是符号。1是负的,0是正的
数据类型为整数类型:
5、16位无符号整数(unsigned 16-bit int - 5)
6、32位无符号整数(unsigned 32-bit int - 6)
8、32位有符号整数(signed 32-bit int - 8)
9、64位无符号整型(unsigned 64-bit int - 9)
10、128位无符号整型(unsigned 128-bit int - 10)
无符号的32位和128位类型可以分别用来存储IPv4和IPv6地址
有符号的32位整数存储使用2的补码表示
7、Map(map - 7)
Map数据类型包含一组键/值对。与其他数据类型不同,映射的长度信息表示它包含多少个键/值对,而不是其长度为字节。这个大小可以是零
以下是用于确定哈希中对数的算法。该算法也被用来确定一个字段的有效载荷的长度
11、数组(array - 11)
数组类型包含一组有序值。数组的长度信息表示它包含多少个值,而不是字节的长度。这个大小可以是零
这种类型使用与Map相同的算法来确定字段的有效载荷的长度
12、数据缓存容器(data cache container - 12)
这是一个特殊的数据类型,它标志着容器用于缓存数据重复。例如,不是反复在数据库中重复字符串“United States”,而是我们将它存储在缓存容器中,并使用指针指向这个容器
数据库中的任何内容都不包含指向该字段本身的指针。相反,各个字段将指向容器
主要原因是生成一个单独的数据类型与内联缓存的数据,数据库dump数据段时,dump工具可以跳过这个缓存。高速缓存内容将被丢弃作为指针进入
13、结束标记(end marker - 13)
结束标记是数据部分的结束的标记。这并不是必需的,但包括这个标记允许数据部分反序列化器来处理流的输入,而不是找到的最后部分之前反序列化
此数据类型不遵循有效载荷,其大小始终为零
14、布尔型(boolean - 14)
真值或假值。布尔类型的长度信息总是0或1,表示值。此字段没有有效载荷
15、浮点型(float - 15)
存储为IEEE-754浮点型大端(Big Endian)格式,长度为4个字节
这种类型主要为提供完整性。由于浮点数存储方式,当序列化然后反序列化时很容易失去精度。建议使用双精度浮点型
--------------------------------------------------
数据字段格式(Data Field Format)
每个字段以控制字节开头。此控制字节提供有关字段的数据类型和有效载荷大小的信息
控制字节的前三位告诉你该字段是什么类型。如果这些位都是0,那么这是一个“扩展”类型,这意味着下一个字节包含实际类型。否则,前三位将包含一个数字从1到7,实际类型为字段
我们已经尝试将最常用的类型作为数字1-7指定为优化
在扩展类型中,第二个字节的类型号是负数7。换句话说,数组(类型11)将以第一个字节中的类型和第二个字节中的4来存储
下面是如何控制字节可以与下一个字节组合来获取类型的例子:
001XXXXX pointer
010XXXXX UTF-8 string
010XXXXX unsigned 32-bit int (ASCII)
000XXXXX 00000011 unsigned 128-bit int (binary)
000XXXXX 00000100 array
000XXXXX 00000110 end marker
--------------------------------------------------
有效载荷的大小(Payload Size)
接下来的五位控制字节告诉你数据字段的有效载荷是多长,除了Map和指针。Map和指针使用这个尺寸信息有点不同
如果五位小于29,那么这些比特位是字节的有效载荷的大小。例如:
01000010 UTF-8 string - 2 bytes long
01011100 UTF-8 string - 28 bytes long
11000001 unsigned 32-bit int - 1 byte long
00000011 00000011 unsigned 128-bit int - 3 bytes long
如果五位等于29,30,或31,那么使用下面的算法计算有效载荷大小
如果该值为29,大小是29+接下来的一个字节,字节类型为无符号整数型
如果该值为30,大小是285+接下来的两个字节,字节类型为无符号整数型
如果该值为31,大小是65821+接下来的三个字节,字节类型为无符号整数类型
一些例子:
01011101 00110011 UTF-8 string - 80 bytes long
在这种情况下,最后五位的控制字节=29。我们把接下来的一个字节看作无符号整数型,接下来的一个字节等于51,所以总大小(29 + 51)= 80
01011110 00110011 00110011 UTF-8 string - 13,392 bytes long
最后五位的控制字节=30。我们把接下来的两个字节看作无符号整数型,接下来的两个字节等于13107,所以总大小(285 + 13107)= 13392
01011111 00110011 00110011 00110011 UTF-8 string - 3,421,264 bytes long
最后五位的控制字节=31。我们把接下来的三个字节看作无符号整数型,接下来的三个字节等于3355443,所以总大小(65821 + 3355443)= 3421264
这意味着单个字段的最大有效载荷大小是16843036字节
二进制数类型总是有一个已知的大小,但为了一致起见,控制字节总是为这些类型指定正确的大小
--------------------------------------------------
Maps(Maps)
Map使用控件字节中的大小(和后面的任意字节)来指示map中键/值对的数目,而不是以字节为单位的有效载荷的大小
这意味着一个Map的键/值对的最大对数是16843036
Map列出每个键之后,紧跟着它的值,其次才是下一对键/值
Map的键总是使用UTF-8编码的字符串,其值可以是任何数据类型,包括Map或指针
一旦我们知道了对的数目,我们就可以依次查看每一对,以确定键大小和键名,以及值的类型和有效载荷
--------------------------------------------------
指针(Pointers)
指针使用控制字节中的最后五位来计算指针值
计算指针的值,我们把开始的五位比特数据分割成两组,前两位表示大小,接下来的三位是值的一部分,所以,我们最终的控制字节格式是这样的:001SSVVV
大小可以是0、1、2、或3
如果大小为0,指针为通过向上一个三位比特数据追加下一个字节来产生的一个11位值
如果大小是1,指针为通过向上一个三位比特数据追加下两个字节来产生的一个19位值+2048
如果大小是2,指针为通过向上一个三位比特数据追加下三个字节来产生的一个27位值+526336
最后,如果大小为3,指针的值将包含在接下来的四字节中作为32位值。在这种情况下,忽略控制字节的最后三位
这意味着,我们被限制在4GB的地址空间的指针,所以对数据库中的数据段大小限制为4GB
--------------------------------------------------
参考实现(Reference Implementations)
写出(Writer)
Perl (https://github.com/maxmind/MaxMind-DB-Writer-perl)
读取(Reader)
C (https://github.com/maxmind/libmaxminddb)
C# (https://github.com/maxmind/MaxMind-DB-Reader-dotnet)
Java (https://github.com/maxmind/MaxMind-DB-Reader-java)
Perl (https://github.com/maxmind/MaxMind-DB-Reader-perl)
PHP (https://github.com/maxmind/MaxMind-DB-Reader-php)
Python (https://github.com/maxmind/MaxMind-DB-Reader-python)
--------------------------------------------------
作者(Authors)
本规范由下列作者创建:
Greg Oschwald <goschwald@maxmind.com>
Dave Rolsky <drolsky@maxmind.com>
Boris Zentner <bzentner@maxmind.com>
--------------------------------------------------
许可(License)
This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA