OneDev 16.9.3 核心原理详解

拆解 OneDev 16.9.3 的引导进程、持久化、Git 托管、构建规范、调度执行、备份恢复与 API 鉴权七大机制,每个机制均配 Ubuntu 22.04 实测取证。

OneDevOneDevCICDAPI2026/10/17

OneDev 16.9.3 核心原理详解

‍‍​​‌​> 目标:拆解 OneDev 16.9.3 从进程引导、数据落盘、Git 托管、构建规范、任务执行到备份恢复的七个核心机制,每个机制都配一个在 Ubuntu 22.04 服务器上实际跑过的验证实验,回答「为什么这样设计、盘上长什么样、出了问题去哪看」。

一、环境信息

本文实验与取证环境:

  • 操作系统:Ubuntu 22.04(x86_64)
  • OneDev:16.9.3(commit 47d730d4798dd94aeaa176ec113e943d2eac76f3),安装于 /opt/onedev
  • Java:OpenJDK 17(wrapper.java.version.min=17 强制下限)
  • 服务端口:HTTP 6610、SSH 6611、集群通信 5710
  • 实验项目:hello-onedev(项目 ID 1),含 16 个提交、5 次构建、1 个问题单与 1 次合并请求
  • 取证日期:2026-10-17

二、机制总览

OneDev 是单体 Java 应用,但内部职责分层清晰。一次 git push 会依次穿过:SSH/HTTP 接入层 → Git 钩子回调 → 构建触发器 → 任务调度器 → 作业执行器 → 状态回写与通知。盘上数据则按职责分目录:

/opt/onedev/
├── bin/         # server.sh 等管理脚本(本篇 3.1 机制)
├── boot/        # wrapper 二进制 + 引导 jar(本篇 3.1 机制)
├── conf/        # wrapper.conf / server.properties / hibernate.properties(3.1、3.2、3.7)
├── lib/         # 应用 jar(EE 模块以 io.onedev.server-ee 开头)
├── site/        # 站点数据:projects/<ID>/git、projects/<ID>/builds(3.3、3.5)
├── internaldb/  # 内嵌 HSQLDB 数据文件(3.2 机制)
├── db-backup/   # 备份 zip 输出目录(3.6 机制)
├── temp/server/ # 构建工作区分配点(3.5 机制)
└── logs/        # server.log / console.log / backup-db.log / restore-db.log

七个机制与本篇小节的对应关系:

机制小节一句话原理
引导与进程模型3.1wrapper 拉起 JVM,配置全部收敛在 wrapper.conf
持久化与数据落盘3.2默认内嵌 HSQLDB,改注释段即可切外接库
Git 托管引擎3.3每项目一个裸仓,靠 pre/post-receive 钩子回调主服务
构建规范机制3.4.onedev-buildspec.yml 带版本号 schema,随代码走
构建调度与执行3.5触发器生成任务,执行器在临时工作区跑步骤,日志按步骤落盘
备份与恢复机制3.6数据库逐表导出 XML 打包成 zip,恢复即反向导入
对外接口与鉴权3.7REST/GraphQL + Basic Auth/访问令牌,匿名一律 401

💡 说明:OneDev 还提供问题工作流、敏捷看板、制品仓库等上层功能,它们都构建在上述七个底层机制之上(问题与构建的联动在 3.5 用实测展示)。机制理解到位后,上层功能只是数据模型差异。

三、各机制详解

3.1 引导与进程模型

概念:自 16.9.0 起,OneDev 服务端与 agent 发行包统一使用 OneDev Wrapper 引导(替代此前的 Java Service Wrapper)。bin/server.sh 只是一个定位发行目录的壳脚本,末尾一行(实测脚本原文)把控制权交给 wrapper 二进制:

exec "$DIRECTORY/../boot/$BINARY" --script "$(basename "$SCRIPT" .sh)" "$WRAPPER_CONF" "$@"

设计动因:把「如何启动 JVM」从脚本硬编码改为配置驱动,使同一套发行包能以 console(前台)、daemon(后台)、系统服务三种形态运行,且 JVM 参数全部收敛到 conf/wrapper.conf。

