ACK Net Exporter 與 sysAK 出擊:一次深水區的網絡疑難問題排查經歷

作者:謝石、碎牙

不久前的一個週五的晚上,某客戶A用戶體驗提升羣正處在一片平靜中,突然,一條簡短的消息出現,打破了這種祥和:

“我們在ACK上創建的集羣,網絡經常有幾百毫秒的延遲。"

偶發,延遲,幾百毫秒。這三個關鍵字迅速集中了我們緊張的神經,來活兒了, 說時遲,那時快,我們馬上就進入到了客戶的問題攻堅羣。

問題的排查過程

常規手段初露鋒芒

客戶通過釘釘羣反饋之前已經進行了基本的排查,具體的現象如下:

  1. 不同的容器之間進行rpc調用時出現延遲,大部份請求在較快,客戶的測試方法中,30min可以出現幾十次超過100ms的延遲。
  2. 延遲的分佈最大有2s,vpc方面已經進行了抓包分析,看到了間隔200ms~400ms的重傳報文與出事報文在比較接近的時間裏出現在node中。

30min內出現幾十次的頻率,確實是比較離譜的,從客戶提供的信息中,我們發現了一個非常熟悉的現象:

正常發送的報文和重傳的報文發出的時間相差400ms,他們在NC物理機/MOC卡上相差400ms,但是幾乎同時在ecs節點中被抓包捕捉到。

image.png

這樣的現象曾經出現過,比較常見的原因就是,ecs節點處理網絡數據包的中斷下半部動作慢了,按照經驗通常是100ms到500ms之間,如下圖所示:

  1. 在第一個NC抓包時機的時候,第一個正常的數據包到達了,並且進入了ecs。
  2. ecs的中斷下半部處理程序ksoftirqd並沒有及時完成處理,因爲某些原因在等待。
  3. 等待時間超過了客戶端的RTO,導致客戶端開始發起重傳,此時重傳的報文也到了第一個NC抓包時機。
  4. 正常報文和重傳的報文都到達了ecs內部,此時ksoftirqd恢復正常並開始收包。
  5. 正常報文和重傳報文同時到達tcpdump的第二次抓包時機,因此出現了上述的現象。

image.png

出現了這種現象,我們的第一反應就是,肯定是有什麼原因導致了節點上存在軟中斷工作進程的調度異常。隨後我們聯繫客戶進行復現,同時開始觀察節點的CPU消耗情況(由於客戶的操作系統並不是alinux,所以只能夠移植net-exporter中斷調度延遲檢測工具net_softirq進行捕捉),在客戶復現的幾乎同時,我們觀察到了現象:

  1. 部分CPU存在極高的sys佔用率,顯示佔用CPU較高的進程竟然是:kubelet。
  2. 存在比較明顯的軟中斷調度延遲,毫無疑問,也是kubelet造成的。

到這裏,問題就變成了,爲什麼kubelet會佔用這個高的sys狀態的CPU。

image.png

sys上下文的CPU調用,通常是由於系統調用操作時,內核進行操作產生的。通過對kubelet進程進行pprof的profiling採集,我們驗證了這一點,kubelet一定是在大量進行syscall,從而讓內核不停的爲他打工,進而干擾了ksofirqd的調度。

爲了進一步定位元兇,我們使用opensnoop進行了一段時間的捕捉,查看kubelet的文件打開行爲,在短短的10s中,kubelet進行了10w次以上的文件打開操作,撈了一部分kubelet嘗試打開的文件,結果發現他們的格式大概是類似於這樣的格式:

/sys/fs/cgroup/cpuset/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podd9639dfe_2f78_40f1_a508_af09ca0c6c90.slice/docker-bcc4b36adcd83ce909541dbcf8d16828275e94ba13eafeae77ed24543ca82aad.scope/uid_0/pid_673/cpuset.cpus

看這個路徑,很明顯是一個cgroupfs的文件,作爲容器技術的基石,cgroup子系統的文件記錄着容器運行狀態的關鍵信息,kubelet去去讀cgroup信息看起來非常合理,只是10s內進行10w次的讀取操作,怎麼看也不是一個合理的行爲,我們對比了kubelet的版本,發現客戶雖然操作系統是特殊的,但是kubelet卻是很尋常,沒有什麼特別,然後我們對比了正常的集羣中的cgroup,發現正常集羣中的文件數量要遠遠小於客戶有問題的集羣:

# 客戶集羣的文件數量
[root@localhost ~]# find  /sys/fs/cgroup/cpu/ -name "*" | wc -l
182055

# 正常的集羣的文件數量
[root@localhost ~]# find  /sys/fs/cgroup/cpu/ -name "*" | wc -l
3163

那麼文件到底多在哪裏呢?

我們開始對比客戶的cgroup子系統與正常集羣之間的差異,果然,客戶集羣的cgroup子系統中的文件頗有玄機,對比如下:

# 客戶集羣的文件
/sys/fs/cgroup/cpuset/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podd9639dfe_2f78_40f1_a508_af09ca0c6c90.slice/docker-bcc4b36adcd83ce909541dbcf8d16828275e94ba13eafeae77ed24543ca82aad.scope/uid_0/pid_673/cpuset.cpus

# 正常集羣對應路徑
[root@localhost ~]# ls -l /sys/fs/cgroup/systemd/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod2315540d_43ae_46b2_a251_7eabe02f4456.slice/docker-261e8046e4333881a530bfd441f1776f6d7eef4a71e35cd26c7cf7b992b61155.scope/
總用量 0
-rw-r--r-- 1 root root 0 2月  27 19:57 cgroup.clone_children
-rw-r--r-- 1 root root 0 2月  27 13:34 cgroup.procs
-rw-r--r-- 1 root root 0 2月  27 19:57 notify_on_release
-rw-r--r-- 1 root root 0 2月  27 19:57 pool_size
-rw-r--r-- 1 root root 0 2月  27 19:57 tasks

很明顯,客戶的cgroup子系統中,比我們正常的ACK集羣要多了兩層,而uid/pid這樣的格式,很明顯是用於做用戶和進程的區分,應該是客戶有意爲之,隨後我們和客戶進行了溝通,但是不巧的是,客戶對這些路徑的來龍去脈也不清楚,但是提供了一個很關鍵的信息,客戶運行的容器是androd容器。雖然沒有真正接觸過android容器,但是很容易聯想到,android對多個用戶和多個Activity進行資源上的隔離,於是爲了證明這些多出來的cgroup文件是由於android容器創建,我們通過eBPF進行了簡單的捕捉,果然,能看到容器內的進程在進行cgroupfs的操作!

image.png

可以看到,pid爲210716的init進程在進行cgroup的初始化動作,這個init進程很明顯是客戶android容器中的1號進程,隨後我們查看了客戶提供的操作系統內核源碼,果然,客戶的patch中存在對這部分不一樣的cgroup層級的兼容。。

看起來問題似乎有了解釋,客戶的業務創建了大量非預期的cgroup文件,導致kubelet在讀取這部分文件時在內核態佔據了大量的計算資源,從而干擾到了正常的收包操作。

那麼怎麼解決呢?很明顯,客戶的業務改造是一件難以推動的事情,重擔只能落在了kubelet上,隨後kubelet組件經過多輪修改和綁定與net_rx中斷錯開的CPU,解決了這種頻繁的偶發超時問題。

棘手問題陰魂不散

按照一般的常見劇本,問題排查到了這裏,提供了綁核的解決方案後,應該是喜大普奔了,但是每年總有那麼幾個不常見的劇本。在進行了綁核操作之後,延遲的發生確實成了小概率事件,然而,依然有詭異的延遲以每天十多次的概率陰魂不散。

上一個現象是由於有任務在內核執行時間過久影響調度導致,我們在net_softirq的基礎上,引入了內核團隊的排查利器sysAK,通過sysAK的nosched工具嘗試一步到位,直接找到除了kubelet之外,還有哪些任務在搗亂。

在經歷漫長的等待後,發現這次問題已經和kubelet無關,新的偶發延遲現象中,大部份是具有這樣的特徵:

  1. 延遲大概在100ms左右,不會有之前超過500ms的延遲。
  2. 延遲發生時,不再是ksoftirqd被調度干擾,而是ksoftirqd本身就執行了很久,也就是說,ksoftirqd纔是那個搗亂的任務。。。

image.png

上圖中可以發現,在延遲發生時,ksoftirqd本身就執行了很久,而kubelet早已經潤到了和網絡rx無關的CPU上去了。

遇到這樣的偶發問題,多年的盲猜經驗讓我嘗試從這些偶發延遲中找一下規律,在對比了多個維度之後,我們發現這些延遲出現大致有兩個特徵:

  1. 出現在固定的核上,一開始是0號CPU,我們將0號CPU也隔離開之後,發現換成了24號CPU出現了一樣的現象,看起來與CPU本身無關。
  2. 出現的時間差比較固定,經過我們對比發現,差不多每隔3小時10分鐘左右會有一波偶發超時,而在此期間,流量並沒有較大的波動。

這樣奇怪的延遲,很明顯已經不再是一個單純的網絡問題,需要更加有力的抓手來幫助我們定位。

硬核方法鞭辟入裏

排查到這一步,已經不再是一個單純的網絡問題了,我們找到內核團隊的同學們一起排查,面對這種週期性的大部分進程包括內核關鍵進程都出現的卡頓,我們通過sysAK nosched捕捉到了軟中斷執行時間過久的內核態信息:

image.png

從堆棧信息中可以發現ksoftirqd本身並沒有執行好事很久的操作,通常讓人懷疑的就是net_rx_action的內核態收包動作慢了,但是經過多輪驗證,我們發現當時收包的動作並沒有出現明顯的變化,於是我們把目光集中在了page的分配和釋放操作中。

