php做网站难么,怎么做购物平台网站,河南建设安全监督网站,小学门户网站建设情况汇报摘要#xff1a; 标签 PostgreSQL , 分区表 , bind , spin lock , 性能分析 , sleep 进程 , CPU空转 , cache 背景 实际上我写过很多文档#xff0c;关于分区表的优化#xff1a; 《PostgreSQL 商用版本EPAS(阿里云ppas) - 分区表性能优化 (堪比pg_pathman)》 《PostgreSQL … 摘要 标签 PostgreSQL , 分区表 , bind , spin lock , 性能分析 , sleep 进程 , CPU空转 , cache 背景 实际上我写过很多文档关于分区表的优化 《PostgreSQL 商用版本EPAS(阿里云ppas) - 分区表性能优化 (堪比pg_pathman)》 《PostgreSQL 传统 hash 分区方法和性能》 《PostgreSQL 10 内置分区 vs pg_pathman perf profiling》 实际上native分区表的性能问题主要还是在于分区表过多的时候执行计划需要耗时很久。 点此查看原文https://yq.aliyun.com/articles/405176?spma2c4e.11153959.teamhomeleft.44.8WKxt7 实际上native分区表的性能问题主要还是在于分区表过多的时候执行计划需要耗时很久。 因此有了 1、PPAS的edb_enable_pruning参数可以在生成执行计划前使用简单SQL的话直接过滤到目标分区从而不需要的分区不需要进入执行计划的环节。 2、pg_pathman则支持的更全面除了简单SQL复杂的表达式immutable都可以进行过滤过滤到目标分区从而不需要的分区不需要进入执行计划的环节。 因分区表过多引发的问题通常出现在OLTP系统主要是OLTP系统的并发高更容易把这种小问题放大本来一次请求只需要1毫秒的但是执行计划可能需要上百毫秒也就是说执行耗时变成了小头而执行计划SPIN LOCK变成了大头。 下面这个例子也是OLTP系统相关的有具体的原因分析。 SQL访问的分区表过多并发高时CPU负载高但是大量的是SLEEP状态的BIND进程。 某个业务系统单次SQL请求很快几十毫秒但是并发一高QPS并没有线性的增长。 而且大量的进程处于BINDSLEEP的状态。 经过诊断 《PostgreSQL 源码性能诊断(perf profiling)指南》 《Linux 性能诊断 perf使用指南》 主要的原因是大量的SPIN LOCK导致CPU空转。
perf record -ag perf report -g 比如某个进程BIND时的pstack
#pstack 18423
#0 0x00002ad051f3ef67 in semop () from /lib64/libc.so.6 -- 这边到了内核上spin lock
#1 0x0000000000656117 in PGSemaphoreLock ()
#2 0x00000000006c274a in LWLockAcquire ()
#3 0x00000000006bd136 in LockAcquireExtended ()
#4 0x00000000006b8768 in LockRelationOid () -- 对所有的子表都会调用这个函数导致spinlock
#5 0x000000000050c10a in find_inheritance_children ()
#6 0x000000000050c212 in find_all_inheritors () -- 找到所有子表
#7 0x0000000000645e4e in expand_inherited_tables ()
#8 0x000000000063a6e8 in subquery_planner ()
#9 0x0000000000618c4f in set_rel_size ()
#10 0x0000000000618e7c in set_rel_size ()
#11 0x0000000000619587 in make_one_rel ()
#12 0x0000000000636bd1 in query_planner ()
#13 0x000000000063862c in grouping_planner ()
#14 0x000000000063a9c4 in subquery_planner ()
#15 0x0000000000618c4f in set_rel_size ()
#16 0x0000000000619587 in make_one_rel ()
#17 0x0000000000636bd1 in query_planner ()
#18 0x000000000063862c in grouping_planner ()
#19 0x000000000063a9c4 in subquery_planner ()
#20 0x0000000000618c4f in set_rel_size ()
#21 0x0000000000619587 in make_one_rel ()
#22 0x0000000000636bd1 in query_planner ()
#23 0x000000000063862c in grouping_planner ()
#24 0x000000000063b0d0 in standard_planner ()
#25 0x00000000006d1597 in pg_plan_queries ()
#26 0x00000000007ca156 in BuildCachedPlan ()
#27 0x00000000007ca525 in GetCachedPlan ()
#28 0x00000000006d1d07 in exec_bind_message ()
#29 0x00000000006d44de in PostgresMain ()
#30 0x000000000066bd5f in PostmasterMain ()
#31 0x00000000005f474c in main () 由于业务使用了prepared statement所以过程会变成bind 过程 1、prepare statement 2、bind parameters 3、代入参数、(设置了constraint_exclusion时)判断哪些分区需要被过滤 4、execute prepared statement 在find_all_inheritors过程中涉及的分区表过多最后每个分区都要取LOCK后面加载了系统的spin lock所以我们会看到CPU很高同时大量的BIND进程处于SLEEP状态也就是CPU空转CPU时间片被独占的状态。 spinlock (自旋锁) 自旋锁是专为防止多处理器并发而引入的一种锁它在内核中大量应用于中断处理等部分(对于单处理器来说防止中断处理中的并发可简单采用关闭中断的方式不需要自旋锁)。 自旋锁最多只能被一个内核任务持有如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁那么这个任务就会一直进行忙循环——旋转——等待锁重新可用。 要是锁未被争用请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。 事实上自旋锁的初衷就是 在短期间内进行轻量级的锁定。一个进程去获取被争用的自旋锁时请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间)所以自旋锁不应该被持有时间过长等待时CPU被独占。如果需要长时间锁定的话, 最好使用信号量睡眠CPU资源可出让 。 简单的说自旋锁在内核中主要用来防止多处理器中并发访问临界区防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁——因为睡眠有可能造成持有锁的内核任务被重新调度而再次申请自己已持有的锁)它能够在中断上下文使用。 死锁假设有一个或多个内核任务和一个或多个资源每个内核都在等待其中的一个资源但所有的资源都已经被占用了。这便会发生所有内核任务都在相互等待但它们永远不会释放已经占有的资源于是任何内核任务都无法获得所需要的资源无法继续运行这便 意味着死锁发生了。自死琐是说自己占有了某个资源然后自己又申请自己已占有的资源显然不可能再获得该资源因此就自缚手脚了。 spinlock特性 防止多处理器并发访问临界区 1、非睡眠该进程/LWP(Light Weight Process)始终处于Running的状态 2、忙等 cpu一直检测锁是否已经被其他cpu释放 3、短期低开销加锁 4、适合中断上下文锁定 5、多cpu的机器才有意义需要等待其他cpu释放锁 以下截取自 http://blog.sina.com.cn/s/blog_458d6ed5010110hv.html Spinlock的目的是用来同步SMP中会被多个CPU同时存取的变量。在Linux中普通的spinlock由于不带额外的语义是用起来反而要非 常小心。 在Linux kernel中执行的代码大体分normal和interrupt context两种。tasklet/softirq可以归为normal因为他们可以进入等待 Spinlock的目的是用来同步SMP中会被多个CPU同时存取的变量。在Linux中普通的spinlock由于不带额外的语义是用起来反而要非常小心。 在Linux kernel中执行的代码大体分normal和interrupt context两种。tasklet/softirq可以归为normal因为他们可以进入等待nested interrupt是interrupt context的一种特殊情况当然也是interrupt context。Normal级别可以被interrupt抢断interrupt会被另一个interrupt抢断但不会被normal中断。各个 interrupt之间没有优先级关系只要有可能每个interrupt都会被其他interrupt中断。 我们先考虑单CPU的情况。在这样情况下不管在什么执行级别我们只要简单地把CPU的中断关掉就可以达到独占处理的目的。从这个角度来说spinlock的实现简单地令人乍舌cli/sti。只要这样我们就关闭了preemption带来的复杂之门。 单CPU的情况很简单多CPU就不那么简单了。单纯地关掉当前CPU的中断并不会给我们带来好运。当我们的代码存取一个shared variable时另一颗CPU随时会把数据改得面目全非。我们需要有手段通知它(或它们你知道我的意思)——spinlock正为此设。这个例子是 我们的第一次尝试
extern spinlock_t lock;
// ...
spin_lock(lock);
// do something
spin_unlock(lock); 他能正常工作吗答案是有可能。在某些情况下这段代码可以正常工作但想一想会不会发生这样的事
// in normal run level
extern spinlock_t lock;
// ...
spin_lock(lock);
// do something
// interrupted by IRQ ... // in IRQ
extern spinlock_t lock;
spin_lock(lock); 喔我们在normal级别下获得了一个spinlock正当我们想做什么的时候我们被interrupt打断了CPU转而执行interrupt level的代码它也想获得这个lock于是“死锁”发生了解决方法很简单看看我们第二次尝试
extern spinlock_t lock;
// ...
cli; // disable interrupt on current CPU
spin_lock(lock);
// do something
spin_unlock(lock);
sti; // enable interrupt on current CPU 在获得spinlock之前我们先把当前CPU的中断禁止掉然后获得一个lock;在释放lock之后再把中断打开。这样我们就防止了死锁。事实上Linux提供了一个更为快捷的方式来实现这个功能:
extern spinlock_t lock;
// ...
spin_lock_irq(lock);
// do something
spin_unlock_irq(lock); 如果没有nested interrupt所有这一切都很好。加上nested interrupt我们再来看看这个例子:
// code 1
extern spinlock_t lock;
// ...
spin_lock_irq(lock);
// do something
spin_unlock_irq(lock); // code 2
extern spinlock_t lock;
// ...
spin_lock_irq(lock);
// do something
spin_unlock_irq(lock); Code 1和code 2都运行在interrupt context下由于中断可以嵌套执行我们很容易就可以想到这样的运行次序
Code 1
extern spinlock_t lock;
// ...
spin_lock_irq(lock); Code 2
extern spinlock_t lock;
// ...
spin_lock_irq(lock);
// do something
spin_unlock_irq(lock); Code 1
// do something
spin_unlock_irq(lock); 问题是在第一个spin_unlock_irq后这个CPU的中断已经被打开“死锁”的问题又会回到我们身边 解决方法是我们在每次关闭中断前纪录当前中断的状态然后恢复它而不是直接把中断打开。
unsigned long flags;
local_irq_save(flags);
spin_lock(lock);
// do something
spin_unlock(lock);
local_irq_restore(flags); Linux同样提供了更为简便的方式:
unsigned long flags;
spin_lock_irqsave(lock, flags);
// do something
spin_unlock_irqrestore(lock, flags); 小结 优化方法 1、假设我们的QUERY进程要查询多个分区指很多个分区那么建议把分区的粒度降低尽量让QUERY减少真正被访问的分区数从而减少LWLockAcquire次数。 2、如果我们的分区很多但是通过QUERY的WHERE条件过滤后实际被访问的分区不多那么分区表的选择就非常重要。目前尽量不要使用NATIVE分区。尽量使用PPAS的edb_enable_pruning。对于PostgreSQL社区版本用户在社区优化这部分代码前请尽量使用pg_pathman分区功能。 扫描二维码获取更多消息