For the complete documentation index, see llms.txt. This page is also available as Markdown.

如何在 FreeBSD 上安装 Bacula 并完成首次备份

关于本文

本文中你将学到:

  • 如何安装 Bacula

  • 如何完成首次备份

我将使用

  • Bacula 7.0.5

  • PostgreSQL 9.3.5

  • FreeBSD 9.3

  • ZFS

你不必使用完全相同的选择也能从本文中有所收获。你将学到 Bacula 的概念,助你跨越常见的误解。我保证你会有所收获。

除非另有说明,Bacula 的所有配置文件都位于 /usr/local/etc/bacula/

Bacula 指令忽略空格。因此 Working DirectoryWorkingDirectory 是相同的。用你喜欢的写法即可。

有些值在“引号”中,有些则没有。用你喜欢的写法即可,但切记如果值包含空格,“就需要把它放在引号里。”

Bacula 组件或概念的词将以加粗、首字母大写的形式呈现。这有助于你区分 File 指令与磁盘上的 file,或 Volume 与卷。

程序或命令会以 bconsole 这样的形式出现。文件会以 /etc/rc.conf 这样的形式出现。

本文涵盖内容

  • 客户端(Clients)

  • 控制器(Director)

  • 存储(Storage)

  • 备份(Backups)

  • 恢复(Restores)

坦白说:我有偏好。我喜欢 PostgreSQL、FreeBSD、ZFS 与 Bacula 的组合。事实上,我太喜欢它了,以至于为 Bacula 编写了 PostgreSQL 后端。从那以后,我使用 Bacula 已超过 10 年。我备份到 HDD、DDS、DLT 与 SDLT。Bacula 已证明可靠,它就是能用。客户端覆盖了广泛的操作系统,Bacula 也不在乎你用的是什么文件系统。

我也是 FreeBSD 的 Port Bacula 维护者。

我最初为 O’Reilly 撰写过关于 Bacula 的文章。后来我把那篇文章转载到了我的日记里。我为本期 FreeBSD 期刊更新了那篇文章。

关于 ZFS,本文其实没有太多有用信息,但写完本文后我发现要涵盖的内容太多,省略 ZFS 的细节是合理的。

Bacula 简介

这里的描述简短而概括。我没有提到的东西,并不意味着 Bacula 做不到。

Bacula 是一款基于网络客户端-服务器模型的备份解决方案。它可备份到磁盘或磁带。社区活跃且蓬勃发展。如果你需要,也有出色的商业支持。备份的控制围绕一个 Director 集中进行。扩展时,你可以拥有任意数量的 Director。每个 Director 有 Client,这些 Client 可与其他 Director 共享。

Director 联系 Client,告诉它:把这个 FileSet 备份到这个 Storage。然后 Client 照办,备份文件。之后,那个备份作业可能被复制或迁移到另一个 Storage,可以是本地的也可以是远程的。例如,你可能备份到磁盘,然后希望把该作业复制到磁带。或复制到远程位置。

无论你对备份做什么,Catalog 都会跟踪备份了什么、何时备份、来自哪个 Client、现在位于何处。Catalog 也用于保留目的、清理与清除。

主要组件

Bacula 不是一个程序,而是多个。这些联网的程序通过 TCP/IP 互相通信。Bacula 的主要组件有:

  1. Director——守护进程(bacula-dir),控制作业。

  2. bconsole——命令行界面控制台。

  3. Storage——守护进程(bacula-sd),从 Client 接收备份并存储到存储设备(例如磁带、磁盘)。

  4. Client——守护进程(bacula-fd),读取文件并发送给 Storage。

  5. Catalog——一个数据库,跟踪已运行的作业及其存储位置。我推荐用 PostgreSQL。也可用 MySQL,但 PostgreSQL 性能更好。我不推荐为此使用 SQLite。

本节余下内容可以跳过。它对理解如何首次运行 Bacula 并非必需。所有这些组件都需要能互相通信。请牢记这一点,尤其要考虑你的防火墙。特别是当你阅读下面内容并看到通信顺序时。

  • bacula-dir 联系 bacula-fd,告诉它备份什么

  • bacula-fd 联系 bacula-sd,把备份发送给它

  • bacula-sd 联系 bacula-dir,汇报已完成备份的状态

  • bacula-dir 联系数据库服务器,汇报状态

  • bconsole 联系 bacula-dir

  • 你可以通过 bconsole 查询 bacula-dirbacula-sdbacula-fd 的状态。执行此操作时,bacula-dir 联系 bacula-sd,再联系 bacula-fd,询问它们的状态并转达给你

  • 如果把作业从一个 storage 复制到另一个,bacula-sd 需要与另一个 bacula-sd 通信

  • 如果你使用 SD Calls Client 指令,bacula-sd 需要能联系 bacula-fd

