一种pod容器动态挂卷方案

一、背景

1.1 个人调试kvm

我们这边基于云平台的k8s+kubevirt,给安卓手机领域的开发工程师们提供了独占式虚拟机资源。这些资源主要用于工程师的个人级开发与调试,因此有如下特点:

  • 使用时间与工作时间强相关,即工程师工作时间使用该资源多,晚上与周末基本不使用;
  • 写代码时对cpu、内存需求不多,整机/多组件编译时可能把分配的虚拟机资源用满,但整机/多组件编译场景次数不多,大部分是不需要这么多资源的模块编译;
  • kubevirt虚拟机平台对资源做了虚拟化与资源隔离,其中cpu配置了超分,内存未超分(底层暂不支持内存超分);
  • 一台物理机上一般会有四五个虚拟机,也就是对应四五个用户,这些用户同时做整机/多组件编译的概率很低。

基于这些特点和相关资源监控数据,我们发现该场景下整个k8s集群节点的资源使用率都很低,于是我们考虑用pod容器来代替虚拟机。之所以考虑使用pod容器主要有两个原因:

  • pod容器支持内存超分,我们可以配置更高的内存超分比,使得一台物理机上可以分配更多的实例给到工程师使用,从而减小资源的分母,达到提升资源使用率的效果;
  • pod容器的性能损耗相比于虚拟机虚拟化的损耗理论上要小,我们实际测试编译耗时数据也证明如此,pod容器在不少项目中都有10+%的编译性能提升。

1.2 kvm切pod容器的挑战

在kvm切换到pod容器过程中,我们的原则是尽可能地减少工程师们对底层资源变化的感知,当然这也包括尽可能地保持工程师们对kvm资源的使用习惯。

于是我们就遇到了一个大挑战:kubevirt的kvm支持动态挂卷,而原生pod容器在挂卷后,对应的容器会发生重建操作,这个重建操作会有如下问题:

  • 容器系统盘数据丢失(后面发现云平台有支持保留系统盘功能);
  • 中断用户的ssh/samba/vscode连接;
  • 有一个磁盘在编译的时候再去挂载/卸载一个盘,会导致编译中的任务终止。

上述变化会严重影响工程师的使用习惯,为了解决这个问题,我们需要重新梳理下工程师kvm的卷:

  • 数据卷:可读写,用户独享。一般大小为2T,作为用户的home目录盘,生命周期与实例生命周期一致;
  • mirror/opt/pkg卷:只读方式挂载,多个用户可以共用一份数据。对应代码和工具等内容,其中mirror中的代码主要是用作增量下载,减少整个pipeline拉取代码的时间。这些卷在实例创建时便会挂载上去,后面会不定时更新;
  • 代码卷:读写方式挂载,用户独享。代码卷中主要是一些.so等中间编译产物,用于减少编译时长。实例启动时不挂载,工程师需要时选择自己对应项目的代码卷挂载,开发完成后则会卸载对应的代码卷;
  • 其它场景:例如允许实例挂一个卷后,作为jenkins slave节点跑构建任务。

再考虑到当前kvm对应的卷容量已经达到PB级别,如果pod容器底层再搭一套独立的存储,不管是从资源层面还是整套流程层面(例如代码卷的制作与维护)都是难以接受的。因此,在kvm切pod容器的挂卷层面我们归纳为两点诉求:

  • 1. pod容器能共用kvm存储卷(共用pvc或者底层存储卷)
  • 2. pod业务容器支持动态挂卷

二、k8s pod挂卷

2.1 pod挂卷

pod是k8s的最小调度单位,一个pod可以包含多个容器(container),给pod中的容器挂卷(本文仅讨论pvc)要考虑两个问题:挂哪个卷以及挂载到容器的哪个目录。pod把这两个点声明成了两个字段:volumevolumeMount

apiVersion: v1
kind: Pod
metadata:test
spec:containers:- name: workervolumeMounts:- name: datamountPath: /work/data- name: code1mountPath: /work/code1- name: code2mountPath: /work/code2...volumes:- name: datapersistentVolumeClaim:claimName: pvc-data- name: code1persistentVolumeClaim:claimName: pvc-code1- name: code2persistentVolumeClaim:claimName: pvc-code2...

如上所示,定义了一个只有一个worker容器的pod,这个容器挂载了三个pvc。 当这个pod Running后,如果编辑pod再挂载一个pvc到worker容器,则会触发worker容器的重建 ,业务层面也就是前面提到的中断连接,影响用户体验。

2.2 CSI

k8s把pvc相关操作(例如建卷、挂载)封装成了CSI(Container Storage Interface),一般一个类型的卷对应一个CSI,而一个完整功能的CSI一般由两部分组成:

  • controller :一般以deployment/statefulSet形式部署,主要负责dynamic provisioning、快照等功能;
  • driver :也有地方叫作node,以daemonSet形式部署,主要负责一个必须在实例宿主机上的操作,例如attach、mount。

在这里插入图片描述

2.3 CSI controller

CSI controller主要负责一些在任意节点都可以完成的操作,我们以创建pvc后的dynamic provisioning流程,看看CSI controller的工作原理(假设底层存储是ceph RBD):

在这里插入图片描述

  1. 用户调用k8s API server创建pvc;
  2. k8s保存pvc对象;
  3. CSI controller监听到创建pvc事件,且发现pvc的storageClass是自己;
  4. CSI controller调底层ceph接口创建ceph卷;
  5. CSI controller根据创建的ceph卷信息创建对应的pv;
  6. CSI controller绑定pvc和pv。

2.4 CSI driver

假设要手动把一个ceph RBD设备挂载到某个物理机上,一般会有如下动作:

  1. 使用rbd或者rbd-nbd命令把RBD设备映射到物理机上,假设映射后的设备名称为/dev/nbd0;
  2. 如果是个新盘,会用mkfs.ext4等命令对/dev/nbd0做些格式化操作;
  3. 使用mount命令把设备挂载到目标目录,例如:mount /dev/nbd0 /targetPath。

CSI在给pod挂卷时,其实也是类似的操作,但是考虑到一个卷可能会被宿主机上多个容器使用,CSI还会额外封装一层给容器使用:

  1. 使用rbd或者rbd-nbd命令把RBD设备映射到物理机上,假设映射后的设备名称为/dev/nbd0;
  2. 如果是个新盘,会用mkfs.ext4等命令对/dev/nbd0做些格式化操作;
  3. 使用mount命令把设备挂载到目标目录,例如:mount /dev/nbd0 /stagingPath。其中stagingPath是一个带有pv名称的路径,一个stagingPath对应一个pv,也就是对应一个底层ceph卷;
  4. 再执行mount --bind /stagingPath /targetPath将stagingPath绑定到targetPath。其中targetPath是一个带有pod uid和pv的路径,后续kubelet会把targetPath作为创建容器参数重建对应需要挂卷的容器。

上述过程就是CSI driver相关功能,并且封装到了两个grpc接口中:

  • NodeStageVolume:负责映射、格式化并mount到stagingPath;
  • NodePublishVolume:负责stagingPath与targetPath的绑定

于是我们来看看给一个运行的pod容器挂卷是什么逻辑:

在这里插入图片描述

  1. 修改pod volume与pod下容器volumeMount字段;
  2. k8s API server保存对pod对象的修改;
  3. pod宿主机上的kubelet监听到pod挂卷信息变化;
  4. kubelet调CSI driver的NodeStageVolume方法;
  5. CSI driver的NodeStageVolume方法中映射ceph设备到宿主机上,假设为/dev/nbd0;
  6. CSI driver的NodeStageVolume方法中检查设备如果需要格式化会先进行格式化,最后再挂载到stagingPath;
  7. kubelet调CSI driver的NodePublishVolume方法;
  8. CSI driver的NodePublishVolume方法中执行mount --bind将stagingPath和targetPath绑定;
  9. kubelet配置targetPath重建容器。

2.5 云平台CSI

在云平台提供的k8s集群环境中,因需要同时支持kubevirt kvm虚拟机和常规pod容器,为了更简单管理,我们把kvm宿主机和pod容器宿主机打了不同标签做了区分。同时因为kubvirt kvm挂卷和pod容器挂卷的实现逻辑不同,于是整个集群的CSI部署如下:

在这里插入图片描述

注:

  1. 底层用的是ceph RBD块存储;
  2. 云平台提供的pod CSI中,映射ceph卷使用的是rbd-nbd

三、pod业务容器动态挂卷

3.1 如何让业务容器动态挂卷不重建

前面说到,如果给一个pod的容器挂卷,这个容器会重建。但是,如果一个pod有两个容器,当给其中一个容器挂卷时,只有被挂卷的容器会重建,未挂卷的容器不受影响。于是考虑,如果给业务容器的pod中再加一个辅助的sidecar容器,当业务容器需要挂卷时,可以把挂卷信息写到sidecar容器中,从而触发kubelet对CSI的调用,然后通过某种手段把这个卷动态挂到业务容器中,这样就实现了worker业务容器的动态挂卷功能。

在这里插入图片描述

但是,需要怎样才能将宿主机上的卷挂载到正在运行的业务容器中呢?又是什么阶段去做这个事情比较合理呢?

3.2 容器与linux namespace

要解决如何给运行中的容器动态挂卷问题,还需要进一步理解容器。容器的本质是一个配置了linux namespace资源隔离与cgroups资源限制的特殊进程,而linux namespace资源隔离有:

  • Mount namespace:隔离文件系统挂载点
  • UTS namespace:隔离主机名和域名
  • IPC namespace:隔离进程间通信资源
  • Pid namespace:隔离进程id空间
  • Network namespace:隔离网络设备、网络接口、ip地址、路由表等
  • User namespace:隔离用户和用户组

在这里,我们需要的显然是进入容器进程的mount namespace执行mount操作。而nsenter就是一个很好进入进程linux namespace的工具,我们可以使用nsenter -t {业务容器Pid} -m进入容器mount namespace再执行mount操作把宿主机上的ceph设备挂载到业务容器中。

再结合前面的CSI driver的NodeStageVolume与NodePublishVolume逻辑,可以发现在NodePublishVolume执行完mount --bind后再用nsenter进入业务容器执行mount操作是更合适的。

3.3 业务容器动态挂卷

有了上面的分析,pod业务容器动态挂卷就有如下逻辑:

在这里插入图片描述

  1. 调k8s API server接口修改pod volume与sidecar容器下的volumeMount;
  2. k8s保存对pod对象的修改;
  3. pod实例宿主机上的kubelet监听到pod挂卷信息变化;
  4. kubelet调CSI driver的NodeStageVolume方法;
  5. CSI driver的NodeStageVolume方法中映射ceph设备到宿主机上,假设为/dev/nbd0;
  6. CSI driver的NodeStageVolume方法中检查设备如果需要格式化会先进行格式化,最后再挂载到stagingPath;
  7. kubelet调CSI driver的NodePublishVolume方法;
  8. CSI driver的NodePublishVolume方法中执行mount --bind将stagingPath和targetPath绑定;
  9. CSI driver的NodePublishVolume方法中根据targetPath中的pod uid找到对应的pod信息,并从pod.status中进一步找到业务容器的容器id。之后通过docker inspect {业务容器id}找到业务容器进程pid,最后再使用nsenter -t {业务容器进程pid} -m mount /dev/nbdx /targetPath进入业务容器进程的mount namespace执行mount操作
  10. kubelet配置targetPath重建sidecar容器。

细心的读者可能会发现,通过上述挂卷过程后,一个卷既挂载到了worker容器中,也挂载到了sidecar容器中,这里之所以还要保留继续挂载到sidecar容器中的逻辑,是考虑到卸载卷的时候kubelet会做一些判断,如果发现sidecar容器未挂卷(挂卷时修改的是sidecar的volumeMount),不会调用CSI的NodePublishVolume方法,这样会导致worker容器中的卷不会卸载。

3.4 新方案CSI部署

上述方案解决了原始需求中的pod业务容器支持动态挂卷问题,但没解决pod容器能共用kvm存储卷问题。其实要解决pod容器能共用kvm存储卷问题很简单,只需要把pod CSI driver的名称和kvm CSI controller/driver的名称保持一致即可,kubelet挂卷时是根据名称来找CSI driver的,于是集群中CSI的部署变成了如下所示:

在这里插入图片描述

四、遇到的问题

4.1 创建pod时挂载动态卷

3.3章节说的是已经pod已经running后给业务容器挂卷场景,但是如果创建pod时就在sidecar容器的volumeMount中给业务容器配置动态挂卷信息,按照上述逻辑是走不下去的。

因为3.3章节中有很重要的一步就是CSI driver中的NodePublishVolume方法中找到业务容器的进程pid,而kubelet在创建pod时是这样的顺序:

  1. kubelet调CSI driver的NodeStageVolume方法;
  2. kubelet调CSI driver的NodePublishVolume方法;
  3. kubelet把宿主机上准备好的卷目录配置给容器并创建容器。

很显然在kubelet调CSI driver的NodePublishVolume方法时,此时业务容器都还没创建,因此并不能找到容器进程pid。

为了解决这种场景,在CSI driver的NodePublishVolume中找业务容器进程id时,如果找不到先给pod打个标记,再返回成功,让kubelet创建pod逻辑可以继续走下去。之后再用一个异步逻辑处理这种打了标记的pod,发现业务容器起来能找到容器进程pid后再做nsenter mount操作。于是kubelet创建pod逻辑为:

在这里插入图片描述

  1. kubelet调CSI driver的NodeStageVolume方法;
  2. CSI driver的NodeStageVolume方法中映射ceph设备到宿主机上,假设为/dev/nbd0;
  3. CSI driver的NodeStageVolume方法中检查设备如果需要格式化会先进行格式化,最后再挂载到stagingPath;
  4. kubelet调CSI driver的NodePublishVolume方法;
  5. CSI driver的NodePublishVolume方法中执行mount --bind将stagingPath和targetPath绑定;
  6. CSI driver的NodePublishVolume方法中发现还找不到容器进程pid,先调用k8s API server接口把pod标记为延迟挂载,并记录挂卷信息
  7. k8s更新pod
  8. kubelet创建worker容器;
  9. kubelet配置targetPath创建sidecar容器;
  10. CSI driver中的异步逻辑监听到本宿主机上的pod有延时挂载标记
  11. CSI driver中的异步逻辑尝试去找对应业务容器的进程pid,如果能找到,说明业务容器已启动,于是执行nsenter进入业务容器进程的mount namespace进程挂卷操作

宿主机重启场景kubelet行为与本场景一致,不再赘述。

4.2 业务容器重启

在给业务容器做动态挂卷的时候,我们是通过nsenter进入业务容器mount namespace执行mount操作挂载的,而容器的本质是一个特殊进程,因此这些挂卷信息会保留在/proc/{pid}/mountinfo中。业务容器因OOM等原因重建后其实是新起了另外一个进程,pid发生了变化,对应的挂卷信息也会丢失。

为了解决这个问题,CSI driver需要监听pod业务容器的重起事件。好在pod.status中对应容器有restartCount或者startedAt字段可以判断业务容器是否发生重启,但是在实现上用informer去watch pod事件可能并不是一个最优解,因为CSI driver是以daemonSet形式部署的,如果集群中节点太多(我们这边单集群有几千个节点),每个节点上都做list-watch,这可能会增加API server的压力。

此时可以考虑定期调kubelet的/pods接口查询本节点上的pod列表,通过对比前后两次查询结果来判断是否业务容器是否发生重启,于是业务容器重启场景有如下逻辑:

在这里插入图片描述

  1. kubelet发现业务容器重启;
  2. kubelet调k8s API server接口更新重启后的业务容器信息;
  3. k8s保存pod的更新,同时kubelet内存中也会通过informer拿到更新后的pod信息;
  4. CSI driver定期调用kubelet /pods接口查询本宿主机上pod列表信息,并根据前后两次查询的结果判断业务容器是否重启;
  5. CSI driver发现业务容器重启,找到新业务容器进程pid,重新执行nsenter进入业务容器做mount操作。

4.3 CSI driver重启

