当我们需要处理电商平台每天产生的TB级用户行为数据时,传统单机数据库已经力不从心。基于Hadoop的分布式架构成为了必然选择,这套系统在我参与的多个电商平台项目中验证了其可靠性和扩展性。
数据采集层采用Flume+Kafka组合拳,这是经过验证的最佳实践。Flume的agent部署在每台前端服务器上,实时收集Nginx日志(每分钟约2GB原始数据),通过Kafka实现削峰填谷。我们曾在一个促销日处理过峰值每秒10万条消息的场景,Kafka的partition数量需要根据业务量预先做好规划。
存储层的HDFS采用三副本策略,配合冷热数据分离存储。热数据(最近30天)放在SSD存储节点,历史数据迁移到普通磁盘。这里有个经验值:当单个目录下文件超过5000个时,NameNode内存占用会显著增加,需要按日期分目录存储。
计算层的选型很有讲究:
在hdfs-site.xml中这几个参数需要特别关注:
xml复制<property>
<name>dfs.blocksize</name>
<value>256m</value> <!-- 电商日志适合中等块大小 -->
</property>
<property>
<name>dfs.namenode.handler.count</name>
<value>100</value> <!-- 高并发访问时需要增加 -->
</property>
YARN资源配置示例(32节点集群):
bash复制# 每个NodeManager可用资源
yarn.nodemanager.resource.memory-mb=122880 # 120GB
yarn.nodemanager.resource.cpu-vcores=32
# 单个容器最大资源
yarn.scheduler.maximum-allocation-mb=24576 # 24GB
原始日志需要经过标准化处理,我们开发的ETL流程包含:
Spark处理代码优化技巧:
scala复制// 使用DataFrame API比RDD效率提升40%
val sessions = spark.read.parquet("/logs/clickstream")
.groupBy($"user_id", $"session_id")
.agg(
count("*").alias("pageviews"),
collect_list($"page_url").alias("path")
)
// 漏斗分析示例
val funnel = sessions.filter($"path".contains("cart"))
.filter($"path".contains("checkout"))
.count()
面对亿级用户UV统计,HyperLogLog是救星。我们在Redis中实现的方案:
python复制import redis
from datetime import date
r = redis.Redis()
today = date.today().isoformat()
# 添加用户
r.pfadd(f"uv:{today}", user_id)
# 获取统计
daily_uv = r.pfcount(f"uv:{today}")
weekly_uv = r.pfmerge("uv:week", *[f"uv:{today-i}" for i in range(7)])
注意:Redis集群模式下PFCOUNT需要所有key在同一个hash slot,可以通过hashtag确保:
uv:{20240301}和uv:{20240302}会被分配到相同slot
HiveQL实现完整RFM计算:
sql复制WITH user_stats AS (
SELECT
user_id,
DATEDIFF(CURRENT_DATE, MAX(order_time)) AS recency,
COUNT(DISTINCT order_id) AS frequency,
SUM(amount) AS monetary
FROM orders
WHERE order_time > DATE_SUB(CURRENT_DATE, 365)
GROUP BY user_id
),
rfm_scores AS (
SELECT
user_id,
NTILE(5) OVER (ORDER BY recency DESC) AS r_score,
NTILE(5) OVER (ORDER BY frequency) AS f_score,
NTILE(5) OVER (ORDER BY monetary) AS m_score
FROM user_stats
)
INSERT INTO TABLE user_profiles
SELECT
user_id,
CONCAT(r_score, f_score, m_score) AS rfm,
CASE
WHEN r_score >=4 AND f_score >=4 THEN '高价值'
WHEN r_score >=3 AND m_score >=3 THEN '潜力客户'
ELSE '一般客户'
END AS seg_name
FROM rfm_scores;
HBase表设计要点:
reversed_uid|timestamp)创建表示例:
bash复制create 'user_profile',
{NAME => 'base', VERSIONS => 1},
{NAME => 'behavior', VERSIONS => 30, TTL => 7776000},
{SPLITS => ['100000','200000','300000']}
Spark ALS算法关键参数:
scala复制val als = new ALS()
.setRank(50) // 隐因子数量
.setMaxIter(15) // 迭代次数
.setRegParam(0.01) // 正则化系数
.setAlpha(1.0) // 隐式反馈系数
.setImplicitPrefs(true) // 使用隐式反馈
.setColdStartStrategy("drop") // 处理冷启动
踩坑记录:当用户数超过1亿时,需要调整executor内存并启用off-heap内存:
spark.executor.memoryOverhead=4g
Flink+Redis的实时推荐管道:
java复制DataStream<UserEvent> events = env
.addSource(new KafkaSource())
.keyBy(UserEvent::getUserId);
// 近实时特征更新
events.process(new FeatureUpdater())
.addSink(new RedisSink());
// 窗口统计
events.window(SlidingEventTimeWindows.of(Time.hours(1), Time.minutes(5)))
.aggregate(new BehaviorAggregator())
.addSink(new HBaseSink());
处理1TB点击日志的优化对比:
| 优化项 | 原耗时 | 优化后 | 参数调整 |
|---|---|---|---|
| Map任务数 | 500 | 2000 | mapreduce.job.maps=2000 |
| 压缩中间结果 | 无 | Snappy | mapreduce.map.output.compress=true |
| Combiner | 无 | 启用 | setCombinerClass(Reducer.class) |
| JVM重用 | 关闭 | 开启 | mapreduce.job.jvm.numtasks=10 |
广告分析查询优化前后对比:
sql复制-- 优化前
SELECT ad_id, COUNT(DISTINCT user_id)
FROM clicks GROUP BY ad_id;
-- 优化后
WITH distinct_users AS (
SELECT ad_id, user_id
FROM clicks
GROUP BY ad_id, user_id
)
SELECT ad_id, COUNT(*)
FROM distinct_users
GROUP BY ad_id;
配合以下配置效果更佳:
bash复制spark.sql.shuffle.partitions=200
spark.sql.adaptive.enabled=true
spark.sql.adaptive.coalescePartitions.enabled=true
中型电商集群参考配置(20节点):
| 组件 | CPU | 内存 | 磁盘 | 网络 |
|---|---|---|---|---|
| NameNode | 16核 | 64GB | 2x1TB SSD RAID1 | 10Gbps |
| DataNode | 32核 | 128GB | 12x4TB HDD JBOD | 25Gbps |
| YARN Node | 32核 | 256GB | 2x1TB SSD | 25Gbps |
| Kafka Broker | 16核 | 64GB | 6x2TB SSD RAID10 | 10Gbps |
必须监控的核心指标:
| 指标类别 | 具体指标 | 报警阈值 |
|---|---|---|
| HDFS | 剩余存储空间 | <20% |
| 丢失块数 | >0 | |
| YARN | 待处理容器数 | >100持续5分钟 |
| 节点健康状态 | 不健康节点>10% | |
| Kafka | 未消费消息积压 | >100万条 |
| 生产者延迟 | >1000ms |
小文件问题:某次促销后HDFS出现2000万个小文件,导致NameNode内存溢出。解决方案:
数据倾斜典型案例:某个大V用户带来严重倾斜,解决方案:
scala复制// 倾斜key单独处理
val skewedKeys = Set("user123", "user456")
val commonData = data.filter(!skewedKeys.contains(_._1))
val skewedData = data.filter(skewedKeys.contains(_._1))
// 分别处理后再合并
val result = commonData.reduceByKey(_ + _)
.union(skewedData.repartition(100).reduceByKey(_ + _))
ZooKeeper连接风暴:凌晨日志归档时所有节点同时连接ZK导致瘫痪。最终采用:
这套系统在日活300万的电商平台稳定运行了两年多,期间经过三次大版本迭代。最大的体会是:分布式系统的监控比开发更重要,必须建立完善的指标体系和自动化报警机制。