Skip to content

feat: 重构OpenResty模块部分以支持动态编译#13291

Draft
Snrat wants to merge 2 commits into
1Panel-dev:dev-v2from
Snrat:feat/openresty-dynamic-modules
Draft

feat: 重构OpenResty模块部分以支持动态编译#13291
Snrat wants to merge 2 commits into
1Panel-dev:dev-v2from
Snrat:feat/openresty-dynamic-modules

Conversation

@Snrat

@Snrat Snrat commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

What this PR does / why we need it?

当前的模块添加/删除采用静态编译,每次升级或者修改模块都需要执行完整的Nginx编译操作,耗时通常长达30分钟,同时在低配置环境下经常出现编译失败情况。

Summary of your change

将原先的静态编译重构为自动识别,在模块支持的情况下默认采用动态编译方式进行引入,只编译对应模块,通过Nginx配置文件方式引入对应模块的加载。

Please indicate you've done the following:

关联 1Panel-dev/appstore#8644 ,关联 #10653

由于涉及到的内容较多,虽然已经进行本地测试通过,但是可能仍然需要团队进一步的测试以确保可靠性。

以下是完整细节:


OpenResty 动态模块支持(.so 本地构建 + load_module 热加载 + 升级自动重编译)

关联 PR:1Panel-dev/appstore(openresty 模板配套改动)。建议 appstore 先合并,面板侧功能依赖模板里的 Dockerfile.modules 与模块目录。

面向的问题

现在 1Panel 里给 OpenResty 加一个模块(比如 rtmp、web_dav),唯一的路是静态编译:把模块参数拼进 RESTY_CONFIG_OPTIONS_MORE,然后整个镜像从源码重新编译一遍,再重建容器。实测在 4 核 OVH VPS 机器上一次要 27~31 分钟,期间面板任务被占住,最后还要重启容器造成秒级闪断,性能和时间消耗过大。

Nginx 从 1.9.11 起支持 load_module 动态加载 .so,OpenResty 编译时带 --with-compat 就能做到二进制兼容。只需要用和运行时镜像一致的基线把模块单独编成 .so,挂载进容器,写一行 load_module,reload 一下就生效了——不重新编译整个 OpenResty,也不重启容器。可以大幅度降低修改模块后的编译等待时间和性能占用。

这个 PR 实现的内容:

  • 面板里每个模块可以选三种构建方式:自动(动态构建)/ 动态 / 静态
  • 动态构建在面板后台跑 docker 编译出 .so,校验通过后热加载,容器不重启(实测构建前后容器 StartedAt 不变)
  • OpenResty 升级时,已启用的动态模块会按新版本自动重编译
  • 静态路径原样保留,老用户的行为完全不变
  • auto 模式下动态构建失败会自动回退静态构建(详见下面"回退语义"一节)

实现方式

模块配置储存

appstore 侧每个 openresty 版本目录下新增两份文件:

  • build/module.catalog.json:模块目录,描述每个模块怎么构建——源码准备脚本(script)、编译期 apt 依赖(packages)、configure 参数(params)、加载顺序(loadOrder)。目前收录自带的 5 个:ngx_brotli、rtmp、web_dav、geoip2、http_substitutions_filter
  • build/module.json:安装时落到应用目录的 per-install 配置,初始内容与 catalog 一致,之后用户在面板上的改动(启用/构建方式/参数)都写在这里

面板后端所有模块操作都围绕应用目录下的 module.json 进行,catalog 只作为初始模板。仓库根新增 scripts/check-openresty-modules.sh 校验两者逐字节一致(这两个文件现在是人工同步的,容易改一个忘一个),并配了一个独立的 GitHub workflow 在 openresty 相关变更时自动跑。

动态构建链路

核心是 agent/app/service/nginx_module.go。一次动态构建的流程:

  1. 解析 target:取 OpenResty 版本、镜像架构(docker image inspect 拿,失败回退宿主机架构并记 WARNING)、镜像 Id、Dockerfile.modules 的 SHA-256,算出 target key。镜像或者 builder 变了,key 就变,旧构建自然标记为 stale,触发重编
  2. 逐模块构建docker buildDockerfile.modules,它就是 FROM 1panel/openresty:<版本>(ABI 基线和运行时完全一致)加一层编译环境,configure 用 --with-compat,模块参数里的 --add-module= 会被规范化成 --add-dynamic-module=。产物从容器里拷出来,带 SHA-256 清单
  3. 加载预检:先在临时容器里做单模块 nginx -t,再按 loadOrder 把所有启用模块的 load_module 拼起来做一次联合 nginx -t——防止模块之间符号冲突
  4. 生效:写 conf/modules-enabled/1panel-module-<loadOrder>-<模块名>.conf(写前快照,失败可还原),容器内 nginx -t 通过后 nginx -s reload。全程容器不重启

