
1. 项目概述为什么要把 Spark、Django 和 MongoDB 拉到一张工作台上你有没有遇到过这样的场景用户在 Django 后台点下“生成月度销售热力图”按钮页面转圈 47 秒数据库连接池告警Celery worker 队列堆到 200而最终返回的却只是一张聚合了 300 万条订单记录的静态图表这不是性能瓶颈这是架构错配——用 Web 框架的“小刀”去切大数据的“整头牛”。我第一次在电商 SaaS 项目里撞上这个问题时运维同事直接把监控截图甩进群“Django 的 ORM 正在用SELECT * FROM orders WHERE created_at 2023-01-01扫描 87GB 表PG 的 shared_buffers 已经在冒烟。”这正是Apache Spark Django MongoDB组合要解决的核心矛盾让实时交互层Django专注响应、路由与状态管理让分析计算层Spark专注吞吐、迭代与容错让文档存储层MongoDB专注灵活 schema、高并发读写与嵌套查询。它不是炫技而是分工——就像餐厅里Django 是前台点单收银员快、准、稳MongoDB 是食材冷柜随时取用、分类清晰Spark 是后厨中央厨房批量备料、统一烹制、支持翻车重做。这个组合特别适合三类真实业务用户行为分析平台MongoDB 存原始埋点{user_id: u123, event: click_cart, props: {item_id: p456, category: electronics}}Spark 每小时跑一次会话路径聚类Django 提供可视化看板推荐系统在线服务MongoDB 存用户画像快照{user_id: u123, interests: [AI, photography], last_active: ISODate(...)}Spark 基于历史行为训练 ALS 模型并导出向量Django 接口实时查向量做近邻召回IoT 设备数据管道MongoDB 接入设备上报的 JSON{device_id: d789, temp: 23.5, humidity: 65, ts: 1712345678}Spark Streaming 实时统计异常温度频次Django 管理员后台触发告警工单。关键词 “Apache Spark”、“Django”、“MongoDB” 在标题里不是并列关系而是分层协作关系Spark 是计算引擎不直接暴露给用户Django 是应用网关不碰原始数据流MongoDB 是数据湖的“浅水区”存清洗后、可即席查询的宽表。接下来我会拆解这个三角如何真正咬合——不是教你怎么装三个软件而是告诉你数据从 MongoDB 流进 Spark、结果回写进 MongoDB、Django 如何安全调用整个链路的每一步实操细节、参数陷阱和线上踩坑记录。2. 架构设计与技术选型逻辑为什么是这三个而不是其他组合2.1 为什么不用 Pandas 替代 Spark新手常问“我用 Django 调 Pandas 读 MongoDB 不就完了”——这在 10 万行数据内确实丝滑。但当数据量突破 500 万行问题立刻暴露内存爆炸Pandas 单进程加载 500 万行 × 20 列的 DataFrame实测内存占用超 3.2GBDjango worker 直接 OOM无容错机制Pandas 计算中途断电所有中间状态丢失必须重跑无法水平扩展加机器Pandas 不认。而 Spark 的 DAG 调度器天然支持失败任务重试、Stage 级别恢复、Executor 动态扩缩。我曾用 Pandas 处理日志分析任务单次运行耗时 18 分钟失败率 12%因内存抖动换成 Spark 后同样逻辑耗时 4.3 分钟失败率归零——因为 Spark 把 18 分钟的长任务拆成 237 个 Task其中 3 个失败后自动在其他节点重跑总耗时仅增加 11 秒。2.2 为什么 MongoDB 而不是 PostgreSQL 或 Elasticsearchvs PostgreSQLPostgreSQL 的 JSONB 字段虽支持嵌套查询但复杂聚合如“统计每个用户最近 3 次点击的品类分布”需多层子查询 LATERAL JOINSQL 写起来反人类且 PG 的并行查询对 JSONB 支持有限。MongoDB 的$unwind$group$facet流水线一行$group: { _id: $user_id, categories: { $push: $props.category } }就搞定。更重要的是MongoDB 的分片集群能线性扩展写入吞吐——我们生产环境用 3 分片集群峰值写入达 42,000 docs/secPG 同配置下写入瓶颈在 8,000 tps。vs ElasticsearchES 擅长全文检索和实时分析但不擅长事务性更新和复杂关联。比如“给用户 u123 的画像文档追加一个recommendation_scores数组”ES 需用 Update API 脚本而 MongoDB 的$push、$set原子操作一行命令解决。且 ES 的_source存储开销大同样数据量比 MongoDB 多占 35% 磁盘。提示MongoDB 版本必须 ≥ 4.2支持聚合管道中的$lookup关联分片集合、Spark 连接器必须用mongo-spark-connector_2.12:10.2.3适配 Spark 3.4 和 Scala 2.12Django 版本建议 4.2 LTS原生支持异步视图避免阻塞 Spark 任务。2.3 为什么 Django 不直接连 SparkDjango 的 HTTP 请求生命周期极短通常 5 秒而 Spark 任务启动开销大Driver 初始化、资源申请、Executor 启动平均 8~12 秒。如果每次请求都spark-submit用户会看到“504 Gateway Timeout”。正确姿势是Django 只负责触发、轮询、展示结果Spark 任务由独立调度器如 Airflow或后台服务如 Spark Thrift Server长期运行。我们采用后者——部署 Spark Thrift Server 作为 JDBC 网关Django 通过pyhive连接像查普通数据库一样发 SQLSpark 自动将 SQL 编译为 DAG 执行。2.4 整体数据流向与组件职责边界[设备/前端] → [Django REST API] → [MongoDB 写入] ↓ [Airflow 定时触发] ↓ [Spark Job: 读 MongoDB → 计算 → 写 MongoDB] ↓ [Django Admin / Dashboard 查询 MongoDB 结果集]关键设计原则零直连Django 代码里绝不出现SparkSession.builder()或sc.parallelize()结果导向Spark 不返回原始数据只写计算结果到 MongoDB 的专用集合如analytics_user_segments幂等写入Spark 任务每次运行前先drop()目标集合避免脏数据累积Django 异步化用户点击“生成报告”后Django 返回{task_id: spark_job_20240515_001}前端轮询/api/task-status/{id}/获取进度。这个架构让每个组件回归本质MongoDB 是数据容器Spark 是计算工厂Django 是服务接口。接下来我们进入最硬核的部分——如何让这三者真正握手。3. 核心实现与实操细节从环境搭建到生产级部署3.1 环境准备版本锁死与依赖隔离生产环境最怕“在我机器上能跑”。我们强制锁死以下版本2024 年实测稳定组合MongoDB6.0.14分片集群3 config server 3 shard server 1 mongosSpark3.4.1Hadoop 3.3.4Scala 2.12Django4.2.11Python 3.11.8连接器mongo-spark-connector_2.12:10.2.3注意_2.12 表示 Scala 版本必须与 Spark 匹配注意Spark 3.4 默认使用 Scala 2.12若误装mongo-spark-connector_2.11运行时抛java.lang.NoClassDefFoundError: scala/Function1——这是类加载器找不到 Scala 标准库不是代码问题。Django 项目依赖管理requirements.txt关键行Django4.2.11 pymongo4.6.1 pyhive[hive]0.7.0 # 用于连接 Spark Thrift Server thrift0.18.1 requests2.31.0 # Spark 连接器不放这里它只在 Spark 集群节点安装Spark 集群依赖安装在每个 Worker 节点执行# 下载 connector JAR必须放 SPARK_HOME/jars/ 下 wget https://repo1.maven.org/maven2/org/mongodb/mongo-spark-connector_2.12/10.2.3/mongo-spark-connector_2.12-10.2.3.jar mv mongo-spark-connector_2.12-10.2.3.jar $SPARK_HOME/jars/ # 验证spark-shell 启动后执行 # scala spark.read.format(mongodb).option(uri,mongodb://localhost:27017/test.coll).load() # 若报错 Failed to find data source: mongodb说明 JAR 未加载成功3.2 MongoDB 数据建模为 Spark 计算优化 SchemaMongoDB 不是“随便存”Schema 设计直接影响 Spark 读取效率。以用户行为日志为例错误设计// ❌ 反模式嵌套过深 类型混杂 { _id: ..., user: { id: u123, profile: { name: Alice, age: 28 } }, events: [ { type: page_view, url: /home, ts: 2024-05-15T10:00:00Z }, { type: click, element: cart_btn, ts: 2024-05-15T10:00:05Z } ] }问题Spark 读取时events数组需explode()才能展开产生大量中间 shuffleprofile嵌套对象导致user.profile.name访问慢。正确建模宽表思想预计算字段// ✅ 生产模式扁平化 时间分区 索引预置 { _id: ObjectId(...), user_id: u123, user_name: Alice, user_age: 28, event_type: page_view, event_url: /home, event_ts: {$date: 2024-05-15T10:00:00Z}, hour_partition: 2024-05-15-10, // 按小时分区便于 Spark filter 下推 day_partition: 2024-05-15 }必须创建的索引在 MongoDB Shell 中执行// 加速 Spark 的 filter 下推WHERE hour_partition 2024-05-15-10 db.events.createIndex({ hour_partition: 1 }) // 加速 groupBySpark 的 reduceByKey 会利用此索引 db.events.createIndex({ user_id: 1, event_type: 1 }) // 复合索引覆盖常用查询 db.events.createIndex({ day_partition: 1, user_id: 1, event_ts: -1 })实测添加hour_partition索引后Spark 读取单小时数据耗时从 210 秒降至 38 秒——因为 MongoDB 能直接定位到对应 chunk无需扫描全量分片。3.3 Spark 任务开发从读取到写入的完整流水线我们以“计算用户 7 日活跃度分层”为例输出到analytics.user_segments集合。核心代码spark_job.pyfrom pyspark.sql import SparkSession from pyspark.sql.functions import col, count, when, lit, current_date, date_sub from pyspark.sql.types import StructType, StructField, StringType, IntegerType, TimestampType # 1. 初始化 SparkSession关键参数 spark SparkSession.builder \ .appName(user-segmentation-job) \ .config(spark.mongodb.input.uri, mongodb://mongo1:27017/myapp.events) \ .config(spark.mongodb.output.uri, mongodb://mongo1:27017/myapp.analytics_user_segments) \ .config(spark.sql.adaptive.enabled, true) \ # 开启自适应查询执行Spark 3.2 .config(spark.serializer, org.apache.spark.serializer.KryoSerializer) \ .getOrCreate() # 2. 定义 Schema强制指定避免 Spark 推断错误类型 schema StructType([ StructField(user_id, StringType(), False), StructField(event_type, StringType(), False), StructField(event_ts, TimestampType(), False), StructField(hour_partition, StringType(), False) ]) # 3. 读取 MongoDBfilter 下推到 MongoDB 层 raw_df spark.read \ .format(mongodb) \ .option(database, myapp) \ .option(collection, events) \ .option(pipeline, [{$match: {hour_partition: {$gte: 2024-05-15-00}}}]) \ # 关键MongoDB 原生 pipeline .schema(schema) \ .load() # 4. 业务逻辑计算 7 日活跃用户数 最近登录时间 from pyspark.sql.window import Window window_spec Window.partitionBy(user_id).orderBy(col(event_ts).desc()) active_users raw_df \ .filter(col(event_type) login) \ .withColumn(rn, row_number().over(window_spec)) \ .filter(col(rn) 1) \ .withColumn(last_login, col(event_ts)) \ .select(user_id, last_login) # 5. 写入 MongoDB覆盖模式确保幂等 active_users.write \ .format(mongodb) \ .mode(overwrite) \ # ⚠️ 生产必须用 overwrite避免数据叠加 .option(database, myapp) \ .option(collection, analytics_user_segments) \ .option(replaceDocument, false) \ # false按_id更新true全量替换慎用 .save() spark.stop()关键参数解析.option(pipeline, ...)将$match下推到 MongoDBSpark 只拉匹配数据网络传输量降 92%.mode(overwrite)生产环境唯一安全模式每次任务先清空目标集合再写入.option(replaceDocument, false)设为false时Spark 用_id字段做 upsert存在则更新不存在则插入避免重复 ID 冲突。提交命令生产环境必须用--master yarn或--master k8s://spark-submit \ --master yarn \ --deploy-mode cluster \ --conf spark.yarn.submit.waitAppCompletionfalse \ # 异步提交Django 不阻塞 --jars $SPARK_HOME/jars/mongo-spark-connector_2.12-10.2.3.jar \ spark_job.py3.4 Django 集成安全调用与状态管理Django 不直接运行 Spark而是通过REST API 触发 Airflow DAG或调用 Spark Thrift Server。我们选择后者更轻量步骤 1启动 Spark Thrift Server# 在 Spark 集群 Master 节点执行 $SPARK_HOME/sbin/start-thriftserver.sh \ --master yarn \ --deploy-mode cluster \ --conf spark.sql.hive.thriftServer.singleSessiontrue \ --conf spark.sql.adaptive.enabledtrue \ --jars $SPARK_HOME/jars/mongo-spark-connector_2.12-10.2.3.jar默认监听thrift://spark-master:10000。步骤 2Django 视图调用 HiveServer2# views.py from pyhive import hive from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt import json csrf_exempt def trigger_segmentation(request): if request.method POST: # 1. 提交 Spark SQL 任务非阻塞 conn hive.Connection(hostspark-master, port10000, usernameadmin) cursor conn.cursor() # 执行预编译的 SQL实际应存入 DB防注入 cursor.execute( INSERT OVERWRITE TABLE analytics_user_segments SELECT user_id, COUNT(*) as login_count, MAX(event_ts) as last_login FROM myapp.events WHERE event_type login AND hour_partition 2024-05-15-00 GROUP BY user_id ) cursor.close() conn.close() # 2. 返回任务 ID 供前端轮询 task_id fseg_{int(time.time())} # 3. 将 task_id 和状态存入 Django DBmodels.TaskStatus TaskStatus.objects.create(task_idtask_id, statusRUNNING) return JsonResponse({task_id: task_id, status: RUNNING})步骤 3前端轮询状态Vue 示例// 轮询 /api/task-status/{id}/ async checkTaskStatus(taskId) { const res await fetch(/api/task-status/${taskId}/); const data await res.json(); if (data.status SUCCESS) { this.segments await this.fetchSegments(); // 从 MongoDB 读结果 } else if (data.status RUNNING) { setTimeout(() this.checkTaskStatus(taskId), 2000); // 2秒后重试 } }Django 读取结果直接查 MongoDB零 Spark 依赖# models.py class UserSegment(models.Model): user_id models.CharField(max_length50) login_count models.IntegerField() last_login models.DateTimeField() class Meta: managed False # 不由 Django 管理表结构 db_table analytics_user_segments # 对应 MongoDB 集合名 # views.py def get_segments(request): segments list(UserSegment.objects.all().values()) # pymongo 驱动自动映射 return JsonResponse({segments: segments})3.5 生产部署 checklist12 项必须验证检查项验证方法不通过后果1. MongoDB 连接器 JAR 是否在$SPARK_HOME/jars/ls $SPARK_HOME/jars/grep mongo2. Spark Thrift Server 是否监听 10000 端口telnet spark-master 10000Django 连接超时500 错误3.hour_partition索引是否存在db.events.getIndexes()Spark 读取全量数据OOM4. Spark 任务是否用overwrite模式查看 job 日志中Saving to Mongo with mode: OverwriteMongoDB 结果集无限膨胀5. Django 的pymongo连接是否启用连接池MONGO_CLIENT MongoClient(..., maxPoolSize100)高并发下连接耗尽503 错误6. Airflow DAG 是否设置retries3default_args{retries: 3}Spark 任务偶发失败导致数据缺失7. MongoDB 分片键是否合理如user_idsh.status()查看 chunk 分布数据倾斜部分 shard CPU 100%8. Spark Executor 内存是否 ≥ 4G--executor-memory 4gExecutor 频繁 GC任务卡死9. Django 视图是否禁用 CSRF对 APIcsrf_exemptPOST 请求 403 Forbidden10. Spark SQL 中INSERT OVERWRITE是否指定 databaseINSERT OVERWRITE TABLE myapp.analytics_user_segments写入 default 库数据丢失11. 用户行为集合是否开启 TTL 索引自动清理db.events.createIndex({ event_ts: 1 }, { expireAfterSeconds: 2592000 })磁盘爆满MongoDB 拒绝写入12. 所有密码是否从环境变量读取os.getenv(MONGO_URI)代码泄露导致数据库被黑4. 常见问题与排查技巧实录线上故障的 7 个真实现场4.1 问题 1Spark 读 MongoDB 报com.mongodb.MongoTimeoutException: Timed out after 30000 ms现象Spark Driver 日志显示Failed to connect to mongodb://mongo1:27017但mongoshell 能连通。根因Spark 连接器默认使用maxConnectionLifeTimeMS0永不过期而 MongoDB 分片集群的 mongos 进程会主动关闭空闲连接默认 60 分钟。当 Spark 任务长时间运行连接被 mongos 断开后续请求超时。解决方案在 SparkSession 配置中显式设置连接生命周期spark SparkSession.builder \ .config(spark.mongodb.input.uri, mongodb://mongo1:27017/myapp.events?maxConnectionLifeTimeMS300000) \ .config(spark.mongodb.output.uri, mongodb://mongo1:27017/myapp.result?maxConnectionLifeTimeMS300000) \ .getOrCreate()300000ms 5分钟确保连接在 mongos 关闭前主动刷新。4.2 问题 2Django 调用 Spark Thrift Server 报TTransportException: Could not connect to spark-master:10000现象Django 日志ConnectionRefusedError: [Errno 111] Connection refused。排查路径kubectl get pods -n sparkK8s或yarn node -listYARN确认 Thrift Server Pod/Container 是否 Runningkubectl logs -n spark thrift-server-pod查看启动日志是否报Failed to bind to 10000端口被占kubectl exec -it thrift-server-pod -- netstat -tuln | grep 10000确认端口监听地址是0.0.0.0:10000而非127.0.0.1:10000后者只允许本地访问。修复在start-thriftserver.sh中添加--conf spark.sql.hive.server2.thrift.bind.host0.0.0.0。4.3 问题 3Spark 写入 MongoDB 后Django 读不到新数据现象Spark 任务日志显示Saved 12,456 rows to mongodb://...但 DjangoUserSegment.objects.count()返回 0。根因MongoDB 集合名大小写敏感Spark 写入analytics_user_segments而 Django model 的db_table Analytics_User_Segments首字母大写。验证在 MongoDB Shell 中执行show collections确认集合名完全一致。修复Django model 中db_table必须与 Spark 写入的 collection 名逐字符相同包括下划线、大小写。4.4 问题 4Spark 任务耗时突增 300%CPU 使用率 100%现象原本 5 分钟的任务某天突然跑 22 分钟YARN UI 显示所有 Executor CPU 持续 100%。根因MongoDB 的hour_partition字段类型从string被误改为int如2024051510Spark 无法下推$match被迫拉取全量数据。排查Spark UI 的 SQL tab 查看 Physical Plan若Scan mongodb下没有PushedFilters说明 filter 未下推MongoDB 日志grep find /var/log/mongodb/mongod.log确认查询是否带hour_partition条件。修复将hour_partition字段统一为字符串并重建索引。4.5 问题 5Django Admin 中 MongoDB 数据显示为None现象Django Admin 列表页所有字段显示None但pymongo直连能查到数据。根因Django 的pymongo驱动默认将 MongoDB 的ObjectId当作str而 Django model 字段定义为CharField类型不匹配导致解析失败。修复在 model 中显式处理_idclass UserSegment(models.Model): _id models.CharField(primary_keyTrue, max_length24) # ObjectId 是 24 位 hex string user_id models.CharField(max_length50) # ... 其他字段并在admin.py中注册admin.register(UserSegment) class UserSegmentAdmin(admin.ModelAdmin): list_display (_id, user_id, login_count) # 显式列出字段4.6 问题 6Spark Thrift Server 内存溢出OOM Killed现象kubectl describe pod thrift-server显示State: Terminated Reason: OOMKilled。根因Thrift Server 默认内存 1G但执行复杂 SQL如多表 join需更多内存。修复启动时增加内存$SPARK_HOME/sbin/start-thriftserver.sh \ --driver-memory 4g \ --executor-memory 8g \ --executor-cores 4 \ ...4.7 问题 7Airflow 触发 Spark 任务后Django 轮询永远返回RUNNING现象Spark 任务已成功写入 MongoDB但 Django 的TaskStatus表中状态仍为RUNNING。根因Airflow DAG 中未调用TaskStatus.objects.filter(task_idxxx).update(statusSUCCESS)。修复模板Airflow PythonOperatordef mark_task_success(**context): task_id context[dag_run].conf.get(task_id) TaskStatus.objects.filter(task_idtask_id).update(statusSUCCESS) mark_success_task PythonOperator( task_idmark_success, python_callablemark_task_success, trigger_ruleall_success # 仅在上游任务成功后执行 )5. 性能调优与扩展实践从单机到千节点集群5.1 Spark 读取 MongoDB 的 5 个加速技巧Pipeline 下推优先级最高# ✅ 最佳MongoDB 原生 pipeline过滤 99% 数据 .option(pipeline, [{$match: {hour_partition: 2024-05-15-10, event_type: login}}, {$project: {user_id: 1, event_ts: 1}}]) # ❌ 次佳Spark filter全量数据进 Spark .filter((col(hour_partition) 2024-05-15-10) (col(event_type) login))Projection 减少网络传输在 pipeline 中用$project只取必要字段实测减少 68% 网络流量。Batch Size 调优默认batchSize0MongoDB 自动决定生产环境设为10000.option(batchSize, 10000)避免小包频繁传输提升吞吐。并行度控制Spark 会根据 MongoDB 的 chunk 数量自动分片但可通过partitioner强制.option(partitioner, MongoShardedPartitioner) \ # 分片集群用此 .option(partitionerOptions.partitionKey, user_id) \ # 指定分片键Kryo 序列化必开比 Java 序列化快 10 倍内存占用少 50%.config(spark.serializer, org.apache.spark.serializer.KryoSerializer) \ .config(spark.kryo.registrator, com.mongodb.spark.sql.DefaultMongoKryoRegistrator)5.2 Django 高并发下的 MongoDB 连接池优化默认pymongo连接池maxPoolSize100但在 1000 QPS 下仍可能耗尽。优化方案连接复用Django 中全局单例 MongoClient# utils.py MONGO_CLIENT MongoClient( os.getenv(MONGO_URI), maxPoolSize500, # 提升至 500 minPoolSize50, # 预热 50 连接 maxIdleTimeMS600000, # 空闲 10 分钟释放 connectTimeoutMS5000, socketTimeoutMS30000 )读写分离为分析集合analytics.*单独配置只读副本集 URI减轻主库压力。5.3 从单机 Spark 到 YARN/K8s 集群的平滑迁移单机开发时用--master local[*]上线必须切集群。关键差异项目单机模式YARN 模式K8s 模式Driver 位置本地进程YARN ApplicationMasterK8s PodJAR 依赖本地路径--jars hdfs:///jars/mongo-connector.jar--jars local:///opt/jars/mongo-connector.jar日志查看控制台yarn logs -applicationId application_XXXkubectl logs -n spark spark-pod资源申请无--num-executors 10 --executor-cores 4--conf spark.kubernetes.driver.request.cores2迁移检查清单✅ 确保 HDFS 或 K8s 存储中存在 connector JAR✅ Spark 配置spark.hadoop.fs.defaultFS指向正确 HDFS 地址✅ K8s 中为 Spark ServiceAccount 绑定spark-roleRBAC 权限。5.4 业务扩展加入实时流处理Spark Streaming Kafka当需求从“小时级批处理”升级到“秒级实时分析”架构演进为[设备] → [Kafka] → [Spark Streaming] → [MongoDB] → [Django] ↓ [实时告警规则引擎]关键改造点Spark Streaming 用foreachBatch将微批次写入 MongoDBdef write_to_mongo(batch_df, batch_id): batch_df.write \ .format(mongodb) \ .mode(append) \ .option(database, myapp) \ .option(collection, realtime_alerts) \ .save() query streaming_df.writeStream \ .foreachBatch(write_to_mongo) \ .start()Django 用 Server-Sent EventsSSE推送实时告警替代轮询。5.5 成本监控