【kubernetes】使用virtual-kubelet扩展k8s

1 何为virtual-kubelet?

kubelet是k8s的agent,负责监听Pod的调度情况,并运行Pod。而virtual-kubelet不是真实跑在宿主机上的,而是一个可以跑在任何地方的进程,该进程向k8s伪装成一个真实的Node,但是实际的操作可以自定义。

也就是说,virtual-kubelet向k8s提供与kubelet兼容的接口,而可以自定义底层的具体实现,通常可以用于不同架构之间的配合使用,例如,virtual-kubelet的底层的具体实现是采用kvm实现,或者用另一种方式实现Pod。

virtual-kubelet的使用场景:

  • 对接原有的平台:Kubernetes Virtual Kubelet with ACI
  • 资源的自动扩容:UCloud UK8S虚拟节点 让用户不再担心集群没有资源

2 virtual-kubelet的整体架构

virtual-kubelet

整个virtual-kubelet仓库重要的目录是:

  • cmd/virtual-kubelet/
    • main.go 主函数,负责自动virtual-kubelet,依旧使用了Cobra实现
    • register.go 注册provider,这里实现了个Mock的provider
    • internal/
      • commands/ 程序命令的定义,
      • provider/
        • provider.go provider的接口定义
        • mock/ mock的provider的实现
  • internal/ 额外实现的一些包供内部调用
  • node/ kubelet中的一些逻辑

因此,整个仓库的整体调用路径是:

  • main.go使用Cobra构建命令行操作,然后调用register.go注册provider,进入事件循环
  • node/目录下有PodController的实现,这里面就会调用注册的provider

3 基于virtual-kubelet库扩展k8s

使用virtual-kubelet扩展k8s有2种方式:

  • 1 直接克隆virtual-kubelet仓库,然后在cmd/virtual-kubelet/internal/provider下面新建一个自己的provider,然后实现provider接口的各种方法,然后在cmd/virtual-kubelet/register.go中注册自己实现的provider即可
  • 2 新开一个仓库,在里面使用virtual-kubelet包,同样的,只需要实现provider,在主函数中注册,然后将provider的接口对接到自己的实现就行

如果是自己实现virtual-kubelet,通常会使用方式2。

当前,部分云厂商已经开发了自己的virtual-kubelet用于对接自己的容器平台,例如,微软开发了对接ACI的azure-aci,下面重点来分析azure-aci的实现,自己实现的方式也类似。

4 Microsoft的azure-aci

virtual-kubelet/azure-aci的目录结构如下:

  • charts/:helm打包生成的压缩包
  • client/:封装对接ACI的接口
    • aci/:对ACI容器组的接口封装
    • api/:封装ACI接口时使用的一些功能函数
    • network/:封装ACI的subnet的接口
    • resourcegroups/:封装ACI的资源组的接口
  • cmd/virtual-kubelet/main.go:主函数,日志和配置处理,注册ACIProvider并启动
  • helm/:helm的配置
  • provider/:ACIProvider的实现,调用client中的函数实现provider

main.go中就是一些环境配置和启动函数:

