ConoHa API V1の大量のオブジェクトストレージファイルをgoroutineで超高速に削除する

# クラウドストレージのファイルが多すぎて課金が止まらない人へ

旧ConoHa(2015年5月17日以前のConoHa)のオブジェクトストレージ(OpenStack Swift=AWSで言うところのS3)にファイルがめちゃくちゃ溜まっている人のための記事です。
コードを変えれば新しいConoHaでも使えると思います。（S3とかも）

## 溜まっていたファイル

<img width="1101" alt="conohaV1a.png" src="https://qiita-image-store.s3.amazonaws.com/0/36931/cce43e01-517a-e09e-f970-c3ebb446107d.png">

一番多いコンテナ（フォルダのようなもの）に１４０万以上のファイルが溜まっていました。
「んじゃ削除すればいいんじゃん！」と思うかもしれませんがいくつかの理由がありできません。

### 前提

まず前提としてコンテナ（フォルダのようなもの）は、コンテナ内のファイルが無い状態でないと削除できず、ファイルシステムでよくやる`` rm -fr hogehoge``　的なことがAPI経由ですらできません。
なのでまずコンテナ内のファイルをすべて削除する必要があります。


### 対応案1. ConoHaのコンパネで削除する

ConoHaのコンパネでは大量のファイルを削除する設計になっておらずブラウザがタイムアウトして大量のファイルは処理できません

### 対応策2. OpenStackクライアントのCyberduckを使う

