Open edX 架构决策解析:如何在 LMS 中限制 Modulestore 的使用(ADR 0011)
Open edX 架构决策解析如何在 LMS 中限制 Modulestore 的使用ADR 0011【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform本文基于 Open edXopenedx-platform仓库中的架构决策记录 ADR 0011Limit LMS Modulestore access to the courseware app 展开。该文档规定了 LMS 侧 Django 应用访问课程内容数据的边界原则并给出了从 Modulestore 迁移到 CourseOverviews、Learning Sequences 等高性能替代 API 的完整转换指南。读完本文你将掌握 LMS 应用脱离 Modulestore 的改造路径、发布时推数据的架构模式以及 Learning Sequences 这一参考实现中信号、Celery 任务与管理命令的完整调用链。背景为什么 LMS 要限制 Modulestore 的访问Open edX 的 LMS 中部署了众多 Django 应用它们常常需要查询由课程团队编写的内容例如 Sequence单元分组、Unit 或 Problem习题。历史上这些应用惯用的手段是直接调用 Modulestore——它可以返回整棵由 XBlock 组成的课程图。但如 ADR 所述Modulestore 是一个庞大且复杂的共享系统长期以来是大量 Bug 和性能问题的来源过去数年 Modulestore 的性能虽有逐步改善但代价是引入了更多复杂性。唯一被豁免的是 courseware 应用本身它必须借助 XBlock runtime 渲染 Unit因此必须访问 Modulestore且在可预见的未来抽取这部分逻辑的成本过高。ADR 给出的核心结论是除 courseware 外LMS 中的新功能不应再调用 Modulestore。下文将完整继承原文档的五条决策、五项目标并结合仓库源码逐层展开转换指南。五项核心决策ADR 的 Decisions 章节给出五条明确规定新功能不得在 LMS 中调用 Modulestore。所有新增功能必须在不访问 Modulestore 的前提下实现。存量功能应顺手清理各团队在修改已有功能时应 opportunistically借机移除其中的 LMS Modulestore 依赖。Studio 进程不受此限应用仍然可以从 Studio 进程rpro访问 Modulestore——Modulestore 访问被允许存在只是被限制在了 Studio 一侧。优先使用更新、更受限的 LMS API包括用于课程配置元数据的 CourseOverviews以及用于课程大纲的 Learning Sequences。其他课程内容数据应通过发布时推送模式处理应用应监听course_published信号启动 Celery 任务在 Studio 进程中查询 Modulestore并把数据推入自己的数据模型。目标这套约束带来什么收益ADR 用五个目标论证了这一约束的价值每一点对应的源码证据都可以在仓库中找到。1. 应用正确性更容易推理。Modulestore 存在大量隐蔽的边界情况非标准的课程层级、Studio 通常不提供选项却可以被设置的继承属性、Section 级 A/B 实验等。而面向 LMS 的关系型数据表如course_overviews、learning_sequences对课程数据做的是有意的、有文档的假设。应用开发者可以放心构建在这些更简单的模型之上而不必惦记 Modulestore 数据中复杂的灵活性。2. 应用行为更可预测。Modulestore 会一次性抓取课程的大块内容导致大课与小课、启用高级功能与未启用的课程之间性能差异巨大。从更简单的应用数据模型提供 LMS 请求运行行为会可预测得多。ADR 的终极目标是只在编写authoring和发布publishing阶段访问 Modulestore绝不在向学员提供内容时访问。3. 测试更容易写、跑得更快。CourseOverview和UserCourseOutlineData对象比一棵 XBlock 树容易创建和 mock也不受复杂发布规则的困扰。ADR 特别指出使用 Modulestore 会给课程创建和修改带来显著的性能惩罚使 Modulestore 访问成为 edx-platform 测试套件运行时间中的主要占比。4. 应用对用户可见故障更 resilient。许多功能今天至少部分实现在 Modulestore 及其返回的 XBlock 中这些功能的变更可能引发波及完全无关功能的 Bug。如果一个 LMS 应用在响应用户请求时查询 Modulestore共享 Modulestore 代码中的意外故障会直接变成用户可见的错误而发布时读一次 Modulestore 并推入自己数据模型的功能即使 Modulestore 出现 Bug结果也只是数据过期stale data而不是整个体验崩溃。5. 有助于缩小 edx-platform 巨石。像course_overviews、learning_sequences这样的小应用未来可能从 edx-platform 中抽取为独立仓库独立应用可以把它们作为依赖引入简化测试环境搭建。Modulestore 已被证明很难被这样抽取——任何依赖 Modulestore 的外部应用都被迫使用更容易随 edx-platform 演进而破坏的依赖反转机制。这也服务于将 Studio 与 LMS 拆分为更独立系统的长期规划。转换指南一课程配置改用 CourseOverviews适用场景应用只是查询存储在根CourseBlock上的课程配置。这类应用应改为查询 CourseOverviews其公开入口是 course_overviews 的 api 模块。需要说明的是ADR 成文时提及的get_course_overview/get_course_overviews函数在当前仓库源码中对应的实际实现包括get_course_overview_or_none、get_course_overview_or_404以及get_course_overviews等函数批量版本返回序列化后的数据。如果所需配置字段还没有被CourseOverview模型捕获ADR 给出的操作步骤是给CourseOverview模型添加字段并设置默认值生成迁移文件migration将CourseOverview.VERSION加一更新CourseOverview._create_or_update让它从 CourseBlock 对象来自 modulestore正确加载数据并写入CourseOverview。两个类中的属性通常同名直接对应。仓库源码印证了这套机制。在 course_overviews/models.py 中第 68 行定义了VERSION 19其上方注释明确要求IMPORTANT: Bump this whenever you modify this model and/or add a migration.每次修改模型或新增迁移都必须递增。类的 docstring 也解释了后果提升 VERSION 会使所有已缓存的课程概览失效触发大量 Modulestore 读以重新缓存每个课程。第 158 行定义了CourseOverview._create_or_update(cls, course)即 ADR 第 4 步要求更新的方法。第 421 行附近的版本校验逻辑if course_overview.version cls.VERSION:就是 ADR 所描述的即时重新生成机制当存储记录的版本号小于当前CourseOverview.VERSION时API 会强制重新生成该 overview防止读到旧数据。因此 ADR 提醒的首次上线会有性能惩罚是刻意为之部署后几分钟内随着各课程按需just-in-time重算惩罚会自然消失。转换指南二课程大纲与 Sequence 元数据改用 Learning Sequences适用场景应用需要课程大纲outline数据以及 sequence 的元数据。应使用 Learning Sequences 的公开 API入口包为 learning_sequences 的 api。ADR 还特别给出两条注意事项不支持旧式 Mongo 课程——即课程 key 形如Org/Course/Run、即将被移除的课程。所有以course-v1:或ccx-v1:开头的课程 key 均受支持。仓库源码中的key_supports_outlines函数见 learning_sequences/api/outlines.py正是这一约束的实现除已废弃的 v1 Library其 Locator 继承自 CourseKey 但不该支持 outline外所有非废弃 CourseKey 都支持——即 SplitMongo 普通课程和 CCX 课程可用Library、Pathways 和 Old Mongo 课程不可用。这是一个新 API未来会持续增加新的 API 函数和数据。转换指南三进阶用例——发布时推数据架构模式当应用所需的数据无法以高性能方式在 LMS 获得时需要为自己的应用建一个数据模型并在课程发布过程中把新数据推入其中。ADR 以 Learning Sequences 本身作为示范实现走查了整个模式。从源码结构看这套实现分布在 Studio 进程./cms/源码树中由五个部件构成。数据抽取get_outline_from_modulestore抽取代码位于 cms/djangoapps/contentstore/outlines.py。get_outline_from_modulestore第 324 行及其辅助函数负责把课程结构和内容数据从 Modulestore 中提取出来也是必须处理各种怪异边界情况如畸形课程结构的地方。当前实现的 docstring 明确了三个要点没有副作用只读取、生成数据不推送、只操作 published 分支、不支持 Old Mongo 课程。其核心代码与 ADR 中给出的示例完全一致store modulestore() with store.branch_setting(ModuleStoreEnum.Branch.published_only, course_key): # Pull course with depth3 so we prefetch Section - Sequence - Unit course store.get_course(course_key, depth3)这里有两个 ADR 强调的要点必须只从 published 分支读取。保存草稿saving a draft同样会触发course_published事件若不显式锁定published_only分支抽取到的可能是草稿数据depth3用于预取 Section → Sequence → Unit 三层结构减少逐层拉取 Modulestore 的次数。由于该函数无副作用其测试类OutlineFromModuleStoreTestCase只需准备 Modulestore 课程结构然后断言生成了预期的CourseOutlineData。容错与反腐层的权衡这段代码在用户点击发布按钮或运行课程导入之后异步执行因此对输入要有一定宽容度不能因个别坏数据而让整个流程失败但同时它必须保持为一层强的反腐层anti-corruption layer不能把不必要的复杂性和隐蔽的数据配置泄漏进应用的核心数据模型。ADR 总结的原则是对自己的应用用严格/简单的数据模型对来自 Modulestore 的数据用宽容的转换。Learning Sequences 采取的具体折中方案是把内容错误提升为一等公民概念get_outline_from_modulestore的返回类型是Tuple[CourseOutlineData, List[ContentErrorData]]——既返回大纲数据也返回一个ContentErrorData对象列表。这两个数据结构定义在 learning_sequences/data.pyContentErrorData在第 50 行CourseOutlineData在第 166 行。ADR 举的例子很典型Learning Sequences 假设一个 Sequence 只属于一个 Section。这个简化假设被写进了learning_sequences应用的数据模型和 URL 结构里但 Modulestore 并不对课程施加这一约束。于是策略是每发现一次违反就记录一条ContentErrorData并跳过该 Sequence 除第一次之外的所有出现。数据模型保持简单同时保留了一份可供课程团队或支持人员事后诊断的记录。写入应用模型update_outline_from_modulestoreupdate_outline_from_modulestore 是一个短函数调用get_outline_from_modulestore生成CourseOutlineData再通过learning_sequences暴露的 API 方法replace_course_outline见 learning_sequences/api/outlines.py把数据推入learning_sequences。该函数还会设置自定义属性set_custom_attribute以便监控性能问题与错误——当前源码中记录的是num_sequences、num_content_errors等计数。ADR 还特别提到写入内容包括课程的version这对排查写入故障很有价值。版本号取自根CourseBlock的course_version属性并转换为字符串存储因为它是 BSON 对象。Celery 任务update_outline_from_modulestore_taskCelery 任务是 cms/djangoapps/contentstore/tasks.py 中的shared_taskupdate_outline_from_modulestore_task它包装了对update_outline_from_modulestore的调用。ADR 强调两点必须用 Celery 异步执行。即使代码看起来够快可以进程内同步跑课程往往启用了各种冷门功能会显著拉长数据抽取时间而这些情况几乎不可能被全面测试覆盖必须对任务失败激进地告警You must be aggressive about alerting on task failures。发布足够不频繁某些内容相关的错误不会触发常规错误率告警而你的任务可能阻塞一个课程的发布因此必须对彻底失败保持极高敏感度。从当前源码看任务对不支持 outline 的课程 key 会记录 warning 并直接返回对真正的异常则会记录后raise重新抛出——so that errors are noted in reporting正好呼应了激进告警的要求。信号处理器listen_for_course_publish信号处理器位于 cms/djangoapps/contentstore/signals/handlers.py。它是 Studio 做发布后数据推送的集中入口但 ADR 也说明你完全可以另写一个处理器监听同一个course_published信号。它的主要职责是做一些日志记录然后入队 Celery 任务。当前源码中的listen_for_course_publish会注册特殊考试、推送学习序列大纲在key_supports_outlines(course_key)为真时调用update_outline_from_modulestore_task.delay(course_key_str)并触发课程搜索索引等任务。ADR 还给出了一个源码中同样存在的实战提醒见该文件第 137 行附近的 DEVELOPER README 注释部分任务应使用transaction.on_commit以避免读到未提交的旧数据有些团队改用等待策略waiting strategy。如果你在调试任务读到了旧数据的问题要考虑 Celery 在进程内运行时不会复现该错误——需要配合 devstack_with_worker 配置必要时在信号发送处加入time.sleep。管理命令backfill 与单次更新ADR 列出的两个管理命令在当前仓库中均已实现backfill_course_outlines为一批缺失大纲的课程批量回填支持--dry只显示将回填的课程不做修改和--force强制为所有课程重新生成而不只是缺失的两个参数。从源码看它通过CourseOverview.objects.values_list(id, flatTrue)与get_course_keys_with_outlines()做差集找出缺失大纲的课程然后为每个课程单独启动一个新的 Celery 任务。ADR 解释了这样做的双重原因控制内存使用跨课程连续访问 Modulestore 会泄漏大量内存以及便于观察哪些课程耗时更长或引发错误。update_course_outline针对单个特定课程更新其大纲。ADR 对管理命令还有三点注意这些命令位于 Studio 进程因为它们调用的是查询 Modulestore 的代码backfill 命令为每个课程单独发任务如上所述长期来看希望有一种方式可以从 Django admin 触发 backfill避免每次都要提支持工单。这一规划已在 contentstore 的 admin.py 中落地——admin 中已能直接update_outline_from_modulestore_task.delay(str(course_key))。LMS 进程侧零 Modulestore 依赖ADR 对 LMS 进程的要求非常强硬你的功能完全不应使用 Modulestore。你的 LMS 应用代码应彻底摆脱 Modulestore 依赖上述所有面向 Modulestore 的代码都应位于./cms/源码树中、运行于 Studio 进程。当 LMS 请求到来时你的应用只看自己的数据模型或看上述某个高性能的 Modulestore 替代 API。此外有两条边界规则LMS 进程不得覆盖课程发布流程写入的模型更不能把数据推回 Modulestore如果应用需要覆盖来自发布的数据就建两个模型一个只由课程内容发布更新另一个在 LMS 侧读写。查询时同时看两个模型。ADR 以 edx-when 应用为例它从 Modulestore 捕获开始时间与截止时间然后在 LMS 提供请求时应用学生级别的覆盖student-specific overrides。关于这一主题的更多背景参见 ADR 5Studio 与 LMS 的 Subdomain 边界。Django Admin只读的运维视图ADR 最后说明了learning_sequences应用的 Django admin 定位只读目的是让支持团队和工程团队更便捷地查看生产环境的数据状态。规划中的方案是在 contentstore Studio 应用中新增一个 Django admin 页面把 backfill 任务作为 action 加入并通过对 CourseOverview 使用代理模型proxy model来获得课程列表。从源码结构看CourseOutlineRegenerate代理模型与update_all_outlines_from_modulestore_task这类批量再生成任务tasks.py已经在仓库中落地与这一规划方向一致。小结改造 Checklist综合 ADR 全文与仓库源码一个 LMS 应用摆脱 Modulestore 的完整改造路径可以归纳为判断数据类别仅课程根配置 → 走 CourseOverviews加字段、生成迁移、递增VERSION、更新_create_or_update课程大纲/Sequence 元数据 → 走 Learning Sequences API注意course-v1:/ccx-v1:课程 key 支持范围其他数据 → 自建模型 发布时推送把 Modulestore 访问全部收进 Studio 进程./cms/树按纯函数抽取 → 写入应用模型 → Celery 任务 → 信号处理器 → 管理命令五件套组织抽取层对数据宽容但保留ContentErrorData式错误记录LMS 侧只读自己的模型如需学员级覆盖则用发布模型 LMS 读写模型的双模型设计对任务失败激进告警并提供--dry/--force式的管理命令与 admin 入口用于回填和运维。这条路线的最终收益是LMS 请求路径上不再触碰 Modulestore行为更可预测、测试更快、故障面更窄并且为 course_overviews、learning_sequences 这类小应用未来独立成仓铺平了道路。【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考