首页 文章 精选 留言 我的

精选列表

搜索[腾讯技术创作特训营S8],共10000篇文章
优秀的个人博客,低调大师

万字长文浅析配置对MySQL服务器的影响 | 京东物流技术团队

有很多的服务器选项会影响这MySQL服务器的性能,比如内存中临时表的大小、排序缓冲区等。有些针对特定存储引擎(如InnoDB)的选项,也会对查询优化很有用。 调整服务器的配置从某种程度来说是一个影响全局的行为,因为每个修改都可能对该服务器上的每个查询造成影响。不过有些选项是针对特定类型的优化的,如果你的请求没有满足条件,它将没有任何作用。 首先我们需要检查的选项是缓冲区大小(buffer size) 每个缓冲区都有其存在的特定原因,一般的规律是大缓冲区意味着高性能,不过仅当请求可以针对该缓冲区扮演的特定角色使用大容量缓存的时候。 当然,增加缓冲区大小是有代价的,下面是一些大缓冲区可能带来的影响。 •交换区 大容量缓冲区可能会导致会使用到操作系统级别的交换区从而造成性能缓慢,这取决于系统内存大小。通常情况下,MySQL服务器在它所需的所有内存都来自物理内存的时候运行最快。当它使用到交换区的时候,性能显著下降。 当为缓冲区分配的内存大小超过服务器的物理内存大小的时候就会使用到交换区。有一些缓冲区是针对每个用户线程的。要确定这些缓冲区究竟需要多少内存,可以用公式 max_connections * buffer_size 来计算。计算出所有缓冲区的内存和,并确保小于mysqld服务器可以使用的内存大小。 •启动时间 mysqld需要分配的内存越多,其启动时间就越长。 •过期数据 我们还会有伸缩性问题,大部分时候是来自线程间的缓存共享。在这些场景中,扩充缓冲区做缓存会产生内存碎片。你通常会在服务器运行数小时后发现内存碎片问题,该问题发生在旧的数据需要从缓冲区中移除以给新数据腾出空间的时候。这会导致高速运转的服务器变慢。 其次我们不仅关注性能优化选项(如优化器选项),我们还会关注一些控制高可用的选项。事务运行得越安全,就需要更多的检查和更慢地执行性能。针对这些选项,只有在你可以为了性能牺牲安全的时候才可以调优它们。 在此,当你调优分配的时候,把性能作为整体来考虑尤为重要,因为每个选项都会影响整个服务器。 MySQL服务器提供了大量的选项,我们可以通过多种方式来对这些选项进行设置。 1.在my.cnf配置文件中进行设置 2.在使用命令行启动服务器的时候设置 3.在服务器正在运行的时候使用变量来设置 其中这些变量有些是GLOBAL类型的,有些是SESSION级别的,本文将阐述一些可以创建或能够产生变化的选项,能够帮助你解决MySQL发生的一些问题。 我们可以根据变量的用途来把它们分成多个不同的组:用来设置服务器的配置目录。限制对硬件资源的使用,改变mysqld应该如何应对一个或多个场景等。依照它们分配时间的不同,它们也可以分为不同的组,例如:当服务器启动的时候,一个线程连接创建的时候, 或者当服务器启动一个特定操作的时候。 一、配置选项介绍 服务器选项 当我们使用mysql客户端可执行文件连接mysql服务器时,需要指定IP地址、用户名及密码等信息,这些信息就是mysql客户端程序启动时的选项,通过这些选项可以连接到具体的mysql服务端上。 对于mysql服务端,在启动时可以指定同时连入的客户端数量、客户端/服务端的通信方式、表的默认存储引擎、查询缓存的大小等信息, 例如:向服务器指定目录或文件,提醒服务器是否打开一个特定的日志等诸如此类的功能。 有俩个典型的故障排除情景是由这种选项导致的: 当某选项使用错误路径的时候,你通常能够在服务器启动的时候注意到此类问题。例如,如果你对 datadir 选项指定一条错误路径,那么 mysqld 会拒绝启动并输出有关的错误消息 [root@dba-test ~]# /export/servers/mysql/bin/mysqld --defaults-file=/export/restore/tool/general_mysql_backup_extract_tool/mysql-5.cnf --user=mysql --datadir=/export/restore/ 2023-03-22T05:50:50.004095Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details). 2023-03-22T05:50:50.004203Z 0 [Note] --secure-file-priv is set to NULL. Operations related to importing and exporting data are disabled 2023-03-22T05:50:50.004237Z 0 [Note] /export/servers/mysql/bin/mysqld (mysqld 5.7.24) starting as process 4232 ... 2023-03-22T05:50:50.012400Z 0 [Note] InnoDB: PUNCH HOLE support available 2023-03-22T05:50:50.012440Z 0 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins 2023-03-22T05:50:50.012450Z 0 [Note] InnoDB: Uses event mutexes 2023-03-22T05:50:50.012458Z 0 [Note] InnoDB: GCC builtin __sync_synchronize() is used for memory barrier 2023-03-22T05:50:50.012468Z 0 [Note] InnoDB: Compressed tables use zlib 1.2.11 2023-03-22T05:50:50.012475Z 0 [Note] InnoDB: Using Linux native AIO 2023-03-22T05:50:50.013078Z 0 [Note] InnoDB: Number of pools: 1 2023-03-22T05:50:50.013294Z 0 [Note] InnoDB: Using CPU crc32 instructions 2023-03-22T05:50:50.017366Z 0 [Note] InnoDB: Initializing buffer pool, total size = 128M, instances = 1, chunk size = 128M 2023-03-22T05:50:50.027751Z 0 [Note] InnoDB: Completed initialization of buffer pool 2023-03-22T05:50:50.030019Z 0 [Note] InnoDB: If the mysqld execution user is authorized, page cleaner thread priority can be changed. See the man page of setpriority(). 2023-03-22T05:50:50.040071Z 0 [Note] InnoDB: The first innodb_system data file 'ibdata1' did not exist. A new tablespace will be created! 2023-03-22T05:50:50.040081Z 0 [Note] InnoDB: Need to create a new innodb_system data file 'ibdata2'. 2023-03-22T05:50:50.040290Z 0 [ERROR] InnoDB: Operating system error number 13 in a file operation. 2023-03-22T05:50:50.040295Z 0 [ERROR] InnoDB: The error means mysqld does not have the access rights to the directory. 2023-03-22T05:50:50.040298Z 0 [ERROR] InnoDB: Operating system error number 13 in a file operation. 2023-03-22T05:50:50.040301Z 0 [ERROR] InnoDB: The error means mysqld does not have the access rights to the directory. 2023-03-22T05:50:50.040305Z 0 [ERROR] InnoDB: Cannot open datafile './ibdata1' 2023-03-22T05:50:50.040311Z 0 [ERROR] InnoDB: Could not open or create the system tablespace. If you tried to add new data files to the system tablespace, and it failed here, you should now edit innodb_data_file_path in my.cnf back to what it was, and remove the new ibdata files InnoDB created in this failed attempt. InnoDB only wrote those files full of zeros, but did not yet use them in any way. But be careful: do not remove old data files which contain your precious data! 2023-03-22T05:50:50.040316Z 0 [ERROR] InnoDB: InnoDB Database creation was aborted with error Cannot open a file. You may need to delete the ibdata1 file before trying to start up again. 2023-03-22T05:50:50.641211Z 0 [ERROR] Plugin 'InnoDB' init function returned error. 2023-03-22T05:50:50.641243Z 0 [ERROR] Plugin 'InnoDB' registration as a STORAGE ENGINE failed. 2023-03-22T05:50:50.641256Z 0 [ERROR] Failed to initialize builtin plugins. 2023-03-22T05:50:50.641264Z 0 [ERROR] Aborting 2023-03-22T05:50:50.641291Z 0 [Note] Binlog end 2023-03-22T05:50:50.641384Z 0 [Note] Shutting down plugin 'CSV' 2023-03-22T05:50:50.641402Z 0 [Note] Shutting down plugin 'MyISAM' 2023-03-22T05:50:50.642273Z 0 [Note] /export/servers/mysql/bin/mysqld: Shutdown complete 但是,当然,如果你在系统启动文件并用守护进程的方式启动 mysqld ,那么你无法在命令行中看到这些错误信息。 [root@exps-test2 ~]# /export/servers/mysql/bin/mysqld_safe --defaults-file=/export/restore/tool/general_mysql_backup_extract_tool/mysql-5.cnf --user=mysql --datadir=/export/restore/ Logging to '/export/restore/exps-test2.err'. 2023-03-22T05:53:47.138456Z mysqld_safe Starting mysqld daemon with databases from /export/restore 2023-03-22T05:53:47.347547Z mysqld_safe mysqld from pid file /export/restore/exps-test2.pid ended 在这种情况下,需要检查错误日志文件中的信息,或者,如果没有任何错误日志文件,那么需要检查操作系统日志中有关mysqld相关的消息。 打开或关闭一个特定功能的时候。例如,关闭特性的选项。 当InnoDB引擎启动失败的时候,虽然服务器成功启动,但是InnoDB引擎并没有成功加载。 如果SQL模式不包含 NO_ENGINE_SUBSTITUTION,我们仍然能够成功创建引擎为InnoDB的表: mysql> CREATE TABLE t1(f1 int) ENGINE=InnoDB; Query OK, 0 rows affected, 2 warnings (0.03 sec) 我们需要检查上面的警告信息,在这个例子中,我们在创建表的时候使用了错误的存储引擎,因此我们在尝试启动 InnoDB 引擎的时候发生了错误: mysql> SHOW WARNINGS; +---------+------+--------------------------------------------+ | Level | Code | Message | +---------+------+--------------------------------------------+ | Warning | 1286 | UnKnown table engine 'InnoDB' | +---------+------+--------------------------------------------+ | Warning | 1266 | Using storage engine MyISAM for table 't1' | +---------+------+--------------------------------------------+ 所以如果某个依赖的功能出现问题,该功能是否存在于服务器实例上。而错误消息清楚的说明了问题出在哪里。 可更改服务器运行方式的变量 这种类型的变量会影响 MySQL 服务器如何处理用户的输入,举一个简单的例子来清楚地说明设这种这种类型的变量而带来的效果。在这个例子中,将SQL模式设置为STRICT_TRANS_TABLES使无效数据插入事务性表的尝试被拒绝,而不是被忽略。 有关硬件资源限制的选项 本分类中的配置选项能够对硬件资源的利用施加限制。它们通常有两个用途:优化性能及限制某此操作。当你希望对客户端与服务器之间的流量施加一些限制,或者防止拒绝服务攻击时,刚才提到的两种用途中,后者对该场景非常有用。它能更好地让特定用户得到更优雅的错误,因为资源不足要好过 mysqld 进程终止,因为 mysqld 进程终止后便不能处理所有传入的请求。 使用--no-defaults 选项 检查 MySQL 服务器是否没有指定选项是较容易的,即,如果它的选项全部使用默认值如果你对于mysqld如何在未配置自定义选项的场景下运行有一个粗略的猜测,那么你就可以使用--no-defaults 选项启动 MySQL 服务器并与你设置了自定义选项的情况进行比较 性能选项 这一类选项通常不会引发错误,但它们有可能对服务器的性能产生巨大的影响。通常可以在不同的真实负载的生产服务器上调整这些选项,直到找到一个适合特定环境的性能选项配置组合。 然而,当你选择了这些选项的时候,有一种可能出现的情况会导致错误,所以从配置文件中移除或降低性能配置选项的权重是有意义的。这个场景是:你的服务器遇到一个资源不足错误。最常见的情况涉及缺少内存或者文件描述符。如果服务器存在此类问题,可以使用--no-defaults 办法来找明设置了太大的选项。 二、整体优化思路 欲速则不达 调整 MySQL 服务器的时候,至少是当你无法 100%确定你知道自己在做什么的时候。 这意味着,如果你认为一组配置选项可以改变 MySQL 服务器的行为,并使它更好地为你的应用程序提服务,那么你可以改变一个选项,然后进行测试,如果结果是有效的,那么你可以继续添加其他选项,以此类推,直到你已经检查完毕每一个相关选项。这可能是一个很缓慢的过程,但如果在这一过程中发生错误,你可以放心的回滚到本次变化之前,并能够迅速地让你的服务器回到工作状态。 在你调整内存缓冲区或者其他选项以限制对硬件资源的利用时,这种方式是非常重要的。但这种方式也同样能够用于服务器行为配置选项的修改。即使你对变量到底在做什么有着良好的认知,这也仅仅能让你可以更容易地发现和修复某个错误,而不是在几十种选项择中找出错误的根源。 当你在使用该方法的时候,需要保存每一个测试结果。例如,如果你正在尝试提升服务器的性能,那么你需要运行基准测试或测试查询执行时间在配置选项改变前后的变化,然后重复同样的测试,再去修改每个选项。 通过基准测试迭代优化 你也许期望 (或者相信自己会期望) 通过建立一套基准测试方案,然后不断迭代地验证对配置项的修改来找到最佳配置方案。通常我们都不建议大家这么做。这需要做非常名的工作和研究,并且大部分情况下潜在的收益是非常小的,这可能导致巨大的时间浪费而把时间花在检查备份、监控执行计划的变动之类的事情上,可能会更有意义。 即使更改一个选项后基准测试出现了提升,也无法知道长期运行后这个变更会有什么副作用。基准测试也不能衡量一切,或者没有运行足够长的时间来检测系统的长期稳定性修改就可能导致如周期性性能抖动或者周期性的慢查询等问题。这是很难察觉到的。 有的时候我们运行某些组合的基准测试,来仔细验证或压测服务器的某些特定部分,使得我们可以更好地理解这些行为。一个很好的例子是,我们使用了很多年的一些基准测试,用来理解InnoDB 的刷新行为,来寻找更好的刷新算法,以适应多种工作负载和多种硬件类型。我们经常测试各种各样的设置,来理解它们的影响以及怎么优化它们。但这不是一件简单的事一一这可能会花费很多天甚至很多个星期一一而且对大部分人来说这没有收益,因为服务器特定部分的认识局限往往会掩盖了其他问题。例如,有时我们发现,特定的设置项组合,在特定的边缘场景可能有更好的性能,但是在实际生产环境这些配置项并不真的合适,例如,浪费大量的内存,或者优化了吞吐量却忽略了崩溃恢复的影响。 如果必须这样做,我们建议在开始配置服务器之前,开发一个定制的基准测试包。你必须做这些事情来包含所有可能的工作负载,其至包含一些边缘的场景,例如很庞大很复杂的查询语句。在实际的数据上重放工作负载通常是一个好办法。如果已经定位到了一个特定的问题点一一例如一个查询语句运行很慢一一也可以尝试专门优化这个点,但是可能不知道这会对其他查询有什么负面影响。 最好的办法是一次改变一个或两个变量,每次一点点,每次更改后运行基准测试,确保运行足够长的时间来确认性能是否稳定。有时结果可能会令你感到惊讶,可能把一个变量调大了一点,观察到性能提升,然后再调大一点,却发现性能大幅下降。如果变更后性能有隐患,可能是某些资源用得太多了,例如,为缓冲区分配太多内存、频繁地申请和释放内存。另外,可能导致 MySQL 和操作系统或硬件之间的不匹配。例如,我们发现 sort_buffer_size的最佳值可能会被 CPU 缓存的工作方式影响,还有 read_buffer_size需要服务器的预读和 I/O 子系统的配置相匹配。更大并不总是更好,还可能更糟糕。一些变量也依赖于一些其他的东西,这需要通过经验和对系统架构的理解来学习。 三、优化配置 影响服务器与客户端行为的选项 服务器相关选项 这些选项影响所有的连接与语句。 •限制与 max_ 变量* 如果你发现mysqld限制了你发送语句或返回结果的大小,你只须检查它们的值。 •权限 如果语句失败,你的用户是否有执行它的权限,或特定数据库或者表的权限。 •SQL模式 当遇到“奇怪”的查询结果时,请检查 SQL 模式并分析它是否影响查询。 •字符集与排序规则 每当你怀疑某些关于字符集与排序规则的事情有错误时,请运行以下两个查询,然后分析查询结果与上下文。 mysql> SHOW VARIABLES LIKE '%char%'; mysql> SHOW VARIABLES LIKE '%coll%'; 通常的安全规则是获取所有的 character_set_* 变量、 collation_* 变量,并且创建选项相同的任何表与一起协同的连接。设置客户端选项最简单方式使用 SET NAMES 语句。 当在排序或者比较过程中遇到问题时,请检查字符集选项与表的定义。 •操作系统处理 lower_case* 参数 lower_case_filesystem 与 lower_case_table_names 选项跟字符集选项的作用非常相似。这些变量确定操作系统如何处理数据库对象的大小写情况。 最好不要修改它们的值,特别是操作系统不区分大小写时。 •初始 SQL 这些选项确定服务器在不同的时间是否应该自动执行某些 SQL 语句 ◦ init_file 如果指定,此变量将命名一个文件,其中包含在启动过程中要读取和执行的 SQL 语句。每条语句必须在一行中,并且不应包含注释。 ◦ init_connect 服务器为每个连接的客户端执行的字符串。该字符串由一个或多个 SQL 语句组成,以分号字符分隔。 ◦init_slave 该变量类似于init_connect,但是是一个字符串,每次复制 SQL 线程启动时由副本服务器执行。字符串的格式与变量的格式相同init_connect。该变量的设置对后续START SLAVE语句生效。 ◦ open_files_limit 这个重要选项限制 MySQL 服务器同时打开文件句柄的数量。限制数量越高,打开的表文件与临时表越多,因此处理的并发连接量数越多。如果这个限制在你的环境中设置得太低,在你试图连接、打开一个表或者执行一个需要创建临时表的查询时就会出现错误。 ◦ log_warnings 是否向错误日志生成额外的警告消息。 当此选项打开(非零)时,就会在服务器的错误日志文件中写人警告信息。它们不是在 SOL 执行期间发出的警告,而是显示服务器内到底是怎么回事的调试消息。如果设置为 2,此选项告诉服务器记录连接错误。当你正在对客户端无法连接或正在失去连接的情况做故障排除时,这非常重要。日志并非总是能找到问题所在,但其警告消息往往对要做什么能给出一些启发。当使用同步复制时,在主服务器上开启此选项非常重要,因为你能确定从服务器 IO 线程何时失去连接。反过来它是网络故障的一种症状,这在将来可能导致更严重的问题。 当在从服务器上设置为 1(默认值) 时,它将输出自己的诊断消息,例如:在二进制日志和中继日志中的位置及其复制状态。从 5.1.38 版本开始,在基于语句模式的同步复制中,需要启用此选项,以便在从服务器输出不安全语句的信息。( 5.1.38版本之前,从服务器在任何情况下都会输出此类消息。) 从 5.1.38 版本开始,可以关闭此选项( 设置为 0),来丢弃你确定不需要该消息的日志。 ◦ log_error_verbosity 服务器将错误、警告和注释消息写入错误日志的详细程度。下表显示了允许的值。默认值为 3。 log_error_verbosity value 允许的消息 1 错误讯息 2 错误和警告消息 3 错误、警告和信息消息 log_error_verbosity在 MySQL 5.7.2 中添加。它优于旧的系统变量,应该使用它来代替旧的log_warnings系统变量。log_warnings有关该变量如何与 相关的信息,请参阅 的描述log_error_verbosity。特别是,赋值给log_warnings赋值给log_error_verbosity,反之亦然。 复制选项 这些选项确定了主从服务器之间的关系。 •binlog-* 与 replicate-* 过滤器 通过 binlog-do-、 replicate-do-、 binllog-ignore-* 与 replicate-ignore-* 选项,在复制过程中, MySQL有能力过滤对象。 binlog-* 选项减少在主服务器上写入二进制日志文件的事件,而 replicate-* 指定在从服务器上记录到二进制日志。从服务器还有replicate-wild-do-* 与 replicate- wild-ignore* 选项,二者允许通过模式匹配,指定哪些应该或哪些不应该同步。 •二进制日志格式a. binlog_format 变量允许你选择复制的格式:STATEMENT、ROW、或MIXED。这是一个动态变量,它能在 SESSION 级别调整。 ◦binlog_direct_non_transactional_updates 此选项指定何时非事务表更新应该写入二进制日志。 默认情况下,当使用事务时,MySQL将非事务表的更新写入事务缓存里,仅当事务提交后,才把缓存刷新到二进制日志里。这样做以便从服务器更有可能与主服务器数据最终一致,即使依赖事务表中的数据来更新非事务表,并且主服务器在许多并发线程中同步更新相同的表。 这是一个动态变量,可以在 SESSION 级别改变它,所以,可以在特定语句下使用它。它的工作原理决定了它只能在基于语句模式的复制中オ生效。 •log_bin_trust_function_creators 这个选项告诉 mysqld 当用户没有 SUPER 权限却试图创建一个不确定的函数时,不要触发警告。 •binloig_cache_size 与类似选项 此条目包括以下选项: ◦binlog_cache_size ◦binlog_stmt_cache_size ◦max_binlog_cache_size ◦max_binlog_stmt_cache_size 在写入二进制日志之前,这些缓存保存在事务期间提交的事务与非事务语句。如果一个事务需要超过这个字节数的内存,服务器会生成一个 “Multi-statement transaction required more than 'max_binlog_cache_size' bytes of storage” 错误。 检查 Binlog_cache_use、Binlog_stmt_cache_use、Binlog_cache_disk_use 与 Binlog_stmt_cache_disk_use状态变量来找出 binlog缓存使用的频率以及事务大小超过Binlog_cache_use与 Binlog_stmt_cache_use的频率。当事务大小操作缓存大小时,将会创建临时表来存储事务缓存。 •slave_skip_errors 此选项允许从服务器SOL线程即使遇到某类错误时还能运行。例如,主服务器运行在宽松的 SQL 模式而从服务器却有一个严格的SQL模式,当插入字符串到整数字段时,由于数据格式不一致,而报告1366( ERROR1366(HY000): Incorrect integer value)错误,可以设置 slave skip errors ,以便从服务器不会出现故障。 这个选项可能导致主服务器从服务器数据不一致而又很难诊断,所以,如果你遇到这样的问题,请检查该选项是否没有设置。 •read_only 该选项使从服务器服务器只读。这意味着,仅仅只有从库SQL线程才能更新其数据,而其他连接只能读数据。该选项对于保持从服务器数据的一致很重要。然而,这个选项并不能限制具有 SUPER 权限的用户更改表。另外,所有用户仍允许创建临时表。 •super_read_only 该选项使服务器甚至禁止拥有 SUPER 。 引擎选项 本节主要介绍 InnoDB 相关选项。 •innodb_autoinc_lock_mode 该选项用于生成自动增量值的锁定模式。 •innodb_file_per_table Innodb默认在共享表空间中存放表和索引数据。使用此选项,你可以告知它将表的索引和数据存放在单独的文件里。共享表空间仍然用来存放表定义。此选项在设置后创建的表上生效;之前创建的表依然使用共享表空间。 •innodb_table_locks 此变量定义了 InnoDB 是如何处理 LOCK TABLES 语句发出的表锁请求。默认(当设置了这刻返回并且内部将表锁住。当关闭时(设置为0),它会接收 LOCK TABLE 语句,线程直到所有锁释放后才从 LOCK TABLES ...WRITE返回。 •innodb_lock_wait_timeout 这是 InnoDB 等待行锁直到放弃的秒数。在 innodb_lock_wait_timeout秒后,它会返回错误 “ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction” 至客户端。我经常看到人们将这个变量设得很大来防止查询失败,这只会导致更严重的问题,因为许多阻塞的事务会互相锁住。尝试在应用程序层处理锁等待错误,并不要将它设置得过高。此参数的最佳值取决于应用程序,通常应该大约为正常事务所消耗的时间。它的默认值是50秒,对于需要立即返回结果的应用程序有些大。 •innodb_rollback_on_timeout 当査询因锁等待错误而中断时,只有最后一条语句回滚了,整个事务还没有中止。如果将选项设置为1你将会改变此行为。这种情况下事务会在锁等待超时后立刻回滚。 •innodb_use_native_aio 指定 InnoDB 是否应该使用 Linux下原生的AIO接口,或者是自己来实现,称作 “模拟AIO“。如果设置 innodb_use native_aio,lmoD将分发IO请求至内核。这提高了可扩展性因为比起模拟AIO新内核能够处理更多的并行IO请求。 此选项默认开启,在正常操作下不应该改变。 运行大量InnoDBI/O 线程,尤其是在同一台服务器上运行多个此类实例,可能会超出 Linux 系统的容量限制。在这种情况下,您可能会收到以下错误: EAGAIN: The specified maxevents exceeds the user's limit of available events. 您通常可以通过将更高的限制写入 来解决此错误/proc/sys/fs/aio-max-nr。 •innodb_locks_unsafe_for_binlog 此变量定义 InnoDB如何使用间隙锁来搜索和扫描索引。默认值(设为0)下,间隙锁开启。如果设为1,大多数操作下会禁用间隙锁。其工作原理类似于隔离级别中的 READ COMMITTED,但由于不太好调节应尽量避免。即使它允许你来处理锁问题,当并行事务插入新行至间隙时它也会带来新的问题。所以推荐用 READ COMMITTED替代它。这个变量不能设置为 SESSION 级别,它会彩响所有事各 连接相关的选项 超时 •innodb_lock_wait_timeout 这是 InnoDB 等待行锁直到放弃的秒数。在 innodb_lock_wait_timeout秒后,它会返回错误 “ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction” 至客户端。我经常看到人们将这个变量设得很大来防止查询失败,这只会导致更严重的问题,因为许多阻塞的事务会互相锁住。尝试在应用程序层处理锁等待错误,并不要将它设置得过高。此参数的最佳值取决于应用程序,通常应该大约为正常事务所消耗的时间。它的默认值是50秒,对于需要立即返回结果的应用程序有些大。 •lock_wait_timeout 它适用于元数据锁。这个锁对所有需要元数据锁的操作有效:DML(数据操纵语言语句,如 INSERT、 UPDATE和 DELETE),DDL、LOCKTABLES等。默认值是3153600秒,也就是一年。所以默认情况下,有效MDL锁永远不会解除。然而,可以更改值为大于1秒的任何值。它是一个动态变量,可以在SESSION级别更改。 •connect_timeout 这个超时使用在 MySQL服务器在响应之前等待连接数据包的秒数。从5.1版本开始,此值默认设置为10秒。 •interactive_timeout 服务器在关闭交互式连接之前等待其活动的秒数。 •wait_timeout 在断开连接前等待任何客户端中活动的时间。如果客户端是交互式的并且 interactive_timeout的值不同于wait_timeout,则以 interactive_timeout为准。 •max_connect_errors 来自主机的连续连接请求在没有成功连接的情况下被中断后max_connect_errors,服务器阻止该主机进一步连接。如果在先前的连接中断后,在少于max_connect_errors尝试次数的时间内成功建立了来自主机的连接,则主机的错误计数将被清除为零。要取消阻止被阻止的主机,请刷新主机缓存。 •max_connections 允许的最大并发客户端连接数。最大有效值是的有效值和实际设置的值中的较小者。 •skip_name_resolve 检查客户端连接时是否解析主机名。如果这个变量是OFF,mysqld 在检查客户端连接时解析主机名。如果是ON,mysqld 只使用 IP 号码;在这种情况下,Host授权表中的所有列值都必须是 IP 地址。 •net_read_timeout 从客户端写入 MySQL服务器等待应答的时间。例如,此超时会在客户端执行大的插入操作时起作用。 •net_write_timeout 客户端从服务器中读取时等待应答的时间。例如,当客户端发送一个 SELECT查询读取结果,如果客户端等待一段时间未收到数据,这个超时会断开此连接。如果客户端需要在处理结果前做一些工作,检查工作的持续时间是否长于这个超时。 与安全相关的选项 •--skip-grant-tables --skip-grant-tables导致服务器不读取mysql系统数据库中的授权表,从而在根本不使用特权系统的情况下启动。这使任何有权访问服务器的人都可以不受限制地访问所有数据库。 •--safe-user-create 不允许使用GRANT语句来创建用户,除非用户有mysql.user 表的INSERT权限才可以。 •secure_auth 如果启用此变量,则服务器会阻止尝试使用以旧(4.1 之前)格式存储密码的帐户的客户端连接。启用此变量以防止所有使用旧格式的密码(因此通过网络进行不安全的通信)。 此变量已弃用;希望在未来的 MySQL 版本中将其删除。它始终处于启用状态,尝试禁用它会产生错误。 •secure_file_priv 限制LOAD FILE函数以及LOAD DATA和SELECT… INTO OUTFILE语句只能使用指定目录。 与性能相关的选项 缓冲区和最大值 •join_buffer_size 这是为连接操作分配的最小缓存大小,这些连接使用普通索引扫描、范围扫描,或者连接不使用索引。两表之间进行全连接时分配缓存。因此,连接两个表的一条查询分配一块缓存,连接3个表的一个分配两块查询缓存,以此类推。这个选项可在 SESSION级使用,能对于特定连接设置。 为了查出是否需要增加join_buffer_size,可以检查 Select_scan状态选项,它包括第一张表执行完整扫描的连接数量,同样 Select_ full_range_ join,它包含使用范围搜索的连接数量。这些状态变量的值不会随着 join buffer size的值的变化而变化,这样你可以利用它们来查出是否需要大的 join buffer size,而不是衡量该值改变后的有效性 •net_buffer_length 服务器在客户端连接后立刻创建的缓存大小,用来保存请求和结果。根据需要这个大小可以增长至 max_allowed_packet正常情况下不用改变默认值(16384字节),但当设置 max_connections选项时要记住此值。 •query_prealloc_size 此缓存为语句解析和执行而分配。语句间缓存是不释放的。如果运行复杂查询,增加缓存是合理的,这样 mysqld不会在执行查询的时候在分配内存上耗时。增加此大小到最大查询的字节数。 •read_buffer_size 该变量是MySQL读入缓冲区大小。对表进行顺序扫描的请求将分配一个读入缓冲区,MySQL会为它分配一段内存缓冲区。read_buffer_size变量控制这一缓冲区的大小。如果对表的顺序扫描请求非常频繁,并且你认为频繁扫描进行得太慢,可以通过增加该变量值以及内存缓冲区大小提高其性能。 •read_rnd_buffer_size 此变量用来控制在排序和发送结果至客户端之间存放读取结果的大小。大的值能提高包含ORDER BY的查询的性能。 •sort_buffer_size 每个线程需要排序的时候会分配此缓存。查明你是否需要增加此缓存的大小,检查 ort merge_ passes状态变量。也可以检查查明你执行了多少个排序操作。sort_range、 sort_rows和sort_scan来这些状态变量显示了排序操作的数量。 为了找出缓存合适的大小,需要检查一条或多条查询排序的行数,并乘以行大小。或者简单地设置不同的值直到sort_merge_passes停止增长。 sort_buffer_size缓存经常会分配,所以大的 GLOBAL值会降低性能而不是增加性能。因此,最好不要设置此选项为 GLOBAL变量,而是当需要时使用 SET SESSION增加它。 •sql_buffer_result 如果启用,则sql_buffer_result强制将语句的结果SELECT放入临时表中。这有助于 MySQL 尽早释放表锁,并且在需要很长时间才能将结果发送到客户端的情况下非常有用。默认值为OFF。 为了找出查询是否在发送结果集上消耗过多时间,执行 SHOW PROCESSLIST来检查查询在“ Sending data”状态下的时间。 •thread_cache_size 为将来使用缓存起来的线程数量。当一个客户端断开连接时,通常其线程也被销毁。如果该选项设置为正值 N,那么连接断开后 N个线程将被缓存起来。在具有良好线程实现的系统上,该选项不能显著地提高性能,但是,对于一个应用使用成百上千连接的情况,还是很有用的。 •thread_stack 每个线程的栈大小。如果该变量设置过小,将会限制SQL语句的复杂性、存储过程的递归深度,以及服务器上其他内存消耗型的操作。对于大部分安装来说,默认值(32位系统是192KB;64位系统是256KB)就可以。如果类似"Thread stack overrun"的错误消息,请增大该参数。 •tmp_table_size 内存中,内部临时表的最大值。服务器默认设置为max_heap_table_size和tmp_table_size二者中的最小值。如果你有足够的内存,并且Created_tmp_disk_tables状态变量在增大,请增大该变量。把需要临时表的所有结果集放在内存中,可以大大提高性能。 •query_cache_size MySQL服务器存储查询及其结果集的缓存大小。默认情况下,查询缓存是禁用的。于5.7.20版本之后弃用。 •table_definition_cache 存储在缓存中的表定义的数量。当表数量很大时,可以增大该值。如果需要,可以调整该值,以便最近的表刷新( FLUSH TABLES)后,保持 Opened_table_definitions于或等于Open_table_definitions。 •table_open_cache 存储在缓存中的表描述符的数量。调整该值,以便 Opened_tables仍小于或等于Open_tables。 控制优化器的选项 这些变量可以在 SESSION 级别设置,所以,你可以实验它们是怎样影响特点查询的。 •optimizer_prune_level 如果该变量设为on,优化器删除即时搜索发现的不太有效的计划;如果设置为off,优化器使用详尽的搜索。默认值为1(on)。如果你怀疑优化器选择的不是最优计划,你可以改变它的值。 •optimizer_search_depth 优化器搜索的最大深度。该值越大,优化器越有可能为复杂的查询找到最优计划。提高该值的代价就是优化器在搜索计划时的时间开销增大。如果设置为0,服务器会自动选择一个合理的值,默认值为62(最大值) •optimizer_switch 该变量变量可以控制优化器的行为。 ◦index_merge 启用或禁用索引合并优化,该优化几个从合并扫描中获取行记录,并把结果合并为一条记录。在 EXPLAIN的结果输出中,Merge列显示的就是这个选项。 ◦index_merge_intersection 启用或禁用索引合并交叉访问算法 当 where从句包含key表示的范围条件并且与AND关键字时,将会使用该算法。 如:key_coll < 10 AND key_col2 = 'foo' 使 key_col2 = 'foo' 得到唯一值,优化器也把它当做范围条件 •index_merge_union 启用或禁用索引合并联合访问算法,当 where从句包含 key 表示的范围条件和OR关键字时,会用到该算法。 如:key_col1 = 'foo' OR (key_co12 = 'bar' AND key_col3 ='baz') •index_merge_sort_union 启用或禁用索引合并排序联合访问算法,当 where从句包含key表示的范围条件和OR关键字时,将使用该算法,但是,不会应用索引合并联合访问算法,如(key coll >10 OR key co12 ='bar)AND key co13 •max_join_size 对于估计可能超过一定限制的 select 语句,(该选项)阻止优化器进行优化(如查看多于 max_join_size 的行记录 )。当调试找出没有使用索引的查询时,该选项会有很大帮助。 •max_length_for_sort_data 当无法使用索引时,如果对 ORDER BY 进行条件优化,MySQL 将使用文件排序算法。该算法有两个变体。原始算法读取所有匹配的行记录,在缓存里存储战对的键值和行指针,该缓存大小受 sort_buffer_size 限制。缓存中的值排序后,该算法再次读取表记录,但是,这次按照一定顺序读取。该算法的缺点是两次读取行记录。 改进后的方法是读取整个行记录到缓冲区,然后排序键值,从缓冲区中读取行记录。该方法的问题是结果集通常超过sort_buffer_size,所以,对于大数据集,磁盘I/O 操作使得该算法很慢。max_length_for_sort_data 变量限制键值和行记录指针对的大小,因此原始算法适用于键值和行指针对中额外列的总大小超过了该限制的情况。 磁盘活动多,并且 CPU 活动少是需要调低该变量的一个信号。 •max_seeks_for_key 根据表扫描必须检查的记录行数量,为使用键值而不是表扫描设置阈值。设置该参数为一个较小的值,比如 100,在表扫描时,可以强制优化器优先查看索引。 •max_sort_length 设置对 BLOB 或 TEXT 值排序时用到的初始字节数,后面的部分将被忽略。 与引擎相关的选项 •innodb_adaptive_hash_index 自适应InnoDB哈希索引是启用还是禁用。根据您的工作负载,可能需要动态启用或禁用自适应哈希索引以提高查询性能。由于自适应哈希索引可能并非对所有工作负载都有用,因此在启用和禁用它的情况下使用实际工作负载进行基准测试。 innodb_additional_mem_pool_size InnoDB 用来存储数据字典信息和其他内部数据结构的内存池的大小。在 MySQL 5.7.4 中删除。 •innodb_autoinc_lock_mode 用于生成 自动增量值的 锁定模式 。允许的值为 0、1 或 2,分别表示传统、连续或交错。默认设置为 1(连续)。 •innodb_buffer_pool_size InnoDB 为存储数据、索引、表结构、自适应散列索引等分配的内存大小,这是影响 innodb 性能最重要的选项。可以将其设置为物理内存的 80%。理想情况下,该缓冲区足够大,以致可以包含所有活动的 InnoDB 表和额外空间。同时,也要把其他缓冲区计算在内,寻找一个好的平衡。 匹配Innodb buffer pool %的,状态变量显示 InnoDB 缓冲池的当前状态。 该变量的设置需要经过实际环境中非常严谨的验证。 •innodb_buffer_pool_instances 该选项设置 InnoDB 缓冲池应切分的实例数量。每个实例有它自己的空闲列表、刷新列表、使用LRU算法的存储对象的列表,以及其他数据结构,并且受到自身互斥体的保护。设置该变量大于 1,可以提高大型系统的并发性能。每个实例的(缓中区)大小是 innodb_buffer_pool_size/innodb_buffer_pool_instances,并且至少是1GB。如果 innodb_buffer_pool_size小于 1GB,该选项不生效。 innodb_buffer_pool_instances 切分缓冲区互斥体,所以,如果有 8个或者更多并行SESSION 同时访问 InnoDB 缓冲池,该选项可以设置为 4~16。该选项取决于innodb_buffer_pool_size值和可用的内存。 •innodb_checksums 默认情况,InnoDB 使用校验和验证磁盘上所有页。该选项可以立即确定数据文件是否由于磁盘坏道或者其他原因而损坏。通常需要开启该功能,但是,在很少的情况下,当不关心数据时( 比如,只读从服务器,不提供(线上) 服务,只做备份 ),关闭该功能,可以提升性能。innodb_checksums已弃用,替换为innodb_checksum_algorith。 •innodb_checksum_algorithm 指定如何生成和验证存储在InnoDB表空间的磁盘块中的校验和。 •innodb_commit_concurrency 可以同时提交事务的线程数量,默认值为 0( 没有限制 )。 •innodb_thread_concurrency InnoDB 内部同时运行的线程数量,不要把它和 MySQL 服务器创建的连接线程的数量混淆。默认值是 0:不限制并行或不检查并行。 尽管大量线程并行运行一般意味着高性能,但是,如果同时并行运行很多用户会话,你可能遇到互斥体争用,如果同时运行的用户线程不超过 16 个,通常不用担心该参数。如果有更多用户 SESSION,可以通过查询 Performance Schema 或者SHOW ENGINE INNODBMUTEX 来监控互斥锁。 如果出现互斥体争用,可以尝试限制该变量为 16 或 32,或者把 mysqld 进程放置到 Linux 的任务集里或者 Solaris 的处理器集里,同时限制它为更少的内核而不是所有内核。 •innodb_concurrency_tickets 当允许一个线程进人InnoDB 时,它接收 innodb_concurrency_tickets张并发“票”( ticket),这些票允许线程离开和重新进人 InoDB,直到它使用完这此票。 默认值是 500。用完这些票后,把线程放置到等待队列中,以获取一组新票。 •innodb_doublewrite 默认情况下,InnoDB 分两次存储数据: 第一次写人双写缓冲,第二次写人数据文件。像innodb_checksums一样,对于数据安全不是最重要的场景,这个安全选项可以关闭 of,以提升性能。 Innodb dblwr_writes 和 Innodb_dblwr pages written 状态变量分别显示两次写操作的数量和写页的数量。 •innodb_flush_log_at_trx_commit 定义何时把更改写人(重做日志文件以及刷新到磁盘。如果设置为 1(默认值)在每个事务提交时更改均会写人和刷新到磁盘。为了得到更好的性能,可以设置该值为 0( 每秒写人日志文件和刷新到磁盘,而每个事务提交时,不做操作)或者2( 每次提交事务时写人日志文件,但是,每秒刷新到磁盘 。注意,只有该选项为1时才符合 ACID 事务要求的。 Innodb_os_log_fsyncs 状态变量存储 fsync() 到日志文件的操作次数。Innodb_os_log_pending_writes 包含挂起的 fsync0写次数。Innodb_log_writes 和 Innodb_os_log_pending_writes分别包含写入次数和挂起的写次数。 •innodb_flush_method 默认情况下,fdatasync()是用来刷新数据文件的,fsync()是用来刷新日志文件到磁盘的,该值可以更改为以下值中的一个: O_DSYNC 操作系统使用 O_SYNC 打开和刷新日志文件,同时使用 fsync()刷新数据文件 O_DIRECT 操作系统使用 O_DIRECT打开数据文件,并且使用 fsync()刷新数据文件。更改innodb_flush_method变量的值,或者提升性能或者降低性能,所以,在(生产)环境中,需要谨慎测试它。 •innodb_io_capacity 后台 InnoDB 任务执行的 I/O 活动的上限。对于大多数现代系统来说,默认值 200是个不错的选择。但是,可以根据系统的 I/O 吞吐量,调整该值。在快速存储器上增大该值,才有意义。 •innodb_log_buffer_size InnoDB 用来把日志文件写人磁盘的缓冲区大小。当缓冲区满时,必须等待日志刷新到磁盘上后,才能继续进行操作。增大该参数,可以减少磁盘 I/O 操作,但是,只有在存在大量事务时,这才有意义。 状态变量Innodb_log_waits 包含缓冲区因太小而需要的 I/O 等待次数 •innodb_log_file_size 每个日志文件的大小。大日志文件降低检查点的活动,节省磁盘I/O。但是,大日忐文件显著地延缓崩溃恢复过程。从 1MB 到不超过 innodb_buffer_pool_size/ innodb_log_files_in_group 的值是有合理的。所有日志文件加起来不能超过 4GB。 最佳实践是在不同的磁盘上存储日志文件、数据文件、如果使用的话,还有二进制日志文件,这样,即使一个设备故障,也不会同时丢失所有文件。 •innodb_log_files_in_group 日志组中的日志文件数。以循环方式写入文件。默认(推荐)值为 2。文件的位置由innodb_log_group_home_dir指定。日志文件的组合大小(innodb_log_file_size*innodb_log_files_in_group)最高可达512GB。 •innodb_dedicated_server 该变量启用时,InnoDB自动配置以下变量: ◦innodb_buffer_pool_size ◦innodb_redo_log_capacity或者,在 MySQL 8.0.30 之前,innodb_log_file_size和innodb_log_files_in_group。 •innodb_old_blocks_time 非零值可防止缓冲池被仅在短时间内引用的数据填充,例如在完整表扫描期间。增加此值可以提供更多保护,防止全表扫描干扰缓冲池中缓存的数据。默认值为1000。 指定插入到旧子列表中的块在第一次访问后必须停留在那里的时间(以毫秒为单位),然后才能移动到新子列表。如果值为0,则插入到旧子列表中的块在第一次访问时会立即移动到新子列表,无论插入后多久进行访问。如果该值大于0,则块将保留在旧的子列表中,直到在第一次访问后至少几毫秒发生访问为止。例如,值1000会导致块在第一次访问后在旧的子列表中停留1秒,然后才有资格移动到新的子列表。 •innodb_open_files 仅当使用innodb_file_per_table 时,该变量才有意义。innodb_open_files是InnoDB可以同时打开的.ibd 文件的数量。默认值是 300,该值增大到 InnoDB 所有的表的数量是有意义的。 •innodb_read_io_threads InnoDB 读操作可以使用的 I/O 线程数。这些操作处理预读: I/O 请求以异步方式将一组页数据装进InnoDB 的缓冲池,然后清空和插人缓冲操作。默认值是 4。 •innodb_write_io_threads InnoDB 从缓冲池中写脏数据的 I/O 线程数量。默认值是 4 •innodb_stats_method 服务器在收集索引值的统计信息时,处理 null 的方式。这会影响索引的基数,优化器因此生成的查询计划。 •innodb_stats_on_metadata 启用该变量(默认值),每次执行元信息语句时,比如,SHOW TABLE STATUS或者 SHOW INDEX,或者当查询 InnoDB 表的INFORMATION SCHEMA 或统计数据时,InnoDB 都会更新统计信息。如果启用该变量,这些查询将和每次查询后执行 ANALYZE TABLE 有一样的效果。如果服务器频繁调用这些语句或者查询具有大量表的数据库时,可以禁用该变量。但是,当禁用该变量时,表统计数据将过时。 •innodb_stats_sample_pages MySQL 优化器用来计算索引分布统计信息的抽样索引页的数量,例如调用ANALYZE TABLE。如果你怀疑基数计算不合理,请增大该值( 默认值是 8)。但是,如果启用innodb_stats_on_metadata,增大该值,打开表的时间会增加。 •innodb_strict_mode 该变量启用时,InnoDB在检查无效或不兼容的表选项时返回错误而不是警告。 •innodb_max_dirty_pages_pct InnoDB尝试从缓冲池中刷新数据,以使脏页的百分比不超过此值。该变量设置建立了冲洗活动的目标,它不影响冲洗速度。 计算选项的安全值 通过增大缓冲区或最大值,试图优化服务器性能时,全面考虑内存使用情况是很重要的,大量缓冲区能因“内存不足”错误而导致服务器崩溃。这一节将提供一些公式帮助计算是否超出了可用内存。这部分不介绍选项本身,可以参考前面的内容或者 MySQL 手册,获取详细介绍。 服务器级选项 这些选项是 GLOBAL 的,影响所有连接和查询,一部分是服务器启动时分配的,而另部分是后来分配的,比如查询缓存,初始值是 0,(后续)不断增长,直至最大值。MySQL 服务器达到所有的限制,分配所有允许的内存,会花费很长时间。因此,需要计算 mysqld 获得的 RAM 大小,和所有的缓冲区大小保证不要超过它。 •query_cache_size •innodb_buffer_pool_size •innodb_log_buffer_size •key_buffer_size 使用下面的公式计算需要为这些缓冲区分配多少 RAM(单位是 MB)。 SELECT (@@query_cache_size +@@innodb_buffer_pool_size + @@innodb_log_buffer_size + @@key_buffer_size)/(1024*1024); 服务器还有限制文件描述符数量和缓存线程数量的选项,该计算可以忽略它们,因为分配给它们的内存仅仅是系统的一个指针大小乘以分配项的数量,这个数量足够小,以至于在现代系统上可以忽略。在这里列举一下,仅供参考: •thread_cache_size •table_definition_cache •table_open_cache •innodb_open_files 线程级选项 这些选项是基于每个线程分配(内存)的,这样,服务器分配 max_connections * sum( thread options )。设置 max_connections 和这些选项,保证物理内存总量 -max_connections* sum(thread option) 服务器级选项大于0。留一些内存给第三类选项和后台操作,这些操作不受这些变量控制。 线程级选项列表如下: •net_buffer_length •thread_stack •query_prealloc_size •binlog_cache_size •binlog_stmt_cache_size 使用下面的公式计算需要为它们分配多少内存(单位是 MB): SELECT @@max_connections * (@@global.net_buffer_length + @@thread_stack + @@global.query_prealloc_size + @@binlog_cache_size + @@binlog_stmt_cache_size) / (1024 * 1024) 为特定操作分配的缓冲区 当服务器执行特殊操作时,根据需要分配的缓冲区。很难计算出分配的内存大小。分析查询,找出哪些需要很多资源,然后像下面这样计算: 缓冲区大小 * 为特定查询分配的缓冲区数量 * 并行执行的查询数量 对于所有变量,这样计算,求出总和。 只要对于大多数查询是足够的,保持这些选项(的值)小一点。如果一个特定查询需要更多内存,只对于这个会话增加这个变量的值。 以下选项针对每个线程分配一次(缓冲区): •read_rnd_buffer_size •sort_buffer_size •myisam_mmap_size •myisam_sort_buffer_size •bulk_insert_buffer_size •preload_buffer_size 以下选项针对每个线程分配多次(缓冲区): •join_buffer_size •read_buffer_size •tmp_table_size 可以使用以下公式计算 MySQL 为每个选项分配的内存的最大数量( 单位是 GB) # set @join_tables = YOUR_ESTIMATE_PER_THREAD; # set @scan_tables = YOUR_ESTIMATE_PER_THREAD; # set @tmp_tables = YOUR_ESTIMATE_PER_THREAD; SELECT @@max_connections * (@@global.read_rnd_buffer_size +@@global.sort_buffer_size + @@myisam_mmap_size + @@global.myisam_sort_buffer_size + @@global.bulk_insert_buffer_size + @@global.preload_buffer_size + @@global.join_buffer_size * IFNULL(@join_tables,1) + @@global.read_buffer_size * IFNULL(@scan_tables,1) +@@global.tmp_table_size * IFNULL(@tmp_tables, 1)) / (1024 * 1024 * 1024); 这里给出一个综合的公式,计算 MySQL 安装时最多可以使用的内存(单位是 GB ) # set @join_tables = YOUR_ESTIMATE_PER_THREAD; # set @scan_tables = YOUR_ESTIMATE_PER_THREAD; # set @tmp_tables = YOUR_ESTIMATE_PER_THREAD; SELECT (@@query_cache_size + @@innodb_buffer_pool_size + @@innodb_log_buffer_size + @@key_buffer_size + @@max_connections * (@@global.net_buffer_length + @@thread_stack + @@global.query_prealloc_size + @@global.read_rnd_buffer_size + @@global.sort_buffer_size + @@myisam_mmap_size + @@global.myisam_sort_buffer_size + @@global.bulk_insert_buffer_size + @@global.preload_buffer_size + @@binlog_cache_size + @@binlog_stmt_cache_size + @@global.join_buffer_size * IFNULL(@join_tables,1) + @@global.read_buffer_size * IFNULL(@scan_tables, 1) +@@global.tmp_table_size * IFNULL(@tmp_tables, 1))) / (1024 * 1024 * 1024) 参考资料 《高性能MySQL》 作者:京东物流 张泽龙 来源:京东云开发者社区 自猿其说Tech

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