ConoHa公式も勧めている[Cyberduck](https://cyberduck.io/)ですが、こちらも大量のファイルの削除になると、ConoHa側のタイムアウトなどで頻繁に止まるので今回は使い物になりません。＼(^o^)／

### 対応案3. 運営にConoHaアカウントを停止してもらう

「ファイルが多すぎて、コンパネでもCyberduckでも無理なの助けて！！！！(T_T)、アカウント停止できますか？」

とメールしましたが

**「できません、自分でやってちょ！」**

との解答が・・・＼(^o^)／


## ConoHaAPIを使ってプログラム的にGo言語のgoroutineで対応する

今回の１４０万ファイルをHTTP APIでConoHaにDELETE命令を送るには並列処理でがんがん回すしかありません。
Go言語のgoroutineでの並列処理が軽く、簡単にかけるということで今回フルスクラッチで書いてみました。

ソース : https://github.com/AKB428/kasumi


### まずはHTTP GETメソッドをgoroutineで実行するサンプル

本格的に作りこむ前にまずサンプルで思い通りの挙動をするか確認してみます。

```go
package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"
	"sync"
)

var wg sync.WaitGroup

// goroutineを生成する総数
var glNum = 10


func main() 
// goroutine生成数の登録
	wg.Add(glNum)

	flag.Parse()
	url := flag.Arg(0)

	for i:=0; i < glNum; i++ {
       // goで並列処理を実行
		go httpAccess(i, url)
	}

  // これがないとhttpAccessの処理が終わる前にmain関数が終了してしまう
	wg.Wait()
}

func httpAccess(i int, url string){
        // この関数の処理が終わったらgoroutine総数の数をデクリメントする
	defer wg.Done()
	response, error := http.Get(url)

	if error != nil {
		fmt.Println(error)
		return
	}
	fmt.Print(strconv.Itoa(i) + " ")
	fmt.Println(response.Status)

	// body, error
	ioutil.ReadAll(response.Body)

}
```

### サンプル実行

```
boku$ go run goroutine_sample_net.go 'https://www.yahoo.co.jp/'
9 200 OK
2 200 OK
7 200 OK
6 200 OK
8 200 OK
3 200 OK
1 200 OK
4 200 OK
0 200 OK
5 200 OK
```

基本的にはスレッドプログラムを書いたことがある人にとっては自然な書き方だと思います。
１０個スレッドを生成して、１０スレッドが終了するのを待ち、各スレッドは並列で実行されるためどの順序で終わるかは不順列。

これをベースとしてConoHaAPI(OpenStack API)の処理を加えていきます。


### goroutineでConoHa APIのファイルdeleteを実行

長いので重要なところを抜粋していきます

```go
package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"sync"
	"time"
)

// Conf ... ConoHa API アクセス情報
type Conf struct {
	AuthURL    string `json:"auth_url"`
	TenantName string `json:"tenantName"`
	Username   string `json:"username"`
	Password   string `json:"password"`
	EndPoint   string `json:"endPoint"`
}

// AuthToken ... Auth APIのレスポンスJSONを定義
type AuthToken struct {
 // 省略、JSONからgoのstructを生成するツールがあるのでそれで作成しています
}

var wg sync.WaitGroup

// goroutineの数　 ※1
var glNum = 200

func main() {

	flag.Parse()
	containerName := flag.Arg(0)
	fmt.Print(containerName)

	argGoroutineNum := flag.Arg(1)

	if argGoroutineNum != "" {
		glNum, _ = strconv.Atoi(argGoroutineNum)
	}

	fmt.Printf("%s %d", "gotoutine num = ", glNum)

	const format = "20060102_150405"
	logFileName := "./log/" + time.Now().Format(format) + ".log"

	logFile, _ := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY, 0666)

	defer logFile.Close()
	log.SetOutput(logFile)

	bytes, err := ioutil.ReadFile("./conf/conoha_api_v1_key.json")
	if err != nil {
		log.Fatal(err)
	}

	var conf Conf

	if err := json.Unmarshal(bytes, &conf); err != nil {
		log.Fatal(err)
	}
	// デコードしたデータを表示
	fmt.Printf("%+v\n", conf)

	token := getToken(conf)

	fmt.Println(token)

	deleteFileCounter := 0

	for {
		// 指定されたコンテナのファイルリストをスライスに取得
		objectList := getContainerList(token, conf.EndPoint, containerName)

		log.Println(fmt.Sprintf("%s: %d", "objectList size", len(objectList)))

		if len(objectList) == 0 {
			break
		}

		// もしファイル数が指定されたgoroutineの数よりも少ない場合はファイル数に合わせる
		if len(objectList) < glNum {
			log.Println(fmt.Sprintf("%s: %d < %d", "glNum < objectFileNum", glNum, len(objectList)))
			glNum = len(objectList)
		}

		counter := 0

		for _, url := range objectList {

			wg.Add(1)
			// goroutineで一気に削除(デフォルト200並列)
			go deleteObject(token, url)

			deleteFileCounter++
			counter++
			if counter == glNum {
				counter = 0
				wg.Wait()
				log.Println("wait done")
				log.Println(fmt.Sprintf("%s: %d", "deleteFileCounter", deleteFileCounter))
			}
		}

		// エラーは無視して順次削除を繰り返す
	}

}

func getToken(conf Conf) string {
// 省略　APIのアクセストークンを取得
}

func getContainerList(token string, baseURL string, containerName string) []string {
// 省略、コンテナのファイル数をスライス（配列）に取得、いくつのファイルがAPIから渡されるか明記されてないが今は１万ファイルのリストがConoHaサーバーから返却される
}

func deleteObject(token string, url string) {
	defer wg.Done()

	// fmt.Println("DELETE: " + url)

	req, _ := http.NewRequest("DELETE", url, nil)
	req.Header.Set("X-Auth-Token", token)

	client := new(http.Client)
	response, err := client.Do(req)

	if err != nil {
		fmt.Printf("%+v\n", err)
	}

	//body, _ := ioutil.ReadAll(response.Body)

	//fmt.Println(response.Status) = string 204 No Content

	if response.StatusCode != 204 {
		fmt.Println(response.Status)
	}

}

```

処理フローとしては最初のサンプルと変わってないので理解しやすいと思います。

- [1]. コンテナ内のファイル名を取得(10,000ファイルずつ)
- [2]. 取得したファイル名から、指定されたgoroutineの数(200)だけ並列処理して削除 
- [3]. 200処理をwaitし、終わったら残り(98,000)を処理 [2]に戻る
- [4]. 10,000処理しきったら、[1]に戻る

### ★重要なポイント　 goroutineの数をいくつにするか

goroutineの生成コストは非常に少なく１万でも１０万でも生成できるのですが今回のプログラムの場合以下が考慮必要になります

- クライアントOS側がどこまで同時接続のHTTPソケットに耐えきれるか
- サーバー側が同時アクセスにどこまで耐えきれるか　（やりすぎるとAPIレートリミットにひっかかりそうです）

上記を考慮し、今回は **200 goroutine**をデフォルト値としました。
やり方としては、50、100、150と順次ためして正常に動作するのを確認しながら数をあげていきます。
２００だとConoHaサーバー側がいくつかエラー（おそらくタイムアウト）を返してきたのでこれ以上あげるのはやばそうでした。


## 結果、無事数時間で１40万処理できました。

５時間で140万ファイルをHTTP API DELETEメソッドで消せたのでかなり早かったと思います。



|time|ファイル数| 
|:-----|:----|
|1時間あたりに削除できる数|299,320|
|1分あたりに削除できるファイル数	|4,988|
|1秒あたりに削除できるファイル数	|83|

<img width="1049" alt="ConoHaV1b.png" src="https://qiita-image-store.s3.amazonaws.com/0/36931/9353cfb9-bdae-b515-914b-7ae2cb69e74c.png">




## 結論

今回はConoHa V1でしたが、最初に述べたとおりクラウドのストレージ事情は新ConoHa、AWS S3、GCP CloudStrageなどでも似たようなものだと思うので、コードを改変しgoroutineを活用すればストレージ課金地獄から抜け出せると思います。