據(jù)信貸風(fēng)控系統(tǒng):從數(shù)據(jù)鏈路到模型驗證)
簡介這是一套基于Hadoop與Spark構(gòu)建的金融信貸風(fēng)險控制系統(tǒng)源碼面向大數(shù)據(jù)開發(fā)、金融風(fēng)控算法及數(shù)據(jù)工程方向的開發(fā)者可用于理解海量借貸數(shù)據(jù)下的信用評估與欺詐檢測全流程。資源包共69個文件大小約72KB以36個Java文件、8個Scala文件組成核心計算邏輯配套12個XML配置及5個properties參數(shù)文件便于調(diào)整Spark/Hive運行環(huán)境與模型參數(shù)另有SQL、JSON、JS等文件輔助數(shù)據(jù)集成與可視化展示。項目覆蓋數(shù)據(jù)接入、預(yù)處理、邏輯回歸/隨機森林等模型訓(xùn)練、風(fēng)險評分及實時批處理結(jié)合等模塊源碼分層清晰適合以此為基礎(chǔ)擴展實時風(fēng)控、特征工程或監(jiān)控告警等實踐。已有1219人學(xué)習(xí)下載對于希望掌握大數(shù)據(jù)風(fēng)控項目架構(gòu)與技術(shù)選型的開發(fā)者來說是一份可直接研讀和二次開發(fā)的參考實現(xiàn)。1. 基于Hadoop、Spark的大數(shù)據(jù)金融信貸風(fēng)險控系統(tǒng)先看懂它的數(shù)據(jù)賬信貸風(fēng)控這個場景有個反直覺的結(jié)論真正決定系統(tǒng)能不能跑起來的不是模型有多先進而是從申請信息、還款流水、外部征信到最終風(fēng)險評分的這條數(shù)據(jù)鏈路是否通暢。我拿到一套大數(shù)據(jù)金融信貸風(fēng)險控系統(tǒng)源碼第一件事從來不是翻模型代碼而是先看它的數(shù)據(jù)處理鏈路是怎么組織的。基于Hadoop、Spark的大數(shù)據(jù)金融信貸風(fēng)險控系統(tǒng)本質(zhì)是用HDFS做全量數(shù)據(jù)底座、用Hadoop生態(tài)做離線批處理、用Spark做高時效計算把信貸審批和貸后監(jiān)控變成一條可審計、可回溯的數(shù)據(jù)流水線。它能解決的是數(shù)據(jù)量大、特征維度多、審批時效要求高的信貸風(fēng)控場景。適合誰讀正在做風(fēng)控平臺技術(shù)選型、準(zhǔn)備把這類源碼落地成生產(chǎn)系統(tǒng)的工程師或者想系統(tǒng)理解數(shù)據(jù)鏈路與模型鏈路如何銜接的新手。2. 從源數(shù)據(jù)到風(fēng)險分Hadoop 做數(shù)據(jù)底座、Spark 做計算引擎的架構(gòu)拆分2.1 分層設(shè)計離線層、近線層、在線層各管哪一段一套完整的信貸風(fēng)控系統(tǒng)數(shù)據(jù)鏈路一定不是一條直線而是按時效性拆成三個層次。離線層跑在Hadoop之上數(shù)據(jù)落在HDFS表結(jié)構(gòu)放在Hive里負(fù)責(zé)T1的全量批處理歷史申請數(shù)據(jù)歸檔、月度特征重算、模型訓(xùn)練樣本生成。近線層用Spark的微批或Structured Streaming負(fù)責(zé)分鐘級到小時級的增量特征刷新比如貸后行為監(jiān)控、額度復(fù)評需要用到的近幾小時交易匯總。在線層則是規(guī)則引擎和模型服務(wù)需要在毫秒級返回風(fēng)險分這部分通常不直接跑Spark而是讀取預(yù)計算好的特征。這里有個常見誤解既然Spark既能做批處理又能做流處理為什么還要保留Hadoop答案是兩者根本不是替代關(guān)系。Hadoop提供的是分布式存儲HDFS和資源調(diào)度YARNSpark是運行在YARN之上的計算框架。換句話說Hadoop是底座Spark是引擎。源碼里如果看到MRv2的作業(yè)或Hive UDF別急著說“這技術(shù)過時了”它們負(fù)責(zé)的是數(shù)據(jù)歸檔、全量重算這類對實時性不敏感但對穩(wěn)定性要求極高的任務(wù)。技術(shù)選型不是追新是讓每類任務(wù)落在它最合適的位置上。三層之間的流轉(zhuǎn)也很關(guān)鍵。離線層產(chǎn)出的特征寬表會被同步到在線層的緩存或特征庫中供實時查詢在線層產(chǎn)生的審批結(jié)果和貸后表現(xiàn)又會回流到Hive表里成為下一輪模型訓(xùn)練的新樣本。這個回環(huán)結(jié)構(gòu)是信貸風(fēng)控系統(tǒng)和一般推薦系統(tǒng)最大的不同風(fēng)控系統(tǒng)的數(shù)據(jù)是自我進化的每放一筆款、每產(chǎn)生一筆逾期都會成為未來模型的訓(xùn)練素材。2.2 特征加工鏈路Spark 做寬表 joinHadoop 做歸檔沉淀特征加工是風(fēng)控系統(tǒng)最重的一塊計算。申請人的基本信息、流水、多頭借貸記錄、歷史逾期記錄分散在幾十張表里必須拉到一起才能計算特征。常見做法是先用Spark SQL做多層寬表加工把明細(xì)數(shù)據(jù)聚合成以申請為粒度的特征表寬表里一行就是一個申請單對應(yīng)的幾百個特征這是后續(xù)模型訓(xùn)練和規(guī)則判斷的輸入。加工過程中Spark的優(yōu)勢體現(xiàn)得很明顯內(nèi)存計算避免了反復(fù)讀寫磁盤同一個口徑下Spark跑一個多表join的耗時通常只有MapReduce的幾分之一這對于需要按天重跑的全量特征任務(wù)來說直接決定了能不能在業(yè)務(wù)開門前跑完。Hadoop在這一環(huán)節(jié)的職責(zé)更多是數(shù)據(jù)沉淀。經(jīng)過Spark加工后的結(jié)果會寫回Hive表以分區(qū)形式組織按業(yè)務(wù)日期分區(qū)存放。這樣設(shè)計的價值在于一旦發(fā)現(xiàn)某個特征口徑算錯了可以只重跑對應(yīng)分區(qū)而不需要動全表另外Hive表配合HDFS的副本機制天然承擔(dān)了數(shù)據(jù)備份和災(zāi)難恢復(fù)的職責(zé)。生產(chǎn)系統(tǒng)里我一般會在離線層保留最近三年的原始明細(xì)數(shù)據(jù)壓縮后存放在HDFS上為后續(xù)的模型回溯分析留足彈藥。用Spark接增量明細(xì)、加工特征再落回Hive分區(qū)表的骨架大概長這樣from pyspark.sql import SparkSession from pyspark.sql.functions import col, to_date, current_timestamp spark SparkSession.builder \ .appName(customer_feature_etl) \ .enableHiveSupport() \ .getOrCreate() biz_date 2025-01-15 df spark.read.format(parquet) \ .load(hdfs:///data/ods/customer_trans/dt{}.format(biz_date)) df_clean df.filter(col(trans_amt) 0) \ .withColumn(etl_time, current_timestamp()) \ .withColumn(feature_dt, to_date(col(trans_time))) df_clean.createOrReplaceTempView(trans_detail) spark.sql( SELECT customer_id, SUM(trans_amt) AS amt_total, COUNT(*) AS cnt_total, AVG(trans_amt) AS amt_avg FROM trans_detail GROUP BY customer_id ).write.mode(overwrite) \ .format(parquet) \ .partitionBy(feature_dt) \ .saveAsTable(dws.customer_feature_agg)代碼邏輯分四步先按業(yè)務(wù)日期讀取ODS層的客戶交易明細(xì)讀取時直接定位到當(dāng)天分區(qū)避免全表掃描然后做基礎(chǔ)清洗過濾掉金額為負(fù)的異常流水同時補上ETL時間和特征日期字段接著注冊成臨時視圖用Spark SQL做聚合最后按feature_dt分區(qū)寫入DWS層特征聚合表。這里有兩個參數(shù)值得留意一是read.load的路徑中dt{}這個日期參數(shù)應(yīng)當(dāng)由調(diào)度平臺傳入保證任務(wù)重跑時能指定正確分區(qū)二是write.mode(overwrite)對于按分區(qū)寫入的場景overwrite模式只覆蓋對應(yīng)分區(qū)不會影響其他日期的數(shù)據(jù)遠(yuǎn)比先刪表再重建安全。2.3 樣本與標(biāo)簽風(fēng)控建模里最容易翻車的時點對齊問題信貸風(fēng)控訓(xùn)練數(shù)據(jù)和普通機器學(xué)習(xí)訓(xùn)練數(shù)據(jù)最大的區(qū)別在于標(biāo)簽不是立刻能拿到的。某筆申請在1月15日通過審批它是不是壞樣本要看放款后90天甚至180天有沒有逾期這就是風(fēng)控里常說的表現(xiàn)期。因此源碼里樣本表的設(shè)計通常包含三個時間字段申請時間、放款時間、表現(xiàn)期截止時間。切樣本的時候只能取那些表現(xiàn)期已經(jīng)走完的歷史申請還在表現(xiàn)期內(nèi)的不能進訓(xùn)練集。特征時點對齊是另一個重災(zāi)區(qū)。訓(xùn)練樣本里某個特征比如近三個月消費金額計算窗口的截止點必須嚴(yán)格卡在申請時間點不能把申請之后的流水也算進去。這個坑如果踩了模型在回測時的AUC會虛高上線后卻立刻失效。典型的錯誤寫法是直接拿當(dāng)前時間作為窗口終點去算歷史樣本的特征這在源代碼評審時是最先要揪出來的問題。正確的做法是把申請時間作為參數(shù)傳入特征計算任務(wù)讓每一條樣本的特征都嚴(yán)格截取在它自己申請時間之前。還有一種常見做法是在數(shù)據(jù)表設(shè)計上做約束申請事實表和特征表都用申請單號做主鍵觀察期、表現(xiàn)期作為字段存下來。這樣不管是訓(xùn)練還是推理都從同一個寬表里取數(shù)避免訓(xùn)練和上線兩套口徑。寬表生成后模型訓(xùn)練只讀這張表不再回源明細(xì)既快又不容易出錯。3. 讀懂這類源碼的四塊核心數(shù)據(jù)接入、特征計算、規(guī)則與模型、任務(wù)調(diào)度3.1 數(shù)據(jù)接入把申請信息與還款流水接進 Hive 的 Spark 作業(yè)骨架拿到源碼包先別急著跑先按目錄結(jié)構(gòu)把模塊劃分認(rèn)清楚。這類系統(tǒng)常見的模塊拆法是數(shù)據(jù)接入層、特征加工層、決策引擎層、管理后臺層。數(shù)據(jù)接入層負(fù)責(zé)把業(yè)務(wù)庫、消息隊列、外部數(shù)據(jù)源里的數(shù)據(jù)搬運到HDFS和Hive里是整個鏈條的起點。信貸場景里的數(shù)據(jù)源有幾種典型形態(tài)業(yè)務(wù)申請數(shù)據(jù)存在業(yè)務(wù)庫中常用Sqoop或DataX同步還款流水和行為埋點走消息隊列外部征信數(shù)據(jù)以文件或接口方式提供。源碼里這一層通常是最穩(wěn)定的因為模式相對固定但也是最先出問題的地方。以還款流水接入為例用Spark接消息隊列再落Hive分區(qū)的骨架代碼如下from pyspark.sql import SparkSession from pyspark.sql.functions import col, from_json, to_date from pyspark.sql.types import StructType, StringType, LongType, DoubleType spark SparkSession.builder \ .appName(repay_flow_ingest) \ .enableHiveSupport() \ .getOrCreate() schema StructType() \ .add(loan_id, StringType()) \ .add(repay_amt, DoubleType()) \ .add(repay_time, LongType()) \ .add(status, StringType()) raw spark.readStream.format(kafka) \ .option(kafka.bootstrap.servers, node01:9092,node02:9092) \ .option(subscribe, repay_flow) \ .option(startingOffsets, latest) \ .load() parsed raw.select( from_json(col(value).cast(string), schema).alias(data) ).select(data.*) parsed parsed.withColumn(dt, to_date(col(repay_time).cast(timestamp))) def write_batch(batch_df, batch_id): batch_df.write.mode(append) \ .format(parquet) \ .partitionBy(dt) \ .saveAsTable(dwd.repay_flow_daily) parsed.writeStream \ .foreachBatch(write_batch) \ .outputMode(append) \ .trigger(processingTime5 minutes) \ .start()這里用了Structured Streaming的foreachBatch模式每5分鐘把攢下的一批數(shù)據(jù)寫入Hive分區(qū)表。foreachBatch比直接writeStream到Hive更可靠因為可以在這里加入去重、格式校驗、甚至調(diào)用其他依賴該批數(shù)據(jù)的任務(wù)。參數(shù)上重點關(guān)注兩處startingOffsets設(shè)為latest表示只消費新數(shù)據(jù)如果要做歷史補數(shù)需要改成earliest或指定offsettrigger的processingTime決定數(shù)據(jù)可見延遲5分鐘一刷對貸后監(jiān)控夠用但需要按業(yè)務(wù)對時效的要求動態(tài)調(diào)整。3.2 特征計算窗口函數(shù)與時間滑窗的典型寫法特征加工是風(fēng)控源碼里代碼量最大的部分也是最值得逐行讀的部分。常見特征按窗口分為近1個月、近3個月、近6個月、近12個月甚至包含節(jié)假日在內(nèi)的特殊時段特征。這類時間滑窗特征在Spark SQL里非常適合用窗口函數(shù)加條件聚合來實現(xiàn)。近3個月還款行為特征的典型SQL寫法如下SELECT loan_id, customer_id, SUM(CASE WHEN repay_status overdue THEN 1 ELSE 0 END) AS overdue_cnt_3m, COUNT(*) AS repay_cnt_3m, SUM(repay_amt) AS repay_amt_3m, AVG(repay_amt) AS repay_avg_amt_3m, MAX(repay_amt) AS repay_max_amt_3m FROM dwd.repay_flow_daily WHERE repay_time TO_DATE(2025-01-15) - INTERVAL 3 MONTHS AND repay_time TO_DATE(2025-01-15) GROUP BY loan_id, customer_id這條SQL把還款明細(xì)按申請單聚合輸出逾期次數(shù)、還款筆數(shù)、總額、均值、最大值五個特征。需要注意的是WHERE條件里窗口終點的寫法用TO_DATE(2025-01-15)而不是CURRENT_DATE。如果用CURRENT_DATE跑歷史樣本的補數(shù)任務(wù)時就會把未來數(shù)據(jù)也算進去導(dǎo)致訓(xùn)練集和線上特征口徑不一致。這個細(xì)節(jié)在多人協(xié)作的項目里反復(fù)出現(xiàn)也是絕大多數(shù)特征口徑事故的根源。另外特征計算任務(wù)最好做成配置驅(qū)動窗口長度、聚合字段、時間字段都放在配置表或配置項里代碼里不寫死。好處有兩個一是新增特征時不用改代碼改配置加一行就行二是模型迭代時方便對照特征清單審查口徑。源碼里如果特征都是硬編碼在一個大SQL里后續(xù)維護成本會很高拿到手后第一步建議就是把它重構(gòu)為配置化。3.3 規(guī)則引擎與模型打分規(guī)則表、PMML、原生 Pipeline 的取舍信貸風(fēng)控決策不是純模型而是規(guī)則與模型融合的結(jié)果。硬規(guī)則負(fù)責(zé)攔截監(jiān)管紅線和極端風(fēng)險比如年齡超限、黑名單命中、身份證格式非法模型負(fù)責(zé)對剩余申請做精細(xì)化風(fēng)險排序。源碼里面向這一塊的代碼通常分兩部分規(guī)則引擎和模型推理服務(wù)。規(guī)則引擎的設(shè)計常見做法是規(guī)則配置化規(guī)則不寫死在代碼里而是放在數(shù)據(jù)庫表或配置文件中支持熱更新。規(guī)則表的結(jié)構(gòu)一般包含這些字段CREATE TABLE risk_rule_set ( rule_id STRING, rule_name STRING, rule_expr STRING, -- 例如 score_model 600 AND apply_amt 50000 action STRING, -- PASS / MANUAL / REJECT priority INT, status STRING, update_time TIMESTAMP );rule_expr里存的是可解析的表達(dá)式規(guī)則引擎啟動時加載全表把表達(dá)式編譯成內(nèi)存對象逐條對申請數(shù)據(jù)做匹配。action表示命中后干什么priority決定多條規(guī)則沖突時聽誰的。把規(guī)則從代碼里抽出來放到表里最大的價值是風(fēng)控業(yè)務(wù)人員可以直接配置、即時生效不需要等開發(fā)排期上線。模型打分這部分選型的核心矛盾是跨語言部署和性能。常見做法有三種把模型導(dǎo)出為PMML格式用Java加載打分導(dǎo)出為ONNX格式用推理引擎加載或者直接用Spark ML庫的Pipeline保存和加載。PMML在信貸風(fēng)控里用得仍然很廣因為它對特征順序敏感字段名一一對應(yīng)部署方不需要關(guān)心訓(xùn)練方的代碼細(xì)節(jié)。用Spark訓(xùn)練完的模型保存并加載做打分流程最順但需要保證運行環(huán)境里有匹配的Spark版本依賴。3.4 任務(wù)調(diào)度與血緣離線任務(wù)為什么必須配調(diào)度信貸系統(tǒng)的離線任務(wù)數(shù)量通常幾十上百個靠crontab或人工觸發(fā)根本管理不過來。源碼包里如果有部署文檔調(diào)度配置一定是重點章節(jié)。調(diào)度平臺要做的事是兩件一是按依賴關(guān)系編排任務(wù)上游產(chǎn)出分區(qū)數(shù)據(jù)后下游才能啟動二是失敗重試和告警任務(wù)掛了自動重試幾次再不行就通知值班的人。任務(wù)依賴編排的核心是數(shù)據(jù)就緒檢測。一個Spark任務(wù)啟動前要先確認(rèn)它依賴的Hive分區(qū)已經(jīng)生成而不是用sleep硬等。常見做法是調(diào)度平臺支持前置檢查節(jié)點檢查SQL返回的分區(qū)數(shù)據(jù)量是否大于0。離線跑批鏈路里ODS同步?jīng)Q定了今天有沒有原材料DWD清洗決定了加工質(zhì)量DWS特征寬表決定了模型和報表能否產(chǎn)出。任何一環(huán)掛掉后面的任務(wù)即使跑成功產(chǎn)出的也是臟數(shù)據(jù)。源碼里這一塊的工程價值最容易被低估但生產(chǎn)事故往往就出在這——任務(wù)報SUCCESS但數(shù)據(jù)是空的比任務(wù)失敗更難排查。血緣管理同樣值得提。每個特征字段要能追溯到它來自哪張源表、經(jīng)過了哪些加工步驟否則排查問題時只能靠翻代碼。實際項目里我會要求所有特征加工腳本都登記一張源表-目標(biāo)表映射表調(diào)度平臺跑完任務(wù)后就更新血緣信息半年下來這套元數(shù)據(jù)比代碼注釋可靠得多。4. 讓源碼落地集群規(guī)劃、打包提交與必調(diào)參數(shù)4.1 環(huán)境選型物理機、云主機還是容器化的判斷標(biāo)準(zhǔn)拿到源碼后的第一個決定是在什么環(huán)境上跑。判斷標(biāo)準(zhǔn)其實就三條數(shù)據(jù)量、任務(wù)節(jié)奏、運維能力。數(shù)據(jù)量在幾TB到幾十TB、任務(wù)以T1批處理為主的用幾臺物理機構(gòu)建小型Hadoop集群最經(jīng)濟。數(shù)據(jù)量在百TB級、還需要彈性擴容應(yīng)對大促高峰的云主機會更合適按需擴容、用完釋放。想跑在容器里的也可以但Hadoop生態(tài)的容器化方案對網(wǎng)絡(luò)和存儲要求比較高運維能力不夠的話不建議一步到位。規(guī)模參考按節(jié)點數(shù)給個常見范圍5臺以內(nèi)屬于入門規(guī)模適合跑通源碼和開發(fā)環(huán)境10到20臺能支撐千萬級用戶的風(fēng)控跑批再往上就要考慮業(yè)務(wù)拆分了。機器規(guī)格方面NameNode節(jié)點對內(nèi)存敏感建議64GB起步DataNode節(jié)點磁盤容量優(yōu)先計算密集時CPU核數(shù)多給一些。Spark的executor和HDFS的DataNode通?;觳吭谕慌?jié)點上這樣數(shù)據(jù)本地性最好——Spark讀數(shù)據(jù)時直接從本機磁盤拿省去網(wǎng)絡(luò)傳輸。4.2 從源碼到運行打包、上傳、提交 Spark 作業(yè)源碼工程一般是Maven或Gradle組織的多模塊工程拿到后先編譯打包再提交到集群運行。打包和提交的命令如下mvn clean package -DskipTests -Pdist hdfs dfs -mkdir -p /apps/credit-risk hdfs dfs -put credit-risk-1.0.jar /apps/credit-risk/ spark-submit \ --master yarn \ --deploy-mode cluster \ --class com.example.credit.CreditBatchMain \ --files hdfs:///apps/credit/risk_rule.conf \ --conf spark.executor.extraJavaOptions-Dconfig.filerisk_rule.conf \ --queue risk \ --executor-memory 16g \ --executor-cores 4 \ --num-executors 20 \ /apps/credit-risk/credit-risk-1.0.jar \ --env prod \ --dt 2025-01-15各參數(shù)作用拆開看--master yarn表示交給集群資源調(diào)度--deploy-mode cluster讓Driver也跑在集群里適合離線批任務(wù)--class指定要運行的入口類--files把外部配置文件分發(fā)到每個執(zhí)行節(jié)點--queue指定YARN隊列把風(fēng)控任務(wù)單獨放一個隊列可以避免和其他業(yè)務(wù)互相搶占資源最后兩個參數(shù)--env和--dt是業(yè)務(wù)參數(shù)--dt就是第2章里提到的業(yè)務(wù)日期特征計算任務(wù)都依賴它來定位分區(qū)。初次啟動時建議先不用--num-executors改用Spark的動態(tài)資源分配讓集群按負(fù)載自動伸縮。等穩(wěn)定運行一段時間后再根據(jù)監(jiān)控數(shù)據(jù)固定executor數(shù)量或直接保留動態(tài)策略。源碼里如果自帶啟動腳本重點看腳本里的參數(shù)值——不同公司集群規(guī)模差異很大直接照搬的后果通常不是OOM就是資源浪費。4.3 必調(diào)參數(shù)executor 內(nèi)存、分區(qū)數(shù)與動態(tài)資源Spark作業(yè)調(diào)參是玄學(xué)但有幾條基本邏輯不是玄學(xué)。Executor內(nèi)存、CPU核數(shù)、分區(qū)數(shù)三者必須放在一起看單獨調(diào)哪一個都可能翻車。常見參數(shù)配置參考如下參數(shù)常見取值調(diào)整依據(jù)spark.executor.memory8g ~ 32g每個任務(wù)的數(shù)據(jù)處理量、是否有大聚合spark.executor.cores2 ~ 4越高并行度越高但要控制單節(jié)點總核數(shù)spark.sql.shuffle.partitions200 ~ 2000和shuffle數(shù)據(jù)量掛鉤過少導(dǎo)致單分區(qū)壓力過大過多導(dǎo)致小文件膨脹spark.dynamicAllocation.enabledtrue / false生產(chǎn)環(huán)境建議開啟讓集群彈性伸縮spark.yarn.executor.memoryOverhead1g ~ 4g存放JVM堆外內(nèi)存頻繁O(jiān)OM時優(yōu)先調(diào)這里spark.sql.adaptive.enabledtrue動態(tài)調(diào)整reduce分區(qū)數(shù)解決數(shù)據(jù)傾斜第一次跑源碼任務(wù)時我一般先把shuffle分區(qū)分母設(shè)成executor總核數(shù)的2到3倍比如20個executor、每個4核、共80核分區(qū)數(shù)設(shè)160到240然后看Spark UI里每個stage的耗時分布。如果某個stage最后幾個task明顯比別的慢很多大概率是數(shù)據(jù)傾斜。解決數(shù)據(jù)傾斜優(yōu)先開spark.sql.adaptive.enabled還不行再考慮加鹽或者拆廣播變量。executor內(nèi)存溢出則優(yōu)先調(diào)memoryOverhead它不是堆內(nèi)存但堆外內(nèi)存不夠一樣會OOM。還有一個容易被忽略的參數(shù)是spark.speculation開啟后Spark會重跑那些明顯滯后的task取最先完成的那個結(jié)果。對大數(shù)據(jù)量的批處理任務(wù)能兜底但也可能帶來重復(fù)計算穩(wěn)妥做法是在跑的慢的節(jié)點上臨時開啟穩(wěn)定后關(guān)掉。參數(shù)調(diào)整后必須觀察Spark UI上的GC時間、shuffle落地量、單task處理時長三個指標(biāo)不要只看任務(wù)成功還是失敗。5. 風(fēng)控系統(tǒng)落地避坑五條值得記住的排查記錄5.1 現(xiàn)象夜間批處理任務(wù)堆積Spark 作業(yè)反復(fù)失敗某次生產(chǎn)環(huán)境凌晨的T1批處理任務(wù)在3點后開始堆積Spark作業(yè)提交成功但運行幾分鐘后就被YARN殺掉。現(xiàn)象是任務(wù)反復(fù)失敗日志里出現(xiàn)大量ExecutorLostFailure和Container killed messages。原因是executor申請的內(nèi)存超過節(jié)點可用內(nèi)存加上多個任務(wù)在同一批節(jié)點上同時運行物理內(nèi)存被打滿后觸發(fā)系統(tǒng)OOM容器直接被回收。這不是單次任務(wù)的問題而是所有夜間批任務(wù)擠在同一時間窗口、資源池沒有隔離的必然結(jié)果。解決分兩步先給風(fēng)控任務(wù)單獨劃分YARN隊列限制隊列最大資源占用防止批任務(wù)之間互相爭搶再調(diào)小單executor內(nèi)存把16g降到8g同時增加executor數(shù)量保住總計算能力。更根本的辦法是把大任務(wù)拆成多個有依賴關(guān)系的小任務(wù)錯峰執(zhí)行讓峰值內(nèi)存需求錯開。5.2 現(xiàn)象實時評分接口延遲抖動從 200ms 漲到 2s風(fēng)控實時評分接口平時穩(wěn)定在200ms左右某段時間開始下午高峰期漲到2s甚至超時?,F(xiàn)象沒有規(guī)律不是每次都慢但慢的時候集中在流量大的時間段。抓線程和GC日志后發(fā)現(xiàn)在線評分服務(wù)讀取特征緩存時每次都用同步方式請求遠(yuǎn)程緩存一旦緩存服務(wù)出現(xiàn)網(wǎng)絡(luò)抖動或者慢查詢調(diào)用方線程全部阻塞在等待上。另外服務(wù)本身的JVM堆設(shè)置偏小高峰期Full GC頻繁也會放大延遲。解決是在評分服務(wù)本地加一層二級緩存把熱點特征緩存到本地內(nèi)存設(shè)置2分鐘的過期時間遠(yuǎn)程緩存只在本地未命中時回源。同時把遠(yuǎn)程緩存查詢改成異步刷新請求進來先查本地緩存查不到就直接回源一次后臺再異步更新。改動后接口延遲穩(wěn)定在100ms到300ms之間抖動問題沒有再出現(xiàn)。5.3 現(xiàn)象訓(xùn)練集和線上特征口徑對不上模型 AUC 虛高模型在回測時AUC到了0.82上線后實際區(qū)分能力明顯達(dá)不到回測水平調(diào)閾值也沒用。排查后發(fā)現(xiàn)訓(xùn)練特征和上線特征不是一個源頭。根因在特征計算任務(wù)的日期參數(shù)上。訓(xùn)練樣本的特征是歷史某一天跑的批任務(wù)產(chǎn)出的當(dāng)時用的窗口終點是當(dāng)天線上評分時特征服務(wù)用的窗口終點是當(dāng)前時刻。對同一個申請單如果特征引用的是未來一段時間的流水訓(xùn)練出來的特征分布和線上完全不同。更隱蔽的是缺失值處理邏輯不一致訓(xùn)練時用了全表均值填充線上實時服務(wù)沒有這個均值表填的0分布直接偏移。解決是整個團隊統(tǒng)一用特征生成腳本的同一份代碼訓(xùn)練和線上共用一套特征邏輯。新增一個特征校驗任務(wù)每天對比訓(xùn)練特征表和線上特征表的同名字段的均值、標(biāo)準(zhǔn)差、分布差異PSI超閾值就報警。這個機制上線后口徑不一致的問題基本做到了早發(fā)現(xiàn)早處理。5.4 現(xiàn)象Hive 小文件暴漲NameNode 內(nèi)存吃緊集群NameNode堆內(nèi)存使用率持續(xù)攀升Hive表查詢越來越慢。查看后發(fā)現(xiàn)DWS層特征表的文件數(shù)量從上萬個漲到了十幾萬個大量幾KB到幾百KB的小文件占滿了NameNode內(nèi)存。原因是特征計算任務(wù)用了動態(tài)分區(qū)寫Hive同時shuffle分區(qū)數(shù)設(shè)得過大Spark寫出的文件數(shù)量等于shuffle分區(qū)數(shù)乘以動態(tài)分區(qū)數(shù)數(shù)據(jù)量沒漲文件數(shù)漲了好幾倍。小文件多不僅拖垮NameNode還會讓后續(xù)讀取任務(wù)啟動大量task每個task都在做元數(shù)據(jù)操作整個集群變慢。解決分兩層Spark寫入前先做一次repartition把輸出分區(qū)數(shù)控制在合理范圍內(nèi)寫入后跑一次合并任務(wù)用Hive的MERGE或Spark repartition按新分區(qū)數(shù)重寫一遍。參數(shù)方面把spark.sql.shuffle.partitions從2000降到400匹配實際數(shù)據(jù)規(guī)模而不是簡單照搬。關(guān)鍵是建立一條小文件巡檢任務(wù)每天統(tǒng)計各層表的文件數(shù)變化超過閾值就自動觸發(fā)合并。5.5 現(xiàn)象模型上線后壞賬率不降反升新模型上線一個月后審批通過率沒有明顯變化壞賬率卻比舊模型時期高了一截。團隊第一反應(yīng)是模型不行但要區(qū)分是模型本身的問題還是客群結(jié)構(gòu)變化的問題。排查后發(fā)現(xiàn)新模型確實更準(zhǔn)但它會把一批舊模型認(rèn)為高風(fēng)險、拒絕掉的人群放進來。這批人其實是舊模型時期被拒絕、沒有真實貸后表現(xiàn)、只能靠拒絕推斷去估計其風(fēng)險的人。新模型使用了更寬泛的特征給了部分原本該拒絕的人通過而這些人的實際表現(xiàn)比訓(xùn)練時預(yù)估的更差。解決思路是引入拒絕推斷的修正樣本把被拒絕人群的風(fēng)險標(biāo)簽做權(quán)重調(diào)整后放進訓(xùn)練集同時上線前做嚴(yán)格的回測和影子評分。更穩(wěn)妥的做法是新模型分階段放量先放行新模型判定為低風(fēng)險的申請觀察幾周真實貸后表現(xiàn)再逐步放開閾值。這樣即使模型有偏差損失也可控。6. 拿來即用的進階驗證回測與影子評分給上線前的模型兜底回測和影子評分是我在信貸風(fēng)控項目里最看重的兩道驗證工序?;販y的核心是時間切分把歷史數(shù)據(jù)按時間切兩段前一段用來訓(xùn)練后一段用來驗證。和一般機器學(xué)習(xí)隨機切分不同風(fēng)控回測必須按時間切因為信貸樣本天然有時序關(guān)系隨機切分會把未來信息泄漏進訓(xùn)練集。切分點一般選在表現(xiàn)期之前比如驗證段要留出90天表現(xiàn)期那就至少把最后90天的申請排除出訓(xùn)練集?;販y指標(biāo)要同時看區(qū)分度、排序穩(wěn)定性和群體穩(wěn)定性三類區(qū)分度看KS和AUC排序穩(wěn)定性看每個分?jǐn)?shù)段通過率是否和訓(xùn)練期一致群體穩(wěn)定性看PSIPSI超過0.25說明推理時的特征分布和訓(xùn)練期差異已經(jīng)很大模型需要重訓(xùn)。影子評分是把新模型旁路部署在線上但不參與實際決策生產(chǎn)請求進來舊模型打分決定審批結(jié)果新模型同時打分但結(jié)果只落庫不生效。跑上一到兩個星期拿新模型的分和舊模型的分做對比看排序一致性、各分?jǐn)?shù)段通過率差異、特征分布偏移情況。影子評分能驗證一件事回測驗證不了的事特征在真實線上環(huán)境里取數(shù)是否穩(wěn)定。我在一個項目上吃過虧回測時AUC很漂亮影子跑了一周發(fā)現(xiàn)新模型的評分均值比舊模型低了將近30分原因是線上特征服務(wù)對某些長尾特征取到的值大量為0和訓(xùn)練時的分布完全脫節(jié)。從那次以后我養(yǎng)成一個習(xí)慣任何模型上線前必須跑至少一周影子評分并對比分位數(shù)、PSI和特征覆蓋率三個報表。這個習(xí)慣雖然讓上線周期拉長了幾天但攔下了不少看著好看、落地就翻車的模型。這套驗證鏈路值得每一個做信貸風(fēng)控的團隊堅持希望幫到你。本文還有配套的精品資源點擊獲取