故障分析 | OceanBase 频繁更新数据后读性能下降的排查

news2024/11/28 1:30:18

本文摘要

本文分析并复现了 OceanBase 频繁更新数据后读性能下降现象的原因,并给出了性能改善建议。

背景

测试在做 OceanBase 纯读性能压测的时候,发现对数据做过更新操作后,读性能会有较为明显的下降。具体复现步骤如下。

复现方式

环境预备

部署OB

使用 OBD 部署单节点 OB。

版本IP
OceanBase4.0.0.0 CE10.186.16.122

参数均为默认值,其中内存以及转储合并等和本次实验相关的重要参数值具体如下:

参数名含义默认值
memstore_limit_percentage设置租户使用 memstore 的内存占其总可用内存的百分比。50
freeze_trigger_percentage触发全局冻结的租户使用内存阈值。20
major_compact_trigger设置多少次小合并触发一次全局合并。0
minor_compact_trigger控制分层转储触发向下一层下压的阈值。当该层的 Mini SSTable 总数达到设定的阈值时,所有 SSTable 都会被下压到下一层,组成新的 Minor SSTable。2

创建 sysbench 租户

create resource unit sysbench_unit max_cpu 26, memory_size '21g';
create resource pool sysbench_pool unit = 'sysbench_unit', unit_num = 1, zone_list=('zone1');
create tenant sysbench_tenant resource_pool_list=('sysbench_pool'), charset=utf8mb4, zone_list=('zone1'), primary_zone=RANDOM set variables ob_compatibility_mode='mysql', ob_tcp_invited_nodes='%';

数据预备

创建 30 张 100 万行数据的表。

sysbench ./oltp_read_only.lua --mysql-host=10.186.16.122 --mysql-port=12881 --mysql-db=sysbenchdb --mysql-user="sysbench@sysbench_tenant"  --mysql-password=sysbench --tables=30 --table_size=1000000 --threads=256 --time=60 --report-interval=10 --db-driver=mysql --db-ps-mode=disable --skip-trx=on --mysql-ignore-errors=6002,6004,4012,2013,4016,1062 prepare

环境调优

手动触发大合并

ALTER SYSTEM MAJOR FREEZE TENANT=ALL;

# 查看合并进度
SELECT * FROM oceanbase.CDB_OB_ZONE_MAJOR_COMPACTION\G

数据更新前的纯读 QPS

sysbench ./oltp_read_only.lua --mysql-host=10.186.16.122 --mysql-port=12881 --mysql-db=sysbenchdb --mysql-user="sysbench@sysbench_tenant"  --mysql-password=sysbench --tables=30 --table_size=1000000 --threads=256 --time=60 --report-interval=10 --db-driver=mysql --db-ps-mode=disable --skip-trx=on --mysql-ignore-errors=6002,6004,4012,2013,4016,1062 run

read_only 的 QPS 表现如下:

第一次第二次第三次第四次第五次
344727.36325128.58353141.76330873.54340936.48

数据更新后的纯读 QPS

执行三次 write_only 脚本,其中包括了 update/delete/insert 操作,命令如下:

sysbench ./oltp_write_only.lua --mysql-host=10.186.16.122 --mysql-port=12881 --mysql-db=sysbenchdb --mysql-user="sysbench@sysbench_tenant"  --mysql-password=sysbench --tables=30 --table_size=1000000 --threads=256 --time=60 --report-interval=10 --db-driver=mysql --db-ps-mode=disable --skip-trx=on --mysql-ignore-errors=6002,6004,4012,2013,4016,1062 run

再执行 read_only 的 QPS 表现如下:

第一次第二次第三次第四次第五次
170718.07175209.29173451.38169685.38166640.62

数据做一次大合并后纯读 QPS

手动触发大合并,执行命令:

ALTER SYSTEM MAJOR FREEZE TENANT=ALL;

# 查看合并进度
SELECT * FROM oceanbase.CDB_OB_ZONE_MAJOR_COMPACTION\G

再次执行 read_only ,QPS 表现如下,可以看到读的 QPS 恢复至初始水平。

第一次第二次第三次第四次第五次
325864.95354866.82331337.10326113.78340183.18

现象总结

对比数据更新前后的纯读 QPS,发现在做过批量更新操作后,读性能下降 17W 左右,做一次大合并后性能又可以提升回来。

排查过程

手法 1:火焰图

火焰图差异对比

收集数据更新前后进行压测时的火焰图,对比的不同点集中在下面标注的蓝框中。