回滚语义:构建失败时旧的 ready 构建和旧配置原样保留,模块行只多一个失败标记和原因;managed conf 写入/校验失败时还原到写入前的快照。(见测试一节)。

签名兼容性潜在问题

最初 Dockerfile.modules 的 configure 参数漏了 --with-http_realip_module --with-http_dav_module,结果编出来的 .so 在运行时 load_module 全部被 nginx 拒绝(is not binary compatible)——nginx 动态模块校验 configure 签名,第 29/31 位对不上就拒载。已进行修正,三个版本的 builder 与运行时镜像签名一致,测试脚本里有针对这个错误的专项检查(not binary compatible 出现即 FAIL)。

另一个安全相关的点:模块 params 用 go-shellwords 解析,但这个库遇到未加引号的 ;&|<>静默截断而不是报错,原来写在解析后的逐参数检查根本摸不到这些字符。现在在解析前对原始串做元字符预检直接拒绝(parseDynamicModuleParams),对应测试 TestNormalizeDynamicModuleParams

auto 模式与回退语义

构建方式里 auto 的含义是"优先动态,动态走不通回退静态兜底":

  • 只有 auto 会回退。用户显式选 dynamic 而构建失败时,只报错不切换
  • 回退动作:任务日志 WARNING 写清是哪个模块、为什么动态构建失败;模块的 buildMode 落盘为 staticlastError 里保留动态失败原因(UI 上能直接看到,用户可以随时改回 auto)
  • 回退的静态构建是先在后台把镜像编译完,成功了才重建容器——静态路径本来就是"先 build 后切换"的顺序,所以回退也是构建期零停机,只有最后换容器的几秒闪断。静态也失败的话,UI 构建入口会把 buildMode 还原回 auto 并报两次错误
  • 升级路径同样接入了回退,差别是静态失败后没有"还原 auto"这一步——升级本身就要重建容器,失败就是升级失败

升级链路的踩的三个坑(实测发现,已修)

拿官方仓库真实版本做升级测试时撞到三个问题,已经进行修复:

  1. 零模块的存量安装升级会失败。升级流程对 OpenResty 无条件调 buildDynamicNginxModules,而它第一步就要读 build/Dockerfile.modules——老安装没这个文件,哪怕一个模块都没启用过,升级也直接炸。现在没有动态构建任务时整个 target 解析直接跳过
  2. 新版本包没有 builder 时升级硬失败。比如从带模块文件的版本升到官方未带模块文件的版本,旧 builder 和新版本 build/tmp 对不上(tarball 版本不匹配),构建报错把升级拖死。现在降级为 WARNING:升级继续,动态模块保持下线,面板里能看到原因。显式在面板点"构建"仍然是明确报错,不会静默
  3. 升级后 compose 丢挂载handleUpgradeCompose 只从旧 compose 保留 deploy/restart,官方新 compose 没有 ./modules./conf/modules-enabled 两个挂载,升级后容器里根本看不到 .so——而且因为 include 是 glob,nginx -t 照样通过,属于毫无征兆的静默失效。现在升级时会把旧 compose 里这两条挂载合并到新 compose

第 2 点有个实测时发现的细节值得记录:只要安装目录里的 Dockerfile.modules 还在,且其 RESTY_VERSION 与新版本上游 OpenResty 版本一致(比如 1.31.1.1-0 → 1.31.1.1-2-1,上游都是 1.31.1.1),升级时动态模块其实能正常按新镜像重编并加载——已经在真实官方包上验证通过。

前端

网站 → OpenResty 设置 → 模块页改为三列:构建方式 / 构建状态(失败带原因 tooltip)/ 兼容性(compatible / stale / static)。构建抽屉支持勾选多个模块一起构建、选 apt 镜像源。编辑抽屉里 auto 的文案写的是"自动(动态构建)",避免误解成"自动检测"。

测试

环境:AlmaLinux 10(4C8G),Docker CE 29.6.2,面板官方 v2.2.3 安装后替换为 PR 编译的二进制,OpenResty 用应用商店正常安装。

单元测试agentgo test ./app/service -run 'Module' 12 个全过,go vet 零错误。覆盖参数规范化、元字符预检、构建记录查找、失败回滚保留旧构建、target 缺 builder 哨兵、compose 挂载合并、回退判定与翻转。

构建器集成测试scripts/openresty-modules/test-builder.sh,不依赖面板,直接从 appstore 模板构建):三个版本(1.27.1.2-5-1-focal / 1.29.2.5-0-noble / 1.31.1.1-0-noble)× 5 个模块全部 PASS,含单模块加载、loadOrder 联合加载、起容器热 reload、负例回滚,每个 .so 有 SHA-256 记录,全程无 not binary compatible

面板端到端

  • 单模块构建:rtmp ready,modules/ 下有 .so + manifest,modules-enabled/ 生成 load_module 配置,容器内 nginx -t 通过,构建前后容器 StartedAt 不变
  • 多模块:web_dav + geoip2 一起构建,modules-enabled/ 文件名按 loadOrder 数字前缀排序
  • 失败回滚:把某模块 script 改成 exit 1 强制重新构建,任务失败,旧 .so 与旧配置完整保留,nginx -t 照常通过
  • 静态兼容:模块切 static 构建,走全量镜像重建(约 31 分钟)+ 容器重建,期间全部动态模块按新 ABI 自动重编
  • 删除模块:managed conf 消失、产物目录清理、nginx -t 正常

升级端到端(共四轮):

  1. 1.29.2.5-0-noble → 1.31.1.1-0-noble(均带模块文件):2 个动态模块自动重编译,新 target key 就绪,容器 Running
  2. 1.31.1.1-0-noble → 1.31.1.1-2-1-noble(官方包,无模块文件,真实混合场景):升级成功,3 个动态模块按新镜像重编并实际加载(nginx -T 可见 4 条 load_module),compose 挂载保留
  3. 零模块纯净安装 1.29.2.5-0-noble → 1.31.1.1-2-1-noble:升级成功(对应上面的坑 1)
  4. auto 回退:构造一个 params 只含 --with-debug 的模块(动态必然拒绝,静态可编),构建 → 日志 WARNING 指名模块 → 落盘 static → 27 分钟编译期旧容器持续在线 → 成功后切换,nginx -V--with-debug;升级路径同场景同样验证通过

诊断scripts/openresty-modules/diagnose-install.sh 在回滚后和升级后各采集一次,8 项检查全过,failed_checks=0。它会核对产物校验和、managed conf、compose 配置、只读挂载、镜像一致性、容器状态与 nginx -t

复测方式:装一台 Linux + Docker,按 scripts/openresty-modules/README.md./test-builder.sh --appstore <appstore目录> 即可复现构建器测试矩阵;面板侧按 HANDOVER 文档的阶段 5-7 操作(面板 API 与 UI 操作等价)。

需要评审注意的点

  • resolveNginxModuleTarget 的 target key 组成:版本 + 架构 + 镜像 Id + builder digest,任何一项变化都会触发全量重编,这是有意为之(宁可多编不可错载)
  • reconcileDynamicNginxModuleConfig 的快照/还原时序:先校验再写 conf、写失败还原、nginx -t 失败也还原
  • 升级预构建块(app_utils.go)里降级与回退的状态迁移:缺 builder → 降级继续;动态失败 → auto 翻转 static 交给后续 buildNginx
  • 前端 operate 抽屉对 static 模块的参数透传:静态 params 会并入 RESTY_CONFIG_OPTIONS_MORE 进全量编译
  • brotli 说明:实测三个官方运行时镜像都预装了 libbrotli 共享库(此前担心镜像没有、要把 brotli 限制为 static-only,验证后推翻),brotli 动态链路可用,构建期只需 builder 里装 libbrotli-dev(focal 镜像无头文件)

尚未实现的部分

  • 1.21.4.3-3-3-focal 这个老版本没加模块文件。升级到/从它升级会走降级路径(升级本身不受影响,但动态模块会下线)。回填模板或在文档里直接声明
  • 构建期要访问 Ubuntu 源,离线环境目前没法构建动态模块。考虑是否需要离线 APT 或者预置构建镜像
  • arm64 只做了代码层兼容(架构取自镜像 inspect),尚未在 arm 实机上进行测试,可能需要补充测试
  • 前端其余 9 个语言文件没有 buildModeAuto 等新增 key(回退显示英文),需要进行翻译补充

