Add ImageFsInfo support

Signed-off-by: Lantao Liu <lantaol@google.com>
This commit is contained in:
Lantao Liu 2017-09-18 07:52:33 +00:00
parent b85be3d0cd
commit 491400c892
13 changed files with 507 additions and 7 deletions

View File

@ -31,6 +31,8 @@ const configFilePathArgName = "config"
// ContainerdConfig contains config related to containerd
type ContainerdConfig struct {
// ContainerdRootDir is the root directory path for containerd.
ContainerdRootDir string `toml:"root"`
// ContainerdSnapshotter is the snapshotter used by containerd.
ContainerdSnapshotter string `toml:"snapshotter"`
// ContainerdEndpoint is the containerd endpoint path.
@ -66,6 +68,8 @@ type Config struct {
EnableSelinux bool `toml:"enable_selinux"`
// SandboxImage is the image used by sandbox container.
SandboxImage string `toml:"sandbox_image"`
// StatsCollectPeriod is the period (in seconds) of snapshots stats collection.
StatsCollectPeriod int `toml:"stats_collect_period"`
}
// CRIContainerdOptions contains cri-containerd command line and toml options.
@ -93,6 +97,9 @@ func (c *CRIContainerdOptions) AddFlags(fs *pflag.FlagSet) {
"/var/run/cri-containerd.sock", "Path to the socket which cri-containerd serves on.")
fs.StringVar(&c.RootDir, "root-dir",
"/var/lib/cri-containerd", "Root directory path for cri-containerd managed files (metadata checkpoint etc).")
fs.StringVar(&c.ContainerdRootDir, "containerd-root-dir",
"/var/lib/containerd", "Root directory path where containerd stores persistent data. "+
"This should be the same with containerd `root`.")
fs.StringVar(&c.ContainerdEndpoint, "containerd-endpoint",
"/run/containerd/containerd.sock", "Path to the containerd endpoint.")
fs.StringVar(&c.ContainerdSnapshotter, "containerd-snapshotter",
@ -113,6 +120,8 @@ func (c *CRIContainerdOptions) AddFlags(fs *pflag.FlagSet) {
false, "Enable selinux support.")
fs.StringVar(&c.SandboxImage, "sandbox-image",
"gcr.io/google_containers/pause:3.0", "The image used by sandbox container.")
fs.IntVar(&c.StatsCollectPeriod, "stats-collect-period",
10, "The period (in seconds) of snapshots stats collection.")
fs.BoolVar(&c.PrintDefaultConfig, "default-config",
false, "Print default toml config of cri-containerd and quit.")
}

View File

@ -17,11 +17,13 @@ limitations under the License.
package os
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
containerdmount "github.com/containerd/containerd/mount"
"github.com/containerd/fifo"
"github.com/docker/docker/pkg/mount"
"golang.org/x/net/context"
@ -41,6 +43,8 @@ type OS interface {
Mount(source string, target string, fstype string, flags uintptr, data string) error
Unmount(target string, flags int) error
GetMounts() ([]*mount.Info, error)
LookupMount(path string) (containerdmount.Info, error)
DeviceUUID(device string) (string, error)
}
// RealOS is used to dispatch the real system level operations.
@ -120,3 +124,36 @@ func (RealOS) Unmount(target string, flags int) error {
func (RealOS) GetMounts() ([]*mount.Info, error) {
return mount.GetMounts()
}
// LookupMount gets mount info of a given path.
func (RealOS) LookupMount(path string) (containerdmount.Info, error) {
return containerdmount.Lookup(path)
}
// DeviceUUID gets device uuid of a device. The passed in device should be
// an absolute path of the device.
func (RealOS) DeviceUUID(device string) (string, error) {
const uuidDir = "/dev/disk/by-uuid"
if _, err := os.Stat(uuidDir); err != nil {
return "", err
}
files, err := ioutil.ReadDir(uuidDir)
if err != nil {
return "", err
}
for _, file := range files {
path := filepath.Join(uuidDir, file.Name())
target, err := os.Readlink(path)
if err != nil {
return "", err
}
dev, err := filepath.Abs(filepath.Join(uuidDir, target))
if err != nil {
return "", err
}
if dev == device {
return file.Name(), nil
}
}
return "", fmt.Errorf("device not found")
}

View File

@ -21,6 +21,7 @@ import (
"os"
"sync"
containerdmount "github.com/containerd/containerd/mount"
"github.com/docker/docker/pkg/mount"
"golang.org/x/net/context"
@ -50,6 +51,8 @@ type FakeOS struct {
MountFn func(source string, target string, fstype string, flags uintptr, data string) error
UnmountFn func(target string, flags int) error
GetMountsFn func() ([]*mount.Info, error)
LookupMountFn func(path string) (containerdmount.Info, error)
DeviceUUIDFn func(device string) (string, error)
calls []CalledDetail
errors map[string]error
}
@ -240,3 +243,29 @@ func (f *FakeOS) GetMounts() ([]*mount.Info, error) {
}
return nil, nil
}
// LookupMount is a fake call that invokes LookupMountFn or just return nil.
func (f *FakeOS) LookupMount(path string) (containerdmount.Info, error) {
f.appendCalls("LookupMount", path)
if err := f.getError("LookupMount"); err != nil {
return containerdmount.Info{}, err
}
if f.LookupMountFn != nil {
return f.LookupMountFn(path)
}
return containerdmount.Info{}, nil
}
// DeviceUUID is a fake call that invodes DeviceUUIDFn or just return nil.
func (f *FakeOS) DeviceUUID(device string) (string, error) {
f.appendCalls("DeviceUUID", device)
if err := f.getError("DeviceUUID"); err != nil {
return "", err
}
if f.DeviceUUIDFn != nil {
return f.DeviceUUIDFn(device)
}
return "", nil
}

View File

@ -17,7 +17,7 @@ limitations under the License.
package server
import (
"errors"
"time"
"golang.org/x/net/context"
@ -26,5 +26,26 @@ import (
// ImageFsInfo returns information of the filesystem that is used to store images.
func (c *criContainerdService) ImageFsInfo(ctx context.Context, r *runtime.ImageFsInfoRequest) (*runtime.ImageFsInfoResponse, error) {
return nil, errors.New("not implemented")
snapshots := c.snapshotStore.List()
timestamp := time.Now().UnixNano()
var usedBytes, inodesUsed uint64
for _, sn := range snapshots {
// Use the oldest timestamp as the timestamp of imagefs info.
if sn.Timestamp < timestamp {
timestamp = sn.Timestamp
}
usedBytes += sn.Size
inodesUsed += sn.Inodes
}
// TODO(random-liu): Handle content store
return &runtime.ImageFsInfoResponse{
ImageFilesystems: []*runtime.FilesystemUsage{
{
Timestamp: timestamp,
StorageId: &runtime.StorageIdentifier{Uuid: c.imageFSUUID},
UsedBytes: &runtime.UInt64Value{Value: usedBytes},
InodesUsed: &runtime.UInt64Value{Value: inodesUsed},
},
},
}, nil
}

View File

@ -0,0 +1,70 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"testing"
"github.com/containerd/containerd/snapshot"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
"k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
snapshotstore "github.com/kubernetes-incubator/cri-containerd/pkg/store/snapshot"
)
func TestImageFsInfo(t *testing.T) {
c := newTestCRIContainerdService()
snapshots := []snapshotstore.Snapshot{
{
Key: "key1",
Kind: snapshot.KindActive,
Size: 10,
Inodes: 100,
Timestamp: 234567,
},
{
Key: "key2",
Kind: snapshot.KindCommitted,
Size: 20,
Inodes: 200,
Timestamp: 123456,
},
{
Key: "key3",
Kind: snapshot.KindView,
Size: 0,
Inodes: 0,
Timestamp: 345678,
},
}
expected := &runtime.FilesystemUsage{
Timestamp: 123456,
StorageId: &runtime.StorageIdentifier{Uuid: testImageFSUUID},
UsedBytes: &runtime.UInt64Value{Value: 30},
InodesUsed: &runtime.UInt64Value{Value: 300},
}
for _, sn := range snapshots {
c.snapshotStore.Add(sn)
}
resp, err := c.ImageFsInfo(context.Background(), &runtime.ImageFsInfoRequest{})
require.NoError(t, err)
stats := resp.GetImageFilesystems()
assert.Len(t, stats, 1)
assert.Equal(t, expected, stats[0])
}

View File

@ -280,3 +280,14 @@ func (in *instrumentedService) RemoveImage(ctx context.Context, r *runtime.Remov
}()
return in.criContainerdService.RemoveImage(ctx, r)
}
func (in *instrumentedService) ImageFsInfo(ctx context.Context, r *runtime.ImageFsInfoRequest) (res *runtime.ImageFsInfoResponse, err error) {
glog.V(4).Infof("ImageFsInfo")
defer func() {
if err != nil {
glog.Errorf("ImageFsInfo failed, error: %v", err)
} else {
glog.V(4).Infof("ImageFsInfo returns filesystem info %+v", res.ImageFilesystems)
}
}()
return in.criContainerdService.ImageFsInfo(ctx, r)
}

View File

@ -20,12 +20,15 @@ import (
"fmt"
"net"
"os"
"path/filepath"
"syscall"
"time"
"github.com/containerd/containerd"
"github.com/containerd/containerd/api/services/tasks/v1"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/plugin"
"github.com/cri-o/ocicni/pkg/ocicni"
"github.com/golang/glog"
"golang.org/x/net/context"
@ -39,6 +42,7 @@ import (
containerstore "github.com/kubernetes-incubator/cri-containerd/pkg/store/container"
imagestore "github.com/kubernetes-incubator/cri-containerd/pkg/store/image"
sandboxstore "github.com/kubernetes-incubator/cri-containerd/pkg/store/sandbox"
snapshotstore "github.com/kubernetes-incubator/cri-containerd/pkg/store/snapshot"
)
const (
@ -60,6 +64,8 @@ type CRIContainerdService interface {
type criContainerdService struct {
// config contains all configurations.
config options.Config
// imageFSUUID is the device uuid of image filesystem.
imageFSUUID string
// server is the grpc server.
server *grpc.Server
// os is an interface for all required os operations.
@ -76,6 +82,8 @@ type criContainerdService struct {
containerNameIndex *registrar.Registrar
// imageStore stores all resources associated with images.
imageStore *imagestore.Store
// snapshotStore stores information of all snapshots.
snapshotStore *snapshotstore.Store
// taskService is containerd tasks client.
taskService tasks.TasksClient
// contentStoreService is the containerd content service client.
@ -113,6 +121,7 @@ func NewCRIContainerdService(config options.Config) (CRIContainerdService, error
sandboxStore: sandboxstore.NewStore(),
containerStore: containerstore.NewStore(),
imageStore: imagestore.NewStore(),
snapshotStore: snapshotstore.NewStore(),
sandboxNameIndex: registrar.NewRegistrar(),
containerNameIndex: registrar.NewRegistrar(),
taskService: client.TaskService(),
@ -121,11 +130,16 @@ func NewCRIContainerdService(config options.Config) (CRIContainerdService, error
client: client,
}
netPlugin, err := ocicni.InitCNI(config.NetworkPluginConfDir, config.NetworkPluginBinDir)
imageFSPath := imageFSPath(config.ContainerdRootDir, config.ContainerdSnapshotter)
c.imageFSUUID, err = c.getDeviceUUID(imageFSPath)
if err != nil {
return nil, fmt.Errorf("failed to get imagefs uuid: %v", err)
}
c.netPlugin, err = ocicni.InitCNI(config.NetworkPluginConfDir, config.NetworkPluginBinDir)
if err != nil {
return nil, fmt.Errorf("failed to initialize cni plugin: %v", err)
}
c.netPlugin = netPlugin
// prepare streaming server
c.streamServer, err = newStreamServer(c, config.StreamServerAddress, config.StreamServerPort)
@ -156,6 +170,15 @@ func (c *criContainerdService) Run() error {
glog.V(2).Info("Start event monitor")
eventMonitorCloseCh := c.eventMonitor.start()
// Start snapshot stats syncer, it doesn't need to be stopped.
glog.V(2).Info("Start snapshots syncer")
snapshotsSyncer := newSnapshotsSyncer(
c.snapshotStore,
c.client.SnapshotService(c.config.ContainerdSnapshotter),
time.Duration(c.config.StatsCollectPeriod)*time.Second,
)
snapshotsSyncer.start()
// Start streaming server.
glog.V(2).Info("Start streaming server")
streamServerCloseCh := make(chan struct{})
@ -209,3 +232,18 @@ func (c *criContainerdService) Stop() {
c.streamServer.Stop() // nolint: errcheck
c.server.Stop()
}
// getDeviceUUID gets device uuid for a given path.
func (c *criContainerdService) getDeviceUUID(path string) (string, error) {
info, err := c.os.LookupMount(path)
if err != nil {
return "", err
}
return c.os.DeviceUUID(info.Source)
}
// imageFSPath returns containerd image filesystem path.
// Note that if containerd changes directory layout, we also needs to change this.
func imageFSPath(rootDir, snapshotter string) string {
return filepath.Join(rootDir, fmt.Sprintf("%s.%s", plugin.SnapshotPlugin, snapshotter))
}

View File

@ -24,6 +24,7 @@ import (
containerstore "github.com/kubernetes-incubator/cri-containerd/pkg/store/container"
imagestore "github.com/kubernetes-incubator/cri-containerd/pkg/store/image"
sandboxstore "github.com/kubernetes-incubator/cri-containerd/pkg/store/sandbox"
snapshotstore "github.com/kubernetes-incubator/cri-containerd/pkg/store/snapshot"
)
const (
@ -32,6 +33,7 @@ const (
// TODO(random-liu): Change this to image name after we have complete image
// management unit test framework.
testSandboxImage = "sha256:c75bebcdd211f41b3a460c7bf82970ed6c75acaab9cd4c9a4e125b03ca113798"
testImageFSUUID = "test-image-fs-uuid"
)
// newTestCRIContainerdService creates a fake criContainerdService for test.
@ -41,9 +43,11 @@ func newTestCRIContainerdService() *criContainerdService {
RootDir: testRootDir,
SandboxImage: testSandboxImage,
},
imageFSUUID: testImageFSUUID,
os: ostesting.NewFakeOS(),
sandboxStore: sandboxstore.NewStore(),
imageStore: imagestore.NewStore(),
snapshotStore: snapshotstore.NewStore(),
sandboxNameIndex: registrar.NewRegistrar(),
containerStore: containerstore.NewStore(),
containerNameIndex: registrar.NewRegistrar(),

110
pkg/server/snapshots.go Normal file
View File

@ -0,0 +1,110 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"context"
"time"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/snapshot"
"github.com/golang/glog"
snapshotstore "github.com/kubernetes-incubator/cri-containerd/pkg/store/snapshot"
)
// snapshotsSyncer syncs snapshot stats periodically. imagefs info and container stats
// should both use cached result here.
// TODO(random-liu): Benchmark with high workload. We may need a statsSyncer instead if
// benchmark result shows that container cpu/memory stats also need to be cached.
type snapshotsSyncer struct {
store *snapshotstore.Store
snapshotter snapshot.Snapshotter
syncPeriod time.Duration
}
// newSnapshotsSyncer creates a snapshot syncer.
func newSnapshotsSyncer(store *snapshotstore.Store, snapshotter snapshot.Snapshotter,
period time.Duration) *snapshotsSyncer {
return &snapshotsSyncer{
store: store,
snapshotter: snapshotter,
syncPeriod: period,
}
}
// start starts the snapshots syncer. No stop function is needed because
// the syncer doesn't update any persistent states, it's fine to let it
// exit with the process.
func (s *snapshotsSyncer) start() {
tick := time.NewTicker(s.syncPeriod)
go func() {
defer tick.Stop()
// TODO(random-liu): This is expensive. We should do benchmark to
// check the resource usage and optimize this.
for {
if err := s.sync(); err != nil {
glog.Errorf("Failed to sync snapshot stats: %v", err)
}
<-tick.C
}
}()
}
// sync updates all snapshots stats.
func (s *snapshotsSyncer) sync() error {
start := time.Now().UnixNano()
collect := func(ctx context.Context, info snapshot.Info) error {
sn, err := s.store.Get(info.Name)
if err == nil {
// Only update timestamp for non-active snapshot.
if sn.Kind == info.Kind && sn.Kind != snapshot.KindActive {
sn.Timestamp = time.Now().UnixNano()
s.store.Add(sn)
return nil
}
}
// Get newest stats if the snapshot is new or active.
sn = snapshotstore.Snapshot{
Key: info.Name,
Kind: info.Kind,
Timestamp: time.Now().UnixNano(),
}
usage, err := s.snapshotter.Usage(ctx, info.Name)
if err != nil {
if errdefs.IsNotFound(err) {
return nil
}
return err
}
sn.Size = uint64(usage.Size)
sn.Inodes = uint64(usage.Inodes)
s.store.Add(sn)
return nil
}
if err := s.snapshotter.Walk(context.Background(), collect); err != nil {
return err
}
for _, sn := range s.store.List() {
if sn.Timestamp >= start {
continue
}
// Delete the snapshot stats if it's not updated this time.
s.store.Delete(sn.Key)
}
return nil
}

View File

@ -92,8 +92,8 @@ func (s *Store) List() []Image {
s.lock.RLock()
defer s.lock.RUnlock()
var images []Image
for _, sb := range s.images {
images = append(images, sb)
for _, i := range s.images {
images = append(images, i)
}
return images
}

View File

@ -99,7 +99,7 @@ func TestImageStore(t *testing.T) {
imgs = s.List()
assert.Len(imgs, 2)
t.Logf("get should return nil after deletion")
t.Logf("get should return empty struct and ErrNotExist after deletion")
img, err := s.Get(testID)
assert.Equal(Image{}, img)
assert.Equal(store.ErrNotExist, err)

View File

@ -0,0 +1,87 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package snapshot
import (
"sync"
"github.com/containerd/containerd/snapshot"
"github.com/kubernetes-incubator/cri-containerd/pkg/store"
)
// Snapshot contains the information about the snapshot.
type Snapshot struct {
// Key is the key of the snapshot
Key string
// Kind is the kind of the snapshot (active, commited, view)
Kind snapshot.Kind
// Size is the size of the snapshot in bytes.
Size uint64
// Inodes is the number of inodes used by the snapshot
Inodes uint64
// Timestamp is latest update time (in nanoseconds) of the snapshot
// information.
Timestamp int64
}
// Store stores all snapshots.
type Store struct {
lock sync.RWMutex
snapshots map[string]Snapshot
}
// NewStore creates a snapshot store.
func NewStore() *Store {
return &Store{snapshots: make(map[string]Snapshot)}
}
// Add a snapshot into the store.
func (s *Store) Add(snapshot Snapshot) {
s.lock.Lock()
defer s.lock.Unlock()
s.snapshots[snapshot.Key] = snapshot
}
// Get returns the snapshot with specified key. Returns store.ErrNotExist if the
// snapshot doesn't exist.
func (s *Store) Get(key string) (Snapshot, error) {
s.lock.RLock()
defer s.lock.RUnlock()
if sn, ok := s.snapshots[key]; ok {
return sn, nil
}
return Snapshot{}, store.ErrNotExist
}
// List lists all snapshots.
func (s *Store) List() []Snapshot {
s.lock.RLock()
defer s.lock.RUnlock()
var snapshots []Snapshot
for _, sn := range s.snapshots {
snapshots = append(snapshots, sn)
}
return snapshots
}
// Delete deletes the snapshot with specified key.
func (s *Store) Delete(key string) {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.snapshots, key)
}

View File

@ -0,0 +1,84 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package snapshot
import (
"testing"
"time"
"github.com/containerd/containerd/snapshot"
assertlib "github.com/stretchr/testify/assert"
"github.com/kubernetes-incubator/cri-containerd/pkg/store"
)
func TestSnapshotStore(t *testing.T) {
snapshots := map[string]Snapshot{
"key1": {
Key: "key1",
Kind: snapshot.KindActive,
Size: 10,
Inodes: 100,
Timestamp: time.Now().UnixNano(),
},
"key2": {
Key: "key2",
Kind: snapshot.KindCommitted,
Size: 20,
Inodes: 200,
Timestamp: time.Now().UnixNano(),
},
"key3": {
Key: "key3",
Kind: snapshot.KindView,
Size: 0,
Inodes: 0,
Timestamp: time.Now().UnixNano(),
},
}
assert := assertlib.New(t)
s := NewStore()
t.Logf("should be able to add snapshot")
for _, sn := range snapshots {
s.Add(sn)
}
t.Logf("should be able to get snapshot")
for id, sn := range snapshots {
got, err := s.Get(id)
assert.NoError(err)
assert.Equal(sn, got)
}
t.Logf("should be able to list snapshot")
sns := s.List()
assert.Len(sns, 3)
testKey := "key2"
t.Logf("should be able to delete snapshot")
s.Delete(testKey)
sns = s.List()
assert.Len(sns, 2)
t.Logf("get should return empty struct and ErrNotExist after deletion")
sn, err := s.Get(testKey)
assert.Equal(Snapshot{}, sn)
assert.Equal(store.ErrNotExist, err)
}