起因⌗

我们的图片资源和其他静态资源都采用 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…