MongoDB杂谈
1 序
前一段时间疫情爆发,周末宅家里闲来无事,写点小玩具,期间使用并稍微深入学习了一下 MongoDB 。
本文主要记录一下在学习、使用 MongoDB 过程中遇到的一些问题和学到的一些姿势。
2 Objectid
2.1 数据结构
在 MongoDB 中,集合中每个文档都需要一个 唯一的 _id 字段作为主键,如果插入的文档没有 _id 字段, MongoDB 会自动生成一个 ObjectId 作为 _id。
ObjectId是一个12字节 BSON 类型数据,最初的数据格式如下:
- a 4-byte value representing the seconds since the Unix epoch (which will not run out of seconds until the year 2106)
- a 3-byte machine identifier (usually derived from the MAC address),
- a 2-byte process id, and
- a 3-byte counter, starting with a random value.
|
|
在 MongoDB 最新版本中,ObjectId 的格式有所变化:
- a 4-byte timestamp value, representing the ObjectId’s creation, measured in seconds since the Unix epoch
- a 5-byte random value
- a 3-byte incrementing counter, initialized to a random value
|
|
如上所示,主要变化在中间5个字节的,由原来的 3字节机器名称hash + 2字节进程id改为 5字节随机值,官方driver里面的描述是 system/process unique data。
这个改动的原因可以在 MongoDB 的特性设计文档中找到:
Random Value: Originally, this field consisted of the Machine ID and Process ID fields. There were numerous divergences between drivers due to implementation choices, and the Machine ID field traditionally used the MD5 hashing algorithm which can’t be used on FIPS compliant machines. In order to allow for a similar behaviour among all drivers and the MongoDB Server, these two fields have been collated together into a single 5-byte random value, unique to a machine and process.
大概的原因是因为 MD5 算法不允许在需要遵循 FIPS 140-2 (美国政府安全标准)的机器上使用,为了统一各种语言的 driver 和平台下的实现和表现,采用了5字节系统唯一的随机数值作为一个机器的标识码。
除此之外,原来 3 byte + 2 byte的形式在有些使用场景中容易获得同样的数值,举个栗子。
在容器中部署 Mongo 的实例,系统编排启动、重启的一批容器,自动启动Mongo的时候,由于容器使用同样的镜像,执行同样的步骤,很大概率会出现相同pid的情况,而且机器名称也很可能是一样的。
2.2 driver实现
MongoDB driver在首次启动/首次生成 ObjectID 的时候,会初始化一次机器识别码和随机一个couter初始值,但是不同driver中间的实现五花八门。
|
|
|
|
|
|
2.3 真的唯一吗
作为一个唯一ID生成方案,ObjectId 真的100%唯一吗?
从实现方案本身分析:
-
当一个进程一秒内生成超过 2^24 个 ObjectId,counter会溢出,得到重复 id
-
机器识别码重复,在一定的时空条件下,在同样的时间戳和couter下生成重复的id
-
一些年久失修的第三方driver错误的实现可能导致一定条件下产生重复id
第1点出现的前提条件达成的概率,毕竟普通服务器单进程千万级别写入/s,cpu要冒烟了。
第2点达成的条件也比较苛刻,但是出现了就真的是,走鬼,见到运了。
第3点比较容易避免,使用官方driver和较新的MongoDB即可。
总的来说,ObjectId 能保证数据库级别的唯一性(不同collection之间),至于系统级的唯一性需要一些防御性的措施做保证。
2.4 其他唯一ID生成方案
常见的方案由UUID、SnowFlake等,各有优劣,篇幅有限不展开细说,可查看https://segmentfault.com/a/1190000020993874
3 TTL索引
TTL indexes are special single-field indexes that MongoDB can use to automatically remove documents from a collection after a certain amount of time or at a specific clock time. Data expiration is useful for certain types of information like machine generated event data, logs, and session information that only need to persist in a database for a finite amount of time.
TTL全称是(Time To Live),TTL索引能对一个单列配置过期属性来实现对文档的自动过期删除
,我们可以在对字段创建索引时添加expireAfterSeconds
选项将索引转换为TTL索引,该字段需要是date类型
,在以下几种场景下即使索引设置了expireAfterSeconds属性也不会生效
- 如果该字段不是date类型,则文档不会过期
- 如果文档没包含索引的这个字段,则文档不会过期
|
|
一次偶然的机会,跑了一个driver的测试用例,发现里面有个检查TTL功能的用例,把 expireAfterSeconds 设置成 1,然后开定时器等待几秒后,执行一个assert
|
|
然而assert失败了,文档还存在,打开 Mongo 客户端上去一看发现确实还在,但是过了一段时间后文档就被删除了。
似乎 MongoDB 是按一定周期去检测、删除过期文档的。
带着疑问,去扒了一下 MongoDB 的源码。
|
|
MongoDB起了一个后台线程,每间隔 ttlMonitorSleepSecs
这段之间检测一次过期的文档并删除,通过搜索 MongoDB 的代码和实测,这个值默认为60秒。
改变ttlMonitorSleepSecs
As of today, it’s not possible, but already tracked in MongoDB JIRA:
SERVER-6712
: Make TTL Collection background task period user defined (command line option)SERVER-8616
: Adding Tunable to TTL Collection threadSERVER-13937
: Allow setting a window and interval for the TTL monitorThere’s also kind of a workaround - you can turn TTL monitor off and on manually:
1 2
db.adminCommand({setParameter: 1, ttlMonitorEnabled: false}); db.adminCommand({setParameter: 1, ttlMonitorEnabled: true});
EDIT: It turned out, that there is a
ttlMonitorSleepSecs
flag. It’s mentioned for example here, but it’s not mentioned in the official docs.
1
db.adminCommand({setParameter: 1, ttlMonitorSleepSecs: 60});
4 MongoDB Wire Protocol
MongoDB Wire Protocol 是一个简单的基于套接字的请求-响应样式协议。Client 端通过常规的 TCP/IP 套接字与数据库服务器通信。
Client 端应使用常规的 TCP/IP 套接字连接到数据库,所有整数都使用低位字节序:即,最低有效字节在前。
协议的消息头
|
|
opCode的类型:
Opcode Name | Value | Comment |
---|---|---|
OP_MSG |
2013 | Send a message using the format introduced in MongoDB 3.6. |
OP_REPLY Deprecated in MongoDB 5.0. |
1 | Reply to a client request. responseTo is set. |
OP_UPDATE Deprecated in MongoDB 5.0. |
2001 | Update document. |
OP_INSERT Deprecated in MongoDB 5.0. |
2002 | Insert new document. |
RESERVED |
2003 | Formerly used for OP_GET_BY_OID. |
OP_QUERY Deprecated in MongoDB 5.0. |
2004 | Query a collection. |
OP_GET_MORE Deprecated in MongoDB 5.0. |
2005 | Get more data from a query. See Cursors. |
OP_DELETE Deprecated in MongoDB 5.0. |
2006 | Delete documents. |
OP_KILL_CURSORS Deprecated in MongoDB 5.0. |
2007 | Notify database that the client has finished with the cursor. |
OP_COMPRESSED |
2012 | Wraps other opcodes using compression |
在较新版本的MongoDB中,很多请求类型都已经废弃,取而代之的是 OP_MSG (特性设计文档)
OP_MSG
is a bi-directional wire protocol opcode introduced in MongoDB 3.6 with the goal of replacing most existing opcodes, merging their use into one extendable opcode.
按文档的描述,OP_MSG
是一个更具拓展性的协议格式,用来取代现有的一些 opcodes,例如 OP_DELETE, OP_QUERY等。
|
|
篇幅有限,这里不详细展开,后续有空再开新坑扒一扒,这里先立个flag。
也是偶然的一次机会,跑了一次 driver 的测试用例,发现某些请求会出现奇奇怪怪的现象,比如请求没回复,排除了一下是MongoDB版本和driver版本对不上,新版本 MongoDB 废弃了某些协议,因此去扒了一下。
5 附录
MongoDB官方文档: https://docs.mongodb.com/manual/
MongoDB特性文档: https://github.com/mongodb/specifications
分布式唯一ID的几种生成方案: https://segmentfault.com/a/1190000020993874
stackoverflow: