Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 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
63 changes: 62 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Giogo

Giogo is a command-line tool that allows you to run processes with specified resource limitations using Linux cgroups.
It provides an easy-to-use interface to limit CPU, memory, and IO resources for a process and its children.
It provides an easy-to-use interface to limit CPU, memory, IO, and network resources for a process and its children.

**Note: Root privileges are required, and cgroups v1 is currently not supported.**

Expand All @@ -16,13 +16,15 @@ It provides an easy-to-use interface to limit CPU, memory, and IO resources for
- [CPU Limitations](#cpu-limitations)
- [Memory Limitations](#memory-limitations)
- [IO Limitations](#io-limitations)
- [Network Limitations](#network-limitations)
- [Examples](#examples)

## Features

- **CPU Limiting**: Restrict CPU usage as a fraction of total CPU time.
- **Memory Limiting**: Set maximum memory usage.
- **IO Limiting**: Control IO read and write bandwidth.
- **Network Limiting**: Set network class identifiers and priorities for network traffic.
- **Cgroups Support**: Works with cgroups v2 only (cgroups v1 is not supported at this time).
- **Process Isolation**: Limits apply to the process and all its child processes.

Expand Down Expand Up @@ -122,6 +124,41 @@ By default, Giogo sets a bandwidth throttle on every block device's IO. The Linu
**Additional Note:**
If your operations utilize the `O_DIRECT` flag, the RAM limit is not required, as `O_DIRECT` bypasses the kernel's caching mechanism.

### Network Limitations

- **`--network-class-id=VALUE`**

Set a class identifier for the container's network packets.

- **`VALUE`**: A numeric identifier (uint32) used for packet classification and traffic control.
- **Example**: `--network-class-id=100` sets the network class identifier to 100.
- **Use Case**: This can be used in conjunction with Linux traffic control (tc) for advanced network QoS configuration.

- **`--network-priority=VALUE`**

Set the priority of network traffic for the container.

- **`VALUE`**: A numeric priority value (uint32), where higher values typically indicate higher priority.
- **Example**: `--network-priority=50` sets the network traffic priority to 50.
- **Use Case**: Helps prioritize network traffic when multiple processes compete for bandwidth.

- **`--network-max-bandwidth=VALUE`**

Set a maximum network bandwidth limit for the container. Requires `--network-class-id` to be set.

- **`VALUE`**: Maximum bandwidth using the same notation as memory (`k`, `m`, `g`).
- **Units**:
- `k` or `K`: Kilobytes per second
- `m` or `M`: Megabytes per second
- `g` or `G`: Gigabytes per second
- **Example**: `--network-max-bandwidth=1m` limits network bandwidth to 1 MB/s.
- **Use Case**: Enforces hard bandwidth limits on network traffic using Linux traffic control (tc) with HTB qdisc.

**Note:**
Network limitations work with cgroups v2's network controller to provide packet classification and prioritization. The priority setting applies to all network interfaces in the container.

When `--network-max-bandwidth` is specified with `--network-class-id`, giogo automatically configures Linux traffic control (tc) with HTB (Hierarchical Token Bucket) to enforce the bandwidth limit. The tc rules are automatically cleaned up when the process exits.

## Examples

### Limit CPU and Memory
Expand All @@ -147,3 +184,27 @@ sudo giogo --io-read-max=2m --io-write-max=1m --ram=2g -- your_io_intensive_comm
```

- **Description**: Runs `your_io_intensive_command` with IO read limited to 2 MB/s and IO write limited to 1 MB/s, while setting a high RAM limit of 2 GB to bypass the default association between `io-write-max` and RAM usage.

### Network Traffic Control

```bash
sudo giogo --network-class-id=100 --network-priority=50 -- your_network_intensive_app
```

- **Description**: Runs `your_network_intensive_app` with network class identifier set to 100 and network priority set to 50, allowing for packet classification and traffic prioritization.

### Network Bandwidth Limiting

```bash
sudo giogo --network-class-id=100 --network-max-bandwidth=1m -- your_app
```

- **Description**: Runs `your_app` with network bandwidth limited to 1 MB/s. This automatically configures traffic control (tc) with HTB qdisc to enforce the limit.

### Combined Resource Limitation

```bash
sudo giogo --cpu=0.5 --ram=512m --network-class-id=200 --network-max-bandwidth=500k -- your_app
```

- **Description**: Runs `your_app` with CPU limited to 50% of one core, RAM limited to 512 MB, network class identifier set to 200, and network bandwidth limited to 500 KB/s.
31 changes: 25 additions & 6 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ import (
)

var (
ram string
cpu string
ioReadMax string
ioWriteMax string
ram string
cpu string
ioReadMax string
ioWriteMax string
networkClassID string
networkPriority string
networkMaxBandwidth string
)

func SetupRootCommand(rootCmd *cobra.Command) {
Expand All @@ -28,6 +31,9 @@ func SetupRootCommand(rootCmd *cobra.Command) {
rootCmd.Flags().StringVar(&cpu, "cpu", "", "CPU limit as a fraction between 0 and 1 (e.g., 0.5)")
rootCmd.Flags().StringVar(&ioReadMax, "io-read-max", limiter.UnlimitedIOValue, "IO read max bandwidth (e.g., 128k, 1m)")
rootCmd.Flags().StringVar(&ioWriteMax, "io-write-max", limiter.UnlimitedIOValue, "IO write max bandwidth (e.g., 128k, 1m)")
rootCmd.Flags().StringVar(&networkClassID, "network-class-id", "", "Network class identifier for container's network packets")
rootCmd.Flags().StringVar(&networkPriority, "network-priority", "", "Network priority for container's network traffic")
rootCmd.Flags().StringVar(&networkMaxBandwidth, "network-max-bandwidth", "", "Maximum network bandwidth (e.g., 1m, 500k) - requires network-class-id")
}

func Execute() {
Expand All @@ -41,7 +47,7 @@ func Execute() {
}

// TODO: This logic should be moved to a separate package as it's part of the core functionality
func CreateLimiters(cpu, ram, ioReadMax, ioWriteMax string) ([]limiter.ResourceLimiter, error) {
func CreateLimiters(cpu, ram, ioReadMax, ioWriteMax, networkClassID, networkPriority, networkMaxBandwidth string) ([]limiter.ResourceLimiter, error) {
var limiters []limiter.ResourceLimiter

if cpu != "" {
Expand Down Expand Up @@ -89,11 +95,24 @@ func CreateLimiters(cpu, ram, ioReadMax, ioWriteMax string) ([]limiter.ResourceL
// Known issue: a minimum amount of memory is required to start a process, so if the memory limit is too low, the process will not start.
}

if networkClassID != "" || networkPriority != "" || networkMaxBandwidth != "" {
netInit := limiter.NetworkLimiterInitializer{
ClassID: networkClassID,
Priority: networkPriority,
MaxBandwidth: networkMaxBandwidth,
}
netLimiter, err := limiter.NewNetworkLimiter(&netInit)
if err != nil {
return nil, fmt.Errorf("invalid network value: %v", err)
}
limiters = append(limiters, netLimiter)
}

return limiters, nil
}

func runCommand(cmd *cobra.Command, args []string) error {
limiters, err := CreateLimiters(cpu, ram, ioReadMax, ioWriteMax)
limiters, err := CreateLimiters(cpu, ram, ioReadMax, ioWriteMax, networkClassID, networkPriority, networkMaxBandwidth)
if err != nil {
return err
}
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestCreateLimiters_IOLimitsAndNoRAM(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}

limiters, err := cli.CreateLimiters(cpu, ram, ioReadMax, ioWriteMax)
limiters, err := cli.CreateLimiters(cpu, ram, ioReadMax, ioWriteMax, "", "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down Expand Up @@ -99,7 +99,7 @@ func TestCreateLimiters_IOLimitsAndNoRAM_ReadLimit(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}

limiters, err := cli.CreateLimiters(cpu, ram, ioReadMax, ioWriteMax)
limiters, err := cli.CreateLimiters(cpu, ram, ioReadMax, ioWriteMax, "", "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down Expand Up @@ -130,7 +130,7 @@ func TestCreateLimiters_IOLimitsAndRAM(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}

limiters, err := cli.CreateLimiters(cpu, ram, ioReadMax, ioWriteMax)
limiters, err := cli.CreateLimiters(cpu, ram, ioReadMax, ioWriteMax, "", "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down
37 changes: 35 additions & 2 deletions internal/executor/executor.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
package executor

import (
"fmt"
"os"

"github.com/pmarchini/giogo/internal/core"
"github.com/pmarchini/giogo/internal/limiter"

specs "github.com/opencontainers/runtime-spec/specs-go"
)

type Executor struct {
Limiters []limiter.ResourceLimiter
Limiters []limiter.ResourceLimiter
NetworkLimiter *limiter.NetworkLimiter
}

func NewExecutor(limiters []limiter.ResourceLimiter) *Executor {
return &Executor{
executor := &Executor{
Limiters: limiters,
}

// Extract NetworkLimiter if present for special handling
for _, l := range limiters {
if netLimiter, ok := l.(*limiter.NetworkLimiter); ok {
executor.NetworkLimiter = netLimiter
break
}
}

return executor
Copy link
Owner

Choose a reason for hiding this comment

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

We should not break the design having special handling here

Copy link
Author

Choose a reason for hiding this comment

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

Fixed in commit 595e495. Refactored to use LifecycleLimiter interface instead of special handling:

  • Added optional LifecycleLimiter interface with Setup/Cleanup methods
  • Executor uses type assertion to check for the interface
  • No special NetworkLimiter field or hardcoded logic
  • Any limiter can implement LifecycleLimiter if needed

The design now maintains clean separation of concerns with interface-based patterns.

}

func (e *Executor) RunCommand(args []string) error {
Expand All @@ -23,6 +37,25 @@ func (e *Executor) RunCommand(args []string) error {
l.Apply(&resources)
}

// Set up traffic control if network limiter with bandwidth is configured
if e.NetworkLimiter != nil && e.NetworkLimiter.MaxBandwidth > 0 {
Copy link
Owner

Choose a reason for hiding this comment

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

Same as above

Copy link
Author

Choose a reason for hiding this comment

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

Fixed in commit 595e495. Removed all special handling by using the LifecycleLimiter interface pattern. The executor now treats all limiters uniformly through interfaces.

// Get default interface - in production this could be configurable
iface := limiter.GetDefaultInterface()

// Setup tc before running the command
if err := e.NetworkLimiter.SetupTrafficControl(iface); err != nil {
return fmt.Errorf("failed to setup traffic control: %v", err)
}

// Ensure cleanup happens when we're done
defer func() {
if err := e.NetworkLimiter.CleanupTrafficControl(iface); err != nil {
// Non-fatal: log the error but don't fail the command
fmt.Fprintf(os.Stderr, "Warning: failed to cleanup traffic control: %v\n", err)
}
}()
}

coreModule, err := core.NewCore(resources)
if err != nil {
return err
Expand Down
148 changes: 148 additions & 0 deletions internal/limiter/network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package limiter

import (
"errors"
"fmt"
"strconv"
"strings"

specs "github.com/opencontainers/runtime-spec/specs-go"
)

// Base error for NetworkLimiter
var ErrInvalidNetworkValue = errors.New("invalid network limiter value")

// NetworkLimiterError represents a custom error with a specific message and underlying cause
type NetworkLimiterError struct {
Message string
Cause error
}

func (e *NetworkLimiterError) Error() string {
return fmt.Sprintf("%s: %v", e.Message, e.Cause)
}

func (e *NetworkLimiterError) Is(target error) bool {
return errors.Is(e.Cause, target)
}

// Custom errors
var (
ErrUnparsableClassID = &NetworkLimiterError{Message: "unparsable class ID value", Cause: ErrInvalidNetworkValue}
ErrUnparsablePriority = &NetworkLimiterError{Message: "unparsable priority value", Cause: ErrInvalidNetworkValue}
ErrUnparsableBandwidth = &NetworkLimiterError{Message: "unparsable bandwidth value", Cause: ErrInvalidNetworkValue}
)

// NetworkLimiter applies network resource limits
type NetworkLimiter struct {
ClassID *uint32
Priority *uint32
MaxBandwidth uint64 // Maximum bandwidth in bytes per second (0 means unlimited)
}

// Apply the network limits to the provided Linux resources
func (n *NetworkLimiter) Apply(resources *specs.LinuxResources) {
if resources.Network == nil {
resources.Network = &specs.LinuxNetwork{}
}

if n.ClassID != nil {
resources.Network.ClassID = n.ClassID
}

if n.Priority != nil {
// Note: The spec uses LinuxInterfacePriority which requires an interface name
// We use an empty string to apply the priority to all interfaces
// This can be extended in the future to support per-interface priorities
resources.Network.Priorities = []specs.LinuxInterfacePriority{
{
Name: "", // Empty string applies to all interfaces
Priority: *n.Priority,
},
}
}
}

// NetworkLimiterInitializer holds the initialization parameters for NetworkLimiter
type NetworkLimiterInitializer struct {
ClassID string
Priority string
MaxBandwidth string
}

// NewNetworkLimiter creates a new NetworkLimiter with validation and error handling
func NewNetworkLimiter(init *NetworkLimiterInitializer) (*NetworkLimiter, error) {
limiter := &NetworkLimiter{}

if init.ClassID != "" {
classID, err := strconv.ParseUint(init.ClassID, 10, 32)
if err != nil {
return nil, ErrUnparsableClassID
}
classIDValue := uint32(classID)
limiter.ClassID = &classIDValue
}

if init.Priority != "" {
priority, err := strconv.ParseUint(init.Priority, 10, 32)
if err != nil {
return nil, ErrUnparsablePriority
}
priorityValue := uint32(priority)
limiter.Priority = &priorityValue
}

if init.MaxBandwidth != "" {
bandwidth, err := parseBandwidth(init.MaxBandwidth)
if err != nil {
return nil, &NetworkLimiterError{Message: "unparsable bandwidth value", Cause: err}
}
limiter.MaxBandwidth = bandwidth
}

return limiter, nil
}

// parseBandwidth parses bandwidth string (e.g., "1m", "500k") to bytes per second
func parseBandwidth(s string) (uint64, error) {
s = strings.TrimSpace(s)
var multiplier int64 = 1
if strings.HasSuffix(s, "g") || strings.HasSuffix(s, "G") {
multiplier = 1024 * 1024 * 1024
s = s[:len(s)-1]
} else if strings.HasSuffix(s, "m") || strings.HasSuffix(s, "M") {
multiplier = 1024 * 1024
s = s[:len(s)-1]
} else if strings.HasSuffix(s, "k") || strings.HasSuffix(s, "K") {
multiplier = 1024
s = s[:len(s)-1]
} else {
multiplier = 1
}
value, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, err
}
return uint64(value * float64(multiplier)), nil
}

// SetupTrafficControl sets up tc (traffic control) rules for bandwidth limiting
// This method should be called after the cgroup is created and classID is set
func (n *NetworkLimiter) SetupTrafficControl(interfaceName string) error {
// Only setup tc if we have both classID and bandwidth limit
if n.ClassID == nil || n.MaxBandwidth == 0 {
return nil
}

return setupHTB(interfaceName, *n.ClassID, n.MaxBandwidth)
}

// CleanupTrafficControl removes tc rules set up by SetupTrafficControl
func (n *NetworkLimiter) CleanupTrafficControl(interfaceName string) error {
// Only cleanup if we have a classID (indicating we set up tc)
if n.ClassID == nil || n.MaxBandwidth == 0 {
return nil
}

return cleanupHTB(interfaceName)
}
Loading