multipath 内核接口及框架介绍

文章目录

      • 1 云主机使用网络存储 io 流程
      • 2 multipath 介绍

1 云主机使用网络存储 io 流程

对于一个云服务环境,大致会有网络节点,存储节点,计算节点,控制节点,其中虚拟云主机在计算节点工作,而虚拟云主机(qemu 虚机)使用的存储一般通过 ceph,drbd,mulitpch,iscsi 使其在存储节点存储。

(1)计算节点云主机访问本地 /dev/vda,/dev/vdb … 块设备,实际访问的是 dm-mulitpath 管理的计算节点的 /dev/dm-0…1,2,3 设备。

/dev/vda-> virtio_blk-> virtio_pci-> 虚拟主机(qemu vhost)->  本地节点 tcm_vhost-> target_core_mod-> target_core_iblock/target_core_pscsi-> 计算节点 /dev/dm-0

(2)/dev/dm-0 实际由 iscsi 客户端管理的 /dev/sda 和 /dev/sdb 聚合而成,可实现故障路径切换,io 负载均衡等能力,实际 io 数据会通过 iscsi_tcp(如果是 fc 服务,则是走光纤通道)到达存储节点的 /dev/drbd0。如果主路径故障,通路不同则会由 multipath 切换到备用路径(这里是/dev/sdb),等待恢复后切换回主路径(回到/dev/sda)。这里假设走的主路径:

/dev/dm-0-> /dev/sda-> iscsi_tcp/fc-> 到达存储节点 iscsi_target_mod/tcm_fc-> target_core_mod-> target_core_iblock/target_core_pscsi-> /dev/drbd0 (drbd 模块)

(3)存储节点的/dev/drbd0由 drbd 内核模块配置,实现块数据拷贝同步到后端存储设备。后端存储设备为/dev/alcache0,该设备实际是一块高速 SSD 设备(如 nvme),实现数据的高速缓存。
(4)/dev/alcache0由 bcache 配置管理,实际对应后端存储为/dev/rbd0。bcache 实现将 nvme 作为高速缓存将数据同步到 /dev/rbd0 中。
(5)/dev/rbd0 设备由 ceph rbd 管理,该模块提供了分布式存储管理能力,如 io 调度等,通过 rbd,数据 io 会实际到达 HHD 等大容量存储设备。

/dev/rbd0-> rbd 模块(ceph 网络协议转发数据)-> ceph rbd 管理(分布式存储管理实际物理磁盘)

2 multipath 介绍

可以看到上述路径中在计算节点使用了 mutlipath 聚合路径来实现负载均衡和路径切换等。

用户态工具相关:
启动脚本

/usr/lib/systemd/system/multipathd.service

udev 命令规则

/usr/lib/udev/rules.d/11-dm-mpath.rules
/usr/lib/udev/rules.d/62-multipath.rules

用户工具

/usr/sbin/multipathd        守护进程,监听系统中路径状态的变化,并做相应的处理。
/usr/sbin/mpathpersist      SCSI PR命令工具,主要用于隔离。
/usr/sbin/mpathconf         修改多路径配置
/usr/sbin/kpartx            DeviceMapper虚拟设备创建工具
/usr/sbin/multipath         多路径命令工具

查看 device mapper 映射表

[wings-client@fedora sections]$ sudo dmsetup table
mpatha: 0 41940992 multipath 1 queue_if_no_path 1 alua 1 1 service-time 0 2 2 8:0 1 1 8:16 1 1[wings-client@fedora sections]$ ll /dev/sd*
brw-rw----. 1 root disk 8,  0 101615:29 /dev/sda
brw-rw----. 1 root disk 8, 16 101615:29 /dev/sdb

查看被映射的多路径设备

[wings-client@fedora sections]$ ll /dev/dm-*
brw-rw----. 1 root disk 253, 0 101615:29 /dev/dm-0
[wings-client@fedora sections]$ 