// 获取环境变量中的配置
// 例如,节点名、kubeconfig文件、污点
o, err := opts.FromEnv()
if err != nil {log.G(ctx).Fatal(err)
}
o.Provider = "azure"
o.Version = strings.Join([]string{k8sVersion, "vk-azure-aci", buildVers, "-")
o.PodSyncWorkers = numberOfWork// 初始化node节点
node, err := cli.New(ctx,cli.WithBaseOpts(o),cli.WithCLIVersion(buildVersion, buildTime),cli.WithProvider("azure", func(cfg provider.InitConfig) (proviProvider, error) {return azprovider.NewACIProvider(cfg.ConfigPath, ResourceManager, cfg.NodeName, cfg.OperatingSystem, InternalIP, cfg.DaemonPort, cfg.KubeClusterDomain)}),cli.WithPersistentFlags(logConfig.FlagSet()),cli.WithPersistentPreRunCallback(func() error {return logruscli.Configure(logConfig, logger)}),cli.WithPersistentFlags(traceConfig.FlagSet()),cli.WithPersistentPreRunCallback(func() error {return opencensuscli.Configure(ctx, &traceConfig, o)}),if err != nil {log.G(ctx).Fatal(err)if err := node.Run(ctx); err != nil {log.G(ctx).Fatal(err)
}

上面的核心代码就是WithProvider(),该函数有两个参数,一个是provider的名称,另一个是provider的初始化函数,这里传的初始化函数就是创建ACIProvider:azprovider.NewACIProvider()。

func WithProvider(name string, f provider.InitFunc) Option {return func(c *Command) {if c.s == nil {c.s = provider.NewStore()}c.s.Register(name, f)}
}

这个地方需要注意的是初始化函数的参数:cfg provider.InitConfig:

type InitConfig struct {ConfigPath        stringNodeName          string // 注册到k8s到节点名称OperatingSystem   string // 节点的操作系统InternalIP        string // 节点的IPDaemonPort        int32 // 节点的端口KubeClusterDomain string // k8s集群域名ResourceManager   *manager.ResourceManager
}

provider.InitConfig里面大部分都是VK节点向k8s集群声明自己的一些信息。这些信息是通过初始化函数直接给到provider到初始化函数的,那么这些参数从哪里获得呢?

第一种方式,前面调用了opts.FromEnv(),该函数会从环境变量中获取一些信息,但是这个只有很少量的信息:节点名、端口、kubconfig、污点。

第二种方式,在执行azure-aci时传一些命令行参数,通过查看cli.New()函数的实现发现,该函数返回的实际上是个Command,该类型的cmd是个cobra.Command,cmd在创建命令时调用了installFlags(cmd.Flags(), o),该函数会添加很多命令行选项,其中包含常见的的cluster-domain、nodename、provider等。其实,直接编译执行也能发现该程序等命令行参数。

这些命令行参数里面,比较重要的是:

  • disable-taint:关闭污点,如果VK节点需要调度Pod,就需要启用该选项
  • nodename:节点名称
  • cluster-domain:集群域名,连接k8s集群时使用
  • no-verify-clients:当请求访问VK节点时,不验证客户端证书

接下来的重点就是azprovider.NewACIProvider()的实现,从上面的目录结构也可以看出,provider是对ACIProvider对provider接口的实现,client是对ACI接口的封装,在实现ACIProvider过程中调用client进行对接:

provider -> client -> ACI

NewACIProvider()函数在provider/aci.go中实现,其中的核心逻辑是:

// 创建ACI客户端
p.aciClient, err = aci.NewClient(azAuth, p.extraUserAgent, p.retryConfig)
if err != nil {return nil, err
}// 设置节点容量
if err := p.setupCapacity(context.TODO()); err != nil {return nil, err
}

接下来就是接口实现,kubelet最本质的工作就是监听Pod的状态变更,然后执行相应的动作,因此,当然是需要实现Pod的相关操作。

下面是VK所有的接口:

// pod控制器调用的接口,用于管理pod的生命周期
type PodLifecycleHandler interface {// 创建PodCreatePod(ctx context.Context, pod *corev1.Pod) error// 更新PodUpdatePod(ctx context.Context, pod *corev1.Pod) error// 删除PodDeletePod(ctx context.Context, pod *corev1.Pod) error// 查询单个Pod,返回的Pod有可能被多个goroutine并发访问,// 因此,最好使用DeepCopy深拷贝GetPod(ctx context.Context, namespace, name string) (*corev1.Pod, error)// 查询单个Pod对应的状态,同样的,需要使用DeepCopyGetPodStatus(ctx context.Context, namespace, name string) (*corev1.PodStatus, error)// 查询provider上运行的所有Pod,同样的,需要使用DeepCopyGetPods(context.Context) ([]*corev1.Pod, error)
}// 下面是必须要实现的函数,除了Pod,还包含其他的相关函数
type Provider interface {node.PodLifecycleHandler// 返回某个容器的日志(kubectl logs)GetContainerLogs(ctx context.Context, namespace, podName, containerName string, opts api.ContainerLogOpts) (io.ReadCloser, error)// 在容器中执行命令(kubectl exec)RunInContainer(ctx context.Context, namespace, podName, containerName string, cmd []string, attach api.AttachIO) error// 设置节点的参数,包含容量、condition等ConfigureNode(context.Context, *v1.Node)
}// 返回Pod的统计
type PodMetricsProvider interface {GetStatsSummary(context.Context) (*statsv1alpha1.Summary, error)
}// 用于支持Pod状态的异步更新
type PodNotifier interface {// 异步通知Pod的状态,注册回调函数,当Pod状态发生变化时就会调用回调函数NotifyPods(context.Context, func(*corev1.Pod))
}type NodeProvider interface {// 用于探测节点是否存活,k8s周期调用该函数确定节点是否存活Ping(context.Context) error// 异步通知节点的状态,注册回调函数,当节点状态发生变化时就会调用回调函数NotifyNodeStatus(ctx context.Context, cb func(*corev1.Node))
}

以上接口中,除了Provider接口必须实现,其他接口都是可选的。

下面以CreatePod()为例看下azure的具体实现:

func (p *ACIProvider) CreatePod(ctx context.Context, pod *v1.Pod) error {...return p.createContainerGroup(ctx, pod.Namespace, pod.Name, &containerGroup)
}func (p *ACIProvider) createContainerGroup(ctx context.Context, podNS, podName string, cg *aci.ContainerGroup) error {ctx = addAzureAttributes(ctx, span, p)cgName := containerGroupName(podNS, podName)_, err := p.aciClient.CreateContainerGroup(ctx,p.resourceGroup,cgName,*cg,)if err != nil {log.G(ctx).WithError(err).Errorf("failed to create container group %v", cgName)}return err
}

CreatePod()首先准备创建ACI容器组的资源,然后调用createContainerGroup(),该函数对接口调用再次封装,然后调用了aciClient的CreateContainerGroup()创建ACI容器组。而CreateContainerGroup()就是调用ACI的API接口创建容器组。PodLifecycleHandler中的函数实现方式都类似,只需要对接后端的接口即可。

实现了Provider接口,剩下的只需实现PodNotifier中的NotifyPods()。

NotifyPods()用于异步通知Pod的状态变化。设想下k8s展示Pod状态的实现,k8s如何知道Pod的状态呢?一种方式是k8s定时调用GetPods()接口就得到当前节点的所有Pod,当Node和Pod较多时,资源消耗还是有些多的。另一种方式就是,节点通过比较k8s认为节点有的Pod和ACI上实际有的容器组,就得到应该更新哪些Pod的状态。

Pod的异步更新实现在provider/podsTracker.go:

type PodsTrackerHandler interface {// 查询存活的PodListActivePods(ctx context.Context) ([]PodIdentifier, error)// 查询Pod的状态FetchPodStatus(ctx context.Context, ns, name string) (*v1.PodStatus, error)// 清理PodCleanupPod(ctx context.Context, ns, name string) error
}type PodsTracker struct {rm       *manager.ResourceManagerupdateCb func(*v1.Pod)handler  PodsTrackerHandler
}// NotifyPods函数的实现,该函数只在VK节点的PodController启动时调用一次
func (p *ACIProvider) NotifyPods(ctx context.Context, notifierCb func(*v1.Pod)) {// Capture the notifier to be used for communicating updates to VKp.tracker = &PodsTracker{rm:       p.resourceManager,updateCb: notifierCb,handler:  p,}go p.tracker.StartTracking(ctx)
}

而在StartTracking()函数中会定时执行Pod的更新(updatePodsLoop)和删除(cleanupDanglingPods):

func (pt *PodsTracker) updatePodsLoop(ctx context.Context) {// 从资源管理器获取当前节点的Podk8sPods := pt.rm.GetPods()for _, pod := range k8sPods {updatedPod := pod.DeepCopy()ok := pt.processPodUpdates(ctx, updatedPod)if ok {pt.updateCb(updatedPod)}}
}// 处理Pod的更新,返回值表明Pod的状态是否更新
func (pt *PodsTracker) processPodUpdates(ctx context.Context, pod *v1.Pod) bool {// 调用ACI的接口获取Pod的状态podStatusFromProvider, err := pt.handler.FetchPodStatus(ctx, pod.Namespace, pod.Name)if err == nil && podStatusFromProvider != nil {// 如果获取状态没有出错,并且返回了状态,则将状态信息更新到PodpodStatusFromProvider.DeepCopyInto(&pod.Status)return true}if errdef.IsNotFound(err) || (err == nil && podStatusFromProvider == nil) {if pod.Status.Phase == v1.PodRunning {// 如果k8s中的状态是Running,但是ACI容器组不存在,则将k8s中容器的状态设置为Failed,此时会重建Podpod.Status.Phase = v1.PodFailedpod.Status.Reason = statusReasonNotFoundpod.Status.Message = statusMessageNotFoundnow := metav1.NewTime(time.Now())for i := range pod.Status.ContainerStatuses {if pod.Status.ContainerStatuses[i].State.Running == nil {continue}// 更新Pod的状态pod.Status.ContainerStatuses[i].State.Terminated = &v1.ContainerStateTerminated{ExitCode:    containerExitCodeNotFound,Reason:      statusReasonNotFound,Message:     statusMessageNotFound,FinishedAt:  now,StartedAt:   pod.Status.ContainerStatuses[i].State.Running.StartedAt,ContainerID: pod.Status.ContainerStatuses[i].ContainerID,}pod.Status.ContainerStatuses[i].State.Running = nil}return true}return false}if err != nil {log.G(ctx).WithError(err).Errorf("failed to retrieve pod %v status from provider", pod.Name)}return false
}// 删除Pod
func (pt *PodsTracker) cleanupDanglingPods(ctx context.Context) {// 获取k8s中的Pod和ACI容器组k8sPods := pt.rm.GetPods()activePods, err := pt.handler.ListActivePods(ctx)if err != nil {log.G(ctx).WithError(err).Errorf("failed to retrive active container groups list")return}if len(activePods) > 0 {for i := range activePods {// 遍历ACI容器组存活的Pod,如果k8s中没有对应的Pod,则删除ACI容器组pod := getPodFromList(k8sPods, activePods[i].namespace, activePods[i].name)if pod != nil {continue}err := pt.handler.CleanupPod(ctx, activePods[i].namespace, activePods[i].name)if err != nil && !errdef.IsNotFound(err) {log.G(ctx).WithError(err).Errorf("failed to cleanup pod %v", activePods[i].name)}}}
}
  • updatePodsLoop:处理k8s中有,ACI容器组不存在的情况
  • cleanupDanglingPods:处理ACI中有,k8s中不存在的情况

那么,virtual kubelet的整体结构如下:

vk架构

总结下上面的三条路径:

  • PodController通过informer机制监听Pod的变化,然后执行Pod的增删改查操作
  • virtual kubelet提供http接口,当用户执行kubectl logs/exec时,就调用对应的函数,然后会调用Provider接口中对应的函数,这里主要难点在于需要实时将数据回传,展示给用户
  • PodNotifier提供了异步更新Pod的接口,apiserver为了让etcd中Pod的数据与节点上Pod的数据保持一致,会定时调用节点的接口查询Pod的状态,当节点和Pod比较多时,比较消耗apiserver的资源。为了节省资源,节点会比较k8s中的Pod的数据和后端实际Pod的数据,如果发现有不一致(k8s中有该Pod,后端没有;k8s中没有该Pod,后端有),则执行状态的更新或者后端容器组的操作
5 关于logs和exec

Provider中剩下logs和exec则比较麻烦:

  • GetContainerLogs 读取日志,如果需要支持-f选项就比较麻烦
  • RunInContainer 执行命令,需要实时将命令的结果传送回来

azure-aci中的logs不支持-f选项,因此,只需要调用ACI容器组的接口,获取日志就行。而exec则需要使用websocket进行实时的命令传送和结果回传。

具体的数据流向如下:

数据流向

当用户在终端输入命令时,首先kubectl会先拿到命令,然后再交给kube-apiserver,kube-apiserver再将命令发送给kubelet,在这里就是virtual kubelet。那么,VK要做的就是读取命令,然后将命令发送给后端的容器组去执行,容器组执行完成后,再将结果推送给VK,VK则将结果推送给kube-apiserver,kube-apiserver将结果推送给kubectl,kubectl打印出来。这时候,用户看到的就类似于输入命令,然后输出结果。

而这些组件之间的数据流转需要实时推送,比较适合的方式就是使用websocket,这些组件之间通过websocket连接,连接之后,通过readmessage()和writemessage()进行数据传输。

从VK的角度看,需要做的就是:

  • 使用websocket连接后端的容器组
  • 从kube-apiserver读取数据,然后发送给后端的容器组
  • 从后端的容器组接收数据,并输出给kube-apiserver
// api.AttachIO是个接口,Stdin()和Stdout()分别返回标准输入和标准输出
// VK需要从Stdin()中读取数据,然后写给后端的容器组,
// 同时,需要接收容器组返回的数据,然后发送给Stdout()
func (p *ACIProvider) RunInContainer(ctx context.Context, namespace, name, container string, cmd []string, attach api.AttachIO) error {out := attach.Stdout()if out != nil {defer out.Close()}// 根据namespace和name获取容器组cg, err := p.getContainerGroup(ctx, namespace, name)if err != nil {return err}// 设置终端默认大小size := api.TermSize{Height: 60,Width:  120,}resize := attach.Resize()if resize != nil {select {case size = <-resize:case <-ctx.Done():return ctx.Err()}}// 获取ACI容器组的websocket的URI和密码ts := aci.TerminalSizeRequest{Height: int(size.Height), Width: int(size.Width)}xcrsp, err := p.aciClient.LaunchExec(p.resourceGroup, cg.Name, container, strings.Join(cmd, " "), ts)if err != nil {return err}wsURI := xcrsp.WebSocketURIpassword := xcrsp.Password// 连接ACI的websocket,并输入密码c, _, _ := websocket.DefaultDialer.Dial(wsURI, nil)if err := c.WriteMessage(websocket.TextMessage, []byte(password)); err != nil {panic(err)}defer c.Close()in := attach.Stdin()if in != nil {// 将读取命令并写入后端的容器组的逻辑放在后台的goroutinego func() {for {// 如果父协程结束,直接退出select {case <-ctx.Done():returndefault:}// 读取kube-apiserver发送的命令,然后发送给后端的容器组var msg = make([]byte, 512)n, err := in.Read(msg)if err != nil {// Handle errorsreturn}if n > 0 {if err := c.WriteMessage(websocket.BinaryMessage, msg[:n]); err != nil {panic(err)}}}}()}if out != nil {// 将接收容器组数据并写到kube-apiserver的逻辑放在前台任务for {// 如果父携程结束,则推出循环select {case <-ctx.Done():breakdefault:}// 从容器组读取数据,然后发送给kube-apiserver_, cr, err := c.NextReader()if err != nil {break}if _, err := io.Copy(out, cr); err != nil {panic(err)}}}return ctx.Err()
}

从上面的实现上看,VK的角色是kubelet,用于连接kube-apiserver和Pod,因此,如果需要实时通信,就需要用websocket分别连接两端,作为桥梁对两边的数据进行中转。

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

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

相关文章

CSS 浮动布局

浮动的设计初衷 float: left/right/both;浮动是网页布局最古老的方式。 浮动一开始并不是为了网页布局而设计&#xff0c;它的初衷是将一个元素拉到一侧&#xff0c;这样文档流就能够包围它。 常见的用途是文本环绕图片&#xff1a; 浮动元素会被移出正常文档流&#xff0c;…

《动手学深度学习 Pytorch版》 7.1 深度卷积神经网络(AlexNet)

7.1.1 学习表征 深度卷积神经网络的突破出现在2012年。突破可归因于以下两个关键因素&#xff1a; 缺少的成分&#xff1a;数据 数据集紧缺的情况在 2010 年前后兴起的大数据浪潮中得到改善。ImageNet 挑战赛中&#xff0c;ImageNet数据集由斯坦福大学教授李飞飞小组的研究人…

Golang 协程池 Ants 实现原理,附详细的图文说明和代码

Golang 协程池 Ants 实现原理&#xff0c;附详细的图文说明和代码。 1 前置知识点 1.1 sync.Locker sync.Locker 是 go 标准库 sync 下定义的锁接口&#xff1a; // A Locker represents an object that can be locked and unlocked. type Locker interface {Lock()Unlock() …

stm32之串口/蓝牙控制led灯

该文章记录学习stm32串口遇到的一些问题&#xff0c;完整代码地址。 一、项目描述 通过串口或蓝牙发送指令来控制led灯。 open ------> led 亮close ------> led 灭其它 -------> 反馈给串口或蓝牙错误指令 二、项目用到的模块 stm32 串口1,PA9(TX), PA10(RX)HC…

udp的简单整理

最近思考udp处理的一些细节&#xff0c;根据公开课&#xff0c;反复思考&#xff0c;终于有所理解&#xff0c;做整理备用。 0&#xff1a;简单汇总 1&#xff1a;udp是基于报文传输的&#xff0c;接收方收取数据时要一次性读完。 2&#xff1a;借助udp进行发包&#xff0c;…

JavaWeb-JavaScript

JavaWeb-JavaScript 什么是JavaScript Web标准 Web标准也称为网页标准&#xff0c;由一系列的标准组成&#xff0c;大部分由W3C ( World Wide Web Consortium&#xff0c;万维网联盟&#xff09;负责制定。三个组成部分&#xff1a; HTML&#xff1a;负责网页的结构&#xf…

32 随机链表的复制

随机链表的复制 题解1 哈希表题解2 回溯哈希哈希思路精简 题解3 优化迭代 给你一个长度为 n 的链表&#xff0c;每个节点包含一个额外增加的随机指针 random &#xff0c;该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点…

OR54 字符串中找出连续最长的数字串

目录 一、题目 二、解答 &#xff08;一&#xff09;问题一&#xff1a;在记录完一组连续字符串后&#xff0c;没有注意判别紧随其后的非数字字符 &#xff08;二&#xff09;问题二&#xff1a;越界访问 &#xff08;三&#xff09;正确 一、题目 字符串中找出连续最长的…

设计模式再探——原型模式

目录 一、背景介绍二、思路&方案三、过程1.原型模式简介2.原型模式的类图3.原型模式代码4.原型模式深度剖析5.原型模式与spring 四、总结五、升华 一、背景介绍 最近在做业务实现的时候&#xff0c;为了通过提升机器来降低开发人员的难度和要求&#xff0c;于是在架构设计…

数据标准化

1、均值方差标准化(Z-Score标准化) 计算过程&#xff1a; 对每个属性/每列分别进行一下操作&#xff0c;将数据按属性/按列减去其均值&#xff0c;并除以其方差&#xff0c;最终使每个属性/每列的所有数据都聚集在均值为0&#xff0c;方差为1附近。 公式&#xff1a;(x-mean(x…

电子信息工程专业课复习知识点总结:(五)通信原理

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 第一章通信系统概述——通信系统的构成、各部分性质、性能指标1.通信系统的组成&#xff1f;2.通信系统的分类&#xff1f;3.调制、解调是什么&#xff1f;有什么用…

【数据结构--排序】堆排序

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

在北京多有钱能称为富

背景 首先声明&#xff0c;此讨论仅限个人的观点&#xff0c;因为我本身不富嘛&#xff0c;所以想法应该非常局限。 举个栗子 富二代问我朋友&#xff0c;100~1000w之间&#xff0c;推荐一款车&#xff1f; 一开始听到这个问题的时候&#xff0c;有被唬住&#xff0c;觉得预…

XXE 漏洞及案例实战

文章目录 XXE 漏洞1. 基础概念1.1 XML基础概念1.2 XML与HTML的主要差异1.3 xml示例 2. 演示案例2.1 pikachu靶场XML2.1.1 文件读取2.1.2 内网探针或者攻击内网应用&#xff08;触发漏洞地址&#xff09;2.1.4 RCE2.1.5 引入外部实体DTD2.1.6 无回显读取文件 3. XXE 绕过3.1 dat…

Nitrux 3.0 正式发布并全面上市

导读乌里-埃雷拉&#xff08;Uri Herrera&#xff09;近日宣布 Nitrux 3.0 正式发布并全面上市&#xff0c;它是基于 Debian、无 systemd、不可变的 GNU/Linux 发行版的最新安装媒体&#xff0c;利用了 KDE 软件。 Nitrux 3.0 由带有 Liquorix 味道的 Linux 6.4.12 内核提供支持…

QT-day4

画一个时钟 widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QPaintEvent> #include <QDebug> #include <QPainter> #include <QTimer> #include <QTime>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } Q…

记一次逆向某医院挂号软件的经历

背景 最近家里娃需要挂专家号的儿保&#xff0c;奈何专家号实在过于抢手&#xff0c;身为程序员的我也没有其他的社会资源渠道可以去弄个号&#xff0c;只能发挥自己的技术力量来解决这个问题了。 出师不利 首先把应用安装到我已经 Root 过的 Pixel 3 上面&#xff0c;点击应…

关于Pandas数据分析

pandas的数据加载与预处理 数据清洗&#xff1a;洗掉脏数据 整理分析&#xff1a;字不如表 数据展现&#xff1a;表不如图 环境搭建 pythonjupyter anaconda Jupyter Notebook Jupyter Notebook可以在网页页面中直接编写代码和运行代码, 代码的运行结果也会直接在代码块下显示…

【 Ubuntu】systemd服务创建、启用、状态查询、自启等

要在 Ubuntu 启动后执行一个守护脚本&#xff0c;您可以使用 Shell 脚本编写一个 systemd 服务单元。systemd 是 Ubuntu 中常用的服务管理工具&#xff0c;可以在系统启动时自动启动和管理服务。 下面是一个示例的守护脚本和 systemd 服务单元的步骤&#xff1a; 创建守护脚本…

Spring之依赖注入源码解析

基于Autowired的依赖注入底层原理 基于Resource注解底层工作流程图&#xff1a; 1 Spring中到底有几种依赖注入的方式&#xff1f; 首先分两种&#xff1a; 手动注入 自动注入 1.1 手动注入 在XML中定义Bean时&#xff0c;就是手动注入&#xff0c;因为是程序员手动给某…