彩票开奖结果 http://www.vczhtn.live 基于Drupal平臺的高性能網站架構與研究實驗室 Tue, 08 Aug 2017 11:42:13 +0000 zh-CN hourly 1 http://wordpress.org/?v=3.8 Authcache+Mobile Switch構建高性能Drupal站點 http://www.vczhtn.live/2014/03/authcache-mobile-switch-high-performance-solution/ http://www.vczhtn.live/2014/03/authcache-mobile-switch-high-performance-solution/#comments Sun, 30 Mar 2014 13:52:45 +0000 http://www.vczhtn.live/?p=1038 引子

安裝了上百個模塊之后的drupal大型網站一般都會遇到性能問題,尤其是當網站流量開始增大,各種性能和穩定性方面的問題也隨之出現。另一方面,我們既想充分適配移動端,又想在PC端保持特殊的設計,這時響應式設計主題出現在了我們的視野,但我覺得作為過渡,更好的方案是使用Mobile Switch模塊只為移動端使用響應式設計主題。

本文結合Authcache模塊和Mobile Switch模塊來討論一下這兩個模塊結合帶來的好處以及實施辦法。

為了獲得drupal高性能,我們需要在用戶請求的各個環節使用緩存技術,這些緩存技術既有普遍意義上的解決方案,例如APC, Memcache, varnish等等,也有根據Drupal自身特點,由第三方模塊實現的緩存機制,例如Boost模塊,Views Content Cache模塊,Entity Cache模塊等等。

大多數人都知道Boost在匿名用戶緩存領域是非常優秀的選擇,因為從Drupal啟動階段來講,Boost通過重寫(Rewrite)技術可以不進入Drupal即可識別和響應對應的頁面緩存,但對登陸用戶來講就失效了。(當然這一點通過修改Boost核心代碼,也可以讓登陸用戶使用Boost緩存,但不建議這么做,除非你的頁面對所有登陸非登陸用戶都是一樣的,或者實現了對動態內容的Ajax加載),由于本文重點在Authcache和Mobile Switch模塊的討論,因此其他性能優化模塊不展開討論了。

什么是Authcache模塊和Mobile Switch模塊

Authcache模塊用來解決對登陸用戶的緩存,支持各種后端緩存媒介,默認建議是使用數據庫后端(cache_page),因為比較好調試,也因為官方支持的最好,同時也支持Boost,Varnish,以及File Cache等機制。緩存是基于角色的,因此適用場景是相同URL對相同角色的不同用戶內容是完全一樣的。如果有所不同,需要利用Ajax或者ESI(Edge Side Includes)技術實現局部動態更新。

Mobile Switch模塊可以識別當前瀏覽設備是桌面瀏覽器,平板電腦還是智能手機,并且支持針對移動設備使用另外一套Drupal主題。由于URL是一樣的,所以我們內容必須繼承,只是樣式針對移動設備進行優化。

是否可以整合這兩個模塊

默認的這兩個模塊是不能一起工作的,同時開啟的話,如果桌面瀏覽器先訪問并生成了緩存,移動設備訪問同樣的地址將使用桌面瀏覽器生成的緩存,反過來,如果是移動設備先訪問,生成的緩存就是響應式的緩存,這時桌面瀏覽器訪問同樣地址就會看到移動設備生成的緩存。

好在Authcache提供了HOOK,我們可以通過實現其HOOK,同時獲得兩個模塊的特性。下面我們來介紹一下實施方法。

原理及參考代碼

其實核心原理就是對于相同URL,要根據不同的設備生成不同的緩存主鍵。因此要實現其微調緩存主鍵的鉤子,以下是在自定義模塊中需要實現的代碼。

/**
 * hook_authcache_key_properties
 * 
 * 根據Mobile switch模塊的檢測結果添加mobile_detect key
 * 
 * @param array $properties
 */
function HOOK_authcache_key_properties() {
  if (function_exists('mobile_switch_mobile_detect')) {
    $properties = array();
    $mobile_detect = '';
    $browser = mobile_switch_mobile_detect();
    
    if (is_array($browser)) {
      if ($browser['istablet']) {
        $mobile_detect .= ':tablet';
      } elseif ($browser['ismobiledevice']) {
        $mobile_detect .= ':mobile';
      } else {
        $mobile_detect .= ':standard';
      }
    }
    
    $properties['mobile_detect'] = $mobile_detect;
    return $properties;
  }
}

/**
 * hook_authcache_enum_key_properties
 * 
 * 根據Mobile switch模塊的檢測結果添加mobile_detect key
 * 
 * @param array $properties
 */
function HOOK_authcache_enum_key_properties() {
  if (function_exists('mobile_switch_mobile_detect')) {
    
    $properties['mobile_detect'] = array(
      'name' => t('Mobile Detect'),
      'choices' => array(':tablet', ':standard', ':mobile'),
    );

    return $properties;
  }
}

/**
 * hook_authcache_enum_anonymous_keys_alter
 * 
 * 枚舉匿名用戶緩存的幾種可能的Key
 * 
 * @global type $base_root
 * @param type $anonymous_keys
 */
function HOOK_authcache_enum_anonymous_keys_alter(&$anonymous_keys) {
  global $base_root;
  
  $anonymous_keys[] = $base_root . '/tablet';
  $anonymous_keys[] = $base_root . '/mobile';
  $anonymous_keys[] = $base_root . '/standard';
  
}

僅僅實現了上面的幾個鉤子是不夠的,還有一個匿名用戶的緩存主鍵是在Drupal啟動到第二步(DRUPAL_BOOTSTRAP_PAGE_CACHE)就已經完成了的,因此需要在前一步(DRUPAL_BOOTSTRAP_CONFIGURATION階段)就要定義自定義主鍵生成器函數,以下示例代碼需要寫在settings.php里:

/**
 * define authcache_key_generator callback
 * 
 * 至少要在DRUPAL_BOOTSTRAP_PAGE_CACHE及以后觸發
 * 
 */
function drupal_authcache_key_generator() {
  global $base_root;
  
  drupal_load('module', 'mobile_switch');
  
  if (function_exists('mobile_switch_mobile_detect')) {
    $browser = mobile_switch_mobile_detect();
    $mobile_detect = '';
    
    if (is_array($browser)) {
      if ($browser['istablet']) {
        $mobile_detect .= 'tablet';
      } elseif ($browser['ismobiledevice']) {
        $mobile_detect .= 'mobile';
      } else {
        $mobile_detect .= 'standard';
      }
    }
    
    if ($mobile_detect) {
      return $base_root . '/' . $mobile_detect;
    }
    
  }
  
  return $base_root;
}

# 啟用Authcache自定義主鍵生成器
$conf['authcache_key_generator'] = 'drupal_authcache_key_generator';

除此之外,我們如果想讓Authcache機制生效,最重要的一點還是在settings.php里聲明啟用Authcache,這和Drupal7其他緩存機制是類似的,參考代碼如下,以數據庫后端為例,如果模塊目錄存放與本文不一致,需要修改路徑。

$conf['cache_backends'][] = 'sites/all/modules/contrib/authcache/authcache.cache.inc';
$conf['cache_backends'][] = 'sites/all/modules/contrib/authcache/modules/authcache_builtin/authcache_builtin.cache.inc';

// 默認推薦使用數據庫緩存,如果使用了Memcache,并且內存非常大的話,也可以考慮放到Memcache里
$conf['cache_class_cache_page'] = 'DrupalDatabaseCache';

Authcache模塊自帶了25個子模塊,為了應用Authcache技術,我們要開啟其中的10多個甚至更多。這里是需要按需啟用的,至少要開啟一個后端,為了實現局部動態顯示需要用到Authcache Ajax或者Authcache ESI還有核心的Authcache Personalization API等子模塊,為了調試你可能需要臨時開啟Authcache Debug子模塊,為了在一些事件發生時(例如Flag,或者Vote)相應的頁面的緩存能夠失效,我們需要開啟Cache Expiration和Authcache Builtin Cache Expiration v1/v2模塊,為了讓一些Drupal常用元素支持Authcache還可以按需開啟自帶的其他子模塊。

總結

Authcache默認緩存主鍵生成機制是基于域名和角色的,本文為其加入了設備這一變量,如果有必要還可以考慮語言等其他變量,但如果把用戶UID這一變量加入進去,就變成了為同一個URL的每個用戶單獨生成一份緩存,這種方法雖然也是可以嘗試的,但比較不劃算,可能會耗費大量的磁盤或者內存空間。

在兩個模塊的作用下,即實現了登陸用戶的緩存,又實現了桌面瀏覽器和移動端分別使用不同的主題的目的,進而我們獲得了性能和體驗上的提升。

本文轉載自:Drupal項目社區,專業Drupal中文項目社區,專注于模塊,主題,發行版中文資料。

]]>
http://www.vczhtn.live/2014/03/authcache-mobile-switch-high-performance-solution/feed/ 0
基于Dropbox實現的免費私有Git版本庫托管 http://www.vczhtn.live/2013/11/free-private-git-repositories-dropbox/ http://www.vczhtn.live/2013/11/free-private-git-repositories-dropbox/#comments Mon, 25 Nov 2013 13:07:47 +0000 http://www.vczhtn.live/?p=928

你是否有私有Git項目需要托管卻又不想把你買午飯的錢用來購買Github的付費賬戶?Git與Dropbox整合使用可以免費的實現這一目的,從而你的代碼庫可以同步到Dropbox并且在多臺電腦上同步,通過這種方式你可以托管任意數量的版本庫,并且是云備份哦!~。Dropbox的免費賬戶就有2G的空間,如果只用來托管代碼是完全足夠的。

下面就來一步一步的實現這個功能。

創建一個普通的本地Git版本庫

$ cd yourproject
$ git init
$ git add *
$ git commit

創建一個git目錄來存放你的版本庫

在你的Dropbox目錄創建一個目錄用于存放Git版本庫,你當然可以將整個Dropbox目錄都用于托管,不過大多數人應該是用Dropbox備份很多私人數據的吧。

$ cd ~/Dropbox
$ mkdir git

創建一個bare版本庫

現在為你的本地工作拷貝創建一個空的版本庫作為Git遠程版本庫,除了這是在本地操作以外,這本質上和用Github或者其他版本庫托管網站的工作方式類似。

$ cd git
$ mkdir yourproject.git
$ cd yourproject.git
$ git --bare init

添加remote origin并且push到“遠程”Dropbox版本庫

每次同步都會被Dropbox進程自動檢測到改變并自動同步到Dropbox服務器

$ cd yourproject
$ git remote add origin ~/Dropbox/git/yourproject.git
$ git push origin master

注意事項

  1. 這個方案只適用于單用戶的私人項目,多個人一起使用這個方案會有沖突。
  2. 其實更好的方案是使用BitBucket這樣的私有項目托管方案,但我更傾向于雙保險,尤其是當BitBucket不定時的出現問題的時候。
  3. 此方案在Mac下測試通過,理論上Linux和Windows也應該是類似的方法。
  4. 類似于Dropbox的服務提供商應該都可以用來做這種備份,但是要注意各家提供商的技術實力,是否會有丟失數據的情況,Dropbox的技術實力應該是比較強的。
  5. 為什么不直接將Git的代碼庫直接放到Dropbox目錄呢,原因是這樣一邊開發一邊同步很可能會產生沖突,而本文描述的方式則不會產生這種沖突。

譯者手語:本文是翻譯之作,并融入了譯者的個人理解,若有翻譯的不好或者不對之處,還請同行朋友指點,如需轉載請注明出處。

]]>
http://www.vczhtn.live/2013/11/free-private-git-repositories-dropbox/feed/ 1
Drupal模塊目錄組織方式總結 http://www.vczhtn.live/2013/08/drupal-modules-directory-structure/ http://www.vczhtn.live/2013/08/drupal-modules-directory-structure/#comments Fri, 16 Aug 2013 15:31:24 +0000 http://www.vczhtn.live/?p=860 我們都知道在drupal系統目錄下模塊可以放在很多目錄下,drupal會按照一定的次序掃描所有的符合規范的目錄下的模塊。但是并不意味著我們可以隨意放置模塊,比如系統的modules目錄下放的都是核心自帶的模塊,為了以后的升級方便,我們不應該將模塊放在/modules目錄里面,那么Drupal的模塊應該怎樣放置呢?

多站點模式

如果你的網站是以Drupal多站點方式運作的,意思是多個網站共享一套Drupal代碼,這時我們的第三方模塊一般都放在/sites/all/modules目錄。而其他模塊則分網站放到/sites/網站名/modules目錄下,如果你的自定義模塊想要跨多站共享的話,也需要放到/sites/all/modules里,這時為了區分,你需要在/sites/all/modules目錄里建子目錄,例如contrib代表第三方模塊目錄,custom代表自定義共享模塊目錄。

Drupal多站點其實還有一種不共享代碼,只共享數據庫的情況,但這與本文要討論的主題無關,就不做過多說明了。

單站點模式

單站點模式是我們最常見的情況,我們就是要用一套Drupal代碼建一個站,如果是一個比較大型的網站,需要使用的模塊眾多,我們就需要做一些規劃了,以下是我在開發過程中的一些心得,希望對大家有所幫助。(下文的目錄都放在/sites/all/modules目錄下,以保證/sites目錄下的站點目錄干凈清爽)

contrib?

毫無疑問,這個目錄是放置第三方模塊的

custom_contrib

放置的也是第三方模塊,但是有一點點與我們的需求不符,又沒有提供足夠的鉤子做擴展,所以我們就需要硬編碼了,放在這個目錄可以提醒你哪些模塊是被修改過的,升級時要多加小心,不要遺漏之前打過的補丁。

custom

存放自定義模塊

features

存放我們經過規劃從后臺導出的一批features,每個feature一般是要圍繞一個功能特性進行打包,不過如何規劃features每個人可能有不同的理解,只要能有清晰的思路,并且以后便于維護即可。

development

存放所有開發相關,而與網站業務邏輯無關的模塊,比如devel, schema等,這些模塊也不一定是只能在本地使用,但一般是不建議在生產服務器啟用的,將這些模塊放在一起,對生產服務器的問題排查和優化有一定的幫助。

localhost

這是一個特殊的目錄,里面存放的是不放入版本控制的模塊,可以是第三方模塊,也可以是自定義模塊,一般我會在里面繼續細分一些子目錄,但localhost目錄則需要根據版本控制軟件設置目錄的ignore屬性,這樣不管里面放了多少代碼都不會因為誤操作上傳到代碼庫。

第三方模塊,之所以有第三方模塊放到localhost是因為在團隊開發過程中,你不能任意的提交模塊到版本庫,而有些開發相關的模塊對本地開發又很有幫助,所以我將版本庫里沒有,但對我有用,對其他人未必有用的第三方模塊,主要是開發相關的模塊,放到localhost下,以提高本地開發調試效率。

自定義模塊,自定義模塊放到localhost當然也是不希望代碼被上傳到版本庫,但為什么有這樣的自定義模塊呢,當然也是為了本地開發效率的提高,這也是一些開發相關的模塊,但大多數情況下,都是對業務邏輯和數據做一些CRUD,比如一鍵插入刪除測試數據,比如一鍵刪除近期測試用戶等等,這對本地開發效率有極大的提升,或者可以保證本地數據庫的精簡。

not_in_use

這個目錄里的模塊來自于custom目錄,因為一些自定義的模塊可能因為某些原因,比如需求變更,模塊功能不再需要,這時如果封裝良好的話,需要把模塊禁用掉,但由于自定義代碼里包含許多業務邏輯,刪除肯定不是一個好辦法,我們以后可能需要把這個功能拿回來重新使用,或者需要參考里面的代碼,所以我們會需要把這樣的模塊放到一個單獨的目錄,從而哪些模塊正在被使用,哪些模塊目前已經不用了,就是一目了然的了。

總結

從以上目錄劃分可以看出,我們就是在通過多放幾個子目錄讓代碼結構,主要是模塊結構,變得更有條例,在實際項目中,由于業務邏輯的不同,我絕對相信大家有需要新建除了上文提到目錄以外的其他目錄,而且根據項目大小的不同,以上的目錄建議也不是都必須存在的,大家可以根據實際情況進行調整。

總之由于網站一般都需要長期維護,而隨著時間的推移很多項目相關的信息你可能都有所遺忘,因此我們開發過程中總要想一些辦法讓今后項目可以比較容易的理解和維護,從小的方面是代碼符合規范,注釋良好,代碼精煉易懂,從大的方面就是項目的目錄結構,技術架構,文檔,任務管理等等。

最后,如果大家對模塊的目錄組織結構也有自己的理解,可以通過回復本文與大家分享。

補充知識點

移動模塊目錄的方法:(感謝流云同學的提議)
移動模塊目錄意味著其定義的hook_menu都不會正常工作了,許多依賴于此模塊的模塊無法正常工作,因此如果代碼書寫不規范,大量移動模塊可能引起白屏問題。這里簡單說下常見的兩種情況的解決方法。

1,如果不發生白屏,則先到后臺的模塊列表頁,這個頁面有一個功能就是重置掃描模塊列表,然后清緩存即可。
2,如果發生白屏,進不去模塊列表頁,可以自己在index.php里,加上registry_rebuild()函數(D7,D6是另一個rebuild函數),執行一次之后刪掉,然后清緩存。

 

]]>
http://www.vczhtn.live/2013/08/drupal-modules-directory-structure/feed/ 5
Drupal系統A/B Test解決方案 http://www.vczhtn.live/2013/03/the-solution-of-ab-testing-in-drupal-system/ http://www.vczhtn.live/2013/03/the-solution-of-ab-testing-in-drupal-system/#comments Mon, 04 Mar 2013 15:21:22 +0000 http://www.vczhtn.live/?p=766 引言

