起因

我们的图片资源和其他静态资源都采用 S3 来进行存储,于是我们为了降低代码复杂度,我们在开发和测试阶段用的存储页时线上 S3 环境,一直以来一切都正常运行着。但是突然有一天,在国内的测试服上传文件到 S3 出现了超时的问题,且这个问题一直无法得到解决。

起初我们考虑在测试服的时候,将文件上传到项目目录的 Storage 下,但是在配置和生成 URL 的时候非常麻烦,不利于 DevOps。

我们的项目中,大量使用自定义的配置或者 ENV 来拼接静态资源的 URL,导致代码的可维护性极差。而且如果要保证访问的一致性,还需要为上传的文件做单独的 Web 服务。

于是乎我就想起了之前在 Github 上看到的一个开源项目 MinIO,没错它就是今天的主角。

MinIO 简介

MinIO 是采用 Go 开发的一套类似于 S3 的存储服务,为什么说用它替代 S3 呢,因为它能兼容 S3 的 API。这样一来,在项目中集成的时候,降低了复杂度。主要在资源 URL 的生成中。

部署 MinIO

我这里采用 Docker Compose 来进行部署,配置文件如下:

services:
  minio:
  image: minio/minio:latest
  restart: always
  hostname: minio
  container_name: minio
  ports:
    - 9000:9000
    - 9001:9001
  volumes:
    - /usr/www/data/minio:/data
  environment:
    - MINIO_DOMAIN=example.dev # 开启 DNS Style Bucket 模式
    - MINIO_ROOT_USER=${MINIO_ROOT_USER}
    - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
  command: server /data --console-address ":9001"

设置 Nginx 代理:

server {
    listen 80;
    listen [::]:80;
    server_name minio.example.dev bucket.example.dev minio-console.example.dev;

    location / {
        return 301 https://$host$request_uri;
    }
}