多路径创建的 dm 设备的主设备号都是 253。
查看设备的 wwid

[wings-client@fedora sections]$ lsscsi -i
[0:0:0:0]    cd/dvd  QEMU     QEMU DVD-ROM     2.5+  /dev/sr0   -
[6:0:0:0]    disk    LIO-ORG  block1           4.0   /dev/sdb   3600140554012e51273044299221f2903
[7:0:0:0]    disk    LIO-ORG  block1           4.0   /dev/sda   3600140554012e51273044299221f2903
[wings-client@fedora sections]$

同一个设备的两条路径,以及被映射的新设备的 wwid 相同
查看当前多路径信息

[wings-client@fedora sections]$ sudo multipath -ll
mpatha (3600140554012e51273044299221f2903) dm-0 LIO-ORG,block1
size=20G features='1 queue_if_no_path' hwhandler='1 alua' wp=rw
`-+- policy='service-time 0' prio=50 status=active|- 7:0:0:0 sda 8:0  active ready running`- 6:0:0:0 sdb 8:16 active ready running
[wings-client@fedora sections]$ 

多路径软件读取/etc/multipath.conf文件,并根据 wwid 将相同设备聚合成一个虚拟设备。

内核中 dm-multipath 源码实现主要在linux/drivers/md/dm-*中。
dm-multipath 模块属于 md 模块的一个后端部分。
md 是多个设备的内核 RAID 实现,其中 dm 是设备映射器实现,允许将一个设备映射到另一个设备(一个或者多个)上,这可用作创建在/dev/mapper 目录中访问的虚拟设备(映射设备)。该设备的所有 IO 都将映射到其他设备。

RAID 控制器
RAID控制器是一种硬件设备或软件程序,用于管理计算机或存储阵列中的硬盘驱动器(HDD)/固态硬盘(SSD),以便它们能如逻辑部件一样工作,各施其职。RAID控制器的功能既可以由硬件也可以由软件来实现。硬件RAID一般用于处理大量数据的RAID模式。随着处理器的能力的不断增强,软件RAID功能已经成为可能,不过当处理大量数据时CPU仍然会显得力不从心。一般的中高档服务器多使用硬件RAID控制器,但是由于硬件RAID控制器的价格昂贵,导致系统成本大大增加。而随着处理器的性能快速发展,使得软件RAID的解决方法得到人们的重视。在这里,我们使用的是软件RAID。

在Linux系统中目前以MD (Multiple Devices)虚拟块设备的方式实现软件RAID,利用多个底层的块设备虚拟出一个新的虚拟块设备,并且利用条带化(stripping)技术将数据块均匀分布到多个磁盘上来提高虚拟设备的读写性能,利用不同的数据冗余算法来保护用户数据不会因为某个块设备的故障而完全丢失,而且还能在设备被替换后将丢失的数据恢复到新的设备上。

MD (Multiple Devices)框架
Multiple Devices虚拟块设备(利用底层多个块设备虚拟出一个新的虚拟块设备)。目前MD支持linear, multipath, raid0 (stripping), raid1 (mirror), raid4, raid5, raid6, raid10等不同的冗余级别和组成方式,当然也能支持多个RAID阵列的层叠组成raid1+0, raid5+1等类型的阵列。
dm 整体框架大致如下:
在这里插入图片描述
这里主要关心 multipath 部分。
内核选中 DM_MULTIPATH 即可启用 multipath 相关内核支持。
DM_MULTIPATH_QL 和 DM_MULTIPATH_ST 为两种可选的路径选择算法。

dm-mpath.c是多路径驱动的核心,负责初始化相关数据结构,以及向 mapping/target interface 注册 dm 的 target type 回调。
dm-selector.c是负责管理路径选择算法的苦函数。
dm-round-robin.c是必备的路径选择算法:在一条路径上完成指定的 IO 次数后就切换到下一条路径,不断循环。
另外两种是可选算法,见上述 Kconfig:
1)dm-service-time.c算法:根据路径的吞吐量以及完成的字节数选择负载较轻的路径。
2)dm-queue-length.c算法:根据正在处理的 IO 个数较少的路径。

