MongoDB,我的道

2020-02-26 13:32:44 浏览数 (1)

本文是罗聪在“我和MongoDB的故事”征文比赛的获奖文章,下面我们一起来欣赏下。

01

一切的开始

我们的用户在2014年提出了一个新需求,存储亿级数量的半结构化数据,并且要求支持高并发查询和更新。对于一个在年底才进入这个团队的菜鸟,我很惊讶技术负责人最终会选择使用MongoDB存储海量的数据,所以对这个团队充满了好奇感,是什么理由选择MongoDB?能否Hold新技术?不怕丢数据?

但是事实说明了一切。这个用户的MongoDB集群从2015年上线以来,到2020年的今天,已经运行了5年时间。承载了每天的数据汇聚和数据同步(到检索服务),平均每8小时就能消耗完一次oplog size的上限(Upper Bound ≈ 50GB)。接下来就是我们近几年使用MongoDB的案例、发生的花絮和对未来的思考。

02

应用和拓展

好的技术只有在不断实践和总结中才能找到打开它那一扇魔法门的钥匙。

NoSQL的第一站

  • 版本:MongoDB 3.2
  • 集群模式:副本集
  • 读写压力比:1:1
  • 磁盘类型:SATA
  • 数据量:2T,3亿条。单集合最大1.5T,近1.4亿条,平均大小12K。
  • 数据类型:核心数据

我们第一次使用MongoDB就发生在刚才的那个案例中。不过刚开始我只会简单使用,并不了解多少原理。直到花絮章节的「魔鬼到来!」,我才意识到即使有好的技术,如果连基本原理都不清楚,那出了问题也只会两眼一抹黑。第二年,也就是2016,市面上关于MongoDB的技术参考书籍屈指可数,《MongoDB权威指南 第二版》也已经过去了3年。为了用好MongoDB,我也坚持看完了这本砖头厚度一般的经典之作,随后的时间基本聚焦在docs.mongodb.com英文资料,这样就逐步了解了MongoDB。机缘巧合,团队中之前负责这块的老同志转互联网公司了,数据库的运维工作就来到了我这边。在Leader的支持下,我开始对应用进行大量优化、定期对实例进行维护、监控和分析集群的各项性能指标…甚至,在官方的mongo charts还没有出来之前,我们自己基于官方驱动做了一个简易版的Web控制台,从此运维工作轻松能完成,基本和Shell手写指令Say Goodbye。

这个集群目前仍运行中,最近一次较大的运维工作发生在2019年5月,从v3.2连续升级到3.6。是为了使用Change Streams特性,为随后需要的跨地域的副本集和实时同步(到其他数据源)做基础。不过3.6除了Change Streams极具吸引力外,还有很多新的feature让老用户看的心动,如可重试写入、增加Causal Consistency的可调一致性、Compass社区免费版等,所以让我们坚定升级到3.6。

MongoDB的官网文档非常强大,用一句话介绍就是只要你肯花时间去阅读和理解它,再勤加练习,肯定能成为一名合格的MongoDB DBA。在这个章节的最后,我也附加了一个副本集版本升级实操,分享如何利用官方docs在不停服下滚动升级实例版本。

全面适应容器化

2017年,业务需要,我们原有的集群需要在异地复制一套,而且必须和其他集群保持隔离。项目工期非常短,所有的应用服务地部署和数据初始化工作也要求在极短几天时间内完成。面对这种持续集成交付和弹性应用部署,我们开始引进Docker容器技术。当时Kubernetes没有这么火,容器编排还是Swarm。但是Swarm在使用的时候有很多限制性条件,所以我们自己使用shell自动化脚本管理容器的生命周期。

MongoDB部署之前,我们也思考了既然要发挥docker的弹性能力,避免过多的人工指令,于是设计了一些实例管理脚本,包括Dockerfile和下面的一段巧妙的集群初始化代码。将这个代码封装在init.js文件中,然后和上层的shell脚本关联起来就能够轻松实现MongoDB副本集的所有初始化工作。

代码语言:javascript复制
cfg = {
    _id: 'rs',
    members: [
        {
            _id: 0,
            host: 'c1.luoc.com:27027'
        },
        {
            _id: 1,
            host: 'c1.luoc.com:27027'
        },
        {
            _id: 2,
            host: 'c1.luoc.com:27027'
        }
    ]
};

rs.initiate(cfg);

while(db.isMaster().ismaster == false) { }

db = db.getSiblingDB('admin');
db.createUser({user:"admin", pwd:"0000", roles:["root"]});
db.createUser({user:"users", pwd:"0000", roles:["userAdminAnyDatabase"]});

db = db.getSiblingDB('test');
db.createUser({user:"test", pwd:"0000", roles:["dbOwner"]});
db.test.insert({key:"hello", value:"world"});

linux shell

代码语言:javascript复制
mongo --quiet init.js

应对海量元数据

2017下半年,我们引入了Hadoop技术栈,深入到更丰富的大数据生态。但是我们在做一个方案的时候卡壳了,如何低成本在一种存储技术中存储海量文件?

  1. 使用GlusterFS或Ceph这种分布式文件存储系统?
  2. 使用MongoDB GFS?
  3. 自行设计方案?