我的备份

当我刚开始用 Bacula 时,我直接备份到磁带。如今 HDD 更大更便宜,我备份到磁盘。有些人认为这就够了。我不这么想。一次电源故障就可能让所有 HDD 报废。ZFS 是最好的文件系统,但它无法抵御灾难性悲剧。我相信备份要多份副本。至少一份在这里,一份在那里。这就是为什么我备份到磁带,然后把磁带移到别处。

我也有计划把备份发送到远程的 bacula-sd。我还没实施,但我会的。很快。

安装

安装 Bacula 相当简单。如果你想用 PostgreSQL(我推荐你这么做),可以从预构建的 package 安装。否则,从 Ports 树安装。

以下是我安装的内容:

我使用 PostgreSQL 9.3,因为它是 FreeBSD Ports/packages 系统当前的默认版本。其他版本也可以。

数据库设置

这是配置 PostgreSQL 配合 Bacula 使用的简要介绍。官方文档更为详尽。

在这些步骤中,我们先创建一个新的 PostgreSQL 数据库集群,然后启动服务器:

输出如下:

接下来,我们将创建 Bacula 要用的数据库,即 Catalog。本例中部分输出已裁剪,因为重复且读起来乏味。

这就是你现在应当看到的。

配置

所有这些配置都将在单一 IP 地址上完成。对入门而言,我推荐这种方式。这能让你先把东西跑起来,再迁移到其他服务器。一步一个脚印。

我使用的 IP 地址是 10.55.0.76,如果所有东西都安装在同一系统上,这是你在配置中唯一需要改的值。

Bacula 有四个主要配置文件。默认位置是 /usr/local/etc/bacula/

  1. bacula-dir.conf

  2. bacula-fd.conf

  3. bacula-sd.conf

  4. bconsole.conf

这些文件名不言自明。接下来几节中,我会给你一套能让你上手的最小配置。我保证。

下面的示例中,我们会慢慢开始,然后加快速度。你看到的第一个 bacula-dir.conf 文件会随着进展不断添加内容。我会在文末提供所有配置文件的链接。

bacula-dir.conf

bacula-dir.conf 是你为 Bacula 创建的最大的配置文件。所幸,你可以把它拆分成逻辑区域,放在任意多个(或少至任意几个)文件中。bacula-dir.conf 指定的内容中包括:

  • 这个特定 bacula-dir 的身份

  • 标识它要备份的 Client

  • 列出将在该 Client 上运行的 Job

  • 指定记录这些备份的 Catalog

  • 列出该 Job 的 FileSet(文件列表)

  • 指定该 Job 的 Schedule

  • 标识要使用的 Storage

还有其他指令(例如 Pool),但我们把那个讨论推迟到后面。我喜欢像这样整理我的配置文件。我觉得这样更易读。这并非必需。

你可以直接使用这份配置。我建议首次设置时保留那个密码。等一切工作正常后再改。

那个 NamePassword 是此 Director 通过 bconsole 登录时接受的凭据。这些值会出现在两处:

  1. bacula-dir.conf 中,让 bacula-dir 知道接受什么凭据

  2. bconsole.conf 中,让 bconsole 知道发送什么凭据

这种“出现在两处”是应当尽早理解的重要概念。Bacula 在多处采用这种设计。我会在后续示例中指出。

随着 bacula-dir.conf 文件增长,你可以把配置拆分到单独的文件中。每个文件都可以用如下命令包含进来:

稍后我会用到这个特性。

Catalog 数据库由第 13-19 行标识。它不指定数据库服务器类型(例如 PostgreSQL);该选项在编译时设置。

第 21-31 行指定消息如何送达你。这份配置假设你的系统上有 SMTP 服务器,但你不需要它也能跟着本示例走。如果你开始依赖备份,则推荐配置。

bacula-fd.conf

下面是一个最小的 bacula-fd.conf

