fix task memory

prior to this patch we were allowing 256MB for every function run, just
because that was the default for the docker driver and we were not using the
memory field on any given route configuration. this fixes that, now docker
containers will get the correct memory limit passed into the container from
the route. the default is still 128.

there is also an env var now, `MEMORY_MB` that is set on each function call,
see the linked issue below for rationale.

closes #186

ran the given function code from #186, and now i only see allocations up to
32MB before the function is killed. yay.

notes:

there is no max for memory. for open source fn i'm not sure we want to
cap it, really. in the services repo we probably should add a cap before prod.
since we don't know any given fn server's ram, we can't try to make sure the
setting on any given route is something that can even be run.

remove envconfig & bytefmt

this updates the glide.yaml file to remove the unused deps, but trying to
install fresh is broken atm so i couldn't remove from vendor/, going to fix
separately (next update we just won't get these). also changed the skip dir to
be the cli dir now that its name has changed (related to brokenness).

fix how ram slots were being allocated. integer division is significantly
slower than subtraction.
This commit is contained in:
Reed Allman
2017-08-02 17:11:47 -07:00
parent ccea601028
commit 3ff28163db
11 changed files with 57 additions and 73 deletions

View File

@@ -72,6 +72,7 @@ func getCfg(t *models.Task) *task.Config {
Image: *t.Image, Image: *t.Image,
ID: t.ID, ID: t.ID,
AppName: t.AppName, AppName: t.AppName,
Memory: 128,
Env: t.EnvVars, Env: t.EnvVars,
Ready: make(chan struct{}), Ready: make(chan struct{}),
Stdin: strings.NewReader(t.Payload), Stdin: strings.NewReader(t.Payload),

View File

@@ -15,14 +15,14 @@ type HTTPSubHandler interface {
} }
type Config struct { type Config struct {
Interval float64 `json:"interval" envconfig:"STATS_INTERVAL"` // seconds Interval float64 `json:"interval"` // seconds
History int // minutes History int // minutes
Log string `json:"log" envconfig:"STATS_LOG"` Log string `json:"log"`
StatHat *StatHatReporterConfig StatHat *StatHatReporterConfig
NewRelic *NewRelicReporterConfig NewRelic *NewRelicReporterConfig
Statsd *StatsdConfig Statsd *StatsdConfig
GCStats int `json:"gc_stats" envconfig:"GC_STATS"` // seconds GCStats int `json:"gc_stats"`
} }
type Statter interface { type Statter interface {

View File

@@ -11,9 +11,9 @@ import (
) )
type StatsdConfig struct { type StatsdConfig struct {
StatsdUdpTarget string `json:"target" mapstructure:"target" envconfig:"STATSD_TARGET"` StatsdUdpTarget string `json:"target" mapstructure:"target"`
Interval int64 `json:"interval" envconfig:"STATSD_INTERVAL"` Interval int64 `json:"interval"`
Prefix string `json:"prefix" envconfig:"STATSD_PREFIX"` Prefix string `json:"prefix"`
} }
type keyCreator interface { type keyCreator interface {

View File

@@ -218,7 +218,6 @@ func (drv *DockerDriver) Prepare(ctx context.Context, task drivers.ContainerTask
var cmd []string var cmd []string
if task.Command() != "" { if task.Command() != "" {
// NOTE: this is hyper-sensitive and may not be correct like this even, but it passes old tests // NOTE: this is hyper-sensitive and may not be correct like this even, but it passes old tests
// task.Command() in swapi is always "sh /mnt/task/.runtask" so fields is safe
cmd = strings.Fields(task.Command()) cmd = strings.Fields(task.Command())
log.WithFields(logrus.Fields{"call_id": task.Id(), "cmd": cmd, "len": len(cmd)}).Debug("docker command") log.WithFields(logrus.Fields{"call_id": task.Id(), "cmd": cmd, "len": len(cmd)}).Debug("docker command")
} }
@@ -234,7 +233,7 @@ func (drv *DockerDriver) Prepare(ctx context.Context, task drivers.ContainerTask
Config: &docker.Config{ Config: &docker.Config{
Env: envvars, Env: envvars,
Cmd: cmd, Cmd: cmd,
Memory: int64(drv.conf.Memory), Memory: int64(task.Memory()),
CPUShares: drv.conf.CPUShares, CPUShares: drv.conf.CPUShares,
Hostname: drv.hostname, Hostname: drv.hostname,
Image: task.Image(), Image: task.Image(),

View File

@@ -4,14 +4,12 @@ import (
"bytes" "bytes"
"context" "context"
"io" "io"
"os"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/fnproject/fn/api/runner/common" "github.com/fnproject/fn/api/runner/common"
"github.com/fnproject/fn/api/runner/drivers" "github.com/fnproject/fn/api/runner/drivers"
"github.com/vrischmann/envconfig"
) )
type taskDockerTest struct { type taskDockerTest struct {
@@ -32,6 +30,7 @@ func (f *taskDockerTest) Timeout() time.Duration { return 30 * time.
func (f *taskDockerTest) Logger() (stdout, stderr io.Writer) { return f.output, nil } func (f *taskDockerTest) Logger() (stdout, stderr io.Writer) { return f.output, nil }
func (f *taskDockerTest) WriteStat(drivers.Stat) { /* TODO */ } func (f *taskDockerTest) WriteStat(drivers.Stat) { /* TODO */ }
func (f *taskDockerTest) Volumes() [][2]string { return [][2]string{} } func (f *taskDockerTest) Volumes() [][2]string { return [][2]string{} }
func (f *taskDockerTest) Memory() uint64 { return 256 * 1024 * 1024 }
func (f *taskDockerTest) WorkDir() string { return "" } func (f *taskDockerTest) WorkDir() string { return "" }
func (f *taskDockerTest) Close() {} func (f *taskDockerTest) Close() {}
func (f *taskDockerTest) Input() io.Reader { return f.input } func (f *taskDockerTest) Input() io.Reader { return f.input }
@@ -90,18 +89,3 @@ func TestRunnerDockerStdin(t *testing.T) {
t.Errorf("Test expected output to contain '%s', got '%s'", expect, got) t.Errorf("Test expected output to contain '%s', got '%s'", expect, got)
} }
} }
func TestConfigLoadMemory(t *testing.T) {
if err := os.Setenv("MEMORY_PER_JOB", "128M"); err != nil {
t.Fatalf("Could not set MEMORY_PER_JOB: %v", err)
}
var conf drivers.Config
if err := envconfig.Init(&conf); err != nil {
t.Fatalf("Could not read config: %v", err)
}
if conf.Memory != 128*1024*1024 {
t.Fatalf("Memory read from config should match 128M, got %d", conf.Memory)
}
}

View File

@@ -8,8 +8,6 @@ import (
"io" "io"
"strings" "strings"
"time" "time"
"code.cloudfoundry.org/bytefmt"
) )
// A DriverCookie identifies a unique request to run a task. // A DriverCookie identifies a unique request to run a task.
@@ -68,26 +66,41 @@ type RunResult interface {
type ContainerTask interface { type ContainerTask interface {
// Command returns the command to run within the container. // Command returns the command to run within the container.
Command() string Command() string
// EnvVars returns environment variable key-value pairs. // EnvVars returns environment variable key-value pairs.
EnvVars() map[string]string EnvVars() map[string]string
// Input feeds the container with data // Input feeds the container with data
Input() io.Reader Input() io.Reader
// Labels returns container label key-value pairs. // Labels returns container label key-value pairs.
Labels() map[string]string Labels() map[string]string
// The id to assign the container
Id() string Id() string
// Image returns the runtime specific image to run. // Image returns the runtime specific image to run.
Image() string Image() string
// Timeout specifies the maximum time a task is allowed to run. Return 0 to let it run forever. // Timeout specifies the maximum time a task is allowed to run. Return 0 to let it run forever.
Timeout() time.Duration Timeout() time.Duration
// Driver will write output log from task execution to these writers. Must be // Driver will write output log from task execution to these writers. Must be
// non-nil. Use io.Discard if log is irrelevant. // non-nil. Use io.Discard if log is irrelevant.
Logger() (stdout, stderr io.Writer) Logger() (stdout, stderr io.Writer)
// WriteStat writes a single Stat, implementation need not be thread safe. // WriteStat writes a single Stat, implementation need not be thread safe.
WriteStat(Stat) WriteStat(Stat)
// Volumes returns an array of 2-element tuples indicating storage volume mounts. // Volumes returns an array of 2-element tuples indicating storage volume mounts.
// The first element is the path on the host, and the second element is the // The first element is the path on the host, and the second element is the
// path in the container. // path in the container.
Volumes() [][2]string Volumes() [][2]string
// Memory determines the max amount of RAM given to the container to use.
// 0 is unlimited.
Memory() uint64
// WorkDir returns the working directory to use for the task. Empty string // WorkDir returns the working directory to use for the task. Empty string
// leaves it unset. // leaves it unset.
WorkDir() string WorkDir() string
@@ -129,40 +142,16 @@ const (
StatusCancelled = "cancelled" StatusCancelled = "cancelled"
) )
// Allows us to implement custom unmarshaling of JSON and envconfig.
type Memory uint64
func (m *Memory) Unmarshal(s string) error {
temp, err := bytefmt.ToBytes(s)
if err != nil {
return err
}
*m = Memory(temp)
return nil
}
func (m *Memory) UnmarshalJSON(p []byte) error {
temp, err := bytefmt.ToBytes(string(p))
if err != nil {
return err
}
*m = Memory(temp)
return nil
}
type Config struct { type Config struct {
Docker string `json:"docker" envconfig:"default=unix:///var/run/docker.sock,DOCKER"` Docker string `json:"docker"`
Memory Memory `json:"memory" envconfig:"default=256M,MEMORY_PER_JOB"` // TODO CPUShares should likely be on a per container basis
CPUShares int64 `json:"cpu_shares" envconfig:"default=2,CPU_SHARES"` CPUShares int64 `json:"cpu_shares"`
} }
// for tests // for tests
func DefaultConfig() Config { func DefaultConfig() Config {
return Config{ return Config{
Docker: "unix:///var/run/docker.sock", Docker: "unix:///var/run/docker.sock",
Memory: 256 * 1024 * 1024,
CPUShares: 0, CPUShares: 0,
} }
} }

View File

@@ -132,7 +132,7 @@ func (r *Runner) hasAsyncAvailableMemory() bool {
func (r *Runner) checkRequiredMem(req uint64) bool { func (r *Runner) checkRequiredMem(req uint64) bool {
r.usedMemMutex.RLock() r.usedMemMutex.RLock()
defer r.usedMemMutex.RUnlock() defer r.usedMemMutex.RUnlock()
return (r.availableMem-r.usedMem)/int64(req)*1024*1024 > 0 return r.availableMem-r.usedMem-(int64(req)*1024*1024) > 0
} }
func (r *Runner) addUsedMem(used int64) { func (r *Runner) addUsedMem(used int64) {
@@ -150,7 +150,7 @@ func (r *Runner) checkMemAndUse(req uint64) bool {
used := int64(req) * 1024 * 1024 used := int64(req) * 1024 * 1024
if (r.availableMem-r.usedMem)/used < 0 { if r.availableMem-r.usedMem-used < 0 {
return false return false
} }
@@ -189,10 +189,6 @@ func (r *Runner) run(ctx context.Context, cfg *task.Config) (drivers.RunResult,
span, ctx := opentracing.StartSpanFromContext(ctx, "run_container") span, ctx := opentracing.StartSpanFromContext(ctx, "run_container")
defer span.Finish() defer span.Finish()
if cfg.Memory == 0 {
cfg.Memory = 128
}
if cfg.Stdout == nil { if cfg.Stdout == nil {
// TODO why? async? // TODO why? async?
cfg.Stdout = cfg.Stderr cfg.Stdout = cfg.Stderr

View File

@@ -44,6 +44,7 @@ func TestRunnerHello(t *testing.T) {
ID: test.taskID, ID: test.taskID,
Image: test.route.Image, Image: test.route.Image,
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
Memory: 128,
Ready: make(chan struct{}), Ready: make(chan struct{}),
Stdin: strings.NewReader(test.payload), Stdin: strings.NewReader(test.payload),
AppName: test.route.AppName, AppName: test.route.AppName,
@@ -103,6 +104,7 @@ func TestRunnerError(t *testing.T) {
ID: fmt.Sprintf("err-%d-%d", i, time.Now().Unix()), ID: fmt.Sprintf("err-%d-%d", i, time.Now().Unix()),
Image: test.route.Image, Image: test.route.Image,
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
Memory: 128,
Ready: make(chan struct{}), Ready: make(chan struct{}),
Stdin: strings.NewReader(test.payload), Stdin: strings.NewReader(test.payload),
Stdout: &stdout, Stdout: &stdout,
@@ -131,3 +133,19 @@ func TestRunnerError(t *testing.T) {
} }
} }
} }
func TestRunnerMemory(t *testing.T) {
// make sure we get MB out of a task.Config when turned into a containerTask
// (so if Config.Memory changes to not be MB we hear about it)
cfg := &task.Config{
Memory: 128,
}
task := &containerTask{cfg: cfg}
const exp = 128 * 1024 * 1024
if task.Memory() != exp {
t.Fatalf("Expected Memory to return %v but got %v", exp, task.Memory())
}
}

View File

@@ -19,6 +19,7 @@ import (
var registries dockerRegistries var registries dockerRegistries
func init() { func init() {
// TODO this is docker specific. and the docker client is capable of doing this, remove & test
// Attempt to fetch it from an environment variable // Attempt to fetch it from an environment variable
regsettings := os.Getenv("DOCKER_AUTH") regsettings := os.Getenv("DOCKER_AUTH")
@@ -54,37 +55,34 @@ func init() {
} }
// TODO task.Config should implement the interface. this is sad :(
// implements drivers.ContainerTask
type containerTask struct { type containerTask struct {
ctx context.Context ctx context.Context
cfg *task.Config cfg *task.Config
canRun chan bool canRun chan bool
} }
func (t *containerTask) Command() string { return "" }
func (t *containerTask) EnvVars() map[string]string { func (t *containerTask) EnvVars() map[string]string {
if protocol.IsStreamable(protocol.Protocol(t.cfg.Format)) { if protocol.IsStreamable(protocol.Protocol(t.cfg.Format)) {
return t.cfg.BaseEnv return t.cfg.BaseEnv
} }
return t.cfg.Env return t.cfg.Env
} }
func (t *containerTask) Input() io.Reader {
return t.cfg.Stdin
}
func (t *containerTask) Labels() map[string]string { func (t *containerTask) Labels() map[string]string {
return map[string]string{ // TODO this seems inaccurate? is this used by anyone (dev or not)?
"LogName": t.cfg.AppName, return map[string]string{"LogName": t.cfg.AppName}
}
} }
func (t *containerTask) Command() string { return "" }
func (t *containerTask) Input() io.Reader { return t.cfg.Stdin }
func (t *containerTask) Id() string { return t.cfg.ID } func (t *containerTask) Id() string { return t.cfg.ID }
func (t *containerTask) Route() string { return "" }
func (t *containerTask) Image() string { return t.cfg.Image } func (t *containerTask) Image() string { return t.cfg.Image }
func (t *containerTask) Timeout() time.Duration { return t.cfg.Timeout } func (t *containerTask) Timeout() time.Duration { return t.cfg.Timeout }
func (t *containerTask) IdleTimeout() time.Duration { return t.cfg.IdleTimeout }
func (t *containerTask) Logger() (io.Writer, io.Writer) { return t.cfg.Stdout, t.cfg.Stderr } func (t *containerTask) Logger() (io.Writer, io.Writer) { return t.cfg.Stdout, t.cfg.Stderr }
func (t *containerTask) Volumes() [][2]string { return [][2]string{} } func (t *containerTask) Volumes() [][2]string { return [][2]string{} }
func (t *containerTask) Memory() uint64 { return t.cfg.Memory * 1024 * 1024 } // convert MB
func (t *containerTask) WorkDir() string { return "" } func (t *containerTask) WorkDir() string { return "" }
func (t *containerTask) Close() {} func (t *containerTask) Close() {}
func (t *containerTask) WriteStat(drivers.Stat) {} func (t *containerTask) WriteStat(drivers.Stat) {}

View File

@@ -148,6 +148,7 @@ func (s *Server) serve(ctx context.Context, c *gin.Context, appName string, rout
baseVars["FN_FORMAT"] = route.Format baseVars["FN_FORMAT"] = route.Format
baseVars["APP_NAME"] = appName baseVars["APP_NAME"] = appName
baseVars["ROUTE"] = route.Path baseVars["ROUTE"] = route.Path
baseVars["MEMORY_MB"] = fmt.Sprintf("%d", route.Memory)
// app config // app config
for k, v := range app.Config { for k, v := range app.Config {

View File

@@ -1,8 +1,7 @@
package: github.com/fnproject/fn package: github.com/fnproject/fn
excludeDirs: excludeDirs:
- fn - cli
import: import:
- package: code.cloudfoundry.org/bytefmt
- package: github.com/funcy/functions_go - package: github.com/funcy/functions_go
version: ^0.1.35 version: ^0.1.35
subpackages: subpackages:
@@ -72,7 +71,6 @@ import:
- package: github.com/opentracing/opentracing-go - package: github.com/opentracing/opentracing-go
- package: github.com/openzipkin/zipkin-go-opentracing - package: github.com/openzipkin/zipkin-go-opentracing
testImport: testImport:
- package: github.com/vrischmann/envconfig
- package: github.com/opencontainers/go-digest - package: github.com/opencontainers/go-digest
branch: master branch: master
- package: github.com/patrickmn/go-cache - package: github.com/patrickmn/go-cache