从源码解析Containerd容器启动流程
从源码解析Containerd容器启动流程
本文从源码的角度分析containerd容器启动流程以及相关功能的实现。
本篇containerd版本为v1.7.9
。
更多文章访问 https://www.cyisme.top
本文从ctr run
命令出发,分析containerd的容器启动流程。
ctr命令
查看文件cmd/ctr/commands/run/run.go
// cmd/ctr/commands/run/run.go
var Command = cli.Command{// 省略其他代码Action: func(context *cli.Context) error {// 省略其他代码// 获取grpc客户端client, ctx, cancel, err := commands.NewClient(context)if err != nil {return err}defer cancel()// 创建容器(基本信息)container, err := NewContainer(ctx, client, context)if err != nil {return err}// 创建任务task, err := tasks.NewTask(ctx, client, container, context.String("checkpoint"), con, context.Bool("null-io"), context.String("log-uri"), ioOpts, opts...)if err != nil {return err}// 省略其他代码 // 用于阻塞进程,等待容器退出var statusC <-chan containerd.ExitStatusif !detach {// 清理容器网络defer func() {if enableCNI {if err := network.Remove(ctx, commands.FullID(ctx, container), ""); err != nil {logrus.WithError(err).Error("network review")}}task.Delete(ctx)}()// 等待容器退出if statusC, err = task.Wait(ctx); err != nil {return err}}// 创建容器网络if enableCNI {// nspath /proc/%d/ns/netnetNsPath, err := getNetNSPath(ctx, task)if err != nil {return err}if _, err := network.Setup(ctx, commands.FullID(ctx, container), netNsPath); err != nil {return err}}// 启动任务(启动容器)if err := task.Start(ctx); err != nil {return err}// 如果是后台(detach)运行,直接返回if detach {// detach运行的任务,containerd不会主动进行数据清理return nil}// 前台运行时, 判断是否开启交互终端if tty {if err := tasks.HandleConsoleResize(ctx, task, con); err != nil {logrus.WithError(err).Error("console resize")}} else {sigc := commands.ForwardAllSignals(ctx, task)defer commands.StopCatch(sigc)}// 等待容器退出status := <-statusCcode, _, err := status.Result()if err != nil {return err}// 非detach模式,会执行清理// 清理任务if _, err := task.Delete(ctx); err != nil {return err}if code != 0 {return cli.NewExitError("", int(code))}return nil},
}
创建容器
在containerd
中,创建容器实际为创建一个container
对象,该对象包含容器的基本信息,如id
、image
、rootfs
等。
// cmd/ctr/commands/run/run.go:162
// client, ctx, cancel, err := commands.NewClient(context)
// if err != nil {
// return err
// }
// defer cancel()
// 创建容器(基本信息)
// container, err := NewContainer(ctx, client, context)
// if err != nil {
// return err
// }
//
// cmd/ctr/commands/run/run_unix.go:88
func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli.Context) (containerd.Container, error) {// 省略其他代码if config {cOpts = append(cOpts, containerd.WithContainerLabels(commands.LabelArgs(context.StringSlice("label"))))opts = append(opts, oci.WithSpecFromFile(context.String("config")))} else {// 省略其他代码if context.Bool("rootfs") {rootfs, err := filepath.Abs(ref)if err != nil {return nil, err}opts = append(opts, oci.WithRootFSPath(rootfs))cOpts = append(cOpts, containerd.WithContainerLabels(commands.LabelArgs(context.StringSlice("label"))))} else {// 省略其他代码// 解压镜像if !unpacked {if err := image.Unpack(ctx, snapshotter); err != nil {return nil, err}}// 省略其他代码}// 省略其他代码// 特权模式判断privileged := context.Bool("privileged")privilegedWithoutHostDevices := context.Bool("privileged-without-host-devices")if privilegedWithoutHostDevices && !privileged {return nil, fmt.Errorf("can't use 'privileged-without-host-devices' without 'privileged' specified")}if privileged {if privilegedWithoutHostDevices {opts = append(opts, oci.WithPrivileged)} else {opts = append(opts, oci.WithPrivileged, oci.WithAllDevicesAllowed, oci.WithHostDevices)}}// 省略其他代码// 参数生成}// 省略其他代码// 创建容器return client.NewContainer(ctx, id, cOpts...)
}
解压镜像
ctr run
命令执行时,强制要求镜像存在。不存在则会退出命令。
镜像存在时,会根据镜像的layer信息,解压镜像到指定目录,生成快照
数据。具体流程可以看《Containerd Snapshots功能解析》这篇文章。这里不再赘述。
// image.go:339
func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...UnpackOpt) error {// 省略其他代码
}
创建容器
创建容器完成后,此时容器为一条记录
,并没有真正调用oci runtime
进行创建,也没有真实运行。 具体流程可以看《Containerd Container管理功能解析》这篇文章。 这里不再赘述。
// client.go:280
func (c *Client) NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error) {// 省略其他代码
}
创建任务
task
是containerd
中真正运行的对象,它包含了容器的所有信息,如rootfs
、namespace
、进程
等。
ctr 本地准备阶段
// task, err := tasks.NewTask(ctx, client, container, context.String("checkpoint"), con, context.Bool("null-io"), context.String("log-uri"), ioOpts, opts...)
// if err != nil {
// return err
// }
// cmd/ctr/commands/task/task_unix.go:71
func NewTask(ctx gocontext.Context, client *containerd.Client, container containerd.Container, checkpoint string, con console.Console, nullIO bool, logURI string, ioOpts []cio.Opt, opts ...containerd.NewTaskOpts) (containerd.Task, error) {// 获取checkpoint信息// checkpoint需要criu支持if checkpoint != "" {im, err := client.GetImage(ctx, checkpoint)if err != nil {return nil, err}opts = append(opts, containerd.WithTaskCheckpoint(im))}// 获取目标容器信息spec, err := container.Spec(ctx)if err != nil {return nil, err}// 省略其他代码// io创建,用于输出容器日志等终端输出var ioCreator cio.Creatorif con != nil {if nullIO {return nil, errors.New("tty and null-io cannot be used together")}ioCreator = cio.NewCreator(append([]cio.Opt{cio.WithStreams(con, con, nil), cio.WithTerminal}, ioOpts...)...)}// 省略其他代码// 创建taskt, err := container.NewTask(ctx, ioCreator, opts...)if err != nil {return nil, err}stdinC.closer = func() {t.CloseIO(ctx, containerd.WithStdinCloser)}return t, nil
}
// container.go:210
func (c *container) NewTask(ctx context.Context, ioCreate cio.Creator, opts ...NewTaskOpts) (_ Task, err error) {// 省略其他代码// 获取容器信息r, err := c.get(ctx)if err != nil {return nil, err}// 处理快照信息if r.SnapshotKey != "" {if r.Snapshotter == "" {return nil, fmt.Errorf("unable to resolve rootfs mounts without snapshotter on container: %w", errdefs.ErrInvalidArgument)}// get the rootfs from the snapshotter and add it to the requests, err := c.client.getSnapshotter(ctx, r.Snapshotter)if err != nil {return nil, err}// 获取挂载位置mounts, err := s.Mounts(ctx, r.SnapshotKey)if err != nil {return nil, err}spec, err := c.Spec(ctx)if err != nil {return nil, err}// 处理挂载信息for _, m := range mounts {if spec.Linux != nil && spec.Linux.MountLabel != "" {context := label.FormatMountLabel("", spec.Linux.MountLabel)if context != "" {m.Options = append(m.Options, context)}}// 快照的挂载信息,最终会添加到容器的根文件系统中// 根文件系统容器不可更改request.Rootfs = append(request.Rootfs, &types.Mount{Type: m.Type,Source: m.Source,Target: m.Target,Options: m.Options,})}}// 省略其他代码t := &task{// grpc客户端client: c.client,// io信息, 用于处理终端数据io: i,// 容器idid: c.id,// 容器对象c: c,}// grpc请求containerd, 创建taskresponse, err := c.client.TaskService().Create(ctx, request)if err != nil {return nil, errdefs.FromGRPC(err)}// shim进程idt.pid = response.Pidreturn t, nil
}
containerd grpc阶段
c.client.TaskService().Create(ctx, request)
会以grpc
方式调用containerd
。
// services/tasks/local.go:166
func (l *local) Create(ctx context.Context, r *api.CreateTaskRequest, _ ...grpc.CallOption) (*api.CreateTaskResponse, error) {// 省略其他代码// 获取容器信息container, err := l.getContainer(ctx, r.ContainerID)if err != nil {return nil, errdefs.ToGRPC(err)}checkpointPath, err := getRestorePath(container.Runtime.Name, r.Options)if err != nil {return nil, err}// jump get checkpointPath from checkpoint imageif checkpointPath == "" && r.Checkpoint != nil {// checkpioint相关,需要criu支持,这里省略}opts := runtime.CreateOpts{Spec: container.Spec,IO: runtime.IO{// 终端信息, 实际为系统中的一个文件// 如:/run/containerd/fifo/1096067688/redis6-stdinStdin: r.Stdin,Stdout: r.Stdout,Stderr: r.Stderr,Terminal: r.Terminal,},// 一些runtime配置Checkpoint: checkpointPath,Runtime: container.Runtime.Name,RuntimeOptions: container.Runtime.Options,TaskOptions: r.Options,SandboxID: container.SandboxID,}// 省略其他代码// 获取runtimertime, err := l.getRuntime(container.Runtime.Name)if err != nil {return nil, err}// 获取任务信息,实际是获取shim相关信息// 这里实际是为了判断任务是否存在_, err = rtime.Get(ctx, r.ContainerID)if err != nil && !errdefs.IsNotFound(err) {return nil, errdefs.ToGRPC(err)}if err == nil {return nil, errdefs.ToGRPC(fmt.Errorf("task %s: %w", r.ContainerID, errdefs.ErrAlreadyExists))}// 创建任务c, err := rtime.Create(ctx, r.ContainerID, opts)if err != nil {return nil, errdefs.ToGRPC(err)}labels := map[string]string{"runtime": container.Runtime.Name}// 将提供的容器添加到监视器中if err := l.monitor.Monitor(c, labels); err != nil {return nil, fmt.Errorf("monitor task: %w", err)}// 在当前返回时,这个pid对应着 runc init进程// 后续会随着容器内进程的启动,pid变为对应着容器内的进程pid, err := c.PID(ctx)if err != nil {return nil, fmt.Errorf("failed to get task pid: %w", err)}return &api.CreateTaskResponse{ContainerID: r.ContainerID,Pid: pid,}, nil
}
启动shim进程
任务创建会启动shim
进程,shim
会与oci runtime
交互,完成容器的创建。
shim进程是一个短暂的进程,它的生命周期与容器一致。它的主要作用是与oci runtime交互,完成容器的创建。
可以理解为,shim进程是oci runtime的代理。
shim有v1和v2两个版本,当前containerd版本使用v2。
// 创建任务
// c, err := rtime.Create(ctx, r.ContainerID, opts)
// if err != nil {
// return nil, errdefs.ToGRPC(err)
// }
// runtime/v2/manager.go:420
func (m *TaskManager) Create(ctx context.Context, taskID string, opts runtime.CreateOpts) (runtime.Task, error) {// 启动shim进程shim, err := m.manager.Start(ctx, taskID, opts)if err != nil {return nil, fmt.Errorf("failed to start shim: %w", err)}// 获取shim客户端shimTask, err := newShimTask(shim)if err != nil {return nil, err}// 通知对应的oci runtime创建容器// 这个函数逻辑比较简单,省略函数解析t, err := shimTask.Create(ctx, opts)if err != nil {// 创建失败会清理shim相关信息// 此处省略return nil, fmt.Errorf("failed to create shim task: %w", err)}return t, nil
}
// m.manager.Start(ctx, taskID, opts)
// runtime/v2/manager.go:184
func (m *ShimManager) Start(ctx context.Context, id string, opts runtime.CreateOpts) (_ ShimInstance, retErr error) {// 省略其他代码if opts.SandboxID != "" {// 省略其他代码// 如果绑定了sandbox,直接获取shim信息,不再创建新的shimshim, err := loadShim(ctx, bundle, func() {})if err != nil {return nil, fmt.Errorf("failed to load sandbox task %q: %w", opts.SandboxID, err)}// 添加shim信息if err := m.shims.Add(ctx, shim); err != nil {return nil, err}return shim, nil}// 启动shim进程shim, err := m.startShim(ctx, bundle, id, opts)if err != nil {return nil, err}defer func() {if retErr != nil {m.cleanupShim(ctx, shim)}}()// 添加shim信息if err := m.shims.Add(ctx, shim); err != nil {return nil, fmt.Errorf("failed to add task: %w", err)}return shim, nil
}
这个阶段完成后,使用runc
命令可以看见一个状态为created
的容器
创建网络
task
准备好之后, 如果容器需要网络,ctr
会调用cni
插件,创建容器网络。
// if enableCNI {
// netNsPath, err := getNetNSPath(ctx, task)
//
// if err != nil {
// return err
// }
//
// if _, err := network.Setup(ctx, commands.FullID(ctx, container), netNsPath); err != nil {
// return err
// }
// }
// 这里不赘述,项目地址:
// https://github.com/containerd/go-cni
func (c *libcni) Setup(ctx context.Context, id string, path string, opts ...NamespaceOpts) (*Result, error) {if err := c.Status(); err != nil {return nil, err}ns, err := newNamespace(id, path, opts...)if err != nil {return nil, err}result, err := c.attachNetworks(ctx, ns)if err != nil {return nil, err}return c.createResult(result)
}
启动任务
启动任务本质是启动容器。启动容器就比较简单了,因为前面的工作都已经完成了,这里只需要调用oci runtime
的start
接口,就可以完成容器的启动。
ctr 本地准备阶段
// if err := task.Start(ctx); err != nil {
// return err
// }
// task.go:215
func (t *task) Start(ctx context.Context) error {// grpc调用containerdr, err := t.client.TaskService().Start(ctx, &tasks.StartRequest{ContainerID: t.id,})if err != nil {if t.io != nil {t.io.Cancel()t.io.Close()}return errdefs.FromGRPC(err)}t.pid = r.Pidreturn nil
}
containerd grpc阶段
// services/tasks/local.go:258
func (l *local) Start(ctx context.Context, r *api.StartRequest, _ ...grpc.CallOption) (*api.StartResponse, error) {// 获取task信息t, err := l.getTask(ctx, r.ContainerID)if err != nil {return nil, err}p := runtime.Process(t)if r.ExecID != "" {if p, err = t.Process(ctx, r.ExecID); err != nil {return nil, errdefs.ToGRPC(err)}}// 启动// start函数最终会调用shim客户端,由shim进程去启动容器// 这里函数逻辑比较简单,不对函数展开分析if err := p.Start(ctx); err != nil {return nil, errdefs.ToGRPC(err)}// 获取容器状态state, err := p.State(ctx)if err != nil {return nil, errdefs.ToGRPC(err)}return &api.StartResponse{Pid: state.Pid,}, nil
}
当这个阶段完成后,可以看见容器的状态变为running
。容器启动完成
总结
task
是containerd
中真正运行的对象,它包含了容器的所有信息,如rootfs、namespace、进程等。创建task
时,会启动shim
进程。shim
进程是一个短暂的进程,它的生命周期与容器一致。它的主要作用是与oci runtime
交互,完成容器的创建。- 容器的网络配置是在task创建之后,由
ctr
调用cni
插件完成的。
相关文章:

从源码解析Containerd容器启动流程
从源码解析Containerd容器启动流程 本文从源码的角度分析containerd容器启动流程以及相关功能的实现。 本篇containerd版本为v1.7.9。 更多文章访问 https://www.cyisme.top 本文从ctr run命令出发,分析containerd的容器启动流程。 ctr命令 查看文件cmd/ctr/comman…...

引迈-JNPF低代码项目技术栈介绍
从 2014 开始研发低代码前端渲染,到 2018 年开始研发后端低代码数据模型,发布了JNPF开发平台。 谨以此文针对 JNPF-JAVA-Cloud微服务 进行相关技术栈展示: 1. 项目前后端分离 前端采用Vue.js,这是一种流行的前端JavaScript框架&a…...

如何处理枚举类型(下)
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO 联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬 上一篇我们通过编写MyB…...

wsj0数据集原始文件.wv1.wv2转换成wav文件
文章目录 准备一、获取WSJO数据集二、安装sph2pipe三、转换代码四、结果展示 最近做语音分离实验需要用到wsj0-2mix数据集,但是从李宏毅语音分离教程里面获取的wsj0-2mix只有一部分。从网上获取到了完整的WSJO数据集后,由于原始的语音文件后缀是wv1或…...

Flask Session 登录认证模块
Flask 框架提供了强大的 Session 模块组件,为 Web 应用实现用户注册与登录系统提供了方便的机制。结合 Flask-WTF 表单组件,我们能够轻松地设计出用户友好且具备美观界面的注册和登录页面,使这一功能能够直接应用到我们的项目中。本文将深入探…...

