learn.lianglianglee.com/专栏/容器实战高手课/14 容器中的内存与IO:容器写文件的延时为什么波动很大?.md.html
2022-05-11 19:04:14 +08:00

680 lines
29 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>14 容器中的内存与IO容器写文件的延时为什么波动很大.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/容器实战高手课/00 开篇词 一个态度两个步骤,成为容器实战高手.md.html">00 开篇词 一个态度两个步骤,成为容器实战高手.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/01 认识容器:容器的基本操作和实现原理.md.html">01 认识容器:容器的基本操作和实现原理.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/02 理解进程1为什么我在容器中不能kill 1号进程.md.html">02 理解进程1为什么我在容器中不能kill 1号进程.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/03 理解进程2为什么我的容器里有这么多僵尸进程.md.html">03 理解进程2为什么我的容器里有这么多僵尸进程.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/04 理解进程3为什么我在容器中的进程被强制杀死了.md.html">04 理解进程3为什么我在容器中的进程被强制杀死了.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/05 容器CPU1怎么限制容器的CPU使用.md.html">05 容器CPU1怎么限制容器的CPU使用.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/06 容器CPU2如何正确地拿到容器CPU的开销.md.html">06 容器CPU2如何正确地拿到容器CPU的开销.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/07 Load Average加了CPU Cgroup限制为什么我的容器还是很慢.md.html">07 Load Average加了CPU Cgroup限制为什么我的容器还是很慢.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/08 容器内存:我的容器为什么被杀了?.md.html">08 容器内存:我的容器为什么被杀了?.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/09 Page Cache为什么我的容器内存使用量总是在临界点.md.html">09 Page Cache为什么我的容器内存使用量总是在临界点.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/10 Swap容器可以使用Swap空间吗.md.html">10 Swap容器可以使用Swap空间吗.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/11 容器文件系统:我在容器中读写文件怎么变慢了.md.html">11 容器文件系统:我在容器中读写文件怎么变慢了.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/12 容器文件Quota容器为什么把宿主机的磁盘写满了.md.html">12 容器文件Quota容器为什么把宿主机的磁盘写满了.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/13 容器磁盘限速:我的容器里磁盘读写为什么不稳定.md.html">13 容器磁盘限速:我的容器里磁盘读写为什么不稳定.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/容器实战高手课/14 容器中的内存与IO容器写文件的延时为什么波动很大.md.html">14 容器中的内存与IO容器写文件的延时为什么波动很大.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/15 容器网络我修改了procsysnet下的参数为什么在容器中不起效.md.html">15 容器网络我修改了procsysnet下的参数为什么在容器中不起效.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/16 容器网络配置1容器网络不通了要怎么调试.md.html">16 容器网络配置1容器网络不通了要怎么调试.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/17 容器网络配置2容器网络延时要比宿主机上的高吗.md.html">17 容器网络配置2容器网络延时要比宿主机上的高吗.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/18 容器网络配置3容器中的网络乱序包怎么这么高.md.html">18 容器网络配置3容器中的网络乱序包怎么这么高.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/19 容器安全1我的容器真的需要privileged权限吗.md.html">19 容器安全1我的容器真的需要privileged权限吗.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/20 容器安全2在容器中我不以root用户来运行程序可以吗.md.html">20 容器安全2在容器中我不以root用户来运行程序可以吗.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/加餐01 案例分析怎么解决海量IPVS规则带来的网络延时抖动问题.md.html">加餐01 案例分析怎么解决海量IPVS规则带来的网络延时抖动问题.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/加餐02 理解perf怎么用perf聚焦热点函数.md.html">加餐02 理解perf怎么用perf聚焦热点函数.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/加餐03 理解ftrace1怎么应用ftrace查看长延时内核函数.md.html">加餐03 理解ftrace1怎么应用ftrace查看长延时内核函数.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/加餐04 理解ftrace2怎么理解ftrace背后的技术tracepoint和kprobe.md.html">加餐04 理解ftrace2怎么理解ftrace背后的技术tracepoint和kprobe.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/加餐05 eBPF怎么更加深入地查看内核中的函数.md.html">加餐05 eBPF怎么更加深入地查看内核中的函数.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/加餐06 BCC入门eBPF的前端工具.md.html">加餐06 BCC入门eBPF的前端工具.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/加餐福利 课后思考题答案合集.md.html">加餐福利 课后思考题答案合集.md.html</a>
</li>
<li>
<a href="/专栏/容器实战高手课/结束语 跳出舒适区,突破思考的惰性.md.html">结束语 跳出舒适区,突破思考的惰性.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>14 容器中的内存与IO容器写文件的延时为什么波动很大</h1>
<p>你好,我是程远。这一讲,我们继续聊一聊容器中写文件性能波动的问题。</p>
<p>你应该还记得,我们上一讲中讲过 Linux 中的两种 I/O 模式Direct I/O 和 Buffered I/O。</p>
<p>对于 Linux 的系统调用 write() 来说Buffered I/O 是缺省模式,使用起来比较方便,而且从用户角度看,在大多数的应用场景下,用 Buffered I/O 的 write() 函数调用返回要快一些。所以Buffered I/O 在程序中使用得更普遍一些。</p>
<p>当使用 Buffered I/O 的应用程序从虚拟机迁移到容器,这时我们就会发现多了 Memory Cgroup 的限制之后write() 写相同大小的数据块花费的时间,延时波动会比较大。</p>
<p>这是怎么回事呢?接下来我们就带着问题开始今天的学习。</p>
<h2>问题再现</h2>
<p>我们可以先动手写一个小程序,用来模拟刚刚说的现象。</p>
<p>这个小程序我们这样来设计:从一个文件中每次读取一个 64KB 大小的数据块,然后写到一个新文件中,它可以不断读写 10GB 大小的数据。同时我们在这个小程序中做个记录,记录写每个 64KB 的数据块需要花费的时间。</p>
<p>我们可以先在虚拟机里直接运行,虚拟机里内存大小是大于 10GB 的。接着,我们把这个程序放到容器中运行,因为这个程序本身并不需要很多的内存,我们给它做了一个 Memory Cgroup 的内存限制,设置为 1GB。</p>
<p>运行结束后,我们比较一下程序写数据块的时间。我把结果画了一张图,图里的纵轴是时间,单位 us横轴是次数在这里我们记录了 96 次。图中橘红色的线是在容器里运行的结果,蓝色的线是在虚拟机上运行的结果。</p>
<p>结果很明显,在容器中写入数据块的时间会时不时地增高到 200us而在虚拟机里的写入数据块时间就比较平稳一直在 3050us 这个范围内。</p>
<p><img src="assets/7c494f4bc587b618f4b7db3db9ce4ac0.jpg" alt="img" /></p>
<p>通过这个小程序,我们再现了问题,那我们就来分析一下,为什么会产生这样的结果。</p>
<h2>时间波动是因为 Dirty Pages 的影响么?</h2>
<p>我们对文件的写入操作是 Buffered I/O。在前一讲中我们其实已经知道了对于 Buffer I/O用户的数据是先写入到 Page Cache 里的。而这些写入了数据的内存页面,在它们没有被写入到磁盘文件之前,就被叫作 dirty pages。</p>
<p>Linux 内核会有专门的内核线程(每个磁盘设备对应的 kworker/flush 线程)把 dirty pages 写入到磁盘中。那我们自然会这样猜测,也许是 Linux 内核对 dirty pages 的操作影响了 Buffered I/O 的写操作?</p>
<p>想要验证这个想法,我们需要先来看看 dirty pages 是在什么时候被写入到磁盘的。这里就要用到 /proc/sys/vm 里和 dirty page 相关的内核参数了,我们需要知道所有相关参数的含义,才能判断出最后真正导致问题发生的原因。</p>
<p>现在我们挨个来看一下。为了方便后面的讲述,我们可以设定一个比值 AA 等于 dirty pages 的内存 / 节点可用内存 *100%。</p>
<p>第一个参数dirty_background_ratio这个参数里的数值是一个百分比值缺省是 10%。如果比值 A 大于 dirty_background_ratio 的话,比如大于默认的 10%,内核 flush 线程就会把 dirty pages 刷到磁盘里。</p>
<p>第二个参数,是和 dirty_background_ratio 相对应一个参数,也就是 dirty_background_bytes它和 dirty_background_ratio 作用相同。区别只是 dirty_background_bytes 是具体的字节数,它用来定义的是 dirty pages 内存的临界值,而不是比例值。</p>
<p>这里你还要注意dirty_background_ratio 和 dirty_background_bytes 只有一个可以起作用,如果你给其中一个赋值之后,另外一个参数就归 0 了。</p>
<p>接下来我们看第三个参数dirty_ratio这个参数的数值也是一个百分比值缺省是 20%。</p>
<p>如果比值 A大于参数 dirty_ratio 的值,比如大于默认设置的 20%,这时候正在执行 Buffered I/O 写文件的进程就会被阻塞住,直到它写的数据页面都写到磁盘为止。</p>
<p>同样,第四个参数 dirty_bytes 与 dirty_ratio 相对应,它们的关系和 dirty_background_ratio 与 dirty_background_bytes 一样。我们给其中一个赋值后,另一个就会归零。</p>
<p>然后我们来看 dirty_writeback_centisecs这个参数的值是个时间值以百分之一秒为单位缺省值是 500也就是 5 秒钟。它表示每 5 秒钟会唤醒内核的 flush 线程来处理 dirty pages。</p>
<p>最后还有 dirty_expire_centisecs这个参数的值也是一个时间值以百分之一秒为单位缺省值是 3000也就是 30 秒钟。它定义了 dirty page 在内存中存放的最长时间,如果一个 dirty page 超过这里定义的时间,那么内核的 flush 线程也会把这个页面写入磁盘。</p>
<p>好了,从这些 dirty pages 相关的参数定义,你会想到些什么呢?</p>
<p>进程写操作上的时间波动,只有可能是因为 dirty pages 的数量很多,已经达到了第三个参数 dirty_ratio 的值。这时执行写文件功能的进程就会被暂停,直到写文件的操作将数据页面写入磁盘,写文件的进程才能继续运行,所以进程里一次写文件数据块的操作时间会增加。</p>
<p>刚刚说的是我们的推理,那情况真的会是这样吗?其实我们可以在容器中进程不断写入数据的时候,查看节点上 dirty pages 的实时数目。具体操作如下:</p>
<pre><code>watch -n 1 &quot;cat /proc/vmstat | grep dirty&quot;
</code></pre>
<p>当我们的节点可用内存是 12GB 的时候,假设 dirty_ratio 是 20%dirty_background_ratio 是 10%,那么我们在 1GB memory 容器中写 10GB 的数据,就会看到它实时的 dirty pages 数目,也就是 / proc/vmstat 里的 nr_dirty 的数值,这个数值对应的内存并不能达到 dirty_ratio 所占的内存值。</p>
<p><img src="assets/ccd0b41e3bd9420c539942b84d88f968.png" alt="img" /></p>
<p>其实我们还可以再做个实验,就是在 dirty_bytes 和 dirty_background_bytes 里写入一个很小的值。</p>
<pre><code>echo 8192 &gt; /proc/sys/vm/dirty_bytes
echo 4096 &gt; /proc/sys/vm/dirty_background_bytes
</code></pre>
<p>然后再记录一下容器程序里每写入 64KB 数据块的时间,这时候,我们就会看到,时不时一次写入的时间就会达到 9ms这已经远远高于我们之前看到的 200us 了。</p>
<p>因此,我们知道了这个时间的波动,并不是强制把 dirty page 写入到磁盘引起的。</p>
<h2>调试问题</h2>
<p>那接下来,我们还能怎么分析这个问题呢?</p>
<p>我们可以用 perf 和 ftrace 这两个工具,对容器里写数据块的进程做个 profile看看到底是调用哪个函数花费了比较长的时间。顺便说一下我们在专题加餐里会专门介绍如何使用 perf、ftrace 等工具以及它们的工作原理,在这里你只要了解我们的调试思路就行。</p>
<p>怎么使用这两个工具去定位耗时高的函数呢?我大致思路是这样的:我们发现容器中的进程用到了 write() 这个函数调用,然后写 64KB 数据块的时间增加了,而 write() 是一个系统调用,那我们需要进行下面这两步操作。</p>
<p>第一步,我们要找到内核中 write() 这个系统调用函数下,又调用了哪些子函数。想找出主要的子函数我们可以查看代码,也可以用 perf 这个工具来得到。</p>
<p>然后是第二步,得到了 write() 的主要子函数之后,我们可以用 ftrace 这个工具来 trace 这些函数的执行时间,这样就可以找到花费时间最长的函数了。</p>
<p>好,下面我们就按照刚才梳理的思路来做一下。首先是第一步,我们在容器启动写磁盘的进程后,在宿主机上得到这个进程的 pid然后运行下面的 perf 命令。</p>
<pre><code>perf record -a -g -p &lt;pid&gt;
</code></pre>
<p>等写磁盘的进程退出之后,这个 perf record 也就停止了。</p>
<p>这时我们再执行 perf report 查看结果。把 vfs_write() 函数展开之后我们就可以看到write() 这个系统调用下面的调用到了哪些主要的子函数,到这里第一步就完成了。</p>
<p><img src="assets/9191caa5db8c0afe2363540bc31e1d9d.png" alt="img" /></p>
<p>下面再来做第二步,我们把主要的函数写入到 ftrace 的 set_ftrace_filter 里,然后把 ftrace 的 tracer 设置为 function_graph并且打开 tracing_on 开启追踪。</p>
<pre><code># cd /sys/kernel/debug/tracing
# echo vfs_write &gt;&gt; set_ftrace_filter
# echo xfs_file_write_iter &gt;&gt; set_ftrace_filter
# echo xfs_file_buffered_aio_write &gt;&gt; set_ftrace_filter
# echo iomap_file_buffered_write
# echo iomap_file_buffered_write &gt;&gt; set_ftrace_filter
# echo pagecache_get_page &gt;&gt; set_ftrace_filter
# echo try_to_free_mem_cgroup_pages &gt;&gt; set_ftrace_filter
# echo try_charge &gt;&gt; set_ftrace_filter
# echo mem_cgroup_try_charge &gt;&gt; set_ftrace_filter
# echo function_graph &gt; current_tracer
# echo 1 &gt; tracing_on
</code></pre>
<p>这些设置完成之后,我们再运行一下容器中的写磁盘程序,同时从 ftrace 的 trace_pipe 中读取出追踪到的这些函数。</p>
<p>这时我们可以看到,当需要申请 Page Cache 页面的时候write() 系统调用会反复地调用 mem_cgroup_try_charge(),并且在释放页面的时候,函数 do_try_to_free_pages() 花费的时间特别长,有 50+us时间单位micro-seconds这么多。</p>
<pre><code> 1) | vfs_write() {
1) | xfs_file_write_iter [xfs]() {
1) | xfs_file_buffered_aio_write [xfs]() {
1) | iomap_file_buffered_write() {
1) | pagecache_get_page() {
1) | mem_cgroup_try_charge() {
1) 0.338 us | try_charge();
1) 0.791 us | }
1) 4.127 us | }
1) | pagecache_get_page() {
1) | mem_cgroup_try_charge() {
1) | try_charge() {
1) | try_to_free_mem_cgroup_pages() {
1) + 52.798 us | do_try_to_free_pages();
1) + 53.958 us | }
1) + 54.751 us | }
1) + 55.188 us | }
1) + 56.742 us | }
1) ! 109.925 us | }
1) ! 110.558 us | }
1) ! 110.984 us | }
1) ! 111.515 us | }
</code></pre>
<p>看到这个 ftrace 的结果,你是不是会想到,我们在容器内存那一讲中提到的 Page Cahe 呢?</p>
<p>是的,这个问题的确和 Page Cache 有关Linux 会把所有的空闲内存利用起来,一旦有 Buffered I/O这些内存都会被用作 Page Cache。</p>
<p>当容器加了 Memory Cgroup 限制了内存之后,对于容器里的 Buffered I/O就只能使用容器中允许使用的最大内存来做 Page Cache。</p>
<p>那么如果容器在做内存限制的时候Cgroup 中 memory.limit_in_bytes 设置得比较小,而容器中的进程又有很大量的 I/O这样申请新的 Page Cache 内存的时候,又会不断释放老的内存页面,这些操作就会带来额外的系统开销了。</p>
<h2>重点总结</h2>
<p>我们今天讨论的问题是在容器中用 Buffered I/O 方式写文件的时候,会出现写入时间波动的问题。</p>
<p>由于这是 Buffered I/O 方式,对于写入文件会先写到内存里,这样就产生了 dirty pages所以我们先研究了一下 Linux 对 dirty pages 的回收机制是否会影响到容器中写入数据的波动。</p>
<p>在这里我们最主要的是理解这两个参数dirty_background_ratio 和 dirty_ratio这两个值都是相对于节点可用内存的百分比值。</p>
<p>当 dirty pages 数量超过 dirty_background_ratio 对应的内存量的时候,内核 flush 线程就会开始把 dirty pages 写入磁盘 ; 当 dirty pages 数量超过 dirty_ratio 对应的内存量,这时候程序写文件的函数调用 write() 就会被阻塞住,直到这次调用的 dirty pages 全部写入到磁盘。</p>
<p>在节点是大内存容量,并且 dirty_ratio 为系统缺省值 20%dirty_background_ratio 是系统缺省值 10% 的情况下,我们通过观察 /proc/vmstat 中的 nr_dirty 数值可以发现dirty pages 不会阻塞进程的 Buffered I/O 写文件操作。</p>
<p>所以我们做了另一种尝试,使用 perf 和 ftrace 工具对容器中的写文件进程进行 profile。我们用 perf 得到了系统调用 write() 在内核中的一系列子函数调用,再用 ftrace 来查看这些子函数的调用时间。</p>
<p>根据 ftrace 的结果,我们发现写数据到 Page Cache 的时候,需要不断地去释放原有的页面,这个时间开销是最大的。造成容器中 Buffered I/O write() 不稳定的原因正是容器在限制内存之后Page Cache 的数量较小并且不断申请释放。</p>
<p>其实这个问题也提醒了我们:在对容器做 Memory Cgroup 限制内存大小的时候,不仅要考虑容器中进程实际使用的内存量,还要考虑容器中程序 I/O 的量,合理预留足够的内存作为 Buffered I/O 的 Page Cache。</p>
<p>比如,如果知道需要反复读写文件的大小,并且在内存足够的情况下,那么 Memory Cgroup 的内存限制可以超过这个文件的大小。</p>
<p>还有一个解决思路是,我们在程序中自己管理文件的 cache 并且调用 Direct I/O 来读写文件,这样才会对应用程序的性能有一个更好的预期。</p>
<h2>思考题</h2>
<p>我们对 dirty_bytes 和 dirty_background_bytes 做下面的设置:</p>
<pre><code>-bash-4.2# echo 8192 &gt; /proc/sys/vm/dirty_bytes
-bash-4.2# echo 4096 &gt; /proc/sys/vm/dirty_background_bytes
</code></pre>
<p>然后再运行下面的 fio 测试,得到的结果和缺省 dirty_* 配置的时候会有差别吗?</p>
<pre><code># fio -direct=1 -iodepth=64 -rw=write -ioengine=libaio -bs=4k -size=10G -numjobs=1 -name=./fio.test
</code></pre>
<p>欢迎你在留言区提出你的思考或是疑问。如果这篇文章对你有帮助的话,也欢迎你分享给你的朋友、同事,一起学习进步。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/容器实战高手课/13 容器磁盘限速:我的容器里磁盘读写为什么不稳定.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/容器实战高手课/15 容器网络我修改了procsysnet下的参数为什么在容器中不起效.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099779ebe013cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>