AWS Global Accelerator 效能量測方法與陷阱

前言

Global Accelerator (GA) 是 AWS 提供的網路加速服務,透過提早 TCP Termination 與品質較佳的 AWS 骨幹網路來加速傳輸,來達到網路傳輸加速效果,其原理類似於 Proxy。

原理

透過 Slow-Start Bandwidth-Delay Product 我們可以知道延遲對於「網速」有很大的影響,這裡的網速指的不只是頻寬,而是「從開始 TCP 握手到收完回應 4 次揮手」的耗時。

因此提早 TCP Termination 可以加速提升 Window Size,可以儘早進入高品質的 AWS 骨幹網路(但不一定是最好)之外,還可以在骨幹中使用 Jumbo Frame 傳輸,因此效率較好

綜合以上概念,如果 Client 本身網路品質不錯,那麼 GA 對於小檔案傳輸的改善有限;如果是較大一點的檔案可以受惠於 TCP Termination 以儘早提升 TCP Window Size,加上骨幹網路使用 Jumbo Frames 傳輸可減少 TCP overhead,以提高傳輸效率。

註:通常付越多錢的上網用戶可以優先享有較近、較快、較穩的線路,傳輸品質肯定比較好。因為好的線路頻寬有限,ISP 會因為線路價格考量來安排優先級。

效能量測方法

根據文件,可以到 AWS Global Accelerator Speed Comparison 網站 測試不同地點的加速效果。

官方也有提供效能量測分析文章 Measuring AWS Global Accelerator performance and analyzing results 來說明如何測試,像是可以透過 iPerf、MTR 等工具每小時做 1000 次採樣等。

以下提供簡易版 HTTP 測試程式碼:

import statistics
import time

import requests

times = 5
direct_url = "https://c7b8c7e-12d0-8f3-4c7-8ef01f8a843-0.direct.us-east-1.prod.endpoints.ubiquity.aws.a2z.com/api/burst/51200"
ga_url = "https://c7b8c7e-12d0-8f3-4c7-8ef01f8a843-0.ubiquity.us-east-1.prod.endpoints.ubiquity.aws.a2z.com/api/burst/51200"

direct_result = []
ga_result = []

def print_statistics(text, v):
    print(text)
    print("  min: " + str(min(v)))
    print("  max: " + str(max(v)))
    print("  avg: " + str(statistics.mean(v)))

if __name__ == "__main__":

    for i in range(times):
        t1 = time.time()
        requests.get(direct_url)
        t2 = time.time()
        direct_result.append(t2 - t1)

        t1 = time.time()
        requests.get(ga_url)
        t2 = time.time()
        ga_result.append(t2 - t1)

    print("## Loop times: " + str(times))
    print_statistics("=== Direct ===", direct_result)
    print_statistics("=== GA ===", ga_result)

結果:

## Loop times: 5
=== Direct ===
  min: 1.2958009243011475
  max: 1.525893211364746
  avg: 1.3494491577148438
=== GA ===
  min: 0.9617481231689453
  max: 1.1492066383361816
  avg: 1.0054255485534669

效能量測陷阱

因為 GA 預設啟用 TCP Termination 功能,如果「從送出請求後開始計算傳輸時間」會使得 GA 耗時看起來比直接連線高一些。T 同事當時舉例給我聽時我滿驚訝的,沒想到會有這種陷阱,以下就舉例給大家看一下。

假設地理位置、延遲設置如下:

用 GA: Client <-5-> GA <----95----> NLB
直接連: Client <-------100---------> NLB

假設耗時計算為「從請求送出開始計算」,且 NLB 與後端 Target 請求處理時間為 0,以下為事件時間軸與耗時:

用 GA:

(已經建立完 TCP 連線)

--> 5(送出請求)
<-- 5(GA ACK)

##客戶端認為請求已送出,開始計算耗時##

