十二/090
关于drupal架构的思考
自从drupalchina推荐我的blog后,很多drupal的开发者和我讨论drupal的性能,drupal的缺陷,大有感触。
很多人都说drupal站点不适合扩展,或者简单的做到数据库和代码分离后就茫然了。最近参考了很多软件和web架构的书,反过来对drupal这个系统做了一些关于架构扩展的思考。
以自己现有水平来看,drupal的瓶颈似乎在PHP代码执行层面。网上说使用APC加速可以大大加强drupal的性能,自己还没有手动进行测试,所以没有发言权。但目前在自己使用过程中,还是看到drupal一些小尴尬。
drupal的缓存系统比较简单,只是对匿名用户(因为匿名用户访问页面变更比较少)进行页面缓存,并且是以数据库的形式进行缓存。直接序列化页面到cache_page表中,这种性能可想而知,如果对百万级的访问量来说,db将会是个噩梦。
drupal用apache的rewrite模块实现了简洁链接,伪静态页面是提高了SEO效果,但是针对生成静态页面来说,还是比较困难的。另外,drupal的代码可以更加精简,否则为了强大的扩展性损失大幅性能也是不可取的。
有人说代码和架构扩展没有关系(忘记哪位大牛说的),其实我不是很赞同这句话,代码和数据库刚开始的设计决定或者制约了很多后面需要扩展的条件,比如水平划分数据库表的时候,最好是选取primary key,但是这个字段最好不是auto_increment的(道理很容易想到),再比如数据库抽象层的代码如果做memcache缓存,好修改吗?
很多时候,代码级别制约扩展性的很大一个因素就是联合查询,因为在做垂直划分数据库的时候,分离的都是一些关联性不大,并且不会牵扯到联合查询的库表,但drupal很多地方都使用的inner join(几乎每个模块都能看到)。当用户并发数越来越大,服务器最大的压力来源于数据库,这时,有些朋友就说了”负载均衡“啊。
没有那么简单,首先我们考虑基于重定向的负载均衡,多做几个后端服务器,使用RR或者简单随即抽取机制来轮流使用服务器,drupal和其他站点不同的一点小小特性就是唯一入口index.php,也就是说在这个文件我们可以写入一些代码,实现重定向的负载均衡,这点是比较合适的。或者您为了避免不是真正的压力平衡,可以使用DNS负载均衡,这样下来,稍微大点的数据量我们勉强顶得住了。
从水平划分的方面来看,首先最大的表应该是drupal的node表,nid字段是primary key,并且auto_increment的,这就很难进行分表存储。另外就算使用了一些别的算法,分N个表出来存储node,问题是如果使那些众多的node相关操作代码使用不会出问题?
另外一种做法就是使用ngnix的反向代理功能,很多java的开发者叫分布式。就算后端服务器性能差异,我们还是可以通过配置文件来决定权重,能者多得的方式,让每个机器发挥到极限。还可以考虑的方式就是使用apache加载我们定义的lua脚本,匹配select和insert,update,delete等字符串,用来区分属于何种sql查询,进一步实现读写分离。不过在机器之间的数据同步也是个麻烦的事情,我没有在运维方面有多少经验,最多就是使用过rsync。
分析种种情景,使用drupal的用户不要担心,只要我们想的到,可扩展的办法还是很多的。性能决定在你自己手里。
十/090
给drupal批量导入外部数据
很多时候,我们想给自己的drupal站点批量导入节点数据,其中包括很多方法,例如csv格式文件的导入,通过别的站点抓取到得信息,整合别的系统时,相互数据的迁移都要用到这些内容。我们建立一个简单的模型,只考虑节点基本数据,不考虑其他因素(比如节点图片,节点术语分类等等),将模型简单化有助于我们解决问题。
以拿抓取页面为例,我们使用正则表达式过滤出需要的信息后,将其存放在一个一维关联数组中,其中键对应数据库中的节点字段,比如title,body,name。。。然后我们手动调用node_submit方法,将一维数组传进去,这个方法会对我们的数组做一些存表之前的工作,我们直接可以在api.drupal.org查看这个方法的代码。
<?php
function node_submit($node) {
global $user;
// Convert the node to an object, if necessary.
$node = (object)$node;
// Auto-generate the teaser, but only if it hasn't been set (e.g. by a
// module-provided 'teaser' form item).
if (!isset($node->teaser)) {
$node->teaser = isset($node->body) ? node_teaser($node->body, isset($node->format) ? $node->format : NULL) : '';
}
if (user_access('administer nodes')) {
// Populate the "authored by" field.
if ($account = user_load(array('name' => $node->name))) {
$node->uid = $account->uid;
}
else {
$node->uid = 0;
}
}
$node->created = !empty($node->date) ? strtotime($node->date) : time();
// Do node-type-specific validation checks.
node_invoke($node, 'submit');
node_invoke_nodeapi($node, 'submit');
$node->validated = TRUE;
return $node;
}
?>
除了一些数据的可用性检查外,该方法调用了所有实现submit和nodeapi中实现submit的函数,之后就简单了,再调用node_save即可。
八/091
剖析ubercart
ubercart是一套很好的搭建电子商务平台代码,被很好的以模块形式应用到了drupal上,接合drupal的强大功能很容易生成电子商务站点。
下载了最新版本的模块看了看,代码量确实巨大。要先学会怎么用,再去看代码我想工作效率会更高,所以启用模块建了几个产品试试,购物车和付费平台什么的功能都走通了之后开始挑几个重要的代码段看了看。因为我要实现一个充值后增加userpoints积分的功能,找了半天终于找到一个相关模块,但是问题很多,issue页面也有人提出了类似的问题。没办法只能靠自己了,了解product模块和feature相关特性后,很轻松的就找到了问题所在。大家需要类似功能的可以参考下。
http://drupal.org/node/514534
解决办法在评论里面,不过我没有测试,只是看到代码流程没有问题了。首先建立一个feature,将产品节点绑定到这个feature里面,再实现order这个hook,在里面判断当订单完成状态后,使用userpoints模块的接口给购买用户增加分数。至于points的分类和数目完全可以自己在后台设置,还是不错的功能,只是纳闷作者为什么这么粗心,错了一个变量,错了一个表名。
八/090
编写安全的drupal代码
本文主要是参考那本经典的drupal 6 模块开发指南,接合自己对脚本安全的一点理解整理出来的,方便E文不是很好的朋友共享。
首先是文本格式转换方面的有效函数,以前的文章我也提过。check_plain()函数能安全的将用户任意输入转换为纯文本,filter_xss()函数放宽了用户的输入,可以输入一些不被过滤的html字符,既有好的样式也保证了安全性,而drupal_urlencode()则对url安全编码,比较常用,是对php的urlencode函数更好的一次封装。mime_header_encode()函数对邮件更好的编码,使用drupal_mail()函数时不需调用,已经内部应用了。
————-为database抽象层使用安全方面预留位置—————
drupal的文件安全已经在htaccess文件中做了很多限制,这个是web容器级别的安全,脚本方面我们仍然要考虑,使用文件路径前,最好调用file_check_location函数。为了避免邮件头部注入,drupal为我们引进了mime_header_encode()函数,文中提到一个很简单的例子作为参考。
如果邮件的标题(subject)是用户输入的,如果输入
Have a nice day%0ABcc:spamtarget@example.com%0A%0AL0w%20c0st%20mortgage!
邮件头部注入后则会变成:
Subject: Have a nice day
Bcc: spamtarget@example.com
L0w c0st mortgage!
…
一目了然。
另外,我们经常会编写独立的php文件来为drupal做一些维护或者数据迁移的工作,
include_once ‘includes/bootstrap.inc’;
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
这两行代码执行后,我们就可以在独立的php文件中使用drupal的db抽象层和相关函数,但最好我们加上uid为1的判断,只有超级用户才可以执行该php文件,如果该文件是操作发布站点数据,则这个限制更显得至关重要。
ajax的安全大家可以参考其他资料,请求脚本程序要格外小心。
最后,drupal的表单api中,title和description等元素必须check_plain()转换为纯文本,而default_value则无需,因为theme_textfield() 在includes/form.inc文件中已经调用一次了,二次转义将会使数据更加混乱。
以上只是接合drupal的安全方面简单陈述,要想更加安全你的代码,请参考更多的PHP安全方面的资料。
八/090
快速从drupal5升级到drupal6
从drupal5升级到drupal6的必要性有必要先说明下(听着有点绕口):一年前drupal6刚出来的时候大家都不建议升级,因为最大的问题就是相关模块支持太少,但到现在drupal6已经相当成熟,包括更严格的菜单路径映射功能(drupal5的时候菜单系统出现很多安全问题),更好的jquery加强站点UE,以前设置权重值很是麻烦,而现在只要进行拖动保存后就可以了,另外很多api更加细化,人性化和简单化。BTW,能升级到windows7的用户赶紧升级吧,因为比XP好用很多,我没有枪手嫌疑,因为我是个忠实的Linux用户。
关于升级服务器端部署问题这篇文章将不涉及,前面文章有介绍过,并且我记得在drupal.org上有篇文章是专门介绍如何从drupal5直接升级到drupal6的,我这里只是介绍模块开发人员要学习和参考的东西。
首先,菜单系统。我想这是在drupal6中最大的变化,首先$items这个序列化数组的键完全不同了,从callback 和 callback arguments变成了page callback,access也变成了access_callback以及access_arguments(包括title属性),这就意味着开发人员可以更好的定制drupal的菜单系统。而且drupal6引入的path arguments更好的规范了drupal的动态菜单,配合load后缀函数方面页面传递参数,这里改动比较复杂,大家参考官方文档。
数据库层改动不大,接下来说说form api几个地方的改动,几个hook的参数发生了改变,form_alter , form_validate, form_submit,第一个变成了三个参数,最后一个参数才是form_id,并且为了性能问题,因为之前form_alter要检测所有的form,加入了modulename_form_formname_alter函数。第二个参数数组发生了变化,现在都用$form_state['value']['element_name']判断,其中element_name是form数组中定义的表单元素。第三个提交后返回地址更加方便,直接修改$form['redirect']的值为内部链接就可以了,不用drupal_goto函数(其实也是对drupal_goto的再次包装),不过记得传递进来的是地址引用,而不是值。
在使用上来看,很多模块都更新了,操作更加方面,所以建议大家升级到drupal6系统。