# 文件上传时用到的域名
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name minio.example.dev;

    # SSL
    ssl_certificate certs/fullchain.cer;
    ssl_certificate_key certs/example.dev.key;

    ssl_stapling on;
    ssl_stapling_verify on;

    proxy_buffering off;
    client_max_body_size 0;

    ssl_trusted_certificate /etc/nginx/certs/ca-bundle.trust.crt;

    include components/security.conf;

    # gzip
    gzip                    on;
    gzip_vary               on;
    gzip_proxied            any;
    gzip_comp_level         6;
    gzip_types              text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

    location / {
        proxy_pass http://minio:9000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# MinIO Bucket 的域名,这样访问上传文件的 URL 中不用加 Bucket 名称
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name bucket.example.dev;

    # SSL
    ssl_certificate certs/fullchain.cer;
    ssl_certificate_key certs/example.dev.key;

    ssl_stapling on;
    ssl_stapling_verify on;

    proxy_buffering off;
    client_max_body_size 0;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    ssl_trusted_certificate /etc/nginx/certs/ca-bundle.trust.crt;

    include components/security.conf;

    # gzip
    gzip                    on;
    gzip_vary               on;
    gzip_proxied            any;
    gzip_comp_level         6;
    gzip_types              text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

    location / {
        #在不开启 DNS Style Bucket 的情况下,这里需要将 MinIO 里创建的 Bucket 作为代理的 Endpoint
        proxy_pass http://minio:9000/bucket/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# MinIO 管理后台的代理配置
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name minio-console.example.dev;

    # SSL
    ssl_certificate certs/fullchain.cer;
    ssl_certificate_key certs/example.dev.key;

    ssl_stapling on;
    ssl_stapling_verify on;

    proxy_buffering off;
    client_max_body_size 0;

    ssl_trusted_certificate /etc/nginx/certs/ca-bundle.trust.crt;

    include components/security.conf;

    location / {
        proxy_pass http://minio:9001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /ws {
        proxy_pass http://minio:9001;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        chunked_transfer_encoding off;
    }
}

MinIO 同样支持 DNS Style Bucket,但默认情况不开启,要开启需要使用环境变量 MINIO_DOMAIN=domain.com 来为 MinIO 设置 FQDN。例如你的 Bucket 名称是 assets,则访问方式就是 assets.domain.com 。开启了 DNS Style Bucket 模式后就不用在通过 Nginx 层来实现 subdomain 到 /path 的映射了。需要注意的是:这种方式需要将 TLD*.TLD 都解析到 MinIO 的代理服务器。

例如 Nginx 中使用通配域名作为 Bucket 的访问域名:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name *.example.dev;

    # SSL
    ssl_certificate certs/fullchain.cer;
    ssl_certificate_key certs/example.dev.key;

    ssl_stapling on;
    ssl_stapling_verify on;

    proxy_buffering off;
    client_max_body_size 0;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    ssl_trusted_certificate /etc/nginx/certs/ca-bundle.trust.crt;

    include components/security.conf;

    # gzip
    gzip                    on;
    gzip_vary               on;
    gzip_proxied            any;
    gzip_comp_level         6;
    gzip_types              text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

    location / {
        #开启 DNS Style Bucket 的情况下,这里就不需要需要将 MinIO 里创建的 Bucket 作为代理的 URL PATH。
        proxy_pass http://minio:9000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

如果你使用的是 Traefik,则可以参考如下配置:

services:
  minio:
    image: minio/minio:latest
    labels:
      - traefik.enable=true

      - traefik.http.routers.minio.tls=true
      - traefik.http.routers.minio.tls.certresolver=step-ca
      - traefik.http.routers.minio.rule=Host(`example.dev`)
      - traefik.http.routers.minio.service=minio
      - traefik.http.routers.minio.entrypoints=http,https
      - traefik.http.services.minio.loadbalancer.server.port=9000

      - traefik.http.routers.minio-console.tls=true
      - traefik.http.routers.minio-console.tls.certresolver=step-ca
      - traefik.http.routers.minio-console.rule=Host(`minio-console.example.dev`) || PathPrefix(`/ws`)
      - traefik.http.routers.minio-console.service=minio-console
      - traefik.http.routers.minio-console.entrypoints=http,https
      - traefik.http.services.minio-console.loadbalancer.server.port=9001

      - traefik.http.routers.bucket.tls=true
      - traefik.http.routers.bucket.tls.certresolver=step-ca
      - traefik.http.routers.bucket.tls.domains[0].main=example.dev
      - traefik.http.routers.bucket.tls.domains[0].sans=*.example.dev
      - traefik.http.routers.bucket.rule=HostRegexp(`^.+.example.dev$`)
      - traefik.http.routers.bucket.service=bucket
      - traefik.http.routers.bucket.priority=10
      - traefik.http.routers.bucket.entrypoints=http,https
      - traefik.http.services.bucket.loadbalancer.server.port=9000
    restart: no
    hostname: minio
    container_name: minio
    networks:
      - traefik
    volumes:
      - minio-data:/data
    environment:
      TZ: Asia/Shanghai
      MINIO_DOMAIN: minio.svc.dev
      MINIO_ROOT_USER: developer
      MINIO_ROOT_PASSWORD: Developer@1994
    command: server /data --console-address ":9001"

需要注意的点

  1. 一定要加上 proxy_set_header 的相关配置,否则无法正常访问 MinIO;
  2. 为 Bucket 单独分配一个域名,这样可以完美模拟 S3 的访问 URL;

Laravel 中集成 MinIO

安装依赖

composer require league/flysystem-aws-s3-v3:~1.0

修改配置文件

修改 config/filesystems.php 文件中的配置如下:

's3' => [
    'driver' => 's3',
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION', 'ap-northeast-1'),
    'bucket' => env('AWS_BUCKET', 'neox'),
    'url' => env('AWS_URL'),
    'endpoint' => env('AWS_ENDPOINT'),
    'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false)
]

修改环境配置

修改 .env 文件中的配置如下:

FILESYSTEM_DRIVER=s3
AWS_BUCKET=static # 这里是你 Bucket 的名称
AWS_URL=https://bucket.example.dev # 用于生成 URL 的前缀
AWS_ENDPOINT=https://minio.example.dev # 用于上传文件时访问的 URL
AWS_DEFAULT_REGION=ch-shanghai # 这个是 MinIO 后台配置的 Region
AWS_ACCESS_KEY_ID=MINIO_ACCESS_KEY_ID # 这里是你在 MinIO 后台创建的 User Access Key ID
AWS_SECRET_ACCESS_KEY=MINIO_ACCESS_KEY_SECRET # 这里是你在 MinIO 后台创建的 User Access Key SECRET
AWS_USE_PATH_STYLE_ENDPOINT=true # 这里一定要用 true,才能完美兼容 S3

上传文件

$uri = Storage::put($path, $request->file('file'), ['visibility' => 'public']);
if ($uri) {
    return response()->json([
        'uri' => $uri,
        'url' => Storage::url($uri),
        'filename' => Str::afterLast($uri, '/')
    ]);
}

响应如下:

{
    "uri": "trend/reports/8q472L1asBz06mM7VK7i4gd1Kyen4eWRaAcxlmX5.jpg",
    "url": "https://bucket.example.dev/trend/reports/8q472L1asBz06mM7VK7i4gd1Kyen4eWRaAcxlmX5.jpg",
    "filename": "8q472L1asBz06mM7VK7i4gd1Kyen4eWRaAcxlmX5.jpg"
}

到这里就完成了 MinIO 的集成和使用了,在生成环境中我们只需要将 .env 的配置项修改为生产环境的配置项就可以了。

预告

因为我们的测试服数据是克隆自生产环境的,所以很多图片资源是存储在 S3 伤的,那么如何将 S3 上的文件同步到测试服的 MinIO 服务上呢?

后面我会分享如何使用 MinIO CLI 进行同步的经验,以及使用事件和消息队列的方式进行资源同步。

I hope this is helpful, Happy hacking…