可延伸 可伸縮的 Oracle 與 PHP

日期:2006-09-21  作者:喜騰小二  來源:PHPChina


瞭解一般的技術和設計,用於編寫與 Oracle 資料庫的使用直接相關的可管理、可伸縮的快速 PHP
程式碼。

在過去九年中,PHP
已經從組裝個人網站的小型語言發展到為世界上某些最大和流量最高的網站提供動力。任何高流量網站的三個最重要的設計方麵是可伸縮性、效能和可維護性。可伸縮性意味着您的應用程式流量負載可以不斷增長,而不會從根本上破壞其工作方式。效能是快速為單個請求提供服務的能力。
可維護性是能夠在不造成過多負擔的情況下修復、重新調整、擴增或變更應用程式的品質。

利用 PHP
來實現這三個設計目的並不困難,但確實需要預先考慮如何設計和建置您的應用程式。關於編寫可管理、可伸縮的快速 PHP
程式碼的論題範圍很廣;針對每個論題都有大量的技術和文章。在本文中,我們將討論那些與使用 Oracle 及 PHP 直接相關的因素。有很多一般(非 Oracle
私用)技術和設計可能非常有用。

我喜歡以一個尖銳的警告作為任何與效能相關的談話或文章的開始:始終要記住,最後總有一天,快速卻不完善的應用程式將毫無價值。效能調整以及對應用程式不利因素的設計提取都很容易分散您的精力。Web
的性質就是這樣,經常發佈版本的方法非常有效。(發佈網站“新版本”的成本很低,因為最終使用者始終需要這些程式碼。)這就允許您延遲對程式碼的重大調整,直到需要這樣做為止。因此,首要目的應該是建立便於重新調整的程式碼。

建立和管理連線

與 Oracle
資料庫最基本的互動之一是連線。要瞭解連線如何影響您的應用程式的效能和可伸縮性,需要瞭解連線的生命週期,如圖 1 所示。每個步驟所涉及的工作如下:

  • 客戶建立連線: 客戶建立與 Oracle
    監聽器的網路連線,提供其認證憑證,並請求工作階段。
  • 伺服器建立一個新工作階段:在認證之後,伺服器為客戶建立一個新工作階段。如果您沒有透過 Oracle 多執行緒伺服器(MTS —
    它在可伸縮性及效能問題上臭名昭着)使用共用工作階段,則此步驟包括伺服器為工作階段建立一個私用處理序。該處理序通常稱為
    影子處理序。建立此處理序需要不少工作量。除了建立處理序的正常開銷之外,影子處理序在其建立期間還必須暫時鎖定某些共用係統資源。
  • 用戶端執行查詢:既然用戶端已經俱有開放的連線,就可以根據需要來執行查詢。
  • 用戶端關閉連線:當用戶端完成工作後,關閉與伺服器的連線。
  • 伺服器毀壞工作階段:與使用者工作階段相關的影子處理序被毀壞,任何未提交的事務被復原。

圖 1:連線的生命週期

由於建立新的影子處理序的成本相當大,我們應該在必要時努力避開它。達到此目的的最簡單方法是使用持續連線。PHP
被設計為一種非工作階段狀態的語言。這意味着在預設情況下,在請求期間建立的任何資訊(或常式化的資源)都會在請求結束時被徹底清除並毀壞。對於 Oracle
客戶連線,我們希望避免這種行為。

為了使連線能夠從一個請求保留到下一個請求,您可以使用以下兩種連線變通方法之一:

OCIPLogin($username, $password [, $tnsname])

OCINLogin($username, $password [, $tnsname])

這兩個函式都建立持續的伺服器連線,儘管 OCINLogin()
將為每個請求建立一個新工作階段句柄。如果您的應用程式要使用事務,並且您希望將同時發生的事務分散到多個工作階段中,則可以使用 OCINLogin()。

使用持續連線的一個副作用是您更容易出現處理序不足的情況。基於私用 Oracle 資料庫執行單個 Apache
Web 伺服器(子處理序的最大預設數量為 256)時,可能從不會遇到問題。但是如果增加到 4 個 Web 伺服器,每個伺服器執行 256 個俱有持續 Oracle
連線的子處理序,現在則要建立 1024 個與 Oracle 資料庫的連線,並且很快就會與 Oracle 實例的資源限制發生衝突。

在 Oracle 實例配置檔案 (init.ora) 中,有兩個可調整的參數:

sessions = NNNNprocesses =
NNNN

