戰(zhàn):廚具銷(xiāo)售數(shù)據(jù)分析與可視化大屏全鏈路)
做大數(shù)據(jù)方向的項(xiàng)目很多人第一反應(yīng)是先把 Hadoop、Spark 裝好然后發(fā)現(xiàn)真正難的從來(lái)不是安裝而是把從數(shù)據(jù)到展示的整條鏈路跑通。最近我在整理一套電商平臺(tái)廚具類(lèi)目的銷(xiāo)售數(shù)據(jù)分析系統(tǒng)技術(shù)棧是 Hadoop Spark Hive Django最后用可視化大屏把結(jié)果呈現(xiàn)出來(lái)。這個(gè)組合在很多教學(xué)項(xiàng)目和實(shí)際業(yè)務(wù)里都很典型但網(wǎng)上能搜到的基本都是單點(diǎn)教程——單獨(dú)講 Spark 優(yōu)化、單獨(dú)講 Django 接口、單獨(dú)講大屏模板很少有人把 Hive 表怎么建、Spark 任務(wù)怎么跑、Django 接口怎么給大屏供數(shù)這條鏈路完整串起來(lái)也沒(méi)有人講調(diào)試時(shí)最容易在哪個(gè)環(huán)節(jié)浪費(fèi)半天。這篇文章會(huì)把整個(gè)項(xiàng)目的實(shí)現(xiàn)邏輯和我在調(diào)試中踩過(guò)的坑完整整理出來(lái)適合準(zhǔn)備做大數(shù)據(jù)課程設(shè)計(jì)、以及剛接觸數(shù)據(jù)倉(cāng)庫(kù)和可視化開(kāi)發(fā)的人參考。我會(huì)從選型理由講起再按數(shù)據(jù)準(zhǔn)備、分析計(jì)算、接口對(duì)接、調(diào)試排障、項(xiàng)目交付的順序往下走最后給一些擴(kuò)展建議。內(nèi)容不求面面俱到但每個(gè)環(huán)節(jié)都會(huì)給出能直接落地的細(xì)節(jié)。1. 為什么這套組合值得做HadoopSparkHiveDjango 的分工與適用邊界1.1 廚具類(lèi)目數(shù)據(jù)量不大為什么還要上 Hadoop我自己也糾結(jié)過(guò)這個(gè)問(wèn)題一個(gè)廚具品類(lèi)的訂單明細(xì)撐死一年也就幾百萬(wàn)到兩千萬(wàn)行單獨(dú)拿 Pandas 也能跑分析為什么還要引入 Hadoop 這一套原因有兩個(gè)。第一個(gè)是數(shù)據(jù)形態(tài)不只是訂單明細(xì)還有商品維表、品牌維表、區(qū)域維表、評(píng)價(jià)流水join 起來(lái)以后中間結(jié)果會(huì)膨脹第二個(gè)是當(dāng)你要做的不是一次性報(bào)表而是可重復(fù)運(yùn)行的“分析系統(tǒng)”就必須有統(tǒng)一的存儲(chǔ)、統(tǒng)一的調(diào)度入口和穩(wěn)定的數(shù)據(jù)產(chǎn)出。Hive 先把數(shù)據(jù)倉(cāng)庫(kù)的分層和元數(shù)據(jù)管理做好Spark 接著跑復(fù)雜的清洗和聚合最后 Django 面向展示層提供接口整條鏈路各管一段后面接手的人也能按層維護(hù)。這個(gè)設(shè)計(jì)不是為了顯得高大上而是為了“好維護(hù)、好替換”。如果你只是臨時(shí)分析一張小表直接讀 CSV 當(dāng)然更快。但項(xiàng)目一旦掛上“系統(tǒng)”兩個(gè)字就意味著每天要跑、每周要更新、不同角色要依賴(lài)它做決策。這個(gè)時(shí)候數(shù)據(jù)分層、任務(wù)調(diào)度、結(jié)果落庫(kù)、接口輸出就不是可有可無(wú)的優(yōu)化項(xiàng)而是基本要求。Hadoop 在這套鏈路里承擔(dān)的其實(shí)是兩件事一是把原始文件安全地放到 HDFS 上二是給 Hive 提供分布式存儲(chǔ)底座Hive 負(fù)責(zé)把文件變成“能寫(xiě) SQL 的表”Spark 負(fù)責(zé)把復(fù)雜計(jì)算跑得足夠快Django 只負(fù)責(zé)把最終結(jié)果以接口形式送出去。1.2 各組件邊界別讓 Django 去算數(shù)也別讓 Hive 去實(shí)時(shí)響應(yīng)我在很多類(lèi)似項(xiàng)目里看到的最常見(jiàn)問(wèn)題是組件職責(zé)混亂。比如有人把大屏需要的數(shù)據(jù)直接用 Django 實(shí)時(shí)查 Hive前端點(diǎn)一次刷新后端就去執(zhí)行一個(gè)分布式 SQL運(yùn)氣好幾秒返回運(yùn)氣不好幾十秒頁(yè)面一直轉(zhuǎn)圈。這種寫(xiě)法在演示時(shí)還能忍真正連續(xù)點(diǎn)起來(lái)就會(huì)把 HiveServer2 的連接池打滿。組件職責(zé)邊界常見(jiàn)誤區(qū)HDFS原始文件與中間結(jié)果存儲(chǔ)把每張臨時(shí)表都存一份垃圾數(shù)據(jù)不清Hive元數(shù)據(jù)管理、分層寬表、離線 SQL 分析讓 Hive 支撐秒級(jí)在線查詢(xún)Spark復(fù)雜 ETL、大規(guī)模 Join、聚合、重分區(qū)全鏈路都寫(xiě)成 Spark 程序連小表也開(kāi)集群Django讀結(jié)果庫(kù)、輸出 JSON、控制權(quán)限在 Django 里直接跑 Hive SQL大屏展示趨勢(shì)、占比、地圖大屏直接連 HiveServer2正確的分工是Spark 算完結(jié)果寫(xiě)成 MySQL 里的聚合結(jié)果表Django 只讀 MySQL把結(jié)果轉(zhuǎn)成 JSON大屏只負(fù)責(zé) fetch 這些 JSON 并繪圖。MySQL 里存的不是原始流水而是按天、按類(lèi)目、按品牌匯總好的結(jié)果所以查詢(xún)量很小響應(yīng)速度才能穩(wěn)定在幾十毫秒級(jí)別。2. 廚具銷(xiāo)售數(shù)據(jù)如何變成 Hive 里可用的事實(shí)表2.1 數(shù)據(jù)來(lái)源與字段清單這套項(xiàng)目里沒(méi)有真實(shí)生產(chǎn)環(huán)境的數(shù)據(jù)授權(quán)我使用的是按電商后臺(tái)導(dǎo)出格式模擬的離線數(shù)據(jù)集字段結(jié)構(gòu)盡量貼近常見(jiàn)場(chǎng)景。如果你有平臺(tái)開(kāi)放接口或脫敏離線包處理流程完全一樣只是字段名需要對(duì)齊。我建議不要為了“真實(shí)”去抓取未經(jīng)授權(quán)的大規(guī)模頁(yè)面數(shù)據(jù)重點(diǎn)放在數(shù)據(jù)建模和分析鏈路上更有價(jià)值。廚具銷(xiāo)售明細(xì)的核心字段大致如下字段名類(lèi)型說(shuō)明order_idSTRING訂單編號(hào)業(yè)務(wù)上的唯一鍵product_idSTRING商品編號(hào)product_nameSTRING商品標(biāo)題category_l1STRING一級(jí)類(lèi)目比如鍋具、儲(chǔ)物、餐具category_l2STRING二級(jí)類(lèi)目比如不粘鍋、保鮮盒brand_nameSTRING品牌名priceDECIMAL(10,2)銷(xiāo)售標(biāo)價(jià)pay_amountDECIMAL(10,2)實(shí)際支付金額quantityINT購(gòu)買(mǎi)數(shù)量provinceSTRING收貨省份citySTRING收貨城市order_timeSTRING下單時(shí)間star_levelINT買(mǎi)家評(píng)價(jià)星級(jí)廚具類(lèi)目有個(gè)特點(diǎn)客單價(jià)波動(dòng)很大價(jià)格帶從幾十塊的調(diào)味盒到幾千塊的炒鍋都有而且消費(fèi)者購(gòu)買(mǎi)頻次低很多人是搬新家或者裝修廚房才會(huì)集中采購(gòu)。所以分析時(shí)不能只盯著銷(xiāo)量還要關(guān)注客單價(jià)、價(jià)格帶結(jié)構(gòu)、地域分布和跨品類(lèi)的連帶購(gòu)買(mǎi)。2.2 分層建表 DDL 與分區(qū)設(shè)計(jì)我的習(xí)慣是第一層 ODS 只做拷貝原字段長(zhǎng)什么樣就什么樣第二層 DWD 做類(lèi)型轉(zhuǎn)換、去重、空值補(bǔ)齊第三層 DWS 按維度輕度匯總最后一層 ADS 直接把給大屏展示的聚合結(jié)果落庫(kù)。這里貼一張 DWD 層的 Hive 建表腳本重點(diǎn)是分區(qū)和存儲(chǔ)格式CREATE TABLE IF NOT EXISTS dwd_kitchen_sale_detail ( order_id STRING, product_id STRING, product_name STRING, category_l1 STRING, category_l2 STRING, brand_name STRING, price DECIMAL(10,2), pay_amount DECIMAL(10,2), quantity INT, province STRING, city STRING, order_time STRING ) PARTITIONED BY (dt STRING) STORED AS ORC;為什么按天分區(qū)因?yàn)殡x線任務(wù)基本都是每天增量灌入按天分區(qū)可以讓每次寫(xiě)入只影響當(dāng)天數(shù)據(jù)歷史分區(qū)不用動(dòng)。為什么用 ORCORC 是列式存儲(chǔ)做聚合時(shí)只讀取需要的列I/O 會(huì)小很多。對(duì)于廚具這個(gè)量級(jí)的數(shù)據(jù)按天分區(qū)已經(jīng)足夠不需要再加二級(jí)分區(qū)。如果單日數(shù)據(jù)量真的到了千萬(wàn)行以上再考慮在分區(qū)字段上加 hash 分桶不要一開(kāi)始就過(guò)度設(shè)計(jì)。2.3 Spark 清洗不能跳過(guò)的三個(gè)環(huán)節(jié)數(shù)據(jù)從 ODS 到 DWD至少要過(guò)三關(guān)。第一關(guān)是去重同一個(gè)訂單可能因?yàn)閿?shù)據(jù)回溯或者重復(fù)同步出現(xiàn)多次用訂單號(hào)做去重保留最后一條第二關(guān)是空值處理品牌為空補(bǔ)“未知”價(jià)格為零或者是負(fù)數(shù)直接剔除坪效和客單價(jià)指標(biāo)最容易被臟數(shù)據(jù)帶偏第三關(guān)是日期規(guī)范化把原始時(shí)間字符串轉(zhuǎn)成標(biāo)準(zhǔn)格式保證后面按天聚合不會(huì)出現(xiàn)“2024/1/1”和“2024-01-01”同時(shí)存在的情況。df spark.sql(SELECT * FROM ods_kitchen_sale WHERE dt 2024-01-01) df df.dropDuplicates([order_id]) df df.filter(df.pay_amount 0) df df.withColumn(dt, F.to_date(F.col(order_time), yyyy-MM-dd HH:mm:ss)) df.write.mode(overwrite).format(hive).saveAsTable( dwd_kitchen_sale_detail, partitionBydt )為什么清洗邏輯放在 Spark 而不是完全依賴(lài) Hive因?yàn)?Hive 執(zhí)行 SQL 也是一樣能做的但 Spark 可以先把多張?jiān)幢聿⑿凶x進(jìn)來(lái)通過(guò) DataFrame 緩存復(fù)用中間結(jié)果寫(xiě)復(fù)雜 ETL 時(shí)代碼更直觀。更重要的是清洗和后續(xù)聚合可以放在同一個(gè) Spark 工程里避免多個(gè)引擎來(lái)回切換。3. 分析層實(shí)戰(zhàn)廚具銷(xiāo)售的核心指標(biāo)與 Spark 計(jì)算細(xì)節(jié)3.1 指標(biāo)清單要跟大屏一一對(duì)應(yīng)大屏不是把一堆圖表堆上去就完事指標(biāo)設(shè)計(jì)會(huì)直接決定數(shù)據(jù)模型的粗粒度。我在這個(gè)項(xiàng)目里的指標(biāo)清單是這樣拆的KPI 卡片總銷(xiāo)售額、總訂單量、客單價(jià)、銷(xiāo)量同比趨勢(shì)線按天/按周的銷(xiāo)售額和訂單量變化類(lèi)目結(jié)構(gòu)一級(jí)類(lèi)目銷(xiāo)售額占比鍋具、餐具、收納各自占多少品牌榜Top 10 品牌的銷(xiāo)量和銷(xiāo)售額地域分布分省銷(xiāo)售額熱力地圖服務(wù)體驗(yàn)退款率和好評(píng)率廚具是低頻高客單品類(lèi)單純看銷(xiāo)量容易失真。兩個(gè)品牌可能在銷(xiāo)量上差不多但一個(gè)靠幾十塊的保鮮盒沖量另一個(gè)靠上千塊的琺瑯鍋賺錢(qián)后者的商業(yè)價(jià)值明顯更高。所以每個(gè)指標(biāo)的背后最好都掛一個(gè)“業(yè)務(wù)問(wèn)題”而不是為了填滿大屏格子硬算。3.2 一個(gè)典型的 Spark SQL品牌價(jià)格帶分析品牌榜是最基礎(chǔ)的分析真正能說(shuō)明問(wèn)題的是疊加價(jià)格帶之后的品牌結(jié)構(gòu)。下面這段 Spark SQL 就是按品牌和價(jià)格檔位計(jì)算訂單數(shù)與銷(xiāo)售額SELECT brand_name, CASE WHEN pay_amount 50 THEN 低客單 WHEN pay_amount 200 THEN 中客單 ELSE 高客單 END AS price_level, COUNT(DISTINCT order_id) AS order_cnt, SUM(quantity) AS sales_cnt, ROUND(AVG(pay_amount), 2) AS avg_order_value FROM dwd_kitchen_sale_detail WHERE dt 2024-01-01 GROUP BY brand_name, CASE WHEN pay_amount 50 THEN 低客單 WHEN pay_amount 200 THEN 中客單 ELSE 高客單 END;大屏上的品牌榜只是這個(gè) SQL 去掉價(jià)格帶后的簡(jiǎn)化版。加價(jià)格帶之后可以很清楚地看到某個(gè)品牌是靠低價(jià)引流還是靠高客單品帶動(dòng)銷(xiāo)售這對(duì)運(yùn)營(yíng)選品和投放策略都有參考價(jià)值。如果數(shù)據(jù)量很大先把價(jià)格帶和銷(xiāo)量做一次預(yù)聚合再去 join 品牌維表可以明顯減少 shuffle 數(shù)據(jù)量。3.3 任務(wù)調(diào)度與結(jié)果回寫(xiě)分析任務(wù)不能每次手動(dòng)跑。我會(huì)把整段分析邏輯寫(xiě)成一個(gè)analysis_job.py每天凌晨用調(diào)度平臺(tái)或 cron 觸發(fā)一次。關(guān)鍵是把結(jié)果按天分區(qū)覆蓋寫(xiě)避免大屏看到過(guò)期或者重復(fù)的數(shù)據(jù)。Spark 任務(wù)的提交命令長(zhǎng)這樣spark-submit \ --master yarn \ --deploy-mode cluster \ --num-executors 4 \ --executor-cores 4 \ --executor-memory 6g \ --driver-memory 2g \ --conf spark.sql.shuffle.partitions200 \ analysis_job.py參數(shù)不要直接抄網(wǎng)上的。executor 數(shù)量和內(nèi)存取決于你實(shí)際集群的資源。我見(jiàn)過(guò)太多人把 executor memory 寫(xiě)到 16g但 Yarn 隊(duì)列里根本沒(méi)那么多資源結(jié)果任務(wù)一直排隊(duì)或者被殺死。先用默認(rèn)參數(shù)跑通再根據(jù) Spark UI 里的運(yùn)行時(shí)間和數(shù)據(jù)量逐步調(diào)整。3.4 處理數(shù)據(jù)傾斜的小經(jīng)驗(yàn)按品牌或區(qū)域聚合時(shí)最容易遇到數(shù)據(jù)傾斜幾個(gè)爆款品牌占了大部分?jǐn)?shù)據(jù)導(dǎo)致某個(gè) executor 處理的數(shù)據(jù)量是其他人的好幾倍整體任務(wù)被拖慢。最簡(jiǎn)單的處理辦法是先把熱點(diǎn)數(shù)據(jù)識(shí)別出來(lái)單獨(dú)計(jì)算再和非熱點(diǎn)結(jié)果合并。更通用的做法是給 join 鍵加上一個(gè)隨機(jī)后綴做“打散”把一個(gè)大 key 拆成多個(gè)小 key最后再按真實(shí) key 聚合。對(duì)于廚具這個(gè)數(shù)據(jù)量先看指標(biāo)結(jié)果是否符合預(yù)期再?zèng)Q定要不要做這些復(fù)雜度較高的優(yōu)化。4. Django 接口與大屏數(shù)據(jù)對(duì)接讓分析結(jié)果真正被“看見(jiàn)”4.1 接口設(shè)計(jì)直接按大屏組件拆Django 端的接口不要設(shè)計(jì)成一個(gè)“萬(wàn)能查詢(xún)接口”。最省事的方式是前端大屏有幾個(gè)組件后端就提供幾個(gè)對(duì)應(yīng)接口每個(gè)接口只返回一個(gè)圖表所需的數(shù)據(jù)。這個(gè)項(xiàng)目的接口大致是這樣接口路徑返回內(nèi)容/api/v1/overview/kpi總銷(xiāo)售額、總訂單量、客單價(jià)/api/v1/overview/trend每日銷(xiāo)售額和訂單量列表/api/v1/category/ratio各一級(jí)類(lèi)目銷(xiāo)售額占比/api/v1/brand/topTop10 品牌銷(xiāo)量與銷(xiāo)售額/api/v1/region/heat各省銷(xiāo)售額熱力數(shù)據(jù)/api/v1/review/cloud評(píng)價(jià)關(guān)鍵詞統(tǒng)計(jì)統(tǒng)一返回格式{code: 0, data: ...}數(shù)據(jù)異常時(shí) code 非 0前端好做統(tǒng)一判斷。Django 視圖層寫(xiě)起來(lái)非常輕下面是其中一個(gè)接口的示例from django.http import JsonResponse from django.core.cache import cache def sale_trend(request): data cache.get(sale_trend) if data is None: data list( ResultTrend.objects.order_by(dt).values(dt, sales) ) cache.set(sale_trend, data, 600) return JsonResponse({code: 0, data: data})大屏頁(yè)面通常幾秒輪詢(xún)一次如果每次請(qǐng)求都打 MySQL即使結(jié)果表只有幾百行壓力也不大但加上緩存更穩(wěn)妥而且能避免大屏多個(gè)圖表同時(shí)刷新時(shí)造成短時(shí)尖峰。4.2 大屏端怎么接數(shù)據(jù)大屏我用的是 ECharts工程結(jié)構(gòu)很簡(jiǎn)單一個(gè) index.html一個(gè) api.js一個(gè) charts.js。前端從接口拿到數(shù)據(jù)之后只需要把數(shù)據(jù)映射到圖表 option 里。比如趨勢(shì)圖的靈魂代碼就這幾行async function loadTrend() { const res await fetch(/api/v1/overview/trend); const json await res.json(); trendChart.setOption({ xAxis: { data: json.data.map(i i.dt) }, series: [{ data: json.data.map(i i.sales) }] }); }有一點(diǎn)要注意大屏上顯示的標(biāo)題、時(shí)間范圍、單位最好都從接口或者統(tǒng)一配置里讀取不要寫(xiě)死在圖表代碼里。廚具銷(xiāo)售數(shù)據(jù)經(jīng)常要按周、按月切換寫(xiě)死之后每次改都要翻代碼維護(hù)成本很高。4.3 為什么要在 Django 和 Hive 之間加一個(gè)結(jié)果庫(kù)這是整套架構(gòu)里最關(guān)鍵的一步。Spark 分析完幾千萬(wàn)行明細(xì)后真正給人看的聚合結(jié)果往往只有幾百行。把這些結(jié)果寫(xiě)進(jìn) MySQL 的一張結(jié)果表Django 每次訪問(wèn)只查詢(xún)幾百行響應(yīng)速度可以穩(wěn)定在幾十毫秒。如果直接把 HiveServer2 暴露給 Django隨便一次刷新都會(huì)觸發(fā)一個(gè)分布式查詢(xún)而且 HiveServer2 還有并發(fā)連接數(shù)限制大屏輪詢(xún)一多就到了瓶頸。結(jié)果庫(kù)的清洗也很重要每天 Spark 任務(wù)跑完后先用TRUNCATE或者按日期刪除舊數(shù)據(jù)再寫(xiě)入新結(jié)果避免昨天和今天的數(shù)據(jù)混在一起。大屏上的數(shù)值出現(xiàn)莫名其妙的跳變十有八九是因?yàn)闆](méi)有做覆蓋寫(xiě)。5. 踩坑記錄環(huán)境、資源、接口三層最容易翻車(chē)的地方5.1 SparkSQL 連 Hive 報(bào)“元數(shù)據(jù)初始化失敗”這個(gè)錯(cuò)幾乎每個(gè)第一次跑 Spark on Hive 的人都會(huì)遇到。現(xiàn)象是程序一提交就報(bào)MetaException或者Could not connect to MetaStore。排查鏈路我一般是這樣走的先確認(rèn)hive-site.xml、core-site.xml、hdfs-site.xml是否在 Spark 任務(wù)的 classpath 里再確認(rèn) Hive Metastore 進(jìn)程是否真的啟動(dòng)了hive --service metastore不能是只在某個(gè)終端里跑了一下就掛了然后看 Metastore 連接 MySQL 的賬號(hào)密碼和權(quán)限有沒(méi)有問(wèn)題最后檢查spark.sql.warehouse.dir對(duì)應(yīng)的 HDFS 目錄是否存在、當(dāng)前用戶有沒(méi)有讀寫(xiě)權(quán)限。本地模式跑 SparkSQL 讀集群 Hive 時(shí)最容易踩的是把一套單機(jī)版的配置文件放到 classpath 里導(dǎo)致 Spark 拿著 localhost 去找集群的 NameNode 和 Metastore結(jié)果當(dāng)然是一堆 Connection refused。先用bin/spark-sql --conf spark.sql.hive.metastore.jarsbuiltin做一個(gè)最小化驗(yàn)證再回到完整工程里排錯(cuò)效率會(huì)高很多。5.2 Yarn 作業(yè)被容器殺掉物理內(nèi)存超限另一個(gè)高頻問(wèn)題是在集群模式提交 Spark 任務(wù)后日志里出現(xiàn)Container is running beyond physical memory limits。很多人的第一反應(yīng)是加內(nèi)存但這樣往往治標(biāo)不治本。先看 Spark UI 中各個(gè) stage 的 Shuffle Read 和 Input 大小如果某個(gè) stage 的輸入特別大說(shuō)明分區(qū)數(shù)太少或者數(shù)據(jù)傾斜加內(nèi)存只是讓問(wèn)題晚一點(diǎn)爆發(fā)。如果確認(rèn)只是 Yarn 容器預(yù)留的內(nèi)存不夠應(yīng)該調(diào)整spark.executor.memoryOverhead給容器預(yù)留出堆外內(nèi)存和基礎(chǔ)進(jìn)程的開(kāi)銷(xiāo)。比如 executor memory 設(shè) 6g 的時(shí)候memoryOverhead 至少也要 1g 到 2g。另外不要把spark.sql.shuffle.partitions隨便設(shè)成 1000分區(qū)數(shù)量過(guò)多會(huì)讓每個(gè) executor 承擔(dān)大量的小 task任務(wù)調(diào)度開(kāi)銷(xiāo)比計(jì)算本身還大。5.3 Django 接口返回空了但 MySQL 里有數(shù)這類(lèi)問(wèn)題最讓人崩潰因?yàn)閿?shù)據(jù)明明在接口卻查不到。我排查一圈之后發(fā)現(xiàn)基本都是三個(gè)原因第一Django ORM 的模型字段名和 MySQL 列名對(duì)不上比如日期字段叫dt模型里寫(xiě)了date第二從 MySQL 讀出的Decimal和datetime類(lèi)型沒(méi)有轉(zhuǎn)成 JSON 可序列化的基本類(lèi)型第三緩存里存了上一次的空結(jié)果沒(méi)有及時(shí)清掉。解決思路也很直接給 ORM 模型字段名和 MySQL 列名做一個(gè)統(tǒng)一約定所有日期字段統(tǒng)一用字符串返回接口層加一個(gè)統(tǒng)一的 JSON 序列化函數(shù)把Decimal、datetime、date都轉(zhuǎn)成字符串或浮點(diǎn)數(shù)開(kāi)發(fā)環(huán)境在重啟 Django 服務(wù)后執(zhí)行一次cache.clear()避免被舊緩存干擾判斷。5.4 時(shí)間都花在版本不匹配上如果項(xiàng)目從零搭版本兼容性是決定調(diào)試效率的關(guān)鍵。我的經(jīng)驗(yàn)是盡量選一整套主版本配好的組合避免自己亂配。組件推薦版本說(shuō)明Hadoop3.3.x穩(wěn)定生態(tài)兼容好Hive3.1.xSpark 集成最常用版本Spark3.3.x對(duì) Hive 3.1 支持成熟Django4.2.x使用 LTS 版本更省心Python3.8 或 3.10不要用最新大版本MySQL8.0Hive 元數(shù)據(jù)和結(jié)果庫(kù)通用JDK 我建議用 JDK 8 或 11。JDK 17 和舊版 Hive 一起跑容易遇到非法反射訪問(wèn)的告警雖然不一定直接報(bào)錯(cuò)但會(huì)把日志刷得很亂影響排障判斷。6. 把整套系統(tǒng)變成能交付的“源碼文檔調(diào)試記錄”6.1 工程目錄怎么組織別人拿到能少問(wèn)十句話很多人交付項(xiàng)目只給一團(tuán)代碼別人拿到之后根本不知道從哪里下手。我習(xí)慣把工程按層拆開(kāi)讓數(shù)據(jù)流一眼能看出來(lái)sale-analysis/ ├── sql/ │ ├── 01_ods_ddl.sql │ ├── 02_dwd_ddl.sql │ └── 03_ads_ddl.sql ├── spark/ │ ├── analysis_job.py │ ├── config.py │ └── requirements.txt ├── django/ │ ├── manage.py │ ├── sale_api/ │ └── result_models.py ├── dashboard/ │ ├── index.html │ ├── api.js │ └── charts.js └── docs/ ├── 環(huán)境部署.md ├── 數(shù)據(jù)導(dǎo)入.md └── 調(diào)試FAQ.mdSQL 腳本放在一起是因?yàn)閿?shù)據(jù)庫(kù)表結(jié)構(gòu)是整個(gè)系統(tǒng)的地基。Spark 工程負(fù)責(zé)清洗和計(jì)算。Django 工程負(fù)責(zé)接口。dashboard 是大屏前端。docs 里放部署文檔和常見(jiàn)問(wèn)題。別人拿到這套目錄之后最多問(wèn)“啟動(dòng)命令是什么”不會(huì)再去問(wèn)“這張表在哪建的”“這個(gè)圖表的數(shù)據(jù)從哪來(lái)”。6.2 文檔要覆蓋的三件事第一件事是環(huán)境版本矩陣。把操作系統(tǒng)、JDK、Python、Hadoop、Hive、Spark、Django、MySQL 的版本全部列出來(lái)避免接手的人裝了一套完全不同的版本跑出各種詭異問(wèn)題。第二件事是啟動(dòng)順序。HDFS、Yarn、Hive Metastore、Spark 任務(wù)、Django 服務(wù)每一步的啟動(dòng)命令和驗(yàn)證方法都要寫(xiě)清楚。第三件事是數(shù)據(jù)初始化和自查方法。比如用SHOW PARTITIONS dwd_kitchen_sale_detail看分區(qū)有沒(méi)有生成用SELECT COUNT(*) FROM dwd_kitchen_sale_detail WHERE dt2024-01-01看當(dāng)天數(shù)據(jù)量是否正常。調(diào)試記錄不要只寫(xiě)最終答案。最有價(jià)值的寫(xiě)法是“報(bào)錯(cuò)關(guān)鍵字 復(fù)現(xiàn)命令 排查路徑 最終解決方式”。我自己調(diào)試的時(shí)候會(huì)把一段卡了兩個(gè)小時(shí)的報(bào)錯(cuò)記錄成模板下次再遇到同類(lèi)問(wèn)題直接在文檔里搜索關(guān)鍵字比重新從零查一遍快得多。6.3 這個(gè)系統(tǒng)還能往哪里擴(kuò)展如果有人打算繼續(xù)做二期我建議從幾個(gè)方向入手。任務(wù)調(diào)度可以替換成正式的調(diào)度平臺(tái)加失敗重試、告警和補(bǔ)數(shù)邏輯結(jié)果存儲(chǔ)可以換成 ClickHouse 或 Doris讓大屏支持任意時(shí)間段拖選不用每次重新觸發(fā)離線任務(wù)實(shí)時(shí)性要求上來(lái)之后可以在訂單鏈路前面加消息隊(duì)列把分鐘級(jí)實(shí)時(shí)趨勢(shì)和離線分析互補(bǔ)如果想做更完整的用戶分析就把加購(gòu)、收藏、瀏覽日志也接進(jìn)來(lái)做轉(zhuǎn)化漏斗和連帶購(gòu)買(mǎi)分析。最后說(shuō)一個(gè)我自己的小習(xí)慣每次改完 Spark 邏輯我會(huì)先清掉 Django 緩存再刷新大屏頁(yè)面確認(rèn)數(shù)據(jù)變化避免被舊數(shù)據(jù)誤導(dǎo)。整套系統(tǒng)做完之后比起某個(gè)算法寫(xiě)得多漂亮更讓我省心的是分層清晰、接口穩(wěn)定、調(diào)試記錄完整——這三樣才是這種數(shù)據(jù)系統(tǒng)能長(zhǎng)期跑下去的關(guān)鍵。