bacula-dir.conf 一样,NamePassword 代表凭据。但这里 NamePassword 标识哪个 Director 可以联系此 Director。两者合起来是与这个 bacula-fd 通信所需的凭据。它会存在两处:

  1. bacula-fd.conf 中,让此 bacula-fd 知道接受什么凭据

  2. bacula-dir.conf 中,让 bacula-dir 知道联系此 bacula-fd 时出示什么凭据。

我稍后会展示第 2 项。

高级设置可以有多个 Director,此时一个 bacula-fd.conf 可以有多个 Director 指令,但本简单示例不涉及。

bacula-sd.conf

这是我们的最小 bacula-sd.conf

我想你已看出规律。那个 NamePassword 标识哪个 Director 可以联系我们。那组凭据会存储在:

  1. bacula-sd.conf 中,让 bacula-sd 知道必须出示什么凭据

  2. bacula-dir.conf 中,让 bacula-dir 知道向此 bacula-sd 出示什么凭据

bacula-fd.conf 一样,我们稍后回到第 2 项。

bconsole.conf

这个最小的 bconsole.conf 能让你与 bacula-dir 交互:

这个 NamePasswordbacula-dir.conf 中出现的相同。它们就是前面提到的与 bacula-dir 通信所需的凭据。bconsole 启动时会读取此文件。

这只是开始

前几节你看到了最基础的内容。接下来几节你会看到它们如何串联起来。我会向你展示 Director 如何知道 Client、Job 与 FileSet。到目前为止,我们只是在定义基础。现在我们开始构建备份系统。

Client

本节中,我们将向 Bacula 配置添加一个 Client。这可以直接加到 bacula-dir.conf 中,但我更愿意把所有与 Client 具体相关的内容放在单独的文件里。

我把以下内容放入 client-myclient.conf

为了把这份配置拉入 bacula-dir.conf,我向 bacula-dir.conf 添加了以下内容:

这份配置会告诉 bacula-dir 包含该文件,文件里是我们 bacula-fd 的联系信息。

还记得凭据如何存两处吗?一处给发送方,一处给接收方?下面是一个示例:

第一处告诉 bacula-fd 接受什么密码。第二处告诉 bacula-dir 联系那个 bacula-fd 时使用什么凭据。

请注意:每当 Director 联系 Client 时,它必须使用的凭据是 Director 的 Name(不是 Client 的 Name)和 bacula-dir.conf 中 Client 资源里指定的 Password。需要说明的是,在我们的示例中,Director 必须传给 Client 的正确值是:

  1. MyBaculaDirector

  2. the bacula-fd password

因此,Client 资源中的 Name 用来命名该 client,并不参与凭据。这非常重要。

FileSet

我们已告诉 Bacula 关于 Client 的信息,接下来要指定我们要备份哪些文件:一个 FileSet。

我把以下内容放入 filesets.conf

在这个 FileSet 中,我们备份一个目录。不要被那个名字 File 搞混。我选择备份 /usr/local/etc/bacula/ 是因为我知道你会有这个目录。

每个文件都会记录其 MD5。这是可选的,但强烈推荐。运行 Verify Job 时会用它来比较磁盘上的内容与备份内容。

我们将排除任何以 ~(波浪号)结尾的文件。

注意:

  • 每个 Job 一个 FileSet,

  • 一个 FileSet 可被多个 Job 使用。

为告诉 bacula-dir 这个 FileSet,我在 bacula-dir.conf 中添加了这一行:

下一步:创建一个备份 Job。

Job

Job 标识要备份什么、它位于哪个 Client 上、将存储此备份的 bacula-sd。这是我加到 client-myclient.conf 中的内容:

现在我们几乎告诉了 Bacula 一切:备份什么、从哪里来、存到哪里。现在我们得告诉 Bacula 何时运行这个 Job。

Schedule

Schedule 非常灵活。我们从简单经典开始。下面告诉我们 Job 何时运行、以何种 Level(Incremental、Differential、Full)运行。我把它放入 schedules.conf

我把这一行加到 bacula-dir.conf 来包含上述内容:

我的服务器使用 UTC 时间,所以这些会在大约 EST 凌晨 3:15 运行(我住在费城郊外)。

  1. 每月第一个星期日运行 Full 备份。

  2. 其余所有星期日运行 Differential 备份。

  3. 每天运行 Incremental 备份。

Level 的最佳描述见 Job Resource 文档。

Storage

在那个 Job 中,我们告诉 Bacula 把备份发到哪里:MyFirstStorage。我们已经在 bacula-sd.conf 中定义了这个 Storage,但还没有在 bacula-dir.conf 中告知它。这听起来奇怪。为什么要指定两次?Bacula 基于组件构建。本示例中所有东西都在同一台计算机上运行。然而,它们可以轻松地在不同计算机上运行。因此,bacula-dir.conf 必须标识它能使用的 bacula-sdbacula-sd.conf 可能在另一台计算机上。

下面是我如何告诉 bacula-dir 它应当使用哪个 bacula-sd。我把这加到 bacula-dir.conf

又是 NamePassword 的概念。但这次略有不同。这里我们指定 bacula-dir 联系该 Address 上的 bacula-sd 时应使用的凭据。

Device 与 Media Type 的值必须与 bacula-sd.conf 中指定的匹配。

我想重复一下把 Client 资源加到 bacula-dir.conf 时提到的一点。这个 Storage 资源的 Name 不构成任何凭据的一部分。当我们的 Director 联系 10.55.0.76 上的 bacula-sd 时,它会使用这些凭据:

  1. MyBaculaDirector

  2. the bacula-sd password

Pool 与 Volume

到目前为止,我对 Pool 一笔带过。用磁带来描述 Pool 更容易引入概念。设想你有一盒用于 Full 备份的磁带。那盒里每盘磁带都保留 12 个月。你还有一盒用于 Incremental 备份的磁带。每盘保留 15 天。另一盒有 Differential 备份,每盘保留 3 个月。

每个盒子就是一个 Pool。每盘磁带就是一个 Volume。

不要把 Bacula Volume 与文件系统或你对卷的其他概念混淆。Bacula Volume 包含一个备份(或备份的一部分),也可能是空的。

Pool 定义用于描述 Volume 的初始和/或共同特性。因此,Pool 可视为共享共同特性(如 Retention)的 Volume 集合。

这些是我加到 pools.conf 的 Pool 定义:

这指定了三个 Pool,每种备份 Level 一个,各有不同的 Retention 值。

如果你想跳到运行作业,可以跳过接下来几节。

Pool 限制

你会注意到我创建的每个 Pool 都有两个最大设置。除非你另行告知,Bacula 会一直使用一个 Volume。我选择通过指定 Maximum Volume Bytes 来限制我的 Pool。当一个 Volume 达到此大小,Bacula 会停止使用它并创建另一个。这种自动创建之所以能发生,是因为这些指令:

  1. Pool 有 LabelFormat,

  2. bacula-sd.conf 中的 Device 设置了 LabelMedia = yes

我还在每个 Pool 中指定了最多 5 个 Volume。这把每个 Pool 的大小限制在 25G。显然,这些只是入门用的值。等东西跑起来后再相应调整你的值。

Pool 文档中还有许多此类选项。

Retention

当你初次想到 Retention 时,多数人会考虑备份保留多久。就 Bacula 而言,这并非严格正确的定义。Bacula Retention 指的是 Catalog,指定元数据在数据库中保留多久。Catalog 记录哪个 Job 备份了哪个 File、它位于哪个 Volume。

Bacula 使用三个 Retention 周期:

  1. File Retention

  2. Job Retention

  3. Volume Retention

File Retention 与 Job Retention 在 bacula-dir.conf 的每个 Client 资源中指定。我没有为我的 Client 指定任何值,因此默认分别为 60 天与 180 天。

Volume Retention 在 Pool 定义中指定。至关重要的是 Volume Retention 的起始点。Volume Retention 周期从一个 Volume 不再可追加时开始。Volume Retention 的倒计时只有在 Volume 状态变为 Append 以外的状态(例如 Full、Used、Purged 等)后才开始。

通过 Pruning 从 Catalog 中移除元数据。你会记得我的每个 Pool 都有 AutoPrune = yes

请留意,一旦某个备份的 File Retention 已过(且 Catalog 已被 prune),你将无法从该备份中选择要恢复的文件。你需要恢复整个 Job,而一旦该备份的 Job Retention 已过(且 Catalog 已被 prune),你将无法在不恢复到 bextractbscan 的情况下恢复该备份。如果不得不这样做,那绝不会是你这一周的高光时刻。

磁盘空间便宜,时间不便宜。把 File 与 Job Retention 设高,让恢复更容易。我认为我的 Catalog 数据库 dump 并压缩后大约 10GB。建议不要为节省磁盘空间而不必要地降低 Job 或 File Retention。

我把 File 与 Job Retention 设为较大的值。然后让 Volume Retention 决定元数据保留多久。这样,我可以把同一个备份复制到不同的 Pool,让该 Pool 的 Volume Retention 决定该备份保留多久。

恢复被 Prune 的数据

如果已发生 Pruning,你可以使用 bscan。我希望你永远不必在紧急情况下使用它。我把 bscan 视为万不得已的最后手段。

回收

按设计,Bacula 避免覆盖备份。它会竭尽全力避免这样做。你必须施加表明允许 Bacula 覆盖的限制。如果不这样做,Bacula 会一直追加到现有 Volume,直到空间耗尽。注意:磁盘空间监控不在本文范围内;使用 Nagios。

一旦施加限制,回收即可发生,Bacula 即可覆盖 Volume。这些限制的细节在文档中定义。我在此不深入回收,但此处给出的示例非常严格。

运行 Job

我们来运行第一个作业。哦……嗯,我们还有些最后的事情要做。

最后的事

以下目录同时被 bacula-dirbacula-sd 使用:

以下日志由 bacula-dir 使用:

以下内容允许入站数据库连接。你可能希望稍后收紧这一点,也许使用密码。我把这条加到 /usr/local/pgsql/data/pg_hba.conf 末尾:

这里是 bacula-sd 配置为存储备份的位置。这里是你的 Volume 将存储的位置。这在 bacula-sd.conf 中设置。

启动守护进程

用这些命令,我启动了 Bacula 守护进程:

它们可以按任何顺序启动。

运行 bconsole

bconsole 是你的朋友。你可以用 bconsole 运行备份与恢复。启动时你应当看到如下内容:

如果没有看到,你可能需要验证 bacula-dir 是否在运行。

状态

本节中,我们将在 Director、Client 与 Storage 上运行 status。

你可以缩写命令。我本可以输入 st di 并得到相同输出。

由于我们只有一个 Client,bacula-dir 自动选择该 Client 并向我们展示状态。执行该命令时,bacula-dir 联系 bacula-fd 询问其状态。看到此类输出可确认 bacula-dir 拥有该 client 的正确凭据。这对运行作业至关重要。

让我们检查 storage。

不必为 ‘Device “MyFirstStorage” (/usr/local/bacula/volumes) is not open’ 这条消息担心。当没有作业进行时,这完全正常。

你的第一个作业

我们来运行一个 Job。

完成。你的第一个备份!

不过,看到那个 JobId 4 了吗?是的,我花了四次尝试才让第一个作业跑起来。我有些配置要修复和调整。如果你复制配置文件、调整 IP 地址并完成所有最后的步骤,应该不会遇到这样的问题。

这里有一些说明帮你理解输出。编号对应上面输出的行号。

  • 18–m 是 messages 的缩写,会显示 Director 的所有待处理消息

  • 20–该作业提升为 Full。当不存在可作 Incremental 或 Differential 基础的 Job 时会发生

  • 23–在 FullFile Pool 中创建了一个新 Volume,使用指定的标签格式。由于我们备份到磁盘,文件名会与 Volume 名匹配

  • 25–标签也存储在 Volume 内部

  • 51-52–保存了 20 个文件。你可能想知道为什么是 20 个文件?这比我配置里的多得多。

我们来看看:

我把默认安装的文件移到了一个名为 INSTALLED 的子目录。有多少个文件?

21 个文件?只备份了 20 个?Bacula 按 FileSet 中的指定忽略了 bacula-dir.conf~

恢复

你只需要一个 Restore Job。恢复是手动进行的,虽然你可以脚本化。所有 Job 参数都可从 bconsole 中更改。这意味着你不必指定所有可能需要的组合。

哦。让我们把那个 Restore Job 加到 bacula-dir.conf

尽管这个 Job 引用了 MyFirstBaculaClient,我有意不把它加到 client-myclient.conf 中,因为它可用于任何 Client。

