Copyright@Sanotsuki

Build a super fast on demand local PyPi mirror

  • 当公司/局域网里有多人都使用 Python 开发,并且几乎都会用到 pip 来部署环境时,虽然已经有各种镜像源了,但是下载仍受限于与外网的宽带速度,并且同样的包可能被多人下载了多次,在包较大时,重复花的时间并不值
  • 当你使用 Docker 来构建不同的 Python 应用/环境时,在测试 Dockerfile 时可能需要不断的删掉之前 build 的版本,从头开始 build 时,pip 下载与上面面临同样的问题——重复消耗不必要的时间

其一解决方案是公司/局域网内部搞一个 PyPi 的镜像源,实际上维护一个完整的镜像源相当麻烦,占用的储存空间太大,在公司/局域网的情况下,大家开发的东西、使用的技术栈相对比较固定,这就导致完整的镜像源里会有很多包其实几乎没人用。

其二的解决方案可以是预先构建好一个或多个 Docker 镜像,其中包含大家都会用到的包,剩余的一些包则在使用时才被少数需要的人安装。这种方案的缺点则是目前 Docker 服务 + 多用户方案在重启之后会丢掉已经配置过的环境,重启之后依旧需要从镜像源下载包。

那么这里相对一劳永逸的方案则是搭建一个本地的按需下载的 PyPi 镜像源,其原理则是在镜像源与公司/局域网内增加了一个高速缓存,并且由于 PyPi 已经提交分发的whl或者tar.gz是不会变的,因此不用顾虑缓存时间的设置。

最后就像这样~ 182KB/s VS. 36.4MB/s
(cache server为千兆有线链接,MacBook为802.11 AC,测试时链接速度585Mbps)

It's apparently super fast after being cached!
It's apparently super fast after being cached!

代码放在了pip-cache~下面是详细的介绍/

由于静态文件占绝大多数,因此选择了 nginx 来为服务器。nginx 的服务设置很简单,其中 /etc/nginx/nginx.conf 做了一些调整,这里我是采用了 Docker 的方式,因此 nginx 的运行用户设置为了 root。如果在物理机上使用这套方案,则可保持运行用户为 www-data,以较低权限运行。

主要调整了 worker_connections 的数量,然后使用了 epoll 模型,在可能的高并发环境下也有不错的性能。

user root;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 4096;
    multi_accept on;
    use epoll;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    gzip on;

    include /etc/nginx/sites-enabled/*;
}

接下来是具体的服务配置文件,/etc/nginx/sites-available/default。这里我“给了” pypi.cocoaneko.moe 这个域名给这个服务——实际上是用 certbot 用 DNS 验证的方式申请了一下 SSL 证书,没有真的在 DNS 上将域名指向到具体的 IP,在局域网中,需要使用这个服务的人则可以修改一下 /etc/hosts,比如这个服务跑在 10.0.1.233 上,那么就加一行

# 在 /etc/hosts 中增加一行
10.0.1.233 pypi.cocoaneko.moe

对于 Docker 集群来说也可以用类似的方法。如果是用 Docker Compose 构建的那就更方便了(这个稍后与 Dockerfile 一起说)

nginx 的服务配置文件 /etc/nginx/sites-available/default 如下所示~申请好的 SSL 证书我放在了 /cert 下,物理机则只需保证 nginx 能找到就行

这里是针对国内的清华大学的 PyPi 源 (https://pypi.tuna.tsinghua.edu.cn/simple) 的配置。清华大学 PyPi 源的包都是统一放在 https://pypi.tuna.tsinghua.edu.cn/packages 目录下的。

因此这里我们让 nginx 缓存所有对 packages 目录的结果到 /srv/pypicache/data。那么对于这个 /srv/pypicache/data 的持久化,物理机上保证 nginx 对这个缓存目录有读写权限即可。在我实际的 Docker 配置中,我创建了一个名为 pypicache 的 volume,并将这个 volume 挂载到了 /srv/pypicache/data 上。

# 创建了一个名为 pypicache 的 volume
sudo docker volume create pypicache

而对 simple 目录的请求则每次都 forward 给清华大学,这样就可以保证我们总能拿到最新的 index。在下面的配置文件中,如果不使用清华大学的 PyPi 源的话,则可以对应修改高亮的部分。SSL 证书如果放在其他地方也需要修改~以及使用的域名,我自己的是 pypi.cocoaneko.moe,在下面也需要修改~

log_format pypicache-default '\$remote_addr - \$remote_user [\$time_local] "\$request" \$status \$body_bytes_sent "\$http_referer" "\$http_user_agent" DEFAULT';
log_format pypicache-other '\$remote_addr - \$remote_user [\$time_local] "\$request" \$status \$body_bytes_sent "\$http_referer" "\$http_user_agent" OTHER';
log_format pypicache-local '\$remote_addr - \$remote_user [\$time_local] "\$request" \$status \$body_bytes_sent "\$http_referer" "\$http_user_agent" LOCAL';
log_format pypicache-remote '\$remote_addr - \$remote_user [\$time_local] "\$request" \$status \$body_bytes_sent "\$http_referer" "\$http_user_agent" REMOTE';

server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;

    ssl on;
    ssl_certificate /cert/pypi.crt;
    ssl_certificate_key /cert/pypi.key;
    ssl_session_cache shared:SSL:10m;

    server_name pypi.cocoaneko.moe;
    root /srv/pypicache/data;
    index index.html index.htm;

    access_log /srv/pypicache/logs/access.log pypicache-default;
    error_log /srv/pypicache/logs/error.log;

    location /packages/ {
        try_files $uri @mirror;
        access_log /srv/pypicache/logs/access.log pypicache-local;
    }

    location / {
        proxy_next_upstream error timeout http_404;
        proxy_pass https://pypi.tuna.tsinghua.edu.cn;
        proxy_redirect off;
        proxy_set_header Host 'pypi.tuna.tsinghua.edu.cn';
        access_log /srv/pypicache/logs/access.log pypicache-other;
    }

    location @mirror {
        proxy_store on;
        proxy_store_access user:rw group:rw all:r;
        proxy_next_upstream error timeout http_404;
        proxy_pass https://pypi.tuna.tsinghua.edu.cn/$request_uri;
        proxy_redirect off;
        proxy_set_header Host 'pypi.tuna.tsinghua.edu.cn';
        access_log /srv/pypicache/logs/access.log pypicache-remote;
    }
}

物理机的话,配置则到这里就结束了,让 nginx 跑起来即可

sudo nginx -t
sudo service nginx restart

使用 Docker 的话,则需要编辑一下 Dockerfile,还有将刚才的配置文件与 SSL 证书组织好

文件的组织如下~

.
|-- Dockerfile                   # 用于 Docker 构建
`-- overlay                      
    |-- cert                     # SSL 证书
    |   |-- pypi.crt             # SSL certificate, full chain
    |   `-- pypi.key             # SSL 私钥
    `-- etc
        `-- nginx                # nginx 配置目录
            |-- nginx.conf       # nginx 全局设置
            `-- sites-available
                `-- default      # 我们构建的镜像服务的设置

Dockerfile 文件如下

# Copyright (c) {Iori,Ryza} Oikawa @ Meowtain
# Distributed under the terms of the Modified BSD License.

FROM nginx

LABEL maintainer="RyzaOikawa <[data deleted]>"

USER root

COPY overlay /
RUN mkdir -p /srv/pypicache/logs/ && \
    mkdir -p /etc/nginx/sites-enabled && \
    ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default

# forward request and error logs to docker log collector

RUN ln -sf /dev/stdout /srv/pypicache/logs/access.log && \
    ln -sf /dev/stderr /srv/pypicache/logs/error.log

STOPSIGNAL SIGTERM

CMD ["nginx", "-g", "daemon off;"]

在准备好上述文件之后,则只需要以下几步~

# 构建 Docker 镜像
sudo docker build -t pip-cache ./
# 运行服务
sudo docker run -d \
     -p 443:443 \
     --mount source=pypicache,target=/srv/pypicache/data \
     --name pypicache \
     pip-cache

这里是默认使用了 443 端口,如果 443 端口已经被占用的话,那么可以将前一个 443 改为其他可用的端口即可~

最后就可以正常使用了~比如这个服务跑在 10.0.1.233 上,域名为 pypi.cocoaneko.moe,端口为 233,那么用户在 pip 安装的时候,则可以先确定自己电脑上的 /etc/hosts 已经包含了

10.0.1.233 pypi.cocoaneko.moe

然后 pip 安装 tensorflow-gpu 的命令则为

pip3 install -i https://pypi.cocoaneko.moe:233/simple tensorflow-gpu

当然,每一个包的某一个版本第一次被请求时,还是会走外网,但是在之后的安装中则会走高速缓存,这时下载起来就会飞快了~

It's apparently super fast after being cached!
It's apparently super fast after being cached!

最后,如果是用 Docker Compose 部署的话,目录组织可以是

.
|-- docker-compose.yml               # 用于 Docker Compose
|-- ...                              # 其他文件
`-- pip-cache                        # *PyPi 高速缓存服务
    |-- Dockerfile                   # 用于 Docker 构建
    |-- overlay
        |-- cert                     # SSL 证书
        |   |-- pypi.crt             # SSL certificate, full chain
        |   `-- pypi.key             # SSL 私钥
        `-- etc
            `-- nginx                # nginx 配置目录
                |-- nginx.conf       # nginx 全局设置
                `-- sites-available
                    `-- default      # 我们构建的镜像服务的设置

然后在 docker-compose.yml 中写好对应的 service 即可~以 jupyter-docker-delpoy 的一个片段为例子~下面高亮的则是增加的部分~

# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

# JupyterHub docker-compose configuration file
version: "3"

services:
  pip-cache:
    build:
      context: pip-cache
      container_name: pypicache
      restart: always
      volumes:
        - "pypicache:/srv/pypicache/data"
      ports:
        - "233:443/tcp"
  hub:
    depends_on:
      - hub-db
      - pip-cache
    links:
      - hub-db
      - pip-cache
    build:
      context: .
      dockerfile: Dockerfile.jupyterhub
      args:
        JUPYTERHUB_VERSION: ${JUPYTERHUB_VERSION}
    restart: always
    image: jupyterhub

...# 中间省略

volumes:
  data:
    external:
      name: ${DATA_VOLUME_HOST}
  db:
    external:
      name: ${DB_VOLUME_HOST}
  pip-cache:
    external:
      name: pypicache

networks:
  default:
    external:
      name: ${DOCKER_NETWORK_NAME}

因为我们将 pip-cache 服务的 container_name 设置为了 pypicache,因此现在可以在构建单用户镜像时增加 /etc/hosts

pypicache pypi.cocoaneko.moe

Leave a Reply

Your email address will not be published. Required fields are marked *

19 − nine =