-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The current implementation sets all the environment variables passed in Process.Env in the current process, one by one, then uses os.Environ to read those back. As pointed out in [1], this is slow, as runc calls os.Setenv for every variable, and there may be a few thousands of those. Looking into how os.Setenv is implemented, it is indeed slow, especially when cgo is enabled. Looking into why it was implemented the way it is, I found commit 9744d72 and traced it to [2], which discusses the actual reasons. It boils down to these two: - HOME is not passed into container as it is set in setupUser by os.Setenv and has no effect on config.Env; - there is a need to deduplication of environment variables. Yet it was decided in [2] to not go ahead with this patch, but later [3] was opened with the carry of this patch, and merged. Now, from what I see: 1. Passing environment to exec is way faster than using os.Setenv and os.Environ (tests show ~20x speed improvement in a simple Go test, and ~3x improvement in real-world test, see below). 2. Setting environment variables in the runc context may result is some ugly side effects (think GODEBUG, LD_PRELOAD, or _LIBCONTAINER_*). 3. Nothing in runtime spec says that the environment needs to be deduplicated, or the order of preference (whether the first or the last value of a variable with the same name is to be used). We should stick to what we have in order to maintain backward compatibility. So, this patch: - switches to passing env directly to exec; - adds deduplication mechanism to retain backward compatibility; - takes care to set PATH from process.Env in the current process (so that supplied PATH is used to find the binary to execute), also to retain backward compatibility; - adds HOME to process.Env if not set; - ensures any StartContainer CommandHook entries with no environment set explicitly are run with the same environment as before. Thanks to @lifubang who noticed that peculiarity. The benchmark added by the previous commit shows ~3x improvement: │ before │ after │ │ sec/op │ sec/op vs base │ ExecInBigEnv-20 61.53m ± 1% 21.87m ± 16% -64.46% (p=0.000 n=10) [1]: #1983 [2]: docker-archive/libcontainer#418 [3]: docker-archive/libcontainer#432 Signed-off-by: Kir Kolyshkin <[email protected]>
- Loading branch information
Showing
5 changed files
with
129 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package libcontainer | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"os" | ||
"slices" | ||
"strings" | ||
) | ||
|
||
// prepareEnv processes a list of environment variables, preparing it | ||
// for direct consumption by unix.Exec. In particular, it: | ||
// - validates each variable is in the NAME=VALUE format and | ||
// contains no \0 (nil) bytes; | ||
// - removes any duplicates (keeping only the last value for each key) | ||
// - sets PATH for the current process, if found in the list. | ||
// | ||
// It returns the deduplicated environment, a flag telling whether HOME | ||
// is present in the input, and an error. | ||
func prepareEnv(env []string) ([]string, bool, error) { | ||
if env == nil { | ||
return nil, false, nil | ||
} | ||
// Deduplication code based on dedupEnv from Go 1.22 os/exec. | ||
|
||
// Construct the output in reverse order, to preserve the | ||
// last occurrence of each key. | ||
out := make([]string, 0, len(env)) | ||
saw := make(map[string]bool, len(env)) | ||
for n := len(env); n > 0; n-- { | ||
kv := env[n-1] | ||
i := strings.IndexByte(kv, '=') | ||
if i == -1 { | ||
return nil, false, errors.New("invalid environment variable: missing '='") | ||
} | ||
if i == 0 { | ||
return nil, false, errors.New("invalid environment variable: name cannot be empty") | ||
} | ||
key := kv[:i] | ||
if saw[key] { // Duplicate. | ||
continue | ||
} | ||
saw[key] = true | ||
if strings.IndexByte(kv, 0) >= 0 { | ||
return nil, false, fmt.Errorf("invalid environment variable %q: contains nul byte (\\x00)", key) | ||
} | ||
if key == "PATH" { | ||
// Needs to be set as it is used for binary lookup. | ||
if err := os.Setenv("PATH", kv[i+1:]); err != nil { | ||
return nil, false, err | ||
} | ||
} | ||
out = append(out, kv) | ||
} | ||
// Restore the original order. | ||
slices.Reverse(out) | ||
|
||
return out, saw["HOME"], nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package libcontainer | ||
|
||
import ( | ||
"slices" | ||
"testing" | ||
) | ||
|
||
func TestPrepareEnvDedup(t *testing.T) { | ||
tests := []struct { | ||
env, wantEnv []string | ||
}{ | ||
{ | ||
env: []string{}, | ||
wantEnv: []string{}, | ||
}, | ||
{ | ||
env: []string{"HOME=/root", "FOO=bar"}, | ||
wantEnv: []string{"HOME=/root", "FOO=bar"}, | ||
}, | ||
{ | ||
env: []string{"A=a", "A=b", "A=c"}, | ||
wantEnv: []string{"A=c"}, | ||
}, | ||
{ | ||
env: []string{"TERM=vt100", "HOME=/home/one", "HOME=/home/two", "TERM=xterm", "HOME=/home/three", "FOO=bar"}, | ||
wantEnv: []string{"TERM=xterm", "HOME=/home/three", "FOO=bar"}, | ||
}, | ||
} | ||
|
||
for _, tc := range tests { | ||
env, _, err := prepareEnv(tc.env) | ||
if err != nil { | ||
t.Error(err) | ||
continue | ||
} | ||
if !slices.Equal(env, tc.wantEnv) { | ||
t.Errorf("want %v, got %v", tc.wantEnv, env) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters