因為做效能測試分析的人來說,HTTP 協議可能是繞不過去的一個檻。在講 HTTP 之前,我們得先知道一些基本的資訊。
HTTP(HyperText Transfer Protocol,超文字傳輸協議),顯然是規定了傳輸的規則,但是它並沒有規定內容的規則。
HTML(HyperText Marked Language,超文字標記語言),規定的是內容的規則。瀏覽器之所以能認識傳輸過來的資料,都是因為瀏覽器具有相同的解析規則。
我們首先關注一下 HTTP 互動的大體內容。想了很久,畫了這麼一張圖,我覺得它展示了我對 HTTP 協議在互動過程上的理解。
在這張圖中,可以看到這些資訊:
1、在互動過程中,資料經過了 Frame、Ethernet、IP、TCP、HTTP 這些層面。不管是傳送和接收端,都必須經過這些層。這就意味著,任何每一層出現問題,都會影響 HTTP 傳輸。
2、在每次傳輸中,每一層都會加上自己的頭資訊。這一點要說重要也重要,說不重要也不重要。重要是因為如果這些頭出了問題,非常難定位(在我之前的一個專案中,就曾經出現過 TCP 包頭的一個 option 因為 BUG 產生了變化,查了兩個星期,一層層抓包,最後才找到原因)。不重要是因為它們基本上不會出什麼問題。
3、HTTP 是請求 - 應答的模式。就是說,有請求,就要有應答。沒有應答就是有問題。
4、客戶端接收到所有的內容之後,還要展示。而這個展示的動作,也就是前端的動作。在當前主流的效能測試工具中,都是不模擬前端時間的,比如說 JMeter。我們在執行結束後只能看到結果,但是不會有響應的資訊。你也可以選擇儲存響應資訊,但這會導致壓力機工作負載高,壓力基本上也上不去。也正是因為不存這些內容,才讓一臺機器模擬成千上百的客戶端有了可能。
如果你希望能理解這些層的頭都是什麼,可以直接抓包來看,比如如下示圖:
從這個圖中,我們就能看到各層的內容都是什麼。
在我看來,只有實踐的操作和理論的結合,才能真正的融會貫通。只講壓力工具而不講原理,是不可能學會處理複雜問題的;空有理論沒有動手能力是不可能解決實際問題的。由於壓力工具並不處理客戶端頁面解析、渲染等動作,所以,以下內容都是從協議層出發的,不包括前端頁面層的部分。
JMeter 指令碼
在這裡只解釋幾個重要資訊:
第一個就是 Protocol。
這個當然重要。從“HTTP”這幾個字元中,我們就能知道這個協議有什麼特點。 HTTP 的特點是建立在 TCP 之上、無連線(TCP 就是它的連線)、無狀態(後來加了 Cookies、Session 技術,用 KeepAlive 來維持,也算是有狀態吧)、請求 - 響應模式等。
第二個是 Method 的選項 GET。
HTTP 中有多少個 Method 呢?我在這裡做個說明。在 RFC 中的 HTTP 相關的定義中(比如 RFC2616、2068),定義了 HTTP 的方法,如下:GET、POST、PUT、PATCH、DELETE、COPY、HEAD、OPTIONS、LINK、UNLINK、PURGE。
GET 方法是怎麼工作的呢?
GET 可以得到由 URI 請求(定義)的各種資訊。同樣的,其他方法也有清楚的規定。我們要注意的是,HTTP 只規定了你要如何互動。它是互動的協議,就是兩個人對話,如何能傳遞過去?小時候一個人手上拿個紙杯子,中間有根線,相互說話能聽到,這就是協議。
第三個是 Path,也就是請求的路徑。
這個路徑是在哪裡規定的呢?在我這個 Spring Boot 的示例中。
@RequestMapping(value = "pabcd") public class PABCDController { @Autowired private PABCDService pabcdService; @Autowired private PABCDRedisService pabcdRedisService; @Autowired private PABCDRedisMqService pabcdRedisMqService; @GetMapping("/redis_mq/query/{id}") public ResultVO<User> getRedisMqById(@PathVariable("id") String id) { User user = pabcdRedisMqService.getById(id); return ResultVO.<User>builder().success(user).build(); }
看到了吧。因為我們定義了 request 的路徑,所以,我們必須在 Path 中寫上/pabcd/redis_mq/query這樣的路徑。
第四個是 Redirect,重定向。HTTP 3XX 的程式碼都和重定向有關,從示意上來看,如下所示。
使用者發了個 URL A 到服務 A 上,服務 A 返回了 HTTP 程式碼 302 和 URL B。 這時使用者看到了接著訪問 URL B,得到了服務 B 的響應。對於 JMeter 來說,它可以處理這種重定向。
第五個是 Content-Encoding,內容編碼。
它是在 HTTP 的標準中對服務端和客戶端之間處理內容做的一種約定。當大家都用相同的編碼時,相互都認識,或者有一端可以根據對端的編碼進行適配解釋,否則就會出現亂碼的情況。
預設是 UTF8。但是我們經常會碰到這種情況。當我們傳送中文字元的時候。比如下面的名字。
當我們傳送出去時,會看到它變成了這種編碼。如下圖所示:
如果服務端不去處理,顯然互動就錯了。如下圖所示:
這時,只能把配置改為如下:
我們這裡用 GBK 來處理中文。就會得到正確的結果。
你就會發現現在用了正常的中文字元。在這個例子,有人選擇用 URL 編碼來去處理,會發現處理不了。這是需要注意的地方。
第六個是超時設定。在 HTTP 協議中,規定了幾種超時時間,分別是連線超時、閘道器超時、響應超時等。
如下所示,JMeter 中可以設定連線和響應超時:
在工具中,我們可以定義連線和響應的超時時間。但通常情況下,我們不用做這樣的規定,只要跟著服務端的超時走就行了。但在有些場景中,不止是應用伺服器有超時時間,網路也會有延遲,這些會影響我們的響應時間。如果 HTTP 預設的 120s 超時時間不夠,我們可以將這裡放大。
在這裡為了演示,我將它設定為 100ms。我們來看一下執行的結果是什麼樣。
從棧的資訊上就可以看到,在讀資料的時候,超時了。
超時的設定是為了保證資料可以正常地傳送到客戶端。做效能分析的時候,經常有人聽到“超時”這個詞就覺得是系統慢導致的,其實有時也是因為配置。
通常,我們會對系統的超時做梳理,每個服務應該是什麼樣的超時設定,我們要有全域性的考量。比如說:
超時應該是逐漸放大的(不管你後面用的是什麼協議,超時都應該是這個樣子)。而我們現在的系統,經常是所有的服務超時都設定得一樣大,或者都是跟著協議的預設超時來。
在壓力小的時候,還沒有什麼問題,但是在壓力大的時候,就會發現系統因為超時設定不合理而導致業務錯誤。
如果倒過來的話,你可以想像,使用者都返回超時報錯了,後端還在處理著呢,那就更有問題了。
而我們效能測試人員,都是在壓力工具中看到的超時錯誤。如果後端的系統鏈路比較長,就需要一層層地往後端去查詢,看具體是哪個服務有問題。所以在架構層級來分析超時是非常有必要的。
第七個,在上圖中,還有一個引數是客戶端實現(Client Implementation)。其中有三個選項:空值、HTTPClient4、Java。
官方給出如下的解釋。
JAVA: 使用 JVM 提供的 HTTP 實現,相比 HttpClient 實現來說,這個實現有一些限制,這個限制我會在後面提到。
HTTPClient4:使用 Apache 的 HTTP 元件 HttpClient 4.x 實現。
空值:如果為空,則依賴 HTTP Request 預設方法,或在jmeter.properties檔案中的jmeter.httpsample定義的值。
用 JAVA 實現可能會有如下限制。
- 在連線複用上沒有任何控制。就是當一個連線已經釋放之後,同一個執行緒有可能複用這個已經釋放掉的連線。
- API 最適用於單執行緒,但是很多設定都是依賴系統屬性值的,所以都應用到所有連線上了。
- 不支援 Kerberos Authentication(這是一種計算機網路授權協議,用在非安全網路中,對個人通訊以安全的手段進行身份認證)。
- 不支援通過 keystore 配置的客戶端證照。
- 更容易控制重試機制。
- 不支援 Virtual hosts。
- 只支援這些方法: GET、POST、HEAD、OPTIONS、PUT、DELETE 和 TRACE。
- 使用 DNS Cache Manager 更容易控制 DNS 快取。
第八個就是 HTTP 層的壓縮。
我們經常會聽到在效能測試過程中,因為沒有壓縮,導致網路頻寬不夠的事情。當我們截獲一個 HTTP 請求時,你會看到如下內容。
這就是有壓縮的情況。在我們常用的 Nginx 中,會用如下常見配置來支援壓縮:
gzip on; #開啟gzip gzip_min_length 2k; #低於2kb的資源不用壓縮 gzip_comp_level 4; #壓縮級別【1-9】值越大,壓縮率就越高,但是CPU消耗也越多,根據我們在網上看到建議,大部分都是建議設定為中間4、5之類的,這裡我建議大家根據自己的專案實際情況,在壓力測試之後給出適合的值。 gzip_types text/plain application/javascript; #設定壓縮型別 gzip_disable "MSIE [1-6]\."; # 禁用gzip的條件,支援正則
在 RFC2616 中,Content Codings 部分定義了壓縮的格式 gzip 和 Deflate,不過我們現在看到的大部分都是 gzip。不過在壓縮這件事情上,我們在壓力工具中並不需要做什麼太多的動作,最多也就是加個頭。
第九個就是併發。
在 RFC2616 中的 8.1.1 節明確說明了為什麼要限制瀏覽器的併發。大概翻譯如下,有興趣的去讀下原文:
- 少開 TCP 連結,可以節省路由和主機(客戶端、服務端、代理、閘道器、通道、快取)的 CPU 資源和記憶體資源。
- HTTP 請求和響應可以通過 Pipelining 在一個連線上傳送。Pipelining 允許客戶端發出多個請求而不用等待每個返回,一個 TCP 連線更為高效。
- 通過減少開啟的 TCP 來減少網路擁堵,也讓 TCP 有充足的時間解決擁堵。
- 後續請求不用在 TCP 三次握手上再花時間,延遲降低。
- 因為報告錯誤時,沒有關閉 TCP 連線的懲罰,而使 HTTP 可以升級得更為優雅(原文使用 gracefully)。
- 如果不限制的話,一個客戶端發出很多個連結到伺服器,伺服器的資源可以同時服務的客戶端就會減少。
HTTPS 只是加了一個 S,就在訪問中加了一層。
因為證照是個非常標準的產品,加在中間,就是加密演算法和位數也會對效能產生影響。如果執行場景時報:javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake,就應該把證照也載入進來。
其實對我們做效能測試的人來說,無需關心 HTTP 的內容,我們只要關心資料的流向和處理的邏輯就可以了。
從效能測試的角度來看,如果你要模擬頁面請求,最多也就是正常實現 HTTP 的方法 GET、POST 之類的。
它傳送和接收的內容,只要符合業務系統的正常流程就可以,這樣業務才能正常執行。
比如說,前面提到的 POST 請求。如果我們傳送了一段 JSON。內容如下:
{ "userNumber": "${Counter}", "userName": "Zee_${Counter}", "orgId": null, "email": "test${Counter}@dunshan.com", "mobile": "18611865555" }
程式碼中的 Service 負責接收 User 物件,同時轉換它的是如下程式碼:
@Override public String toString() { return "User{" + "id='" + id + '\'' + ", userNumber='" + userNumber + '\'' + ", userName='" + userName + '\'' + ", orgId='" + orgId + '\'' + ", email='" + email + '\'' + ", mobile='" + mobile + '\'' + ", createTime=" + createTime + '}'; }
然後通過 Service 的 add 方法 insert 到資料庫中,這裡後面使用的 MyBatis:
Boolean result = paRedisService.add(user);
而這些,都屬於業務邏輯處理的部分,我們分析時把這個鏈路都想清楚才可以一層層剝離。
總結
對於 HTTP 協議來說,我們在效能分析中,主要關心的部分就是傳輸位元組的大小、超時的設定以及壓縮等內容。
在編寫指令碼的時候,要注意 HTTP 頭部,至於 Body 的內容,只要能讓業務跑起來即可。