5  lcnt - 锁性能分析器

5 lcnt - 锁性能分析器

在 Erlang 运行时系统内部,锁用于保护资源,防止它们以致命的方式从多个线程更新。锁对于确保运行时系统正常工作是必要的,但也带来了一些限制。锁争用和锁开销。

锁争用是指当一个线程锁定一个资源时,另一个线程或多个线程试图同时获取该资源。锁将拒绝其他线程访问资源,该线程将被阻止继续执行。第二个线程必须等到第一个线程完成对资源的访问并解锁它。该 lcnt 工具测量这些锁冲突。

锁在执行时间和内存空间方面存在固有成本。初始化、销毁、获取或释放锁都需要时间。为了减少锁争用,有时需要使用更细粒度的锁定策略。这通常也会增加锁开销,因此在锁争用和开销之间存在权衡。一般来说,锁争用随着并发运行的线程数量的增加而增加。该 lcnt 工具不测量锁开销。

为了调查模拟器中的锁,我们使用了一个名为 lcnt (lock-count 的缩写)的内部工具。需要启用此选项才能编译 VM。要编译包含锁计数功能的 VM 以及普通 VM,请使用

cd $ERL_TOP
./configure --enable-lock-counter

以这种方式启动锁计数 VM

$ERL_TOP/bin/erl -emu_type lcnt

要验证是否启用了锁计数,请检查 VM 启动时状态文本中是否出现 [lock-counting]

Erlang/OTP 20 [erts-9.0] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe]
 [kernel-poll:false] [lock-counting]

一旦您拥有启用了锁计数的 VM,就可以使用模块 lcnt。该模块旨在从当前运行的节点 shell 中使用。要访问远程节点,请使用 lcnt:clear(Node)lcnt:collect(Node)

所有锁都会被持续监控,其统计信息会不断更新。使用 lcnt:clear/0 在运行任何特定测试之前先清除所有计数器。此命令还将重置内部的持续时间计时器。

要检索锁统计信息,请使用 lcnt:collect/0,1。collect 操作将在尚未启动的情况下启动一个 lcnt 服务器。所有收集到的数据将被构建成一个 Erlang 项并上传到服务器,持续时间也会被上传。此持续时间是 lcnt:clear/0,1lcnt:collect/0,1 之间的时间。

数据收集到服务器后,可以通过多种方式进行过滤、排序和打印。

有关每个函数的描述,请参见 参考手册

从 Erlang shell

Erlang R13B03 (erts-5.7.4) [source] [smp:8:8] [rq:8] [async-threads:0] [hipe]
 [kernel-poll:false] [lock-counting]
1> lcnt:rt_opt({copy_save, true}).
false
2> lcnt:clear(), big:bang(1000), lcnt:collect().
ok
3> lcnt:conflicts().
                   lock   id  #tries  #collisions  collisions [%]  time [us]  duration [%]
                  -----  --- ------- ------------ --------------- ---------- -------------
         alcu_allocator   50 4113692       158921          3.8632     215464        4.4962
               pix_lock  256 4007140         4882          0.1218      12221        0.2550
              run_queue    8 2287246         6949          0.3038       9825        0.2050
              proc_main 1029 3115778        25755          0.8266       1199        0.0250
              proc_msgq 1029 2467022         1910          0.0774       1048        0.0219
            proc_status 1029 5708439         2435          0.0427        706        0.0147
 message_pre_alloc_lock    8 2008569          134          0.0067         90        0.0019
              timeofday    1   54065            8          0.0148         22        0.0005
                gc_info    1    7071            7          0.0990          5        0.0001
ok

另一种对特定函数进行性能分析的方法是使用 lcnt:apply/3lcnt:apply/1,它会在函数调用之前执行 lcnt:clear/0,并在函数调用之后执行 lcnt:collect/0。此方法只应在微基准测试中使用,因为它会将 copy_save 设置为 true,持续整个函数调用,如果在负载下尝试,这可能会导致模拟器内存不足。

Erlang R13B03 (erts-5.7.4) [source] [smp:8:8] [rq:8] [async-threads:0] [hipe]
 [kernel-poll:false] [lock-counting]
1> lcnt:apply(fun() -> big:bang(1000) end).
4384.338
2> lcnt:conflicts().
                   lock   id  #tries  #collisions  collisions [%]  time [us]  duration [%]
                  -----  --- ------- ------------ --------------- ---------- -------------
         alcu_allocator   50 4117913       183091          4.4462     234232        5.1490
              run_queue    8 2050398         3801          0.1854       6700        0.1473
               pix_lock  256 4007080         4943          0.1234       2847        0.0626
              proc_main 1028 3000178        28247          0.9415       1022        0.0225
              proc_msgq 1028 2293677         1352          0.0589        545        0.0120
            proc_status 1028 5258029         1744          0.0332        442        0.0097
 message_pre_alloc_lock    8 2009322          147          0.0073         82        0.0018
              timeofday    1   48616            9          0.0185         13        0.0003
                gc_info    1    7455           12          0.1610          9        0.0002
ok

进程锁按其类别排序,就像所有其他锁一样。查看特定进程和端口作为类别很方便。我们可以通过交换类别和类别标识符来实现这一点 lcnt:swap_pid_keys/0

3> lcnt:swap_pid_keys().
ok
4> lcnt:conflicts([{print, [name, tries, ratio, time]}]).
                   lock  #tries  collisions [%]  time [us]
                  ----- ------- --------------- ----------
         alcu_allocator 4117913          4.4462     234232
              run_queue 2050398          0.1854       6700
               pix_lock 4007080          0.1234       2847
 message_pre_alloc_lock 2009322          0.0073         82
  <[email protected]>   13493          1.4452         41
  <[email protected]>   13504          1.1404         36
  <[email protected]>   13181          1.6235         35
  <[email protected]>   13534          0.8202         22
   <[email protected]>    8744          5.8326         22
  <[email protected]>   13335          1.1174         19
  <[email protected]>   13452          1.3678         19
  <[email protected]>   13497          1.8745         18
  <[email protected]>   11009          2.5343         18
  <[email protected]>   13131          1.2566         16
  <[email protected]>   13216          1.7327         15
  <[email protected]>   13156          1.1098         15
  <[email protected]>   13420          0.7303         14
  <[email protected]>   13141          1.6437         14
  <[email protected]>   13346          1.2064         13
  <[email protected]>   13076          1.1701         13
ok

从 Erlang shell

Erlang R13B03 (erts-5.7.4) [source] [smp:8:8] [rq:8] [async-threads:0] [hipe]
 [kernel-poll:false] [lock-counting]

Eshell V5.7.4  (abort with ^G)
1> Conf=[{db_nodes, [node()]}, {driver_nodes, [node()]}, {replica_nodes, [node()]},
 {n_drivers_per_node, 10}, {n_branches, 1000}, {n_accounts_per_branch, 10},
 {replica_type, ram_copies}, {stop_after, 60000}, {reuse_history_id, true}].
[{db_nodes,[nonode@nohost]},
 {driver_nodes,[nonode@nohost]},
 {replica_nodes,[nonode@nohost]},
 {n_drivers_per_node,10},
 {n_branches,1000},
 {n_accounts_per_branch,10},
 {replica_type,ram_copies},
 {stop_after,60000},
 {reuse_history_id,true}]
2> mnesia_tpcb:init([{use_running_mnesia, false}|Conf]).
ignore

基准测试的初始配置已完成。现在该对实际基准测试和 Mnesia 进行性能分析了

3> lcnt:apply(fun() -> {ok,{time, Tps,_,_,_,_}} = mnesia_tpcb:run([{use_running_mnesia,
 true}|Conf]), Tps/60 end).
12037.483333333334
ok
4> lcnt:swap_pid_keys().
ok

id 标题表示使用选项 {combine, true} (默认情况下启用)时类别下唯一标识符的数量。否则它将显示特定标识符。该 db_tab 列表显示 722287 个唯一锁,每个创建的 ets 表都有一个,Mnesia 为每个事务创建一个。

5> lcnt:conflicts().
                   lock     id   #tries  #collisions  collisions [%]  time [us]  duration [%]
                  -----    ---  ------- ------------ --------------- ---------- -------------
         alcu_allocator     50 56355118       732662          1.3001    2934747        4.8862
                 db_tab 722287 94513441        63203          0.0669    1958797        3.2613
              timeofday      1  2701048       175854          6.5106    1746079        2.9071
               pix_lock    256 24306168       163214          0.6715     918309        1.5289
              run_queue      8 11813811       152637          1.2920     357040        0.5945
 message_pre_alloc_lock      8 17671449        57203          0.3237     263043        0.4380
          mnesia_locker      4 17477633      1618548          9.2607      97092        0.1617
              mnesia_tm      4  9891408       463788          4.6888      86353        0.1438
                gc_info      1   823460          628          0.0763      24826        0.0413
     meta_main_tab_slot     16 41393400         7193          0.0174      11393        0.0190
 <[email protected]>      4  4331412          333          0.0077       7148        0.0119
            timer_wheel      1   203185           30          0.0148       3108        0.0052
 <[email protected]>      4  4291098          210          0.0049        885        0.0015
 <[email protected]>      4  4294702          288          0.0067        442        0.0007
 <[email protected]>      4  4346066          235          0.0054        390        0.0006
 <[email protected]>      4  4348159          287          0.0066        379        0.0006
 <[email protected]>      4  4279309          290          0.0068        325        0.0005
 <[email protected]>      4  4292190          302          0.0070        315        0.0005
 <[email protected]>      4  4208858          265          0.0063        276        0.0005
 <[email protected]>      4  4377502          267          0.0061        276        0.0005