【运维】hive 高可用详解: Hive MetaStore HA、hive server HA原理详解;hive高可用实现
文章目录 一. hive高可用原理说明1. Hive MetaStore HA2. hive server HA 二. hive高可用实现1. 配置2. beeline链接测试3. zookeeper相关操作 一. hive高可用原理说明 1. Hive MetaStore HA Hive元数据存储在MetaStore中,包括表的定义、分区、表的属性等信息。 hi…...

C#开发的OpenRA游戏之属性SelectionDecorations(13)
C#开发的OpenRA游戏之属性SelectionDecorations(13) 在前面分析SelectionDecorations属性类时,会发现它有下面这个属性: public class SelectionDecorations : SelectionDecorationsBase, IRender { readonly Interactable interactable; 它是定义了一个Interactabl…...

接手了一个外包开发的项目,我感觉我的头快要裂开了~
嗨,大家好,我是飘渺。 最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代…...
git常规使用方法,常规命令
Git是一种分布式版本控制系统,它可以记录软件的历史版本,并提供了多人协作开发、版本回退等功能。以下是Git的基本使用方法: 安装Git:下载安装包并进行安装,安装完成后在命令行中输入 git --version 进行验证。 初始化…...

【JavaScript】3.3 JavaScript工具和库
文章目录 1. 包管理器2. 构建工具3. 测试框架4. JavaScript 库总结 在你的 JavaScript 开发之旅中,会遇到许多工具和库。这些工具和库可以帮助你更有效地编写和管理代码,提高工作效率。在本章节中,我们将探讨一些常见的 JavaScript 工具和库&…...
开发基于 ChatGPT 分析热点事件并生成文章的网站应用【热点问天】把百度等热点用chatGPT来对热点事件分析海量发文章 开发步骤 多种方式获取利润
这样做的优点: 1.不用每个人都问chatGPT同样的问题。 2.已经生成的,反应快速。 3.内容分析的客观,真实,基于数据,无法造假。 4.无其它目的这种基于 ChatGPT 分析热点事件并生成文章的网站,可以通过多种方式…...

龙迅LT8668SXC适用于TPYE-C/DP/HDMI转EDP/VBO同时环出一路HDMI/DP,支持分辨率缩放功能。
1.描述 应用功能:LT8668SXC适用于TYPE-C/DP1.4/HDMI2.1转EDP/VBO同时环出一路HDMI/DP应用方案 分辨率:高达8K30HZ, 工作温度范围:−40C to 85C 产品封装:QFN88 (10*10)最小包装数:1680pcs 2.产品应用 •视频…...
跳板机原理
跳板机原理 跳板机(Jump Server)是一种网络安全设备或计算机,用于管理和保护内部网络中的其他计算机或系统。跳板机通常位于内部网络和外部网络之间,充当连接这两个网络的中间节点或跳板。以下是跳板机的主要功能和用途࿱…...

璞华大数据产品入选中国信通院“铸基计划”
武汉璞华大数据技术有限公司HawkEye设备数字化管理平台产品,凭借优秀的产品技术能力,通过评估后,入选中国信通院“铸基计划”《高质量数字化转型产品及服务全景图(2023)》的工业数字化领域。 “铸基计划”是中国信通院…...
1146. 新的开始,prim算法,超级原点
发展采矿业当然首先得有矿井,小 FF 花了上次探险获得的千分之一的财富请人在岛上挖了 n 口矿井,但他似乎忘记了考虑矿井供电问题。 为了保证电力的供应,小 FF 想到了两种办法: 在矿井 i 上建立一个发电站,费用为 vi&…...
HTTP常见响应码
HTTP(Hypertext Transfer Protocol)是用于在客户端和服务器之间传输资源的协议。HTTP响应码(HTTP status code)用来表示服务器对请求的处理结果。以下是常见的HTTP响应码及其概要: 1. 响应码大类: 主要分…...
物联网边缘计算是什么?如何实现物联网边缘计算?
物联网边缘计算是一种在物联网设备和网络中实施计算和数据处理的技术。它允许在物联网设备或网络边缘进行数据分析和处理,而不需要将所有数据传输到远程数据中心或云端进行处理。物联网边缘计算将计算和数据处理的能力迁移到物联网设备的边缘,使得设备能…...