CSI driver在映射一个ceph卷到宿主机时使用的是rbd-nbd map命令,这个命令会在CSI driver容器中起一个用户态的进程来维护与存储卷的连接。当CSI driver重启后这些进程也就没了,但是pod容器中/proc/{pid}/mountinfo信息仍然存在,也就是此时用户仍能看到挂卷信息,但是访问卷内容时,在一些低版本内核中会出现卡死情况(此时必须重启宿主机才能恢复),高版本内核则会返回IO错误。

CSI driver的重启我们认为是避免不了的,例如版本更新、代码bug导致OOM、运维误操作等都有可能导致CSI driver的重启。面对这种场景,我们的选择是:

  1. 宿主机使用高版本内核,防止出现卡死而重启宿主机,同时业务层面对高版本内核做好测试;
  2. CSI driver启动时,把宿主机上已挂卷的pod做一个remount操作(卸载+挂载),这样可以清理掉旧容器中的残留挂卷信息,并让容器重新挂卷,代价就是业务容器会重建。相比于低版本内核的重启宿主机,以及结合业务的重要程度考虑,运维窗口期变更CSI driver让业务容器重建是可以接受的。

4.4 rbd-nbd默认最大设备数

rbd-nbd默认最大设备数是16,在CSI中也就意味着一个宿主机最多只能挂载16个不同的pvc/pv,这个数值不满足我们的业务要求。可以通过修改nbd内核模块的参数重新加载内核模块,并通过/sys/module/nbd/parameters/nbds_max查看是否修改成功。

4.5 rbd-nbd map并发挂卷出现D进程

在高版本内核(5.14.0,有修改)中给一个pod配置多个卷,执行创建+删除+再创建,必现rbd-nbd map进程变为D进程卡死问题。内核同事帮忙分析,发现rbd-nbd进程执行NBD_SET_SOCK操作时,有IO派发超时导致卡住,怀疑是多个线程操作同一个nbd设备导致。

梳理CSI driver整体逻辑,并查看rbd-nbd源码,发现的确可能并发操作同一nbd设备。修改rbd-nbd源码不太现实,于是在CSI driver中执行rbd-nbd map操作时加了个全局锁,问题得到解决。低版本内核未出现过这个问题,但两个版本内核逻辑差异太大,待进一步分析高版本内核为什么会出现这个问题。

在这里插入图片描述

4.6 同一设备多个挂载点时df -h显示非预期值

在用nsenter给业务容器挂卷时,其实也是mount了两次,之所以mount两次,是考虑到一个卷同时挂载到worker容器和sidecar容器,并且有pvc的accessMode与pod volumeMount下readOnly两层访问权限配置:

nsenter -t {pid} -m -- mount /dev/nbdx /mnt/{pvName}
nsenter -t {pid} -m -- mount --bind /mnt/{pvName} {targetPath} 

挂卷后再业务容器中使用df -h命令,正常看到的是/dev/nbdx -> targetPath的映射,但是测试时发现某些卷显示的是/dev/nbdx -> /mnt/{pvName}。

经过验证发现,当targetPath路径长于/mnt/{pvName}时,显示的是/mnt/{pvName},短于/mnt/{pvName}时显示的是targetPath,也就是当一个设备有多个挂载点时,df -h只显示路径最短的那一条。

当前df的版本是8.25,在df 8.22版本中测试发现显示的都是targetPath,对比df 8.25与8.22版本源码,发现的确加了长度判断逻辑。相关代码:https://github.com/coreutils/coreutils/blob/68f73f23866d6137e9c8d88d86073b33588d7b56/src/df.c#L644

五、总结

本文主要介绍了一种pod业务容器动态挂卷的方案,文中只介绍了挂卷方面的逻辑,卸载卷的逻辑是挂卷逻辑的逆过程,本文没有详述。

文中内容都在讲pod业务容器的动态挂卷方案,其实对于用户的home数据卷(参考1.2章节内容),因其生命周期和实例生命周期一致而无需动态挂/卸载,在实际中我们是创建的时候直接挂载到业务容器中的,也就是修改的是worker容器的volumeMount,挂卷实现上与前文的动态挂卷逻辑并不冲突。

