containerd简介
Containerd是一个工业标准的容器运行时,重点是它简洁,健壮,便携,在Linux和window上可以作为一个守护进程运行,它可以管理主机系统上容器的完整的生命周期:镜像传输和存储,容器的执行和监控,低级别的存储和网络。
containerd和docker不同,containerd重点是继承在大规模的系统中,例如kubernetes,而不是面向开发者,让开发者使用,更多的是容器运行时的概念,承载容器运行。
如何使用containerd
- 通过ctr(官方提供的client);
- 自己写一个client,官方提供了一个client package供开发者调用来写自己的client。
注:需要安装对应版本的runc和golang版本1.9x。
安装containerd
我们以v1.2.7版本为例
wget https://github.com/containerd/containerd/archive/v1.2.7.zip
unzip v1.2.7.zip
解压后会生成一个containerd-1.2.7
包,即containerd的代码包,你可以看到containerd.service
service配置文件,如果你使用systemd,可以使用它。
containerd守护进程有一个配置文件在/etc/containerd/config.toml
目录下,用户可以根据自己的需要配置各项参数,如下例子:
root = "/var/lib/containerd"
state = "/run/containerd"
oom_score = 0
[grpc]
address = "/run/containerd/containerd.sock"
uid = 0
gid = 0
max_recv_message_size = 16777216
max_send_message_size = 16777216
[debug]
address = ""
uid = 0
gid = 0
level = ""
[metrics]
address = ""
grpc_histogram = false
[cgroup]
path = ""
[plugins]
[plugins.cgroups]
no_prometheus = false
[plugins.cri]
stream_server_address = ""
stream_server_port = "10010"
enable_selinux = false
sandbox_image = "hyc-cloud-private-integration-docker-local.artifactory.swg-devops.com/ibmcom/pause:3.1"
stats_collect_period = 10
systemd_cgroup = false
[plugins.cri.containerd]
snapshotter = "overlayfs"
[plugins.cri.containerd.default_runtime]
runtime_type = "io.containerd.runtime.v1.linux"
container_runtime = ""
runtime_root = ""
[plugins.cri.containerd.untrusted_workload_runtime]
runtime_type = ""
container_runtime = ""
runtime_root = ""
[plugins.cri.cni]
bin_dir = "/opt/cni/bin"
conf_dir = "/etc/cni/net.d"
[plugins.cri.registry]
[plugins.cri.registry.mirrors]
[plugins.cri.registry.mirrors."docker.io"]
endpoint = ["https://registry-1.docker.io"]
[plugins.diff-service]
default = ["walking"]
[plugins.linux]
shim = "containerd-shim"
runtime = "runc"
runtime_root = ""
no_shim = false
shim_debug = false
[plugins.scheduler]
pause_threshold = 0.02
deletion_threshold = 0
mutation_threshold = 100
schedule_delay = "0s"
startup_delay = "100ms"
可以通过containerd config default > /etc/containerd/config.toml
命令生成config.toml配置。
使用containerd
我们自己写一个main.go
来调用containerd:
package main
import (
"log"
"github.com/containerd/containerd"
)
func main() {
if err := redisExample(); err != nil {
log.Fatal(err)
}
}
func redisExample() error {
client, err := containerd.New("/run/containerd/containerd.sock")
if err != nil {
return err
}
defer client.Close()
return nil
}
通过默认的containerd.sock创建一个client,因为我们正在使用GRPC上的守护进程,所以我们需要创建一个用于调用客户端方法的上下文,对于调用containerd的API来说,containerd也是分namespace的,创建上下文后,我们也应该创建一个namespace。
ctx := namespaces.WithNamespace(context.Background(), "example")
Pull 镜像
创建了client,我们可以使用client来pull镜像啦。
image, err := client.Pull(ctx, "docker.io/library/redis:alpine", containerd.WithPullUnpack)
if err != nil {
return err
}
containerd client在很多函数调用中使用golang中的Opts
模式。在这个函数中我们传入了 containerd.WithPullUnpack
参数,这个参数的作用是,我们在pull image的同时,我们不仅将镜像内容提取并下载到containerd的content存储库中,还将其解压到一个snapshotter中以用作根文件系统。
让我们把代码集合到一起,运行一个完整的例子:
首先将containerd代码包放在gopath目录中,我的路径是 /root/gopath/src/github.com/containerd/containerd
package main
import (
"context"
"log"
"github.com/containerd/containerd"
"github.com/containerd/containerd/namespaces"
)
func main() {
if err := redisExample(); err != nil {
log.Fatal(err)
}
}
func redisExample() error {
client, err := containerd.New("/run/containerd/containerd.sock")
if err != nil {
return err
}
defer client.Close()
ctx := namespaces.WithNamespace(context.Background(), "example")
image, err := client.Pull(ctx, "docker.io/library/redis:alpine", containerd.WithPullUnpack)
if err != nil {
return err
}
log.Printf("Successfully pulled %s image\n", image.Name())
return nil
}
root@winking1:~/gopath# go build main.go
root@winking1:~/gopath# ls
bin main main.go pkg src
root@winking1:~/gopath# ./main
2019/06/18 22:53:58 Successfully pulled docker.io/library/redis:alpine image
创建一个 OCI Spec 和 Container
前面我们已经pull了镜像,我们需要生成一个OCI 运行时规范,容器可以基于新容器。containerd为生成OCI运行时规范提供了合理的默认值。
如果你已经有一个OCI规范,可以直接使用containerd.WithSpec(spec)设置。
当为容器创建一个新snapshot的时候,我们需要提供一个snapshot ID和容器基于的镜像,通过提供一个单独的snapshot ID而不是容器的ID,我们可以很容易的重复利用已经存在的snapshot在创建不容的容器的时候。
container, err := client.NewContainer(
ctx,
"redis-server",
containerd.WithNewSnapshot("redis-server-snapshot", image),
containerd.WithNewSpec(oci.WithImageConfig(image)),
)
if err != nil {
return err
}
defer container.Delete(ctx, containerd.WithSnapshotCleanup)
root@winking1:~/gopath# ./main
2019/06/18 23:41:33 Successfully pulled docker.io/library/redis:alpine image
2019/06/18 23:41:33 Successfully created container with ID redis-server and snapshot with ID redis-server-snapshot
创建运行的task
Container和task的不同,容器是分配和附加资源的元数据对象,task是系统中动态的运行的的进程,task在每次运行后应该被删除掉,但是容器能够被多次使用,更新,查询。
我们创建的新的task的实际上是一个运行在系统中的进程,我们使用cio.WithStdio
所以所有的来自容器中的IO是从main.go这个进程发送的,这是个cio.Opt配置Streams,通过NewCreator为这个新的task返回一个cio.IO。
如果你熟悉OCI运行时,如果当前有个任务处于“已创建”状态,这意味着命名空间,根文件系统和各种容器级别设置已经完成初始化,但用户定义的进程(在此示例中为“redis-server”)尚未启动。这使用户有机会去设置容器的网络或添加不同的工具来监视容器。 containerd也借此机会监控您的容器。此时会设置等待容器退出状态和cgroup指标等内容。
task, err := container.NewTask(ctx, cio.NewCreator(cio.WithStdio))
if err != nil {
return err
}
defer task.Delete(ctx)
如果您熟悉prometheus,可以通过curl得到容器度量标准(在我们创建的config.toml中)以查看容器的度量准:
[metrics]
address = "127.0.0.1:1338"
grpc_histogram = false
curl 127.0.0.1:1338/v1/metrics
现在我们在创建状态下有一个任务,我们需要确保等待任务退出。必须等待任务完成,以便我们可以关闭我们的示例并清理我们创建的资源。你总是希望在调用任务开始之前确保等待。如果任务有一个像/ bin / true这样的简单程序在调用start后立即退出,这可以确保你不会遇到任何比赛。
exitStatusC, err := task.Wait(ctx)
if err != nil {
return err
}
if err := task.Start(ctx); err != nil {
return err
}
Killing the task
time.Sleep(3 * time.Second)
if err := task.Kill(ctx, syscall.SIGTERM); err != nil {
return err
}
status := <-exitStatusC
code, exitedAt, err := status.Result()
if err != nil {
return err
}
fmt.Printf("redis-server exited with status: %d\n", code)
我们等待我们设置的退出状态通道以确保任务已完全退出并获得退出状态。如果您必须重新加载容器或错过等待任务,则删除也将在您最终删除任务时返回退出状态。我们让你满意。
status, err := task.Delete(ctx)
完整代码示例:
package main
import (
"context"
"fmt"
"log"
"syscall"
"time"
"github.com/containerd/containerd"
"github.com/containerd/containerd/cio"
"github.com/containerd/containerd/oci"
"github.com/containerd/containerd/namespaces"
)
func main() {
if err := redisExample(); err != nil {
log.Fatal(err)
}
}
func redisExample() error {
// create a new client connected to the default socket path for containerd
client, err := containerd.New("/run/containerd/containerd.sock")
if err != nil {
return err
}
defer client.Close()
// create a new context with an "example" namespace
ctx := namespaces.WithNamespace(context.Background(), "example")
// pull the redis image from DockerHub
image, err := client.Pull(ctx, "docker.io/library/redis:alpine", containerd.WithPullUnpack)
if err != nil {
return err
}
// create a container
container, err := client.NewContainer(
ctx,
"redis-server",
containerd.WithImage(image),
containerd.WithNewSnapshot("redis-server-snapshot", image),
containerd.WithNewSpec(oci.WithImageConfig(image)),
)
if err != nil {
return err
}
defer container.Delete(ctx, containerd.WithSnapshotCleanup)
// create a task from the container
task, err := container.NewTask(ctx, cio.NewCreator(cio.WithStdio))
if err != nil {
return err
}
defer task.Delete(ctx)
// make sure we wait before calling start
exitStatusC, err := task.Wait(ctx)
if err != nil {
fmt.Println(err)
}
// call start on the task to execute the redis server
if err := task.Start(ctx); err != nil {
return err
}
// sleep for a lil bit to see the logs
time.Sleep(3 * time.Second)
// kill the process and get the exit status
if err := task.Kill(ctx, syscall.SIGTERM); err != nil {
return err
}
// wait for the process to fully exit and print out the exit status
status := <-exitStatusC
code, _, err := status.Result()
if err != nil {
return err
}
fmt.Printf("redis-server exited with status: %d\n", code)
return nil
}
root@winking1:~/gopath# go build main.go
root@winking1:~/gopath# ./main
1:C 19 Jun 2019 07:35:53.211 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 19 Jun 2019 07:35:53.211 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 19 Jun 2019 07:35:53.211 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
1:M 19 Jun 2019 07:35:53.213 # You requested maxclients of 10000 requiring at least 10032 max file descriptors.
1:M 19 Jun 2019 07:35:53.213 # Server can't set maximum open files to 10032 because of OS error: Operation not permitted.
1:M 19 Jun 2019 07:35:53.213 # Current maximum open files is 1024. maxclients has been reduced to 992 to compensate for low ulimit. If you need higher maxclients increase 'ulimit -n'.
1:M 19 Jun 2019 07:35:53.214 * Running mode=standalone, port=6379.
1:M 19 Jun 2019 07:35:53.214 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
1:M 19 Jun 2019 07:35:53.214 # Server initialized
1:M 19 Jun 2019 07:35:53.214 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
1:M 19 Jun 2019 07:35:53.214 * Ready to accept connections
1:signal-handler (1560929756) Received SIGTERM scheduling shutdown...
1:M 19 Jun 2019 07:35:56.324 # User requested shutdown...
1:M 19 Jun 2019 07:35:56.325 * Saving the final RDB snapshot before exiting.
1:M 19 Jun 2019 07:35:56.330 * DB saved on disk
1:M 19 Jun 2019 07:35:56.331 # Redis is now ready to exit, bye bye...
redis-server exited with status: 0