containerd源代码分析: 整体架构

本文从代码的大的整体组织上来熟悉containerd项目
containerd项目总的说是一个cs模式的原生控制台程序组。containerd作为服务端来接收处理client的各种请求,如常用的拉取推送镜像,创建查询停止容器,生成快照,发送消息等。client/server之间通过grpc和ttrpc框架进行交互。
我们可以先看一下contanerd源代码中的cmd下文件夹如图:

image.png
每一个目录都会生成一个二进制文件:
containerd 为服务主程序。
containerd-shim-runc-v2: 为主程序和runtime程序之间交互的垫片二进制
contianerd-stress:不是实际生产使用的程序,而是为了对containerd进行压力测试使用
ctr: 是containerd的客户端二进制,可以发送各种命令请求。上一个用法图:

image.png

我们看到了项目的最终输出的物理文件。那么具体的交互逻辑或者说流程是什么样的。其实每个具体的功能都是通过各个相应的插件来完成。containerd背后有各种标准如oci、cni、csi等,采用插件的形式方便了各个供应商扩展自己的功能。我们先从静态代码上梳理一下.在项目的core目录下包含了containerd实现的大模块,如容器、内容、差异、镜像、元数据存储、租约、指标、挂载点、镜像注册中心、快照、沙箱、运行时。

image.png
我们以content模块为例将进行探索。
在core/content/content.go中抽象出来了content的接口类型如:

type Store interface {
   Manager
   Provider
   IngestManager
   Ingester
}

这个是个接口聚合,每个都可以展开成一个具体接口或者接口组合。
如:

// InfoProvider provides info for content inspection.
type InfoProvider interface {
   // Info will return metadata about content available in the content store.
   //
   // If the content is not present, ErrNotFound will be returned.
   Info(ctx context.Context, dgst digest.Digest) (Info, error)
}

我们刚才说每个功能都是由插件实现(插件会在server启动时加载,先埋下伏笔)那么进入plugins/content/local/store.go,可以看到它实现了上面的InfoProvider接口

func (s *store) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
   p, err := s.blobPath(dgst)
   if err != nil {
      return content.Info{}, fmt.Errorf("calculating blob info path: %w", err)
   }

   fi, err := os.Stat(p)
   if err != nil {
      if os.IsNotExist(err) {
         err = fmt.Errorf("content %v: %w", dgst, errdefs.ErrNotFound)
      }

      return content.Info{}, err
   }
   var labels map[string]string
   if s.ls != nil {
      labels, err = s.ls.Get(dgst)
      if err != nil {
         return content.Info{}, err
      }
   }
   return s.info(dgst, fi, labels), nil
}

Manager的接口也被实现了,这里不列出了。现在是实现有了。插件在哪里使用它呢,通过在鼠标右键->查找用法(IDEA+go插件环境)找到
cmd/containerd/server/server.go文件中的

func LoadPlugins(ctx context.Context, config *srvconfig.Config) ([]plugin.Registration, error) {
   // load all plugins into containerd
   //  .............
   // load additional plugins that don't automatically register themselves
   registry.Register(&plugin.Registration{
      Type: plugins.ContentPlugin,
      ID:   "content",
      InitFn: func(ic *plugin.InitContext) (interface{}, error) {
         root := ic.Properties[plugins.PropertyRootDir]
         ic.Meta.Exports["root"] = root
         return local.NewStore(root)
      },
   })
   
   //....................
}

第11行,return local.NewStore(root) 对store进行了实例化。
插件类型为plugins.ContentPlugin,id为content.到此完成了插件对接口实现的包装和注册。
plugins/content/local/store.go对store的实现可以在本地直接调用。没有涉及到客户端client发送请求调用。
客户端请求的插件同样可以在上述的loadplugins函数中找到

clients := &proxyClients{}
for name, pp := range config.ProxyPlugins {
   var (
      t plugin.Type
      f func(*grpc.ClientConn) interface{}

      address = pp.Address
      p       v1.Platform
      err     error
   )

   switch pp.Type {
    //........

   case string(plugins.ContentPlugin), "content":
      t = plugins.ContentPlugin
      f = func(conn *grpc.ClientConn) interface{} {
         return csproxy.NewContentStore(csapi.NewContentClient(conn))
      }
      //......
      
      ```
registry.Register(&plugin.Registration{
   Type: t,
   ID:   name,
   InitFn: func(ic *plugin.InitContext) (interface{}, error) {
      ic.Meta.Exports = exports
      ic.Meta.Platforms = append(ic.Meta.Platforms, p)
      conn, err := clients.getClient(address)
      if err != nil {
         return nil, err
      }
      return f(conn), nil
   },
})

第1行声明了客户端proxyClients,第17行创建了生成store的函数体。在第29行对proxyClients实例化创建了到server的连接,并在第33行调用前面声明的函数体完成初始化的逻辑,同时在第23行也实现了对插件的注册。
在这个函数里还进行了snapshotsandboxdiff插件的注册。
如果再进一步看下第18行的代码发现它是调用core/content/proxy/content_store.go中的函数func NewContentStore(client contentapi.ContentClient) content.Store{...}
可以在plugins/services/content/service.go中找到具体的grpc plugin content

func init() {
   registry.Register(&plugin.Registration{
      Type: plugins.GRPCPlugin,
      ID:   "content",
      Requires: []plugin.Type{
         plugins.ServicePlugin,
      },
      InitFn: func(ic *plugin.InitContext) (interface{}, error) {
         cs, err := ic.GetByID(plugins.ServicePlugin, services.ContentService)
         if err != nil {
            return nil, err
         }
         return contentserver.New(cs.(content.Store)), nil
      },
   })
}

可以看到在第6行又依赖了plugins.ServicePlugin插件类型。serviceplugin类型在plugins/services/content/store.go文件中可以找到

func init() {
   registry.Register(&plugin.Registration{
      Type: plugins.ServicePlugin,
      ID:   services.ContentService,
      Requires: []plugin.Type{
         plugins.EventPlugin,
         plugins.MetadataPlugin,
      },
      InitFn: func(ic *plugin.InitContext) (interface{}, error) {
         m, err := ic.GetSingle(plugins.MetadataPlugin)
         if err != nil {
            return nil, err
         }
         ep, err := ic.GetSingle(plugins.EventPlugin)
         if err != nil {
            return nil, err
         }

         s, err := newContentStore(m.(*metadata.DB).ContentStore(), ep.(events.Publisher))
         return s, err
      },
   })
}

id为services.ContentService的插件。并且调用插件返回content.store,并在第13行作为参数传入contentserver的new构造函数创建contentserver实例。
contentserver主要完成的接收grpc的请求然后调用store的实现。
如info功能的业务逻辑如下:

func (s *service) Info(ctx context.Context, req *api.InfoRequest) (*api.InfoResponse, error) {
   dg, err := digest.Parse(req.Digest)
   if err != nil {
      return nil, status.Errorf(codes.InvalidArgument, "%q failed validation", req.Digest)
   }

   bi, err := s.store.Info(ctx, dg)
   if err != nil {
      return nil, errdefs.ToGRPC(err)
   }

   return &api.InfoResponse{
      Info: infoToGRPC(bi),
   }, nil
}

由于使用了grpc的通讯框架,content的协议定义文件为api/services/content/v1/content.proto,里面定义了消息格式

message InfoRequest {
 string digest = 1;
}

message InfoResponse {
 Info info = 1;
}

和服务接口

service Content {
 // Info returns information about a committed object.
 //
 // This call can be used for getting the size of content and checking for
 // existence.
 rpc Info(InfoRequest) returns (InfoResponse);
 // ......
 }

生成的go grpc实现的文件为:api/services/content/v1/content_grpc.pb.go 其中info功能的服务功能如下:

func _Content_Info_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
   in := new(InfoRequest)
   if err := dec(in); err != nil {
      return nil, err
   }
   if interceptor == nil {
      return srv.(ContentServer).Info(ctx, in)
   }
   info := &grpc.UnaryServerInfo{
      Server:     srv,
      FullMethod: "/containerd.services.content.v1.Content/Info",
   }
   handler := func(ctx context.Context, req interface{}) (interface{}, error) {
      return srv.(ContentServer).Info(ctx, req.(*InfoRequest))
   }
   return interceptor(ctx, in, info, handler)
}

在第7行、第14行调用了上述插件中的contentserver的info实现。
此handler也封装到了Content_ServiceDesc结构中然后通过此文件中的

func RegisterContentServer(s grpc.ServiceRegistrar, srv ContentServer) {
   s.RegisterService(&Content_ServiceDesc, srv)
}

函数封装了contentserver,此函数也被plugins/services/content/contentserver/contentserver.go中的

func (s *service) Register(server *grpc.Server) error {
   api.RegisterContentServer(server, s)
   return nil
}

调用,此函数在插件加载完成后又被server注册到本地缓存中具体见文章开始的cmd/containerd/server/server.go中的new函数代码段:

for _, p := range loaded {
   id := p.URI()
   log.G(ctx).WithFields(log.Fields{"id": id, "type": p.Type}).Info("loading plugin")
   var mustSucceed int32

   initContext := plugin.NewContext(
      ctx,
      initialized,
      map[string]string{
         plugins.PropertyRootDir:      filepath.Join(config.Root, id),
         plugins.PropertyStateDir:     filepath.Join(config.State, id),
         plugins.PropertyGRPCAddress:  config.GRPC.Address,
         plugins.PropertyTTRPCAddress: config.TTRPC.Address,
      },
   )
   initContext.RegisterReadiness = func() func() {
      atomic.StoreInt32(&mustSucceed, 1)
      return s.RegisterReadiness()
   }

   // load the plugin specific configuration if it is provided
   if p.Config != nil {
      pc, err := config.Decode(ctx, id, p.Config)
      if err != nil {
         return nil, err
      }
      initContext.Config = pc
   }
   result := p.Init(initContext)
   if err := initialized.Add(result); err != nil {
      return nil, fmt.Errorf("could not add plugin result to plugin set: %w", err)
   }

   instance, err := result.Instance()
   if err != nil {
      if plugin.IsSkipPlugin(err) {
         log.G(ctx).WithFields(log.Fields{"error": err, "id": id, "type": p.Type}).Info("skip loading plugin")
      } else {
         log.G(ctx).WithFields(log.Fields{"error": err, "id": id, "type": p.Type}).Warn("failed to load plugin")
      }
      if _, ok := required[id]; ok {
         return nil, fmt.Errorf("load required plugin %s: %w", id, err)
      }
      // If readiness was registered during initialization, the plugin cannot fail
      if atomic.LoadInt32(&mustSucceed) != 0 {
         return nil, fmt.Errorf("plugin failed after registering readiness %s: %w", id, err)
      }
      continue
   }

   delete(required, id)
   // check for grpc services that should be registered with the server
   if src, ok := instance.(grpcService); ok {
      grpcServices = append(grpcServices, src)
   }
   if src, ok := instance.(ttrpcService); ok {
      ttrpcServices = append(ttrpcServices, src)
   }
   if service, ok := instance.(tcpService); ok {
      tcpServices = append(tcpServices, service)
   }

   s.plugins = append(s.plugins, result)
}
if len(required) != 0 {
   var missing []string
   for id := range required {
      missing = append(missing, id)
   }
   return nil, fmt.Errorf("required plugin %s not included", missing)
}

// register services after all plugins have been initialized
for _, service := range grpcServices {
   if err := service.Register(grpcServer); err != nil {
      return nil, err
   }
}

第一行loaded表示所以加载后的插件,在29行初始化插件,第34行得到cotnent.store接口的实例,第54行把实例放到缓存grpcservices中,
最后在第75行中调用contentserver的register函数。
上面主要是grpc server端的服务逻辑。客户端的使用逻辑可以在
core/content/proxy/content_store.go文件中找到,看info函数代码:

func (pcs *proxyContentStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
   resp, err := pcs.client.Info(ctx, &contentapi.InfoRequest{
      Digest: dgst.String(),
   })
   if err != nil {
      return content.Info{}, errdefs.FromGRPC(err)
   }

   return infoFromGRPC(resp.Info), nil
}

在第2行中调用grpc client代理的info方法向服务器发送请求。

项目中调用的地方不止一处。由下图可见在ctr客户端的cmd/ctr/commands/content/content.go文件中也有使用

image.png

// Nothing updated, do no clear
if len(paths) == 0 {
   info, err = cs.Info(ctx, info.Digest)
} else {
   info, err = cs.Update(ctx, info, paths...)
}

如第3行的info和第5行的update均是grpc通信示例。
至此从迷宫一样的代码中梳理出了一个骨架结构,是否可以学到一些设计思想呢?snapshot、diff、sandbox模块的逻辑类似content,具体的细节功能不再展开,后面将就容器的创建流程在对代码进行梳理。不对的地方,请不吝批评指正!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/481422.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

About Online Taxis

About Online Taxis 关于网络预约出租汽车(网约车) 1)网络预约出租汽车 驾驶员证(人证) 2)网络预约出租汽车 运输证(车证) 民之苦已久......

【PHP】通过PHP安装数据库并使数据初始化

一、前言 有些CMS在部署的时候不用使用数据库工具,而是通过数据库安装页面就能完成数据库创建和数据填充,所以自己就想动手做一个这样的功能,这样在给别人安装系统的时候就不用再那么麻烦了,直接一键安装解决了。 二、效果图 输…

jupyter notebook使用教程

首先是打开jupyter notebook 下载安装好之后,直接在命令行中输入‘jupyter notebook’即可跳转到对应页面 还可以进入想要打开的文件夹,然后再文件夹中打开中断,执行‘jupyter notebook’命令,就能够打开对应文件界面的jupyter …

iOS模拟器 Unable to boot the Simulator —— Ficow笔记

本文首发于 Ficow Shen’s Blog,原文地址: iOS模拟器 Unable to boot the Simulator —— Ficow笔记。 内容概览 前言终结模拟器进程命令行改权限清除模拟器缓存总结 前言 iOS模拟器和Xcode一样不靠谱,问题也不少。😂 那就有病治…

时序预测 | Matlab基于BiTCN-LSTM双向时间卷积长短期记忆神经网络时间序列预测

时序预测 | Matlab基于BiTCN-LSTM双向时间卷积长短期记忆神经网络时间序列预测 目录 时序预测 | Matlab基于BiTCN-LSTM双向时间卷积长短期记忆神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab基于BiTCN-LSTM双向时间卷积长短期记忆神经网络时…

PSO-CNN-SVM,基于PSO粒子群优化算法优化卷积神经网络CNN结合支持向量机SVM数据分类(多特征输入多分类)-附代码

PSO-CNN-SVM,基于PSO粒子群优化算法优化卷积神经网络CNN结合支持向量机SVM数据分类 下面是一个大致的步骤: 数据准备: 准备训练集和测试集数据。对数据进行预处理,包括归一化、标准化等。 设计CNN模型: 设计合适的CNN…

vue.config.js配置项

vue.config.js配置项 vue-cli3 脚手架搭建完成后,项目目录中没有 vue.config.js 文件,需要手动创建 创建vue.config.js vue.config.js(相当于之前的webpack.config.js) 是一个可选的配置文件,如果项目的 (和 package.json 同级的) 根目录中存…

基于OrangePi Zero2的智能家居项目(准备阶段)

一、需求及项目准备(前期准备) 1、各类的需求 以及复习巩固的东西 2、系统框架图 3、硬连接线 3.1硬件准备 USB充电头(当前实测可用:5V/2.5A)x1、USB转TYPE-Cx1、SU-03Tx1、烟雾报警模块x1、4路继 电器x1、 OLEDx1、 电磁锁x1&a…

【C语言】多文件编程以及static关键字

1、多文件编程 把函数声明放在头文件xxx.h中&#xff0c;在主函数中包含相应头文件在头文件对应的xxx.c中实现xxx.h声明的函数 a、主文件 #include<stdio.h> #include "MyMain.h"//需要包含头文件&#xff0c;头文件包含我们自定义的函数int main(){int num…

JWT的实现及其适用场景

官方文档 一、什么是JWT JWT&#xff08;全称JSON Web Token&#xff09;是一种开放标准&#xff08;RFC 7519&#xff09;&#xff0c;它定义了一种紧凑且自包含的方式&#xff0c;用于作为JSON对象在各方之间安全地传输信息。此信息是经过数字签名的&#xff0c;因此可以验…

分页多线程处理大批量数据

1.业务场景 因为需要从一个返利明细表中获取大量的数据&#xff0c;生成返利报告&#xff0c;耗时相对较久&#xff0c;作为后台任务执行。但是后台任务如果不用多线程处理&#xff0c;也会要很长时间才能处理完。 另外考虑到数据量大&#xff0c;不能一次查询所有数据在内存…

LaTeX论文汇报ppt模板

在 LaTeX 的 beamer 类中&#xff0c;您可以使用不同的主题和模板来创建适合论文汇报的演示文稿。以下是一个使用了比较正式的 Madrid 主题的模板&#xff0c;您可以基于这个模板进行定制和扩展&#xff0c;以满足您论文汇报的需求。当需要在ppt输入中文的时候需要将第一行中的…

北京中科富海低温科技有限公司确认出席2024第三届中国氢能国际峰会

会议背景 随着全球对清洁能源的迫切需求&#xff0c;氢能能源转型、工业应用、交通运输等方面具有广阔前景&#xff0c;氢能也成为应对气候变化的重要解决方案。根据德勤的报告显示&#xff0c;到2050年&#xff0c;绿色氢能将有1.4万亿美元市场。氢能产业的各环节的关键技术突…

大数据技术在工厂生产数字转型中的应用与价值

hello宝子们...我们是艾斯视觉擅长ui设计和前端开发10年经验&#xff01;希望我的分享能帮助到您&#xff01;如需帮助可以评论关注私信我们一起探讨&#xff01;致敬感谢感恩&#xff01; 随着大数据技术的快速发展&#xff0c;越来越多的企业开始关注并应用大数据技术&#x…

C语言:自定义类型(结构体)

目录 一、结构的特殊声明二、结构的自引用三、结构体内存对齐1.对齐规则2.为什么存在内存对齐(1)平台原因 (移植原因)&#xff1a;(2)性能原因&#xff1a; 3.修改默认对齐数 四、结构体传参五、结构体实现位段1.什么是位段2.位段的内存分配3.位段的跨平台问题4.位段使用的注意…

tftp使用

下载 sudo apt-get install tftpd-hpa 创建文件夹 mkdir /home/ljl/work/tftpd mkdir /home/ljl/tftpd chmod 777 tftpd/编辑 sudo vim /etc/default/tftpd-hpa //服务器端 sudo apt-get install tftp-hpa //客户端编辑权限 sudo vi /etc/default/tftpd-hpa 内容&#xff1…

jenkins构建完成后部署到本机,无法读取容器外文件夹

项目背景&#xff1a; Dockerjenkins 构建完成后&#xff0c;要把打包的dist文件夹内容移动到网站目录 /www/wwwroot/xxxxxx 文件夹下&#xff1b;但是获取不到jenkins容器外的文件夹。 解决办法&#xff1a; 在容器中&#xff0c;添加挂载/映射本机目录&#xff0c;把网站…

两直线交点算法 C

求两直线交点算法 有中间交点 则CD在AB异侧 A B A C A B A D \nobreak AB \times AC \newline AB \times AD ABACABAD 异号 叉乘后相乘小于零 等于零的几种情况 A B C与AB共线 D与AB共线 求交点&#xff0c;可由面积比例用叉乘计算 C E C D S A B C S A B C D . \frac…

yarn的使用与安装

文章目录 1.安装方式一&#xff1a;全局安装yarn2.安装方式二&#xff1a;通过开启corepack安装3.其他部分yarn命令4.Yarn镜像配置5.Pnpm使用方法同yarn无区别,可按照以上yarn的安装以及使用方式来使用pnmp 1.安装方式一&#xff1a;全局安装yarn 全局安装yarn npm i yarn -g…

视频记录历史播放位置效果

简介 每次打开页面视频从上一次的播放位置开始播放 利用lodash库做节流 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-sca…