第一个方案要求在Hadoop技术栈之外,再引入一种新存储技术,架构复杂度太高。并且我们团队中对Ceph之类的技术驾驭并没有充分的信心。我们的目标是存储小到几KB大到上GB的文件对象,所以从性价比上考虑第二个也没有被采用。通过已经掌握的MongoDB和HDFS技术,我们设计了一个优化方案。以下是核心流程

写路径
  1. 应用传输文件。
  2. 所有文件直接上传到HDFS。
  3. 在上传之前,我们设计了存储优化服务将该文件元信息(Meta)存到MongoDB。
  4. 优化服务后台控制线程,定期对Meta进行聚合统计,如果未做compact的文件大小(不计数量)累积超过HDFS Block(128MB)的阈值(默认80%),启动新线程对所有文件进行compact并写入到SequenceFile,每个文件在SequenceFile的offset和size更新到对应Meta中。
  5. 如果没有超过阈值,就放弃本次compact,等待下个检查点。
  6. 如果单个文件大于该阈值,就跳过compact。
读路径
  1. 应用接口发起文件读请求。
  2. 请求首先通过优化服务路由到MongoDB并获取该文件Meta。
  3. 优化服务使用Meta定位在HDFS的Sequencefile。
  4. 最后打开HDFS Sequencefile从offset位置读取指定size的字节构建成文件返回。

整体思路就是选择MongoDB存储用户上传文件的元数据,即满足了元数据管理的合规要求,也利用了HDFS的分布式文件存储能力,还消除了HDFS NameNode面对海量小文件的内存膨胀问题。对于我们团队来说,运用已经掌握的技术解决新问题,比再引进一种新存储技术带来的运维成本要低很多。

该方案也已经在生产环境中运行了近2年时间,随着元数据的增长,未来是能够很轻松通过增加MongoDB实例进行水平扩展。

版本升级

MongoDB v3.2 > 3.6
  • 副本集集群。
  • 滚动升级。
  • 升级路径 3.2 > 3.4 > 3.6。
准备
  • mongodb-linux-x86_64-rhel62-3.4.20.tgz
  • mongodb-linux-x86_64-rhel62-3.6.12.tgz
关键步骤
  1. mongostat命令行评估集群节点压力,在读写压力小的时间段做升级更合适。
  2. mongo命令行登录到admin数据库并执行以下命令。 # 滚动日志 db.runCommand({ logRotate: 1 }) # 在Primary上锁定写操作和flush内存数据到磁盘 db.fsyncLock();
  3. 直接使用cp -R拷贝数据文件进行备份。
  4. mongo命令行设置优先Primary。 cfg = rs.conf() cfg.members[0].priority = 1 cfg.members[1].priority = 0.5 cfg.members[2].priority = 0.5 rs.reconfig(cfg)
  5. Secondary执行以下命令(每次操作一个Secondary或者保证有(n/2) 1个节点在线)。 # 关闭数据库 db.shutdownServer(); # 替换bin目录的可执行文件 cp /opt/mongodb-linux-x86_64-rhel62-3.4.20/bin/* bin/ # 重启mongod实例(startup.sh是自定义脚本) ./startup.sh
  6. Secondary正常升级后,再在Primary上执行以下命令。 # 强制重新选举,Primary降级为Secondary rs.stepDown(); # 关闭数据库 db.shutdownServer(); cp /opt/mongodb-linux-x86_64-rhel62-3.4.20/bin/* bin/ # 重启mongod实例 ./startup.sh
  7. 开启版本兼容性特性。 db.adminCommand( { setFeatureCompatibilityVersion: "3.4" } )

