首页 文章 精选 留言 我的

精选列表

搜索[告别],共769篇文章
优秀的个人博客,低调大师

浅析MySQL代价模型:告别盲目使用EXPLAIN,提前预知索引优化策略 | 京东云技术团队

背景 在 MySQL 中,当我们为表创建了一个或多个索引后,通常需要在索引定义完成后,根据具体的数据情况执行 EXPLAIN 命令,才能观察到数据库实际使用哪个索引、是否使用索引。这使得我们在添加新索引之前,无法提前预知数据库是否能使用期望的索引。更为糟糕的是,有时甚至在添加新的索引后,数据库在某些查询中会使用它,而在其他查询中则不会使用,这种情况下,我们无法确定索引是否发挥了预期的作用,让人感到非常苦恼。这种情况基本上意味着 MySQL 并没有为我们选择最优的索引,而我们不得不在茫茫数据中摸索,试图找到问题的症结所在。我们可能会尝试调整索引,甚至删除索引,然后重新添加,希望 MySQL 能从中找到最优的索引选择。然而,这样的过程既耗时又费力,而且往往收效甚微。 如果在添加索引之前,我们能够预知索引的使用情况,那么对于表设计将大有裨益。我们可以在设计表结构时,更加明确地知道应该选择哪些索引,如何优化索引,以提高查询效率。我们不再需要依赖盲目尝试和猜测,而是可以基于实际的数据和查询情况,做出更加明智的决策。因此,对于 MySQL 用户来说,能够预知索引走势的需求非常迫切。我们希望能有一种方法,能够让我们在添加索引之前,就清楚地了解 MySQL 将如何使用索引,以便我们能够更好地优化表结构,提高查询效率。这将极大地减轻我们的工作负担,提高我们的工作效率,让我们能够更加专注于业务逻辑的处理,而不是在索引的海洋中挣扎。 为了解决这个问题,我们可以深入研究 MySQL 的索引选择机制。实际上,这个机制的核心就是代价模型,它通过一个公式来决定索引的选择策略。相对于 MySQL 其他复杂的概念,代价模型实现起来要简单得多。熟悉代价模型之后,我们可以预先了解 MySQL 在执行查询时会如何选择索引,从而更有效地进行索引优化。在接下来的文章中,我将结合近期进行索引优化的具体案例,来详细解释如何运用代价模型来优化索引。 MySQL代价模型浅析  MySQL数据库主要由4层组成: 1. 连接层:客户端和连接服务,主要完成一些类似于连接处理、授权管理、以及相关的安全方案。 2. 服务层:主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化以及内部函数的执行。 3. 引擎层:负责MySQL中数据的存储和提取,服务器通过AP1与存储引擎进行通信。 4. 存储层:将数据存储文件系统上,并完成与存储引擎的交互。  索引策略选择在SQL优化器进行的 SQL 优化器会分析所有可能的执行计划,选择成本最低的执行,这种优化器称之为:CBO(Cost-based Optimizer,基于成本的优化器)。 Cost = Server Cost + Engine Cost = CPU Cost + IO Cost 其中,CPU Cost 表示计算的开销,比如索引键值的比较、记录值的比较、结果集的排序 ...... 这些操作都在 Server 层完成; IO Cost 表示引擎层 IO 的开销,MySQL 可以通过区分一张表的数据是否在内存中,分别计算读取内存 IO 开销以及读取磁盘 IO 的开销。 源码简读 MySQL的数据源代码采用了5.7.22版本,后续的代价计算公式将基于此版本进行参考。   opt_costconstants.cc【代价模型——计算所需代价计算系数】 /* 在Server_cost_constants类中定义为静态常量变量的成本常量的值。如果服务器管理员没有在server_cost表中添加新值,则将使用这些默认成本常数值。 5.7版本开始可用从数据库加载常量值,该版本前使用代码中写的常量值 */ // 计算符合条件的⾏的代价,⾏数越多,此项代价越⼤ const double Server_cost_constants::ROW_EVALUATE_COST= 0.2; // 键⽐较的代价,例如排序 const double Server_cost_constants::KEY_COMPARE_COST= 0.1; /* 内存临时表的创建代价 通过基准测试,创建Memory临时表的成本与向表中写入10行的成本一样高。 */ const double Server_cost_constants::MEMORY_TEMPTABLE_CREATE_COST= 2.0; // 内存临时表的⾏代价 const double Server_cost_constants::MEMORY_TEMPTABLE_ROW_COST= 0.2; /* 内部myisam或innodb临时表的创建代价 创建MyISAM表的速度是创建Memory表的20倍。 */ const double Server_cost_constants::DISK_TEMPTABLE_CREATE_COST= 40.0; /* 内部myisam或innodb临时表的⾏代价 当行数大于1000时,按顺序生成MyISAM行比生成Memory行慢2倍。然而,没有非常大的表的基准,因此保守地将此系数设置为慢5倍(即成本为1.0)。 */ const double Server_cost_constants::DISK_TEMPTABLE_ROW_COST= 1.0; /* 在SE_cost_constants类中定义为静态常量变量的成本常量的值。如果服务器管理员没有在engine_cost表中添加新值,则将使用这些默认成本常数值。 */ // 从主内存缓冲池读取块的成本 const double SE_cost_constants::MEMORY_BLOCK_READ_COST= 1.0; // 从IO设备(磁盘)读取块的成本 const double SE_cost_constants::IO_BLOCK_READ_COST= 1.0; opt_costmodel.cc【代价模型——部分涉及方法】 double Cost_model_table::page_read_cost(double pages) const { DBUG_ASSERT(m_initialized); DBUG_ASSERT(pages >= 0.0); // 估算聚集索引内存中页面数占其所有页面数的比率 const double in_mem= m_table->file->table_in_memory_estimate(); const double pages_in_mem= pages * in_mem; const double pages_on_disk= pages - pages_in_mem; DBUG_ASSERT(pages_on_disk >= 0.0); const double cost= buffer_block_read_cost(pages_in_mem) + io_block_read_cost(pages_on_disk); return cost; } double Cost_model_table::page_read_cost_index(uint index, double pages) const { DBUG_ASSERT(m_initialized); DBUG_ASSERT(pages >= 0.0); double in_mem= m_table->file->index_in_memory_estimate(index); const double pages_in_mem= pages * in_mem; const double pages_on_disk= pages - pages_in_mem; const double cost= buffer_block_read_cost(pages_in_mem) + io_block_read_cost(pages_on_disk); return cost; } handler.cc【代价模型——部分涉及方法】 // 聚集索引扫描IO代价计算公式 Cost_estimate handler::read_cost(uint index, double ranges, double rows) { DBUG_ASSERT(ranges >= 0.0); DBUG_ASSERT(rows >= 0.0); const double io_cost= read_time(index, static_cast<uint>(ranges), static_cast<ha_rows>(rows)) * table->cost_model()->page_read_cost(1.0); Cost_estimate cost; cost.add_io(io_cost); return cost; } // 表全量扫描代价相关计算(IO-cost) Cost_estimate handler::table_scan_cost() { const double io_cost= scan_time() * table->cost_model()->page_read_cost(1.0); Cost_estimate cost; cost.add_io(io_cost); return cost; } // 覆盖索引扫描代价相关计算 Cost_estimate handler::index_scan_cost(uint index, double ranges, double rows) { DBUG_ASSERT(ranges >= 0.0); DBUG_ASSERT(rows >= 0.0); const double io_cost= index_only_read_time(index, rows) * table->cost_model()->page_read_cost_index(index, 1.0); Cost_estimate cost; cost.add_io(io_cost); return cost; } /** 估算在指定 keynr索引进行覆盖扫描(不需要回表),扫描 records条记录,需要读取的索引页面数 @param keynr Index number @param records Estimated number of records to be retrieved @return Estimated cost of 'index only' scan */ double handler::index_only_read_time(uint keynr, double records) { double read_time; uint keys_per_block= (stats.block_size/2/ (table_share->key_info[keynr].key_length + ref_length) + 1); read_time=((double) (records + keys_per_block-1) / (double) keys_per_block); return read_time; } sql_planner.cc【用于ref访问类型索引费用计算】 double tmp_fanout= 0.0; if (table->quick_keys.is_set(key) && !table_deps && //(C1) table->quick_key_parts[key] == cur_used_keyparts && //(C2) table->quick_n_ranges[key] == 1+MY_TEST(ref_or_null_part)) //(C3) { tmp_fanout= cur_fanout= (double) table->quick_rows[key]; } else { // Check if we have statistic about the distribution if (keyinfo->has_records_per_key(cur_used_keyparts - 1)) { cur_fanout= keyinfo->records_per_key(cur_used_keyparts - 1); if (!table_deps && table->quick_keys.is_set(key) && // (1) table->quick_key_parts[key] > cur_used_keyparts) // (2) { trace_access_idx.add("chosen", false) .add_alnum("cause", "range_uses_more_keyparts"); is_dodgy= true; continue; } tmp_fanout= cur_fanout; } else { rec_per_key_t rec_per_key; if (keyinfo->has_records_per_key( keyinfo->user_defined_key_parts - 1)) rec_per_key= keyinfo->records_per_key(keyinfo->user_defined_key_parts - 1); else rec_per_key= rec_per_key_t(tab->records()) / distinct_keys_est + 1; if (tab->records() == 0) tmp_fanout= 0.0; else if (rec_per_key / tab->records() >= 0.01) tmp_fanout= rec_per_key; else { const double a= tab->records() * 0.01; if (keyinfo->user_defined_key_parts > 1) tmp_fanout= (cur_used_keyparts * (rec_per_key - a) + a * keyinfo->user_defined_key_parts - rec_per_key) / (keyinfo->user_defined_key_parts - 1); else tmp_fanout= a; set_if_bigger(tmp_fanout, 1.0); } cur_fanout= (ulong) tmp_fanout; } if (ref_or_null_part) { // We need to do two key searches to find key tmp_fanout*= 2.0; cur_fanout*= 2.0; } if (table->quick_keys.is_set(key) && table->quick_key_parts[key] <= cur_used_keyparts && const_part & ((key_part_map)1 << table->quick_key_parts[key]) && table->quick_n_ranges[key] == 1 + MY_TEST(ref_or_null_part & const_part) && cur_fanout > (double) table->quick_rows[key]) { tmp_fanout= cur_fanout= (double) table->quick_rows[key]; } } ······ ······ // Limit the number of matched rows const double tmp_fanout= min(cur_fanout, (double) thd->variables.max_seeks_for_key); if (table->covering_keys.is_set(key) || (table->file->index_flags(key, 0, 0) & HA_CLUSTERED_INDEX)) { // We can use only index tree const Cost_estimate index_read_cost= table->file->index_scan_cost(key, 1, tmp_fanout); cur_read_cost= prefix_rowcount * index_read_cost.total_cost(); } else if (key == table->s->primary_key && table->file->primary_key_is_clustered()) { const Cost_estimate table_read_cost= table->file->read_cost(key, 1, tmp_fanout); cur_read_cost= prefix_rowcount * table_read_cost.total_cost(); } else cur_read_cost= prefix_rowcount * min(table->cost_model()->page_read_cost(tmp_fanout), tab->worst_seeks); handler.cc【用于range访问类型索引费用计算】 handler::multi_range_read_info_const(uint keyno, RANGE_SEQ_IF *seq, void *seq_init_param, uint n_ranges_arg, uint *bufsz, uint *flags, Cost_estimate *cost) { KEY_MULTI_RANGE range; range_seq_t seq_it; ha_rows rows, total_rows= 0; uint n_ranges=0; THD *thd= current_thd; /* Default MRR implementation doesn't need buffer */ *bufsz= 0; DBUG_EXECUTE_IF("bug13822652_2", thd->killed= THD::KILL_QUERY;); seq_it= seq->init(seq_init_param, n_ranges, *flags); while (!seq->next(seq_it, &range)) { if (unlikely(thd->killed != 0)) return HA_POS_ERROR; n_ranges++; key_range *min_endp, *max_endp; if (range.range_flag & GEOM_FLAG) { min_endp= &range.start_key; max_endp= NULL; } else { min_endp= range.start_key.length? &range.start_key : NULL; max_endp= range.end_key.length? &range.end_key : NULL; } int keyparts_used= 0; if ((range.range_flag & UNIQUE_RANGE) && // 1) !(range.range_flag & NULL_RANGE)) rows= 1; /* there can be at most one row */ else if ((range.range_flag & EQ_RANGE) && // 2a) (range.range_flag & USE_INDEX_STATISTICS) && // 2b) (keyparts_used= my_count_bits(range.start_key.keypart_map)) && table-> key_info[keyno].has_records_per_key(keyparts_used-1) && // 2c) !(range.range_flag & NULL_RANGE)) { rows= static_cast<ha_rows>( table->key_info[keyno].records_per_key(keyparts_used - 1)); } else { DBUG_EXECUTE_IF("crash_records_in_range", DBUG_SUICIDE();); DBUG_ASSERT(min_endp || max_endp); if (HA_POS_ERROR == (rows= this->records_in_range(keyno, min_endp, max_endp))) { /* Can't scan one range => can't do MRR scan at all */ total_rows= HA_POS_ERROR; break; } } total_rows += rows; } if (total_rows != HA_POS_ERROR) { const Cost_model_table *const cost_model= table->cost_model(); /* The following calculation is the same as in multi_range_read_info(): */ *flags|= HA_MRR_USE_DEFAULT_IMPL; *flags|= HA_MRR_SUPPORT_SORTED; DBUG_ASSERT(cost->is_zero()); if (*flags & HA_MRR_INDEX_ONLY) *cost= index_scan_cost(keyno, static_cast<double>(n_ranges), static_cast<double>(total_rows)); else *cost= read_cost(keyno, static_cast<double>(n_ranges), static_cast<double>(total_rows)); cost->add_cpu(cost_model->row_evaluate_cost( static_cast<double>(total_rows)) + 0.01); } return total_rows; } 验证公式 创建验证需要的表 CREATE TABLE `store_goods_center` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id', `sku_id` bigint(20) NOT NULL COMMENT '商品skuid', `station_no` varchar(20) NOT NULL COMMENT '门店编号', `org_code` bigint(20) NOT NULL COMMENT '商家编号', `extend_field` text COMMENT '扩展字段', `version` int(11) DEFAULT '0' COMMENT '版本号', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `create_pin` varchar(50) DEFAULT '' COMMENT '创建人', `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间', `update_pin` varchar(50) DEFAULT '' COMMENT '更新人', `yn` tinyint(4) DEFAULT '0' COMMENT '删除标示 0:正常 1:删除', `ts` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '时间戳', PRIMARY KEY (`id`), UNIQUE KEY `uniq_storegoods` (`station_no`, `sku_id`) USING BTREE, KEY `idx_storegoods_org` (`org_code`, `sku_id`, `station_no`), KEY `idx_sku_id` (`sku_id`), KEY `idx_station_no_and_id` (`station_no`, `id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='门店商品关系表'; 通过存储过程初始化测试数据 DELIMITER // CREATE PROCEDURE callback() BEGIN DECLARE num INT; SET num = 1; WHILE num <= 100000 DO INSERT INTO store_goods_center(sku_id, station_no, org_code) VALUES (num + 10000000, floor(50+rand()*(100-50+1)), num); SET num = num + 1; END WHILE; END; 执行存储过程生成数据 CALL callback(); 1.全表扫描计算代价公式 计算过程: // 不同引擎计算方式有所区别 // innodb引擎实现handler.h // 预估记录数:ha_innobase::info_low // 页数量:ha_innobase::scan_time【数据总大小(字节) / 页大小】 // 查询全表数据大小(7880704) SHOW TABLE STATUS LIKE 'store_goods_center'; // 查询数据库页大小(默认:16384) SHOW VARIABLES LIKE 'innodb_page_size'; // 全表扫描计算代价 // 页数量 page = 数据总大小(字节) / 页大小 = 7880704 / 16384 = 481; // 预估范围行数(总数据条数:10万,预估数据条数:99827,有一定误差) records = 99827; // 计算总代价 // 481 * 1 中的系数1 代表从主内存缓冲池读取块的成本(SE_cost_constants::IO_BLOCK_READ_COST= 1.0) // 99827 * 0.2 中的系数0.2 代表计算符合条件的⾏的代价(ROW_EVALUATE_COST= 0.2) cost = IO-cost + CPU-cost = (481 * 1) + (99827 * 0.2) = 481 + 19965.4 = 20446.4 验证结果: explain format = json select * from store_goods_center; "cost_info": {"query_cost": "20446.40"} 总结公式: 全表扫描代价 = 数据总大小 / 16384 + 预估范围行数 * 0.2 2.覆盖索引扫描计算代价公式 计算过程: // 查询全表数据大小(7880704) SHOW TABLE STATUS LIKE 'store_goods_center'; // 查询数据库页大小(默认:16384) SHOW VARIABLES LIKE 'innodb_page_size'; // 预估范围行数(总数据条数:1999,预估数据条数:1999,有一定误差) 1999; records = 1999 // keys_per_block计算 // block_size是文件的block大小,mysql默认为16K; // key_len是索引的键长度; // ref_len是主键索引的长度; keys_per_block = (stats.block_size / 2 / (table_share->key_info[keynr].key_length + ref_length) + 1); // table_share->key_info[keynr].key_length 为联合索引,分别是station_no和sku_id // station_no 为varchar(20)且为utf8mb4,长度 = 20 * 4 + 2 (可变长度需要加2) = 82 // sku_id bigint类型,长度为8 // 主键索引为bigint类型,长度为8 keys_per_block = 16384 / 2 / (82 + 8 + 8) + 1 ≈ 84 // 计算总代价 read_time = ((double) (records + keys_per_block - 1) / (double) keys_per_block); read_time = (1999 + 84 - 1) / 84 = 24.78; // 计算总代价 // 24.78 * 1 中的系数1 代表从主内存缓冲池读取块的成本(SE_cost_constants::IO_BLOCK_READ_COST= 1.0) // 1999 * 0.2 中的系数0.2 代表计算符合条件的⾏的代价(ROW_EVALUATE_COST= 0.2) cost = IO-cost + CPU-cost = (24.78 * 1) + (1999 * 0.2) = 24.78 + 399.8 = 424.58 验证结果: explain format = json select station_no from store_goods_center where station_no = '53'; "cost_info": {"query_cost": "424.58"} 总结公式: keys_per_block = 8192 / 索引长度 + 1 覆盖索引扫描代价 = (records + keys_per_block - 1) / keys_per_block + 预估范围行数 * 0.2 公式简化(去除影响较小的复杂计算) 覆盖索引扫描代价 = (records * 涉及索引长度) / 8192 + 预估范围行数 * 0.2 3.ref索引扫描计算代价公式 计算过程: // cardinality = 49(基数,即有多少个不同key统计。) SHOW TABLE STATUS LIKE 'store_goods_center'; // 页数量 page = 数据总大小(字节) / 页大小 = 7880704 / 16384 = 481; // 计算代价最低索引(sql_planner.cc 中find_best_ref函数) // IO COST最坏不会超过全表扫描IO消耗的3倍(或者总记录数除以10) // 其中s->found_records表示表上的记录数,s->read_time在innodb层表示page数 // s-> worst_seeks = min((double) s -> found_records / 10, (double) s -> read_time * 3); // cur_read_cost= prefix_rowcount * min(table->cost_model() -> page_read_cost(tmp_fanout), tab -> worst_seeks); // 预估范围行数(总数据条数:10万,预估数据条数:99827,有一定误差) total_records = 99827; // 预估范围行数(总数据条数:1999,预估数据条数:1999,有一定误差) 1999; records = 1999 // 计算总代价 // 1999 * 0.2 中的系数0.2 代表计算符合条件的⾏的代价(ROW_EVALUATE_COST= 0.2) // s-> worst_seeks = min((double) s -> found_records / 10, (double) s -> read_time * 3) -> min(99827 / 10, 481 * 3) = 481 * 3 // min(table->cost_model() -> page_read_cost(tmp_fanout), tab -> worst_seeks) -> min(page_read_cost(1999), 481 * 3) = 481 * 3 cost = IO-cost + CPU-cost = 481 * 3 + (1999 * 0.2) = 1443 + 399.8 = 1842.80 验证结果: explain format = json select * from store_goods_center where station_no = '53'; "cost_info": {"query_cost": "1842.80"} 总结公式: 下面3个公式,取值最低的 1.(数据总大小 / 16384) * 3 + 预估范围行数 * 0.2 2.总记录数 / 10 + 预估范围行数 * 0.2 3.扫描出记录数 + 预估范围行数 * 0.2 4.range索引扫描计算代价公式 // 预估范围行数(总数据条数:1299,预估数据条数:1299,有一定误差) 1299; records = 1299 // 计算代价最低索引(handler.cc 中 multi_range_read_info_const 函数) // 计算总代价 // 1299 * 0.2 计算公式:cost_model->row_evaluate_cost(static_cast<double>(total_rows)) // + 0.01 计算公式:cost->add_cpu(cost_model->row_evaluate_cost(static_cast<double>(total_rows)) + 0.01); // 1299 + 1 中的 +1 :单个扫描区间( id > 35018 ) // 1299 + 1 计算公式:*cost= read_cost(keyno, static_cast<double>(n_ranges), static_cast<double>(total_rows)); // (1299 * 0.2 + 0.01 + 1299) * 1 中的系数1 代表从主内存缓冲池读取块的成本(SE_cost_constants::IO_BLOCK_READ_COST= 1.0) // 1299 * 0.2 中的系数0.2 代表计算符合条件的⾏的代价(ROW_EVALUATE_COST= 0.2) cost = IO-cost + CPU-cost = ((1299 * 0.2 + 0.01 + 1299 + 1) * 1) + (1299 * 0.2) = 1559.81 + 259.8 = 1819.61 验证结果: explain format = json select * from store_goods_center where station_no = '53' and id > 35018; "cost_info": {"query_cost": "1819.61"} 总结公式: range扫描代价 = 预估范围行数 * 1.4 + 0.01 + 范围数 公式简化(去除影响较小的复杂计算) range扫描代价 = 预估范围行数 * 1.4 索引冲突案例 门店商品系统中主要存储门店与商品的关联信息,并为B端提供根据门店ID查询关联商品的功能。由于门店关联的商品数据量较大,需要分页查询关联商品数据。为避免深分页问题,我们选择基于上次最新主键进行查询(核心思想:通过主键索引,每次定位到ID所在位置,然后往后遍历N个数据。这样,无论数据量多少,查询性能都能保持稳定。我们将所有数据根据主键ID进行排序,然后分批次取出,将当前批次的最大ID作为下次查询的筛选条件)。 select 字段1,字段2 ... from store_goods_center where station_no = ‘门店id’ and id > 上次查询最大id order by id asc 为了确保门店与商品组合的唯一性,我们在MySQL表中为门店ID和商品ID添加了组合唯一索引【UNIQUE KEY uniq_storegoods (station_no, sku_id) USING BTREE】。由于该索引包含门店ID并且在联合索引的第一个位置,查询会使用该索引。但是,当分页查询命中该索引后,由于排序字段无法使用索引,产生了【Using filesort】,导致门店商品系统出现了一些慢查询。为了解决这个问题,我们对慢查询进行了优化,优化思路是创建一个新的索引,使该SQL可以使用索引的排序来规避【Using filesort】的负面影响,新添加的索引为【KEY idx_station_no_and_id (station_no, id)】。添加该索引后,效果立竿见影。 然而,我们发现仍然有慢查询产生,并且这些慢查询仍然使用uniq_storegoods索引,而不是idx_station_no_and_id索引。我们开始思考,为什么MySQL没有为我们的系统推荐使用最优的索引?是MySQL索引推荐有问题,还是我们创建索引有问题?如何做才能让MySQL帮我们推荐我们认为最优的索引? 当然,我们也可以使用FORCE INDEX强行让MySQL走我们提前预设的索引,但是这种方式局限太大,后期索引维护成本变得很高,甚至可能使用该SQL的其他业务性能变低。为了突破整体优化的卡点状态,我们需要了解一下MySQL索引推荐底层逻辑,即MySQL代价模型。了解相应规则后,现阶段的问题将迎刃而解。 案例分析及优化 在回顾刚才的问题时,我们发现问题源于原始索引产生了【Using filesort】,从而导致了慢查询的出现。为了解决这个问题,我们新增了一个索引,即【KEY idx_station_no_and_id (station_no, id)】,以替代原有的索引【UNIQUE KEY uniq_storegoods (station_no, sku_id)】。然而,尽管新增索引后大部分慢查询得到了解决,但仍有部分慢查询未能消除。进一步分析发现,这些慢查询是由于SQL没有使用我们期望的索引,而是使用了老索引,从而引发了【Using filesort】问题。在通过explain进行分析后,我们暂时还没有找到合适的解决方案。 问题:尽管我们新增了索引,并且大部分SQL已经能够使用新索引进行优化,但仍存在一些SQL没有使用新索引。 // 通过代价模型进行分析 // 使用上面的测试数据进行分析 // 新增索引后都没有走新索引 // 老索引,扫描行数:1999,代价计算值:1842.80,ref类型索引 // 新索引,扫描行数:1999,代价计算值:1850.46,range类型索引 select 字段1,字段2 ... from store_goods_center where station_no = ‘门店id’ and id > -1 order by id asc; // 新增索引后走新索引 // 老索引,扫描行数:1999,代价计算值:1842.80,ref类型索引 // 新索引,扫描行数:1299,代价计算值:1819.61,range类型索引 select 字段1,字段2 ... from store_goods_center where station_no = ‘门店id’ and id > 35018 order by id asc; 经过分析MySQL的代价模型,我们发现MySQL在选择使用哪个索引时,主要取决于扫描出的数据条数。具体来说,扫描出的数据条数越少,MySQL就越倾向于选择该索引(由于MySQL的索引数据访问类型各异,计算公式也会有所不同。因此,在多个索引的扫描行数相近的情况下,所选索引可能与我们期望的索引有所不同)。顺着这个思路排查,我们发现当id > -1时,无论是使用storeId + skuId还是storeId + id索引进行查询,扫描出的数据条数是相同的。这是因为这两种查询方式都是根据门店查询商品数据,且id值肯定大于1。因此,对于MySQL来说,由于这两种索引扫描出的数据条数相同,所以使用哪种索引效果相差不多。这就是为什么一部分查询走新索引,而另一部分查询走老索引的原因。然而,当查询条件为id > n时,storeId + id索引的优势便得以显现。因为它能够直接从索引中扫描并跳过id <= n的数据,而storeId + skuId索引却无法直接跳过这部分数据,因此真正扫描的数据条数storeId + skuId要大于storeId + id。因此,在查询条件为id > n时,MySQL更倾向于使用新索引。(需要注意的是,示例给出的数据索引数据访问类型不同,一个是range索引类型,一个是ref索引类型。由于算法不同,即使某个索引的检索数据率略高于另一个索引,也可能导致系统将其推荐为最优索引) 问题已经分析清楚,主要原因是存在多个索引,且根据索引代价计算公式的代价相近,导致难以抉择。因此,解决这个问题的方法不应该是同时定义两个会让MySQL"纠结"的索引选择。相反,应该将两个索引融合为一个索引。具体的解决方案是根据门店查询,将原来的主键id作为上次查询的最大id替换为skuId。在算法切换完成后,删除新的门店+主键id索引。然而,这种方式可能会引发另一个问题。由于底层排序算法发生了变化(由原来的主键id改为skuId),可能导致无法直接从底层服务切换。此时,应考虑从下游使用此接口服务的应用进行切换。需要注意的是,如果下游系统是单机分页迭代查询门店数据,那么下游系统可以直接进行切换。但如果这种分页查询动作同时交给多台应用服务器执行,切换过程将变得相当复杂,他们的切换成本与底层切换成本相同。但是,这个系统的对外服务属于这种情况,下游调用系统会有多台应用服务器协作分页迭代查询数据,为这次优化带来很大影响。 最终,让底层独立完成切换方式最为合适。在切换过程中,关键在于正确区分新老算法。老算法在迭代过程中不应切换至新算法。原系统对外服务提供的下次迭代用的id可用来进行区分。新算法在返回下次迭代用的id基础上增加一个常量值,例如10亿(加完后不能与原数据冲突,也可以将迭代id由整数转换成负数以区分新老算法)。因此,如果是第一次访问,直接使用新算法;如果不是第一次访问,需要根据下次迭代用的id具体规则来判断是否切换新老算法。 总结与后续规划 使用Explan执行计划存在无法提前预知索引选择的局限性。然而,只要熟悉MySQL底层代价模型的计算公式,我们就能预知索引的走向。借助代价模型,我们不仅可以分析索引冲突的原因,还可以在发生冲突之前进行预警。甚至在添加索引之前,我们也可以根据代价模型公式来排查潜在问题。此外,根据数据业务密度,我们还可以预估当前索引的合理性,以及是否可能出现全表扫描等情况。因此,深入研究MySQL代价模型对于优化索引管理具有关键意义。 未来我们的系统应用将结合MySQL代价模型进行集成,实现自动分析数据库和表的信息,以发现当前索引存在的问题,例如索引冲突或未使用索引导致的全表扫描。此外,该工具还可以针对尚未添加索引的表,根据数据情况提供合适的索引推荐。同时,该工具还能够预测当数据达到某种密度时,可能出现全表扫描的问题,从而帮助提前做好优化准备。 为了实现这些功能,我们将首先对MySQL代价模型进行深入研究,全面了解其计算公式和原理。这将有助于我们编写相应的算法,自动分析数据库和表的信息,找出潜在的索引问题。此外,我们还关注易用性和实用性,确保用户能够轻松地输入相关数据库和表的信息,并获取有关优化建议。 该工具的开发将有助于提高数据库性能,减少全表扫描的发生,降低系统资源消耗。同时,它还可以为数据库管理员和开发人员提供便利,使他们能够更加专注于其他核心业务。通过结合MySQL代价模型,我们相信这个工具将在优化索引管理方面发挥重要作用,为企业带来更高的效益。 参考资料 https://github.com/mysql/mysql-server 作者:京东零售 王多友 来源:京东云开发者社区 转载请注明来源

