記一次 Kubernetes 機器內核問題排查


此次排查發生在 2020-11 月份, 一直沒時間寫博客描述事情經過, 本次正好一起寫了吧.

具體現象

在線上環境中的某個應用出現了接口緩慢的問題!!

就憑這個現象, 能列出來的原因數不勝數. 本篇博客主要敘述一下幾次排查以及最後如何確定原因的過程, 可能不一定適用於其他集羣, 就當是提供一個參考吧. 排查過程比較冗長, 過去太久了, 我也不太可能回憶出所有細節, 希望大家見諒.

網絡拓撲結構

網絡請求流入集羣時, 對於我們集羣的結構:

用戶請求=>Nginx=>Ingress =>uwsgi

不要問爲什麼有了 Ingress 還有 Nginx. 這是歷史原因, 有些工作暫時需要由 Nginx 承擔.

初次定位

請求變慢一般馬上就會考慮, 程序是不是變慢了, 所以在發現問題後, 首先在 uwsgi 中增加 簡單的小接口, 這個接口是處理快並且馬上返回數據, 然後定時請求該接口. 在運行幾天之後, 確認到該接口的訪問速度也很慢, 排除程序中的問題, 準備在鏈路中查找原因.

再次定位 – 簡單的全鏈路數據統計

由於我們的 Nginx 有 2 層, 需要針對它們分別確認, 看看究竟是哪一層慢了. 請求量是比較大的, 如果針對每個請求去查看, 效率不高, 而且有可能掩蓋真正原因, 所以這個過程採用統計的方式. 統計的方式是分別查看兩層 Nginx 的日誌情況. 由於我們已經在 elk 上接入了日誌. elk 中篩選數據的腳本簡單如下:

{
  "bool": {
    "must": [
      {
        "match_all": {}
      },
      {
        "match_phrase": {
          "app_name": {
            "query""xxxx"
          }
        }
      },
      {
        "match_phrase": {
          "path": {
            "query""/app/v1/user/ping"
          }
        }
      },
      {
        "range": {
          "request_time": {
            "gte"1,
            "lt"10
          }
        }
      },
      {
        "range": {
          "@timestamp": {
            "gt""2020-11-09 00:00:00",
            "lte""2020-11-12 00:00:00",
            "format""yyyy-MM-dd HH:mm:ss",
            "time_zone""+08:00"
          }
        }
      }
    ]
  }
}

數據處理方案

根據 trace_id 可以獲取到 Nignx 日誌以及 Ingress 日誌, 通過 elk 的 api 獲得.

# 這個數據結構用來記錄統計結果,
# [[0, 0.1], 3]表示 落在 0~0.1區間的有3條記錄
# 因爲小數的比較和區間比較麻煩, 所以採用整數, 這裏的0~35其實是0~3.5s區間
# ingress_cal_map = [
#     [[0, 0.1], 0],
#     [[0.1, 0.2], 0],
#     [[0.2, 0.3], 0],
#     [[0.3, 0.4], 0],
#     [[0.4, 0.5], 0],
#     [[0.5, 1], 0],
# ]
ingress_cal_map = []
for x in range(0351):
    ingress_cal_map.append(
        [[x, (x+1)], 0]
    )
nginx_cal_map = copy.deepcopy(ingress_cal_map)
nginx_ingress_gap = copy.deepcopy(ingress_cal_map)
ingress_upstream_gap = copy.deepcopy(ingress_cal_map)


