博客 / 詳情

返回

1TB數據,ES卻收到了2TB?揪出那個客户端中的“隱形復讀機”

image.png你是否經歷過這樣的“靈異事件”:

業務監控顯示,你的日誌服務每秒只寫入了 50MB 的數據,全天累計寫入 1TB。

但在雲廠商的賬單,或者內網交換機的監控上,流量卻高達 100MB/s,全天消耗了 2TB 的帶寬。

網卡經常莫名其妙被打滿,造成正常的業務請求卡頓、丟包。

排查了一圈:

  • 不是 TCP 重傳(Retransmission 正常)。
  • 不是 SSL 握手膨脹(HTTPS 開銷沒那麼大)。
  • 也不是監控系統算錯了(交換機端口統計實打實的跑滿了)。

最後抓包一看,差點氣暈過去:

你的客户端,為了把這 1TB 的數據發給服務端,實際上在網線上跑了 2TB 的量。

因為它每發一次數據之前,都要先“假裝”發一次被拒,然後再“真”發一次。

今天,我們就來揭秘這吃光你帶寬的“隱形復讀機”——非搶先認證(Non-Preemptive Auth),並教你如何用更優雅的方式幫公司省下一半的流量費。

一、案發現場:帶寬莫名其妙“爆”了

故事發生在一個大數據量的日誌寫入場景。

  • 業務側:開發拍着胸脯説:“我算過了,每條日誌 1KB,每秒 5萬條,流量絕對只有 50MB/s,千兆網卡綽綽有餘。”
  • 網絡側:運維看着監控大屏一臉懵逼:“大哥,網卡出口流量已經頂到 100MB/s 了,帶寬利用率 100%,開始丟包了!”

這憑空多出來的 50MB/s 是哪來的?

最離譜的是,客户端日誌一切正常, ES 集羣也一切正常,寫入成功率 100%,彷彿只是默默地吞下了這雙倍的流量。

二、傳統排查:終端裏的一眼定乾坤

在自建環境中,要查清楚帶寬去哪了,不需要複雜的分析工具,用 tcpdump 看一眼報文實體就真相大白了

祭出 tcpdump(抓“實錘”)

懷疑是重傳?直接在生產環境抓取端口流量,並用 -A 參數打印包的內容:

# -A: 以 ASCII 打印包內容,能看到 HTTP Body
# -s 0: 抓取完整包,防止截斷 Body
tcpdump -i eth0 port 9200 -A -s 0 -c 100 -w bandwidth_leak.pcap

當你打開抓包文件,你會看到令人崩潰的一幕:

每一個 POST 請求的 Body(業務數據),在網絡上傳輸了兩次!

  1. 第一次傳輸(無效)

    • Header: POST /_bulk (無 Auth 頭)
    • Body: {"index":{...}} ... (5MB 的真實數據被髮出去了!)
    • Response: 401 Unauthorized (ES 拒收,但這 5MB 流量已經佔用了帶寬)
  2. 第二次傳輸(有效)

    • Header: POST /_bulk (帶 Auth 頭)
    • Body: {"index":{...}} ... (同樣的 5MB 數據,又完整發了一遍)
    • Response: 200 OK

結論: 你的客户端不僅是在“虛晃一槍”,它是“全量試探”——每次被拒之前,都先把沉甸甸的數據包完整地發一遍。帶寬就是這麼翻倍的。

無法抓包?應用層也有“呈堂證供”

如果你沒有服務器的 root 權限無法運行 tcpdump,或者想從應用層進一步確認,日誌也能提供確鑿的證據。

  • 搜查客户端日誌:將客户端(如 Apache HttpClient)的日誌級別調至 DEBUG。你會發現日誌裏充斥着 Authentication required —— 每一條成功的請求背後,都緊跟在一次失敗的嘗試之後。
  • 調閲服務端審計(高危):臨時開啓 ES 的 Audit Log(警告:全量審計極其消耗性能,生產環境慎開)。你會看到同一個請求總是成對出現:先是 access_denied,緊接着才是 access_granted

    三、深度解析:為什麼客户端這麼“傻”?

    為什麼客户端不能先問問需不需要密碼,非要先把數據扔過去被拒一次?

    1. 默認行為的代價(RFC 的鍋)

    老版本的 Java 客户端(Apache HttpClient 4.x 內核)和部分 Python 客户端,默認遵循 RFC 2617 的“被動認證”流程:

它假設服務器可能不需要密碼。為了“兼容性”,它直接把請求(包含 Header 和 Body)發過去。

  • Round 1: 客户端發送 Header + Body (1GB)
  • Round 2: 服務端收到,發現沒權限。丟棄收到的 1GB Body,返回 401,並在 Header 裏喊話:“我要密碼!”。
  • Round 3: 客户端收到 401,發現需要密碼。於是帶上密碼,重新發送 Header + Body (1GB)
  • Round 4: 服務端校驗通過,接收數據。
    image.png

    2. “帶寬黑洞”的形成

    在內網 ES 這種必須鑑權的場景下,運氣永遠是不好的。

於是,邏輯變成了:

  • 你要寫 1GB 數據?
  • 系統實際傳輸:先發 1GB (被拒) + 再發 1GB (成功) = 消耗 2GB 帶寬