回滚

面板二进制与 /opt/1panel/db 有备份可直接还原;OpenResty 侧用面板"重建"即回官方模板。

@Snrat Snrat changed the title 重构OpenResty模块部分以支持动态编译 feat: 重构OpenResty模块部分以支持动态编译 Jul 18, 2026
@HynoR

HynoR commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

人工简单过了一下 , 想法很棒,希望多打磨,多自己思考一下。最好gpt回了啥就不要直接粘贴到pr里了, 多说说自己的想法和怎么测的。

以下是几个人工看到的问题,具体逻辑还没看完,如果抽空看到问题我会丢一下review

i18n没做完,ai偷懒了,只做了zh en

nginx_module.go 里有大量openai gpt家模型特别喜欢干的一件事,做一堆碎函数,然后整个业务代码看着跳来跳去。测试函数和某些部分也有gpt最爱的过度防御代码. 以及有一对临时乱丢的全局变量等,代码很不干净

如果可以 很多字符串建议不要内嵌代码,而是用常量

regxp正则建议请统一管理,而不是定义一个全局,否则这种正则表达式找起来很麻烦

最好让大模型自己画一个状态机、泳道图,看看他到底想根据你的目标做什么,并且pr一定要有自己的测试过程和想法。如果可以,希望多打磨打磨思考思考(别把我的回复丢给ai让他继续罐slop)。

@Snrat

Snrat commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

人工简单过了一下 , 想法很棒,希望多打磨,多自己思考一下。最好gpt回了啥就不要直接粘贴到pr里了, 多说说自己的想法和怎么测的。

以下是几个人工看到的问题,具体逻辑还没看完,如果抽空看到问题我会丢一下review

i18n没做完,ai偷懒了,只做了zh en

nginx_module.go 里有大量openai gpt家模型特别喜欢干的一件事,做一堆碎函数,然后整个业务代码看着跳来跳去。测试函数和某些部分也有gpt最爱的过度防御代码. 以及有一对临时乱丢的全局变量等,代码很不干净

如果可以 很多字符串建议不要内嵌代码,而是用常量

regxp正则建议请统一管理,而不是定义一个全局,否则这种正则表达式找起来很麻烦