def trace_statisics():
    trace_ids = []
    # 這裏的trace_id是提前查找過, 那些響應時間比較久的請求所對應的trace_id
    with open(trace_id_file) as f:
        data = f.readlines()
        for d in data:
            trace_ids.append(d.strip())

    cnt = 0
    for trace_id in trace_ids:
        try:
            access_data, ingress_data = get_igor_trace(trace_id)
        except TypeError as e:
            # 繼續嘗試一次
            try:
                access_data, ingress_data = get_igor_trace.force_refresh(trace_id)
            except TypeError as e:
                print("Can't process trace {}: {}".format(trace_id, e))
                continue
        if access_data['path'] != "/app/v1/user/ping":  # 過濾髒數據
            continue
        if 'request_time' not in ingress_data:
            continue

        def get_int_num(data):  # 數據統一做*10處理
            return int(float(data) * 10)

        # 針對每個區間段進行數據統計, 可能有點羅嗦和重複, 我當時做統計夠用了
        ingress_req_time = get_int_num(ingress_data['request_time'])
        ingress_upstream_time = get_int_num(ingress_data['upstream_response_time'])
        for cal in ingress_cal_map:
            if ingress_req_time >= cal[0][0and ingress_req_time < cal[0][1]:
                cal[1] += 1
                break

        nginx_req_time = get_int_num(access_data['request_time'])
        for cal in nginx_cal_map:
            if nginx_req_time >= cal[0][0and nginx_req_time < cal[0][1]:
                cal[1] += 1
                break

        gap = nginx_req_time - ingress_req_time
        for cal in nginx_ingress_gap:
            if gap >= cal[0][0and gap <= cal[0][1]:
                cal[1] += 1
                break

        gap = ingress_req_time - ingress_upstream_time
        for cal in ingress_upstream_gap:
            if gap >= cal[0][0and gap <= cal[0][1]:
                cal[1] += 1
                break

我分別針對request_time(nginx), request_time(ingress), 以及requet_time(nginx) - request_time(ingress),做了統計.

最後的統計結果大概如下:

結果分析

我們總共有約 3000 條數據!

圖一: 超過半數的請求落在 11.1s 區間, 1s2s 的請求比較均勻, 之後越來越少了.

圖二: 大約 1/4 的請求其實已經在 0.1s 內返回了, 但是 1~1.1s 也有 1/4 的請求落上去了, 隨後的結果與圖一類似.

從圖 1 圖 2 結合來看, 部分請求在 Ingress 側處理的時間其實比較短的,

圖三: 比較明顯了, 2/3 的請求在響應時間方面能夠保持一致, 1/3 的請求會有 1s 左右的延遲.

總結

從統計結果來看, Nginx => Ingress 以及 Ingress => upstream, 都存在不同程度的延遲, 超過 1s 的應用, 大約有 2/3 的延遲來自 Ingress=>upstream, 1/3 的延遲來自 Nginx=>Ingress.

再深入調查 - 抓包處理

抓包調查主要針對Ingress=>uwsgi, 由於數據包延遲的情況只是偶發性現象, 所以需要抓取所有的數據包再進行過濾… 這是一條請求時間較長的數據, 本身這個接口返回應該很快.

{
  "_source": {
    "INDEX""51",
    "path""/app/v1/media/",
    "referer""",
    "user_agent""okhttp/4.8.1",
    "upstream_connect_time""1.288",
    "upstream_response_time""1.400",
    "TIMESTAMP""1605776490465",
    "request""POST /app/v1/media/ HTTP/1.0",
    "status""200",
    "proxy_upstream_name""default-prod-XXX-80",
    "response_size""68",
    "client_ip""XXXXX",
    "upstream_addr""172.32.18.194:6000",
    "request_size""1661",
    "@source""XXXX",
    "domain""XXX",
    "upstream_status""200",
    "@version""1",
    "request_time""1.403",
    "protocol""HTTP/1.0",
    "tags": ["_dateparsefailure"],
    "@timestamp""2020-11-19T09:01:29.000Z",
    "request_method""POST",
    "trace_id""87bad3cf9d184df0:87bad3cf9d184df0:0:1"
  }
}

Ingress 側數據包

uwsgi 側數據包

數據包流轉情況

回顧一下 TCP 三次握手:

首先從 Ingress 側查看, 連接在21.585446開始, 22.588023時, 進行了數據包重新發送的操作.

從 Node 側查看, node 在 ingress 數據包發出後不久馬上就收到了 syn, 也立刻進行了 syn 的返回, 但是不知爲何 1s 後纔出現在 ingress 處.

有一點比較令人在意, 即便是數據包發生了重傳, 但是也沒有出現丟包的問題, 從兩臺機器數據包的流轉來看, 此次請求中, 大部分的時間是因爲數據包的延遲到達造成的, 重傳只是表面現象, 真正的問題是發生了數據包的延遲.

不止是 ack 數據包發生了延遲

從隨機抓包的情況來看, 不止是SYN ACK發生了重傳:

有些FIN ACK也會, 數據包的延遲是有概率的行爲!!!

總結

單單看這個抓包可能只能確認是發生了丟包, 但是如果結合 Ingress 與 Nginx 的日誌請求來看, 如果丟包發生在 tcp 連接階段, 那麼在 Ingress 中, 我們就可以查看upstream_connect_time 這個值來大致估計下超時情況. 當時是這麼整理的記錄:

我初步猜測這部分時間主要消耗在了 TCP 連接建立時, 因爲建立連接的操作在兩次 Nginx 轉發時都存在, 而我們的鏈路全部使用了短連接, 下一步我準備增加$upstream_connect_time變量, 記錄建立連接花費的時間. http://nginx.org/en/docs/http/ngx_http_upstream_module.html

後續工作

既然可以瞭解到 tcp 連接的建立時間比較久, 我們可以用它來作爲一個衡量指標, 我把 wrk 也修改了下, 增加了對於連接時間的測量, 具體的 PR 見這裏(https://github.com/wg/wrk/pull/447), 我們可以利用這一項指標衡量後端的服務情況.

尋找大佬, 看看是否遇到類似問題

上面的工作前前後後我進行了幾次, 也沒有什麼頭緒, 遂找到公司的其他 K8S 大佬諮詢問題, 大佬提供了一個思路:

宿主機延遲也高的話,那就暫時排除宿主機到容器這條路徑。我們這邊此前排查過一個延遲問題, 是由於 k8s 的監控工具定期 cat proc 系統下的 cgroup 統計信息, 但由於 docker 頻繁銷燬重建以及內核 cache 機制,使得每次 cat 時間很長佔用內核導致網絡延遲, 可否排查一下你們的宿主機是否有類似情形?不一定是 cgroup,其他需要頻繁陷入到內核的操作都可能導致延遲很高

這個跟我們排查的 cgroup 太像了,宿主機上有一些週期性任務,隨着執行次數增多,佔用的內核資源越來越多,達到一定程度就影響了網絡延遲

大佬們也提供了一個內核檢查工具(可以追蹤和定位中斷或者軟中斷關閉的時間):

https://github.com/bytedance/trace-irqoff

有問題的 ingress 機器 的 latency 特別多,好多都是這樣的報錯, 其他機器沒有這個日誌:

👉
img

而後, 我針對機器中的 kubelet 進行了一次追蹤, 從火焰圖中可以確認, 大量的時間耗費在了讀取內核信息中.

其中具體的代碼如下:

總結

根據大佬所給的方向, 基本能夠確定問題發生的真正原因: 機器上定時任務的執行過多, 內核緩存一直增加, 導致內核速度變慢了. 它一變慢, 引發了 tcp 握手時間變長, 最後造成用戶體驗下降. 既然發現了問題, 解決方案也比較容易搜索到了, 增加任務, 檢查內核是否變慢, 慢了的話就清理一次:

$ sync && echo 3 > /proc/sys/vm/drop_caches

總結

這次的排查過程是由於應用層出現了影響用戶體驗的問題後, 進一步延伸到了網絡層, 其中經歷了漫長的抓包過程, 也增加了自己的腳本用於指標衡量, 隨後又通過內核工具定位到了具體應用, 最後再根據應用的pprof工具製作出的火焰圖定位到了更加精確的異常位置, 期間自己一個人沒法處理問題, 遂請其他大佬來幫忙, 大佬們見多識廣, 可以給出一些可能性的猜想, 還是很有幫助的.

當你發現某臺機器無論做什麼都慢, 而 cpu 和內核卻不是瓶頸的時候, 那有可能是內核慢了.

希望本文能對大家未來排查集羣問題時有所幫助.


原文鏈接:https://corvo.myseu.cn/2021/03/21/2021-03-21-%E8%AE%B0%E4%B8%80%E6%AC%A1kubernetes%E6%9C%BA%E5%99%A8%E5%86%85%E6%A0%B8%E9%97%AE%E9%A2%98%E7%9A%84%E6%8E%92%E6%9F%A5/



你可能還喜歡

點擊下方圖片即可閱讀

工程師應該怎麼學習

雲原生是一種信仰 🤘


關注公衆號

後臺回覆◉k8s◉獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!



點擊 "閱讀原文" 獲取更好的閱讀體驗!


發現朋友圈變“安靜”了嗎?

本文分享自微信公衆號 - 雲原生實驗室(cloud_native_yang)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章