本文是我在工作中遇到A/B Test的需求,經過幾天的研究的一篇心得筆記,不一定多專業,希望能給有類似需求的同行一些參考和靈感,在學習的過程中,也發現一些比較優秀的國外博文,我打算在近期翻譯幾篇,而不是把只言片語融入到本文當中,本文的重點仍然是drupal A/B Test的解決方案,介紹性的文字來源于最近幾天的學習心得,是給不熟悉A/B Test的同行的一些入門介紹。

PS1:所說的A/B Test,我想正確的說法應該是A/B Testing,但國人可能更習慣說A/B Test,本文也不是咬文嚼字,所以全篇也都叫A/B Test了。

PS2:本文以drupal 6環境為依托來介紹,Drupal 7也許會有更好的解決方案。

A/B Test 簡介

這一部分主要是簡單介紹一下A/B Test的概念和相關知識。從而讓讀者對A/B Test有一個大概的了解。

什么是A/B Test?

A/B Test,是指對某一個特定的頁面或者頁面上的一部分,制作兩個或兩個以上版本,然后將網站的訪問流量分發到不同的版本上,繼而觀察網站該頁面或者該部分帶來的轉化率,或者一些重要指標的變化,從而得出其中的一個版本比另一個版本更好的結論,并以此決定最終上線的版本。

借用網上的一個圖來幫助大家理解A/B Test
AB Test 示意圖

最開始的時候,我覺得主要是市場人員希望知道改變了字體大小,顏色,文案之后,網站的轉化率究竟發生了怎樣的變化,但現在我們已經把A/B Test的內容進行了擴展和深化,可以應用于網站的方方面面,前端后端,是持續運營網站的一個有效工具。前端很好理解,后端的A/B Test舉個例子來說,比如為了給用戶推薦一個產品列表,我們設計了幾個不同的算法,推薦結果是不同的,但從功能上又是相同的,那么怎么決定最終用哪個算法呢,這時候就可以針對算法做一次A/B Test。

什么是Bucket Test

Bucket Test 是A/B Test概念的擴展,中文翻譯成分桶測試,外國人也有叫Split Test的, 其實都是一個意思,就是同時做多個A/B Test, 不是只有一個B版本,而是B1, B2 … BN 一共N個版本,做一次Bucket Test 就可以在多個改進版本中選出一個最優的版本。

實現Bucket Test的系統,就是Bucket Test System, 簡稱BTS, 一般來說只有有這樣的系統才可以做好Bucket Testing, 因為他可以幫助我們很方便的管理每組測試,為每個Bucket分配流量,實時監控每個Bucket的運行效果,轉化率等等各項指標。BTS根據各個公司的開發實力以及需求,可以做的很復雜,尤其是對自身業務關鍵指標的抽象,計算。如果是小公司,可以直接使用第三方系統做流量切割,然后使用半手動的方式評估每個版本的效果。

但是在實際測試中,一定要注意每個bucket要有足夠多的測試樣本,流量不平均分配也沒事,但數量要夠大,這樣你才能對測試結果有信心,測試才有意義。

多因素測試

多因素測試是一種Bucket Test, 不是所有的Bucket Test都這樣做。 比如在一次改版任務中, 我們認為提升轉化率的關鍵因素有3個,標題字體大小有3個版本,標題顏色有2個版本,標題文案有2個版本,那么就有3x2x2一共12種組合, 所以我們就相當于有了12個Bucket, 這樣我們就可以進行Bucket Test了, 最后我們得到一個結論,這3個關鍵因素的某個組合轉化率最好,那么這個版本就是我們最應該采用的版本,擁有最優化的關鍵因素組合。

大多數時候,我們其實每次改版改動都是很大的,不去區分究竟哪些地方改變了,或者我們改動了很多地方,這樣做多因素測試就是不現實的,這時往往我們就弄一兩個自己認為關鍵的版本(往往就一個)就可以了,這樣得到的結果未必是此次改版所能達到的最好效果,只能看這個版本和原版本的比較結果,但即使這樣也是有意義的,比直接上線新版一段時間,然后再觀察各項關鍵指標要客觀。

A/B Test的原則

  1. 只測試少量因素
  2. 因素改版要有顯著變化
  3. 針對大流量頁面做測試
  4. 持續測試,僅僅做一兩次A/B Test是達不到提高轉化率的目的的
  5. 各版本頁面傳達的信息應該是一致的只是形式稍有不同
  6. 應該在同一時間測試
  7. 各版本得到的流量都要比較大,這樣結果才有說服力
  8. 不要過早的停止A/B Test
  9. 要讓訪問測試版本的訪客始終看到那個版本

Drupal系統的A/B Test解決方案

BTS系統有很多,這里主要推薦Google Analytics Content Experiments工具,關于怎樣使用,這里有一個比較好的中文介紹,利用Google Analytics測試優化網站內容

我們是Drupal程序員,那么我們在做A/B Test的時候,需要做些什么以及如何做呢,本文給出的是一個完全原創的代碼級解決方案,需要使用GA Content Experiments做流量分發和效果監控,本文主要是告訴大家在這套方案里我們的代碼應該怎樣寫。

將GA A/B Test相關的JS代碼嵌入head標簽

首先,GA A/B Test工具中配置一個A/B Test實驗時,會讓你在原始版本添加一些GA統計代碼到head標簽,Drupal自身對這件事情支持的不是特別好,所以我的方案是在page.tpl.php里加入新的自定義變量到head標簽,然后在template.php里加邏輯設置這個變量,最后在settings.php里對GA統計代碼隨時添加刪除,這樣做是因為A/B Test一般都是臨時的,所以某些時候我們需要快速開啟和關閉A/B Test,沒有必要每次都走release流程。

編輯page.tpl.php,因為GA 的AB Testing工具要求統計代碼放到head標簽最開始的位置。(這里是因為不加空格就顯示不出來)

< head >
< ?php print $head_prepend;? >

編輯template.php, 我們在主題的template.php里為新加的模板變量復制,注意值是在settings.local.php里設置的,其實這里如果做的復雜一些,我們可以為模板變量的復制單獨開發一個管理后臺,但簡單就是美,這里還是以突出核心概念為主。

function MYTHEME_preprocess_page(&$vars) {
  $vars['head_prepend'] = zinchus_process_page_head_prepend();
}

自定義函數,放到了template.php,因為不是API

function MYTHEME_get_page_head_prepend() {
  $request_uri = request_uri();

  $head_prepend = variable_get('head_prepend', array());
  if (array_key_exists($request_uri, $head_prepend)) {
    return $head_prepend[$request_uri];
  }
  return '';
}

在settings.php里設置統計代碼,注意里面的key是request_uri,也就是實際路徑,不是Drupal Path

if (empty($_GET['v'])) {
  $conf['head_prepend']['/node/374'] = [GA CODE];
}

為原始代碼實現多個版本

前面的邏輯都寫好以后,我們就可以很方便的在任意頁面插入統計JS代碼了,但同時GA Content Experiments還需要多個版本,這里以一個版本為例,我們是可以隨意開發一個新的版本用一個新的url,但要注意,這只是一個實驗,在得出結果以后,好的版本會留下,不好的版本會被淘汰,這時如果我們讓網站同時存在兩個版本,時間長了會越來越難維護,有很多冗余代碼在里面,為了解決這個問題,我寫了一個簡單的入口函數abtest()。也就是說我給出的是一個函數級別A/B Test的方案,可以給任意函數添加新版本。

下面來簡單看一下這個函數的用法

// 注意第二個參數可以傳多個同功能新版函數
$output = abtest('func_a', array('func_b'));

當測試結束是,我們一般會隨時希望將效果好的版本正式上線,這時我們會選擇使用settings.php。如果想更加易用一般會為這個setting開發后臺用于開關和版本選擇,但settings.php一般來說是我們更加常用的settings設置手段,因為這種settings照顧到了可配置性,而且一般配置好了也不經常修改。

// 如果設置成0就是強制啟用原始版本,如果是2就是強制使用新版本2(如果有的話)
$conf['abtest_force_func_a', 1);

入口函數源碼

