案例 | 騰訊廣告 AMS 的容器化之路

騰訊雲原生發表於2021-06-28

作者

張煜,15年加入騰訊並從事騰訊廣告維護工作。20年開始引導騰訊廣告技術團隊接入公司的TKEx-teg,從業務的日常痛點並結合騰訊雲原生特性來完善騰訊廣告自有的容器化解決方案

專案背景

騰訊廣告承載了整個騰訊的廣告流量,並且接入了外部聯盟的請求,在所有流量日益增大的場景下,流量突增後如何快速調配資源甚至自動排程,都成為了廣告團隊所需要考慮的問題。尤其是今年整體廣告架構(投放、播放)的條帶化容災優化,對於按需分配資源、按區域分配資源等功能都有著更強的依賴。
在廣告內部,播放流系統承載了整個廣告播出的功能,這裡的穩定性直接決定了整個騰訊廣告的收入,以下為架構圖:

業務特點:

  • 請求量大,日均請求量近千億級別,機器量佔了AMS自有機器量的60%以上,整體效能即使是少量的上下波動都會涉及大量機器的變動。
  • 鏈路拓撲復雜及效能壓力極大,整個播放鏈路涉及40+的細分模組。在100~200毫秒(不同流量要求有差異)的極短時間內,需要訪問所有模組,計算出一個最好的廣告。
  • 計算密集型,大量的使用了綁核和關核能力,來面對上百萬個廣告訂單檢索的壓力。

上雲方案選型

在20年騰訊廣告已經在大規模上雲,主要使用的是 AMD 的 SA2 cvm 雲主機,並且已經完成了對網路、公司公共元件、廣告自有元件等相容和除錯。在此基礎上,基於 CVM 的 Node 雲原生也開始進行調優和業務使用,彈性伸縮、Docker 化改造,大量使用各種 PAAS 服務,充分發揮雲的高階功能。
以下為廣告使用的 TKE 架構:

  • 前期資源準備(左上):從騰訊內部雲官網平臺申請 CVM 和 CLB 等資源,並且同時雲官網申請 master,node,pods 所需要的 subnet 網段( subnet 是區分割槽域的,例如深圳光明,需要注意 node 和 pods 在區域上的網段分配,需要一致)。CVM 和 CLB 都匯入至 TKEx-teg 中,在選擇FIP模式的時候,產生的 PODS 從分配好的 subnet 中獲取自己的 EIP。
  • 倉庫及映象的使用(右上):廣告運維側提供基礎映象(mirrors.XXXXX.com/XXXXX/XXXXXXX-base:latest),業務在 from 基礎映象的同時,拉取 git 後通過藍盾進行映象 build,完成業務映象的構建。
  • 容器的使用模式(下半部分):通過 TKEx-teg 平臺,拉取業務映象進行容器的啟動,再通過 clb、北極星等服務進行對外使用。

容器化歷程(困難及應對)

困難一:通用性
(1)面對廣告84個技術團隊,如何實現所有業務的適配
(2)映象管理:基礎環境對於業務團隊的透明化
(3)騰訊廣告容器配置規範
困難二:CPU密集型檢索
(1)廣告訂單數量:百萬級
(2)綁核:各個應用之間的 CPU 綁核隔離
(3)關核:關閉超執行緒
困難三:有狀態服務升級中的高可用
(1)廣告資源在容器升級過程中的持續可用
(2)在迭代、銷燬重建過程中的持續高可用

通用性

1. 廣告基礎映象介紹

廣告運維側提供了一套覆蓋大部分應用場景的基礎映象,其中以 XXXXXXX-base:latest 為基礎,這裡整合了原先廣告在物理機上面的各個環境配置、基礎 agent、業務 agent 等。
並且基於這個基礎映象,提供了多個業務環境映象,映象列表如下:

