Skip to content

feat: implement zswap support #11199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions api/resource/definitions/block/block.proto
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,17 @@ message VolumeStatusSpec {
string parent_id = 19;
}

// ZswapStatusSpec is the spec for ZswapStatus resource.
message ZswapStatusSpec {
uint64 total_size_bytes = 1;
string total_size_human = 2;
uint64 stored_pages = 3;
uint64 pool_limit_hit = 4;
uint64 reject_reclaim_fail = 5;
uint64 reject_alloc_fail = 6;
uint64 reject_kmemcache_fail = 7;
uint64 reject_compress_fail = 8;
uint64 reject_compress_poor = 9;
uint64 written_back_pages = 10;
}

6 changes: 6 additions & 0 deletions cmd/talosctl/cmd/talos/cgroupsprinter/presets/swap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ columns:
template: '{{ .MemorySwapHigh.HumanizeIBytes | printf "%8s" }}'
- name: SwapMax
template: '{{ .MemorySwapMax.HumanizeIBytes | printf "%8s" }}'
- name: ZswapCurrent
template: '{{ .MemoryZswapCurrent.HumanizeIBytes | printf "%8s" }}'
- name: ZswapMax
template: '{{ .MemoryZswapMax.HumanizeIBytes | printf "%8s" }}'
- name: ZswapWriteback
template: '{{ .MemoryZswapWriteback }}'
7 changes: 7 additions & 0 deletions hack/release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ This feature can be enable by using [SwapVolumeConfig](https://www.talos.dev/v1.
title = "VMware"
description = """\
Talos VMWare platform now supports `arm64` architecture in addition to `amd64`.
"""

[notes.zswap]
title = "Zswap Support"
description = """\
Talos now supports zswap, a compressed cache for swap pages.
This feature can be enabled by using [ZswapConfig](https://www.talos.dev/v1.11/reference/configuration/block/zswapconfig/) document in the machine configuration.
"""

[make_deps]
Expand Down
5 changes: 5 additions & 0 deletions hack/test/patches/ephemeral-nvme.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ machine:
extraConfig:
memorySwap:
swapBehavior: LimitedSwap
---
apiVersion: v1alpha1
kind: ZswapConfig
maxPoolPercent: 25
shrinkerEnabled: true
115 changes: 115 additions & 0 deletions internal/app/machined/pkg/controllers/block/zswap_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package block

import (
"context"
"fmt"
"strconv"

"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"github.com/siderolabs/gen/optional"
"go.uber.org/zap"

configconfig "github.com/siderolabs/talos/pkg/machinery/config/config"
"github.com/siderolabs/talos/pkg/machinery/resources/config"
"github.com/siderolabs/talos/pkg/machinery/resources/runtime"
)

// ZswapConfigController provides zswap configuration based machine configuration.
type ZswapConfigController struct{}

// Name implements controller.Controller interface.
func (ctrl *ZswapConfigController) Name() string {
return "block.ZswapConfigController"
}

// Inputs implements controller.Controller interface.
func (ctrl *ZswapConfigController) Inputs() []controller.Input {
return []controller.Input{
{
Namespace: config.NamespaceName,
Type: config.MachineConfigType,
ID: optional.Some(config.ActiveID),
Kind: controller.InputWeak,
},
}
}

// Outputs implements controller.Controller interface.
func (ctrl *ZswapConfigController) Outputs() []controller.Output {
return []controller.Output{
{
Type: runtime.KernelParamSpecType,
Kind: controller.OutputShared,
},
}
}

// Run implements controller.Controller interface.
//
//nolint:gocyclo
func (ctrl *ZswapConfigController) Run(ctx context.Context, r controller.Runtime, _ *zap.Logger) error {
for {
select {
case <-r.EventCh():
case <-ctx.Done():
return nil
}

// load config if present
cfg, err := safe.ReaderGetByID[*config.MachineConfig](ctx, r, config.ActiveID)
if err != nil && !state.IsNotFoundError(err) {
return fmt.Errorf("error fetching machine configuration")
}

r.StartTrackingOutputs()

var zswapCfg configconfig.ZswapConfig

if cfg != nil {
zswapCfg = cfg.Config().ZswapConfig()
}

if zswapCfg != nil { // enabled
if err := safe.WriterModify(ctx, r, runtime.NewKernelParamSpec(runtime.NamespaceName, "sys.module.zswap.parameters.enabled"),
func(p *runtime.KernelParamSpec) error {
p.TypedSpec().Value = "Y"

return nil
}); err != nil {
return fmt.Errorf("error setting zswap config: %w", err)
}

if err := safe.WriterModify(ctx, r, runtime.NewKernelParamSpec(runtime.NamespaceName, "sys.module.zswap.parameters.max_pool_percent"),
func(p *runtime.KernelParamSpec) error {
p.TypedSpec().Value = strconv.Itoa(zswapCfg.MaxPoolPercent())

return nil
}); err != nil {
return fmt.Errorf("error setting zswap config: %w", err)
}

if err := safe.WriterModify(ctx, r, runtime.NewKernelParamSpec(runtime.NamespaceName, "sys.module.zswap.parameters.shrinker_enabled"),
func(p *runtime.KernelParamSpec) error {
if zswapCfg.ShrinkerEnabled() {
p.TypedSpec().Value = "Y"
} else {
p.TypedSpec().Value = "N"
}

return nil
}); err != nil {
return fmt.Errorf("error setting zswap config: %w", err)
}
}

if err = safe.CleanupOutputs[*runtime.KernelParamSpec](ctx, r); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is cleaning up enough, or do we need to explicitly set sys.module.zswap.parameters.enabled to N?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, cleaning up is enough - the kernel param controller reverts the value to the previous setting ('N') if the spec is removed

return fmt.Errorf("error cleaning up volume configuration: %w", err)
}
}
}
131 changes: 131 additions & 0 deletions internal/app/machined/pkg/controllers/block/zswap_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package block

import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"time"

"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/dustin/go-humanize"
"go.uber.org/zap"

machineruntime "github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
"github.com/siderolabs/talos/pkg/machinery/resources/block"
"github.com/siderolabs/talos/pkg/machinery/resources/runtime"
)

// ZswapStatusController provides a view of active swap devices.
type ZswapStatusController struct {
V1Alpha1Mode machineruntime.Mode
}

// Name implements controller.Controller interface.
func (ctrl *ZswapStatusController) Name() string {
return "block.ZswapStatusController"
}

// Inputs implements controller.Controller interface.
func (ctrl *ZswapStatusController) Inputs() []controller.Input {
return []controller.Input{
{
// not really a dependency, but we refresh zswap status kernel param change
Namespace: runtime.NamespaceName,
Type: runtime.KernelParamStatusType,
Kind: controller.InputWeak,
},
}
}

// Outputs implements controller.Controller interface.
func (ctrl *ZswapStatusController) Outputs() []controller.Output {
return []controller.Output{
{
Type: block.ZswapStatusType,
Kind: controller.OutputExclusive,
},
}
}

// Run implements controller.Controller interface.
//
//nolint:gocyclo
func (ctrl *ZswapStatusController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
// in container mode, no zswap applies
if ctrl.V1Alpha1Mode == machineruntime.ModeContainer {
return nil
}

// there is no way to watch for zswap status, so we are going to poll every minute
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
return nil
case <-r.EventCh():
case <-ticker.C:
}

r.StartTrackingOutputs()

// try to read a single status file to see if zswap is enabled
if _, err := os.ReadFile("/sys/kernel/debug/zswap/pool_total_size"); err == nil {
if err = safe.WriterModify(ctx, r, block.NewZswapStatus(block.NamespaceName, block.ZswapStatusID),
func(zs *block.ZswapStatus) error {
for _, p := range []struct {
name string
out *uint64
}{
{"pool_total_size", &zs.TypedSpec().TotalSizeBytes},
{"stored_pages", &zs.TypedSpec().StoredPages},
{"pool_limit_hit", &zs.TypedSpec().PoolLimitHit},
{"reject_reclaim_fail", &zs.TypedSpec().RejectReclaimFail},
{"reject_alloc_fail", &zs.TypedSpec().RejectAllocFail},
{"reject_kmemcache_fail", &zs.TypedSpec().RejectKmemcacheFail},
{"reject_compress_fail", &zs.TypedSpec().RejectCompressFail},
{"reject_compress_poor", &zs.TypedSpec().RejectCompressPoor},
{"written_back_pages", &zs.TypedSpec().WrittenBackPages},
} {
if err := ctrl.readZswapParam(p.name, p.out); err != nil {
return err
}
}

zs.TypedSpec().TotalSizeHuman = humanize.IBytes(zs.TypedSpec().TotalSizeBytes)

return nil
},
); err != nil {
return fmt.Errorf("failed to create zswap status: %w", err)
}
}

if err := safe.CleanupOutputs[*block.ZswapStatus](ctx, r); err != nil {
return fmt.Errorf("failed to cleanup outputs: %w", err)
}
}
}

func (ctrl *ZswapStatusController) readZswapParam(name string, out *uint64) error {
content, err := os.ReadFile(filepath.Join("/sys/kernel/debug/zswap", name))
if err != nil {
return fmt.Errorf("failed to read zswap parameter %q: %w", name, err)
}

*out, err = strconv.ParseUint(string(bytes.TrimSpace(content)), 10, 64)
if err != nil {
return fmt.Errorf("failed to parse zswap parameter %q: %w", name, err)
}

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error
V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(),
},
&block.VolumeManagerController{},
&block.ZswapConfigController{},
&block.ZswapStatusController{
V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(),
},
&cluster.AffiliateMergeController{},
cluster.NewConfigController(),
&cluster.DiscoveryServiceController{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func NewState() (*State, error) {
&block.VolumeMountRequest{},
&block.VolumeMountStatus{},
&block.VolumeStatus{},
&block.ZswapStatus{},
&cluster.Affiliate{},
&cluster.Config{},
&cluster.Identity{},
Expand Down
14 changes: 8 additions & 6 deletions internal/app/machined/pkg/startup/cgroups.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,10 @@ func CreateSystemCgroups(ctx context.Context, log *zap.Logger, rt runtime.Runtim
name: constants.CgroupApid,
resources: &cgroup2.Resources{
Memory: &cgroup2.Memory{
Min: pointer.To[int64](constants.CgroupApidReservedMemory),
Low: pointer.To[int64](constants.CgroupApidReservedMemory * 2),
Max: zeroIfRace(pointer.To[int64](constants.CgroupApidMaxMemory)),
Min: pointer.To[int64](constants.CgroupApidReservedMemory),
Low: pointer.To[int64](constants.CgroupApidReservedMemory * 2),
Max: zeroIfRace(pointer.To[int64](constants.CgroupApidMaxMemory)),
Swap: pointer.To[int64](0),
},
CPU: &cgroup2.CPU{
Weight: pointer.To[uint64](cgroup.MillicoresToCPUWeight(cgroup.MilliCores(constants.CgroupApidMillicores))),
Expand All @@ -165,9 +166,10 @@ func CreateSystemCgroups(ctx context.Context, log *zap.Logger, rt runtime.Runtim
name: constants.CgroupTrustd,
resources: &cgroup2.Resources{
Memory: &cgroup2.Memory{
Min: pointer.To[int64](constants.CgroupTrustdReservedMemory),
Low: pointer.To[int64](constants.CgroupTrustdReservedMemory * 2),
Max: zeroIfRace(pointer.To[int64](constants.CgroupTrustdMaxMemory)),
Min: pointer.To[int64](constants.CgroupTrustdReservedMemory),
Low: pointer.To[int64](constants.CgroupTrustdReservedMemory * 2),
Max: zeroIfRace(pointer.To[int64](constants.CgroupTrustdMaxMemory)),
Swap: pointer.To[int64](0),
},
CPU: &cgroup2.CPU{
Weight: pointer.To[uint64](cgroup.MillicoresToCPUWeight(cgroup.MilliCores(constants.CgroupTrustdMillicores))),
Expand Down
23 changes: 23 additions & 0 deletions internal/integration/api/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,29 @@ func (suite *VolumesSuite) TestSwapOnOff() {
}))
}

// TestZswapStatus verifies that all zswap-enabled machines have zswap running.
func (suite *VolumesSuite) TestZswapStatus() {
for _, node := range suite.DiscoverNodeInternalIPs(suite.ctx) {
suite.Run(node, func() {
ctx := client.WithNode(suite.ctx, node)

cfg, err := suite.ReadConfigFromNode(ctx)
suite.Require().NoError(err)

if cfg.ZswapConfig() == nil {
suite.T().Skipf("skipping test, zswap is not enabled on node %s", node)
}

rtestutils.AssertResource(ctx, suite.T(), suite.Client.COSI,
block.ZswapStatusID,
func(vs *block.ZswapStatus, asrt *assert.Assertions) {
suite.T().Logf("zswap total size %s, stored pages %d", vs.TypedSpec().TotalSizeHuman, vs.TypedSpec().StoredPages)
},
)
})
}
}

func init() {
allSuites = append(allSuites, new(VolumesSuite))
}
10 changes: 10 additions & 0 deletions internal/pkg/cgroups/cgroups.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ type Node struct {
MemorySwapMax Value
MemorySwapPeak Value

MemoryZswapCurrent Value
MemoryZswapMax Value
MemoryZswapWriteback Value

PidsCurrent Value
PidsEvents FlatMap
PidsMax Value
Expand Down Expand Up @@ -280,6 +284,12 @@ func (n *Node) Parse(filename string, r io.Reader) error {
return parseSingleValue(ParseNewlineSeparatedValues, &n.MemorySwapMax, r)
case "memory.swap.peak":
return parseSingleValue(ParseNewlineSeparatedValues, &n.MemorySwapPeak, r)
case "memory.zswap.current":
return parseSingleValue(ParseNewlineSeparatedValues, &n.MemoryZswapCurrent, r)
case "memory.zswap.max":
return parseSingleValue(ParseNewlineSeparatedValues, &n.MemoryZswapMax, r)
case "memory.zswap.writeback":
return parseSingleValue(ParseNewlineSeparatedValues, &n.MemoryZswapWriteback, r)
case "pids.current":
return parseSingleValue(ParseNewlineSeparatedValues, &n.PidsCurrent, r)
case "pids.events":
Expand Down
Loading