Elgg分布式扩展和性能优化(二)
目录
(前文回顾: Elgg分布式扩展和性能优化(一) )
继续来谈关于Elgg的种种,首先从LAMP的MySQL开始
4. MySQL InnoDB vs MyISAM
传统的LAMP应用默认都使用的是MySQL的MyISAM引擎,关于InnoDB引擎和MyISAM引擎的比较,网上可以搜到的资料很多,这里就不详细展开了,简单来说InnoDB的事务、行级锁定等特性在高并发的情况下对数据库性能有相当的提升。实际上,MySQL在5.5版本之后已经将默认引擎更换为InnoDB了。因此把Elgg的数据库表转换为InnoDB类型是个简单有效的优化手段。
对Elgg而言,大部分的表都可以直接转换为InnoDB类型,不过由于InnoDB不支持MyISAM的全文搜索功能,因为有几个加了全文搜索的表暂时不能转换,仍然保留MyISAM类型。如果想进一步完全转换到InnoDB,就要考虑去掉利用数据库的全文搜索功能,使用自己实现的第三方全文搜索来替代(如Lucene)。
总结一下,Elgg数据库里可以转换为InnoDB类型的表包括(表前缀elgg_)
elgg_access_collection_membership elgg_access_collections elgg_annotations elgg_api_users elgg_config elgg_datalists elgg_entities elgg_entity_relationships elgg_entity_subtypes elgg_metadata elgg_metastrings elgg_private_settings elgg_river elgg_users_sessions
保留MyISAM类型的表包括
elgg_groups_entity elgg_objects_entity elgg_sites_entity elgg_system_log elgg_users_entity
还剩下两个是Memory类型的,不需要改变。
5. Elgg Metadata non thread-safe bug
Elgg的Metadata在实现上有一个很严重的bug,这个bug会导致数据库数据异常,而且这个bug到目前为止官方都没有修复,和数据库也有一定的关系,所以在这里先写出来。
Elgg的metadata提供了一种灵活的给对象附加属性的方式,比如 $blog->author=‘Daniel’ 这样一句话就可以对一个blog对象赋予一个名字为author、值为Daniel的metadata,而在数据库里不用事先申明这个author字段(是不是有点NoSQL的感觉)。并且,Elgg说明可以为一个metadata赋予string或者array类型的值,问题就出在这里。
在实现上,->实际上是PHP5的magic methods,最终会调用ElggEntity类里的setMetaData这个方法
public function setMetaData($name, $value, $value_type = null, $multiple = false) {// normalize value to an array that we will loop over // remove indexes if value already an array. if (is_array($value)) { $value = array_values($value); } else { $value = array($value); } // saved entity. persist md to db. if ($this->guid) { // if overwriting, delete first. if (!$multiple) { $options = array( 'guid' => $this->getGUID(), 'metadata_name' => $name, 'limit' => 0 ); // @todo in 1.9 make this return false if can't add metadata // http://trac.elgg.org/ticket/4520 // // need to remove access restrictions right now to delete // because this is the expected behavior $ia = elgg_set_ignore_access(true); if (false === elgg_delete_metadata($options)) { return false; } elgg_set_ignore_access($ia); } // add new md $result = true; foreach ($value as $value_tmp) { // at this point $value should be appended because it was cleared above if needed. $md_id = create_metadata($this->getGUID(), $name, $value_tmp, $value_type, $this->getOwnerGUID(), $this->getAccessId(), $multiple); if (!$md_id) { return false; } } return $result; } // unsaved entity. store in temp array // returning single entries instead of an array of 1 element is decided in // getMetaData(), just like pulling from the db. else { // if overwrite, delete first if (!$multiple || !isset($this->temp_metadata[$name])) { $this->temp_metadata[$name] = array(); } // add new md $this->temp_metadata[$name] = array_merge($this->temp_metadata[$name], $value); return true; } }</pre>
bug出现在:当有多个进程同时并发的写同一个metadata值时,比如有100个进程同时写$blog->author=‘Daniel’ ,在数据库中就会重复出现相同的多条记录,如果并发持续的写的话,重复记录会迅速增长,严重的情况下,取该条metadata值会耗尽php可用内存,直接导致网站崩溃。
这是一个明显的race condition问题,其原因就是在上面的那段代码里,每当为一个metadata赋值时,会首先检查是否存在该metadata,如果存在,那么就先删除该条metadata的所有记录,然后再创建新值。在并发写的情况下,可能某个进程已经删除了该metadata,另一个进程在查询时会发现没有该记录,于是直接创建一条新记录,而此时前一个进程又会继续执行创建新记录,这样就导致了数据的重复。
实际上,“先删除再创建”这种机制最好的解决方法就是 - 使用事务(还记得之前我们为什么要把数据库换成InnoDB么),把上面的过程用事务-回滚的机制改写,就能彻底解决这个问题。
这个问题出现的根本原因,实际上是由于Elgg没有限制metadata为一个值或多个值,导致无法事先确定该metadata会有几条记录存在,因此只能用“先删除后创建”的笨办法保证逻辑的正确。所以这是一个设计上的问题,而Elgg的开发者现在也没有想出什么好的办法来解决,除非使用事务,这个bug也一直存在于Elgg的代码中:(
如果不想使用事务这种相对复杂的逻辑来解决这个问题,我也想出了一个临时的解决方案,不过首先要保证避免在开发中使用metadata来存储array,使得metadata的逻辑更简单。然后只需要在上述代码中注释掉
// if overwriting, delete first. if (!$multiple) { $options = array( ‘guid’ => $this->getGUID(), ‘metadata_name’ => $name, ‘limit’ => 0 ); // @todo in 1.9 make this return false if can’t add metadata // http://trac.elgg.org/ticket/4520 // // need to remove access restrictions right now to delete // because this is the expected behavior $ia = elgg_set_ignore_access(true); if (false === elgg_delete_metadata($options)) { return false; } elgg_set_ignore_access($ia); }这一部分就可以了,因为后面create_metadata方法实际上会检查如果已存在值,就更新,如果没有,就插入。这样做就能保证在并发写的情况下,只有一条记录被创建或更新,当然,最终的赋值是什么,要取决于最终是哪个进程最后调用并成功执行了sql语句。