从2PC和容错共识算法讨论zookeeper中的Create请求 | 京东云技术团队

最近在读《数据密集型应用系统设计》,其中谈到了zookeeper对容错共识算法的应用。这让我想到之前参考的zookeeper学习资料中,误将容错共识算法写成了2PC(两阶段提交协议),所以准备以此文对共识算法和2PC做梳理和区分,也希望它能帮助像我一样对这两者有误解的同学。 1. 2PC(两阶段提交协议) 两阶段提交 (two-phase commit) 协议是一种用于实现跨多个节点的原子事务(分布式事务)提交的算法。它能确保所有节点提交或所有节点中止,并在某些数据库内部使用,也以XA事务的形式在分布式服务中使用。 在 Java EE 中,XA事务使用 JTA(Java Transaction API) 实现。 2PC的实现 2PC包含准备阶段和提交阶段两个阶段,需要借助协调者(事务管理器,如阿里巴巴的Seata) 来实现,参与分布式事务的数据库节点为参与者。当分布式服务中的节点准备提交时,协调者开始准备阶段:发送一个准备请求到每个节点,询问它们是否能够提交,然后协调者会跟踪参与者的响应 如果所有参与者都回答"是",表示它们已经准备好提交,那么协调者在提交阶段发出提交请求,分布式事务提交 如果任意一个参与者回答"否",则协调者在提交阶段中向所有节点发送中止请求,分布式事务回滚 上图是2PC提交成功的情况,我们来详述下过程: 当服务启动一个分布式事务时,它会向协调者请求一个事务ID,此事务ID是全局唯一的 在每个参与者上启动单节点事务,每个单节点事务会持有这个全局事务 ID。所有的读写都是在这些单节点事务中各自完成的。如果在这个阶段出现任何问题(节点崩溃或请求超时),则协调者或任何参与者都可以中止 当应用准备提交时,对应准备阶段:协调者向所有参与者发送一个准备请求,同样也持有全局事务 ID 。如果任意一个请求失败或超时,则协调者向所有参与者发送针对该事务 ID 的中止请求,即2PC提交中止的情况 参与者收到准备请求时,需要确保在任意情况下都可以提交事务。这包括将所有事务数据写入磁盘(出现故障,电源故障,或硬盘空间不足都不能是稍后拒绝提交的理由)以及检查是否存在任何冲突或违反约束。通过向协调者回答 “是”,节点承诺这个事务一定可以不出差错地提交。也就是说:参与者没有实际提交,同时放弃了中止事务的权利 当协调者收到所有准备请求的答复时,会就提交或中止事务作出明确的决定(只有在所有参与者投赞成票的情况下才会提交),这里对应提交阶段。协调者必须把这个提交或中止事务的决定写到磁盘上的事务日志中,记录为提交点(commit point) 。如果它随后崩溃,能通过提交点进行恢复 一旦协调者的决定已经保存在事务日志中,提交或中止请求会发送给所有参与者。如果这个请求失败或超时,协调者必须永远保持重试,直到成功为止,对于已经做出的决定,协调者不管需要多少次重试它都必须被执行 2PC协议中有两个关键的不归路需要注意: 一旦协调者做出决定,这一决定是不可撤销的 参与者投票 “是” 时,它承诺它稍后肯定能够提交(尽管协调者可能仍然选择放弃),即使参与者在此期间崩溃,事务也需要在其恢复后提交,而且由于参与者投了赞成,它不能在恢复后拒绝提交 这些承诺保证了2PC的原子性。 协调者失效的情况 如果协调者失效并且所有参与者都收到了准备请求并投了是,那么参与者什么都做不了只能等待,而且这种情况解决方案是等待协调者恢复或数据库管理员介入操作来提交或回滚事务,当然如果在生产期间这需要承担运维压力。 所以,协调者在向参与者发送提交或中止请求之前,将其提交或中止决定写入磁盘上的事务日志(提交点)。这样就能在协调者发生崩溃恢复后,通过读取其事务日志来确定所有存疑事务的状态,任何在协调者事务日志中没有提交记录的事务都会被终止。因此两阶段提交在第二阶段(提交阶段)存在阻塞等待协调者恢复的情况,所以两阶段提交又被称为阻塞原子提交协议。 番外:3PC 三阶段提交协议也是应用在分布式事务提交中的算法,它的提出是为了解决两阶段提交协议中存在的阻塞问题。它分为CanCommit阶段、PreCommit阶段和DoCommit阶段,通过引入参与者超时判断机制来解决2PC中存在的参与者依赖协调者的提交请求而阻塞导致的资源占用等问题。 上图为在DoCommit阶段,参与者判断DoCommit请求超时情况的流程图,我们详述下它的避免阻塞的流程 服务在每个参与者上启动单节点事务,当参与者准备提交时,对应CanCommit阶段,协调者会向所有参与者发送CanCommit请求,如果任意一个请求失败或超时,则协调者会向所有参与者发送针对该事务的中止请求,执行事务回滚 当协调者收到所有CanCommit请求的答复时,如果全是“是”那么则进入PreCommit阶段,否则发送中止请求,执行事务回滚 进入PreCommit阶段后,协调者会向所有参与者发送PreCommit请求,同样还是如果存在请求失败或超时,会发送中止请求执行事务回滚 协调者收到所有PreCommit请求的答复时,如果全是“是”那么则进入DoCommit阶段,否则发送中止请求,执行事务回滚 进入DoCommit阶段后,协调者会向所有参与者发送DoCommit请求,注意这里,如果某个参与者没有收到该请求,它默认认为协调者会发送提交请求,那么便本地执行提交事务,从而避免阻塞 3PC虽然解决了2PC中存在的阻塞问题,但是也引入了新的问题: 如果协调者在DoCommit阶段回复的是中止请求,那么超时节点自顾自地提交事务就会发生数据不一致的情况 通讯次数增加和实现相对复杂 3PC使原子提交协议变成非阻塞的,但是3PC假定网络延迟和节点响应时间有限,在大多数具有无限网络延迟和进程暂停的实际系统中,它并不能保证原子性。非阻塞原子提交需要一个完美的故障检测器来以可靠的机制判断一个节点是否已经崩溃,而在无限延迟的网络中,超时并不是一种可靠的故障检测机制,因为即使节点没有崩溃也会因为网络延迟而超时,出于这个原因,2PC仍然被使用,尽管存在协调者故障的问题。 2. 容错共识算法 容错共识算法用于节点间数据同步,保证各个副本间数据的一致性和集群的高可用。它的通常形式是一个或多个节点可以提议(propose) 某些值,而共识算法决定(decides) 采用其中的某个值,并让这些节点就提议达成一致。共识算法必须具备如下性质: 一致同意:没有两个节点的决定不同 完整性:没有节点决定两次 有效性:如果节点决定了v值,那么v由该节点所提议 终止性:由所有未崩溃的节点来最终决定值 终止性实质上是说:容错共识算法不能简单地永远闲坐着等待,而是需要根据大多数节点来达成一项决定,因此终止属性也暗含着不超过一半的节点崩溃或不可达的信息。 一致同意和完整性是共识的核心思想,即所有节点决定了相同的结果并且决定后不能改变主意。 容错共识算法在节点集群内部都以某种形式使用一个领导者,并定义了一个纪元编号(epoch number) 来确保在每个时代中,领导者都是唯一的。每当现任领导宕机时,节点间会开始一场投票,来选出一个新的领导,每次选举被赋予一个新的纪元编号(全序且单调递增),如果有两个不同时代的领导者之间出现冲突(脑裂问题),那么带有更高纪元编号的领导者说了算。领导者每想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应并赞成提案。法定人数通常(但不总是)由多数节点组成(一般为过半),只有在没有发现任何带有更高纪元编号的领导者的情况下,一个节点才会投票赞成提议。 容错共识算法的局限性 节点在做出决定之前对提议进行投票的过程是一种同步复制 共识系统总是需要有法定人数的节点存活来保证运转 大多数共识算法假定参与投票的节点是固定的集合,这意味着你不能简单的在集群中添加或删除节点 共识系统通常依靠超时来检测失效的节点,在网络延迟高度变化的环境中,特别是在地理上散布的系统,经常发生一个节点由于暂时的网络问题,错误地认为领导者已经失效。虽然这种错误不会损害安全属性,但频繁的领导者选举会导致糟糕的性能表现,所以共识算法对网络问题比较敏感,而在面对不可靠的网络相关的共识算法研究仍在进展中 3. 2PC和容错共识算法的区别 负责解决的问题不同:2PC解决的是分布式事务的一致性,各个节点存储的数据各有不同,目标侧重于保证事务的ACID;容错共识算法解决的是节点副本间数据的一致性和保证集群的高可用,节点间存储的数据完全一致,目标侧重于数据的复制和同步 每个提议通过要求的参与节点数不同:2PC要求所有的参与者表决成功才通过;容错共识算法只需要遵循基于法定人数的表决即可,这也是容错共识算法终止属性(由所有未崩溃的节点来决定最终值) 的体现 集群的高可用保证:2PC的协调者不是通过选举产生的,而是单独部署并人为指定的组件,所以它没有选主机制,不具备高可用性;应用容错共识算法的集群领导者是通过选举机制来指定的,并且在发生异常情况时(主节点宕机)能够选出新的领导者,并进入一致的状态,以此来保证集群的高可用 4. zookeeper中的一次Create请求 一些资料中会提到zookeeper在执行CRUD请求时,使用的是2PC,而实际上它使用的是容错共识算法。我们以Create请求的流程为例(如下图),来加深和记忆这一知识 客户端发create请求到 Leader,即使请求没落到 Leader 上,那么其他节点也会将写请求转发到 Leader Leader 会先发一个 提议(proposal)请求给各个 Follower,且自己将数据写到本地文件 Follower 集群收到 proposal 请求后会将数据写到本地文件,写成功后返回给 Leader 一个 ack回复 Leader 发现收到 ack 回复的数量为法定人数(过半,包含当前 Leader 节点)时,则提交一个 commit 请求给各个 Follower 节点。发送 commit 请求就代表该数据在集群内同步情况没有问题,并且可以对外提供访问了,此时Leader会把数据写到内存中 Follower 收到 commit 请求后也会将数据写到各自节点的内存中,同时Leader会将数据发给 Observer集群,通知 Observer集群 将数据写到内存 巨人的肩膀 《数据密集型应用系统设计》第九章:分布式事务与共识 百度百科:三阶段提交 浅谈分布式一致性协议之3PC 分布式事务(2PC) vs 共识协议(Paxos/raft) 《深度剖析zookeeper核心原理》 原文收录:GitHub-Enthusiasm 作者:京东物流 王奕龙 内容来源:京东云开发者社区

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