优秀的个人博客,低调大师

简洁实用,告别繁琐,解锁云端相册新体验——LSKY PRO

云相册顾名思义专业提供手机相片存储服务。 特点有自动上传、自动同步、轻松分享。 设备在经过用户的允许后,用户在手机上拍摄的照片上传到云相册。 用户就能随时随地在云相册上看到用户手机拍摄的照片。 应用简览 Lsky Pro,中文名兰空图床,是一款强大的在线图片上传和管理工具,可用作个人云上相册,同时也是写作和创作过程中的绝佳图片库。这令人印象深刻的图床项目首次亮相于2017年10月,起初采用ThinkPHP 5作为开发平台。经过多次版本升级和改进,兰空图床在2021年末启动了一项全新的重写计划,并于2022年3月发布了备受期待的2.0版本。其引人注目的特点包括简洁易用的界面、丰富的功能集合,以及快速的图片上传和管理能力。这些特性为项目赢得了广泛的认可,兰空图床在GitHub上拥有近3.4k的星标,证明了其受欢迎程度。 主要功能 支持本地等多种第三方云储存 AWS S3、阿里云 OSS、腾讯云 COS、七牛云、又拍云、SFTP、FTP、WebDav、Minio 多种数据库驱动支持,MySQL 5.7+、PostgreSQL 9.6+、SQLite 3.8.8+、SQL Server 2017+ 支持配置使用多种缓存驱动,Memcached、Redis、DynamoDB、等其他关系型数据库,默认以文件的方式缓存 多图上传、拖拽上传、粘贴上传、动态设置策略上传、复制、一键复制链接 强大的图片管理功能,瀑布流展示,支持鼠标右键、单选多选、重命名等操作 自由度极高的角色组配置,可以为每个组配置多个储存策略,同时储存策略可以配置多个角色组 可针对角色组设置上传文件、文件夹路径命名规则、上传频率限制、图片审核等功能 支持图片水印、文字水印、水印平铺、设置水印位置、X/y 轴偏移量设置、旋转角度等 支持通过接口上传、管理图片、管理相册 支持在线增量更新、跨版本更新 图片广场 应用特色 一、"直观控制台" - Lsky Pro 提供基本信息一目了然 Lsky Pro的控制台以直观的方式呈现基础信息,让用户能够迅速了解系统状态和重要数据。这个用户友好的界面帮助用户轻松管理和监控其图床系统,确保一切正常运行。 二、"多元存储选择" - Lsky Pro 提供多种存储选项 Lsky Pro为用户提供了多种存储方式的选择,包括本地存储,以及第三方云存储服务如AWS S3、阿里云 OSS、腾讯云 COS、七牛云、又拍云、SFTP、FTP、WebDav和Minio。这意味着您可以根据需要轻松地将您的图片和文件保存在不同的存储环境中,以满足各种需求和场景。无论是寻求高度可定制性还是极致便捷,Lsky Pro都能满足您的存储需求。 三、"多样化图片管理功能" - Lsky Pro 提供丰富的图片处理选项 Lsky Pro 提供了强大的图片管理功能,包括图片水印、文字水印、水印平铺、自定义水印位置、X/Y轴偏移量设置、图像旋转等多项功能。此外,它还支持瀑布流展示,允许鼠标右键操作、单选和多选功能,以及文件重命名等操作。这一系列功能使您能够轻松处理和管理您的图片资源,满足不同的图像处理需求,为您的创作和展示提供更多选择和便捷操作。 四、"灵活的角色权限管理" - Lsky Pro 允许详细的权限配置 Lsky Pro 提供了高度自定义的角色权限配置,允许为每个角色组定义多个储存策略,同时,储存策略也可以应用于多个角色组。这意味着您可以精细地控制不同用户组的权限和访问策略。此外,您可以根据角色组的需求设置上传文件和文件夹路径的命名规则,限制上传频率,甚至开启图片审核等功能。这种角色权限的精细化配置确保您能够满足各种安全性和管理需求,确保数据的可控性和安全性。 五、"广泛数据库兼容性" - Lsky Pro 支持多种数据库操作 Lsky Pro 提供了多种数据库驱动支持,包括 MySQL 5.7+、PostgreSQL 9.6+、SQLite 3.8.8+ 以及 SQL Server 2017+。这意味着您可以根据自己的数据库偏好和要求,轻松地将 Lsky Pro 集成到各种数据库环境中。不管您选择哪种数据库,Lsky Pro 都能提供可靠的数据操作支持,确保您能够高效管理和检索您的图像和文件数据。 安装指南 进入云原生应用商店 搜索 Lsky Pro 进入详情,选择包类型(本应用支持,docker安装,ram安装) 点击安装,执行相应命令即可。如有疑问可参阅使用文档 或加入社区 关于云原生应用市场 云原生应用市场是一个汇聚了各类开源软件的应用市场,不仅可以作为你自己的 Helm Chart 仓库,提供丰富多样的Helm应用,还有 Docker 应用、Rainbond 应用模板、信创应用等多种选择。 官网:https://hub.grapps.cn/ 微信群:关注 云原生应用市场 公众号加入技术交流群

优秀的个人博客,低调大师

Kafka ETL 的应用及架构解析|告别 Kafka Streams,让轻量级流处理更加简单

作者:竹恩、岁月、不周 关键词:Kafka ETL,高弹性、免运维、低成本 引言: 阿里云消息队列 Kafka 版提供兼容 Apache Kafka 生态的全托管服务,彻底解决开源产品长期的痛点,是大数据生态中不可或缺的产品之一。随着 Kafka 越来越流行,最初只是作为简单的消息总线,后来逐渐成为数据集成系统,Kafka 可靠的传递能力让它成为流式处理系统可靠的数据来源。在大数据工程领域,Kafka 在承接上下游、串联数据流管道方面发挥了重要作用,Kafka 应用流式框架处理消息也逐渐成为趋势。 消息流处理框架选型 说到流计算,常用的便是 Storm、Spark Streaming 和 Flink,目前这些框架都已经完美的支持流计算,并且都有相应的使用案例,但这些框架使用起来门槛相对较高,首先要学习框架和各种技术、规范的使用,然后要将业务迁移到这些框架中,最后线上使用、运维这些流计算框架,对于简单的流处理应用来说,可能较为复杂。 在与传统流处理系统对接中,由于所有的数据基础都要从一个系统流入 Kafka 然后再流入到另一个系统中,以至于引发 Kafka 社区的思考:与其把数据从一个系统传递到下一个系统中做处理,为何不自己实现一套流处理框架呢?基于这个考量,从 0.10.0 版本开始,Kafka 不仅为每一个流行的流式处理框架提供了可靠的数据来源,还提供了一个强大的流式处理类库 Kafka Streams,并将其作为客户端类的一部分。这样,开发人员就可以在应用程序里读取、处理和生成事件,而不需要再依赖外部的处理框架。 但由于 Kafka Streams 本身是一个 Java 客户端库,需要开发人员自行打包和部署;同时 Kafka Streams 是开源版本,可靠性和可用性不能得到很好的保障,也不能实现按需使用;此外使用过程中需要用到流的编程,使用的门槛也相对较高。 消息流处理框架主要面临的问题 通过前面对常见的消息流处理的介绍,不论是传统的流处理架构还是 Kafka Streams,对于开发人员来说都会面临一些问题,尤其是在面对 70%以上简单流场景的需求,原有的方案弊端被不断放大,客户仍然需要投入较大的人力成本和较高的资源,同时整个架构也很复杂。总体来说,目前面临主要是四个方面的问题: 1、运维成本较大,研发团队自行编写代码,后期持续维护,运维成本较大; 2、技术成本较大,对于很多轻量或简单计算需求,需要进行技术选型,引入一个全新组件的技术成本过高; 3、学习成本不可预期,在某组件选定后,需要研发团队进行学习并持续维护,这就带来了不可预期的学习成本; 4、自行选用开源组件后,可靠性和可用性不能得到有效保证。 面对这些问题,阿里消息队列 Kafka 也推出了相应的解决方案:Kafka ETL。 阿里云的解决方案 - Kafka ETL Kafka ETL 简介 阿里云消息队列 Kafka 版推出更低成本的 Kafka –ETL 组件,是一款免运维的流计算组件,主要特性是支持配置化流式处理消息。Kafka ETL 组件主要提供的是非时间窗口相关的流计算服务,客户可以配置,甚至简单写入几行代码就能满足包括格式转换、内容富化、本地聚合、路由分发等常用的数据处理需求。 Kafka ETL 在使用上拆分成有向无环图,在计算节点转换时,把 Topic 作为一个存储,在 Topic 里进行有状态的计算,还可以支持消息的转储。 目前 Kafka ETL 已支持的模版包括: 1)数据清洗:规则过滤; 2)转换模版:字符串替换,添加前后缀、字符串大小写转换、空格去除数; 3)数据富化模版:数据富化; 4)Split 模版:Topic Split; 5)路由模版:Topic 路由。 Kafka ETL 优势 通过对 Kafka ETL 基础应用及功能的介绍可以看到,相比于 Storm、Spark Streaming、Flink、Kafka Streams,Kafka ETL 的优势主要体现在以下四个方面: 1)开箱即用,免运维; 2)节省成本,不用额外购买其他流计算产品,目前 Kafka ETL 仍处于公测免费阶段; 3)低代码,支持快速上线,学习成本低,一站式体验,技术投入小,时间成本节省 80%; 4)便于监控排查,控制台上相关日志信息比较全面。 Kafka ETL 操作 通过以上 Kafka ETL 应用和优势的介绍可以看到 Kafka ETL 在使用中的具备轻量、低成本等特性,不仅如此,Kafka ETL 的操作也比较简单,仅需三步便可完成 ETL 操作。 1)第一步:创建任务 选择 Kafka 来源实例、来源 Topic,以及对应的选择 Kafka 目标实例、目标 Topic。并配置消息初始位置、失败处理以及创建资源方式。 2)第二步:编写 ETL 主逻辑 选择 Python 3 作为函数语言。这里提供了多种数据清洗、数据转化模板,比如规则过滤、字符串替换、添加前/后缀等常用函数。 3)第三步:设置任务运行、异常参数配置,并执行 Kafka ETL 应用场景 基于 Kafka ETL 的功能和优势,目前 Kafka ETL 主要应用在下面这些场景中: 1)转储场景,支持格式化数据,以方便数据进行转储; 2)流式处理场景,流式计算,支持消息的流式处理,主要提供的是非时间窗口相关的流计算服务; 3)实时行为计算场景,包括风控,金融,电商等需要实时行为计算场景; 4)还支持其他一些场景,包括实时报表,自动化运营场景等。 Kafka ETL 的架构解析 通过前三部分介绍,想必大家对阿里云 Kafka ETL 有了一定了解,本节的主要内容是对 Kafka ETL 的架构进行解析,帮助大家对 Kafka ETL 有更深入的理解。Kafka ETL 是基于 Kafka connect + 函数计算,为云上的用户提供一套数据流转和计算的一站式解决方案。 在当今的大数据、云计算时代,一个复杂的大型系统一般都会由许多处理特定任务的子系统构成。各个子系统一般会由不同的团队开发,因此,各系统中的数据在内容和格式上,存在天然的不一致性。当数据在各个子系统之间流转的时候,需要对数据进行格式处理,以消除各系统数据之间格式的不同。此外,还可能需要收集来自各个子系统中的异构数据,对采集到的数据做一些加工和计算,然后投递到数据仓库进行后续的数据分析。在这个过程中,可以抽象出两个典型场景:数据流转场景和数据计算场景。 数据流转场景主要面对的问题是,异构系统间数据如何流转? 数据计算场景主要面对的问题是,如何在数据流转过程中,进行数据的加工计算? 下面就展开对这两个主要场景进行介绍。 数据流转场景 在数据流转场景中,可能需要将各种关系型和非关系型数据库中的数据导入到数据仓库;或是将 mysql 的数据导入到 ElasticSearch,用来提高查询体验;此外一些数据还会导入到图形数据库。这些场景面临的主要问题是: 1)各种不同源之间的数据如何拷贝; 2)如何满足传递的实时性。 比如,mysql 里的一个变更,希望马上能在 ElasticSearch 中反映出来,不然就会导致后台数据变更了,用户却查不出最新的数据。除此之外,还需要保证数据拷贝的高可用、可伸缩性以及可扩展性。 为应对这一问题,传统的方案可能是:为各数据源之间都专门做一个数据拷贝工具。这种方案会带来以下问题: 1)首先是工作量问题,需要为每个场景都写一个专门的工具,工作量会非常大; 2)业务耦合严重,比如想监听价格变化,就需要在所有变化价格的业务里,都加上一个 producer。假设上层 schema 发生了变化,下层就需要修改代码,因此上层需要感知到所有下层的存在。 专门的工具看起来不太可行,那么,是否做一个完全通用的工具,让它支持任意数据源之间数据拷贝。这个听起来不错,但是实际却不可行,正因为它要求太通用了,很难去制定各种规范。 Kafka connect 正是为解决以上异构数据同步问题而生的。它解决的思路是在各个数据源之间加一层消息中间件,所有的数据都经过消息中间件进行存储和分发。这样做的好处有: 1)通过消息中间件做异步解耦,所有系统只用和消息中间件通信; 2)需要开发的解析工具数量,也从原来的 n 平方个,变成线性的 2*n 个。 Kafka connect 则用于连接消息系统和数据源,根据数据的流向不同,连接可以分为 source connector 和 sink connector。其原理也很简单,souce connector 负责解析来源数据,转换成标准格式的消息,通过 Kafka producer 发送到 Kafka broker 中。同理,sink connector 则通过 Kafka consumer 消费对应的 Topic,然后投递到目标系统中。在整个过程中,Kafka connect 统一解决了任务调度、与消息系统交互、自动扩缩容、容错以及监控等问题,大大减少了重复劳动。但是,如何将来源系统的数据解析成 message、或是将 message 解析成目标系统数据,这两件事情是需要根据不同的数据系统而做不同实现的。对于目前主流的系统,各大厂商均有提供相应的 connector 实现。 阿里云消息队列 Kafka 版就提供了全托管、免运维的 Kafka Connect,用于消息队列 Kafka 版和其他阿里云服务之间的数据同步。可以看到消息队列 Kafka 版支持了 Mysql source connector、OSS sink connector、MaxCompute sink connector 以及 FC sink connector 等主流的 connector。如果用户想要使用这些 connector 进行数据同步,只用在消息队列 Kafka 控制台的图形界面上做几个配置,就可以一键拉起 connector 任务。 数据计算场景 Kafka connect 解决了异构数据源之间数据同步的问题,虽然也提供了 transformer,解决部分数据转换需求,但是依旧缺乏实时计算能力。为应对以上场景的数据实时处理需求,市场上出现了许多优秀的处理工具,从最初的 Hadoop,Hive 到 Spark,Flink 以及 Kafka streams 等,都提供了对应的组件模块和上下游解决方案。 但这些处理方案中都存在或多或少的问题,主要的问题是: 首先处理框架比较重,占用资源多。比如当下流行的 Spark 和 Flink 都需要先搭建一个集群,集群本身运行起来就要不少资源。集群规模一般按照流量峰值配置,在大多数时候,资源是浪费的。 其次在诸多框架中,需要根据实际需求做技术选型,后期可能需要专门的团队或者人去运维,这个过程需要较大的学习成本和后期维护成本。 针对部分无状态的简单计算,函数计算或许是一个很好的选择。阿里云上的函数计算,是事件驱动的全托管计算服务。使用函数计算时,用户无需采购与管理服务器等基础设施,只需编写并上传代码即可。函数计算会帮助用户准备好计算资源,弹性地、可靠地运行任务,并提供日志查询、性能监控和报警等功能。可以看到,函数计算以简单易用的方式给用户的许多场景提供了计算能力。 阿里云消息队列 Kafka 版近期推出的 Kafka ETL 组件,通过 Kafka+Kafka connect+函数计算的架构,能够很好的应对数据转储+实时计算问题。具有轻量,学习成本低,开发周期短,资源动态伸缩,简单快速等优点。 Kafka+Kafka connect+函数计算的云原生数据应用解决方案,通过 Kafka connect 作为实时处理任务触发器,能够实时接收到新发送到消息队列集群的数据,然后转发到函数计算,触发实时数据处理任务的运行。在这个数据流转阶段,将大量异构系统中的数据以各种方式汇集到 Kafka 中,然后围绕 Kafka 为中心,做后续的处理。作为后续数据流转中的一环,Kafka connect 除了保障数据的实时性以外,还解决了任务调度、与消息系统交互、自动扩缩容、容错以及监控等问题,大大减少了重复劳动。数据到了函数计算以后,会自动触发用户自己编写的数据处理逻辑,对原始消息内容进行计算。最后,函数计算可以将加工完成的数据,投递到用户指定的目标端,例如投递回消息队列 Kafka,或者是投递到 Max compute 进行下一步的数据分析。以上所说的整个任务的配置、创建、运行,都只用通过云上的 Kafka 控制台图形页面进行操作即可完成。 应用场景详解 接下来一起来看一个 Kafka ETL 的应用示例。在这个示例中,用户的一个大致使用场景是这样的:从一个电商业务系统中,采集日志,存储到 Kafka 侧,然后需要对日志数据进行加工,最后将加工好的数据投递到两个目标端:一个是投递到 MaxCompute 进行数据分析;另一个是投递到 ElasticSearch 进行日志检索。 现在分节点来看,如何利用消息队列 Kafka 版来做这个事情: 1)第一步:采集原始日志到消息队列 Kafka 版的 Topic 中 这里可以使用一些比较成熟的开源组件例如 FileBeat、Logstash、Flume 等,将用户应用端的日志消息,投递到 Kafka 中。一般情况下,这个步骤会将原始的日志信息投递到 Kafka。当然这里也可以做一些简单的转换,但一般不这么做,而是保留一份原始信息,原始的日志可能来自各个关联的应用,内容和格式会存在些许差异。 在这个例子,订单应用中生成一条日志。日志中包含用户 Id、action、订单 Id 以及当前状态: 从支付应用中,又生成一条日志。日志中同样包含以上信息,只是格式上存在一些小差别。 这两条来自不同子系统的日志,都被采集到 Kafka 的一个 Topic,叫做 user_order_raw。这两条日志,最终对应这个 Topic 里的两条消息,key 均为 null,value 为日志的原始内容。可以看到,由于来源系统日志格式不一样,这个 Topic 里包含的这两条消息,消息格式上也存在一定差别。 2)第二步是对 Topic 中的消息,做简单的数据加工计算 数据到达 Kafka 的 Topic 之后,Kafka connect 会消费消息,并投递到函数计算中。数据到了函数计算后,需要对这个数据进行加工计算,计算的目标是抽取 UserId、Action、OrderId 以及 Status,并将数据都转换为大写字母。然后所有处理后的消息发往 MaxCompute 进行分析,此外还需要筛选 Action 为 pay 的所有消息发往 Elastic Search 中。 这个步骤,可以在 Kafka 控制台图形界面上创建 ETL 任务,用户选择数据来源 Topic:user_order_raw,然后写一段对数据的处理代码。这里,ETL 已经提供了部分模板,可以在模板的基础上,做稍许改动即可。 本示例的代码如下图所示。在这个例子中,用户需要写一段从不同格式的日志中,抽取UserId、Action、OrderId 以及 Status 的代码,然后将所有处理过的消息路由到目标 Topic。 3)最后一步,可以将处理完的消息,再次投递到目标端。 函数计算将处理完的消息,投递回 Kafka。经过这一步处理,所有消息被路由到目标Topic:user_order_processed,此时这个 Topic 中会包含两条消息,消息 key 为null,value 如下所示: 另外,Action 为 pay 的消息还会被路由到 Topic: user_order_pay_info 中,此时这个 Topic 会包含一条消息,key 为 null,value 如下所示: 可以看到,此时的消息格式已经统一了。 这个例子中,将 Topic:user_order_processed 中所有处理完的订单相关消息,投递到 MaxCompute 中进行数据分析。将 Topic: user_order_pay_info 中的支付信息,投递到 ElasticSearch 中进行后续搜索。 这一步,可以一键创建相应的 Kafka connect 任务,将数据投递到相应的目标端。 总结一下上述整个过程。在这个示例中,所要做的仅仅是在消息队列 Kafka 控制台上配置一个 ETL 任务,写一小段处理代码即可。上述步骤中,第二步处理完数据之后可以不经过第三步投递回 Kafka,而是在处理完之后,直接路由到 MaxCompute 和 ES 中。在该例子中采用的方式是将处理完的数据再次发送回 Kafka 中,然后再投递到目标系统中。这种方式可以在 Kafka 端保留一份处理后的数据,用户还可以比较灵活地对这份数据做进一步处理或者继续投递到其他第三方系统中。 阿里云消息队列Kafka 版的优势 最后,给大家额外分享一下阿里云上的消息队列 Kafka 在内核层面的差异化优势。阿里云上的消息队列 Kafka 版在发展过程中除了解决易用性和稳定性方面的问题以外,还做到了有区分度,并在内核层面做出自己的核心竞争力和优势。 阿里云消息队列 Kafka 版支持云存储和 Local 存储这两种存储引擎。其中 Local Topic 指的就是以 Kafka 原生的方式存储数据,保留开源 Kafka 全部特性,100%兼容开源 Kafka。云存储是接下来要介绍的重点,消息队列 Kafka 通过自研云存储引擎,彻底解决了原生 Kafka 一些深层的 bug,以及因为本身架构而难以解决的问题,实现了支持海量分区、通过多副本技术降低存储成本,以及支持无缝迁移弹缩性。接下来,将详细介绍这三大特性和其中的技术细节。 支持海量分区 在消息引擎中,常见的消息存储方式有碎片式存储和集中式存储。 碎片式存储通常以 Topic 或者分区纬度存储,其主要优势是架构简单,可以针对 Topic 或者分区,控制持久化的容量。Kafka 在架构上,是基于分区的碎片式存储,在分区规模不大的情况下,可以通过磁盘的顺序读写,获得高效的消息读写性能。通常情况下,一般规格的 Kafka 集群可以支持到千级别的分区规模。如果分区规模持续扩大,且大部分分区都有读写请求时,由于这种设计上的问题,原本的顺序读写就变成了随机读写,从而导致 Kafka 的读写性能急剧下降。 不同于碎片式存储,集中式存储则将所有消息集中存储到同一个 Commit Log,然后根据 Topic 和分区信息构建队列,队列通常作为索引使用。相比于碎片式存储,集中式存储的主要优势是,支持分区数多,很容易通过删除旧的 Commit Log 的形式控制磁盘水位。在阿里云消息队列 Kafka 中,底层的自研云存储引擎正是采用了集中式的存储方式,云存储引擎相比 Kafka 原生的存储的主要优势有: 1)解决了 Kafka 分区规模扩大时,性能急剧下降的问题,相比于原生 Kafka 千级别的分区规模,其支持的分区规模可以达到十万级别; 2) 在大量分区同时写的场景下,相比原生 Kafka 的碎片式存储,自研云存储引擎能获得更好的性能;同时,对写入耗时做了优化,减少了毛刺的产生。 多副本技术优化 为保证 Kafka 集群的高可靠和高可用性,通常情况下会为所有 Topic 设置 3 副本存储。这样,在出现机器宕机时,Kafka 可以快速从可用的 Follower 副本中选出新的 Leader,接替宕机机器上的 Leader 继续提供服务。消息队列 Kafka 在选择块存储设备时,选择的是阿里云上的云盘。云盘是阿里云为云服务器 ECS 提供的,数据块级别的块存储产品,具有低时延、高性能、持久性、高可靠等特点。云盘本身采用了分布式三副本机制,为 ECS 实例提供了极强的数据可靠性和可用性保证。 在这种背景下,在 Kafka 层面设置 3 副本,由于使用了云盘,实际会有 9 个副本。同时,由于 Kafka 层面的 Follower 需要主动从 Leader 同步数据,这也会消耗集群的计算和网络资源,将用户的业务流量扩大至 3 倍。但是,如果在 Kafka 层面设置单副本,由于 Kafka 本身不能利用到云盘的 3 副本能力,其高可用性就不能保证。因此,如何利用好云盘的 3 副本能力,降低的存储成本和网络成本,就成了面临的一大挑战。 阿里云通过接入自研云存储引擎,解决了存储成本和网络成本问题。其核心原理主要是:在自研存储引擎中引入了逻辑队列和物理队列两个概念。逻辑队列也就是暴露给用户的概念,在这里可以直接理解成客户端看到的 partition,而物理队列则用于实际存储数据。通过映射关系,将逻辑队列和物理队列绑定在一起。在自研引擎中,所有的分区在逻辑上都是单副本的。数据的可靠性和可用性由云盘底层的 3 副本机制保证。在正常情况下,发送到特定逻辑 partition 的数据,都会根据映射关系,写入到对应的物理队列中。同理,消费也是根据映射关系从实际的物理队列中拉取。 接下来来看云存储是如何做到容错和高可用的。例如,在节点 0 的 ECS 宕机时,可以通过 QueueMapper,秒级切换逻辑队列 0 的映射关系到节点 1 中的已有队列 Queue-3,或者新增一个物理队列 Queue-4。此时,发往逻辑队列-0 的消息,将被路由到Queue-3 或者 Queue-4 中。这种情况下,用户的发送业务不会受到影响,依旧可以继续发送成功,并且最新的消息也能被消费到。当然,在这种 Failover 期间,会存在一个问题:逻辑队列-0 在节点-0 上的消息,暂时不能消费;但是,对大多数应用场景来说,短暂的部分消息消费延迟并不是大问题,只要不影响发送就能满足要求。 在节点-0 的 ECS 宕机后,阿里云备用 ECS 会迅速生成新的机器替换节点-0,挂载原有云盘,分钟级时间内恢复节点-0 服务。在节点-0 恢复后,只用重新将逻辑队列-0 的映射关系切回 Queue-0,系统又重新恢复了原有状态。此时,发送/消费依旧能保持原生Kafka 的特性。 通过以上方式,将存储成本节省到原生 Kafka 的大约三分之一。同时,由于在 Kafka 层面,副本数是 1,从而避免了 Follower 从 Leader 中同步数据的操作,网络流量也节省到原生 Kafka 的大约三分之一。 水平扩容,秒级数据均衡 弹性扩缩能力是消息队列的核心能力之一。由于 Kafka 服务端节点是有状态的,因此新增了若干节点之后,需要重新均衡各个 Topic 的队列,使得客户端往集群中发送或是消费的流量,能均衡地打到后端各个服务节点上。 开源 Kafka 在水平扩展了机器之后,做数据均衡的主要方式有两种: 第一种是在新的 broker 中新增队列。这种方式主要的痛点是: 1)系统状态发生改变,这种情况下一些多语言客户端的早期版本,需要客户端主动重启,否则无法消费新分区; 2) 第二是 Kafka 设计上的问题,分片数无法下降,导致后续无法缩容。 第二种做法是数据迁移。这种方式的主要痛点是: 1)流量复制,产生网络风暴,干扰正常使用; 2)均衡与数据量有关,如果数据量巨大,可能要花费几天来迁移。 那么,云存储引擎是怎么解决以上弹缩问题的呢? 前文提到消息队列 Kafka 引入了两级队列:第一级为逻辑队列,第二级为物理队列,也就是阿里云自研云存储队列。逻辑队列对外暴露,物理队列则用于存储实际数据。通过 QueueMapper 模块维护逻辑队列与物理队列之间的映射关系,如下图所示。 一个逻辑队列可以由多个物理队列拼接而成,通过位点分段映射,保证顺序。扩容时,只需要将逻辑队列指向新机器上的物理队列即可,这样新写入的消息就可以根据新的映射关系,直接写入到新加的机器。同样的,在消费时,可以根据位点分段映射关系,找到实际的物理队列,然后从物理队列中读取消息。 可以看到,通过两级队列分段映射,解决了消息队列弹缩和迁移问题,具有如下优点: 1)服务端扩缩容后,不变更队列数量,保持系统状态不变; 2)扩缩容时无需迁移数据,耗时短,可以在秒级时间内完成 Topic 队列重新均衡; 3)兼顾了吞吐与扩展性,不影响原有消息队列的性能。 总结 简单对主要介绍内容进行总结,Kafka 在流式处理场景中,传统方案一般会采用 Storm、Spark Streaming、Flink 和 Kafka Streams 等流式处理框架,但是开发人员在使用的过程中会遇到不少问题,尤其是在面对 70% 以上简单流场景的需求,会遇到运维成本较高、技术成本较大、学习成本不可预期和可用性、可靠性较低等痛点问题,阿里云消息队列 Kafka 发布 Kafka ETL 组件,是一款免运维的流计算组件,通过 Kafka+Kafka connect+函数计算的架构,能够很好的应对数据转储+实时计算问题,具备免运维、低成本、低代码、易监控等优势。 ​目前 Kafka ETL 正处于免费公测阶段,欢迎大家体验试用。阿里云消息队列 Kafka 版具备内核层面的差异化优势,欢迎前往下方链接了解更多详情。 ​​​​​https://www.aliyun.com/product/kafka​​​​​ ​​如有更多问题,欢迎扫描加入 Kafka 钉钉交流群。​ 点击​​此处​​​,即可查看直播回放了解更多详情!​ ​

