mirror of
https://github.com/percona/percona-toolkit.git
synced 2025-09-20 10:55:01 +00:00
PT-1978 Implemented duplicated indexes detection (#537)
* PT-1978 Implemented duplicated indexes detection * WIP * PT-1978 WIP * PT-1978 New tool pt-mongodb-check-index * Removed unused file * Added version command * Updated required params * Added system.profile to the ingored dbs * Fixed for CR
This commit is contained in:
2
go.mod
2
go.mod
@@ -40,9 +40,11 @@ require (
|
|||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.15.1 // indirect
|
github.com/klauspost/compress v1.15.1 // indirect
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1 // indirect
|
||||||
github.com/smartystreets/goconvey v1.7.2 // indirect
|
github.com/smartystreets/goconvey v1.7.2 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||||
github.com/tklauser/numcpus v0.4.0 // indirect
|
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||||
|
5
go.sum
5
go.sum
@@ -13,6 +13,9 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO
|
|||||||
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||||
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
|
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
|
||||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||||
|
github.com/alecthomas/kong v0.5.0 h1:u8Kdw+eeml93qtMZ04iei0CFYve/WPcA5IFh+9wSskE=
|
||||||
|
github.com/alecthomas/kong v0.5.0/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0=
|
||||||
|
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
|
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
|
||||||
@@ -242,6 +245,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
|
|||||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/shirou/gopsutil v2.20.8+incompatible h1:8c7Atn0FAUZJo+f4wYbN0iVpdWniCQk7IYwGtgdh1mY=
|
github.com/shirou/gopsutil v2.20.8+incompatible h1:8c7Atn0FAUZJo+f4wYbN0iVpdWniCQk7IYwGtgdh1mY=
|
||||||
github.com/shirou/gopsutil v2.20.8+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
github.com/shirou/gopsutil v2.20.8+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||||
|
48
src/go/pt-mongodb-index-check/README.md
Normal file
48
src/go/pt-mongodb-index-check/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# pt-mongodb-index-check
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This tool can perform checks on MongoDB indexes.
|
||||||
|
|
||||||
|
Currently, these checks are available:
|
||||||
|
|
||||||
|
### Duplicated indexes
|
||||||
|
|
||||||
|
Check for indexes that are the prefix of other indexes. For example if we have these 2 indexes
|
||||||
|
|
||||||
|
```
|
||||||
|
db.getSiblingDB("testdb").test_col.createIndex({"f1": 1, "f2": -1, "f3": 1, "f4": 1}, {"name": "idx_01"});
|
||||||
|
db.getSiblingDB("testdb").test_col.createIndex({"f1": 1, "f2": -1, "f3": 1}, {"name": "idx_02"});
|
||||||
|
```
|
||||||
|
|
||||||
|
The index `idx_02` is the prefix of `idx_01` because it has the same keys in the same order so, `idx_02` can be dropped.
|
||||||
|
|
||||||
|
### Unused indexes.
|
||||||
|
|
||||||
|
This check gets the `$indexstats` for all indexes and reports those having `accesses.ops` = 0.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Run the program as `pt-mongodb-index-check <command> [flags]`
|
||||||
|
|
||||||
|
#### Available commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ---------------- | ---------------------------------- |
|
||||||
|
| check-duplicated | Run checks for duplicated indexes. |
|
||||||
|
| check-unused | Run check for unused indexes. |
|
||||||
|
| check-all | Run all checks |
|
||||||
|
|
||||||
|
#### Available flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
| ---- | ----------- |
|
||||||
|
|--all-databases|Check in all databases excluding system dbs.|
|
||||||
|
|--databases=DATABASES,...|Comma separated list of databases to check.|
|
||||||
|
|--all-collections|Check in all collections in the selected databases.|
|
||||||
|
|--collections=COLLECTIONS,...|Comma separated list of collections to check.|
|
||||||
|
|--mongodb.uri=<connection string>|Connection URI|
|
||||||
|
|--json|Show output as JSON|
|
||||||
|
|
103
src/go/pt-mongodb-index-check/indexes/duplicated.go
Normal file
103
src/go/pt-mongodb-index-check/indexes/duplicated.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package indexes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type collectionIndex struct {
|
||||||
|
Name string `bson:"name"`
|
||||||
|
Namespace string `bson:"ns"`
|
||||||
|
V int `bson:"v"`
|
||||||
|
Key primitive.D `bson:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (di collectionIndex) ComparableKey() string {
|
||||||
|
str := ""
|
||||||
|
for _, elem := range di.Key {
|
||||||
|
str += sign(elem) + elem.Key
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func sign(elem primitive.E) string {
|
||||||
|
sign := "+"
|
||||||
|
switch elem.Value.(type) {
|
||||||
|
case int32: // internal MongoDB indexes like _id_ or lastUsed have the sign field as int32.
|
||||||
|
if elem.Value.(int32) < 0 {
|
||||||
|
sign = "-"
|
||||||
|
}
|
||||||
|
case float64: // All other indexes have the sign field as float64.
|
||||||
|
if elem.Value.(float64) < 0 {
|
||||||
|
sign = "-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sign
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexKey holds the list of fields that are part of an index, along with the field order.
|
||||||
|
type IndexKey []primitive.E
|
||||||
|
|
||||||
|
// String returns the index fields as a string. The + sign means ascending on this field
|
||||||
|
// and a - sign indicates a descending order for that field.
|
||||||
|
func (di IndexKey) String() string {
|
||||||
|
str := ""
|
||||||
|
for _, elem := range di {
|
||||||
|
str += sign(elem) + elem.Key + " "
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// DuplicateIndex represents a duplicated index pair.
|
||||||
|
// An index is considered as the duplicate of another one if it is it's prefix.
|
||||||
|
// Example: the index +f1-f2 is the prefix of +f1-f2+f3.
|
||||||
|
type Duplicate struct {
|
||||||
|
Namespace string
|
||||||
|
Name string
|
||||||
|
Key IndexKey
|
||||||
|
ContainerName string
|
||||||
|
ContainerKey IndexKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindDuplicated(ctx context.Context, client *mongo.Client, database, collection string) ([]Duplicate, error) {
|
||||||
|
di := []Duplicate{}
|
||||||
|
|
||||||
|
cursor, err := client.Database(database).Collection(collection).Indexes().List(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []collectionIndex
|
||||||
|
if err = cursor.All(context.TODO(), &results); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
return results[i].ComparableKey() < results[j].ComparableKey()
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := 0; i < len(results)-1; i++ {
|
||||||
|
for j := i + 1; j < len(results); j++ {
|
||||||
|
if strings.HasPrefix(results[j].ComparableKey(), results[i].ComparableKey()) {
|
||||||
|
idx := Duplicate{
|
||||||
|
Namespace: database + "." + collection,
|
||||||
|
Name: results[i].Name,
|
||||||
|
Key: make([]primitive.E, len(results[i].Key)),
|
||||||
|
ContainerName: results[j].Name,
|
||||||
|
ContainerKey: make([]primitive.E, len(results[j].Key)),
|
||||||
|
}
|
||||||
|
copy(idx.Key, results[i].Key)
|
||||||
|
copy(idx.ContainerKey, results[j].Key)
|
||||||
|
di = append(di, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return di, nil
|
||||||
|
}
|
116
src/go/pt-mongodb-index-check/indexes/duplicated_test.go
Normal file
116
src/go/pt-mongodb-index-check/indexes/duplicated_test.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package indexes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlekSi/pointer"
|
||||||
|
tu "github.com/percona/percona-toolkit/src/go/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
"gopkg.in/mgo.v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDuplicateIndexes(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := tu.TestClient(ctx, tu.MongoDBShard1PrimaryPort)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot get a new MongoDB client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbname := "test_db"
|
||||||
|
collname := "test_col"
|
||||||
|
|
||||||
|
database := client.Database(dbname)
|
||||||
|
database.Drop(ctx) //nolint:errcheck
|
||||||
|
defer database.Drop(ctx) //nolint:errcheck
|
||||||
|
|
||||||
|
_, err = database.Collection(collname).InsertOne(ctx, bson.M{"f1": 1, "f2": "2", "f3": "a", "f4": "c"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
testCases := []primitive.D{
|
||||||
|
{{"f1", 1}, {"f2", -1}, {"f3", 1}, {"f4", 1}},
|
||||||
|
{{"f1", 1}, {"f2", -1}, {"f3", 1}, {"f4", 1}}, // this will throw a duplicate index error
|
||||||
|
{{"f1", 1}, {"f2", -1}, {"f3", 1}},
|
||||||
|
{{"f1", 1}, {"f2", -1}},
|
||||||
|
{{"f3", -1}},
|
||||||
|
}
|
||||||
|
|
||||||
|
errCount := 0
|
||||||
|
for i, tc := range testCases {
|
||||||
|
mod := mongo.IndexModel{
|
||||||
|
Keys: tc,
|
||||||
|
Options: &options.IndexOptions{
|
||||||
|
Name: pointer.ToString(fmt.Sprintf("idx_%02d", i)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := database.Collection(collname).Indexes().CreateOne(ctx, mod)
|
||||||
|
if err != nil {
|
||||||
|
errCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
At this point we have 5 indexes: _id: (MongoDB's default), idx_00, idx_02, idx_03, idx_04.
|
||||||
|
idx_01 wasn't created since it duplicates idx_00 and errCount=1.
|
||||||
|
*/
|
||||||
|
|
||||||
|
assert.Equal(t, 1, errCount)
|
||||||
|
|
||||||
|
want := []Duplicate{
|
||||||
|
{
|
||||||
|
Name: "idx_03",
|
||||||
|
Namespace: "test_db.test_col",
|
||||||
|
Key: IndexKey{
|
||||||
|
{Key: "f1", Value: int32(1)},
|
||||||
|
{Key: "f2", Value: int32(-1)},
|
||||||
|
},
|
||||||
|
ContainerName: "idx_02",
|
||||||
|
ContainerKey: IndexKey{
|
||||||
|
{Key: "f1", Value: int32(1)},
|
||||||
|
{Key: "f2", Value: int32(-1)},
|
||||||
|
{Key: "f3", Value: int32(1)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "idx_03",
|
||||||
|
Namespace: "test_db.test_col",
|
||||||
|
Key: IndexKey{
|
||||||
|
{Key: "f1", Value: int32(1)},
|
||||||
|
{Key: "f2", Value: int32(-1)},
|
||||||
|
},
|
||||||
|
ContainerName: "idx_00",
|
||||||
|
ContainerKey: IndexKey{
|
||||||
|
{Key: "f1", Value: int32(1)},
|
||||||
|
{Key: "f2", Value: int32(-1)},
|
||||||
|
{Key: "f3", Value: int32(1)},
|
||||||
|
{Key: "f4", Value: int32(1)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "idx_02",
|
||||||
|
Namespace: "test_db.test_col",
|
||||||
|
Key: IndexKey{
|
||||||
|
{Key: "f1", Value: int32(1)},
|
||||||
|
{Key: "f2", Value: int32(-1)},
|
||||||
|
{Key: "f3", Value: int32(1)},
|
||||||
|
},
|
||||||
|
ContainerName: "idx_00",
|
||||||
|
ContainerKey: IndexKey{
|
||||||
|
{Key: "f1", Value: int32(1)},
|
||||||
|
{Key: "f2", Value: int32(-1)},
|
||||||
|
{Key: "f3", Value: int32(1)},
|
||||||
|
{Key: "f4", Value: int32(1)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
di, err := FindDuplicated(ctx, client, dbname, collname)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, want, di)
|
||||||
|
}
|
62
src/go/pt-mongodb-index-check/indexes/unused.go
Normal file
62
src/go/pt-mongodb-index-check/indexes/unused.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package indexes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"gopkg.in/mgo.v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
var systemDBs = []string{"admin", "config", "local", "system.profile"} //nolint:gochecknoglobals
|
||||||
|
// IndexStat hold an index usage statistics.
|
||||||
|
type IndexStat struct {
|
||||||
|
Accesses struct {
|
||||||
|
Ops int64 `bson:"ops"`
|
||||||
|
Since primitive.DateTime `bson:"since"`
|
||||||
|
} `bson:"accesses"`
|
||||||
|
Spec struct {
|
||||||
|
Name string `bson:"name"`
|
||||||
|
Namespace string `bson:"ns"`
|
||||||
|
V int32 `bson:"v"`
|
||||||
|
Key primitive.D `bson:"key"`
|
||||||
|
} `bson:"spec"`
|
||||||
|
Name string `bson:"name"`
|
||||||
|
Key primitive.D `bson:"key"`
|
||||||
|
Host string `bson:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func in(search string, items []string) bool {
|
||||||
|
for _, item := range items {
|
||||||
|
if search == item {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindUnusedIndexes returns a list of unused indexes for the given database and collection.
|
||||||
|
func FindUnused(ctx context.Context, client *mongo.Client, database, collection string) ([]IndexStat, error) {
|
||||||
|
aggregation := mongo.Pipeline{
|
||||||
|
{{Key: "$indexStats", Value: primitive.M{}}},
|
||||||
|
{{Key: "$match", Value: primitive.M{"accesses.ops": 0}}},
|
||||||
|
{{Key: "$match", Value: primitive.M{"name": bson.M{"$ne": "_id_"}}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
if in(database, systemDBs) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor, err := client.Database(database).Collection(collection).Aggregate(ctx, aggregation)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot run $indexStats for unused indexes")
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats []IndexStat
|
||||||
|
if err = cursor.All(ctx, &stats); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot get $indexStats for unused indexes")
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
81
src/go/pt-mongodb-index-check/indexes/unused_test.go
Normal file
81
src/go/pt-mongodb-index-check/indexes/unused_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package indexes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlekSi/pointer"
|
||||||
|
tu "github.com/percona/percona-toolkit/src/go/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
"gopkg.in/mgo.v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnusedIndexes(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := tu.TestClient(ctx, tu.MongoDBShard1PrimaryPort)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot get a new MongoDB client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbname := "test_db"
|
||||||
|
collname := "test_col"
|
||||||
|
|
||||||
|
database := client.Database(dbname)
|
||||||
|
database.Drop(ctx) //nolint:errcheck
|
||||||
|
defer database.Drop(ctx) //nolint:errcheck
|
||||||
|
|
||||||
|
testCases := []primitive.D{
|
||||||
|
{{"f1", 1}, {"f2", -1}, {"f3", 1}, {"f4", 1}},
|
||||||
|
{{"f3", -1}},
|
||||||
|
{{"f4", -1}},
|
||||||
|
}
|
||||||
|
|
||||||
|
errCount := 0
|
||||||
|
for i, tc := range testCases {
|
||||||
|
mod := mongo.IndexModel{
|
||||||
|
Keys: tc,
|
||||||
|
Options: &options.IndexOptions{
|
||||||
|
Name: pointer.ToString(fmt.Sprintf("idx_%02d", i)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := database.Collection(collname).Indexes().CreateOne(ctx, mod)
|
||||||
|
if err != nil {
|
||||||
|
errCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
_, err = database.Collection(collname).InsertOne(ctx,
|
||||||
|
bson.M{"f1": rand.Int63n(1000), "f2": rand.Int63n(1000), "f3": rand.Int63n(1000), "f4": rand.Int63n(1000)})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make use of idx_02: {"f4": -1} to exclude it from the results so we ensure only unused indexes
|
||||||
|
// are being listed.
|
||||||
|
_, err = database.Collection(collname).Find(ctx, primitive.M{"f4": primitive.M{"$gt": 500}})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
want := []string{"idx_00", "idx_01"}
|
||||||
|
|
||||||
|
ui, err := FindUnused(ctx, client, dbname, collname)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
got := make([]string, 0, len(ui))
|
||||||
|
for _, idx := range ui {
|
||||||
|
// compare only names because the index struct has a timestamp in it and it is variable.
|
||||||
|
got = append(got, idx.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(got)
|
||||||
|
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
189
src/go/pt-mongodb-index-check/main.go
Normal file
189
src/go/pt-mongodb-index-check/main.go
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
|
||||||
|
"github.com/percona/percona-toolkit/src/go/pt-mongodb-index-check/indexes"
|
||||||
|
"github.com/percona/percona-toolkit/src/go/pt-mongodb-index-check/templates"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cmdlineArgs struct {
|
||||||
|
CheckUnused struct{} `cmd:"" name:"check-unused" help:"Check for unused indexes."`
|
||||||
|
CheckDuplicated struct{} `cmd:"" name:"check-duplicates" help:"Check for duplicated indexes."`
|
||||||
|
CheckAll struct{} `cmd:"" name:"check-all" help:"Check for unused and duplicated indexes."`
|
||||||
|
ShowHelp struct{} `cmd:"" default:"1"`
|
||||||
|
Version struct{} `cmd:"" name:"version"`
|
||||||
|
|
||||||
|
AllDatabases bool `name:"all-databases" xor:"db" help:"Check in all databases excluding system dbs"`
|
||||||
|
Databases []string `name:"databases" xor:"db" help:"Comma separated list of databases to check"`
|
||||||
|
|
||||||
|
AllCollections bool `name:"all-collections" xor:"colls" help:"Check in all collections in the selected databases."`
|
||||||
|
Collections []string `name:"collections" xor:"colls" help:"Comma separated list of collections to check"`
|
||||||
|
URI string `name:"mongodb.uri" required:"" placeholder:"mongodb://host:port/admindb?options" help:"Connection URI"`
|
||||||
|
JSON bool `name:"json" help:"Show output as JSON"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
Unused []indexes.IndexStat
|
||||||
|
Duplicated []indexes.Duplicate
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TOOLNAME = "pt-mongodb-index-check"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Build string = "2020-04-23" //nolint
|
||||||
|
GoVersion string = "1.14.1" //nolint
|
||||||
|
Version string = "3.4.0" //nolint
|
||||||
|
Commit string //nolint
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args cmdlineArgs
|
||||||
|
kongctx := kong.Parse(&args, kong.UsageOnError())
|
||||||
|
|
||||||
|
if kongctx.Command() == "version" {
|
||||||
|
fmt.Println(TOOLNAME)
|
||||||
|
fmt.Printf("Version %s\n", Version)
|
||||||
|
fmt.Printf("Build: %s using %s\n", Build, GoVersion)
|
||||||
|
fmt.Printf("Commit: %s\n", Commit)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if !strings.HasPrefix(args.URI, "mongodb") && !strings.HasPrefix(args.URI, "mongodb+srv") {
|
||||||
|
args.URI = "mongodb://" + args.URI
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := mongo.Connect(ctx, options.Client().ApplyURI(args.URI))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Cannot connect to the database: %q", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.AllDatabases {
|
||||||
|
args.Databases, err = client.ListDatabaseNames(context.TODO(), primitive.D{})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("cannot list all databases: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if args.AllCollections {
|
||||||
|
args.Collections = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := response{}
|
||||||
|
|
||||||
|
switch kongctx.Command() {
|
||||||
|
case "check-unused":
|
||||||
|
resp.Unused = findUnused(ctx, client, args.Databases, args.Collections)
|
||||||
|
case "check-duplicates":
|
||||||
|
resp.Duplicated = findDuplicated(ctx, client, args.Databases, args.Collections)
|
||||||
|
case "check-all":
|
||||||
|
resp.Unused = findUnused(ctx, client, args.Databases, args.Collections)
|
||||||
|
resp.Duplicated = findDuplicated(ctx, client, args.Databases, args.Collections)
|
||||||
|
default:
|
||||||
|
kong.DefaultHelpPrinter(kong.HelpOptions{}, kongctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(output(resp, args.JSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
func output(resp response, asJson bool) string {
|
||||||
|
if asJson {
|
||||||
|
jsonStr, err := json.MarshalIndent(resp, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("cannot encode the response as json")
|
||||||
|
}
|
||||||
|
return string(jsonStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
t := template.Must(template.New("duplicated").Parse(templates.Duplicated))
|
||||||
|
if err := t.Execute(buf, resp.Duplicated); err != nil {
|
||||||
|
log.Fatal(errors.Wrap(err, "cannot parse clusterwide section of the output template"))
|
||||||
|
}
|
||||||
|
|
||||||
|
t = template.Must(template.New("unused").Parse(templates.Unused))
|
||||||
|
if err := t.Execute(buf, resp.Unused); err != nil {
|
||||||
|
log.Fatal(errors.Wrap(err, "cannot parse clusterwide section of the output template"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func findUnused(ctx context.Context, client *mongo.Client, databases []string, collections []string) []indexes.IndexStat {
|
||||||
|
unused := []indexes.IndexStat{}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
colls := make([]string, len(collections))
|
||||||
|
copy(colls, collections)
|
||||||
|
|
||||||
|
for _, database := range databases {
|
||||||
|
if len(collections) == 0 {
|
||||||
|
colls, err = client.Database(database).ListCollectionNames(ctx, primitive.D{})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("cannot get the list of collections for the database %s", database)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, collection := range colls {
|
||||||
|
idx, err := indexes.FindUnused(ctx, client, database, collection)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while checking unused indexes in %s.%s: %s", database, collection, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
unused = append(unused, idx...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unused
|
||||||
|
}
|
||||||
|
|
||||||
|
func findDuplicated(ctx context.Context, client *mongo.Client, databases []string, collections []string) []indexes.Duplicate {
|
||||||
|
duplicated := []indexes.Duplicate{}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
colls := make([]string, len(collections))
|
||||||
|
copy(colls, collections)
|
||||||
|
|
||||||
|
for _, database := range databases {
|
||||||
|
if len(collections) == 0 {
|
||||||
|
colls, err = client.Database(database).ListCollectionNames(ctx, primitive.D{})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("cannot get the list of collections for the database %s", database)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, collection := range colls {
|
||||||
|
dups, err := indexes.FindDuplicated(ctx, client, database, collection)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while checking duplicated indexes in %s.%s: %s", database, collection, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicated = append(duplicated, dups...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return duplicated
|
||||||
|
}
|
10
src/go/pt-mongodb-index-check/templates/duplicated.go
Normal file
10
src/go/pt-mongodb-index-check/templates/duplicated.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
// {{if $i}},{{end}} adds a comma after the first element.
|
||||||
|
// When $i == 0 (first element) {{ if $i }} returns false (0)
|
||||||
|
|
||||||
|
var Duplicated = `
|
||||||
|
Duplicated indexes
|
||||||
|
{{ range . }}
|
||||||
|
{{ .Namespace }}, index '{{ .Name }}', with fields { {{- range $i, $val := .Key }}{{if $i}}, {{end}}{{ $val.Key }}:{{ $val.Value }}{{ end -}} } is the prefix of '{{ .ContainerName }}' with fields { {{- range $i, $val := .ContainerKey }}{{if $i}}, {{end}}{{ $val.Key }}:{{ $val.Value }}{{ end -}} }{{ end}}
|
||||||
|
`
|
10
src/go/pt-mongodb-index-check/templates/unused.go
Normal file
10
src/go/pt-mongodb-index-check/templates/unused.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
// {{if $i}},{{end}} adds a comma after the first element.
|
||||||
|
// When $i == 0 (first element) {{ if $i }} returns false (0)
|
||||||
|
|
||||||
|
var Unused = `
|
||||||
|
Unused indexes since last restart
|
||||||
|
{{ range . }}
|
||||||
|
{{ .Spec.Namespace }}, index '{{ .Name }}' with fields { {{- range $i, $val := .Key }}{{if $i}}, {{end}}{{ $val.Key }}:{{ $val.Value }} }{{ end }}{{ end}}
|
||||||
|
`
|
Reference in New Issue
Block a user