Python自动化测试的配置层实现方式对标与落地 | 京东云技术团队

Python中什么是配置文件,配置文件如何使用,有哪些支持的配置文件等内容,话不多说,让我们一起看看吧~ 1 什么是配置文件? 配置文件是用于配置计算机程序的参数和初始化设置的文件,如果没有这些配置程序可能无法运行或是影响运行(运行速度、便捷性等),使用配置文件的好处在于,部分内容以及环境运行时只需要修改配置文件的参数内容,而无需去代码里查找并修改,提高便捷性、提高可维护性。 2 配置文件有哪几种? 配置主要有四种形式: 第一种是YAML、JSON、XML、TOML、INI、Linux系统中的.bashrc一类,主要应用于软件测试领域,在软件测试的领域行业中,大多数公司采用最新的YAML形式来作为配置文件,例如数据库地址、用例数据等内容的存放,而少部分公司仍然采用旧的INI配置形式 第二种是excel表格的形式,在excel表格中会有固定的title代表每个字段列的含义,有多列,以此来进行配置,多用于游戏领域中,在游戏行业大量使用excel表格的形式,已经是一个常态了。 第三种是py文件,py文件对于一个纯Python项目而言是非常方便的,它不需要做数据的读取操作,只需要进行导入即可,不好的点在于,没有YAML这一类灵活,YAML配置文件无论是Python、Java等语言,都是支持的,且数据类型支持很多,而py的配置文件,就只能用作于python,有一定的局限性。 第四种是txt文本格式,通过读取的方式来识别到txt文本内容,通常而言是测开或者测试工程师制作的简便工具,供给业务层面的测试人员进行使用,降低了YAML这种配置的理解难度,也避免了打开excel的缓慢,用轻量级txt来代替是一个不错的选择。 2.1 ini python3自带的ini .ini 文件是Initialization File的缩写,即初始化文件,是windows的系统配置文件所采用的存储格式,统管windows的各项配置 2.1.1 ini文件的定义 .ini 文件通常由节(Section)、键(key)和值(value)组成。具体形式如下: db.ini [mysql] host = 127.0.0.1 port = 3306 user = root password = 123456 database = test 2.1.2 python读取ini文件 使用python内置的 configparser 标准库进行解析ini文件。 read() 读取文件内容 items() 获取指定节的所有键值对 # -*- coding: utf-8 -*- ''' * @Author : wxy * @Date : 2022-08-24 11:11:06 * @Description : 读取ini文件 * @LastEditTime : 2022-08-24 11:11:06 ''' from configparser import ConfigParser from pprint import pprint import pymysql # ini文件路径 ini_file = './db.ini' # 读取ini的节(Section) db_name = 'mysql' # configparser实例化 text = ConfigParser() # 读取ini文件内容 text.read(ini_file) # text.items()返回list,元素为tuple,元组格式为 key,value db_tuple = text.items(db_name) print(db_tuple) # 将元组转换成dict db_dict = dict(text.items(db_name)) print(db_dict) 2.2 json JSON(JavaScript Object Notation,) 是一种轻量级的数据交换格式。 2.2.1 json文件的定义 简单的json示例 { "mysql": { "host": "127.0.0.1", "port": 3306, "user": "root", "password": "123456", "database": "test" } } 2.2.2 python读取json文件 load() 从json文件中读取json格式数据 loads() 将字符串类型数据转化为json格式数据 dump() 将json格式数据保存到文件 dumps() 将json格式数据保存为字符串类型 # -*- coding: utf-8 -*- ''' * @Author : wxy * @Date : 2022-8-24 11:39:44 * @Description : 读取json文件 * @LastEditTime : 2022-8-24 11:39:44 ''' import json from pprint import pprint import pymysql json_file = "./db.json" db_name = "mysql" web = "web" with open(json_file) as f: cfg = json.load(f)[db_name] print(cfg) with open(json_file) as f: cfg = json.load(f)[web] print(cfg['user']) 2.3 toml TOML 是 Github 联合创始人 Tom Preston-Werner 所提出的一种配置文件格式,是一种旨在成为一个小规模、易于使用的语义化的配置文件格式,它被设计为可以无二义性的转换为一个哈希表。 2.3.1 定义toml文件 语法: TOML的语法广泛地由key = “value”、[节名]、#注释构成。 支持以下数据类型:字符串、整形、浮点型、布尔型、日期时间、数组和图表。 # db.toml [mysql] [mysql.config] host = "127.0.0.1" user = "root" port = 3306 password = "123456" database = "test" [mysql.parameters] pool_size = 5 charset = "utf8" [mysql.fields] course_cols = ["cno", "cname", "ccredit", "cdept"] 2.3.2 python读取toml文件 使用外部库 toml 解析toml文件 # -*- coding: utf-8 -*- ''' * @Description : 读取toml文件 * @LastEditTime : 2022-08-14 11:31:07 ''' import toml from pprint import pprint import pymysql toml_file = "./db.toml" cfg = toml.load(toml_file)['mysql'] pprint(cfg) 2.4 yaml YAML(YAML Ain’t a Markup Language”, YAML不是一种标记语言) 格式是目前较为流行的一种配置文件,它早在 2001 由一个名为 Clark Evans 的人提出;同时它也是目前被广泛使用的配置文件类型。 2.4.1 定义yaml文件 # db.yaml mysql: config: host: "127.0.0.1" port: 3306 user: "root" password: "" database: "stu_sys" parameters: pool_size: 5 charset: "utf8" fileds: course_cols: - cno - cname - ccredit - cdept 2.4.2 python读取yaml文件 使用外部库 pyyaml 解析toml文件。 # -*- coding: utf-8 -*- ''' * @Author : wxy * @Date : 2022-8-24 11:34:37 * @Description : 读取yaml文件 * @LastEditTime : 2022-8-24 11:34:37 ''' import yaml from pprint import pprint import pymysql yaml_file = "./db.yaml" with open(yaml_file, 'r') as f: cfg = yaml.safe_load(f) print(cfg) 2.5 Python xlrd 读取 操作Excel python Excel库对比 本次主要关注python xlrd读取 操作excel 2.5.1 xlrd模块介绍 1.什么是xlrd模块? python操作excel主要用到xlrd和xlwt这两个库,即xlrd是读excel,xlwt是写excel的库。 2.为什么使用xlrd模块? 在UI自动化或者接口自动化中数据维护是一个核心,所以此模块非常实用。 xlrd模块可以用于读取Excel的数据,速度非常快,推荐使用! 官方文档:https://xlrd.readthedocs.io/en/latest/ 2.5.2 安装xlrd模块 到python官网下载http://pypi.python.org/pypi/xlrd 模块安装,前提是已经安装了python 环境。 或者在cmd窗口 pip install xlrd 最新的xlrd不支持Excel xlsx文件的读取。所以需要安装旧版本 pip install xlrd==1.2.0 2.5.3 使用介绍 1.常用单元格的数据类型 empty(空的) string(text) number date boolean error blank(空白表格) 2.导入模块 import xlrd 3.打开Excel文件读取数据 data = xlrd.open_workbook(filename)#文件名以及路径,如果路径或者文件名有中文给前面加一个 r 4.常用的函数 excel中最重要的方法就是book和sheet的操作 获取book(excel文件)中一个工作表 table = data.sheets()[0] #通过索引顺序获取 table = data.sheet_by_index(sheet_indx) #通过索引顺序获取 table = data.sheet_by_name(sheet_name) #通过名称获取 # 以上三个函数都会返回一个xlrd.sheet.Sheet()对象 names = data.sheet_names() #返回book中所有工作表的名字 data.sheet_loaded(sheet_name or indx) # 检查某个sheet是否导入完毕 行的操作 nrows = table.nrows # 获取该sheet中的行数,注,这里table.nrows后面不带(). table.row(rowx) # 返回由该行中所有的单元格对象组成的列表,这与tabel.raw()方法并没有区别。 table.row_slice(rowx) # 返回由该行中所有的单元格对象组成的列表 table.row_types(rowx, start_colx=0, end_colx=None) # 返回由该行中所有单元格的数据类型组成的列表; # 返回值为逻辑值列表,若类型为empy则为0,否则为1 table.row_values(rowx, start_colx=0, end_colx=None) # 返回由该行中所有单元格的数据组成的列表 table.row_len(rowx) # 返回该行的有效单元格长度,即这一行有多少个数据 列(colnum)的操作 ncols = table.ncols # 获取列表的有效列数 table.col(colx, start_rowx=0, end_rowx=None) # 返回由该列中所有的单元格对象组成的列表 table.col_slice(colx, start_rowx=0, end_rowx=None) # 返回由该列中所有的单元格对象组成的列表 table.col_types(colx, start_rowx=0, end_rowx=None) # 返回由该列中所有单元格的数据类型组成的列表 table.col_values(colx, start_rowx=0, end_rowx=None) # 返回由该列中所有单元格的数据组成的列表 单元格的操作 table.cell(rowx,colx) # 返回单元格对象 table.cell_type(rowx,colx) # 返回对应位置单元格中的数据类型 table.cell_value(rowx,colx) # 返回对应位置单元格中的数据 2.5.4 实战训练 使用xlrd模块进行读取: import xlrd xlsx = xlrd.open_workbook('./3_1 xlrd 读取 操作练习.xlsx') # 通过sheet名查找:xlsx.sheet_by_name("sheet1") # 通过索引查找:xlsx.sheet_by_index(3) table = xlsx.sheet_by_index(0) # 获取单个表格值 (2,1)表示获取第3行第2列单元格的值 value = table.cell_value(2, 1) print("第3行2列值为",value) # 获取表格行数 nrows = table.nrows print("表格一共有",nrows,"行") # 获取第4列所有值(列表生成式) name_list = [str(table.cell_value(i, 3)) for i in range(1, nrows)] print("第4列所有的值:",name_list) 2.6 Python xlwt 写入 操作Excel(仅限xls格式!) xlwt可以用于写入新的Excel表格或者在原表格基础上进行修改,速度也很快,推荐使用! 官方文档:https://xlwt.readthedocs.io/en/latest/ 2.6.1 pip安装xlwt pip install xlwt 2.6.2 使用xlwt创建新表格并写入 编写xlwt新表格写入程序: # 3.2.2 使用xlwt创建新表格并写入 def fun3_2_2(): # 创建新的workbook(其实就是创建新的excel) workbook = xlwt.Workbook(encoding= 'ascii') # 创建新的sheet表 worksheet = workbook.add_sheet("My new Sheet") # 往表格写入内容 worksheet.write(0,0, "内容1") worksheet.write(2,1, "内容2") # 保存 workbook.save("新创建的表格.xls") 2.6.3 xlwt 设置字体格式 程序示例: # 3.2.3 xlwt设置字体格式 def fun3_2_3(): # 创建新的workbook(其实就是创建新的excel) workbook = xlwt.Workbook(encoding= 'ascii') # 创建新的sheet表 worksheet = workbook.add_sheet("My new Sheet") # 初始化样式 style = xlwt.XFStyle() # 创建字体样式 font = xlwt.Font() font.name = 'Times New Roman' #字体 font.bold = True #加粗 font.underline = True #下划线 font.italic = True #斜体 # 设置样式 style.font = font # 往表格写入内容 worksheet.write(0,0, "内容1") worksheet.write(2,1, "内容2",style) # 保存 workbook.save("新创建的表格.xls") # 设置列宽 worksheet.col(0).width = 256*20 # 设置行高 style = xlwt.easyxf('font:height 360;') # 18pt,类型小初的字号 row = worksheet.row(0) row.set_style(style) # 合并 第1行到第2行 的 第0列到第3列 worksheet.write_merge(1, 2, 0, 3, 'Merge Test') # 设置边框样式 borders = xlwt.Borders() # Create Borders borders.left = xlwt.Borders.DASHED borders.right = xlwt.Borders.DASHED borders.top = xlwt.Borders.DASHED borders.bottom = xlwt.Borders.DASHED borders.left_colour = 0x40 borders.right_colour = 0x40 borders.top_colour = 0x40 borders.bottom_colour = 0x40 2.7 Python xlutils 修改 操作Excel xlutils可用于拷贝原excel或者在原excel基础上进行修改,并保存; 官方文档:https://xlutils.readthedocs.io/en/latest/ 2.7.1 pip安装xlutils pip install xlutils 2.7.2 xlutils拷贝源文件(需配合xlrd使用) 程序示例: # 3.3.2 拷贝源文件 def fun3_3_2(): workbook = xlrd.open_workbook('3_3 xlutils 修改操作练习.xlsx') # 打开工作簿 new_workbook = copy(workbook) # 将xlrd对象拷贝转化为xlwt对象 new_workbook.save("new_test.xls") # 保存工作簿 2.7.3 xlutils 读取 写入 (也就是修改)Excel 表格信息 程序示例: # 3.3.3 xlutils读取 写入 Excel 表格信息 def fun3_3_3(): # file_path:文件路径,包含文件的全名称 # formatting_info=True:保留Excel的原格式(使用与xlsx文件) workbook = xlrd.open_workbook('3_3 xlutils 修改操作练习.xlsx') new_workbook = copy(workbook) # 将xlrd对象拷贝转化为xlwt对象 # 读取表格信息 sheet = workbook.sheet_by_index(0) col2 = sheet.col_values(1) # 取出第二列 cel_value = sheet.cell_value(1, 1) print(col2) print(cel_value) # 写入表格信息 write_save = new_workbook.get_sheet(0) write_save.write(0, 0, "xlutils写入!") new_workbook.save("new_test.xls") # 保存工作簿 2.8 Python xlwings 读取 写入 修改 操作Excel 2.8.1 pip安装xlwings pip install xlwings 2.8.2 基本操作 引入库 import xlwings as xw (1)打开已存在的Excel文档 # 导入xlwings模块 import xlwings as xw # 打开Excel程序,默认设置:程序可见,只打开不新建工作薄,屏幕更新关闭 app=xw.App(visible=True,add_book=False) app.display_alerts=False app.screen_updating=False # 文件位置:filepath,打开test文档,然后保存,关闭,结束程序 filepath=r'g:\Python Scripts\test.xlsx' wb=app.books.open(filepath) wb.save() wb.close() app.quit() (2)新建Excel文档,命名为test.xlsx,并保存在D盘 import xlwings as xw app=xw.App(visible=True,add_book=False) wb=app.books.add() wb.save(r'd:\test.xlsx') wb.close() app.quit() (3) xlwings 读写 Excel 新建test.xlsx,在sheet1的第一个单元格输入 “人生” ,然后保存关闭,退出Excel程序。 def fun3_4_4(): # 新建Excle 默认设置:程序可见,只打开不新建工作薄,屏幕更新关闭 app = xw.App(visible=True, add_book=False) app.display_alerts = False app.screen_updating = False # 打开已存在的Excel文件 wb=app.books.open('./3_4 xlwings 修改操作练习.xlsx') # 获取sheet对象 print(wb.sheets) sheet = wb.sheets[0] # sheet = wb.sheets["sheet1"] # 读取Excel信息 cellB1_value = sheet.range('B1').value print("单元格B1内容为:",cellB1_value) # 清除单元格内容和格式 sheet.range('A1').clear() # 写入单元格 sheet.range('A1').value = "xlwings写入" # 保存工作簿 wb.save('example_3.xlsx') # 退出工作簿 wb.close() # 退出Excel app.quit()l 2.9 Python openpyxl 读取 写入 修改 操作Excel 在openpyxl中,主要用到三个概念:Workbooks,Sheets,Cells。 Workbook就是一个excel工作表; Sheet是工作表中的一张表页; Cell就是简单的一个格。 openpyxl就是围绕着这三个概念进行的,不管读写都是“三板斧”:打开Workbook,定位Sheet,操作Cell。 官方文档:https://openpyxl.readthedocs.io/en/stable/ 1.安装 pip install openpyxl 2.打开文件 (1)新建 from openpyxl import Workbook # 实例化 wb = Workbook() # 激活 worksheet ws = wb.active (2)打开已有 from openpyxl import load_workbook wb = load_workbook('文件名称.xlsx') 3.写入数据 # 方式一:数据可以直接分配到单元格中(可以输入公式) ws['A1'] = 42 # 方式二:可以附加行,从第一列开始附加(从最下方空白处,最左开始)(可以输入多行) ws.append([1, 2, 3]) # 方式三:Python 类型会被自动转换 ws['A3'] = datetime.datetime.now().strftime("%Y-%m-%d") 4.创建表(sheet) # 方式一:插入到最后(default) ws1 = wb.create_sheet("Mysheet") # 方式二:插入到最开始的位置 ws2 = wb.create_sheet("Mysheet", 0) 5.选择表(sheet) # sheet 名称可以作为 key 进行索引 >>> ws3 = wb["New Title"] >>> ws4 = wb.get_sheet_by_name("New Title") >>> ws is ws3 is ws4 True 6.查看表名(sheet) # 显示所有表名 >>> print(wb.sheetnames) ['Sheet2', 'New Title', 'Sheet1'] # 遍历所有表 >>> for sheet in wb: ... print(sheet.title) 7.保存数据 wb.save('文件名称.xlsx') 8.其它 (1)改变sheet标签按钮颜色 ws.sheet_properties.tabColor = "1072BA" # 色值为RGB16进制值 (2)获取最大行,最大列 # 获得最大列和最大行 print(sheet.max_row) print(sheet.max_column) (3)获取每一行每一列 sheet.rows为生成器, 里面是每一行的数据,每一行又由一个tuple包裹。 sheet.columns类似,不过里面是每个tuple是每一列的单元格。 # 因为按行,所以返回A1, B1, C1这样的顺序 for row in sheet.rows: for cell in row: print(cell.value) # A1, A2, A3这样的顺序 for column in sheet.columns: for cell in column: print(cell.value) (4)根据数字得到字母,根据字母得到数字 from openpyxl.utils import get_column_letter, column_index_from_string # 根据列的数字返回字母 print(get_column_letter(2)) # B # 根据字母返回列的数字 print(column_index_from_string('D')) # 4 (5)删除工作表 # 方式一 wb.remove(sheet) # 方式二 del wb[sheet] 项目实操—-UI自动化中实践项目 需求:业务写入拣货容器,使用一次不可使用第二次,且脚本中固定读取固定位置 import openpyxl from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE from openpyxl.utils import get_column_letter, column_index_from_string # 向sheetobj中的columnname列从start_row开始写入listdata def insert_listdata_to_column(sheetobj,listdata,column_name,start_row=3): # 根据列名获取列索引 colindex = column_index_from_string(column_name) print('colindex为{}'.format(colindex)) # 循环从开始行数到数据写入后最后一行 for rowindex in range(start_row, start_row + len(listdata)): # 写入list数值根据索引取值,从0开始 val = listdata[rowindex - start_row] print('val{}'.format(val)) print('rowindex{}'.format(rowindex)) try: sheetobj.cell(row = rowindex,column = colindex,value = val) except: # 出现非法字符时,可以将字符串的非法字符替换掉 val = ILLEGAL_CHARACTERS_RE.sub(r'',val) sheetobj.cell(row = rowindex,column = colindex,value = val) delrow = start_row + len(listdata) print('*********{}'.format(delrow)) sheetobj.delete_rows(delrow) def del_excel(): xlsx = xlrd.open_workbook(r'D:\pytest\inbound_data.xlsx') table =xlsx.sheet_by_index(2) # 获取第2列所有值 cel_value =table.col_values(1) cel_value=cel_value[3::] wb = openpyxl.load_workbook(r'D:\pytest\inbound_data.xlsx') sheet = wb["B2B出库"] print(sheet) insert_listdata_to_column(sheet,cel_value,'B',3) wb.save(r'D:\pytest\inbound_data.xlsx') del_excel() 3 总结 在本文中,简单介绍了几种配置文件和使用。根据不同的用例,复杂的工具/框架并不总是比简单的软件包更好。但无论选择哪一种,都应始终考虑可读性,可维护性以及如何尽早地发现错误。事实上,可以说配置文件只是另一种类型的代码。可以根据自己的项目框架来灵活运用啦~ 作者:京东物流王小云 来源:京东云开发者社区

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