dm-mapth.c核心:

/*-----------------------------------------------------------------* Module setup*---------------------------------------------------------------*/
static struct target_type multipath_target = {.name = "multipath",.version = {1, 13, 0},.features = DM_TARGET_SINGLETON | DM_TARGET_IMMUTABLE |DM_TARGET_PASSES_INTEGRITY,.module = THIS_MODULE,.ctr = multipath_ctr,.dtr = multipath_dtr,.clone_and_map_rq = multipath_clone_and_map,.release_clone_rq = multipath_release_clone,.rq_end_io = multipath_end_io,.map = multipath_map_bio,.end_io = multipath_end_io_bio,.presuspend = multipath_presuspend,.postsuspend = multipath_postsuspend,.resume = multipath_resume,.status = multipath_status,.message = multipath_message,.prepare_ioctl = multipath_prepare_ioctl,.iterate_devices = multipath_iterate_devices,.busy = multipath_busy,
};static int __init dm_multipath_init(void)
{int r;kmultipathd = alloc_workqueue("kmpathd", WQ_MEM_RECLAIM, 0);if (!kmultipathd) {DMERR("failed to create workqueue kmpathd");r = -ENOMEM;goto bad_alloc_kmultipathd;}/** A separate workqueue is used to handle the device handlers* to avoid overloading existing workqueue. Overloading the* old workqueue would also create a bottleneck in the* path of the storage hardware device activation.*/kmpath_handlerd = alloc_ordered_workqueue("kmpath_handlerd",WQ_MEM_RECLAIM);if (!kmpath_handlerd) {DMERR("failed to create workqueue kmpath_handlerd");r = -ENOMEM;goto bad_alloc_kmpath_handlerd;}r = dm_register_target(&multipath_target);if (r < 0) {DMERR("request-based register failed %d", r);r = -EINVAL;goto bad_register_target;}return 0;bad_register_target:destroy_workqueue(kmpath_handlerd);
bad_alloc_kmpath_handlerd:destroy_workqueue(kmultipathd);
bad_alloc_kmultipathd:return r;
}

在启动阶段dm_multipath_init向 dm 的 mapping/target interface 注册 target type,并且申请两个工作队列,用于异步执行耗时的 IO 操作。
使用的数据结构struct target_type:

