[containerd] ContentPlugin插件源码分析

文章目录

  • 1. 概述
  • 2. 环境
  • 3. 注册
  • 4. 核心概念
    • 4.1. blob
    • 4.2. ingest
  • 5. 抽象接口
    • 5.1. Manager接口
    • 5.2. Provider
    • 5.3. IngestManager
    • 5.4. Ingester
  • 6. 核心实现
    • 6.1. Info
    • 6.2. Update
    • 6.3. Walk
    • 6.4. Delete
    • 6.5. ReaderAt
    • 6.6. Status
    • 6.7. ListStatuses
    • 6.8. Abort
    • 6.9. Writer
  • 7. 总结

1. 概述

  今天我们来看一下ContentPlugin,研究一下ContentPlugin究竟给containerd提供了什么功能。

  虽然下图中标记出来的为ContentService,但是这篇文章并不分析ContentService。这是因为ContentService这个服务的底层依赖就是ContentService

在这里插入图片描述

  我们一起来看一下ContentService这个服务的注册代码,如下:

// services/content/store.go
func init() {plugin.Register(&plugin.Registration{Type: plugin.ServicePlugin,ID:   services.ContentService,Requires: []plugin.Type{plugin.EventPlugin,plugin.MetadataPlugin,},InitFn: func(ic *plugin.InitContext) (interface{}, error) {// 获取元数据插件,其实元数据插件的实现原理就是依赖boltdb来存储KV键值对m, err := ic.Get(plugin.MetadataPlugin)if err != nil {return nil, err}// 获取事件插件,实际上这里获取的就是事件biz层,有点类似于service注入biz层依赖ep, err := ic.Get(plugin.EventPlugin)if err != nil {return nil, err}// 元数据的contentStore实际上就是对于blob的增删改查s, err := newContentStore(m.(*metadata.DB).ContentStore(), ep.(events.Publisher))return s, err},})
}// metadata/plugin/plugin.go
func init() {plugin.Register(&plugin.Registration{Type: plugin.MetadataPlugin,ID:   "bolt",Requires: []plugin.Type{plugin.ContentPlugin, // 这里表明MetadataPlugin插件依赖ContentPluginplugin.SnapshotPlugin,},Config: &BoltConfig{ContentSharingPolicy: SharingPolicyShared,},// 省略不重要的代码...}
}

  所谓的ContentService,实际上就是插件类型为ServicePlugin,且IDContentService的插件,从上面的注册代码可以看出,这个插件依赖MetadataPlugin插件的ContentStore服务,通过debug源码可以知道,MetadataPlugin插件的ContentStore能力就是我们今天需要分析的ContentPlugin

2. 环境

  • containerd tag版本:v1.7.2

3. 注册

  ContentPlugin注册代码如下,这里省略了一些不重要的细节。

// services/server/server.go
func LoadPlugins(ctx context.Context, config *srvconfig.Config) ([]*plugin.Registration, error) {// load all plugins into containerd// 如果没有指定插件的位置,那么默认从/var/lib/containerd/plugins目录中加载插件path := config.PluginDirif path == "" {path = filepath.Join(config.Root, "plugins")}// 实际上这里目前是空的,并不会加载任何插件if err := plugin.Load(path); err != nil {return nil, err}// load additional plugins that don't automatically register themselves// TODO content插件究竟干了啥?// 这个插件和content-service插件有何区别?plugin.Register(&plugin.Registration{Type: plugin.ContentPlugin,ID:   "content",InitFn: func(ic *plugin.InitContext) (interface{}, error) {// TODO 这里暴露的数据有何作用?ic.Meta.Exports["root"] = ic.Root// 注意,每个插件在初始化的时候都被修改了root目录,规则为:<root>/<plugin-type>.<plugin-id>// 对于content插件来说,root目录为:/var/lib/containerd/io.containerd.content.v1.contentreturn local.NewStore(ic.Root)},})// 省略不重要的代码...}

  containerd的注册代码还是比较简单,注册也仅仅是把插件相关的元信息封装为Registration,并没有做其它事情。

  可以看到,这里注册ContentPlugin实际上就是实例化local.store

  接下来我们看看,local.store到底提供了什么能力?

4. 核心概念

4.1. blob

  起初看代码时,看到这个概念听懵逼的,后来搜索了一下,blob应该是Binary Large Object的缩写,也就是二进制大对象。这个概念并不是containerd发明的,在存储世界中,很早就有的一个概念。

  blob就是数据存储的一种方式,跟对象存储有点类似。在containerd中,blob可以理解为镜像层。我们知道,一个镜像是由多层叠加而成的,尤其在下载镜像的时候特别明显。镜像下载完成之后,会被保存到/var/lib/containerd/io.containerd.content.v1.content/blobs当中

root@containerd:/var/lib/containerd/io.containerd.content.v1.content/blobs# tree
.
└── sha256├── 00a1f6deb2b5d3294cb50e0a59dfc47f67650398d2f0151911e49a56bfd9c355├── 01085d60b3a624c06a7132ff0749efc6e6565d9f2531d7685ff559fb5d0f669f├── 029a81f05585f767fb7549af85a8f24479149e2a73710427a8775593fbe86159├── 05a79c7279f71f86a2a0d05eb72fcb56ea36139150f0a75cd87e80a4272e4e39├── 06212d50621c9654d97e7ec78e972b5017e139b11763375ee4c28bace1fcc087├── 0bbbd1f379fc1f577c5db15c9deac4a218637e4af0196d97e6771f59d9815355

4.2. ingest

  ingest这个概念实际上也挺让人费解的,这个概念在containerd中也可以理解为镜像层,不过与blob不同的是,ingest专指没有下载完成的镜像。所谓的没有下载完成的镜像,就是在镜像下载过程中,由于某些原因,譬如网络,用户执行ctrl + c,导致镜像下载中断,此时的镜像就会保存在ingest目录当中

  ingest所对应的镜像,一般会存储在:/var/lib/containerd/io.containerd.content.v1.content/ingest目录当中。一般情况下,这个目录是空的,但是我们可以通过中断镜像下载来看到ingest数据。

root@containerd:/var/lib/containerd/io.containerd.content.v1.content/ingest#
root@containerd:/var/lib/containerd/io.containerd.content.v1.content/ingest# ls
root@containerd:/var/lib/containerd/io.containerd.content.v1.content/ingest# tree
.0 directories, 0 files
root@containerd:/var/lib/containerd/io.containerd.content.v1.content/ingest#
root@containerd:/var/lib/containerd/io.containerd.content.v1.content/ingest# nerdctl image rm redis:6.2
FATA[0000] 1 errors:
no such image: redis:6.2
docker.io/library/redis:6.2:                                                      resolved       |++++++++++++++++++++++++++++++++++++++|
index-sha256:9e75c88539241ad7f61bc9c39ea4913b354064b8a75ca5fc40e1cef41b645bc0:    done           |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:3b2deb4fdf85229e72229c44bb80c3939e0f93ce93ce8a00cb6b363b0e40b490: done           |++++++++++++++++++++++++++++++++++++++|
config-sha256:808c9871bf9dae251c8be67691c3a827742c06f3fb5cf8658568aa7eb0738227:   downloading    |--------------------------------------|    0.0 B/7.6 KiB
elapsed: 4.2 s                                                                    total:  3.4 Ki (817.0 B/s)
^C
root@containerd:/var/lib/containerd/io.containerd.content.v1.content/ingest# tree
.
└── 9b372caec3ac7f53c08336ba4b8aba9006b1cf5d56f205fe9f22e828bf9d2ffa├── data├── ref├── startedat├── total└── updatedat1 directory, 5 files
root@containerd:/var/lib/containerd/io.containerd.content.v1.content/ingest#

5. 抽象接口

5.1. Manager接口

  Manager接口封装了对于blob数据的查看、更新、遍历、删除动作,接口功能很简单,不做过多解释,后面看看具体实现就很简单了

// Manager实际上就是对于镜像层获取信息、修改信息、遍历镜像层以及删除镜像层的封装
type Manager interface {// Info will return metadata about content available in the content store.//// If the content is not present, ErrNotFound will be returned.// 获取摘要所对应的镜像层的大小、创建时间、更新时间、标签信息,dgst相当于镜像层的ID,Info是直接通过读取操作系统中的镜像层文件返回的Info(ctx context.Context, dgst digest.Digest) (Info, error)// Update updates mutable information related to content.// If one or more fieldpaths are provided, only those// fields will be updated.// Mutable fields://  labels.*// 更新镜像层的标签信息 TODO 看起来containerd并没有实现镜像层信息更新Update(ctx context.Context, info Info, fieldpaths ...string) (Info, error)// Walk will call fn for each item in the content store which// match the provided filters. If no filters are given all// items will be walked.// 遍历containerd存储的镜像层,并根据指定的过滤器过滤不满足要求的镜像层,这里的过滤器可以根据摘要、标签或者大小,不过根据源码显示// 根据大小过滤以及根据标签过滤并没有实现Walk(ctx context.Context, fn WalkFunc, filters ...string) error// Delete removes the content from the store.// 根据摘要删除某个镜像层Delete(ctx context.Context, dgst digest.Digest) error
}

5.2. Provider

  Provider封装了对于blob的写入

// 此接口可以用于读取某镜像层(通过摘要)数据,并且可以指定偏移量
type Provider interface {// ReaderAt only requires desc.Digest to be set.// Other fields in the descriptor may be used internally for resolving// the location of the actual data.ReaderAt(ctx context.Context, desc ocispec.Descriptor) (ReaderAt, error)
}

5.3. IngestManager

  IngestManager接口封装了对于ingest类型的数据的查看、终端功能。Abort接口的实现实际上就是删除ingest的过程,有点没有搞懂为啥这个接口叫做Abort,而不是Delete

// IngestManager provides methods for managing ingestions. An ingestion is a
// not-yet-complete writing operation initiated using Ingester and identified
// by a ref string.
// 到底如何理解ingest这个概念? 根据注释的含义,实际上就是ingest就是一个还未完成的写操作,这里的写操作肯定是指的镜像的写操作
// IngestManager用于抽象还未完成镜像层的查询、删除操作
type IngestManager interface {// Status returns the status of the provided ref.Status(ctx context.Context, ref string) (Status, error)// ListStatuses returns the status of any active ingestions whose ref match// the provided regular expression. If empty, all active ingestions will be// returned.// 返回所有镜像的信息,并根据过滤器过滤不需要的镜像ListStatuses(ctx context.Context, filters ...string) ([]Status, error)// Abort completely cancels the ingest operation targeted by ref.// 移除镜像所指向的ingest的所有数据Abort(ctx context.Context, ref string) error
}

5.4. Ingester

  Ingester接口抽象了对于ingest数据的写入。

// Ingester writes content
// ingest的写入接口
type Ingester interface {// Writer initiates a writing operation (aka ingestion). A single ingestion// is uniquely identified by its ref, provided using a WithRef option.// Writer can be called multiple times with the same ref to access the same// ingestion.// Once all the data is written, use Writer.Commit to complete the ingestion.Writer(ctx context.Context, opts ...WriterOpt) (Writer, error)
}

6. 核心实现

6.1. Info

  Info接口主要是根据摘要信息,读取/var/lib/containerd/io.containerd.content.v1.content/blobs目录下的blob,其中包含blob的大小、创建时间以及更新时间。

// Info Content服务实现Info非常简单,就是根据摘要信息拼接出这个摘要对应的镜像层的位置,然后当成一个普通文件读取其大小、创建时间、更新时间等
// Info接口用于根据摘要镜像层的信息,其实就是查看的二进制文件信息,在containerd中被称为blob
func (s *store) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {// blob为binary large object的缩写,也就是二进制形式的大对象// blob的概念可以参考这个连接:https://www.cloudflare.com/zh-cn/learning/cloud/what-is-blob-storage/// 这里实现的逻辑很简单,就是根据摘要信息拼接处此摘要指向的镜像层的路径,目录为:/var/lib/containerd/io.containerd.content.v1.content/blobs/sha256/<digest>p, err := s.blobPath(dgst)if err != nil {return content.Info{}, fmt.Errorf("calculating blob info path: %w", err)}// 判断这个摘要对应的镜像层是否存在,毕竟在操作系统中,bolb就是一个普通文件而已,还是有可能被用户删除的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]stringif s.ls != nil {labels, err = s.ls.Get(dgst)if err != nil {return content.Info{}, err}}// 直接读取操作系统中文件的大小、修改时间、创建时间等等return s.info(dgst, fi, labels), nil
}

6.2. Update

  Update接口用于更新blob的标签以及更新时间

// Update 用于更新镜像层的标签信息,TODO 看起来containerd并没有实现镜像层信息更新
// 根据摘要更新镜像层的信息,镜像层其实就是一个二进制文件,在containerd中被称为blob。
func (s *store) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {// 如果没有初始化标签存储器,肯定是不能更改的if s.ls == nil {return content.Info{}, fmt.Errorf("update not supported on immutable content store: %w", errdefs.ErrFailedPrecondition)}// 获取镜像层的存储路径:/var/lib/containerd/io.containerd.content.v1.content/blobs/sha256/<digest>p, err := s.blobPath(info.Digest)if err != nil {return content.Info{}, fmt.Errorf("calculating blob path for update: %w", err)}// 判断镜像层是否存在fi, err := os.Stat(p)if err != nil {if os.IsNotExist(err) {err = fmt.Errorf("content %v: %w", info.Digest, errdefs.ErrNotFound)}return content.Info{}, err}var (all    boollabels map[string]string)if len(fieldpaths) > 0 {for _, path := range fieldpaths {if strings.HasPrefix(path, "labels.") {if labels == nil {labels = map[string]string{}}key := strings.TrimPrefix(path, "labels.")labels[key] = info.Labels[key]continue}switch path {case "labels":all = truelabels = info.Labelsdefault:return content.Info{}, fmt.Errorf("cannot update %q field on content info %q: %w", path, info.Digest, errdefs.ErrInvalidArgument)}}} else {all = truelabels = info.Labels}if all {err = s.ls.Set(info.Digest, labels)} else {labels, err = s.ls.Update(info.Digest, labels)}if err != nil {return content.Info{}, err}info = s.info(info.Digest, fi, labels)info.UpdatedAt = time.Now()if err := os.Chtimes(p, info.UpdatedAt, info.CreatedAt); err != nil {log.G(ctx).WithError(err).Warnf("could not change access time for %s", info.Digest)}return info, nil
}

6.3. Walk

  Walk接口实现的功能很简单,写过golnag遍历目录的同学应该不会感到陌生。此接口会遍历/var/lib/containerd/io.containerd.content.v1.content/blobs,同时根据过滤器筛选出满足条件的blob,然后调用用户传递的fn函数。

// Walk 遍历containerd当前所有的镜像层,镜像层其实就是一个二进制文件,在containerd中被称为blob。
// 同时,如果制定了过滤器,那就按照指定的过滤器遍历符合条件的镜像层
func (s *store) Walk(ctx context.Context, fn content.WalkFunc, fs ...string) error {// 获取blob对象的存储路径:/var/lib/containerd/io.containerd.content.v1.content/blobsroot := filepath.Join(s.root, "blobs")filter, err := filters.ParseAll(fs...)if err != nil {return err}var alg digest.Algorithm// 中规中矩的遍历目录return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {if err != nil {return err}// 如果当前镜像层的算法不可用,直接退出if !fi.IsDir() && !alg.Available() {return nil}// TODO(stevvooe): There are few more cases with subdirs that should be// handled in case the layout gets corrupted. This isn't strict enough// and may spew bad data.// 忽略根目录if path == root {return nil}if filepath.Dir(path) == root {alg = digest.Algorithm(filepath.Base(path))if !alg.Available() {alg = ""return filepath.SkipDir}// descending into a hash directoryreturn nil}dgst := digest.NewDigestFromEncoded(alg, filepath.Base(path))if err := dgst.Validate(); err != nil {// log error but don't reportlog.L.WithError(err).WithField("path", path).Error("invalid digest for blob path")// if we see this, it could mean some sort of corruption of the// store or extra paths not expected previously.}var labels map[string]stringif s.ls != nil {labels, err = s.ls.Get(dgst)if err != nil {return err}}info := s.info(dgst, fi, labels)if !filter.Match(content.AdaptInfo(info)) {return nil}return fn(info)})
}

6.4. Delete

  Delete接口用于根据摘要信息删除blob

// Delete removes a blob by its digest.
//
// While this is safe to do concurrently, safe exist-removal logic must hold
// some global lock on the store.
// 根据摘要删除镜像层,镜像层其实就是一个二进制文件,在containerd中被称为blob
func (s *store) Delete(ctx context.Context, dgst digest.Digest) error {// 找到镜像层的存储路径:/var/lib/containerd/io.containerd.content.v1.content/blobs/sha256/<digest>bp, err := s.blobPath(dgst)if err != nil {return fmt.Errorf("calculating blob path for delete: %w", err)}// 删除文件if err := os.RemoveAll(bp); err != nil {if !os.IsNotExist(err) {return err}return fmt.Errorf("content %v: %w", dgst, errdefs.ErrNotFound)}return nil
}

6.5. ReaderAt

  ReaderAt接口也非常简单,使用过file.ReaderAt同学会比较熟悉。store.ReaderAt接口返回的contentReaderAt实际上就是对于blob的读取。

// ReaderAt returns an io.ReaderAt for the blob.
// ReaderAt方法用于根据摘要读取镜像层的信息,其实就是读取blob文件(可以理解为镜像层就是一个二进制文件,在containerd中被称为blob)
func (s *store) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {// 拼接出当前摘要所指向的镜像层的路径:/var/lib/containerd/io.containerd.content.v1.content/blobs/sha256/<digest>p, err := s.blobPath(desc.Digest)if err != nil {return nil, fmt.Errorf("calculating blob path for ReaderAt: %w", err)}reader, err := OpenReader(p)if err != nil {return nil, fmt.Errorf("blob %s expected at %s: %w", desc.Digest, p, err)}return reader, nil
}

6.6. Status

  Status接口用于读取/var/lib/containerd/io.containerd.content.v1.content/ingest目录中的文件信息

// Status 实际上就是通过镜像的信息
// 根据镜像名读取ingest信息
func (s *store) Status(ctx context.Context, ref string) (content.Status, error) {return s.status(s.ingestRoot(ref))
}// status works like stat above except uses the path to the ingest.
func (s *store) status(ingestPath string) (content.Status, error) {dp := filepath.Join(ingestPath, "data")fi, err := os.Stat(dp)if err != nil {if os.IsNotExist(err) {err = fmt.Errorf("%s: %w", err.Error(), errdefs.ErrNotFound)}return content.Status{}, err}ref, err := readFileString(filepath.Join(ingestPath, "ref"))if err != nil {if os.IsNotExist(err) {err = fmt.Errorf("%s: %w", err.Error(), errdefs.ErrNotFound)}return content.Status{}, err}startedAt, err := readFileTimestamp(filepath.Join(ingestPath, "startedat"))if err != nil {return content.Status{}, fmt.Errorf("could not read startedat: %w", err)}updatedAt, err := readFileTimestamp(filepath.Join(ingestPath, "updatedat"))if err != nil {return content.Status{}, fmt.Errorf("could not read updatedat: %w", err)}// because we don't write updatedat on every write, the mod time may// actually be more up to date.if fi.ModTime().After(updatedAt) {updatedAt = fi.ModTime()}return content.Status{Ref:       ref,Offset:    fi.Size(),Total:     s.total(ingestPath),UpdatedAt: updatedAt,StartedAt: startedAt,}, nil
}

6.7. ListStatuses

  同Status接口一样,不过此接口是返回的是一个数组,并且调用方可以指定过滤器。

// ListStatuses 遍历containerd所包含的所有镜像的ingest信息
func (s *store) ListStatuses(ctx context.Context, fs ...string) ([]content.Status, error) {fp, err := os.Open(filepath.Join(s.root, "ingest"))if err != nil {return nil, err}defer fp.Close()fis, err := fp.Readdir(-1)if err != nil {return nil, err}filter, err := filters.ParseAll(fs...)if err != nil {return nil, err}var active []content.Statusfor _, fi := range fis {p := filepath.Join(s.root, "ingest", fi.Name())stat, err := s.status(p)if err != nil {if !os.IsNotExist(err) {return nil, err}// TODO(stevvooe): This is a common error if uploads are being// completed while making this listing. Need to consider taking a// lock on the whole store to coordinate this aspect.//// Another option is to cleanup downloads asynchronously and// coordinate this method with the cleanup process.//// For now, we just skip them, as they really don't exist.continue}if filter.Match(adaptStatus(stat)) {active = append(active, stat)}}return active, nil
}

6.8. Abort

  Abort接口实际上就是根据ref删除ingest

// Abort an active transaction keyed by ref. If the ingest is active, it will
// be cancelled. Any resources associated with the ingest will be cleaned.
// 移除镜像所指向的ingest的所有数据
func (s *store) Abort(ctx context.Context, ref string) error {// 获取镜像的ingest路径:/var/lib/containerd/io.containerd.content.v1.content/ingest/<digest>root := s.ingestRoot(ref)if err := os.RemoveAll(root); err != nil {if os.IsNotExist(err) {return fmt.Errorf("ingest ref %q: %w", ref, errdefs.ErrNotFound)}return err}return nil
}

6.9. Writer

  ingest数据写入接口

// Writer begins or resumes the active writer identified by ref. If the writer
// is already in use, an error is returned. Only one writer may be in use per
// ref at a time.
//
// The argument `ref` is used to uniquely identify a long-lived writer transaction.
// 用于生成ingest文件
func (s *store) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {var wOpts content.WriterOptsfor _, opt := range opts {if err := opt(&wOpts); err != nil {return nil, err}}// TODO(AkihiroSuda): we could create a random string or one calculated based on the context// https://github.com/containerd/containerd/issues/2129#issuecomment-380255019if wOpts.Ref == "" {return nil, fmt.Errorf("ref must not be empty: %w", errdefs.ErrInvalidArgument)}var lockErr error// 要想写入这个ingest文件,首先必须锁住这个文件,否则其他人可能会对这个文件进行读写for count := uint64(0); count < 10; count++ {if err := tryLock(wOpts.Ref); err != nil {if !errdefs.IsUnavailable(err) {return nil, err}lockErr = err} else {lockErr = nilbreak}time.Sleep(time.Millisecond * time.Duration(randutil.Intn(1<<count)))}if lockErr != nil {return nil, lockErr}w, err := s.writer(ctx, wOpts.Ref, wOpts.Desc.Size, wOpts.Desc.Digest)if err != nil {unlock(wOpts.Ref)return nil, err}return w, nil // lock is now held by w.
}

7. 总结

  一个系统中,数据是非常核心的。因此数据的持久化更是非常重要的,containerd中的数据主要有镜像、容器、快照、事件、checkpointer、任务、Lease、沙箱等等,这些数据有些是通过文件的方式存储的,譬如镜像,而有些则是通过KV数据库保存的,譬如容器、事件、Lease等等

  ContentPlugincontainerd非常核心的一个组件,ContentPlugin插件会把镜像的每一层以blob的形式保存在/var/lib/containerd/io.containerd.content.v1.content/blobs目录当中;如果镜像在下载过程中断,会把镜像保存在/var/lib/containerd/io.containerd.content.v1.content/ingest目录当中

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL进阶--存储引擎

目录 一、简介二、什么是存储引擎&#xff1f;三、MySQL中常用的存储引擎1.InnoDB2.MyISAM3.Memory4.三种存储引擎对比 四、存储引擎的选择PS: 一、简介 本文的内容讲的是MySQL中的存储引擎的相关知识&#xff0c;初步认识MySQL中的存储引擎及各引擎的特点~ 二、什么是存储引…

Redis 安装以及配置隧道连接

目录 1.CentOS 1. 安装Redis 2. Redis 启动和停⽌ 3. 操作Redis 2.Ubuntu 1. 安装Redis 2. Redis 启动/停⽌ 3. 操作 Redis 3.开启隧道 3.1 Xshell 配置隧道 3.2 windTerm 配置隧道 3.3 FinalShell配置隧道 4.可视化客户端连接 Another Redis Desktop Manager 1.Cen…

Unity Image(RawImage) 实现按轴心放大缩小,序列化存储轴心信息,实现编译器窗口保存轴心

工作时分配给我的要实现的功能&#xff0c;写的时候遇到挺多的坑的&#xff0c;在此记录一下 效果 放大缩小的效果 2.编译器扩展窗口记录 实现点 1.Json序列化存储图片轴心位置, 放大倍率&#xff0c;放大所需要的事件 2.用了编译器扩展工具便于保存轴心信息坑点 1.Imag…

深度学习:探究Tensor和Numpy

目录 引言 1 pytorch中Tensor 1.1 什么是Tensor 1.2 为什么需要Tensor 1.3 如何创建Tensor 1.3.1 从已有其他数据结构转化创建为Tensor 1.3.2 随机初始化一个Tensor 1.3.3 从已保存文件加载一个Tensor 1.4 Tensor的特性 1.4.1 丰富的常用函数操作 1.4.2 灵活的dtype和…

maven开发利器:idea安装maven依赖分析插件 Maven Helper,谁用谁知道!

大家好&#xff0c;我是三叔&#xff0c;很高兴这期又和大家见面了&#xff0c;一个奋斗在互联网的打工人。 这篇博客给大家介绍一款博主实战开发中一直在使用的pom开发分析利器&#xff0c;教大家玩转maven&#xff1a;使用idea安装 Maven Helper 插件&#xff0c;可以分析依…

企业电子招标采购系统源码Spring Boot + Mybatis + Redis + Layui + 前后端分离 构建企业电子招采平台之立项流程图 tbms

&#xfeff; 项目说明 随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大&#xff0c;公司对内部招采管理的提升提出了更高的要求。在企业里建立一个公平、公开、公正的采购环境&#xff0c;最大限度控制采购成本至关重要。符合国家电子招投标法律法规及相关规范&am…

数据结构-链表

&#x1f5e1;CSDN主页&#xff1a;d1ff1cult.&#x1f5e1; &#x1f5e1;代码云仓库&#xff1a;d1ff1cult.&#x1f5e1; &#x1f5e1;文章栏目&#xff1a;数据结构专栏&#x1f5e1; 目录 目录 代码总览&#xff1a; 接口slist.h&#xff1a; slist.c: 1.什么是链表 1.1链…

用C语言构建一个数字识别卷积神经网络

卷积神经网络的具体原理和对应的python例子参见末尾的参考资料2.3. 这里仅叙述卷积神经网络的配置, 其余部分不做赘述&#xff0c;构建和训练神经网络的具体步骤请参见上一篇: 用C语言构建一个手写数字识别神经网路 卷积网络同样采用简单的三层结构&#xff0c;包括输入层con…

k8s pod数据存储Volumes

一、说在前面的话 在 Kubernetes 的 Deployment 中&#xff0c;您可以使用多种类型的 Volumes 来管理 Pod 中的数据。 作用是用来共享目录及配置&#xff0c;不用在每个pod里进行配置。 本文主要概述怎么使用HostPath、PersistentVolumeClaim、ConfigMap。 二、k8s有哪些Vol…

《golang设计模式》第一部分·创建型模式-04-抽象工厂模式(Abstract Factory)

文章目录 1. 概述1.1 角色1.2 类图 2. 代码示例2.1 设计2.2 代码2.3 类图 1. 概述 1.1 角色 AbstractFactory&#xff08;抽象工厂&#xff09;&#xff1a;它声明了一组用于创建产品的方法&#xff0c;每一个方法对应一种产品。ConcreteFactory&#xff08;具体工厂&#xf…

vue el-input 使用 回车键会刷新页面的问题

场景&#xff1a; vue项目中 在输入框输入字符并按下回车键搜索时&#xff0c;不会进行搜索&#xff0c; 而是会刷新页面 原因&#xff1a; 当form表单中只有一个input时&#xff0c;按下回车建会自动触发页面的提交功能&#xff0c; 产生刷新页面的行为 解决&#xff1a; 在…

Spring 事务详解(注解方式)

目 录 序言 1、编程式事务 2、配置声明式事务 2.1 基于TransactionProxyFactoryBean的方式&#xff08;不常用&#xff0c;因为要为每一个类配置TransactionProxyFactoryBean&#xff09; 2.2 基于AspectJ的XML方式&#xff08;常用&#xff0c;可配置在某些类下的所有子…

无涯教程-Perl - unless...else 语句函数

Perl 除非语句后可以跟可选的 else 语句&#xff0c;该语句在布尔表达式为true时执行。 unless...else - 语法 Perl编程语言中的unless... else 语句的语法为- unless(boolean_expression) {# statement(s) will execute if the given condition is false } else {# stateme…

322. 零钱兑换

322. 零钱兑换 原题链接&#xff1a;完成情况&#xff1a;一开始错误原因 解题思路&#xff1a;参考代码&#xff1a;__322 零钱兑换__错误思路还得是dp去做 原题链接&#xff1a; 零钱兑换 完成情况&#xff1a; 一开始错误 原因 /*解题思路&#xff1a;1.先sort一下coins…

react ant icon的简单使用

refer: 快速上手 - Ant Design 1.引入ant npm install antd --save 2.在页面引用&#xff1a; import { StarOutlined } from ant-design/icons; 如果想要引入多个icon&#xff0c;可以这样书写&#xff1a; import { UserOutlined, MailOutlined, PieChartOutlined } fr…

2023年第三届工业自动化、机器人与控制工程国际会议 | IET独立出版 | EI检索

会议简介 Brief Introduction 2023年第三届工业自动化、机器人与控制工程国际会议&#xff08;IARCE 2023&#xff09; 会议时间&#xff1a;2023年10月27 -30日 召开地点&#xff1a;中国成都 大会官网&#xff1a;www.iarce.org 2023年第三届工业自动化、机器人与控制工程国际…

SocialFi 的开发中如何利用 NFTScan API 获取 NFT 数据

SocialFi 作为社交媒体与 Web3 的创新融合&#xff0c;致力于构建更加开放去中心化的社交平台。它赋能用户拥有数据控制权、实现内容价值&#xff0c;并通过代币经济建立起激励与治理机制&#xff0c;这正是 Web3 社交的独特魅力所在。SocialFi 为我们描绘了一个更加用户驱动、…

数据安全能力框架模型-详细解读(三)

数据安全能力框架内涵 “奇安信数据安全能力框架”体现了数据安全治理从组织机构安全治理&#xff0c;到数字化环境具体管控、分析能力分层逐步落实的工程方法。 它以企业数据安全的战略目标和风险容忍度为输入&#xff0c;明确数据安全治理的组织&#xff1b;以合规与治理需…

AtcoderABC227场

A - Last CardA - Last Card 题目大意 一共 K 张卡片分发给 N 个人&#xff0c;这些人的编号为 1, 2, …, N 从第 A 个人开始&#xff0c;按照顺序依次将卡片发给以下人员&#xff1a;A, A1, A2, …, N, 1, 2, …问最后一个卡片将发给哪个人&#xff1f; 具体来说&#xff0c;…

uniapp自定义头部导航栏

有时我们需要一些特殊的头部导航栏页面&#xff0c;取消传统的导航栏&#xff0c;来增加页面的美观度。 下面我就教大家如何配置&#xff1a; 一、效果图 二、实现 首先在uniapp中打开pages.json配置文件&#xff0c;在单个路由配置style里面设置导航栏样式​​​​​​nav…