mirrors.XXXXX.com/XXXXX/XXXXXXX-base:latest
mirrors.XXXXX.com/XXXXX/XXXXXXX-nodejs:latest
mirrors.XXXXX.com/XXXXX/XXXXXXX-konajdk:latest
mirrors.XXXXX.com/XXXXX/XXXXXXX-python:3
mirrors.XXXXX.com/XXXXX/XXXXXXX-python:2
mirrors.XXXXX.com/XXXXX/XXXXXXX-tnginx:latest

具體映象使用情況如下:

在廣告的基礎映象中,由於許可權集設定未使用到 systemd,所以使用啟動指令碼作為1號 PID,並且在基礎映象中內建了一份通用的騰訊通用 Agent & 廣告獨有 Agent 的啟動指令碼,在業務映象啟動過程中,可以在各自的啟動指令碼中選擇是否呼叫。

2. 容器化CI/CD

原先大量使用了其他平臺的 CD 部分,但現在使用 TKE 後,其他平臺已經無法使用。而 TKEx-teg 上的持續化整合部分對於自動化流水線實現較弱,需手動參與,所以在廣告內部引入的 CI/CD 方案是騰訊內部的持續化整合和持續化部署方案:藍盾。

這裡全程實現流水線釋出,除了稽核外無需人工參與,減少人為因素的問題影響。

stage1:主要使用手動觸發、git 自動觸發、定時觸發、遠端觸發

  • 手動觸發:容易理解,需要手動點選後開始流水線。

  • 自動觸發:當 git 產生 merge 後,可自動觸發該流水,適用於業務的敏捷迭代。

  • 定時觸發:定時每天某個時間點開始整個流水線的觸發,適用於 oteam 協同開發的大型模組,預定多少時間內迭代一次,所有參與的人員來確認本次迭代的修改點。

  • 遠端觸發:依賴外部其他平臺的使用,例如廣告的評審機制在自己的平臺上(Leflow),可以在整個釋出評審結束後,遠端觸發整個流水線的執行。

stage2 & stage3:持續化整合,拉取 git 後進行自定義的編譯

藍盾提供了預設的CI映象進行編譯,不進行二進位制編譯的可以選擇預設(例如 php、java、nodejs等),而後臺業務騰訊廣告內部大量使用 blade,通常使用 mirrors.XXXXXX.com/XXXXXX/tlinux2.2-XXXXXX-landun-ci:latest 作為構建映象,此映象由騰訊廣告效能團隊提供,內部整合了騰訊廣告在持續化整合過程中的各種環境和配置。
編譯完成後通過映象外掛,依賴 git 庫中的 dockerfile 進行映象 build,然後推送至倉庫中,同時保留一份在織雲中。

stage4:線上灰度 set 釋出,用於觀察灰度流量下的資料表現。通過叢集名、ns 名、workload 名來對某個工作負載進行映象 tag 的迭代,並且使用一份 TKEx-teg 內部的 token 進行認證。

stage5:確認 stage4 沒問題後,開始線上的全量,每次都經過稽核確認。

stage6 & stage7:資料統計。

另外有一個藍盾中機器人群通知的功能,可以自定義把需要告知的流程資訊,推送到某個企業微信群中,以便大家進行確認並稽核。

3. 騰訊廣告容器配置規範

廣告內部的母機都是用的騰訊雲星星海 AMD(SA2),這裡是90核超執行緒 cpu+192G 記憶體,磁碟使用的是高速雲硬碟3T,在日常使用中這樣的配置這個機型是騰訊雲現階段能提供的最大機型(已經開始測試SA3,最高機型配置會更大)。

  • 所以業務在使用的時候,不建議 pods 核數太大(例如超過32核),由於TKE親和性的預設設定會盡量在空閒的母機中拉取各個容器,這樣在使用到中後期(例如叢集已經使用了2/3)導致的碎片化問題,會導致超過32核的 pods 幾乎無法擴容。所以建議使用者在拉取容器的時候如果可以橫向擴容,都是把原有的高核服務拆分成更多的低核 pods(單 pods 核數減半,整體 pods 數兩倍)。

  • 在建立 workload 的時候,對日誌目錄進行 emptyDir 臨時目錄的掛載,這樣可以保證在升級過程中該目錄不會丟失資料,方便後續的問題排查。(銷燬重建仍舊會刪除該目錄下的所有檔案)