合适的才是最好的,如果业务上接受挂/卸卷后业务容器重建,也就不需要本文的内容,本文仅提供一种动态挂卷方案供大家参考。

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

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

相关文章

Docker Compose 构建 EMQX 集群 实现mqqt 和websocket

EMQX 集群化管理mqqt真香 目录 #目录 /usr/emqx 容器构建 vim docker-compose.yml version: 3services:emqx1:image: emqx:5.8.3container_name: emqx1environment:- "EMQX_NODE_NAMEemqxnode1.emqx.io"- "EMQX_CLUSTER__DISCOVERY_STRATEGYstatic"- …

如何在没有 iCloud 的情况下将数据从 iPhone 传输到 iPhone

概括 您可能会遇到将数据从 iPhone 转移到 iPhone 的情况,尤其是当您获得新的 iPhone 15/14 时,您会很兴奋并希望将数据转移到它。 使用iCloud最终可以做到这一点,但它的缺点也不容忽视,阻碍了你选择它。例如,您需要…

CUDA与Microsoft Visual Studio不兼容问题

简介:在安装一些 python库时,涉及到第三方库(特别是需要引用 C 代码)时,通常的安装方式会涉及到编译过程,通常称为"源代码安装"(source installation),或是 “…

Unity2D无限地图的实现(简单好抄)

说明:本教程实现的是在2D游戏中玩家在游戏中上下左右移动的时候自动进行地图拼接的功能,如果你只想实现左右移动的无限地图,那么这篇博客也能起到一定参考作用。 思路 第一步: 创建一个10*10的2D游戏对象当做地图 第二步创建一个…

C13.【C++ Cont】初识string类字符串的迭代器

目录 1.迭代器的定义 2.迭代器的作用 3.string类字符串的常用迭代器 4.第3点的两个迭代器的使用 示例代码1:解引用 运行结果 示例代码2 运行结果 示例代码3:用迭代器正序遍历字符串 运行结果 示例代码4:用迭代器逆序遍历字符串 运行结果 示例代码5:用迭代器修改字…

HTML——13.超链接

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>超链接</title></head><body><!--超链接:从一个网页链接到另一个网页--><!--语法&#xff1a;<a href"淘宝网链接的地址"> 淘宝…

《代码随想录》Day22打卡!

回溯算法 《代码随想录》回溯算法&#xff1a;组合 本题完整题目如下&#xff1a; 本题的完整思路如下&#xff1a; 1.本题使用回溯算法&#xff0c;其实回溯和递归是一样的道理&#xff0c;也是分为三步曲进行&#xff1a; 2.第一步&#xff1a;确定递归函数的返回值和参数&…

鱼眼相机模型与去畸变实现

1.坐标系说明 鱼眼相机模型涉及到世界坐标系、相机坐标系、图像坐标系、像素坐标系之间的转换关系。对于分析鱼眼相机模型&#xff0c;假定世界坐标系下的坐标点,经过外参矩阵的变换转到相机坐标系&#xff0c;相机坐标再经过内参转换到像素坐标&#xff0c;具体如下 进一步进…

Windows 下安装 triton 教程

目录 背景解决方法方法一&#xff1a;&#xff08;治标不治本&#xff09;方法二&#xff1a;&#xff08;triton-windows&#xff09;- 安装 MSVC 和 Windows SDK- vcredist 安装- whl 安装- 验证 背景 triton 目前官方只有Linux 版本&#xff0c;若未安装&#xff0c;则会出…

如何使用网络工具进行网络性能评估

网络评估是对IT基础设施的系统评估&#xff0c;以确保它能够很好地满足企业的核心运营需求&#xff0c;确定了基础设施中需要改进的领域&#xff0c;并定义了改进的范围。 网络评估工具分析IT基础设施的各个方面&#xff0c;它通过评估网络设备、网络性能和安全威胁来仔细检查…

Vue项目整合与优化

前几篇文章&#xff0c;我们讲述了 Vue 项目构建的整体流程&#xff0c;从无到有的实现了单页和多页应用的功能配置&#xff0c;但在实现的过程中不乏一些可以整合的功能点及可行性的优化方案&#xff0c;就像大楼造完需要进行最后的项目验收改进一样&#xff0c;有待我们进一步…

