提示

返回博客列表

aria2 凭什么比 requests 快 10 倍?把下载引擎换成 aria2 的完整指南

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 不适合大文件下载

先看一个事实:用 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 的本质就是把"等待"的时间利用起来。单连接下载时,你有一半时间在等服务器响应;16 个连接时,等待被分摊了,带宽自然就跑满了。如果你现在的下载器还在用 requests 单线程跑,不妨试试换成 aria2——体验过那种速度之后,就再也回不去了。

本文由 VidDown 技术博客原创发布。VidDown 桌面客户端内置了 aria2 下载引擎,支持多连接并发下载、断点续传和限速控制。配合 yt-dlp 内核,可以高速下载 30+ 平台的视频。访问 VidDown 了解更多。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部