Skip to content

Commit 380a689

Browse files
committed
enhance memory ballast extension. set ballast size in percentage is supported for containers and physical hosts
1 parent cdc1634 commit 380a689

File tree

16 files changed

+238
-55
lines changed

16 files changed

+238
-55
lines changed

extension/ballastextension/README.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,38 @@ Memory Ballast extension enables applications to configure memory ballast for th
66

77
The following settings can be configured:
88

9-
- `size_mib` (default = 0, disabled): Is the memory ballast size, in MiB.
9+
- `size_mib` (default = 0, disabled): Is the memory ballast size, in MiB.
10+
Takes higher priority than `size_in_percentage` if both are specified at the same time.
11+
- `size_in_percentage` (default = 0, disabled): Set the memory ballast based on the
12+
total memory in percentage, value range is `1-100`.
13+
It is supported in both containerized(eg, docker, k8s) and physical host environments.
14+
15+
**How ballast size is calculated with percentage configuration**
16+
When `size_in_percentage` is enabled with the value(1-100), the absolute `ballast_size` will be calculated by
17+
`size_in_percentage * totalMemory / 100`. The `totalMemory` can be retrieved for hosts and containers(in docker, k8s, etc) by the following steps,
18+
1. Look up Memory Cgroup subsystem on the target host or container, find out if there is any total memory limitation has been set for the running collector process.
19+
Check the value in `memory.limit_in_bytes` file under cgroup memory files (eg, `/sys/fs/cgroup/memory/memory.limit_in_bytes`).
20+
21+
2. If `memory.limit_in_bytes` is positive value other than `9223372036854771712`(`0x7FFFFFFFFFFFF000`). The `ballest_size`
22+
will be calculated by `memory.limit_in_bytes * size_in_percentage / 100`.
23+
If `memory.limit_in_bytes` value is `9223372036854771712`(`0x7FFFFFFFFFFFF000`), it indicates there is no memory limit has
24+
been set for the collector process or the running container in cgroup. Then the `totalMemory` will be determined in next step.
25+
26+
3. if there is no memory limit set in cgroup for the collector process or container where the collector is running. The total memory will be
27+
calculated by `github.com/shirou/gopsutil/mem`[[link]](https://github.com/shirou/gopsutil/) on `mem.VirtualMemory().total` which is supported in multiple OS systems.
1028

11-
Example:
1229

30+
Example:
31+
Config that uses 64 Mib of memory for the ballast:
1332
```yaml
1433
extensions:
1534
memory_ballast:
1635
size_mib: 64
1736
```
37+
38+
Config that uses 20% of the total memory for the ballast:
39+
```yaml
40+
extensions:
41+
memory_ballast:
42+
size_in_percentage: 20
43+
```

extension/ballastextension/config.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,29 @@
1515
package ballastextension
1616

1717
import (
18+
"fmt"
19+
1820
"go.opentelemetry.io/collector/config"
1921
)
2022

21-
// Config has the configuration for the fluentbit extension.
23+
// Config has the configuration for the ballast extension.
2224
type Config struct {
2325
config.ExtensionSettings `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct
2426

2527
// SizeMiB is the size, in MiB, of the memory ballast
2628
// to be created for this process.
27-
SizeMiB uint32 `mapstructure:"size_mib"`
29+
SizeMiB uint64 `mapstructure:"size_mib"`
30+
31+
// SizeInPercentage is the maximum amount of memory ballast, in %, targeted to be
32+
// allocated. The fixed memory settings SizeMiB has a higher precedence.
33+
SizeInPercentage uint64 `mapstructure:"size_in_percentage"`
34+
}
35+
36+
// Validate checks if the extension configuration is valid
37+
func (cfg *Config) Validate() error {
38+
// no need to validate less than 0 case for uint64
39+
if cfg.SizeInPercentage > 100 {
40+
return fmt.Errorf("size_in_percentage is not in range 0 to 100")
41+
}
42+
return nil
2843
}

extension/ballastextension/config_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,23 @@ func TestLoadConfig(t *testing.T) {
4545
&Config{
4646
ExtensionSettings: config.NewExtensionSettings(config.NewIDWithName(typeStr, "1")),
4747
SizeMiB: 123,
48+
SizeInPercentage: 20,
4849
},
4950
ext1)
5051

5152
assert.Equal(t, 1, len(cfg.Service.Extensions))
5253
assert.Equal(t, config.NewIDWithName(typeStr, "1"), cfg.Service.Extensions[0])
5354
}
55+
56+
func TestLoadInvalidConfig(t *testing.T) {
57+
factories, err := componenttest.NopFactories()
58+
assert.NoError(t, err)
59+
60+
factory := NewFactory()
61+
factories.Extensions[typeStr] = factory
62+
_, err = configtest.LoadConfigAndValidate(path.Join(".", "testdata", "config_invalid.yaml"), factories)
63+
64+
require.NotNil(t, err)
65+
assert.Equal(t, err.Error(), "extension \"memory_ballast\" has invalid configuration: size_in_percentage is not in range 0 to 100")
66+
67+
}

extension/ballastextension/factory.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ import (
2020
"go.opentelemetry.io/collector/component"
2121
"go.opentelemetry.io/collector/config"
2222
"go.opentelemetry.io/collector/extension/extensionhelper"
23+
"go.opentelemetry.io/collector/internal/iruntime"
2324
)
2425

2526
const (
2627
// The value of extension "type" in configuration.
2728
typeStr = "memory_ballast"
2829
)
2930

31+
// memHandler returns the total memory of the target host/vm
32+
var memHandler = iruntime.TotalMemory
33+
3034
// NewFactory creates a factory for FluentBit extension.
3135
func NewFactory() component.ExtensionFactory {
3236
return extensionhelper.NewFactory(
@@ -42,5 +46,5 @@ func createDefaultConfig() config.Extension {
4246
}
4347

4448
func createExtension(_ context.Context, set component.ExtensionCreateSettings, cfg config.Extension) (component.Extension, error) {
45-
return newMemoryBallast(cfg.(*Config), set.Logger), nil
49+
return newMemoryBallast(cfg.(*Config), set.Logger, memHandler), nil
4650
}

extension/ballastextension/memory_ballast.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,32 @@ import (
2525
const megaBytes = 1024 * 1024
2626

2727
type memoryBallast struct {
28-
cfg *Config
29-
logger *zap.Logger
30-
ballast []byte
28+
cfg *Config
29+
logger *zap.Logger
30+
ballast []byte
31+
getTotalMem func() (uint64, error)
3132
}
3233

3334
func (m *memoryBallast) Start(_ context.Context, _ component.Host) error {
35+
var ballastSizeBytes uint64
36+
// absolute value supersedes percentage setting
3437
if m.cfg.SizeMiB > 0 {
35-
ballastSizeBytes := uint64(m.cfg.SizeMiB) * megaBytes
38+
ballastSizeBytes = m.cfg.SizeMiB * megaBytes
39+
} else {
40+
totalMemory, err := m.getTotalMem()
41+
if err != nil {
42+
return err
43+
}
44+
ballastPercentage := m.cfg.SizeInPercentage
45+
ballastSizeBytes = ballastPercentage * totalMemory / 100
46+
}
47+
48+
if ballastSizeBytes > 0 {
3649
m.ballast = make([]byte, ballastSizeBytes)
37-
m.logger.Info("Using memory ballast", zap.Uint32("MiBs", m.cfg.SizeMiB))
3850
}
51+
52+
m.logger.Info("Setting memory ballast", zap.Uint32("MiBs", uint32(ballastSizeBytes/megaBytes)))
53+
3954
return nil
4055
}
4156

@@ -44,9 +59,10 @@ func (m *memoryBallast) Shutdown(_ context.Context) error {
4459
return nil
4560
}
4661

47-
func newMemoryBallast(cfg *Config, logger *zap.Logger) *memoryBallast {
62+
func newMemoryBallast(cfg *Config, logger *zap.Logger, getTotalMem func() (uint64, error)) *memoryBallast {
4863
return &memoryBallast{
49-
cfg: cfg,
50-
logger: logger,
64+
cfg: cfg,
65+
logger: logger,
66+
getTotalMem: getTotalMem,
5167
}
5268
}

extension/ballastextension/memory_ballast_test.go

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,64 @@ import (
2323
"go.uber.org/zap"
2424

2525
"go.opentelemetry.io/collector/component/componenttest"
26+
"go.opentelemetry.io/collector/internal/iruntime"
2627
)
2728

2829
func TestMemoryBallast(t *testing.T) {
29-
config := &Config{
30-
SizeMiB: 13,
30+
tests := []struct {
31+
name string
32+
config *Config
33+
getTotalMem func() (uint64, error)
34+
expect int
35+
}{
36+
{
37+
name: "test_abs_ballast",
38+
config: &Config{
39+
SizeMiB: 13,
40+
},
41+
getTotalMem: iruntime.TotalMemory,
42+
expect: 13 * megaBytes,
43+
},
44+
{
45+
name: "test_abs_ballast_priority",
46+
config: &Config{
47+
SizeMiB: 13,
48+
SizeInPercentage: 20,
49+
},
50+
getTotalMem: iruntime.TotalMemory,
51+
expect: 13 * megaBytes,
52+
},
53+
{
54+
name: "test_ballast_zero_val",
55+
config: &Config{},
56+
getTotalMem: iruntime.TotalMemory,
57+
expect: 0,
58+
},
59+
{
60+
name: "test_ballast_in_percentage",
61+
config: &Config{
62+
SizeInPercentage: 20,
63+
},
64+
getTotalMem: mockTotalMem,
65+
expect: 20 * megaBytes,
66+
},
3167
}
3268

33-
mbExt := newMemoryBallast(config, zap.NewNop())
34-
require.NotNil(t, mbExt)
35-
assert.Nil(t, mbExt.ballast)
69+
for _, tt := range tests {
70+
t.Run(tt.name, func(t *testing.T) {
71+
mbExt := newMemoryBallast(tt.config, zap.NewNop(), tt.getTotalMem)
72+
require.NotNil(t, mbExt)
73+
assert.Nil(t, mbExt.ballast)
3674

37-
assert.NoError(t, mbExt.Start(context.Background(), componenttest.NewNopHost()))
38-
assert.Equal(t, 13*megaBytes, len(mbExt.ballast))
75+
assert.NoError(t, mbExt.Start(context.Background(), componenttest.NewNopHost()))
76+
assert.Equal(t, tt.expect, len(mbExt.ballast))
3977

40-
assert.NoError(t, mbExt.Shutdown(context.Background()))
41-
assert.Nil(t, mbExt.ballast)
78+
assert.NoError(t, mbExt.Shutdown(context.Background()))
79+
assert.Nil(t, mbExt.ballast)
80+
})
81+
}
4282
}
4383

44-
func TestMemoryBallast_ZeroSize(t *testing.T) {
45-
config := &Config{}
46-
47-
mbExt := newMemoryBallast(config, zap.NewNop())
48-
require.NotNil(t, mbExt)
49-
assert.Nil(t, mbExt.ballast)
50-
51-
assert.NoError(t, mbExt.Start(context.Background(), componenttest.NewNopHost()))
52-
assert.Nil(t, mbExt.ballast)
53-
54-
assert.NoError(t, mbExt.Shutdown(context.Background()))
55-
assert.Nil(t, mbExt.ballast)
84+
func mockTotalMem() (uint64, error) {
85+
return uint64(100 * megaBytes), nil
5686
}

extension/ballastextension/testdata/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ extensions:
22
memory_ballast:
33
memory_ballast/1:
44
size_mib: 123
5+
size_in_percentage: 20
56

67
# Data pipeline is required to load the config.
78
receivers:
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
extensions:
2+
memory_ballast:
3+
size_in_percentage: 200
4+
5+
# Data pipeline is required to load the config.
6+
receivers:
7+
nop:
8+
processors:
9+
nop:
10+
exporters:
11+
nop:
12+
13+
service:
14+
extensions: [memory_ballast]
15+
pipelines:
16+
traces:
17+
receivers: [nop]
18+
processors: [nop]
19+
exporters: [nop]

internal/cgroups/cgroups.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func NewCGroupsForCurrentProcess() (CGroups, error) {
107107

108108
// MemoryQuota returns the total memory a
109109
// It is a result of `memory.limit_in_bytes`. If the value of
110-
// `memory.limit_in_bytes` was not set (-1), the method returns `(-1, false, nil)`.
110+
// `memory.limit_in_bytes` was not set (-1) or (9223372036854771712), the method returns `(-1, false, nil)`.
111111
func (cg CGroups) MemoryQuota() (int64, bool, error) {
112112
memCGroup, exists := cg[_cgroupSubsysMemory]
113113
if !exists {

internal/iruntime/mem_info.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package iruntime
16+
17+
import (
18+
"github.com/shirou/gopsutil/mem"
19+
)
20+
21+
// readMemInfo returns the total memory
22+
// supports in linux, darwin and windows
23+
func readMemInfo() (uint64, error) {
24+
vmStat, err := mem.VirtualMemory()
25+
return vmStat.Total, err
26+
}

0 commit comments

Comments
 (0)