containerd Snapshots功能解析
snapshot是containerd的一個核心功能,用于創建和管理容器的文件系統。
本篇containerd版本為v1.7.9
。
本文以 ctr i pull
命令為例,分析containerd的snapshot “創建” 相關的功能。
ctr命令
ctr image
相關命令的實現在cmd/ctr/commands/images
目錄中。
查看文件cmd/ctr/commands/images/pull.go
// 查看Action中注冊的函數
func(context *cli.Context) error {// 省略內容....// 獲取grpc客戶端client, ctx, cancel, err := commands.NewClient(context)if err != nil {return err}defer cancel()// 這里的功能是將pull動作,通過grpc調用完全交給遠端實現。// 當前的代碼版本, 此塊代碼永遠不會執行。if !context.BoolT("local") {// grpc調用return client.Transfer(ctx, reg, is, transfer.WithProgress(pf))}// 省略內容....// fetch階段img, err := content.Fetch(ctx, client, ref, config)if err != nil {return err}// 省略內容....// unpack階段// 根據平臺信息,解壓鏡像,創建快照等start := time.Now()for _, platform := range p {fmt.Printf("unpacking %s %s...\n", platforms.Format(platform), img.Target.Digest)i := containerd.NewImageWithPlatform(client, img, platforms.Only(platform))err = i.Unpack(ctx, context.String("snapshotter"))if err != nil {return err}if context.Bool("print-chainid") {diffIDs, err := i.RootFS(ctx)if err != nil {return err}chainID := identity.ChainID(diffIDs).String()fmt.Printf("image chain ID: %s\n", chainID)}}fmt.Printf("done: %s\t\n", time.Since(start))return nil}
fetch階段
fetch階段分為兩步:
- 下載鏡像
- 在數據庫中添加鏡像記錄
查看文件client/client.go
func (c *Client) Fetch(ctx context.Context, ref string, opts ...RemoteOpt) (images.Image, error) {// 省略內容....ctx, done, err := c.WithLease(ctx)if err != nil {return images.Image{}, err}defer done(ctx)// 下載鏡像img, err := c.fetch(ctx, fetchCtx, ref, 0)if err != nil {return images.Image{}, err}// 在數據庫中添加鏡像記錄return c.createNewImage(ctx, img)
}
下載鏡像
c.fetch(ctx, fetchCtx, ref, 0)
下載鏡像沒什么好說的,需要注意的是,當前的代碼版本,下載的功能是在ctr
中實現的,而不是調用grpc
接口實現的。
createNewImage
// c.createNewImage(ctx, img)
func (c *Client) createNewImage(ctx context.Context, img images.Image) (images.Image, error) {// 省略內容....is := c.ImageService()for {if created, err := is.Create(ctx, img); err != nil {if !errdefs.IsAlreadyExists(err) {return images.Image{}, err}// 如果鏡像已經存在,則更新鏡像updated, err := is.Update(ctx, img)if err != nil {// if image was removed, try create againif errdefs.IsNotFound(err) {continue}return images.Image{}, err}img = updated} else {img = created}return img, nil}
}
最終會以grpc
方式調用containerd
的ImageService
接口。
containerd
中的接口,均是以plugin
的方式注冊實現的。plugin
的實現我們后面再分析。
// image service注冊
// services/images/service.go
func init() {plugin.Register(&plugin.Registration{Type: plugin.GRPCPlugin,ID: "images",Requires: []plugin.Type{plugin.ServicePlugin,},InitFn: func(ic *plugin.InitContext) (interface{}, error) {// 省略內容....},})
}
images.create
沒有太多邏輯,主要是在bbolt
中添加一條數據記錄。
bbolt
是一個key-value
數據庫,containerd
中的大部分數據都是存儲在bbolt
中的。
https://pkg.go.dev/go.etcd.io/bbolt#section-readme
func (l *local) Create(ctx context.Context, req *imagesapi.CreateImageRequest, _ ...grpc.CallOption) (*imagesapi.CreateImageResponse, error) {// 省略內容....// 在bbolt中添加一條數據記錄created, err := l.store.Create(ctx, image)if err != nil {return nil, errdefs.ToGRPC(err)}resp.Image = imageToProto(&created)// 發布事件// 事件發布是containerd中的一個重要功能,后面會詳細分析。if err := l.publisher.Publish(ctx, "/images/create", &eventstypes.ImageCreate{Name: resp.Image.Name,Labels: resp.Image.Labels,}); err != nil {return nil, err}l.emitSchema1DeprecationWarning(ctx, &image)return &resp, nil}
images.update
對比create
,多了一些邏輯,主要是更新鏡像的某些字段。
func (l *local) Update(ctx context.Context, req *imagesapi.UpdateImageRequest, _ ...grpc.CallOption) (*imagesapi.UpdateImageResponse, error) {// 省略內容....// 更新鏡像的某些字段if req.UpdateMask != nil && len(req.UpdateMask.Paths) > 0 {fieldpaths = append(fieldpaths, req.UpdateMask.Paths...)}if req.SourceDateEpoch != nil {tm := req.SourceDateEpoch.AsTime()ctx = epoch.WithSourceDateEpoch(ctx, &tm)}// 在bbolt中更新一條數據記錄// fieldpaths 為需要更新的字段updated, err := l.store.Update(ctx, image, fieldpaths...)if err != nil {return nil, errdefs.ToGRPC(err)}// 省略內容....// 發布事件....return &resp, nil
}
unpack階段
fetch
階段下載的鏡像,可以理解為壓縮包,unpack
階段就是解壓鏡像,創建快照等操作。
解壓鏡像好理解,創建快照是什么意思呢?
鏡像的文件系統是只讀的,容器的文件系統是可寫的,容器的文件系統是基于鏡像的文件系統創建的,這個過程就是創建快照。
在containerd中, 每個容器都有一個自己的快照,利用這個特性,可以實現容器的快速創建和銷毀。containerd實現有兩種
Snapshotter
,一種是通過overlayfs
實現,一種是通過native
實現。
overlayfs
是linux
內核的一個功能,native
是containerd
自己實現的一種快照方式。在
native
實現中,所有的快照都將是完全copy,所以native
的快照方式,會占用更多的磁盤空間。
以下代碼為ctr
部分實現。
// unpack主要代碼
// i := containerd.NewImageWithPlatform(client, img, platforms.Only(platform))
// err = i.Unpack(ctx, context.String("snapshotter"))
// image.go
func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...UnpackOpt) error {// 省略內容....manifest, err := i.getManifest(ctx, i.platform)if err != nil {return err}// 獲取鏡像的所有層layers, err := i.getLayers(ctx, i.platform, manifest)if err != nil {return err}var (// 用于對比鏡像的層和快照的層,如果鏡像的層和快照的層一致,則不需要創建快照a = i.client.DiffService()// 用于更新數據cs = i.client.ContentStore()chain []digest.Digestunpacked bool)// 獲取snapshottersnapshotterName, err = i.client.resolveSnapshotterName(ctx, snapshotterName)if err != nil {return err}sn, err := i.client.getSnapshotter(ctx, snapshotterName)if err != nil {return err}// 省略內容...for _, layer := range layers {// 獲取鏡像的層的數據、創建快照unpacked, err = rootfs.ApplyLayerWithOpts(ctx, layer, chain, sn, a, config.SnapshotOpts, config.ApplyOpts)if err != nil {return err}if unpacked {// Set the uncompressed label after the uncompressed// digest has been verified through apply.cinfo := content.Info{Digest: layer.Blob.Digest,Labels: map[string]string{labels.LabelUncompressed: layer.Diff.Digest.String(),},}// 更新數據庫if _, err := cs.Update(ctx, cinfo, "labels."+labels.LabelUncompressed); err != nil {return err}}chain = append(chain, layer.Diff.Digest)}// 省略內容....// 更新數據庫_, err = cs.Update(ctx, cinfo, fmt.Sprintf("labels.containerd.io/gc.ref.snapshot.%s", snapshotterName))return err
}
// rootts/apply.go
func ApplyLayerWithOpts(ctx context.Context, layer Layer, chain []digest.Digest, sn snapshots.Snapshotter, a diff.Applier, opts []snapshots.Opt, applyOpts []diff.ApplyOpt) (bool, error) {// 省略內容....// 以grpc方式獲取快照狀態,判斷是否存在if _, err := sn.Stat(ctx, chainID); err != nil {// 省略內容....// 對比差異, 同步數據if err := applyLayers(ctx, []Layer{layer}, append(chain, layer.Diff.Digest), sn, a, opts, applyOpts); err != nil {if !errdefs.IsAlreadyExists(err) {return false, err}} else {applied = true}}return applied, nil
}func applyLayers(ctx context.Context, layers []Layer, chain []digest.Digest, sn snapshots.Snapshotter, a diff.Applier, opts []snapshots.Opt, applyOpts []diff.ApplyOpt) error {// 省略內容....for {key = fmt.Sprintf(snapshots.UnpackKeyFormat, uniquePart(), chainID)// Prepare snapshot with from parent, label as root// 以grpc方式調用,準備快照mounts, err = sn.Prepare(ctx, key, parent.String(), opts...)if err != nil {if errdefs.IsNotFound(err) && len(layers) > 1 {// 遞歸調用if err := applyLayers(ctx, layers[:len(layers)-1], chain[:len(chain)-1], sn, a, opts, applyOpts); err != nil {if !errdefs.IsAlreadyExists(err) {return err}}// Do no try applying layers againlayers = nilcontinue} else if errdefs.IsAlreadyExists(err) {// Try a different keycontinue}// Already exists should have the caller retryreturn fmt.Errorf("failed to prepare extraction snapshot %q: %w", key, err)}break}defer func() {// 失敗回滾操作if err != nil {if !errdefs.IsAlreadyExists(err) {log.G(ctx).WithError(err).WithField("key", key).Infof("apply failure, attempting cleanup")}// 以grpc方式調用,刪除快照if rerr := sn.Remove(ctx, key); rerr != nil {log.G(ctx).WithError(rerr).WithField("key", key).Warnf("extraction snapshot removal failed")}}}()// 以grpc方式調用,對比,提取數據diff, err = a.Apply(ctx, layer.Blob, mounts, applyOpts...)if err != nil {err = fmt.Errorf("failed to extract layer %s: %w", layer.Diff.Digest, err)return err}if diff.Digest != layer.Diff.Digest {err = fmt.Errorf("wrong diff id calculated on extraction %q", diff.Digest)return err}// 以grpc方式調用,提交快照,更新數據庫if err = sn.Commit(ctx, chainID.String(), key, opts...); err != nil {err = fmt.Errorf("failed to commit snapshot %s: %w", key, err)return err}return nil
}
a.Apply
,sn.Prepare
,sn.Commit
等接口,均在congainerd
實現。以plugin
的方式注冊,grpc調用。
diff
接口實現。
// services/diff/local.go
type local struct {// 用于存儲處理函數differs []differ
}
// 將快照數據(差異數據)同步到指定位置
func (l *local) Apply(ctx context.Context, er *diffapi.ApplyRequest, _ ...grpc.CallOption) (*diffapi.ApplyResponse, error) {// 省略內容....for _, differ := range l.differs {// 執行同步操作// 這里不展開分析ocidesc, err = differ.Apply(ctx, desc, mounts, opts...)if !errdefs.IsNotImplemented(err) {break}}// 省略內容....return &diffapi.ApplyResponse{Applied: fromDescriptor(ocidesc),}, nil
}
// 快照數據比對差異
func (l *local) Diff(ctx context.Context, dr *diffapi.DiffRequest, _ ...grpc.CallOption) (*diffapi.DiffResponse, error) {// 省略內容....for _, d := range l.differs {// 執行對比操作// 提供已經存在的掛載(快照),和新的鏡像層進行差異比對// 這里不展開分析ocidesc, err = d.Compare(ctx, aMounts, bMounts, opts...)if !errdefs.IsNotImplemented(err) {break}}// 省略內容....return &diffapi.DiffResponse{Diff: fromDescriptor(ocidesc),}, nil
}
snapshotter
接口實現。
// services/snapshots/service.go
type service struct {// Snapshotter的具體實現,上文提到的overlayfs或者nativess map[string]snapshots.Snapshottersnapshotsapi.UnimplementedSnapshotsServer
}
// 準備快照
// 準備好的快照會交給diff接口,進行數據同步
func (s *service) Prepare(ctx context.Context, pr *snapshotsapi.PrepareSnapshotRequest) (*snapshotsapi.PrepareSnapshotResponse, error) {// 省略內容....// 返回快照掛載位置,以及當前快照的父快照// 默認掛載位置/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/自增數字id/fsmounts, err := sn.Prepare(ctx, pr.Key, pr.Parent, opts...)if err != nil {return nil, errdefs.ToGRPC(err)}return &snapshotsapi.PrepareSnapshotResponse{Mounts: fromMounts(mounts),}, nil
}
// 提交快照
// 提交的快照可以進行使用
func (s *service) Commit(ctx context.Context, cr *snapshotsapi.CommitSnapshotRequest) (*ptypes.Empty, error) {// 省略內容....// 提交快照// 提交后快照將變為active狀態if err := sn.Commit(ctx, cr.Name, cr.Key, opts...); err != nil {return nil, errdefs.ToGRPC(err)}return empty, nil
}
總結
ctr image pull
命令執行可以大致分為兩步:
- fetch階段,下載鏡像,創建鏡像記錄
- unpack階段,解壓鏡像,創建快照
快照的創建中包含差異對比,可以大大減少磁盤空間的占用。
當獲取系統中第一個鏡像時, 每一個layer都會創建一個快照。
當獲取系統中第二個鏡像時, 如果第二個鏡像的layer和第一個鏡像的layer一致,則不會創建快照,只會創建一條鏡像記錄。
快照創建流程梳理, 以單一layer為例。