// 這個是用于分發各個版本的入口函數,很簡單不是么
// 第一個參數是原始函數,第二個參數是多個新版函數名組成的數組,第三個參數是傳給函數的參數。
function abtest($test_function, $buckets = array(), $args = array()) {
  !is_array($buckets) && $buckets = (array) $buckets;
  $bucket = variable_get('abtest_force_' . $test_function, NULL);

  if (!isset($bucket)) {
    if (isset($_GET['bucket_name'])) {
      $bucket_name = check_plain($_GET['bucket_name']);
    } else {
      $bucket_name = variable_get('abtest_bucket_name', 'v');
    }
    if (isset($_GET[$bucket_name])) {
      $bucket = check_plain($_GET[$bucket_name]);
    } else {
      $bucket = 0;
    }
  }
}

這里需要注意的是即使是Drupal頁面級的A/B Test也可以從這個方案中獲益

// 示例代碼
$item['page callback'] = 'abtest';
$item['page arguments'] = array('func_a', array('func_b'));

最后,為了清理代碼方便,原則上建議把新版本的代碼規劃好,比如每個新版本都放到不同的文件夾中,雜揉在一起在將來刪除時會浪費不少時間。

小結

A/B Test是一種數據驅動運營型網站的常用運營工具,因為我們需要不斷優化,改版增加網站的用戶體驗,提高轉化率,我們不僅要改,還要知道修改的部分對網站轉化率的影響,還要知道影響的程度,這些數據有的可以通過BTS給出來,但有的需要對現有數據庫進行數據分析,為此還可能需要在各個版本里嵌入自己的關鍵數據收集代碼。總之一切都是為了做出更好的網站和提高我們的收入。

補充內容

在Drupal項目中實施A/B Test的過程中,我們遇到了很多非典型問題,下面補充一些相關內容

B版本只有匿名用戶能看到,而A版本登錄和匿名用戶都能看到

這個解決起來很簡單,只要登錄用戶不加載GA的實驗代碼,就不會進入統計了。

GA的A/B Test流量分流不是平均分配

這個大家在使用過程中一定會遇到,因為這是GA故意為之的,目的也很好理解,A/B兩個版本的線上測試,如果B版本比A版本好太多,意味著無論是Page views,還是實際銷售額肯定相差很大,如果平均分配流量,是會損害網站的核心利益的,所以GA從算法上,使得根據轉化率自動調整流量分配,轉化率高的,得到的流量就多。

但有的時候,我們是需要流量平均分配的,這個時候,trick的解決方案是設置一個永遠不可達到的Goal頁面。經測試這種方式是十分有效的。

有興趣的可以繼續閱讀這篇GA文檔

A/B 兩個版本的Goal頁面不是同一個

我們知道GA的A/B Test實驗只能設置一個,這種兩個Goal頁面的事情,其實是在打破A/B Test的規則,這也數據測試的內容太多的問題,我們這是兩個產品(頁面同時上線,同時測),然后從總的轉化上把握效果,不管是否對錯,但總是要先實施出來,這里的實施方法是,在GA里只設置A版本的Goal頁面,然后看B版本有多少去了A版本的Goal,算出比例,然后把A版本帶來的轉化量去掉這個比例后再進行評估。

參考文獻和資源

1,http://www.analyticskey.com/content-experiment-google-analytics/
詳細介紹了Google Analytics Content Experiments的用法。我們的A/B Test方案中,流量分發和部分的數據監控都是交給GA的,我覺得這樣沒什么不好,可以讓我們更專心專注于業務邏輯即新版本的開發。

2,http://drupal.org/project/multivariate
Drupal Way的A/B Test模塊,即提供了BTS的管理后臺,又可以監控,以及A/B Test實施,我覺得做目前還用不上這個模塊,或者說這個模塊不能解決我們網站遇到的問題。

3, http://phpabtest.com
PHP級別的A/B Test解決方案,也是與GA繼承,但似乎其并沒有用到Content Experiments功能,而是簡單的往GA發送數據的方式。不過還是值得借鑒的。

4,http://whichtestwon.com/
一個投票網站,投A/B 兩個版本哪個你覺得更好,這個就沒什么轉化的概念的,所以雖然也像是一個A/B Test,但我覺得更像是一個游戲。

5, http://visualwebsiteoptimizer.com
一個BTS系統,據說比GA提供的工具更強大,其實還有很多類似的第三方系統,不一一列舉,我們目前是還沒有到使用這樣的系統頻繁做大量A/B Test的程度。

6, http://www.smashingmagazine.com/2010/06/24/the-ultimate-guide-to-a-b-testing/
這篇文章的主要看點是看看各個網站是怎么做的A/B Test,又因為A/B Test提高了多少轉化率,可謂是A/B Test里的經典案例了。

]]>
http://www.vczhtn.live/2013/03/the-solution-of-ab-testing-in-drupal-system/feed/ 2
Drupal實用本地調試函數 http://www.vczhtn.live/2012/10/drupal-local-debug-helpler/ http://www.vczhtn.live/2012/10/drupal-local-debug-helpler/#comments Mon, 29 Oct 2012 06:10:42 +0000 http://www.vczhtn.live/?p=669 drupal開發中,調試是必不可少的,可以幫助我們找到bug,或者性能優化、或者改善用戶體驗等等。而關于調試的話題,本站已經有過幾篇文章專門論述了,大家可以參考如下幾篇:

Drupal調試之Devel模塊使用技巧
Drupal/PHP性能分析工具之xDebug

本文的重點不是向大家推薦其他需要安裝的工具,而是一段我認為非常有用的調試代碼,有了這段代碼,可以很清晰的了解一些debug信息。

define('DEBUG_INIT', 0x0001);
define('DEBUG_STOP', 0x0002);
define('DEBUG_SHOW', 0x0004 | DEBUG_STOP);
define('DEBUG_RETURN', 0x0008 | DEBUG_STOP);
 
/**
 * Debug helper for time consuming, check real SQL to investigate bug or performance issue.
 *
 * @global type $conf
 * @global type $queries
 * @staticvar array $timers
 * @staticvar array $run_times
 * @staticvar array $queries_timer
 * @staticvar array $last_info
 * @staticvar null $old_values
 * @param system $timer Name your debug code
 * @param type $mode could be DEBUG_SHOW or DEBUG_RETURN generally
 * @return type
 * @version 1.0
 * @author Richard Yu (vipzhicheng#gmail.com)
 */
function local_debug($timer = NULL, $mode = DEBUG_INIT) {
 
  if (!isset($timer)) {
    $timer = __FUNCTION__;
  }
 
  global $conf, $queries;
  static $timers = array();
  static $run_times = array();
  static $queries_timer = array();
  static $last_info = array();
 
  if (!isset($run_times[$timer])) {
    $run_times[$timer] = 0;
  }
 
  $run_times[$timer]++;
 
  if ($run_times[$timer] == 2 && $mode === DEBUG_INIT) {
    $mode = DEBUG_SHOW | DEBUG_STOP;
    $run_times[$timer] = 0;
  }
  else if ($mode & DEBUG_STOP) {
    $run_times[$timer] = 0;
  }
  else {
    $queries = array();
    timer_start($timer);
 
    if (!isset($timers[$timer])) {
      $timers[$timer] = array();
    }
  }
 
  static $old_values = NULL;
 
  if (!isset($old_values[$timer])) {
    $old_values[$timer] = $conf['dev_query'];
    $conf['dev_query'] = 1;
 
  }
  else if ($mode & DEBUG_SHOW || $mode & DEBUG_RETURN) {
    $conf['dev_query'] = $old_values[$timer];
    $old_values = NULL;
  }
 
  $record_info = FALSE;
  foreach ($timers as $key => $value) {
 
    if ($key === $timer && $mode & DEBUG_STOP) {
      $timers[$key]['read'] = timer_read($key);
      timer_stop($key);
      $record_info = TRUE;
      break;
    }
  }
 
  foreach ($queries as $key => $query) {
    $queries[$key][1] = round($queries[$key][1] * 1000, 2);
    $queries_timer[$timer] += $queries[$key][1];
 
    // Don't show if it is slave query
    if (isset($queries[$key][2])) {
      unset($queries[$key][2]);
    }
  }
 
  $info = array(
    'queries' => $queries,
    'total_timer' => $timers[$timer]['read'],
    'total_query_timer' => $queries_timer[$timer],
    'total_process_timer' => ((float)$timers[$timer]['read'] - (float)$queries_timer[$timer]),
 
    'this_timer' => $timers[$timer]['read'] - $last_info[$timer]['total_timer'],
    'this_query_timer' => $queries_timer[$timer] - $last_info[$timer]['total_query_timer'],
    'this_process_timer' => ((float)$timers[$timer]['read'] - (float)$queries_timer[$timer] - $last_info[$timer]['total_process_timer'])
  );
 
  $last_info[$timer] = $info;
 
  if ($record_info) {
    $timers[$timer]['infos'][] = $info;
  }
 
  if ($mode & DEBUG_SHOW) {
    dpm($info, $timer);
  }
  else if ($mode & DEBUG_RETURN) {
    return $info;
  }
}

