基于准静态自适应环型缓存器(QSARC)的taskBus万兆吞吐实现
文章目录
- 概要
- 整体架构流程
- 技术名词解释
- 技术细节
- 1. 数据结构
- 2. 自适应计算队列大小
- 3. 生产者拼接缓存
- 4. 高效地通知消费者
- 小结
- 1. 性能表现情况
- 2. 主要改进和局限
- 3. 源码和发行版
概要
准静态自适应环形缓存器(Quasi-Static Adaptive Ring Cache)是taskBus用于数据吞吐的软件结构。
- 准静态:缓存器的大小并不是静态分配,而是随着吞吐需求的提高,缓慢增长,并最终适应最高峰时的内存消耗。当缓存器已经达峰后,不再有堆内存分配。
- 自适应:根据包大小的不同,缓存器在恒定的最大峰值内存容积下,根据统计获得包的大小数据,决定环状缓存队列元素的个数、每个元素的大小。
- 环形: 队列收尾相接形成环状,生产者、消费者使用两个时钟前后相随,时钟本身采用atomic保护,无需额外的锁。
使用该缓存器,基于增强管道数据流转技术(EPDR)的业余软线无线电平台taskBus可在Linux 系统 i7 6700K 主频 4GHz下达到3GBps(24Gbps)的总交换能力。该交换能力被各个通道均分,共同支撑taskBus平台按照工程的连接关系,把各个生产者产出的数据包及时、完整、有序地输送给消费者。尽管采用了本技术,但与采用内核层面的zero-copy函数进行管道直接交换的性能极限还差了20倍。
整体架构流程
taskBus的整体架构是一种多进程的合作机制,平台管理N个子进程,各个进程可以充当消费者和生产者的角色。平台收集各个子进程的stdout输出,并按照用户给定的消费关系,送入消费者的stdout.在这样的整体架构下,数据流转流程如下:
在202409版本之前,taskBus使用的是消费者队列。由1个生产者产出的数据,会复制N份进入各个消费者的队列。这种情况下,平台既要为生产者部位保留一个用于包拼接的缓存,又要做N次memcpy。在消费者显著多于生产者时,吞吐能力下降的很厉害。
202409版本之后,设计成每份包只有唯一的1个副本,存储在生产者队列。消费者根据索引去拉取:
- 平台为每个生产者维护1个环状缓存队列。
- 平台按照消费关系,把每个包的队列位置索引播发给各个消费者。
- 播发时,会维护一个待消费计数器,将值加1
- 消费者消费包后,会将计数器的值减1
- 如果生产者队列绕了一圈,发现下一个位置的计数器不为0,说明消费的速度赶不上生产速度,此时平台不再接受生产者的数据,等待。
整个流程如下图所示(动图):
注意的是,上图中队列的大小是4个包,即使包消费完毕,可用的容积也不会释放,这样,随着程序的运行,渐渐地动态内存分配会越来越少,速度会越来越快。当然,如果包长是固定的,则不存在此问题,可以提前全部分配。
值得注意的是,在时钟11时,因为下一个位置依旧有消费者在驻留,因此生产被暂停等待。这种架构的缺点是整体的吞吐能力存在“木桶效应”,即由消费最慢的消费者决定1秒内能够流转多少包。
技术名词解释
- 包:用于一次功能操作的数据单位,可以理解为1段连续内存的数据。包由包头、数据段组成。包头含有长度指示,正常的数据包之间是紧密衔接的。
- 时钟:用于控制生产、消费的整形变量,从0开始无限增长。每处理1个包,时钟会加1。
- 当前位置:生产者、消费者操作队列的当前位置,数值= 时钟 % 队列大小。
技术细节
1. 数据结构
生产者为taskNode类型,拥有如下成员变量维护自己的生产队列:
QVector<int> m_status_stdin;
QVector<QByteArray> m_array_stdin;
QVector<qsizetype> m_size_stdin;
QVector<QAtomicInteger<qsizetype> > m_cnt_stdin;
变量1 控制生产的状态。管道到来的数据,是一个无限有序无损流,类似TCP。但是,每次流可能会被切断,导致包头、数据块可能被切割到多次调用里。这个变量用于在各次调用中记忆上次的生产状态。
变量2 就是缓存的内存本身。这个列表的QByteArray元素个数是缓存的包个数,每个QByteArray会保持在历史最大包的高位。这样,只有到来的包大于当前体积时,才会重新分配内存。
变量3 存储缓存内当前的写入位置。如果包已经写完了,则等于包长。如果是空闲,则为0。
变量4 就是待消费计数器,每次生产完毕1个包,会根据消费者等待队列广播包的索引(时钟),并把计数设置为消费者个数。消费者消费完,会减一。
2. 自适应计算队列大小
这种队列存在1个重大的问题,就是很难控制内存的用量。由于各个QByteArray都只增加不释放,因此期望的用量是:
C = M ∗ N C = M * N C=M∗N
M是最大包大小,N是队列包个数。
由于taskBus设计时,建议包的大小小于64KB,在假设包的种类小于K时,可以预先统计前K个包的最大长度M,从而确定按照最大内存门限,如C=128MB,要设置的N。
N = C / M N = C/M N=C/M
当然,这种算法是极为简陋的。这是建立在taskBus的具体应用场景上的。对包变化幅度很大、无法简单统计的情况,要考虑一定的shink策略,如在每次消费指针归0时,裁剪队列的大小,并释放尾部的内存。
//Adjust buf sizeif (m_pos_stdin==8){qsizetype sza = 0;for (int ia = 0;ia<8;++ia)if (sza < m_size_stdin[ia])sza = m_size_stdin[ia];if (sza < 32)sza = 32;m_bufsize_adjust = taskCell::default_ringcache * 1024 * 1024 / sza;if (m_bufsize_adjust > m_bufsize_stdin)m_bufsize_adjust = m_bufsize_stdin;if (m_bufsize_adjust < 8)m_bufsize_adjust = 8;emit_message(QString("Adjusted buffer ring size : %1 MB / %2 Bytes = %3 frames.").arg(taskCell::default_ringcache).arg(sza).arg(m_bufsize_adjust).toUtf8());}
3. 生产者拼接缓存
在当前生产位置上,平台为生产者拼接一个完整的包。这里用到了状态机。状态机拥有如下状态:
状态名 | 取值 | 意义 |
---|---|---|
头部捕获中 | 0 | 正在捕获头部 |
数据缓存中 | 1 | 正在根据头部指示的状态,缓存数据 |
数据缓存完毕 | 2 | 包接收完整,触发消费。 |
数据缓存完毕 | 3 | 触发消费完毕,待回收。 |
在每个状态上,都会进行进程管道读操作,不断的从stdout获取数据。直到状态2,会触发消费,并在全部消费通知播发后,进入状态3。当下一次生产者访问状态3的队列成员时,如果没有消费者还在消费这个包,则会把包位置归零,状态归零。否则,会阻塞生产者,直到消费者消费完毕为止。
void taskNode::slot_readyReadStandardOutput()
{LOG_PROFILE("IO","Start Recieving packs.");qsizetype total_sz = m_process->size();int badHeader = 0;while (total_sz){const qsizetype pos = m_pos_stdin % m_bufsize_adjust;QByteArray & curr_array = m_array_stdin[pos];qsizetype & readBufMax = m_size_stdin[pos];QAtomicInteger<qsizetype> & cnt = m_cnt_stdin[pos];int & stat = m_status_stdin[pos];//生产者被阻塞了,因为下一个缓存位置依旧有消费者在消费数据。if (cnt>0)break;//Old dataif (stat==3){readBufMax = 0;stat = 0;}auto * header = reinterpret_cast<const TASKBUS::subject_package_header *> (curr_array.data());//Headerif (stat==0){if (total_sz<sizeof(TASKBUS::subject_package_header))break;auto needRead = sizeof(TASKBUS::subject_package_header) - readBufMax ;auto red = m_process->read(curr_array.data()+readBufMax,needRead);readBufMax += red;if (readBufMax == sizeof(TASKBUS::subject_package_header)){if (header->prefix[0]==0x3C && header->prefix[1]==0x5A && header->prefix[2]==0x7E && header->prefix[3]==0x69){stat = 1;}else{++badHeader;readBufMax = 0;}}Q_ASSERT(readBufMax <= sizeof(TASKBUS::subject_package_header));}//dataif (stat==1){const qsizetype packAllSize = sizeof(TASKBUS::subject_package_header)+header->data_length;if (curr_array.size()<packAllSize){curr_array.resize(packAllSize);header = reinterpret_cast<const TASKBUS::subject_package_header *> (curr_array.data());}auto needRead = packAllSize - readBufMax ;auto red = m_process->read(curr_array.data()+readBufMax,needRead);readBufMax += red;if (readBufMax==packAllSize){stat = 2;}Q_ASSERT(readBufMax <= packAllSize);}//Sendif (stat==2){const qsizetype pack_size = sizeof(TASKBUS::subject_package_header)+header->data_length;extern QAtomicInteger<quint64> g_totalrev;g_totalrev += readBufMax;++m_spackage_sent;m_sbytes_sent += sizeof(TASKBUS::subject_package_header)+header->data_length;if (header->subject_id == TB_SUBJECT_CMD){//Command must endwith \0const char * pCmd = (const char *)header+sizeof(TASKBUS::subject_package_header);QString cmd = QString::fromUtf8(pCmd,header->data_length);QMap<QString, QVariant> map_z = taskCell::string_to_map(cmd);//remember uuidif (map_z.contains("source")){if(m_uuid.size()==0 )m_uuid = map_z["source"].toString();if (map_z.contains("destin"))emit sig_new_command(map_z);}}else if (m_currPrj)m_currPrj->routing_new_package(this,pos);if (m_bDebug)log_package(true,(char *)header,pack_size);stat = 3;++m_pos_stdin;//Adjust buf sizeif (m_pos_stdin==8){qsizetype sza = 0;for (int ia = 0;ia<8;++ia)if (sza < m_size_stdin[ia])sza = m_size_stdin[ia];if (sza < 32)sza = 32;m_bufsize_adjust = taskCell::default_ringcache * 1024 * 1024 / sza;if (m_bufsize_adjust > m_bufsize_stdin)m_bufsize_adjust = m_bufsize_stdin;if (m_bufsize_adjust < 8)m_bufsize_adjust = 8;emit_message(QString("Adjusted buffer ring size : %1 MB / %2 Bytes = %3 frames.").arg(taskCell::default_ringcache).arg(sza).arg(m_bufsize_adjust).toUtf8());}}total_sz = m_process->size();}if (badHeader)emit_message(QByteArray("Error header recieved. ""Header must be 0x3C, 0x5A, 0x7E,"" 0x69. Aborting."));LOG_PROFILE("IO","End Recieving packs.");
}
4. 高效地通知消费者
如果每个包很小,则QEvent触发的密度会很大,开销很大。我们设置一个消费者的索引队列,存储待消费的生产者队列、索引:
QMutex m_mtx_queue;QList<taskNode *> m_write_queue;QList<qsizetype> m_write_pos;
而后,只在队列为0时,触发Event。
bool taskNode::enqueue_write(taskNode * node, qsizetype pos)
{m_mtx_queue.lock();int z = m_write_queue.size() + m_write_cmd.size();m_write_queue.push_back( node);m_write_pos.push_back(pos);m_mtx_queue.unlock();if (!z){QCoreApplication::postEvent(this,new QEvent(m_nPackEvent));}return true;
}
同时,在消费时,一次性获取队列,并清空。这样减少锁的碰撞。
void taskNode::flush_write()
{m_mtx_queue.lock();QList<taskNode *> write_queue = m_write_queue;QList<qsizetype> write_pos = m_write_pos;QByteArrayList write_cmd = m_write_cmd;m_write_queue.clear();m_write_pos.clear();m_write_cmd.clear();//qDebug()<<write_queue.size();m_mtx_queue.unlock();while (write_queue.size()){taskNode * node = write_queue.first();qsizetype pos = write_pos.first();write_queue.pop_front();write_pos.pop_front();QByteArray & arr = node->get_stdin_array(pos);m_process->write(arr.constData(),sz);--cnt;}
小结
尽管采用了环形队列,由于在QProcess上还是存在mem-alloc,这使得峰值状态下CPU占用还是很大的。整体速率距离PCI总线和DDR4的能力还相去甚远,即使和bash直接管道连接相比,也有不小的差距。不过,为了构造灵活的管道吞吐能力,允许数据被多对多流转和反馈回环,损失一些性能也差强人意。
1. 性能表现情况
通过上述操作,taskBus的吞吐能力得到了保证,在只使用用户态的内存操作情况下,缓存64MB时,可以获得10Gbps以上的性能。在Linux下,可达 24Gbps,即3GBps1。
平台 | 系统 | 峰值吞吐 | 单路流量 | 平均来回延迟 |
---|---|---|---|---|
i7-10700U1 | Linux x64 | 3354MBps | 1340MBps | 1ms |
i7-6700K | Linux x64 | 2844MBps | 1050MBps | 2.2ms |
i7-10700U2 | win10 home x64 | 1561MBps | 400MBps | 22ms |
i7-6700K | win10 home x64 | 1345MBps | 340MBps | 40ms |
RaspberryPi 4(8GB) | Rasbain 64 | 263MBps | 102MBps | 6ms |
上表是运行双进程点对点双向PING的状态下达到的。多进程下,总速率会稍高。可以发现,同样的硬件配置下:
- windows下taskBus的吞吐能力要比Linux少1倍
- windows下taskBus的带宽利用率要低于Linux,总速率/单路速率Linux更优。
- windows下的延迟更大。
这是非常奇怪的现象,讲起来windows应该更快才对。对于里面的细节原因,只有后面慢慢研究了。
2. 主要改进和局限
与2024年8月版本相比,少了一层生产者–>消费者的memcpy,转而只是传递int类型的索引,在生产者:消费者个数=1:N的情况下,吞吐能力会得到较大提高。这种memcpy次数的降低,对于老旧CPU影响更大,即使在2进程互PING(1:1)的测试中,也能达到 20-30%的提速。
同时要注意到,3GBps已经很接近用户态内存的吞吐极限。若要追求极为苛刻的传输,需要按照文章开始的链接里的vmsplice zero-copy来定制,取得额外10-20倍的性能提升。
3. 源码和发行版
源码和发行版参考
GitCode.net
或者
GitCode.com
i7-10700U是一个笔记本上的低功耗cpu,在最大睿频4GHz下的Manjaro Linux系统上,3GBps持续了5秒。由于温度上升,温度墙导致频率下降到1.8GHz,速度降低1倍。 ↩︎ ↩︎
i7-10700U是一个笔记本上的低功耗cpu,通过在windows-10下去除温度墙,可以在97摄氏度的高温状态下,保持在4GHz,维持1.5GBps的流量。 ↩︎
相关文章:

基于准静态自适应环型缓存器(QSARC)的taskBus万兆吞吐实现
文章目录 概要整体架构流程技术名词解释技术细节1. 数据结构2. 自适应计算队列大小3. 生产者拼接缓存4. 高效地通知消费者 小结1. 性能表现情况2. 主要改进和局限3. 源码和发行版 概要 准静态自适应环形缓存器(Quasi-Static Adaptive Ring Cache)是task…...

C++笔记---指针常量和常量指针
巧记方法(方法来自于网络出处忘记了):const读作常量,*读作指针,按顺序读即可。例如: const int * ptr; //const在前*在后读作常量指针 const * int ptr; //const在前*在后读作常量指针 int * const prt; /…...

Python习题 177:设计银行账户类并实现存取款功能
(编码题)Python 实现一个简单的银行账户类 BankAccount,包含初始化方法、存款、取款、获取余额等功能。 参考答案 分析需求如下。 Python 类 BankAccount,用于模拟银行账户的基本功能。该类应包含以下方法: 初始化方法: 接受两个参数:account_holder(账户持有人的姓…...

IPhone 16:它的 “苹果智能 “包括哪些内容?
IPhone 16 的发布让科技界看到了该公司的人工智能产品 “苹果智能”(Apple Intelligence)究竟能做些什么。 苹果公司发布了拥有人工智能硬件升级的最新款 iPhone 16,进一步进军人工智能领域。苹果公司首席执行官蒂姆-库克(Tim Coo…...

【中国国际航空-注册/登录安全分析报告】
前言 由于网站注册入口容易被黑客攻击,存在如下安全问题: 1. 暴力破解密码,造成用户信息泄露 2. 短信盗刷的安全问题,影响业务及导致用户投诉 3. 带来经济损失,尤其是后付费客户,风险巨大,造…...

【ArcGIS】栅格计算器原理及案例介绍
ArcGIS:栅格计算器原理及案例介绍 栅格计算器(Raster Calculator)原理介绍案例案例1:计算栅格数据平均值 参考 栅格计算器(Raster Calculator)原理介绍 描述:在类似计算器的界面中,…...

LOOKUP函数和VLOOKUP函数知识讲解与案例演示
〇、需求 在 Excel 文档中,根据查找值从查找域和结果域构成的数组中,找到对应的结果值。 一、知识点讲解 LOOKUP函数(比较常用,推荐)和VLOOKUP函数 两个公式都可以实现上述需求。 1. LOOKUP 函数 1.1 单个查询条件…...

Java技术深度探索:高并发场景下的线程安全与性能优化
Java技术深度探索:高并发场景下的线程安全与性能优化 在当今的软件开发领域,随着互联网应用的日益复杂和用户量的激增,高并发成为了一个不可忽视的技术挑战。Java,作为一门广泛应用于企业级开发的编程语言,其内置的并发支持机制如线程(Thread)、锁(Lock)、并发集合(…...

Vulnhub-RickdiculouslyEasy靶场(9个flag)
flag1 端口9090有一个flag flag2 13337端口 flag3 使用dirb进行扫描网站的80端口,发现一些敏感文件 访问80端口,没有发现有效信息 访问passwords目录 访问FLAG.txt 再返回访问passwords.html文件 查看页面源代码发现一个密码 flag4 之前扫描到了robo…...

Android Studio Menu制作
文章目录 在Activity上新建onCreateOptionsMenu新建menu目录及资源文件新建Menu一级菜单在Activity上加载Menu 在Activity上新建onCreateOptionsMenu Overridepublic boolean onCreateOptionsMenu(Menu menu) {return super.onCreateOptionsMenu(menu);}新建menu目录及资源文件…...

【mybatis】使用模糊查询时报错:Encountered unexpected token: “?“ “?“
报错信息如下: Mapper.xml报错代码: AND HILIST_NAME like %#{hilistName}% 解决方案: 把模糊查询的 sql 语句改为使用 CONCAT 命令拼接, 就不会报错了。 AND HILIST_NAME like CONCAT(%, #{hilistName},%)...

【Linux】文件权限与类型全解:你的文件安全指南
欢迎来到 CILMY23 的博客 🏆本篇主题为:文件权限与类型全解:你的文件安全指南 🏆个人主页:CILMY23-CSDN博客 🏆系列专栏:Python | C | C语言 | 数据结构与算法 | 贪心算法 | Linux | 算法专题…...

解析DNS查询报文,探索DNS工作原理
目录 1. 用 tcpdump工具监听抓包 2. 用 host 工具获取域名对应的IP地址 3. 分析DNS以太网查询数据帧 3.1 linux下查询DNS服务器IP地址 3.2 DNS以太网查询数据帧 (1)数据链路层 (2)网络层 (3)传输层…...

Unity让摄像机跟随物体的方法(不借助父子关系)
在Unity中,不使用子对象的方式让相机跟随物体移动,我们通过编写脚本来实现。下面放一个从工程中摘出来的的C#脚本示例,用于将相机绑定到一个Target对象上并跟随其移动: using UnityEngine; public class FollowCamera : MonoBeh…...

misc音频隐写
一、MP3隐写 (1)题解:下载附件之后是一个mp3的音频文件;并且题目提示keysyclovergeek;所以直接使用MP3stego对音频文件进行解密;mp3stego工具是音频数据分析与隐写工具 (2)mp3stego工具的使用:…...

如何启动网络安全计划:首先要做的事情
目录 数据分类:网络安全的基石 为什么它很重要? 如何对数据进行分类? 风险分析 什么是风险分析? 如何进行风险分析? 业务影响分析 (BIA) BIA 的用途是什么? BIA 是如何进行的? 安全解…...

Java零基础-三维数组详解!
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互…...

数据分析-20-时间序列预测之基于PyTorch的LSTM数据准备及模型训练流程
文章目录 1 数据加载2 去除异常值3 数据归一化4 切分窗口5 制作数据集加载器6 定义模型7 训练模型8 模型评估9 参考附录1 数据加载 参考数据集kaggle下载DailyDelhiClimate import pandas as pd import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] = SimHei # 设…...

vue2中使用web worker启动定时器
vue2中使用web worker启动定时器,避免浏览器最小化或切换标签页时定时器不能按设定周期执行【一般是周期小于60s时,大于60s一般可正常执行】 1、添加worker-loader2、修改vue.config.js3、创建timer.worker.js4、创建TimerWorker.js5、使用TimerWorker启…...

【Python 学习】Numpy的基础和应用
目录 1 数组基础1.1 Numpy简介1.2 Numpy数组基础1.3 创建数组1.3.1 使用np.array()函数生成数组1.3.2 利用内置函数产生特定形式的数组1.3.2.1 简单内置函数1.3.2.2 特殊内置函数 1.3.3 生成随机数组 1.4 数组的数据类型1.5 数组的迭代1.6数组的索引和切片1.6.1 一维数组的索引…...

基于python+django+vue+MySQL的酒店推荐系统
作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】pythondjangovueMySQL的酒店推…...

什么是 PD 电压诱骗?
在这篇博客中,我们将深入了解 PD 电压诱骗 的概念,解释其工作原理,并通过简单的例子来帮助你理解整个过程。虽然看起来复杂,但我会尽量用通俗易懂的方式讲解每一个知识点。 什么是 PD 协议?要理解电压诱骗,我们首先需要知道什么是 PD 协议。 PD 协议(Power Delivery 协…...

【漏洞复现】用友 NC pagesServlet Sql注入漏洞
免责声明: 本文内容旨在提供有关特定漏洞或安全漏洞的信息,以帮助用户更好地了解可能存在的风险。公布此类信息的目的在于促进网络安全意识和技术进步,并非出于任何恶意目的。阅读者应该明白,在利用本文提到的漏洞信息或进行相关测…...

边缘检测运用
文章目录 一、简介1.边缘检测的概念2.边缘检测的目的 二、代码实现三、边缘检测的方法1.1Canny边缘检测器1.2.Canny代码实现2.1Sobel边缘检测器2.2Sobel代码实现3.1Laplacian边缘检测器3.2Laplacian代码实现4.1Scharr边缘检测器4.2Scharr代码实现 四、边缘检测的应用 一、简介 …...

应用宝自动下载安装
import uiautomator2 as u2 from threading import Thread import logging import sys import os loggerlogging.getLogger("uiautomator2") logger.setLevel(logging.INFO) d u2.connect()"""下载模块""" class yingyongbao(object…...

Vue 2 中实现双击事件的几种方法
在 Vue 2 中处理用户交互,特别是双击事件,是一个常见的需求。Vue 提供了一种简洁的方式来绑定事件,包括双击事件。本文将介绍几种在 Vue 2 中实现双击事件的方法。 1. 使用 dblclick 指令 Vue 允许你直接在模板中使用 dblclick 指令来监听双…...

windows服务管理插件 nssm
NSSM是一个windows下服务管理插件,可以填加、删除、启动、停止服务 1.下载 官网:http://nssm.cc 下载页面:http://nssm.cc/download 直接下载:http://nssm.cc/release/nssm-2.24.zip 2.食用 以填加php8.2为例 2.1.将nssm.ex…...

【读书笔记-《30天自制操作系统》-19】Day20
本篇的内容围绕系统调用展开。为了让应用程序能够调用操作系统功能,引入了系统调用以及API的概念。首先实现了显示单个字符的API,让应用程序通过传递地址的方式进行调用;接下来又改进为通过中断的方式进行调用。在此基础上继续实现了显示字符…...

Kubernetes服务注册与发现
Kubernetes服务注册与发现 1、服务注册2、服务发现2.1 DNS服务发现2.2 环境变量(较少使用)💖The Begin💖点点关注,收藏不迷路💖 在Kubernetes中,服务注册与发现确保了Pod间的高效通信。 1、服务注册 当创建Service时,其信息被存储在Kubernetes的ETCD数据库中。Pod…...

【 html+css 绚丽Loading 】000047 玄武流转盘
前言:哈喽,大家好,今天给大家分享htmlcss 绚丽Loading!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏关注哦 💕…...