如果是已經上線的 workload,則可以通過修改 yaml 來增加目錄的掛載:

        - mountPath: /data/log/adid_service
          name: adid-log
      volumes:
      - emptyDir: {}
        name: adid-log
  • 在騰訊廣告內部,大量使用了條帶化功能,也就是服務不在受限於上海、深圳、天津這樣的範圍。條帶化可以做到更細化的區分,例如上海-南匯這樣以機房為單位的部署,這樣可以實現容災的實現(大部分的網路故障都是以機房為單位,這樣可以快速切到另一路)。也可以實現條帶化部署後的耗時減少,例如同上海的兩個機房,由於距離的原因自帶3ms的耗時,在大包傳輸的過程中,跨機房部署的耗時問題會被放大,在廣告內部有出現10ms的 gap 出現。

所以在廣告的 TKE 條帶化使用過程中,我們會去通過 label 的方式來指定機房選擇,騰訊雲對各個機房的 CVM 都預設打了 label,可以直接呼叫。

存量的 workload 也可以修改 yaml 來進行強制的排程。

      spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: failure-domain.beta.kubernetes.io/zone
                operator: In
                values:
                - "370004"
  • 廣告內部後臺都是建議使用4~16核的容器配置,前端平臺大多使用1核,這樣可以保證在叢集使用率偏高的場景下,也可以進行緊急擴容。並且如果希望親和性強制隔離各個 pods,也可以使用如下配置進行隔離(values 為具體的 workload 名稱):
         affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: k8s-app
                  operator: In
                  values:
                  - proxy-tj
              topologyKey: kubernetes.io/hostname
            weight: 100

4. HPA設定

在容器化的使用過程中,有兩種方式可以面對業務流量的突增。

  • 設定容器的request和limit,request資源可以理解為確定100%可以分配到業務的,而Limit則是超賣的資源,是在buffer池中共享的資源部分。這樣的好處在於每個業務可以配置平時正常使用的request資源,當發生流量突增時由limit部分的資源來承擔超過request之後的效能問題。

注:但這裡的超賣並不是萬能的,他有兩個問題也非常明顯:

  • 在當前node剩餘使用的資源如果小於limit設定的值,會發生PODS自動遷移到其他node。2)如果需要使用到綁核功能,是需要qos的功能,這裡強制request和limit必須設定一樣的配置。

  • 設定自動擴容,這裡可以根據大家各自的效能瓶頸來設定閾值,最後來實現自動擴容的功能。

大部分的業務都是cpu的效能為瓶頸,所以通用方式可以針對cpu的request使用率來設定擴容

百萬廣告訂單檢索

1. 廣告核心檢索模組

廣告對於每個流量都存在一個站點集的概念,每個站點集分了不同的 set,為了區分開各個流量之間的影響和不同的耗時要求。在20年我們對每個模組都拆出了一個 set 進行了 CVM 的上雲,在此基礎上,21年我們針對核心模組 sunfish 進行了容器化的上雲。這個模組的特點就是 CPU 高度密集型的檢索,所以他無法使用超執行緒(超執行緒的排程會導致耗時增加),並且內部的程式都進行了綁核處理(減少多程式之間的 CPU 排程)。

2. 容器綁核

這裡是廣告最大的一個特性,而且也是 TKE 和 CVM/物理機的最大區別。

在 CVM/物理機的場景中,虛擬化技術是可以從 /proc/cpuinfo 中獲取到正確的 cpu 單核資訊,所以在原先的業務綁核過程中,都是從 /proc/cpuinfo 中獲取 cpu 的核數和資訊,進行每個程式的綁核操作。

但在容器中 cpu 資訊產生了很大的偏差,原因是 /proc/cpuinfo 是根據容器自身的核數進行的排序,但這個順序並不是該容器在母機上的真實cpu序列,真實的 cpu 序列需要從 /sys/fs/cgroup/cpuset/cpuset.cpus 中獲取,例如下圖兩個舉例:

/proc/cpuinfo 中的 CPU 序號展示(虛假):

/sys/fs/cgroup/cpuset/cpuset.cpus 中的 CPU 序號展示(真實):

從上面兩張圖中可以看到,/proc/cpuinfo 中只是按照分配到的 cpu 核數進行了一個排序,但並不是真正對應母機的核數序列,這樣在綁核的過程中,如果繫結了15號核,其實是對母機的15號核進行繫結,但母機的第15個CPU並不是分配給了該容器。

所以需要從 /sys/fs/cgroup/cpuset/cpuset.cpus 中獲取在母機中真正對應的 cpu 序列,才能實現綁核,如上圖2。並且可以通過在啟動指令碼中加入下面的命令,可以實現對 cpu 真實核數的格式轉換,方便繫結。

cpuset_cpus=$(cat /sys/fs/cgroup/cpuset/cpuset.cpus)
cpu_info=$(echo ${cpuset_cpus} | tr "," "\n")
for cpu_core in ${cpu_info};do
  echo ${cpu_core} | grep "-" > /dev/null 2>&1
  if [ $? -eq 0 ];then
    first_cpu=$(echo ${cpu_core} | awk -F"-" '{print $1}')
    last_cpu=$(echo ${cpu_core} | awk -F"-" '{print $2}')
    cpu_modify=$(seq -s "," ${first_cpu} ${last_cpu})
    cpuset_cpus=$(echo ${cpuset_cpus} | sed "s/${first_cpu}-${last_cpu}/${cpu_modify}/g")
  fi
done
echo "export cpuset_cpus=${cpuset_cpus}" >> /etc/profile

source /etc/profile 呼叫環境變數,轉換後的格式如下:

注意:綁核依賴 qos 配置(也就是 request 和 limit 必須設定成一致)

3. 關閉超執行緒

超執行緒在大部分場景下都是開啟的,但在計算密集型的場景下需要關閉,此處的解決方法是在申請CVM的時候就選擇關閉超執行緒。

然後對關核的母機做汙點並打上 label,讓普通的拉取不會拉到關核母機,在需要分配關核資源的時候,在 yaml 中開啟容忍和設定 label,就可以獲取到相應的關核資源。

yunti 資源申請時的關核配置:

有狀態服務升級中的高可用

無狀態容器的升級最為簡單,業務埠的可用即為容器的可用。

但有狀態業務的啟動較為複雜,需要在啟動指令碼中完成狀態的前期準備工作。在廣告這裡主要涉及在廣告訂單資源的推送和載入。

1. 廣告資源在容器升級過程中的持續可用

容器的升級較於物理機最大的區別就在於容器會銷燬原有的容器,然後從新的映象中拉起新的容器提供服務,原有容器的磁碟、程式、資源都會被銷燬。

但廣告這裡的廣告訂單資源都是百萬級別,檔案如果在每次升級都需要重新拉取,會直接導致啟動過慢,所以我們在容器中都加入了臨時掛在目錄。

案例 | 騰訊廣告 AMS 的容器化之路 案例 | 騰訊廣告 AMS 的容器化之路

這樣的掛載方式,可以讓容器在升級過程中保留上述目錄下的檔案,不需要重新拉取。但 emptyDir 只能在升級場景下保留,銷燬重建仍舊會銷燬後重新拉取,以下為存量服務直接修改 yaml 的方法:

              volumeMounts:
        - mountPath: /data/example/
          name: example-bf
      volumes:
      - emptyDir: {}
        name: example-bf

2. 升級過程中的業務高可用

在業務迭代的過程中,其實有兩個問題會導致業務提供了有損服務。

  • 如果針對 workload 關聯了負載均衡,這裡容器在執行啟動指令碼的第一句話就會變成 running 可用狀態,這時候會幫大家投入到關聯過的負載均衡中,但這時候業務程式並未就緒,尤其是有狀態服務必須得完成前置的狀態後才可以啟動。這時候業務就會由於加入了不可用的服務導致業務報錯。

  • 在升級的過程中,除了 deployment 模式的升級外,其他都是先銷燬原有的容器,再拉取新的容器服務。這時候就產生了一個問題就是,我們在升級的時候是先從關聯過的負載均衡裡剔除,然後立馬進入到銷燬階段。如果上游是L5呼叫那其實並不能快速同步到該 pods 已經剔除,會繼續往下游已經銷燬的容器中傳送請求,這時候整個業務就會報錯。

所以我們這裡的一個最主要的思路就是:

  • 如何把業務的狀態,和容器狀態進行繫結。
  • 在升級/銷燬重建的過程中,是否可以做一個後置指令碼,在銷燬之前我們可以做一些邏輯處理,最簡單的就是sleep一段時間。

這裡我們引入業務的兩個升級的概念:

  • 探針就緒

  • 後置指令碼

    1 )探針就緒
    需要在workload建立的時候,選擇針對埠進行做就緒探測,這樣在業務埠啟動後才會投入到關聯好的負載均衡裡。

    案例 | 騰訊廣告 AMS 的容器化之路 也可以在存量的workload中修改yaml
      readinessProbe:
          failureThreshold: 1
          periodSeconds: 3
          successThreshold: 1
          tcpSocket:
            port: 8080
          timeoutSeconds: 2

出現類似的unhealty,就是在容器啟動後,等待業務埠的可用的過程

2 )後置指令碼

後置指令碼的核心功能就是在從關聯的負載均衡中剔除後,到銷燬容器之間,可以執行一系列業務自定義的動作。

執行順序是:提交銷燬重建/升級/縮容的操作 → 剔除北極星/L5/CLB/service → 執行後置指令碼 → 銷燬容器

最簡單的一個功能就是在上游使用L5呼叫的時候,在剔除L5後sleep 60s,可以讓上游更新到該pods剔除後再進行銷燬操作。

           lifecycle:
          preStop:
            exec:
              command:
              - sleep
              - "60"
  lifecycle:
          preStop:
            exec:
              command:
              - /data/scripts/stop.sh

長期的使用經驗來看,主要問題在L5這方面,如果是大流量的服務,那sleep 60s 內就行,如果是請求量較小的,希望一個報錯都沒的,需要 sleep 90s。

成果展示

CVM 和 TKE 的使用率和耗時對比

這裡在相同配置下,對比了普通機器 CVM 和 TKE 容器之間的 CPU 和耗時,可以看到基本沒太大差異,耗時也無變化。

CVM:


TKE:

整體收益

結語

不同於其他從底層介紹雲原生的分享,本文主要從業務角度,來介紹雲原生在大型線上服務中的優勢和使用方法,並結合騰訊廣告自有的特點及策略,來實現騰訊廣告在高併發、自動化等場景下的容器化實踐。
對於業務團隊來說,任何的工作都是從質量、效率以及成本出發,而云原生則是能從這三個方面都有所提升,希望未來能有更多來自於我們騰訊廣告的分享。


容器服務(Tencent Kubernetes Engine,TKE)是騰訊雲提供的基於 Kubernetes,一站式雲原生 PaaS 服務平臺。為使用者提供整合了容器叢集排程、Helm 應用編排、Docker 映象管理、Istio服務治理、自動化DevOps以及全套監控運維體系的企業級服務。
【騰訊雲原生】雲說新品、雲研新術、雲遊新活、雲賞資訊,掃碼關注同名公眾號,及時獲取更多幹貨!!

相關文章