最好让大模型自己画一个状态机、泳道图,看看他到底想根据你的目标做什么,并且pr一定要有自己的测试过程和想法。如果可以,希望多打磨打磨思考思考(别把我的回复丢给ai让他继续罐slop)。

  1. 我一开始的想法是直接将目前塞在 OpenRestry 镜像中的自带模块源码和依赖直接独立出来变成已经构建好的动态模块文件,但是这样需要 1Panel 在镜像发版前就完成构建,不过好处是可以显著降低 Docker 镜像的分发大小,用户在引入自带的5个常用模块时只需要下载和修改配置文件就可以,不再需要执行编译过程。只有在引入自定义模块时才需要引入对应的环境依赖和执行编译操作。目前的实现是为了最小化对整个 OpenRestry Docker 构建过程进行修改。

  2. i18n 主要集中在新增的几个状态显示上,我可以在 review 后补上

  3. 零碎函数这部分确实存在,需要进行重构,目前的可读性很差。测试部分也存在过度防御,因为只是在本地测试测试,一直在频繁进行一些变动,导致最后的脚本变得冗余。

  4. 正则部分我准备在 utils/re/re.go 部分进行配置,去除 nginx_module.go 中的全局变量

  5. 状态机、泳道图这部分其实有看过,总体的实现和我想要实现的其实相差不多。测试部分主要集中在动态编译失败回退静态编译,例如生成错误的so文件时的判断能力,确保配置文件、SHA256部分出现错误能识别并且进行回退。
    如果还有其他建议和需要修改的地方,也请一并提出来,我是更希望做成1中的那种模式。

    1. 模块状态机
      stateDiagram-v2                                                                                                                     
          [*] --> Disabled: 应用安装(载入catalog)                                                                                         
          Disabled --> Building: 启用并点构建                                                                                             
          Building --> Ready: 构建+联合预检通过, 热加载(容器不重启)                                                                       
          Building --> Failed: 构建或校验失败, 旧构建与旧配置保留                                                                         
          Failed --> Building: 修复后重新构建                                                                                             
          Failed --> StaticBuilding: auto模式回退, buildMode落盘static                                                                    
          StaticBuilding --> StaticReady: 全量编译成功, 重建容器                                                                          
          StaticBuilding --> Failed: 静态也失败, 还原auto                                                                                 
          Ready --> Stale: 升级或镜像变更, target key变化                                                                                 
          Stale --> Ready: 按新target自动重编                                                                                             
          Ready --> Disabled: 禁用, 撤配置+reload, 产物保留                                                                               
          StaticReady --> Disabled: 禁用, 二进制中移除需全量重建                                                                          
          Disabled --> [*]: 删除, 清理配置与产物目录                                                                                      
    
    Loading
    1. 泳道图:UI 动态构建流程
      sequenceDiagram                                                                                                                     
          participant U as 用户                                                                                                           
          participant P as 面板 agent                                                                                                     
          participant D as Docker 构建器                                                                                                  
          participant C as 运行时容器                                                                                                     
                                                                                                                                          
          U->>P: 勾选模块点构建                                                                                                           
          P->>P: 解析target: 版本+架构+镜像Id+builder digest                                                                              
          alt 无动态构建任务                                                                                                              
              P-->>U: 直接成功返回(零模块跳过)                                                                                            
          end                                                                                                                             
          P->>D: docker build(script备源码+params+packages)                                                                               
          alt 构建失败                                                                                                                    
              D-->>P: error                                                                                                               
              P->>P: 记录失败, 保留旧ready构建                                                                                            
              alt buildMode=auto                                                                                                          
                  P->>P: 翻转static, 日志WARNING指名模块                                                                                  
                  P->>D: 全量镜像编译(旧容器照常服务)                                                                                     
                  D-->>P: 编译成功                                                                                                        
                  P->>C: 重建容器(秒级切换)                                                                                               
              else buildMode=dynamic                                                                                                      
                  P-->>U: 任务失败, 旧配置不动                                                                                            
              end                                                                                                                         
          else 构建成功                                                                                                                   
              D-->>P: .so+SHA-256                                                                                                         
              P->>P: 校验产物+单模块/联合加载预检(临时容器nginx -t)                                                                       
              P->>P: 写modules-enabled配置(先快照)                                                                                        
              P->>C: nginx -t → nginx -s reload                                                                                           
              alt 校验或reload失败                                                                                                        
                  P->>P: 还原配置快照                                                                                                     
                  P-->>U: 任务失败, 旧配置仍在                                                                                            
              else 成功                                                                                                                   
                  P-->>U: ready/compatible, 容器未重启                                                                                    
              end                                                                                                                         
          end                                                                                                                             
    
    Loading
    1. 泳道图:升级流程
      sequenceDiagram                                                                                                                     
          participant U as 用户                                                                                                           
          participant P as 面板 agent                                                                                                     
          participant D as Docker                                                                                                         
          participant C as 容器                                                                                                           
                                                                                                                                          
          U->>P: 应用商店升级OpenResty                                                                                                    
          P->>P: 复制新版本build文件(缺builder则保留旧)                                                                                   
          P->>D: 预构建启用中动态模块(目标=新版本)                                                                                        
          alt 缺builder                                                                                                                   
              P->>P: WARNING降级: 模块保持下线, 升级继续                                                                                  
          else 动态失败且auto                                                                                                             
              P->>P: 翻转static, 后续buildNginx走静态链路                                                                                 
          else 动态失败且dynamic                                                                                                          
              P-->>U: 升级失败                                                                                                            
          end                                                                                                                             
          P->>C: compose down(预构建已完成, 停机自此起算)                                                                                 
          P->>P: upgrade.sh+写compose(合并模块挂载)                                                                                       
          P->>D: buildNginx(有static则全量编译)                                                                                           
          P->>C: compose up                                                                                                               
          C->>C: nginx -t                                                                                                                 
          P-->>U: 升级成功(纯动态路径停机约等于秒级)                          
    
    
    
    Loading

@zhengkunwang223

Copy link
Copy Markdown
Member

我的建议是 尽量收敛逻辑 把静态编译和动态编译分开 减少可能带来的麻烦
1.取消 auto 模式失败回退到静态编译 而是让用户自己选择 当然 也需要判断是否有静态编译模块
2.暂时限制 openresty 版本在 1.31.1.1-0-noble 版本才可以用动态编译
因为当前只有升级才会增加文件 所以我会在后续新增一个 openresty 版本来支持动态编译 低于这个版本的全部走静态编译
3. i18n 需要全部语言

@wanghe-fit2cloud
wanghe-fit2cloud marked this pull request as draft July 20, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants