Jellyfin CDN:面向大众的 Mirrorbits
本页面由 PageTurner AI 翻译(测试版)。 未经项目官方认可。 发现错误? 报告问题 →
对多数项目而言,分发二进制文件很简单:上传到 GitHub 就大功告成了。很少有人会深入思考这个问题。但在 Jellyfin,我们需要更强大的解决方案——能比 GitHub 或基础 Web 服务器更优雅地满足需求。无论是对此感兴趣的开发者,还是支持类似项目的同行,我都想分享我们的实现方式。
序幕 - 10.6.0 版本之前
在 10.6.0 版本发布前,我们采用相当简单的仓库架构:所有构建都在名为 build1 的 Debian VPS 上响应 GitHub Webhook 完成。这台位于 Digital Ocean 多伦多区域的服务器,既承担构建任务,也通过 NGiNX 直接提供主仓库服务。
但这次发布首次暴露了问题。Jellyfin 是全球化项目,虽然我个人在加拿大安大略省,但绝大多数用户并非如此。尤其是欧洲和亚洲用户,下载版本时频频遭遇问题:主要投诉是下载速度极慢,甚至偶发完全超时。我们必须设计更好的方案。
身为骨子里的 DIY 爱好者,并领导着由 DIY 爱好者创建的项目,我不满足于简单套用 CloudFlare(且不论对该提供商的顾虑)。我需要完全可控的方案,于是开始寻找有效创建文件下载 CDN 的途径。
Mirrorbits 登场
幸运的是,我们并非孤军奋战。多年前,另一个 FLOSS 项目也曾遭遇相同困境。开发了优秀媒体播放器 VLC 的 VideoLAN,面临同样的文件分发难题。于是他们才华横溢的开发者创造了解决方案:Mirrorbits。
Mirrorbits 是个专注单一目标的 Go 程序:基于客户端 GeoIP 信息,将请求从中央仓库智能分发到多地镜像仓库,无缝处理可用性与文件时效性。它完美契合了我们的需求。
但存在一个问题:Mirrorbits 的实际操作文档稀缺,我们花费大量时间试错才掌握使用方法。希望以下分享能帮助他人规避这些困扰。
文件布局
首要考量是文件布局。Jellyfin 的仓库庞大而复杂,包含服务器、客户端、插件及各类辅助文件。Mirrorbits 要求所有内容置于单一目录下,我们需要便捷的全局同步方案。同时面临"归档文件"问题——那些无需同步到所有镜像服务器浪费空间的旧版稳定发行包。
我最终设计的目录结构如下:
/srv/repository
/mirror
/releases
/client
/server
/plugin
etc.
/debian
/dists
/pool
/ubuntu
/dists
/pool
/archive
etc.
/releases --symlink--> /mirror/releases
/debian --symlink--> /mirror/debian
etc.
实际上所有内容都位于 /mirror 目录下,通过外部符号链接提供所需的根级访问路径。
增设 VPS 与同步方案
确定可行的文件布局后,下一步是增设 VPS 并制定文件同步策略。
由于源服务器位于多伦多,我计划确保广泛地域覆盖,并为源服务器所在区域配备专用 CDN 以防过载。因此增设了 4 台 VPS:
-
tor1.mirror.jellyfin.org:加拿大安大略省多伦多(覆盖北美/南美东部) -
sfo1.mirror.jellyfin.org:美国加利福尼亚州旧金山(覆盖北美/南美西部) -
fra1.mirror.jellyfin.org:德国法兰克福(注意非常见误解:不是法国!覆盖欧洲与非洲) -
sgp1.mirror.jellyfin.org: 新加坡节点,服务亚太地区
最初搭建这4个VPS节点时,我曾担心它们会不堪重负。但经历了一个主版本更新和多次小版本发布后,关于下载速度的抱怨已完全消失,证明这套方案行之有效。未来随着Digital Ocean区域扩展,我们还能增设更多节点,比如印度班加罗尔、非洲地区,以及更多的欧洲和南美节点。
VPS部署完成后,接下来要考虑文件同步方案。我脑海中只有一个选择:rsync。虽然多数用户通过SSH使用rsync的scp替代功能,但我需要更健壮的方案——无需处理SSH密钥,且具备未来扩展性(比如对接不受信任的第三方镜像站)。
因此我选择rsync守护进程模式。它在873端口监听,能够执行rsync-over-SSH所能做的一切操作,同时避免加密开销和SSH/shell认证的复杂性。
首先在源服务器(build1)配置本地/etc/rsyncd.conf。Debian的rsync包默认不启动守护进程,但添加此配置文件后即可启用服务。
为处理前文提到的"归档文件",我将仓库拆分为两个"组件":含归档文件和不含归档文件的版本。这样镜像站可选择同步完整归档,或仅同步当前稳定版仓库。当我们开始提供每日可能生成数十次的"不稳定构建"时,我也将其归入"归档"组件。
这是我们的配置方案:
# /etc/rsyncd.conf
[mirror]
path = /srv/repository/mirror
comment = Jellyfin mirror (stable only)
exclude = *unstable* *archive*
[mirror-full]
path = /srv/repository/mirror
comment = Jellyfin mirror (all)
多数情况下需要安全加固,但由于我们希望开放给公众使用,我完全公开了rsync端点。因此如果你想搭建本地Jellyfin镜像站——完全可以!只需同步此rsync目标,就能获得完整的Jellyfin镜像副本!
镜像服务器仍需获取内容。最终我决定所有"官方"镜像都应同步完整内容(含归档和不稳定构建),因此它们都同步mirror-full源。
每个节点设置了简单的cron任务,每15分钟从源服务器同步更新:
# /etc/cron.d/mirror-sync
12,27,42,57 * * * * root rsync -au --delete rsync://build1.jellyfin.org/mirror-full/ /srv/repository/mirror/
这个特殊时间点是精心设计的——若未来正式支持第三方镜像站,它们可在整点(如00、30等)从这些"官方"镜像同步,而非直接连接build1。这样能确保第三方同步前内容已更新完毕,避免额外延迟。虽然尚未正式支持此功能,但如果流量持续增长,我们计划向第三方镜像站和更多Digital Ocean区域扩展。
rsync命令本应自动创建目标目录,但为保险起见,我选择手动预先创建。至此,我们拥有5台内容完全同步的服务器,镜像节点每15分钟从源服务器同步一次。
Web服务器配置
我们自始至终采用NGiNX作为Web服务器。原因很简单:极致配置灵活性与卓越性能。Apache虽有其优势,但我们不需要其额外功能,且早期预算紧张。NGiNX的优异表现让我们从未考虑更换方案。
镜像节点的配置极其简洁:仅配置单个"站点"提供仓库目录的完整访问。SSL证书由Let's Encrypt提供。
# /etc/nginx/sites-enabled/jellyfin-mirror (from fra1.mirror.jellyfin.org)
server {
listen [::]:80 default_server ipv6only=on;
listen 80 default_server;
listen [::]:443 ssl ipv6only=on;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/fra1.mirror.jellyfin.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/fra1.mirror.jellyfin.org/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
server_name fra1.mirror.jellyfin.org _;
root /srv/repository;
index index.php index.html index.htm;
access_log /var/log/nginx/access.log;
aio threads;
directio 1M;
output_buffers 3 1M;
sendfile on;
sendfile_max_chunk 0;
autoindex on;
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_param HTTP_PROXY "";
fastcgi_pass unix:/run/php/php7.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
在测试过程中,我们注意到一个可能对其他管理员有用的现象。当大量大文件请求同时涌入时,性能会显著下降。最终我将问题追溯到NGiNX内部的I/O堆栈瓶颈。通过查阅相关文档,我设置了上述的aio threads、directio、output_buffers和sendfile选项。这些配置确保NGiNX对超过1MB的文件使用直接I/O,并提供3个输出缓冲区(最大块大小设为0),从而提升高负载下的性能表现。
在源服务器上,NGiNX配置要复杂得多。由于Mirrorbits仅负责分发实际的大文件,我需要在源服务器上预先处理文件重定向 逻辑。这样客户端会被引导至正确的文件位置,再由Mirrorbits将该请求分发到镜像节点。
# /etc/nginx/sites-enabled/jellyfin-origin (on build1.jellyfin.org)
server {
listen 80 default_server proxy_protocol;
listen [::]:80 default_server proxy_protocol;
server_name repo.jellyfin.org build.jellyfin.org repo1.jellyfin.org build1.jellyfin.org _;
access_log /var/log/nginx/access.log proxy;
aio threads;
directio 1M;
output_buffers 3 1M;
sendfile on;
sendfile_max_chunk 0;
autoindex on;
root /srv/repository/mirror;
index index.php index.html index.htm index.nginx-debian.html;
location / {
autoindex off;
}
#
# Kodi Repository redirection
#
location ^~ /releases/client/kodi/py2 {
index index.html index.php;
autoindex on;
}
location ^~ /releases/client/kodi/py3 {
index index.html index.php;
autoindex on;
}
location ^~ /releases/client/kodi {
# Kodi 20
if ($http_user_agent ~* "(Kodi.*/20.*)") {
rewrite ^/releases/client/kodi/(.*)$ /releases/client/kodi/py3/$1 last;
}
# Kodi 19
if ($http_user_agent ~* "(Kodi.*/19.*)") {
rewrite ^/releases/client/kodi/(.*)$ /releases/client/kodi/py3/$1 last;
}
# Kodi 18 and older
if ($http_user_agent ~* "(Kodi.*)") {
rewrite ^/releases/client/kodi/(.*)$ /releases/client/kodi/py2/$1 last;
}
index index.html index.php;
autoindex on;
}
#
# Main repository
#
# Main release directories
location /releases {
autoindex on;
}
# Main archive directories (not forwarded to Mirrorbits)
location ^~ /archive {
try_files $uri $uri/ =404;
autoindex on;
}
# Mirrorbits fallback
location ^~ /master {
try_files $uri $uri/ =404;
autoindex on;
}
# Mirrorbits forwards for large file types
location ~ ^/(?<fwd_path>.*)(?<fwd_file>\.apk|\.buildinfo|\.bz|\.changes|\.db|\.deb|\.dmg|\.dsc|\.exe|\.gz|\.md5|\.lzma|\.rpm|\.sha256sum|\.xml|\.xz|\.zip|\.css|\.ttf|\.woff2|\.json)$ {
proxy_pass http://127.0.0.1:8080;
proxy_buffering off;
}
location ~ ^/(?<fwd_path>.*)(/mirrorlist)$ {
proxy_pass http://127.0.0.1:8080/$fwd_path?mirrorlist;
proxy_buffering off;
}
location ~ ^/(?<fwd_path>.*)(/mirrorstats)$ {
proxy_pass http://127.0.0.1:8080/$fwd_path?mirrorstats;
proxy_buffering off;
}
location /mirrorstats {
proxy_pass http://127.0.0.1:8080/?mirrorstats;
proxy_buffering off;
}
#
# PHP handling
#
location ~ \.php$ {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_param HTTP_PROXY "";
fastcgi_pass unix:/run/php/php7.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
部分配置值得详细说明:
Kodi选择器的存在是因为其后端从Python 2迁移到Python 3。某些旧版本需要Python 2资源,而新版本需要Python 3资源。该选择器根据客户端发送的User-Agent判断其使用的Kodi版本,从而引导至正确路径。
核心路径包括/archive、/releases和/master。其中第一个存放历史归档文件(尽管文件已在上文同步),通过try_files指令绕过Mirrorbits转发。第二个是主仓库目录,而第三个作为用于回退的重复位置,同样不重定向到Mirrorbits。
接下来是Mirrorbits主处理模块。转发逻辑基于请求文件的扩展名实现。因此当加载PHP索引页 等文件时,请求不会被转发;仅当请求指定类型的文件时,才会交由Mirrorbits进程分发给镜像节点。
后续三项配置用于Mirrorbits状态页面,展示当前可用镜像信息。在任何文件(例如https://repo.jellyfin.org/releases/server/debian/stable/meta/jellyfin_10.7.2-1_all.deb)的URL后添加/mirrorlist或/mirrorinfo即可查看镜像信息。您也可以亲自尝试:https://repo.jellyfin.org/releases/server/debian/stable/meta/jellyfin_10.7.2-1_all.deb/mirrorlist。而/mirrorstats页面(文件级或根域名级如https://repo.jellyfin.org/mirrorstats)则展示全局镜像状态,包括离线节点。
这些NGiNX配置共同构成了Mirrorbits的运行基础,也是耗时最长的环节。特别感谢GitHub上的@PalinuroSec提供的优秀配置示例。
Mirrorbits:核心引擎
安装Mirrorbits相当简单:我从其仓库下载最新二进制版本(0.5.1),将mirrorbits可执行文件放入/usr/local/bin,示例配置文件放入/etc/mirrorbits.conf