Discuz!NT千万级数据量上的两驾马车--TokyoCabinet,MongoDB

      在Discuz!NT的企业版设计过程中,处理大数据表一直是一个让人头疼的问题,特别是像主题表(topic),用户表(user)等,因为对于一个流量和发帖量都很大的论坛而言,在运行几年之后,这两个表的数据量可能会破千万(注:因为帖子表采用分表机制,所以这里暂未涉及,但出于性能考虑,也提供了本文中类似的解决方案)。当时考虑的架构设计中有两种思路来解决这种问题:
      一种是采用类似MYSPACE的方式,即按一定记录KEY值(比如用户表的UID)来对大数据表中的记录进行分割,比如前200万用户(即:UID<200w)放入一个表,200-400万的用户放入另一个表,以此类推。当然可以把几个表都放到一个数据库中,也可以放到别的MSSQL数据库上或实例上。但这种方案有一些问题,例如当用户表需要被联表(如LEFT JION)查询时使用,比如我们的帖子表进行分页查询时就需要左联user表,这时如采用分表或分布式布署就可能面临这样的问题,不仅业务逻辑要变化,就连存储过程中也要产生不小的变化,这里还不考虑效率上的问题。当然有人建议可以使用数据冗余的方式,比如在帖子表中冗余用户信息相应字段,但这种方案同样要大幅度的修改即有代码,同时如果用户信息发生变化时,不仅要更新用户表,还要更新帖子表中的相应冗余字段,如果这两者不同步,就会造成数据显示异常,当然在数据库层面增加存储成本也是不得不付出的。
      第二种就是使用能处理大数据量表格的第三方工具,比如本文所说的TokyoTyrant,Mongodb等,这类NOSQL软件从一问世就是面向海量数据存储访问的,而且这类软件往往都是开源的,另外通过与打算布署企业版的用户接触,发现虽然他们的服务器配置很高,但数量即不多,所以就要考虑如何最大限度的复用已有的机器资源,而这类NOSQL软件往往都是‘性价比’很高的,即用不多的资源(内存,CPU等)就能达到意想不到的效果。当然我目前对其还是很谨慎的使用,即不会马上把它当做主力数据存储工具,而是辅助MSSQL数据库工具,所以大家在看完本文后会发现,这两个工具在企业版中的角色顶多就是一个高级的MEMCACEHD。不过我的想法很简单,就是任何工具和技术,如果不是很了解它或者它很新,那么必定要有一个“考核期”,如果在‘任间’内它通过考核,才委以重任,如未通过考核,也不会让系统平台承担过多的技术层面上的‘风险’。

     综上所述,最终我把方向放到了TokyoTyrant,Mongodb上,之所以选择了这两个工具,主要基于下面因素:
   
    1.海量数据的解决方案应该可以跑在LINUX和WINDOW平台上。当然有人会说Mongodb完全可以跑这两个平台,那还为什么要引入TokyoTyrant呢?其实这里有一些产品的特殊情况要考虑,比如我们的用户中绝大多数对于数据的读写比在 4:1,即5条SQL访问中有4条是SELECT操作,1条是CUD操作,这就造成了读写比例的失衡。虽然Mongodb在读写性能上非常优异和稳定,但在并发读上相对于TokyoTyrant+cabinet还是有一些差距(注:更多内容参见该链接,然后这只限于在我们产品中压力测试环境下的结果,不具备普遍性,所以希望大家具体问题具体分析)

    2.考虑到有些用户公司是有相应技术储备的,两种方案也便于用户公司进行的技术选型(当然因为采用接口方式,用户完全可以引入其它第三方的NOSQL工具来实现)。

    好了,说了这么多,开始今天的正文吧。
   
    前面说过,该方案使用了接口方式,这里就先看一下相应的接口声明:
    
   Discuz!NT千万级数据量上的两驾马车--TokyoCabinet,MongoDB_第1张图片    
   

     可以看到,目前在企业版中,对主题表(dnt_topics),用户表(dnt_users),在线表(dnt_online)以及帖子表(dnt_posts)进行了NOSQL数据支持,所以定义了如下的几个接口(图中):

    

