MySQL主从延时问题

过线上 MySQL 维护经验的童鞋都知道,主从延迟往往是一个让人头疼不已的问题。

不仅仅是其造成的潜在问题比较严重,而且主从延迟原因的定位尤其考量 DBA 的综合能力:既要熟悉复制的内部原理,又能解读主机层面的资源使用情况,甚至还要会分析 binlog。


导致主从延迟的一个常见原因是,对于 binlog 中的事务,从库上只有一个 SQL 线程进行重放,而这些事务在主库中是并发写入的。

就好比你多个人(多线程)挖坑,我一个人(单线程)来填,本来就双拳难敌四手,在你挖坑速度不快的情况下,我尚能应付。一旦你稍微加速,我则力有不逮,只能眼睁睁地看着你挖的坑越来越深。

具体在 MySQL 中,则意味着 Seconds_Behind_Master 的值越来越大。

本文主要包括以下几部分:

  1. 主从延迟的危害

  2. 并行复制方案简介

  3. MySQL 5.7 基于组提交的并行复制方案,包括 Commit-Parent-Based 方案和 Lock-Based 方案

  4. MySQL 8.0 基于 WRITESET 的并行复制方案

  5. 对 COMMIT_ORDER,WRITESET_SESSION,WRITESET 这三种方案的压测结果

  6. 如何开启并行复制

一、主从延迟的危害

主从延迟带来的问题,主要体现在以下两个方面:

1、对于读写分离的业务,主从延迟意味着业务会读到旧数据。

2、主从延迟过大,会影响数据库的高可用切换。这一点尤其需要注意。

  • 如果等待从库应用完差异的 binlog 才做高可用切换,无疑会影响数据库服务的可用性。

  • 如果不等待,直接切换,则意味着没应用完的这部分 binlog 的数据会丢失,业务不一定能接受这种情况。

二、并行复制方案简介

MySQL官方先后提出了多个不同的并行复制方案,具体如下。

  1. MySQL 5.6 基于库级别的并行复制方案。

  2. MySQL 5.7 基于组提交的并行复制方案。

  3. MySQL 8.0 基于 WRITESET 的并行复制方案。

因为线上大部分环境都是单库多表的,所以基于库级别的并行复制实际上用得并不多。

下面,重点看看后两个方案的实现原理。

三、基于组提交的并行复制方案

MySQL 5.7 基于组提交的并行复制方案,先后经历了两个版本的迭代:Commit-Parent-Based 方案和 Lock-Based 方案。

3.1 Commit-Parent-Based 方案

MySQL 会将一个事务拆分为两个阶段进行处理:Prepare 阶段和 Commit 阶段。

另外,InnoDB 使用的锁机制是悲观锁。在悲观锁中,事务是在操作之初执行加锁操作,如果锁资源被其它事务占用了,则该事务会被阻塞。

基于这两点,我们不难推断出,两个事务如果都进入了 Prepare 阶段,则意味着它们之间是没有锁冲突的,在从库重放时可并行执行。这就是 Commit-Parent-Based 方案的核心思想。

具体实现上:

1、主库有个全局计数器(global counter),每次在事务存储引擎层提交之前,都会增加这个计数器。

2、在事务进入 Prepare 阶段之前,会将全局计数器的当前值记录在事务中,这个值称为事务的 commit-parent。

3、这个 commit-parent 会写入 binlog,记录在事务的头部。

4、从库重放时,如果发现两个事务的 commit-parent 相同,会并行执行这两个事务。

以下面这 7 个事务为例,看看这 7 个事务在从库的并行执行情况。

Trx1 ------------P----------C-------------------------------->                            |Trx2 ----------------P------+---C---------------------------->                            |   |Trx3 -------------------P---+---+-----C---------------------->                            |   |     |Trx4 -----------------------+-P-+-----+----C----------------->                            |   |     |    |Trx5 -----------------------+---+-P---+----+---C------------->                            |   |     |    |   |Trx6 -----------------------+---+---P-+----+---+---C---------->                            |   |     |    |   |   |Trx7 -----------------------+---+-----+----+---+-P-+--C------->                            |   |     |    |   |   |  |

