起因

我们的图片资源和其他静态资源都采用 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_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 / {
        # 这里需要将 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;
    }
}

需要注意的点

  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…