在閱讀了__free_pages_ok的代碼後,我們發現了在釋放page的過程中是有獲取同步鎖的操作,同時會進行中斷的關閉,那麼,如果對於page所在的zone的鎖的爭搶過程出現了卡頓,就會導致__free_pages_ok本身執行變慢!

static void __free_pages_ok(struct page *page, unsigned int order)
{
  unsigned long flags;
  int migratetype;
  unsigned long pfn = page_to_pfn(page);

  if (!free_pages_prepare(page, order, true))
    return;

  migratetype = get_pfnblock_migratetype(page, pfn);
    // 這裏在進行進行關中斷
  local_irq_save(flags);
  __count_vm_events(PGFREE, 1 << order);
  free_one_page(page_zone(page), page, pfn, order, migratetype);
  local_irq_restore(flags);
}

static void free_one_page(struct zone *zone,
        struct page *page, unsigned long pfn,
        unsigned int order,
        int migratetype)
{
    // 這裏有一個同步鎖
  spin_lock(&zone->lock);
  if (unlikely(has_isolate_pageblock(zone) ||
    is_migrate_isolate(migratetype))) {
    migratetype = get_pfnblock_migratetype(page, pfn);
  }
  __free_one_page(page, pfn, zone, order, migratetype, true);
  spin_unlock(&zone->lock);
}

考慮到這一點,我們打算使用sysAK irqoff來追蹤是否存在我們推測的情況。在經歷了好幾個三小時週期的嘗試後,我們終於看到了預測中的信息:

image.png

從上圖可以很明顯的查看到,在一次ksoftirqd出現延遲的同時,有一個用戶進程在長時間的持有zone->lock!!!

到了這裏,內核週期性產生收包進程執行時間過久的元兇也找到了,如下圖:

image.png

當客戶的icr_encoder週期性執行到pagetypeinfo_showfree_print方法時,會長時間的持有zone->lock 的鎖,儘管不在相同的cpu上,但是持有page關聯的zone的鎖仍然會導致此時其他關聯的進程產生延遲。

pagetypeinfo_showfree_print這個函數是/proc/pagetyeinfo文件註冊在procfs中的方法。

當有用戶態的讀取/proc/pagetyeinfo操作時,內核會執行pagetypeinfo_showfree_print來獲取當前page的分配數據,我們在本地環境進行測試時,發現直接訪問這個文件,並不會產生比較大的延遲:

image.png

那麼爲什麼讀取/proc/pagetyeinfo在客戶的環境中會產生這麼大的耗時呢?經過對比觀察,我們發現了原因:

image.png

上圖是在客戶環境中出現延遲後的pagetypeinfo的結果,可以發現一個很誇張的區別,那就是某些類型的page的數量已經到到了10w以上的數字。

而分析pagetypeinfo_showfree_print的代碼可以發現,內核在進行統計時實際上是會在搶佔鎖的同時去遍歷所有的page!!

static void pagetypeinfo_showfree_print(struct seq_file *m,
          pg_data_t *pgdat, struct zone *zone)
{
  int order, mtype;

  for (mtype = 0; mtype < MIGRATE_TYPES; mtype++) {
    for (order = 0; order < MAX_ORDER; ++order) {
          // 在持有zone->lock的情況遍歷各個order的area->free_list
      area = &(zone->free_area[order]);
      list_for_each(curr, &area->free_list[mtype]) {
        if (++freecount >= 100000) {
          overflow = true;
          break;
        }
      }
      seq_printf(m, "%s%6lu ", overflow ? ">" : "", freecount);
      spin_unlock_irq(&zone->lock);
            // 可能這個問題已經有人發現,因此特地增加了支持內核態搶佔的調度時機
      cond_resched();
      spin_lock_irq(&zone->lock);
    }
  }
}

從代碼中不難發現,每次進行遍歷之後就會進行unlock,而在下一次遍歷之前會進行lock,當page數量過多的時候,就會產生佔據鎖過久的情況,從而引發上述的問題。

與此同時,也有一些不規律的偶發延遲的出現,我們也通過net-exporter和sysAK定位到了兩個不那麼規律的根因:

  1. 由於ipvs老版本的estimation_timer導致的偶發延遲,目前內核團隊已經修復,詳見:https://github.com/alibaba/cloud-kernel/commit/265287e4c2d2ca3eedd0d3c7c91f575225afd70f
  2. 由於多numa引發的page migration,導致進程卡頓進而產生延遲,多個類似的案例出現,後面會對這種場景進行詳細的分析。

問題的背後

後續客戶通過這些方式解決了大多數的延遲:

  1. 針對kubelet進行綁核操作,與net_rx的中斷對應的cpu錯開。
  2. 通過cronjob定期進行內存碎片的回收。

經過優化後的網絡,偶發延遲的出現已經大大減少,滿足了交付標準。

一個看似不經意問題的背後,其實大有玄機。在這個問題的排查中,涉及到的技術棧包括內核網絡、內核內存管理、procfs虛擬文件系統等技術領域,當然更離不開阿里雲內部多個技術團隊的通力合作。

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