Skip to content

Commit

Permalink
Merge pull request #809 from garden-io/container-limits-2
Browse files Browse the repository at this point in the history
feat(container): add configurable CPU and memory limits
  • Loading branch information
thsig committed May 31, 2019
2 parents df47c3c + 77e71df commit dba8982
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 7 deletions.
27 changes: 27 additions & 0 deletions docs/reference/module-types/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,30 @@ If this module uses the `hotReload` field, the container will be run with these
| Type | Required |
| ---- | -------- |
| `array[string]` | No
### `services[].limits`
[services](#services) > limits

Specify resource limits for the service.

| Type | Required |
| ---- | -------- |
| `object` | No
### `services[].limits.cpu`
[services](#services) > [limits](#services[].limits) > cpu

The maximum amount of CPU the service can use, in millicpus (i.e. 1000 = 1 CPU)

| Type | Required |
| ---- | -------- |
| `number` | No
### `services[].limits.memory`
[services](#services) > [limits](#services[].limits) > memory

The maximum amount of RAM the service can use, in megabytes (i.e. 1024 = 1 GB)

| Type | Required |
| ---- | -------- |
| `number` | No
### `services[].ports[]`
[services](#services) > ports

Expand Down Expand Up @@ -672,6 +696,9 @@ services:
command:
tcpPort:
hotReloadArgs:
limits:
cpu: 1000
memory: 1024
ports:
- name:
protocol: TCP
Expand Down
27 changes: 27 additions & 0 deletions docs/reference/module-types/maven-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,30 @@ If this module uses the `hotReload` field, the container will be run with these
| Type | Required |
| ---- | -------- |
| `array[string]` | No
### `services[].limits`
[services](#services) > limits

Specify resource limits for the service.

| Type | Required |
| ---- | -------- |
| `object` | No
### `services[].limits.cpu`
[services](#services) > [limits](#services[].limits) > cpu

The maximum amount of CPU the service can use, in millicpus (i.e. 1000 = 1 CPU)

| Type | Required |
| ---- | -------- |
| `number` | No
### `services[].limits.memory`
[services](#services) > [limits](#services[].limits) > memory

The maximum amount of RAM the service can use, in megabytes (i.e. 1024 = 1 GB)

| Type | Required |
| ---- | -------- |
| `number` | No
### `services[].ports[]`
[services](#services) > ports

Expand Down Expand Up @@ -703,6 +727,9 @@ services:
command:
tcpPort:
hotReloadArgs:
limits:
cpu: 1000
memory: 1024
ports:
- name:
protocol: TCP
Expand Down
26 changes: 26 additions & 0 deletions garden-service/src/plugins/container/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ import { baseTaskSpecSchema, BaseTaskSpec } from "../../config/task"
import { baseTestSpecSchema, BaseTestSpec } from "../../config/test"
import { joiStringMap } from "../../config/common"

export const defaultContainerLimits: ServiceLimitSpec = {
cpu: 1000, // = 1000 millicpu = 1 CPU
memory: 1024, // = 1024MB = 1GB
}

export interface ContainerIngressSpec {
annotations: Annotations
hostname?: string
Expand Down Expand Up @@ -61,6 +66,11 @@ export interface ServiceHealthCheckSpec {
tcpPort?: string,
}

export interface ServiceLimitSpec {
cpu: number
memory: number
}

interface Annotations {
[name: string]: string
}
Expand All @@ -73,6 +83,7 @@ export interface ContainerServiceSpec extends CommonServiceSpec {
env: PrimitiveMap,
healthCheck?: ServiceHealthCheckSpec,
hotReloadArgs?: string[],
limits: ServiceLimitSpec,
ports: ServicePortSpec[],
volumes: ServiceVolumeSpec[],
}
Expand Down Expand Up @@ -150,6 +161,18 @@ const healthCheckSchema = Joi.object()
.description("Set this to check the service's health by checking if this TCP port is accepting connections."),
}).xor("httpGet", "command", "tcpPort")

const limitsSchema = Joi.object()
.keys({
cpu: Joi.number()
.default(defaultContainerLimits.cpu)
.min(10)
.description("The maximum amount of CPU the service can use, in millicpus (i.e. 1000 = 1 CPU)"),
memory: Joi.number()
.default(defaultContainerLimits.memory)
.min(64)
.description("The maximum amount of RAM the service can use, in megabytes (i.e. 1024 = 1 GB)"),
})

export const portSchema = Joi.object()
.keys({
name: joiUserIdentifier()
Expand Down Expand Up @@ -218,6 +241,9 @@ const serviceSchema = baseServiceSpecSchema
these arguments instead of those in \`args\` when the service is deployed
with hot reloading enabled.`,
),
limits: limitsSchema
.description("Specify resource limits for the service.")
.default(() => defaultContainerLimits, JSON.stringify(defaultContainerLimits)),
ports: joiArray(portSchema)
.unique("name")
.description("List of ports that the service container exposes."),
Expand Down
2 changes: 2 additions & 0 deletions garden-service/src/plugins/google/google-cloud-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Provider, providerConfigBaseSchema } from "../../config/project"
import { ConfigureModuleParams, ConfigureModuleResult } from "../../types/plugin/module/configure"
import { DeployServiceParams } from "../../types/plugin/service/deployService"
import { GetServiceStatusParams } from "../../types/plugin/service/getServiceStatus"
import { ServiceLimitSpec } from "../container/config"

const gcfModuleSpecSchema = baseServiceSpecSchema
.keys({
Expand All @@ -44,6 +45,7 @@ export interface GcfModuleSpec extends CommonServiceSpec {
entrypoint?: string,
function: string,
hostname?: string
limits: ServiceLimitSpec
path: string,
project?: string,
tests: ExecTestSpec[],
Expand Down
9 changes: 4 additions & 5 deletions garden-service/src/plugins/kubernetes/container/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ import { containerHelpers } from "../../container/helpers"
import { LogEntry } from "../../../logger/log-entry"
import { DeployServiceParams } from "../../../types/plugin/service/deployService"
import { DeleteServiceParams } from "../../../types/plugin/service/deleteService"
import { millicpuToString, kilobytesToString } from "../util"

export const DEFAULT_CPU_REQUEST = "10m"
export const DEFAULT_CPU_LIMIT = "500m"
export const DEFAULT_MEMORY_REQUEST = "128Mi"
export const DEFAULT_MEMORY_LIMIT = "512Mi"
export const DEFAULT_MEMORY_REQUEST = "64Mi"

export async function deployContainerService(params: DeployServiceParams<ContainerModule>): Promise<ServiceStatus> {
const { ctx, service, runtimeContext, force, log, hotReload } = params
Expand Down Expand Up @@ -127,8 +126,8 @@ export async function createDeployment(
memory: DEFAULT_MEMORY_REQUEST,
},
limits: {
cpu: DEFAULT_CPU_LIMIT,
memory: DEFAULT_MEMORY_LIMIT,
cpu: millicpuToString(spec.limits.cpu),
memory: kilobytesToString(spec.limits.memory * 1024),
},
},
imagePullPolicy: "IfNotPresent",
Expand Down
36 changes: 36 additions & 0 deletions garden-service/src/plugins/kubernetes/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,39 @@ export async function getPortForward(
})
})
}

/**
* Converts the given number of millicpus (1000 mcpu = 1 CPU) to a string suitable for use in pod resource limit specs.
*/
export function millicpuToString(mcpu: number) {
mcpu = Math.floor(mcpu)

if (mcpu % 1000 === 0) {
return (mcpu / 1000).toString(10)
} else {
return `${mcpu}m`
}
}

/**
* Converts the given number of kilobytes to a string suitable for use in pod resource limit specs.
*/
export function kilobytesToString(kb: number) {
kb = Math.floor(kb)

for (const [suffix, power] of Object.entries(suffixTable)) {
if (kb % (1024 ** power) === 0) {
return `${(kb / (1024 ** power))}${suffix}`
}
}

return `${kb}Ki`
}

const suffixTable = {
Ei: 5,
Pi: 4,
Ti: 3,
Gi: 2,
Mi: 1,
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const gardenPlugin = (): GardenPlugin => ({
}],
env: {},
healthCheck: { tcpPort: "http" },
limits: s.spec.limits,
ports: [
{
name: "http",
Expand Down
21 changes: 20 additions & 1 deletion garden-service/test/unit/src/plugins/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import {
import { moduleFromConfig } from "../../../../src/types/module"
import { ModuleConfig } from "../../../../src/config/module"
import { LogEntry } from "../../../../src/logger/log-entry"
import { ContainerModuleSpec, ContainerModuleConfig } from "../../../../src/plugins/container/config"
import {
ContainerModuleSpec,
ContainerModuleConfig,
defaultContainerLimits,
} from "../../../../src/plugins/container/config"
import { containerHelpers as helpers, minDockerVersion } from "../../../../src/plugins/container/helpers"

describe("plugins.container", () => {
Expand Down Expand Up @@ -379,6 +383,10 @@ describe("plugins.container", () => {
port: "http",
},
},
limits: {
cpu: 123,
memory: 456,
},
ports: [{
name: "http",
protocol: "TCP",
Expand Down Expand Up @@ -445,6 +453,10 @@ describe("plugins.container", () => {
},
healthCheck:
{ httpGet: { path: "/health", port: "http" } },
limits: {
cpu: 123,
memory: 456,
},
ports: [{ name: "http", protocol: "TCP", containerPort: 8080, servicePort: 8080 }],
volumes: [],
}],
Expand Down Expand Up @@ -491,6 +503,10 @@ describe("plugins.container", () => {
},
healthCheck:
{ httpGet: { path: "/health", port: "http" } },
limits: {
cpu: 123,
memory: 456,
},
ports: [{ name: "http", protocol: "TCP", containerPort: 8080, servicePort: 8080 }],
volumes: [],
},
Expand Down Expand Up @@ -560,6 +576,7 @@ describe("plugins.container", () => {
port: "bla",
},
],
limits: defaultContainerLimits,
env: {},
ports: [],
volumes: [],
Expand Down Expand Up @@ -620,6 +637,7 @@ describe("plugins.container", () => {
port: "bla",
},
},
limits: defaultContainerLimits,
ports: [],
volumes: [],
}],
Expand Down Expand Up @@ -670,6 +688,7 @@ describe("plugins.container", () => {
healthCheck: {
tcpPort: "bla",
},
limits: defaultContainerLimits,
ports: [],
volumes: [],
}],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { dataDir, makeTestGarden, expectError } from "../../../../../helpers"
import { Garden } from "../../../../../../src/garden"
import { moduleFromConfig } from "../../../../../../src/types/module"
import { createIngressResources } from "../../../../../../src/plugins/kubernetes/container/ingress"
import { defaultContainerLimits } from "../../../../../../src/plugins/container/config"
import {
ServicePortProtocol,
ContainerIngressSpec,
Expand Down Expand Up @@ -326,8 +327,9 @@ describe("createIngressResources", () => {
args: [],
daemon: false,
dependencies: [],
ingresses,
env: {},
ingresses,
limits: defaultContainerLimits,
ports,
volumes: [],
}
Expand Down
46 changes: 46 additions & 0 deletions garden-service/test/unit/src/plugins/kubernetes/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { expect } from "chai"
import { millicpuToString, kilobytesToString } from "../../../../../src/plugins/kubernetes/util"

describe("millicpuToString", () => {
it("should return a string suffixed with 'm'", () => {
expect(millicpuToString(300)).to.equal("300m")
})

it("should return whole thousands as a single integer string", () => {
expect(millicpuToString(3000)).to.equal("3")
})

it("should round off floating points", () => {
expect(millicpuToString(100.5)).to.equal("100m")
})
})

describe("kilobytesToString", () => {
it("should return whole exabytes with an Ei suffix", () => {
expect(kilobytesToString(2 * (1024 ** 5))).to.equal("2Ei")
})

it("should return whole petabytes with a Pi suffix", () => {
expect(kilobytesToString(3 * (1024 ** 4))).to.equal("3Pi")
})

it("should return whole terabytes with a Ti suffix", () => {
expect(kilobytesToString(1 * (1024 ** 3))).to.equal("1Ti")
})

it("should return whole gigabytes with a Gi suffix", () => {
expect(kilobytesToString(7 * (1024 ** 2))).to.equal("7Gi")
})

it("should return whole megabytes with an Mi suffix", () => {
expect(kilobytesToString(2 * (1024 ** 1))).to.equal("2Mi")
})

it("should otherwise return the kilobytes with a Ki suffix", () => {
expect(kilobytesToString(1234)).to.equal("1234Ki")
})

it("should round off floating points", () => {
expect(kilobytesToString(100.5)).to.equal("100Ki")
})
})

0 comments on commit dba8982

Please sign in to comment.