Ctrl K
ComunidadGuías…

auditor/storage.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package auditor

import (
	"cmp"
	"encoding/json"
	"fmt"
	"slices"

	"github.com/coroot/coroot/model"
	"github.com/coroot/coroot/timeseries"
	"github.com/coroot/coroot/utils"
	"github.com/dustin/go-humanize"
)

func (a *appAuditor) storage() {
	report := a.addReport(model.AuditReportStorage)

	ioCheck := report.CreateCheck(model.Checks.StorageIOLoad)
	spaceCheck := report.CreateCheck(model.Checks.StorageSpace)

	ioLatencyChart := report.GetOrCreateChartGroup("Average I/O latency <selector>, seconds", model.NewDocLink("inspections", "storage", "average-io-latency"))
	ioLoadChart := report.GetOrCreateChartGroup("I/O load (total latency) <selector>, seconds/second", model.NewDocLink("inspections", "storage", "io-load"))
	iopsChart := report.GetOrCreateChartGroup("IOPS <selector>", model.NewDocLink("inspections", "storage", "iops"))
	bandwidthChart := report.GetOrCreateChartGroup("Bandwidth <selector>, bytes/second", model.NewDocLink("inspections", "storage", "bandwidth"))
	ioUtilizationChart := report.GetOrCreateChartGroup("I/O utilization <selector>, %", model.NewDocLink("inspections", "storage", "io-utilization"))
	pressureChart := report.GetOrCreateChartGroup("I/O pressure <selector>, seconds/second", model.NewDocLink("inspections", "storage", "io-pressure"))
	spaceChart := report.GetOrCreateChartGroup("Disk space <selector>, bytes", model.NewDocLink("inspections", "storage", "disk-space"))

	ioCheck.AddWidget(ioLoadChart.Widget())
	ioCheck.AddWidget(ioLatencyChart.Widget())
	spaceCheck.AddWidget(spaceChart.Widget())

	seenVolumes := false
	investigated := map[string]bool{}
	ioInvestigated := map[string]bool{}
	isK8s := a.app.IsK8s()
	for _, i := range a.app.Instances {
		if pressureChart != nil && len(i.Containers) > 0 {
			pressureSome := timeseries.NewAggregate(timeseries.NanSum)
			pressureFull := timeseries.NewAggregate(timeseries.NanSum)
			for _, c := range i.Containers {
				pressureSome.Add(c.IOPressureSome)
				pressureFull.Add(c.IOPressureFull)
			}
			pressureChart.GetOrCreateChart("some").AddSeries(i.Name, pressureSome).Feature()
			pressureChart.GetOrCreateChart("full").AddSeries(i.Name, pressureFull)
		}
		for _, v := range i.Volumes {
			fullName := i.Name + ":" + v.MountPoint
			if i.Node != nil {
				if isK8s && v.Name.Value() == "" {
					continue
				}
				seenVolumes = true
				if d := i.Node.Disks[v.Device.Value()]; d != nil {
					if ioLatencyChart != nil {
						ioLatencyChart.GetOrCreateChart(v.MountPoint).Feature().AddSeries(i.Name, d.Await)
						ioLatencyChart.
							GetOrCreateChart(i.Name+":"+v.MountPoint).
							AddSeries("read", timeseries.Div(d.ReadTime, d.ReadOps), "blue").
							AddSeries("write", timeseries.Div(d.WriteTime, d.WriteOps), "amber")

					}
					if ioUtilizationChart != nil {
						ioUtilizationChart.GetOrCreateChart(v.MountPoint).AddSeries(i.Name, d.IOUtilizationPercent)
					}
					ioLoad := timeseries.NewAggregate(timeseries.NanSum).Add(d.ReadTime, d.WriteTime).Get()
					if ioLoadChart != nil {
						ioLoadChart.
							GetOrCreateChart(v.MountPoint).
							Feature().
							AddSeries(i.Name, ioLoad)
						ioLoadChart.
							GetOrCreateChart(i.Name+":"+v.MountPoint).
							Stacked().
							AddSeries("read", d.ReadTime, "blue").
							AddSeries("write", d.WriteTime, "amber")
					}
					load := ioLoad.Get().Last()
					if load > ioCheck.Value() {
						ioCheck.SetValue(load)
					}
					if load > ioCheck.Threshold {
						ioCheck.AddItem("%s:%s", i.Name, v.MountPoint)
						if !ioInvestigated[i.Name] {
							ioInvestigated[i.Name] = true
							pgIOFindings(i, d, load, ioCheck)
						}
					}
					if iopsChart != nil {
						iopsChart.GetOrCreateChart(fullName).Stacked().Sorted().
							AddSeries("read", d.ReadOps, "blue").
							AddSeries("write", d.WriteOps, "amber")
					}
					if bandwidthChart != nil {
						profileType := model.ProfileTypeNodeAgentIORead
						if d.WrittenBytes.Reduce(timeseries.NanSum) > d.ReadBytes.Reduce(timeseries.NanSum) {
							profileType = model.ProfileTypeNodeAgentIOWrite
						}
						query, _ := json.Marshal(map[string]any{"type": profileType}) //nolint:errchkjson // a single string value, cannot fail
						bandwidthChart.GetOrCreateChart(fullName).Stacked().Sorted().
							AddSeries("read", d.ReadBytes, "blue").
							AddSeries("written", d.WrittenBytes, "amber").
							DrillDownLink = model.NewRouterLink("profile", "overview").
							SetParam("view", "applications").
							SetParam("id", a.app.Id).
							SetParam("report", model.AuditReportProfiling).
							SetArg("query", string(query))
					}

					latencyMs := model.NewTableCell().SetUnit("ms").SetValue(utils.FormatFloat(d.Await.Last() * 1000))
					ioLoadCell := model.NewTableCell()
					if !timeseries.IsNaN(load) {
						ioLoadCell.SetValue(utils.FormatFloat(load))
					}
					space := model.NewTableCell()
					capacity := v.CapacityBytes.Last()
					usage := v.UsedBytes.Last()
					if usage > 0 && capacity > 0 {
						percentage := usage / capacity * 100
						space.SetValue(fmt.Sprintf(
							"%.0f%% (%s / %s)",
							percentage,
							humanize.Bytes(uint64(usage)),
							humanize.Bytes(uint64(capacity))),
						)
						if percentage > spaceCheck.Value() {
							spaceCheck.SetValue(percentage)
						}
						if percentage > spaceCheck.Threshold {
							spaceCheck.AddItem("%s:%s", i.Name, v.MountPoint)
							if !investigated[i.Name] {
								investigated[i.Name] = true
								diskUsageFindings(i, capacity, a.w.Ctx, spaceCheck)
							}
						}
					}
					report.GetOrCreateTable("Volume", "Latency", "I/O load", "Space", "Device").AddRow(
						model.NewTableCell(fullName),
						latencyMs,
						ioLoadCell,
						space,
						model.NewTableCell(v.Device.Value()).SetUnit(v.Name.Value()),
					)
				}
				if spaceChart != nil {
					spaceChart.GetOrCreateChart(fullName).Stacked().
						AddSeries("used", v.UsedBytes).
						SetThreshold("total", v.CapacityBytes)
				}
			}
		}
	}
	if ioLoadChart != nil {
		for _, ch := range ioLoadChart.Charts {
			ch.DrillDownLink = model.NewRouterLink("profile", "overview").
				SetParam("view", "applications").
				SetParam("id", a.app.Id).
				SetParam("report", model.AuditReportProfiling).
				SetArg("query", model.ProfileCategoryIO)
		}
	}
	if !seenVolumes {
		a.delReport(model.AuditReportStorage)
	}
}

