Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
41 changes: 40 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,27 @@ 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.

**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.

## Examples

### Limit CPU and Memory
Expand All @@ -147,3 +170,19 @@ 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.

### Combined Resource Limitation

```bash
sudo giogo --cpu=0.5 --ram=512m --network-class-id=200 --network-priority=75 -- 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 priority set to 75.
28 changes: 22 additions & 6 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import (
)

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

func SetupRootCommand(rootCmd *cobra.Command) {
Expand All @@ -28,6 +30,8 @@ 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")
}

func Execute() {
Expand All @@ -41,7 +45,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 string) ([]limiter.ResourceLimiter, error) {
var limiters []limiter.ResourceLimiter

if cpu != "" {
Expand Down Expand Up @@ -89,11 +93,23 @@ 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 != "" {
netInit := limiter.NetworkLimiterInitializer{
ClassID: networkClassID,
Priority: networkPriority,
}
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)
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
92 changes: 92 additions & 0 deletions internal/limiter/network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package limiter

import (
"errors"
"fmt"
"strconv"

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}
)

// NetworkLimiter applies network resource limits
type NetworkLimiter struct {
ClassID *uint32
Priority *uint32
}

// 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
}

// 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
}

return limiter, nil
}
Loading