<?php
include_once $_SERVER['DOCUMENT_ROOT'] . '/include/shared-manual.inc';
$TOC = array();
$TOC_DEPRECATED = array();
$PARENTS = array();
include_once dirname(__FILE__) ."/toc/book.yac.inc";
$setup = array (
  'home' => 
  array (
    0 => 'index.php',
    1 => 'PHP Manual',
  ),
  'head' => 
  array (
    0 => 'UTF-8',
    1 => 'zh',
  ),
  'this' => 
  array (
    0 => 'yac.memory-management.php',
    1 => '内存管理',
    2 => '内存管理',
  ),
  'up' => 
  array (
    0 => 'book.yac.php',
    1 => 'Yac',
  ),
  'prev' => 
  array (
    0 => 'yac.resources.php',
    1 => '资源类型',
  ),
  'next' => 
  array (
    0 => 'yac.constants.php',
    1 => '预定义常量',
  ),
  'alternatives' => 
  array (
  ),
  'source' => 
  array (
    'lang' => 'zh',
    'path' => 'reference/yac/memory.xml',
  ),
  'history' => 
  array (
  ),
);
$setup["toc"] = $TOC;
$setup["toc_deprecated"] = $TOC_DEPRECATED;
$setup["parents"] = $PARENTS;
manual_setup($setup);

contributors($setup);