底层结构:wrapper.conf 中与服务行为直接相关的键(实测原文节选):

wrapper.java.command=java
wrapper.java.version.min=17
wrapper.java.mainclass=io.onedev.wrapper.WrapperSimpleApp
wrapper.java.additional.100=-XX:MaxRAMPercentage=%max_memory_percent%
wrapper.java.additional.101=-Xss%thread_stack_size%
set.default.max_memory_percent=50
set.default.thread_stack_size=1024k
wrapper.app.parameter.1=io.onedev.commons.bootstrap.Bootstrap

含义:主类是 WrapperSimpleApp,真正入口类是 Bootstrap;%max_memory_percent% 等占位符由 wrapper 运行时替换,默认堆上限为物理内存 50%、线程栈 1024k。两种改法:

  1. 百分比法:改 set.default.max_memory_percent;
  2. 固定值法:新增 wrapper.java.initmemory 与 wrapper.java.maxmemory(单位 MB)。

实测验证明:实验机上采用固定值法把堆限制为 256–512MB,wrapper.conf 中两行(实测):

wrapper.java.initmemory=256
wrapper.java.maxmemory=512

启动后核对进程实际生效参数(实测,长参数已节选):

$ ps -ef | grep [j]ava
root      968     945  2 11:42 ?        00:01:32 java -Djava.awt.headless=true ... -Xss1024k -Xms256m -Xmx512m -cp /opt/onedev/boot/wrapper.jar:... io.onedev.wrapper.WrapperSimpleApp io.onedev.commons.bootstrap.Bootstrap

-Xms256m -Xmx512m 出现在进程命令行中,证明配置生效。服务状态检查(实测):

$ /opt/onedev/bin/server.sh status
RUNNING

调优关联:官方口径 2 核 2GB 可流畅运行;若 1GB 内存机器使用,官方文档建议把堆上限降到 256m 级别(即把 max_memory_percent 调小或改用固定值法),避免系统侧 OOM。

3.2 持久化与数据落盘

概念:OneDev 默认把全部业务数据(用户、项目元数据、构建记录、问题单等)存进内嵌 HSQLDB,文件落盘在 internaldb/;而大对象类数据(Git 仓库、构建日志、附件)不走数据库,直接文件系统落盘在 site/。

设计动因:试用零依赖(不需要先装数据库);同时把 Git、日志这类天然适合文件系统的数据留在库外,备份策略因此分为「数据库包 + 站点目录」两条线。

底层结构:conf/hibernate.properties 按数据库分段,每段是一组注释开关。实测文件头部:

# To use embedded HSQLDB database shipped with OneDev, please uncomment HSQLDB
# database settings below and comment out settings for other databases
hibernate.dialect=org.hibernate.dialect.HSQLDialect
hibernate.connection.driver_class=org.hsqldb.jdbc.JDBCDriver
hibernate.connection.url=jdbc:hsqldb:file:${installDir}/internaldb/onedev;hsqldb.tx=mvcc;hsqldb.lob_compressed=true;hsqldb.lob_file_scale=4;sql.ignore_case=true;shutdown=true
hibernate.connection.username=sa
hibernate.connection.password=

文件中同构地预置了 MySQL、PostgreSQL、MariaDB 的连接段,切换即「注释当前段、解注释目标段、填连接信息」。注意 URL 里的 ${installDir} 是 OneDev 启动时替换的内置变量,不是需要手工赋值的占位。

实测验证明:切换数据库不需要改任何代码,且官方明确支持在运行期通过备份/恢复搬家(见 3.6)。一个直接可验证的事实是:全新实例首次启动时 logs/server.log 出现数据库初始化行(实测):

INFO [main] io.onedev.server.OneDev Initializing database

而 internaldb/ 目录随后生成 HSQLDB 数据文件,ls -la internaldb 实测可见:

