クラウドストレージのファイルが多すぎて課金が止まらない人へ
旧ConoHa(2015年5月17日以前のConoHa)のオブジェクトストレージ(OpenStack Swift=AWSで言うところのS3)にファイルがめちゃくちゃ溜まっている人のための記事です。
コードを変えれば新しいConoHaでも使えると思います。(S3とかも)
溜まっていたファイル
一番多いコンテナ(フォルダのようなもの)に140万以上のファイルが溜まっていました。
「んじゃ削除すればいいんじゃん!」と思うかもしれませんがいくつかの理由がありできません。
前提
まず前提としてコンテナ(フォルダのようなもの)は、コンテナ内のファイルが無い状態でないと削除できず、ファイルシステムでよくやる rm -fr hogehoge 的なことがAPI経由ですらできません。
なのでまずコンテナ内のファイルをすべて削除する必要があります。
対応案1. ConoHaのコンパネで削除する
ConoHaのコンパネでは大量のファイルを削除する設計になっておらずブラウザがタイムアウトして大量のファイルは処理できません
対応策2. OpenStackクライアントのCyberduckを使う
ConoHa公式も勧めているCyberduckですが、こちらも大量のファイルの削除になると、ConoHa側のタイムアウトなどで頻繁に止まるので今回は使い物になりません。\(^o^)/
対応案3. 運営にConoHaアカウントを停止してもらう
「ファイルが多すぎて、コンパネでもCyberduckでも無理なの助けて!!!!(T_T)、アカウント停止できますか?」
とメールしましたが
「できません、自分でやってちょ!」
との解答が・・・\(^o^)/
ConoHaAPIを使ってプログラム的にGo言語のgoroutineで対応する
今回の140万ファイルをHTTP APIでConoHaにDELETE命令を送るには並列処理でがんがん回すしかありません。
Go言語のgoroutineでの並列処理が軽く、簡単にかけるということで今回フルスクラッチで書いてみました。
ソース : https://github.com/AKB428/kasumi
まずはHTTP GETメソッドをgoroutineで実行するサンプル
本格的に作りこむ前にまずサンプルで思い通りの挙動をするか確認してみます。
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
基本的にはスレッドプログラムを書いたことがある人にとっては自然な書き方だと思います。
10個スレッドを生成して、10スレッドが終了するのを待ち、各スレッドは並列で実行されるためどの順序で終わるかは不順列。
これをベースとしてConoHaAPI(OpenStack API)の処理を加えていきます。
goroutineでConoHa APIのファイルdeleteを実行
長いので重要なところを抜粋していきます
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から渡されるか明記されてないが今は1万ファイルのリストが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の生成コストは非常に少なく1万でも10万でも生成できるのですが今回のプログラムの場合以下が考慮必要になります
- クライアントOS側がどこまで同時接続のHTTPソケットに耐えきれるか
- サーバー側が同時アクセスにどこまで耐えきれるか (やりすぎるとAPIレートリミットにひっかかりそうです)
上記を考慮し、今回は 200 goroutineをデフォルト値としました。
やり方としては、50、100、150と順次ためして正常に動作するのを確認しながら数をあげていきます。
200だとConoHaサーバー側がいくつかエラー(おそらくタイムアウト)を返してきたのでこれ以上あげるのはやばそうでした。
結果、無事数時間で140万処理できました。
5時間で140万ファイルをHTTP API DELETEメソッドで消せたのでかなり早かったと思います。
| time | ファイル数 |
|---|---|
| 1時間あたりに削除できる数 | 299,320 |
| 1分あたりに削除できるファイル数 | 4,988 |
| 1秒あたりに削除できるファイル数 | 83 |
結論
今回はConoHa V1でしたが、最初に述べたとおりクラウドのストレージ事情は新ConoHa、AWS S3、GCP CloudStrageなどでも似たようなものだと思うので、コードを改変しgoroutineを活用すればストレージ課金地獄から抜け出せると思います。