這兩個參數控制着實例可以支援的最大工作階段數和最大處理序數。如果您需要支援 1024 個同時出現的連線,則需要至少
1024 個工作階段(因為
OCINLogin()連線和某些遞迴查詢可能在每個連線中需要多個工作階段),而需要的處理序還會更多(因為我們還需要考慮 Oracle
後臺處理序)。不幸的是,不能任意將這些處理序設得很高。Oracle 處理序消耗不少的私用記憶體(在多數係統中每個處理序需要 2 到
3MB)。從個體來說,這些處理序很小,但當把它們作為一組並與伺服器係統全域區 (SGA)
所需要的共用記憶體相結合時,很快就會讓您因為資料庫伺服器的實體記憶體限制而感到煩惱。

來自 MySQL 環境的使用者可能試圖避開持續連線(這是 MySQL 環境中的建議)。由 Oracle
影子處理序啓動所導緻的栓鎖和檔案爭用使得非持續連線的使用效率極低。那麼解決方案是什麼呢?

  • 應該確定我們的資料庫能夠支援多少個同時發生的工作階段,並相應地設定其限制。有些文章詳細幫助了如何完成此工作,但主要是一個計算過程:計算係統中實體記憶體的總量,並減去內核、支援程式和
    Oracle 後臺處理序所使用的記憶體。然後減去所有被配置為 Oracle
    共用記憶體的那些記憶體(共用池和緩衝區高速快取)。剩下的記憶體可以用於影子處理序。將該數量除以一個影子處理序所使用的私用處理序記憶體的平均數量(應該自己測出該數量,因為它根據您所執行的查詢性質而變化),則我們得到可支援的處理序數量。
  • 對我們的 Web 伺服器進行配置,以便使其永遠不能建立超過您的處理序設定允許數量的連線。其實現方法是將每個
    Web 伺服器的 MaxChildren 可調參數設定得足夠低,使得所有 Web 伺服器總共擁有的子處理序數量低於可支援的 Oracle 連線數。這意味着每個
    Apache 實例不再支援 256 個子處理序。
  • 重新設計我們的應用程式,使其不會遺漏額外的子處理序。我們在後文中還會討論這個話題。

這樣有多重要?作為一個老闆,我們即使啓用了持續連線,也一直遇到栓鎖問題。任何遇到過嚴重栓鎖爭用的人都能證實,這是一個極為嚴重的問題,伺服器在很大程度上不回應,因為它花費過多時間來進行鎖定操作。我們在調查中發現,儘管在終止前有大量處理序為數以百計的請求提供服務,但很多處理序隻服務於單個請求。這一切是由於我們將
Apache 的
MaxSpareServers設定得太低。我們所使用的負載均衡裝置的一些問題導緻了“突然爆發”的行為,此時 Web 伺服器被多個同時發生的請求所衝擊,然後閒置數秒時間。在 Apache
內部,這導緻要建立額外的子處理序,來為高請求等級提供服務;但當它一旦平息時(幾乎立即平息),就會出現大部分目前處於閒置狀態的子處理序(終止處理序並關閉其 Oracle
連線)。總體看來,這與執行非持續處理序的效果相似。將
MaxSpareServers設得較高就可消除此問題,並消除了栓鎖爭用。

執行 SQL

任何 Oracle 客戶伺服器關係的主要內容是執行查詢。這裡沒有篇幅來談論對查詢的調整 —
那是需要整本書來討論的主題。相反,我們將集中討論如何盡可能地使已經調整過的查詢高效執行。

使用 PHP 編寫良好的 Oracle 應用程式程式碼的第一步是始終使用繫結
SQL。當我們編寫類似以下的查詢時:

SELECT * FROM USERS WHERE USERNAME = 'george'

Oracle 必須對該查詢進行軟分析,檢視以前是否曾編譯過該查詢。在預設情況下,"george"
值被作為文字項,這意味着如果我們使用不同的名字 ('bob') 執行此查詢,則 Oracle 將把它看作完全不同的查詢。Oracle
在其共用池中保留它執行的每個查詢的分析副本,因此如果您使用數韆個名字來執行此查詢,則在您的共用池中將有數韆個該查詢的不同副本。即使在輕度活躍的網站上,這也會導緻嚴重的記憶體碎片以及
ORA-4031 錯誤增殖。

對這個問題的解決方案是使用繫結 SQL。繫結 SQL 允許我們將 WHERE
子句中的文字值取代為占位符,如下所示:

SELECT * FROM USERS WHERE USERNAME = ':NAME'

在這裡,查詢隻須被完全分析一次(硬分析);以後所有的分析都是所謂的軟分析,此時引擎隻是簡單地從
SGA 中提取已編譯的查詢。此外,將隻有一個單次分析的副本被存儲,從而顯着減少了不斷執行的查詢對記憶體的需求。