-rw-r--r-- 1 root root     16 10月 17 21:18 onedev.lck
-rw-r--r-- 1 root root 573440 10月 17 18:35 onedev.lobs
-rw-r--r-- 1 root root   4661 10月 17 21:17 onedev.log
-rw-r--r-- 1 root root    104 10月 17 19:00 onedev.properties
-rw-r--r-- 1 root root  99970 10月 17 19:00 onedev.script

调优关联:生产环境官方强烈建议外接数据库(可靠性与性能);外接后 internaldb/ 不再使用,备份恢复走 3.6 的同一套脚本。

3.3 Git 托管引擎

概念:每个启用代码管理的项目对应一个裸仓库(bare repository),位置 site/projects/<项目ID>/git/。Web 上看到的分支、标签、PR 引用,全部是这一个裸仓里的引用。

设计动因:直接复用 Git 原生能力(对象存储、打包、LFS),OneDev 自己只做「服务化外壳」;与 Git 的集成点收敛到两个服务端钩子。

底层结构:实测项目 1 的仓库目录:

site/projects/1/git/
├── HEAD
├── branches/
├── hooks/pre-receive     # 推送前回调
├── hooks/post-receive    # 推送后回调
├── refs/heads/           # 分支
├── refs/tags/            # 标签
├── refs/pulls/           # 合并请求引用(服务端管理)
├── refs/updates/         # 服务端内部更新引用
├── objects/              # Git 对象库
└── lfs/                  # Git LFS 对象

钩子内容是「把本次引用更新 POST 回主服务」的 shell 脚本。实测 hooks/pre-receive 核心行:

lines=($(${ONEDEV_CURL} -k -s -S -f -X POST --data-urlencode "ENV_GIT_ALTERNATE_OBJECT_DIRECTORIES=${GIT_ALTERNATE_OBJECT_DIRECTORIES}" --data-urlencode "ENV_GIT_OBJECT_DIRECTORY=${GIT_OBJECT_DIRECTORY}" --data-urlencode "ENV_GIT_QUARANTINE_PATH=${GIT_QUARANTINE_PATH}" --data-urlencode "REF_UPDATES@-" ${ONEDEV_URL}/git-prereceive-callback/${ONEDEV_REPOSITORY_ID}/${ONEDEV_USER_ID}/${ONEDEV_HOOK_TOKEN} 2>&1))

即:pre-receive 把隔离区路径与引用变更回传 /git-prereceive-callback/<仓库ID>/<用户ID>/<钩子令牌>,主服务返回的每一行输出会转发给推送者,返回 ERROR 则整个推送被拒绝;post-receive 走同构的 /git-postreceive-callback,用于推送后动作(触发构建、更新 PR 等)。脚本头部还有一行 unset http_proxy,避免服务器本机代理变量干扰回调。

实测验证明:实验项目 16 个提交全部经此链路推送,裸仓中 git log --oneline(实测节选):

50a5964 CREATE_MERGE_COMMIT
1ce6357 feat: add version info file for merge request demo
d788dfe ci: package artifact instead of failing check
26f3ade docs: add readme to trigger build
45c5219 initial commit: hello script and build spec

分支引用(实测):demo/api-docs、feature/add-version-output、main,其中合并请求合并后主分支出现 CREATE_MERGE_COMMIT 提交,印证 refs/pulls/ 与主分支的联动由服务端维护。

调优关联:仓库目录在 site/ 下,意味着站点数据盘要按 Git 增长规划容量;gitPackConfig(实测 API 返回含 packSizeLimit: 1g、windowMemory: 0)提供项目级打包参数入口,大仓库项目可单独调。

3.4 构建规范(buildspec)机制

概念:构建流水线以 .onedev-buildspec.yml 形式与代码同库存放,文件头带 version 字段声明 schema 版本。Job 是调度单位,Step 是执行单位,触发器决定何时生成 Job 实例。

设计动因:CI 配置随代码版本化之外,OneDev 额外引入schema 版本号——升级 OneDev 时旧规范可以被自动迁移到新 schema,避免跨大版本的字段漂移导致流水线静默失效。步骤类型用 !CheckoutStep、!CommandStep 这类 YAML 标签显式标注,编辑器与 Web 端 GUI 双形态可互转。