struct target_type {uint64_t features;const char *name;struct module *module;unsigned version[3];dm_ctr_fn ctr;dm_dtr_fn dtr;dm_map_fn map;dm_clone_and_map_request_fn clone_and_map_rq;dm_release_clone_request_fn release_clone_rq;dm_endio_fn end_io;dm_request_endio_fn rq_end_io;dm_presuspend_fn presuspend;dm_presuspend_undo_fn presuspend_undo;dm_postsuspend_fn postsuspend;dm_preresume_fn preresume;dm_resume_fn resume;dm_status_fn status;dm_message_fn message;dm_prepare_ioctl_fn prepare_ioctl;
#if 1 /* CONFIG_BLK_DEV_ZONED */dm_report_zones_fn report_zones;
#endifdm_busy_fn busy;dm_iterate_devices_fn iterate_devices;dm_io_hints_fn io_hints;dm_dax_direct_access_fn direct_access;dm_dax_copy_iter_fn dax_copy_from_iter;dm_dax_copy_iter_fn dax_copy_to_iter;/* For internal device-mapper use. */struct list_head list;
};ctr = multipath_ctr
当用户程序加载 DeviceMapper 映射表时,如果其中的映射类型为 multipath,则会根据映射表中的参数调用此函数,
用于构建一个 Target Device:申请该实例内存,并进行简单初始化;解析参数,并根据参数创建优先级组,解析每个优先级组的路径算法;设置一些基本的 DeviceMapper 属性;
一个设备的多路径可以使用不同类型,划分成多个优先组,每个组需要包含一个路径选择器(路径选择算法)以及至少一条路径。参数组成部分:特性参数 硬件参数 优先级组以及路径参数参数解析实例:mpatha: 0 41940992 multipath 1 queue_if_no_path 1 alua 1 1 service-time 0 2 2 8:0 1 1 8:16 1 1其中 "1 queue_if_no_path 1 alua 1 1 service-time 0 2 2 8:0 1 1 8:16 1 1" 为传递给驱动的参数。1:表示特性参数,后跟特性参数,反之为 0,不跟参数1: 表示硬件参数,后跟硬件参数(alua),反之为 0,不跟参数1: 表示优先级组个数1: 下一个优先级组序号,主要用于解析优先级组时使用service-time:当前的优先组使用 service-time 算法0: 算法的参数个数为 02: 该优先组包含两条路径2: 每条路径的参数有两个8:0  1 1: 第一条路径的设备号为 8:0,每进行 1 次 IO 就切换路径,路径的吞吐量权重为 18:16 1 1: 第一条路径的设备号为 8:16,每进行 1 次 IO 就切换路径,路径的吞吐量权重为 1
最终用户态程序看到的结果就是:
mpatha (3600140554012e51273044299221f2903) dm-0 LIO-ORG,block1
size=20G features='1 queue_if_no_path' hwhandler='1 alua' wp=rw
`-+- policy='service-time 0' prio=50 status=active|- 7:0:0:0 sda 8:0  active ready running`- 6:0:0:0 sdb 8:16 active ready runningclone_and_map_rq = multipath_clone_and_map(IO 映射)
判断是否需要切换路径,并获取当前可用路径,把路径信息保存在请求的私有数据中
根据路径信息重定向请求,设置其目的队列
调用路径算法的 start_io 函数。
如果成功则返回 DM_MAPIO_REMAPPED,表明映射成功,通知 dm 框架重新投递请求。rq_end_io = multipath_end_io
映射完成后,进行 IO 结束时会调用 multipath_end_io 函数进行资源回收,
如果 IO 错误且 pgpath 不为空则使用 fail_path 函数通过 uevent 通知用户层此路径已经失效。
如果设置了 queue_if_no_path 为 0,则会返回 DM_ENDIO_REQUEUE,dm 会把这个请求重新加入队列,
否则直接返回 EIO 错误,最后清除在开始映射时创建的上下文信息,并调用路径选择算法的 end_io 函数。message = multipath_message
多路径驱动除了根据在给定的算法和配置下进行 IO 错误时以外,并不主动探测路径的状态,增加减少路径,
以及状态改变,这些都是由用户态的 multipathd 和 multipath 来控制完成,然后通过 dm 框架提供的接口,
调用 target type 驱动的 message 函数来完成。这里实现了切换优先组,失效以及使能路径等功能。

dm-path-selector.c路径选择算法管理库
使用链表对路径算法进行管理,注册,取消注册,获取,释放等。

