pkg/metrics_store: Cache byte slices instead of strings

When converting a string to a byte slice, in Golang would do a full copy
which introduces memory allocations. On an HTTP scrape request WriteAll
in the metrics store component is called, which writes all cached
metrics into the given io.Writer. io.Writer takes a byte slice, whereas
metrics are cached as strings. The connect the two, the metrics store
component converts the metric strings to byte slices. This results in
`runtime.stringtoslicebyte` [1] being a prominent CPU user.

Instead of caching metrics as strings, cache them as byte slices,
removing the additional translation from the hot-path.

[1] https://golang.org/src/runtime/string.go?s=4063:4115#L145
This commit is contained in:
Max Leonard Inden 2019-04-02 16:30:03 +02:00
parent bfdfa8b87e
commit bbb1dca85d
No known key found for this signature in database
GPG Key ID: 5403C5464810BC26
8 changed files with 32 additions and 34 deletions

View File

@ -31,14 +31,14 @@ type generateMetricsTestCase struct {
Obj interface{}
MetricNames []string
Want string
Func func(interface{}) []metricsstore.FamilyStringer
Func func(interface{}) []metricsstore.FamilyByteSlicer
}
func (testCase *generateMetricsTestCase) run() error {
metricFamilies := testCase.Func(testCase.Obj)
metricFamilyStrings := []string{}
for _, f := range metricFamilies {
metricFamilyStrings = append(metricFamilyStrings, f.String())
metricFamilyStrings = append(metricFamilyStrings, string(f.ByteSlice()))
}
metric := strings.Split(strings.Join(metricFamilyStrings, ""), "\n")

View File

@ -26,19 +26,13 @@ type Family struct {
Metrics []*Metric
}
// String returns the given Family in its string representation.
func (f Family) String() string {
// ByteSlice returns the given Family in its string representation.
func (f Family) ByteSlice() []byte {
b := strings.Builder{}
for _, m := range f.Metrics {
b.WriteString(f.Name)
m.Write(&b)
}
return b.String()
}
// FamilyStringer represents a metric family that can be converted to its string
// representation.
type FamilyStringer interface {
String() string
return []byte(b.String())
}

View File

@ -70,9 +70,9 @@ func ExtractMetricFamilyHeaders(families []FamilyGenerator) []string {
// ComposeMetricGenFuncs takes a slice of metric families and returns a function
// that composes their metric generation functions into a single one.
func ComposeMetricGenFuncs(familyGens []FamilyGenerator) func(obj interface{}) []metricsstore.FamilyStringer {
return func(obj interface{}) []metricsstore.FamilyStringer {
families := make([]metricsstore.FamilyStringer, len(familyGens))
func ComposeMetricGenFuncs(familyGens []FamilyGenerator) func(obj interface{}) []metricsstore.FamilyByteSlicer {
return func(obj interface{}) []metricsstore.FamilyByteSlicer {
families := make([]metricsstore.FamilyByteSlicer, len(familyGens))
for i, gen := range familyGens {
families[i] = gen.Generate(obj)

View File

@ -17,6 +17,7 @@ limitations under the License.
package metric
import (
"fmt"
"math"
"strconv"
"strings"
@ -56,7 +57,10 @@ type Metric struct {
func (m *Metric) Write(s *strings.Builder) {
if len(m.LabelKeys) != len(m.LabelValues) {
panic("expected labelKeys to be of same length as labelValues")
panic(fmt.Sprintf(
"expected labelKeys %q to be of same length as labelValues %q",
m.LabelKeys, m.LabelValues,
))
}
labelsToString(s, m.LabelKeys, m.LabelValues)

View File

@ -34,7 +34,7 @@ func TestFamilyString(t *testing.T) {
}
expected := "kube_pod_info{namespace=\"default\"} 1"
got := strings.TrimSpace(f.String())
got := strings.TrimSpace(string(f.ByteSlice()))
if got != expected {
t.Fatalf("expected %v but got %v", expected, got)

View File

@ -24,10 +24,10 @@ import (
"k8s.io/apimachinery/pkg/types"
)
// FamilyStringer represents a metric family that can be converted to its string
// FamilyByteSlicer represents a metric family that can be converted to its string
// representation.
type FamilyStringer interface {
String() string
type FamilyByteSlicer interface {
ByteSlice() []byte
}
// MetricsStore implements the k8s.io/client-go/tools/cache.Store
@ -40,7 +40,7 @@ type MetricsStore struct {
// metric families, containing a slice of metrics. We need to keep metrics
// grouped by metric families in order to zip families with their help text in
// MetricsStore.WriteAll().
metrics map[types.UID][]string
metrics map[types.UID][][]byte
// headers contains the header (TYPE and HELP) of each metric family. It is
// later on zipped with with their corresponding metric families in
// MetricStore.WriteAll().
@ -48,15 +48,15 @@ type MetricsStore struct {
// generateMetricsFunc generates metrics based on a given Kubernetes object
// and returns them grouped by metric family.
generateMetricsFunc func(interface{}) []FamilyStringer
generateMetricsFunc func(interface{}) []FamilyByteSlicer
}
// NewMetricsStore returns a new MetricsStore
func NewMetricsStore(headers []string, generateFunc func(interface{}) []FamilyStringer) *MetricsStore {
func NewMetricsStore(headers []string, generateFunc func(interface{}) []FamilyByteSlicer) *MetricsStore {
return &MetricsStore{
generateMetricsFunc: generateFunc,
headers: headers,
metrics: map[types.UID][]string{},
metrics: map[types.UID][][]byte{},
}
}
@ -74,10 +74,10 @@ func (s *MetricsStore) Add(obj interface{}) error {
defer s.mutex.Unlock()
families := s.generateMetricsFunc(obj)
familyStrings := make([]string, len(families))
familyStrings := make([][]byte, len(families))
for i, f := range families {
familyStrings[i] = f.String()
familyStrings[i] = f.ByteSlice()
}
s.metrics[o.GetUID()] = familyStrings
@ -131,7 +131,7 @@ func (s *MetricsStore) GetByKey(key string) (item interface{}, exists bool, err
// given list.
func (s *MetricsStore) Replace(list []interface{}, _ string) error {
s.mutex.Lock()
s.metrics = map[types.UID][]string{}
s.metrics = map[types.UID][][]byte{}
s.mutex.Unlock()
for _, o := range list {

View File

@ -30,28 +30,28 @@ import (
// Mock metricFamily instead of importing /pkg/metric to prevent cyclic
// dependency.
type metricFamily struct {
value string
value []byte
}
// Implement FamilyStringer interface.
func (f *metricFamily) String() string {
// Implement FamilyByteSlicer interface.
func (f *metricFamily) ByteSlice() []byte {
return f.value
}
func TestObjectsSameNameDifferentNamespaces(t *testing.T) {
serviceIDS := []string{"a", "b"}
genFunc := func(obj interface{}) []FamilyStringer {
genFunc := func(obj interface{}) []FamilyByteSlicer {
o, err := meta.Accessor(obj)
if err != nil {
t.Fatal(err)
}
metricFamily := metricFamily{
fmt.Sprintf("kube_service_info{uid=\"%v\"} 1", string(o.GetUID())),
[]byte(fmt.Sprintf("kube_service_info{uid=\"%v\"} 1", string(o.GetUID()))),
}
return []FamilyStringer{&metricFamily}
return []FamilyByteSlicer{&metricFamily}
}
ms := NewMetricsStore([]string{"Information about service."}, genFunc)

View File

@ -82,7 +82,7 @@ func serviceCollector(kubeClient clientset.Interface) *collector.Collector {
return collector.NewCollector(store)
}
func generateServiceMetrics(obj interface{}) []metricsstore.FamilyStringer {
func generateServiceMetrics(obj interface{}) []metricsstore.FamilyByteSlicer {
sPointer := obj.(*v1.Service)
s := *sPointer
@ -97,5 +97,5 @@ func generateServiceMetrics(obj interface{}) []metricsstore.FamilyStringer {
Metrics: []*metric.Metric{&m},
}
return []metricsstore.FamilyStringer{&family}
return []metricsstore.FamilyByteSlicer{&family}
}