Mongo 代理程序实现-复制集搭建及抓包篇

1,546 阅读5分钟
原文链接: zhuanlan.zhihu.com

如标题所述,本系列教程是教你如何手撸一个 mongo 代理程序。教程分为两篇,复制集搭建及抓包篇代码实战篇

Q: 这程序有什么卵用?
A: mongo 代理程序主要是为了给后端 mongo 数据库加一层保护层,防止数据库因请求过于频繁而被打爆。

为了更加贴近实际生产环境,我会从数据库复制集搭建,wireshark 抓取解析 mongo 包以及实际 go 逻辑编写,三个部分进行详细的讲解,最终目标是为了实现一个具备读写分离,自动主备切换的稍微健壮一点的 mongo 代理程序。

文章可能有点长,请耐心阅读。

复制集搭建

复制集架构我选择了一主 (Primary),一备 (Secondary),一仲裁 (Arbiter):

  • 主节点 (Primary): 只能有一个,用于接收所有的写操作,并将写入的数据同步到其它备份节点上
  • 备份节点 (Secondary): 可以有多个,用于备份主节点的数据,可以参与复制集选举
  • 仲裁节点 (Arbiter): 不参与选举,不同步主节点数据,当主节点挂了后,自动从备份节点中选举一个作为主节点

闲话不多说,直接上配置文件吧:

## mgo_conf1.yaml
processManagement:
   fork: true  #mongod以守护进程的方式在后台执行
net:
   bindIp: 127.0.0.1
   port: 21000
storage:
   dbPath: /data/db1 #mongo数据文件存储地址
systemLog:
   destination: file
   path: "/data/db1/log/mongo.log" #系统日志存储路径
   logAppend: true #日志以追加的方式写入文件
storage:
   journal:
      enabled: true #日志优先
replication:
   replSetName: mongo_dal #复制集名
#security:
#   keyFile: /Users/geemo/etc/mongo/keyfile #用于复制集成员间安全验证,指定这个字段相当于同时开启了数据库权限验证

根据上面文件配置三份,注意 net.port, storage.dbPath, systemLog.path 不要重复啦。

接下来根据创建好的三份配置来启动 mongod 守护进程:

$ mongod -f mgo_conf1.yaml

启动失败的话,请查看下是否是端口占用啦,或者是否是指定的存储路径根本就没有创建。

全部启动成功后,用 mongo client 连上第一个节点。

$ mongo 127.0.0.1:21000

接下来需要进行复制集初始化工作:

> rs.initiate({
    _id: 'mongo_dal', //复制集名
    members: [
      { _id: 0, host: '127.0.0.1:21000' }
    ]
  })

添加备份节点以及裁节点:

> rs.add('127.0.0.1:22000')
> rs.addArb('127.0.0.1:23000')

所有都成功后,重新用 mongo client 连接第一个节点,会发现命令行提示符变成了 mongo_dal:PRIMARY>。此时我们可以用 rs.status() 查看复制集状态。

自此,一个简单的复制集就搭建完毕了。

wireshark 抓取解析 mongo 协议包

代理程序按理来说只要将客户端发送的数据以及服务端响应的数据透传给对方,不管数据是否加密,通过代理连接的客户端和服务端还是能相互通信的。那为什么还需要费力的去解析 mongo 的协议包呢?

文章开头我们说了,我需要实现一个具备读写分离,自动主备切换的 mongo 代理程序,简版的透传方式实现的 mongo 代理程序仅仅是连接了主节点 (Primary),读写操作全部依赖主节点。那么当主节点挂了,我们的代理程序没法知道谁是新的主节点,后续依然把客户端操作发给已经挂了的主节点,最终导致客户端操作超时断连,因此解析 mongo 协议包是必要的。

好在高版本 wireshark 支持解析 mongo 协议,接下来我们来抓个包试试:

以上截图是我通过 mongo 客户端直连复制集节点得到的抓包结果,我们发现只有 query (Opcode 2004) 和 reply (Opcode 1) 两条数据被完整的解析了,后续我无论进行查询,插入或删除操作,结果都是 Unknown (Opcode 为 2010 或 2011 的操作) 无法被解析。

这和官网介绍的那么多种 Opcode 完全不一样啊,此时我相信你有一句 mmp 不知当讲不当讲。不过仔细想想,我们是通过 mongo client 命令行操作的,它把我们的操作及返回结果封进 Opcode 2010 (command) 和 Opcode 2011 (command reply) 数据包里也属正常,证据如下图所示:

不过即使知道了这个,不能直观的解析也是一件很蛋疼的事。那我们难道就没办法直接解析出官网协议指出的那些数据了吗?不,我们还没试过 mongo 驱动发上来的包是否能解析。

以 mongo node 驱动为例,快速编写一个测试用例:

'use strict'
const MongoClient = require('mongodb').MongoClient;

(async () => {
  let db = await MongoClient.connect('mongodb://127.0.0.1:21000/test');
  let coll = db.collection('cats');

  await coll.insert({name: 'yuyuan'});
  let res = await coll.find({}).toArray()
  console.log("res: ", res);
  await coll.remove({})
})();

执行后的结果如下:

通过以上抓包截图我们发现,无论是 mongo client 直连,还是 mongo node driver 方式连接,握手成功后发的第一个包都是 isMaster 数据包。这个请求数据包是用来首次连接获取复制集状态信息的。之所以上面的截图发现 isMaster 包并不是第一个抓取的包,第一个抓取的包是 Unknown,是因为那些 Unknown 包有很大一部分是复制集成员内部通信的心跳包

其次,通过 node driver 方式连接操作后,我们不断开连接,driver 会每隔一段时间发一个 ismaster 心跳包用来保活连接。注意,这个 ismaster 心跳包不同于首次发送的 isMaster,并不会携带客户端meta 信息。

自此,复制集搭建及抓包教程已完成,下一篇我会介绍如何用 golang 实现 mongo 代理,敬请期待!