消失的死锁:从 JSF 线程池满到 JVM 初始化原理剖析 | 京东云技术团队

一、问题描述 在一次上线时,按照正常流程上线后,观察了线上报文、接口可用率十分钟以上,未出现异常情况,结果在上线一小时后突然收到jsf线程池耗尽的报警,并且该应用一共有30台机器,只有一台机器出现该问题,迅速下线该机器的jsf接口,恢复线上。然后开始排查问题。 报错日志信息: [WARN]2023-04-10 18:03:34.847 [ - ][] |[JSF-23002]Task:java.util.concurrent.FutureTask@502cdfa0 has been reject for ThreadPool exhausted! pool:200, active:200, queue:0, taskcnt: 2159[BusinessPool#:][JSF-SEV-WORKER-225-T-8] 二、问题分析 1、出现问题原因: a)因为只有一台机器出现线程池耗尽,其他机器均正常运行。所以第一时间判断是否为有大量流量负载不均衡导致; b)业务代码存在并发锁; c)业务代码处理时间较长; d)访问数据源(如DB、redis)变慢; 排查接口流量UMP监控,按照机器纬度看,发现每个机器流量是均衡的,排除a)项; 排查业务量大的接口UMP KEY监控,按照机器纬度看,正常机器和异常机器耗时基本一致,并于往常一致,无较大差异,排除c)项; 排查数据库监控,无慢sql,读写均无耗时较长的情况,排除d)项; 综上,只剩下b)项,确认问题原因是代码存在并发锁,故开始排查日志及业务代码。 2、根据已确认的原因排查思路: 1)down下dump文件,发现极多JSF线程处于RUNNABLE状态,并且堆栈处于SerializersHelper类 "JSF-BZ-22000-223-T-200" #1251 daemon prio=5 os_prio=0 tid=0x00007fd15005c000 nid=0xef6 in Object.wait() [0x00007fce287ac000] java.lang.Thread.State: RUNNABLE at com.jd.purchase.utils.serializer.helper.SerializersHelper.ofString(SerializersHelper.java:79) at com.jd.ldop.pipe.proxy.OrderMiddlewareCBDExportServiceProxy.getAddress(OrderMiddlewareCBDExportServiceProxy.java:97) at com.jd.ldop.pipe.proxy.OrderMiddlewareCBDExportServiceProxy.findOrder(OrderMiddlewareCBDExportServiceProxy.java:211) 根据堆栈信息排查代码,发现代码会初始化一个自定义的序列化工厂类:SerializerFactory 并且此时初始化时会打印日志: log.info("register: {} , clazz : {}", serializer.getCode(), serializer.getClass().getName()); 故根据此日志关键字排查初始化加载日志,发现正常机器都加载了两个序列化对象,只有出问题的那个机器只加载了这一个序列化对象。 于是问题初步定位到出问题的机器初始化ProtoStuffSerializer这个类时失败。 初始化此类时static代码块为: static { STRATEGY = new DefaultIdStrategy(IdStrategy.DEFAULT_FLAGS); } 2)开始排查为什么初始化这个类会失败 由于不同机器存在初始化成功和失败的独立性,首先考虑jar包是否冲突 a)于是发现这里存在jar冲突,但是将冲突jar排除后,多次重启机器后发现依然存在此ProtoStuffSerializer初始化失败情况。 b)存在死锁,但是正常逻辑下,存在死锁的话,应该所有机器都会存在此类情况,但是此时大概只有5%的几率出现死锁,并且排查jstack发现200个线程都卡在获取ProtoStuffSerializer。 山重水尽疑无路 柳暗花明又一村 3、找到问题 此时排除了所有没可能的选项,剩下一个可能性再低也是正确选项。 如果存在死锁情况的话,那jstack的线程堆栈信息肯定会报出来,于是根据jstack线程信息逐个排查每一个线程。 最后发现下面这个线程的堆栈: "jcase-jmq-reporter-t-0" #1010 daemon prio=5 os_prio=0 tid=0x00007fd258004800 nid=0x9ba in Object.wait() [0x00007fd10fffd000] java.lang.Thread.State: RUNNABLE at io.protostuff.runtime.RuntimeEnv.<clinit>(RuntimeEnv.java:229) at io.protostuff.runtime.IdStrategy.<clinit>(IdStrategy.java:53) at io.protostuff.runtime.ExplicitIdStrategy$Registry.<init>(ExplicitIdStrategy.java:67) at com.jd.tp.jcase.util.RecordSerializers$ProtostuffIdRegistry.<init>(RecordSerializers.java:108) at com.jd.tp.jcase.util.RecordSerializers.<clinit>(RecordSerializers.java:34) at com.jd.tp.jcase.recording.agent.reporter.impl.JmqReporter$ReportRunner.run(JmqReporter.java:106) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) 发现此线程(in Object.wait())也依然处于等待状态,并且此线程的堆栈信息中包含了protostuff这个关键字(由于上面线程都等待在初始化protostuffprotostuff导致的!) 于是乎开始分析此线程! 从此行栈信息开始排查 at com.jd.tp.jcase.recording.agent.reporter.impl.JmqReporter$ReportRunner.run(JmqReporter.java:106) xml中存在以下bean: <bean id="jcaseJmqReporter" class="com.jd.tp.jcase.recording.agent.reporter.impl.JmqReporter" init-method="start" destroy-method="stop"> <constructor-arg name="name" value="jmq"/> <constructor-arg name="recorder" ref="jcaseRecorder"/> <constructor-arg name="topic" value="${jmq.topic.ldopjcasereporter}"/> <constructor-arg name="producer" ref="jcaseJmqProducer"/> <property name="config" ref="jcaseConfig"/> </bean> 发现以下代码符合堆栈信息: 根据此线程的堆栈信息逐行排查代码,发现该线程执行JmqReporter.run方法时,会初始化RecordSerializers类;并在RecordSerializers中的静态代码块会执行如下代码: RecordSerializers.ProtostuffIdRegistry registry = new RecordSerializers.ProtostuffIdRegistry(); 于是执行这个类的无参构造时会new出类变量: 于是线程开始初始化ExplicitIdStrategy这个类: 开始执行父类的有参构造: 于是开始初始化IdStrategy类,并且执行IdStrategy类的static静态代码块: 于是此处开始初始化RuntimeEnv,并且执行RuntimeEnv的静态代码块;线程堆栈信息就显示等待在此类了, 排查RuntimeEnv的static代码块时发现存在和上一个线程使用了相同的类: new DefaultIdStrategy(); 类加载的问题? 首次应该怀疑是类加载的问题,因为除了两百个线程停留在加载protostuffprotostuff(初始化有new DefaultIdStrategy()的代码)这个类上,此线程也处于等待状态,并且也在加载DefaultIdStrategy()的类上。 然后再分析一下这个线程的堆栈信息。 "jcase-jmq-reporter-t-0" #1010 daemon prio=5 os_prio=0 tid=0x00007fd258004800 nid=0x9ba in Object.wait() [0x00007fd10fffd000] java.lang.Thread.State: RUNNABLE at io.protostuff.runtime.RuntimeEnv.<clinit>(RuntimeEnv.java:229) at io.protostuff.runtime.IdStrategy.<clinit>(IdStrategy.java:53) at io.protostuff.runtime.ExplicitIdStrategy$Registry.<init>(ExplicitIdStrategy.java:67) at com.jd.tp.jcase.util.RecordSerializers$ProtostuffIdRegistry.<init>(RecordSerializers.java:108) at com.jd.tp.jcase.util.RecordSerializers.<clinit>(RecordSerializers.java:34) at com.jd.tp.jcase.recording.agent.reporter.impl.JmqReporter$ReportRunner.run(JmqReporter.java:106) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - <0x00000000c81fce28> (a java.util.concurrent.ThreadPoolExecutor$Worker) 可以看到在RuntimeEnv、IdStrategy后都有<clinit>; 从名字上来不难猜到是正在做类的初始化,那我们先来了解下类的初始化过程。 类的初始化过程 当我们第一次主动调用某个类的静态方法就会触发这个类的初始化,当然还有其他的触发情况,类的初始化说白了就是在类加载起来之后,在某个合适的时机执行这个类的clinit方法。 clinit方法是什么? 比如我们在类里声明一段static代码块,或者有静态属性,javac会将这些代码都统一放到一个叫做clinit的方法里,在类初始化的时候来执行这个方法,但是JVM必须要保证这个方法只能被执行一次,如果有其他线程并发调用触发了这个类的多次初始化,那只能让一个线程真正执行clinit方法,其他线程都必须等待,当clinit方法执行完之后,然后再唤醒其他等待这里的线程继续操作,当然不会再让它们有机会再执行clinit方法,因为每个类都有一个状态,这个状态可以保证这一点。 public static class ClassState { public static final InstanceKlass.ClassState ALLOCATED = new InstanceKlass.ClassState("allocated"); public static final InstanceKlass.ClassState LOADED = new InstanceKlass.ClassState("loaded"); public static final InstanceKlass.ClassState LINKED = new InstanceKlass.ClassState("linked"); public static final InstanceKlass.ClassState BEING_INITIALIZED = new InstanceKlass.ClassState("beingInitialized"); public static final InstanceKlass.ClassState FULLY_INITIALIZED = new InstanceKlass.ClassState("fullyInitialized"); public static final InstanceKlass.ClassState INITIALIZATION_ERROR = new InstanceKlass.ClassState("initializationError"); private String value; private ClassState(String value) { this.value = value; } public String toString() { return this.value; } } 当有个线程正在执行这个类的clinit方法的时候,就会设置这个类的状态为being_initialized,当正常执行完之后就马上设置为fully_initialized,然后才唤醒其他也在等着对其做初始化的线程继续往下走,在继续走下去之前,会先判断这个类的状态,如果已经是fully_initialized了说明有线程已经执行完了clinit方法,因此不会再执行clinit方法了 类加载的动作 void TemplateTable::checkcast() { ... call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::quicken_io_cc)); ... } IRT_ENTRY(void, InterpreterRuntime::quicken_io_cc(JavaThread* thread)) // Force resolving; quicken the bytecode int which = get_index_u2(thread, Bytecodes::_checkcast); constantPoolOop cpool = method(thread)->constants(); // We'd expect to assert that we're only here to quicken bytecodes, but in a multithreaded // program we might have seen an unquick'd bytecode in the interpreter but have another // thread quicken the bytecode before we get here. // assert( cpool->tag_at(which).is_unresolved_klass(), "should only come here to quicken bytecodes" ); klassOop klass = cpool->klass_at(which, CHECK); thread->set_vm_result(klass); IRT_END klassOop klass_at(int which, TRAPS) { constantPoolHandle h_this(THREAD, this); return klass_at_impl(h_this, which, CHECK_NULL); } klassOop constantPoolOopDesc::klass_at_impl(constantPoolHandle this_oop, int which, TRAPS) { ... klassOop k_oop = SystemDictionary::resolve_or_fail(name, loader, h_prot, true, THREAD); ... } //SystemDictionary::resolve_or_fail最终会调用到下面这个方法 klassOop SystemDictionary::resolve_instance_class_or_null(Symbol* name, Handle class_loader, Handle protection_domain, TRAPS) { ... // Class is not in SystemDictionary so we have to do loading. // Make sure we are synchronized on the class loader before we proceed Handle lockObject = compute_loader_lock_object(class_loader, THREAD); check_loader_lock_contention(lockObject, THREAD); ObjectLocker ol(lockObject, THREAD, DoObjectLock); ... //此时会调用ClassLoader.loadClass来加载类了 ... } Handle SystemDictionary::compute_loader_lock_object(Handle class_loader, TRAPS) { // If class_loader is NULL we synchronize on _system_loader_lock_obj if (class_loader.is_null()) { return Handle(THREAD, _system_loader_lock_obj); } else { return class_loader; } } SystemDictionary::resolve_instance_class_or_null这个方法非常关键了,在里面我们看到会获取一把锁ObjectLocker,其相当于我们java代码里的synchronized关键字,而对象对应的是lockObject,这个对象是上面的SystemDictionary::compute_loader_lock_object方法返回的,从代码可知只要不是bootstrapClassloader加载的类就会返回当前classloader对象,也就是说当我们在加载一个类的时候其实是会持有当前类加载对象的锁的,在获取了这把锁之后就会调用ClassLoader.loadClass来加载类了。 小结 看到这里是否能解释了我们线上为什么会有那么多线程会卡在某一个地方了?因为这个类的状态是being_initialized,所以只能等了。 这个类加载的锁,不过遗憾的是因为这把锁不是java层面来显示加载的,因此我们在jstack线程dump的输出里居然看不到这把锁的存在。 从dump来看确实是死锁了,那这个场景当时是怎么发生的呢? 如图所示,最后A、B线程均在等待对方初始化完成,然后C、D、E等两百个线程需要使用ProtoStuffSerializer时,就在等待A线程初始化ProtoStuffSerializer完成。因此造成了JSF线程池爆满。 "JSF-BZ-22000-223-T-1" #980 daemon prio=5 os_prio=0 tid=0x00007fd164002000 nid=0x99a in Object.wait() [0x00007fd1de8b7000] java.lang.Thread.State: RUNNABLE at com.jd.purchase.utils.serializer.impl.ProtoStuffSerializer.<clinit>(ProtoStuffSerializer.java:42) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) 只有此线程获取到了ProtoStuffSerializer的初始化锁也间接证明了这一点。 三、解决方案 了解到是由于A、B线程互相争夺对方的初始化锁后,那么为了打破这一点,就让其中某一个线程提前初始化这些类就可以了。 这里选择提前加载这个bean:初始化业务所使用到的类 <bean class="com.jd.purchase.utils.serializer.starter.CustomSerializerStarter"/> 四、Demo验证 Demo代码: public class JVMTest { public static void main(String[] args) { new Thread(){ public void run(){ B.test(); } }.start(); new Thread(){ public void run(){ A.test(); } }.start(); } } class A{ static{ int a=0; System.out.println(a); B.test(); } static void test(){ System.out.println("调用了A的test方法"); } } class B{ static{ int b=0; System.out.println(b); A.test(); } static void test(){ System.out.println("调用了B的test方法"); } } 结果: Demo现象解释 我们Demo里的那两个线程,从dump来看确实是死锁了,那这个场景当时是怎么发生的呢? 线程1首先执行B.test(),于是会对B类做初始化,设置B的类状态为being_initialized,接着去执行B的clinit方法,但是在clinit方法里要去调用A.test方法,理论上此时会对A做初始化并调用其test方法,但是就在设置完B的类状态之后,执行其clinit里的A.test方法之前; 线程2却执行了A.test方法,此时线程2会优先负责对A的初始化工作,即设置A类的状态为being_initialized,然后再去执行A的clinit方法,此时线程1发现A的类状态是being_initialized了,那线程1就认为有线程对A类正在做初始化,于是就等待了,而线程2同样发现B的类状态也是being_initialized,于是也开始等待,这样就形成了两个线程都在等待另一个线程完成初始化的情况,造成了类死锁的现象。 五、总结 类加载的死锁很隐蔽了,但是类初始化的死锁更隐蔽,所以大家要谨记在类的初始化代码里产生循环依赖,另外对于jdk8的defalut特性也要谨慎,因为这会直接触发接口的初始化导致更隐蔽的循环依赖。 推荐阅读: JDK的sql设计不合理导致的驱动类初始化死锁问题:https://blog.csdn.net/xichenguan/article/details/39578401 java虚拟机规范—初始化:https://blog.csdn.net/weixin_38233104/article/details/125251345 JVM常用命令:https://zhuanlan.zhihu.com/p/401563061 作者:京东物流 李键屿 来源:京东云开发者社区

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

辅助测试和研发人员的一款小插件【数据安全】 | 京东云技术团队

一、为什么要做一款这样的小插件 数据,一直在思考如何让数据更安全的流转和服务于客户,围绕这样的想法,我们做过许多方面的扩展。我们落地了服务端的数据切片支持场景化的设计,实现了基于JDBC协议对SQL的拦截与切片,实现了在应用层的全链路数据库审计方案和实现,实现了WEB端明暗水印和文档水印等等,但这些都是在应用服务端的改造;那么围绕以上服务端的思想产生了在端上做一些事情,分析了集团内部服务,多以WEB服务端对客户和用户提供便利的功能和数据使用场景。WEB服务多以依赖浏览器的存在进行访问,所以就试图把数据安全防护的方案前置到浏览器上,在浏览器上做辅助性的数据探测,可以在研发阶段和测试阶段发现数据使用的安全问题,比如某个接口的返回了明文手机号,某个页面未使用身份信息但是调用的接口返回了等等场景... 所以我们要做一个浏览插件把数据安全防护能力前置到端上实现,它不会影响开发者和测试人员... 二、解决的思路和想法 第一步说明了我们为什么要做一款这样的小工具,这个章节就是如何去解决上面的问题和想法。 1、浏览器占比分析 市场上的浏览器chrome/firefox/safri....从市场上数据分析目前chrome浏览器占比份额为66.93%,基本主导了浏览器市场,排名第二的是MicrosoftEdge浏览器。根据这样的一个结果我们优先从chrome插件去着手。 2、知识面的延伸 我们团队侧重于后端的方向研发,在前端侧的能力相对来说比较弱势特别是在浏览器插件又涉及到一些底层的运转机制等;首先官方文档去了解chrome插件开发,明确了目前chrome支持的插件版本为v2和v3,其中不好的消息就是v2在2023年的时候会被后续的版本放弃,意味着v2和v3要同时进行,所以又去了解了一下版本的差异,两个版本在网络拦截方面有了比较大的改动,后者变成了规则的形式,对拦截方面不是太友好。 3、想法和功能设计 要把数据安全部分防护能力前置到浏览器端,意味着要对当前的功能做分析和实现,首先插件能力不能对研发和测试人员产生影响,其次使用要方便。就有了以下的四个点的想法: 第一是WEB水印能力 通过对插件机制的了解,可以在安装插件之后,对生效的站点修改dom的结构,把水印自动的增加到页面中。 第二个想到的是敏感的数据 应用的敏感数据来源分为三个部分:页面渲染的资源,接口请求的数据,接口返回的数据,围绕这一层的构思,最终定义要实现对页面和请求的拦截。 第三想法是对页面的操作的事件监听 这个的来源于某些站点会提供大量的数据,用户在使用的时候可以直接使用把数据复制到其它数据,这样的操作属于用户个人行为也不是文件的形式,用户在复制的过程中无法第一时间感知是否为敏感,没有办法做到第一时间的追踪和防护。 第四个..其实是额外的延伸 我们目前同时也做数据流动链相关的项目,在项目进行的过程中发现会在前端路由与后端路由的链条缺失,想到浏览器插件的运行机制是可以通过dom的对象拿到前端页面的路由,这样的思路就可以填补了这块数据链关系的缺失。 三、落地的路上很多坑 1、简易逻辑图 2、devtool的坑 在chrome插件的生命周期中,在background能监听请求发出、返回等事件,一般能获取的事件如下: // web请求监听,最后一个参数表示阻塞式,需单独声明权限:webRequestBlocking chrome.webRequest.onBeforeRequest.addListener(details => { // cancel 表示取消本次请求 if(details.type == 'image' || details.type === 'medis') return {cancel: false}; }, {urls: ["<all_urls>"]}); //ajax生命周期开始 chrome.webRequest.onBeforeRequest.addListener(details => { console.log('onBeforeRequest', details); }, { urls: ["*://*.jd.com/**"] }); chrome.webRequest.onBeforeSendHeaders.addListener(details => { console.log('onBeforeSendHeaders', details) }, { urls: ["*://*.jd.com/**"] }, ['blocking', 'requestHeaders', 'extraHeaders'] ); // 可以拦截ajax chrome.webRequest.onResponseStarted.addListener(details => { console.log('onResponseStarted', details); }, {urls: ["*://*.jd.com/**"]}); // 请求完成,但是取不到response结果 chrome.webRequest.onCompleted.addListener(details => { console.log('onCompleted', details); }, {urls: ["*://*.jd.com/**"] 该方式无法拿到response内容,从网上常见的解决方案,是给浏览器插件添加devtools_page模块,然后在devtools的页面添加request结束事件,如下: chrome.devtools.network.onRequestFinished.addListener( function(request) { //request 包含请求响应数据,如:url,响应内容等 //request.request.url 接口 的url //request.getContent 接口返回的内容 } ); 但是该方式有个问题,就是想要触发devtools的页面内容,需要在页面按F12呼出浏览器的控制台,这个体验就很一般,所以devtools这种形式更像是为开发人员提供debug工具时引入的。 3、xhr/fetch 因为上文使用两种方式的局限性,所以考虑重写xhr,然后在页面加载时进行注入替换,重写xhr的方法如下: (function () { var open = XMLHttpRequest.prototype.open; var send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function (body) { this["hookQuery"] = body; send.apply(this, arguments); }; XMLHttpRequest.prototype.open = function () { this.addEventListener("load", function () { if (this.responseType === "" || this.responseType === "text") { window.dispatchEvent( new CustomEvent("pageScript", { ... }) ); } }); open.apply(this, arguments); }; })() 主要改写了原有send方法和open方法,在send时,将请求的参数记录到xhr实体的一个属性里,然后在请求触发并返回(load事件)后,将请求的参数、响应的返回内容,通过事件发送出去,事件会在content_scripts进行监听和处理,content_scripts部分代码如下: // 加载重写的xhr所在js文件,然后注入到当前页面的document中,这样业务的页面会加载这段js var hook = document.createElement("script"); hook.src = chrome.runtime.getURL("script/hook.js"); hook.onload = function () { this.remove(); }; (document.head || document.documentElement).appendChild(hook); // 监听xhr发送过来的消息 window.addEventListener( "pageScript", function (event) { // 处理逻辑 }, false ); 五、效果演示 六、未完待续 第一版的功能以敏感数据防护为主导线,后续会增加数据安全其它的防护和感知能力,从端上协助业务解决数据安全的风险 作者:CCO体系 郝帅卫 来源:京东云开发者社区

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

线上问题处理案例:出乎意料的数据库连接池 | 京东云技术团队

导读 本文是线上问题处理案例系列之一,旨在通过真实案例向读者介绍发现问题、定位问题、解决问题的方法。本文讲述了从垃圾回收耗时过长的表象,逐步定位到数据库连接池保活问题的全过程,并对其中用到的一些知识点进行了总结。 一、问题描述 大促期间,某接口超时次数增多,经排查直接原因是GC耗时过长,查看监控FullGC达500ms以上,接口超时时间与FullGC发生时间吻合。 图1 FullGC耗时监控 二、应用基本情况 容器:8C12G; JVM配置:-XX:+UseConcMarkSweepGC -Xms6144m -Xmx6144m -Xmn2048m -XX:ParallelGCThreads=8 -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+ParallelRefProcEnabled; 数据库类型:MySQL; 数据库连接池:DBCP; 三、排查过程 1、 GC耗时过长,说明内存中垃圾对象很多。 2、 首先怀疑是否有内存泄漏,观察FullGC后堆内存回收情况,尚属正常,暂时排除内存泄漏原因。 图2 发生FullGC后堆内存回收监控 3、 推断FullGC耗时过长是否因为老年代有大量死亡对象,遂导出FullGC前后堆内存dump,通过比对“保留大小”,发现FullGC后大量数据库相关对象被回收。 图3 堆内存对象分析 4、 数据库连接正常应该不会频繁创建和断开,进入老年代后,正常不应该被回收,通过堆dump内容OQL分析每个数据库连接数量,发现很多库连接数都大于“maxActive”数量,可以肯定有很多失效连接。 5、 初步判断直接原因是很多失效数据库连接进入老年代,导致FullGC耗时过长。 6、 怀疑连接池验证周期过长,导致数据库因空闲过长关闭连接,将连接池参数“ timeBetweenEvictionRunsMillis”由1分钟调整到10秒,问题依旧。 7、 阅读DBCP源码,发现是通过 org.apache.commons.pool.impl.GenericObjectPool.Evictor定时任务,按照timeBetweenEvictionRunsMillis配置的周期定时驱逐失效连接,驱逐条件:若连接空闲时间大于“minEvictableIdleTimeMillis”,则会驱逐连接,等待垃圾回收。若开启“testWhileIdle”则会执行“validationQuery”。进一步阅读代码,发现执行“validationQuery”后,连接空闲时间并不会重新计算,导致连接在业务低谷时很容易被淘汰,而数据库连接会关联大量对象,创建、回收成本昂贵,并且影响GC。 8、 反向思考,为何只有在大促期间才发生问题? 图4 平时和大促时回收频率对比 可以看到平时由于业务量小,GC不频繁,过期连接没有达到进入老年代阈值,在年轻代被回收。而大促时业务量大,GC频繁,连接在进入老年代以后才过期,导致老年代FullGC时间过长。 9、 至此,基本可以肯定问题原因是数据库连接池不具备“保活”能力,导致连接不断淘汰和新建,在业务高峰时段,连接进入老年代然后失效,造成FullGC耗时过长,最终导致接口超时次数增多。 四、解决方案 方案1:改为G1回收器,对老年代回收是分块进行,可以防止长时间停顿。另外默认MaxTenuringThreshold值是15,可以防止失效连接过早进入老年代; 方案2: minEvictableIdleTimeMillis设置为0,使数据库连接不会自动失效,进入老年代以后一直存活,避免在老年代失效回收; 五、问题总结 数据库连接池并不具备通常理解的“保活”能力,数据库连接在业务不活跃的应用中,会不断淘汰和重连,而连接会通过虚引用方式( com.mysql.jdbc.NonRegisteringDriver$ConnectionPhantomReference)携带大量对象,如果连接存活时间内YGC次数达到寿命阈值,则会进入老年代,老年代是使用“标记-清除”算法,回收成本更高,进而造成FullGC耗时过长。 六、拓展知识点 1、 Druid连接池同样存在不能“保活”问题,较新版本提供“KeepAlive”选项(未验证); 2、 Druid连接池配置的“validationQuery”语句通常并不会被执行,MySqlValidConnectionChecker在检查连接有效性时,会判断驱动是否实现pingInternal方法,如果实现则会通过此方法验证有效性。MySQL的JDBC驱动实现了该方法,因此“validationQuery”配置的语句通常不会执行; 图5 连接有效性校验代码 3、 DBCP和Druid连接池默认都是FILO,如果业务不繁忙,会导致只有最前边的连接被使用-归还-使用,后边连接基本都在无谓的驱逐、重建连接; 4、 虚引用对GC的影响:这些引用只有经过两次GC才能被回收掉,如果进入老年代,则必须经过两次FullGC才能释放内存。本例中由于不断有新的虚引用对象在老年代失效,导致FullGC后,内存水位仍然偏高,会加剧GC压力。新版本JVM已对此做了优化,一次GC可以回收掉; 5、 类似的影响还有finalize方法; 6、 CMS回收器默认MaxTenuringThreshold为6,而ParallelGC和G1均默认15; 结语 本文对数据库连接失效引起的GC问题进行了详细分析,希望读者通过本文对数据库连接“保活”机制、GC问题基本分析方法有所收益,后续该系列文章会继续推出其他案例分享。 作者:京东零售 王利辉 内容来源:京东云开发者社区

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

【实践篇】教你玩转JWT认证---从一个优惠券聊起 | 京东云技术团队

引言 最近面试过程中,无意中跟候选人聊到了JWT相关的东西,也就联想到我自己关于JWT落地过的那些项目。 关于JWT,可以说是分布式系统下的一个利器,我在我的很多项目实践中,认证系统的第一选择都是JWT。它的优势会让你欲罢不能,就像你领优惠券一样。 大家回忆一下一个场景,如果你和你的女朋友想吃某江家的烤鱼了,你会怎么做呢? 传统的时代,我想场景是这样的:我们走进一家某江家餐厅,会被服务员引导一个桌子,然后我们开始点餐,服务原会记录我们点餐信息,然后在送到后厨去。这个过程中,那个餐桌就相当于session,而我们的点餐信息回记录到这个session之中,然后送到后厨。这个是一个典型的基于session的认证过程。但我们也发现了它的弊端,就是基于session的这种认证,对服务器强依赖,而且信息都是存储在服务器之上,灵活性和扩展性大大降低。 而互联网时代,大众点评、美团、饿了么给了我们另一个选择,我们可能第一时间会在这些平台上搜索江边城外的优惠券,这个优惠券中可能会描述着两人实惠套餐明细。这张优惠券就是我们的 JWT,我们可以在任何一家有参与优惠活动的餐厅使用这张优惠券,而不必被限制在同一家餐厅。同时这张优惠券中直接记录了我们的点餐明细,等我们到了餐厅,只需要将优惠券二维码告知服务员,服务员就会给我们端上我们想要的食物。 好了,以上只是一个小例子,其实只是想说明一下JWT相较于传统的基于session的认证框架的优势。 JWT 的优势在于它可以跨域、跨服务器使用,而 Session 则只能在本域名下使用。而且,JWT 不需要在服务端保存用户的信息,只需要在客户端保存即可,这减轻了服务端的负担。 这一点在分布式架构下优势还是很明显的。 什么是JWT 说了这么多,如何定义JWT呢? JWT(JSON Web Token)是一种用于在网络应用中进行身份验证的开放标准(RFC7519)。它可以安全地在用户和服务器之间传输信息,因为它使用数字签名来验证数据的完整性和真实性。 JWT包含三个部分:头部、载荷和签名。头部包含算法和类型信息,载荷包含用户的信息,签名用于验证数据的完整性和真实性。 额外说一下poload,也就是负荷部分,这块是jwt的核心模块,它内部包括一些声明(claims)。声明由三个类型组成: Registered Claims:这是预定义的声明名称,主要包括以下几种: iss:Token 发行者 sub:Token 主题 aud:Token的受众 exp:Token 过期时间 iat:Token发行时间 jti:Token唯一标识符 Public Claims:公共声明是自己定义的声明名称,以避免冲突。 Private Claims:私有声明与公共声明类似,不同之处在于它是用于在双方之间共享信息的。 当用户登录时,服务器将生成一个JWT,并将其作为响应返回给客户端。客户端将在后续的请求中发送此JWT。服务器将使用相同的密钥验证JWT的签名,并从载荷中获取用户信息。如果签名验证通过并且用户信息有效,则服务器将允许请求继续进行。 JWT优点 JWT优点如果我们系统的总结一下, 如下: 跨语言和平台:JWT是基于JSON标准的,因此可以在不同的编程语言和平台之间进行交换和使用。无状态:由于JWT包含所有必要的信息,服务器不需要在每个请求中存储任何会话数据,因此可以轻松地进行负载均衡。 安全性:JWT使用数字签名来验证数据的完整性和真实性,因此可以防止数据被篡改或伪造。 可扩展性:JWT可以包含任何用户信息,因此可以轻松地扩展到其他应用程序中。 一个基于JWT认证的方案 我将举一个我实际业务落地的一个例子。 我的业务场景中一般都会有一个业务网关,该网关的核心功能就是鉴权和上线文转换。用户请求会将JWT字符串存与header之中,然后到网关后进行JWT解析,解析后的上下文信息,会转变成明文K-V的方式在此存于header之中,供系统内部各个微服务之间互相调用时提供明文上下文信息。具体时序图如下: 基于Spring security的JWT实践 JWT原理很简单,当然,你可以完全自己实现JWT的全流程,但是,实际中,我们一般不需要这么干,因为有很多成熟和好用的轮子提供给我们,而且封装性和安全性也远比自己匆忙的封装一个简单的JWT来的高。 如果是基于学习JWT,我是建议大家自己手写一个demo的,但是如果重实践的角度触发,我们完全可以使用Spring Security提供的JWT组件,来高效快速的实现一个稳定性和安全性都非常高的JWT认证框架。 以下是我基于我的业务实际情况,根据保密性要求,简化了的JWT实践代码。也算是抛砖引玉,希望可以给大家在业务场景中运用JWT做一个参考。 maven依赖 首先,我们需要添加以下依赖到pom.xml文件中: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> JWT工具类封装 然后,我们可以创建一个JwtTokenUtil类来生成和验证JWT令牌: import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; @Component public class JwtTokenUtil { private static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60; @Value("${jwt.secret}") private String secret; public String generateToken(UserDetails userDetails) { Map<String, Object> claims = newHashMap <>(); return createToken(claims, userDetails.getUsername()); } private String createToken(Map<String, Object> claims, String subject) { Date now = new Date(); Date expiration = new Date(now.getTime() + JWT_TOKEN_VALIDITY * 1000); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(now) .setExpiration(expiration) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public boolean validateToken(String token, UserDetails userDetails) { final String username = extractUsername(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } public Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private Claims extractAllClaims(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } } 在这个实现中,我们使用了jjwt库来创建和解析JWT令牌。我们定义了以下方法: generateToken:生成JWT令牌。 createToken:创建JWT令牌。 validateToken:验证JWT令牌是否有效。 isTokenExpired:检查JWT令牌是否过期。 extractUsername:从JWT令牌中提取用户名。 extractExpiration:从JWT令牌中提取过期时间。 extractClaim:从JWT令牌中提取指定的声明。 extractAllClaims:从JWT令牌中提取所有声明。 UserDetailsService类定义 接下来,我们可以创建一个自定义的UserDetailsService,用于验证用户登录信息: import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service public class JwtUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserEntity user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("User not found with username: " + username); } return new User(user.getUsername(), user.getPassword(), new ArrayList<>()); } } 在这个实现中,我们使用了UserRepository来检索用户信息。我们实现了UserDetailsService接口,并覆盖了loadUserByUsername方法,以便验证用户登录信息。 JwtAuthenticationFilter定义 接下来,我们可以创建一个JwtAuthenticationFilter类,用于拦截登录请求并生成JWT令牌: import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collections; import java.util.Date; public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private final AuthenticationManager authenticationManager; private final JwtTokenUtil jwtTokenUtil; public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil) { this.authenticationManager = authenticationManager; this.jwtTokenUtil = jwtTokenUtil; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { LoginRequest loginRequest = new ObjectMapper().readValue(request.getInputStr eam(), LoginRequest.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword(), Collections.emptyList()) ); } catch (IOException e) { throw new RuntimeException(e); } } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)throwsIOException,ServletException { UserDetails userDetails = (UserDetails) authResult.getPrincipal(); String token = jwtTokenUtil.generateToken(userDetails); response.addHeader("Authorization", "Bearer " + token); } private static class LoginRequest { private String username; private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } } 在这个实现中,我们继承了 UsernamePasswordAuthenticationFilter类,并覆盖了attemptAuthentication和successfulAuthentication方法,以便在登录成功时生成JWT令牌并将其添加到HTTP响应头中。 Spring Security配置类 最后,我们可以创建一个Spring Security配置类,以便配置验证和授权规则: import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtUserDetailsService jwtUserDetailsService; @Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired private JwtTokenUtil jwtTokenUtil; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests().antMatchers("/authenticate").permitAll() .anyRequest().authenticated().and() .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(newJwtAuthenticationFilter(authenticationManager(), jwtTokenUtil), UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder()); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } 在这个实现中,我们使用JwtUserDetailsService来验证用户登录信息,并使用 JwtAuthenticationEntryPoint来处理验证错误。 我们还配置了JwtAuthenticationFilter来生成JWT令牌,并将其添加到HTTP响应头中。我们还定义了一个PasswordEncoderbean,用于加密用户密码。 调试接口验证 现在,我们可以向/authenticate端点发送POST请求,以验证用户登录信息并生成JWT令牌。例如: bash curl -X POST \ http://localhost:8080/authenticate \ -H 'Content-Type: application/json'\ -d '{ "username": "user", "password": "password" }' 如果登录信息验证成功,将返回一个带有JWT令牌的HTTP响应头。我们可以使用这个令牌来访问需要授权的端点。例如: bash curl -X GET \ http://localhost:8080/hello \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjI0MDM2NzA4LCJleHAiOjE2MjQwMzc1MDh9.9fZS7jPp0NzB0JyOo4y4jO4x3s3KjV7yW1nLzV7cO_c' 在这个示例中,我们向/hello端点发送GET请求,并在HTTP头中添加JWT令牌。如果令牌有效并且用户有权访问该端点,则返回一个成功的HTTP响应。 总结 JWT是一种简单、安全和可扩展的身份验证机制,适用于各种应用程序和场景。它可以减少服务器的负担,提高应用程序的安全性,并且可以轻松地扩展到其他应用程序中。 但是JWT也有一定的缺点,比如他的payload模块并没有明确说明一定要加密传输,所以当你没有额外做一些安全性措施的情况下,jwt一旦被别人截获,很容易泄漏用户信息。所以,如果要增加JWT的在实际项目中的安全性,安全加固措施必不可少,包括加密方式,秘钥的保存,JWT的过期策略等等。 当然实际中的认证鉴权框架不止有JWT,JWT只是解决了用户上下文传输的问题。实际项目中经常是JWT结合其他认证系统一同使用,比如OAuth2.0。这里篇幅有限,就不展开。以后有机会再单独写一篇关于OAuth2.0认证架构的文章。 作者:京东物流 赵勇萍 内容来源:京东云开发者社区

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

【问题排查篇】一次业务问题对 ES 的 cardinality 原理探究 | 京东云技术团队

作者:京东科技 王长春 业务问题 小编工作中负责业务的一个服务端系统,使用了 Elasticsearch 服务做数据存储,业务运营人员反馈,用户在使用该产品时发现,用户后台统计的订单笔数和导出的订单笔数不一致! 交易订单笔数不对,出现差错订单了?这一听极为震撼!出现这样的问题,在金融科技公司里面是绝对不允许发生的,得马上定位问题并解决! 小编马上联系业务和相关人员,通过梳理上游系统的调用关系,发现业务系统使用到的是我这边的 ES 的存储服务,然后对线上情况进行复现,基本了解问题的现象: 用户操作后台里的订单总笔数:商户页面的"订单总笔数","订单总笔数"使用的是小编 ES 存储服务中 ES 的统计聚合功能,其中订单总笔数是使用了 cardinality 操作,并且使用的是 orderId(订单编号)进行统计去重。 导出功能里的订单总笔数:导出功能使用的是 ES 存储服务中的 ES 条件查询功能,导出功能是进行分页查询的。 问题定位 这两个查询数量不一致,首先看查询条件是否一致呢? 经过一番排查,业务系统在调用查询订单总数和导出订单总数的这两个查询条件是一致的,也就是请求到我这边 ES 服务时,统计聚合的查询和分页导出的查询条件是一致的,但是为什么会在 ES 里面查询的结果是不一致的呢?难道 ES 里面的数据不全?统计聚合或分页导出的其中有一个不准了? 为了具体排查哪个操作可能存在问题,于是通过相同条件下查询数据库的总数和 ES 里面的数据进行对比。发现相同条件下,数据库里面的数据和 ES 条件查询的总数是一致的, 同时业务的 orerId 字段是没有重复,所以可以确定的是:通过 orderId 进行统计聚合去重的操作是有问题的。 数据库查询:数据库是做分库分表,此处数据库查询使用的是公司内的数据部银河大表——公司数据部会 T+1日从业务从库数据库中抽取 T 日的增量数据放在建立的"大表"中, 方便各业务进行数据使用。 运营后台查询:运营后台查询是直接查询 ES 存储服务。 数据部大表数量 = MySQL 数据库分库分表表里数量 = 运营控制台查询数量 = ES 存储文档数量 问题定位:ES 存储服务对外给业务提供的: 通过 orderId 进行统计聚合去重(cardinality)的功能应该是有问题的。 ES 的 cardinality 原理探究 上面说过,小编负责的 ES 存储服务对外给业务提供了通过指定业务字段进行统计聚合去重的功能,统计聚合去重使用的是 ES 的 cardinality 功能。通过业务的查询的条件,使用 ES 的聚合功能 cardinality 操作,映射到 ES 层的操作命令如下代码所示, 执行业务的查询条件操作,从 ES 的管理端后台里面查询竟然复现了和线上生产一样的结果,聚合统计的是 21514,条件查询的是 21427!!! 可以确定的就是这个 cardinality 操作,导致了两个查询的数据不一致,如下图所示: GET datastore_big_es_1_index/datastore_big_es_1_type/_search { "size": 3, "query": { "bool": { "must": [ { "match": { "v021.raw": "selfhelp" } }, { "match": { "v012.raw": "1001" } }, { "match": { "typeId": "00029" } }, { "range": { "createdDate": { "gte": "2021-02-01", "lt": "2021-03-01" } } }, { "bool": { "should": [ { "match": { "v031.raw": "113692300" } } ] } } ] } }, "aggs": { "distinct_orderId": { "cardinality": { "field": "v033.raw" } } } } 为什么 cardinality 操作会出现这样的结果呢? 小编开始陷入了想当然的陷阱—— 以为这就是一个简简单单的统计去重的功能,ES 做的多好,帮你去重并统计数量了。然后事实并不是,通过 Elasticsearch 对 cardinality 官方文档解释,终于找到了原因。 可以参考Elasticsearch 2.x 版本官方文档对 cardinality的解释:cardinality 其中对 cardinality 算法核心解释是: 可以总结如下: cardinality 并不是像关系型数据库 MySQL 一样精确去重的,cardinality做的是一个近似值,是 ES 帮你"估算"出的,这个估算使用的HyperLogLog++(HLL)算法,在速度上非常快,遍历一次即可统计去重,具体可看文档中推荐的论文。 ES 做cardinality估算,是可以设置估算精确度,即设置参数 precision_threshold 参数,但是这个参数在 0-40000, 这个值越大意味着精度越高,同时意味着损失更多的内存,是以内存空间换精度。 在小数据量下,ES 的这个"估算"精度是非常高的,几乎可以说是等于实际数量。 ES 中 cardinality 参数验证 下面对 ES 的 cardinality 的precision_threshold参数进行验证: 1、大数据量下,设置最高精度及其以上,仍然会存在误差: 2、小数据量下,设置最高精度,可以和实际数量保持一致: 那么线上的为什么聚合统计的是 21514,条件查询的是 21427? 线上代码运行和ES集群设置都没有主动设置过 precision_threshold 参数,那么可以知道,这个应该是 ES 集群设置的默认值。线上 ES 集群版本为 5.4x 因此找到 5.4 版本的官方文档,发现 5.4 版本中设置的是默认值 precision_threshold=3000, 在此条件下查询的统计聚合出来的值是 21514。 另外 ES 官方对 cardinality 操作中的precision_threshold参数也做了研究,研究了官方文档中precision_threshold设置和cardinality查询失败率、查询数据量级的关系,可作为我们在业务开发中进行参考,如下图所示: Elasticsearch 5.4版本官方文档对cardinality中precision_threshold参数的研究文档:precision_threshold 总结与方案 通过对 cardinality 的原理探究, 需要明白的是 : 我们使用 cardinality 是需要区分使用场景的。 对于精确统计的业务场景,是不建议使用的。例如:订单数的统计(统计结果会引起歧义)的场景下,不建议使用。 **对于非精确统计的业务场景,那么可以说是很有用了,尤其是在大数据量的场景下,在保持一定的准确性下,同时能提供高性能。**例如:监控指标数据,大盘比例计算等场景,在非精确统计下,是有很大用处。 基于小编的这个业务场景,对商户订单进行统计,是属于精确统计场景,那 cardinality 操作就不适合了。又因为业务的 orderId 是不会重复的,理论上在我们 ES 集群中每个记录的 orderId 都是唯一的,因此可以不用进行去重,而可以直接使用 ES 的 count 操作,将订单数统计汇总出,对应 Elasticsearch 开发包中 COUNT API 如下: org.springframework.data.elasticsearch.core.ElasticsearchTemplate #count(org.springframework.data.elasticsearch.core.query.SearchQuery, java.lang.Class<T>) public <T> long count(SearchQuery searchQuery, Class<T> clazz) { QueryBuilder elasticsearchQuery = searchQuery.getQuery(); QueryBuilder elasticsearchFilter = searchQuery.getFilter(); return elasticsearchFilter == null ? this.doCount(this.prepareCount(searchQuery, clazz), elasticsearchQuery) : this.doCount(this.prepareSearch(searchQuery, clazz), elasticsearchQuery, elasticsearchFilter); } 最后欢迎大家点赞、收藏、评论,转发!❤️❤️❤️

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