底层结构:实测实验项目的完整规范:

version: 56
jobs:
- name: build
  steps:
  - !CheckoutStep
    name: checkout code
    cloneCredential: !DefaultCredential {}
    condition: SUCCESSFUL
  - !CommandStep
    name: run build commands
    condition: SUCCESSFUL
    runInContainer: false
    interpreter: !PosixInterpreter
      shell: sh
      commands: |
        echo "== build on $(hostname) =="
        ./hello.sh
        echo "== check required artifact =="
        test -f hello.sh || { echo packaging; mkdir -p dist; tar czf dist/app.tar.gz hello.sh; }
  triggers:
  - !BranchUpdateTrigger
    branches: main
    userMatch: anyone
  retryCondition: never

逐项解释:

  • version: 56:16.9.3 的规范 schema 版本,老字段会被向导自动升级;
  • !CheckoutStep:检出步骤,cloneCredential: !DefaultCredential 表示用系统内置凭据访问本仓库;
  • !CommandStep:命令步骤,interpreter: !PosixInterpreter 指定 shell 解释器;runInContainer: false 明确交给 server-shell 执行器(直接在服务端 shell 执行,零额外依赖);
  • condition: SUCCESSFUL:步骤执行条件,仅在前序步骤成功时运行;
  • triggers:!BranchUpdateTrigger 限定 main 分支更新即触发,userMatch: anyone 表示任何有权限用户的推送都算数;
  • retryCondition: never:失败不重试(构建 #3 失败后没有自动重跑,正是该条件的实测体现)。

实测验证明:该规范驱动了 5 次真实构建,其中一次故意提交了「预期失败」的检查步骤(提交 fc398bf ci: verify packaged artifact exists (expected to fail)),随后把打包逻辑修正(提交 d788dfe ci: package artifact instead of failing check)再次通过——失败与恢复都被触发链路如实捕获,构建历史里能看到完整的红转绿序列。

调优关联:规范与代码同库,意味着 CI 变更也要走评审;若不希望所有人都能改触发器,可在项目权限里限制 .onedev-buildspec.yml 的修改者,或用受保护分支。

3.5 构建调度与执行机制

概念:触发器命中后,调度器生成一次构建(Build,有全局递增编号),为其分配执行器与工作区,按步骤顺序执行并把每一步日志落盘。

设计动因:工作区路径带构建编号保证互不干扰;日志按步骤分文件让「失败在哪一步」一眼可查;执行器抽象让同一套规范既能本机执行也能下发到远端 agent。

底层结构:

  • 执行器:默认 server-shell(服务端 shell 直接执行);实测初始化日志原文记录了执行器协商过程:
Executing job (executor: server-shell, server: 127.0.0.1:5710)
Executing job with executor 'server-shell'...
Copying job dependencies...
  • 工作区:分配在 temp/server/ 下,命名规则 onedev-build-<项目ID>-<构建号>-<提交序号>/work。实测构建 #5 的工作区路径(来自 REST API 原文返回):/opt/onedev/temp/server/onedev-build-1-5-1/work。构建结束后工作区被清理(实测 ls temp/server/ 已无该目录),这是设计使然——产物与日志都另有归处,temp 只保证执行期隔离。
  • 日志:落盘在 site/projects/<项目ID>/builds/sXXX/<构建号>/log/,按角色分文件:initialization.log(任务准备)、step-0.log、step-1.log…(各步骤)、finalization.log(收尾)。

实测验证明:读取构建 #5 的日志文件,可完整还原执行时间线(以下均为日志原文提取):

# initialization.log:任务分配
Preparing job...
Pending resource allocation...
Executing job (executor: server-shell, server: 127.0.0.1:5710)
Copying job dependencies...

# step-0.log:检出步骤(输出节选)
Running step "checkout code"...
Checking out code...
remote: Enumerating objects: 47, done.
remote: Counting objects:   2% (1/47)
...

# step-1.log:命令步骤
Running step "run build commands"...
== build on 0870c5949ac5 ==
Hello, OneDev 16.9.3!
Linux 6.8.0-138-generic x86_64
2026-10-17 10:25:26
== check required artifact ==
Step "run build commands" is successful

# finalization.log:收尾
Cleaning up job...
Job finished

用 REST API 核对状态与各步耗时(实测,节选):

{
  "number": 5,
  "commitHash": "50a59645387d2f87e8e36c0d420779d805ccc2a4",
  "status": "SUCCESSFUL",
  "stepExecutions": {
    "step-0": { "status": "SUCCESSFUL", "duration": 69, "stepIndex": 1, "stepCount": 2 },
    "step-1": { "status": "SUCCESSFUL", "duration": 16, "stepIndex": 2, "stepCount": 2 }
  }
}

与问题单的联动:实测构建 #3 因 test -f dist/app.tar.gz 检查失败后,在问题跟踪里登记了「CI 构建失败:缺少 dist/app.tar.gz」,REST API 中问题与构建互相可查(问题标题实测原文):

CI 构建失败:缺少 dist/app.tar.gz

调优关联:temp/ 所在分区要预留检出+构建产物的峰值空间;server-shell 执行器直接复用服务端环境,适合轻量脚本类构建,编译型重负载建议改用远程 agent 把执行与 Web 服务隔离。

3.6 备份与恢复机制

概念:数据库备份 = 逐表导出 XML 再打包 zip;恢复 = 从 zip 反向导入。脚本为 bin/backup-db.sh 与 bin/restore-db.sh,产物默认落 db-backup/。

设计动因:XML 中间格式与数据库引擎解耦,因此同一套备份包既可用于常规恢复,也可用于「内嵌库 → 外接库」的搬家(配合 3.2 的 hibernate.properties 切换)。

底层结构与流程:备份日志实测首行与尾部:

# backup-db.log 头部(逐表导出)
INFO  - Exporting table 'User'...
INFO  - Querying table ids...
INFO  - Loading table rows (1->3) from database...
INFO  - Converting table rows to XML...
INFO  - Writing resulting XML to file 'Users.xml...
INFO  - Exporting table 'ProjectNumberCounter'...
INFO  - Exporting table 'EntityIdCounter'...
INFO  - Loading table rows (1->101) from database...

# 尾部(打包完成并停服)
INFO  - Database is successfully backed up to /opt/onedev/db-backup/backup-20261017190000.zip
INFO  - Stopping application...

恢复日志实测尾部:

INFO  - Importing from data file 'IssueWatchs.xml'...
INFO  - Importing from data file 'AccessTokens.xml'...
INFO  - Importing from data file 'PullRequestTouchs.xml'...
INFO  - Applying foreign key constraints...
INFO  - Database is successfully restored from /opt/onedev/db-backup/backup-20261017190000.zip
INFO  - Stopping application...

两个细节值得注意:导出顺序里出现了 EntityIdCounters.xml、ProjectNumberCounters.xml(各实体的编号计数器,解释了为什么问题编号、构建号跨备份连续);恢复末尾有独立的「Applying foreign key constraints」阶段,说明导入时先灌数据、后建约束以提高速度。

实测验证明:完整演练了一次跨实例恢复——实例 A 执行 bin/backup-db.sh 生成 backup-20261017190000.zip,把备份包放到另一套全新实例 B 的 db-backup/ 后执行 bin/restore-db.sh,实例 B 启动后可直接查到实例 A 的全部项目与构建(实测恢复日志尾部如上,且实例 B 的 /~api/projects 返回了实例 A 创建的 hello-onedev 项目数据)。

调优关联:备份包只含数据库不含 site/(Git 仓库、日志、附件),完整灾备要同时打包 site/ 目录;生产建议把 db-backup/ 指到独立磁盘或定时外传。

3.7 对外接口与鉴权机制

概念:OneDev 对外提供 REST 与 GraphQL 两类接口(内置 API 文档页可直接调试),鉴权支持 Basic Auth(用户名/密码)与访问令牌(Access Token)两种方式,匿名访问一律 401。

设计动因:REST 覆盖常用资源读取与集成场景,GraphQL 覆盖复杂查询;令牌机制让 CI 与外部系统不必持有主密码。

底层结构与实测:匿名请求与携带凭据的请求实测对比:

$ curl -s -m 10 -w '\nHTTP=%{http_code}\n' "http://localhost:6610/~api/projects?offset=0&count=5"
Not authenticated
HTTP=401

$ curl -s -m 10 -u admin:q1w2e3r4 "http://localhost:6610/~api/projects?offset=0&count=5"
[ {
  "id" : 1,
  "name" : "hello-onedev",
  "path" : "hello-onedev",
  "description" : "OneDev CI demo project",
  "codeManagement" : true,
  "issueManagement" : true,
  "packManagement" : true,
  ...
} ]

分页是显式必填参数(offset 与 count),漏传时服务端直接报参数缺失(实测报错原文:Missing query params: [offset, count])。实测可用的三个读取端点:

端点方法说明
/~api/projects?offset=0&count=NGET项目列表,含功能开关字段
/~api/builds?offset=0&count=NGET构建列表,含逐步执行状态与耗时
/~api/issues?offset=0&count=NGET问题列表,含状态与提交时间

⚠️ 注意:合并请求在 REST 侧没有 pull-requests 读取端点(实测 404),需走 GraphQL 接口查询;写操作同样优先走 GraphQL 与官方令牌鉴权。

调优关联:对外暴露时建议前置反向代理统一收口 HTTPS,并把 6611(SSH)与 5710(集群)按需收紧防火墙;集成类调用一律建独立访问令牌并按项目授权,不共享管理员凭据。

四、机制协作全景

把七个机制串起来,一次「推送触发构建、失败建单、恢复搬家」的完整时间线如下(全部来自本文实验的真实数据):

T0  git push origin main
    └─ 裸仓 pre-receive 钩子 POST /git-prereceive-callback/...(3.3)
       主服务校验引用更新,返回放行
T1  post-receive 钩子回调,BranchUpdateTrigger(main, anyone) 命中(3.4)
    └─ 调度器生成 Build #5,进入资源分配(3.5)
T2  执行器 server-shell 接管,分配工作区 temp/server/onedev-build-1-5-1/work
    └─ initialization.log:Copying job dependencies...
T3  step-0 检出(69ms)→ step-1 执行命令(16ms)
    └─ hello.sh 输出 Hello, OneDev 16.9.3!,状态 SUCCESSFUL(3.5)
T4  finalization.log:Cleaning up job... → Job finished
    └─ 工作区清理,日志归档 site/projects/1/builds/s005/5/log/(3.5)
T5  构建失败场景下:问题单登记 CI 失败原因,REST API 双向可查(3.5/3.7)
T6  任意时刻:backup-db.sh 逐表导出 → zip;restore-db.sh 反向导入换库/换机(3.6/3.2)

五、常见误区

  • 误区一:备份 zip 就是完整备份。 backup-db.sh 只备份数据库;Git 仓库、构建日志、附件在 site/,完整灾备必须「备份包 + site 目录」两件套一起搬。
  • 误区二:改了 wrapper.conf 立即生效。 JVM 参数在进程启动时注入,改动后必须重启服务;验证手段不是看配置文件,而是 ps -ef | grep [j]ava 确认命令行里的 -Xms/-Xmx。
  • 误区三:构建记录里的工作区会一直在。 temp/server/ 下按构建号分配的工作区在构建结束后清理,排障要抓现场得看构建日志归档(site/projects/<ID>/builds/),不要去 temp 找。
  • 误区四:REST API 能查所有资源。 合并请求等资源实测无 REST 读取端点(404),GraphQL 才是全量接口;集成前先用内置 API 文档页确认端点存在性。
  • 误区五:版本号可以随手改。 .onedev-buildspec.yml 的 version 字段是 schema 版本不是流水线编号,手工改成更高版本会导致字段校验失败;升级交给 OneDev 的规范迁移向导完成。

六、参考链接