--> 95(請求到 NLB,此時不用計算 ACK,因為跟請求處理為同步進行)(但這個 95 跟上一個 ACK 是同步進行,因此實際上要算 90)
<-- 95(NLB 回應請求)
<-- 5(GA 回應給 Client)

##客戶端收到回應##

=========================
實際耗時 = 5 + 5 + (95-5) + 95 + 5 = 200
客戶端認為耗時 = 95 + 95 + 5 = 195

直接連線:

(已經建立完 TCP 連線)

--> 100(送出請求)
<-- 100(NLB ACK)

##客戶端認為請求已送出,開始計算耗時##

<-- 0(NLB 回應請求,因為假設請求處理時間為 0,因此會跟上一個 ACK 同時間送出封包,在此就把時間設定為 0)

##客戶端收到回應##

=========================
實際耗時 = 100 + 100 = 200
客戶端認為耗時 = 0

總結以上資訊,我們可以看到不論 用 GA 還是 直接連線 的總耗時是一樣的,但計算方式不同會誤以為 GA 耗時較高。因此比較好的方式還是從「請求送出前就開始計時,直到收到回應才停止」會是比較好的計算方式。

實驗

口說無憑,我們來寫程式驗證這件事情。下面程式有四個迴圈:

  • 前 2 個迴圈是對 GA 與 NLB 做 2 種計時
    1. 建立連線前
    2. 發送請求後
  • 後 2 個迴圈一樣對 GA 與 NLB 發起連線,只是在送出請求前會先 sleep 一秒鐘,並且在送出請求後才開始計時
    • 這個是要確保 GA 已經跟 NLB 握手完成

(以下公網 IP 地址經過變造,如有雷同純屬巧合)

import socket
import statistics
import time

nlb_ip = "13.215.123.77"
ga_ip = "15.197.219.136"
times = 5
file_size = (
    12 * 1024 * 1024
)  # 12MB, please make sure this value is smaller than your actual response size
payload = b"GET / HTTP/1.1\r\nHost: "

no_sleep_nlb_t1 = []
no_sleep_nlb_t2 = []
no_sleep_ga_t1 = []
no_sleep_ga_t2 = []
sleep_nlb = []
sleep_ga = []

def recvall(sock, n):
    # Helper function to recv n bytes or return None if EOF is hit
    data = bytearray()
    while len(data) < n:
        packet = sock.recv(n - len(data))
        if not packet:
            return None
        data.extend(packet)
    return data

"""Without sleep

The normal behavior of users. Compare between "whole time" and "response time".
It illustrates the wrong calculation method reverses the result.
"""

for i in range(times):
    t1 = time.time()
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = (nlb_ip, 80)
    sock.connect(server_address)
    sock.sendall(payload + nlb_ip.encode() + b"\r\n\r\n")
    t2 = time.time()
		# sock.recv(10)
    recvall(sock, file_size)
    end = time.time()
    no_sleep_nlb_t1.append(end - t1)
    no_sleep_nlb_t2.append(end - t2)

for i in range(times):
    t1 = time.time()
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = (ga_ip, 80)
    sock.connect(server_address)
    sock.sendall(payload + ga_ip.encode() + b"\r\n\r\n")
    t2 = time.time()
		# sock.recv(10)
    recvall(sock, file_size)
    end = time.time()
    no_sleep_ga_t1.append(end - t1)
    no_sleep_ga_t2.append(end - t2)

"""With sleep

Make sure GA has forward the request to origin for fairness.
Just for comparison, this behavior doesn't exist in real world.
"""

for i in range(times):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = (nlb_ip, 80)
    sock.connect(server_address)
    time.sleep(1)
    sock.sendall(payload + nlb_ip.encode() + b"\r\n\r\n")
    start = time.time()
		# sock.recv(10)
    recvall(sock, file_size)
    end = time.time()
    sleep_nlb.append(end - start)

for i in range(times):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = (ga_ip, 80)
    sock.connect(server_address)
    time.sleep(1)
    sock.sendall(payload + ga_ip.encode() + b"\r\n\r\n")
    start = time.time()
		# sock.recv(10)
    recvall(sock, file_size)
    end = time.time()
    sleep_ga.append(end - start)

def print_statistics(text, v):
    print(text)
    print("  min: " + str(min(v)))
    print("  max: " + str(max(v)))
    print("  avg: " + str(statistics.mean(v)))

print("## Loop times: " + str(times))
print_statistics("=== No sleep NLB (whole time) ===", no_sleep_nlb_t1)
print_statistics("=== No sleep NLB (after request) ===", no_sleep_nlb_t2)
print_statistics("=== No sleep GA (whole time) ===", no_sleep_ga_t1)
print_statistics("=== No sleep GA (after request) ===", no_sleep_ga_t2)
print_statistics("=== Sleep NLB (after request) ===", sleep_nlb)
print_statistics("=== Sleep GA (after request) ===", sleep_ga)

結果

  1. 從 東京(ap-northeast-1)連 新加坡(ap-southeast-1)請求 249 bytes 檔案
    • 「No sleep NLB (whole time)」與「No sleep GA (whole time)」結果差不多,因為請求檔案小加速效果不大
    • 可以看到「No sleep NLB (after request)」時間比「No sleep GA (after request)」少了一半!這就是錯誤計算方式導致的結果
    • 「Sleep NLB (after request) 」與「Sleep GA (after request) 」結果差不多,因為 Sleep 起了作用
## Loop times: 5
=== No sleep NLB (whole time) ===
  min: 0.13748788833618164
  max: 0.1437385082244873
  avg: 0.14207563400268555
=== No sleep NLB (after request) ===
  min: 0.06859135627746582
  max: 0.07164549827575684
  avg: 0.0708247184753418
=== No sleep GA (whole time) ===
  min: 0.13784289360046387
  max: 0.1636338233947754
  avg: 0.143807315826416
=== No sleep GA (after request) ===
  min: 0.13602924346923828
  max: 0.16110968589782715
  avg: 0.14150776863098144
=== Sleep NLB (after request) ===
  min: 0.06822395324707031
  max: 0.07148408889770508
  avg: 0.06983485221862792
=== Sleep GA (after request) ===
  min: 0.0698554515838623
  max: 0.07162070274353027
  avg: 0.07061533927917481
  1. 從 東京(ap-northeast-1)連 新加坡(ap-southeast-1)請求 12MiB 檔案
    • 因為是大檔案請求,GA 明顯有作用
    • 「No sleep NLB (after request)」時間沒有比 GA 短,因為大檔案傳輸時間遠大於 TCP Termination 提早回應 ACK 時間
## Loop times: 5
=== No sleep NLB (whole time) ===
  min: 0.9568026065826416
  max: 2.7421250343322754
  avg: 1.3382397174835206
=== No sleep NLB (after request) ===
  min: 0.8890306949615479
  max: 2.670349597930908
  avg: 1.268001174926758
=== No sleep GA (whole time) ===
  min: 0.8354196548461914
  max: 0.8420989513397217
  avg: 0.8386162281036377
=== No sleep GA (after request) ===
  min: 0.8332135677337646
  max: 0.8386590480804443
  avg: 0.8356685638427734
=== Sleep NLB (after request) ===
  min: 0.9029538631439209
  max: 0.993417501449585
  avg: 0.9541794776916503
=== Sleep GA (after request) ===
  min: 0.7665829658508301
  max: 0.7844345569610596
  avg: 0.7727417945861816

常見問題

  • 為何 GA 沒有明顯加速
    • 可能使用者網路品質本來就很好,甚至比 AWS GA 還好
    • 可能是 Client 因為一些因素沒有就近訪問 GA 節點(繞遠路了)
    • 可能 GA 節點、骨幹出問題
  • 如何診斷問題
  • 為何 GA 會造成一些奇怪的問題
    • 請開案例問 Support 工程師

comments powered by Disqus