服務端效能測試 - 入門指南 (慎入: 6000 字長文)

在路上發表於2020-12-31

原文見:在路上的部落格

服務端效能測試系列下一篇:工具篇 (Jmeter),從效能測試常用功能角度,熟悉 Jemter。

從 14 年 11 月到 18 年 6 月,一直專注於服務端效能測試,發現有些同學經常對一些基礎概念和指標有異議,故寫本文,希望對大家認識效能測試有一定幫助。也歡迎大家多多指正。

因效能測試知識跟自己的從業經歷強相關,但效能知識範圍甚廣,不同的業務、不同的技術架構,我們的關注點和指標要求可能會區別較大,歡迎大家指正文中不足或錯誤之處,並附上相關資料連結,方便大家傳閱。

全文包括:

  • 什麼是效能
  • 效能對使用者和產品收入影響
  • 效能測試目的效能關注指標效能測試型別效能測試流程
  • 常用的效能測試工具對比

1、前言

隨著 5G 時代的到來,以及萬物互聯時代的到來,雲應用和雲服務會越來越多,資料量會指數級增長。尤其是 2020 年全球疫情的時代意義,會導致各行各業開始上雲。從而會催生出極具個性化的各類產品的誕生。

所有行業的生態會像鯨落效應一樣,圍繞若干個巨無霸公司衍生出滿足人們各種需求的中小型產品。大部分產品的形態可能會變成重服務端、輕客戶端。

所以,服務端效能測試的需求也有可能會出現井噴式增長。但是服務端效能測試需求對於中小型公司,尤其是大部分不關注使用者體驗的公司來說,效能測試需求特點是週期短、時間緊。

因此,對於大部分測試從業人員來說,瞭解並掌握常見的效能測試知識是必不可少的,雖然不會經常用到。

2、什麼是效能

不同角色關注的效能是不一樣的,效能測試這項系統工程,需要各角色在其關注的緯度提供資訊或幫助。

使用者眼中的效能

效能對於使用者來說,就是操作的響應速度、產品的崩潰是否影響自己的生活。比如滴滴之前在情人節當天叫車服務的效能事故。

老闆眼中的效能

老闆主要關心產品的收入、成本(用了多少錢服務了多少使用者)、使用者滿意度(使用者對產品是否滿意)。

運維眼中的效能

開發眼中的效能

測試眼中的效能

3、效能的影響

3.1 效能對使用者的影響

對於大部分商業公司的 ToC 產品,效能關乎產品命運和增長。如下圖所示,雖然現在阿里京東壟斷不太容易更換應用,但是遇到效能很差的時候,心裡還是忍不住問候幾句。

3.2 效能對收入的影響

大家都知道效能對產品收入有非常大的影響,但是很少公司有全面的運營分析來證明這件事。
以下是《2016 全球零售業數字化效能基準報告》中關於效能對收入影響的資料。
從圖中可以看到,響應時間對轉化率的影響非常大,比如沃爾瑪夠硬核了吧,沃爾瑪的響應時間降低 0.1 秒,收入即可增加 1%,是非常大的收入提升。

4、效能的組成

以中小型電商網站為例,如下圖所示,效能基本組成:

  • 客戶端 (Web、移動端、小程式) 效能
  • DNS 效能
  • 負載均衡服務效能
  • Nginx 叢集效能、折損率
  • CDN 快取效能(回源率、穿透率)
  • 應用伺服器效能
  • DB 效能(Mysql/Redis/Memcache)

由此可見,中大型專案的效能測試,從來都是一項系統工程,需要非常多人跨部門合作,且持續時間長,耗費資源大。

5、效能測試基礎知識和注意事項

熟悉效能測試之前,首先了解效能測試的目標是什麼。帶著目標去思考會更有利於理解下面的內容。

5.1 效能測試目的

效能測試的最終目的是為了最大限度的滿足使用者的需求,通常要達成以下目標:
(1)效能評估:測試中評估系統的 QPS、響應時間、成功率等;
(2)尋找系統瓶頸,進行系統調優;
(3)檢測軟體中的問題;
(4)驗證穩定性和可靠性;

5.2 效能應該關注的指標

一般來說,效能測試要統一考慮這麼幾個因素:Thoughput 吞吐量,Latency 響應時間,資源利用(CPU/MEM/IO/Bandwidth…),成功率,系統穩定性。

(1)響應時間:你得定義一個系統的響應時間 latency,建議是 TP95 或以上。響應時間具體要求多少,一般讀不超過 200ms,寫不超過 500ms。要是實在不知道,對標同行業競品。
(2)最高吞吐量:TPS(每秒事務請求數)或 QPS(每秒請求量),在目標響應時間要求下,系統可支撐的最高吞吐量。
(3)成功率:在關注 QPS 和響應時間的同時,還要關注成功率。如果 QPS 和響應時間都滿足效能要求時,請求成功率只有 50%,使用者也是不會接受的。
(4)效能拐點:一般服務都有效能臨界點。當超過臨界點時,吞吐量非線性下降,響應時間指數級增加,成功率降低。
找到出現效能拐點的主要原因:
基於效能拐點主要原因設定高危效能報警線。此為高風險注意事項,因為一旦達到效能拐點,有可能會出現雪崩現象,造成極其嚴重的事故。
觀察超過效能拐點後,系統是否會出現假死、崩潰等高風險事件。
(5)系統穩定性:保持最高吞吐量(目標響應時間下的最高吞吐量),持續執行 7*24 小時。然後收集 CPU,記憶體,硬碟/網路 IO,等指標,檢視系統是否穩定,比如,CPU 是平穩的,記憶體使用也是平穩的。那麼,這個值就是系統的效能。
(6)極限吞吐量:階梯式增加併發壓力,找到系統的極限值。比如:在成功率 100% 的情況下(不考慮響應時間的長短),系統能堅持 10 分鐘的吞吐量。
(7)系統健壯性:做 Burst Test。用第二步得到的吞吐量執行 5 分鐘,然後在第四步得到的極限值執行 1 分鐘,再回到第二步的吞吐量執行 5 鍾,再到第四步的許可權值執行 1 分鐘,如此往復個一段時間,比如 2 天。收集系統資料:CPU、記憶體、硬碟/網路 IO 等,觀察他們的曲線,以及相應的響應時間,確保系統是穩定的。
(8)低吞吐量和網路小包的測試:有時候,在低吞吐量的時候,可能會導致 latency 上升,比如 TCP_NODELAY 的引數沒有開啟會導致 latency 上升(詳見 TCP 的那些事),而網路小包會導致頻寬用不滿也會導致效能上不去,所以,效能測試還需要根據實際情況有選擇的測試一下這兩咱場景。

5.3 效能測試型別

首先簡單分析效能測試的壓力模型。
如下圖所示,隨著單位時間壓力的不斷增長,被測系統和伺服器的壓力不斷增加,TPS 會因為這些因素而發生變化,而且通常符合一下規律。

為得到效能關注的指標,基本分為以下效能測試型別:

  • 效能測試(狹義)

    • 說明:效能測試方法是透過模擬生產執行的業務壓力量和使用場景組合,測試系統的效能是否滿足生產效能要求。通俗地說,這種方法就是要在特定的執行條件下驗證系統的能力狀態。
    • 特點: 1、這種方法的主要目的是驗證系統是否有系統宣稱具有的能力。 2、這種方法要事先了解被測試系統經典場景,並具有確定的效能目標。 3、這種方法要求在已經確定的環境下執行。 也就是說,這種方法是對系統效能已經有了解的前提,並對需求有明確的目標,並在已經確定的環境下進行的。
  • 負載測試 (Load Test)

    • 說明:透過在被測系統上不斷加壓,直到效能指標達到極限(例如 “響應時間”)超過預定指標或都某種資源已經達到飽和狀態。  
    • 特點: 1、這種效能測試方法的主要目的是找到系統處理能力的極限。 2、這種效能測試方法需要在給定的測試環境下進行,通常也需要考慮被測試系統的業務壓力量和典型場景、使得測試結果具有業務上的意義。 3、這種效能測試方法一般用來了解系統的效能容量,或是配合效能調優來使用。 也就是說,這種方法是對一個系統持續不段的加壓,看你在什麼時候已經超出 “我的要求” 或系統崩潰。
  • 壓力測試(強度測試)(Stress Test)

    • 說明:壓力測試方法測試系統在一定飽和狀態下,例如 cpu、記憶體在飽和使用情況下,系統能夠處理的會話能力,以及系統是否會出現錯誤
    • 特點: 1、這種效能測試方法的主要目的是檢查系統處於壓力效能下時應用的表現。 2、這種效能測試一般透過模擬負載等方法,使得系統的資源使用達到較高的水平。 3、這種效能測試方法一般用於測試系統的穩定性。 也就是說,這種測試是讓系統處在很大強度的壓力之下,看系統是否穩定,哪裡會出問題。
  • 併發測試(Concurrency Testing)

    • 說明:併發測試方法透過模擬使用者併發訪問,測試多使用者併發訪問同一個應用、同一個模組或者資料記錄時是否存在死鎖或其者他效能問題。
    • 特點: 1、這種效能測試方法的主要目的是發現系統中可能隱藏的併發訪問時的問題。 2、這種效能測試方法主要關注系統可能存在的併發問題,例如系統中的記憶體洩漏、執行緒鎖和資源爭用方面的問題。 3、這種效能測試方法可以在開發的各個階段使用需要相關的測試工具的配合和支援。 也就是說,這種測試關注點是多個使用者同時(併發)對一個模組或操作進行加壓。
  • 配置測試(Configuration Testing)

    • 說明:配置測試方法透過對被測系統的軟\硬體環境的調整,瞭解各種不同對系統的效能影響的程度,從而找到系統各項資源的最優分配原則。
    • 特點: 1、這種效能測試方法的主要目的是瞭解各種不同因素對系統效能影響的程度,從而判斷出最值得進行的調優操作。 2、這種效能測試方法一般在對系統效能狀況有初步瞭解後進行。 3、這種效能測試方法一般用於效能調優和規劃能力。 也就是說,這種測試關注點是 “微調”,透過對軟硬體的不段調整,找出這他們的最佳狀態,使系統達到一個最強的狀態。
  • 可靠性測試

    • 說明:透過給系統載入一定業務壓力(例如資源在 70%-90% 的使用率),使系統執行一段時間,以此檢測系統是否穩定執行。
    • 特點: 1、這種效能測試方法的主要目的是驗證是否支援長期穩定的執行。 2、這種效能測試方法需要在壓力下持續一段時間的執行。(2~3 天)3、測試過程中需要關注系統的執行狀況。 如果測試過程中發現,隨著時間的推移,響應時間有明顯的變化,或是系統資源使用率有明顯波動,都可能是系統不穩定的徵兆。 也就是說,這種測試的關注點是 “穩定”,不需要給系統太大的壓力,只要系統能夠長期處於一個穩定的狀態。
  • 失效恢復測試

    • 說明:如果系統區域性發生故障,使用者是否能夠繼續使用系統,以及如果這種情況發生,使用者將受到多大程度的影響。
    • 特點: 1.這種效能測試方法的主要目的是驗證在區域性故障情況下,系統能否繼續使用。 2.這種效能測試方法還需要指出,當問題發生時,“能支援多少使用者訪問” 的結論和 “採取何種應急措施” 的方案。 3.一般來說,只有對系統持續執行指標有明確要求的系統才需要進行這種型別的測試。
  • 大資料量測試:針對某些系統儲存、傳輸、統計查詢等業務進行大資料量的測試。

注意:在做效能測試時請忘掉分類。例如,執行 8 個小時來測試系統是否可靠,而這個測試極有可能包含了可靠效能測、強度測試、併發測試、負載測試,等等。因此,在實施效能測試時決不能割裂它們的內部聯絡去進行,而應該基於測試目標,分析它們之間的關係,以一種高效率的方式來設計效能測試。

5.4 效能測試流程

(1)效能需求分析

效能需求分析是整個效能測試工作開展的基礎,如果連效能的需求都沒弄清楚,後面的效能測試工具以及執行就無從談起了。
在這一階段,效能測試人員需要與 PM、DEV 及專案相關的人員進行溝通,同時收集各種專案資料,對系統進行分析,確認測試的目標。並將其轉化為可衡量的具體效能指標。
測試需求分析階段的主要任務是分析被測系統及其效能需求,建立效能測試資料模型,分析效能需求,確定合理效能目標,並進行評審;

(2)效能測試準備

主要包括:設計場景,根據場景編寫程式、編寫指令碼、準備測試環境,構造測試資料,環境預調優等;
設計場景:針對系統的特點設計出合理的測試場景。為了讓測試結果更加準確,這裡需要很細緻的工作。如建立使用者模型,只有知道真實的使用者是如何對系統產生壓力,才可以設計出有代表性的壓力測試場景。這就涉及到很多資訊,如使用者群的分佈、各型別使用者用到的功能、使用者的使用習慣、工作時間段、系統各模組壓力分佈等等。只有從多方面不斷的積累這種資料,才會讓壓力場景更有意義。最後將設計場景轉換成具體的用例。
測試資料:測試資料的設計也是一個重點且容易出問題的地方。生成測試資料量達到未來預期數量只是最基礎的一步,更需要考慮的是資料的分佈是否合理,需要仔細的確認程式中使用到的各種查詢條件,這些重點列的數值要儘可能的模擬真實的資料分佈, 否則測試的結果可能是無效的。測試資料最好使用線上脫敏後的資料,儘可能接近真實使用者行為。
預調優:指根據系統的特點和團隊的經驗,提前對系統的各個方面做一些最佳化調整,避免測試執行過程中的無謂返工。比如一個高併發的系統,10000 人線上,連線池和執行緒池的配置還用預設的,顯然是會測出問題的。

(3)執行效能測試

執行階段工作主要包含兩個方面的內容:一是執行測試用例模型,包括執行指令碼和場景;其次測試過程監控,包括測試結果、記錄效能指標和效能計數器的值

(4)結果分析與效能調優

發現問題或者效能指標達不到預期,及時的分析定位,處理後重複測試過程。
效能問題通常是相互關聯相互影響的,表面上看到的現象很可能不是根本問題,而是另一處出現問題後引起的反應。這就要求監控收集資料時要全面,從多方面多個角度去判斷定位。調優的過程其實也是一種平衡的過程,在系統的多個方面達到一個平衡即可。

(5)效能報告與總結

編寫效能測試報告,闡明效能測試目標、效能結果、測試環境、資料構造規則、遇到的問題和解決辦法等。並對此次效能測試經驗進行總結與沉澱。具體效能測試報告的編寫可以參考《效能測試報告模板》。
上面所有內容中,如果排除技術上的問題,效能測試中最難做好的,就是使用者模型的分析。它直接決定了壓力測試場景是否能夠有效的模擬真實世界壓力,而正是這種對真實壓力的模擬,才使效能測試有了更大的意義。可以說,效能測試做到一定程度,差距就體現在了模型建立上。

至於效能問題的分析、定位或者調優,很大程度是一種技術問題,需要多方面的專業知識。資料庫、作業系統、網路、開發都是一個合格的效能測試人員需要擁有的技能,只有這樣,才能從多角度全方位的去考慮分析問題。

6、效能工具效能對比

基於目前市場上主流的效能工具,進行橫向對比測試,以幫助我們在不同的環境靈活選擇不同的測試工具。

6.1 效能工具對比結果

測試物件:Nginx index.html(612Byte),CPU:16 核 / 記憶體:16GB / 磁碟:500GB
壓力機:Ubuntu18.04, CPU: 8 核 / 記憶體:8G / 磁碟: 500GB

在此只進行了最基礎的效能對比測試,僅供基本的工具選擇判斷。

6.2 效能工具介紹

ngrinder(待補充)

阿里京東也在用,天生為分散式開發的,易用性很好

(1)wrk / wrk2

wrk 是一款針對 Http 協議的基準測試工具,它能夠在單機多核 CPU 的條件下,使用系統自帶的高效能 I/O 機制,如 epoll,kqueue 等,透過多執行緒和事件模式,對目標機器產生大量的負載。
PS: 其實,wrk 是複用了 redis 的 ae 非同步事件驅動框架,準確來說 ae 事件驅動框架並不是 redis 發明的, 它來至於 Tcl 的直譯器 jim, 這個小巧高效的框架, 因為被 redis 採用而被大家所熟知。

優勢:

輕量級效能測試工具;
安裝簡單(相對 Apache ab 來說);
學習曲線基本為零,幾分鐘就能學會咋用了;
基於系統自帶的高效能 I/O 機制,如 epoll, kqueue, 利用非同步的事件驅動框架,透過很少的執行緒就可以壓出很大的併發量;

劣勢:

wrk 目前僅支援單機壓測,後續也不太可能支援多機器對目標機壓測,因為它本身的定位,並不是用來取代 JMeter, LoadRunner 等專業的測試工具,wrk 提供的功能,對後端開發人員來說,應付日常介面效能驗證還是比較友好的。
之前在樂視我們的測試架構師基於 wrk2 主導開發並支援了分散式,開發成本還是略高的。

基礎使用:

子命令引數說明:

使用方法: wrk <選項> <被測HTTP服務的URL>                            
  Options:                                            
    -c, --connections <N>  跟伺服器建立並保持的TCP連線數量  
    -d, --duration    <T>  壓測時間           
    -t, --threads     <N>  使用多少個執行緒進行壓測(為了減少現成的上下文切換,官方建議thread數量等同CPU核數)  

    -s, --script      <S>  指定Lua指令碼路徑       
    -H, --header      <H>  為每一個HTTP請求新增HTTP頭      
        --latency          在壓測結束後,列印延遲統計資訊   
        --timeout     <T>  超時時間     
    -v, --version          列印正在使用的wrk的詳細版本資訊

  <N>代表數字引數,支援國際單位 (1k, 1M, 1G)
  <T>代表時間引數,支援時間單位 (2s, 2m, 2h)

PS: 關於執行緒數,並不是設定的越大,壓測效果越好,執行緒設定過大,反而會導致執行緒切換過於頻繁,效果降低,一般來說,推薦設定成壓測機器 CPU 核心數的 2 倍到 4 倍就行了。

# 示例
wrk -c400 -t24 -d30s --latency http://10.60.82.91/

報告解析:

Running 30s test @ http://www.baidu.com (壓測時間30s)
  12 threads and 400 connections (共12個測試執行緒,400個連線)
                          (平均值) (標準差)  (最大值)(正負一個標準差所佔比例)
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    (延遲)
    Latency   386.32ms  380.75ms   2.00s    86.66%
    (每秒請求數)
    Req/Sec    17.06     13.91   252.00     87.89%
  Latency Distribution (延遲分佈)
     50%  218.31ms
     75%  520.60ms
     90%  955.08ms
     99%    1.93s 
  4922 requests in 30.06s, 73.86MB read (30.06s內處理了4922個請求,耗費流量73.86MB)
  Socket errors: connect 0, read 0, write 0, timeout 311 (發生錯誤數)
Requests/sec:    163.76 (QPS 163.76,即平均每秒處理請求數為163.76)
Transfer/sec:      2.46MB (平均每秒流量2.46MB)


Running 30s test @ http://10.60.82.91/ (壓測時間30s)
  32 threads and 400 connections (共32個測試執行緒,400個連線)
              (平均值) (標準差)  (最大值)(正負一個標準差所佔比例)
  Thread Stats   Avg      Stdev     Max   +/- Stdev
  Latency(延遲)  10.31ms   40.13ms 690.32ms   98.33%
  Req/Sec(每秒請求數) 2.14k   482.15     6.36k    77.39%
  Latency Distribution
     50%    5.11ms
     75%    7.00ms
     90%    9.65ms
     99%  212.68ms

  (30.10s內處理了2022092個請求,耗費流量1.62GB)
  2022092 requests in 30.10s, 1.62GB read
Requests/sec:  67183.02  (QPS 67183.02,即平均每秒處理請求數為67183.02)
Transfer/sec:     55.03MB (平均每秒流量55.03MB)

(2)Jmeter

Jmeter 是 Java 開發的、基於多執行緒併發模型的壓測工具,也是目前最流行的開源壓測工具,工作原理類似,如下圖:

  • 其所謂的虛擬使用者 (vuser) 就是對應一個執行緒
  • 在單個執行緒中,每個請求(query)都是同步呼叫的,下一個請求要等待前一個請求完成才能進行
  • 一個請求(query)分成三部分:
    • send - 施壓端傳送開始,直到承壓端接收完成
    • wait - 承壓端接收完成開始,直至業務處理結束
    • recv - 承壓端返回資料,直至施壓端接收完成
  • 同一執行緒中連續的兩個請求之間存在等待時間這種概念,即圖中的空白處

在多執行緒併發模型下,是不是可以透過不斷增加執行緒數量生產出更大的壓力?
答案是否定的。

事實上一個程序在一個時間點只能執行一個執行緒,而所謂的併發是指在程序裡不斷切換執行緒實現了看上去的多個任務的併發,但是執行緒上下文切換有很高的成本,過多的執行緒數反而會造成效能的嚴重下滑。

從應用角度來看,基於多執行緒的併發模型,往往需要設定最大併發數引數,而如果壓測場景需要不斷往上加壓,那這類工具其實挺難應付的。

(3)Locust

Locust 是用 Python 開發的分散式壓測工具,近年來在國內比較流行。Locust 並不是基於 Python 的多執行緒,而是 coroutine(協程,gevent 提供),gevent 使用了 libev 或者 libuv 作為 eventloop。
Locust 響應時間失真問題:
Locust 當壓力機 CPU 達到瓶頸後,響應時間會嚴重失真。
比如當 Locust normal 模式下,8 程序,CPU 瓶頸後,90% 響應時間為 340ms。同時 wrk 獲取的響應時間為 59.41ms.

基本使用介紹:

裝飾器 task 可以設定壓力比例

  • HttpUser 示例:
from locust import HttpUser, task

class QuickstartUser(HttpUser):
    # task為每個正在執行的使用者建立一個greenlet(微執行緒)
    @task(1)
    def detail(self):
        self.client.get("http://10.60.82.91/")

    def on_start(self):
        pass
  • FastHttpUser 示例
from locust import task
from locust.contrib.fasthttp import FastHttpUser

class QuickstartUser(FastHttpUser):
    # task為每個正在執行的使用者建立一個greenlet(微執行緒)
    @task(1)
    def detail(self):
        self.client.get("http://10.60.82.91/")

    def on_start(self):
        pass
  • 啟動及分散式
# -c 併發使用者數
# -r 每秒啟動使用者數
# -t 持續執行時間
locust -f load_test.py --host=http://10.60.82.91 --no-web -c 10 -r 10 -t 1m

# 分散式
nohup locust -f locust_files/fast_http_user.py --master &
nohup locust -f locust_files/fast_http_user.py --worker --master-host=10.60.82.90 &

6.3 測試記錄

(1)wrk 測試記錄

2 執行緒: -c1000 -t1(因最少 2 執行緒)QPS: 35560.79

wrk -c1000 -t1 -d30s --latency http://10.60.82.91/

3 執行緒:( -c1000 -t2 QPS: 66941.77 )

wrk -c1000 -t2 -d30s --latency http://10.60.82.91/

8 執行緒: ( -c1000 -t8 QPS: 75579.30 )

wrk -c1000 -t8 -d30s --latency http://10.60.82.91/
Nginx: 86% * 16 = 1376% , 達到 CPU 瓶頸

Wrk: CPU = 40% * 8 = 320%

(2)Locust HttpUser 記錄

1 程序:(10 併發,QPS:512)

Nginx:(CPU:8.6%)

Locust:(CPU:100%, 單核 CPU 達到瓶頸)

8 程序:(100 併發,QPS:3300)

Nginx: (CPU:50%)

locust:(CPU:800%, CPU 達到瓶頸)

(3)Locust FastHttpUser 記錄

1 程序:(10 併發,QPS:1836, 90%RT: 5ms)

Nginx:(CPU:24%)

Locust:(CPU:100%, 單核 CPU 達到瓶頸)

8 程序:(100 併發,QPS:11000, 90%RT: 7ms)

Nginx:(CPU:150%)

locust:(CPU:800%, CPU 達到瓶頸)

(4)Jmeter 測試記錄

8 核 (100 併發,QPS:38500)

Nginx:(CPU:397.3%)

Jmeter:(CPU:681%)

參考資料

  • 效能測試之併發模型對比 (Jmeter/Locust/Gatling)
  • 左耳朵耗子 - 效能測試應該怎麼做?
  • Dynatrace-2016 全球零售業數字化效能基準報告
  • 支付寶效能測試實戰 [劍風]
  • 網銀線上效能測試指南(京東金融)
  • 聊聊 ab、wrk、JMeter、Locust 這些壓測工具的併發模型差別
  • 效能工具 wrk 介紹
  • Locust 官方文件
  • mac 安裝 locust 問題:解決 MacOS 升級後出現 xcrun: error: invalid active developer path, missing xcrun 的問題

相關文章