我的编程空间,编程开发者的网络收藏夹
学习永远不晚
位置:首页-资讯-开源

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

特别是像主题表(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工具来实现)。

    好了,说了这么多,开始今天的正文吧。

   

    前面说过,该方案使用了接口方式,这里就先看一下相应的接口声明:

    

       

   

     可以看到,目前在企业版中,对主题表(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:

    

    

     MongoDB(目前比TTACEHD多了帖子分表转换功能):

   

 

 

      最后在压力测试过程中,还出现了一些小问题,好在对着官方文档,逐步优化解决了,这里要特别说一下MONGDOB,其文件的详细程度要好于TTCACHED,基本上主要的功能都有详细的介绍说明页面,呵呵。当然TTCACHED的诞生时间要比MONGODB早,所以在生产环境下的成功案例也相对多一些。

    

    

     下面列了一下使用过程中的小问题,仅作记录:           

     

      TokyoTyrant的使用问题:尽量不要在查询的列表中使用排序操作,因为它的排序效率还不如数据库高。尽量使用索引进行查询

                   键值操作。2000w记录以下查询效率很高,但更高的数据量上目前没做过压力测试(包括CRUD操作)

     

      Mongodb:尽量使用_ID做为查询键值操作,包括排序等,对索引进行优化(单列或多列进行索引)。

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

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

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

特别是像主题表(topic),用户表(user)等,因为对于一个流量和发帖量都很大的论坛而言,在运行几年之后,这两个表的数据量可能会破千万(注:因为帖子表采用分表机制,所以这里暂未涉及,但出于性能考虑,也提供了本文中类似的解决方案)。当时考
2022-06-12

编程热搜

  • wordpress错位如何解决
    这篇文章主要介绍“wordpress错位如何解决”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“wordpress错位如何解决”文章能帮助大家解决问题。wordpress
    wordpress错位如何解决
  • 帝国cms调用栏目别名的修改步骤
    请留意下面的修改方法 修改后栏目别名使用 !--bname--] 调用 打开e/class/t_functions.php搜索定位到带模板的栏目导航标签修改1复制代码代码如下://替换变量$bclassname=$class_r[$clas
    帝国cms调用栏目别名的修改步骤
  • 动易Cms MAC验证视图失败的解决方法
    “/”应用程序中的服务器错误。 -------------------------------------------------------------------------------- 验证视图状态 MAC
    动易Cms  MAC验证视图失败的解决方法
  • DEDECMS如何支持中文水印
    DEDECMS如何支持中文水印?修改/include/inc_photograph.php,在165行处加这行代码:$w_text = iconv("GB2312","UTF-8",$w_text);OK,现在可以用中文做水印了,但默认的字体大小是5,中文字体
    DEDECMS如何支持中文水印
  • 快速了解Discuz!程序文件功能
    相信有不少人热衷于基于修改原有系统文件所做的插件,当然了解程序文件功能是最重要的,下面我把Discuz! X2.0主要的程序文件功能说一下。Tips:Q:针对说明的文件是?A:我只基于原版文件对upload目录中基本的程序文件(php,ht
    快速了解Discuz!程序文件功能
  • DedeCMS编辑器fck更换成eWebEditor编辑器具体步骤
    将eWebEditor编辑器插入DEDE后台的效果: 完全Word在线编辑的功能,让你从此摆脱发可编辑器卡得死,进入流畅干净编辑界面新时代。 下面是具体步骤: 首先,你要知道eWebEditor是一个什么工具。 eWebEditor是由国
    DedeCMS编辑器fck更换成eWebEditor编辑器具体步骤
  • 允许 WordPress 上传任意文件的方法
    此时如果上传一个不在预定义编程客栈的安全扩展名列表,如.lrc,会报kAtKhHRl错: F编程客栈ile type does not meet security guidelines. Try another. 解决此问题有两方法: 在
    允许 WordPress 上传任意文件的方法
  • dedecms 软件下载模块中添加下载方式为迅雷下载联盟代码
    修改 /plus/download.php 文件 www.cppcns.com查找
    dedecms 软件下载模块中添加下载方式为迅雷下载联盟代码
  • dedecms网站tag标签全部静态化的解决方法
    更改tags.php文件在根目录 找到$tag = FilterSearch(urldecode($tag)); 替换为:$tag = urldecode($tag); 更改文件arc.taglist.class.php文件文件所在的路径是
    dedecms网站tag标签全部静态化的解决方法
  • 帝国CMS远程保存图片的方法
    本文实例讲述了帝国cms远程保存图片的方法。分享给大家供大家参考。 具体实现方法如下:复制代码代码如下:
    帝国CMS远程保存图片的方法

目录