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

做者

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

項目背景

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

業務特色:java

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

上雲方案選型

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

  • 前期資源準備(左上):從騰訊內部雲官網平臺申請 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)在迭代、銷燬重建過程當中的持續高可用python

通用性

1. 廣告基礎鏡像介紹

廣告運維側提供了一套覆蓋大部分應用場景的基礎鏡像,其中以 XXXXXXX-base:latest 爲基礎,這裏集成了原先廣告在物理機上面的各個環境配置、基礎 agent、業務 agent 等。
而且基於這個基礎鏡像,提供了多個業務環境鏡像,鏡像列表以下:linux

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:latestnginx

具體鏡像使用狀況以下:
git

在廣告的基礎鏡像中,因爲權限集設置未使用到 systemd,因此使用啓動腳本做爲1號 PID,而且在基礎鏡像中內置了一份通用的騰訊通用 Agent & 廣告獨有 Agent 的啓動腳本,在業務鏡像啓動過程當中,能夠在各自的啓動腳本中選擇是否調用。docker

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. 廣告資源在容器升級過程當中的持續可用

容器的升級較於物理機最大的區別就在於容器會銷燬原有的容器,而後重新的鏡像中拉起新的容器提供服務,原有容器的磁盤、進程、資源都會被銷燬。

但廣告這裏的廣告訂單資源都是百萬級別,文件若是在每次升級都須要從新拉取,會直接致使啓動過慢,因此咱們在容器中都加入了臨時掛在目錄。

<img src="https://main.qcloudimg.com/raw/a4579ab4826d06e19e688e283ed2fee3.png" style="zoom:67%;" />

<img src="https://main.qcloudimg.com/raw/6865d1b811ed0d3060083b65d22a5ee6.png" style="zoom:67%;" />

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

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

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

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

  • 若是針對 workload 關聯了負載均衡,這裏容器在執行啓動腳本的第一句話就會變成 running 可用狀態,這時候會幫你們投入到關聯過的負載均衡中,但這時候業務進程並未就緒,尤爲是有狀態服務必須得完成前置的狀態後才能夠啓動。這時候業務就會因爲加入了不可用的服務致使業務報錯。
  • 在升級的過程當中,除了 deployment 模式的升級外,其餘都是先銷燬原有的容器,再拉取新的容器服務。這時候就產生了一個問題就是,咱們在升級的時候是先從關聯過的負載均衡裏剔除,而後立馬進入到銷燬階段。若是上游是L5調用那其實並不能快速同步到該 pods 已經剔除,會繼續往下游已經銷燬的容器中發送請求,這時候整個業務就會報錯。

因此咱們這裏的一個最主要的思路就是:

  • 如何把業務的狀態,和容器狀態進行綁定。
  • 在升級/銷燬重建的過程當中,是否能夠作一個後置腳本,在銷燬以前咱們能夠作一些邏輯處理,最簡單的就是sleep一段時間。

這裏咱們引入業務的兩個升級的概念:

  • 探針就緒
  • 後置腳本

    1 )探針就緒
    須要在workload建立的時候,選擇針對端口進行作就緒探測,這樣在業務端口啓動後纔會投入到關聯好的負載均衡裏。

    <img src="https://main.qcloudimg.com/raw/15cdd81315a26cab80a6fb26eefe8700.png" style="zoom:67%;" />
    也能夠在存量的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以及全套監控運維體系的企業級服務。
【騰訊雲原生】雲說新品、雲研新術、雲遊新活、雲賞資訊,掃碼關注同名公衆號,及時獲取更多幹貨!!
相關文章
相關標籤/搜索