SPDK技术浅析

news2024/11/19 16:38:40

目录

      • SPDK基础知识
        • SPDK架构
      • SPDK使用
        • rpc后台启动
        • 基础机制分析
      • 后端vhost
      • 异步I/O

写该篇的来由是因为翻阅到了TriCache: A User-Transparent Block Cache Enabling High-Performance Out-of-Core Processing with In-Memory Programs文章,其中对SPDK的运用的炉火纯青,将I/O性能提升到了一个新的档次

SPDK基础知识

SPDK Storage Performance Development Kit,存储性能开发工具包,提供了一组工具和库,用于编写高性能,可伸缩的用户模式存储应用程序

相关资料:

链接
Introduction to the Storage Performance Development Kit (SPDK)
spdk 官网 文档
spdk 源码 GitHub spdk/spdk
spdk 技术文章

还有一个技术名为DPDK,架构和SPDK类似,想学习该书的推荐看《深入浅出DPDK

优点:

  • 将所有必需的驱动程序移动到用户空间,这样可以避免系统调用并启用应用程序的零拷贝访问
  • 轮询硬件在于是否完成而非依赖中断抢占,这降低了总延迟和延迟差异
  • 避免I / O路径中的所有锁定,而是依赖于消息传递(使用了无锁消息队列)

编译和安装

git clone https://github.com/spdk/spdk
cd spdk
git submodule update --init   # 拉取子模块
sudo scripts/pkgdep.sh        # 安装依赖性
./configure
make

运行单元测试,以下表示测试成功

(base) root@nizai8a-desktop:~/tt/spdk# ./test/unit/unittest.sh
=====================
All unit tests passed
=====================
WARN: lcov not installed or SPDK built without coverage!
WARN: neither valgrind nor ASAN is enabled!

分配大页面并且从本机内核驱动程序中取消绑定任何NVMe设备