Python、R用深度学习神经网络组合预测优化能源消费总量时间序列预测及ARIMA、xgboost对比...

全文链接&#xff1a;https://tecdat.cn/?p38726 分析师&#xff1a;Qingxia Wang 在能源领域&#xff0c;精准预测能源消费总量对制定合理能源战略至关重要。当前&#xff0c;能源消费预测分析主要运用单一模型&#xff08;如灰色预测法、时间序列分析法等&#xff09;和组合…

STM32使用UART发送字符串与printf输出重定向

首先我们先看STM32F103C8T6的电路图 由图可知&#xff0c;其PA9和PA10引脚分别为UART的TX和RX(注意&#xff1a;这个电路图是错误的&#xff0c;应该是PA9是X而PA9是RX&#xff0c;我们看下图的官方文件可以看出)&#xff0c;那么接下来我们应该找到该引脚的定义是什么&#xf…

Kotlin在医疗大健康域的应用实例探究与编程剖析(下)

四、Kotlin医疗编程实例分析 4.1 移动医疗应用实例 4.1.1 患者健康监测应用 在当今数字化医疗时代,患者健康监测应用为人们提供了便捷的健康管理方式。利用Kotlin开发的患者健康监测应用,能够实时采集患者的各类生理数据,如心率、血压、血氧饱和度等,并通过直观的可视化…

Redis 5设计与源码分析读书笔记

目录 引言Redis 5.0的新特性Redis源码概述Redis安装与调试 简单动态字符串数据结构基本操作创建字符串释放字符串拼接字符串扩容策略 其余API 本章小结兼容C语言字符串、保证二进制安全sdshdr5的特殊之处是什么SDS是如何扩容的 跳跃表简介跳跃表节点与结构跳跃表节点跳跃表结构…

Golang学习历程【第五篇 复合数据类型:数组切片】

Golang学习历程【第五篇 复合数据类型&#xff1a;数组&切片】 1. 数组&#xff08;Array&#xff09;1.1 数组的定义1.2 初始化数组1.3 数据的循环遍历1.4 多维数组 2. 切片&#xff08;Slice&#xff09;2.1 切片声明、初始化2.2 基于数组创建切片2.2 切片的长度(len)和容…

【Unity】 HTFramework框架(五十七)通过Tag、Layer批量搜索物体

更新日期&#xff1a;2024年12月30日。 Github源码&#xff1a;[点我获取源码] Gitee源码&#xff1a;[点我获取源码] 索引 问题再现通过Tag搜索物体&#xff08;SearchByTag&#xff09;打开SearchByTag窗口搜索标记指定Tag的所有物体批量修改Tag搜索Undefined状态的所有物体 …

基于feapder爬虫与flask前后端框架的天气数据可视化大屏

# 最近又到期末了&#xff0c;有需要的同学可以借鉴。 一、feapder爬虫 feapder是国产开发的新型爬虫框架&#xff0c;具有轻量且数据库操作方便、异常提醒等优秀特性。本次设计看来利用feapder进行爬虫操作&#xff0c;可以加快爬虫的速率&#xff0c;并且简化数据入库等操作…

PCL点云库入门——PCL库点云滤波算法之统计滤波(StatisticalOutlierRemoval)

1、算法原理 统计滤波算法是一种利用统计学原理对点云数据进行处理的方法。它主要通过计算点云中每个点的统计特性&#xff0c;如均值、方差等&#xff0c;来决定是否保留该点。算法首先会设定一个统计阈值&#xff0c;然后对点云中的每个点进行分析。如果一个点的统计特性与周…

CentOS7 解决ping:www.baidu.com 未知的名称或服务

CentOS7 解决ping&#xff1a;www.baidu.com“未知的名称或服务 在VM查看网络配置 查看虚拟网络编辑器 编辑网络配置文件 vi /etc/sysconfig/network-scripts/ifcfg-ens33注意&#xff1a;不同机器的配置文件名可能不相同&#xff0c;通过 ip addr 命令查看 将 ONBOOT 从 no 改…