Discuz!X集群部署的系统方案和优化方式
多 web 部署时,面临的核心问题是 web 服务器间的数据共享和同步。就数据存储的方式而言,Discuz 数据包含两部分:一部分存储在 MySQL 数据库中(用户、帖子等文本类、结构化的数据),一部分存储在文件系统(附件、缓存文件等)。其中存储在 MySQL 中的数据可以方便地在多服务器间共享,扩展和冗余也已经有比较成熟的方案。这里我们主要讨论 Discuz 文件类型的数据,部分涉及到多台服务器的内容。
数据梳理
Discuz 文件类型的数据都存储于 DISCUZ_ROOT/data 目录,各目录主要功能如下:
目录 | 数据说明 |
---|---|
data/attachment | 附件类 |
data/log | 运行日志 |
data/cache | 配置参数类缓存文件(默认是sql,配置参数通过pre_common_syscache表缓存)、CSS缓存、部分JS缓存 |
data/template | 模板缓存 |
data/threadcache | 论坛页面缓存(针对游客的优化) |
DISCUZ_ROOT/data目录下有几个重要的文件(文件锁)
- data/install.lock,安装程序锁定。如果该文件存在,DISCUZ_ROOT/install/ 中的安装程序不能执行。
- data/sendmail.lock, 发送邮件锁。Discuz 默认通过类似 home.php?mod=misc&ac=sendmail&rand=1379315574 这个隐藏页面调用,由用户的浏览行为触发邮件发送流程(浏览器侧用一个300秒的 cookie 控制频率,服务器侧通过 sendmail.lock 文件的 mtime 控制频率5秒)。如果可以控制服务器,应该优化掉这个机制。
- data/updatetime.lock, 某管理后台使用的锁。
- data/update.lock, 系统升级锁。执行版本升级程序(如x2升级到x3)时,会生成这个文件锁。
下面这些功能会涉及到多 web 服务器间的数据共享和同步,默认 Discuz 通过 MySQL 实现。
- 用户session, 表 pre_common_session
- 管理面板session, 表 pre_common_admincp_session
- 系统配置项缓存, 表 pre_common_syscache
NFS
我们假设部署两台 web 服务器的场景(且 web 服务器也是 php 应用服务器)。我们需要解决 data 目录共享的问题,引入 NFS 服务可以简单解决这个问题。服务器复用,此处不表。这里会有一个选择,哪些目录放置在 NFS 上,从上面的分析来看,将 data 目录放置在NFS上即可,即各 web 服务器均独立部署程序文件,将NFS挂载到 data 目录节点,缺点是需要将程序文件部署到每一台 web 服务器上,要解决程序文件更新部署的问题,优点是可以节省 web 服务器通过网络取 NFS 上的程序文件的开销。如果图方便,也可以把程序文件也放到 NFS 上,则所有文件都只有一个副本了,程序更新也很方便,缺点是会增加 web 服务器通过网络取程序文件的开销。这两者需要权衡,建议第一种。
上面的方案存在一些问题。当用户访问一个附件时,web服务器都需要通过网络从 NFS 上取文件,这给内网网络带来了压力和一些不必要的开销,这可以通过在web前端增加缓存机制来缓解(如 squid,nginx 的 proxy cache 等)。为静态资源配置单独的域名供访问也是值得实施的工作,Discuz 可以很简单的做到这一点,通过配置“本地附件 URL 地址”项就可以实现附件类(data/attachment目录里的)文件URL重构,但后台发布的广告不行,有 BUG(在X3版本测试)。
在使用文件锁,且依赖于文件的 mtime 等时间值执行逻辑时,请务必保证服务器时钟的一致性。
上面的方案简单,且对 Discuz 的改造很小,维护成本低,适合单台服务器向多台服务器(数量较少)扩展时选择。随着访问量和 web 节点的增加,内网流量,NFS,MySQL 均需要进行扩展。MySQL 的扩展有较成熟的方案,如主重复制机制。NFS 这个稍稍麻烦一点,且很多人诟病 NFS 的文件共享机制不安全;论坛附件以较小尺寸(几百字节不等)的文件居多,而 linux 的 ext 文件系统的块大小一般是4K,从而浪费了存储空间,对 inode 的利用率也不好;从长远来看,NFS 终将成为系统的瓶颈,我们有必要重新规划文件共享/同步机制。
这里我们先讨论文件共享问题,MySQL 扩展涉及的问题后面一点再说明。 目前很多公司都有解决大量小文件存储的方案,如国内某大互联网公司使用基于 MangoDB 的 GridFS 等。实现的细节不在讨论范围,其基本思想就是构建文件存储服务,把附件类静态文件存储到远端(远端系统返回一个 URL 供访问),并且由这个远端系统处理用户访问的各种优化等。我们讨论如果已经有了这样的一个服务,Discuz 接入到这样的一个服务需要注意些什么。但在这之前,我们来看一下 Discuz 的附件上传、存储流程。
帖子附件的上传和帖子的发布是异步的。附件上传后的实体文件会被存储到类似 data/attachment/forum/201307/20/
路径,同时会在附件表中(pre_forum_attachment_unused)添加相应记录;发布帖子时,这些记录被散列分布到相应的附件表(pre_corum_attachment_[0-9])中,并标识其所属的 pid, tid。这是附件本地存储的流程。
远程附件
Discuz 支持“远程附件”功能(全局-上传设置-远程附件),“远程附件”功能支持将附件通过 FTP 的方式存储到远端系统。如果网站当前没有文件存储服务,但又想将文件存储分离,使用这个内建功能也是一种不错的选择,毕竟 FTP 很好维护,也不需要对 Discuz 进行改造。虽然 Discuz 默认只支持 FTP 方式,但远端存储在功能接口层面基本是共同的概念,添加、删除之类。所以当要把“远程附件”扩展到自有的远端文件存储服务时,一个比较好的实践是继承 Discuz 的 ftp 类,用远端文件存储的功能重写 Discuz 的 ftp 类定义的各方法,然后在 ftp 类实例化的地方,调用这个新的子类;如果不打算保留默认的 FTP 机制,甚至可以直接修改 Discuz 的 ftp 类实现,这样连 ftp 类实例化的地方也不用修改了。这样的处理对 Discuz 的改造最小,细节都隐藏在了 ftp 类的实现中,遵守与 ftp 相同的行为模式。
远程附件将大部分的静态资源流量分离,我们可以分别的优化两个系统。但 Discuz 的远程附件的行为模式仍需要我们注意,我们必须清楚它是怎样运行的,是否有某种陷阱,以便于系统的某些功能行为表现异常时,心里有底。
开启远程附件时,附件上传、存储流程
附件异步上传阶段与本地存储是一样的,附件始终会先被上传到类似 data/attachment/forum/201307/20/
路径,同时会在附件表中(pre_forum_attachment_unused)添加相应记录,发布帖子时,这些记录被散列分布到相应的附件表(pre_corum_attachment_[0-9])中,并标识其所属的pid, tid。然后向远端上传附件数据,上传完成后,会删除掉本地的副本,并将附件表(pre_corum_attachment_[0-9])的remote字段标识为远程附件类型。
在什么时间点执行向远端上传
附件上传到远端的时间点是发布帖子的时候,且在该进程周期内,上传该帖子的所有附件(阻塞模型)。这会有一些风险点,如果上传附件到远端系统花费的时间较长(一个帖子的附件很多、很大,或网络带宽限制),用户界面的响应体验会有很明显的卡顿感,甚至页面超时失败。
已知会被存储到远端的数据
帖子附件
已知不会存储到远端的数据(或者说会被存储在本地)
- 帖子附件在上传到远端之前,会存储在本地。
- “游客查看小图”(需要启用“游客登陆查看大图”功能),小图是用类似forum.php?mod=image&aid=4&size=100x100&key=05869b37379ff990&type=1的调用生成的。小图生成并存储在类似data/attachment/image/000/00/00/123.gif ,且会在本地保留副本。
- 活动贴的封面不会存储为远程附件。
- 编辑帖子时,显示的缩略图(所见即所得)是即时生成的(通过网络访问远端获取原图),生成这个缩略图的过程中,会把图片存储到data/attachment/temp目录,图片数据内容输出到浏览器后,该位置存储的的缩略图被会删除。
- 图片附件的缩略图都是通过forum.php?mod=image&xxx 这个模块生成的。用户可以通过拼装请求,使服务器侧执行生成远程附件的本地副本的流程,所以这里可能存在某种风险。
从上面关于“远程附件”的讨论我们看到,即使启用了“远程附件”机制,但 Discuz 仍然会在众多功能上用到 data 目录下的多个子目录,且有的子目录还必须在多个 web 服务器间共享(如:data/attachment)。所以如果不对这些功能点进行改造,我们仍然需要 NFS 这个设备,毕竟远程附件已经将大部分的流量分走了,NFS 是保证业务正常运行的最简单的办法,谁知道还有多少其它功能会依赖于此呢?
MySQL扩展
在Discuz的业务模型上,对MySQL进行扩展使用最广泛的机制是“主从复制”、“读写分离”。Discuz 本身也支持这些机制,最近的版本还支持分库的部署,我们不讨论这些机制部署的细节,我们讨论将 Discuz 部署到这样的环境中时,需要做出的一些调整。
复制延迟造成的主库、从库数据不同步。
复制机制的延迟虽然很小,但总存在,即使是这种很小延迟,也足以给系统带来行为异常,特别是实施“读写分离”的场景。在 Disuz 业务场景中,以下情况是曾经出现过的:在后台配置的参数不起作用,有可能是配置项写入了主库(pre_common_setting),同时在主库删除了配置的缓存(pre_common_syscache),但在数据同步到从库之前,另一个请求发现(pre_common_syscache)没有缓存,于是触发了生成缓存的流程,这个流程会在从库中读到旧的配置项(“读”操作是在从库)。类似的场景还有很多,如刷积分,管理员进后台异常等。解决类似这种与时序相关的场景,往往需要针对个例去处理,就缓存这个问题,建议是在程序更新时间点,统一生成好缓存,且避免线上对配置的操作。
仅将需持久化的数据同步到从库。
如pre_common_session、pre_common_admincp_session, pre_forum_threadaddviews这类数据是不应该同步到从库的,它们更新非常频繁,且都是临时性的,它们应该被配置为忽略同步的表(replicate_wild_ignore_table选项),或者更好的办法是通过其它机制处理(如memcache、redis等)来实现,从而彻底从数据库中分离。从库同步主库的写操作时,同样会使用写锁,而这些性能开销是不必要的、应该优化,以使从库最大限度的服务于核心内容的读取查询。
其它一些多web部署时要注意的问题
改造内置计划任务。
默认Discuz内置的计划任务是通过用户浏览行为触发的,如果能控制 服务器,这应该改成用操作系统的计划任务驱动,Discuz!提供了api.php?mod=cron,稍作改造即可。仅在某一台web服务器上部署,并且将Discuz的每个任务单独部署成操作系统计划任务的一项,有一种选择是只部署一个“每分钟”周期的计划任务,然后由这个任务每分钟的轮询操作来驱动Discuz内置的计划任务机制,不建议这种做法,计划任务的数量毕竟是很有限的、执行的频率也是有计划的。
使用“文件”缓存配置项数据。即
$_config['cache']['type'] = 'file'
# Discuz默认是使用的MySQL存储配置缓存(表pre_common_syscache)。
$_config['cache']['type'] = 'sql'
如果开启了内存缓存机制,如memcache,这些缓存数据会放到缓存中,以缓解数据库的压力。这些缓存项是每个动态页面都需要调用的,所以如果浏览量高的话,这种方式在网络等方面的开销累计起来就很可观了。 建议使用文件缓存这部分数据,缓存文件跟着程序文件一起部署,每套程序文件都有一套配置项的缓存文件的副本,从而完全优化掉了这部分开销。优点:将配置项的缓存写入文件是 Discuz 内置的机制,无需改造;磁盘文件IO稳定性是最好的,成本是最低的,避免中心节点故障带来的风险;上线时间点所有配置项缓存已经生成,客观上达到了“暖缓存”效果。缺点:在系统上线部署之前,需要生成全部的配置项缓存文件,Discuz 默认的生成缓存文件的策略(Discuz 默认的策略是“找不到缓存再生成”,对于短时间并发较高的系统,这种策略往往会造成多个处理进程同时触发写缓存的情况)不能满足这个需求,这需要一些开发量。
在具体实施时,有一些建议。生成的缓存文件同程序源代码一样纳入版本控制,以便于跟踪配置变化。而通过后台UI操作,存储在数据库中的配置数据则没有这么方便。配置项缓存的生成比较简单,按照 pre_common_syscache 表的记录生成即可。
同理,模板缓存文件也可以在上线部署前完成生成,只是模板缓存文件初始化会麻烦一些,建议收集最常用页面的入口,建立脚本来触发。
如果要覆盖 Discuz 默认的配置项的值,建议启用一个配置文件,用新值覆盖旧值,尽量避免管理后台UI操作,特别是线上环境,因为配置参数最终需要与配置项缓存文件同步才能起作用。
所有web服务器上部署程序的路径要完全一样。Discuz 的缓存项,会生成一些绝对路径,一个例子是“本地附件保存位置”配置项(通过管理后台“全局-上传设置-基本设置”操作)。
mysql> select * from pre_common_setting where skey='attachdir';
+-----------+-------------------+
| skey | svalue |
+-----------+-------------------+
| attachdir | ./data/attachment |
+-----------+-------------------+
表pre_common_syscache cname=‘setting’
mysql> SELECT * FROM pre_common_syscache WHERE `data` LIKE '%attachment%';
在输出结果中查找 ‘attachdir’,有类似下面的内容
s:9:"attachdir";s:39:"D:/Apache/htdocs/./data/attachment/";
这些绝对路径需要在所有 web 服务器上存在,且功能匹配。如果网站的部署路径发生了变更,重新生成这些缓存项值。
论坛页面缓存
- “论坛首页缓存”(只针对游客有效)
- 缓存帖子;这些缓存数据默认被存储于 data/threadcache 目录,特别的,这些缓存数据没有过期删除机制,往往会生成大量缓存文件,需要注意监控缓存目录的情况。有些建议是使用类似 memcache 这样的有过期控制机制的设备来改造这个缓存,可能需要权衡网络的负载。
优化“更新主题浏览量”、“附件下载量延迟更新”选项。
如果是通过写文件的方式缓存这部分数据,建议把这些文件存储在各 web 服务器上,而不是写到 NFS 上(以减少网络开销,各种锁),在把数据汇总到数据库中时,需清理每台 web 服务器上的相关日志。最近的X版本通过 pre_forum_threadaddviews 表来缓存主题浏览量,个人感觉写文件的方式对于均衡性能方面更好。 通过上面的优化步骤后,只有 data/attachment 目录需要在各 web 服务器间共享,所以把 NFS 挂载到该目录即可。保持这个位置可以在 web 服务器间共享,可以在最小改造下避免众多已知和不预知的问题场景出现。
mount /PATH/TO/DISCUZ_ROOT/data/attachment NFS_SERVER:/PATH