示例中的 Trx 指的是事务,P 指的是事务在进行 Prepare 阶段之前,读取 commit-parent 的时间点。C 指的是事务在进行 Commit 阶段之前,增加全局计数器的时间点。

下面看看这 7 个事务的并行执行情况。

  • Trx1、Trx2、Trx3 并行执行

  • Trx4 串行执行

  • Trx5、Trx6 并行执行

  • Trx7 串行执行

这在很大程度上实现了并行,但还不够完美。

实际上,Trx4、Trx5、Trx6 可并行执行,因为它们同时进入了 Prepare 阶段。同理,Trx6、Trx7 也可并行执行。

基于此,官方迭代了并行复制方案,推出了新的 Lock-Based 方案。

3.2 Lock-Based 方案

该方案引入了锁区间(locking interval)的概念,锁区间定义了一个事务持有锁的时间范围。具体来说,

  1. 将 Prepare 阶段,最后一个 DML 语句获取锁的时间点,定义为锁区间的开始点

  2. 将存储引擎层提交之前,锁释放的时间点,定义为锁区间的结束点

如果两个事务的锁区间存在交集,则意味着这两个事务没有锁冲突,可并行重放。例如,

Trx1 -----L---------C------------>Trx2 ----------L---------C------->

反之,则不可并行重放,例如,​​​​​​​

Trx1 -----L----C----------------->Trx2 ---------------L----C------->

这里的 L 代表锁区间的开始点,C 代表锁区间的结束点。

在具体实现上,主库引入了以下 4 个变量:

  1. global.transaction_counter:事务计数器

  2. transaction.sequence_number:事务序列号

在事务进入 Prepare 阶段之前,会将 global.transaction_counter 自增加 1 并赋值给 transaction.sequence_number。

transaction.sequence_number = ++global.transaction_counter

序列号不是一直递增的,每切换一个 binlog,都会将 transaction.sequence_number 重置为 1。

3、global.max_committed_transaction:当前已提交事务的最大序列号。

在事务进行存储引擎层提交之前,会取 global.max_committed_transaction 和当前事务的 sequence_number 的最大值,赋值给 global.max_committed_transaction。​​​​​​​

global.max_committed_transaction = max(global.max_committed_transaction,                                           transaction.sequence_number)

4、transaction.last_committed:在事务进入 Prepare 阶段之前,已提交事务的最大序列号

transaction.last_committed = global.max_committed_transaction

在这 4 个变量中,transaction.sequence_number 和 transaction.last_committed 会写入 binlog。

具体来说,对于 GTID 复制,它们会写入 GTID_LOG_EVENT;对于非 GTID 复制,则写入 ANONYMOUS_GTID_LOG_EVENT 。

对于示例中的 7 个事务,记录在 binlog 中的 last_committed、sequence_number 如下所示:​​​​​​​

Trx1: last_committed=0 sequence_number=1Trx2: last_committed=0 sequence_number=2Trx3: last_committed=0 sequence_number=3Trx4: last_committed=1 sequence_number=4Trx5: last_committed=2 sequence_number=5Trx6: last_committed=2 sequence_number=6Trx7: last_committed=5 sequence_number=7
3.3 从库并行重放的逻辑

下面说说从库并行重放的逻辑。

从库引入了一个事务队列( transaction_sequence ),包含了当前正在执行的事务。

该队列是有序的,按照事务的 sequence_number 从小到大排列。这个队列中的事务可并行执行。

一个新的事务能否插入这个队列,唯一的判断标准是,事务的 last_committed 是否小于队列中第一个事务的 sequence_number。只有小于才允许插入。

transaction.last_committed < transaction_sequence[0].sequence_number

最后,回到示例中的 7 个事务,结合 binlog 中的 last_committed 和 sequence_number,我们看看这 7 个事务的并行执行情况。

  • Trx1、Trx2、Trx3 并行执行

  • Trx1 执行完毕后,Trx4 可加入队列

  • Trx2 执行完毕后,Trx5、Trx6 可加入队列

  • Trx5 执行完毕后,Trx7 可加入队列

不难发现,相对于 Commit-Parent-Based 方案,Lock-Based 方案的并行度确实大大提高了。

3.4 组提交方案小结

无论是 Commit-Parent-Based 方案,还是 Lock-Based 方案,依赖的都是组提交(Group Commit)。

组提交方案有以下两个特点:

  1. 适用于高并发场景。因为只有在高并发场景下,才会有更多的事务放到一个组(Group)中提交。

  2. 在级联复制中,层级越深,并行度越低。

针对低并发场景,如果要提升从库的并行效率,可调整以下两个参数:

binlog_group_commit_sync_delay

binlog 刷盘(fsync)之前等待的时间。单位微秒,默认为 0,不等待。

该值越大,一个组内的事务就越多,相应地,从库的并行度也就越高。但该值越大,客户端的响应时间也会越长。

binlog_group_commit_sync_no_delay_count

在 binlog_group_commit_sync_delay 时间内,允许等待的最大事务数。

如果 binlog_group_commit_sync_delay 设置为 0,则此参数无效。

四、WRITESET 方案

MySQL 8.0 推出了 WRITESET 方案。该方案推出的初衷实际上是为 Group Replication 服务的,主要是用于认证阶段(Certification)的冲突检测。

WRITESET 方案的核心思想是,两个来自不同节点的并发事务,只要没修改同一行,就不存在冲突。对于没有冲突的并发事务,在写入relay log 中时,可以共享一个 last_committed。

这里的冲突检测,实际上比较的是两个事务之间的写集合(writeset)。

注意,writeset 和 WRITESET 两者的区别,前者指的是事务的写集合,后者则特指 WRITESET 方案。

4.1 事务写集合的生成过程

下面来看看事务 writeset 的生成过程。具体步骤如下:

1、首先提取被修改行的主键、唯一索引、外键信息。一张表,如果有主键和一个唯一索引,则每修改一行,会提取两条约束信息:一条针对主键,另一条针对唯一索引。针对主键的,提取的信息包括主键名、库名、表名、主键值,这些信息会拼凑为一个字符串。

2、计算该字符串的哈希值,具体的哈希算法由 transaction_write_set_extraction 参数指定。

3、将计算后的哈希值插入当前事务的写集合。

4.2  WRITESET 方案的实现原理

接下来,结合源码看看 WRITESET 方案的实现原理。

 
void Writeset_trx_dependency_tracker::get_dependency(THD *thd,                                                     int64 &sequence_number,                                                     int64 &commit_parent) {  Rpl_transaction_write_set_ctx *write_set_ctx =      thd->get_transaction()->get_transaction_write_set_ctx();  std::vector<uint64> *writeset = write_set_ctx->get_write_set();
#ifndef NDEBUG  /* 空事务的写集合必须为空 */  if (is_empty_transaction_in_binlog_cache(thd)) assert(writeset->size() == 0);#endif
  /*    判断一个事务能否使用 WRITESET 方案  */  bool can_use_writesets =      // 事务写集合的大小不为 0 或者事务为空事务      (writeset->size() != 0 || write_set_ctx->get_has_missing_keys() ||       is_empty_transaction_in_binlog_cache(thd)) &&      // 事务的 transaction_write_set_extraction 必须与全局设置一致      (global_system_variables.transaction_write_set_extraction ==       thd->variables.transaction_write_set_extraction) &&      // 不能被其它表外键关联      !write_set_ctx->get_has_related_foreign_keys() &&      // 事务写集合的大小不能超过 binlog_transaction_dependency_history_size      !write_set_ctx->was_write_set_limit_reached();  bool exceeds_capacity = false;
  if (can_use_writesets) {    /*     检查 m_writeset_history 加上事务写集合的大小是否超过 m_writeset_history 的上限,     m_writeset_history 的上限由参数 binlog_transaction_dependency_history_size 决定     */    exceeds_capacity =        m_writeset_history.size() + writeset->size() > m_opt_max_history_size;
    /*     计算所有冲突行中最大的 sequence_number,并将被修改行的哈希值插入 m_writeset_history    */    int64 last_parent = m_writeset_history_start;    for (std::vector<uint64>::iterator it = writeset->begin();         it != writeset->end(); ++it) {      Writeset_history::iterator hst = m_writeset_history.find(*it);      if (hst != m_writeset_history.end()) {        if (hst->second > last_parent && hst->second < sequence_number)          last_parent = hst->second;
        hst->second = sequence_number;      } else {        if (!exceeds_capacity)          m_writeset_history.insert(              std::pair<uint64, int64>(*it, sequence_number));      }    }    // 如果表上都存在主键,则会取 last_parent 和 commit_parent 的较小值作为事务的 commit_parent。if (!write_set_ctx->get_has_missing_keys()) {      commit_parent = std::min(last_parent, commit_parent);    }  }
  if (exceeds_capacity || !can_use_writesets) {    m_writeset_history_start = sequence_number;    m_writeset_history.clear();  }}

该函数的处理流程如下:

  1. 调用函数时,会传入事务的 sequence_number,commit_parent(last_committed),这两个值是基于 Lock-Based 方案生成的

  2. 获取事务的写集合。可以看到,事务的写集合是数组类型

  3. 判断一个事务能否使用 WRITESET 方案

以下场景不能使用 WRITESET 方案,此时,只能使用 Lock-Based 方案生成的 last_committed。

  • 事务没有写集合。常见的原因是表上没有主键

  • 当前事务 transaction_write_set_extraction 的设置与全局不一致

  • 表被其它表外键关联

  • 事务写集合的大小超过 binlog_transaction_dependency_history_size

4、如果能使用 WRITESET 方案。

4.1、首先判断 m_writeset_history 的容量是否超标。

具体来说,m_writeset_history + writeset 的大小是否超过 binlog_transaction_dependency_history_size 的设置。

4.2、将 m_writeset_history_start 赋值给变量 last_parent。

m_writeset_history_start 代表不在 m_writeset_history 中最后一个事务的 sequence_number,其初始值为 0。

当参数 binlog_transaction_dependency_tracking 发生变化或清空 m_writeset_history 时,会更新 m_writeset_history_start。

4.3、循环遍历事务的写集合,判断被修改行对应的哈希值是否在 m_writeset_history 存在。

若存在,则意味着 m_writeset_history 存在同一行的操作。既然是同一行的不同操作,自然就不能并行重放。这个时候,会将 m_writeset_history 中该行的 sequence_number 赋值给 last_parent。

需要注意的是,这里会循环遍历完事务的写集合,毕竟这个事务中可能有多条记录在 m_writeset_history 中存在。

在遍历的过程中,会判断 m_writeset_history 中冲突行的 sequence_number 是否大于 last_parent,只有大于才会赋值。换言之,这里会取所有冲突行中最大的 sequence_number,赋值给 last_parent。

若不存在,则判断 m_writeset_history 的容量是否超标,若不超标,则会将被修改行的哈希值插入 m_writeset_history。

可以看到,m_writeset_history 是个字典类型。其中 key 存储的是被修改行的哈希值,value 存储的是事务的 sequence_number。

5、判断被操作的表上是否都存在主键

若存在,才会取 last_parent 和 commit_parent 的较小值作为事务的 commit_parent。否则,使用的还是 Lock-Based 方案生成的commit_parent。

6、如果 m_writeset_history 容量超标或者事务不能使用 WRITESET 方案,则会将当前事务的 sequence_number 赋值给m_writeset_history_start,同时清空 m_writeset_history。

4.3 WRITESET 方案的相关参数

下面看看 WRITESET 方案的三个参数。

binlog_transaction_dependency_tracking

指定基于何种方案决定事务的依赖关系。对于同一个事务,不同的方案可生成不同的 last_committed。

该参数有以下取值:

  • COMMIT_ORDER:基于 Lock-Based 方案决定事务的依赖关系。

  • 默认值。

  • WRITESET:基于 WRITESET 方案决定事务的依赖关系。

  • WRITESET_SESSION:同 WRITESET 类似,只不过同一个会话中的事务不能并行执行。

transaction_write_set_extraction

指定事务写集合的哈希算法,可设置的值有:OFF,MURMUR32,XXHASH64(默认值)。

对于 Group Replication,该参数必须设置为 XXHASH64。

注意,若要将 binlog_transaction_dependency_tracking 设置为 WRITESET 或 WRITESET_SESSION,则该参数不能设置为 OFF。

binlog_transaction_dependency_history_size

m_writeset_history 的上限,默认 25000。

一般来说,binlog_transaction_dependency_history_size 越大,m_writeset_history 能存储的行的信息就越多。在不出现行冲突的情况下,m_writeset_history_start 也会越小。相应地,新事务的 last_committed 也会越小,在从库重放的并发度也会越高。

五、压测结果

接下来,看看 MySQL 官方对于 COMMIT_ORDER,WRITESET_SESSION,WRITESET 这三种方案的压测结果。

主库环境:16 核,SSD,1个数据库,16 张表,共 800w 条数据。

压测场景:OLTP Read/Write, Update Indexed Column 和 Write-only。

压测方案:在关闭复制的情况下,在不同的线程数下,注入 100w 个事务。开启复制,观察不同线程数下,不同方案的从库重放速度。

三个场景下的压测结果如图所示。

图片

图片

图片

分析压测结果,我们可以得出以下结论。

对于 COMMIT_ORDER 方案,主库并发度越高,从库的重放速度越快。

对于 WRITESET 方案,主库的并发线程数对其几乎没有影响。甚至,单线程下 WRITESET 的重放速度都超过了 256 线程下的COMMIT_ORDER。

与 COMMIT_ORDER 一样,WRITESET_SESSION 也依赖于主库并发。

只不过,在主库并发线程数较低(4 线程、8 线程)的情况下,WRITESET_SESSION 也能实现较高的吞吐量。

六、如何开启并行复制

在从库上设置以下三个参数。​​​​​​​

slave_parallel_type = LOGICAL_CLOCKslave_parallel_workers = 16slave_preserve_commit_order = ON

下面看看这三个参数的的具体含义。

slave_parallel_type

设置从库并行复制的类型。该参数有以下取值:

  • DATABASE:基于库级别的并行复制。MySQL 8.0.27 之前的默认值

  • LOGICAL_CLOCK:基于组提交的并行复制

slave_parallel_workers

设置 Worker 线程的数量。开启了多线程复制,原来的 SQL 线程将演变为 1 个 Coordinator 线程和多个 Worker 线程。

slave_preserve_commit_order

事务在从库上的提交顺序是否与主库保持一致,建议开启。

需要注意的是,调整这三个参数,需要重启复制才能生效。

从 MySQL 5.7.22、MySQL 8.0 开始,可使用 WRITESET 方案进一步提升并行复制的效率,此时,需在主库上设置以下参数。​​​​​​​

binlog_transaction_dependency_tracking = WRITESET_SESSIONtransaction_write_set_extraction = XXHASH64binlog_transaction_dependency_history_size = 25000binlog_format = ROW

注意,基于 WRITESET 的并行复制方案,只在 binlog 格式为 ROW 的情况下才生效。

七、参考资料

  1. WL#6314: MTS: Prepared transactions slave parallel applier:

    https://dev.mysql.com/worklog/task/?id=6314

  2. WL#6813: MTS: ordered commits (sequential consistency):

    https://dev.mysql.com/worklog/task/?id=6813

  3. WL#7165: MTS: Optimizing MTS scheduling by increasing the parallelization window on master:

    https://dev.mysql.com/worklog/task/?id=7165

  4. WL#8440: Group Replication: Parallel applier support:

    https://dev.mysql.com/worklog/task/?id=8440

  5. WL#9556: Writeset-based MTS dependency tracking on master:

    https://dev.mysql.com/worklog/task/?id=9556

  6. WriteSet并行复制:

    https://www.jianshu.com/p/616703533310

  7. Improving the Parallel Applier with Writeset-based Dependency Tracking:

    https://mysqlhighavailability.com/improving-the-parallel-applier-with-writeset-based-dependency-tracking/

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用PHP编写采集药品官方数据的程序

目录 一、引言 二、程序设计和实现 1、确定采集目标 2、使用PHP的cURL库进行数据采集 3、解析JSON数据 4、数据处理和存储 5、数据验证和清理 6、数据输出和可视化 7、数据分析和挖掘 三、注意事项 1、合法性原则 2、准确性原则 3、完整性原则 4、隐私保护原则 …

如何在 Windows 10/11 上高质量地将 WAV 转换为 MP3

WAV 几乎完全准确地存储了录音硬件所听到的内容&#xff0c;这使得它变得很大并占用了更多的存储空间。因此&#xff0c;WAV 格式在作为电子邮件附件发送、保存在便携式音频播放器上、通过蓝牙或互联网从一台设备传输到另一台设备等时可能无法正常工作。 如果您遇到 WAV 问题&…

【Android Studio调试报错】setContentView(R.layout.activity_main);

报错如下&#xff1a; 解决方法&#xff1a; 1、把参数删除到只剩 .&#xff0c;用自动补齐的方式来查看当前文件的位置是不是&#xff0c;当前左侧工程中layout 所在的位置。在的话它会在自动补齐列表有选项。否则我们选中第一个。 2、选中之后是这样的 然后问题解决&#xf…

飞天使-url路由进阶应用

url 的name属性 为 app01的url设定命名空间 app_name app01urlpatterns [path(, views.index, nameindex),path(login/, views.login, namelogin), ] 上面的namelogin 同时 from django.shortcuts import render,reverse,redirect 便于项目引用 def index(request):userna…

spring cloud-注册中心(Eureka)

一、服务注册中心组件(*) 定义&#xff1a;服务注册中心就是在整个微服务架构单独抽取一个服务&#xff0c;该服务不做项目中任何业务功能&#xff0c;仅用来在微服务中记录微服务、对微服务进行健康状态检查&#xff0c;及服务元数据信息存储常用的注册中心&#xff1a;eurek…

用placement label代替keep margin解决绕线问题

我正在「拾陆楼」和朋友们讨论有趣的话题&#xff0c;你⼀起来吧&#xff1f; 拾陆楼知识星球入口 通常我们用keepout margin去降低多pin cell类型的密度&#xff0c;这里提供一种替代方案&#xff0c;即使用placement label。好处是只限制多pin cell彼此间距&#xff0c;不会…

echarts官网卡?

全网echarts案例资源大总结和echarts的高效使用技巧&#xff08;细节版&#xff09; - 掘金 drawnLine() {let myChart echarts.init(document.getElementById("grade"));// 绘制图表myChart.setOption({title: {left: "center",},tooltip: {trigger: &qu…

2023.11.13 Spring Bean 的生命周期

目录 Spring 执行流程 Bean 的生命周期 五个阶段 深入理解 Bean 初始化 实例理解 总结梳理 经典面试题 Spring 执行流程 Bean 的生命周期 Spring 中 Bean 的生命周就是 Bean 在 Spring 中从创建到销毁的整个过程 五个阶段 1. 实例化 Bean 为 Bean 对象分配内存空间 …

千万富翁分享:消费多少免单多少,电商运营高手实战秘籍拆解

千万富翁分享&#xff1a;消费多少免单多少&#xff0c;电商运营高手实战秘籍拆解 后疫情时代&#xff0c;国内电商圈层进程依然是在高速发展阶段&#xff0c;今年2023年双十一也彻底落下帷幕&#xff0c;但这次相较于往常却没有公布具体的成交规模数据&#xff0c;那么&#x…

程序员,你的护城河挖好了吗?

程序员的护城河 在遥远的古代&#xff0c;护城河是一种防御工事&#xff0c;通常用于保护城市或城堡免受外部攻击。它是由人工挖掘或天然形成的河流、壕沟或城墙等&#xff0c;可以作为防御屏障&#xff0c;阻止敌人的进入。 而对于程序员而言&#xff0c;“护城河”是一种比喻…

Linux线程池

文章目录&#xff1a; 线程池了解线程池模拟实现 线程池了解 线程池是一种常见的线程使用模式。线程过多会带来调度开销&#xff0c;进而影响缓存局部性和整体性能。而线程池维护着多个线程&#xff0c;等待着监督管理者分配可并发执行的任务&#xff0c;以避免在处理短时间任…

Word或者WPS批量调整文中图片大小的快捷方法

文章目录 0、前言1、编写宏代码2、在文档中调用宏实现一键批量调整3、就这么简单&#xff01; 0、前言 不知道大家是不是也和我一样&#xff0c;经常需要在编写的Word&#xff08;或者WPS&#xff09;文档里插入大量的图片&#xff0c;但是这些图片的尺寸大小一般都不一样&…

2D 3D 工业组态技术 meta2d JavaScript

本心、输入输出、结果 文章目录 2D 3D 工业组态技术 meta2d JavaScript前言2D 3D 工业组态技术 meta2d JavaScript 简介2D 3D 工业组态技术 meta2d JavaScript 特性丰富的组态能力0代码数据通信组态的应用多端适配能力强大的扩展能力追求卓越性能丰富的组件库资源广泛的应用场景…

致刘家窑中医院龚洪海医生:患者的感谢与敬意

你们好!我曾经是咱们这的一名患者&#xff0c;我叫李刚&#xff0c;今年45岁&#xff0c;不知道你们还有印象吗?我曾去过一些医院进行就诊&#xff0c;但都没有得到恰当的治疗&#xff0c;症状一直没有消失。得了这个病之后对我的生活以及工作打击都十分的大。经朋友介绍说刘家…

【Linux系统编程十七】:(基础IO4)--文件系统(inode与软硬链接)

【Linux系统编程十六】&#xff1a;文件系统&#xff08;inode与软硬链接&#xff09; 一.磁盘硬件二.文件系统(inode)三.软硬链接 一.磁盘硬件 Linux下的文件在磁盘中存储&#xff0c;文件的内容和属性是分开存储的&#xff01; 文件的内容存储在数据块。 文件的属性存储在in…

专业的软件第三方检测机构如何做性能测试?收费标准是多少?

随着软件信息技术的飞速发展&#xff0c;人们对于软件产品越来越依赖&#xff0c;从而用户对软件产品的稳定性和质量问题愈发看重。软件系统性能的好坏将严重影响该软件的质量和软件开发者的利益&#xff0c;为了更好的保障软件产品质量&#xff0c;软件企业会将性能测试交由软…

python 爬虫之urllib 库的相关模块的介绍以及应用

文章目录 urllib.request 模块打开 URL&#xff1a;发送 HTTP 请求&#xff1a;处理响应&#xff1a; 应用如何读取并显示网页内容提交网页参数使用HTTP 代理访问页面 urllib.request 模块 在 Python 中&#xff0c;urllib.request 模块是用于处理 URL 请求的标准库模块之一。…

vscode删除后重装还有原来的配置问题,彻底删除vscode,删除vscode安装过的插件和缓存

VSCode卸载后进行重新安装&#xff0c;发现新安装的还有原来的一些配置&#xff0c;卸载的不彻底&#xff0c;有时候也容易出问题&#xff0c;可按照如下方法卸载干净&#xff1a; 1.进入控制面板卸载VSCode&#xff0c;也可以在VSCode的安装目录下用程序自带的卸载程序 2.这…

灯光相机已就位!Cinerama LAND 销售活动开启序幕!

你准备好参加 The Sandbox 元宇宙中的重磅活动了吗&#xff1f;Cinerama LAND 拍卖即将来临&#xff0c;这是你踏入电影梦想世界的好机会。准备好构建你自己的沉浸式电影宇宙吧&#xff01;绝对不容错过&#xff01; 简要概括 &#x1f37f;活动开始日期&#xff1a;11 月 9 日…

如何在Jupyter Lab中安装不同的Kernel

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…