iis服务器助手广告广告
返回顶部
首页 > 资讯 > 精选 >如何理解kubernetes数据卷管理的源码
  • 183
分享到

如何理解kubernetes数据卷管理的源码

2023-06-19 09:06:51 183人浏览 八月长安
摘要

本篇文章给大家分享的是有关如何理解kubernetes数据卷管理的源码,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。概述volume是k8s中很重要的一个环节,主要用来存储k8

本篇文章给大家分享的是有关如何理解kubernetes数据卷管理的源码,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。

概述

volume是k8s中很重要的一个环节,主要用来存储k8s中pod生产的一些系统或者业务数据。k8s在kubelet中提供了volume管理的逻辑

源码分析

首先是kubelet启动方法

func main() {       s := options.NewKubeletServer()       s.AddFlags(pflag.CommandLine)       flag.InitFlags()       logs.InitLogs()       defer logs.FlushLogs()       verflag.PrintAndExitIfRequested()       if err := app.Run(s, nil); err != nil {              fmt.Fprintf(os.Stderr, "error: %v\n", err)              os.Exit(1)       }}

很容易发现run方法中包含了kubelet所有重要信息

func run(s *options.KubeletServer, kubeDeps *kubelet.KubeletDeps) (err error) {        //配置验证    ...       if kubeDeps == nil {              ...              kubeDeps, err = UnsecuredKubeletDeps(s)              ...       }       //初始化cAdvisor以及containerManager等管理器       ...       if err := RunKubelet(&s.KubeletConfiguration, kubeDeps, s.RunOnce, standaloneMode); err != nil {              return err       }       ...}

里面有两个与volume管理相关的重要方法

  • UnsecuredKubeletDeps:会初始化Docker client、网络管理插件、数据管理插件等系统核心组件,因为不方便对外部开放,所以命名为unsecure。其中我们需要关注的是它对volume plugin的初始化操作

    func UnsecuredKubeletDeps(s *options.KubeletServer) (*kubelet.KubeletDeps, error) {    ...return &kubelet.KubeletDeps{Auth:               nil, CAdvisorInterface:  nil, Cloud:              nil, ContainerManager:   nil,DockerClient:       dockerClient,KubeClient:         nil,ExternalKubeClient: nil,Mounter:            mounter,NetworkPlugins:     ProbeNetworkPlugins(s.networkPluginDir, s.CNIConfDir, s.CNIBinDir),OOMAdjuster:        oom.NewOOMAdjuster(),OSInterface:        kubecontainer.RealOS{},Writer:             writer,VolumePlugins:      ProbeVolumePlugins(s.VolumePluginDir),TLSOptions:         tlsOptions,}, nil}

    在初始化volume plugin的时候会传递VolumePluginDir作为自定义plugin的路径,默认路径为**/usr/libexec/kubernetes/kubelet-plugins/volume/exec/**

    func ProbeVolumePlugins(pluginDir string) []volume.VolumePlugin {allPlugins := []volume.VolumePlugin{}allPlugins = append(allPlugins, aws_ebs.ProbeVolumePlugins()...)allPlugins = append(allPlugins, empty_dir.ProbeVolumePlugins()...)allPlugins = append(allPlugins, GCe_pd.ProbeVolumePlugins()...)allPlugins = append(allPlugins, git_repo.ProbeVolumePlugins()...)allPlugins = append(allPlugins, host_path.ProbeVolumePlugins(volume.VolumeConfig{})...)allPlugins = append(allPlugins, nfs.ProbeVolumePlugins(volume.VolumeConfig{})...)allPlugins = append(allPlugins, secret.ProbeVolumePlugins()...)allPlugins = append(allPlugins, iscsi.ProbeVolumePlugins()...)allPlugins = append(allPlugins, glusterfs.ProbeVolumePlugins()...)allPlugins = append(allPlugins, rbd.ProbeVolumePlugins()...)allPlugins = append(allPlugins, cinder.ProbeVolumePlugins()...)allPlugins = append(allPlugins, quobyte.ProbeVolumePlugins()...)allPlugins = append(allPlugins, cephfs.ProbeVolumePlugins()...)allPlugins = append(allPlugins, downwardapi.ProbeVolumePlugins()...)allPlugins = append(allPlugins, fc.ProbeVolumePlugins()...)allPlugins = append(allPlugins, flocker.ProbeVolumePlugins()...)allPlugins = append(allPlugins, flexvolume.ProbeVolumePlugins(pluginDir)...)allPlugins = append(allPlugins, Azure_file.ProbeVolumePlugins()...)allPlugins = append(allPlugins, configmap.ProbeVolumePlugins()...)allPlugins = append(allPlugins, vsphere_volume.ProbeVolumePlugins()...)allPlugins = append(allPlugins, azure_dd.ProbeVolumePlugins()...)allPlugins = append(allPlugins, photon_pd.ProbeVolumePlugins()...)allPlugins = append(allPlugins, projected.ProbeVolumePlugins()...)allPlugins = append(allPlugins, portworx.ProbeVolumePlugins()...)allPlugins = append(allPlugins, scaleio.ProbeVolumePlugins()...)return allPlugins}

    可以观察到众多插件中,有一个名为flexvolume,只有这个插件带有参数pluginDir,说明只有这个插件支持自定义实现。具体kubelet怎么和这些插件交互,以及这些插件提供哪些接口,还需要继续阅读代码

  • RunKubelet:这才是kubelet服务的启动方法,其中最重要的功能都藏在startKubelet中

    func RunKubelet(kubeCfg *componentconfig.KubeletConfiguration, kubeDeps *kubelet.KubeletDeps, runOnce bool, standaloneMode bool) error {//初始化启动器...if runOnce {if _, err := k.RunOnce(podCfg.Updates()); err != nil {return fmt.Errorf("runonce failed: %v", err)}glog.Infof("Started kubelet %s as runonce", version.Get().String())} else {startKubelet(k, podCfg, kubeCfg, kubeDeps)glog.Infof("Started kubelet %s", version.Get().String())}return nil}

    startKubelet包含两个环节

    func startKubelet(k kubelet.KubeletBootstrap, podCfg *config.PodConfig, kubeCfg *componentconfig.KubeletConfiguration, kubeDeps *kubelet.KubeletDeps) {// 同步pod信息Go wait.Until(func() { k.Run(podCfg.Updates()) }, 0, wait.NeverStop)// 启动kubelet服务if kubeCfg.EnableServer {go wait.Until(func() {k.ListenAndServe(net.ParseIP(kubeCfg.Address), uint(kubeCfg.Port), kubeDeps.TLSOptions, kubeDeps.Auth, kubeCfg.EnableDebuggingHandlers, kubeCfg.EnableContentionProfiling)}, 0, wait.NeverStop)}if kubeCfg.ReadOnlyPort > 0 {go wait.Until(func() {k.ListenAndServeReadOnly(net.ParseIP(kubeCfg.Address), uint(kubeCfg.ReadOnlyPort))}, 0, wait.NeverStop)}}

    跟踪同步pod信息的Run方法,会追查到这段代码

    func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {    ...go kl.volumeManager.Run(kl.sourcesReady, wait.NeverStop)if kl.kubeClient != nil {//同步node信息go wait.Until(kl.syncNodeStatus, kl.nodeStatusUpdateFrequency, wait.NeverStop)}// 同步pod信息kl.pleg.Start()kl.syncLoop(updates, kl)}

    kl.volumeManager是kubelet进行数据卷管理的核心接口

    type VolumeManager interface {Run(sourcesReady config.SourcesReady, stopCh <-chan struct{})WaitForAttachAndMount(pod *v1.Pod) errorGetMountedVolumesForPod(podName types.UniquePodName) container.VolumeMapGetExtraSupplementalGroupsForPod(pod *v1.Pod) []int64GetVolumesInUse() []v1.UniqueVolumeNameReconcilerStatesHasBeenSynced() boolVolumeIsAttached(volumeName v1.UniqueVolumeName) boolMarkVolumesAsReportedInUse(volumesReportedAsInUse []v1.UniqueVolumeName)}

    VolumeManager的Run会执行一个异步循环,当pod被调度到该node,它会检查该pod所申请的所有volume,根据这些volume与pod的关系做attach/detach/mount/unmount操作

    func (vm *volumeManager) Run(sourcesReady config.SourcesReady, stopCh <-chan struct{}) {defer runtime.HandleCrash()go vm.desiredStateOfWorldPopulator.Run(sourcesReady, stopCh)glog.V(2).Infof("The desired_state_of_world populator starts")glog.Infof("Starting Kubelet Volume Manager")go vm.reconciler.Run(stopCh)<-stopChglog.Infof("Shutting down Kubelet Volume Manager")}

    其中重点关注的地方是vm.desiredStateOfWorldPopulator.Runvm.reconciler.Run这两个方法。在介绍这两个方法之前,需要补充一个关键信息,这也是理解这两个方法的关键信息。

    kubelet管理volume的方式基于两个不同的状态:

    理解了这两个状态,就能大概知道vm.desiredStateOfWorldPopulator.Run这个方法是干什么的呢。很明显,它就是根据从apiserver同步到的pod信息,来更新DesiredStateOfWorld。另外一个方法vm.reconciler.Run,是预期状态和实际状态的协调者,它负责将实际状态调整成与预期状态。预期状态的更新实现,以及协调者具体如何协调,需要继续阅读代码才能理解

    追踪vm.desiredStateOfWorldPopulator.Run,我们发现这段逻辑

    func (dswp *desiredStateOfWorldPopulator) findAndAddNewPods() {for _, pod := range dswp.podManager.GetPods() {if dswp.isPodTerminated(pod) {continue}dswp.processPodVolumes(pod)}}

    kubelet会同步新增的pod到desiredStateOfWorldPopulator的podManager中。这段代码就是轮询其中非结束状态的pod,并交给desiredStateOfWorldPopulator处理

    func (dswp *desiredStateOfWorldPopulator) processPodVolumes(pod *v1.Pod) {...for _, podVolume := range pod.Spec.Volumes {volumeSpec, volumeGidValue, err :=dswp.createVolumeSpec(podVolume, pod.Namespace)if err != nil {glog.Errorf("Error processing volume %q for pod %q: %v",podVolume.Name,fORMat.Pod(pod),err)continue}_, err = dswp.desiredStateOfWorld.AddPodToVolume(uniquePodName, pod, volumeSpec, podVolume.Name, volumeGidValue)if err != nil {glog.Errorf("Failed to add volume %q (specName: %q) for pod %q to desiredStateOfWorld. err=%v",podVolume.Name,volumeSpec.Name(),uniquePodName,err)}glog.V(10).Infof("Added volume %q (volSpec=%q) for pod %q to desired state.",podVolume.Name,volumeSpec.Name(),uniquePodName)}dswp.markPodProcessed(uniquePodName)}

    desiredStateOfWorldPopulator并不处理很重的逻辑,只是作为一个代理,将控制某个pod预期状态的逻辑交付给desiredStateOfWorld,并标记为已处理

    func (dsw *desiredStateOfWorld) AddPodToVolume(podName types.UniquePodName,pod *v1.Pod,volumeSpec *volume.Spec,outerVolumeSpecName string,volumeGidValue string) (v1.UniqueVolumeName, error) {...dsw.volumesToMount[volumeName].podsToMount[podName] = podToMount{podName:             podName,pod:                 pod,spec:                volumeSpec,outerVolumeSpecName: outerVolumeSpecName,}return volumeName, nil}

    这段逻辑中,我们忽略了前面一系列预处理操作,直接关注最核心的地方:确定预期状态的方式就是,用一个映射表结构,绑定volume到pod之间的关系,这个关系表就是绑定关系的参考依据

    看完了desiredStateOfWorldPopulator的处理逻辑,接着进入另一个核心接口reconciler。它才是volume manager中最重要的控制器

    追踪reconciler的Run方法,我们定位到最核心的一段代码

    func (rc *reconciler) reconcile() {//umountfor _, mountedVolume := range rc.actualStateOfWorld.GetMountedVolumes() {if !rc.desiredStateOfWorld.PodExistsInVolume(mountedVolume.PodName, mountedVolume.VolumeName) {...err := rc.operationExecutor.UnmountVolume(mountedVolume.MountedVolume, rc.actualStateOfWorld)...}}// attach/mountfor _, volumeToMount := range rc.desiredStateOfWorld.GetVolumesToMount() {volMounted, devicePath, err := rc.actualStateOfWorld.PodExistsInVolume(volumeToMount.PodName, volumeToMount.VolumeName)volumeToMount.DevicePath = devicePathif cache.IsVolumeNotAttachedError(err) {...err := rc.operationExecutor.AttachVolume(volumeToAttach, rc.actualStateOfWorld)...} else if !volMounted || cache.IsRemountRequiredError(err) {...err := rc.operationExecutor.MountVolume(rc.waitForAttachTimeout,volumeToMount.VolumeToMount,rc.actualStateOfWorld)...}}//detach/unmountfor _, attachedVolume := range rc.actualStateOfWorld.GetUnmountedVolumes() {if !rc.desiredStateOfWorld.VolumeExists(attachedVolume.VolumeName) &&!rc.operationExecutor.IsOperationPending(attachedVolume.VolumeName, nestedpendingoperations.EmptyUniquePodName) {if attachedVolume.GloballyMounted {...err := rc.operationExecutor.UnmountDevice(attachedVolume.AttachedVolume, rc.actualStateOfWorld, rc.mounter)...} else {...err := rc.operationExecutor.DetachVolume(attachedVolume.AttachedVolume, false,rc.actualStateOfWorld)...}}}}

    我略去了多余的代码,保留最核心的部分。这段控制逻辑就是一个协调器,具体要做的事情就是,根据实际状态与预期状态的差异,做协调操作

    如果采用自定义的flexvolume插件,上述这些方法会对插件中实现的方法进行系统调用

    flex volume提供的lvm插件。如果需要支持mount和unmount操作,可以在这个脚本中补充

    #!/bin/bash# Copyright 2015 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.# Notes:#  - Please install "jq" package before using this driver.usage() {err "Invalid usage. Usage: "err "\t$0 init"err "\t$0 attach <JSON params> <nodename>"err "\t$0 detach <mount device> <nodename>"err "\t$0 waitforattach <mount device> <json params>"err "\t$0 mountdevice <mount dir> <mount device> <json params>"err "\t$0 unmountdevice <mount dir>"err "\t$0 isattached <json params> <nodename>"exit 1}err() {echo -ne $* 1>&2}log() {echo -ne $* >&1}ismounted() {MOUNT=`findmnt -n ${MNTPATH} 2>/dev/null | cut -d' ' -f1`if [ "${MOUNT}" == "${MNTPATH}" ]; thenecho "1"elseecho "0"fi}getdevice() {VOLUMEID=$(echo ${JSON_PARAMS} | jq -r '.volumeID')VG=$(echo ${JSON_PARAMS}|jq -r '.volumegroup')# LVM substitutes - with --VOLUMEID=`echo $VOLUMEID|sed s/-/--/g`VG=`echo $VG|sed s/-/--/g`DMDEV="/dev/mapper/${VG}-${VOLUMEID}"echo ${DMDEV}}attach() {JSON_PARAMS=$1SIZE=$(echo $1 | jq -r '.size')DMDEV=$(getdevice)if [ ! -b "${DMDEV}" ]; thenerr "{\"status\": \"Failure\", \"message\": \"Volume ${VOLUMEID} does not exist\"}"exit 1filog "{\"status\": \"Success\", \"device\":\"${DMDEV}\"}"exit 0}detach() {log "{\"status\": \"Success\"}"exit 0}waitforattach() {shiftattach $*}domountdevice() {MNTPATH=$1DMDEV=$2FSTYPE=$(echo $3|jq -r '.["kubernetes.io/fsType"]')if [ ! -b "${DMDEV}" ]; thenerr "{\"status\": \"Failure\", \"message\": \"${DMDEV} does not exist\"}"exit 1fiif [ $(ismounted) -eq 1 ] ; thenlog "{\"status\": \"Success\"}"exit 0fiVOLFSTYPE=`blkid -o udev ${DMDEV} 2>/dev/null|grep "ID_FS_TYPE"|cut -d"=" -f2`if [ "${VOLFSTYPE}" == "" ]; thenmkfs -t ${FSTYPE} ${DMDEV} >/dev/null 2>&1if [ $? -ne 0 ]; thenerr "{ \"status\": \"Failure\", \"message\": \"Failed to create fs ${FSTYPE} on device ${DMDEV}\"}"exit 1fifimkdir -p ${MNTPATH} &> /dev/nullmount ${DMDEV} ${MNTPATH} &> /dev/nullif [ $? -ne 0 ]; thenerr "{ \"status\": \"Failure\", \"message\": \"Failed to mount device ${DMDEV} at ${MNTPATH}\"}"exit 1filog "{\"status\": \"Success\"}"exit 0}unmountdevice() {MNTPATH=$1if [ ! -d ${MNTPATH} ]; thenlog "{\"status\": \"Success\"}"exit 0fiif [ $(ismounted) -eq 0 ] ; thenlog "{\"status\": \"Success\"}"exit 0fiumount ${MNTPATH} &> /dev/nullif [ $? -ne 0 ]; thenerr "{ \"status\": \"Failed\", \"message\": \"Failed to unmount volume at ${MNTPATH}\"}"exit 1filog "{\"status\": \"Success\"}"exit 0}isattached() {log "{\"status\": \"Success\", \"attached\":true}"exit 0}op=$1if [ "$op" = "init" ]; thenlog "{\"status\": \"Success\"}"exit 0fiif [ $# -lt 2 ]; thenusagefishiftcase "$op" inattach)attach $*;;detach)detach $*;;waitforattach)waitforattach $*;;mountdevice)domountdevice $*;;unmountdevice)unmountdevice $*;;isattached)                isattached $*                ;;*)log "{ \"status\": \"Not supported\" }"exit 0esacexit 1

    值得注意的是,为什么会有两次mount操作,一次mountdevice,一次mount。分别是做什么的?

    其实k8s提供的volume管理方式是,一个volume可以被多个pod挂载,如果某个device需要作为多个pod的volume,就需要多次挂载。但是device只能被挂载一次。所以,k8s采用的方式是,先用mountdevice将device挂载到一个全局目录,然后这个全局目录就可以被多次挂载到pod的卷目录。如此一来,就能完成多pod挂载同一个volume

    • AttachVolume:调用attach

    • DetachVolume:调用detach

    • MountVolume:调用mountdevice,mount

    • UnmountVolume:调用unmount

    • UnmountDevice:调用umountdevice

    • volume和pod的预期状态不存在绑定关系,则detach volume,并对pod和volume执行unmount操作

    • volume和pod的预期状态存在绑定关系,则attach volume,并对pod和volume执行mount操作

    • DesiredStateOfWorld:预期中,pod对volume的使用情况,简称预期状态。当pod.yaml定制好volume,并提交成功,预期状态就已经确定

    • ActualStateOfWorld:实际中,pod对voluem的使用情况,简称实际状态。实际状态是kubelet的后台线程监控的结果

    • 不断同步apiserver的pod信息,根据新增、删除的pod对volume状态进行同步更新

    • 启动服务,监听controller manager的请求。其中controller manager可以辅助kubelet管理volume,用户也可以选择禁用controller manager的管理

只有理解了volume manager的代码,在使用它提供的volume plugin或者实现自定义flex volume plugin时才能驾轻就熟。以上代码,都是基于k8s v1.6.6版本

以上就是如何理解kubernetes数据卷管理的源码,小编相信有部分知识点可能是我们日常工作会见到或用到的。希望你能通过这篇文章学到更多知识。更多详情敬请关注编程网精选频道。

--结束END--

本文标题: 如何理解kubernetes数据卷管理的源码

本文链接: https://www.lsjlt.com/news/295371.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 如何理解kubernetes数据卷管理的源码
    本篇文章给大家分享的是有关如何理解kubernetes数据卷管理的源码,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。概述volume是k8s中很重要的一个环节,主要用来存储k8...
    99+
    2023-06-19
  • CentOS下Kubernetes存储卷如何管理
    在CentOS下使用Kubernetes管理存储卷通常需要使用持久卷(Persistent Volume)和持久卷声明(Persis...
    99+
    2024-05-09
    CentOS Kubernetes
  • 如何理解Kubernetes在大数据的应用
    本篇内容介绍了“如何理解Kubernetes在大数据的应用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!&...
    99+
    2024-04-02
  • nlp如何处理问卷数据
    NLP(自然语言处理)可以用于处理问卷数据的各个方面,包括预处理、分类、情感分析和主题建模等。下面是一些常用的NLP技术在问卷数据处...
    99+
    2023-09-21
    nlp
  • 如何用Mybatis源码解析事务管理
    如何用Mybatis源码解析事务管理,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。Mybatis事务管理我们可以在mybatis-config.xml中配置事务管理器的实现...
    99+
    2023-06-22
  • Docker中数据卷(volume)管理的两种方式
    上篇文章给大家介绍过 docker基础知识之挂载本地目录的方法 ,今天给大家介绍Docker中数据卷(volume)管理的两种方式,具体内容如下所示: 什么是数据卷 数据卷...
    99+
    2024-04-02
  • 如何理解Mybatis源码
    本篇内容介绍了“如何理解Mybatis源码”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!为什么纠结因为面试...
    99+
    2024-04-02
  • 如何理解ArrayList源码
    本篇内容主要讲解“如何理解ArrayList源码”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何理解ArrayList源码”吧!ArrayList类图如下:A...
    99+
    2024-04-02
  • SpringBoot依赖管理的源码解析
    目录一. 依赖管理Ⅰ. 部分dependency导入时为啥不需要指定版本?1.1 父依赖启动器的工作1.2 问题答案Ⅱ. 项目运行依赖的JAR包是从何而来的2.1 分析源码2.2 问...
    99+
    2023-05-18
    SpringBoot 依赖管理 SpringBoot 源码
  • Centos7如何使用SSM管理LVM卷
    本篇文章为大家展示了Centos7如何使用SSM管理LVM卷,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。系统存储管理器(SSM)提供了一个命令行接口来管理各种技术中的存储。通过使用DM、LVM和M...
    99+
    2023-06-05
  • 如何进行C++代码的资源管理?
    如何进行C++代码的资源管理以C++为例,资源管理是程序开发中非常重要的一环。良好的资源管理可以提高程序的性能和稳定性,减少内存泄漏和资源浪费。本文将介绍一些常用的C++资源管理技术和最佳实践。1.使用智能指针:C++11引入了智能指针的概...
    99+
    2023-11-02
    内存管理 资源分配 C++资源管理
  • 如何理解kubernetes中的Ingress
    如何理解kubernetes中的Ingress,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。一:简介Ingress资源对象,用于将不同URL的访问请求转发到后端不同的Ser...
    99+
    2023-06-04
  • Docker中数据卷管理的方式有哪几种
    本篇内容介绍了“Docker中数据卷管理的方式有哪几种”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!什么是数据卷数据卷( volume ):...
    99+
    2023-06-20
  • Linux管理硬件资源该如何理解
    这期内容当中小编将会给大家带来有关Linux管理硬件资源该如何理解,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。计算机的硬件主要包括内存(RAM)、中央处理器(CPU)、输入/输出(I/O)设备和硬盘(H...
    99+
    2023-06-28
  • Git中如何管理二维码资源?
    Git中如何管理二维码资源? 二维码是一种常用的信息传递方式,特别是在移动互联网时代,二维码的应用越来越广泛。在开发中,我们经常需要将二维码资源存储到代码库中,并对其进行管理。本文将介绍如何使用Git进行二维码资源的管理。 一、为什么需要管...
    99+
    2023-09-26
    二维码 path git
  • 如何理解LevelDB源码中的SSTable
    如何理解LevelDB源码中的SSTable,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。MemTable是内存表,而当内存表...
    99+
    2024-04-02
  • 如何理解Redis 代码库源码
    本篇文章给大家分享的是有关如何理解Redis 代码库源码,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。Redis是一个用ANSI C &nbs...
    99+
    2024-04-02
  • Docker存储卷如何使用与管理
    Docker存储卷是一种用于在容器内部进行数据持久化的技术。它允许将数据存储在主机上的一个目录,并将这个目录挂载到容器内部。这样,即...
    99+
    2024-05-07
    Docker
  • 如何理解Java代码的内存管理
    这期内容当中小编将会给大家带来有关如何理解Java代码的内存管理,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。小编通过几个方面来介绍Java代码的内存管理。有的代码,GC根本就回收不了,直接系统挂掉。GC...
    99+
    2023-06-17
  • Flutter状态管理scopedmodel源码解读
    目录一、什么是 scoped_model二、用法三、实现原理四、结束一、什么是 scoped_model 本文主要从 scoped_model 的简单使用说起,然后再深入源码进行剖析...
    99+
    2022-11-16
    Flutter状态管理 scoped model Flutter scoped model
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作