這段代碼一般可以粘貼到settings.php里,就可以到處運行了。我一般會在本地開發一個不進SVN的local模塊,把這個函數放在local模塊里。注意,本函數依賴devel模塊開啟

這個模塊的運行結果截圖如下:

調試信息

從調試信息的截圖,我們可以看到,調試信息里有真正執行的SQL語句,有其執行時間,還有整個代碼的執行時間。看調試信息有total_開頭的時間,還有this_開頭的時間,因為如果給local_debug第一個參數傳同樣的計時器名字,則時間是會累加的,時間的單位是毫秒。

給調試代碼命名的示例代碼如下:

local_debug('sql-1');
db_query("SELECT 1 FROM {users} LIMIT 1");
local_debug('sql-1');
 
local_debug('sql-2');
db_query("SELECT 1 FROM {node} LIMIT 1");
local_debug('sql-2');
 
// sql-1的運行總時間會累加
local_debug('sql-1');
db_query("SELECT 1 FROM {system} LIMIT 1");
local_debug('sql-1');

如果是希望將調試信息記錄到日志里則:

local_debug('sql-1');
db_query("SELECT 1 FROM {node} LIMIT 1");
YOUR_LOG_FUNCTION(local_debug('sql-1', DEBUG_RETURN);

注意事項是本函數目前只支持串行的使用,還不支持嵌套

本函數的使用場景:

  1. 想看看一個db_query執行時替換后的SQL語句的原貌
  2. 想看看一個我們自定義函數或者一大段代碼里執行了哪些SQL語句
  3. 想看看我們代碼的性能如何
  4. 診斷BUG,一般配合devel模塊的php在線運行功能
  5. 為記錄系統日志提供一定的信息

總之這個函數就是一個助手函數,我相信可以給大家的日常開發幫助帶來一些幫助。本函數在以后實際使用中會根據需要再次進行調整,大家有什么好的建議也可以提出來。

]]>
http://www.vczhtn.live/2012/10/drupal-local-debug-helpler/feed/ 0
MYSQL獲取隨機結果集的解決方案 http://www.vczhtn.live/2012/01/solution_for_mysql_random_results/ http://www.vczhtn.live/2012/01/solution_for_mysql_random_results/#comments Wed, 18 Jan 2012 14:01:42 +0000 http://www.vczhtn.live/?p=314 在我們的業務需求當中,經常有需要取得隨機結果的需求,比如隨機會員,隨機文章列表,隨機文章跳轉等等,我們大家都知道MYSQL的ORDER BY RAND()有性能問題,本文翻譯自國外的一篇博文,大家來學習一下作者是如何解決這個問題的,這個解決方案具有在生產環境中實施的可行性。

譯文開始:

作為第一個例子,我們假設數據的ID從1開始,并且在1和最大值之間是連續的。

把事情交給應用層(PHP, JSP, Python, Ruby …)

第一個思路:我們可以簡化整個工作,如果我們可以預先在應用層計算出隨機ID

SELECT MAX(id) FROM random;
## 在應用層生成隨機ID <random-id>
SELECT name FROM random WHERE id = <random-id>

因為MAX(id) == COUNT(id), 我們僅僅是在1和最大值之間生成了隨機數,然后傳給數據庫取出隨機記錄。

第一個SELECT是已經被優化好的,不需要任何計算。第二個是eq_ref(參見MYSQL EXPLAN語句)是一個常量,所以也非常快。

把事情交給數據庫

在應用層做這件事真的是必要的么?我們不能在數據庫中做么?

# 生成一個隨機的ID

> SELECT RAND() * MAX(id) FROM random;
+------------------+
| RAND() * MAX(id) |
+------------------+
|  689.37582507297 |
+------------------+

# 哇,是一個浮點型數字,我們需要的是一個整型的隨機數

> SELECT CEIL(RAND() * MAX(id)) FROM random;
+-------------------------+
| CEIL(RAND() * MAX(id)) |
+-------------------------+
|                    1000000  |
+-------------------------+

# 看起來好了一些,但性能呢?

> EXPLAIN 
   SELECT CEIL(RAND() * MAX(id)) FROM random;
+----+-------------+-------+-------+------+-------------+
| id | select_type | table | type  | rows | Extra       |
+----+-------------+-------+-------+------+-------------+
|  1 | SIMPLE      | random  | index | 1000000  | Using index |
+----+-------------+-------+-------+------+-------------+

## 需要掃描索引 ? 我們還沒有對 MAX()進行優化。

> EXPLAIN 
   SELECT CEIL(RAND() * (SELECT MAX(id) FROM random));
+----+-------------+-------+------+------+------------------------------+
| id | select_type | table | type | rows | Extra                        |
+----+-------------+-------+------+------+------------------------------+
|  1 | PRIMARY     | NULL  | NULL | NULL | No tables used               |
|  2 | SUBQUERY    | NULL  | NULL | NULL | Select tables optimized away |
+----+-------------+-------+------+------+------------------------------+

## 一個簡單的子查詢讓我們重新獲得了我們想要的性能。
好吧,現在我知道了怎么獲得一個隨機數,那么怎么獲得一條隨機記錄呢?

