aria2 凭什么比 requests 快 10 倍?把下载引擎换成 aria2 的完整指南
你有没有注意过:用 Python 的
requests下载一个 500MB 的视频大概要 3 分钟,但同样的网络环境下,yt-dlp 加上--downloader aria2c参数后只需要 20 秒。这不是魔法——aria2 从设计之初就是一个为"下载"而生的工具,它的并发模型、磁盘 I/O 策略、连接复用机制都和通用 HTTP 库有本质区别。这篇文章从原理层面解释 aria2 为什么快,然后给出在 Python 项目里集成 aria2 的三种方案:命令行调用、RPC 远程控制、以及直接用 libaria2 绑定。TL;DR:aria2 快的核心原因:① 多连接并发下载同一个文件(每个线程下载文件的不同部分),② 异步事件驱动(单进程处理数千连接),③ 磁盘写入优化(内存缓冲 + 顺序写入),④ BitTorrent/Metalink 内置支持。Python 集成推荐方案:小项目用
subprocess调命令行,大项目用 JSON-RPC 通过 WebSocket 控制 aria2 守护进程。yt-dlp 的--downloader aria2c参数是最简单的接入方式。
目录
- 一、为什么 requests 不适合大文件下载
- 二、aria2 的架构设计:为什么这么快
- 三、Python 集成方案一:subprocess 命令行调用
- 四、Python 集成方案二:JSON-RPC 远程控制
- 五、Python 集成方案三:yt-dlp 联动
- 六、aria2 参数调优指南
- 七、在服务端部署 aria2 的最佳实践
- 八、合规与温馨提示
一、为什么 requests 不适合大文件下载
先看一个事实:用 requests 下载大文件时,你的 CPU 几乎空闲,网络带宽也只用了 30% 左右。时间都花在哪了?
requests 下载 500MB 文件的实际耗时拆解:
TCP 握手 + TLS 协商 1.2s ← 不可避免
HTTP 请求 → 首字节响应 0.3s ← 服务器处理时间
数据接收(单连接串行) 156s ← 瓶颈在这里!
写入磁盘 12s ← 每次 write 后等待 fsync
总耗时:约 170s(带宽利用率 ~30%)
瓶颈在于:单 TCP 连接 + 同步 I/O。requests 在一个连接上串行读取数据,读一段写一段,中间的大量等待时间都被浪费了。而 aria2 的做法完全不同。
二、aria2 的架构设计:为什么这么快
2.1 多连接分片下载
aria2 最核心的能力:把一个文件拆成多段,每段用一个独立的 TCP 连接下载。
requests(单连接):
[=====连接1: 整个文件=====] → 耗时 170s
aria2(16 连接):
[=连接1: 0-32MB=]
[=连接2: 32-64MB=]
[=连接3: 64-96MB=]
... → 耗时 ~12s
[=连接16: 480-512MB=]
每个连接独立进行 TCP 握手和 TLS 协商,但它们并行执行。虽然单个连接的带宽可能受限于拥塞控制,但 16 个连接叠加起来,总带宽轻松跑满。
这个技术叫 HTTP Range 分片下载,基于 HTTP 的 Range 请求头:
GET /video.mp4 HTTP/1.1
Range: bytes=0-33554431
GET /video.mp4 HTTP/1.1
Range: bytes=33554432-67108863
服务端返回 206 Partial Content 和对应范围的数据。前提是服务端支持 Range 请求(响应头包含 Accept-Ranges: bytes)。
2.2 异步事件驱动
aria2 是 C++ 写的,基于事件驱动模型。单进程可以同时管理数千个 TCP 连接——新连接建立、数据到达、写入完成都是事件,通过回调处理。这和 Python 的 asyncio 理念一样,但 aria2 没有 GIL 的限制,效率更高。
线程模型(requests + 线程池):
线程1 [等待I/O] 线程2 [等待I/O] ... 线程16 [等待I/O]
→ 大量线程在空转,上下文切换有开销
事件驱动(aria2):
单线程 → 事件循环 → 哪个连接有数据就处理哪个
→ 零上下文切换,CPU 全部用于数据处理
2.3 磁盘 I/O 优化
requests 的写入方式:
resp.iter_content(chunk_size=8192)
→ 每 8KB write() 一次
→ 500MB 文件 = 64000 次系统调用
aria2 的写入方式:
内存缓冲 16MB → 一次性 write()
→ 500MB 文件 = 32 次系统调用
→ 减少 99.95% 的 write 系统调用
此外 aria2 使用 fallocate 预分配磁盘空间,避免写入过程中文件系统频繁分配新块。
2.4 连接复用与 Keep-Alive
aria2 默认启用 HTTP Keep-Alive,同一个服务器的多个 Range 请求可以复用同一个 TCP 连接(取决于服务器配置)。对于 M3U8 下载场景——几百个 TS 分片都来自同一个 CDN 域名——这种复用能省掉大量的握手时间。
三、Python 集成方案一:subprocess 命令行调用
最简单的方式,适合脚本和小工具。
3.1 基础用法
import subprocess
import os
def download_with_aria2(url, output_path, headers=None):
"""用 aria2 命令行下载单个文件"""
cmd = [
'aria2c',
url,
'-o', os.path.basename(output_path),
'-d', os.path.dirname(output_path) or '.',
'--max-connection-per-server=16', # 每个服务器最多 16 个连接
'--split=16', # 文件分成 16 段下载
'--min-split-size=1M', # 每段最少 1MB
'--max-concurrent-downloads=5', # 最多同时下载 5 个文件
'--continue=true', # 断点续传
'--retry-wait=3', # 重试等待 3 秒
'--max-tries=5', # 最多重试 5 次
'--console-log-level=warn', # 只显示警告和错误
'--summary-interval=0', # 不打印进度摘要
]
# 添加自定义请求头
if headers:
for key, value in headers.items():
cmd.extend(['--header', f'{key}: {value}'])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"aria2 下载失败: {result.stderr}")
return output_path
3.2 批量下载 M3U8 分片
import tempfile
def download_m3u8_with_aria2(segments, output_path, headers=None):
"""用 aria2 批量下载 M3U8 分片然后合并"""
# 创建临时目录
with tempfile.TemporaryDirectory() as tmpdir:
# 写一个 aria2 输入文件(每行一个 URL)
input_file = os.path.join(tmpdir, 'urls.txt')
with open(input_file, 'w', encoding='utf-8') as f:
for seg in segments:
# aria2 输入格式:URL\n out=文件名\n
f.write(f"{seg['url']}\n")
f.write(f" out=seg_{seg['seq']:05d}.ts\n")
# 构建命令
cmd = [
'aria2c',
'-i', input_file,
'-d', tmpdir,
'--max-concurrent-downloads=16',
'--max-connection-per-server=8',
'--split=8',
'--continue=true',
'--allow-overwrite=true',
'--console-log-level=warn',
'--summary-interval=0',
]
if headers:
for key, value in headers.items():
cmd.extend(['--header', f'{key}: {value}'])
subprocess.run(cmd, check=True)
# 合并分片
concat_list = os.path.join(tmpdir, 'concat.txt')
with open(concat_list, 'w') as f:
for seg in sorted(segments, key=lambda s: s['seq']):
f.write(f"file 'seg_{seg['seq']:05d}.ts'\n")
subprocess.run([
'ffmpeg', '-f', 'concat', '-safe', '0',
'-i', concat_list, '-c', 'copy', output_path
], check=True)
return output_path
3.3 进度解析
aria2 默认把进度输出到 stderr。可以解析这些输出来实现进度条:
import re
import subprocess
import threading
def download_with_progress(url, output_path, progress_callback=None):
"""带进度回调的 aria2 下载"""
cmd = [
'aria2c', url,
'-o', os.path.basename(output_path),
'-d', os.path.dirname(output_path) or '.',
'--max-connection-per-server=16',
'--split=16',
'--summary-interval=1', # 每秒输出一次进度
]
process = subprocess.Popen(
cmd,
stderr=subprocess.PIPE,
stdout=subprocess.DEVNULL,
text=True,
bufsize=1
)
# 正则匹配 aria2 的进度行
# 格式:[#SIZE 速度 进度%]
progress_pattern = re.compile(r'\((\d+)%\)')
size_pattern = re.compile(r'DL:(\d+\.?\d*)\s*([KMGT]?iB)')
for line in process.stderr:
match = progress_pattern.search(line)
if match and progress_callback:
pct = int(match.group(1))
size_match = size_pattern.search(line)
downloaded = ''
if size_match:
downloaded = f"{size_match.group(1)}{size_match.group(2)}"
progress_callback(pct, downloaded)
process.wait()
if process.returncode != 0:
raise RuntimeError("下载失败")
四、Python 集成方案二:JSON-RPC 远程控制
对于需要长期运行的下载服务,启动 aria2 为守护进程,通过 JSON-RPC 控制更灵活。
4.1 启动 aria2 RPC 服务
# 启动 aria2 守护进程(后台运行)
aria2c \
--enable-rpc \
--rpc-listen-all \
--rpc-allow-origin-all \
--rpc-secret=your_secret_token \
--dir=/data/downloads \
--max-concurrent-downloads=20 \
--max-connection-per-server=16 \
--split=16 \
--continue=true \
--daemon=true
4.2 Python 客户端
import requests
import json
import uuid
class Aria2RPC:
"""aria2 JSON-RPC 客户端"""
def __init__(self, host='127.0.0.1', port=6800, secret=''):
self.url = f'http://{host}:{port}/jsonrpc'
self.secret = secret
def _call(self, method, params=None):
"""调用 JSON-RPC 方法"""
payload = {
'jsonrpc': '2.0',
'id': str(uuid.uuid4())[:8],
'method': method,
'params': params or []
}
if self.secret:
# secret 作为第一个参数
payload['params'] = [f'token:{self.secret}'] + payload['params']
resp = requests.post(self.url, json=payload)
result = resp.json()
if 'error' in result:
raise Exception(f"aria2 RPC 错误: {result['error']}")
return result.get('result')
def add_uri(self, url, options=None):
"""添加下载任务"""
params = [[url], options or {}]
gid = self._call('aria2.addUri', params)
return gid
def add_uris(self, urls, options=None):
"""批量添加下载(多个 URL 下载同一个文件,互为镜像)"""
return self._call('aria2.addUri', [urls, options or {}])
def pause(self, gid):
"""暂停下载"""
return self._call('aria2.pause', [gid])
def unpause(self, gid):
"""恢复下载"""
return self._call('aria2.unpause', [gid])
def remove(self, gid):
"""删除下载任务"""
return self._call('aria2.remove', [gid])
def remove_download_result(self, gid):
"""删除任务并删除已下载的文件"""
return self._call('aria2.removeDownloadResult', [gid])
def tell_status(self, gid):
"""查询任务状态"""
result = self._call('aria2.tellStatus', [gid])
return {
'gid': result.get('gid'),
'status': result.get('status'), # active/waiting/paused/error/complete/removed
'total_length': int(result.get('totalLength', 0)),
'completed_length': int(result.get('completedLength', 0)),
'download_speed': int(result.get('downloadSpeed', 0)),
'files': result.get('files', []),
'error_message': result.get('errorMessage', ''),
}
def tell_active(self):
"""查询所有活动任务"""
return self._call('aria2.tellActive')
def tell_waiting(self, offset=0, num=100):
"""查询等待中的任务"""
return self._call('aria2.tellWaiting', [offset, num])
def tell_stopped(self, offset=0, num=100):
"""查询已停止的任务"""
return self._call('aria2.tellStopped', [offset, num])
def get_global_stat(self):
"""获取全局统计"""
result = self._call('aria2.getGlobalStat')
return {
'download_speed': int(result.get('downloadSpeed', 0)),
'upload_speed': int(result.get('uploadSpeed', 0)),
'num_active': int(result.get('numActive', 0)),
'num_waiting': int(result.get('numWaiting', 0)),
'num_stopped': int(result.get('numStopped', 0)),
}
def change_option(self, gid, options):
"""动态修改任务参数"""
return self._call('aria2.changeOption', [gid, options])
# 使用示例
if __name__ == '__main__':
client = Aria2RPC(secret='your_secret_token')
# 添加下载
gid = client.add_uri('https://example.com/video.mp4', {
'out': 'my_video.mp4',
'dir': '/data/downloads',
'split': '16',
'header': [
'Referer: https://www.example.com',
'User-Agent: Mozilla/5.0 ...',
]
})
# 轮询进度
import time
while True:
status = client.tell_status(gid)
if status['status'] in ('complete', 'error', 'removed'):
print(f"\n任务结束: {status['status']}")
if status['error_message']:
print(f"错误: {status['error_message']}")
break
total = status['total_length']
done = status['completed_length']
speed = status['download_speed']
pct = (done / total * 100) if total else 0
print(f"\r进度: {pct:.1f}% ({done}/{total}) "
f"速度: {speed/1024/1024:.1f} MB/s", end='')
time.sleep(1)
4.3 WebSocket 实时推送
aria2 也支持 WebSocket,可以在任务状态变化时主动推送通知:
import asyncio
import json
import websockets
async def aria2_ws_monitor(host='127.0.0.1', port=6800, secret=''):
"""通过 WebSocket 监听 aria2 事件"""
uri = f'ws://{host}:{port}/jsonrpc'
async with websockets.connect(uri) as ws:
# 发送订阅请求
payload = {
'jsonrpc': '2.0',
'id': 'monitor',
'method': 'system.multicall',
'params': [[
{
'methodName': 'aria2.onDownloadStart',
'params': [f'token:{secret}']
},
{
'methodName': 'aria2.onDownloadPause',
'params': [f'token:{secret}']
},
{
'methodName': 'aria2.onDownloadStop',
'params': [f'token:{secret}']
},
{
'methodName': 'aria2.onDownloadComplete',
'params': [f'token:{secret}']
},
{
'methodName': 'aria2.onDownloadError',
'params': [f'token:{secret}']
},
{
'methodName': 'aria2.onBtDownloadComplete',
'params': [f'token:{secret}']
},
]]
}
await ws.send(json.dumps(payload))
# 接收事件通知
async for message in ws:
data = json.loads(message)
event = data.get('method', '')
gid = data.get('params', [{}])[0].get('gid', '')
event_names = {
'aria2.onDownloadStart': '开始下载',
'aria2.onDownloadPause': '暂停',
'aria2.onDownloadStop': '停止',
'aria2.onDownloadComplete': '下载完成',
'aria2.onDownloadError': '下载失败',
}
print(f"[{event_names.get(event, event)}] GID: {gid}")
五、Python 集成方案三:yt-dlp 联动
如果你的项目已经用了 yt-dlp,那是最省事的方式——yt-dlp 内置了 aria2 支持:
import yt_dlp
def download_with_ytdlp_aria2(url, output_path):
"""通过 yt-dlp 使用 aria2 下载"""
ydl_opts = {
'outtmpl': output_path,
'format': 'bestvideo+bestaudio/best',
'merge_output_format': 'mp4',
'downloader': 'aria2c', # 使用 aria2 作为下载器
'downloader_args': {
'aria2c': [
'-x', '16', # 16 个连接
'-s', '16', # 16 个分片
'-k', '1M', # 最小分片 1MB
'--max-tries=5',
'--retry-wait=3',
]
},
'quiet': True,
'no_warnings': True,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
这是最推荐的方式——yt-dlp 负责解析和格式选择,aria2 负责高速下载,各司其职。
六、aria2 参数调优指南
6.1 核心参数速查
| 参数 | 含义 | 推荐值 | 说明 |
|---|---|---|---|
-x / --max-connection-per-server |
每服务器最大连接数 | 16 | 过大可能被限流 |
-s / --split |
分片数 | 16 | 不应超过 -x |
-k / --min-split-size |
最小分片大小 | 1M | 文件小于此值的 N 倍则减少分片 |
-j / --max-concurrent-downloads |
同时下载任务数 | 5-16 | 根据服务器承受能力调整 |
--continue |
断点续传 | true | 必开 |
--max-tries |
最大重试次数 | 5 | 0 表示无限重试 |
--retry-wait |
重试等待秒数 | 3-10 | 配合指数退避 |
--timeout |
连接超时 | 60 | |
--max-overall-download-limit |
全局下载限速 | 0(不限) | 避免占满带宽 |
--file-allocation |
文件分配策略 | falloc | Linux 推荐,Win 用 none |
6.2 不同场景的推荐配置
场景一:下载单个大文件(> 500MB)
aria2c -x 16 -s 16 -k 1M --file-allocation=falloc \
"https://example.com/large-video.mp4"
场景二:批量下载 M3U8 分片(几百个小文件)
aria2c -i urls.txt -j 20 -x 4 -s 4 --continue \
--max-tries=3 --retry-wait=2
场景三:限速下载(不影响其他网络活动)
aria2c -x 8 -s 8 \
--max-overall-download-limit=5M \
--max-download-limit=2M \
"https://example.com/video.mp4"
6.3 配置文件
把常用参数写到 ~/.aria2/aria2.conf,不用每次敲:
# ~/.aria2/aria2.conf
max-concurrent-downloads=10
max-connection-per-server=16
split=16
min-split-size=1M
continue=true
max-tries=5
retry-wait=5
timeout=60
file-allocation=falloc
disk-cache=32M
console-log-level=warn
summary-interval=0
# 自定义 UA
user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
七、在服务端部署 aria2 的最佳实践
7.1 使用 Docker 部署
# docker-compose.yml
version: '3'
services:
aria2:
image: p3terx/aria2-pro
container_name: aria2
restart: unless-stopped
ports:
- "6800:6800" # RPC
- "6888:6888" # BT
- "6888:6888/udp"
volumes:
- ./data:/downloads
- ./config:/config
environment:
- RPC_SECRET=your_secret_token
- RPC_PORT=6800
7.2 健康监控
import time
import requests
def monitor_aria2_health(rpc_client, check_interval=60):
"""定期检查 aria2 是否正常运行"""
while True:
try:
stats = rpc_client.get_global_stat()
print(f"[健康检查] 活跃: {stats['num_active']}, "
f"等待: {stats['num_waiting']}, "
f"速度: {stats['download_speed']/1024/1024:.1f} MB/s")
except Exception as e:
print(f"[告警] aria2 RPC 连接失败: {e}")
# 在这里触发告警(钉钉/邮件等)
time.sleep(check_interval)
7.3 下载完成后的回调
def on_download_complete(gid, status, callback):
"""下载完成后的处理"""
files = status.get('files', [])
for f in files:
path = f.get('path', '')
# 触发后续处理:转码、上传到 OSS、发通知等
callback({
'gid': gid,
'path': path,
'size': int(f.get('length', 0)),
})
八、合规与温馨提示
- aria2 是一个开源合法的下载工具,但使用方式决定合法性
- 过多并发连接可能被服务器视为 DoS 攻击,请合理控制连接数
- 部分平台的 CDN 禁止多线程下载,可能封禁 IP,使用前先测试
- 更多讨论见 下载视频算侵权吗?聊聊个人备份与版权的那条线
aria2 的本质就是把"等待"的时间利用起来。单连接下载时,你有一半时间在等服务器响应;16 个连接时,等待被分摊了,带宽自然就跑满了。如果你现在的下载器还在用 requests 单线程跑,不妨试试换成 aria2——体验过那种速度之后,就再也回不去了。
本文由 VidDown 技术博客原创发布。VidDown 桌面客户端内置了 aria2 下载引擎,支持多连接并发下载、断点续传和限速控制。配合 yt-dlp 内核,可以高速下载 30+ 平台的视频。访问 VidDown 了解更多。