CSI-external-provisioner
main()
这段Go代码是一个CSI(容器存储接口)Provisioner(供应器)的实现,用于在Kubernetes集群中动态提供持久卷。代码涉及多个组件和步骤,下面是对关键部分的解释:
- 初始化和配置
- 命令行标志和环境变量:代码使用
flag
包处理命令行参数,如feature-gates
、kubeconfig
等。同时,从环境变量中获取配置,如NODE_NAME
。 - 日志和度量:使用
klog
进行日志记录,并配置Prometheus度量收集器。
- Kubernetes客户端配置
- 构建KubeConfig:根据是否提供了
master
或kubeconfig
参数,决定是使用集群内配置(rest.InClusterConfig
)还是外部配置(clientcmd.BuildConfigFromFlags
)。 - 客户端创建:使用配置创建Kubernetes客户端(
kubernetes.NewForConfig
)和CSI快照客户端(snapclientset.NewForConfig
)。
- CSI驱动连接和验证
- 连接到CSI驱动:通过gRPC连接到CSI驱动,并进行基本的探测(
ctrl.Probe
)以确保驱动可用。 - 获取驱动名称和能力:从CSI驱动获取驱动名称(
ctrl.GetDriverName
)和能力(ctrl.GetDriverCapabilities
)。
- 拓扑和节点信息
- 拓扑支持:如果CSI驱动支持拓扑,则创建相应的informer来监视节点和CSINode对象。
- 节点部署:如果启用了节点部署(
--enable-node-deployment
),则获取节点信息并配置节点部署对象。
- Provisioner和Controller创建
- Provisioner创建:使用获取的配置和客户端创建CSI Provisioner对象,该对象实现了Provisioner接口。
- 容量控制器:如果启用了容量功能(
--enable-capacity
),则创建容量控制器来发布存储容量信息。
- HTTP服务器和度量
- HTTP服务器:如果指定了度量地址(
--metrics-address
)或HTTP端点(--http-endpoint
),则启动HTTP服务器来暴露度量和可能的调试端点(如pprof)。
- Informers和队列
- Informer和队列:创建各种资源的Informer来监视Kubernetes对象的变化,并使用工作队列处理事件。
- 运行
- 启动Informer和控制器:启动Informer工厂和控制器,开始监视和处理事件。
总结
这段代码是一个复杂的CSI Provisioner实现,它集成了Kubernetes客户端、CSI驱动、度量收集、拓扑感知、容量管理等多个组件。通过精心设计的架构和模块化的代码,它能够在Kubernetes集群中高效地提供和管理持久卷。
func main() {var config *rest.Configvar err errorflag.Var(utilflag.NewMapStringBool(&featureGates), "feature-gates", "A set of key=value pairs that describe feature gates for alpha/experimental features. "+"Options are:\n"+strings.Join(utilfeature.DefaultFeatureGate.KnownFeatures(), "\n"))klog.InitFlags(nil)flag.CommandLine.AddGoFlagSet(goflag.CommandLine)flag.Set("logtostderr", "true")flag.Parse()ctx := context.Background()if err := utilfeature.DefaultMutableFeatureGate.SetFromMap(featureGates); err != nil {klog.Fatal(err)}node := os.Getenv("NODE_NAME")if *enableNodeDeployment && node == "" {klog.Fatal("The NODE_NAME environment variable must be set when using --enable-node-deployment.")}if *showVersion {fmt.Println(os.Args[0], version)os.Exit(0)}klog.Infof("Version: %s", version)if *metricsAddress != "" && *httpEndpoint != "" {klog.Error("only one of `--metrics-address` and `--http-endpoint` can be set.")os.Exit(1)}addr := *metricsAddressif addr == "" {addr = *httpEndpoint}// get the KUBECONFIG from env if specified (useful for local/debug cluster)kubeconfigEnv := os.Getenv("KUBECONFIG")if kubeconfigEnv != "" {klog.Infof("Found KUBECONFIG environment variable set, using that..")kubeconfig = &kubeconfigEnv}if *master != "" || *kubeconfig != "" {klog.Infof("Either master or kubeconfig specified. building kube config from that..")config, err = clientcmd.BuildConfigFromFlags(*master, *kubeconfig)} else {klog.Infof("Building kube configs for running in cluster...")config, err = rest.InClusterConfig()}if err != nil {klog.Fatalf("Failed to create config: %v", err)}config.QPS = *kubeAPIQPSconfig.Burst = *kubeAPIBurstclientset, err := kubernetes.NewForConfig(config)if err != nil {klog.Fatalf("Failed to create client: %v", err)}// snapclientset.NewForConfig creates a new Clientset for VolumesnapshotV1ClientsnapClient, err := snapclientset.NewForConfig(config)if err != nil {klog.Fatalf("Failed to create snapshot client: %v", err)}var gatewayClient gatewayclientset.Interfaceif utilfeature.DefaultFeatureGate.Enabled(features.CrossNamespaceVolumeDataSource) {// gatewayclientset.NewForConfig creates a new Clientset for GatewayClientgatewayClient, err = gatewayclientset.NewForConfig(config)if err != nil {klog.Fatalf("Failed to create gateway client: %v", err)}}metricsManager := metrics.NewCSIMetricsManagerWithOptions("", /* driverName */// Will be provided via default gatherer.metrics.WithProcessStartTime(false),metrics.WithSubsystem(metrics.SubsystemSidecar),)grpcClient, err := ctrl.Connect(ctx, *csiEndpoint, metricsManager)if err != nil {klog.Error(err.Error())os.Exit(1)}err = ctrl.Probe(ctx, grpcClient, *operationTimeout)if err != nil {klog.Error(err.Error())os.Exit(1)}// Autodetect provisioner nameprovisionerName, err := ctrl.GetDriverName(grpcClient, *operationTimeout)if err != nil {klog.Fatalf("Error getting CSI driver name: %s", err)}klog.V(2).Infof("Detected CSI driver %s", provisionerName)metricsManager.SetDriverName(provisionerName)translator := csitrans.New()supportsMigrationFromInTreePluginName := ""if translator.IsMigratedCSIDriverByName(provisionerName) {supportsMigrationFromInTreePluginName, err = translator.GetInTreeNameFromCSIName(provisionerName)if err != nil {klog.Fatalf("Failed to get InTree plugin name for migrated CSI plugin %s: %v", provisionerName, err)}klog.V(2).Infof("Supports migration from in-tree plugin: %s", supportsMigrationFromInTreePluginName)// Create a new connection with the metrics manager with migrated labelmetricsManager = metrics.NewCSIMetricsManagerWithOptions(provisionerName,// Will be provided via default gatherer.metrics.WithProcessStartTime(false),metrics.WithMigration())migratedGrpcClient, err := ctrl.Connect(ctx, *csiEndpoint, metricsManager)if err != nil {klog.Error(err.Error())os.Exit(1)}grpcClient.Close()grpcClient = migratedGrpcClienterr = ctrl.Probe(ctx, grpcClient, *operationTimeout)if err != nil {klog.Error(err.Error())os.Exit(1)}}// Prepare http endpoint for metrics + leader election healthzmux := http.NewServeMux()gatherers := prometheus.Gatherers{// For workqueue and leader election metrics, set up via the anonymous imports of:// https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/component-base/metrics/prometheus/workqueue/metrics.go// https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/component-base/metrics/prometheus/clientgo/leaderelection/metrics.go//// Also to happens to include Go runtime and process metrics:// https://github.com/kubernetes/kubernetes/blob/9780d88cb6a4b5b067256ecb4abf56892093ee87/staging/src/k8s.io/component-base/metrics/legacyregistry/registry.goL46-L49legacyregistry.DefaultGatherer,// For CSI operations.metricsManager.GetRegistry(),}pluginCapabilities, controllerCapabilities, err := ctrl.GetDriverCapabilities(grpcClient, *operationTimeout)if err != nil {klog.Fatalf("Error getting CSI driver capabilities: %s", err)}// Generate a unique ID for this provisionertimeStamp := time.Now().UnixNano() / int64(time.Millisecond)identity := strconv.FormatInt(timeStamp, 10) + "-" + strconv.Itoa(rand.Intn(10000)) + "-" + provisionerNameif *enableNodeDeployment {identity = identity + "-" + node}factory := informers.NewSharedInformerFactory(clientset, ctrl.ResyncPeriodOfCsiNodeInformer)var factoryForNamespace informers.SharedInformerFactory // usually nil, only used for CSIStorageCapacity// -------------------------------// Listers// Create informer to prevent hit the API server for all resource requestscLister := factory.Storage().V1().StorageClasses().Lister()claimLister := factory.Core().V1().PersistentVolumeClaims().Lister()var vaLister storagelistersv1.VolumeAttachmentListerif controllerCapabilities[csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME] {klog.Info("CSI driver supports PUBLISH_UNPUBLISH_VOLUME, watching VolumeAttachments")vaLister = factory.Storage().V1().VolumeAttachments().Lister()} else {klog.Info("CSI driver does not support PUBLISH_UNPUBLISH_VOLUME, not watching VolumeAttachments")}var nodeDeployment *ctrl.NodeDeploymentif *enableNodeDeployment {nodeDeployment = &ctrl.NodeDeployment{NodeName: node,ClaimInformer: factory.Core().V1().PersistentVolumeClaims(),ImmediateBinding: *nodeDeploymentImmediateBinding,BaseDelay: *nodeDeploymentBaseDelay,MaxDelay: *nodeDeploymentMaxDelay,}nodeInfo, err := ctrl.GetNodeInfo(grpcClient, *operationTimeout)if err != nil {klog.Fatalf("Failed to get node info from CSI driver: %v", err)}nodeDeployment.NodeInfo = *nodeInfo}var nodeLister listersv1.NodeListervar csiNodeLister storagelistersv1.CSINodeListerif ctrl.SupportsTopology(pluginCapabilities) {if nodeDeployment != nil {// Avoid watching in favor of fake, static objects. This is particularly relevant for// Node objects, which can generate significant traffic.csiNode := &storagev1.CSINode{ObjectMeta: metav1.ObjectMeta{Name: nodeDeployment.NodeName,},Spec: storagev1.CSINodeSpec{Drivers: []storagev1.CSINodeDriver{{Name: provisionerName,NodeID: nodeDeployment.NodeInfo.NodeId,},},},}node := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeDeployment.NodeName,},}if nodeDeployment.NodeInfo.AccessibleTopology != nil {for key := range nodeDeployment.NodeInfo.AccessibleTopology.Segments {csiNode.Spec.Drivers[0].TopologyKeys = append(csiNode.Spec.Drivers[0].TopologyKeys, key)}node.Labels = nodeDeployment.NodeInfo.AccessibleTopology.Segments}klog.Infof("using local topology with Node = %+v and CSINode = %+v", node, csiNode)// We make those fake objects available to the topology code via informers which// never change.stoppedFactory := informers.NewSharedInformerFactory(clientset, 1000*time.Hour)csiNodes := stoppedFactory.Storage().V1().CSINodes()nodes := stoppedFactory.Core().V1().Nodes()csiNodes.Informer().GetStore().Add(csiNode)nodes.Informer().GetStore().Add(node)csiNodeLister = csiNodes.Lister()nodeLister = nodes.Lister()} else {csiNodeLister = factory.Storage().V1().CSINodes().Lister()nodeLister = factory.Core().V1().Nodes().Lister()}}var referenceGrantLister referenceGrantv1beta1.ReferenceGrantListervar gatewayFactory gatewayInformers.SharedInformerFactoryif utilfeature.DefaultFeatureGate.Enabled(features.CrossNamespaceVolumeDataSource) {gatewayFactory = gatewayInformers.NewSharedInformerFactory(gatewayClient, ctrl.ResyncPeriodOfReferenceGrantInformer)referenceGrants := gatewayFactory.Gateway().V1beta1().ReferenceGrants()referenceGrantLister = referenceGrants.Lister()}// -------------------------------// PersistentVolumeClaims informerrateLimiter := workqueue.NewItemExponentialFailureRateLimiter(*retryIntervalStart, *retryIntervalMax)claimQueue := workqueue.NewNamedRateLimitingQueue(rateLimiter, "claims")claimInformer := factory.Core().V1().PersistentVolumeClaims().Informer()// Setup optionsprovisionerOptions := []func(*controller.ProvisionController) error{controller.LeaderElection(false), // Always disable leader election in provisioner lib. Leader election should be done here in the CSI provisioner level instead.controller.FailedProvisionThreshold(0),controller.FailedDeleteThreshold(0),controller.RateLimiter(rateLimiter),controller.Threadiness(int(*workerThreads)),controller.CreateProvisionedPVLimiter(workqueue.DefaultControllerRateLimiter()),controller.ClaimsInformer(claimInformer),controller.NodesLister(nodeLister),}if utilfeature.DefaultFeatureGate.Enabled(features.HonorPVReclaimPolicy) {provisionerOptions = append(provisionerOptions, controller.AddFinalizer(true))}if supportsMigrationFromInTreePluginName != "" {provisionerOptions = append(provisionerOptions, controller.AdditionalProvisionerNames([]string{supportsMigrationFromInTreePluginName}))}// Create the provisioner: it implements the Provisioner interface expected by// the controllercsiProvisioner := ctrl.NewCSIProvisioner(clientset,*operationTimeout,identity,*volumeNamePrefix,*volumeNameUUIDLength,grpcClient,snapClient,provisionerName,pluginCapabilities,controllerCapabilities,supportsMigrationFromInTreePluginName,*strictTopology,*immediateTopology,translator,scLister,csiNodeLister,nodeLister,claimLister,vaLister,referenceGrantLister,*extraCreateMetadata,*defaultFSType,nodeDeployment,*controllerPublishReadOnly,*preventVolumeModeConversion,)var capacityController *capacity.Controllerif *enableCapacity {// Publishing storage capacity information uses its own client// with separate rate limiting.config.QPS = *kubeAPICapacityQPSconfig.Burst = *kubeAPICapacityBurstclientset, err := kubernetes.NewForConfig(config)if err != nil {klog.Fatalf("Failed to create client: %v", err)}namespace := os.Getenv("NAMESPACE")if namespace == "" {klog.Fatal("need NAMESPACE env variable for CSIStorageCapacity objects")}var controller *metav1.OwnerReferenceif *capacityOwnerrefLevel >= 0 {podName := os.Getenv("POD_NAME")if podName == "" {klog.Fatal("need POD_NAME env variable to determine CSIStorageCapacity owner")}var err errorcontroller, err = owner.Lookup(config, namespace, podName,schema.GroupVersionKind{Group: "",Version: "v1",Kind: "Pod",}, *capacityOwnerrefLevel)if err != nil {klog.Fatalf("look up owner(s) of pod: %v", err)}klog.Infof("using %s/%s %s as owner of CSIStorageCapacity objects", controller.APIVersion, controller.Kind, controller.Name)}var topologyInformer topology.Informerif nodeDeployment == nil {topologyInformer = topology.NewNodeTopology(provisionerName,clientset,factory.Core().V1().Nodes(),factory.Storage().V1().CSINodes(),workqueue.NewNamedRateLimitingQueue(rateLimiter, "csitopology"),)} else {var segment topology.Segmentif nodeDeployment.NodeInfo.AccessibleTopology != nil {for key, value := range nodeDeployment.NodeInfo.AccessibleTopology.Segments {segment = append(segment, topology.SegmentEntry{Key: key, Value: value})}}klog.Infof("producing CSIStorageCapacity objects with fixed topology segment %s", segment)topologyInformer = topology.NewFixedNodeTopology(&segment)}go topologyInformer.RunWorker(ctx)managedByID := "external-provisioner"if *enableNodeDeployment {managedByID = getNameWithMaxLength(managedByID, node, validation.DNS1035LabelMaxLength)}// We only need objects from our own namespace. The normal factory would give// us an informer for the entire cluster. We can further restrict the// watch to just those objects with the right labels.factoryForNamespace = informers.NewSharedInformerFactoryWithOptions(clientset,ctrl.ResyncPeriodOfCsiNodeInformer,informers.WithNamespace(namespace),informers.WithTweakListOptions(func(lo *metav1.ListOptions) {lo.LabelSelector = labels.Set{capacity.DriverNameLabel: provisionerName,capacity.ManagedByLabel: managedByID,}.AsSelector().String()}),)// We use the V1 CSIStorageCapacity API if available.clientFactory := capacity.NewV1ClientFactory(clientset)cInformer := factoryForNamespace.Storage().V1().CSIStorageCapacities()// This invalid object is used in a v1 Create call to determine// based on the resulting error whether the v1 API is supported.invalidCapacity := &storagev1.CSIStorageCapacity{ObjectMeta: metav1.ObjectMeta{Name: "%123-invalid-name",},}createdCapacity, err := clientset.StorageV1().CSIStorageCapacities(namespace).Create(ctx, invalidCapacity, metav1.CreateOptions{})switch {case err == nil:klog.Fatalf("creating an invalid v1.CSIStorageCapacity didn't fail as expected, got: %s", createdCapacity)case apierrors.IsNotFound(err):// We need to bridge between the v1beta1 API on the// server and the v1 API expected by the capacity code.klog.Info("using the CSIStorageCapacity v1beta1 API")clientFactory = capacity.NewV1beta1ClientFactory(clientset)cInformer = capacity.NewV1beta1InformerBridge(factoryForNamespace.Storage().V1beta1().CSIStorageCapacities())case apierrors.IsInvalid(err):klog.Info("using the CSIStorageCapacity v1 API")default:klog.Fatalf("unexpected error when checking for the V1 CSIStorageCapacity API: %v", err)}capacityController = capacity.NewCentralCapacityController(csi.NewControllerClient(grpcClient),provisionerName,clientFactory,// Metrics for the queue is available in the default registry.workqueue.NewNamedRateLimitingQueue(rateLimiter, "csistoragecapacity"),controller,managedByID,namespace,topologyInformer,factory.Storage().V1().StorageClasses(),cInformer,*capacityPollInterval,*capacityImmediateBinding,*operationTimeout,)legacyregistry.CustomMustRegister(capacityController)// Wrap Provision and Delete to detect when it is time to refresh capacity.csiProvisioner = capacity.NewProvisionWrapper(csiProvisioner, capacityController)}if addr != "" {// Start HTTP server, regardless whether we are the leader or not.// Register provisioner metrics manually to be able to add multiplexer in front of itm := libmetrics.New("controller")reg := prometheus.NewRegistry()reg.MustRegister([]prometheus.Collector{m.PersistentVolumeClaimProvisionTotal,m.PersistentVolumeClaimProvisionFailedTotal,m.PersistentVolumeClaimProvisionDurationSeconds,m.PersistentVolumeDeleteTotal,m.PersistentVolumeDeleteFailedTotal,m.PersistentVolumeDeleteDurationSeconds,}...)provisionerOptions = append(provisionerOptions, controller.MetricsInstance(m))gatherers = append(gatherers, reg)// This is similar to k8s.io/component-base/metrics HandlerWithReset// except that we gather from multiple sources. This is necessary// because both CSI metrics manager and component-base manage// their own registry. Probably could be avoided by making// CSI metrics manager a bit more flexible.mux.Handle(*metricsPath,promhttp.InstrumentMetricHandler(reg,promhttp.HandlerFor(gatherers, promhttp.HandlerOpts{})))if *enableProfile {klog.InfoS("Starting profiling", "endpoint", httpEndpoint)mux.HandleFunc("/debug/pprof/", pprof.Index)mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)mux.HandleFunc("/debug/pprof/profile", pprof.Profile)mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)mux.HandleFunc("/debug/pprof/trace", pprof.Trace)}go func() {klog.Infof("ServeMux listening at %q", addr)err := http.ListenAndServe(addr, mux)if err != nil {klog.Fatalf("Failed to start HTTP server at specified address (%q) and metrics path (%q): %s", addr, *metricsPath, err)}}()}logger := klog.FromContext(ctx)provisionController = controller.NewProvisionController(logger,clientset,provisionerName,csiProvisioner,provisionerOptions...,)csiClaimController := ctrl.NewCloningProtectionController(clientset,claimLister,claimInformer,claimQueue,controllerCapabilities,)run := func(ctx context.Context) {factory.Start(ctx.Done())if factoryForNamespace != nil {// Starting is enough, the capacity controller will// wait for sync.factoryForNamespace.Start(ctx.Done())}cacheSyncResult := factory.WaitForCacheSync(ctx.Done())for _, v := range cacheSyncResult {if !v {klog.Fatalf("Failed to sync Informers!")}}if utilfeature.DefaultFeatureGate.Enabled(features.CrossNamespaceVolumeDataSource) {if gatewayFactory != nil {gatewayFactory.Start(ctx.Done())}gatewayCacheSyncResult := gatewayFactory.WaitForCacheSync(ctx.Done())for _, v := range gatewayCacheSyncResult {if !v {klog.Fatalf("Failed to sync Informers for gateway!")}}}if capacityController != nil {go capacityController.Run(ctx, int(*capacityThreads))}if csiClaimController != nil {go csiClaimController.Run(ctx, int(*finalizerThreads))}provisionController.Run(ctx)}if !*enableLeaderElection {run(ctx)} else {// this lock name pattern is also copied from sigs.k8s.io/sig-storage-lib-external-provisioner/controller// to preserve backwards compatibilitylockName := strings.Replace(provisionerName, "/", "-", -1)// create a new clientset for leader electionleClientset, err := kubernetes.NewForConfig(config)if err != nil {klog.Fatalf("Failed to create leaderelection client: %v", err)}le := leaderelection.NewLeaderElection(leClientset, lockName, run)if *httpEndpoint != "" {le.PrepareHealthCheck(mux, leaderelection.DefaultHealthCheckTimeout)}if *leaderElectionNamespace != "" {le.WithNamespace(*leaderElectionNamespace)}le.WithLeaseDuration(*leaderElectionLeaseDuration)le.WithRenewDeadline(*leaderElectionRenewDeadline)le.WithRetryPeriod(*leaderElectionRetryPeriod)le.WithIdentity(identity)if err := le.Run(); err != nil {klog.Fatalf("failed to initialize leader election: %v", err)}}
}
NewProvisionController()
- 获取主机名和生成唯一ID:
- 使用
os.Hostname()
获取当前主机名,如果获取失败,则记录错误日志并退出程序。 - 将主机名与一个UUID结合,生成一个唯一的ID,以避免在同一主机上运行的多个进程发生冲突。
- 使用
- 初始化事件记录器:
- 使用
record.NewBroadcaster()
创建一个事件广播器,并配置其进行结构化日志记录和事件记录。 - 创建一个
eventRecorder
,用于记录事件。
- 使用
- 创建并初始化
ProvisionController
实例:- 初始化
ProvisionController
结构体,包括客户端、供应器名称、供应器实现、ID、组件名、事件记录器等字段。 - 设置一系列默认值,如重同步周期、错误时的指数退避策略、线程数、失败阈值等。
- 初始化指标相关配置。
- 初始化
- 处理选项函数:
- 遍历传入的选项函数列表,对每个函数进行调用,以配置
ProvisionController
实例。如果某个选项函数执行失败,则记录错误日志并退出程序。
- 遍历传入的选项函数列表,对每个函数进行调用,以配置
- 初始化速率限制器和工作队列:
- 根据配置创建速率限制器,并用于初始化
claimQueue
和volumeQueue
两个工作队列。
- 根据配置创建速率限制器,并用于初始化
- 初始化Informer和事件处理器:
- 使用
informers.NewSharedInformerFactory
创建共享Informer工厂。 - 为PersistentVolumeClaims(PVCs)、PersistentVolumes(PVs)和StorageClasses分别设置事件处理器和Indexer。
- Informer用于监听Kubernetes资源的变化,并根据变化触发相应的事件处理函数。
- 使用
- 初始化VolumeStore:
- 根据配置选择使用
NewVolumeStoreQueue
或NewBackoffStore
来初始化volumeStore
,用于处理PV的创建和保存逻辑。
- 根据配置选择使用
// NewProvisionController creates a new provision controller using
// the given configuration parameters and with private (non-shared) informers.
func NewProvisionController(logger klog.Logger,client kubernetes.Interface,provisionerName string,provisioner Provisioner,options ...func(*ProvisionController) error,
) *ProvisionController {id, err := os.Hostname()if err != nil {logger.Error(err, "Error getting hostname")klog.FlushAndExit(klog.ExitFlushTimeout, 1)}// add a uniquifier so that two processes on the same host don't accidentally both become activeid = id + "_" + string(uuid.NewUUID())component := provisionerName + "_" + id// TODO: Once the following PR is merged, change to use StartLogging and StartRecordingToSinkWithContext// https://github.com/kubernetes/kubernetes/pull/120729v1.AddToScheme(scheme.Scheme)broadcaster := record.NewBroadcaster()broadcaster.StartStructuredLogging(0)broadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface: client.CoreV1().Events(v1.NamespaceAll)})eventRecorder := broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: component})controller := &ProvisionController{client: client,provisionerName: provisionerName,provisioner: provisioner,id: id,component: component,eventRecorder: eventRecorder,resyncPeriod: DefaultResyncPeriod,exponentialBackOffOnError: DefaultExponentialBackOffOnError,threadiness: DefaultThreadiness,failedProvisionThreshold: DefaultFailedProvisionThreshold,failedDeleteThreshold: DefaultFailedDeleteThreshold,leaderElection: DefaultLeaderElection,leaderElectionNamespace: getInClusterNamespace(),leaseDuration: DefaultLeaseDuration,renewDeadline: DefaultRenewDeadline,retryPeriod: DefaultRetryPeriod,metrics: metrics.New(controllerSubsystem),metricsPort: DefaultMetricsPort,metricsAddress: DefaultMetricsAddress,metricsPath: DefaultMetricsPath,addFinalizer: DefaultAddFinalizer,hasRun: false,hasRunLock: &sync.Mutex{},}for _, option := range options {err := option(controller)if err != nil {logger.Error(err, "Error processing controller options")klog.FlushAndExit(klog.ExitFlushTimeout, 1)}}var rateLimiter workqueue.RateLimiterif controller.rateLimiter != nil {// rateLimiter set via parameter takes precedencerateLimiter = controller.rateLimiter} else if controller.exponentialBackOffOnError {rateLimiter = workqueue.NewMaxOfRateLimiter(workqueue.NewItemExponentialFailureRateLimiter(15*time.Second, 1000*time.Second),&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},)} else {rateLimiter = workqueue.NewMaxOfRateLimiter(workqueue.NewItemExponentialFailureRateLimiter(15*time.Second, 15*time.Second),&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},)}controller.claimQueue = workqueue.NewNamedRateLimitingQueue(rateLimiter, "claims")controller.volumeQueue = workqueue.NewNamedRateLimitingQueue(rateLimiter, "volumes")informer := informers.NewSharedInformerFactory(client, controller.resyncPeriod)// ----------------------// PersistentVolumeClaimsclaimHandler := cache.ResourceEventHandlerFuncs{AddFunc: func(obj interface{}) { controller.enqueueClaim(obj) },UpdateFunc: func(oldObj, newObj interface{}) { controller.enqueueClaim(newObj) },DeleteFunc: func(obj interface{}) {// NOOP. The claim is either in claimsInProgress and in the queue, so it will be processed as usual// or it's not in claimsInProgress and then we don't care},}if controller.claimInformer != nil {controller.claimInformer.AddEventHandlerWithResyncPeriod(claimHandler, controller.resyncPeriod)} else {controller.claimInformer = informer.Core().V1().PersistentVolumeClaims().Informer()controller.claimInformer.AddEventHandler(claimHandler)}err = controller.claimInformer.AddIndexers(cache.Indexers{uidIndex: func(obj interface{}) ([]string, error) {uid, err := getObjectUID(obj)if err != nil {return nil, err}return []string{uid}, nil}})if err != nil {logger.Error(err, "Error setting indexer for pvc informer", "indexer", uidIndex)klog.FlushAndExit(klog.ExitFlushTimeout, 1)}controller.claimsIndexer = controller.claimInformer.GetIndexer()// -----------------// PersistentVolumesvolumeHandler := cache.ResourceEventHandlerFuncs{AddFunc: func(obj interface{}) { controller.enqueueVolume(obj) },UpdateFunc: func(oldObj, newObj interface{}) { controller.enqueueVolume(newObj) },DeleteFunc: func(obj interface{}) { controller.forgetVolume(obj) },}if controller.volumeInformer != nil {controller.volumeInformer.AddEventHandlerWithResyncPeriod(volumeHandler, controller.resyncPeriod)} else {controller.volumeInformer = informer.Core().V1().PersistentVolumes().Informer()controller.volumeInformer.AddEventHandler(volumeHandler)}controller.volumes = controller.volumeInformer.GetStore()// --------------// StorageClasses// no resource event handler needed for StorageClassesif controller.classInformer == nil {controller.classInformer = informer.Storage().V1().StorageClasses().Informer()}controller.classes = controller.classInformer.GetStore()if controller.createProvisionerPVLimiter != nil {logger.V(2).Info("Using saving PVs to API server in background")controller.volumeStore = NewVolumeStoreQueue(client, controller.createProvisionerPVLimiter, controller.claimsIndexer, controller.eventRecorder)} else {if controller.createProvisionedPVBackoff == nil {// Use linear backoff with createProvisionedPVInterval and createProvisionedPVRetryCount by default.if controller.createProvisionedPVInterval == 0 {controller.createProvisionedPVInterval = DefaultCreateProvisionedPVInterval}if controller.createProvisionedPVRetryCount == 0 {controller.createProvisionedPVRetryCount = DefaultCreateProvisionedPVRetryCount}controller.createProvisionedPVBackoff = &wait.Backoff{Duration: controller.createProvisionedPVInterval,Factor: 1, // linear backoffSteps: controller.createProvisionedPVRetryCount,// Cap: controller.createProvisionedPVInterval,}}logger.V(2).Info("Using blocking saving PVs to API server")controller.volumeStore = NewBackoffStore(client, controller.eventRecorder, controller.createProvisionedPVBackoff, controller)}return controller
}
syncClaim()
- 判断是否应该进行供给:
- 调用
ctrl.shouldProvision(ctx, claim)
方法来判断是否需要对这个PVC进行供给操作。如果返回错误,则更新供给统计信息并返回错误。 - 如果
should
为true
,表示需要进行供给操作。
- 调用
- 供给操作:
- 记录供给操作的开始时间。
- 从上下文中获取logger对象。
- 调用
ctrl.provisionClaimOperation(ctx, claim)
方法进行供给操作,返回操作状态和可能的错误。 - 更新供给统计信息,传入错误和开始时间。
- 处理供给操作的结果:
- 如果供给操作没有错误或者状态是
ProvisioningFinished
,表示供给操作已经完成或者不需要进行。根据错误类型进行不同的处理:- 如果没有错误,记录日志并删除该PVC在
claimsInProgress
中的记录。 - 如果错误是
errStopProvision
,记录日志并将错误置为nil
(调用者会重新排队处理)。 - 其他错误类型,记录日志。
- 如果没有错误,记录日志并删除该PVC在
- 如果供给状态是
ProvisioningInBackground
,表示供给操作正在后台进行,记录日志并将PVC添加到claimsInProgress
中。 - 如果供给状态是
ProvisioningNoChange
,不做任何修改,保持claimsInProgress
的状态不变。
- 如果供给操作没有错误或者状态是
- 返回错误:
- 如果不需要进行供给操作或者供给操作已经完成并且没有需要处理的错误,则返回
nil
。 - 否则,返回供给操作中的错误。
这段代码的主要逻辑是围绕PVC的供给状态进行操作,根据供给的结果更新内部状态(如claimsInProgress
),并记录相关的日志信息。通过这种方式,ProvisionController
能够管理多个PVC的供给过程,确保每个PVC都能够被正确地处理。
- 如果不需要进行供给操作或者供给操作已经完成并且没有需要处理的错误,则返回
func (ctrl *ProvisionController) syncClaim(ctx context.Context, obj interface{}) error {claim, ok := obj.(*v1.PersistentVolumeClaim)if !ok {return fmt.Errorf("expected claim but got %+v", obj)}should, err := ctrl.shouldProvision(ctx, claim)if err != nil {ctrl.updateProvisionStats(claim, err, time.Time{})return err} else if should {startTime := time.Now()logger := klog.FromContext(ctx)status, err := ctrl.provisionClaimOperation(ctx, claim)ctrl.updateProvisionStats(claim, err, startTime)if err == nil || status == ProvisioningFinished {// Provisioning is 100% finished / not in progress.switch err {case nil:logger.V(5).Info("Claim processing succeeded, removing PVC from claims in progress", "claimUID", claim.UID)case errStopProvision:logger.V(5).Info("Stop provisioning, removing PVC from claims in progress", "claimUID", claim.UID)// Our caller would requeue if we pass on this special error; return nil instead.err = nildefault:logger.V(2).Info("Final error received, removing PVC from claims in progress", "claimUID", claim.UID)}ctrl.claimsInProgress.Delete(string(claim.UID))return err}if status == ProvisioningInBackground {// Provisioning is in progress in background.logger.V(2).Info("Temporary error received, adding PVC to claims in progress", "claimUID", claim.UID)ctrl.claimsInProgress.Store(string(claim.UID), claim)} else {// status == ProvisioningNoChange.// Don't change claimsInProgress:// - the claim is already there if previous status was ProvisioningInBackground.// - the claim is not there if if previous status was ProvisioningFinished.}return err}return nil
}
shouldProvision()
- 检查PVC是否已指定卷名:
- 如果
claim.Spec.VolumeName
不为空,表示这个PVC已经绑定到了一个具体的卷上,因此不需要再进行供给。方法返回false, nil
。
- 如果
- 检查Provisioner是否实现了Qualifier接口:
- 通过类型断言
ctrl.provisioner.(Qualifier)
检查ctrl.provisioner
是否实现了Qualifier
接口。 - 如果实现了,并且
Qualifier
接口的ShouldProvision
方法返回false
,则表示不需要进行供给。方法返回false, nil
。
- 通过类型断言
- 检查PVC的注解以确定Provisioner:
- 首先尝试从PVC的注解中获取
annStorageProvisioner
的值。 - 如果不存在,则尝试获取
annBetaStorageProvisioner
的值。 - 这两个注解用于指定负责供给卷的Provisioner。
- 首先尝试从PVC的注解中获取
- 检查找到的Provisioner是否是已知的:
- 如果找到了Provisioner的注解,并且这个Provisioner是控制器已知的(通过
ctrl.knownProvisioner(provisioner)
检查),则继续下一步。
- 如果找到了Provisioner的注解,并且这个Provisioner是控制器已知的(通过
- 检查StorageClass的VolumeBindingMode:
- 通过
util.GetPersistentVolumeClaimClass(claim)
获取PVC所属的StorageClass。 - 通过
ctrl.getStorageClass(claimClass)
获取这个StorageClass的详细信息。 - 检查StorageClass的
检查StorageClass的VolumeBindingMode
。如果设置为storage.VolumeBindingWaitForFirstConsumer
(即延迟绑定模式),则需要进一步检查PVC的注解中是否有annSelectedNode
。- 如果有
annSelectedNode
且其值不为空,表示已经选定了节点,可以进行供给。方法返回true, nil
。 - 如果没有或值为空,则不进行供给。方法返回
false, nil
。
- 如果有
- 通过
- 默认进行供给:
- 如果StorageClass的
VolumeBindingMode
不是延迟绑定模式,或者没有找到VolumeBindingMode
,则默认需要进行供给。方法返回true, nil
。
- 如果StorageClass的
- 未找到Provisioner:
- 如果在PVC的注解中没有找到任何Provisioner的标识,则不进行供给。方法返回
false, nil
。
- 如果在PVC的注解中没有找到任何Provisioner的标识,则不进行供给。方法返回
总结来说,这段代码通过检查PVC的各种属性和注解,以及关联的StorageClass的配置,来决定是否需要对这个PVC进行卷的供给。这涉及到检查是否已经指定了卷、是否满足特定的供给条件、是否使用了延迟绑定模式等多个方面
// shouldProvision returns whether a claim should have a volume provisioned for
// it, i.e. whether a Provision is "desired"
func (ctrl *ProvisionController) shouldProvision(ctx context.Context, claim *v1.PersistentVolumeClaim) (bool, error) {if claim.Spec.VolumeName != "" {return false, nil}if qualifier, ok := ctrl.provisioner.(Qualifier); ok {if !qualifier.ShouldProvision(ctx, claim) {return false, nil}}provisioner, found := claim.Annotations[annStorageProvisioner]if !found {provisioner, found = claim.Annotations[annBetaStorageProvisioner]}if found {if ctrl.knownProvisioner(provisioner) {claimClass := util.GetPersistentVolumeClaimClass(claim)class, err := ctrl.getStorageClass(claimClass)if err != nil {return false, err}if class.VolumeBindingMode != nil && *class.VolumeBindingMode == storage.VolumeBindingWaitForFirstConsumer {// When claim is in delay binding mode, annSelectedNode is// required to provision volume.// Though PV controller set annStorageProvisioner only when// annSelectedNode is set, but provisioner may remove// annSelectedNode to notify scheduler to reschedule again.if selectedNode, ok := claim.Annotations[annSelectedNode]; ok && selectedNode != "" {return true, nil}return false, nil}return true, nil}}return false, nil
}
provisionClaimOperation()
- 获取PVC的类别:
- 使用
util.GetPersistentVolumeClaimClass(claim)
获取PVC的存储类别(StorageClass)。
- 使用
- 日志记录:
- 使用Kubernetes的日志库klog来记录日志,包括PVC和StorageClass的信息。
- 检查PV是否已经存在:
- 通过
ctrl.getProvisionedVolumeNameForClaim(claim)
获取预期的PV名称,然后检查这个PV是否已经在ctrl.volumes
中存在。如果存在,说明PV已经被分配,函数返回ProvisioningFinished
和errStopProvision
。
- 通过
- 获取PVC的引用:
- 使用
ref.GetReference(scheme.Scheme, claim)
获取PVC的引用,以便在后续操作中引用这个PVC对象。
- 使用
- 检查是否可以分配:
- 调用
ctrl.canProvision(ctx, claim)
检查当前的ProvisionController是否可以处理这个PVC的分配请求。如果不能,记录事件并返回错误。
- 调用
- 获取StorageClass信息:
- 通过
ctrl.getStorageClass(claimClass)
获取PVC指定的StorageClass的信息。如果获取失败或StorageClass的Provisioner不被当前ProvisionController支持,则记录错误并返回。
- 通过
- 获取选定的节点:
- 如果PVC的注解中指定了选定的节点(
annSelectedNode
或annAlphaSelectedNode
),则尝试获取这个节点的信息。如果节点不存在,调用ctrl.provisionVolumeErrorHandling
处理错误。
- 如果PVC的注解中指定了选定的节点(
- 准备分配选项:
- 创建一个
ProvisionOptions
对象,包含StorageClass、PV名称、PVC对象和选定的节点信息。
- 创建一个
- 记录正常事件:
- 使用
ctrl.eventRecorder.Event
记录一个正常事件,表示外部Provisioner正在为PVC分配存储卷。
- 使用
- 调用Provisioner进行分配:
- 调用
ctrl.provisioner.Provision(ctx, options)
尝试分配存储卷。如果分配失败,根据错误类型进行相应的错误处理。
- 调用
- 设置PVC的引用和Finalizer:
- 如果分配成功,设置PV的
ClaimRef
为PVC的引用,并根据需要添加Finalizer。
- 如果分配成功,设置PV的
- 更新PV的元数据和存储类别:
- 更新PV的注解和存储类别信息。
- 存储和添加PV:
- 使用
ctrl.volumeStore.StoreVolume
存储PV信息,并将PV添加到ctrl.volumes
中。
- 使用
- 返回结果:
- 如果所有操作都成功,函数返回
ProvisioningFinished
和nil
表示成功完成分配。
- 如果所有操作都成功,函数返回
这个函数涵盖了从检查PV是否存在到实际分配存储卷,再到更新内部状态和记录相关事件的整个过程。它是Kubernetes存储卷分配流程中的一个关键部分,确保了PVC能够被正确地处理和分配存储资源。
func (ctrl *ProvisionController) provisionClaimOperation(ctx context.Context, claim *v1.PersistentVolumeClaim) (ProvisioningState, error) {// Most code here is identical to that found in controller.go of kube's PV controller...claimClass := util.GetPersistentVolumeClaimClass(claim)logger := klog.LoggerWithValues(klog.FromContext(ctx), "PVC", klog.KObj(claim), "StorageClass", claimClass)logger.V(4).Info("Started")// A previous doProvisionClaim may just have finished while we were waiting for// the locks. Check that PV (with deterministic name) hasn't been provisioned// yet.pvName := ctrl.getProvisionedVolumeNameForClaim(claim)_, exists, err := ctrl.volumes.GetByKey(pvName)if err == nil && exists {// Volume has been already provisioned, nothing to do.logger.V(4).Info("PersistentVolume already exists, skipping", "PV", pvName)return ProvisioningFinished, errStopProvision}// Prepare a claimRef to the claim early (to fail before a volume is// provisioned)claimRef, err := ref.GetReference(scheme.Scheme, claim)if err != nil {logger.Error(err, "Unexpected error getting claim reference")return ProvisioningNoChange, err}// Check if this provisioner can provision this claim.if err = ctrl.canProvision(ctx, claim); err != nil {ctrl.eventRecorder.Event(claim, v1.EventTypeWarning, "ProvisioningFailed", err.Error())logger.Error(err, "Failed to provision volume")return ProvisioningFinished, errStopProvision}// For any issues getting fields from StorageClass (including reclaimPolicy & mountOptions),// retry the claim because the storageClass can be fixed/(re)created independently of the claimclass, err := ctrl.getStorageClass(claimClass)if err != nil {logger.Error(err, "Error getting claim's StorageClass's fields")return ProvisioningFinished, err}if !ctrl.knownProvisioner(class.Provisioner) {// class.Provisioner has either changed since shouldProvision() or// annDynamicallyProvisioned contains different provisioner than// class.Provisioner.logger.Error(nil, "Unknown provisioner requested in claim's StorageClass", "provisioner", class.Provisioner)return ProvisioningFinished, errStopProvision}var selectedNode *v1.Node// Get SelectedNodeif nodeName, ok := getString(claim.Annotations, annSelectedNode, annAlphaSelectedNode); ok {if ctrl.nodeLister != nil {selectedNode, err = ctrl.nodeLister.Get(nodeName)} else {selectedNode, err = ctrl.client.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) // TODO (verult) cache Nodes}if err != nil {// if node does not exist, reschedule and remove volume.kubernetes.io/selected-node annotationif apierrs.IsNotFound(err) {ctx2 := klog.NewContext(ctx, logger)return ctrl.provisionVolumeErrorHandling(ctx2, ProvisioningReschedule, err, claim)}err = fmt.Errorf("failed to get target node: %v", err)ctrl.eventRecorder.Event(claim, v1.EventTypeWarning, "ProvisioningFailed", err.Error())return ProvisioningNoChange, err}}options := ProvisionOptions{StorageClass: class,PVName: pvName,PVC: claim,SelectedNode: selectedNode,}ctrl.eventRecorder.Event(claim, v1.EventTypeNormal, "Provisioning", fmt.Sprintf("External provisioner is provisioning volume for claim %q", klog.KObj(claim)))volume, result, err := ctrl.provisioner.Provision(ctx, options)if err != nil {if ierr, ok := err.(*IgnoredError); ok {// Provision ignored, do nothing and hope another provisioner will provision it.logger.V(4).Info("Volume provision ignored", "reason", ierr)return ProvisioningFinished, errStopProvision}ctx2 := klog.NewContext(ctx, logger)err = fmt.Errorf("failed to provision volume with StorageClass %q: %v", claimClass, err)return ctrl.provisionVolumeErrorHandling(ctx2, result, err, claim)}logger.V(4).Info("Volume is provisioned", "PV", volume.Name)// Set ClaimRef and the PV controller will bind and set annBoundByController for usvolume.Spec.ClaimRef = claimRef// Add external provisioner finalizer if it doesn't already have itif ctrl.addFinalizer && !ctrl.checkFinalizer(volume, finalizerPV) {volume.ObjectMeta.Finalizers = append(volume.ObjectMeta.Finalizers, finalizerPV)}metav1.SetMetaDataAnnotation(&volume.ObjectMeta, annDynamicallyProvisioned, class.Provisioner)volume.Spec.StorageClassName = claimClasslogger.V(4).Info("Succeeded")if err := ctrl.volumeStore.StoreVolume(logger, claim, volume); err != nil {return ProvisioningFinished, err}if err = ctrl.volumes.Add(volume); err != nil {utilruntime.HandleError(err)}return ProvisioningFinished, nil
}
相关文章:
CSI-external-provisioner
main() 这段Go代码是一个CSI(容器存储接口)Provisioner(供应器)的实现,用于在Kubernetes集群中动态提供持久卷。代码涉及多个组件和步骤,下面是对关键部分的解释: 初始化和配置 命令行标志和…...
OpenAI为抢跑AI,安全底线成牺牲品?
几年前,如果你问任何一个AI从业者,安全测试需要多长时间,他们可能会淡定地告诉你:“至少几个月吧,毕竟这玩意儿可能改变世界,也可能毁了它。”而现在,OpenAI用实际行动给出了一个新答案——几天…...
单片机任意普通IO引脚使用定时器扩展外部中断的巧妙方法
在嵌入式系统中,将任意一个IO端口配置为外部中断源是一种常见的需求,尤其是在硬件资源有限的情况下。通过定时器扩展外部中断的方法,可以在不依赖专用中断引脚的情况下,实现对外部信号的实时响应。以下是一种基于定时器扩展外部中…...
arcgis几何与游标(1)
本节我们对几何进行展开学习 ArcPy 的几何对象 在 ArcPy 中,几何对象是表示地理空间数据的核心。它包括点(Point)、多点(Multipoint)、线(Polyline)和面(Polygon)等类型…...
安全密码处理实践
1. 引言 在现代应用程序中,密码存储和验证的安全性 直接关系到用户数据的保护。密码泄露事件频繁发生,通常是由于不安全的存储方式 或 弱加密处理 导致的。为了提高密码的安全性,开发者需要遵循一系列安全密码处理 的最佳实践。 本篇文章将详细介绍如何在应用程序中安全地…...
can‘t set boot order in virtualbox
Boot order setting is ignored if UEFI is enabled https://forums.virtualbox.org/viewtopic.php?t99121 如果勾选EFI boot order就是灰色的 传统BIOS就是可选的 然后选中任意介质,通过右边的上下箭头调节顺序,最上面的应该是优先级最高的 然后就…...
电池分选机详细介绍
在当今这个科技飞速发展的时代,电池作为能源存储的重要载体,其性能的一致性和稳定性对于各类电子设备和电动汽车等应用至关重要。而电池分选机,作为电池生产过程中的关键环节,正扮演着越来越重要的角色。本文将带您深入了解电池分…...
深入理解浏览器的 Cookie:全面解析与实践指南
在现代 Web 开发中,Cookie 扮演着举足轻重的角色。它不仅用于管理用户会话、记录用户偏好,还在行为追踪、广告投放以及安全防护等诸多方面发挥着重要作用。随着互联网应用场景的不断丰富,Cookie 的使用和管理也日趋复杂,如何在保障…...
浙江大学DeepSeek系列专题线上公开课第二季第五期即将上线!deepseek人文艺术之美专场来啦!
浙江大学DeepSeek系列专题线上公开课第二季第五期即将重磅上线! 其中,今晚7点半,浙大AI大神陈为教授将带来硬核的deepseek公开课讲座。 讲座 主题: DeepSeek时代,让AI更懂中国文化的美与善 主讲人: 陈为 …...
5分钟学会接口自动化测试框架
今天,我们来聊聊接口自动化测试。 接口自动化测试是什么?如何开始?接口自动化测试框架如何搭建? 自动化测试 自动化测试,这几年行业内的热词,也是测试人员进阶的必备技能,更是软件测试未来发…...
Flink DataStream API深度解析(Scala版):窗口计算、水位线与状态编程
在前面的文章中Flink 编程基础:Scala 版 DataStream API 入门-CSDN博客,我们已经介绍了 Flink 的 Datastream API 编程模型、窗口划分以及时间语义(处理时间、事件时间等)。本篇文章将深入讲解窗口计算的进阶内容,包括…...
【从零实现高并发内存池】内存池整体框架设计 及 thread cache实现
📢博客主页:https://blog.csdn.net/2301_779549673 📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson 📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! &…...
#MES系统中的一些相关的名词
📌MES系统 部分 术语表 缩写英文全称中文名称详细解释MESManufacturing Execution System制造执行系统用于连接计划系统与生产现场,实时管理和控制整个生产过程,覆盖物料、人员、设备、质量、指令等。ERPEnterprise Resource Planning企业资…...
《灵活的接口设计:如何支持多种后端数据存取实现》
《灵活的接口设计:如何支持多种后端数据存取实现》 一、引言:从单一适配到多样需求 在现代软件开发中,系统通常需要与不同的数据存储后端进行交互,例如关系型数据库(MySQL、PostgreSQL)、NoSQL 数据库(MongoDB、Redis)或文件存储(JSON、CSV)。为了增强系统的可扩展性…...
Spark-SQL核心编程(一)
一、Spark-SQL 基础概念 1.定义与起源:Spark SQL 是 Spark 用于结构化数据处理的模块,前身是 Shark。Shark 基于 Hive 开发,提升了 SQL-on-Hadoop 的性能,但因对 Hive 依赖过多限制了 Spark 发展,后被 SparkSQL 取代&…...
Qt:解决MSVC编译器下qDebug输出中文乱码的问题
问题描述: 使用msvc编译器,通过qDebug输出打印信息为乱码(显示问号或者乱码) 百度到以下方案,但是没有效果 最终解决: 在.pro文件中添加如下,重新构建运行即可显示中文内容 msvc:QMAKE_CXXFLAGS -exec…...
Go:接口
接口既约定 Go 语言中接口是抽象类型 ,与具体类型不同 ,不暴露数据布局、内部结构及基本操作 ,仅提供一些方法 ,拿到接口类型的值 ,只能知道它能做什么 ,即提供了哪些方法 。 func Fprintf(w io.Writer, …...
js | 网页上的 json 数据怎么保存到本地表格中?
1.思路 json 转为 csv 保存到本地或者:json 转为 html 显示到网页中,然后复制到excel中。 (2) 数据 wjl{"code":1,"data":[{"chrmiRNA":"chr1","0":"chr1","startmiRNA":&quo…...
智能Todo协作系统开发日志(二):架构优化与安全增强
📅 2025年4月14日 | 作者:Aphelios380 🌟 今日优化目标 在原Todo单机版基础上进行三大核心升级: 组件化架构改造 - 提升代码可维护性 本地数据加密存储 - 增强隐私安全性 无障碍访问支持 - 践行W3C标准 一、组件化架构改造 …...
buctoj_算法设计与分析(5)
问题 A: 没有上司的舞会 题目描述 Ural大学有N名职员,编号为1~N。 他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。 每个职员有一个快乐指数,用整数 HiHi 给出,其中 1≤i≤N。 现在要召开一场周年庆宴会&#x…...
VUE项目中的package.json中的启动脚本
"scripts": {"dev": "vite","build:prod": "vite build","build:stage": "vite build --mode staging","preview": "vite preview"}vite build 和 vite build --mode staging 是 V…...
目标追踪数据标注
在将 YOLO(目标检测) 和 DeepSORT(目标追踪) 结合时,数据标注需要同时满足 检测 和 追踪 的需求。以下是具体的分阶段标注策略和操作指南: 一、标注的核心要求 检测标注:每帧中目标的 边界框&a…...
详细介绍7大排序算法
1.排序的概念及其运用 1.1 排序的概念 排序 :所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。 稳定性 :假定在待排序的记录序列中,存在多个具有相同的关键字的记…...
TGCTF web
AAA偷渡阴平 这个题是一个非预期的无参RCE <?php$tgctf2025$_GET[tgctf2025];if(!preg_match("/0|1|[3-9]|\~|\|\|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\|\|\{|\[|\]|\}|\:|\|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $tgctf2025)){//hi…...
RTPS数据包分析
DDS-RTPS 常见子消息_dds pdp消息-CSDN博客Fast RTPS原理与代码分析(3):动态发现协议之端点发现协议EDP_fast-rtps 原理-CSDN博客 在RTPS(Real-Time Publish-Subscribe,实时发布订阅)协议中,DATA(r)和DATA是两种不同的…...
go语言gRPC使用流程
1. 安装工具和依赖 安装 Protocol Buffers 编译器 (protoc) 下载地址:https://github.com/protocolbuffers/protobuf/releases 使用说明:https://protobuf.dev/ 【centos环境】yum方式安装:protoc[rootlocalhost demo-first]# yum install …...
回溯算法的要点
可以用树结构(解空间树)来表示用回溯法解决的问题的所有选项。 叶节点则对应着最终的状态. 回溯过程:深度遍历,在任意时刻,算法只保存从根结点到当前结点的路径。 “剪枝”:当某一节点不包含问题的解&am…...
爬虫: 一文掌握 pycurl 的详细使用(更接近底层,性能更高)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、PycURL概述1.1 PycURL介绍1.2 基本安装1.3 安装依赖(Linux/macOS)1.4 常用选项参考二、基本使用2.1 简单 GET 请求2.2 获取响应信息2.3 设置请求头2.4 超时设置2.5 跟随重定向三、高级功能3.1 POST 请求3.2 文件上…...
大模型文生图
提示词分4个部分:质量,主体,元素,风格 质量:杰作,高质量,超细节,完美的精度,高分辨率,大师级的; 权重:把图片加括号,&am…...
c# AI编程助手 — Fitten Code
前言 前有Copilot各种酷炫操作,今有国产软件杀出重围。给大家介绍的是一款国内的国产编程神器,可与微软GitHub Copilot比比身手。关键它还是完全免费。它就是:非十团队国产自主研发的Fitten Code。此工具的速度是GitHub Copilot的两倍&#x…...
《植物大战僵尸融合版v2.4.1》,塔防与创新融合的完美碰撞
《植物大战僵尸融合版》是基于经典塔防游戏《植物大战僵尸》的创意同人改版,由“蓝飘飘fly”等开发者主导制作。它在保留原版核心玩法的基础上,引入了独特的植物融合机制,玩家可以将不同的植物进行组合,创造出全新的植物种类&…...
深度学习总结(12)
层:深度学习的基础模块 神经网络的基本数据结构是层。层是一个数据处理模块,它接收一个或多个张量作为输入,并输出一个或多个张量。有些层是无状态的,但大多数层具有状态,即层的权重。权重是利用随机梯度下降学到的一个或多个张量…...
pyqt环境配置
文章目录 1 概述2 PyQt6和PySide6区别3 环境配置4 配置PySide65 配置PyQt66 配置外部工具7 添加模板8 使用pyside6-project构建工程9 常见错误10 相关地址 更多精彩内容👉内容导航 👈👉Qt开发 👈👉python开发 …...
YOLO11改进——融合BAM注意力机制增强图像分类与目标检测能力
深度学习在计算机视觉领域的应用取得了显著进展,尤其是在目标检测(Object Detection)和图像分类(Image Classification)任务中。YOLO(You Only Look Once)系列算法凭借其高效的单阶段检测框架和…...
考研单词笔记 2025.04.14
amount n数量,数额v(数量)达到,总计(to),意味着,相当于 couple n一对,一双,一些,几个,夫妻,情侣v连接,结合 …...
AI云游戏盒子:未来娱乐的新纪元
AI云游戏盒子:未来娱乐的新纪元 随着科技的不断进步,人工智能(AI)与云计算技术的结合正在重新定义我们享受数字娱乐的方式。2025年,一款名为“AI云游戏盒子”的产品正逐渐成为家庭娱乐的核心设备,它不仅集…...
第八章 文件操作
第八章 文件操作 文章目录 第八章 文件操作1 文件读取1 将文件整个读取内存2 按字节读取文件 1 文件读取 1 将文件整个读取内存 类似于python的 with open(filename, modert, encodingutf-8) as f:res f.read()go中的书写方式: 方式一: package ma…...
《extern:如何在编译时“暗通款曲“》
C中extern关键字的完整用法总结 extern是C中管理链接性(linkage)的重要关键字,主要用于声明外部定义的变量或函数。以下是详细的用法分类和完整示例: 一、基本用法 1. 声明外部全局变量 // globals.cpp int g_globalVar 42; …...
活动图与流程图的区别与联系:深入理解两种建模工具
目录 前言1. 活动图概述1.1 活动图的定义1.2 活动图的基本构成要素1.3 活动图的应用场景 2. 流程图概述2.1 流程图的定义2.2 流程图的基本构成要素2.3 流程图的应用场景 3. 活动图与流程图的联系4. 活动图与流程图的区别4.1 所属体系不同4.2 表达能力差异4.3 使用目的与语境4.4…...
FinanceRAG获奖方案解读:ACM-ICAIF ’24的FinanceRAG挑战赛
ACM-ICAIF 24 FinanceRAG Challenge提供一套整合的文本和表格财务数据集。这些数据集旨在测试系统检索和推理财务数据的能力。参与者将受益于 Github 上的基线示例和官方提交代码,其位于FinanceRAG,以及在 huggingface 上的简化的数据集访问,…...
Linux LED驱动(gpio子系统)
0. gpio子系统 gpio子系统是linux内核当中用于管理GPIO资源的一套系统,它提供了很多GPIO相关的API接口,驱动程序中使用GPIO之前需要向gpio子系统申请。 gpio子系统的主要目的就是方便驱动开发者使用gpio,驱动开发者在设备树中添加gpio相关信息…...
场外期权交易和结算方式的区别是什么?
场外期权交易的核心在于双方协商一致的合约条款。这些条款包括但不限于期权的类型(看涨或看跌)、执行价格、到期日、合约规模以及支付的期权费。由于每份合约都是独一无二的,因此交易双方需要具备高度的专业知识和谈判技巧,下文为…...
自注意力的机制内涵和设计逻辑
在自注意力机制中,查询(Q)、键(K)和值(V)的交互过程是核心设计,其背后的数学和语义内涵可以从以下角度理解: 1. 数学视角:动态加权聚合 自注意力机制的公式可…...
VIM学习笔记
1. ex模式 vim中,按:触发的命令行模式,称为 ex模式,具体命令参见如下笔记: https://blog.csdn.net/u010250151/article/details/51868751?ops_request_misc%257B%2522request%255Fid%2522%253A%2522814b671a9898c95…...
Windows系统docker desktop安装(学习记录)
目前在学习docker,在网上扒了很多老师的教程,终于装好了,于是决定再装一遍做个记录,省的以后再这么麻烦 一:什么是docker Docker 是一个开源的应用容器引擎,它可以让开发者打包他们的应用以及依赖包到一个…...
操作系统学习笔记——[特殊字符]超详细 | 如何唤醒被阻塞的 socket 线程?线程阻塞原理、线程池、fork/vfork彻底讲明白!
💡超详细 | 如何唤醒被阻塞的 socket 线程?线程阻塞原理、线程池、fork/vfork彻底讲明白! 一、什么是阻塞?为什么线程会阻塞?二、socket线程被阻塞的典型场景🧠 解法思路: 三、线程的几种阻塞状…...
GIC驱动程序对中断的处理流程
承接上一篇,我们来讲讲GIC的处理流程: 我们先来看看老版本的CPU是怎么去处理中断的: 这种的话,基本上就是一开始就确定你有多少个中断,就为你分配好了多少个irq_desc,这样子每一个硬件中断,也就…...
罗庚机器人:机器人打磨领域的先行者
近日,记者在广东罗庚机器人有限公司(以下简称罗庚机器人)总经理蒲小平处了解到,该公司是一家研发与为客户提供高精度自适应机器人打磨抛光集成工艺的高科技企业,是机器人打磨领域的先行者。在国内外机器人打磨抛光应用…...
如何在 Java 中对 PDF 文件进行数字签名(教程)
Java 本身并不原生支持 PDF 文件,因此若要对 PDF 进行数字签名,您需要使用一些专用的软件。本教程将演示如何使用 JPedal PDF 库来对 PDF 文件进行数字签名。 步骤: • 下载 JPedal 并将 Jar 文件添加到项目中 • 创建一个 PKCS#12 密…...
一文了解:北斗短报文终端是什么,有哪些应用场景?
在通信技术飞速发展的今天,人们已习惯于依赖地面基站和互联网实现即时通信。然而,当自然灾害突发、远洋航行遇险或深入无人区勘探时,传统通信手段往往失效。北斗短报文终端——这一由中国自主研发的卫星通信技术,正以“无网络通信…...