> EXPLAIN
SELECT name
  FROM random
 WHERE id = (SELECT CEIL(RAND() *
                         (SELECT MAX(id)
                            FROM random));
+----+-------------+--------+------+---------------+------+---------+------+---------+------------------------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows    | Extra                        |
+----+-------------+--------+------+---------------+------+---------+------+---------+------------------------------+
|  1 | PRIMARY     | random | ALL  | NULL          | NULL | NULL    | NULL | 1000000 | Using where                  |
|  3 | SUBQUERY    | NULL   | NULL | NULL          | NULL | NULL    | NULL |    NULL | Select tables optimized away |
+----+-------------+--------+------+---------------+------+---------+------+---------+------------------------------+
> show warnings;
+-------+------+------------------------------------------+
| Level | Code | Message                                  |
+-------+------+------------------------------------------+
| Note  | 1249 | Select 2 was reduced during optimization |
+-------+------+------------------------------------------+

不,不,一定不要這樣做。這雖然是最顯而易見的做法,但同時也是最錯誤的做法,原因是: 子查詢里的select將在外層查詢時對每一行分別執行,行數越多,性能越差。
我們需要找到一種方法確保隨機ID只生成一次:

SELECT name
  FROM random JOIN
       (SELECT CEIL(RAND() *
                    (SELECT MAX(id)
                       FROM random)) AS id
        ) AS r2
       USING (id);
+----+-------------+------------+--------+------+------------------------------+
| id | select_type | table      | type   | rows | Extra                        |
+----+-------------+------------+--------+------+------------------------------+
|  1 | PRIMARY     | <derived2> | system |    1 |                              |
|  1 | PRIMARY     | random     | const  |    1 |                              |
|  2 | DERIVED     | NULL       | NULL   | NULL | No tables used               |
|  3 | SUBQUERY    | NULL       | NULL   | NULL | Select tables optimized away |
+----+-------------+------------+--------+------+------------------------------+

內層SELECT生成了一個常量臨時表,JOIN也僅僅是JOIN一行,完美,沒有Sorting, 沒有用應用層,大部分的查詢都是優化了的。

如果ID不是連續的呢…?

根據上面得到的結論,我們可以這樣做

SELECT name
  FROM random AS r1 JOIN
       (SELECT (RAND() *
                     (SELECT MAX(id)
                        FROM random)) AS id)
        AS r2
 WHERE r1.id >= r2.id
 ORDER BY r1.id ASC
 LIMIT 1;
+----+-------------+------------+--------+------+------------------------------+
| id | select_type | table      | type   | rows | Extra                        |
+----+-------------+------------+--------+------+------------------------------+
|  1 | PRIMARY     | <derived2> | system |    1 |                              |
|  1 | PRIMARY     | r1         | range  |  689 | Using where                  |
|  2 | DERIVED     | NULL       | NULL   | NULL | No tables used               |
|  3 | SUBQUERY    | NULL       | NULL   | NULL | Select tables optimized away |
+----+-------------+------------+--------+------+------------------------------+

JOIN語句找出所有比隨機ID大的數據,并且在沒有直接匹配的情況下會選擇最接近的一個,一旦找到我們就停止(LIMIT 1), 我們讀取數據時對索引字段進行了排序,由于我們用的是>=,所以我們也就不必再使用CEIL函數來得到整型隨機ID了。我們做了更少的事情,但效果是一樣的。

數據分布導致的問題

一旦ID的分布不平均,我們得到的就不是真正的隨機數據,通過統計我們可以看出:

> select * from holes;
+----+----------------------------------+----------+
| id | name                             | accesses |
+----+----------------------------------+----------+
|  1 | d12b2551c6cb7d7a64e40221569a8571 |      107 |
|  2 | f82ad6f29c9a680d7873d1bef822e3e9 |       50 |
|  4 | 9da1ed7dbbdcc6ec90d6cb139521f14a |      132 |
|  8 | 677a196206d93cdf18c3744905b94f73 |      230 |
| 16 | b7556d8ed40587a33dc5c449ae0345aa |      481 |
+----+----------------------------------+----------+

如果隨機ID是9到15之間的數字,則我們總會取出id=16的那一條數據。

針對這一問題,有一種不是真正的解決方案,但當你的數據大部分是不經常改變時,你可以添加一個map表,他分配一個連續ID給真正的ID。

> create table holes_map ( row_id int not NULL primary key, random_id int not null);
> SET @id = 0;
> INSERT INTO holes_map SELECT @id := @id + 1, id FROM holes;
> select * from holes_map;
+--------+-----------+
| row_id | random_id |
+--------+-----------+
|      1 |         1 |
|      2 |         2 |
|      3 |         4 |
|      4 |         8 |
|      5 |        16 |
+--------+-----------+

現在row_id就是我們的連續ID了,我們可以這樣寫我們的查詢:

SELECT name FROM holes
  JOIN (SELECT r1.random_id
         FROM holes_map AS r1
         JOIN (SELECT (RAND() *
                      (SELECT MAX(row_id)
                         FROM holes_map)) AS row_id)
               AS r2
        WHERE r1.row_id >= r2.row_id
        ORDER BY r1.row_id ASC
        LIMIT 1) as rows ON (id = random_id);

通過1000次查詢,我們得到如下統計,可以看出這次我們的隨機訪問是均勻的了。

> select * from holes;
+----+----------------------------------+----------+
| id | name                             | accesses |
+----+----------------------------------+----------+
|  1 | d12b2551c6cb7d7a64e40221569a8571 |      222 |
|  2 | f82ad6f29c9a680d7873d1bef822e3e9 |      187 |
|  4 | 9da1ed7dbbdcc6ec90d6cb139521f14a |      195 |
|  8 | 677a196206d93cdf18c3744905b94f73 |      207 |
| 16 | b7556d8ed40587a33dc5c449ae0345aa |      189 |
+----+----------------------------------+----------+

通過觸發器維護連續ID映射表

首先我們先初始化表:

DROP TABLE IF EXISTS r2;CREATE TABLE r2 (
  id SERIAL,
  name VARCHAR(32) NOT NULL UNIQUE
);

DROP TABLE IF EXISTS r2_equi_dist;
CREATE TABLE r2_equi_dist (
  id SERIAL,
  r2_id bigint unsigned NOT NULL UNIQUE
);

當我們在r2中改變了一些記錄,我們希望r2_equi_dist也能隨之更新

DELIMITER $$
DROP TRIGGER IF EXISTS tai_r2$$
CREATE TRIGGER tai_r2
 AFTER INSERT ON r2 FOR EACH ROW
BEGIN
  DECLARE m BIGINT UNSIGNED DEFAULT 1;

  SELECT MAX(id) + 1 FROM r2_equi_dist INTO m;
  SELECT IFNULL(m, 1) INTO m;
  INSERT INTO r2_equi_dist (id, r2_id) VALUES (m, NEW.id);
END$$
DELIMITER ;

DELETE FROM r2;

INSERT INTO r2 VALUES ( NULL, MD5(RAND()) );
INSERT INTO r2 VALUES ( NULL, MD5(RAND()) );
INSERT INTO r2 VALUES ( NULL, MD5(RAND()) );
INSERT INTO r2 VALUES ( NULL, MD5(RAND()) );

SELECT * FROM r2;
+----+----------------------------------+
| id | name                             |
+----+----------------------------------+
|  1 | 8b4cf277a3343cdefbe19aa4dabc40e1 |
|  2 | a09a3959d68187ce48f4fe7e388926a9 |
|  3 | 4e1897cd6d326f8079108292376fa7d5 |
|  4 | 29a5e3ed838db497aa330878920ec01b |
+----+----------------------------------+

SELECT * FROM r2_equi_dist;
+----+-------+
| id | r2_id |
+----+-------+
|  1 |     1 |
|  2 |     2 |
|  3 |     3 |
|  4 |     4 |
+----+-------+

INSERT是非常簡單的,當DELETE時我們必須更新映射表來維護重建針對新不連續集合的映射

DELIMITER $$DROP TRIGGER IF EXISTS tad_r2$$
CREATE TRIGGER tad_r2
 AFTER DELETE ON r2 FOR EACH ROW
BEGIN
  DELETE FROM r2_equi_dist WHERE r2_id = OLD.id;
  UPDATE r2_equi_dist SET id = id - 1 WHERE r2_id > OLD.id;
END$$
DELIMITER ;

DELETE FROM r2 WHERE id = 2;

SELECT * FROM r2;
+----+----------------------------------+
| id | name                             |
+----+----------------------------------+
|  1 | 8b4cf277a3343cdefbe19aa4dabc40e1 |
|  3 | 4e1897cd6d326f8079108292376fa7d5 |
|  4 | 29a5e3ed838db497aa330878920ec01b |
+----+----------------------------------+

SELECT * FROM r2_equi_dist;
+----+-------+
| id | r2_id |
+----+-------+
|  1 |     1 |
|  2 |     3 |
|  3 |     4 |
+----+-------+

UPDATE 也是很直接的,我們只需要維護外鍵的約束。

DELIMITER $$DROP TRIGGER IF EXISTS tau_r2$$
CREATE TRIGGER tau_r2
 AFTER UPDATE ON r2 FOR EACH ROW
BEGIN
  UPDATE r2_equi_dist SET r2_id = NEW.id WHERE r2_id = OLD.id;
END$$
DELIMITER ;

UPDATE r2 SET id = 25 WHERE id = 4;

SELECT * FROM r2;
+----+----------------------------------+
| id | name                             |
+----+----------------------------------+
|  1 | 8b4cf277a3343cdefbe19aa4dabc40e1 |
|  3 | 4e1897cd6d326f8079108292376fa7d5 |
| 25 | 29a5e3ed838db497aa330878920ec01b |
+----+----------------------------------+

SELECT * FROM r2_equi_dist;
+----+-------+
| id | r2_id |
+----+-------+
|  1 |     1 |
|  2 |     3 |
|  3 |    25 |
+----+-------+

一次取出多行隨機結果

如果你想取出多行結果,你可以這樣做

* 執行這個查詢多次
* 寫存儲過程,把結果存入臨時表
* 使用UNION語句

存儲過程的方法

存儲過程提供給你一些你從其他喜歡的編程語言中了解的結構

* 循環
* 控制
* 順序執行

針對這個目標,我們只需要一個循環

DELIMITER $$
DROP PROCEDURE IF EXISTS get_rands$$
CREATE PROCEDURE get_rands(IN cnt INT)
BEGIN
  DROP TEMPORARY TABLE IF EXISTS rands;
  CREATE TEMPORARY TABLE rands ( rand_id INT );

loop_me: LOOP
    IF cnt < 1 THEN
      LEAVE loop_me;
    END IF;

    INSERT INTO rands
       SELECT r1.id
         FROM random AS r1 JOIN
              (SELECT (RAND() *
                            (SELECT MAX(id)
                               FROM random)) AS id)
               AS r2
        WHERE r1.id >= r2.id
        ORDER BY r1.id ASC
        LIMIT 1;

    SET cnt = cnt - 1;
  END LOOP loop_me;
END$$
DELIMITER ;

CALL get_rands(4);
SELECT * FROM rands;
+---------+
| rand_id |
+---------+
|  133716 |
|  702643 |
|  112066 |
|  452400 |
+---------+

這里作者留給讀者去修復一些仍然存在的問題

* 使用動態SQL來確定要操作的臨時表,以便于讓同一個存儲過程適用于多種隨機需求
* 使用UNIQUE 索引來避免隨機數據的重復

性能

現在,讓我們來看一看性能怎么樣,我們有3個不同的查詢來解決我們的問題。

* Q1. ORDER BY RAND()
* Q2. RAND() * MAX(ID)
* Q3. RAND() * MAX(ID) + ORDER BY ID

Q1 預期的代價是 N * log2(N), Q2 and Q3 都接近常數。

我們用真實數據來測試,從100行到100萬行,并且執行1000次做如下統計

   100        1.000      10.000     100.000    1.000.000
Q1  0:00.718s  0:02.092s  0:18.684s  2:59.081s  58:20.000s
Q2  0:00.519s  0:00.607s  0:00.614s  0:00.628s   0:00.637s
Q3  0:00.570s  0:00.607s  0:00.614s  0:00.628s   0:00.637s

正如你所看到的,Q1在僅僅100行數據時就已經落后于我們優化后的SQL了。

筆者會盡最大的努力翻譯出原文中所有要表達的意思,但錯誤之處在所難免,請大家不吝指出。

對原文感興趣的讀者,請參考作者博文原文

http://jan.kneschke.de/projects/mysql/order-by-rand/

上面還有很多其他MYSQL方面的博文,我會盡量挑其中優秀的文章翻譯給大家。

]]>
http://www.vczhtn.live/2012/01/solution_for_mysql_random_results/feed/ 1
Drupal調試之Devel模塊使用技巧 http://www.vczhtn.live/2011/12/how-to-use-devel/ http://www.vczhtn.live/2011/12/how-to-use-devel/#comments Thu, 08 Dec 2011 16:11:04 +0000 http://www.vczhtn.live/?p=198 drupal開發中,必然會遇到需要代碼調試的時候,這時候有人可能會想說用xdebug之類的調試工具,但有的時候你只是想得到一些中間值或者Drupal流程中的一些統計值,抑或是某個函數的輸出,使用xdebug顯然就顯得不那么合適了。在眾多調試工具當中,Devel是其中必備的一個,其他各種調試工具,將會在后續文章中一一闡述。