const diskFindingMinGrowthBytes = 256 * 1024 * 1024
const diskFindingSignificantFraction = 0.02

func diskFindingMinGrowth(capacity float32) float32 {
	minGrowth := float32(diskFindingMinGrowthBytes)
	if capacity > 0 && capacity*diskFindingSignificantFraction > minGrowth {
		minGrowth = capacity * diskFindingSignificantFraction
	}
	return minGrowth
}

func diskUsageFindings(i *model.Instance, capacity float32, ctx timeseries.Context, check *model.Check) {
	switch {
	case i.Postgres != nil:
		pgDiskUsageFindings(i, capacity, ctx, check)
	case i.Mysql != nil:
		mysqlDiskUsageFindings(i, capacity, ctx, check)
	case i.Mongodb != nil:
		mongoDiskUsageFindings(i, capacity, check)
	}
}

const diskFindingMinGrowthRateBytesPerSecond = 16 * 1024 // ~1.35 GB/day

func avgGrowthRate(ts *timeseries.TimeSeries) float32 {
	if ts.IsEmpty() {
		return 0
	}
	var sum float32
	var n int
	iter := ts.Iter()
	for iter.Next() {
		if _, v := iter.Value(); !timeseries.IsNaN(v) {
			sum += v
			n++
		}
	}
	if n == 0 {
		return 0
	}
	return sum / float32(n)
}