?>
<div id="yac.memory-management" class="chapter">
 <h1 class="title">内存管理</h1>


 <p class="simpara">
  Yac 把数据存放在两个相互独立的共享内存池里，分别由
  <a href="yac.configuration.php#ini.yac.keys-memory-size" class="link">yac.keys_memory_size</a>
  和
  <a href="yac.configuration.php#ini.yac.values-memory-size" class="link">yac.values_memory_size</a>
  配置。两个池写满和释放的方式各不相同，因此在调整任何一个参数之前，先弄清楚症状属于哪个池会很有帮助。
 </p>

 <div id="yac.memory-layout" class="section">
  <h2 class="title">每个池各存放什么</h2>
  <p class="simpara">
   两个池分别存放一个条目的两半。键池存放键：每个被缓存的键占一个槽位，槽位里带着键本身（最长 48 字节）、哈希值、TTL、命中次数和最后访问时间；值只通过一个指向值池的指针被引用。值池存放值：每个存储的值占用一块序列化后的字节——如果条目经过了
   <a href="yac.configuration.php#ini.yac.compress-threshold" class="link">yac.compress_threshold</a>，则是压缩后的形式。
  </p>
  <p class="simpara">
   唯一的例外是嵌入值（embedded values）：极小的标量——<strong><code><a href="reserved.constants.php#constant.null">null</a></code></strong>、
   <strong><code><a href="reserved.constants.php#constant.true">true</a></code></strong>、<strong><code><a href="reserved.constants.php#constant.false">false</a></code></strong>、大多数整数（在 64 位构建下能装进 60 个有符号位的整数）、最长 7 字节的字符串和空数组——直接存放在槽位内部，用的正是本来要引用值块的那个指针。这样的值在值池中完全不占空间，只有它们的键占位。
  </p>
  <p class="para">
   粗略的容量规划方法：
   <ul class="itemizedlist">
    <li class="listitem">
     <p class="simpara">
      键池每 MB 大约能容纳 8,000 个键，因此
      <a href="yac.configuration.php#ini.yac.keys-memory-size" class="link">yac.keys_memory_size</a>
      可以按不同键的数量除以每 MB 8,000 个、再向上取整来估算——默认的 <code class="literal">8M</code> 大约能容纳 64,000 个键；
     </p>
    </li>
    <li class="listitem">
     <p class="simpara">
      值池必须容纳所有仍可能被读取的值，因此
      <a href="yac.configuration.php#ini.yac.values-memory-size" class="link">yac.values_memory_size</a>
      可以按存活值的数量乘以平均序列化（压缩后）大小来估算，并预留大约两倍的空间：这个池是一个环，只有当分配器的游标绕回来覆盖某个值时，它才算消亡。
     </p>
    </li>
   </ul>
   嵌入值和其他条目一样占用槽位，但在值池中不占空间，因此计算第二项时不必把它们算进去。
  </p>
 </div>

 <div id="yac.memory-keys" class="section">
  <h2 class="title">键池（槽位）</h2>
  <p class="simpara">
   <a href="yac.configuration.php#ini.yac.keys-memory-size" class="link">yac.keys_memory_size</a>
   维护着一张固定大小的槽位表——默认的 <code class="literal">8M</code>
   大约有 65,536 个槽位。每个存储的键恰好占用一个槽位，因此这个池限制了同时能存在的条目数量；与值池不同，槽位永远不会被单独释放。过期的槽位——TTL 已过的，或者 <span class="methodname"><a href="yac.delete.php" class="methodname">Yac::delete()</a></span> 留下的墓碑——在新键需要时会被免费回收。只有当某条探测路径上的四个候选槽位全部被存活条目占据时，才会驱逐其中一个来腾出空间——这记一次 <code class="literal">kick</code>（对应
   <span class="methodname"><a href="yac.info.php" class="methodname">Yac::info()</a></span> 的
   <code class="literal">kicks</code> 计数器）。
  </p>
  <p class="para">
   驱逐只在发生碰撞的探测路径上的四个存活候选者中挑选：
   <ul class="itemizedlist">
    <li class="listitem">
     <p class="simpara">
      最近最少使用的那个（<code class="literal">atime</code> 最旧）会被驱逐；
     </p>
    </li>
    <li class="listitem">
     <p class="simpara">
      打平时选命中次数最少的，再打平选探测位置最靠前的。
     </p>
    </li>
   </ul>
  </p>
  <p class="simpara">
   一个常见的误解：<code class="literal">slots_used</code> 达到
   <code class="literal">slots_size</code> <em>并不</em>是错误状态。一个键的工作集大于槽位表的缓存，此后只是以 100% 的占用率运行，按需驱逐和重新插入而已。判断键池大小是否合适的唯一指标是命中率（<code class="literal">hits / (hits + miss)</code>，按两次
   <span class="methodname"><a href="yac.info.php" class="methodname">Yac::info()</a></span> 快照之间的增量计算，而不是全生命周期的平均值）。单看 <code class="literal">kicks</code> 计数很高并不能说明有问题——只是键的分布不均匀，某些探测路径碰撞更多而已。只有当命中率和 <code class="literal">kicks</code> <em>同时</em>很糟时，才说明槽位表对这个键集合来说太小了，解决办法是调大
   <a href="yac.configuration.php#ini.yac.keys-memory-size" class="link">yac.keys_memory_size</a>。
  </p>
  <p class="simpara">
   槽位永不释放还有一个后果：没有 TTL（<code class="literal">ttl = 0</code>）又再也没被读取过的条目会一直占着槽位，直到某次驱逐恰好选中它。如果应用要存放大量这类一次性数据，请给这些条目设置 TTL，让它们过期后被回收，而不是挤掉存活的条目；或者干脆按全部键集合的大小来规划键池。
  </p>
 </div>

 <div id="yac.memory-values" class="section">
  <h2 class="title">值池（段）</h2>
  <p class="simpara">
   <a href="yac.configuration.php#ini.yac.values-memory-size" class="link">yac.values_memory_size</a>
   被切分成每段 4M，按环形管理：写入推进各段的游标，空间从不按条目释放。当一次分配放不下时，游标绕回某段的起点——这记一次 <code class="literal">recycle</code>（对应
   <span class="methodname"><a href="yac.info.php" class="methodname">Yac::info()</a></span> 的
   <code class="literal">recycles</code> 计数器）。一次回收并不会立即使整段失效：被覆盖的值在绕回的游标真正覆盖到它们之前仍然可读，一旦被覆盖，对它们的读取就会通不过完整性校验而退化为未命中。
  </p>
  <p class="simpara">
   这个池有两个大小值得关注：总的
   <a href="yac.configuration.php#ini.yac.values-memory-size" class="link">yac.values_memory_size</a>
   必须装得下存活值的工作集，而单个条目存储后最多占 1 MB
   （<strong><code><a href="yac.constants.php#constant.yac-max-raw-compressed-len">YAC_MAX_RAW_COMPRESSED_LEN</a></code></strong>）。因此更大的值在存储前总会被压缩；一个压不进 1 MB 以内的值——最常见的原因是数据本身是随机的——会被拒绝，并使 <code class="literal">fails</code> 计数器加一。值本身的绝对上限要高得多：序列化后超过 64 MB
   （<strong><code><a href="yac.constants.php#constant.yac-max-value-raw-len">YAC_MAX_VALUE_RAW_LEN</a></code></strong>，即
   <code class="literal">(1 &lt;&lt; 26) - 1</code> 字节）的值会被直接拒绝。
  </p>
 </div>

 <div id="yac.memory-tuning" class="section">
  <h2 class="title">容量规划与观察指标</h2>
  <p class="simpara">
   从默认值起步，然后观察
   <span class="methodname"><a href="yac.info.php" class="methodname">Yac::info()</a></span> 的各计数器——它们从 <code class="literal">start_time</code> 起累加，所以要比较相隔一段时间的两个快照：
  </p>
  <ul class="itemizedlist">
   <li class="listitem">
    <p class="simpara">
     命中率健康（比如 &gt;= 90%）：缓存工作正常，无论其他计数器显示什么，都不用管；
    </p>
   </li>
   <li class="listitem">
    <p class="simpara">
     命中率低且 <code class="literal">kicks</code> 攀升：键池对这个键集合来说太小——存活条目在被重新读取之前就被驱逐了。调大
     <a href="yac.configuration.php#ini.yac.keys-memory-size" class="link">yac.keys_memory_size</a>；
    </p>
   </li>
   <li class="listitem">
    <p class="para">
     <code class="literal">recycles</code> 频繁：这是真问题，不是无害的计数。一次回收意味着值分配器已经绕回、即将覆盖条目——被覆盖的值在重新读取之前就消亡了，存它花掉的字节白花了，命中率也会受损。这说明值池对存活数据的体量来说太小了。按效果排序：
     <ul class="itemizedlist">
      <li class="listitem">
       <p class="simpara">
        给条目设置 TTL。以 <code class="literal">ttl = 0</code> 写入的值永远存活，一直占着池子，迫使游标更早绕回。TTL 限定了每个条目的存活时长，缩小了池子必须容纳的存活工作集；
       </p>
      </li>
      <li class="listitem">
       <p class="simpara">
        调大
        <a href="yac.configuration.php#ini.yac.values-memory-size" class="link">yac.values_memory_size</a>，让池子装得下整个存活值集合（记住要按存活占用大约两倍来预算——值只有在游标绕回来覆盖它时才消亡）；
       </p>
      </li>
      <li class="listitem">
       <p class="simpara">
        减少单个条目的存储量：如果
        <a href="yac.configuration.php#ini.yac.compress-threshold" class="link">yac.compress_threshold</a>
        设置得高于 <code class="literal">1024</code> 这个下限，把它调低，让大的载荷被压缩；精简那些不需要完整缓存的值；
       </p>
      </li>
     </ul>
    </p>
   </li>
   <li class="listitem">
    <p class="simpara">
     <code class="literal">fails</code> 增长：有值存不进去，最常见的是单个值压缩后仍超过 1 MB 的存储上限——把值拆分。
    </p>
   </li>
  </ul>
 </div>

</div>
<?php manual_footer($setup); ?>