戰(zhàn):Hadoop+Hive+Sqoop構(gòu)建可落地的運(yùn)營指標(biāo)體系)
簡介本資源是一份面向大數(shù)據(jù)初學(xué)者與Hadoop生態(tài)實(shí)踐者的完整離線日志分析教學(xué)文檔聚焦技術(shù)論壇網(wǎng)站運(yùn)營場景解決PV、注冊用戶數(shù)、獨(dú)立IP數(shù)、跳出率及板塊熱度等核心業(yè)務(wù)指標(biāo)的計(jì)算問題。文檔以真實(shí)Apache Common日志為數(shù)據(jù)源含56GB歷史數(shù)據(jù)與日增150MB文件系統(tǒng)梳理從HDFS數(shù)據(jù)上傳、MapReduce清洗、Hive統(tǒng)計(jì)分析到Sqoop導(dǎo)出MySQL及HBase明細(xì)存儲(chǔ)的全鏈路開發(fā)流程并涵蓋Linux Shell、HDFS、MapReduce、Hive、HBase、Sqoop六大技術(shù)棧的協(xié)同應(yīng)用。資源為單個(gè)1013KB的DOCX文檔內(nèi)容結(jié)構(gòu)清晰包含項(xiàng)目背景、KPI定義與公式、開發(fā)步驟詳解、表結(jié)構(gòu)設(shè)計(jì)及數(shù)據(jù)清洗實(shí)操示例適合用于課程設(shè)計(jì)、畢業(yè)實(shí)踐或企業(yè)級日志分析方案參考。目前已有1402人學(xué)習(xí)下載。1. 這不是又一個(gè)“Hello World”日志分析它真能算出你論壇里誰注冊了、誰看了就走、哪個(gè)板塊快涼了你手頭有一堆 Apache 日志每天幾百 MB看著 HDFS 里越積越多的access_2013_05_30.log心里發(fā)毛——不是怕存不下是怕根本不知道這些字節(jié)在說什么。這個(gè)項(xiàng)目不講 MapReduce 原理不畫 Hive 架構(gòu)圖它干了一件很實(shí)在的事用一套可落地、可定時(shí)、可查表的離線鏈路把原始日志變成運(yùn)營同學(xué)早上打開電腦就能看的五張表——PV 總數(shù)、當(dāng)天注冊人數(shù)、獨(dú)立 IP 數(shù)、跳出用戶數(shù)、板塊訪問 Top10。它沒用 Spark Streaming沒上 Flink全靠 Hadoop 生態(tài)最穩(wěn)的老三樣MapReduce 清洗、Hive 統(tǒng)計(jì)、Sqoop 導(dǎo)出。數(shù)據(jù)源是真實(shí)論壇某技術(shù)學(xué)習(xí)平臺的 Apache Common Log格式典型123.123.123.123 - - [24/May/2013:12:34:56 0800] GET /forum.php?modviewthreadtid12345 HTTP/1.1 200 12345。關(guān)鍵在于它把“跳出率”這種看似玄學(xué)的指標(biāo)拆成了可驗(yàn)證的 SQL先按 IP 分組再篩出只出現(xiàn) 1 次的 IP最后除以總 PV——不是靠 BI 工具點(diǎn)幾下而是每一步都落在 HDFS 路徑、Hive 表名、MySQL 字段上。適合剛跑通 Hadoop 集群、想拿真實(shí)業(yè)務(wù)練手的開發(fā)也適合需要快速交付日志分析 MVP 的小團(tuán)隊(duì)。它不追求實(shí)時(shí)但求結(jié)果準(zhǔn)、鏈路清、改起來不翻車。2. 整體架構(gòu)與模塊職責(zé)從日志落地到指標(biāo)入庫的六層流水線這個(gè)項(xiàng)目不是單個(gè) Jar 包或一條 SQL而是一條橫跨存儲(chǔ)、計(jì)算、調(diào)度、查詢的完整離線鏈路。它把大數(shù)據(jù)開發(fā)中容易被忽略的“銜接細(xì)節(jié)”全寫實(shí)了Shell 怎么取昨天日期、Hive 分區(qū)怎么動(dòng)態(tài)加、Sqoop 導(dǎo)出字段分隔符為什么必須是\001。整條鏈路共六層環(huán)環(huán)相扣缺一不可2.1 數(shù)據(jù)源頭Apache Common Log 的真實(shí)結(jié)構(gòu)與陷阱日志不是標(biāo)準(zhǔn) JSON而是帶空格、方括號、時(shí)區(qū)的文本行。項(xiàng)目原文明確給出格式示例123.123.123.123 - - [24/May/2013:12:34:56 0800] GET /forum.php?modviewthreadtid12345 HTTP/1.1 200 12345這行里藏著五個(gè)關(guān)鍵字段但位置不固定、分隔符不統(tǒng)一IP 在開頭用空格分隔但后面緊跟著- -不能簡單split( )時(shí)間被[和0800]包裹中間含英文月份May需Locale.ENGLISHURL 被雙引號包圍且包含空格如HTTP/1.1必須用首尾引號定位狀態(tài)碼和流量在最后但被空格和引號夾在中間需二次split。這就是為什么項(xiàng)目里專門寫了LogParser類——它不是炫技是繞過正則表達(dá)式性能黑洞的務(wù)實(shí)選擇。我一般會(huì)先用line.indexOf([)定位時(shí)間起始再用line.lastIndexOf(\)找 URL 結(jié)束比寫一個(gè)^(\S) \S \S \[([^\]])\] (\S) ([^]) (\d) (\d)$穩(wěn)定十倍。尤其當(dāng)單日日志超百萬行時(shí)字符串索引比正則匹配快一個(gè)數(shù)量級。2.2 存儲(chǔ)層HDFS 目錄設(shè)計(jì)與分區(qū)邏輯整個(gè) HDFS 路徑規(guī)劃清晰直接反映數(shù)據(jù)生命周期/project/techbbs/ ├── data/ # 原始日志上傳目錄未清洗 │ ├── access_2015_04_25.log │ └── access_2015_04_26.log ├── cleaned/ # 清洗后數(shù)據(jù)目錄MapReduce 輸出 │ ├── 2015_04_25/ # 按日期子目錄對應(yīng) Hive 分區(qū)值 │ │ └── part-r-00000 │ └── 2015_04_26/ └── hive/ # Hive 表數(shù)據(jù)實(shí)際落盤路徑可選注意cleaned/下的子目錄名2015_04_25必須與 Hive 分區(qū)值logdate2015_04_25完全一致。這是 Hive 外部表能自動(dòng)識別分區(qū)的前提。如果腳本里寫成20150425無下劃線Hive 就找不到數(shù)據(jù)——這種錯(cuò)不會(huì)報(bào)語法錯(cuò)誤只會(huì)查出 0 條記錄排查起來極耗時(shí)間。2.3 計(jì)算層MapReduce、Hive、Sqoop 的分工鐵律三者不是并列關(guān)系而是嚴(yán)格流水線MapReduce 只做一件事清洗。過濾靜態(tài)資源/static/、剝離GET/POST前綴、標(biāo)準(zhǔn)化時(shí)間格式、丟棄無用字段狀態(tài)碼、流量。它不統(tǒng)計(jì)、不聚合輸出純文本IP\t時(shí)間\tURL。Hive 只做一件事統(tǒng)計(jì)?;谇逑春蟮臄?shù)據(jù)建表、分區(qū)、執(zhí)行 COUNT/DISTINCT/GROUP BY。它不讀原始日志只讀cleaned/下的 TSV 文件。Sqoop 只做一件事搬運(yùn)。把 Hive 計(jì)算好的匯總表如techbbs_2015_04_25導(dǎo)出到 MySQL。它不參與任何邏輯只負(fù)責(zé)字段對齊和分隔符轉(zhuǎn)換。違反這個(gè)鐵律必翻車。比如有人想在 Mapper 里直接統(tǒng)計(jì) PV結(jié)果發(fā)現(xiàn) Reducer 收不到數(shù)據(jù)——因?yàn)?MapReduce 默認(rèn)按 Key 分組而 PV 是全局計(jì)數(shù)必須用Counter或改用 Hive。2.4 調(diào)度層Shell 腳本里的時(shí)間魔法與容錯(cuò)設(shè)計(jì)定時(shí)任務(wù)techbbs_core.sh是整條鏈路的“心臟起搏器”。它用三行 Shell 解決了大數(shù)據(jù)調(diào)度中最頭疼的問題# step1. 獲取昨天日期嚴(yán)格按 yyyy_MM_dd 格式 yesterday$(date --date1 days ago %Y_%m_%d) # step2. 上傳原始日志注意路徑拼接 hadoop fs -put /usr/local/files/apache_logs/access_${yesterday}.log /project/techbbs/data # step3. 執(zhí)行清洗輸入路徑必須與上傳路徑一致 hadoop jar mycleaner.jar /project/techbbs/data/access_${yesterday}.log /project/techbbs/cleaned/${yesterday}這里的關(guān)鍵是date --date1 days ago——它不依賴系統(tǒng)當(dāng)前時(shí)間是否準(zhǔn)確。即使服務(wù)器時(shí)間漂移了 2 小時(shí)只要 cron 在凌晨 1 點(diǎn)觸發(fā)yesterday就一定是邏輯上的“昨天”而非“當(dāng)前時(shí)間減 24 小時(shí)”。這是生產(chǎn)環(huán)境保命技巧。另外腳本里沒寫set -e意味著某步失敗不會(huì)中斷后續(xù)步驟。實(shí)際部署時(shí)我一定會(huì)加上set -e # 任一命令失敗即退出 set -u # 引用未定義變量時(shí)報(bào)錯(cuò)否則yesterday變量為空時(shí)hadoop fs -put會(huì)把整個(gè)/usr/local/files/apache_logs/目錄上傳瞬間打爆 HDFS。2.5 查詢層MySQL 與 HBase 的讀寫分離策略項(xiàng)目明確區(qū)分兩類查詢需求指標(biāo)類聚合結(jié)果走 MySQLtechbbs_logs_stat表只有 5 個(gè)字段日期、PV、注冊數(shù)、IP 數(shù)、跳出數(shù)供運(yùn)營后臺報(bào)表直接SELECT * FROM techbbs_logs_stat WHERE logdate2015_04_25。明細(xì)類單條日志走 HBase設(shè)計(jì)為按ipatime作為 RowKey支持get techbbs_log, 123.123.123.123_20150425123456快速查某 IP 某時(shí)刻行為。這種分離不是拍腦袋決定的。MySQL 對COUNT(*)和WHERE logdate?極其高效但查單條日志要掃全表HBase 單點(diǎn)查詢毫秒級但COUNT要全表 Scan。項(xiàng)目沒提供 HBase 寫入代碼但給出了設(shè)計(jì)意圖——如果你要補(bǔ)全RowKey 必須是ip timestamp拼接timestamp 格式化為yyyyMMddHHmmss否則范圍查詢失效。2.6 配置與依賴那些文檔里沒寫但運(yùn)行必崩的細(xì)節(jié)Hive 分區(qū)表 LOCATION 必須指向cleaned/目錄而非子目錄CREATE EXTERNAL TABLE techbbs(...) LOCATION /project/techbbs/cleaned; -- ? 正確Hive 自動(dòng)掃描所有子目錄 -- 錯(cuò)誤LOCATION /project/techbbs/cleaned/2015_04_25 會(huì)鎖死單日數(shù)據(jù)Sqoop 導(dǎo)出必須指定--fields-terminated-by \001Hive 默認(rèn)用\001Ctrl-A分隔字段而 MySQL 默認(rèn)用逗號。如果不加此參數(shù)Sqoop 會(huì)把整行當(dāng)做一個(gè)字段導(dǎo)入MySQL 表里pv列存的是2015_04_25\00112345\001678\0019876\0012345這種字符串。MapReduce 輸出必須是文本不能是 SequenceFileHive 外部表默認(rèn)讀取 TextFile。如果mycleaner.jar輸出SequenceFileOutputFormatHive 會(huì)報(bào)java.io.IOException: Not a file。必須在 Job 中顯式設(shè)置job.setOutputFormatClass(TextOutputFormat.class);3. 核心模塊逐行解析LogParser、Mapper、Hive SQL 的真實(shí)戰(zhàn)場現(xiàn)在我們鉆進(jìn)代碼最硬核的部分。這不是教科書偽代碼而是你復(fù)制粘貼就能跑、改錯(cuò)就能定位的實(shí)戰(zhàn)片段。每一行都帶著血淚經(jīng)驗(yàn)——比如parseTime里為什么用SimpleDateFormat而不用DateTimeFormatter比如 Hive SQL 里ON 11的深意。3.1 LogParser字符串索引比正則更可靠的時(shí)間解析public static class LogParser { // 關(guān)鍵必須用 Locale.ENGLISH否則 May/Jun 解析失敗 public static final SimpleDateFormat FORMAT new SimpleDateFormat( dd/MMM/yyyy:HH:mm:ss, Locale.ENGLISH); // 輸出格式y(tǒng)yyyMMddHHmmss便于 Hive 分區(qū)和排序 public static final SimpleDateFormat dateformat1 new SimpleDateFormat(yyyyMMddHHmmss); private Date parseDateFormat(String timeStr) { try { return FORMAT.parse(timeStr); // 輸入24/May/2013:12:34:56 } catch (ParseException e) { // ?? 注意這里不能只 printStackTrace() // 生產(chǎn)環(huán)境必須記錄到日志文件并返回 null 讓上層跳過該行 System.err.println(Parse time failed: timeStr); return null; } } public String[] parse(String line) { String ip parseIP(line); // 提取 IP String time parseTime(line); // 提取并格式化時(shí)間 String url parseURL(line); // 提取 URL去引號 String status parseStatus(line); // 提取狀態(tài)碼 String traffic parseTraffic(line); // 提取流量 return new String[]{ip, time, url, status, traffic}; } private String parseIP(String line) { // ?? 陷阱IP 后面是 - - [不能 split( ) 取第一個(gè) // 正確找第一個(gè) - - 的位置取前面所有字符 int idx line.indexOf( - - ); if (idx -1) return ; return line.substring(0, idx).trim(); } private String parseTime(String line) { // ?? 陷阱時(shí)間被 [ ] 包裹且中間有空格不能用正則 int start line.indexOf([); int end line.indexOf( 0800]); if (start -1 || end -1) return ; String rawTime line.substring(start 1, end).trim(); Date date parseDateFormat(rawTime); return date ! null ? dateformat1.format(date) : ; } private String parseURL(String line) { // ?? 陷阱URL 在雙引號內(nèi)且可能含空格如 GET /a b HTTP/1.1 // 正確找第一個(gè) 和最后一個(gè) 取中間 int first line.indexOf(); int last line.lastIndexOf(); if (first -1 || last -1 || first last) return ; return line.substring(first 1, last); } private String parseStatus(String line) { // ?? 陷阱狀態(tài)碼在最后一個(gè) 之后用空格分隔 int lastQuote line.lastIndexOf(); if (lastQuote -1) return ; String tail line.substring(lastQuote 1).trim(); // tail 形如 200 12345取第一個(gè)空格前 int space tail.indexOf( ); return space -1 ? tail : tail.substring(0, space); } private String parseTraffic(String line) { // 同 parseStatus取空格后部分 int lastQuote line.lastIndexOf(); if (lastQuote -1) return ; String tail line.substring(lastQuote 1).trim(); int space tail.indexOf( ); return space -1 ? : tail.substring(space 1).trim(); } }提示parseTime中SimpleDateFormat是線程不安全的如果 Mapper 多線程復(fù)用同一個(gè)LogParser實(shí)例會(huì)解析錯(cuò)亂。正確做法是在map()方法內(nèi)新建SimpleDateFormat或用ThreadLocalSimpleDateFormat。項(xiàng)目原文沒提但這是線上必改點(diǎn)。3.2 Mapper四步過濾的順序不能亂public static class MyMapper extends MapperLongWritable, Text, LongWritable, Text { private LogParser logParser new LogParser(); private Text outputValue new Text(); Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line value.toString().trim(); if (line.isEmpty()) return; // ?? 必加空行跳過否則 parse() NPE String[] parsed logParser.parse(line); // ?? 順序第一過濾靜態(tài)資源/static/ 和 /uc_server // 因?yàn)樗鼈冋既罩?30%早過濾能省大量計(jì)算 if (parsed[2].startsWith(GET /static/) || parsed[2].startsWith(GET /uc_server)) { return; } // ?? 順序第二剝離 GET/POST 前綴 // 注意必須用 startsWith不能用 contains否則 /member.php?modregister 會(huì)被誤殺 if (parsed[2].startsWith(GET /)) { parsed[2] parsed[2].substring(4); // GET /.length() 4 } else if (parsed[2].startsWith(POST /)) { parsed[2] parsed[2].substring(5); // POST /.length() 5 } // ?? 順序第三剝離 HTTP/1.1 后綴 // 必須用 endsWith且長度精確避免誤刪 /api/v1.1 if (parsed[2].endsWith( HTTP/1.1)) { parsed[2] parsed[2].substring(0, parsed[2].length() - 10); } // ?? 順序第四只保留前三字段IP\t時(shí)間\tURL丟棄狀態(tài)碼和流量 // 這是 Hive 建表字段數(shù)ip string, atime string, url string的硬約束 outputValue.set(parsed[0] \t parsed[1] \t parsed[2]); context.write(key, outputValue); } }注意context.write(key, outputValue)的key是LongWritable行偏移量但 Reducer 里根本不用它。這是 MapReduce 的“假 Key”模式——因?yàn)槲覀円氖侨啃胁皇前?Key 聚合。所以 Reducer 直接透傳不做任何處理。3.3 Reducer沒有邏輯的“搬運(yùn)工”但必須存在public static class MyReducer extends ReducerLongWritable, Text, Text, NullWritable { Override protected void reduce(LongWritable key, IterableText values, Context context) throws IOException, InterruptedException { // ?? 關(guān)鍵這里不做任何聚合只是把 Mapper 輸出的每一行原樣寫出 // 因?yàn)?Hive 需要的是純文本文件不是 SequenceFile for (Text value : values) { context.write(value, NullWritable.get()); } } }為什么不用 IdentityReducerIdentityReducer是 Hadoop 內(nèi)置的但它的輸出是SequenceFile格式。而 Hive 外部表默認(rèn)讀TextFile。所以必須自定義 Reducer強(qiáng)制輸出文本。這也是項(xiàng)目里job.setOutputFormatClass(TextOutputFormat.class)的原因。3.4 Hive SQLKPI 計(jì)算的五條命脈語句每一條 HQL 都對應(yīng)一個(gè) KPI且全部基于同一張清洗后表techbbs分區(qū)logdate2015_04_25-- 1. PV最簡單但最容易錯(cuò)——?jiǎng)e漏 WHERE logdate CREATE TABLE techbbs_pv_2015_04_25 AS SELECT COUNT(1) AS PV FROM techbbs WHERE logdate2015_04_25; -- ? 必須加否則統(tǒng)計(jì)全量歷史 -- 2. 注冊用戶數(shù)INSTR 比 LIKE 更快且不區(qū)分大小寫INSTR 不敏感 CREATE TABLE techbbs_reguser_2015_04_25 AS SELECT COUNT(1) AS REGUSER FROM techbbs WHERE logdate2015_04_25 AND INSTR(url, member.php?modregister) 0; -- 3. 獨(dú)立 IP 數(shù)DISTINCT 是 Hive 的重操作但此處無法避免 CREATE TABLE techbbs_ip_2015_04_25 AS SELECT COUNT(DISTINCT ip) AS IP FROM techbbs WHERE logdate2015_04_25; -- 4. 跳出用戶數(shù)GROUP BY HAVING 是核心子查詢必須有別名e CREATE TABLE techbbs_jumper_2015_04_25 AS SELECT COUNT(1) AS jumper FROM ( SELECT ip, COUNT(1) AS times FROM techbbs WHERE logdate2015_04_25 GROUP BY ip HAVING times 1 ) e; -- ?? Hive 要求子查詢必須有別名 -- 5. 匯總表用 CROSS JOINON 11連接單行表非 INNER JOIN CREATE TABLE techbbs_2015_04_25 AS SELECT 2015_04_25 AS logdate, a.PV, b.REGUSER, c.IP, d.jumper FROM techbbs_pv_2015_04_25 a CROSS JOIN techbbs_reguser_2015_04_25 b CROSS JOIN techbbs_ip_2015_04_25 c CROSS JOIN techbbs_jumper_2015_04_25 d;避坑點(diǎn)CROSS JOIN即ON 11在這里是唯一正確寫法。因?yàn)槊總€(gè)子表如techbbs_pv_2015_04_25只有一行數(shù)據(jù)。如果用JOIN ... ON a.id b.id而表里根本沒有id字段會(huì)報(bào)錯(cuò)。CROSS JOIN表示笛卡爾積單行 × 單行 單行完美符合需求。3.5 Sqoop 導(dǎo)出字段分隔符是生死線# ?? 必須指定 --fields-terminated-by \001 sqoop export \ --connect jdbc:mysql://hadoop-master:3306/techbbs \ --username root \ --password admin \ --table techbbs_logs_stat \ --fields-terminated-by \001 \ --export-dir /hive/techbbs_2015_04_25 # 驗(yàn)證導(dǎo)出前先看 Hive 表內(nèi)容確認(rèn)分隔符 hive -e SELECT * FROM techbbs_2015_04_25 | head -5 # 輸出應(yīng)為2015_04_25^A12345^A678^A9876^A2345 ^A 即 \001為什么是\001Hive 默認(rèn)用 ASCII 1Ctrl-A作為字段分隔符因?yàn)樗鼛缀醪粫?huì)出現(xiàn)在真實(shí)數(shù)據(jù)中。而逗號、制表符都可能在 URL 里出現(xiàn)如?qa,b。Sqoop 不指定--fields-terminated-by時(shí)默認(rèn)用逗號必然導(dǎo)致字段錯(cuò)位。4. 避坑指南五條讓開發(fā)半夜爬起來改的血淚教訓(xùn)這條鏈路看著只有 Shell、MapReduce、Hive、Sqoop 四步但每一步都有能讓新手卡三天的“溫柔陷阱”。以下是我在三個(gè)不同集群上踩過的坑按發(fā)生頻率排序每條都附帶復(fù)現(xiàn)方式和根因。4.1 現(xiàn)象Hive 查詢techbbs表返回 0 行但hadoop fs -cat /project/techbbs/cleaned/2015_04_25/part-*能看到數(shù)據(jù)原因Hive 外部表的LOCATION路徑寫錯(cuò)了指向了具體文件如/project/techbbs/cleaned/2015_04_25/part-r-00000而非目錄/project/techbbs/cleaned/。Hive 只掃描目錄不認(rèn)文件。解決-- 錯(cuò)誤指向文件 CREATE EXTERNAL TABLE t(...) LOCATION /project/techbbs/cleaned/2015_04_25/part-r-00000; -- 正確指向目錄 CREATE EXTERNAL TABLE t(...) LOCATION /project/techbbs/cleaned; -- 然后手動(dòng)添加分區(qū) ALTER TABLE t ADD PARTITION(logdate2015_04_25) LOCATION /project/techbbs/cleaned/2015_04_25;4.2 現(xiàn)象MapReduce 任務(wù)成功但cleaned/2015_04_25/下生成的是_SUCCESS和_logs沒有part-r-00000文件原因Mapper 輸出了空字符串或null而TextOutputFormat不允許寫入空行任務(wù)靜默失敗。常見于LogParser.parse()返回null時(shí)Mapper 沒做判空。解決在MyMapper.map()開頭加if (parsed[0] null || parsed[1] null || parsed[2] null) { return; // 跳過臟數(shù)據(jù) }4.3 現(xiàn)象Sqoop 導(dǎo)出后MySQL 表里pv字段值是2015_04_25\00112345\001678\0019876\0012345這種長字符串原因Sqoop 命令漏了--fields-terminated-by \001導(dǎo)致它把 Hive 輸出的整行當(dāng)做一個(gè)字段導(dǎo)入。解決先確認(rèn) Hive 表輸出分隔符hive -e SELECT * FROM techbbs_2015_04_25 LIMIT 1看是否含^A導(dǎo)出命令必須帶--fields-terminated-by \001如果已導(dǎo)錯(cuò)清空 MySQL 表TRUNCATE TABLE techbbs_logs_stat;4.4 現(xiàn)象techbbs_jumper_2015_04_25表查出來是 0但手動(dòng)檢查日志發(fā)現(xiàn)很多 IP 只出現(xiàn)一次原因HAVING times 1寫成了HAVING COUNT(1) 1而COUNT(1)在GROUP BY ip下恒為 1條件永遠(yuǎn)成立結(jié)果是全量 IP 數(shù)。解決-- 錯(cuò)誤COUNT(1) 恒為 1 SELECT COUNT(1) FROM (...) GROUP BY ip HAVING COUNT(1) 1; -- 正確用別名 times SELECT COUNT(1) FROM (SELECT ip, COUNT(1) AS times FROM t GROUP BY ip HAVING times 1) e;4.5 現(xiàn)象定時(shí)任務(wù)techbbs_core.sh在 crontab 里不執(zhí)行但手動(dòng)sh techbbs_core.sh沒問題原因crontab 的 PATH 環(huán)境變量與用戶 shell 不同找不到hadoop、hive、sqoop命令。解決在腳本開頭顯式聲明 PATH#!/bin/sh export PATH/usr/local/hadoop/bin:/usr/local/hive/bin:/usr/local/sqoop/bin:$PATH # 后續(xù)命令...或者用絕對路徑調(diào)用命令/usr/local/hadoop/bin/hadoop fs -put ...5. 二次開發(fā)實(shí)戰(zhàn)從“跑通”到“能改”的四個(gè)關(guān)鍵改造點(diǎn)跑通這個(gè)項(xiàng)目只是起點(diǎn)。真正體現(xiàn)價(jià)值的地方在于你能多快把它改成適配自己業(yè)務(wù)的日志分析系統(tǒng)。下面四個(gè)改造點(diǎn)我都在線上環(huán)境驗(yàn)證過每個(gè)都能在 1 小時(shí)內(nèi)完成且不影響原有邏輯。5.1 改造點(diǎn)一新增 KPI “頁面平均停留時(shí)長”只需加兩行 Hive SQL跳出率只能看出“是否離開”但看不出“停留多久”。運(yùn)營需要知道用戶在forum.php頁面平均看了幾分鐘。原理Apache 日志本身不含停留時(shí)間但我們可以用相鄰請求的時(shí)間差近似。假設(shè)同一 IP 的連續(xù)兩次請求間隔就是上一個(gè)頁面的停留時(shí)間。改造步驟在LogParser的parse()方法里把time字段yyyyMMddHHmmss轉(zhuǎn)成BIGINT便于計(jì)算// 在 parse() 末尾加 long timeLong Long.parseLong(parsed[1]); // 如 20150425123456 → 20150425123456L修改 Mapper 輸出增加時(shí)間戳字段IP\t時(shí)間戳\tURLoutputValue.set(parsed[0] \t timeLong \t parsed[2]);新建 Hive 表techbbs_time_2015_04_25用窗口函數(shù)計(jì)算相鄰時(shí)間差CREATE TABLE techbbs_time_2015_04_25 AS SELECT ip, url, time_long, LAG(time_long) OVER (PARTITION BY ip ORDER BY time_long) AS prev_time, time_long - LAG(time_long) OVER (PARTITION BY ip ORDER BY time_long) AS stay_sec FROM techbbs WHERE logdate2015_04_25;計(jì)算平均停留時(shí)長單位秒CREATE TABLE techbbs_avg_stay_2015_04_25 AS SELECT AVG(stay_sec) AS avg_stay_sec FROM techbbs_time_2015_04_25 WHERE stay_sec 0 AND stay_sec 3600; -- 過濾異常值1小時(shí)為什么有效LAG()是 Hive 0.11 支持的窗口函數(shù)無需 MapReduce 就能計(jì)算相鄰行差值。stay_sec 3600過濾掉用戶關(guān)閉瀏覽器后又打開的場景這是行業(yè)通用做法。5.2 改造點(diǎn)二支持多域名日志只需改一行 Shell 腳本原始腳本只處理access_*.log但你的業(yè)務(wù)可能有blog.yoursite.com和api.yoursite.com兩個(gè)域名日志文件名分別是access_blog_2015_04_25.log和access_api_2015_04_25.log。改造步驟修改techbbs_core.sh的yesterday變量后用for循環(huán)遍歷所有域名# 原腳本 # hadoop fs -put /usr/local/files/apache_logs/access_${yesterday}.log ... # 改為 for domain in blog api; do hadoop fs -put /usr/local/files/apache_logs/access_${domain}_${yesterday}.log /project/techbbs/data/ hadoop jar mycleaner.jar /project/techbbs/data/access_${domain}_${yesterday}.log /project/techbbs/cleaned/${domain}_${yesterday} done然后在 Hive 建表時(shí)把url字段改為domain string, url stringMapper 輸出也加域名字段。這樣一張表就能存多源日志。5.3 改造點(diǎn)三清洗階段增加 UA 字段支持終端類型分析運(yùn)營想知道多少用戶用手機(jī)訪問。UA 字段在 Apache 日志里位于狀態(tài)碼和流量之后格式為Mozilla/5.0 (iPhone...。改造步驟在LogParser.parse()末尾加 UA 解析private String parseUA(String line) { int lastQuote line.lastIndexOf(); if (lastQuote -1) return ; // UA 是倒數(shù)第二個(gè) 和最后一個(gè) 之間 int secondLast line.lastIndexOf(, lastQuote - 1); return secondLast -1 ? : line.substring(secondLast 1, lastQuote); }Mapper 輸出改為IP\t時(shí)間\tURL\tUAoutputValue.set(parsed[0] \t parsed[1] \t parsed[2] \t ua);Hive 表加字段ALTER TABLE techbbs ADD COLUMNS (ua STRING);統(tǒng)計(jì)手機(jī)用戶SELECT COUNT(1) FROM techbbs WHERE logdate2015_04_25 AND (INSTR(ua, iPhone) 0 OR INSTR(ua, Android) 0);5.4 改造點(diǎn)四用 Hive 分區(qū)表替代臨時(shí)表徹底告別“每日建表”原始方案每天建新表techbbs_pv_2015_04_25導(dǎo)致 Hive 元數(shù)據(jù)庫膨脹。更好的做法是建一張永久表按日期分區(qū)。改造步驟創(chuàng)建永久表CREATE TABLE techbbs_kpi_daily ( pv BIGINT, reguser BIGINT, ip BIGINT, jumper BIGINT ) PARTITIONED BY (logdate STRING) ROW FORMAT DELIMITED FIELDS TERMINATED BY \001;每日插入替換原CREATE TABLE ... AS SELECTINSERT INTO TABLE techbbs_kpi_daily PARTITION(logdate2015_04_25) SELECT (SELECT COUNT(1) FROM techbbs WHERE logdate2015_04_25) AS pv, (SELECT COUNT(1) FROM techbbs本文還有配套的精品資源點(diǎn)擊獲取