現在我們可以執行如下:

$db = OCIPLogin('scott', 'tiger', 'testdb');
$stmt = OCIParse("SELECT * FROM USERS WHERE USERNAME = ':NAME'");
OCIBindByName($stmt, ":NAME", "george");
OCIExecute($stmt);
?>

在 Oracle8i 之前,我們必須手動繫結查詢;從 8i 開始,透過設定
init.ora 的參數 "cursor_sharing =
FORCE",我們可以指示最佳化器為我們完成這一工作。該設定通知最佳化器尋找可以繫結的文字值,並手動執行繫結。自 9i起,我們可以使用設定
“cursor_sharing = SIMILAR”,該設定指示最佳化器深入檢視基表的統計資訊,瞭解自動繫結文字是否有好處(如果某個欄位的分佈極不均勻,則可能沒有好處)。儘管應該啓用這些設定(在
8i 中為
FORCE,在 9i 及更高版本中為
SIMILAR),但深入到查詢中分析潛在繫結的這種操作對於最佳化器而言成本很高,因此在可能的情況下,應該手動繫結您的查詢。

Oracle 客戶伺服器在 SQLNet 協定基礎上執行,眾所周知,這種協定的對話很多。例如,如果您執行一個返回
100
行的查詢,則會有分析查詢的對話交換、對每個繫結變數的交換、對執行的查詢以及對每個提取行的查詢。其中每次交換都包括用戶端與伺服器之間的網路資料包交換(稱為一次
往返)。減少往返次數可能俱有深刻的效能影響。

解決這個問題的第一種方法是建立客戶的預取緩衝區。在網路上傳送單獨一行的效率極低,因此最好主動將很多行集中在一起,並將它們從本機緩衝區中讀出。如果我們將陳述式的句柄設定如下,則
Oracle 客戶庫能夠自動執行這種服務:

$stmt = OCIParse($db, $query);
OCISetPreFetch($stmt, 1000);
// execute and fetch

這些設定指示 OCI 客戶庫每次內部緩衝 1000 行,對任何返回超過一行的查詢提供積極的回報。1000
是個相當隨意的數字 — 在選擇預取緩衝區大小時僅有的限制因素是:

  1. 在執行返回之前需要填充緩衝區 — 如果對部分結果集的即時訪問非常重要,將不希望緩衝區過於龐大。
  2. 被緩衝的結果集保留在本機記憶體中,直到被重新整理為止 —
    如果您俱有非常大的行或非常大的結果集,則不應將緩衝區設定得過大,以防止用戶端出現記憶體不足。

管理與 Oracle 的互動操作

現在您已看到了一些透過 PHP 改進與 Oracle
資料庫互動操作方式的技術。對您的程式作出這類結構性變更是管理上的爭論焦點,因為它通常需要對程式碼中的很多地方作出不少變更。解決問題的正確方法是將所有的資料庫訪問程式碼封裝在一個打包的庫中,這樣就可以對如何準備和執行查詢等內部問題進行變更,而不必審計您的全部編碼內容。

我說的是“打包的庫”而不是“抽象層”,因為很多所謂的抽象層強調不僅嚮您隱藏低級資料庫調用的細節,而且還隱藏
SQL。它們實施各自的與資料庫無關的語法,以便您能夠毫不費力地將您的應用程式改到其他資料庫中。我認為這種方法存在三個主要問題:

  1. SQL
    是很多開發人員瞭解的一種功能強大的描述性語言。不讓他們使用這種語言並要求他們學習一種新的、靈活性較差的語言是很可笑的。
  2. 每種主要的資料庫產品都俱有非標準的 SQL
    語法,以便完成特定的工作。放鍥這些差異會限制您的靈活性,並使您失去所選擇平臺的某些價值。如果隻考慮它們所支援特性的交集,最終隻得到最低程度的通用特性。
  3. 對於多數人而言,更換資料庫供應商是很少見的現象,無論您的應用程式是否識別資料庫,這都會涉及到相當多的資料移植工作。