关于聚合根,领域事件的那点事---深入浅出理解DDD | 京东云技术团队

作者:京东物流 赵勇萍 前言 最近有空会跟同事讨论DDD架构的实践落地的情况,但真实情况是,实际中对于领域驱动设计中的实体,值对象,聚合根,领域事件这些战术类的实践落地,每个人理解依然因人而异,大概率是因为这些概念还是有一些抽象,同时有有别于传统的MVC架构开发。 在此,通过小demo的方式跟大家分享一下我对DDD中战术层级的理解,算是抛砖引玉,该理解仅代表我个人在现阶段的一个理解,也可能未来随着业务经验深入,还会有不同的理解。 既然说是小demo,还是要从业务场景出发,也就是我最熟知的电商业务场景说起。但是该篇文章里, 我会简化一些实际业务场景中的复杂度,通过最小颗粒度的demo,来反映实践过程中的基本问题。 一个简单的demo业务场景 话不多说,我先抛出我自己假设的一个业务场景,就是我们熟知的电商网站下单购物的场景。具体细节如下: 1. 实体: • 商品:拥有唯一标识、名称、价格、库存等属性。 • 订单:拥有唯一标识、下单时间、状态等属性。订单包含多个订单项。 2. 值对象: • 地址:拥有省、市、区、详细地址等属性。 3. 领域事件: • 订单创建事件:当用户下单时触发该事件,包含订单信息、商品信息等数据。 • 订单支付事件:当用户完成支付时触发该事件,包含订单信息、支付金额等数据。 • 订单发货事件:当商家发货时触发该事件,包含订单信息、快递公司、快递单号等数据。 4. 聚合根: • 商品聚合根:包含商品实体和相关的值对象,负责商品的创建、修改、查询等操作。 • 订单聚合根:包含订单实体和相关的值对象,负责订单的创建、修改、查询等操作。 5. 对外接口服务: • 创建订单接口:用户提交购买请求后,系统创建相应的订单,并触发订单创建事件。 • 支付订单接口:用户完成支付后,系统更新订单状态,并触发订单支付事件。 • 发货接口:商家发货后,系统更新订单状态,并触发订单发货事件。 • 查询订单接口:用户可以根据订单号等条件查询自己的订单信息。 该demo中,商品和订单是两个核心领域概念,分别由对应的聚合根负责管理。同时,通过定义领域事件,实现了不同业务场景下的数据更新和通知。最后,对外提供了一组简单的接口服务,方便系统的使用和扩展。 demo的java代码实现 好了,有了以上我们对业务场景的充分剖析,确定了子域,接下来我们该写我们的代码。 1. 商品实体类: // 省略getter/setter方法 public class Product { private Long id; private String name; private BigDecimal price; private Integer stock; } 2. 订单实体类 // 省略getter/setter方法 public class Order { private Long id; private LocalDateTime createTime; private Integer status; private List orderItems; } 3. 订单项实体类 // 省略getter/setter方法 public class OrderItem { private Long id; private Product product; private Integer quantity; private BigDecimal price; } 4. 地址值对象 // 省略getter/setter方法 public class Address { private String province; private String city; private String district; private String detail; } 5. 领域事件类 //订单创建领域事件 public class OrderCreatedEvent { private Order order; private List orderItems; public OrderCreatedEvent(Order order, List orderItems) { this.order = order; this.orderItems = orderItems; } } //订单支付领域事件 public class OrderPaidEvent { private Order order; private BigDecimal amount; public OrderPaidEvent(Order order, BigDecimal amount) { this.order = order; this.amount = amount; } } //订单 public class OrderShippedEvent { private Order order; private String expressCompany; private String expressNo; public OrderShippedEvent(Order order, String expressCompany, String expressNo) { this.order = order; this.expressCompany = expressCompany; this.expressNo = expressNo; } } 6. 商品聚合根 public class ProductAggregate { private ProductService productService; public void createProduct(Product product) { productService.create(product); } public void updateProduct(Product product) { productService.update(product); } public void deleteProduct(Long productId) { productService.delete(productId); } public Product getProductById(Long productId) { return productService.getById(productId); } } 7. 订单聚合根 public class OrderAggregate { private OrderService orderService; public void createOrder(Order order, List orderItems) { orderService.create(order); // 触发订单创建事件 DomainEventPublisher.publish(new OrderCreatedEvent(order, orderItems)); } public void payOrder(Long orderId, BigDecimal amount) { orderService.pay(orderId, amount); // 触发订单支付事件 DomainEventPublisher.publish(new OrderPaidEvent(orderService.getById(orderId), amount)); } public void shipOrder(Long orderId, String expressCompany, String expressNo) { orderService.ship(orderId, expressCompany, expressNo); // 触发订单发货事件 DomainEventPublisher.publish(new OrderShippedEvent(orderService.getById(orderId), expressCompany, expressNo)); } public Order getOrderById(Long orderId) { return orderService.getById(orderId); } } 总结 通过以上demo,对于实体和值对象,大家会很好理解,并且很直观。但是, 我额外想重点解释一下聚合根和领域事件的概念 1. 聚合根 从上面的demo可以看出,在合根类中,我们定义了商品和订单的增、删、查等操作,并且为订单定义了创建订单、支付订单、发货等业务逻辑代码。 聚合根是一个对象,它代表一组相关联的对象的整体。在聚合根内部,可以包含多个实体对象和值对象。聚合根通常可以通过唯一标识符来进行识别和访问。它是整个聚合的管理者,负责维护聚合之内的一致性,并协调各个实体对象之间的关系。聚合根通常具有丰富的行为和操作,可以对聚合内部的对象进行复杂的操作。 所以说,真正的聚合根内的方法是基于充血模型封装的,而不是仅仅是对对象的数据封装。在聚合根中,对象不仅封装了数据,还包含了相应的行为和业务逻辑。这意味着在一个聚合根中,对象可以自己处理自己的业务逻辑,而不需要外部的控制。就如同demo中所写的那样,订单对象可能包含一些关于订单处理和交付的方法,如确认订单、取消订单、发货等。 2. 领域事件 领域事件是DDD中最重要的概念之一,他是解决子域之间耦合的重要手段,因为它们提供了一种将领域概念和业务语言转化为代码的方法。当一个领域事件发生时,它会触发一些操作,这些操作可能会更改系统的状态,也可能会导致其他领域事件的发生。通过对领域事件进行建模,我们可以更好地了解业务过程并设计出更加符合实际需求的系统。 在DDD中,领域事件通常由三个部分组成: 1. 事件名称:这个名称应该能够简洁明了地描述事件所代表的业务意义。 2. 相关数据:这些数据包含了事件发生时与事件相关的所有信息。例如,在一个电子商务系统中,如果订单被提交,则订单信息以及买家和卖家的信息都应该包括在该事件中。 3. 发送者和接收者:发送者通常是触发事件的对象,接收者则是事件处理的对象。 领域事件在DDD中有很多用途。例如,它们可以用来触发其他业务流程、更新数据库或通知其他子系统。它们还可以用于解决一些复杂的业务逻辑问题,例如并发、数据同步和错误处理等等。 总之,领域事件是DDD架构中非常重要的概念,它可以帮助我们更好地理解业务过程,设计出更加符合实际需求的系统,并提高系统的可维护性和可扩展性。

资源下载

更多资源
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文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册