带着GPT-4V(ision)上路,自动驾驶新探索
On the Road with GPT-4V(ision): Early Explorations of Visual-Language Model on Autonomous Driving GitHub | https://github.com/PJLab-ADG/GPT4V-AD-Exploration arXiv | https://arxiv.org/abs/2311.05332 自动驾驶技术的追求取决于对感知、决策和控制系统的复杂集成。…...
19. Python 数据处理之 Pandas
目录 1. 认识 Pandas2. 安装和导入 Pandas3. Pandas 数据结构4. Pandas 基本功能5. Pandas 数据分析 1. 认识 Pandas Pandas 是 Python 的核心数据分析支持库,提供了快速、灵活、明确的数据结构,旨在简单、直观地处理关系型、标记型数据。 Pandas 的出…...

【计网 可靠数据传输RDT】 中科大笔记 (十 一)
目录 0 引言1 RDT的原理RDT的原理: 2 RDT的机制与作用2.1 重要协议停等协议(Stop-and-Wait):连续ARQ协议: 2.2 机制与作用实现机制:RDT的作用: 🙋♂️ 作者:海码007📜 专栏&#x…...
Python|GIF 解析与构建(5):手搓截屏和帧率控制
目录 Python|GIF 解析与构建(5):手搓截屏和帧率控制 一、引言 二、技术实现:手搓截屏模块 2.1 核心原理 2.2 代码解析:ScreenshotData类 2.2.1 截图函数:capture_screen 三、技术实现&…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地
借阿里云中企出海大会的东风,以**「云启出海,智联未来|打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办,现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...

2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...

短视频矩阵系统文案创作功能开发实践,定制化开发
在短视频行业迅猛发展的当下,企业和个人创作者为了扩大影响力、提升传播效果,纷纷采用短视频矩阵运营策略,同时管理多个平台、多个账号的内容发布。然而,频繁的文案创作需求让运营者疲于应对,如何高效产出高质量文案成…...

基于Java+MySQL实现(GUI)客户管理系统
客户资料管理系统的设计与实现 第一章 需求分析 1.1 需求总体介绍 本项目为了方便维护客户信息为了方便维护客户信息,对客户进行统一管理,可以把所有客户信息录入系统,进行维护和统计功能。可通过文件的方式保存相关录入数据,对…...

基于PHP的连锁酒店管理系统
有需要请加文章底部Q哦 可远程调试 基于PHP的连锁酒店管理系统 一 介绍 连锁酒店管理系统基于原生PHP开发,数据库mysql,前端bootstrap。系统角色分为用户和管理员。 技术栈 phpmysqlbootstrapphpstudyvscode 二 功能 用户 1 注册/登录/注销 2 个人中…...