近年來愛奇藝快速發展,優質內容層出不窮,愛奇藝廣告也隨之發展和壯大,廣告線上服務同時服務於品牌、中小、DSP 等不同客戶,形成了可以滿足不同需求型別的較為完善的商業廣告變現佈局,廣告庫存涵蓋視訊、資訊流、泡泡社交(愛奇藝的社交平臺)和開機屏等多種場景。愛奇藝效果廣告是 2015 年開始全新搭建的一個廣告投放平臺,隨著資訊流業務的增長,整個投放平臺也經歷了一次大的架構調整和多次重要的升級優化。
愛奇藝廣告投放平臺的概要架構如下圖所示。本文主要介紹線上服務相關的內容,線上投放服務即圖中虛線所框出的部分,主要包括線上的投放和計費服務。
架構背後的業務需求
架構肯定是為業務需求而生的,先來看看我們面對的業務需求及其特點。
愛奇藝效果廣告投放平臺目前採用代理商模式,平臺主要滿足兩大類業務需求:面向代理商(廣告主)的和麵向產品及運營團隊的需求。具體來看看。
1、面向代理商的需求: 本質上是要幫助代理商降低轉化成本
支援多種廣告位:貼片、暫停、浮層、資訊流、視訊關聯位和推薦位等
支援多種結算型別:支援 CPC、CPM 和 CPV 等廣告結算型別,oCPC 結算方式在規劃中
豐富的定向功能:常用定向維度(平臺、地域等)及人群精準定向(地域定向 - 支援區縣級別、人群屬性定向和 DMP 人群定向),關鍵詞定向
靈活的排期及預算設定:支援分鐘粒度的排期設定,支援日預算的任意增減
特殊的業務功能:廣告去重功能、動態創意、創意優選和平滑消耗等,都是為了提升廣告的轉化效果
頻次控制:避免對相同使用者短時間的大量曝光
2、面向產品及運營團隊:主要是提升產品控制能力,促進整體系統的良好運轉
流量控制:通過黑白名單控制某些流量上不可以 / 可以投放哪些廣告
AB 測試功能:影響較大的功能全量釋出之前需要進行 AB 測試以確認效果符合預期
計費相關:延遲曝光不計費,曝光、點選異常檢測及過濾
負反饋:根據使用者反饋自動調整廣告投放策略優化使用者體驗,同時也是對廣告主的一種制約
從上面描述的業務需求可以看出,業務的特點有:
業務邏輯複雜:流程包括很多環節(場景資訊獲取,廣告召回,預算控制,頻次控制,點選率預估,創意優選,平滑消耗,廣告去重,結果排序,結果篩選,概率投放,AB 測試);下圖中綠框的部分僅展示投放服務的主要流程:
業務變更非常快:平均每週 5 次的系統功能變更;
廣告主數量多,訂單量大,訂單平均預算較小,並且訂單設定會頻繁變化。
系統架構
愛奇藝效果廣告於 2016 年正式上線。起步伊始,業務邏輯簡單,廣告和訂單數量較少,整體架構相對比較簡單。為了快速完成系統的搭建和上線應用,複用了品牌廣告投放平臺的架構,並做了剪裁,系統架構圖如下:
接入層 包括 QLB(iQiYi Load Balance)、Nginx 前端機,主要做流量的反向代理和整體的限流與降級功能。
流量分發層:包括策略服務和流量平臺服務;策略服務支援公司層面的策略控制和日常的運營需求;流量平臺服務主要控制流量在各投放平臺上的分配和請求邏輯,投放平臺包括品牌廣告投放平臺,效果廣告投放平臺和外部 DSP。
投放服務:前文介紹的業務邏輯都包含在這裡,由單一的模組來實現。
日誌收集:接收曝光點選等日誌,主要完成計費、頻控和去重等業務邏輯,也是由單一的模組來實現。
計費系統:利用 Redis 主從同步機制把訂單的實時消耗資料同步到投放服務。
頻次系統:使用 Couchbase 機群來做使用者資料儲存。
資料同步層:這一層涉及的資料種類很多,其中相對較重要的有兩種:業務資料和日誌資料,業務資料主要包括廣告的定向、排期和預算等內容。
我們利用業務資料做了兩方面的優化工作:
通過業務資料分發一些對時效性要求不高的資料給到投放服務,避免了一些網路 IO;
在業務資料中進行空間換時間的優化,包括生成索引及一些投放服務所需要的資料的預計算,譬如提前計算計費系統中的 key 值。
隨著業務增長,架構也遇到了一些挑戰。
流量增長:系統上線之後很好地滿足了廣告主對轉化效果的要求,這個正向的效果激發了廣告主對流量的需求,為此產品和運營團隊不斷地開闢新的廣告位,同時愛奇藝的使用者數和流量也在持續增長,這些原因共同為效果廣告平臺帶來了巨大的流量。
廣告主數量和訂單數量增長:這個增長包括兩方面,一方面與流量增長相輔相成,相互促進;愛奇藝的優質流量和良好的轉化效果吸引了更多的廣告主;另一方面,由於商務政策上的原因,廣告主和訂單量在季度末會有階段性的增長。
效能問題:流量和訂單量的增長使得系統的負載快速增加,因為訂單是全量召回的,當訂單量增長到一定數量之後,會使得長尾請求增多,影響整體服務效能,無法通過水平擴容解決。
超投問題:由於曝光和點選的延遲,以及投放計費環路的延遲,不可避免的存在超投問題,這也是廣告系統的固有問題;品牌廣告是先簽訂合同,投放量達到即可按照合同收款,超出部分不會對廣告主收費,品牌廣告預定量都很大,超投比率較小;和品牌廣告不同,效果廣告實時扣費,如果沿用品牌思路的話,超投部分會造成多餘的扣費,而中小廣告主對此非常敏感,也會增加技術團隊問題分析排查工作,同時因為效果廣告的預算少,預算調整變化很快,使得超投比率要比品牌廣告大;針對效果廣告的超投問題,技術團隊要做的事情分成兩個層面,一是保證超投的部分不會計費,不給廣告主帶來損失,二是從根本上減少超投,即減少我們自己的收入損失;分別稱為 超投不計費 和 減少超投;
針對上面的幾個情況,我們的架構做了調整:
對比上線伊始的架構,此階段架構調整體現在以下幾個方面:
投放服務效能優化 – 包括索引分片和增加粗排序模組,主要解決了上述流量增長、廣告主數量訂單增長等方面帶來的效能問題
索引分片是把原來的一份索引拆分成多份,對應的一個請求會被拆分成多個子請求並行處理,這樣每個請求的處理時間會減少,從而有效減少長尾請求數量。
粗排序:全量召回的好處是收益最大化,缺點是效能會隨著訂單量增加而線性下降;粗排序在召回階段過濾掉沒有競爭力的低價值的(ECPM 較低的)廣告,低價值廣告被投放的概率和產生轉化的概率很低,因此粗排序的過濾對整體收入影響很小,同時能有效減少進入後續核心計算邏輯(包括精排序及其他的業務邏輯)的訂單數量,使得服務壓力不隨訂單量而線性增長。
計費服務架構優化 - 主要是提升系統的可擴充套件性和解決超投問題
可擴充套件性通過服務拆分來解決,把單一模組的計費服務拆分成三個模組,拆分之後日誌收集模組對外提供服務,主要職責是接收日誌請求並立即返回,保證極低的響應時間;然後對計費日誌和非計費日誌進行不同的處理;檢測過濾模組主要職責是進行定向檢查和異常日誌識別。計費服務把有效計費資料更新到計費系統。拆分成三個模組之後,每個模組都很簡單,符合微服務基本原則之一:單一職責。
關於超投, 先看第一個問題:超投不計費。
主要難點在於:
同一個廣告的計費請求是併發的;
計費系統是分散式的,出於效能考慮,請求的處理流程需要是無鎖的。
我們在計費系統中解決這個問題的思路如下:
首先,要嚴格準確地計費,就要對並行的請求進行序列處理,Redis 的單執行緒模型天然滿足序列計費的需求,我們決定基於 Redis 來實現這個架構,把計費的邏輯以指令碼的形式在 Redis 執行緒中執行,避免了先讀後寫的邏輯,這樣兩個根本原因都消除了。
接下來的任務就是設計一個基於 Redis 的高可用高效能的架構。我們考慮了兩種可選方案。
方案 1:資料分片,架構中有多個主 Redis,每個主 Redis 儲存一個分數分片,日誌收集服務處理有效計費請求時要更新主 Redis;每個主 Redis 都有對應的只讀從 Redis,投放服務根據分片演算法到對應的從 Redis 上獲取廣告的實時消耗資料。
該方案的優點是可擴充套件性強,可以通過擴容來解決效能問題;缺點是運維複雜,要滿足高可用系統架構還要更復雜;
方案 2:資料不分片,所有的計費請求都匯聚到唯一的主 Redis,同時只讀從 Redis 可以下沉到投放服務節點上,可以減少網路 IO,架構更加簡潔;但主 Redis 很容易成為效能的瓶頸;
在實踐中我們採用了第二種 不分片 的方案。主要基於以下考慮:
在業務層面,效果廣告中有很大比率的是 CPC 廣告,而點選日誌的數量相對較少,基本不會對系統帶來效能壓力;對於剩下的 CPM 計費的廣告,系統會對計費日誌進行聚合以降低主 Redis 的壓力;因為從 Redis 是下沉到投放上的,可以不做特殊的高可用設計;主 Redis 的高可用採用 Redis Sentinel 的方案可以實現自動的主從切換,日誌收集服務通過 Sentinel 介面獲取最新的主 Redis 節點。
在序列計費的情形下,最後一個計費請求累加之後還是可能會超出預算,這裡有一個小的優化技巧,調整最後一個計費請求的實際計費值使得消耗與預算剛好吻合。
關於超投的第二個問題 減少超投,這個問題不能徹底解決,但可以得到緩解,即降低超投不計費的比率,把庫存損失降到最低;我們的解決方案是在廣告的計費消耗接近廣告預算時執行按概率投放,消耗越接近預算投放的概率越小;該方法有一個弊端,就是沒有考慮到廣告的差異性,有些廣告的 ECPM 較低,本身的投放概率就很小,曝光(或點選)延遲的影響也就很小;針對這一點,我們又做了一次優化:基於歷史資料估算廣告的預算消耗速度和計費延遲的情況,再利用這兩個資料來修正投放概率值。
這個方案的最大特點是實現簡單,在現有的系統中做簡單的開發即可實現,不需要增加額外的系統支援,不依賴於準確的業務場景預測(譬如曝光率,點選率等),而且效果也還不錯;我們還在嘗試不同的方式繼續進行優化超投比率,因為隨著收入的日漸增長,超投引起的收入損失還是很可觀的。
關於微服務架構改造的思考
微服務架構現在已經被業界廣泛接受和推廣實踐,我們從最初就對這個架構思想有很強的認同感; 廣告線上服務在 2014 年完成了第一版主要架構的搭建,那時的微觀架構(虛框表示一臺伺服器)是這樣的:
在同一臺機器上部署多個服務,上游服務只請求本機的下游服務,服務之間使用 http 協議傳輸 protobuf 資料,每個機器都是一個完備的投放系統。
這個架構有很多的優點:結構清晰,運維簡單,網路延遲最小化等。
當然也有一些缺點,同一臺機器上可部署的服務數量是有限的,因而會限制架構的增長,多個模組混合部署不利於整體的效能優化,一個服務的異常會影響整個機器的服務質量;這個架構在微觀上滿足了單一服務的原則,但在巨集觀上還不是真正的微服務化,為了解決上面的一些問題,按照自然的演進我們必然走上微服務化這條路;我們從 16 年中開始進行微服務化的實踐。
微服務化過程中我們也遇到了很多問題,分享一下我們的解決方法及效果:
1. 技術選型問題
RPC 選型,必須滿足的條件是要支援 C++、protobuf 協議和非同步程式設計模型。最初的可選項有 sofa-pbrpc、pbrpc 和 grpc,這三者中我們選中了 grpc,主要看中了它通用(多語言、多平臺和支援代理)、流控、取消與超時等特性;在我們選定 grpc 之後不久百度開源了它的高效能 rpc 框架 brpc,相比之下 brpc 更具有優勢:健全的文件,高效能,內建檢測服務等非常多的特性;為此我們果斷地拋棄了 grpc 和已經在上面投入的一些開發成本,快速地展開了 brpc 相關的基礎功能開發和各服務的改造。
名字服務選型,排除了 zookeeper,etcd 等,最終選定的是 consul+consul template 這個組合,它很完美地支援了我們的業務需求;除服務註冊與發現外,還有健康檢查,服務列表本地備份,支援權重設定等功能,這些功能可以有效地減少團隊成員的運維工作量,增強系統的可用性,成為服務的標準配置。
2. 運維成本增加
這是微服務化帶來的問題之一,微服務化要做服務拆分,服務節點的型別和數量會增多,同時還要額外運維一些基礎服務(譬如,名字服務的 Agency)。考慮到大部分運維工作都是同一個任務在多個機器上重複執行,這樣的問題最適合交由機器來完成,所以我們的解決方案就是自動化運維。我們基於 Ansible 自研了一個視覺化的自動運維繫統。其實研發這個系統最初目的並不是為了支援微服務化,而是為了消除人工運維事故,因為人的狀態是不穩定的(有時甚至是不靠譜的),所以希望由機器來替代人來完成重複的標準動作;後來隨著微服務化的推進,這個系統很自然地就接管了相關的運維工作。現在這個系統完成了整個團隊 90% 以上的運維工作量。
自動運維繫統架構
1. 問題發現和分析定位
業界通用的方式是全鏈路追蹤系統(dapper & zipkip)和智慧運維,我們也在正在進行這方面的工作;除此之外,我們還做了另外兩件事情:異常檢測和 Staging 環境建設;
異常檢測:主要是從業務層面發現各種巨集觀指標的異常,對於廣告投放系統、庫存量、曝光量、點選率和計費率等都是非常受關注的業務指標;異常檢測系統可以預測業務指標在當前時刻的合理範圍值,然後跟實時資料作對比;如果實時資料超出預測範圍就會發出報警並附帶分析資料輔助進行問題分析;這部分工作由線上服務和資料團隊共同完成,這個系統有效地提高了問題發現的效率。
Staging 環境建設:系統變更(包括運維和新功能釋出)是引起線上故障的主要原因,所以我們需要一個系統幫助我們以很小的代價快速發現變更異常。
在功能釋出時大家都會採用梯度釋出的方法,譬如先升級 5% 的服務,然後觀察核心指標的變化,沒有明顯異常就繼續推進直到全量;這個方法並不是總能有效發現問題,假如一個新功能中的 bug 會導致 1% 的訂單曝光下降 50%,那麼在全量釋出之後系統的整體曝光量也只有 0.5% 的變化,也可能因為其他訂單的填充使得整體曝光量沒有變化,所以僅通過整體曝光量很難發現這個問題。只有對所有訂單的曝光量進行對比分析才能準確地發現這個問題。
我們在實踐中利用向量餘弦相似度來發現系統變更引起異常,即把一段時間內(5min)曝光的廣告數量轉換成向量並計算餘弦相似度。那麼如何得到兩個向量呢?可以按照梯度釋出的時間進行分割前後各生成一個向量,這個方法不夠健壯,不同時間的向量本身就有一定的差異。
我們是這樣來解決的:部署一個獨立的投放環境(我們稱為 Staging 環境,相對的原本的投放環境稱為 Base 環境)承載線上的小流量(譬如 3%),所有的系統變更都先在這裡進行;然後用 Staging 環境的向量與 Base 環境的向量進行相似度計算。
因為對差異非常敏感,使用餘弦相似度做監控會有誤報發生;不過這個並不難解決,通過一些 bad case 的分析,我們定位並消除了兩個環境之間的差異(非 bug)因素;在正常情況下兩個環境的相似度會保持在 95% 左右,並在遇到真正的異常時會有明顯的下降觸發報警。Staging 環境及相似度檢測功能在實踐中多次幫助我們發現系統異常。
架構設計過程中積累的經驗
最後分享幾點我在架構設計過程中總結的經驗。
深入理解業務。 在架構設計方面,業務和架構是要互相配合的,架構在滿足業務需求的同時,也可以反過來給業務提需求甚至要求改變業務邏輯已達到系統的最優,這裡的關鍵就是充分理解業務。架構上很難解決的問題,可能在業務上做個微小的調整就搞定了,能有這樣的效果,何樂而不為呢。在系統或者架構優化方面,優化理論和策略已經研究的非常充分,剩下的只是如何跟業務場景進行結合和利用。
設計階段要追求完美,實踐階段要考慮價效比,採用分階段遞進的方式演進到完美的架構。** 在設計階段可以暫時拋開實現成本或者其他一些客觀條件的束縛,按照理想的情況去做架構設計,這樣得到的一個結果是我們所追求的一個理想目標,這個目標暫時達不到沒關係,因為它的作用就是指明架構將來的發展或者演化的大方向;然後在結合實際的限制條件逐步調整這個完美的架構到一個可實際落地的程度,這個過程中還可以保留多箇中間版本,作為架構演進升級過程的 Milestone。也可以這樣理解,從現實出發,著眼於未來,隨著技術發展的速度越來越快,在設計之初遇到的限制和障礙很快就會被解決,避免被這些暫時的限制和障礙遮住了對未來的想象。
監控先行。 監控資訊是瞭解系統執行狀態的重要資訊,大部分監控資訊都要持久化用來做資料分析使用,它可以做異常檢測也可以輔助進行問題的分析和定位;做好監控工作是改善 TTA(Time To Detection)和 TTM(Time To Mitigation)指標的方法之一;這裡還要強調的是要在設計階段就考慮到相關的各種監控指標、統計粒度等細節內容,在開發階段就在系統中進行相關指標的計算和統計,在服務部署階段將這些指標同步到監控系統中;確保服務上線之初就有相應的監控“保駕護航”,避免裸奔。
容錯能力。 這個世界是不完美的,不完美世界中的系統要面對各種各樣的問題;在一個系統的整個生命週期中,研發運維人員要花費大量的時間來應對和解決各種錯誤甚至是災難;兩個方面去考慮,即 Design By Failure 和災難演練(Netflex 已經開源了他們的相關工具)。我想談談自己的實際體會:
首先,在設計之初可以先劃定系統的邊界,分出系統內部和系統外部;從成本的角度考慮,系統內部因為可控性強,可以設定一些假設以減少相關的考量和系統容錯設計;其餘的系統內部問題以及系統外部的問題,優先解決影響較大的問題(譬如,外部服務不可用,對外介面訪問量突增)和高頻發生的問題(硬碟故障,網路割接),這樣的問題大部分都有可借鑑的方案,如果因業務場景特殊而不能複用已有方案,那就要考慮自己來實現;應對外部服務不可用進行熔斷並增加保底策略,訪問量突增做限流,專線故障時走外網,硬碟做 Raid;其他的未考慮到問題在問題首次發生時要評估損失和應對成本來決定要否立即解決;
其次,災難演練的這個想法跟消防演練是一樣的,消防演練一方面可以發現逃生流程上的缺陷,更重要的是培養參與者的逃生常識和實操經驗,在問題真正發生時能正確應對;災難演練同理,在做自動化的同時,也要安排專人(尤其是新人)進行故障處理,要有老司機陪同進行 review 在必要時進行指導或者接管處理動作。這樣才會使得團隊整體的容錯應急處理能力不斷地提升。這個世界註定是不完美的,因此才會有也更需要完美主義者來讓這個世界變得完美,哪怕是隻有一點點。
在此我向大家推薦一個架構學習交流群。交流學習群號: 744642380, 裡面會分享一些資深架構師錄製的視訊錄影:有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能優化、分散式架構等這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良