mirror of
https://github.com/minio/minio.git
synced 2025-04-08 21:55:44 -04:00
Add basic bandwidth monitoring for replication. (#10501)
This change tracks bandwidth for a bucket and object - [x] Add Admin API - [x] Add Peer API - [x] Add BW throttling - [x] Admin APIs to set replication limit - [x] Admin APIs for fetch bandwidth
This commit is contained in:
parent
071c004f8b
commit
c2f16ee846
@ -35,17 +35,19 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/config"
|
"github.com/minio/minio/cmd/config"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/cmd/logger/message/log"
|
"github.com/minio/minio/cmd/logger/message/log"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
|
bandwidth "github.com/minio/minio/pkg/bandwidth"
|
||||||
|
bucketBandwidth "github.com/minio/minio/pkg/bucket/bandwidth"
|
||||||
"github.com/minio/minio/pkg/handlers"
|
"github.com/minio/minio/pkg/handlers"
|
||||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
xnet "github.com/minio/minio/pkg/net"
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
"github.com/minio/minio/pkg/sync/errgroup"
|
||||||
trace "github.com/minio/minio/pkg/trace"
|
trace "github.com/minio/minio/pkg/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1425,6 +1427,66 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BandwidthMonitorHandler - GET /minio/admin/v3/bandwidth
|
||||||
|
// ----------
|
||||||
|
// Get bandwidth consumption information
|
||||||
|
func (a adminAPIHandlers) BandwidthMonitorHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "BandwidthMonitor")
|
||||||
|
|
||||||
|
// Validate request signature.
|
||||||
|
_, adminAPIErr := checkAdminRequestAuthType(ctx, r, iampolicy.BandwidthMonitorAction, "")
|
||||||
|
if adminAPIErr != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setEventStreamHeaders(w)
|
||||||
|
peers := newPeerRestClients(globalEndpoints)
|
||||||
|
bucketsRequestedString := r.URL.Query().Get("buckets")
|
||||||
|
var bucketsRequested []string
|
||||||
|
reports := make([]*bandwidth.Report, len(peers))
|
||||||
|
selectBuckets := bucketBandwidth.SelectAllBuckets()
|
||||||
|
if bucketsRequestedString != "" {
|
||||||
|
bucketsRequested = strings.Split(bucketsRequestedString, ",")
|
||||||
|
selectBuckets = bucketBandwidth.SelectBuckets(bucketsRequested...)
|
||||||
|
}
|
||||||
|
reports = append(reports, globalBucketMonitor.GetReport(selectBuckets))
|
||||||
|
g := errgroup.WithNErrs(len(peers))
|
||||||
|
for index, peer := range peers {
|
||||||
|
if peer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
index := index
|
||||||
|
g.Go(func() error {
|
||||||
|
var err error
|
||||||
|
reports[index], err = peer.MonitorBandwidth(ctx, bucketsRequested)
|
||||||
|
return err
|
||||||
|
}, index)
|
||||||
|
}
|
||||||
|
consolidatedReport := bandwidth.Report{
|
||||||
|
BucketStats: make(map[string]bandwidth.Details),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, report := range reports {
|
||||||
|
for bucket := range report.BucketStats {
|
||||||
|
d, ok := consolidatedReport.BucketStats[bucket]
|
||||||
|
if !ok {
|
||||||
|
consolidatedReport.BucketStats[bucket] = bandwidth.Details{}
|
||||||
|
d = consolidatedReport.BucketStats[bucket]
|
||||||
|
d.LimitInBytesPerSecond = report.BucketStats[bucket].LimitInBytesPerSecond
|
||||||
|
}
|
||||||
|
d.CurrentBandwidthInBytesPerSecond += report.BucketStats[bucket].CurrentBandwidthInBytesPerSecond
|
||||||
|
consolidatedReport.BucketStats[bucket] = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
err := enc.Encode(consolidatedReport)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
|
||||||
|
}
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
}
|
||||||
|
|
||||||
// ServerInfoHandler - GET /minio/admin/v3/info
|
// ServerInfoHandler - GET /minio/admin/v3/info
|
||||||
// ----------
|
// ----------
|
||||||
// Get server information
|
// Get server information
|
||||||
|
@ -214,6 +214,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
|
|||||||
// -- OBD API --
|
// -- OBD API --
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/obdinfo").
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/obdinfo").
|
||||||
HandlerFunc(httpTraceHdrs(adminAPI.OBDInfoHandler))
|
HandlerFunc(httpTraceHdrs(adminAPI.OBDInfoHandler))
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/bandwidth").
|
||||||
|
HandlerFunc(httpTraceHdrs(adminAPI.BandwidthMonitorHandler))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,6 +373,20 @@ func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.Buc
|
|||||||
return meta.bucketTargetConfig, nil
|
return meta.bucketTargetConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBucketTarget returns the target for the bucket and arn.
|
||||||
|
func (sys *BucketMetadataSys) GetBucketTarget(bucket string, arn string) (madmin.BucketTarget, error) {
|
||||||
|
targets, err := sys.GetBucketTargetsConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
return madmin.BucketTarget{}, err
|
||||||
|
}
|
||||||
|
for _, t := range targets.Targets {
|
||||||
|
if t.Arn == arn {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return madmin.BucketTarget{}, errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
// GetConfig returns a specific configuration from the bucket metadata.
|
// GetConfig returns a specific configuration from the bucket metadata.
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetConfig(bucket string) (BucketMetadata, error) {
|
func (sys *BucketMetadataSys) GetConfig(bucket string) (BucketMetadata, error) {
|
||||||
|
@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/bucket/bandwidth"
|
||||||
"github.com/minio/minio/pkg/bucket/replication"
|
"github.com/minio/minio/pkg/bucket/replication"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||||
@ -119,7 +120,7 @@ func mustReplicater(ctx context.Context, r *http.Request, bucket, object string,
|
|||||||
return cfg.Replicate(opts)
|
return cfg.Replicate(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func putReplicationOpts(dest replication.Destination, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions) {
|
func putReplicationOpts(ctx context.Context, dest replication.Destination, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions) {
|
||||||
meta := make(map[string]string)
|
meta := make(map[string]string)
|
||||||
for k, v := range objInfo.UserDefined {
|
for k, v := range objInfo.UserDefined {
|
||||||
if k == xhttp.AmzBucketReplicationStatus {
|
if k == xhttp.AmzBucketReplicationStatus {
|
||||||
@ -168,6 +169,7 @@ func putReplicationOpts(dest replication.Destination, objInfo ObjectInfo) (putOp
|
|||||||
if crypto.S3.IsEncrypted(objInfo.UserDefined) {
|
if crypto.S3.IsEncrypted(objInfo.UserDefined) {
|
||||||
putOpts.ServerSideEncryption = encrypt.NewSSE()
|
putOpts.ServerSideEncryption = encrypt.NewSSE()
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,16 +186,15 @@ func replicateObject(ctx context.Context, objInfo ObjectInfo, objectAPI ObjectLa
|
|||||||
}
|
}
|
||||||
tgt := globalBucketTargetSys.GetRemoteTargetClient(ctx, cfg.RoleArn)
|
tgt := globalBucketTargetSys.GetRemoteTargetClient(ctx, cfg.RoleArn)
|
||||||
if tgt == nil {
|
if tgt == nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("failed to get target for bucket:%s arn:%s", bucket, cfg.RoleArn))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, readLock, ObjectOptions{
|
gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, readLock, ObjectOptions{
|
||||||
VersionID: objInfo.VersionID,
|
VersionID: objInfo.VersionID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
objInfo = gr.ObjInfo
|
objInfo = gr.ObjInfo
|
||||||
size, err := objInfo.GetActualSize()
|
size, err := objInfo.GetActualSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -224,11 +225,26 @@ func replicateObject(ctx context.Context, objInfo ObjectInfo, objectAPI ObjectLa
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
putOpts := putReplicationOpts(dest, objInfo)
|
|
||||||
|
|
||||||
|
target, err := globalBucketMetadataSys.GetBucketTarget(bucket, cfg.RoleArn)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("failed to get target for replication bucket:%s cfg:%s err:%s", bucket, cfg.RoleArn, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
putOpts := putReplicationOpts(ctx, dest, objInfo)
|
||||||
replicationStatus := replication.Complete
|
replicationStatus := replication.Complete
|
||||||
_, err = tgt.PutObject(ctx, dest.Bucket, object, gr, size, "", "", putOpts)
|
|
||||||
gr.Close()
|
// Setup bandwidth throttling
|
||||||
|
peerCount := len(globalEndpoints)
|
||||||
|
b := target.BandwidthLimit / int64(peerCount)
|
||||||
|
var headerSize int
|
||||||
|
for k, v := range putOpts.Header() {
|
||||||
|
headerSize += len(k) + len(v)
|
||||||
|
}
|
||||||
|
r := bandwidth.NewMonitoredReader(ctx, globalBucketMonitor, objInfo.Bucket, objInfo.Name, gr, headerSize, b)
|
||||||
|
|
||||||
|
_, err = tgt.PutObject(ctx, dest.Bucket, object, r, size, "", "", putOpts)
|
||||||
|
r.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
replicationStatus = replication.Failed
|
replicationStatus = replication.Failed
|
||||||
}
|
}
|
||||||
|
@ -207,14 +207,14 @@ func (sys *BucketTargetSys) Init(ctx context.Context, buckets []BucketInfo, objA
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTarget updates target to reflect metadata updates
|
// UpdateAllTargets updates target to reflect metadata updates
|
||||||
func (sys *BucketTargetSys) UpdateTarget(bucket string, cfg *madmin.BucketTargets) {
|
func (sys *BucketTargetSys) UpdateAllTargets(bucket string, tgts *madmin.BucketTargets) {
|
||||||
if sys == nil {
|
if sys == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sys.Lock()
|
sys.Lock()
|
||||||
defer sys.Unlock()
|
defer sys.Unlock()
|
||||||
if cfg == nil || cfg.Empty() {
|
if tgts == nil || tgts.Empty() {
|
||||||
// remove target and arn association
|
// remove target and arn association
|
||||||
if tgts, ok := sys.targetsMap[bucket]; ok {
|
if tgts, ok := sys.targetsMap[bucket]; ok {
|
||||||
for _, t := range tgts {
|
for _, t := range tgts {
|
||||||
@ -225,10 +225,10 @@ func (sys *BucketTargetSys) UpdateTarget(bucket string, cfg *madmin.BucketTarget
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.Targets) > 0 {
|
if len(tgts.Targets) > 0 {
|
||||||
sys.targetsMap[bucket] = cfg.Targets
|
sys.targetsMap[bucket] = tgts.Targets
|
||||||
}
|
}
|
||||||
for _, tgt := range cfg.Targets {
|
for _, tgt := range tgts.Targets {
|
||||||
tgtClient, err := sys.getRemoteTargetClient(&tgt)
|
tgtClient, err := sys.getRemoteTargetClient(&tgt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
@ -238,7 +238,7 @@ func (sys *BucketTargetSys) UpdateTarget(bucket string, cfg *madmin.BucketTarget
|
|||||||
sys.clientsCache[tgtClient.EndpointURL().String()] = tgtClient
|
sys.clientsCache[tgtClient.EndpointURL().String()] = tgtClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sys.targetsMap[bucket] = cfg.Targets
|
sys.targetsMap[bucket] = tgts.Targets
|
||||||
}
|
}
|
||||||
|
|
||||||
// create minio-go clients for buckets having remote targets
|
// create minio-go clients for buckets having remote targets
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7/pkg/set"
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
|
"github.com/minio/minio/pkg/bucket/bandwidth"
|
||||||
|
|
||||||
humanize "github.com/dustin/go-humanize"
|
humanize "github.com/dustin/go-humanize"
|
||||||
"github.com/minio/minio/cmd/config/cache"
|
"github.com/minio/minio/cmd/config/cache"
|
||||||
@ -149,6 +150,7 @@ var (
|
|||||||
globalEnvTargetList *event.TargetList
|
globalEnvTargetList *event.TargetList
|
||||||
|
|
||||||
globalBucketMetadataSys *BucketMetadataSys
|
globalBucketMetadataSys *BucketMetadataSys
|
||||||
|
globalBucketMonitor *bandwidth.Monitor
|
||||||
globalPolicySys *PolicySys
|
globalPolicySys *PolicySys
|
||||||
globalIAMSys *IAMSys
|
globalIAMSys *IAMSys
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@ -35,6 +36,7 @@ import (
|
|||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/cmd/rest"
|
"github.com/minio/minio/cmd/rest"
|
||||||
|
"github.com/minio/minio/pkg/bandwidth"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
xnet "github.com/minio/minio/pkg/net"
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
@ -884,3 +886,20 @@ func newPeerRESTClient(peer *xnet.Host) *peerRESTClient {
|
|||||||
|
|
||||||
return &peerRESTClient{host: peer, restClient: restClient}
|
return &peerRESTClient{host: peer, restClient: restClient}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MonitorBandwidth - send http trace request to peer nodes
|
||||||
|
func (client *peerRESTClient) MonitorBandwidth(ctx context.Context, buckets []string) (*bandwidth.Report, error) {
|
||||||
|
values := make(url.Values)
|
||||||
|
values.Set(peerRESTBuckets, strings.Join(buckets, ","))
|
||||||
|
|
||||||
|
respBody, err := client.callWithContext(ctx, peerRESTMethodGetBandwidth, values, nil, -1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer http.DrainBody(respBody)
|
||||||
|
|
||||||
|
dec := gob.NewDecoder(respBody)
|
||||||
|
var bandwidthReport bandwidth.Report
|
||||||
|
err = dec.Decode(&bandwidthReport)
|
||||||
|
return &bandwidthReport, err
|
||||||
|
}
|
||||||
|
@ -57,10 +57,12 @@ const (
|
|||||||
peerRESTMethodListen = "/listen"
|
peerRESTMethodListen = "/listen"
|
||||||
peerRESTMethodLog = "/log"
|
peerRESTMethodLog = "/log"
|
||||||
peerRESTMethodGetLocalDiskIDs = "/getlocaldiskids"
|
peerRESTMethodGetLocalDiskIDs = "/getlocaldiskids"
|
||||||
|
peerRESTMethodGetBandwidth = "/bandwidth"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
peerRESTBucket = "bucket"
|
peerRESTBucket = "bucket"
|
||||||
|
peerRESTBuckets = "buckets"
|
||||||
peerRESTUser = "user"
|
peerRESTUser = "user"
|
||||||
peerRESTGroup = "group"
|
peerRESTGroup = "group"
|
||||||
peerRESTUserTemp = "user-temp"
|
peerRESTUserTemp = "user-temp"
|
||||||
|
@ -30,6 +30,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/bandwidth"
|
||||||
|
b "github.com/minio/minio/pkg/bucket/bandwidth"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
trace "github.com/minio/minio/pkg/trace"
|
trace "github.com/minio/minio/pkg/trace"
|
||||||
@ -628,7 +630,7 @@ func (s *peerRESTServer) LoadBucketMetadataHandler(w http.ResponseWriter, r *htt
|
|||||||
}
|
}
|
||||||
|
|
||||||
if meta.bucketTargetConfig != nil {
|
if meta.bucketTargetConfig != nil {
|
||||||
globalBucketTargetSys.UpdateTarget(bucketName, meta.bucketTargetConfig)
|
globalBucketTargetSys.UpdateAllTargets(bucketName, meta.bucketTargetConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1047,6 +1049,34 @@ func (s *peerRESTServer) IsValid(w http.ResponseWriter, r *http.Request) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBandwidth gets the bandwidth for the buckets requested.
|
||||||
|
func (s *peerRESTServer) GetBandwidth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !s.IsValid(w, r) {
|
||||||
|
s.writeErrorResponse(w, errors.New("Invalid request"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bucketsString := r.URL.Query().Get("buckets")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
defer close(doneCh)
|
||||||
|
|
||||||
|
var report *bandwidth.Report
|
||||||
|
selectBuckets := b.SelectAllBuckets()
|
||||||
|
if bucketsString != "" {
|
||||||
|
selectBuckets = b.SelectBuckets(strings.Split(bucketsString, ",")...)
|
||||||
|
}
|
||||||
|
report = globalBucketMonitor.GetReport(selectBuckets)
|
||||||
|
|
||||||
|
enc := gob.NewEncoder(w)
|
||||||
|
if err := enc.Encode(report); err != nil {
|
||||||
|
s.writeErrorResponse(w, errors.New("Encoding report failed: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
}
|
||||||
|
|
||||||
// registerPeerRESTHandlers - register peer rest router.
|
// registerPeerRESTHandlers - register peer rest router.
|
||||||
func registerPeerRESTHandlers(router *mux.Router) {
|
func registerPeerRESTHandlers(router *mux.Router) {
|
||||||
server := &peerRESTServer{}
|
server := &peerRESTServer{}
|
||||||
@ -1085,4 +1115,5 @@ func registerPeerRESTHandlers(router *mux.Router) {
|
|||||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodBackgroundHealStatus).HandlerFunc(server.BackgroundHealStatusHandler)
|
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodBackgroundHealStatus).HandlerFunc(server.BackgroundHealStatusHandler)
|
||||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLog).HandlerFunc(server.ConsoleLogHandler)
|
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLog).HandlerFunc(server.ConsoleLogHandler)
|
||||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodGetLocalDiskIDs).HandlerFunc(httpTraceHdrs(server.GetLocalDiskIDs))
|
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodGetLocalDiskIDs).HandlerFunc(httpTraceHdrs(server.GetLocalDiskIDs))
|
||||||
|
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodGetBandwidth).HandlerFunc(httpTraceHdrs(server.GetBandwidth))
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ import (
|
|||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
|
"github.com/minio/minio/pkg/bucket/bandwidth"
|
||||||
"github.com/minio/minio/pkg/certs"
|
"github.com/minio/minio/pkg/certs"
|
||||||
"github.com/minio/minio/pkg/color"
|
"github.com/minio/minio/pkg/color"
|
||||||
"github.com/minio/minio/pkg/env"
|
"github.com/minio/minio/pkg/env"
|
||||||
@ -158,6 +159,9 @@ func newAllSubsystems() {
|
|||||||
// Create new bucket metadata system.
|
// Create new bucket metadata system.
|
||||||
globalBucketMetadataSys = NewBucketMetadataSys()
|
globalBucketMetadataSys = NewBucketMetadataSys()
|
||||||
|
|
||||||
|
// Create the bucket bandwidth monitor
|
||||||
|
globalBucketMonitor = bandwidth.NewMonitor(GlobalServiceDoneCh)
|
||||||
|
|
||||||
// Create a new config system.
|
// Create a new config system.
|
||||||
globalConfigSys = NewConfigSys()
|
globalConfigSys = NewConfigSys()
|
||||||
|
|
||||||
|
28
pkg/bandwidth/bandwidth.go
Normal file
28
pkg/bandwidth/bandwidth.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bandwidth
|
||||||
|
|
||||||
|
// Details for the measured bandwidth
|
||||||
|
type Details struct {
|
||||||
|
LimitInBytesPerSecond int64 `json:"limitInBits"`
|
||||||
|
CurrentBandwidthInBytesPerSecond float64 `json:"currentBandwidth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report captures the details for all buckets.
|
||||||
|
type Report struct {
|
||||||
|
BucketStats map[string]Details `json:"bucketStats,omitempty"`
|
||||||
|
}
|
87
pkg/bucket/bandwidth/measurement.go
Normal file
87
pkg/bucket/bandwidth/measurement.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bandwidth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// betaBucket is the weight used to calculate exponential moving average
|
||||||
|
betaBucket = 0.1 // Number of averages considered = 1/(1-betaObject)
|
||||||
|
)
|
||||||
|
|
||||||
|
// bucketMeasurement captures the bandwidth details for one bucket
|
||||||
|
type bucketMeasurement struct {
|
||||||
|
lock sync.Mutex
|
||||||
|
bytesSinceLastWindow uint64 // Total bytes since last window was processed
|
||||||
|
startTime time.Time // Start time for window
|
||||||
|
expMovingAvg float64 // Previously calculate sliding window
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBucketMeasurement creates a new instance of the measurement with the initial start time.
|
||||||
|
func newBucketMeasurement(initTime time.Time) *bucketMeasurement {
|
||||||
|
return &bucketMeasurement{
|
||||||
|
startTime: initTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// incrementBytes add bytes reported for a bucket.
|
||||||
|
func (m *bucketMeasurement) incrementBytes(bytes uint64) {
|
||||||
|
atomic.AddUint64(&m.bytesSinceLastWindow, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateExponentialMovingAverage processes the measurements captured so far.
|
||||||
|
func (m *bucketMeasurement) updateExponentialMovingAverage(endTime time.Time) {
|
||||||
|
// Calculate aggregate avg bandwidth and exp window avg
|
||||||
|
m.lock.Lock()
|
||||||
|
defer func() {
|
||||||
|
m.startTime = endTime
|
||||||
|
m.lock.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if endTime.Before(m.startTime) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := endTime.Sub(m.startTime)
|
||||||
|
|
||||||
|
bytesSinceLastWindow := atomic.SwapUint64(&m.bytesSinceLastWindow, 0)
|
||||||
|
|
||||||
|
if m.expMovingAvg == 0 {
|
||||||
|
// Should address initial calculation and should be fine for resuming from 0
|
||||||
|
m.expMovingAvg = float64(bytesSinceLastWindow) / duration.Seconds()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
increment := float64(bytesSinceLastWindow) / duration.Seconds()
|
||||||
|
m.expMovingAvg = exponentialMovingAverage(betaBucket, m.expMovingAvg, increment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// exponentialMovingAverage calculates the exponential moving average
|
||||||
|
func exponentialMovingAverage(beta, previousAvg, incrementAvg float64) float64 {
|
||||||
|
return (1-beta)*incrementAvg + beta*previousAvg
|
||||||
|
}
|
||||||
|
|
||||||
|
// getExpMovingAvgBytesPerSecond returns the exponential moving average for the bucket in bytes
|
||||||
|
func (m *bucketMeasurement) getExpMovingAvgBytesPerSecond() float64 {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
return m.expMovingAvg
|
||||||
|
}
|
175
pkg/bucket/bandwidth/monitor.go
Normal file
175
pkg/bucket/bandwidth/monitor.go
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bandwidth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/bandwidth"
|
||||||
|
"github.com/minio/minio/pkg/pubsub"
|
||||||
|
)
|
||||||
|
|
||||||
|
// throttleBandwidth gets the throttle for bucket with the configured value
|
||||||
|
func (m *Monitor) throttleBandwidth(ctx context.Context, bucket string, bandwidthBytesPerSecond int64) *throttle {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
throttle, ok := m.bucketThrottle[bucket]
|
||||||
|
if !ok {
|
||||||
|
throttle = newThrottle(ctx, bandwidthBytesPerSecond)
|
||||||
|
m.bucketThrottle[bucket] = throttle
|
||||||
|
return throttle
|
||||||
|
}
|
||||||
|
throttle.SetBandwidth(bandwidthBytesPerSecond)
|
||||||
|
return throttle
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeToBuckets subscribes to buckets. Empty array for monitoring all buckets.
|
||||||
|
func (m *Monitor) SubscribeToBuckets(subCh chan interface{}, doneCh <-chan struct{}, buckets []string) {
|
||||||
|
m.pubsub.Subscribe(subCh, doneCh, func(f interface{}) bool {
|
||||||
|
if buckets != nil || len(buckets) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
report, ok := f.(*bandwidth.Report)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, b := range buckets {
|
||||||
|
_, ok := report.BucketStats[b]
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor implements the monitoring for bandwidth measurements.
|
||||||
|
type Monitor struct {
|
||||||
|
lock sync.Mutex // lock for all updates
|
||||||
|
|
||||||
|
activeBuckets map[string]*bucketMeasurement // Buckets with objects in flight
|
||||||
|
|
||||||
|
bucketMovingAvgTicker *time.Ticker // Ticker for calculating moving averages
|
||||||
|
|
||||||
|
pubsub *pubsub.PubSub // PubSub for reporting bandwidths.
|
||||||
|
|
||||||
|
bucketThrottle map[string]*throttle
|
||||||
|
|
||||||
|
startProcessing sync.Once
|
||||||
|
|
||||||
|
doneCh <-chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMonitor returns a monitor with defaults.
|
||||||
|
func NewMonitor(doneCh <-chan struct{}) *Monitor {
|
||||||
|
m := &Monitor{
|
||||||
|
activeBuckets: make(map[string]*bucketMeasurement),
|
||||||
|
bucketMovingAvgTicker: time.NewTicker(1 * time.Second),
|
||||||
|
pubsub: pubsub.New(),
|
||||||
|
bucketThrottle: make(map[string]*throttle),
|
||||||
|
doneCh: doneCh,
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectionFunction for buckets
|
||||||
|
type SelectionFunction func(bucket string) bool
|
||||||
|
|
||||||
|
// SelectAllBuckets will select all buckets
|
||||||
|
func SelectAllBuckets() SelectionFunction {
|
||||||
|
return func(bucket string) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectBuckets will select all the buckets passed in.
|
||||||
|
func SelectBuckets(buckets ...string) SelectionFunction {
|
||||||
|
return func(bucket string) bool {
|
||||||
|
for _, b := range buckets {
|
||||||
|
if b != "" && b == bucket {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReport gets the report for all bucket bandwidth details.
|
||||||
|
func (m *Monitor) GetReport(selectBucket SelectionFunction) *bandwidth.Report {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
return m.getReport(selectBucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Monitor) getReport(selectBucket SelectionFunction) *bandwidth.Report {
|
||||||
|
report := &bandwidth.Report{
|
||||||
|
BucketStats: make(map[string]bandwidth.Details),
|
||||||
|
}
|
||||||
|
for bucket, bucketMeasurement := range m.activeBuckets {
|
||||||
|
if !selectBucket(bucket) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
report.BucketStats[bucket] = bandwidth.Details{
|
||||||
|
LimitInBytesPerSecond: m.bucketThrottle[bucket].bytesPerSecond,
|
||||||
|
CurrentBandwidthInBytesPerSecond: bucketMeasurement.getExpMovingAvgBytesPerSecond(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Monitor) process(doneCh <-chan struct{}) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.bucketMovingAvgTicker.C:
|
||||||
|
m.processAvg()
|
||||||
|
case <-doneCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Monitor) getBucketMeasurement(bucket string, initTime time.Time) *bucketMeasurement {
|
||||||
|
bucketTracker, ok := m.activeBuckets[bucket]
|
||||||
|
if !ok {
|
||||||
|
bucketTracker = newBucketMeasurement(initTime)
|
||||||
|
m.activeBuckets[bucket] = bucketTracker
|
||||||
|
}
|
||||||
|
return bucketTracker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Monitor) processAvg() {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
for _, bucketMeasurement := range m.activeBuckets {
|
||||||
|
bucketMeasurement.updateExponentialMovingAverage(time.Now())
|
||||||
|
}
|
||||||
|
m.pubsub.Publish(m.getReport(SelectAllBuckets()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// track returns the measurement object for bucket and object
|
||||||
|
func (m *Monitor) track(bucket string, object string, timeNow time.Time) *bucketMeasurement {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
m.startProcessing.Do(func() {
|
||||||
|
go m.process(m.doneCh)
|
||||||
|
})
|
||||||
|
b := m.getBucketMeasurement(bucket, timeNow)
|
||||||
|
return b
|
||||||
|
}
|
157
pkg/bucket/bandwidth/monitor_test.go
Normal file
157
pkg/bucket/bandwidth/monitor_test.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bandwidth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/bandwidth"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
oneMiB uint64 = 1024 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMonitor_GetThrottle(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
bucketThrottles map[string]*throttle
|
||||||
|
bucket string
|
||||||
|
bpi int64
|
||||||
|
}
|
||||||
|
t1 := newThrottle(context.Background(), 100)
|
||||||
|
t2 := newThrottle(context.Background(), 200)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want *throttle
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Existing",
|
||||||
|
fields: fields{
|
||||||
|
bucketThrottles: map[string]*throttle{"bucket": t1},
|
||||||
|
bucket: "bucket",
|
||||||
|
bpi: 100,
|
||||||
|
},
|
||||||
|
want: t1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new",
|
||||||
|
fields: fields{
|
||||||
|
bucketThrottles: map[string]*throttle{"bucket": t1},
|
||||||
|
bucket: "bucket2",
|
||||||
|
bpi: 200,
|
||||||
|
},
|
||||||
|
want: t2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := &Monitor{
|
||||||
|
bucketThrottle: tt.fields.bucketThrottles,
|
||||||
|
}
|
||||||
|
if got := m.throttleBandwidth(context.Background(), tt.fields.bucket, tt.fields.bpi); got.bytesPerInterval != tt.want.bytesPerInterval {
|
||||||
|
t.Errorf("throttleBandwidth() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonitor_GetReport(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
activeBuckets map[string]*bucketMeasurement
|
||||||
|
endTime time.Time
|
||||||
|
update2 uint64
|
||||||
|
endTime2 time.Time
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
m0 := newBucketMeasurement(start)
|
||||||
|
m0.incrementBytes(0)
|
||||||
|
m1MiBPS := newBucketMeasurement(start)
|
||||||
|
m1MiBPS.incrementBytes(oneMiB)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want *bandwidth.Report
|
||||||
|
want2 *bandwidth.Report
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ZeroToOne",
|
||||||
|
fields: fields{
|
||||||
|
activeBuckets: map[string]*bucketMeasurement{
|
||||||
|
"bucket": m0,
|
||||||
|
},
|
||||||
|
endTime: start.Add(1 * time.Second),
|
||||||
|
update2: oneMiB,
|
||||||
|
endTime2: start.Add(2 * time.Second),
|
||||||
|
},
|
||||||
|
want: &bandwidth.Report{
|
||||||
|
BucketStats: map[string]bandwidth.Details{"bucket": {LimitInBytesPerSecond: 1024 * 1024, CurrentBandwidthInBytesPerSecond: 0}},
|
||||||
|
},
|
||||||
|
want2: &bandwidth.Report{
|
||||||
|
BucketStats: map[string]bandwidth.Details{"bucket": {LimitInBytesPerSecond: 1024 * 1024, CurrentBandwidthInBytesPerSecond: (1024 * 1024) / start.Add(2*time.Second).Sub(start.Add(1*time.Second)).Seconds()}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OneToTwo",
|
||||||
|
fields: fields{
|
||||||
|
activeBuckets: map[string]*bucketMeasurement{
|
||||||
|
"bucket": m1MiBPS,
|
||||||
|
},
|
||||||
|
endTime: start.Add(1 * time.Second),
|
||||||
|
update2: 2 * oneMiB,
|
||||||
|
endTime2: start.Add(2 * time.Second),
|
||||||
|
},
|
||||||
|
want: &bandwidth.Report{
|
||||||
|
BucketStats: map[string]bandwidth.Details{"bucket": {LimitInBytesPerSecond: 1024 * 1024, CurrentBandwidthInBytesPerSecond: float64(oneMiB)}},
|
||||||
|
},
|
||||||
|
want2: &bandwidth.Report{
|
||||||
|
BucketStats: map[string]bandwidth.Details{"bucket": {
|
||||||
|
LimitInBytesPerSecond: 1024 * 1024,
|
||||||
|
CurrentBandwidthInBytesPerSecond: exponentialMovingAverage(betaBucket, float64(oneMiB), 2*float64(oneMiB))}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
thr := throttle{
|
||||||
|
bytesPerSecond: 1024 * 1024,
|
||||||
|
}
|
||||||
|
m := &Monitor{
|
||||||
|
activeBuckets: tt.fields.activeBuckets,
|
||||||
|
bucketThrottle: map[string]*throttle{"bucket": &thr},
|
||||||
|
}
|
||||||
|
m.activeBuckets["bucket"].updateExponentialMovingAverage(tt.fields.endTime)
|
||||||
|
got := m.GetReport(SelectAllBuckets())
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("GetReport() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
m.activeBuckets["bucket"].incrementBytes(tt.fields.update2)
|
||||||
|
m.activeBuckets["bucket"].updateExponentialMovingAverage(tt.fields.endTime2)
|
||||||
|
got = m.GetReport(SelectAllBuckets())
|
||||||
|
if !reflect.DeepEqual(got, tt.want2) {
|
||||||
|
t.Errorf("GetReport() = %v, want %v", got, tt.want2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
86
pkg/bucket/bandwidth/reader.go
Normal file
86
pkg/bucket/bandwidth/reader.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bandwidth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MonitoredReader monitors the bandwidth
|
||||||
|
type MonitoredReader struct {
|
||||||
|
bucket string // Token to track bucket
|
||||||
|
bucketMeasurement *bucketMeasurement // bucket measurement object
|
||||||
|
object string // Token to track object
|
||||||
|
reader io.Reader // Reader to wrap
|
||||||
|
lastStop time.Time // Last timestamp for a measurement
|
||||||
|
headerSize int // Size of the header not captured by reader
|
||||||
|
throttle *throttle // throttle the rate at which replication occur
|
||||||
|
monitor *Monitor // Monitor reference
|
||||||
|
closed bool // Reader is closed
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMonitoredReader returns a io.ReadCloser that reports bandwidth details
|
||||||
|
func NewMonitoredReader(ctx context.Context, monitor *Monitor, bucket string, object string, reader io.Reader, headerSize int, bandwidthBytesPerSecond int64) *MonitoredReader {
|
||||||
|
timeNow := time.Now()
|
||||||
|
b := monitor.track(bucket, object, timeNow)
|
||||||
|
return &MonitoredReader{
|
||||||
|
bucket: bucket,
|
||||||
|
object: object,
|
||||||
|
bucketMeasurement: b,
|
||||||
|
reader: reader,
|
||||||
|
lastStop: timeNow,
|
||||||
|
headerSize: headerSize,
|
||||||
|
throttle: monitor.throttleBandwidth(ctx, bucket, bandwidthBytesPerSecond),
|
||||||
|
monitor: monitor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read wraps the read reader
|
||||||
|
func (m *MonitoredReader) Read(p []byte) (n int, err error) {
|
||||||
|
if m.closed {
|
||||||
|
err = io.ErrClosedPipe
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p = p[:m.throttle.GetLimitForBytes(int64(len(p)))]
|
||||||
|
|
||||||
|
n, err = m.reader.Read(p)
|
||||||
|
stop := time.Now()
|
||||||
|
update := uint64(n + m.headerSize)
|
||||||
|
|
||||||
|
m.bucketMeasurement.incrementBytes(update)
|
||||||
|
m.lastStop = stop
|
||||||
|
unused := len(p) - (n + m.headerSize)
|
||||||
|
m.headerSize = 0 // Set to 0 post first read
|
||||||
|
|
||||||
|
if unused > 0 {
|
||||||
|
m.throttle.ReleaseUnusedBandwidth(int64(unused))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops tracking the io
|
||||||
|
func (m *MonitoredReader) Close() error {
|
||||||
|
rc, ok := m.reader.(io.ReadCloser)
|
||||||
|
m.closed = true
|
||||||
|
if ok {
|
||||||
|
return rc.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
107
pkg/bucket/bandwidth/throttle.go
Normal file
107
pkg/bucket/bandwidth/throttle.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bandwidth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
throttleInternal = 250 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// throttle implements the throttling for bandwidth
|
||||||
|
type throttle struct {
|
||||||
|
generateTicker *time.Ticker // Ticker to generate available bandwidth
|
||||||
|
freeBytes int64 // unused bytes in the interval
|
||||||
|
bytesPerSecond int64 // max limit for bandwidth
|
||||||
|
bytesPerInterval int64 // bytes allocated for the interval
|
||||||
|
cond *sync.Cond // Used to notify waiting threads for bandwidth availability
|
||||||
|
}
|
||||||
|
|
||||||
|
// newThrottle returns a new bandwidth throttle. Set bytesPerSecond to 0 for no limit
|
||||||
|
func newThrottle(ctx context.Context, bytesPerSecond int64) *throttle {
|
||||||
|
if bytesPerSecond == 0 {
|
||||||
|
return &throttle{}
|
||||||
|
}
|
||||||
|
t := &throttle{
|
||||||
|
bytesPerSecond: bytesPerSecond,
|
||||||
|
generateTicker: time.NewTicker(throttleInternal),
|
||||||
|
}
|
||||||
|
|
||||||
|
t.cond = sync.NewCond(&sync.Mutex{})
|
||||||
|
t.SetBandwidth(bytesPerSecond)
|
||||||
|
t.freeBytes = t.bytesPerInterval
|
||||||
|
go t.generateBandwidth(ctx)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLimitForBytes gets the bytes that are possible to send within the limit
|
||||||
|
// if want is <= 0 or no bandwidth limit set, returns want.
|
||||||
|
// Otherwise a value > 0 will always be returned.
|
||||||
|
func (t *throttle) GetLimitForBytes(want int64) int64 {
|
||||||
|
if want <= 0 || atomic.LoadInt64(&t.bytesPerInterval) == 0 {
|
||||||
|
return want
|
||||||
|
}
|
||||||
|
t.cond.L.Lock()
|
||||||
|
defer t.cond.L.Unlock()
|
||||||
|
for {
|
||||||
|
var send int64
|
||||||
|
freeBytes := atomic.LoadInt64(&t.freeBytes)
|
||||||
|
send = want
|
||||||
|
if freeBytes < want {
|
||||||
|
send = freeBytes
|
||||||
|
if send <= 0 {
|
||||||
|
t.cond.Wait()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
atomic.AddInt64(&t.freeBytes, -send)
|
||||||
|
return send
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBandwidth sets a new bandwidth limit in bytes per second.
|
||||||
|
func (t *throttle) SetBandwidth(bandwidthBiPS int64) {
|
||||||
|
bpi := int64(throttleInternal) * bandwidthBiPS / int64(time.Second)
|
||||||
|
atomic.StoreInt64(&t.bytesPerInterval, bpi)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseUnusedBandwidth releases bandwidth that was allocated for a user
|
||||||
|
func (t *throttle) ReleaseUnusedBandwidth(bytes int64) {
|
||||||
|
atomic.AddInt64(&t.freeBytes, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateBandwidth periodically allocates new bandwidth to use
|
||||||
|
func (t *throttle) generateBandwidth(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-t.generateTicker.C:
|
||||||
|
// A new window is available
|
||||||
|
t.cond.L.Lock()
|
||||||
|
atomic.StoreInt64(&t.freeBytes, atomic.LoadInt64(&t.bytesPerInterval))
|
||||||
|
t.cond.Broadcast()
|
||||||
|
t.cond.L.Unlock()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -49,6 +49,8 @@ const (
|
|||||||
ServerInfoAdminAction = "admin:ServerInfo"
|
ServerInfoAdminAction = "admin:ServerInfo"
|
||||||
// OBDInfoAdminAction - allow obtaining cluster on-board diagnostics
|
// OBDInfoAdminAction - allow obtaining cluster on-board diagnostics
|
||||||
OBDInfoAdminAction = "admin:OBDInfo"
|
OBDInfoAdminAction = "admin:OBDInfo"
|
||||||
|
// BandwidthMonitorAction - allow monitoring bandwidth usage
|
||||||
|
BandwidthMonitorAction = "admin:BandwidthMonitor"
|
||||||
|
|
||||||
// ServerUpdateAdminAction - allow MinIO binary update
|
// ServerUpdateAdminAction - allow MinIO binary update
|
||||||
ServerUpdateAdminAction = "admin:ServerUpdate"
|
ServerUpdateAdminAction = "admin:ServerUpdate"
|
||||||
@ -131,6 +133,7 @@ var supportedAdminActions = map[AdminAction]struct{}{
|
|||||||
KMSKeyStatusAdminAction: {},
|
KMSKeyStatusAdminAction: {},
|
||||||
ServerInfoAdminAction: {},
|
ServerInfoAdminAction: {},
|
||||||
OBDInfoAdminAction: {},
|
OBDInfoAdminAction: {},
|
||||||
|
BandwidthMonitorAction: {},
|
||||||
ServerUpdateAdminAction: {},
|
ServerUpdateAdminAction: {},
|
||||||
ServiceRestartAdminAction: {},
|
ServiceRestartAdminAction: {},
|
||||||
ServiceStopAdminAction: {},
|
ServiceStopAdminAction: {},
|
||||||
@ -173,6 +176,7 @@ var adminActionConditionKeyMap = map[Action]condition.KeySet{
|
|||||||
ServerInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
ServerInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
||||||
DataUsageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
DataUsageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
||||||
OBDInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
OBDInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
||||||
|
BandwidthMonitorAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
||||||
TopLocksAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
TopLocksAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
||||||
ProfilingAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
ProfilingAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
||||||
TraceAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
TraceAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
|
||||||
|
@ -75,7 +75,7 @@ var AdminDiagnostics = Policy{
|
|||||||
Actions: NewActionSet(ProfilingAdminAction,
|
Actions: NewActionSet(ProfilingAdminAction,
|
||||||
TraceAdminAction, ConsoleLogAdminAction,
|
TraceAdminAction, ConsoleLogAdminAction,
|
||||||
ServerInfoAdminAction, TopLocksAdminAction,
|
ServerInfoAdminAction, TopLocksAdminAction,
|
||||||
OBDInfoAdminAction),
|
OBDInfoAdminAction, BandwidthMonitorAction),
|
||||||
Resources: NewResourceSet(NewResource("*", "")),
|
Resources: NewResourceSet(NewResource("*", "")),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
61
pkg/madmin/bandwidth.go
Normal file
61
pkg/madmin/bandwidth.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package madmin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/bandwidth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetBucketBandwidth - Get a snapshot of the bandwidth measurements for replication buckets. If no buckets
|
||||||
|
// generate replication traffic an empty map is returned.
|
||||||
|
func (adm *AdminClient) GetBucketBandwidth(ctx context.Context, buckets ...string) (bandwidth.Report, error) {
|
||||||
|
queryValues := url.Values{}
|
||||||
|
if len(buckets) > 0 {
|
||||||
|
queryValues.Set("buckets", strings.Join(buckets, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
reqData := requestData{
|
||||||
|
relPath: adminAPIPrefix + "/bandwidth",
|
||||||
|
queryValues: queryValues,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := adm.executeMethod(ctx, http.MethodGet, reqData)
|
||||||
|
if err != nil {
|
||||||
|
closeResponse(resp)
|
||||||
|
return bandwidth.Report{}, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return bandwidth.Report{}, httpRespToErrorResponse(resp)
|
||||||
|
}
|
||||||
|
dec := json.NewDecoder(resp.Body)
|
||||||
|
for {
|
||||||
|
var report bandwidth.Report
|
||||||
|
err = dec.Decode(&report)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return bandwidth.Report{}, err
|
||||||
|
}
|
||||||
|
return report, nil
|
||||||
|
}
|
||||||
|
}
|
50
pkg/madmin/examples/bucket-bandwidth.go
Normal file
50
pkg/madmin/examples/bucket-bandwidth.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/madmin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY and my-bucketname are
|
||||||
|
// dummy values, please replace them with original values.
|
||||||
|
|
||||||
|
// API requests are secure (HTTPS) if secure=true and insecure (HTTP) otherwise.
|
||||||
|
// New returns an MinIO Admin client object.
|
||||||
|
madminClient, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
report, err := madminClient.GetBucketBandwidth(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Report: %+v\n", report)
|
||||||
|
report, err = madminClient.GetBucketBandwidth(ctx, "sourceBucket", "sourceBucket2")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Report: %+v\n", report)
|
||||||
|
}
|
@ -42,7 +42,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
target := madmin.BucketTarget{Endpoint: "site2:9000", Credentials: creds, TargetBucket: "destbucket", IsSSL: false, Type: madmin.ReplicationArn}
|
target := madmin.BucketTarget{Endpoint: "site2:9000", Credentials: creds, TargetBucket: "destbucket", IsSSL: false, Type: madmin.ReplicationArn, BandwidthLimit: 2 * 1024 * 1024}
|
||||||
// Set bucket target
|
// Set bucket target
|
||||||
if err := madmClnt.SetBucketTarget(ctx, "srcbucket", &target); err != nil {
|
if err := madmClnt.SetBucketTarget(ctx, "srcbucket", &target); err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
|
@ -84,17 +84,18 @@ func ParseARN(s string) (*ARN, error) {
|
|||||||
|
|
||||||
// BucketTarget represents the target bucket and site association.
|
// BucketTarget represents the target bucket and site association.
|
||||||
type BucketTarget struct {
|
type BucketTarget struct {
|
||||||
SourceBucket string `json:"sourcebucket"`
|
SourceBucket string `json:"sourcebucket"`
|
||||||
Endpoint string `json:"endpoint"`
|
Endpoint string `json:"endpoint"`
|
||||||
Credentials *auth.Credentials `json:"credentials"`
|
Credentials *auth.Credentials `json:"credentials"`
|
||||||
TargetBucket string `json:"targetbucket"`
|
TargetBucket string `json:"targetbucket"`
|
||||||
Secure bool `json:"secure"`
|
Secure bool `json:"secure"`
|
||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
API string `json:"api,omitempty"`
|
API string `json:"api,omitempty"`
|
||||||
Arn string `json:"arn,omitempty"`
|
Arn string `json:"arn,omitempty"`
|
||||||
Type ServiceType `json:"type"`
|
Type ServiceType `json:"type"`
|
||||||
Region string `json:"omitempty"`
|
Region string `json:"omitempty"`
|
||||||
Label string `json:"label,omitempty"`
|
Label string `json:"label,omitempty"`
|
||||||
|
BandwidthLimit int64 `json:"bandwidthlimit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone returns shallow clone of BucketTarget without secret key in credentials
|
// Clone returns shallow clone of BucketTarget without secret key in credentials
|
||||||
|
Loading…
x
Reference in New Issue
Block a user