ok

列表显示 mnesia_locker,一个进程,具有高度争用的锁。

6> lcnt:inspect(mnesia_locker).
          lock          id  #tries  #collisions  collisions [%]  time [us]  duration [%]
         -----         --- ------- ------------ --------------- ---------- -------------
 mnesia_locker   proc_msgq 5449930        59374          1.0894      69781        0.1162
 mnesia_locker   proc_main 4462782      1487374         33.3284      14398        0.0240
 mnesia_locker proc_status 7564921        71800          0.9491      12913        0.0215
 mnesia_locker   proc_link       0            0          0.0000          0        0.0000
ok

不使用类别组合器的列表。

7> lcnt:conflicts([{combine, false}, {print, [name, id, tries, ratio, time]}]).
                   lock                        id   #tries  collisions [%]  time [us]
                  -----                       ---  ------- --------------- ----------
                 db_tab mnesia_transient_decision   722250          3.9463    1856852
              timeofday                 undefined  2701048          6.5106    1746079
         alcu_allocator                 ets_alloc  7490696          2.2737     692655
         alcu_allocator                 ets_alloc  7081771          2.3294     664522
         alcu_allocator                 ets_alloc  7047750          2.2520     658495
         alcu_allocator                 ets_alloc  5883537          2.3177     610869
               pix_lock                        58 11011355          1.1924     564808
               pix_lock                        60  4426484          0.7120     262490
         alcu_allocator                 ets_alloc  1897004          2.4248     219543
 message_pre_alloc_lock                 undefined  4211267          0.3242     128299
              run_queue                         3  2801555          1.3003     116792
              run_queue                         2  2799988          1.2700     100091
              run_queue                         1  2966183          1.2712      78834
          mnesia_locker                 proc_msgq  5449930          1.0894      69781
 message_pre_alloc_lock                 undefined  3495672          0.3262      65773
 message_pre_alloc_lock                 undefined  4189752          0.3174      58607
              mnesia_tm                 proc_msgq  2094144          1.7184      56361
              run_queue                         4  2343585          1.3115      44300
                 db_tab                    branch  1446529          0.5229      38244
                gc_info                 undefined   823460          0.0763      24826
ok

在这种情况下,保护 ets 表 mnesia_transient_decision 的锁花费了大部分时间等待。在运行了 60 秒的测试中,这花费了 1.8 秒。该时间也分布在八个不同的调度程序线程上。

8> lcnt:inspect(db_tab, [{print, [name, id, tries, colls, ratio, duration]}]).
   lock                        id  #tries  #collisions  collisions [%]  duration [%]
  -----                       --- ------- ------------ --------------- -------------
 db_tab mnesia_transient_decision  722250        28502          3.9463        3.0916
 db_tab                    branch 1446529         7564          0.5229        0.0637
 db_tab                   account 1464500         8203          0.5601        0.0357
 db_tab                    teller 1464529         8110          0.5538        0.0291
 db_tab                   history  722250         3767          0.5216        0.0232
 db_tab              mnesia_stats  750332         7057          0.9405        0.0180
 db_tab        mnesia_trans_store      61            0          0.0000        0.0000
 db_tab        mnesia_trans_store      61            0          0.0000        0.0000
 db_tab        mnesia_trans_store      53            0          0.0000        0.0000
 db_tab        mnesia_trans_store      53            0          0.0000        0.0000
 db_tab        mnesia_trans_store      53            0          0.0000        0.0000
 db_tab        mnesia_trans_store      53            0          0.0000        0.0000
 db_tab        mnesia_trans_store      53            0          0.0000        0.0000
 db_tab        mnesia_trans_store      53            0          0.0000        0.0000
 db_tab        mnesia_trans_store      53            0          0.0000        0.0000
 db_tab        mnesia_trans_store      53            0          0.0000        0.0000
 db_tab        mnesia_trans_store      53            0          0.0000        0.0000
 db_tab        mnesia_trans_store      53            0          0.0000        0.0000
 db_tab        mnesia_trans_store      53            0          0.0000        0.0000
 db_tab        mnesia_trans_store      53            0          0.0000        0.0000
ok

通常,较高的 time 值不好,这通常是需要查找的东西。但是,还应该注意高锁获取频率(#tries),因为锁会产生开销,并且因为即使在特定测试中没有显示出来,如果它们开始发生冲突,高频率也会变得有问题。

LCNT 参考手册