# MinIO源码阅读开端


MinIO 是一个高性能并且兼容亚马逊 S3 协议的对象存储服务。支持 Docker、单节点、分布式部署三种方式
<!--more-->

## 什么是MinIO

> MinIO is a High Performance Object Storage released under GNU Affero General Public License v3.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads.
>  
> This README provides quickstart instructions on running MinIO on bare metal hardware, including container-based installations. For Kubernetes environments, use the [MinIO Kubernetes Operator](https://github.com/minio/operator/blob/master/README.md).


MinIO 是一个高性能并且兼容亚马逊 S3 协议的对象存储服务。支持 Docker、单节点、分布式部署三种方式

## MinIO 基本组件

MinIO 的基本组件分为

- `server` MinIO 服务，提供对象存储服务
- `gateway` 网关。支持 s3、nas、hdfs、azure、gcs
- `console` MinIO内置的一个可视化的图形界面
- `client` mc(MinIO Client)客户端工具
- `cli` 用于启动服务的命令行工具

![](https://imgs.lambdahru.com/images/202201232126995.png#crop=0&crop=0&crop=1&crop=1&id=knEPu&originHeight=392&originWidth=739&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)

## server

MioIO Server 是 MinIO 服务，本质上是启动一个 HTTP 服务

### 裸机单节点部署

使用单节点部署 MinIO 非常简单，只需要指定一个挂载点即可，单机单挂载点部署使用的是**文件系统**，也无法使用桶复制、多版本等高级功能，如果要使用纠删码模式至少需要 4 块磁盘或者 4 个挂载点

```shell
./minio server /minio/data
```

上面的命令会启动一个 MinIO 服务，默认端口为 `9000` ，还会启动一个`随机`的 `console`的端口。

如果要指定 server 的端口，需要使用 `--address :port`

如果要指定 console 的端口，需要使用 `--console-address :port`

**冒号(:)是不能省略的**

需要注意的是，MinIO 支持两种服务模式，一种是本地 `server`一种是网关`gateway`。使用网关也很简单:

```shell
export MINIO_ROOT_USER=custom_access_key
export MINIO_ROOT_PASSWORD=custom_secret_key
minio gateway s3
```

网关模式支持`s3、nas、hdfs、azure、gcs`

### Docker 中单节点部署

```shell
docker run \
  -p 9000:9000 \
  -p 9001:9001 \
  -e "MINIO_ROOT_USER=AKIAIOSFODNN7EXAMPLE" \
  -e "MINIO_ROOT_PASSWORD=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \
  quay.io/minio/minio server /data --console-address ":9001"
```

### 纠删码（Erasure code）模式

纠删码是一种磁盘数据冗余算法或者说编码模式。通过纠删码将数据分割成片段，把冗余数据块扩展、编码，并将其存储在不同的位置，可以保证在丢失或者损坏一部分磁盘的情况下恢复所有数据。

纠删码分为**数据块**和**校验块**，也就是 n = k + m，通过算法将总磁盘数(挂载点个数) **n** ，分为 **k** 个数据块以及 **m**个校验块。

纠删码有**RS(Reed-Solomon)**、**LDPC(LowDensity Parity Check Code)** 等等

MinIO使用**RS**(Reed-Solomon)码将对象分割成可变数据和奇偶校验块，RS 的核心是**矩阵运算**

默认情况下，MinIO将对象分片到**N/2个数据块和N/2个奇偶校验块**，也可以自定义配置。但是官方建议的还是各 N/2 的模式，因为这样在损失半数磁盘的情况下也能够恢复数据。

单节点上使用纠删码只需要指定 4 个及以上的磁盘（挂载点即可）

```shell
 ./minio server --address :9000 --console-address :8080 /minio/data{1...4}
```

### 分布式部署

MinIO 的纠删码是可以跨主机跨磁盘的，所以采用分布式部署的时候默认就是使用**纠删码模式**的，所以**必须保证所有主机的**`**总磁盘数至少为 4**`** **，例如采用 2 节点部署，那么每台主机的磁盘数（挂载点）至少为 2，如果采用 4 节点部署，那么每台主机的挂载点至少为 1

```shell
export MINIO_ROOT_USER=<ACCESS_KEY>
export MINIO_ROOT_PASSWORD=<SECRET_KEY>
minio server http://host{1...n}/export{1...m}
```

host{1...n} 为主机地址，export{1...m} 为挂载点

#### 扩容

**1 . 停机扩容**

**2. ETCD扩容**

### 多租户(Multi-Tenancy)

MinIO 的一个租户是一个服务实例，在我们单机部署的一个服务也就是一个租户。只要是通过`./minio server ...`或者`./minio gateway ...`启动的一个服务就称为一个租户

```shell
minio server --address :9001 /data/tenant1
minio server --address :9002 /data/tenant2
minio server --address :9003 /data/tenant3
```

上面就代表在本机器启动了 3 个租户，也就是 3 个 MinIO Server。租户之间的数据是相互隔离的，这样保证了服务的隔离性以及灵活性。

更多关于多租户的部署方式可以看官方文档

> [https://docs.min.io/docs/multi-tenant-minio-deployment-guide.html](https://docs.min.io/docs/multi-tenant-minio-deployment-guide.html)


## gateway

MinIO 目前支持 **nas**、**azure**、**hdfs**、**gcs**、**s3**

## console

console 是 MinIO 提供的一个可视化的操作界面，其封装了和映射了调用 server 的接口。所以界面所请求的接口和 server 的接口不一样

![](https://imgs.lambdahru.com/images/202201231044968.png#crop=0&crop=0&crop=1&crop=1&id=Y4ELV&originHeight=1269&originWidth=1918&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)

## mc(MinIO Client)

`mc` 是一个命令行客户端工具，和 console 一样，用于操作 server。对于管理员使用 `mc admin`命令

> [https://docs.min.io/minio/baremetal/reference/minio-mc.html](https://docs.min.io/minio/baremetal/reference/minio-mc.html)
>  
> [https://docs.min.io/minio/baremetal/reference/minio-mc-admin.html](https://docs.min.io/minio/baremetal/reference/minio-mc-admin.html)


# 源码分析入门

这里不做源码的深入探讨和理解，只是提供一个阅读源码的方向引入

阅读源码之前不建议每一个方法都点击进去看细节，这样很容易迷失在各种细节之中。应当先理清楚大致逻辑，然后再针对某些点深入阅读

## main

MinIO 主程位于 cmd 包下面，最重要的两个文件是 `server-main.go` 以及 `gateway-main.go`，这个也就是 MinIO 的两种启动方式

主程序入口 `main.go`

源码根目录下的**main.go**

```go
package main // import "github.com/minio/minio"

import (
	"os"

	// MUST be first import.
	_ "github.com/minio/minio/internal/init"
	"github.com/minio/minio/internal/logger"

	minio "github.com/minio/minio/cmd"

	// Import gateway
	// 导入 gateway，里面会自动执行 init 函数，该包位于 @/cmd/gateway
	_ "github.com/minio/minio/cmd/gateway"
)

func main() {
    // 调用 cmd/main.go，这里还不会真正执行
	minio.Main(os.Args)
}
```

`main.go`中非常简单，主要是引入 **cmd 和 gateway**两个包，gateway 使用匿名导入，虽然没有调用，但是会执行各个 `init`函数

**cmd/gateway.go**

```go
package gateway

import (
	// Import all gateways please keep the order

	// NAS
	_ "github.com/minio/minio/cmd/gateway/nas"

	// Azure
	_ "github.com/minio/minio/cmd/gateway/azure"

	// S3
	_ "github.com/minio/minio/cmd/gateway/s3"

	// HDFS
	_ "github.com/minio/minio/cmd/gateway/hdfs"

	// GCS (use only if you must, GCS already supports S3 API)
	_ "github.com/minio/minio/cmd/gateway/gcs"
	// gateway functionality is frozen, no new gateways are being implemented
	// or considered for upstream inclusion at this point in time. if needed
	// please keep a fork of the project.
)
```

拿 s3 举例

**cmd/gateway/s3/gateway-s3.go**

```go
func init() {
    // ... 省略模板配置
    // 注册 gateway 的命令
    minio.RegisterGatewayCommand(cli.Command{
        Name:               minio.S3BackendGateway,
        Usage:              "Amazon Simple Storage Service (S3)",
        Action:             s3GatewayMain, // 此处的 Action 由 cli 调用并执行，在 s3GatewayMain 方法中会调用 gateway-main.go 中的 StartGateway 启动
        CustomHelpTemplate: s3GatewayTemplate,
        HideHelpCommand:    true,
    })
}

// Handler for 'minio gateway s3' command line.
func s3GatewayMain(ctx *cli.Context) {
	// 省略无关代码
	// Start the gateway..
	minio.StartGateway(ctx, &S3{
		host:  args.First(),
		debug: env.Get("_MINIO_SERVER_DEBUG", config.EnableOff) == config.EnableOn,
	})
}
```

gateway 的模式此处不再深入，继续看 **minio.Main(os.Args)**

进入到 **cmd/main.go**后，这里可以说是真正的主程运行的地方，我们看一下 main 方法：

**cmd/main.go**

```go
// Main main for minio server.
func Main(args []string) {
	// Set the minio app name.
	appName := filepath.Base(args[0])

	// Run the app - exit on error.
	// 1. newApp 初始化所有的对象
	// app.Action 会在 cli 包的 Run 方法执行 server_main.go 执行 serverMain， gateway_main.go 执行 StartGateway
	if err := newApp(appName).Run(args); err != nil {
		os.Exit(1)
	}
}
```

`newApp(appName).Run(args)`这是两句。`newApp()`方法主要就是初始化各种命令，这个由 `cli`包提供，这里就是初始化各种可执行的命令，帮助文档等等，以及最重要的启动方式`Action`，调用了 `Run`方法后，就会执行这个 `Action` 真正运行服务，而**命令行的解析、校验、纠删码、服务的初始化、子系统、启动等等也都在 Action 中执行**。

跳转到 newApp 方法中

**cmd/main.go#newApp**

```go
func newApp(name string) *cli.App {
    // ...
    
	// registerCommand registers a cli command.
	// 注册命令的函数
	registerCommand := func(command cli.Command) {
		commands = append(commands, command)
		commandsTree.Insert(command.Name)
	}
	// Register all commands.
	// 注册命令。分为 server 和 gateway 两种模式
	// gateway 的命令支持 s3 等，在 main.go 中引入了 gateway 包，触发了 init 函数植入的 Action
	registerCommand(serverCmd) // cmd/server-main.go
	registerCommand(gatewayCmd) // cmd/gateway-main.go
    
    // ...

	// cli 命令
	app := cli.NewApp()
    
    // ...
    
	return app
}
```

**cli/app.go#Run**

```go
func (a *App) Run(arguments []string) (err error) {
    // ...
	// Run default Action
	// 运行 Action 方法
	err = HandleAction(a.Action, context)
    // ...
	return err
}
```

可以看到 `registerCommand(serverCmd)`以及`registerCommand(gatewayCmd)`这两句是核心。注册`server、gateway`两个命令，里面就包含了 **server、gateway**两种模式所有的Flags以及 Action 方法。

## serverCmd

**serverCmd**就是 server 模式的所有命令集合，以及启动的 Action，在 Action中会对系统进行一系列的初始化，并**启动 HTTP 服务**

```go
var serverCmd = cli.Command{
	Name:   "server", // server 模式
	Usage:  "start object storage server",
	Flags:  append(ServerFlags, GlobalFlags...), // 所有可用的命令和配置项
	Action: serverMain,                          // 初始化的动作, 在 cli/app.go#Run 方法中执行 ！！！
	CustomHelpTemplate: `...`// 省略配置代码
}
```

serverMain 中包括了系统的初始化、比如分析器的采样速率、命令参数的校验、环境变量的处理、子系统的初始化以及验证是否使用纠删码模式、是否采用分布式部署等等

**cmd/server-main.go#serverMain** 此处省略了很多代码

```go
// serverMain handler called for 'minio server' command.
func serverMain(ctx *cli.Context) {
	// 信号量的处理
	signal.Notify(globalOSSignalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)

	go handleSignals()

	// 设置分析器的速率，采样速率
	setDefaultProfilerRates()

	// Initialize globalConsoleSys system
	// 初始化全局日志
	globalConsoleSys = NewConsoleLogger(GlobalContext)
	logger.AddTarget(globalConsoleSys)

	// Perform any self-tests
	// 系统自检
	bitrotSelfTest()
	erasureSelfTest()
	compressSelfTest()

	// Handle all server command args.
	// 处理命令行参数。会对所输入的命令进行校验
    // 并设置全局的一些变量，比如是否使用纠删码模式、是否是分布式部署、全局的挂载点处理等等
    // 其中 server 根据挂载的磁盘数量来决定是单机模式还是纠偏码模式。gateway 根据后面的命令参数来决定是使用什么代理模式进行，目前支持 azure gcs hdfs nas s3。
	serverHandleCmdArgs(ctx)

	// Handle all server environment vars.
	// 处理环境变量
	serverHandleEnvVars()

	// Set node name, only set for distributed setup.
	// 设置分布式节点名称
	globalConsoleSys.SetNodeName(globalLocalNodeName)

	// Initialize all help
	// 初始化帮助信息
	initHelp()

	// Initialize all sub-systems
	// 初始化子系统，随后会调用各个子系统的 Init 函数进行初始化
	newAllSubsystems()

	// Configure server.
	// 配置路由信息！！！
	handler, err := configureServerHandler(globalEndpoints)
	if err != nil {
		logger.Fatal(config.ErrUnexpectedError(err), "Unable to configure one of server's RPC services")
	}

	// 设置 http 服务
	httpServer := xhttp.NewServer(addrs).
		UseHandler(setCriticalErrorHandler(corsHandler(handler))).
		UseTLSConfig(newTLSConfig(getCert)).
		UseShutdownTimeout(ctx.Duration("shutdown-timeout")).
		UseBaseContext(GlobalContext).
		UseCustomLogger(log.New(ioutil.Discard, "", 0)) // Turn-off random logging by Go stdlib

    // 启动 HTTP 服务
	go func() {
		globalHTTPServerErrorCh <- httpServer.Start(GlobalContext)
	}()
    
	// Initialize users credentials and policies in background right after config has initialized.
	// IAM is  Identity and Access Management 身份和访问管理
	go globalIAMSys.Init(GlobalContext, newObject, globalEtcdClient, globalNotificationSys, globalRefreshIAMInterval)

	<-globalOSSignalCh
}
```

更加深入的这里不做探讨了，也就是所有的 server 模式启动方式都会经历以下生命周期

- 信号量处理
- 设置分析器的速率，采样速率
- 初始化全局日志
- 处理命令行参数
- ...

## gatewayCmd

待补充

## 从源码角度看 MinIO 的纠删码模式

MinIO 中要求最小的磁盘（挂载点）为 4，最大为 16 个**一组**，且，会比如有64 个磁盘（挂载点）那么将会分成 4组，每组 16 个磁盘。所以如果要使用纠删码，需要至少有 4 个挂载点：

在单机上部署纠删码模式的 server

```shell
./minio server --address :9000 --console-address :8080 /minio/data{1...4}
```

MinIO 可以使用 `...`来简写主机地址或者挂载点，在创建服务挂载点时会自动的解析：

```go
// endpoint-ellipses.go
func createServerEndpoints(serverAddr string, args ...string) (endpointServerPools EndpointServerPools, setupType SetupType, err error) {
    // args: /minio/data{1...4}
    if !ellipses.HasEllipses(args...) {
        // ...
    }
    // ...
}
```

**使用纠删码，需要至少有 4 个挂载点**主要体现在解析处理命令参数时候对挂载点的数据封装

**cmd/endpoint-ellipses.go**

```go
// Supported set sizes this is used to find the optimal
// single set size.
var setSizes = []uint64{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}

// getSetIndexes returns list of indexes which provides the set size
// on each index, this function also determines the final set size
// The final set size has the affinity towards choosing smaller
// indexes (total sets)
func getSetIndexes(args []string, totalSizes []uint64, customSetDriveCount uint64, argPatterns []ellipses.ArgPattern) (setIndexes [][]uint64, err error) {
    // ...
    // setSizes[0] == 4
    // 如果挂载点少于 4 个则直接返回错误！！！
    if totalSize < setSizes[0] || totalSize < customSetDriveCount {
        msg := fmt.Sprintf("Incorrect number of endpoints provided %s", args)
        return nil, config.ErrInvalidNumberOfErasureEndpoints(nil).Msg(msg)
    }
    // ...
}
```

当使用了纠删码过后，对象（文件）也会变成一个**文件夹**的形式，在这个文件下存放的是分片的**元数据 **`**xl.meta**`

在初始对象存储层的时候也是根据挂载点的个数来确定是初始**文件系统模式**还是**纠删码模式**

```go
// Initialize object layer with the supplied disks, objectLayer is nil upon any error.
func newObjectLayer(ctx context.Context, endpointServerPools EndpointServerPools) (newObject ObjectLayer, err error) {
	// For FS only, directly use the disk.
    // 如果挂载点只有一个，则使用文件系统模式初始化对象层
	if endpointServerPools.NEndpoints() == 1 {
		// Initialize new FS object layer.
		return NewFSObjectLayer(endpointServerPools[0].Endpoints[0].Path)
	}

	return newErasureServerPools(ctx, endpointServerPools)
}
```