Devel模塊作為Drupal的一個調試模塊有其天然優勢,首先他使用Drupal的機制開發,所以輸出信息可以和Drupal很好的整合,另外Devel模塊中可以直接調用Drupal的API函數,并且隨著社區的貢獻,以及Drupal版本的更新,Devel也會因開源的特質越來越貼近開發者的實際需要,另外Devel模塊在Drupal性能優化方面也表現不凡,比如SQL查詢時間統計、整合xhprof模塊的性能優化(xhprof的使用會后續寫相關文章討論),因此Devel模塊是Drupaler們的必備工具。

相信每一個Drupaler都知道Devel模塊,(Devel模塊的的官方地址是 http://www.drupal.org/project/devel),并且在自己的項目中使用他,所以本文不打算流水帳一樣講述所有Devel的特性,而是從中挑出幾個或巧妙或不顯而易見的一些用法。

1. devel/php (頁面)

比如:http://www.example.com/devel/php

這是一個執行PHP代碼的Drupal path,對生產服務器來說,這是危險的,但對程序員調試來說是非常有用的。

最簡單的,我可以在里面查看任何變量或者函數返回值。

//dpm是devel提供的一個代替var_dump/drupal_set_message的函數,是打印調試信息的利器,請查閱官方介紹
dpm(user_load(1));

但更重要的是我可以用來調試,比如有只有生產服務器才存在無法在本地重現的BUG,我一般就是臨時開啟Debug模塊,然后對大概有問題的函數使用此頁面調試,我可以通過函數返回值來定位,如果定位到另外一個函數,就構造輸入去那個函數里繼續檢查輸出,這樣可以在不打斷服務器運行的情況下,做代碼跟蹤調試,有時可以很快的找到問題所在。

2. devel/switch/登錄名 (頁面)

比如: http://www.example.com/devel/switch/user-a

有時某些BUG是只有某個用戶才會看到的,這是我們需要那個賬戶的登錄信息,但密碼顯然我們是不容易得到的,這時我們可以用這個功能來切換到這個用戶的登錄狀態,devel還封裝了切換用戶的block,但更常見的情況是使用這個鏈接,后面加用戶登錄名,快速切換到那個賬戶,以那個用戶的身份去觀察BUG。

如果你觀察這個hook_menu的代碼實現,你會發現一個很有意思的核心函數devel_switch_user,他是切換用戶功能的主要函數,邏輯也很簡單,但這個函數隱藏了一個切換回來的功能,這個功能只能在代碼中才能用到,因為原始用戶信息是存在靜態變量里的。

devel_switch_user('tom'); // 切換成用戶tom
 
// 做一些操作
 
devel_switch_user(); // 切換回來

如果沒有這個函數,我們會這么寫,比較一下就會發現devel模塊的寫法更簡潔。

global $user;
$old_user = $user;
$user = user_load(array('name' => 'tom'));
 
// 做一些操作
$user = $old_user;

3. dd(函數)

有時候當你在調試一些功能時dpm是沒有那么好用的,比如調試batch或者沒有界面的drupal腳本,我們需要一種新的方式幫助我們調試,dd函數會在這種情況下發揮作用,dd函數的全稱是devel_debug, 看代碼實現就會發現他將信息輸出到一個文件里,之后我們可以通過那個文件查看調試信息,這樣我們就可以通過linux的tail命令實時查看調試信息了。

$test = "this is my test";
dd($test, $label = NULL);

dd函數會在網站服務器的臨時目錄(drupal的文件系統設置或者/tmp目錄下)產生一個名為 drupal_debug.txt的文件,可以在服務器上查看相應的輸出。

這個命令的缺點在于調試信息的輸出文件的位置和名稱是寫死在代碼中的,未來最好可以傳參定義或者配置。

4. devel/source?file=Drupal的相對路徑 (頁面)

這個URL是用來查看源代碼的,這個功能在某些情況下也會用到,比如生產服務器出現BUG,你想快速check代碼,使用ssh登陸,切換路徑,然后來回切換文件查看就不如直接在瀏覽器里訪問方便,前提是你要足夠熟悉Drupal的文件結構以及你自己項目的文件結構。當然出于這個目的,這個功能還有點簡單,應該可以做一下簡單的文件瀏覽器。

5. SQL調試

Devel在數據庫查詢的SQL這方面做了一定的努力,比如后臺可以開啟sql log,并顯示在頁面最下方,可以按照觸發順序或者執行時間順序排序顯示等等。但是一旦開啟了sql log,我們就不得不log所有的sql, 這樣查看自己想要看的那個sql就不是那么方便了。

其實devel模塊還提供了一個調試方法,就是db_queryd函數,就是把我們常用的db_query后面加一個d,在執行效果不變的前提下,還會print出執行的sql.這個方法比上面的方法簡潔了一些,但問題是在頁面上print往往不是個好主意,而且你要調試的sql很有可能是被某個第三方API在執行,比如$view->query(),所以我們還要想想有沒有別的辦法,通過閱讀db_query的代碼,以及devel的代碼,我發現還有一種方法。

global $conf, $queries;
$conf['dev_query'] = 1; //臨時開啟sql log
 
db_query(); //可以是任意長度的包含sql執行的代碼,或者第三方模塊的操作數據的API。
$conf['dev_query'] = 0; //關閉sql log
dpm($queries); // 顯示sql

$queries是一個全局變量,用于存儲sql log, 只有dev_query開啟才會記錄log,這里假設默認是關閉的,因為我們只想看我們關心的sql。這里沒有用variable_get喝variable_set是因為variable_set會觸發清緩存操作,性能不好。

如果覺得寫起來麻煩,可以考慮封裝成函數,比如devel_query_start(); devel_query_end();

最后,Devel模塊提供給我們很多查看信息的頁面,有時候我們確實需要這些頁面查看一些信息,最簡單的devel/phpinfo可以查看php的安裝信息,我們不需要自己再去創建包含phpinfo()的文件來查看了。Devel模塊還提供了一些子模塊,還有一些第三方模塊與devel模塊配合使用,這些話題我將另起一篇文章和大家分享。

希望本文中分享的技巧能對各位Drupaler的工作有所幫助。

]]>
http://www.vczhtn.live/2011/12/how-to-use-devel/feed/ 0
MySQL InnoDB數據庫備份與還原 http://www.vczhtn.live/2011/10/local-innodb-backup-and-restore/ http://www.vczhtn.live/2011/10/local-innodb-backup-and-restore/#comments Fri, 07 Oct 2011 17:08:42 +0000 http://www.vczhtn.live/?p=88 MySQL數據庫MyISAM類型的備份和管理很方便,本文就如何備份還原InnoDB數據庫做一下經驗分享。

首先,為什么要這么做呢,因為我工作于一個大型drupal項目,數據庫文件很大,而且每個新特性都在分支上進行開發,這樣意味著我需要搭建多個本地環境,并且經常需要刪除舊的分支,建立新的分支。

由于數據庫使用的大部分是InnoDB引擎,而InnoDB在分配了空間以后是不釋放,也不可以被重用的(這里可能是我還沒找對方法),所以由于我這樣頻繁的建立和刪除數據庫導致了InnoDB的數據庫文件越來越大。所以決定給數據庫文件瘦身。

InnoDB的數據庫文件默認是公用ibdata1,在我本地已經達到17G之多,并且還在不斷變大。

以下是瘦身步驟,注意瘦身之前對原始的ibdata1文件做好備份。

1,在命令行或者PHPMyAdmin里刪除不需要的數據庫。

2,備份所有的數據庫:

mysqldump -uDBuser -pPassword --quick --force --routines --add-drop-database --all-databases --add-drop-table > /your_backup_place/mysqldump.sql