此处是常见的错误,表示/dev/nvme1n1是使用中,因此,如果想取绑设备,必须要将其umount并格式化
用户驱动程序利用uio/vfio中的功能将设备的PCI BAR映射到当前进程,从而允许驱动程序直接执行MMIO(默认采用uio

(base) root@nizai8a-desktop:~/tt/spdk# sudo scripts/setup.sh
0000:81:00.0 (144d a808): Active mountpoints on nvme1n1:nvme1n1, so not binding PCI dev
0000:01:00.0 (15b7 5009): nvme -> uio_pci_generi

为什么要分配大页?
原理:dpdk大页内存原理

  • 所有大页以及大页表都以共享内存存放在共享内存中,永远都不会因为内存不足而导致被交换到磁盘swap分区中
  • 由于所有进程都共享一个大页表,减少了页表的开销,无形中减少了内存空间的占用, 使得系统支持更多的进程同时运行
  • 减轻TLB的压力
  • 减轻查内存的压力

默认情况下,脚本分配2048MB的大页面。要更改此数字,请指定HUGEMEM

sudo HUGEMEM = 4096 scripts/setup.sh

查看系统支持的大页类型

(base) root@nizai8a-desktop:/sys/kernel/mm/hugepages# ls
hugepages-1048576kB  hugepages-2048kB

SPDK架构

本节主要是针对SPDK的三个优点展开的


用户态驱动程序

VVMe的出现后,软件成为了I/O密集型的场景下的瓶颈,有两种方法对内核进行优化

  • io_ring:提供一套新的系统调用,基于系统调用路径的优化
  • 绕过内核kernel bypass,整个I/O操作不需要陷入到内核中,SPDK即该存储加速方案

Intel对其定义是利用用户态、异步、轮询方式NVMe驱动,用于加速NVMe SSD作为后端存储使用的应用软件的加速库

NVMe协议是一种为了SSD固态硬盘和主机通信的速度更快而定义的规范,将其本地存储性能推广到网络有NVMe-oF协议,用来支持InfiniBand、光纤或者以太网等。在针对网络存储解决方案中,当前主要有DASNAS以及SAN

SPDK基于UIOVFIO的支持直接将存储设备的的地址空间映射到应用空间的方式,并利用NVMe规范来初始化NVMe SSD设备,实现基本的I/O操作,从而构筑用户态驱动,所以整个过程不需要陷入到内核中

SPDK用户态驱动的方案中,这一行为被异步轮询所取代,通过CPU不断轮询的方式,一旦查询到操作完成,则立马触发回调函数,给到上层用户程序,这样用户程序可以按需发送多个请求,以此提升性能

移除系统中断另一个显而易见的好处是避免了上下文的切换

除此之外,SPDK利用CPU的亲和性,将线程和CPU核做绑定,设计了线程模型,应用程序从收到这个核的I/O操作到运行结束,都在该核上完成,这样可以更高效的利用缓存,同时也避免多核之间的内存同步问题

与此同时,在单核上的内存资源的管理,利用了大页存储来加速


线程

SPDK中的线程模型架构上,每一个CPU核中拥有一个内核线程,会初始化一个reactor

每一个reactor下可持有零到多个SPDK抽象出的轻量用户态线程spdk_thread,为了提高在reactor间通信与同步的效率,SPDK放弃了传统加锁的方式,而是通过向每个reactorspdk_ring来发送消息,在抽象得到的spdk_thread下拥有poller,用来注册用户函数

在这里插入图片描述

SPDK使用

rpc后台启动

SPDKv20.x的版本开始已经切换为json配置文件的格式,行可执行程序时可以通过 --json 参数来传递json配置文件。当启动SPDK的应用程序时指定了json文件,那在SPDK的初始化流程中就会以rpc的模式来执行其中subsystems的初始化并执行json文件中指定的操作

rpc是一种为大家所熟知在程序启动后动态灵活地执行操作的方法。其主要使用了unix socket来在客户端和服务端之间传递消息数据
SPDK也集成或者实现了rpc的交互通道,可以支持动态的操作

服务端

SPDK中的rpc服务端在函数 spdk_rpc_initialize中完成初始化。如果没有指定用于监听的地址,那就会使用默认的监听地址/var/tmp/spdk.sockrpc客户端访问时使用的默认监听地址就是这个。每个需要提供rpc调用的模块或者功能都可以通过SPDK_RPC_REGISTER 来注册其提供服务的函数到g_rpc_methods链表中。函数 jsonrpc_handler是处理所有从客户端过来的请求的入口,其中就会从g_rpc_methods链表中来根据请求匹配具体的处理函数

客户端

SPDKrpc客户端提供的功能大部分是以 ./spdk/scripts/rpc.py脚本为入口来进行调用的。该脚本会包含./spdk/python/spdk/rpc目录下的python脚本,各个rpc功能的客户端处理,以及公共的用于和服务端进行交互的函数就定义在这些被包含的脚本中。每个模块提供的rpc功能中客户端的相应处理逻辑都归集在以模块名字作为名字的python文件中

如果想查询有哪些已经支持的rpc调用功能,则可以直接执行./rpc.py -h查询

如果想要添加新的rpc的功能,那就需要通过SPDK_RPC_REGISTER 注册新的功能,并在rpc客户端添加相应的python脚本逻辑

基础机制分析

SPDK中的分核并行、免锁及Run to completion 的编程特性,主要是由reactor、events、pollerio channel的机制构成


Reactors

DPDKrte_eal_init函数被执行时,其会在除当前运行的CPU main cpu核外的各个指定可用的CPU核上创建线程

并通过修改线程的亲和参数将其绑定在对应的CPU核上运行。每个线程的执行函数是eal_thread_loop,一直在等待从pipe中接收数据并执行

/* Launch threads, called at application init(). */
int
rte_eal_init(int argc, char **argv)
{
	// ...
	RTE_LCORE_FOREACH_WORKER(i) {

		/*
		 * create communication pipes between main thread
		 * and children
		 */
		if (pipe(lcore_config[i].pipe_main2worker) < 0)
			rte_panic("Cannot create pipe\n");
		if (pipe(lcore_config[i].pipe_worker2main) < 0)
			rte_panic("Cannot create pipe\n");

		lcore_config[i].state = WAIT;

		/* create a thread for each lcore */
		// 创建线程,执行函数为eal_thread_loop
		ret = pthread_create(&lcore_config[i].thread_id, NULL,
				     eal_thread_loop, NULL); 
		if (ret != 0)
			rte_panic("Cannot create thread\n");

		/* Set thread_name for aid in debugging. */
		snprintf(thread_name, sizeof(thread_name),
				"lcore-worker-%d", i);
		rte_thread_setname(lcore_config[i].thread_id, thread_name);
		// 增加线程亲和性
		ret = pthread_setaffinity_np(lcore_config[i].thread_id,
			sizeof(rte_cpuset_t), &lcore_config[i].cpuset);
		if (ret != 0)
			rte_panic("Cannot set affinity\n");
	}
	// ...
}

DPDK中提供了 rte_mempool rte_ring 的机制来支持针对内存方面的需求

每个rte_mempool实例都是一个些由大页内存组成的内存池,并且以特定的数据结构进行组织,其中支持的每个分配和使用的单元可以用于存储调用者的数据

当创建rte_mempool时会同时在各个可用的CPU创建cache buffers,以便当调用rte_mem_get时直接从cache buffer中获取,加速分配的过程(有点像per-cpu cache

/**
 * The RTE mempool structure.
 */
struct rte_mempool {
	/*
	 * Note: this field kept the RTE_MEMZONE_NAMESIZE size due to ABI
	 * compatibility requirements, it could be changed to
	 * RTE_MEMPOOL_NAMESIZE next time the ABI changes
	 */
	char name[RTE_MEMZONE_NAMESIZE]; /**< Name of mempool. */
	RTE_STD_C11
	union {
		void *pool_data;         /**< Ring or pool to store objects. */
		uint64_t pool_id;        /**< External mempool identifier. */
	};
	void *pool_config;               /**< optional args for ops alloc. */
	const struct rte_memzone *mz;    /**< Memzone where pool is alloc'd. */
	unsigned int flags;              /**< Flags of the mempool. */
	int socket_id;                   /**< Socket id passed at create. */
	uint32_t size;                   /**< Max size of the mempool. */
	uint32_t cache_size;
	/**< Size of per-lcore default local cache. */

	uint32_t elt_size;               /**< Size of an element. */
	uint32_t header_size;            /**< Size of header (before elt). */
	uint32_t trailer_size;           /**< Size of trailer (after elt). */

	unsigned private_data_size;      /**< Size of private data. */
	/**
	 * Index into rte_mempool_ops_table array of mempool ops
	 * structs, which contain callback function pointers.
	 * We're using an index here rather than pointers to the callbacks
	 * to facilitate any secondary processes that may want to use
	 * this mempool.
	 */
	int32_t ops_index;

	struct rte_mempool_cache *local_cache; /**< Per-lcore local cache */

	uint32_t populated_size;         /**< Number of populated objects. */
	struct rte_mempool_objhdr_list elt_list; /**< List of objects in pool */
	uint32_t nb_mem_chunks;          /**< Number of memory chunks */
	// 内存池
	struct rte_mempool_memhdr_list mem_list; /**< List of memory chunks */
}  __rte_cache_aligned;

rte_ring则是用于传递消息的队列,每个在rte_ring中传递的单元是一个内存指针,可以参见 spdk_thread_send_msg函数中的使用

rte_ring使用了无锁的队列模型,支持多生产者多消费者的模式,当前SPDK中使用的是多生产者单消费者的模式

SPDK启动后,在每个指定可用的CPU核上均会运行一个reactor,且在除主核外的CPU核上,SPDKreactoreal_thread_loop有一一对应的关系

reactor->events是基于rte_ring来实现的,可以用于在reactors之间传递消息,通过调用spdk_event_allocatespdk_event_call可以向任一spdk使用的CPU核来发送需消息,以便运行私有的逻辑操作

该操作的使用场景

  • 需要在当前CPU核上延迟地做某个动作,但还有没有对应的spdk_thread可使用
  • 需要在其他SPDK使用地CPU核上做某个动作,但还没有关联的spdk_thread可用
DEFINE_STUB(spdk_event_allocate, struct spdk_event *, (uint32_t core, spdk_event_fn fn, void *arg1,
		void *arg2), NULL);
		
/* DEFINE_STUB is for defining the implmentation of stubs for SPDK funcs. */
#define DEFINE_STUB(fn, ret, dargs, val) \
	bool ut_ ## fn ## _mocked = true; \
	ret ut_ ## fn = val; \
	ret fn dargs; \
	ret fn dargs \
	{ \
		return MOCK_GET(fn); \
	}
DEFINE_STUB_V(spdk_event_call, (struct spdk_event *event));

/* DEFINE_STUB_V macro is for stubs that don't have a return value */
#define DEFINE_STUB_V(fn, dargs) \
	void fn dargs; \
	void fn dargs \
	{ \
	}

SPDK默认的调度策略是static类型,即reactorthread都运行在polling模式


SPDK的线程模型

spdk_thread 不是常规意义下的线程,实际是个逻辑上的概念,它没有具体的执行函数,其所有相关的操作均在reactor的执行函数中来执行

spdk_threadreactor的关系是N:1的对应关系,即每个reactor上可以有很多的spdk_thread,但每个spdk_thread需要属于且只能属于一个具体的reactor


Poll

每个注册的spdk_poller存放于spdk_thread->timed_pollers的红黑树结构或者spdk_thread->active_pollers链表中。所以如果想要使用poller,那首先需要创建一个spdk_thread

有了spdk_thread后就可以通过注册spdk_poller来重复或者周期性的运行某个函数。如果注册poller时的周期指定为0,那么poller对应的执行函数就会在每个reactor的循环中均进行调用;如果周期不为0,那各次reactor的循环中就会检查是否满足执行的周期时才执行

struct spdk_poller {
	TAILQ_ENTRY(spdk_poller)	tailq;

	/* Current state of the poller; should only be accessed from the poller's thread. */
	enum spdk_poller_state		state;

	uint64_t			period_ticks;
	uint64_t			next_run_tick;
	uint64_t			run_count;
	uint64_t			busy_count;
	spdk_poller_fn			fn;
	void				*arg;
	struct spdk_thread		*thread;
	int				interruptfd;
	spdk_poller_set_interrupt_mode_cb set_intr_cb_fn;
	void				*set_intr_cb_arg;

	char				name[SPDK_MAX_POLLER_NAME_LEN + 1];
};

当创建了spdk_thread后,就可以使用spdk_thread_send_msg函数来执行具体的函数,通过选择合适的spdk_thread可以实现在当前CPU核或其他SPDK使用的CPU核上去执行操作

这个函数中传递的msg就是从g_spdk_msg_mempool (一个rte_mempool实例) 中分配的,传递时就使用了rte_ring的无锁队列

总的来说就是Reactor之间通过event(rte_ring)通信,不同cpu核的spdk_thread或者同一cpu核的spdk_thread通过Message(rte_ring)来协调无锁队列的


io_channel

IO channel 是一个用于在每个可用的CPU核上分别单独执行相同操作的抽象机制,不做概述

后端vhost


设备查找

扫描设备并将设备和控制器绑定以及数据的读写操作

  • probe_cb:找到NVMe controller之后进行回调
  • attach_cb:一旦NVMe控制器已连接到用户空间驱动程序后调用

查找设备和绑定驱动的过程均在spdk_nvme_probe函数中实现

在代码中,我们通过指定transport id来对PCI总线上的设备进行扫描,通过两个全局链表来保存驱动和设备,遍历驱动,将找到的设备和驱动进行匹配

int
spdk_nvme_probe(const struct spdk_nvme_transport_id *trid, void *cb_ctx,
		spdk_nvme_probe_cb probe_cb, spdk_nvme_attach_cb attach_cb,
		spdk_nvme_remove_cb remove_cb)
{
	struct spdk_nvme_transport_id trid_pcie;
	struct spdk_nvme_probe_ctx *probe_ctx;

	if (trid == NULL) {
		memset(&trid_pcie, 0, sizeof(trid_pcie));
		spdk_nvme_trid_populate_transport(&trid_pcie, SPDK_NVME_TRANSPORT_PCIE);
		trid = &trid_pcie;
	}

	probe_ctx = spdk_nvme_probe_async(trid, cb_ctx, probe_cb,
					  attach_cb, remove_cb);
	if (!probe_ctx) {
		SPDK_ERRLOG("Create probe context failed\n");
		return -1;
	}

	/*
	 * Keep going even if one or more nvme_attach() calls failed,
	 *  but maintain the value of rc to signal errors when we return.
	 */
	return nvme_init_controllers(probe_ctx);
}

重点看一下读写过程

HOST 就是NVMe卡所插入的系统,·HOST·和·Controller·之间的交互通过·Qpair·进行

pair分为IO QpairAdmin Qpair,顾名思义,Admin Qpair用于控制命令的传输,而IO Qpair用于IO命令的传输

Qpair对由提交队列(Submission Queue, SQ)和完成队列(Completion Queue, CQ)组成的固定元素数量的环形队列,提交队列是由固定元素数量的64字节的命令组成的数组,加上2个整数(头和尾索引)。完成队列由固定元素数量的16字节命令加上2个整数(头和尾索引)所组成的环形队列。另外还有两个32位寄存器(Doorbell),Head DoorbellTail Doorbell

HOST需要向NVMe写入数据时,需要指明数据在内存中的地址,以及写入到NVMe中的位置,HOSTNVMe读数据也是一样的,需要指明NVMe地址和内存地址,这样HOSTNVMe才知道去哪里取数据,取完后数据放到哪里,两种数据地址表示的方式,一种是PRP,还有一种是SGL

PRP指向一个物理内存页。PRP和正常的寻址方式相似,基地址加上偏移地址。PRP指向一个物理地址页


SPDKI/O提交到本地PCIe设备过程

通过构造一个64字节的命令,将I/O提交到NVMe设备,将其放入提交队列尾部索引当前位置的提交队列中,然后将提交队列尾部的新索引写入提交队列Tail Doorbell。也可以写多条命令到SQ,然后只写一次Doorbell就可以提交所有命令。

命令本身描述了操作,还描述了主机内存中包含与命令关联的主机内存数据的位置,也就是我们要写入数据的位置,或将读取的数据放置到内存中的位置。通过DMA的方式将数据传输到该地址或从该地址传输数据

完成队列的工作方式类似,设备将命令的响应消息写入到CQ中。CQ中的每个元素包含一个相位Phase Tag,在整个环的每个循环上在01之间切换。设备通过中断通知HOST CQ的更新,但是SPDK不启用中断,而是轮询相位位以检测CQ的更新

有点像io_uring的机制了

异步I/O

此处大量使用了异步I/O,在Linux中一般默认使用的是AIOio_uring

其中,io_uring弥补了aio的一些不足之处

io_uring有时也称为aio_ring,io_ring,ring_io

一个小example

/**
* 读取文件
**/
#include <bits/stdc++.h>
#include <liburing.h>
#include <unistd.h>

char buf[1024] = {0};

int main() {
  int fd = open("1.txt", O_RDONLY, 0);
  
  io_uring ring;
  io_uring_queue_init(32, &ring, 0); // 初始化
  auto sqe = io_uring_get_sqe(&ring); // 从环中得到一块空位
  io_uring_prep_read(sqe, fd, buf, sizeof(buf), 0); // 为这块空位准备好操作
  io_uring_submit(&ring); // 提交任务
  io_uring_cqe* res; // 完成队列指针
  io_uring_wait_cqe(&ring, &res); // 阻塞等待一项完成的任务
  assert(res);
  std::cout << "read bytes: " << res->res << " \n";
  std::cout << buf << std::endl;
  io_uring_cqe_seen(&ring, res); // 将任务移出完成队列
  io_uring_queue_exit(&ring); // 退出
  return 0;
}

Io_uring 有三个东西:提交队列、完成队列、任务实体

参考文章:

  • dpdk中uio技术
  • Linux AIO
  • 【SPDK】一、概述
  • 初识SPDK
  • 剖析SPDK读写NVMe盘过程–从hello_world开始
  • 图解原理|Linux I/O 神器之 io_uring
  • AIO 的新归宿:io_uring

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

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

相关文章

数据结构(1)并查集

(4条消息) 第五课、Trie树、并查集、堆和堆排序_yan__kai_的博客-CSDN博客 活动 - AcWing 并查集作用&#xff1a;一群元素将可以归类到一个代表元素上。可以维护元素到根节点的距离。可以维护每个并查集的大小。 基本操作回顾基础课&#xff0c;特别是“食物链”那道题 目录…

【Django项目开发】部门管理模块的开发(八)

文章目录一、模型类设计二、视图设计1.都有哪些接口三、序列化器类设计四.分页操作1.utils工具中定义pagination.py2.视图类中使用五.路由配置一、模型类设计 一个部门下面可能会有很多子部门&#xff0c;一个子部门上面可能会有父部门&#xff1b;即部门内部之间进行关联&…

国科大模式识别与机器学习2022年期末总结

我根据本学期老师说的考试重点和我自身的情况总结的&#xff0c;希望能帮助到你&#xff0c;如有错误欢迎指正 目录第三章 判别函数Fisher线性判别感知机算法第四章 特征选择和提取K-L变换第五章 统计学习学习基础损失函数风险正则化过拟合欠拟合泛化误差第六章 有监督学习有监…

【jQuery】常用API——jQuery内容文本值

要针对元素的内容还有表单的值操作。 普通元素内容 html()&#xff08;相当于原生 inner HTML) html(); // 获取元素的内容html(内容); // 设置元素的内容<script src"../jquery.min.js"></script> </head><body><div><span>我是…

118页4万字智慧检务大数据平台解决方案

【版权声明】本资料来源网络&#xff0c;知识分享&#xff0c;仅供个人学习&#xff0c;请勿商用。【侵删致歉】如有侵权请联系小编&#xff0c;将在收到信息后第一时间删除&#xff01;完整资料领取见文末&#xff0c;部分资料内容&#xff1a; 目录 第1章 前言 1.1、 政策背…

docker-compose keep-alive mysql8 互为主从

一、准备2台物理机器master-1、master-2&#xff0c;目标虚拟VIP。   VIP:192.168.1.139   master-1:192.168.1.17   master-2:192.168.1.20    二、然后分别在2台物理机器master-1、master-2上使用docker-compose安装mysql8&#xff0c;并配置互为主从。 1&#xff09…

优先级队列、仿函数和反向迭代器

文章目录优先级队列priority_queue的模拟实现框架无参的构造(默认构造)迭代器区间构造向上调整向下调整插入删除取堆顶的数据求数据个数验满初识仿函数模拟实现仿函数更改后的向上调整仿函数更改后的向下调整反向迭代器具体实现优先级队列 1.优先队列是一种容器适配器&#xf…

微信转账api(企业付款)

企业付款介绍 提供企业向用户付款的功能&#xff0c;支持企业通过API接口付款&#xff0c;或通过微信支付商户平台网页功能操作付款。 1. 充值 登录微信支付商户平台&#xff0c;通过网页充值功能充值&#xff08;商户平台-资金管理-现金管理-充值&#xff09;。 温馨提示&a…

BreederDAO x DigiCult AMA——要点总结

问&#xff1a;为什么 BreederDAO 决定花费 200ETH 用于购买 Mythic DigiDaigaku Genesis — Ifrit&#xff1f; 答&#xff1a;除了投资之外&#xff0c;这也是为了确保这个领域中有更多的可触达性&#xff0c;尤其是随着我们 DigiDaigaku 市场工具的推出之后。这也是我们进入…

(十七)Async异步和多线程-语言进阶1

&#xff08;十七&#xff09;Async异步和多线程-语言进阶1一、进程-线程-多线程&#xff0c;同步和异步1.概念2.同步和异步3.异步与多线程异同点二、委托启动异步调用1.同步方法2.异步方法三、多线程的特点四、异步的回调和状态参数1.顺序控制2.状态参数五、异步等待三种方式1…

开学季,护眼灯什么牌子好?2023年护眼台灯推荐

2023年开始了&#xff0c;时间飞逝&#xff0c;而每个父母都越来越紧张自家娃的学业情况&#xff0c;我国近视人数超过7亿人&#xff0c;而儿童时期是视力发育的关键时期&#xff0c;为啥有那么高的近视率呢&#xff1f;主要是用眼过度&#xff0c;以及用眼习惯&#xff0c;而现…

微信小程序——模板与配置,数据绑定,事件绑定

一.数据绑定1.数据绑定的基本原则在data中定义数据在WXML中使用数据2.在data中定义页面的数据在页面对应的.js文件中&#xff0c;把数据定义到data对象中即可3. Mustache 语法的格式把 data 中的数据绑定到页面中渲染&#xff0c;使用 Mustache 语法&#xff08;双大括号&#…

想提高办公效率?可试试java开源工作流框架

在数据化管理越来越规范的当今社会&#xff0c;采用优质的办公软件平台能提高企业的办公协作效率&#xff0c;因而受到了广泛的欢迎和喜爱。那么&#xff0c;什么是java开源工作流框架&#xff1f;我们可以从它的特点、发展前景等方面来加以了解&#xff0c;一起来了解这一产品…

微信公众号运营工具有哪些?赶紧收藏

再厉害的公众号运营大神背后都有一套宝藏工具大全&#xff0c;辅助运营人一路披荆斩棘&#xff0c;堪称神器&#xff01; 我相信网上一搜也能出来很多的运营工具或是网站&#xff0c;但是这里再来给大家来一个大汇总&#xff0c;这次整理绝对是非常详细和实用的&#xff0c;纯…

Fiddler中常用的功能

Fiddler中常用的功能如下&#xff1a; 停止抓包-清空会话窗-内容过滤请求-解码-设置断点 一、 停止抓包 二、清空会话窗 方法一&#xff0c;工具栏工具&#xff1a; 方法二&#xff0c;命令行形式&#xff1a; 当然&#xff0c;命令行工具也还支持其他命令的输入&#xff0c…

word排版技巧:如何将段中文字生成标题目录

在许多Word文档里面&#xff0c;目录页是非常重要的一页内容&#xff0c;因为目录页展示的是当前文档的内容框型和结构。通过目录页&#xff0c;我们能知道这个文档主要分为哪几部分。就像看书一样&#xff0c;起到了检索的作用。今天&#xff0c;我们就来给大家分享一个偏门的…

焕新古文化传承之路,AI为古彝文识别赋能

目录1 古彝文与古典保护2 古文识别的挑战2.1 西文与汉文OCR2.2 古彝文识别难点3 合合信息&#xff1a;古彝文保护新思路3.1 图像矫正3.2 图像增强3.3 语义理解3.4 工程技巧4 总结1 古彝文与古典保护 彝文指的是云南、贵州、四川等地的彝族人使用的文字&#xff0c;区别于现代意…

【Linux】常用基本指令(续)

文章目录&#x1f3aa; Linux下基本指令1.1 &#x1f680; whoami1.2 &#x1f680; tree1.3 &#x1f680; echo(浅析)1.4 &#x1f680; zip/unzip1.5 &#x1f680; tar1.6 &#x1f680; bc1.7 &#x1f680; history1.8 &#x1f680; uname1.9 &#x1f680; nano1.10 &a…

数据结构基础之动态顺序表详解

文章目录前言一、动态顺序表的概念二、顺序表的结构体三、基本接口1.SeqListInit&#xff08;初始化数组&#xff09;2.SeqListDestory&#xff08;销毁数组&#xff09;3. SeqListCheckCapacity&#xff08;检查改顺序表是否需要扩容&#xff09;4.SeqListPushBack&#xff08…

用真实业务场景告诉你,高并发下如何设计数据库架构?

目录&#xff1a; 用一个创业公司的发展作为背景引入用多台服务器来分库支撑高并发读写大量分表来保证海量数据下查询性能读写分离来支撑按需扩容及性能提升高并发下的数据库架构设计总结 这篇文章&#xff0c;我们来聊一下对于一个支撑日活百万用户的高并系统&#xff0c;他…