前言
很久很久沒(méi)有更新博客了。久到我都不記得上一次更新博客是什么時(shí)候,久到我們博客主機(jī)都過(guò)期了,一度我還想停掉這個(gè)博客。好在有simon的堅(jiān)持才決定博客繼續(xù)整下去。2013年對(duì)我來(lái)說(shuō)是一個(gè)比較折騰的一年。一年之類換了兩份
工作。找工作的時(shí)候才發(fā)現(xiàn)理想與現(xiàn)實(shí)之間的差距是如此的巨大。期間經(jīng)歷了落差、失望、彷徨……
一、為什么要使用檢查點(diǎn)
為什么要使用檢查點(diǎn),那就要說(shuō)明一下LR如何判斷腳本是否執(zhí)行成功。
LR判斷腳本是否執(zhí)行成功是根據(jù)服務(wù)器返回的狀態(tài)來(lái)確定的,如果服務(wù)器返回的HTTP狀態(tài)為 200 OK ,那么VuGen 就認(rèn)為腳本正確地運(yùn)行了,并且是運(yùn)行通過(guò)的。在絕大多數(shù)系統(tǒng)出錯(cuò)時(shí)會(huì)返回錯(cuò)誤頁(yè)面碼? 不會(huì)一般系統(tǒng)都會(huì)返回一個(gè)消息提示框,來(lái)提升用戶感受。例如,“網(wǎng)站繁忙,請(qǐng)稍后”。其實(shí)這個(gè)時(shí)候網(wǎng)站已經(jīng)無(wú)法正確響應(yīng)用戶請(qǐng)求了,但是VuGen 腳本無(wú)法識(shí)別,會(huì)錯(cuò)誤地認(rèn)為網(wǎng)站還能正確訪問(wèn),導(dǎo)致分析錯(cuò)誤。所以這時(shí)需要一種檢查點(diǎn)函數(shù)幫助驗(yàn)證請(qǐng)求發(fā)送出去后,服務(wù)器的返回是不是期望的內(nèi)容,如果不是,那么就說(shuō)明服務(wù)器返回?zé)o法提供正常的服務(wù)了。
另外,需要特別說(shuō)明的是,檢查點(diǎn)一般跟事務(wù)結(jié)合起來(lái)使用。
二、事務(wù)結(jié)束的四種狀態(tài)
前面說(shuō)到檢查點(diǎn)一般跟事務(wù)結(jié)合使用。這樣需要說(shuō)明一下事務(wù)(transaction)。我認(rèn)為事務(wù)是LR中非常非常重要的一個(gè)概念。因?yàn)橥瓿梢粋€(gè)事務(wù)所需要的時(shí)間是響應(yīng)時(shí)間(Trans Response Time),一秒鐘可以完成多少事務(wù)是TPS(Trans/Sec)。響應(yīng)時(shí)間和TPS是
性能測(cè)試中非常重要的兩個(gè)指標(biāo)??梢酝ㄟ^(guò)這兩個(gè)指標(biāo)來(lái)分析系統(tǒng)是否出現(xiàn)瓶頸。
LR中事務(wù)結(jié)束的時(shí)候需要帶上一個(gè)狀態(tài)碼(Transaction Status)。LR中Transaction Status有四個(gè), 分別為L(zhǎng)R_PASS, LR_FAIL, LR_AUTO , LR_STOP。
LR_AUTO:事物的狀態(tài)被自動(dòng)設(shè)置,如果事務(wù)執(zhí)行成功,狀態(tài)設(shè)置為PASS,如果執(zhí)行失敗,狀態(tài)設(shè)置為FAIL,如果由于異常中斷,狀態(tài)被設(shè)置成STOP.
LR_PASS:事務(wù)如果執(zhí)行成功,代碼的返回狀態(tài)就是PASS。
LR_FAIL:事務(wù)如果執(zhí)行失敗,代碼的返回狀態(tài)就是FAIL。
一般情況下會(huì)選擇LR_AUTO,讓LR自動(dòng)判斷事務(wù)的狀態(tài)??墒侨缟厦嫠f(shuō)LR判斷腳本是否執(zhí)行成功是根據(jù)服務(wù)器返回的狀態(tài)來(lái)確定的,也就是只要服務(wù)器返回的HTTP狀態(tài)為 200 OK ,事務(wù)執(zhí)行后狀態(tài)總是被置為L(zhǎng)R_PASS。這樣不科學(xué)的地方在于,LR提示成功的事務(wù)你不知道實(shí)際上它到底是成功的還是失敗的。所以,這就需要檢查點(diǎn)了。使用只有事務(wù)執(zhí)行成功才會(huì)出現(xiàn)的文本或者圖片(PS:雖然圖片檢查點(diǎn)我從來(lái)沒(méi)用過(guò))來(lái)作為事務(wù)是否執(zhí)行成功的標(biāo)準(zhǔn)。
三、如何添加檢查點(diǎn)
使用文本檢查點(diǎn)(下面簡(jiǎn)稱檢查點(diǎn))需要使用web_reg_find函數(shù)。函數(shù)的使用詳見(jiàn)LR幫助手冊(cè),如果不會(huì)用可以下面的實(shí)戰(zhàn)部分。
另外說(shuō)明一下如果web_reg_find查找中文失敗,完全可以查找英文,只要能實(shí)現(xiàn)檢查點(diǎn)功能就可以了,如果執(zhí)意要查找英文,請(qǐng)將Record-Options 中勾選support charset中的UTF-8后重新錄制。
四、如何選擇檢查點(diǎn)
選擇什么作為檢查點(diǎn),這是困擾了很久了一個(gè)問(wèn)題。就拿登陸來(lái)說(shuō),并不是所有的系統(tǒng)登陸完成后系統(tǒng)跳轉(zhuǎn)到登陸成功的頁(yè)面。這里這需要借助運(yùn)行時(shí)查看器(run-time viewer)來(lái)幫助我們查找合適的檢查點(diǎn)。運(yùn)行時(shí)查看器在選擇工具(tools)-常規(guī)選項(xiàng)(general Options)-顯示(Display) ,選擇 打開(kāi)運(yùn)行時(shí)查看器和自動(dòng)排列窗口。
運(yùn)行時(shí)查看器的使用我還得感謝我們組成員文武同學(xué)。我還老吐槽他老開(kāi)著這沒(méi)用的玩意干嘛,我承認(rèn)我錯(cuò)了。
五、實(shí)戰(zhàn)
以網(wǎng)站www.huhoo.com(一不小心透露了經(jīng)緯度)登陸舉例,錄制登陸腳本后回放,回放的時(shí)候打開(kāi)運(yùn)行時(shí)查看器。可以看到登陸成功后發(fā)回這樣的信息。
圖1 登陸成功后返回信息
然后修改腳本,故意將用戶名的密碼弄錯(cuò),重新回放
圖2 登陸錯(cuò)誤后返回信息
結(jié)合兩張圖可以看出,用”code”:0作為檢查點(diǎn)比較合適。因?yàn)檫@樣的文本在登陸失敗時(shí)沒(méi)有出現(xiàn)。但是如果用“msg”來(lái)作為檢查點(diǎn)就不合適,因?yàn)椴还艿顷懗晒褪《汲霈F(xiàn)了這也的文本,不能作為檢查登陸成功的標(biāo)準(zhǔn)。
檢查點(diǎn)示例代碼如下:
web_reg_find("Text=\"code\":0","SaveCount=Code_Count", LAST ); lr_start_transaction("login"); web_submit_data("login.quick", "Action=http://www.huhoo.com/p/login.quick/", …… "Name=account", "Value=XXXXXX", ENDITEM, "Name=password", "Value=XXXXX", ENDITEM, LAST); if (atoi(lr_eval_string("{Code_Count}")) > 0){ //lr_output_message("Log on successful."); lr_end_transaction("login", LR_PASS); } else{ lr_error_message("Log on failed"); lr_end_transaction("login", LR_FAIL); return -1; } |
完整腳本下載:傳送門
說(shuō)明:
腳本中用戶名,密碼已經(jīng)被我用XXX替換,如果想用此腳本做實(shí)驗(yàn),請(qǐng)到虎虎官網(wǎng)www.huhoo.com注冊(cè)用戶,并將用戶名寫入腳本中。
可能有些讀者并不連接什么是Ant以及入可使用它,但只要使用通過(guò)
Linux系統(tǒng)得讀者,應(yīng)該知道m(xù)ake這個(gè)命令。當(dāng)編譯Linux內(nèi)核及一些軟件的源程序時(shí),經(jīng)常要用這個(gè)命令。Make命令其實(shí)就是一個(gè)
項(xiàng)目管理工具,而Ant所實(shí)現(xiàn)功能與此類似。像make,gnumake和nmake這些編譯工具都有一定的缺陷,但是Ant卻克服了這些工具的缺陷。最初Ant開(kāi)發(fā)者在開(kāi)發(fā)跨平臺(tái)的應(yīng)用時(shí),用樣也是基于這些缺陷對(duì)Ant做了更好的設(shè)計(jì)。
Ant 與 makefile
Makefile有一些不足之處,比如很多人都會(huì)碰到的煩人的Tab問(wèn)題。最初的Ant開(kāi)發(fā)者多次強(qiáng)調(diào)”只是我在Tab前面加了一個(gè)空格,所以我的命令就不能執(zhí)行”。有一些工具在一定程度上解決了這個(gè)問(wèn)題,但還是有很多其他的問(wèn)題。Ant則與一般基于命令的工具有所不同,它是
Java類的擴(kuò)展。Ant運(yùn)行需要的XML格式的文件不是Shell命令文件。它是由一個(gè)Project組成的,而一個(gè)Project又可分成可多target,target再細(xì)分又分成很多task,每一個(gè)task都是通過(guò)一個(gè)實(shí)現(xiàn)特定接口的java類來(lái)完成的。
Ant的優(yōu)點(diǎn)
Ant是Apache軟件基金會(huì)JAKARTA目錄中的一個(gè)子項(xiàng)目,它有以下的優(yōu)點(diǎn)??缙脚_(tái)性。Ant是存Java語(yǔ)言編寫的,所示具有很好的跨平臺(tái)性。操作簡(jiǎn)單。Ant是由一個(gè)內(nèi)置任務(wù)和可選任務(wù)組成的。Ant運(yùn)行時(shí)需要一個(gè)XML文件(構(gòu)建文件)。Ant通過(guò)調(diào)用target樹,就可以執(zhí)行各種task。每個(gè)task實(shí)現(xiàn)了特定接口對(duì)象。由于Ant構(gòu)建文件時(shí)XML格式的文件,所以和容易維護(hù)和書寫,而且結(jié)構(gòu)很清晰。Ant可以集成到開(kāi)發(fā)環(huán)境中。由于Ant的跨平臺(tái)性和操作簡(jiǎn)單的特點(diǎn),它很容易集成到一些開(kāi)發(fā)環(huán)境中去。
Ant 開(kāi)發(fā)Ant的構(gòu)建文件當(dāng)開(kāi)始一個(gè)新的項(xiàng)目時(shí),首先應(yīng)該編寫Ant構(gòu)建文件。構(gòu)建文件定義了構(gòu)建過(guò)程,并被團(tuán)隊(duì)開(kāi)發(fā)中每個(gè)人使用。Ant構(gòu)建文件默認(rèn)命名為build.xml,也可以取其他的名字。只不過(guò)在運(yùn)行的時(shí)候把這個(gè)命名當(dāng)作參數(shù)傳給Ant。構(gòu)建文件可以放在任何的位置。一般做法是放在項(xiàng)目頂層目錄中,這樣可以保持項(xiàng)目的簡(jiǎn)潔和清晰。下面是一個(gè)典型的項(xiàng)目層次結(jié)構(gòu)。
(1) src存放文件。
(2) class存放編譯后的文件。
(3) lib存放第三方JAR包。
(4) dist存放打包,發(fā)布以后的代碼。
Ant構(gòu)建文件是XML文件。每個(gè)構(gòu)建文件定義一個(gè)唯一的項(xiàng)目(Project元素)。每個(gè)項(xiàng)目下可以定義很多目標(biāo)(target元素),這些目標(biāo)之間可以有依賴關(guān)系。當(dāng)執(zhí)行這類目標(biāo)時(shí),需要執(zhí)行他們所依賴的目標(biāo)。每個(gè)目標(biāo)中可以定義多個(gè)任務(wù),目標(biāo)中還定義了所要執(zhí)行的任務(wù)序列。Ant在構(gòu)建目標(biāo)時(shí)必須調(diào)用所定義的任務(wù)。任務(wù)定義了Ant實(shí)際執(zhí)行的命令。Ant中的任務(wù)可以為3類。
(1) 核心任務(wù)。核心任務(wù)是Ant自帶的任務(wù)。
(2) 可選任務(wù)??蛇x任務(wù)實(shí)來(lái)自第三方的任務(wù),因此需要一個(gè)附加的JAR文件。
(3) 用戶自定義的任務(wù)。用戶自定義的任務(wù)實(shí)用戶自己開(kāi)發(fā)的任務(wù)。
1.<project>標(biāo)簽
每個(gè)構(gòu)建文件對(duì)應(yīng)一個(gè)項(xiàng)目。<project>標(biāo)簽時(shí)構(gòu)建文件的根標(biāo)簽。它可以有多個(gè)內(nèi)在屬性,就如代碼中所示,其各個(gè)屬性的含義分別如下。
(1) default表示默認(rèn)的運(yùn)行目標(biāo),這個(gè)屬性是必須的。
(2) basedir表示項(xiàng)目的基準(zhǔn)目錄。
(3) name表示項(xiàng)目名。
(4) description表示項(xiàng)目的描述。
每個(gè)構(gòu)建文件都對(duì)應(yīng)于一個(gè)項(xiàng)目,但是大型項(xiàng)目經(jīng)常包含大量的子項(xiàng)目,每一個(gè)子項(xiàng)目都可以有自己的構(gòu)建文件。
2.<target>標(biāo)簽
一個(gè)項(xiàng)目標(biāo)簽下可以有一個(gè)或多個(gè)target標(biāo)簽。一個(gè)target標(biāo)簽可以依賴其他的target標(biāo)簽。例如,有一個(gè)target用于編譯程序,另一個(gè)target用于聲稱可執(zhí)行文件。在生成可執(zhí)行文件之前必須先編譯該文件,因策可執(zhí)行文件的target依賴于編譯程序的target。Target的所有屬性如下。
(1) name表示標(biāo)明,這個(gè)屬性是必須的。
(2) depends表示依賴的目標(biāo)。
(3) if表示僅當(dāng)屬性設(shè)置時(shí)才執(zhí)行。
(4) unless表示當(dāng)屬性沒(méi)有設(shè)置時(shí)才執(zhí)行。
(5) description表示項(xiàng)目的描述。
Ant的depends屬性指定了target的執(zhí)行順序。Ant會(huì)依照depends屬性中target出現(xiàn)順序依次執(zhí)行每個(gè)target。在執(zhí)行之前,首先需要執(zhí)行它所依賴的target。程序中的名為run的target的depends屬性compile,而名為compile的target的depends屬性是prepare,所以這幾個(gè)target執(zhí)行的順序是prepare->compile->run。一個(gè)target只能被執(zhí)行一次,即使有多個(gè)target依賴于它。如果沒(méi)有if或unless屬性,target總會(huì)被執(zhí)行。3.<mkdir>標(biāo)簽
該標(biāo)簽用于創(chuàng)建一個(gè)目錄,它有一個(gè)屬性dir用來(lái)指定所創(chuàng)建的目錄名,其代碼如下:<mkdir dir=”${class.root}”/>通過(guò)以上代碼就創(chuàng)建了一個(gè)目錄,這個(gè)目錄已經(jīng)被前面的property標(biāo)簽所指定。
4.<jar>標(biāo)簽
該標(biāo)簽用來(lái)生成一個(gè)JAR文件,其屬性如下。
(1) destfile表示JAR文件名。
(2) basedir表示被歸檔的文件名。
(3) includes表示別歸檔的文件模式。
(4) exchudes表示被排除的文件模式。
5.<javac>標(biāo)簽
該標(biāo)簽用于編譯一個(gè)或一組java文件,其屬性如下:
(1).srcdir表示源程序的目錄。
(2).destdir表示class文件的輸出目錄。
(3).include表示被編譯的文件的模式。
(4).excludes表示被排除的文件的模式。
(5).classpath表示所使用的類路徑。
(6).debug表示包含的調(diào)試信息。
(7).optimize表示是否使用優(yōu)化。
(8).verbose 表示提供詳細(xì)的輸出信息。
(9).fileonerror表示當(dāng)碰到錯(cuò)誤就自動(dòng)停止。
6.<java>標(biāo)簽
該標(biāo)簽用來(lái)執(zhí)行編譯生成的.class文件,其屬性如下。
(1).classname 表示將執(zhí)行的類名。
(2).jar表示包含該類的JAR文件名。
(3).classpath所表示用到的類路徑。
(4).fork表示在一個(gè)新的虛擬機(jī)中運(yùn)行該類。
(5).failonerror表示當(dāng)出現(xiàn)錯(cuò)誤時(shí)自動(dòng)停止。
(6).output 表示輸出文件。
(7).append表示追加或者覆蓋默認(rèn)文件。
7.<delete>標(biāo)簽
該標(biāo)簽用于刪除一個(gè)文件或一組文件,去屬性如下:
(1).file表示要?jiǎng)h除的文件。
(2).dir表示要?jiǎng)h除的目錄。
(3).includeEmptyDirs 表示指定是否要?jiǎng)h除空目錄,默認(rèn)值是刪除。
(4).failonerror 表示指定當(dāng)碰到錯(cuò)誤是否停止,默認(rèn)值是自動(dòng)停止。
(5).verbose表示指定是否列出所刪除的文件,默認(rèn)值為不列出。
8.<copy>標(biāo)簽
該標(biāo)簽用于文件或文件集的拷貝,其屬性如下。
(1).file 表示源文件。
(2).tofile 表示目標(biāo)文件。
(3).todir 表示目標(biāo)目錄。
(4).overwrite 表示指定是否覆蓋目標(biāo)文件,默認(rèn)值是不覆蓋。
(5).includeEmptyDirs 表示制定是否拷貝空目錄,默認(rèn)值為拷貝。
(6).failonerror 表示指定如目標(biāo)沒(méi)有發(fā)現(xiàn)是否自動(dòng)停止,默認(rèn)值是停止。
(7).verbose 表示制定是否顯示詳細(xì)信息,默認(rèn)值不顯示。
Ant的數(shù)據(jù)類型
在構(gòu)建文件中為了標(biāo)識(shí)文件或文件組,經(jīng)常需要使用數(shù)據(jù)類型。數(shù)據(jù)類型包含在
org.apache.tool.ant.types包中。下面簡(jiǎn)單介紹構(gòu)建文件中一些常用的數(shù)據(jù)類型。
1. argument 類型
由Ant構(gòu)建文件調(diào)用的程序,可以通過(guò)<arg>元素向其傳遞命令行參數(shù),如apply,exec和java任務(wù)均可接受嵌套<arg>元素,可以為各自的過(guò)程調(diào)用指定參數(shù)。以下是<arg>的所有屬性。
(1).values 是一個(gè)命令參數(shù)。如果參數(shù)種有空格,但又想將它作為單獨(dú)一個(gè)值,則使用此屬性。
(2).file表示一個(gè)參數(shù)的文件名。在構(gòu)建文件中,此文件名相對(duì)于當(dāng)前的工作目錄。
(3).line表示用空格分隔的多個(gè)參數(shù)列表。
(4).path表示路徑。
2.ervironment 類型
由Ant構(gòu)建文件調(diào)用的外部命令或程序,<env>元素制定了哪些環(huán)境變量要傳遞給正在執(zhí)行的系統(tǒng)命令,<env>元素可以接受以下屬性。
(1).file表示環(huán)境變量值得文件名。此文件名要被轉(zhuǎn)換位一個(gè)絕對(duì)路徑。
(2).path表示環(huán)境變量的路徑。Ant會(huì)將它轉(zhuǎn)換為一個(gè)本地約定。
(3).value 表示環(huán)境變量的一個(gè)直接變量。
(4).key 表示環(huán)境變量名。
注意 file path 或 value只能取一個(gè)。
3.filelist類型,Filelist 是一個(gè)支持命名的文件列表的數(shù)據(jù)類型,包含在一個(gè)filelist類型中的文件不一定是存在的文件。以下是其所有的屬性。
(1).dir是用于計(jì)算絕對(duì)文件名的目錄。
(2).files 是用逗號(hào)分隔的文件名列表。
(3).refid 是對(duì)某處定義的一個(gè)<filelist>的引用。
注意 dir 和 files 都是必要的,除非指定了refid(這種情況下,dir和files都不允許使用)。
4.fileset類型
Fileset 數(shù)據(jù)類型定義了一組文件,并通常表示為<fileset>元素。不過(guò),許多ant任務(wù)構(gòu)建成了隱式的fileset,這說(shuō)明他們支持所有的fileset屬性和嵌套元素。以下為fileset 的屬性列表。
(1).dir表示fileset 的基目錄。
(2).casesensitive的值如果為false,那么匹配文件名時(shí),fileset不是區(qū)分大小寫的,其默認(rèn)值為true。
(3).defaultexcludes 用來(lái)確定是否使用默認(rèn)的排除模式,默認(rèn)為true。
(4).excludes 是用逗號(hào)分隔的需要派出的文件模式列表。
(5).excludesfile 表示每行包含一個(gè)排除模式的文件的文件名。
(6).includes 是用逗號(hào)分隔的,需要包含的文件模式列表。
(7).includesfile 表示每行包括一個(gè)包含模式的文件名。
5.patternset 類型
Fileset 是對(duì)文件的分組,而patternset是對(duì)模式的分組,他們是緊密相關(guān)的概念。<patternset>支持4個(gè)屬性:includes excludex includexfile 和 excludesfile,與fileset相同。Patternset 還允許以下嵌套元素:include,exclude,includefile 和 excludesfile。
6.filterset 類型
Filterset定義了一組過(guò)濾器,這些過(guò)濾器將在文件移動(dòng)或復(fù)制時(shí)完成文件的文本替換。
主要屬性如下:
(1).begintoken 表示嵌套過(guò)濾器所搜索的記號(hào),這是標(biāo)識(shí)其開(kāi)始的字符串。
(2).endtoken表示嵌套過(guò)濾器所搜索的記號(hào)這是標(biāo)識(shí)其結(jié)束的字符串。
(3).id是過(guò)濾器的唯一標(biāo)志符。
(4).refid是對(duì)構(gòu)建文件中某處定義一個(gè)過(guò)濾器的引用。
7.Path類型
Path元素用來(lái)表示一個(gè)類路徑,不過(guò)它還可以用于表示其他的路徑。在用作揖個(gè)屬性時(shí),路經(jīng)中的各項(xiàng)用分號(hào)或冒號(hào)隔開(kāi)。在構(gòu)建的時(shí)候,此分隔符將代替當(dāng)前平臺(tái)中所有的路徑分隔符,其擁有的屬性如下。
(1).location 表示一個(gè)文件或目錄。Ant在內(nèi)部將此擴(kuò)展為一個(gè)絕對(duì)路徑。
(2).refid 是對(duì)當(dāng)前構(gòu)建文件中某處定義的一個(gè)path的引用。
(3).path表示一個(gè)文件或路徑名列表。
8.mapper類型
Mapper類型定義了一組輸入文件和一組輸出文件間的關(guān)系,其屬性如下。
(1).classname 表示實(shí)現(xiàn)mapper類的類名。當(dāng)內(nèi)置mapper不滿足要求時(shí),用于創(chuàng)建定制mapper。
(2).classpath表示查找一個(gè)定制mapper時(shí)所用的類型路徑。
(3).classpathref是對(duì)某處定義的一個(gè)類路徑的引用。
(4).from屬性的含義取決于所用的mapper。
(5).to屬性的含義取決于所用的mapper。
(6).type屬性的取值為identity,flatten glob merge regexp 其中之一,它定義了要是用的內(nèi)置mapper的類型。
Ant 的運(yùn)行
安裝好Ant并且配置好路徑之后,在命令行中切換到構(gòu)建文件的目錄,輸入Ant命令就可以運(yùn)行Ant.若沒(méi)有指定任何參數(shù),Ant會(huì)在當(dāng)前目錄下查詢build.xml文件。如果找到了就用該文件作為構(gòu)建文件。如果使用了 –find 選項(xiàng),Ant 就會(huì)在上級(jí)目錄中找構(gòu)建文件,直至到達(dá)文件系統(tǒng)得跟目錄。如果構(gòu)建文件的名字不是build.xml ,則Ant運(yùn)行的時(shí)候就可以使用 –buildfile file,這里file 指定了要使用的構(gòu)建文件的名稱,示例如下:
Ant如下說(shuō)明了表示當(dāng)前目錄的構(gòu)建文件為build.xml 運(yùn)行 ant 執(zhí)行默認(rèn)的目標(biāo)。
Ant –buildfile test.xml使用當(dāng)前目錄下的test.xml 文件運(yùn)行Ant ,執(zhí)行默認(rèn)的目標(biāo)
講解了鉤子程序的攻防實(shí)戰(zhàn),并實(shí)現(xiàn)了一套對(duì)框架頁(yè)的監(jiān)控方案,將防護(hù)作用到所有子頁(yè)面。
到目前為止,我們防護(hù)的深度已經(jīng)差不多,但廣度還有所欠缺。
例如,我們的屬性鉤子只考慮了 setAttribute,卻忽視還有類似的 setAttributeNode。盡管從來(lái)不用這方法,但并不意味人家不能使用。
例如,創(chuàng)建元素通常都是 createElement,事實(shí)上 createElementNS 同樣也可以。甚至還可以利用現(xiàn)成的元素 cloneNode,也能達(dá)到目的。因此,這些都是邊緣方法都是值得考慮的。
下面我們對(duì)之前討論過(guò)的監(jiān)控點(diǎn),進(jìn)行逐一審核。
內(nèi)聯(lián)事件執(zhí)行 eval
在第一篇
文章結(jié)尾談到,在執(zhí)行回調(diào)的時(shí)候,最好能監(jiān)控 eval,setTimeout('...') 這些能夠解析代碼的函數(shù),以防止執(zhí)行儲(chǔ)存在其他地方的
XSS 代碼。
先來(lái)列舉下這類函數(shù):
eval
setTimeout(String) / setInterval(String)
Function
execScript / setImmediate(String)
事實(shí)上,利用上一篇的鉤子技術(shù),完全可以把它們都監(jiān)控起來(lái)。但現(xiàn)實(shí)并沒(méi)有我們想象的那樣簡(jiǎn)單。
eval 重寫有問(wèn)題嗎
eval 不就是個(gè)函數(shù),為什么不可以重寫?
var raw_fn = window.eval;
window.eval = function(exp) {
alert('執(zhí)行eval: ' + exp);
return raw_fn.apply(this, arguments);
};
console.log(eval('1+1'));
完全沒(méi)問(wèn)題啊。那是因?yàn)榇a太簡(jiǎn)單了,下面這個(gè) Demo 就可以看出山寨版 eval 的缺陷:
(function() {
eval('var a=1');
})();
alert(typeof a);
Run
按理說(shuō)應(yīng)該 undefined 才對(duì),結(jié)果卻是 number。局部變量都跑到全局上來(lái)了。這是什么情況?事實(shí)上,eval 并不是真正意義的函數(shù),而是一個(gè)關(guān)鍵字!想了解詳情請(qǐng)戳這里。
Function 重寫有意義嗎
Function 是一個(gè)全局變量,重寫 window.Function 理論上完全可行吧。
var raw_fn = window.Function;
window.Function = function() {
alert('調(diào)用Function');
return raw_fn.apply(this, arguments);
};
var add = Function('a', 'b', 'return a+b');
console.log( add(1, 2) );
重寫確實(shí)可行。但現(xiàn)實(shí)卻是不堪一擊的:因?yàn)樗泻瘮?shù)都是 Function 類的實(shí)例,所以訪問(wèn)任何一個(gè)函數(shù)的 constructor 即可得到原始的 Function。
例如 alert.constructor,就可以繞過(guò)我們的鉤子。甚至可以用匿名函數(shù):
(function(){}).constructor
所以,F(xiàn)unction 是永遠(yuǎn)鉤不住的。
額外的執(zhí)行方法
就算不用這類函數(shù),仍有相當(dāng)多的辦法執(zhí)行字符串,例如:
創(chuàng)建腳本,innerHTML = 代碼
創(chuàng)建腳本,路徑 = data:代碼
創(chuàng)建框架,路徑 = javascript:代碼
......
看來(lái),想完全把類似 eval 的行為監(jiān)控起來(lái),是不現(xiàn)實(shí)的。不過(guò)作為預(yù)警,我們只監(jiān)控 eval,setTimeout/Interval 也就足夠了。
可疑模塊攔截
第二篇談了站外模塊的攔截。之所以稱之『模塊』而不是『腳本』,并非只有腳本元素才具備執(zhí)行能力。框架頁(yè)、插件都是可以運(yùn)行代碼的。
可執(zhí)行元素
我們列舉下,能執(zhí)行遠(yuǎn)程模塊的元素:
腳本
<script src="..." />
框架
<iframe src="...">
<frame src="...">
插件(Flash)
<embed src="...">
<object data="...">
<object><param name="moive|src" value="..."></object>
插件(其他)
<applet codebase="...">
這些元素的路徑屬性,都應(yīng)該作為排查的對(duì)象。
不過(guò),有這么個(gè)元素的存在,可能導(dǎo)致我們的路徑檢測(cè)失效,它就是:
<base href="...">
它能重定義頁(yè)面的相對(duì)路徑,顯然是不容忽視的。
事實(shí)上,除了使用元素來(lái)執(zhí)行站外模塊,還可以使用網(wǎng)絡(luò)通信,獲得站外的腳本代碼,然后再調(diào)用 eval 執(zhí)行:
AJAX
目前主流瀏覽器都支持跨域請(qǐng)求,只要服務(wù)端允許就可以。因此,我們需監(jiān)控 XMLHttpRequest::open 方法。如果請(qǐng)求的是站外地址,就得做策略匹配。不通過(guò)則放棄向上調(diào)用,或者拋出一個(gè)異常,或者給 XHR 產(chǎn)生一個(gè) 400 狀態(tài)。
WebSocket
WebSocket 和 XHR 類似,也能通過(guò)鉤子的方法進(jìn)行監(jiān)控。
不過(guò),值得注意的是,WebSocket 并非是個(gè)函數(shù),而是一個(gè)類。因此,在返回實(shí)例的時(shí)候,別忘了將 constructor 改成自己的鉤子,否則就會(huì)泄露原始接口:
var raw_class = window.WebSocket; window.WebSocket = function WebSocket(url, arg) { alert('WebSocket 請(qǐng)求:' + url); var ins = new raw_class(url, arg); // 切記 ins.constructor = WebSocket; return ins; }; var ws = new WebSocket('ws://127.0.0.1:1000'); |
另外,因?yàn)樗且粋€(gè)類,所以不要忽略了靜態(tài)方法或?qū)傩裕?/div>
WebSocket.CONNECTING
WebSocket.OPEN
...
因此,還需將它們拷貝到鉤子上。
框架頁(yè)消息
HTML5 賦予了框架頁(yè)跨域通信的能力。如果沒(méi)有為框架元素建立白名單的話,攻擊者可以嵌入自己的框架頁(yè)面,然后將 XSS 代碼 postMessage 給主頁(yè)面,通過(guò) eval 執(zhí)行。
不過(guò)為了安全考慮,HTML5 在消息事件里保存了來(lái)源地址,以識(shí)別消息是哪個(gè)頁(yè)面發(fā)出的。
因?yàn)槭莻€(gè)事件,我們可以使用第一篇文章里提到的方法,對(duì)其進(jìn)行捕獲。每當(dāng)有消息收到時(shí),可以根據(jù)策略,決定是否阻止該事件的傳遞。
// 我們的防御系統(tǒng) (function() { window.addEventListener('message', function(e) { if (confirm('發(fā)現(xiàn)來(lái)自[' + e.origin + ']的消息:\n\n' + e.data + '\n\n是否攔截?')) { e.stopImmediatePropagation(); } }, true); })(); window.addEventListener('message', function(e) { alert('收到:' + e.data) }) postMessage('hello', '*'); Run |
提問(wèn):如何對(duì)所發(fā)現(xiàn)的缺陷進(jìn)行嚴(yán)密的等級(jí)劃分?
回答:測(cè)試過(guò)程中發(fā)現(xiàn)的缺陷一般分為如下幾類
功能問(wèn)題(FunctionError):對(duì)產(chǎn)品、項(xiàng)目質(zhì)量有影響,但尚難以確定是否是錯(cuò)誤,暫時(shí)無(wú)法解決
功能缺陷(FunctionDefect):不滿足用戶需求等bug的總稱
頁(yè)面缺陷(UIDefect):頁(yè)面美觀性、協(xié)調(diào)性、錯(cuò)別字等
建議類(Suggestion):對(duì)產(chǎn)品、項(xiàng)目的建議性意見(jiàn),不強(qiáng)制要求修改
硬件性能:進(jìn)行
性能測(cè)試時(shí)使用,暫定:網(wǎng)絡(luò)延時(shí)、內(nèi)存問(wèn)題、CPU占用、硬盤問(wèn)題
安全性問(wèn)題:進(jìn)行系統(tǒng)
安全測(cè)試時(shí)使用,暫不訂具體標(biāo)準(zhǔn)
業(yè)務(wù)流程問(wèn)題:進(jìn)行業(yè)務(wù)流程測(cè)試時(shí)進(jìn)行
模塊間接口問(wèn)題:涉及有模塊間數(shù)據(jù)傳遞時(shí)使用
其他(Other):其它
根據(jù)各類缺陷的嚴(yán)重程度將缺陷分為5個(gè)等級(jí),具體如下:
1)低(Low)
建議類錯(cuò)誤,對(duì)軟件的改進(jìn)意見(jiàn)或者建議。如:
a)功能建議
b)操作建議
c)校驗(yàn)建議
d)說(shuō)明建議
e)UI建議
2)中(Medium)
使操作者不合理或者不方便或操作遇到麻煩,但它不影響執(zhí)行
工作功能或重要功能,次要功能,對(duì)產(chǎn)品使用影響不大。如:
界面錯(cuò)誤
a)使操作者不方便或者遇到麻煩,但不影響執(zhí)行工作功能的實(shí)現(xiàn)
b)界面、控件的擺布、圖標(biāo)、輸入輸出不規(guī)范
提示類錯(cuò)誤
a)刪除操作未給出提示
b)長(zhǎng)時(shí)間操作未給出提示
c)提示窗口文字未采用行業(yè)術(shù)語(yǔ)
d)出錯(cuò)沒(méi)有提示
其他錯(cuò)誤
a)不符合編碼標(biāo)準(zhǔn)
b)輔助說(shuō)明描述不清楚、不規(guī)范
c)快捷鍵無(wú)效,快捷鍵錯(cuò)誤操作
d)打印內(nèi)容、格式錯(cuò)誤
3)高(High)
影響系統(tǒng)正常運(yùn)行的缺陷,主要功能出現(xiàn)錯(cuò)誤,影響到產(chǎn)品的使用。如:
數(shù)據(jù)庫(kù)缺陷:數(shù)據(jù)庫(kù)設(shè)計(jì)未達(dá)到第三范式的要求或需求規(guī)格說(shuō)明的格式水平
操作錯(cuò)誤:因錯(cuò)誤操作迫使程序中斷
功能錯(cuò)誤:
a)程序功能無(wú)法實(shí)現(xiàn)
b)程序功能實(shí)現(xiàn)錯(cuò)誤
其他錯(cuò)誤:
a)腳本錯(cuò)誤
b)軟件產(chǎn)品的編譯,打包,安裝,卸載錯(cuò)誤
4)非常高(Very High)
規(guī)定的功能沒(méi)有實(shí)現(xiàn)或不完整或產(chǎn)生錯(cuò)誤結(jié)果;設(shè)計(jì)不合理造成性能低下,影響系統(tǒng)的運(yùn)營(yíng);使系統(tǒng)不穩(wěn)定、或破壞數(shù)據(jù);而且是常規(guī)操作中經(jīng)常發(fā)生或非常規(guī)操作中不可避免的主要問(wèn)題,且沒(méi)有辦法更正(重新安裝或重新啟動(dòng)軟件不屬更正辦法),須盡快修正,如:
數(shù)據(jù)缺陷
a)數(shù)據(jù)計(jì)算錯(cuò)誤
b)數(shù)據(jù)約束錯(cuò)誤
c)數(shù)據(jù)輸入、輸出錯(cuò)誤
數(shù)據(jù)庫(kù)缺陷
a)數(shù)據(jù)庫(kù)發(fā)生死鎖
b)數(shù)據(jù)庫(kù)的表、業(yè)務(wù)規(guī)則、缺省值未加完整性等約束條件
c)數(shù)據(jù)庫(kù)連接錯(cuò)誤
d)數(shù)據(jù)通訊錯(cuò)誤
接口缺陷
a)程序接口錯(cuò)誤
b)硬件接口、通訊錯(cuò)誤
功能錯(cuò)誤:
a)程序功能無(wú)法實(shí)現(xiàn)
b)程序功能實(shí)現(xiàn)錯(cuò)誤
5)緊急(Critical)
不能執(zhí)行正常工作或重要功能,使系統(tǒng)崩潰或資源嚴(yán)重不足,數(shù)據(jù)丟失(金幣,包子)非常死機(jī)等導(dǎo)致系統(tǒng)不能繼續(xù)運(yùn)行須馬上修正,如:
a)由于程序所引起的死機(jī),非法退出
b)程序死循環(huán)
c)性能與需求不一致(壓力測(cè)試)
d)存在安全性與保密性問(wèn)題
e)文件打開(kāi)與保存錯(cuò)誤
總結(jié):
1級(jí)-建議問(wèn)題的軟件缺陷(Enhancemental):由問(wèn)題提出人對(duì)測(cè)試對(duì)象的改進(jìn)意見(jiàn)或測(cè)試人員提出的建議、質(zhì)疑。
2級(jí)—較小錯(cuò)誤的軟件缺陷(Minor),使操作者不方便或遇到麻煩,但它不影響功能過(guò)的操作和執(zhí)行,如錯(cuò)別字、界面不規(guī)范(字體大小不統(tǒng)一,文字排列不整齊,可輸入?yún)^(qū)域和只讀區(qū)域沒(méi)有明顯的區(qū)分標(biāo)志),輔助說(shuō)明描述不清楚。
3級(jí)—一般錯(cuò)誤的軟件缺陷(major):次要功能沒(méi)有完全實(shí)現(xiàn)但不影響使用。如提示信息不太準(zhǔn)確,或用戶界面差,操作時(shí)間長(zhǎng),模塊功能部分失效等,打印內(nèi)容、格式錯(cuò)誤,刪除操作未給出提示,數(shù)據(jù)庫(kù)表中有過(guò)多的空字段等。
4級(jí)—嚴(yán)重錯(cuò)誤的軟件缺陷(critical):系統(tǒng)的主要功能部分喪失、數(shù)據(jù)不能保存,系統(tǒng)的次要功能完全喪失。問(wèn)題局限在本模塊,導(dǎo)致模塊功能失效或異常退出。如致命的錯(cuò)誤聲明,程序接口錯(cuò)誤,數(shù)據(jù)庫(kù)的表、業(yè)務(wù)規(guī)則、缺省值未加完整性等約束條件
5級(jí)—致命的軟件缺陷(Fatal):造成系統(tǒng)或應(yīng)用程序崩潰、死機(jī)、系統(tǒng)掛起,或造成數(shù)據(jù)丟失,主要功能完全喪失,導(dǎo)致本模塊以及相關(guān)模塊異常等問(wèn)題。如代碼錯(cuò)誤,死循環(huán),數(shù)據(jù)庫(kù)發(fā)生死鎖、與數(shù)據(jù)庫(kù)連接錯(cuò)誤或數(shù)據(jù)通訊錯(cuò)誤,未考慮異常操作,功能錯(cuò)誤等。
原帖地址:http://bbs.51testing.com/thread-1000328-1-1.html
反射機(jī)制簡(jiǎn)介
反射機(jī)制應(yīng)用示例
簡(jiǎn)單的Ioc實(shí)現(xiàn)
代理模式
Java動(dòng)態(tài)代理
簡(jiǎn)單的Aop實(shí)現(xiàn)
“程序運(yùn)行時(shí),允許改變程序結(jié)構(gòu)或變量類型,這種語(yǔ)言稱為動(dòng)態(tài)語(yǔ)言”。從這個(gè)觀點(diǎn)看,Perl,
Python,
Ruby是動(dòng)態(tài)語(yǔ)言,C++,Java,C#不是動(dòng)態(tài)語(yǔ)言。
盡管在這樣得定義與分類下Java不是動(dòng)態(tài)語(yǔ)言,它卻有著一個(gè)非常突出的動(dòng)態(tài)相關(guān)的機(jī)制:反射機(jī)制 (Reflection)。
什么是反射?
反射的概念是由Smith在1982年首次提出的,主要是指程序可以訪問(wèn)、檢測(cè)和修改它本身狀態(tài)或者行為的一種能力。
JAVA反射機(jī)制都是在運(yùn)行狀態(tài)中,對(duì)于任意一個(gè)類,都能夠知道這個(gè)類的所有屬性和方法;對(duì)于任意一個(gè)對(duì)象,都能夠調(diào)用它的任意一個(gè)方法;這種動(dòng)態(tài)獲取的信息以及動(dòng)態(tài)調(diào)用對(duì)象的方法的功能稱為Java語(yǔ)言的反射機(jī)制。
JAVA反射機(jī)制(Reflection)
動(dòng)態(tài)獲取類的信息,以及動(dòng)態(tài)調(diào)用對(duì)象的方法的功能。
主要提供了以下功能:
在運(yùn)行時(shí)判斷任意一個(gè)對(duì)象所屬的類;
在運(yùn)行時(shí)構(gòu)造任意一個(gè)類的對(duì)象;
在運(yùn)行時(shí)判斷任意一個(gè)類所具有的成員變量和方法;
在運(yùn)行時(shí)調(diào)用任意一個(gè)對(duì)象的方法;
生成動(dòng)態(tài)代理。
JAVA反射機(jī)制包
在 JDK 中,主要由以下類來(lái)實(shí)現(xiàn) Java 反射機(jī)制,這些類都位于 java.lang.reflect包中。
Class 類:代表一個(gè)類。
Field 類:代表類的成員變量(成員變量也稱為類的屬性)。
Method 類:代表類的方法。
Constructor 類:代表類的構(gòu)造方法。
Array 類:提供了動(dòng)態(tài)創(chuàng)建數(shù)組,以及訪問(wèn)數(shù)組元素的靜態(tài)方法。
java.lang.Class
static Class forName(String className)
返回描述類名為className的Class對(duì)象
Object newInstance()
返回一個(gè)類的一個(gè)新實(shí)例
Field[] getFields()
返回包含F(xiàn)ield對(duì)象的數(shù)組,這些對(duì)象記錄了這個(gè)類或者其超類的公共域
Field[] getDeclaredField()
返回包含F(xiàn)ield對(duì)象的數(shù)組,這些對(duì)象記錄了這個(gè)類的全部域
Method[] getMethods() 返回這個(gè)類或者其超類所有的公有方法
Method[] getDeclareMethods() 返回這個(gè)類或者接口的所有方法,不包括超類繼承的方法
Constructor[] getConstructors() 返回所有包含了Class對(duì)象所描述類的公有構(gòu)造器
Constructor[] getDeclaredConstructors() 返回包含了Class對(duì)象所描述的類的所有構(gòu)造器
Java.lang.reflect.Constructor
Class[] getParameterTypes()
返回一個(gè)用于描述參數(shù)類型的Class對(duì)象數(shù)組
getReturnType()
返回一個(gè)用于描述返回類型的Class對(duì)象
int getModifiers()
返回一個(gè)用于描述方法拋出的異常類型的Class對(duì)象數(shù)組
Class getDeclaringClass()
返回一個(gè)用于描述類中定義的構(gòu)造器、方法或域的Class對(duì)象
通過(guò)反射實(shí)例化參數(shù)
平常實(shí)例化對(duì)象通常使用new關(guān)鍵字。但是使用new關(guān)鍵字實(shí)例化的對(duì)象具有強(qiáng)耦合性。New對(duì)象無(wú)法勝任未知對(duì)象的實(shí)例化,這時(shí)候只能通過(guò)反射動(dòng)態(tài)生成。例如Spring的DI。
實(shí)例化無(wú)參構(gòu)造函數(shù)的對(duì)象
Class.newInstance()
Class.getConstructor(new Class[]{}).newInstance(new Object[]{})
實(shí)例化帶參構(gòu)造函數(shù)的對(duì)象
Clazz.getConstructor(Class<?> ...ParameterType).newInstance(Object ...initargs)
反射機(jī)制舉例
import java.lang.reflect.*; public class DumpMethods { public static void main(String args[]) throws Exception{ //加載并初始化命令行參數(shù)指定的類 Class classType = Class.forName(args[0]); //獲得類的所有方法 Method methods[] = classType.getDeclaredMethods(); for(int i = 0; i < methods.length; i++) System.out.println(methods[i].toString()); } } |
輸入:java DumpMethods java.util.Stack public synchronized java.lang.Object java.util.Stack.pop() public java.lang.Object java.util.Stack.push(java.lang.Object) public boolean java.util.Stack.empty() public synchronized java.lang.Object java.util.Stack.peek() public synchronized int java.util.Stack.search(java.lang.Object) |
運(yùn)用反射機(jī)制調(diào)用方法
getMethod和invoke方法的時(shí)序圖

獲取反射對(duì)象
import java.lang.reflect.*; public class ReflectTester { public Object copy(Object object) throws Exception{ //獲得對(duì)象的類型 Class classType=object.getClass(); System.out.println("Class:"+classType.getName()); //通過(guò)默認(rèn)構(gòu)造方法創(chuàng)建一個(gè)新的對(duì)象 Object objectCopy=classType.getConstructor(new Class[]{}). newInstance(new Object[]{}); //獲得對(duì)象的所有屬性 Field fields[]=classType.getDeclaredFields(); for(int i=0; i<fields.length;i++){ Field field=fields[i]; String fieldName=field.getName(); String firstLetter=fieldName.substring(0,1).toUpperCase(); //獲得和屬性對(duì)應(yīng)的 getXXX()方法的名字 String getMethodName="get"+firstLetter+fieldName.substring(1); //獲得和屬性對(duì)應(yīng)的 setXXX()方法的名字 String setMethodName="set"+firstLetter+fieldName.substring(1); //獲得和屬性對(duì)應(yīng)的 getXXX()方法 Method getMethod=classType.getMethod(getMethodName,new Class[]{}); //獲得和屬性對(duì)應(yīng)的 setXXX()方法 Method setMethod=classType.getMethod(setMethodName,new Class[]{field.getType()}); //調(diào)用原對(duì)象的 getXXX()方法 Object value=getMethod.invoke(object,new Object[]{}); System.out.println(fieldName+":"+value); //調(diào)用復(fù)制對(duì)象的 setXXX()方法 setMethod.invoke(objectCopy,new Object[]{value}); } return objectCopy; } public static void main(String[] args) throws Exception{ Customer customer=new Customer("Tom",21); customer.setId(new Long(1)); Customer customerCopy=(Customer)new ReflectTester().copy(customer); System.out.println("Copy information:"+customerCopy.getName()+ “ "+ customerCopy.getAge()); } } |
運(yùn)用反射機(jī)制調(diào)用方法
class Customer{ //Customer 類是一個(gè) JavaBean private Long id; private String name; private int age; public Customer(){} public Customer(String name,int age){ this.name=name; this.age=age; } public Long getId(){return id;} public void setId(Long id){this.id=id;} public String getName(){return name;} public void setName(String name){this.name=name;} public int getAge(){return age;} public void setAge(int age){this.age=age;} } |
import java.lang.reflect.*; public class InvokeTester { public int add(int param1,int param2){ return param1+param2; } public String echo(String msg){ return "echo:"+msg; } public static void main(String[] args) throws Exception{ Class classType=InvokeTester.class; Object invokeTester=classType.newInstance(); //調(diào)用 InvokeTester 對(duì)象的 add()方法 Method addMethod=classType.getMethod("add",new Class[]{int.class,int.class}); Object result=addMethod.invoke(invokeTester, new Object[]{new Integer(100),new Integer(200)}); System.out.println((Integer)result); //調(diào)用 InvokeTester 對(duì)象的 echo()方法 Method echoMethod=classType.getMethod("echo",new Class[]{String.class}); result=echoMethod.invoke(invokeTester,new Object[]{"Hello"}); System.out.println((String)result); } } |
本文作者Alex已經(jīng)從事Java開(kāi)發(fā)15年了,最近幫助開(kāi)發(fā)了COBOL和Magik語(yǔ)言的JVM 。當(dāng)前,他正致力于Micro Focus的Java性能測(cè)試工具。在本文中,他闡述了在標(biāo)準(zhǔn)硬件中實(shí)現(xiàn)高速、多線程虛擬內(nèi)存的可能性及方案。原文內(nèi)容如下。
你想在標(biāo)準(zhǔn)硬件上運(yùn)行TB級(jí)甚至PB級(jí)內(nèi)存的JVM嗎?你想與內(nèi)存交互一樣讀寫文件,且無(wú)需關(guān)心文件的打開(kāi)、關(guān)閉、讀、寫嗎?
JVM的64位地址空間使這些成為可能。
首先,不要在觀念上將內(nèi)存和磁盤進(jìn)行區(qū)分,而是統(tǒng)一處理為內(nèi)存映射文件。在32位地址空間時(shí),內(nèi)存映射文件只是為了高速訪問(wèn)磁盤;因?yàn)槭芟抻谔摂M機(jī)的有限地址空間,并不支持大規(guī)模的虛擬內(nèi)存或大文件。如今JVM已經(jīng)發(fā)展為64位,而且可以在64位
操作系統(tǒng)上運(yùn)行。在一個(gè)進(jìn)程的地址空間中,內(nèi)存映射文件大小就可以達(dá)到TB甚至PB。
進(jìn)程無(wú)需關(guān)心內(nèi)存是在RAM或是磁盤上。操作系統(tǒng)會(huì)負(fù)責(zé)處理,而且處理得非常高效。
我們可以使用Java的MappedByteBuffer類訪問(wèn)內(nèi)存映射文件。該類的實(shí)例對(duì)象與普通的ByteBuffer一樣,但包含的內(nèi)存是虛擬的——可能是在磁盤上,也可能是在RAM中。但無(wú)論哪種方式,都是由操作系統(tǒng)負(fù)責(zé)處理。因?yàn)榈腂yteBuffer的大小上限是Intger.MAX_VALUE,我們需要若干個(gè)ByteBuffer來(lái)映射大量?jī)?nèi)存。在這個(gè)示例中,我映射了40GB。
這是因?yàn)槲业腗ac只有16GB內(nèi)存,下面代碼證明了這一點(diǎn)!
public MapperCore(String prefix, long size) throws IOException { coreFile = new File(prefix + getUniqueId() + ".mem"); // This is a for testing - to avoid the disk filling up coreFile.deleteOnExit(); // Now create the actual file coreFileAccessor = new RandomAccessFile(coreFile, "rw"); FileChannel channelMapper = coreFileAccessor.getChannel(); long nChunks = size / TWOGIG; if (nChunks > Integer.MAX_VALUE) throw new ArithmeticException("Requested File Size Too Large"); length = size; long countDown = size; long from = 0; while (countDown > 0) { long len = Math.min(TWOGIG, countDown); ByteBuffer chunk = channelMapper.map(MapMode.READ_WRITE, from, len); chunks.add(chunk); from += len; countDown -= len; } } |
上面的代碼在虛擬內(nèi)存創(chuàng)建了40GB MappedByteBuffer對(duì)象列表。讀取和寫入時(shí)只需要注意處理兩個(gè)內(nèi)存模塊的跨越訪問(wèn)。完整代碼的可以在這里找到。
線程
一個(gè)極其強(qiáng)大且簡(jiǎn)單易用的方法就是線程。但是普通的Java IO簡(jiǎn)直就是線程的噩夢(mèng)。兩個(gè)線程無(wú)法在不引起沖突的情況下同時(shí)訪問(wèn)相同的數(shù)據(jù)流或RandomAccessFile 。雖然可以使用非阻塞IO,但是這樣做會(huì)增加代碼的復(fù)雜性并對(duì)原有的代碼造成侵入。
與其他的內(nèi)存線程一樣,內(nèi)存映射文件也是由操作系統(tǒng)來(lái)處理??梢愿鶕?jù)讀寫需要,在同一時(shí)刻盡可能多的使用線程。我的測(cè)試代碼有128個(gè)線程,而且工作得很好(雖然機(jī)器發(fā)熱比較大)。唯一重要的技巧是復(fù)用MappedByteBuffer對(duì)象,避免自身位置狀態(tài)引發(fā)問(wèn)題。
現(xiàn)在可以執(zhí)行下面的測(cè)試:
@Test public void readWriteCycleThreaded() throws IOException { final MapperCore mapper = new MapperCore("/tmp/MemoryMap", BIG_SIZE); final AtomicInteger fails = new AtomicInteger(); final AtomicInteger done = new AtomicInteger(); Runnable r = new Runnable() { public void run() { try { // Set to 0 for sequential test long off = (long) ((BIG_SIZE - 1024) * Math.random()); System.out.println("Running new thread"); byte[] bOut = new byte[1024]; double counts = 10000000; for (long i = 0; i < counts; ++i) { ByteBuffer buf = ByteBuffer.wrap(bOut); long pos = (long) (((BIG_SIZE - 1024) * (i / counts)) + off) % (BIG_SIZE - 1024); // Align with 8 byte boundary pos = pos / 8; pos = pos * 8; for (int j = 0; j < 128; ++j) { buf.putLong(pos + j * 8); } mapper.put(pos, bOut); byte[] bIn = mapper.get(pos, 1024); buf = ByteBuffer.wrap(bIn); for (int j = 0; j < 128; ++j) { long val = buf.getLong(); if (val != pos + j * 8) { throw new RuntimeException("Error at " + (pos + j * 8) + " was " + val); } } } System.out.println("Thread Complete"); } catch (Throwable e) { e.printStackTrace(); fails.incrementAndGet(); } finally { done.incrementAndGet(); } } }; int nThreads = 128; for (int i = 0; i < nThreads; ++i) { new Thread(r).start(); } while (done.intValue() != nThreads) { try { Thread.sleep(1000); } catch (InterruptedException e) { // ignore } } if (fails.intValue() != 0) { throw new RuntimeException("It failed " + fails.intValue()); } } |
我曾嘗試進(jìn)行其他形式的IO,但是只要像上面那樣運(yùn)行128個(gè)線程,性能都不如上面的方法。我在四核、超線程I7 Retina MacBook Pro上嘗試過(guò)。代碼運(yùn)行時(shí)會(huì)啟動(dòng)128個(gè)線程,超出CPU的最大負(fù)載(800%),直到操作系統(tǒng)檢測(cè)到該進(jìn)程的內(nèi)存不足。在這個(gè)時(shí)候,系統(tǒng)開(kāi)始對(duì)內(nèi)存映射文件的讀寫進(jìn)行分頁(yè)。為實(shí)現(xiàn)這一目標(biāo),內(nèi)核會(huì)占用一定的CPU,Java進(jìn)程的性能會(huì)下降到650~750%。Java無(wú)需關(guān)心讀取、寫入、同步或類似的東西——操作系統(tǒng)會(huì)負(fù)責(zé)處理。
結(jié)果會(huì)有所不同
如果讀取和寫入點(diǎn)不是連續(xù)而是隨機(jī)的,性能下降有所區(qū)別(帶有交換時(shí)會(huì)達(dá)到750%,否則會(huì)達(dá)到250%)。我相信這種方式可能更適合處理少量的大數(shù)據(jù)對(duì)象,而不適用于大量的小數(shù)據(jù)對(duì)象。對(duì)于后者,可能的處理辦法是預(yù)先將大量小數(shù)據(jù)對(duì)象加載到緩存中,再將其映射到虛擬內(nèi)存。
應(yīng)用程序
到目前為止,我使用的技術(shù)都是虛擬內(nèi)存系統(tǒng)。在示例中,一旦與虛擬內(nèi)存交互完成,就會(huì)刪除底層文件。但是,這種方法可以很容易地進(jìn)行數(shù)據(jù)持久化。
例如,視頻編輯是一個(gè)非常具有挑戰(zhàn)性的工程問(wèn)題。一般來(lái)說(shuō),有兩個(gè)有效的方法:無(wú)損耗存儲(chǔ)整個(gè)視頻,并編輯存儲(chǔ)的信息;或根據(jù)需要重新生成視頻。因?yàn)镽AM的制約,后一種方法越來(lái)越普遍。然而,視頻是線性的——這是一種理想的數(shù)據(jù)類型,可用來(lái)存儲(chǔ)非常大的映射虛擬內(nèi)存。由于在視頻算法上取得的進(jìn)步,可以將它作為原始字節(jié)數(shù)組訪問(wèn)。操作系統(tǒng)會(huì)根據(jù)需要將磁盤到虛擬內(nèi)存的緩沖區(qū)進(jìn)行分頁(yè)處理。
另一個(gè)同樣有效的應(yīng)用場(chǎng)景是替代文檔服務(wù)中過(guò)度設(shè)計(jì)的RAM緩存解決方案。想想看,我們有一個(gè)幾TB的中等規(guī)模的文檔庫(kù)。它可能包含圖片、短片和PDF文件。有一種常見(jiàn)的快速訪問(wèn)磁盤的方法,使用文件的RAM緩存弱引用或軟引用。但是,這會(huì)對(duì)JVM垃圾收集器產(chǎn)生重大影響,并且增加操作難度。如果將整個(gè)文檔映射到虛擬內(nèi)存,可以更加簡(jiǎn)單地完成同樣的工作。操作系統(tǒng)會(huì)根據(jù)需要將數(shù)據(jù)讀入內(nèi)存。更重要的是,操作系統(tǒng)將盡量保持RAM中最近被訪問(wèn)的內(nèi)存頁(yè)。這意味著內(nèi)存映射文件就像RAM緩存一樣,不會(huì)對(duì)Java或JVM垃圾收集器產(chǎn)生任何影響。
最后,內(nèi)存映射文件在科學(xué)計(jì)算和建模等應(yīng)用中非常有效。在用來(lái)處理代表真實(shí)世界系統(tǒng)的計(jì)算模型時(shí),經(jīng)常需要大量的數(shù)據(jù)才能正常工作。在我的音頻處理系統(tǒng)Sonic Field中,通過(guò)混合和處理單一聲波,可以模擬真實(shí)世界中的音頻效果。例如,創(chuàng)建原始音頻副本是為模擬從硬表面反射的聲波,并將反射回來(lái)的聲波與原聲波混合。這種方法需要大量的存儲(chǔ)空間,這時(shí)就可以把音頻信號(hào)放在虛擬內(nèi)存中(也是這項(xiàng)工作的最初動(dòng)機(jī))。
讀取一致性
· 強(qiáng)一致性
在任何時(shí)間訪問(wèn)集群中任一結(jié)點(diǎn),得到的數(shù)據(jù)結(jié)果一致;
· 用戶一致性
對(duì)同一用戶,訪問(wèn)集群期間得到的數(shù)據(jù)一致;
解決用戶一致性:使用粘性會(huì)話,將會(huì)話綁定到特定結(jié)點(diǎn)來(lái)處理;
這樣會(huì)降低負(fù)載均衡器的性能;
· 最終一致性
集群中各結(jié)點(diǎn)間由于數(shù)據(jù)同步不及時(shí)造成暫時(shí)的數(shù)據(jù)不一致,但數(shù)據(jù)同步完成后,最終具有一致性;
更新一致性
· 悲觀方式
使用寫鎖
大幅降低系統(tǒng)響應(yīng)能力
可能導(dǎo)致死鎖
· 樂(lè)觀方式
先讓沖突發(fā)生,再檢測(cè)順序
自動(dòng)合并的處理方式極具“領(lǐng)域特定”問(wèn)題
放寬“一致性約束”
· CAP定理
一致性(Consistency)、可用性(Availability)和分區(qū)耐受性(Partition tolerance),3個(gè)屬性只可能同時(shí)滿足2個(gè);
分區(qū)耐受性的解釋:集群因通信故障而劃分為多個(gè)時(shí)仍然可用
· CA系統(tǒng)
單服務(wù)器
集群中出現(xiàn)”分區(qū)“,就不可用
· PA/PC
集群出現(xiàn)”分區(qū)“時(shí),需要在”一致性“ 和“可用性”間權(quán)衡
一般會(huì)犧牲部分一致性(eg:使用最終一致性),保證可用性
放寬“持久性”約束
更嚴(yán)格的持久性,意味著更多的性能損失;
· 犧牲“持久性”換取更好的性能
· 復(fù)制“持久性”故障
主節(jié)點(diǎn)故障,未同步到從節(jié)點(diǎn)的數(shù)據(jù)丟失
主節(jié)點(diǎn)恢復(fù),故障期間更新的數(shù)據(jù)沖突
解決方案:針對(duì)單個(gè)請(qǐng)求指定其所需的持久性
附思維導(dǎo)圖
下面介紹一些開(kāi)發(fā)者在
數(shù)據(jù)庫(kù)操作中要注意的
SQL編碼準(zhǔn)則。雖然本文不能覆蓋所有的準(zhǔn)則,但還是希望能給開(kāi)發(fā)者帶來(lái)些許幫助。下面就來(lái)看看在編碼實(shí)踐中哪些應(yīng)該做,哪些不應(yīng)該做。
1. 在長(zhǎng)時(shí)間運(yùn)行的查詢和短查詢中使用事務(wù)
如果預(yù)期有一個(gè)長(zhǎng)時(shí)間運(yùn)行的查詢,并且有大量的數(shù)據(jù)輸出時(shí),開(kāi)發(fā)者就應(yīng)該在BEGIN TRAN 和END TRAN之間使用事務(wù)。
這樣事務(wù)會(huì)在緩沖區(qū)緩存為獨(dú)立事務(wù),并會(huì)被分配特定內(nèi)存,以此來(lái)提高處理速度。
2. 不要使用SELECT *
如果使用SELECT * 來(lái)選擇表中的所有記錄,那么一些不必要的記錄也被讀取、緩存,增加了磁盤的I/O和內(nèi)存消耗。
3. 避免在WHERE子句中使用顯式或隱式函數(shù),比如Convert ()
4. 避免在觸發(fā)器中執(zhí)行長(zhǎng)時(shí)間的操作
5. 適當(dāng)使用臨時(shí)表和表變量
當(dāng)結(jié)果集較小的時(shí)候,請(qǐng)盡量使用表變量;當(dāng)結(jié)果集相當(dāng)大時(shí),使用臨時(shí)表。
6. 使用連接(JOIN)代替子查詢(Sub-Queries)
子查詢通常作為內(nèi)聯(lián)代碼來(lái)使用,而連接(JOIN)則作為表來(lái)使用,這樣速度會(huì)更快。所以,應(yīng)盡量避免在連接中使用子查詢。
7. 連接條件中表的順序
在連接條件中,應(yīng)盡量首先使用較小的表,然后逐步使用較大的表。
8. 循環(huán)優(yōu)化
如果操作在循環(huán)內(nèi)部沒(méi)有任何影響,那么應(yīng)盡量將操作放到循環(huán)外面,這樣可以減少不必要的重復(fù)
工作。因?yàn)?,SQL Server優(yōu)化器不會(huì)自動(dòng)識(shí)別這種低效率的代碼,更不會(huì)自動(dòng)優(yōu)化(其他一些語(yǔ)言的編譯器可以)。
9. 參數(shù)探測(cè)
不要在正執(zhí)行的SP(存儲(chǔ)過(guò)程)中使用SP參數(shù),這樣會(huì)導(dǎo)致參數(shù)探測(cè)(Parameter Sniffing)。應(yīng)該在聲明和設(shè)置后再使用SP參數(shù)。由于這個(gè)原因,SP的行為在每次運(yùn)行期間都不相同。
10. 當(dāng)使用條件語(yǔ)句時(shí),可以使用Index(索引)Hint(提示)
比如在SQL Server 2008中,可以使用Index hint,也可以使用fixed plan hint強(qiáng)制在查詢中使用hint,以提高運(yùn)行速度。
11. 在聲明中明確指定存儲(chǔ)過(guò)程中數(shù)據(jù)類型的大小
開(kāi)發(fā)者隨機(jī)聲明數(shù)據(jù)類型的大小是不可取的,如:Varchar (500)。這在執(zhí)行時(shí)會(huì)在緩沖區(qū)中增加不必要的預(yù)留空間。
12. 在查詢中有效利用MAXDOP(最大并行度)設(shè)置
詢問(wèn)數(shù)據(jù)庫(kù)管理員關(guān)于四核CPU可用性的設(shè)置,包括內(nèi)存的設(shè)置,然后適當(dāng)使用hint,可以有效改善查詢速度。
13. SQL Server 2008中的GROUPING SETS
如果數(shù)據(jù)庫(kù)服務(wù)器為SQL Server 2008,那么可以在所有的Unions中使用Grouping Set來(lái)代替Group By。這樣在Union中重新進(jìn)行g(shù)roup by排序時(shí),優(yōu)化器不會(huì)每次都制定一個(gè)計(jì)劃。
14. 當(dāng)發(fā)生死鎖時(shí),總是使用With (nolock) 和With (rowlock)