這里主要是看后面的可選參數,這個操作是備份的全部數據庫,如果本地數據庫較多,會很慢。當然導入的時候會更慢,我是為了省空間不得已為之,大家如果也有和我一樣的想法,使用本文的方案后果自負。

3,刪除ibdata1文件,導入時會自動建立的。其余數據庫不用管,因為導入時會先刪后建。

4,進入MYSQL命令行使用source命令進行導入。

最后,不管大家是否能執行成功,反正我是成功了。:)

]]>
http://www.vczhtn.live/2011/10/local-innodb-backup-and-restore/feed/ 1
Drupal性能優化實戰4則 http://www.vczhtn.live/2011/10/performance-optimization-in-action/ http://www.vczhtn.live/2011/10/performance-optimization-in-action/#comments Fri, 07 Oct 2011 16:28:27 +0000 http://www.vczhtn.live/?p=85 本文側重于drupal性能優化實戰,問題較為具體,如果大家想從全局上了解怎樣提高drupal網站性能,請參見本站另外一篇文章:

《讓豬去飛-漫談Drupal性能優化經驗貼》

這里列舉幾點筆者在實踐中的幾點總結,僅供參考。

1,給Views加緩存。

Views可以生成一些列表,一般這些列表都不需要實時性,所以我們可以對其使用緩存,當我們察覺到一個使用了Views的頁面加載比較慢時,通過Views后臺配置頁面的Preview,以及Devel模板的調試信息可以看到一個Views在SQL執行階段和渲染階段的執行時間,我們會發現這兩部分都是時間花費比較長的,但SQL執行部分的消耗我們可以通過開啟Views緩存來解決,這樣不僅頁面加載更快,同時也可以少占一次MYSQL查詢,意味著更大的數據庫吞吐量。

Views cache option

Views 緩存選項

緩存選項可以設定數據緩存和整體緩存的時間,一般來講設置成一樣即可,除非還通過Hook在結果里加入了自定義的內容。

另外一般我們用Views大部分是在讀取node表,當網站數據日益龐大,讀取node表的Views會產生很多性能問題,這樣開啟Cache就不是可選,而是必須了。同時也可以間接說明實時性較高的網站,特別是SNS網站的實時性功能不適合使用Views。當node表過大時有可能使得Views的查詢變得極慢,這時當緩存更新時執行的Views SQL語句有可能會花很長時間,甚至是執行失敗,這時就應該考慮使用其他技術解決,比如solr

2,不要用Ad模塊來部署網站廣告

Ad模塊是一個強大的廣告發布管理模塊,有很靈活的廣告管理方式,有很多廣告內置類型以及第三方廣告類型,并且和許多模塊可以一起使用。單從功能上來說,Ad模塊是一個不錯的模塊,但是當在實際的網站中使用時,會給我們帶來嚴重的性能問題。

經過調試,原因是Ad模塊自帶了統計功能包括廣告的展示次數和點擊次數,使用的方法和Google Analystics一樣,使用一個1px的圖片接收若干統計變量,但問題在于由Drupal網站自己處理這樣的請求,并且這個1px的請求帶來的是Drupal完整的加載流程,無論是對CPU,內存還是MYSQL鏈接數都是成倍增加的。
我遇到的問題是一個網站在線用戶只有200人的時候,網站就已經非常慢的,看表面現象就是CPU始終居高不下,內存也幾乎用盡,實際上是因為每個頁面都有5~8個Ad廣告,這樣,每一個人訪問網站就相當于有5~8個人同時訪問網站,導致網站請求數瞬間達到最大值,出現排隊。

最后,通過禁用Ad模塊證明之前的分析是正確的,而解決方案是換成Google Ad Manager模塊,這樣統計部分就托管給Google了,從而解決了這一性能問題。

3,小心實現Authcache的動態回調

Authcache是一個對針對匿名和登錄用戶都有效的緩存方案,并且配置靈活,可以說是緩存模塊中一個比較好用的模塊,但是當一個網站有大量動態實時信息時其實是不適合使用Authcache的,而當網站只有少量動態內容時,Authcache是一個不錯的選擇,其動態內容的實現是通過JS回調,關于JS回調,本站有另外一篇文章可以作為參考《Drupal性能優化之-將Boost模塊用到極致》。

Authcache的JS回調有其自己的方法,一般來說默認Authcache的回調是沒有完全加載Drupal的,這樣對性能的影響就不是很大。但是當不小心在一個回調里寫了drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);時,這就是一個完全加載,而使得我們的緩存幾乎失去意義,而當網頁上存在多個這樣的回調時,性能反而不如不緩存來的快。

Authcache的相關文章:
Authcache模塊高級篇-動態加載內容
Drupal緩存 – Authcache模塊原理詳解

4,content_profile模塊的問題

content_profile模塊極大的利用了node的特性實現了對用戶profile的靈活管理,具體來說就是使用了內容類型的概念來區分不同類別的profile項,每一項是一個CCK字段。管理維護以及使用都很方便。但在實際應用中就遇到了意想不到的問題。

筆者需要使用Views對網站數據做報表,報表要求包含多個不同內容類型的content profile,這樣views就自動把相關的內容類型的表關聯在一起了,理論上,一個用戶在每種profile內容類型下,應該只有一個node。但由于網站在創建profile node時是腳本操作的,難免存在BUG,結果造成了有的人在某個profile內容類型里有多個node,這樣關聯時就會帶來翻倍的結果集,也就是關聯的內容類型表越多,用戶擁有的多余profile node越多,結果集就翻倍增加。最嚴重的情況在于,由于某些代碼存在BUG,導致數據庫中針對匿名用戶在各個內容類型表中存在大量profile node.大概是每個類型有300多個profile node.這樣在Views做關聯以后,導致了只匿名用戶(uid=0)就帶來幾十億的結果集,使得這個SQL成為慢查詢,頁面無法呈現。

解決方法就是編寫一個腳本,不斷清理各類型里錯誤的profile node,使得結果集成為理論值。另外就目前和同事討論的結論來看,content_profile是個雙刃劍,及時沒有以上BUG,在大型網站里使用也會為網站帶來性能問題,核心問題在于其在頻繁操作node表。當表數據達到千萬級別,性能問題開始顯現時,content_profile講成為問題。

以上幾個場景是筆者在實際工作中遇到的與性能有關的問題,希望對大家有所幫助。

]]>
http://www.vczhtn.live/2011/10/performance-optimization-in-action/feed/ 0
使用APC優化Drupal時要注意的細節 http://www.vczhtn.live/2011/09/apc-shm-size-tuning/ http://www.vczhtn.live/2011/09/apc-shm-size-tuning/#comments Tue, 27 Sep 2011 13:01:35 +0000 http://www.vczhtn.live/?p=44 此前博文《drupal性能優化經驗貼》中提到,一般類似drupal這樣的PHP框架,我們為了提高性能必須要使用opcode來提高PHP的執行速度,PHP也有這樣的模塊。
我們都知道使用APC或者eAccelerator這樣的opcode緩存可以提高網站的性能,但要注意的是需要根據網站的規模做細節的調優,只使用默認設置,可能帶來的結果是性能還不如不用他們之前的效果。

這里要說的Case是,APC配置中有一項是shm_size,這是用來控制劃分多少內存給APC使用,用來緩存文件或者opcode,對于文件緩存,原理大概相當于,預先讀取到內存中緩存起來,下次再使用時就不需要占用磁盤I/O了,這個想法當然是很好的,依賴來說效果也是非常明顯的。但使用Drupal做的大型項目,開啟的模塊是很多的,那就意味了需要加載的文件很多,他們放到一起占用的內存是很可觀的,一般APC默認的配置可能16M, 那么當文件緩存的內存占用超過16M之后,會有什么問題呢,帶來的問題是include_once這樣的函數花費很長時間決定如何加載文件,這里可能存在一些算法上的問題,需要覺得那些緩存丟棄,把新讀取的文件緩存放在內存中什么位置之類的,總之在這種情況下,APC反而會讓性能下降,大概下降3,4倍左右。

所以,在實際應用環境中,需要知道啟用了多少個模塊,需要提供大概多少內存,對于一個服務器提供多個站點,并且各個站點的Drupal是獨立目錄的場景中,因為APC的緩存機制,緩存所需內存更是成倍增加的。所以要么不用,用的話,一定要留出足夠的內存,另外eAccelerator也是同樣的道理,也許在內存溢出時,由于調度算法的差異,性能下降程度可能不一樣,但總是會有影響的。

因為本文只記錄了結論,沒有包含分析過程,大家可以參考一下原文的詳細分析。

High PHP execution times for Drupal, and tuning APC for include_once() performance

]]>
http://www.vczhtn.live/2011/09/apc-shm-size-tuning/feed/ 0
安徽福彩15选5走势图