type diskGrower struct {
	name string
	rate float32 // bytes/second
	size float32 // current total size, 0 if unknown
}

func tableGrowers(noun string, growthRate, size map[model.DbTableKey]*timeseries.TimeSeries) []diskGrower {
	var res []diskGrower
	for k, ts := range growthRate {
		if rate := avgGrowthRate(ts); rate >= diskFindingMinGrowthRateBytesPerSecond {
			res = append(res, diskGrower{noun + " " + k.String(), rate, lastSize(size[k])})
		}
	}
	return res
}

func seriesGrowthRate(ts *timeseries.TimeSeries, ctx timeseries.Context) (rate, size float32) {
	window := float32(ctx.To - ctx.From)
	if window <= 0 {
		return 0, 0
	}
	growth, last := seriesGrowth(ts)
	return growth / window, last
}

func reportGrowers(instanceName string, check *model.Check, growers []diskGrower) int {
	slices.SortFunc(growers, func(a, b diskGrower) int { return cmp.Compare(b.rate, a.rate) })
	growers = growers[:min(3, len(growers))]
	for _, g := range growers {
		if g.size > 0 {
			check.AddDetail("%s: %s is growing at %s/s (%s total)", instanceName, g.name,
				humanize.Bytes(uint64(g.rate)), humanize.Bytes(uint64(g.size)))
		} else {
			check.AddDetail("%s: %s is growing at %s/s", instanceName, g.name, humanize.Bytes(uint64(g.rate)))
		}
	}
	return len(growers)
}

func reportLargest(instanceName, noun string, capacity float32, tableSize map[model.DbTableKey]*timeseries.TimeSeries, dbSize map[string]*timeseries.TimeSeries, check *model.Check) {
	minSize := diskFindingMinGrowth(capacity)
	type item struct {
		name string
		size float32
	}
	var items []item
	for k, ts := range tableSize {
		if s := lastSize(ts); s >= minSize {
			items = append(items, item{noun + " " + k.String(), s})
		}
	}
	if len(items) == 0 {
		for db, ts := range dbSize {
			if s := lastSize(ts); s >= minSize {
				items = append(items, item{"database " + db, s})
			}
		}
	}
	slices.SortFunc(items, func(a, b item) int { return cmp.Compare(b.size, a.size) })
	for _, it := range items[:min(3, len(items))] {
		check.AddDetail("%s: %s is %s", instanceName, it.name, humanize.Bytes(uint64(it.size)))
	}
}

func lastSize(ts *timeseries.TimeSeries) float32 {
	if ts == nil {
		return 0
	}
	if last := ts.Last(); !timeseries.IsNaN(last) {
		return last
	}
	return 0
}