注意 Restore Job 包含一个 Where 指令。这指定了所有要恢复文件目录名的前缀。这允许把文件恢复到与备份位置不同的位置。如果未指定 Where,文件会恢复到其原始位置。Where 也可从 bconsole 中指定/覆盖(见后面的示例)。

我倾向于始终指定 Where 指令,作为安全/理智预防措施。

现在。我们来运行那个 Restore。

在所列选项中,我推荐 #5 与 #6。其余的也不错,但这俩是我最常用的。

我们只有一个备份,所以我选 #5:

Bacula 自动选择了 Client 与 FileSet,因为各只有一个。

18 个文件?我以为我们备份了 20 个文件?是的我们备份了,而其中两个“文件”是目录。此时我们处于一个特殊的 Bacula“shell”中,可以在目录中移动并选择要恢复的文件。我们本可以指定 restore all 来自动标记所有文件以恢复,或现在发出 restore all 命令。

我将标记 /usr/local/etc/bacula 中的所有 *.conf 文件。

你现在可以用 mod 命令覆盖任何 Job 指令。我直接回答 yes,让文件进入 /tmp/bacula-restores

完成。轰。就这样。

我们来看看它们。

我们在这里做一下 diff:

我第一次读到时困扰了一秒,但没错,这完全正确。Restore 作业是在初始备份运行后添加的。

但等等!还有更多!很多很多!

你刚刚走过了不少内容。关于 Bacula 有很多要学。有许多配置选项与可能的选择。我喜欢这一点。做事没有唯一正确的方式。你可以挑选。

我很想很快尝试的是 bacula-sdbacula-sd 的 Job 复制。我想把一些备份存到远程位置,而不必把磁带运过去。

本节余下内容是我在写本文时想到的各种事情。我认为它们应当被包含,值得各自独立成节。

我使用的 Retention 周期

我把 File 与 Job Retention 值设得非常高,高于我的 Volume Retention。一个 Volume 在其包含的所有 Job/File 从数据库(Catalog)中被 prune 之前无法被回收。通过把 File 与 Job Retention 设为 3 年,我确保我的 Volume 至少 3 年或 Volume Retention 周期(取较小者)内不受影响。

Volume 上可能仍有你的备份

即使 File、Job 与 Volume Retention 都已过,且 Volume 状态变为 Purged,备份数据仍完整保留在该 Volume 上。直到 Volume 被回收并向 Volume 写入新数据,你的备份数据才会丢失。

这一设计考量是 Bacula 绝对不愿覆盖你数据的一部分。如果你绝对需要来自 Purged Volume 的备份,你可以用 Bacula 实用程序取回。我推荐尝试 blsbextract

你也应当研究在你的 Job 中使用 Bootstrap Files,因为它们与 bextract 配合使用。

Catalog 与配置的备份

除了用 Bacula 备份我的 Catalog dump 外,我还把那个 dump、所有 Bacula 配置文件、所有 Bootstrap Files 复制到多个位置。我这样做是为了避免不得不使用 bscan 从 Volume 重建 Catalog。

问题?

bacula-dir 通常是最难配置和启动的守护进程。留意 /var/log/bacula.log/var/log/messages。如果其他方法都失败,尝试直接从命令行启动 bacula-dir

感谢阅读

我在本文中使用的 Bacula 配置文件可从 http://www.langille.org/examples/bacula7/ 下载。

Bacula 为我提供了一个备份系统的出色平台。它每天都被使用。我备份到启用压缩与自动快照的 ZFS 文件系统。这太棒了。每天,这些备份都被复制到磁带。一些磁带存到远程位置,以防万一。希望我永远不必从真正的灾难中恢复,但我知道 Bacula 多次在我删除/覆盖某些东西时救了我。

希望它对你同样有用。

Dan Langille 自 1998 年起使用 FreeBSD,几乎立刻就开始记录他的经历。这份在线日记后来成了 The FreeBSD Diary。他非常擅长描述执行各种任务的分步过程,从更改提示符到创建和维护 Jail。

作为软件工程师出身,Dan 一路掌握了系统管理与数据库技能。这份热情引领他进入 Cisco Talos 安全情报与研究小组 的当前 DevOps 工作,在那里他既做编码也做系统管理,乐在其中。

他是 BSDCan、PGCon、FreshPorts 与 FreshSource 的创始人。他热爱山地自行车,似乎在截止日期前如鱼得水。

最后更新于