public   interface  ICacheTopics
public   interface  ICacheUsers
public   interface  ICacheOnlineUser
public   interface  ICachePosts

 

   
      因为目前只是把这类NOSQL工具当作高级的‘缓存’来用,所以接口命名上都带着‘Cache’的字样。
   
      然后我使用了一个叫做DBCacheService的类,提供获取这几个接口实例的方法,比如ICacheTopics的实例代码如下:    
    

     ///   <summary>
    
///  该类用于获取NoSqlDb声明的缓存服务
    
///   </summary>
     public   class  DBCacheService
    {
      
        
static  ICacheTopics iCacheTopics  =   null ;
        
        
public   static  ICacheTopics GetTopicsService()
        {
            
if  (iCacheTopics  ==   null )
            {
                
lock  (lockHelper)
                {
                    
if  (iCacheTopics  ==   null )
                    {
                        
try
                        {
                            
if  (EntLibConfigs.GetConfig().Cachetopics.Enable)
                            {
                                iCacheTopics 
=  (ICacheTopics)Activator.CreateInstance(Type.GetType(
                                     EntLibConfigs.GetConfig().Cachetopics.CacheType 
==   2   ?
                                      
" Discuz.EntLib.TokyoTyrant.Data.Topics, Discuz.EntLib.TokyoTyrant "  :
                                      
" Discuz.EntLib.MongoDB.Data.Topics, Discuz.EntLib.MongoDB " false true ));
                            }
                        }
                        
catch
                        {
                            
throw   new  Exception( " 请检查 "   +  (EntLibConfigs.GetConfig().Cachetopics.CacheType  ==   2   ?
                                      
" Discuz.EntLib.TokyoTyrant.dll "  :
                                      
" Discuz.EntLib.MongoDB.dll " +   " 文件是否被放置到了bin目录下! " );
                        }
                    }
                }
            }
            
return  iCacheTopics;
        }
    }

 

    
       从上面代码可以看出,使用反射方式获取相应DLL文件(分别是Discuz.EntLib.TokyoTyrant.dll和Discuz.EntLib.MongoDB.dll)中的 类信息并初始化该实例。当然,这里还定义了一个配置文件,也就是EntLibConfigs.GetConfig()这个方法所获取的配置文件信息, 相应 配置文件内容包括:  
    

///   <summary>
    
///  提供数据库缓存服务,将在线表主题表这类大表放入缓存之中
    
///   </summary>
     public   class  DBCache
    {
        
///   <summary>
        
///  是否有效
        
///   </summary>
         public   bool  Enable  =   false ;
        
///   <summary>
        
///  服务地址
        
///   </summary>
         public   string  Host  =   "" ;
        
///   <summary>
        
///  服务地址
        
///   </summary>
         public   int  Port  =   0 ;
        
///   <summary>
        
///  链接池名称
        
///   </summary>
         public   string  PoolName  =   " dnt " ;
        
///   <summary>
        
///  初始化链接数
        
///   </summary>
         public   int  IntConnections  =   4 ;
        
///   <summary>
        
///  最少链接数
        
///   </summary>
         public   int  MinConnections  =   4 ;
        
///   <summary>
        
///  最大连接数
        
///   </summary>
         public   int  MaxConnections  =   4 ;
        
///   <summary>
        
///  avaiable pool池中线程的最大空闲时间
        
///   </summary>
         public   int  MaxIdle  =   30000 ;
        
///   <summary>
        
///   busy pool中线程的最大忙碌时间
        
///   </summary>
         public   int  MaxBusy  =   50000 ;
        
///   <summary>
        
///  维护线程休息时间
        
///   </summary>
         public   int  MaintenanceSleep  =   300000 ;
        
///   <summary>
        
///  TcpClient读操作超时时间
        
///   </summary>
         public   int  TcpClientTimeout  =   3000 ;
        
///   <summary>
        
///  TcpClient链接超时时间
        
///   </summary>
         public   int  TcpClientConnectTimeout  =   30000 ;
        
///   <summary>
        
///  缓存类型1为mongodb,2为tokyotyrnat
        
///   </summary>
         public   int  CacheType  =   1 ;
    }

 

    
       
      上面是配置文件中‘可复用信息’的基类,下面是具体的配置类实例声明:      

     ///   <summary>
    
///  企业版配置信息类文件
    
///   </summary>
     public   class  EntLibConfigInfo : IConfigInfo
    {
        
///   <summary>
        
///  提供数据库缓存服务,将在线表(dnt_online)放入CACHE中
        
///   </summary>
         public  DBCache Cacheonlineuser  =   new  DBCache();
        
///   <summary>
        
///  提供数据库缓存服务,将用户表(dnt_users)放入CACHE中
        
///   </summary>
         public  DBCache Cacheusers  =   new  DBCache();
        
///   <summary>
        
///  提供数据库缓存服务,将主题表(dnt_topic)放入CACHE中
        
///   </summary>
         public  DBCache Cachetopics  =   new  DBCache();
         
///   <summary>
        
///  提供数据库缓存服务,将主题表(dnt_topic)放入CACHE中
        
///   </summary>
         public  DBCache Cacheposts  =   new  DBCache(); 
    }

 

    
      通过该类,就可以用如下配置文件内容初始化相应的实例了:   

< EntLibConfigInfo >
    
< Cacheonlineuser >
    
<!-- 在开启该功能之前,请确保相关服务已配置完毕 -->
    
< Host > 10.0 . 4.119 </ Host >
    
< Port > 27017 </ Port >
    
< Enable > false </ Enable >
    
< PoolName > dnt_online </ PoolName >
    
< IntConnections > 4 </ IntConnections >
    
< MinConnections > 4 </ MinConnections >
    
< MaxConnections > 4 </ MaxConnections >
    
< MaxIdle > 30000 </ MaxIdle >
    
< MaxBusy > 50000 </ MaxBusy >
    
< MaintenanceSleep > 300000 </ MaintenanceSleep >
    
< TcpClientTimeout > 3000 </ TcpClientTimeout >
    
< TcpClientConnectTimeout > 30000 </ TcpClientConnectTimeout >
    
< CacheType > 1 </ CacheType >
  
</ Cacheonlineuser >
  
< Cacheusers >
    
<!-- 在开启该功能之前,请确保相关服务已配置完毕 -->
    
< Host > 10.0 . 4.66 </ Host >
    
< Port > 112121 </ Port >
    
< Enable > false </ Enable >
    
< PoolName > dnt_users </ PoolName >
    
< IntConnections > 4 </ IntConnections >
    
< MinConnections > 4 </ MinConnections >
    
< MaxConnections > 4 </ MaxConnections >
    
< MaxIdle > 30000 </ MaxIdle >
    
< MaxBusy > 50000 </ MaxBusy >
    
< MaintenanceSleep > 300000 </ MaintenanceSleep >
    
< TcpClientTimeout > 3000 </ TcpClientTimeout >
    
< TcpClientConnectTimeout > 30000 </ TcpClientConnectTimeout >
    
< CacheType > 1 </ CacheType >
  
</ Cacheusers >
  
< Cachetopics >
    
<!-- 在开启该功能之前,请确保相关服务已配置完毕 -->
    
< Host > 10.0 . 4.5 </ Host >
    
< Port > 27017 </ Port >
    
< Enable > false </ Enable >
    
< PoolName > dnt_topics </ PoolName >
    
< IntConnections > 25 </ IntConnections >
    
< MinConnections > 25 </ MinConnections >
    
< MaxConnections > 25 </ MaxConnections >
    
< MaxIdle > 30000 </ MaxIdle >
    
< MaxBusy > 5000 </ MaxBusy >
    
< MaintenanceSleep > 300000 </ MaintenanceSleep >
    
< TcpClientTimeout > 300000 </ TcpClientTimeout >
    
< TcpClientConnectTimeout > 30000 </ TcpClientConnectTimeout >   
    
< CacheType > 1 </ CacheType >
  
</ Cachetopics >
  
< Cacheposts >
    
<!-- 在开启该功能之前,请确保相关服务已配置完毕 -->
    
< Host > 10.0 . 4.5 </ Host >
    
< Port > 27017 </ Port >
    
< Enable > false </ Enable >
    
< PoolName > dnt_posts </ PoolName >
    
< IntConnections > 25 </ IntConnections >
    
< MinConnections > 25 </ MinConnections >
    
< MaxConnections > 25 </ MaxConnections >
    
< MaxIdle > 30000 </ MaxIdle >
    
< MaxBusy > 5000 </ MaxBusy >
    
< MaintenanceSleep > 300000 </ MaintenanceSleep >
    
< TcpClientTimeout > 300000 </ TcpClientTimeout >
    
< TcpClientConnectTimeout > 30000 </ TcpClientConnectTimeout >
    
< CacheType > 1 </ CacheType >
  
</ Cacheposts >
</ EntLibConfigInfo >

 

      当然,因为使用的开源的客户源工具在配置上有一定的的差异性(比如命名上等),所以这里有些参数可以对TTCACHE有效,却对MONGODB无效, 不过这并不影响对这两种工具的使用。
 
      这里要说明的是,对于TokyoTrant而言,这里使用的是我开发的这款客户端软件:

      http://www.cnblogs.com/daizhj/archive/2010/06/08/tokyotyrantclient.html


      Mongodb使用的是:http://github.com/samus/mongodb-csharp
    
      这里还有个小插曲,之前园子里有朋友介绍了这个客户端NoRM ,不过在我写了一个LINQ示例并进行压力测试后,发现速度不快,比samus的那个客户端慢了不少,在苦找原因无果的情况下,最终选择了samus,不过在samus中目前也支持LINQ的写法(也算是扩展和尝试吧),如下面的写法(更多具体示例还是参见其官方源码包中的相应内容):   

     Mongo db  =   new  Mongo( " Servers=10.0.4.5:27017;ConnectTimeout=30000;ConnectionLifetime=300000;MinimumPoolSize=64;MaximumPoolSize=256;Pooled=true " );
     db.Connect();
     var topicColl 
=  db.GetDatabase( " dnt_mongodb " ).GetCollection < Discuz.EntLib.MongoDB.Entity.TopicInfo > ( " topics " );
     var topicInfoList 
=  topicColl.Linq().Where(t  =>  t.Fid  ==   2   &&  t.Displayorder  ==   0 ).Skip(skip).OrderByDescending(t => t.Lastpostid).Take( 16 ).ToList();
     Discuz.Common.Generic.List
< TopicInfo >  topicList  =   new  List < TopicInfo > ();
     
foreach  (var topic   in  topicInfoList)
     {
         topicList.Add(LoadTopicInfo(topic));
     }
     db.Disconnect();
     
return  topicList;

 

           
     不过在使用上述代码进行1500万主题分页时,发现LR的测试周期延长(前者(document方式)从2:10秒延长到后者(linq)2:30秒)和吞吐量降低。
    
     所以这里还是最终延用了samus的document访问方式,参照上面的LINQ写法,下面是document写法,形如:     

public  Discuz.Common.Generic.List < TopicInfo >  GetTopicList( int  fid,  int  pageSize,  int  pageIndex,  int  startNumber)
{
    
int  skip  =   0 ;
    
if  (pageIndex  <=   1 )
        pageSize 
=  pageSize  -  startNumber;
    
else
        skip 
=  (pageIndex  -   1 *  pageSize  -  startNumber;

    Discuz.Common.Generic.List
< TopicInfo >  topicInfoList  =   new  Common.Generic.List < TopicInfo > ();
    System.Collections.Generic.List
< Document >  docList  =  MongoDbHelper.Find(mongoDB,  " topics " ,
                   
new  Document().Add( " fid " , fid).Add( " displayorder " 0 ),  " lastpostid " , IndexOrder.Descending, pageSize, skip);
   
    
return  docList;
}

 

        
        如果在你的项目中非要使用LINQ方式的话,那在这里再要介绍的一个samus的属性绑定功能,这个功能对于那些数据库字段与代码中的属性存在 “大小写”差异的情况下,非常有用,即对相应实体类进行‘别名’的绑定,比如对于主题表(需引入MongoDB.Attributes名空间): 

///   <summary>
///  主题信息描述类
///   </summary>
public   class  TopicInfo : Discuz.Entity.TopicInfo
{
  
    [MongoAlias(
" attention " )]
    
public   new   int  Attention {  get set ; }
    

    
/// <summary>
    
/// 主题tid
    
/// </summary>
    [MongoAlias( " tid " )]
    
public   new   int  Tid {  get set ; }
    
    
///   <summary>
    
///  板块名称
    
///   </summary>
    [MongoAlias( " forumname " )]
    
public   new   string  Forumname {  get set ; }
   
    
/// <summary>
    
/// 版块fid
    
/// </summary>
    [MongoAlias( " fid " )]
    
public   new   int  Fid {  get set ; }
   
    
/// <summary>
    
/// 主题图标id
    
/// </summary>
    [MongoAlias( " iconid " )]
    
public   new   int  Iconid {  get set ; }

 ......

 

     
     
     上面的MongoAlias属性就是属性别名,它就是MONGODB中所存储的数据字段名称。
    
    
     介绍到这里,再回到正文。
    
      因为这两个工具都是在数据库层面进行缓存的,所以它对于原有的DISCUZ!NT中的缓存系统而言,与数据库帖的更近,所以对原有的业务逻辑改造,
  就停留在了数据访问层"DISCUZ.DATA.dll"中了,其实到这里,就看出了当初为什么要分层,以及分层带来的好处了。
 
     比如在Discuz.Data.Topics这个类中添加了这两个静态变量:    

 

///   <summary>
///  是否启用TokyoTyrantCache缓存用户表
///   </summary>
public   static   bool  appDBCache  =  (EntLibConfigs.GetConfig()  !=   null   &&  EntLibConfigs.GetConfig().Cachetopics.Enable);

public   static  ICacheTopics ITopicService  =  appDBCache  ?  DBCacheService.GetTopicsService() :  null ;

 

    

       前者用户判断是否启用主题缓存,后者则获取相应的缓存服务实例(前面配置文件中已做相应说明)。

       这样,在已有的数据访问代码中加入相应的缓存逻辑,比如获取主题信息:

 

///   <summary>
///  获得主题信息
///   </summary>
///   <param name="tid"> 要获得的主题ID </param>
///   <param name="fid"> 版块ID </param>
///   <param name="mode"> 模式选择, 0=当前主题, 1=上一主题, 2=下一主题 </param>
public   static  TopicInfo GetTopicInfo( int  tid,  int  fid,  byte  mode)
{
    TopicInfo topicInfo 
=   null ;
    
    
if  (appDBCache) // 新增代码
        topicInfo  =  ITopicService.GetTopicInfo(tid, fid, mode);

    
if (topicInfo  ==   null )
    {
        
// 原代码
        IDataReader reader  =  DatabaseProvider.GetInstance().GetTopicInfo(tid, fid, mode);
        
if  (reader.Read())
            topicInfo 
=  LoadSingleTopicInfo(reader);

        reader.Close();

        
if  (appDBCache  &&  topicInfo  !=   null )
            ITopicService.CreateTopic(topicInfo);
    }
    
return  topicInfo;
}

 

        
      当然,因为使用了缓存方式,所以就牵扯到缓存中的数据与数据库中数据的一致性问题,所以对于主题的CUD操作,也要对应有相应的对缓存的操作,这基本上就是一个工作量的问题了。因为无论是TTCACHED,还是MONGODB,都支持更新操作。

      比如同样是更新主题附件类型的操作,下面是TTCACHED的写法:
     

///   <summary>
///  更新主题附件类型
///   </summary>
///   <param name="tid"> 主题Id </param>
///   <param name="attType"> 附件类型,1普通附件,2为图片附件 </param>
///   <returns></returns>
public   int  UpdateTopicAttachmentType( int  tid,  int  attType)
{
    var qrecords 
=  TokyoTyrantService.QueryRecords(pool,  new  Query().NumberEquals( " tid " , tid));
    
foreach  ( string  key  in  qrecords.Keys)
    {
        var column 
=  qrecords[key];
        column[
" attachment " =  attType.ToString();
        TokyoTyrantService.PutColumns(pool, column[
" tid " ], column,  true );
        
break ;
    }
    
return   1 ;
}

 

 

      下面是MongoDB的写法

///   <summary>
///  更新主题附件类型
///   </summary>  
///   <param name="tid"> 主题Id </param>
///   <param name="attType"> 附件类型,1普通附件,2为图片附件 </param>
///   <returns></returns>
public   int  UpdateTopicAttachmentType( int  tid,  int  attType)
{
    MongoDbHelper.Update(mongoDB, 
" topics " ,
           
new  Document() { {  " $set " new  Document() { {  " attachment " , attType } } } },
            
new  Document().Add( " _id " , tid));        
    
return   1 ;
}

 

     
     
      通过对比可以看出,MONGODB可以对某一字段进行操作,而TTCACEHD则只能通过查询先获取整条记录,然后修改某一‘字段’,之后再整条提交更新,所以单从这一角度讲,MONGDOB要比TTCACHED更新性能要高许多(之后的测试结果也说明了这一点)。
  
      正如之前所说的那样,如用户对于这两个接口实现方案均不满意,那么他可以使用其它类型的NOSQL数据库,只要实现了相应的接口:
     public interface ICacheTopics
     public interface ICacheUsers
     public interface ICacheOnlineUser
     public interface ICachePosts     
       并在配置文件中进行相应的配置就可以了,当然本文中代码因为时间问题还是有待考量的,但主要的架构设计思想基本被确定下来了。
 
 
      当然对于原有的数据库中的记录,如果要使用本方案,我提供了转换工具,用于把数据转到TTCACHED或MONGODB中的任一服务端上。如下:
 
     TTCACEHD:
     Discuz!NT千万级数据量上的两驾马车--TokyoCabinet,MongoDB_第2张图片
    
     MongoDB(目前比TTACEHD多了帖子分表转换功能):
    Discuz!NT千万级数据量上的两驾马车--TokyoCabinet,MongoDB_第3张图片
 
 
      最后在压力测试过程中,还出现了一些小问题,好在对着官方文档,逐步优化解决了,这里要特别说一下MONGDOB,其文件的详细程度要好于TTCACHED,基本上主要的功能都有详细的介绍说明页面,呵呵。当然TTCACHED的诞生时间要比MONGODB早,所以在生产环境下的成功案例也相对多一些。
    
    
     下面列了一下使用过程中的小问题,仅作记录:           
     
      TokyoTyrant的使用问题:尽量不要在查询的列表中使用排序操作,因为它的排序效率还不如数据库高。尽量使用索引进行查询
                   键值操作。2000w记录以下查询效率很高,但更高的数据量上目前没做过压力测试(包括CRUD操作)
     
      Mongodb:尽量使用_ID做为查询键值操作,包括排序等,对索引进行优化(单列或多列进行索引)。

 

       原文链接:http://www.cnblogs.com/daizhj/archive/2010/07/20/1781140.html

       BLOG: http://daizhj.cnblogs.com/

       作者:daizhj,代震军


 

 

你可能感兴趣的:(TokyoCabinet)