這不僅打爆了網卡,還浪費了 ES 的 HTTP 解析線程和 SSL 解密開銷(如果是 HTTPS)。

四、解決方案:更優雅的“搶答模式”

解決思路非常簡單:不要試探,第一次請求就直接把“身份證”亮出來。

1. Java 客户端

場景 A:ES 8.x 新版 Java Client (官方推薦)

如果你使用的是最新的 co.elastic.clients,雖然上層 API 變了,但底層依然依賴 RestClient。你需要確保在底層的 RestClientBuilder 中正確配置憑據。

// 1. 準備憑據提供者
final CredentialsProvider credentialsProvider = 
    new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
    new UsernamePasswordCredentials("user", "password"));

// 2. 配置底層 RestClient
RestClient restClient = RestClient.builder(
    new HttpHost("es-host", 9200))
    .setHttpClientConfigCallback(httpClientBuilder -> 
        // 關鍵點:注入默認憑據提供者
        // 這會激活 HttpClient 內部的 AuthCache,實現搶佔式認證
        httpClientBuilder
            .setDefaultCredentialsProvider(credentialsProvider)
    ).build();

// 3. 構建 ES8 Client
ElasticsearchTransport transport = new RestClientTransport(
    restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);

場景 B:老版本 RestHighLevelClient (維護模式)

很多老系統還在用這個。務必檢查是否禁用了緩存,或者忘記配置 CredentialsProvider

// 方式一(優雅):配置 CredentialsProvider(同上)
// 方式二(硬核):直接焊死 Header,絕對不給 401 任何機會

Header[ ] defaultHeaders = new Header[ ]{
    new BasicHeader("Authorization",
    "Basic " + Base64.getEncoder().encodeToString("u:p".getBytes()))
};
RestClientBuilder builder = RestClient
    .builder(new HttpHost("es-host", 9200))
    .setDefaultHeaders(defaultHeaders); 

2. Python 客户端

場景 A:Python Client v8 (官方推薦)

在 v8 版本中,官方廢棄了 http_auth,改用 basic_auth。使用標準寫法時,默認就是搶佔式的,無需額外操心。

from elasticsearch import Elasticsearch

# ✅ 官方推薦寫法:使用 basic_auth
# 底層邏輯已優化,默認開啓 Preemptive Auth,不會浪費帶寬
client = Elasticsearch(
    "http://es-host:9200",
    basic_auth=("user", "password")
)

場景 B:手動注入 Header (全版本通用)

如果你還在用老版本,或者不確定 SDK 內部行為,手動注入 Header 是最穩妥的。

import base64
import requests

# 構造 Header
token = base64.b64encode(b"user:password").decode("ascii")
headers = { 'Authorization': f'Basic {token}' }

# 第一包數據就會帶上 Auth,絕無浪費
r = requests.post(url, data=big_payload, headers=headers)

💡 Pro Tips:鑑權最佳實踐

解決“401 試探”最徹底的方法是使用 API Key(它不受 Basic Auth 協議的“試探”邏輯束縛,天生就是搶佔式的),但需要注意:

  • PaaS / 自建用户:強烈推薦使用 API Key 取代傳統的賬號密碼。不僅性能更好,權限控制也更精細,安全性更高
  • Serverless 用户:目前 ES Serverless 暫不支持 API Key。請使用上述的 Basic Auth 搶佔式配置方案(如 basic_authCredentialsProvider),同樣可以完美解決帶寬翻倍問題。

五、Serverless 價值:從“黑盒抓瞎”到“上帝視角”

看到這裏,你可能會問:“原理我懂了,但我怎麼知道我的系統裏有沒有藏着這個流量黑洞?總不能天天去生產環境抓包吧?”

這正是自建 ES 集羣的痛點:你只看得到帶寬爆了,卻不知道是哪部分流量在搞鬼。

而在 阿里雲 ES Serverless 中,這顯而易見。

1. 狀態碼大盤:一眼定真偽

Serverless 提供了基於網關層的全鏈路監控。你只需要點開控制枱的“端到端請求指標”監控:

如果你看到 401 的曲線200 的曲線 高度重合(甚至數量 1:1),如下圖所示:
image.png

這就意味着:你每一字節的有效數據,都伴隨着一字節的無效帶寬消耗。

2. 全量 Access Log:精準定責

如果監控曲線異常,下一步就是找“誰幹的”。 Serverless 的“日誌查詢”中可以直接查看每個請求的 user_agentremote_ip。 瞬間就能找到是哪個開發團隊乾的,隨後你就可以把截圖甩給負責該業務的開發團隊:“看,這 50% 的廢流量都是你們服務發出來的。”
image.png

結語

image.png

只有看得見,才能省下來。
帶寬就是金錢,但看不見的浪費,才是最大的成本。別讓你的預算消耗在毫無意義的“被動試探”上。

快去檢查一下你的監控大盤:

  • 流量是不是比業務量大一倍?
  • 401 佔比是不是 50%?

也許只需改一行配置,你的集羣帶寬壓力就能瞬間減半,流量費立省 50%。

立即體驗阿里雲 ES Serverless,用端到端監控,讓流量黑洞無處遁形!

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.