用於 PHP 的兩種最流行的資料庫封裝/抽象庫是 PEAR::DB(從 http://pear.php.net/ 獲取)和 ADODB(從 http://php.weblogs.com/adodb獲取)。不管這兩種實施方法的流行程度(和品質)如何,,我通常從頭開始實施自己的資料庫打包的庫。我不需要 ADODB 或 PEAR::DB
中的很多進階特性,而保持庫的簡單性可以使它速度更快並且更易於維護。沒有復雜的特性,一個緊密的庫可以用大約 100 行程式碼來完成它。如果提供了 Oracle
資料庫和陳述式句柄中所執行的資訊量,則我更喜歡麵嚮物件的封裝,儘管我也曾看到過效果良好的程式性封裝。

清單 1顯示了一個完整的打包的庫,它包含一個圍繞資料庫連線的包 (DB_Oracle) 和一個圍繞遊標的包
(DB_OracleStatement)。

這些類的目的是使對資料庫互動操作的管理變得簡單、直接和清晰。以下顯示了如何使用它們來執行一個簡單的查詢:

include_once("DB_Oracle.inc");
$dbh =& new DB_Oracle('scott', 'tiger', 'testdb');
$stmt =& $dbh->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute(array(':name' => 'george'));
$result = $stmt->fetch();
// ...
?>

由於封裝是麵嚮物件的,透過對類進行延伸,可以輕易隱藏所有的連線參數。這對於提供一個簡單的、無參數的連線類非常有用,如下所示:

class DB_Oracle_Test extends DB_Oracle {
var $user = "scott";
var $pass = "tiger";
var $tnsname = "testdb";

function DB_Oracle_Test() {}
}

這個新類嚮程式設計人員隱藏了所有的連線標準,允許透明地變更實例的
TNSNAME 或連線標準。還要注意如何嚮使用者隱藏
OCISetPreFetch()調用。如果您需要移除它,或者需要新增另一個連線修改器,其操作很容易,並且對您的所有連線起作用。這就是使用打包的庫的價值。

使您的處理序更加高效

在文章的開始部分,我提到我將說明您不遺漏在 Apache
中為滿足可伸縮性需求而減少的子處理序。利用更少資源來完成工作的三種最簡單方法是:

  1. 更快地完成工作。任何人都會告訴您,增加工作負載的最快方法是更快地完成現有的工作。無論是安裝編譯器高速快取、配置程式碼還是調整資料庫查詢,您能夠從應用程式中取得的任何效能增益都會在可伸縮性方麵得到回報。
  2. 將某些工作轉包給專家。例如,網站通常包含動態元件(您的 PHP 指令檔就是這種元件)和靜態元件(影像和非動態 HTML)。訪問 Oracle 的
    Apache 實例在服務於靜態內容時,您受到嚴格的約束,這是在浪費時間。可將它外包給私用於處理這類負載的 Web 伺服器。
  3. 完全跳過工作。分析應用程式時經常會發現一些資料庫驅動的元件,這些元件不必為每個請求而從資料庫生成。找到這些元件並盡可能將其重新設計為靜態元件,這樣會顯着提高效能和可伸縮性。

卸載靜態內容。如果 Web
應用程式中的页面一般包含九幅影像,則發送到您的 Web
伺服器的請求中隻有百分之十實際上使用了為其分配的持續連線。換句話說,百分之九十的請求正在浪費有用的(從可伸縮性的觀點來看,還是成本很高的)Oracle
連線句柄。您的目的應該是確保隻有那些需要 Oracle 連線(或至少需要動態內容)的請求才能接受動態 Web 伺服器的服務。這會增加由每個處理序所完成的與
Oracle 相關的工作量,而這又會減少生成動態內容所需的子處理序數量。

改善這種狀況的最簡單方法是將您的所有影像卸載到單獨的 Web 伺服器(或一組 Web
伺服器)上。這非常容易。第一步是建立第二個 Web 伺服器處理靜態請求。雖然您可以為此而使用 Apache,但是還有些專門擅長於為靜態資料提供服務的其他 Web
伺服器(例如 tux 和 thttpd),它們的結果或許更好。應該建立這個 Web 伺服器,為另外子域的請求提供服務。常見做法是將
"
www.example.com" 的影像流量委託管給
"
images.example.com"。某些硬體負載均衡器實際上允許您為同一域中的影像提供服務,並在內部進行分配 - 有關詳細資訊請檢視負載均衡器文檔。

一旦建立了域,就應該建立一個全域配置檔案,它包含您在整個應用程式中使用的所有全域常數,並應該至少新增下麵一行:

define(CDN_URL, "http://images.example.com");

此檔案可以手動地包含在每個檔案的頂部,或者可以在 php.ini
配置檔案中新增下行,從而在每個指令檔開始時能夠自動執行該檔案:

auto_prepend_file = /path/to/config.inc

現在,不管何時在 HTML 中建立影像標記,都應該新增以下程式碼:


如果您更願意擁有標記編寫庫,則可以編寫一個影像標記建立函式,如下所示:

function img_tag($local_uri, $attr)
{
$attribute = '';
foreach ($attr as $k=>$v) {
$k = urlencode($k);
$v = urlencode($v);
$attribute .= " $k="$v" ";
}
return "";
}

即使不準備立即使用影像私用網路,還是應該對您的應用程式進行編碼,為影像使用單獨的基礎路徑,並簡單地將其設定如下:

define(CDN_URL, "http://www.example.com/images");

這允許您在任何時候僅僅變更單行程式碼,就將整個網站削減為一個替代性的影像服務網路。根據您的網站中靜態內容/影像的比例,可以看到伺服器資源明顯減少甚至極大地減少。在一個用戶端,將靜態內容從
Apache 移動到私用的係列 thttpd 箱中,可以使它們的總體基礎架構減少百分之五十。

為什麼要在不必工作的時候而工作呢?對任何查詢的最終效能增強是根本不執行查詢。很多“動態”Web
页面實際上不是動態的,在短期時間內純粹是靜態的。假設有一個新網站:在新聞項目更新之前,其內容並不改變。無論這種更新是每分鐘發生還是每小時發生,在兩次更新之間,網站是靜態的。這意味着,不必為每個页面請求而在當前新聞項目中進行資料庫尋找,隻需在每個更新週期中作一次尋找。

最簡單類型的高速快取是全頁隨需高速快取。在這種情況下,當請求到來時,應用程式在高速快取中尋找所需檔案。如果存在高速快取的副本,則將其返回給請求者;否則建立一個高速快取副本,並將其返回給使用者。要重新整理高速快取,隻需簡單地將舊的高速快取副本移除即可
- 下一個請求將會自動重新生成高速快取。

圖 2顯示了我們希望完成的流程圖:當一個請求需要 /archive/123.html 時,Web
伺服器應該檢視該檔案是否實際存在。如果該檔案存在,則返回該檔案。如果該檔案不存在,使用者會被重定嚮到一個 PHP 页面 generate.php,並將页面識別碼
"123" 作為參數傳遞。隨後該页面生成页面的高速快取項。在 Apache 環境中,執行這種高速快取尋找的兩種典型方法是使用自訂的 ErrorHandler
或者 mod_rewrite。mod_rewrite 提供更好的靈活性,因此我將實施這種方法。

圖 2:全頁隨需高速快取流程圖

首先,需要在 httpd.conf 中建立重寫規則。以下是一個範例片段:

RewriteEngine On
RewriteConf %{REQUEST_FILENAME} ^/archive/[0-9]+.html
RewriteConf %{REQUEST_FILENAME} !-f
RewriteRule ^/archive/([0-9]+).html /generate.php?id=$1

此範例首先啓動重寫引擎,然後幫助將會重寫任何與歸檔模式 (^/archive/[0-9]+.html)
相比對並且不存在 (!-f) 的被請求檔案名,將页面識別碼作為參數傳遞,轉到生成页面處。

生成器页面同樣很簡單:

$id = $_GET['id'];
$dbh =& new DB_Oracle_TestDB;
$cursor =& $dbh->execute("SELECT content FROM news WHERE id = $id");
$result = $cursor->fetch();
if(!$result) {
header("HTTP/1.0 404 Not Found");
exit
}
} else
echo $result['CONTENT'];
$outfile = $_SERVER['DOCUMENT_ROOT']."/archive/$id.html";
if(($fp = fopen($outfile, "w")) === false) {
exit;
}
fwrite($fp, $result['CONTENT']);
fclose($fp);
}
?>

在本範例中,生成器假定新聞表的內容列中包含完全格式化的內容。實際上,您也最可能在指令檔中執行某種格式化,使用輸出緩衝功能來擷取輸出。

結論

您在這裡看到的隻是關於延伸 Oracle 與 PHP
技術的簡短幫助。儘管這些範例可能很有用,但我希望您從本文中瞭解的主要內容是:

  • 嚴密管理您的 Oracle 連線,以避免資源耗竭。
  • 速度為您帶來高效率,這有助於可伸縮性。
  • 在可能的情況下,使用高速快取技術來完全避免資料庫查詢。

George Schlossnagle
OmniTI Computer Consulting 的負責人,這家位於馬利蘭的技術公司專門從事高容量 Web 及電子郵件係統方麵的工作。在加入 OmniTI
之前,Schlossnagle 在幾家高水平的社區網站主持技術工作,在這些地方他獲得了在非常大的企業環境中管理 PHP 的經驗。Schlossnagle 經常為
PHP 團隊作出貢獻。在 PHP 核心以及 PEAR 和 PECL 延伸庫中可以發現他的蹤迹。

<<<返回技術中心

技術文章

站內新聞

我要啦免费统计