企業在進行營銷推廣時,廣告投放通常是必備環節之一。為了避免投放“亂燒錢”,在大規模投放前,企業和廣告優化師都會希望在多種廣告策略中,找準效果更好策略才進行投放。早期這樣的方案決策只能通過“拍腦袋”,或者簡易的分流投放測試來粗略進行。在火山引擎AB測試推出“廣告投放AB實驗”后,可逐步支撐企業快速、科學地驗證不同投放策略的平均轉化成本數據效果,并根據實驗報告得到計劃中不同素材、不同落地頁、不同人群包、不同預算等變量到底哪種更好。
廣告投放AB實驗背后,所需的數據能力支撐繁瑣而復雜,開啟廣告實驗后,如果數據不能夠及時準確的送達,會對報告結論造成影響,甚至影響最終決策,而這均依賴于AB實驗平臺底層的基礎投放能力。
基礎投放能力主要包括如下三塊:賬號授權管理、計劃創編和數據查詢。賬號授權是將廣告賬號授權給開發者應用;計劃創編包括物料管理、落地頁管理、應用管理、廣告編輯;數據查詢指廣告投放數據的事實查詢分析。一個廣告投放AB實驗的順利開展,需要上述三個模塊的緊密配合,才可保證最終結果的準確性。
在早期,由于廣告投放業務流程繁瑣,火山引擎DataTester在廣告投放AB實驗項目的迭代中遇到了如下問題:
需要支持多個廣告平臺,授權邏輯日益雜亂;
授權、數據抓取和業務邏輯耦合嚴重,出現問題不易排查;
一類數據抓取就對應一個定時任務,導致定時任務過多,難以維護;
數據模型設計不合理,報表數據越來越多,查詢變得緩慢;
定制特性太多,代碼難以維護;
上述問題的積累導致實驗平臺的開發和維護成本越來越高,線上問題頻發,為了保證平臺實驗數據的科學性和準確性,火山引擎DataTester決定對廣告投放基礎能力進行重構。
1. 火山引擎AB測試-廣告投放項目架構
上圖展示了火山引擎DataTester重構后的廣告投放模塊交互圖,主要解決了以下問題:
針對耦合嚴重、定時任務過多問題:服務拆分,根據業務功能拆分為授權服務、數據抓取服務、業務后端服務和少量定時任務,各類服務各司其職,職責單一;
針對查詢緩慢問題:重新設計數據模型,使用 MySQL 和 ClickHouse 存儲元數據和報表數據,兼顧修改和查詢效率;
針對代碼難以維護問題:引入DDD領域驅動設計思想,面向接口編程,不同廣告平臺分別實現接口,方便維護;
針對代碼質量問題:嚴格控制單測覆蓋率,保證代碼質量;輔以CI/CD流水線,讓bug無處可藏;
針對SaaS/私有化部署問題:使用同一套代碼,底層利用環境變量做兼容,降低開發成本。
授權服務是使用投放的第一步,其主要作用就是對接各個廣告平臺的授權邏輯,將廣告賬號授權給預定義的開發者賬號,保存Token或密碼憑證,然后調用抓取服務下發賬號粒度的抓取任務。
數據抓取服務的主要作用就是保證投放平臺與廣告平臺數據一致性,對于授權的廣告賬戶添加天粒度和小時粒度的數據抓取任務,保證元數據和報表數據的及時更新;對于Oauth2類型的渠道,提供自定義間隔時間的Access Token刷新任務;同時提供實時抓取接口,方便實時數據的獲取。業務后端的主要作用就是使用授權的賬號完成計劃創編工作,對數據進行匯總查詢。
2. 賬號授權
2.1 授權分類
廣告平臺的賬號授權方式可以分為兩類:Oauth2授權 和 賬號密碼授權。賬號密碼授權是比較簡單的授權方式,填寫所需的表單數據保存即可,弊端是容易造成密碼的泄露;OAuth2 是基于令牌Token的授權,在無需暴露用戶密碼的情況下,使應用能獲取對用戶數據的有限訪問權限。這種模式會為開發者的應用頒發一個有時效性的令牌 Token,使得第三方應用能夠通過該令牌獲取相關的資源。需要注意的是,每個平臺的 Token 過期時間不同,需要定時刷新保證 Token 的可用性。
2.2 OAuth2 授權
對接不同的廣告平臺完成OAuth2授權,最主要的是閱讀幫助文檔,一步步完成授權流程。下面就以抖音集團旗下某業務平臺授權為例,簡單介紹授權流程。
注冊開發者賬號,將開發者信息預先保存至數據庫中;
將權限信息、開發者賬戶信息以及需要希望回調時帶回的數據,統一拼裝至授權鏈接后跳轉至廣告平臺;
用戶點擊授權,廣告平臺回調開發者賬號填寫的回調地址,并攜帶 auth_code;
回調地址對應的服務需要處理該請求,根據 auth_code 獲取 Access Token 和 Refresh Token 并保存至數據庫;
該業務平臺的 Access Token 和 Refresh Token 失效時間分別是 24 小時和30天,在 Access Token 過期前,需要調用刷新接口,使用 Refresh Token 刷新 Access Token,此時會得到兩個新的 Token。如此循環往復,Access Token 則永不過期,可以完成各類接口調用工作。
回到編碼層面來看,由于對接各個渠道授權流程基本類似,如果每對接一個渠道都重寫一遍的話,相似代碼會越來越多,可以使用設計模式中的模板方法來避免此類問題。
如下圖所示,模板方法模式定義了一個授權過程的骨架,而將一些步驟延遲到子類中,使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。對應到授權業務上,抽象類可以實現授權過程的不變部分,如接收回調、保存賬號數據等,將可變的行為留給子類來實現,如生成授權URL、獲取Auth Code和獲取Token 等。
3. 數據抓取
數據抓取服務的定位是一個定時任務處理系統,用于完成小時級和天級的廣告數據抓取。在該系統中,我們用DAG來定義任務對象,Manager 負責管理 DAG 的生成和寫入,Scheduler 根據 DAG 中的參數和時間生成任務下發至消息隊列,Worker 負責具體任務的執行。
3.1 數據模型
廣告數據可以分為兩類,元數據和報表數據。元數據是指廣告各個層級的屬性數據,包括ID、名稱、創建時間等屬性字段,而報表數據是指點擊、展示、消耗等指標數據。對于各個廣告平臺的廣告層級,各不相同。
對于元數據層級,各個廣告平臺各不相同。巨量引擎舊版曾使用賬戶-廣告組-計劃-創意四個層級,2.0則使用賬號-項目-廣告層級,百度是賬戶-推廣計劃-推廣單元-創意四個層級,快手是賬號-廣告計劃-廣告組-廣告創意。為了對接多個廣告平臺,需要拉齊廣告數據。由于元數據需要經常的查詢更新,可以存儲在MySQL中。
對于報表數據,每個渠道的指標數量和名稱差異更大,同時多賬號、小時級+天級的數據拉取會保存大量數據,為了保證拓展性和查詢效率,可以將投放報表數據存儲在 ClickHouse 中,CLickHouse中的 Map 字段可以很好的支持報表類多字段的拓展性。
在最終的查詢分析時,需要綜合MySQL和CLickHouse數據得到報告。
3.2 DAG:
在圖論中,如果一個有向圖從任意頂點出發無法經過若干條邊回到該點,則這個圖是一個有向無環圖(DAG,Directed Acyclic Graph)。下圖中,4→6→1→2是一條路徑,4→6→5也是一條路徑,并且圖中不存在從頂點經過若干條邊后能回到該點,這種圖就可以稱為有向無環圖。
DAG 可以用來定義一組相互依賴的操作單元,并基于依賴性、容錯性、并發及調度方式來擴展。在廣告數據抓取中,報表數據是依賴于元數據的抓取,如果元數據不存在,報表數據則無從談起,基于這種依賴關系我們可以構造DAG。DAG 中可以添加屬性,如下列舉了幾個簡單屬性字段:
dag.schedule_interval
string
cron表達式
dag.dag_id
string
Dag id,唯一
dag.tasks
array
dag的詳細任務
tasks[0].task_id
string
任務id,dag內唯一
tasks[0].upstream_task_ids
array[string]
上游依賴任務id
tasks[0].downstream_task_ids
array[string]
下游依賴任務id
tasks[0].is_dummy
bool
是否為空任務
tasks[0].operator_type
string
任務類型
tasks[0].operator_name
string
任務名稱
利用JSON可以組織任務的依賴關系,在如下示例中,我們定義了四個任務,第一個任務為 dummy_task,它是一個空任務,利用它組織下游的 account_meta_task 和 ad_meta_task 并列關系,這兩個任務是可以并發執行的;但是對于任務 ad_daily_insight_task,則需要等待 ad_meta_task 執行完后才會執行。
JSON
{
"schedule_interval":"*/60 * * * *",
"dag_id":"${account_id}_today_insights",
"tasks":[
{
"task_id":"dummy_task",
"downstream_task_ids":[
"account_meta_task",
"ad_meta_task"
],
"is_dummy":true,
"operator_name":"DummyOperator"
},
{
"task_id":"account_meta_task",
"operator_type":"basic",
"operator_name":"ad_meta_operator"
},
{
"task_id":"ad_meta_task",
"downstream_task_ids":[
"ad_daily_insight_task"
],
"operator_name":"ad_meta_operator"
},
{
"task_id":"ad_daily_insight_task",
"operator_name":"insight_operator"
}
]
}
如上的任務依賴關系,經過Scheduler 解析后得到如下流水線:
在實際的應用中,我們會針對每個廣告平臺都預先定義好 DAG 模板,授權完成后 Manager 填充模板數據并存儲在數據庫中,Scheduler 將待執行的 DAG生成任務后下發至 Worker。
3.3 時間輪算法:
從上面的 DAG 定義,可以看到 schedule_interval 這個屬性,它以Cron表達式的形式定義了任務的運行頻率。如此多的任務如何精確運行呢,時間輪算法就是一個很好的解法。
時間輪算法的核心是:輪詢時不再遍歷所有任務,而是僅僅遍歷時間刻度。好比指針不斷在時鐘上旋轉,如果發現某一時刻上有任務,那么就會執行該任務。顯而易見,時間輪算法解決了遍歷效率低的問題。如果以小時為單位,有 10w 個任務,我們不需要遍歷所有任務,僅僅需要遍歷 24 個時間刻度。
如果要將時間精度設為秒,那么整個時間輪將需要 86400 個單位的時間刻度,此時時間輪算法的遍歷效率將會大打折扣。那么如何解決這個問題呢,可以采用分層時間輪算法,多個時間輪相互配合來完成任務。
數據抓取服務目前支持周粒度到秒粒度的任務,我們使用了一個 7*24 個刻度的天級時間輪 和 一個3600刻度的秒級時間輪,當需要添加一個任務時,先添加到天級時間輪上,當指針判斷需要運行該任務時,再將其丟至秒級時輪,最終在精確的時間內完成任務的執行。目前有數以萬計的定時任務在系統上有條不紊的運行,加上重試策略,可以保證平臺的數據報告做到及時、準確,完全滿足對實時性要求很高的廣告數據。
4. 后端業務
對于業務代碼來說,除了可用之外,穩定性和可拓展性是很重要的指標,它關乎著我們當前服務的可用性以及后續的業務迭代是否方便。除了分層解耦之外,也可以額外使用一些方法,增強可維護性和可拓展性。對于廣告創建,存在緊密的前后邏輯關聯和復雜邏輯校驗,如果放在業務中去處理,會使得主要邏輯混亂不堪,甚至難以維護,領域驅動設計(Domain Driven Design,DDD) 方法可以很好的解決該問題。其次,單元測試的編寫,也有助于降低代碼間的耦合,減少bug,提高可拓展性。
4.1 領域驅動設計 DDD
領域驅動設計(Domain Driven Design,DDD) 是由 Eric Evans最早提出的綜合軟件系統分析和設計的面向對象建模方法,如今已經發展成為了一種針對大型復雜系統的領域建模與分析方法。它完全改變了傳統軟件開發工程師針對數據庫進行的建模方法,將要解決的業務概念和業務規則轉換為軟件系統中的類型以及類型的屬性與行為,通過合理運用面向對象的封裝裝、繼承和多態等設計要素,降低或隱藏整個系統的業務復雜性,并使得系統具有更好的擴展性,應對紛繁多變的現實業務問題。
上圖是DDD設計方法的封層結構,領域層將數據和行為綁定在一起,將對象變為充血模型,更方便的完成復雜業務邏輯的處理:
1.用戶接口層
用戶接口層主要負責接收外部輸入并返回結果。該層不含業務邏輯,可以做一些簡單的入參校驗和返回數據的封裝。
2.應用層
應用層通常是用戶接口層的直接使用者。但是在應用層中并不實現真正的業務規則,而是根據實際的 use case 來調用領域層提供的能力,可以理解為工作編排。
3.領域層
領域層是整個業務的核心層。我們一般會使用充血模型來建模實際的對象,同時,由于業務的核心價值在于其運作模式,而不是具體的技術手段或實現方式。因此,領域層的編碼是不允許依賴其他外部對象的。
4.基礎設施層
基礎設施層是在技術上具體的實現細節,它為上面各層提供通用的技術能力。
比如我們使用了哪種數據庫,數據是怎么存儲的,有沒有用到緩存、消息隊列等,都是在這一層要實現的。
4.2 單元測試
對于一個優秀的倉庫來說,單元測試是必要的,有如下幾個好處:
有利于提高開發/重構/協作效率:
調試的時候更高效,有單元測試的時候能更快縮小Bug的范圍。
改動代碼時不需要每次都回歸全量測試。
有利于代碼結構設計
強迫你寫出高內聚低耦合的代碼。如果你發現你寫不了單元測試,很可能說明代碼結構混亂
測試會讓你從使用方的角度重新思考接口的設計劃分是否合理
有利于提高代碼質量
有單元測試把關,能夠避免很多“手誤”出現的隱秘Bug
重構的時候能避免將正確的功能修改出Bug
對于一個多人協作的項目,在項目創建時就需要規劃單測,嚴格設置單測覆蓋率和增量覆蓋率門檻,沒有達到目標則不允許合入。如果開了放行的口子,會逐漸導致單測成了一個擺設。同時在寫單測時,有幾點需要注意:
破除外部依賴:單元測試一般不允許有任何外部依賴(文件依賴,網絡依賴,數據庫依賴等),這些依賴會分散我們的測試焦點,嚴重時可能還會因為依賴不穩定導致無法進行單元測試或測試結果不可復現。我們通過mock/stub來構造出真實依賴下的各種行為。在Go項目中,GoMock 和 GoMonkey 就是一個比較好的Mock工具。
提高編寫效率:Go項目中,原生的go test工具只能滿足最基本的測試需求,可以使用一些斷言工具來提高單測編寫效率。如 testing/testify
MySQL 和 Redis依賴:如果對于數據庫和緩存需要測試,可以使用 docker-compose ,構建 Service + MySQL + Redis 的鏡像,可以完成真實的依賴測試。
利用gitlab、github提供的流水線工具,每次提交時自動運行單測,生成全量覆蓋率和增量覆蓋率,提升開發效率
5. 總結
火山引擎AB測試DataTester源自字節跳動長期沉淀,截至2023年6月,字節已通過DataTester累計做過240萬余次AB實驗,日新增實驗 4000余個,同時運行實驗5萬余個。廣告實驗是DataTester的重要一環,已經積累了豐富的廣告場景實驗經驗。本文是火山引擎DataTester團隊在廣告實驗基礎能力上的重構實踐分享,通過本次重構,服務的穩定性和可拓展性得到了大幅提升,并進一步增強了廣告A/B實驗能力。
DataTester目前服務了包括美的、得到、凱叔講故事等在內的上百家企業,為業務的用戶增長、轉化、產品迭代、運營活動等各個環節提供科學的決策依據,將成熟的“數據驅動增長”經驗賦能給各行業。
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.