优秀的个人博客,低调大师

Android 图片三级缓存之内存缓存(告别软引用(SoftRefrerence)和弱引用(WeakReference))

在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。 但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象, 这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放, 这就有潜在的风险造成应用程序的内存溢出并崩溃。所以看到还有很多相关文章还在推荐用软引用或弱引用 (SoftReference or WeakReference),就有点out了。 下面是谷歌官方给的图片缓存例子(其实就是翻译上面链接的文章): 在你应用程序的UI界面加载一张图片是一件很简单的事情,但是当你需要在界面上加载一大堆图片的时候,情况就变得复杂起来。在很多情况下, (比如使用ListView, GridView 或者 ViewPager 这样的组件),屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,最终导致OOM。 为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用, 从而对这些图片进行GC操作。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后, 用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生。 这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。下面我们就来看一看如何使用内存缓存技术来对图片进行缓存, 从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。 内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。 这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移 除。 在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了, 因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。 另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放, 这就有潜在的风险造成应用程序的内存溢出并崩溃。 为了能够选择一个合适的缓存大小给LruCache, 有以下多个因素应该放入考虑范围内,例如: 你的设备可以为每个应用程序分配多大的内存? 设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上? 你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。 图片的尺寸和大小,还有每张图片会占据多少内存空间。 图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。 你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。 并没有一个指定的缓存大小可以满足所有的应用程序,这是由你决定的。你应该去分析程序内存的使用情况,然后制定出一个合适的解决方案。一个太小的缓存空间, 有可能造成图片频繁地被释放和重新加载,这并没有好处。而一个太大的缓存空间,则有可能还是会引起 java.lang.OutOfMemory 的异常。 下面是一个使用 LruCache 来缓存图片的例子: [java] view plain copy privateLruCache<String,Bitmap>mMemoryCache; @Override protectedvoidonCreate(BundlesavedInstanceState){ //获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 //LruCache通过构造函数传入缓存值,以KB为单位。 intmaxMemory=(int)(Runtime.getRuntime().maxMemory()/1024); //使用最大可用内存值的1/8作为缓存的大小。 intcacheSize=maxMemory/8; mMemoryCache=newLruCache<String,Bitmap>(cacheSize){ @Override protectedintsizeOf(Stringkey,Bitmapbitmap){ //重写此方法来衡量每张图片的大小,默认返回图片数量。 returnbitmap.getByteCount()/1024; } }; } publicvoidaddBitmapToMemoryCache(Stringkey,Bitmapbitmap){ if(getBitmapFromMemCache(key)==null){ mMemoryCache.put(key,bitmap); } } publicBitmapgetBitmapFromMemCache(Stringkey){ returnmMemoryCache.get(key); } 在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。 一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(800*480*4)。因此,这个缓存大小可以存储2.5页的图片。 当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。 [java] view plain copy publicvoidloadBitmap(intresId,ImageViewimageView){ finalStringimageKey=String.valueOf(resId); finalBitmapbitmap=getBitmapFromMemCache(imageKey); if(bitmap!=null){ imageView.setImageBitmap(bitmap); }else{ imageView.setImageResource(R.drawable.image_placeholder); BitmapWorkerTasktask=newBitmapWorkerTask(imageView); task.execute(resId); } } BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中。 [java] view plain copy classBitmapWorkerTaskextendsAsyncTask<Integer,Void,Bitmap>{ //在后台加载图片。 @Override protectedBitmapdoInBackground(Integer...params){ finalBitmapbitmap=decodeSampledBitmapFromResource( getResources(),params[0],100,100); addBitmapToMemoryCache(String.valueOf(params[0]),bitmap); returnbitmap; } } 本文转自农夫山泉别墅博客园博客,原文链接:http://www.cnblogs.com/yaowen/p/5071184.html,如需转载请自行联系原作者

优秀的个人博客,低调大师

告别数据填报低效!专业表格解决方案,帮开发者搞定全场景需求

前言: 在数字化时代,数据是企业运营的核心资产。表格产品作为数据管理的重要工具,在数据填报场景中提供了多样化的解决方案,极大地提升了数据收集、处理和分析的效率。本文将深入探讨表格产品在数据填报场景中的各类解决方案,涵盖动态表格生成、智能化数据管理、企业级扩展功能等多个方面。 数据源接入: SpreadJS 的 DataManager 是一款功能强大且全面的数据管理工具,它在数据处理流程中扮演着核心枢纽的角色,主要负责数据加载、管理以及数据源更新等关键任务。 DataManager 具备出色的兼容性,支持多种常见的数据格式,无论是从本地文件还是远端服务器,都能轻松实现数据加载。这种灵活性使得用户可以将不同类型、不同来源的数据整合在一起,进行统一高效的管理,打破了数据孤岛的限制。 此外,DataManager 拥有强大的数据同步能力。在绝大多数应用场景下,它能够自动将数据的变化实时更新至数据源,或者通过请求的方式将数据变动同步回服务端,确保数据在多端之间的一致性与时效性。 目前 dataManager 有三种方式设置数据源: Http请求: 本地数据data 本地json文件 打开 SpreadJS 在线表格编辑器,如下图所示,依次点击"数据"---->"数据源"---->"添加表",在读取input框中,配置数据请求的连接。 动态表格生成与交互设计: 交叉报表 交叉报表是行、列方向均设有分组的基础报表类型,通过横纵双向分组将数据组织为二维结构,常用于多维度数据统计与分析。其核心特征体现在行列标题确定交叉区域,由双向分组字段动态扩展生成网格化布局,交叉点通过汇总函数计算数值,与传统纵向分组报表形成显著区别。 SpreadJS表格产品通过行列动态配置技术,能够根据用户的选择自动生成动态行列扩展的交叉报表。例如下面的场景,在统计报表场景中,客户需要做一个教育水平与职业选择关系的统计。希望动态生成纵向按照教育水平为行,横向按照职业类别为列的表格进行统计分析。 SpreadJS报表在接入数据源后,从左侧的数据源列表中拖入相应的字段。 通过右侧的设置面板,将对应字段设置为横向扩展。 拖入汇总字段(人数),通过右侧设置面板,将数据类型设置为汇总类型。 2.补齐装饰元素(对角线,边框,表头等) 3.点击预览查看效果 主子报表 主子报表是一种通过主表与子表结构展示层级化数据的特殊报表类型。其核心特征是将数据集划分为主表数据集与子表数据集,主表通常以自由格式展示汇总信息,子表则以列表形式呈现明细数据,形成一对多的关联关系。 上下文(Context)是SpreadJS报表中一个非常重要的概念。它会影响报表的布局,因为默认情况下,它会在所有上下文中过滤子单元格中的数据,并且所有子单元格都会随着上下文重复显示。单个单元格可以有两种上下文: 垂直上下文:在此情况下,单元格将左侧具有垂直溢出方向的单元格用作垂直上下文。 水平上下文:在此情况下,单元格将上方具有水平溢出方向的单元格用作水平上下文。 使用上下文功能可以更加遍历的创建主从报表,例如下面的场景,用户希望通过订单与订单明细之间的关系,创建一张主从报表,在显示订单列表的同时,显示每条订单的订单明细。在SpreadJS报表中只需要进行如下的操作: 构建报表的基本轮廓,在SpreadJS报表在接入数据源后,从左侧的数据源列表中拖入相应的字段,统计字段设置统计公式。 2.对报表外观进行美化,设置背景色,边框,字体,合并单元格 3.子订单信息的子表与主表订单表存在主子关系,需要设置上下文进行关联。设置后,生成报表时会按照主子关系进行生成。 4.设置过滤条件,过滤掉子表没有内容的数据。 5.点击预览查看效果 交互式填报界面 上文配置的字段,基于报表展示的结果,可以对相应的字段区域(单元格)进行填报,从而实现网页端在线填报。这种技术在多表关联填报中尤为重要,支持主从表、交叉表、分页表等多种复杂表样设计,满足业务场景的多样化需求。例如,在采购管理系统中,采购商品报价需要处理大量的输入字段和联动计算公式,表格产品能够有效解决这些技术难点,提供比Excel文件更好的业务信息交互方式。 数据管理能力 数据校验规则 表格产品内置了多种数据校验规则,包括主数据校验、格式验证及公式联动校验。主数据校验可以确保部门名称等数据的标准化,格式验证则通过正则表达式等方式保证数据的准确性。公式联动校验则能够自动检查数据之间的逻辑关系,避免错误数据的录入。这些校验规则大大提高了数据填报的准确性和可靠性。 如下图所示:设置了一个数字验证器,其中数字应该大于或等于5,填报内容小于5的填报项会用红框圈出,具有警示效果。 导入,导出 SpreadJS报表支持导入导出Excel文件,保留原有数据的同时实现无失真转换。这一功能在企业数据管理中尤为重要,因为它允许用户无缝地从传统Excel工作方式过渡到在线填报系统,而无需担心数据丢失或格式混乱。 例如:制作复杂报表时,往往有参照的Excel样本,SpreadJS报表工具支持导入模板功能,可以利用此功能导入已有的Excel模板,在其基础上进行设计,避免了重头设计导致很多重复的工作。 最终生成的报表需要做离线填报的时候,可以使用SpreadJS报表的导出功能,导出成Excel文件,Excel文件能够还原整个报表的样式,边框,数据验证等元素。满足用户线下离线填报的需求。 打印 通过SpreadJS报表工具的打印功能,可以将报表的填报内容进行打印,便于纸质留存,在一些特殊行业中纸质文档有其存在的必要性,例如在医疗、法律等受严格监管的行业,纸质文档是法律凭证和存档的必备形式,打印功能可确保文件符合行业规范要求。 自动化汇总分析 SpreadJS报表工具提供汇总功能,其内部嵌入的公式引擎,支持超过400种以上的Excel公式计算,利用Excel公式计算可以支持支持多层级数据汇总求和。通过填报提交进行上报。另外搭配Wyn 商业智能BI软件,可以对整个状态进行监控,后续利用其数据分析功能,可以进一步对上报的数据进行实时分析。这一功能在财务填报、生产报价、人力资源管理等高频业务场景中尤为实用。通过自动化汇总分析,企业可以快速生成各类报表,为管理层提供深入的数据洞察,支持更为精准的战略决策。 企业级扩展功能 权限控制体系 表格产品实现了单元格级别的数据权限管理,可以从单元格->行列->工作表->工作簿实现不同级别下的权限管理,例如,部门间的数据可以设置为不可见,可以根据部门权限对部门的数据进行隐藏处理,这样确保数据的安全性。此外,通过角色权限管理,企业可以为不同用户设置不同的读写权限,避免数据被误操作或泄露。这种权限控制体系在大型企业中尤为重要,能够有效保护敏感数据的安全。 国际化支持 表格产品支持多语言配置,适配跨国企业的数据采集需求。例如支持阿拉伯语(Left to Right)的输入方式,大大方便了跨国企业的数据采集操作,提高了工作效率。 低代码开发支持 模板化快速搭建 SpreadJS报表产品可以内嵌至低代码平台(活字格),通过拖拽式设计器生成报表模板,使用低代码快速配置数据源和页面,降低了技术门槛。例如,活字格低代码开发平台提供了一套全面的表格报表解决方案,支持多种报表样式的灵活展示和数据填报功能,包括行式报表、分组报表、交叉报表等。用户无需编写复杂的代码,即可快速搭建符合自身需求的报表模板。 数据库直连能力 活字格低代码平台支持SQLServer、Oracel、MySQL等多种数据库对接,实现填报数据实时入库。这一功能使得企业能够直接连接现有的数据库系统,无需手动导入导出数据,大大提高了数据处理的效率。 实际应用案例 生产采购管理系统 在ERP系统应用中,SpreadJS报表产品展现出了显著的价值。它有效解决了用户从传统Excel操作习惯到现代化信息化办公的转变难题,将Excel的操作体验融入到Web应用中,降低了学习成本,提高了工作效率。同时,它还简化了报表制作流程,提高了报表制作效率,帮助快速生成各类报表,满足不同业务场景的需求。例如: 港澳车务系统是一款专为优化客户车务管理业务而设计的ERP应用软件。在项目启动阶段,团队经过周密的规划和深入的业务分析,成功构建了系统框架并完成了各业务模块的开发。然而,系统上线后却遭遇了一系列的挑战,主要源于客户长期依赖的传统Excel操作习惯于现有的技术框架之间存在不兼容。客户在日常工作中普遍使用Excel,虽然他们期望实现现代化的信息办公方式,但仍希望新系统能够保留Excel操作的便利性。为此,团队迫切需要构建一套基于BS架构且能兼容Excel操作习惯的应用系统。在此背景下,ERP应用开发团队引入SpreadJS表格插件,有效地解决了这一操作系统的转换问题,确保系统能够在BS架构下兼容Excel的操作习惯,满足其现代化办公的需求。 在系统中通过报表插件展示报表数据: 利用填报功能实现即时在线填报: 利用低代码开发平台完成整体系统开发,嵌入SpreadJS报表产品完成核心业务逻辑 未来发展趋势 随着人工智能技术的不断发展,智能生成多样化表格解决方案应运而生。这些软件不仅能够自动识别、整理和生成表格,还能根据用户需求进行数据排序、筛选、合并等操作。例如,生成表格的软件支持多种表格格式输出,如Excel、CSV、PDF等,满足了不同用户的需求。 此外,AI智能生成多样化表格解决方案通过自动识别数据类型和结构,生成相应的计算公式,简化了数据解决流程。例如,某高校利用生成表格公式快速计算出学生的总分、平均分等指标,提高了工作效率。 表格产品在数据填报场景中提供了多样化的解决方案,从动态表格生成到智能化数据管理,再到企业级扩展功能和低代码开发支持,极大地提升了数据收集、处理和分析的效率。随着技术的不断进步,表格产品将继续创新,为企业提供更为强大和便捷的数据管理工具。 体验地址 SpreadJS在线表格编辑器

优秀的个人博客,低调大师

告别CRUD地狱!活字格低代码3小时交付业务系统,效率提升10倍 | 葡萄城技术团队

引言 在数字化转型浪潮中,企业IT团队正面临严峻挑战:80%的开发精力消耗在表单、审批流等重复性功能上,而业务部门却抱怨“需求响应太慢”。如何打破这一僵局?西安葡萄城推出的活字格企业级低代码平台,凭借可视化开发、AI智能生成、开放集成三大核心能力,正将业务系统开发效率提升至传统模式的10倍——本文将通过真实案例拆解其颠覆性价值。 一、CRUD困局:数字化转型的隐形成本 调研显示,企业业务系统开发存在两大效率黑洞: 重复劳动占比畸高 基础功能开发(表单设计、权限管理、报表配置)占用70%以上工时 某零售企业IT日志显示:每月需修改156次相似表单字段 交付周期严重脱节 传统开发模式下,一个供应商管理系统平均需2周交付 业务规则变更时,代码级修改导致平均3天停服 > 客户痛点实录: > > “我们5人团队每年开发20个系统,其中16个是不同名字的CRUD套娃。” > > ——某制造集团CIO访谈 二、活字格破局之道:三大技术引擎解析 ✅ 1. 流程中心:拖拽式设计,复杂逻辑秒级配置 BPMN可视化引擎:采用简化版BPMN 2.0标准,像绘制流程图一样搭建审批/派单流程(流程开发指南) 智能路由决策: 自动识别组织架构中的审批路径(如“部门经理→财务总监”) 支持动态加签、并行会签等复杂模式 热更新技术:流程规则修改后即时生效,无需重新发布 > 案例:某物流公司用活字格重构运单系统,审批流配置时间从3天→2小时,错误率下降90%。 ✅ 2. 开放集成:企业级系统的“连接器” 单点登录(SSO):无缝嵌入SAP、用友等系统界面,用户无感知切换(集成方案) 混合开发模式: 高性能操作(如万级数据导入)通过服务端命令实现 前端保留低代码快速迭代优势 遗产系统迁移:独家支持Access应用一键转Web系统,历史数据自动继承 ✅ 3. AI革命:自然语言驱动开发 智能体(Agent): 用户说“创建采购审批流程”,自动生成流程图+数据模型 支持多轮对话修正(如“增加质检环节”) 设计时辅助: 输入“供应商管理系统”,AI推荐字段结构(名称、信用等级、合作年限) 自动生成移动端适配页面 三、效率实测:3小时交付供应商管理系统 时间轴(某科技企业真实案例): 时间 任务 关键技术 09:00-09:30 AI生成数据模型 自然语言→数据库表自动生成 09:30-10:30 搭建供应商准入流程 拖拽式BPMN设计器 10:30-11:00 对接天眼查API校验企业资质 服务端命令调用REST API 11:00-11:30 发布至企业微信/钉钉 多端自适应渲染引擎 > 效果对比: > > - 传统开发:14人天,成本≈3.2万元 > - 活字格:3小时,成本≈0.2万元 四、行业实践:低代码的千企千面 行业 典型场景 效率提升 教育 智能组卷系统 出题效率提升8倍 零售 动态会员管理体系 积分规则调整周期1天→1小时 制造 设备报修-MES工单联动 故障响应速度提升60% 五、为什么选择活字格? 能力维度 传统开发 活字格方案 开发速度 周级交付 小时级交付 修改成本 需停服+全量回归测试 实时热更新 技术门槛 需专业程序员 业务人员可参与 TCO(总拥有成本) 高(人力+运维) 降低60%以上 结语:低代码是生产力革命,不是妥协方案 国外某机构预测,2025年70%的新应用将由低代码构建时,活字格已证明: 对IT团队:将创造力从CRUD中释放,专注架构优化与创新 对业务部门:获得“所想即所得”的数字化能力,试错成本趋近于零 对企业:用软件迭代速度构筑市场竞争壁垒 正如首批尝鲜者所言:“以前不敢想的定制化系统,现在今天提需求、明天就能用——这才是数字化的本来面目。” > 行动指南: > > 1. 下载活字格试用版 > 2. 参加《3天速成训练营》(附实战案例包) > 3. 联系葡萄城专家获取行业解决方案白皮书

优秀的个人博客,低调大师

【快讯】基于 Fastcms 的微信插件上线了:Fastcms 基于插件的扩展能力,彻底告别臃肿的代码

基于Fastcms的微信支付插件上线了,Fastcms是首个把支付做成插件的开源系统,本文带您尝个鲜 准备工作 下载并安装Fastcms 下载Fastcms 安装Fastcms 下载Fastcms微信支付插件 下载微信支付插件包 插件安装与使用 安装插件 进入Fastcms系统后台,安装插件如下图 配置插件 点击插件列表右边配置按钮,进入如下界面 使用插件 PC网站使用支付插件,如下图 代码托管 Gitee | Github

优秀的个人博客,低调大师

告别百度网盘,一个公私兼备的网盘系统-Cloudreve

应用简览 Cloudreve 是一个开源的网盘系统,支持不同的云存储平台,用户在实际使用时无须关心物理存储方式。你可以使用 Cloudreve 搭建个人用网盘、文件分享系统,亦或是针对大小团体的公有云系统。 主要特性 ☁️ 支持本机、从机、七牛、阿里云 OSS、腾讯云 COS、又拍云、OneDrive (包括世纪互联版) 、S3兼容协议 作为存储端 📤 上传/下载 支持客户端直传,支持下载限速 💾 可对接 Aria2 离线下载,可使用多个从机节点分担下载任务 📚 在线 压缩/解压缩、多文件打包下载 💻 覆盖全部存储策略的 WebDAV 协议支持 ⚡ 拖拽上传、目录上传、流式上传处理 🗃️ 文件拖拽管理 👩‍👧‍👦 多用户、用户组、多存储策略 🔗 创建文件、目录的分享链接,可设定自动过期 👁️‍🗨️ 视频、图像、音频、 ePub 在线预览,文本、Office 文档在线编辑 🎨 自定义配色、黑暗模式、PWA 应用、全站单页应用、国际化支持 🚀 All-In-One 打包,开箱即用 🌈 ... ... 应用特色 一、多样化的外部存储、流畅的文件上传、管理体验 Cloudreve 支持对接多种外部存储,如本机、从机、七牛、阿里云 OSS、腾讯云 COS、又拍云、OneDrive (包括世纪互联版) 、S3兼容协议等,文件的上传和下载全部为客户端直传,无需经过服务器中转。 同时,在 Web 端,Cloudreve 提供了强大的文件管理及上传组件:通过拖拽管理文件;多选、范围选择批量操作文件,对文件进行分享、移动、复制、压缩等操作。功能丰富的上传任务管理器,支持目录上传、断点续传、并行上传、拖拽文件上传。 二、离线下载,构建私人离线资源库 将磁力链、种子文件、下载链接交给 Cloudreve 处理,Cloudreve 会在服务端下载您指定的文件, 并自动上传到对应的存储端中.您还可以通过从机 Cloudreve 节点,将不同的离线下载和转存任务分配到不同服务器处理,减轻主节点的负载压力。 三、快速分享你的文件、目录 对 Cloudreve 中的文件创建分享链接,别人可以通过此链接访问、下载或预览文件。分享链接完全由你控制,提供了丰富的配置选项。支持密码保护、自动过期等选项。 四、多端同步,从多种设备、应用中访问文件 通过 Cloudreve iOS 客户端,你可以在移动设备上便捷地管理文件; 通过 WebDAV 协议,还可以在所有支持 WebDAV 协议的应用中访问并操作文件,包括 系统文件管理器、 各种文件管理器客户端、视频播放器、笔记软件等,比如:Cloudreve for iOS、nPlayer、Solid Explorer File Manager、Joplin等。 安装指南 进入云原生应用商店 搜索 Cloudreve 进入详情,选择包类型(本应用支持,docker安装,ram安装) 点击安装,执行相应命令即可。如有疑问可参阅使用文档 或加入社区 关于云原生应用市场 云原生应用市场是一个汇聚了各类开源软件的应用市场,不仅可以作为你自己的 Helm Chart 仓库,提供丰富多样的Helm应用,还有 Docker 应用、Rainbond 应用模板、信创应用等多种选择。 官网:https://hub.grapps.cn/ 微信群:关注 云原生应用市场 公众号加入技术交流群

优秀的个人博客,低调大师

告别宕机!KubeSphere v4.1.3 联手 K8s v1.32.5,手把手教你打造“永不掉线”的云原生底座

本文首发:运维有术,作者术哥。 近期我在深入研究 MCP(Model Context Protocol)、大语言模型(LLM)与 Kubernetes / KubeSphere 的融合,期望为企业容器平台引入智能化运维与资源调度能力。 本文将基于最新的 KubeSphere v4.1.3 和 Kubernetes v1.32.5,手把手教大家搭建一套企业级的高可用集群。这套方案不仅确保了 Kubernetes 控制平面的高可用性,还实现了 KubeSphere 所有核心服务的多副本部署,可以作为生产环境的最佳实践参考。 实战服务器配置(架构1:1复刻小规模生产环境,只是配置略有不同) | 节点角色 | 主机名 | CPU(核) | 内存(GB) | 系统盘(GB) | 数据盘(GB) | IP | 备注 | | :----------: | :-----------: | :-----: | :------: | :--------: | :--------: | :----------: | :--------------: | | Control 节点 | ksp-control-1 | 4 | 8 | 50 | 100 | 192.168.9.91 | 控制节点 | | Control 节点 | ksp-control-2 | 4 | 8 | 50 | 100 | 192.168.9.92 | 控制节点 | | Control 节点 | ksp-control-3 | 4 | 8 | 50 | 100 | 192.168.9.93 | 控制节点 | | Worker 节点 | ksp-worker-1 | 8 | 32 | 50 | 100 | 192.168.9.94 | 部署通用工作负载 | | Worker 节点 | ksp-worker-2 | 8 | 32 | 50 | 100 | 192.168.9.95 | 部署通用工作负载 | | Worker 节点 | ksp-worker-3 | 8 | 32 | 50 | 100 | 192.168.9.96 | 部署通用工作负载 | | 合计 | 6 | 36 | 120 | 300 | 600 | | | 实战环境涉及软件版本信息 操作系统:openEuler 24.03 LTS SP1 x86_64 KubeSphere:v4.1.3 Kubernetes:v1.32.5 KubeKey: v3.1.10 前置条件 1.1 初始化操作系统 可选配置 ,可以参考 Kubernetes 集群节点 openEuler 22.03 LTS SP3 系统初始化指南,完成操作系统初始化配置。 如果没有挂载额外的数据盘,无需定制数据存储目录,可以选择使用 KubeKey 自动完成前置条件的安装配置。 虽然,初始化指南以 openEuler 22.03 LTS SP3 为例,但配置过程同样适用于其他 Linux 发行版,只是命令略有不同。 选配:社群成员可以使用 Ansible 脚本完成系统初始化: # 验证节点连通性 ansible -m ping k8s # 系统初始化 ansible-playbook ../../playbooks/init-os.yaml -l k8s # 数据磁盘初始化(使用 LVM) ansible-playbook ../../playbooks/init-disk-lvm.yaml -l k8s # kubesphere/k8s 节点初始化(安装 k8s 依赖组件) ansible-playbook ../../playbooks/deploy-ksp-nodes.yaml 部署 Kubernetes 集群 本文使用 KubeSphere 官方提供的集群部署工具 KubeKey,搭建一套生产级高可用 Kubernetes 集群。该集群由 3 个 Control 节点组成控制平面,确保集群管理组件的高可用性;同时配备 3 个 Worker 节点用于运行业务工作负载,实现计算资源的合理分配。这种架构设计既保证了集群的稳定性和可靠性,又提供了良好的业务负载承载能力。 在 ksp-control-1 节点上执行以下操作。该节点作为集群的主要控制节点,将用于执行集群部署和管理任务。 2.1 下载 KubeKey 下载最新版的 KubeKey(v3.1.10) mkdir /srv/kubekey cd /srv/kubekey/ # 选择中文区下载(访问 GitHub 受限时使用) export KKZONE=cn curl -sfL https://get-kk.kubesphere.io | sh - 查看 KubeKey 支持的 Kubernetes 版本列表 ./kk version --show-supported-k8s $ ./kk version --show-supported-k8s v1.19.0 v1.19.8 v1.19.9 v1.19.15 ......(受限于篇幅,中间的不展示,请读者根据需求查看) v1.30.11 v1.30.12 v1.30.13 v1.31.0 v1.31.1 v1.31.2 v1.31.3 v1.31.4 v1.31.5 v1.31.6 v1.31.7 v1.31.8 v1.31.9 v1.32.0 v1.32.1 v1.32.2 v1.32.3 v1.32.4 v1.32.5 v1.33.0 v1.33.1 KubeKey 支持的 Kubernetes 版本非常丰富且及时更新。本文选择最新稳定版 v1.32.5。对于生产环境,建议选择: 次要版本号为双数的版本(如 v1.30.x、v1.32.x) 补丁版本号不低于 5 的版本(如 v1.30.13、v1.32.5) 避免使用太旧的版本,建议选择最近 2-3 个次要版本 可以优先考虑 v1.30.13 或 v1.32.5 这类经过充分验证的版本 选择版本时需要平衡新特性和稳定性,既要确保关键功能可用,又要避免可能的兼容性问题。 2.2 创建 Kubernetes 集群部署配置 创建集群配置文件 本文选择了 v1.32.5 版本的 Kubernetes。为了便于后续管理和维护,我们将配置文件命名为 ksp-k8s-v1325.yaml ,这个命名方式清晰地表明了所使用的 Kubernetes 版本。如果不指定配置文件名称,KubeKey 会使用默认的文件名 config-sample.yaml。 ./kk create config -f ksp-k8s-v1325.yaml --with-kubernetes v1.32.5 重要提示: 请妥善保管安装配置文件 ksp-k8s-v1325.yaml。该文件对于后续的集群维护工作(如节点扩容、集群卸载等)至关重要。如果文件丢失,您将需要重新创建配置文件,这可能会带来额外的工作量。 配置文件包含大量可配置选项。如需了解全部配置参数及其用法,请查阅官方文档中的 KubeKey 配置示例。这将帮助您更好地定制集群配置。 修改配置文件 本示例采用 6 节点高可用部署架构,其中: 3 个 Control 节点: 部署 control-plane 和 etcd 组件 3 个 Worker 节点: 部署业务工作负载 请使用 vi 编辑器,编辑配置文件 ksp-k8s-v1325.yaml,修改 kind: Cluster 小节中的以下配置项: hosts:指定节点的 IP、ssh 用户、ssh 密码 roleGroups:分别设置etcd、control-plane、worker 节点对应的 hosts 名称 internalLoadbalancer: 启用内置的 HAProxy 负载均衡器 domain:自定义域名 lb.opsxlabs.cn ,没特殊需求可使用默认值 lb.kubesphere.local clusterName:自定义 opsxlabs.cn ,没特殊需求可使用默认值 cluster.local autoRenewCerts:该参数可以实现证书到期自动续期,默认为 true containerManager:容器运行时使用 containerd storage.openebs.basePath:默认没有,新增配置 ,指定 openebs 默认存储路径为 /data/openebs/local registry.privateRegistry:可选配置, 解决 Docker 官方镜像不可用的问题 registry.namespaceOverride: 可选配置, 解决 Docker 官方镜像不可用的问题 下面是完整的配置示例,请根据实际环境修改相关配置: apiVersion: kubekey.kubesphere.io/v1alpha2 kind: Cluster metadata: name: opsxlabs spec: hosts: - {name: ksp-control-1, address: 192.168.9.91, internalAddress: 192.168.9.91, user: root, password: "OpsXlab2025!"} - {name: ksp-control-2, address: 192.168.9.92, internalAddress: 192.168.9.92, user: root, password: "OpsXlab2025!"} - {name: ksp-control-3, address: 192.168.9.93, internalAddress: 192.168.9.93, user: root, password: "OpsXlab2025!"} - {name: ksp-worker-1, address: 192.168.9.94, internalAddress: 192.168.9.94, user: root, password: "OpsXlab2025!"} - {name: ksp-worker-2, address: 192.168.9.95, internalAddress: 192.168.9.95, user: root, password: "OpsXlab2025!"} - {name: ksp-worker-3, address: 192.168.9.96, internalAddress: 192.168.9.96, user: root, password: "OpsXlab2025!"} roleGroups: etcd: - ksp-control-1 - ksp-control-2 - ksp-control-3 control-plane: - ksp-control-1 - ksp-control-2 - ksp-control-3 worker: - ksp-worker-1 - ksp-worker-2 - ksp-worker-3 controlPlaneEndpoint: ## Internal loadbalancer for apiservers internalLoadbalancer: haproxy domain: lb.opsxlabs.cn address: "" port: 6443 kubernetes: version: v1.32.5 clusterName: opsxlabs.cn #dnsDomain: opsxlabs.cn autoRenewCerts: true containerManager: containerd etcd: type: kubekey network: plugin: calico kubePodsCIDR: 10.233.64.0/18 kubeServiceCIDR: 10.233.0.0/18 ## multus support. https://github.com/k8snetworkplumbingwg/multus-cni multusCNI: enabled: false storage: openebs: basePath: /data/openebs/local # 默认没有的新增配置,base path of the local PV registry: privateRegistry: "registry.cn-beijing.aliyuncs.com" #使用 KubeSphere 在阿里云的镜像仓库 namespaceOverride: "kubesphereio" registryMirrors: [] insecureRegistries: [] addons: [] 2.3 部署 Kubernetes 集群 现在我们使用上面生成的配置文件来创建 Kubernetes 集群。执行以下命令开始部署: export KKZONE=cn ./kk create cluster -f ksp-k8s-v1325.yaml --with-local-storage 重要说明: 本文使用 OpenEBS LocalPV 作为默认存储解决方案,需要在命令中添加 --with-local-storage 参数启用本地存储功能 该参数会自动部署 OpenEBS LocalPV 相关组件,简化存储配置流程 执行命令后,KubeKey 会首先进行环境依赖检查,确保满足部署 Kubernetes 的所有要求。检查通过后,系统会要求确认安装,输入 yes 并按 Enter 键继续部署流程。 $ ./kk create cluster -f ksp-k8s-v1325.yaml --with-local-storage _ __ _ _ __ | | / / | | | | / / | |/ / _ _| |__ ___| |/ / ___ _ _ | \| | | | '_ \ / _ \ \ / _ \ | | | | |\ \ |_| | |_) | __/ |\ \ __/ |_| | \_| \_/\__,_|_.__/ \___\_| \_/\___|\__, | __/ | |___/ 09:26:42 CST [GreetingsModule] Greetings 09:26:42 CST message: [ksp-worker-3] Greetings, KubeKey! 09:26:43 CST message: [ksp-control-3] Greetings, KubeKey! 09:26:43 CST message: [ksp-control-1] Greetings, KubeKey! 09:26:43 CST message: [ksp-control-2] Greetings, KubeKey! 09:26:44 CST message: [ksp-worker-1] Greetings, KubeKey! 09:26:44 CST message: [ksp-worker-2] Greetings, KubeKey! 09:26:44 CST success: [ksp-worker-3] 09:26:44 CST success: [ksp-control-3] 09:26:44 CST success: [ksp-control-1] 09:26:44 CST success: [ksp-control-2] 09:26:44 CST success: [ksp-worker-1] 09:26:44 CST success: [ksp-worker-2] 09:26:44 CST [NodePreCheckModule] A pre-check on nodes 09:26:48 CST success: [ksp-control-1] 09:26:48 CST success: [ksp-control-2] 09:26:48 CST success: [ksp-worker-1] 09:26:48 CST success: [ksp-worker-2] 09:26:48 CST success: [ksp-worker-3] 09:26:48 CST success: [ksp-control-3] 09:26:48 CST [ConfirmModule] Display confirmation form +---------------+------+------+---------+----------+-------+-------+---------+-----------+--------+--------+------------+------------+-------------+------------------+--------------+ | name | sudo | curl | openssl | ebtables | socat | ipset | ipvsadm | conntrack | chrony | docker | containerd | nfs client | ceph client | glusterfs client | time | +---------------+------+------+---------+----------+-------+-------+---------+-----------+--------+--------+------------+------------+-------------+------------------+--------------+ | ksp-control-1 | y | y | y | y | y | y | y | y | y | | | | | | CST 09:26:47 | | ksp-control-2 | y | y | y | y | y | y | y | y | y | | | | | | CST 09:26:48 | | ksp-control-3 | y | y | y | y | y | y | y | y | y | | | | | | CST 09:26:48 | | ksp-worker-1 | y | y | y | y | y | y | y | y | y | | | | | | CST 09:26:48 | | ksp-worker-2 | y | y | y | y | y | y | y | y | y | | | | | | CST 09:26:48 | | ksp-worker-3 | y | y | y | y | y | y | y | y | y | | | | | | CST 09:26:48 | +---------------+------+------+---------+----------+-------+-------+---------+-----------+--------+--------+------------+------------+-------------+------------------+--------------+ This is a simple check of your environment. Before installation, ensure that your machines meet all requirements specified at https://github.com/kubesphere/kubekey#requirements-and-recommendations Install k8s with specify version: v1.32.5 Continue this installation? [yes/no]: yes 重要说明: 系统检查显示 nfs-client、ceph-client、glusterfs-client 等存储客户端组件未安装。这些组件将在后续存储系统对接实战中单独安装配置 容器运行时(docker/containerd)将根据配置文件中 containerManager 参数自动选择安装 整个部署过程预计需要 5-10 分钟完成。具体耗时取决于: 网络带宽状况 服务器硬件配置 集群节点数量 本次实战环境部署耗时 7 分钟。 当终端显示以下信息时,表示 Kubernetes 集群已成功创建并完成初始化: 09:33:26 CST Pipeline[CreateClusterPipeline] execute successfully Installation is complete. Please check the result using the command: kubectl get pod -A 验证 k8s 集群状态 3.1 查看集群节点信息 在控制节点1运行 kubectl 命令获取 Kubernetes 集群上的可用节点列表。 kubectl get nodes -o wide 在输出结果中可以看到,当前的 Kubernetes 集群有 6 个可用节点(3 个 Control 节点和 3 个 Worker 节点)。每个节点的详细信息包括: 节点角色(control-plane/worker) Kubernetes 版本(v1.32.5) 节点内部 IP 地址 操作系统类型(openEuler 24.03 LTS-SP1) Linux 内核版本 容器运行时类型及版本(containerd v1.7.13) 所有节点状态均为 Ready,表明集群已经成功部署并正常运行。 $ kubectl get nodes -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME ksp-control-1 Ready control-plane 69s v1.32.5 192.168.9.91 <none> openEuler 24.03 (LTS-SP1) 6.6.0-72.0.0.76.oe2403sp1.x86_64 containerd://1.7.13 ksp-control-2 Ready control-plane 46s v1.32.5 192.168.9.92 <none> openEuler 24.03 (LTS-SP1) 6.6.0-72.0.0.76.oe2403sp1.x86_64 containerd://1.7.13 ksp-control-3 Ready control-plane 45s v1.32.5 192.168.9.93 <none> openEuler 24.03 (LTS-SP1) 6.6.0-72.0.0.76.oe2403sp1.x86_64 containerd://1.7.13 ksp-worker-1 Ready worker 41s v1.32.5 192.168.9.94 <none> openEuler 24.03 (LTS-SP1) 6.6.0-72.0.0.76.oe2403sp1.x86_64 containerd://1.7.13 ksp-worker-2 Ready worker 42s v1.32.5 192.168.9.95 <none> openEuler 24.03 (LTS-SP1) 6.6.0-72.0.0.76.oe2403sp1.x86_64 containerd://1.7.13 ksp-worker-3 Ready worker 42s v1.32.5 192.168.9.96 <none> openEuler 24.03 (LTS-SP1) 6.6.0-72.0.0.76.oe2403sp1.x86_64 containerd://1.7.13 3.2 查看 Pod 信息 使用以下命令查看集群中所有命名空间的 Pod 运行状态和详细信息。 kubectl get pods -A -o wide 输出结果符合预期, 所有 Pod 的状态都是 Running。 [root@ksp-control-1 kubekey]# kubectl get pods -A -o wide NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES kube-system calico-kube-controllers-678fc69664-6hr2k 1/1 Running 0 38s 10.233.96.3 ksp-worker-3 <none> <none> kube-system calico-node-4nnxh 1/1 Running 0 38s 192.168.9.95 ksp-worker-2 <none> <none> kube-system calico-node-ch5q7 1/1 Running 0 38s 192.168.9.93 ksp-control-3 <none> <none> kube-system calico-node-d94p9 1/1 Running 0 38s 192.168.9.92 ksp-control-2 <none> <none> kube-system calico-node-g8fl5 1/1 Running 0 38s 192.168.9.96 ksp-worker-3 <none> <none> kube-system calico-node-jnhzw 1/1 Running 0 38s 192.168.9.91 ksp-control-1 <none> <none> kube-system calico-node-kjs7j 1/1 Running 0 38s 192.168.9.94 ksp-worker-1 <none> <none> kube-system coredns-5c68fb7f85-46pj8 1/1 Running 0 18s 10.233.101.1 ksp-control-3 <none> <none> kube-system coredns-5c68fb7f85-thqvq 1/1 Running 0 75s 10.233.96.2 ksp-worker-3 <none> <none> kube-system haproxy-ksp-worker-1 1/1 Running 0 50s 192.168.9.94 ksp-worker-1 <none> <none> kube-system haproxy-ksp-worker-2 1/1 Running 0 50s 192.168.9.95 ksp-worker-2 <none> <none> kube-system haproxy-ksp-worker-3 1/1 Running 0 50s 192.168.9.96 ksp-worker-3 <none> <none> kube-system kube-apiserver-ksp-control-1 1/1 Running 0 82s 192.168.9.91 ksp-control-1 <none> <none> kube-system kube-apiserver-ksp-control-2 1/1 Running 0 60s 192.168.9.92 ksp-control-2 <none> <none> kube-system kube-apiserver-ksp-control-3 1/1 Running 0 59s 192.168.9.93 ksp-control-3 <none> <none> kube-system kube-controller-manager-ksp-control-1 1/1 Running 0 81s 192.168.9.91 ksp-control-1 <none> <none> kube-system kube-controller-manager-ksp-control-2 1/1 Running 0 60s 192.168.9.92 ksp-control-2 <none> <none> kube-system kube-controller-manager-ksp-control-3 1/1 Running 0 59s 192.168.9.93 ksp-control-3 <none> <none> kube-system kube-proxy-7524z 1/1 Running 0 46s 192.168.9.94 ksp-worker-1 <none> <none> kube-system kube-proxy-8dscp 1/1 Running 0 47s 192.168.9.95 ksp-worker-2 <none> <none> kube-system kube-proxy-b6rk5 1/1 Running 0 46s 192.168.9.92 ksp-control-2 <none> <none> kube-system kube-proxy-bvkd6 1/1 Running 0 45s 192.168.9.96 ksp-worker-3 <none> <none> kube-system kube-proxy-ns5rs 1/1 Running 0 45s 192.168.9.93 ksp-control-3 <none> <none> kube-system kube-proxy-rhkhd 1/1 Running 0 46s 192.168.9.91 ksp-control-1 <none> <none> kube-system kube-scheduler-ksp-control-1 1/1 Running 0 80s 192.168.9.91 ksp-control-1 <none> <none> kube-system kube-scheduler-ksp-control-2 1/1 Running 0 60s 192.168.9.92 ksp-control-2 <none> <none> kube-system kube-scheduler-ksp-control-3 1/1 Running 0 59s 192.168.9.93 ksp-control-3 <none> <none> kube-system nodelocaldns-7blvp 1/1 Running 0 60s 192.168.9.93 ksp-control-3 <none> <none> kube-system nodelocaldns-7vdm7 1/1 Running 0 72s 192.168.9.91 ksp-control-1 <none> <none> kube-system nodelocaldns-gtw74 1/1 Running 0 57s 192.168.9.95 ksp-worker-2 <none> <none> kube-system nodelocaldns-l29sv 1/1 Running 0 61s 192.168.9.92 ksp-control-2 <none> <none> kube-system nodelocaldns-rl9c8 1/1 Running 0 56s 192.168.9.96 ksp-worker-3 <none> <none> kube-system nodelocaldns-xctwp 1/1 Running 0 56s 192.168.9.94 ksp-worker-1 <none> <none> kube-system openebs-localpv-provisioner-9644bcfd5-rvgqz 1/1 Running 0 29s 10.233.94.1 ksp-worker-1 <none> <none> 3.3 查看 Image 列表 使用 crictl 命令查看 Control 节点上已经下载的容器镜像列表,可以看到所有核心组件的镜像都已经成功拉取 [root@ksp-control-1 ~]# crictl image ls IMAGE TAG IMAGE ID SIZE registry.cn-beijing.aliyuncs.com/kubesphereio/cni v3.27.4 dc6f84c32585f 88.8MB registry.cn-beijing.aliyuncs.com/kubesphereio/coredns 1.9.3 5185b96f0becf 14.8MB registry.cn-beijing.aliyuncs.com/kubesphereio/k8s-dns-node-cache 1.22.20 ff71cd4ea5ae5 30.5MB registry.cn-beijing.aliyuncs.com/kubesphereio/kube-apiserver v1.32.5 495c5ce47cf7c 28.8MB registry.cn-beijing.aliyuncs.com/kubesphereio/kube-controller-manager v1.32.5 85dcaf69f0001 26.4MB registry.cn-beijing.aliyuncs.com/kubesphereio/kube-controllers v3.27.4 6b1e38763f401 33.5MB registry.cn-beijing.aliyuncs.com/kubesphereio/kube-proxy v1.32.5 f532b7356fac4 30.9MB registry.cn-beijing.aliyuncs.com/kubesphereio/kube-scheduler v1.32.5 2729fb488407e 20.8MB registry.cn-beijing.aliyuncs.com/kubesphereio/node v3.27.4 3dd4390f2a85a 117MB registry.cn-beijing.aliyuncs.com/kubesphereio/pause 3.10 873ed75102791 320kB registry.cn-beijing.aliyuncs.com/kubesphereio/pause 3.9 e6f1816883972 322kB registry.cn-beijing.aliyuncs.com/kubesphereio/pod2daemon-flexvol v3.27.4 72bfa61e35b35 7.64MB 查看 Worker 节点上已经下载的 Image 列表 [root@ksp-worker-1 ~]# crictl images ls IMAGE TAG IMAGE ID SIZE registry.cn-beijing.aliyuncs.com/kubesphereio/cni v3.27.4 dc6f84c32585f 88.8MB registry.cn-beijing.aliyuncs.com/kubesphereio/coredns 1.9.3 5185b96f0becf 14.8MB registry.cn-beijing.aliyuncs.com/kubesphereio/haproxy 2.9.6-alpine 52687313354fc 12.3MB registry.cn-beijing.aliyuncs.com/kubesphereio/k8s-dns-node-cache 1.22.20 ff71cd4ea5ae5 30.5MB registry.cn-beijing.aliyuncs.com/kubesphereio/kube-controllers v3.27.4 6b1e38763f401 33.5MB registry.cn-beijing.aliyuncs.com/kubesphereio/kube-proxy v1.32.5 f532b7356fac4 30.9MB registry.cn-beijing.aliyuncs.com/kubesphereio/node v3.27.4 3dd4390f2a85a 117MB registry.cn-beijing.aliyuncs.com/kubesphereio/pause 3.9 e6f1816883972 322kB registry.cn-beijing.aliyuncs.com/kubesphereio/pod2daemon-flexvol v3.27.4 72bfa61e35b35 7.64MB registry.cn-beijing.aliyuncs.com/kubesphereio/provisioner-localpv 3.3.0 739e82fed8b2c 28.8MB 至此,我们已经成功完成了一个生产级高可用 Kubernetes 集群的部署。该集群由 3 台 Control 节点组成的控制平面和 3 台 Worker 节点组成的工作负载平面构成,不仅确保了集群管理组件的高可用性,也为业务应用提供了可靠的运行环境。 部署 KubeSphere 完成 Kubernetes 集群部署后,我们将通过 Helm 安装 KubeSphere v4.1.3,为集群提供强大的可视化管理能力。 4.1 安装核心组件 KubeSphere Core 使用 Helm 安装 KubeSphere 的核心组件 KubeSphere Core。KubeSphere Core 是 KubeSphere 的基础组件,提供了用户认证、多租户管理、RBAC 等核心功能。 说明: KubeKey 部署 Kubernetes 集群时会自动安装 Helm,无需手动安装 建议使用最新版本的 KubeSphere Core Chart,本文使用的是 v1.1.4 版本 helm upgrade --install -n kubesphere-system --create-namespace ks-core https://charts.kubesphere.io/main/ks-core-1.1.4.tgz \ --set global.imageRegistry=swr.cn-southwest-2.myhuaweicloud.com/ks \ --set extension.imageRegistry=swr.cn-southwest-2.myhuaweicloud.com/ks \ --set ha.enabled=true \ --set redisHA.enabled=true \ --set multicluster.hostClusterName=opsxlabs-main \ --debug \ --wait 重要配置说明: ha.enabled: 设置为 true,启用 KubeSphere 组件高可用部署 redisHA.enabled: 设置为 true,启用 Redis 高可用集群 multicluster.hostClusterName: 自定义主集群名称,默认为 host。版本兼容性提示: ks-core-1.1.4 之前版本使用 hostClusterName imageRegistry: 指定组件镜像仓库国内镜像源地址 部署过程预计需要 5-10 分钟完成,具体取决于: 网络带宽状况 服务器配置 Redis HA 集群初始化时间(占比较大) 当安装命令执行完成后,系统将输出 KubeSphere 控制台访问信息,包括: Web 控制台访问地址 默认管理员账号 默认管理员密码 看到以下信息表明 KubeSphere Core 已成功完成部署。 NOTES: Thank you for choosing KubeSphere Helm Chart. Please be patient and wait for several seconds for the KubeSphere deployment to complete. 1. Wait for Deployment Completion Confirm that all KubeSphere components are running by executing the following command: kubectl get pods -n kubesphere-system 2. Access the KubeSphere Console Once the deployment is complete, you can access the KubeSphere console using the following URL: http://192.168.9.161:30880 3. Login to KubeSphere Console Use the following credentials to log in: Account: admin Password: P@88w0rd NOTE: It is highly recommended to change the default password immediately after the first login. For additional information and details, please visit https://kubesphere.io. 4.2 命令行验证 KubeSphere Core 状态 查看 Pod 列表: kubectl get pods -n kubesphere-system -o wide 正确执行后,输出结果如下 : $ kubectl get pods -n kubesphere-system -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES extensions-museum-67bf6499df-f5xf4 1/1 Running 0 3m18s 10.233.68.15 ksp-worker-2 <none> <none> ks-apiserver-677647f489-rq6qb 1/1 Running 2 (3m13s ago) 3m18s 10.233.101.19 ksp-control-3 <none> <none> ks-apiserver-677647f489-w6bw2 1/1 Running 2 (3m13s ago) 3m18s 10.233.71.18 ksp-control-2 <none> <none> ks-apiserver-677647f489-wpjm4 1/1 Running 2 (3m12s ago) 3m18s 10.233.93.22 ksp-control-1 <none> <none> ks-console-5855b59f77-6fv9f 1/1 Running 0 2m42s 10.233.68.16 ksp-worker-2 <none> <none> ks-console-5855b59f77-8x76h 1/1 Running 0 2m38s 10.233.94.7 ksp-worker-1 <none> <none> ks-console-5855b59f77-9pzhg 1/1 Running 0 2m36s 10.233.71.20 ksp-control-2 <none> <none> ks-controller-manager-86b4b4cf8-qlk48 1/1 Running 0 3m18s 10.233.101.18 ksp-control-3 <none> <none> ks-controller-manager-86b4b4cf8-xhwsr 1/1 Running 0 3m18s 10.233.71.16 ksp-control-2 <none> <none> ks-controller-manager-86b4b4cf8-ztwv4 1/1 Running 0 3m18s 10.233.93.19 ksp-control-1 <none> <none> ks-core-redisha-haproxy-5d48d7cc88-5p7zw 1/1 Running 0 3m18s 10.233.71.17 ksp-control-2 <none> <none> ks-core-redisha-haproxy-5d48d7cc88-92td4 1/1 Running 0 3m18s 10.233.93.20 ksp-control-1 <none> <none> ks-core-redisha-haproxy-5d48d7cc88-drqrf 1/1 Running 0 3m18s 10.233.101.17 ksp-control-3 <none> <none> ks-core-redisha-server-0 3/3 Running 0 3m18s 10.233.93.23 ksp-control-1 <none> <none> ks-core-redisha-server-1 3/3 Running 0 2m9s 10.233.71.21 ksp-control-2 <none> <none> ks-core-redisha-server-2 3/3 Running 0 80s 10.233.101.21 ksp-control-3 <none> <none> 分析 Pod 列表中的核心组件分布情况。 KubeSphere 核心组件高可用部署: ks-apiserver、ks-console、ks-controller-manager 各部署了3个实例 这些实例分布在不同节点上,确保了控制组件的高可用性 Redis 集群高可用部署: redisha-haproxy 和 redisha-server 各部署了3个实例 实例分布在不同节点上,实现了 Redis 服务的高可用性 KubeSphere 控制台概览 5.1 工作台 我们打开浏览器访问 Control-1 节点的 IP 地址和端口 30880,可以打开熟悉的 KubeSphere 管理控制台的登录页面。 输入默认用户 admin 和默认密码 P@88w0rd,然后点击「登录」。 登录后,系统会要求您更改 KubeSphere 默认用户 admin 的默认密码,输入新的密码并点击「提交」。 提交完成后,系统会跳转到新的风格的 KubeSphere 用户工作台页面。 5.2 集群管理 在「工作台」页面,点击「集群管理」,进入集群管理页面,页面风格更方便多集群管理。 注意! 集群名称符合 我们自定义的 opsxlabs-main ,默认名称 host 。 点击「opsxlabs-main 」主集群,进入集群管理页面。新版本的集群管理菜单更加简洁,默认只有基本的 K8s 管理功能。 以上,就是我今天分享的全部内容。 本次实战,我们聚焦于 KubeSphere v4.1.3 高可用集群的部署。从环境准备到 KubeKey 的配置与执行,我们详细梳理了在生产环境中搭建稳定 Kubernetes 集群的关键步骤。通过 3 个 Control 节点和 3 个 Worker 节点的部署实践,我们验证了 KubeKey 在简化复杂集群部署方面的强大能力,并强调了配置细节对集群稳定性的重要影响。 我知道,对于很多朋友来说,搭建一套生产级的高可用集群,听起来可能有些望而却步。但通过这篇实战,你是否发现,只要掌握了正确的方法和工具,这一切并非遥不可及?我们不仅搭建了一个由 3 个 Control 节点和 3 个 Worker 节点组成的强大集群,更深入了解了 KubeKey 如何简化部署流程,以及如何通过精细配置确保集群的稳定与高效。 当然,这仅仅是个开始!KubeSphere 的魅力远不止于此。在接下来的系列文章中,我将继续带你深入探索 KubeSphere 的更多高级特性,比如如何无缝对接各种存储方案,如何玩转多集群管理,以及如何利用 KubeSphere 强大的可观测性功能,让你的云原生环境尽在掌握。 免责声明: 笔者水平有限,尽管经过多次验证和检查,尽力确保内容的准确性,但仍可能存在疏漏之处。敬请业界专家大佬不吝指教。 本文所述内容仅通过实战环境验证测试,读者可学习、借鉴,但严禁直接用于生产环境 。由此引发的任何问题,作者概不负责! 本文由博客一文多发平台 OpenWrite 发布!

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册