天天看點

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;
		}
	}
           

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語句。