放大到方法里进一步查看,发现低 QPS 火焰图顶部多了几个 ‘平台’,指向同一个方法 oceanbase::blocksstable::ObMultiVersionMicroBlockRowScanner::inner_get_next_row

查看源码

火焰图中指向的方法,会进一步调用 ObMultiVersionMicroBlockRowScanner::inner_get_next_row_impl。后者的主要作用是借嵌套 while 循环进行多版本数据行的读取,并将符合条件的行合并融合(do_compact 中会调用 fuse_row),返回一个合并后的行(ret_row)作为最终结果,源码如下:

int ObMultiVersionMicroBlockRowScanner::inner_get_next_row_impl(const ObDatumRow *&ret_row)
{
  int ret = OB_SUCCESS;
  // TRUE:For the multi-version row of the current rowkey, when there is no row to be read in this micro_block
  bool final_result = false;
  // TRUE:For reverse scanning, if this micro_block has the last row of the previous rowkey
  bool found_first_row = false;
  bool have_uncommited_row = false;
  const ObDatumRow *multi_version_row = NULL;
  ret_row = NULL;
 
  while (OB_SUCC(ret)) {
    final_result = false;
    found_first_row = false;
    // 定位到当前要读取的位置
    if (OB_FAIL(locate_cursor_to_read(found_first_row))) {
      if (OB_UNLIKELY(OB_ITER_END != ret)) {
        LOG_WARN("failed to locate cursor to read", K(ret), K_(macro_id));
      }
    }
    LOG_DEBUG("locate cursor to read", K(ret), K(finish_scanning_cur_rowkey_),
              K(found_first_row), K(current_), K(reserved_pos_), K(last_), K_(macro_id));
 
    while (OB_SUCC(ret)) {
      multi_version_row = NULL;
      bool version_fit = false;
      // 读取下一行
      if (read_row_direct_flag_) {
        if (OB_FAIL(inner_get_next_row_directly(multi_version_row, version_fit, final_result))) {
          if (OB_UNLIKELY(OB_ITER_END != ret)) {
            LOG_WARN("failed to inner get next row directly", K(ret), K_(macro_id));
          }
        }
      } else if (OB_FAIL(inner_inner_get_next_row(multi_version_row, version_fit, final_result, have_uncommited_row))) {
        if (OB_UNLIKELY(OB_ITER_END != ret)) {
          LOG_WARN("failed to inner get next row", K(ret), K_(macro_id));
        }
      }
      if (OB_SUCC(ret)) {
        // 如果读取到的行版本不匹配,则不进行任何操作
        if (!version_fit) {
          // do nothing
        }
        // 如果匹配,则进行合并融合
        else if (OB_FAIL(do_compact(multi_version_row, row_, final_result))) {
          LOG_WARN("failed to do compact", K(ret));
        } else {
          // 记录物理读取次数
          if (OB_NOT_NULL(context_)) {
            ++context_->table_store_stat_.physical_read_cnt_;
          }
          if (have_uncommited_row) {
            row_.set_have_uncommited_row();
          }
        }
      }
      LOG_DEBUG("do compact", K(ret), K(current_), K(version_fit), K(final_result), K(finish_scanning_cur_rowkey_),
                "cur_row", is_row_empty(row_) ? "empty" : to_cstring(row_),
                "multi_version_row", to_cstring(multi_version_row), K_(macro_id));
      // 该行多版本如果在当前微块已经全部读取完毕,就将当前微块的行缓存并跳出内层循环
      if ((OB_SUCC(ret) && final_result) || OB_ITER_END == ret) {
        ret = OB_SUCCESS;
        if (OB_FAIL(cache_cur_micro_row(found_first_row, final_result))) {
          LOG_WARN("failed to cache cur micro row", K(ret), K_(macro_id));
        }
        LOG_DEBUG("cache cur micro row", K(ret), K(finish_scanning_cur_rowkey_),
                  "cur_row", is_row_empty(row_) ? "empty" : to_cstring(row_),
                  "prev_row", is_row_empty(prev_micro_row_) ? "empty" : to_cstring(prev_micro_row_),
                  K_(macro_id));
        break;
      }
    }
    // 结束扫描,将最终结果放到ret_row,跳出外层循环。
    if (OB_SUCC(ret) && finish_scanning_cur_rowkey_) {
      if (!is_row_empty(prev_micro_row_)) {
        ret_row = &prev_micro_row_;
      } else if (!is_row_empty(row_)) {
        ret_row = &row_;
      }
      // If row is NULL, means no multi_version row of current rowkey in [base_version, snapshot_version) range
      if (NULL != ret_row) {
        (const_cast<ObDatumRow *>(ret_row))->mvcc_row_flag_.set_uncommitted_row(false);
        const_cast<ObDatumRow *>(ret_row)->trans_id_.reset();
        break;
      }
    }
  }
  if (OB_NOT_NULL(ret_row)) {
    if (!ret_row->is_valid()) {
      LOG_ERROR("row is invalid", KPC(ret_row));
    } else {
      LOG_DEBUG("row is valid", KPC(ret_row));
      if (OB_NOT_NULL(context_)) {
        ++context_->table_store_stat_.logical_read_cnt_;
      }
    }
  }
  return ret;
}

分析

从火焰图来看,QPS 降低,消耗集中在对多版本数据行的处理上,也就是一行数据的频繁更新操作对应到存储引擎里是多条记录,查询的 SQL 在内部处理时,实际可能需要扫描的行数量可能远大于本身的行数。

手法 2:分析 SQL 执行过程

通过 GV$OB_SQL_AUDIT 审计表,可以查看每次请求客户端来源、执行服务器信息、执行状态信息、等待事件以及执行各阶段耗时等。

GV$OB_SQL_AUDIT 用法参考:https://www.oceanbase.com/docs/common-oceanbase-database-cn-10000000001699453

对比性能下降前后相同 SQL 的执行信息

由于本文场景没有实际的慢sql,这里选择在 GV$OB_SQL_AUDIT 中,根据 SQL 执行耗时(elapsed_time)筛出 TOP10,取一条进行排查:SELECT c FROM sbtest% WHERE id BETWEEN ? AND ? ORDER BY c

执行更新操作前(也就是高 QPS 时):

MySQL [oceanbase]> select TRACE_ID,TENANT_NAME,USER_NAME,DB_NAME,QUERY_SQL,RETURN_ROWS,IS_HIT_PLAN,ELAPSED_TIME,EXECUTE_TIME,MEMSTORE_READ_ROW_COUNT,SSSTORE_READ_ROW_COUNT,DATA_BLOCK_READ_CNT,DATA_BLOCK_CACHE_HIT,INDEX_BLOCK_READ_CNT,INDEX_BLOCK_CACHE_HIT from GV$OB_SQL_AUDIT where TRACE_ID='YB42AC110005-0005F9ADDCDF0240-0-0' \G
*************************** 1. row ***************************
               TRACE_ID: YB42AC110005-0005F9ADDCDF0240-0-0
            TENANT_NAME: sysbench_tenant
              USER_NAME: sysbench
                DB_NAME: sysbenchdb
              QUERY_SQL: SELECT c FROM sbtest20 WHERE id BETWEEN 498915 AND 499014 ORDER BY c
                PLAN_ID: 10776
            RETURN_ROWS: 100
            IS_HIT_PLAN: 1
           ELAPSED_TIME: 16037
           EXECUTE_TIME: 15764
MEMSTORE_READ_ROW_COUNT: 0
 SSSTORE_READ_ROW_COUNT: 100
    DATA_BLOCK_READ_CNT: 2
   DATA_BLOCK_CACHE_HIT: 2
   INDEX_BLOCK_READ_CNT: 2
  INDEX_BLOCK_CACHE_HIT: 1
1 row in set (0.255 sec)

执行更新操作后(低 QPS 值时):

MySQL [oceanbase]> select TRACE_ID,TENANT_NAME,USER_NAME,DB_NAME,QUERY_SQL,RETURN_ROWS,IS_HIT_PLAN,ELAPSED_TIME,EXECUTE_TIME,MEMSTORE_READ_ROW_COUNT,SSSTORE_READ_ROW_COUNT,DATA_BLOCK_READ_CNT,DATA_BLOCK_CACHE_HIT,INDEX_BLOCK_READ_CNT,INDEX_BLOCK_CACHE_HIT from GV$OB_SQL_AUDIT where TRACE_ID='YB42AC110005-0005F9ADE2E77EC0-0-0' \G
*************************** 1. row ***************************
               TRACE_ID: YB42AC110005-0005F9ADE2E77EC0-0-0
            TENANT_NAME: sysbench_tenant
              USER_NAME: sysbench
                DB_NAME: sysbenchdb
              QUERY_SQL: SELECT c FROM sbtest7 WHERE id BETWEEN 501338 AND 501437 ORDER BY c
                PLAN_ID: 10848
            RETURN_ROWS: 100
            IS_HIT_PLAN: 1
           ELAPSED_TIME: 36960
           EXECUTE_TIME: 36828
MEMSTORE_READ_ROW_COUNT: 33
 SSSTORE_READ_ROW_COUNT: 200
    DATA_BLOCK_READ_CNT: 63
   DATA_BLOCK_CACHE_HIT: 63
   INDEX_BLOCK_READ_CNT: 6
  INDEX_BLOCK_CACHE_HIT: 4
1 row in set (0.351 sec)

分析

上面查询结果显示字段 IS_HIT_PLAN 的值为 1,说明 SQL 命中了执行计划缓存,没有走物理生成执行计划的路径。我们根据 PLAN_ID 进一步到 V$OB_PLAN_CACHE_PLAN_EXPLAIN 查看物理执行计划(数据更新前后执行计划相同,下面仅列出数据更新后的执行计划)。

注:访问 V$OB_PLAN_CACHE_PLAN_EXPLAIN,必须给定 tenant_idplan_id 的值,否则系统将返回空集。

MySQL [oceanbase]>  SELECT * FROM V$OB_PLAN_CACHE_PLAN_EXPLAIN WHERE tenant_id = 1002 AND plan_id=10848 \G
*************************** 1. row ***************************
   TENANT_ID: 1002
      SVR_IP: 172.17.0.5
    SVR_PORT: 2882
     PLAN_ID: 10848
  PLAN_DEPTH: 0
PLAN_LINE_ID: 0
    OPERATOR: PHY_SORT
        NAME: NULL
        ROWS: 100
        COST: 51
    PROPERTY: NULL
*************************** 2. row ***************************
   TENANT_ID: 1002
      SVR_IP: 172.17.0.5
    SVR_PORT: 2882
     PLAN_ID: 10848
  PLAN_DEPTH: 1
PLAN_LINE_ID: 1
    OPERATOR:  PHY_TABLE_SCAN
        NAME: sbtest20
        ROWS: 100
        COST: 6
    PROPERTY: table_rows:1000000, physical_range_rows:100, logical_range_rows:100, index_back_rows:0, output_rows:100, est_method:local_storage, avaiable_index_name[sbtest20], pruned_index_name[k_20], estimation info[table_id:500294, (table_type:12, version:-1--1--1, logical_rc:100, physical_rc:100)]
2 rows in set (0.001 sec)

V$OB_PLAN_CACHE_PLAN_EXPLAIN 查询结果看,执行计划涉及两个算子:范围扫描算子 PHY_TABLE_SCAN 和排序算子 PHY_SORT。根据范围扫描算子 PHY_TABLE_SCAN 中的 PROPERTY 信息,可以看出该算子使用的是主键索引,不涉及回表,行数为 100。综上来看,该 SQL 的执行计划正确且已是最优,没有调整的空间。

再对比两次性能压测下 GV$OB_SQL_AUDIT 表,当性能下降后,MEMSTORE_READ_ROW_COUNT(MemStore 中读的行数)和 SSSTORE_READ_ROW_COUNT (SSSTORE 中读的行数)加起来读的总行数为 233,是实际返回行数的两倍多。符合上面观察到的火焰图上的问题,即实际读的行数大于本身的行数,该处消耗了系统更多的资源,导致性能下降。

结论

OceanBase 数据库的存储引擎基于 LSM-Tree 架构,以基线加增量的方式进行存储,当在一个表中进行大量的插入、删除、更新操作后,查询每一行数据的时候需要根据版本从新到旧遍历所有的 MemTable 以及 SSTable,将每个 Table 中对应主键的数据熔合在一起返回,此时表现出来的就是查询性能明显下降,即读放大。

性能改善方式

对于已经运行在线上的 buffer 表问题,官方文档中给出的应急处理方案如下:

  1. 对于存在可用索引,但 OB 优化器计划生成为全表扫描的场景。需要进行执行计划 binding 来固定计划。
  2. 如果 SQL 查询的主要过滤字段无可用索引,此时推荐在线创建可用索引并绑定该计划。
  3. 如果业务场景暂时无法创建索引,或者执行的 SQL 多为范围扫描,此时可根据业务场景需要决定是否手动触发合并,将删除或更新的数据版本进行清理,降低全表扫描的数据量,提升速度。

另外,从 2.2.7 版本开始,OceanBase 引入了 buffer minor merge 设计,实现对 Queuing 表的特殊转储机制,彻底解决无效扫描问题,通过将表的模式设置为 queuing 来开启。对于设计阶段已经明确的 Queuing 表场景,推荐开启该特性作为长期解决方案。

ALTER TABLE table_name TABLE_MODE = 'queuing';

但是社区版 4.0.0.0 的发布记录中看到,不再支持 Queuing 表。后查询社区有解释:OB 在 4.x 版本(预计 4.1 完成)采用自适应的方式支持 Queuing 表的这种场景,不需要再人为指定,也就是 Release Note 中提到的不再支持 Queuing 表。

参考资料

  1. Queuing 表查询缓慢问题
  2. 大批量数据处理后访问慢问题处理
  3. OceanBase Queuing 表(buffer 表)处理最佳实践
  4. ob4.0 确定不支持 Queuing 表了吗?

本文关键字:#OceanBase# #火焰图# #性能调优#

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

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

相关文章

部门新来一00后,给我卷崩溃了...

2022年已经结束结束了&#xff0c;最近内卷严重&#xff0c;各种跳槽裁员&#xff0c;相信很多小伙伴也在准备今年的金三银四的面试计划。 在此展示一套学习笔记 / 面试手册&#xff0c;年后跳槽的朋友可以好好刷一刷&#xff0c;还是挺有必要的&#xff0c;它几乎涵盖了所有的…

NFT数字藏品平台

在 NFT &#xff08;非同质化代币&#xff09;发行和交易中&#xff0c;数字藏品交易平台&#xff08;以下简称“交易平台”&#xff09;的运营模式和法律地位至关重要。本文对数字藏品交易平台的运营方式进行梳理&#xff0c;并对其中可能存在的法律风险进行分析。 2021年以来…

NFS服务器搭建(案例)

目录标题 第一个问题1.安装软件包2.进入配置文件进行定义&#xff0c;并创建对应的资源文件3.客户端进行挂载&#xff0c;并查看挂载信息&#xff0c;修改挂载权限4.客户端查看挂载的信息 第二个问题1.服务端配置文件进行定义&#xff0c;并创建对应资源文件2.客户端进行挂载3.…

电子邮件是最有效的营销方式吗?解析邮件营销的优势和劣势

互联网的快速发展&#xff0c;使得新媒体营销、短视频营销、微信营销等新型营销方式渗入到大家的生活中。很多人会认为邮件营销已经落后。但事实上&#xff0c;邮件营销仍然有着广泛的市场需求&#xff0c;特别是对于出海或者做跨境电商的企业来讲&#xff0c;邮件营销仍然发挥…

深度学习笔记(续)——数值稳定性、模型初始化与激活函数

深度学习笔记[续]——数值稳定性、模型初始化与激活函数 引言回顾&#xff1a;没有激活函数参与&#xff0c;输入输出分布的变化情况 Xavier \text{Xavier} Xavier方法 存在激活函数的情况假设激活函数是线性函数激活函数的底层逻辑 总结 引言 继续上一节介绍激活函数在神经网…

Google IO 2023推出Android Studio官方AI工具Studio Bot

Google推出Android Studio官方AI工具Studio Bot Studio Bot 简介 在2023 Google I/O大会上&#xff0c;Google 宣布在 Android Studio 中推出了一款名为 Studio Bot 的新 AI 功能&#xff0c;它将为开发者提供更高效、智能的开发体验。Studio Bot 是一个基于机器学习的助手&a…

想要快速进入车载测试?这份车载面试题集锦送给你,建议收藏!

1.道路测试流程 测试场地 1.测试场地有哪些&#xff1f; 对于一些要求不太高/简单的测试场景可以找一些封闭的场地&#xff0c;如断头路或者是城市所在地当地政府提供的诸如智能网联专用道路之类的测试道路&#xff0c;对于复杂或相对要求高一些的测试场景可以到专业的国家智…

【运维杂谈】如何将ios镜像挂载到Linux系统中?

本期我们实现将CentOS7.9系统镜像挂载到Linux系统当中&#xff0c;首先明确什么是挂载&#xff0c;我理解的挂载就是把系统镜像与Linux操作系统建立直接的联系&#xff0c;将镜像的顶级目录连接到Linux根目录下的某一目录中&#xff0c;就好比我们Windows系统插入一张光盘&…

C++学习记录——이십삼 哈希表

文章目录 1、unordered_map unordered_set2、哈希表1、闭散列2、开散列&#xff08;拉链法/哈希桶&#xff09;继续优化 3、封装unordered和迭代器 1、unordered_map unordered_set C11提供&#xff0c;功能和map、set完全类似&#xff0c;不过它们底层实现是红黑树&#xff0…

JDBC~

文章目录 JDBCJDBC API详解DriverManagerConnectionStatementResultSetPreparedStatementPreparedStatement原理 JDBC JDBC就是使用JAVA语言操作关系型数据库的一套API JDBC是java处理数据库的接口 JDBC API详解 DriverManager 驱动管理类: 注册驱动&#xff0c;获取数据库连接…

Cplex的数据类型结构及基本语法功能

本序列将会重开一门新的序列----数学求解器cplex,文章不做简单介绍&#xff0c;不灌水&#xff0c;直接给大家进行讲述如何上手实操&#xff0c;并有针对性的给出相应案例分析。 OPL编程 OPL是ILOG团队为运筹学专家量身定制的一种优化建模语言&#xff0c;语法相对简单&#x…

C++系列之入门基础知识

&#x1f497; &#x1f497; 博客:小怡同学 &#x1f497; &#x1f497; 个人简介:编程小萌新 &#x1f497; &#x1f497; 如果博客对大家有用的话&#xff0c;请点赞关注再收藏 &#x1f31e; 命名空间 常见域的种类 常见的域的种类有&#xff1a;全局域&#xff0c;局部…

快速构建springboot项目

需求&#xff1a;浏览器发送 /hello 请求&#xff0c;服务器接受请求并处理&#xff0c;响应 Hello World 字符串分析 构建 Spring Boot 项目&#xff0c;事实上建立的就是一个 Maven 项目 1.创建maven 工程 在 IDEA上新建一个空的jar类型 的 maven 工程 2.修改pom.xml 1.在…

【Tkinter.Floodgauge】当程序需要长时间运行,可以用这个组件显示进度【文末附源码地址】

文章目录 效果展示源码解析导包Floodgauge组件界面初始化创建窗口修改数值运行 源码地址 效果展示 我在使用tkinter进行界面化操作的时候&#xff0c;会遇到运行很慢的程序&#xff0c;比如&#xff1a;爬虫下载视频、压缩解压文件&#xff0c;这些操作会很耗时间。 Floodgau…

力扣 ~ JavaScript ~ 35. 搜索插入位置

《跃动青春》好可爱的女孩子的友谊哇 35. 搜索插入位置 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 示例 1: …

redis(12)

1)添加地理位置坐标:已经添加的数据&#xff0c;无法再添加 geoadd key 经度 纬度 变量名字 geoadd china:city 121.47 31.23 shanghai 2)获取指定地区的坐标值: geopos key member(变量名字) geopos china:city shnaghai 3)获取到两个坐标之间的距离&#xff0c;后面是可选参…

95后的自述,文科女吃上IT饭了,历经5个月学习成功上岸软件测试......

想不到我一个女文科生也能吃上IT饭&#xff0c;真的是太香了&#xff01;&#xff01;&#xff01; 程序员的待遇怎么这么好&#xff01;&#xff08;请大家原谅我没有见过市面的感叹&#xff0c;对于我这种刚上岗的新手而言&#xff0c;能够在厦门这个全国工资房价比最低的地方…

景区手绘图的配准、切图及上传

目录 1 增加一个项目 2 基础配置 3 获取自动上传需要的3个参数 3.1 生成切片上传验证码 3.2 按F5刷新页面 3.3 查看验证码以及其他2个参数 4 配准 5 切图 6 到迅达平台看一下切图效果 7 软件免费下载 这个免费客户端专用于迅达平台&#xff0c;对手绘图背景进行配准&…

java 调用 python 方法

目录 1.JPython 1.1 JPython介绍 1.2 JPython实践 2.使用Runtime.getRuntime()执行python脚本文件&#xff0c;推荐使用 一个项目可能需要用到人工智能算法&#xff0c;在java后端中需要调用python算法&#xff0c;这篇文章就简单介绍一下其中的两种java调用python方法。 …

一个简单的基于QT的图像浏览器

以前学习前端的时候&#xff0c;对于多张图片的布局一般使用瀑布流布局&#xff08;CSS总结——瀑布流布局_css 瀑布流_黑白程序员的博客-CSDN博客&#xff09;&#xff0c;然后再通过懒加载&#xff08;如何实现图片懒加载&#xff0c;预加载&#xff01;&#xff01; - 简书&…