/* Register a path selector */
int dm_register_path_selector(struct path_selector_type *type);/* Unregister a path selector */
int dm_unregister_path_selector(struct path_selector_type *type);/* Returns a registered path selector type */
struct path_selector_type *dm_get_path_selector(const char *name);/* Releases a path selector  */
void dm_put_path_selector(struct path_selector_type *pst);每个路径算法实现下面的结构回调:
/* Information about a path selector type */
struct path_selector_type {char *name;struct module *module;unsigned int table_args;unsigned int info_args;/** Constructs a path selector object, takes custom arguments*/int (*create) (struct path_selector *ps, unsigned argc, char **argv);void (*destroy) (struct path_selector *ps);/** Add an opaque path object, along with some selector specific* path args (eg, path priority).*/int (*add_path) (struct path_selector *ps, struct dm_path *path,int argc, char **argv, char **error);/** Chooses a path for this io, if no paths are available then* NULL will be returned.*/struct dm_path *(*select_path) (struct path_selector *ps,size_t nr_bytes);/** Notify the selector that a path has failed.*/void (*fail_path) (struct path_selector *ps, struct dm_path *p);/** Ask selector to reinstate a path.*/int (*reinstate_path) (struct path_selector *ps, struct dm_path *p);/** Table content based on parameters added in ps_add_path_fn* or path selector status*/int (*status) (struct path_selector *ps, struct dm_path *path,status_type_t type, char *result, unsigned int maxlen);int (*start_io) (struct path_selector *ps, struct dm_path *path,size_t nr_bytes);int (*end_io) (struct path_selector *ps, struct dm_path *path,size_t nr_bytes);
};create:实例化一个选择器
destroy:销毁一个选择器add_path:向该选择器增加一条路径
select_path:选择进行 IO 的路径fail_path:告诉选择器该路径失效
reinstate_path:告诉选择器该路径可用status:返回选择器的状态start_io:使用者进行 IO 前必须调用该函数(如路径算法记录 IO 次数)
end_io:使用者完成 IO 后必须调用该函数

round-robin路径选择器
每条路径只有一个参数,就是每条路径执行多少次 IO 后切换路径。
添加一条路径:

static int rr_add_path(struct path_selector *ps, struct dm_path *path,int argc, char **argv, char **error)
{struct selector *s = ps->context;struct path_info *pi;unsigned repeat_count = RR_MIN_IO;char dummy;unsigned long flags;if (argc > 1) {*error = "round-robin ps: incorrect number of arguments";return -EINVAL;}/* First path argument is number of I/Os before switching path */if ((argc == 1) && (sscanf(argv[0], "%u%c", &repeat_count, &dummy) != 1)) {*error = "round-robin ps: invalid repeat count";return -EINVAL;}if (repeat_count > 1) {DMWARN_LIMIT("repeat_count > 1 is deprecated, using 1 instead");repeat_count = 1;}/* allocate the path */pi = kmalloc(sizeof(*pi), GFP_KERNEL);if (!pi) {*error = "round-robin ps: Error allocating path context";return -ENOMEM;}pi->path = path;pi->repeat_count = repeat_count;path->pscontext = pi;spin_lock_irqsave(&s->lock, flags);list_add_tail(&pi->list, &s->valid_paths);spin_unlock_irqrestore(&s->lock, flags);return 0;
}

路径选择策略:
在每条路径进行repeat_count次 IO 后就切换到下一条路径,不断循环。(目前已经废弃,总是 1 次后就切换)

static struct dm_path *rr_select_path(struct path_selector *ps, size_t nr_bytes)
{unsigned long flags;struct selector *s = ps->context;struct path_info *pi = NULL;spin_lock_irqsave(&s->lock, flags);if (!list_empty(&s->valid_paths)) {pi = list_entry(s->valid_paths.next, struct path_info, list);list_move_tail(&pi->list, &s->valid_paths);}spin_unlock_irqrestore(&s->lock, flags);return pi ? pi->path : NULL;
}

service-time 路径选择器
添加一条路径:

static int st_add_path(struct path_selector *ps, struct dm_path *path,int argc, char **argv, char **error)
{struct selector *s = ps->context;struct path_info *pi;unsigned repeat_count = ST_MIN_IO;unsigned relative_throughput = 1;char dummy;unsigned long flags;
...
...

每条路径需要指定它的重复次数repeat_count,以及吞吐量权重值relative_throughput。

路径选择策略:
在 IO 开始与结束时,分别增加和减少该路径正在处理的 IO 字节数:

static int st_start_io(struct path_selector *ps, struct dm_path *path,size_t nr_bytes)
{struct path_info *pi = path->pscontext;atomic_add(nr_bytes, &pi->in_flight_size);return 0;
}static int st_end_io(struct path_selector *ps, struct dm_path *path,size_t nr_bytes)
{struct path_info *pi = path->pscontext;atomic_sub(nr_bytes, &pi->in_flight_size);return 0;
}在多个路径中,选择正在处理的数据量与吞吐量比值最小的那条路径
static int st_compare_load(struct path_info *pi1, struct path_info *pi2,size_t incoming)
{size_t sz1, sz2, st1, st2;sz1 = atomic_read(&pi1->in_flight_size);sz2 = atomic_read(&pi2->in_flight_size);/** Case 1: Both have same throughput value. Choose less loaded path.*/if (pi1->relative_throughput == pi2->relative_throughput)return sz1 - sz2;
...
...static struct dm_path *st_select_path(struct path_selector *ps, size_t nr_bytes)
{struct selector *s = ps->context;struct path_info *pi = NULL, *best = NULL;struct dm_path *ret = NULL;unsigned long flags;spin_lock_irqsave(&s->lock, flags);if (list_empty(&s->valid_paths))goto out;list_for_each_entry(pi, &s->valid_paths, list)if (!best || (st_compare_load(pi, best, nr_bytes) < 0))best = pi;if (!best)goto out;/* Move most recently used to least preferred to evenly balance. */list_move_tail(&best->list, &s->valid_paths);ret = best->path;
out:spin_unlock_irqrestore(&s->lock, flags);return ret;
}

queue-length 路径选择器:
添加一条路径:
只需要指定repeat_count即可

static int ql_add_path(struct path_selector *ps, struct dm_path *path,int argc, char **argv, char **error)
{struct selector *s = ps->context;struct path_info *pi;unsigned repeat_count = QL_MIN_IO;char dummy;unsigned long flags;
...
...

路径选择策略:
在 IO 开始和结束时,分别增加和减少正在处理的 IO 个数:

static int ql_start_io(struct path_selector *ps, struct dm_path *path,size_t nr_bytes)
{struct path_info *pi = path->pscontext;atomic_inc(&pi->qlen);return 0;
}static int ql_end_io(struct path_selector *ps, struct dm_path *path,size_t nr_bytes)
{struct path_info *pi = path->pscontext;atomic_dec(&pi->qlen);return 0;
}选择正在处理的 IO 个数最少的那条路径
static struct dm_path *ql_select_path(struct path_selector *ps, size_t nr_bytes)
{struct selector *s = ps->context;struct path_info *pi = NULL, *best = NULL;struct dm_path *ret = NULL;unsigned long flags;spin_lock_irqsave(&s->lock, flags);if (list_empty(&s->valid_paths))goto out;list_for_each_entry(pi, &s->valid_paths, list) {if (!best ||(atomic_read(&pi->qlen) < atomic_read(&best->qlen)))best = pi;if (!atomic_read(&best->qlen))break;}if (!best)goto out;/* Move most recently used to least preferred to evenly balance. */list_move_tail(&best->list, &s->valid_paths);ret = best->path;
out:spin_unlock_irqrestore(&s->lock, flags);return ret;
}

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

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

相关文章

【MySQL】常用存储引擎,数据库管理,数据表管理,数据库账户管理

目录 一 常用的数据引擎(4) 1.1 InnoDB存储引擎 1.2 MyISAM存储引擎 1.3 Memory存储引擎 1.4 ARCHIVE存储引擎 二 数据库管理 2.1 元数据库概念与分类 2.2 相关操作命令 三 数据表的管理 3.1 三大范式 3.2 数据类型 四 数据库账户管理 五 思维导图 一 常用的数据…

高德地图经纬度坐标导出工具

https://tool.xuexiareas.com/map/amap 可以导出单个点&#xff0c;也可以导出多个&#xff0c;多个点可以连成线&#xff0c;可用于前端开发时自己模拟“线“数据

【docker】安装 Redis

查看可用的 redis版本 docker search redis拉取 redis最新镜像 docker pull redis:latest查看本地镜像 docker images创建挂在文件 mkdir -pv /test1/docker_volume/redis/datamkdir -pv /test1/docker_volume/redis/confcd /test1/docker_volume/redis/conf/touch redis.con…

Taro多端统一开发框架:小程序开发轻松驾驭,一套代码多端运行

引言 在如今不断增长的小程序市场中&#xff0c;小程序的数量迅速增多。这是因为小程序具有诸多优势&#xff0c;例如轻量化、便捷性和良好的用户体验&#xff0c;吸引了越来越多的开发者和企业加入这一领域。随着小程序的普及&#xff0c;各个行业都纷纷推出自己的小程序&…

图像分割 分水岭法 watershed

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 本文的C#版本请访问&#xff1a;图像分割 分水岭法 watershed&#xff08;C#&#xff09;-CSDN博客 Watershed算法是一种图像处理算…

如何给NVIDIA JetsonOrin Nano开发套件安装CSI摄像头

Jetson Orin Nano开发套件沿袭先前系列产品的设计重点&#xff0c;也提供两个CSI/MIPI摄像头接口&#xff0c;如下图右两个黄色圆框圈出的地方&#xff0c;不过这次的22针接口与先前系列产品的15针接口并不相同&#xff0c;不过先前产品所支持的摄像头规格&#xff08;例如IMX2…

Langchain访问OpenAI ChatGPT API Account deactivated的另类方法,访问跳板机API

笔者曾经写过 ChatGPT OpenAI API请求限制 尝试解决 Account deactivated. Please contact us through our help center at help.openai.com if you need assistance. 结果如何&#xff1f; 没有啥用。目前发现一条曲线救国的方案。 1. 在官方 openai 库中使用 此处为最新Op…

【Java并发】深入浅出 synchronized关键词原理-上

一个问题的思考 建设我们有两个线程&#xff0c;一个进行5000次的相加操作&#xff0c;另一个进行5000次的减操作。那么最终结果是多少 package com.jia.syn;import java.util.concurrent.TimeUnit;/*** author qxlx* date 2024/1/2 10:08 PM*/ public class SynTest {privat…

Android Matrix剪切clipPath缩放scale图片postTranslate圆形放大镜,Kotlin(1)

Android Matrix剪切clipPath缩放scale图片postTranslate圆形放大镜&#xff0c;Kotlin&#xff08;1&#xff09; 实现查看图片的放大镜&#xff0c;放大镜随着手指在屏幕上的移动&#xff0c;放大镜里面展示手指触点为中心、半径长度的圆形放大后的图片。 剪切出一块圆形Path…

掌握静态S5:从入门到精通的指南

在现今的数据驱动时代&#xff0c;静态S5作为一款强大的数据分析工具&#xff0c;越来越受到各行各业的青睐。然而&#xff0c;如何从入门到精通&#xff0c;全面掌握静态S5的各项功能&#xff0c;成为了许多用户面临的挑战。本文将为你提供一份详尽的指南&#xff0c;助你顺利…

milvus学习(一)cosin距离和欧式距离

参考&#xff1a;https://blog.csdn.net/qq_36560894/article/details/115408613 归一化以后的cosin距离和欧式距离可以相互转化&#xff0c;未归一化的不可以相互转化&#xff08;因为距离带单位&#xff09;。

一个人去广东怎么找工作

广东这么大&#xff0c;不用怕没有学历活不下去。没有学历想好好活下去&#xff0c;就得卖力气。 广东找工作上 吉鹿力招聘网 打开 吉鹿力招聘网 “注册账号”&#xff0c;然后输入个人基本信息&#xff0c;进行注册&#xff08;可使用手机号注册&#xff0c;也可以使用邮箱注…