MongoDB在58同城百亿量级数据下的应⽤实践
58同城作为中国最⼤的⽣活服务平台,涵盖了房产、招聘、⼆⼿、⼆⼿车、黄页等核⼼业务。58同城发展之初,⼤规模使⽤关系型数据库(SQL Server、MySQL等),随着业务扩展速度增加,数据量和并发量演变的越来越有挑战,此阶段58的数据存储架构也需要相应的调整以更好的满⾜业务快速发展的需求。
MongoDB经过⼏个版本的迭代,到2.0.0以后,变的越来越稳定,它具备的⾼性能、⾼扩展性、Auto-Sharding、Free-Schema、类SQL 的丰富查询和索引等特性,⾮常诱惑,同时58同城在⼀些典型业务场景下使⽤MongoDB也较合适,2011年,我们开始使⽤MongoDB,逐步扩⼤了使⽤的业务线,覆盖了58帮帮、58交友、58招聘、信息质量等等多条业务线。
随着58每天处理的海量数据越来越⼤,并呈现不断增多的趋势,这为MongoDB在存储与处理⽅⾯带来了诸多的挑战。⾯对百亿量级的数据,我们该如何存储与处理,本⽂将详细介绍MongoDB遇到的问题以及最终如何“完美”解决。
本⽂详细讲述MongoDB在58同城的应⽤实践:MongoDB在58同城的使⽤情况;为什么要使⽤MongoDB;MongoDB在58同城的架构设计与实践;针对业务场景我们在MongoDB中如何设计库和表;数据量增⼤和业务并发,我们遇到典型问题及其解决⽅案;MongoDB如何监控。
MongoDB在58同城的使⽤情况
MongoDB在58同城的众多业务线都有⼤规模使⽤:58转转、58帮帮、58交友、58招聘、58信息质量、58测试应⽤等,如[图1]所⽰。相关⼚商内容
相关赞助商
ArchSummit深圳2017,7⽉7-8⽇,深圳·华侨城洲际酒店,
图1 MongoDB典型的使⽤场景:转转
为什么要使⽤MongoDB?
MongoDB这个来源英⽂单词“humongous”,homongous这个单词的意思是“巨⼤的”、“奇⼤⽆⽐的”,从MongoDB单词本⾝可以看出它的⽬标是提供海量数据的存储以及管理能⼒。MongoDB是⼀款⾯向⽂档的NoSQL数据库,MongoDB具备较好的扩展性以及⾼可⽤性,在数据复制⽅⾯,⽀持Master-Slaver(主从)和Replica-Set(副本集)等两种⽅式。通过这两种⽅式可以使得我们⾮常⽅便的扩展数据。
MongoDB较⾼的性能也是它强有⼒的卖点之⼀,存储引擎使⽤的内存映射⽂件(MMAP的⽅式),将
内存管理⼯作交给操作系统去处理。MMAP的机制,数据的操作写内存即是写磁盘,在保证数据⼀致性的前提下,提供了较⾼的性能。
除此之外,MongoDB还具备了丰富的查询⽀持、较多类型的索引⽀持以及Auto-Sharding的功能。在所有的NoSQL产品中,MongoDB 对查询的⽀持是最类似于传统的RDBMS,这也使得应⽤⽅可以较快的从RDBMS转换到MonogoDB。
在58同城,我们的业务特点是具有较⾼的访问量,并可以按照业务进⾏垂直的拆分,在每个业务线内部通过MongoDB提供两种扩展机制,当业务存储量和访问量变⼤,我们可以较易扩展。同时我们的业务类型对事务性要求低,综合业务这⼏点特性,在58同城使⽤MongoDB是较合适的。
如何使⽤MongoDB?
MongoDB作为⼀款NoSQL数据库产品,Free Schema是它的特性之⼀,在设计我们的数据存储时,不需要我们固定Schema,提供给业务应⽤⽅较⾼的⾃由度。那么问题来了,Free Schema真的Free吗?
第⼀:Free Schema意味着重复Schema。在MongoDB数据存储的时候,不但要存储数据本⾝,Schema(字段key)本⾝也要重复的存储(例如:{“name”:”zhuanzhuan”, “infoid”:1,“infocontent”:”这个是转转商品”}),必然会造成存储空间的增⼤。
第⼆:Free Schema意味着All Schema,任何⼀个需要调⽤MongoDB数据存储的地⽅都需要记录数据存储的Schema,这样才能较好的解析和处理,必然会造成业务应⽤⽅的复杂度。
那么我们如何应对呢?在字段名Key选取⽅⾯,我们尽可能减少字段名Key的长度,⽐如:name字段名使⽤n来代替,infoid字段名使⽤i来代替,infocontent字段名使⽤c来代替(例如:{“n”:”zhuanzhuan”, “i”:1, “c”:”这个是转转商品”})。使⽤较短的字段名会带来较差的可读性,我们通过在使⽤做字段名映射的⽅式( #defineZZ_NAME ("n")),解决了这个问题;同时在数据存储⽅⾯我们启⽤了数据存储的压缩,尽可能减少数据存储的量。
MongoDB提供了⾃动分⽚(Auto-Sharding)的功能,经过我们的实际测试和线上验证,并没有使⽤这个功能。我们启⽤了MongoDB的库级Sharding;在CollectionSharding⽅⾯,我们使⽤⼿动Sharding的⽅式,⽔平切分数据量较⼤的⽂档。
MongoDB的存储⽂档必须要有⼀个“_id”字段,可以认为是“主键”。这个字段值可以是任何类型,默认⼀个ObjectId对象,这个对象使⽤了12个字节的存储空间,每个字节存储两位16进制数字,是⼀个24位的字符串。这个存储空间消耗较⼤,我们实际使⽤情况是在应⽤程序端,使⽤其他的类型(⽐如int)替换掉到,⼀⽅⾯可以减少存储空间,另外⼀⽅⾯可以较少MongoDB服务端⽣成“_id”字段的开销。
在每⼀个集合中,每个⽂档都有唯⼀的“_id”标⽰,来确保集中每个⽂档的唯⼀性。⽽在不同集合中,不同集合中的⽂档“_id”是可以相同的。⽐如有2个集合Collection_A和Collection_B,Collection_A中有⼀个⽂档的“_id”为1024,在Collection_B中的⼀个⽂档
的“_id”也可以为1024。
MongoDB集部署
MongoDB集部署我们采⽤了Sharding+Replica-Set的部署⽅式。整个集有Shard Server节点(存储节点,采⽤了Replica-Set的复制⽅式)、Config Server节点(配置节点)、Router Server(路由节点、Arbiter Server(投票节点)组成。每⼀类节点都有多个冗余构成。满⾜58业务场景的⼀个典型MongoDB集部署架构如下所⽰[图2]:
图2 58同城典型业务MongoDB集部署架构
在部署架构中,当数据存储量变⼤后,我们较易增加Shard Server分⽚。Replica-Set的复制⽅式,分⽚内部可以⾃由增减数据存储节点。在节点故障发⽣时候,可以⾃动切换。同时我们采⽤了读写分离的⽅式,为整个集提供更好的数据读写服务。
图3 Auto-Sharding MAY is not that Reliable
针对业务场景我们在MongoDB中如何设计库和表
MongoDB本⾝提供了Auto-Sharding的功能,这个智能的功能作为MongoDB的最具卖点的特性之⼀,真的⾮常靠谱吗(图3)?也许理想是丰满的,现实是⾻⼲滴。
⾸先是在Sharding Key选择上,如果选择了单⼀的Sharding Key,会造成分⽚不均衡,⼀些分⽚数据⽐较多,⼀些分⽚数据较少,⽆法充分利⽤每个分⽚集的能⼒。为了弥补单⼀Sharding Key的缺点,引⼊复合Sharing Key,然⽽复合Sharding Key会造成性能的消耗;
第⼆count值计算不准确的问题,数据Chunk在分⽚之间迁移时,特定数据可能会被计算2次,造成count值计算偏⼤的问题;
第三Balancer的稳定性&智能性问题,Sharing的迁移发⽣时间不确定,⼀旦发⽣数据迁移会造成整个系统的吞吐量急剧下降。为了应对Sharding迁移的不确定性,我们可以强制指定Sharding迁移的时间点,具体迁移时间点依据业务访问的低峰期。⽐如IM系统,我们的流量低峰期是在凌晨1点到6点,那么我们可以在这段时间内开启Sharding迁移功能,允许数据的迁移,其他的时间不进⾏数据的迁移,从⽽做到对Sharding迁移的完全掌控,避免掉未知时间Sharding迁移带来的⼀些风险。
如何设计库(DataBase)?
我们的MongoDB集线上环境全部禁⽤了Auto-Sharding功能。如上节所⽰,仅仅提供了指定时间段的数据迁移功能。线上的数据我们开启了库级的分⽚,通过db.runCommand({“enablesharding”: “im”});命令指定。并且我们通过
db.runCommand({movePrimary:“im”, to: “sharding1”});命令指定特定库到某⼀固定分⽚上。通过这样的⽅式,我们保证了数据的⽆迁移性,避免了Auto-Sharding带来的⼀系列问题,数据完全可控,从实际使⽤情况来看,效果也较好。
既然我们关闭了Auto-Sharding的功能,就要求对业务的数据增加情况提前做好预估,详细了解业务半年甚⾄⼀年后的数据增长情况,在设计MongoDB库时需要做好规划:确定数据规模、确定数据库分⽚数量等,避免数据库频繁的重构和迁移情况发⽣。
那么问题来了,针对MongoDB,我们如何做好容量规划?
MongoDB集⾼性能本质是MMAP机制,对机器内存的依赖较重,因此我们要求业务热点数据和索引的总量要能全部放⼊内存中,即:Memory > Index + Hot Data。⼀旦数据频繁地Swap,必然会造成MongoDB集性能的下降。当内存成为瓶颈时,我们可以通过Scale Up或者Scale Out的⽅式进⾏扩展。
第⼆:我们知道MongoDB的数据库是按⽂件来存储的:例如:db1下的所有collection都放在⼀组⽂件内db1.0,db1.1,db1.2,db1.3……db1.n。数据的回收也是以库为单位进⾏的,数据的删除将会造成数据的空洞或者碎⽚,碎⽚太多,会造成数据库空间占⽤较⼤,加载到内存时也会存在碎⽚的问题,内存使⽤率不⾼,会造成数据频繁地在内存和磁盘之间Swap,影响MongoDB集性能。因此将频繁更新删除的表放在⼀个独⽴的数据库下,将会减少碎⽚,从⽽提⾼性能。
第三:单库单表绝对不是最好的选择。原因有三:表越多,映射⽂件越多,从MongoDB的内存管理⽅式来看,浪费越多;同理,表越多,回写和读取的时候,⽆法合并IO资源,⼤量的随机IO对传统硬盘是致命的;单表数据量⼤,索引占⽤⾼,更新和读取速度慢。
第四:Local库容量设置。我们知道Local库主要存放oplog,oplog⽤于数据的同步和复制,oplog同样要消耗内存的,因此选择⼀个合适的oplog值很重要,如果是⾼插⼊⾼更新,并带有延时从库的副本集需要⼀个较⼤的oplog值(⽐如50G);如果没有延时从库,并且数据更新速度不频繁,则可以适当调⼩oplog值(⽐如5G)。总之,oplog值⼤⼩的设置取决于具体业务应⽤场景,⼀切脱离业务使⽤场景来设置oplog的值⼤⼩都是耍流氓。
如何设计表(Collection)?
MongoDB在数据逻辑结构上和RDBMS⽐较类似,如图4所⽰:MongoDB三要素:数据库(DataBase)
、集合(Collection)、⽂档(Document)分别对应RDBMS(⽐如MySQL)三要素:数据库(DataBase)、表(Table)、⾏(Row)。
图4 MongoDB和RDBMS数据逻辑结构对⽐
MongoDB作为⼀⽀⽂档型的数据库允许⽂档的嵌套结构,和RDBMS的三范式结构不同,我们以“⼈”描述为例,说明两者之间设计上的区别。“⼈”有以下的属性:姓名、性别、年龄和住址;住址是⼀个复合结构,包括:国家、城市、街道等。针对“⼈”的结构,传统的RDBMS的设计我们需要2张表:⼀张为People表[图5],另外⼀张为Address表[图6]。这两张表通过住址ID关联起来(即Addess ID是People表的外键)。在MongoDB表设计中,由于MongoDB⽀持⽂档嵌套结构,我可以把住址复合结构嵌套起来,从⽽实现⼀个Collection结构[图7],可读性会更强。
图 5 RDBMSPeople表设计
图 6 RDBMS Address表设计
图7 MongoDB表设计
MongoDB作为⼀⽀NoSQL数据库产品,除了可以⽀持嵌套结构外,它⼜是最像RDBMS的产品,因此也可以⽀持“关系”的存储。接下来会详细讲述下对应RDBMS中的⼀对⼀、⼀对多、多对多关系在Mon
goDB中我们设计和实现。
IM⽤户信息表,包含⽤户uid、⽤户登录名、⽤户昵称、⽤户签名等,是⼀个典型的⼀对⼀关系,在MongoDB可以采⽤类RDBMS的设计,我们设计⼀张IM⽤户信息表user:{_id:88, loginname:musicml, nickname:musicml,sign:love},其中_id为主键,_id实际为uid。IM⽤户消息表,⼀个⽤户可以收到来⾃他⼈的多条消息,⼀个典型的⼀对多关系。
我们如何设计?
⼀种⽅案,采⽤RDBMS的“多⾏”式设计,msg表结构为:{uid,msgid,msg_content},具体的记录为:123, 1, 你好;123,2,在吗。
另外⼀种设计⽅案,我们可以使⽤MongoDB的嵌套结构:{uid:123, msg:{[{msgid:1,msg_content:你好},{msgid:2, msg_content:在吗}]}}。
采⽤MongoDB嵌套结构,会更加直观,但也存在⼀定的问题:更新复杂、MongoDB单⽂档16MB的限制问题。采⽤RDBMS的“多
⾏”设计,它遵循了范式,⼀⽅⾯查询条件更灵活,另外通过“多⾏式”扩展性也较⾼。
在这个⼀对多的场景下,由于MongoDB单条⽂档⼤⼩的限制,我们并没采⽤MongoDB的嵌套结构,⽽是采⽤了更加灵活的类RDBMS的设计。
在User和Team业务场景下,⼀个Team中有多个User,⼀个User也可能属于多个Team,这种是典型的多对多关系。
在MongoDB中我们如何设计?⼀种⽅案我们可以采⽤类RDBMS的设计。⼀共三张表:Team表{teamid,teamname, ……},User表{userid,username,……},Relation表{refid, userid, teamid}。其中Team表存储Team本⾝的元信息,User表存储User本⾝的元信
息,Relation表存储Team和User的所属关系。
在MongoDB中我们可以采⽤嵌套的设计⽅案:⼀种2张表:Team表{teamid,teamname,teammates:{[userid, userid, ……]},存储了Team所有的User成员和User表{useid,usename,teams:{[teamid, teamid,……]}},存储了User所有参加的Team。
在MongoDB Collection上我们并没有开启Auto-Shariding的功能,那么当单Collection数据量变⼤后,我们如何Sharding?对Collection Sharding 我们采⽤⼿动⽔平Sharding的⽅式,单表我们保持在千万级别⽂档数量。当Collection数据变⼤,我们进⾏⽔平拆分。⽐如IM⽤户信息表:{uid, loginname, sig
n, ……},可⽤采⽤uid取模的⽅式⽔平扩展,⽐如:uid%64,根据uid查询可以直接定位特定的Collection,不⽤跨表查询。
通过⼿动Sharding的⽅式,⼀⽅⾯根据业务的特点,我们可以很好满⾜业务发展的情况,另外⼀⽅⾯我们可以做到可控、数据的可靠,从⽽避免了Auto-Sharding带来的不稳定因素。对于Collection上只有⼀个查询维度(uid),通过⽔平切分可以很好满⾜。
但是对于Collection上有2个查询维度,我们如何处理?⽐如商品表:{uid, infoid, info,……},存储了商品发布者,商品ID,商品信息等。我们需要即按照infoid查询,⼜能⽀持按照uid查询。为了⽀持这样的查询需求,就要求infoid的设计上要特殊处理:infoid包含uid的信息(infoid最后8个bit是uid的最后8个bit),那么继续采⽤infoid取模的⽅式,⽐如:infoid%64,这样我们既可以按照infoid查询,⼜可以按照uid查询,都不需要跨Collection查询。
数据量、并发量增⼤,遇到问题及其解决⽅案
⼤量删除数据问题及其解决⽅案
我们在IM离线消息中使⽤了MongoDB,IM离线消息是为了当接收⽅不在线时,需要把发给接收者的消息存储下来,当接收者登录IM后,读取存储的离线消息后,这些离线消息不再需要。已读取离线消息
的删除,设计之初我们考虑物理删除带来的性能损耗,选择了逻辑标识删除。IM离线消息Collection包含如下字段:msgid, fromuid, touid, msgcontent, timestamp, flag。其中touid为索引,flag表⽰离线消息是否已读取,0未读,1读取。
当IM离线消息已读条数积累到⼀定数量后,我们需要进⾏物理删除,以节省存储空间,减少Collection⽂档条数,提升集性能。既然我们通过flag==1做了已读取消息的标⽰,第⼀时间想到了通过flag标⽰位来删除:ve({“flag” :1}};⼀条简单的命令就可以搞定。表⾯上看很容易就搞定了?!实际情况是IM离线消息表5kw条记录,近200GB的数据⼤⼩。mongodb和mysql结合
悲剧发⽣了:晚上10点后部署删除直到早上7点还没删除完毕;MongoDB集和业务监控断续有报警;从库延迟⼤;QPS/TPS很低;业务⽆法响应。事后分析原因:虽然删除命令ve({“flag” : 1}};很简单,但是flag字段并不是索引字段,删除操作等价于全部扫描后进⾏,删除速度很慢,需要删除的消息基本都是冷数据,⼤量的冷数据进⼊内存中,由于内存容量的限制,会把内存中的热数据swap到磁盘上,造成内存中全是冷数据,服务能⼒急剧下降。
遇到问题不可怕,我们如何解决呢?⾸先我们要保证线上提供稳定的服务,采取紧急⽅案,到还在执⾏的opid,先把此命令杀掉(kill opid),恢复服务。长期⽅案,我们⾸先优化了离线删除程序[图8],把已读IM离线消息的删除操作,每晚定时从库导出要删除的数据,通过脚本按照objectid主键(_i
d)的⽅式进⾏删除,并且删除速度通过程序控制,从避免对线上服务影响。其次,我们通过⽤户的离线消息的读取⾏为来分析,⽤户读取离线消息时间分布相对⽐较均衡,不会出现⽐较密度读取的情形,也就不会对MongoDB的更新带来太⼤的影响,基于此我们把⽤户IM离线消息的删除由逻辑删除优化成物理删除,从⽽从根本上解决了历史数据的删除问题。
图8 离线删除优化脚本
⼤量数据空洞问题及其解决⽅案
MongoDB集⼤量删除数据后(⽐如上节中的IM⽤户离线消息删除)会存在⼤量的空洞,这些空洞⼀⽅⾯会造成MongoDB数据存储空间较⼤,另外⼀⽅⾯这些空洞数据也会随之加载到内存中,导致内存的有效利⽤率较低,在机器内存容量有限的前提下,会造成热点数据频繁的Swap,频繁Swap数据,最终使得MongoDB集服务能⼒下降,⽆法提供较⾼的性能。
通过上⽂的描述,⼤家已经了解MongoDB数据空间的分配是以DB为单位,⽽不是以Collection为单位的,存在⼤量空洞造成MongoDB性能低下的原因,问题的关键是⼤量碎⽚⽆法利⽤,因此通过碎⽚整理、空洞合并收缩等⽅案,我们可以提⾼MongoDB集的服务能⼒。
那么我们如何落地呢?
⽅案⼀:我们可以使⽤MongoDB提供的在线数据收缩的功能,通过Compact命令(db.yourCollection.runCommand(“compact”);)进⾏Collection级别的数据收缩,去除Collectoin所在⽂件碎⽚。此命令是以Online的⽅式提供收缩,收缩的同时会影响到线上的服务,其次从我们实际收缩的效果来看,数据空洞收缩的效果不够显著。因此我们在实际数据碎⽚收缩时没有采⽤这种⽅案,也不推荐⼤家使⽤这种空洞数据的收缩⽅案。
既然这种数据⽅案不够好,我们可以采⽤Offline收缩的⽅案⼆:此⽅案收缩的原理是:把已有的空洞数据,remove掉,重新⽣成⼀份⽆空洞数据。那么具体如何落地?先预热从库;把预热的从库提升为主库;把之前主库的数据全部删除;重新同步;同步完成后,预热此库;把此库提升为主库。
具体的操作步骤如下:检查服务器各节点是否正常运⾏ (ps -ef |grep mongod);登⼊要处理的主节点 /mongodb/bin/mongo--port 88888;做降权处理rs.stepDown(),并通过命令 rs.status()来查看是否降权;切换成功之后,停掉该节点;检查是否已经降权,可以通过web页⾯查看status,我们建议最好登录进去保证有数据进⼊,或者是mongostat 查看; kill 掉对应mongo的进程: kill 进程号;删除数据,进⼊对应的分⽚删除数据⽂件,⽐如: rm -fr /mongodb/shard11/*;重新启动该节点,执⾏重启命令,⽐如:
如:/mongodb/bin/mongod --config /f;通过⽇志查看进程;数据同步完成后,在修改后的主节点上执⾏命令rs.stepDown() ,做降权处理。
通过这种Offline的收缩⽅式,我们可以做到收缩率是100%,数据完全⽆碎⽚。当然做离线的数据收缩会带来运维成本的增加,并且在Replic-Set集只有2个副本的情况下,还会存在⼀段时间内的单点风险。通过Offline的数据收缩后,收缩前后效果⾮常明显,如[图9,图10]所⽰:收缩前85G存储⽂件,收缩后34G存储⽂件,节省了51G存储空间,⼤⼤提升了性能。
图9 收缩MongoDB数据库前存储数据⼤⼩
图10 收缩MongoDB数据库后存储数据⼤⼩
MongoDB集监控
MongoDB集有多种⽅式可以监控:mongosniff、mongostat、mongotop、db.xxoostatus、web控制台监控、MMS、第三⽅监控。我们使⽤了多种监控相结合的⽅式,从⽽做到对MongoDB整个集完全Hold住。
第⼀是mongostat[图11],mongostat是对MongoDB集负载情况的⼀个快照,可以查看每秒更新量、加锁时间占操作时间百分⽐、缺页中断数量、索引miss的数量、客户端查询排队长度(读|写)、当前连接数、活跃客户端数量(读|写)等。
图11 MongoDB mongostat监控
mongstat可以查看的字段较多,我们重点关注Locked、faults、miss、qr|qw等,这些值越⼩越好,最好都为0;locked最好不要超过10%;造成faults、miss原因主要是内存不够或者内冷数据频繁Swap,索引设置不合理;qr|qw堆积较多,反应了数据库处理慢,这时候我们需要针对性的优化。
图12 MongoDB Web控制台监控
图13 MongoDB MMS监控
第四是第三⽅监控,MongoDB开源爱好者和团队⽀持者较多,可以在常⽤监控框架上扩展,⽐如:zabbix,可以监控CPU负荷、内存使⽤、磁盘使⽤、⽹络状况、端⼝监视、⽇志监视等;nagios,可以监控监控⽹络服务(HTTP等)、监控主机资源(处理器负荷、磁盘利⽤率等)、插件扩展、报警发送给联系⼈(EMail、短信、⽤户定义⽅式)、⼿机查看⽅式;cacti,可以基于PHP,MySQL,SNMP及RRDTool开发的⽹络流量监测图形分析⼯具。
最后我要感谢公司和团队,在MongoDB集的⼤规模实战中积累了宝贵的经验,才能让我有机会撰写了此⽂,由于MongoDB社区不断发展,特别是MongoDB 3.0,对性能、数据压缩、运维成本、锁级别、Sharding以及⽀持可插拔的存储引擎等的改进,MongoDB越来越强⼤。⽂中可能会存在⼀些不妥的地⽅,欢迎⼤家交流指正。
讲师介绍
孙⽞,极客邦培训专家讲师,58同城系统架构师、技术委员会架构组主任、产品技术学院优秀讲师,58同城即时通讯、C2C技术负责⼈,负责58核⼼系统的架构以及优化⼯作。分布式系统存储专家,2007年开始从事⼤规模⾼性能分布式存储系统架构设计实现⼯作。涉及⾃主研发分布式存储系统、MongoDB、MySQL、Memcached、Redis等。前百度⾼级⼯程师,参与社区搜索部多个基础系统的设计与实现。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论