db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } ) ```

  1. 按照以上第567步骤再从v3.4升级到3.6即完成。
Troubleshoot
  1. 升级后数据如何预热到内存中?
    1. 编写MapReduce,提交到Secondary执行。
    2. MR 在遍历数据的时候会将数据load到内存。
    3. MR 不适合超大数据库或_id没有采用默认ObjectId的超大数据集合。
    4. mongo提供touch命令可以将磁盘上的数据文件预热到内存。但是仅适用于MMAPv1存储引擎,不支持WiredTiger
    5. 不支持WiredTiger,那怎么预热?
  2. 两次升级过程中配置文件需要修改吗?
    1. 3.2 > 3.4过程中需要将配置文件中废弃的参数移除才能启动mongod实例,如:nohttpinterface
    2. 3.4 > 3.6过程中需要在启动指令中加入--bind_ip参数。
  3. db.stepDown()命令有哪些注意事项?
    1. 降级有效时间默认只有60s,执行后如果没有在60s内关闭数据库,集群会使用priority规则重新发起选主。
    2. 可以将自定义秒数传入方法来延长时间,如:db.stepDown(600)
  4. 如何确认数据库升级完成?
    1. db.version()确认实例版本。
    2. rs.status()确认集群节点状态正常。
  5. db.fsyncLock()作用是什么?不需要解锁吗?
    1. 阻塞Primary上的写请求,防止在物理备份期间发生数据不一致。
    2. 解锁请使用db.fsyncUnlock()
  6. 为什么不采用mongodump方式来备份数据?
    1. mongodump不适合超大数据库或_id没有采用默认ObjectId的超大数据集合。
  7. 会不会发生db.shutdownServer()执行后无响应?
    1. 可能会(这次就是)。
    2. 如果执行后等待时间超过1h请使用kill -2 <pid>再尝试关闭mongod实例,如果仍然无效,请评估风险后自行使用kill -9
  8. kill -9 <pid>的风险是什么?
    1. 前提是mongod实例开启了journal,否则可能造成数据丢失。
    2. kill -9不应该在生产环境任何一种数据库中使用。
  9. 版本升级有哪些权威资料可以参考?
    • https://docs.mongodb.com/v3.4/release-notes/3.4-upgrade-replica-set
    • https://docs.mongodb.com/v3.6/release-notes/3.6-upgrade-replica-set

03

花絮

魔鬼到来!

2016年集群上线不久,发生了一次严重事故,一个晚上丢失了几百万条数据。随后团队紧急会议、分析和处理事故、最后线索断了… oplog全部被delete操作指令集覆盖,问题的原因当时没有立刻分析出来。但是我猜测可能是我们使用GUI误操作导致整个集合的数据被遍历删除。如果说前几天成功复飞的长征5号"胖五"在2017年的那次失利的问题被航天队伍称为"魔鬼",那这次事故对于我们团队来说也是一次"魔鬼"到来。因为很难复现,所以大家认为不应该出现这种情况。

3个月后,在基本掌握MongoDB的原理后,我多次模拟事故变量,终于复现了之前发生的一切。

  1. A 登陆了GUI,连接配置中的Read Preference使用默认的Primay,即连接到Primary节点。
  2. B 也登陆了GUI,但是连接配置和A有区别,Replica Set members列表仅填写了一个副本节点,且读选项选择了Secondary Preferred,即连接到Secondary节点。
  3. A 使用GUI Shell执行了db.coll.remove({x})语法,但是x值在上下文不能保证非null,即remove(null),这是遍历删除集合中的所有数据!
  4. B 在随后的排查过程中,验证GUI Shell的remove方法时,虽然执行了setSlaveOk(),但是将"not master"的提示错误解读为不可操作,漏掉了这个主要细节。
  5. A 的请求因为只会发送给Primary节点,所以不会遇到B的报错。

并没有魔鬼,只是我们没有做好克服这份恐惧的准备。

架构师的征程

在以前学习MongoDB原理过程中,我借助对书本的阅读和官方文档的理解,彻底改变了对知识获取方式的认识。并不一定要使用最新的书籍或源代码才能掌握不过时的技术,知识其实是不分新和旧,只要我们一直保持着一颗敬畏之心。比如,NoSQL中BASE和CAP理论已经出来了很长时间,只是在Hbase/MongoDB/Cassandra这种技术普及后才被更多人了解。但是不论你认不认识它,它都在,当你认真去对待它的时候,你才会认为这些是新的事物。更多的像SQL on Hadoop,没有接触Hadoop技术栈的人可能都会认为此SQL非彼SQL,和RDBMS中的不一样。其实并非如此,新瓶装旧酒,还是那些SQL,底层实现换成了并行处理,只是对于开发者透明。SQL技术中的核心流程:语法检查 > 语义检查 > 优化器 > 执行器等一个都不会缺。

2015到2020年,刚好5年,我从一个菜鸟到能够给大数据产品做些架构设计,基本完成了初步目标。接下来就是收拾心情,继续前行!

04

未来

RDBMS,NoSQL,NewSQL

如果我们经常关注db-engines.com排行榜,就会发现RDBMS阵营已经开始向多模型数据库的趋势发展,加入Document Store(部分有License限制)。NoSQL系列中的MongoDB也通过加入分布式事务来进一步增强对开发者的吸引力。还有像TiDB这种新型的NewSQL,未来给用户的选择依然有很多。如果你想入门分布式数据库,推荐从MongoDB这种开箱即用、对开发者友好的技术开始,再去深入研究分布式原理,你将会事半功倍!

连接Hadoop

如果要设计一个新的数据中心架构,去IOE化,你的方案会是什么?以下灼见

  1. MongoDB做核心数据的存储服务。
  2. 使用Change Streams将数据变化实时同步到Hadoop做数据开发、治理和OLAP。
  3. 最后形成的资产数据可以回流MongoDB,如果用户需要SQL,也可以通过Hadoop中的Phoenix进行SQL即席查询和操作型分析。
  4. 数据接口服务根据用户的需求和自定义配置连接MongoDB的数据集并生成RESTful API,提供给应用层。
  5. 数据集成过程中如果有非结构化、二进制大对象等,就可以根据数据规模和数据特征来选择MongoDB GFS、HDFS和Hbase 2.0 MOB灵活实现对象存储服务。
  6. 流式数据可以通过Kafka和Connector连接器分发到计算引擎,如果流式传输大对象,MongoDB可以作为海量数据切片的元数据最佳存储库。

作者:罗聪

长城软件大数据实验室,1Data产品负责人。

0 人点赞