learn.lianglianglee.com/专栏/容器实战高手课/09 Page Cache:为什么我的容器内存使用量总是在临界点.md.html
2022-05-11 18:57:05 +08:00

889 lines
28 KiB
HTML
Raw 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>09 Page Cache为什么我的容器内存使用量总是在临界点.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 class="current-tab" 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 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>09 Page Cache为什么我的容器内存使用量总是在临界点</h1>
<p>你好,我是程远。</p>
<p>上一讲,我们讲了 Memory Cgroup 是如何控制一个容器的内存的。我们已经知道了,如果容器使用的物理内存超过了 Memory Cgroup 里的 memory.limit_in_bytes 值,那么容器中的进程会被 OOM Killer 杀死。</p>
<p>不过在一些容器的使用场景中,比如容器里的应用有很多文件读写,你会发现整个容器的内存使用量已经很接近 Memory Cgroup 的上限值了,但是在容器中我们接着再申请内存,还是可以申请出来,并且没有发生 OOM。</p>
<p>这是怎么回事呢?今天这一讲我就来聊聊这个问题。</p>
<h2>问题再现</h2>
<p>我们可以用这里的代码做个容器镜像,然后用下面的这个脚本启动容器,并且设置容器 Memory Cgroup 里的内存上限值是 100MB104857600bytes</p>
<pre><code># !/bin/bash
docker stop page_cache;docker rm page_cache
if [ ! -f ./test.file ]
then
dd if=/dev/zero of=./test.file bs=4096 count=30000
echo &quot;Please run start_container.sh again &quot;
exit 0
fi
echo 3 &gt; /proc/sys/vm/drop_caches
sleep 10
docker run -d --init --name page_cache -v $(pwd):/mnt registry/page_cache_test:v1
CONTAINER_ID=$(sudo docker ps --format &quot;{{.ID}}\t{{.Names}}&quot; | grep -i page_cache | awk '{print $1}')
echo $CONTAINER_ID
CGROUP_CONTAINER_PATH=$(find /sys/fs/cgroup/memory/ -name &quot;*$CONTAINER_ID*&quot;)
echo 104857600 &gt; $CGROUP_CONTAINER_PATH/memory.limit_in_bytes
cat $CGROUP_CONTAINER_PATH/memory.limit_in_bytes
</code></pre>
<p>把容器启动起来后,我们查看一下容器的 Memory Cgroup 下的 memory.limit_in_bytes 和 memory.usage_in_bytes 这两个值。</p>
<p>如下图所示,我们可以看到容器内存的上限值设置为 104857600bytes100MB而这时整个容器的已使用内存显示为 104767488bytes这个值已经非常接近上限值了。</p>
<p>我们把容器内存上限值和已使用的内存数值做个减法104857600104767488= 90112bytes只差大概 90KB 左右的大小。</p>
<p><img src="assets/1192c3b6430dc1de84199e9da153502c.png" alt="img" /></p>
<p>但是,如果这时候我们继续启动一个程序,让这个程序申请并使用 50MB 的物理内存,就会发现这个程序还是可以运行成功,这时候容器并没有发生 OOM 的情况。</p>
<p>这时我们再去查看参数 memory.usage_in_bytes就会发现它的值变成了 103186432bytes比之前还少了一些。那这是怎么回事呢</p>
<p><img src="assets/79ed17b066762a5385bf70758d4de87b.png" alt="img" /></p>
<h2>知识详解Linux 系统有那些内存类型?</h2>
<p>要解释刚才我们看到的容器里内存分配的现象,就需要先理解 Linux 操作系统里有哪几种内存的类型。</p>
<p>因为我们只有知道了内存的类型,才能明白每一种类型的内存,容器分别使用了多少。而且,对于不同类型的内存,一旦总内存增高到容器里内存最高限制的数值,相应的处理方式也不同。</p>
<h2>Linux 内存类型</h2>
<p>Linux 的各个模块都需要内存,比如内核需要分配内存给页表,内核栈,还有 slab也就是内核各种数据结构的 Cache Pool用户态进程里的堆内存和栈的内存共享库的内存还有文件读写的 Page Cache。</p>
<p>在这一讲里,我们讨论的 Memory Cgroup 里都不会对内核的内存做限制比如页表slab 等。所以我们今天主要讨论与用户态相关的两个内存类型RSS 和 Page Cache。</p>
<h3>RSS</h3>
<p>先看什么是 RSS。RSS 是 Resident Set Size 的缩写,简单来说它就是指进程真正申请到物理页面的内存大小。这是什么意思呢?</p>
<p>应用程序在申请内存的时候,比如说,调用 malloc() 来申请 100MB 的内存大小malloc() 返回成功了,这时候系统其实只是把 100MB 的虚拟地址空间分配给了进程,但是并没有把实际的物理内存页面分配给进程。</p>
<p>上一讲中,我给你讲过,当进程对这块内存地址开始做真正读写操作的时候,系统才会把实际需要的物理内存分配给进程。而这个过程中,进程真正得到的物理内存,就是这个 RSS 了。</p>
<p>比如下面的这段代码,我们先用 malloc 申请 100MB 的内存。</p>
<pre><code> p = malloc(100 * MB);
if (p == NULL)
return 0;
</code></pre>
<p>然后,我们运行 top 命令查看这个程序在运行了 malloc() 之后的内存我们可以看到这个程序的虚拟地址空间VIRT已经有了 106728KB100MB),但是实际的物理内存 RSStop 命令里显示的是 RES就是 Resident 的简写,和 RSS 是一个意思)在这里只有 688KB。</p>
<p><img src="assets/fce68702702bd94539357134ba32ab09.png" alt="img" /></p>
<p>接着我们在程序里等待 30 秒之后,我们再对这块申请的空间里写入 20MB 的数据。</p>
<pre><code> sleep(30);
memset(p, 0x00, 20 * MB)
</code></pre>
<p>当我们用 memset() 函数对这块地址空间写入 20MB 的数据之后,我们再用 top 查看这时候可以看到虚拟地址空间VIRT还是 106728不过物理内存 RSSRES的值变成了 21432大小约为 20MB 这里的单位都是 KB。</p>
<p><img src="assets/8ca5fda34f50166cbc48f6aab93479ed.png" alt="img" /></p>
<p>所以,通过刚才上面的小实验,我们可以验证 RSS 就是进程里真正获得的物理内存大小。</p>
<p>对于进程来说RSS 内存包含了进程的代码段内存,栈内存,堆内存,共享库的内存, 这些内存是进程运行所必须的。刚才我们通过 malloc/memset 得到的内存,就是属于堆内存。</p>
<p>具体的每一部分的 RSS 内存的大小,你可以查看 /proc/[pid]/smaps 文件。</p>
<h3>Page Cache</h3>
<p>每个进程除了各自独立分配到的 RSS 内存外如果进程对磁盘上的文件做了读写操作Linux 还会分配内存,把磁盘上读写到的页面存放在内存中,这部分的内存就是 Page Cache。</p>
<p>Page Cache 的主要作用是提高磁盘文件的读写性能,因为系统调用 read() 和 write() 的缺省行为都会把读过或者写过的页面存放在 Page Cache 里。</p>
<p>还是用我们这一讲最开始的的例子:代码程序去读取 100MB 的文件,在读取文件前,系统中 Page Cache 的大小是 388MB读取后 Page Cache 的大小是 506MB增长了大约 100MB 左右,多出来的这 100MB正是我们读取文件的大小。</p>
<p><img src="assets/19e5319f0ac8821986efb005de8235df.png" alt="img" /></p>
<p>在 Linux 系统里只要有空闲的内存,系统就会自动地把读写过的磁盘文件页面放入到 Page Cache 里。那么这些内存都被 Page Cache 占用了,一旦进程需要用到更多的物理内存,执行 malloc() 调用做申请时,就会发现剩余的物理内存不够了,那该怎么办呢?</p>
<p>这就要提到 Linux 的内存管理机制了。 Linux 的内存管理有一种内存页面回收机制page frame reclaim会根据系统里空闲物理内存是否低于某个阈值wartermark来决定是否启动内存的回收。</p>
<p>内存回收的算法会根据不同类型的内存以及内存的最近最少用原则,就是 LRULeast Recently Used算法决定哪些内存页面先被释放。因为 Page Cache 的内存页面只是起到 Cache 作用,自然是会被优先释放的。</p>
<p>所以Page Cache 是一种为了提高磁盘文件读写性能而利用空闲物理内存的机制。同时,内存管理中的页面回收机制,又能保证 Cache 所占用的页面可以及时释放,这样一来就不会影响程序对内存的真正需求了。</p>
<h3>RSS &amp; Page Cache in Memory Cgroup</h3>
<p>学习了 RSS 和 Page Cache 的基本概念之后,我们下面来看不同类型的内存,特别是 RSS 和 Page Cache 是如何影响 Memory Cgroup 的工作的。</p>
<p>我们先从 Linux 的内核代码看一下,从 mem_cgroup_charge_statistics() 这个函数里,我们可以看到 Memory Cgroup 也的确只是统计了 RSS 和 Page Cache 这两部分的内存。</p>
<p>RSS 的内存,就是在当前 Memory Cgroup 控制组里所有进程的 RSS 的总和;而 Page Cache 这部分内存是控制组里的进程读写磁盘文件后,被放入到 Page Cache 里的物理内存。</p>
<p><img src="assets/a55c7d3e74e17yy613d83e220f93223e.png" alt="img" /></p>
<p>Memory Cgroup 控制组里 RSS 内存和 Page Cache 内存的和,正好是 memory.usage_in_bytes 的值。</p>
<p>当控制组里的进程需要申请新的物理内存,而且 memory.usage_in_bytes 里的值超过控制组里的内存上限值 memory.limit_in_bytes这时我们前面说的 Linux 的内存回收page frame reclaim就会被调用起来。</p>
<p>那么在这个控制组里的 page cache 的内存会根据新申请的内存大小释放一部分,这样我们还是能成功申请到新的物理内存,整个控制组里总的物理内存开销 memory.usage_in_bytes 还是不会超过上限值 memory.limit_in_bytes。</p>
<h2>解决问题</h2>
<p>明白了 Memory Cgroup 中内存类型的统计方法,我们再回过头看这一讲开头的问题,为什么 memory.usage_in_bytes 与 memory.limit_in_bytes 的值只相差了 90KB我们在容器中还是可以申请出 50MB 的物理内存?</p>
<p>我想你应该已经知道答案了,容器里肯定有大于 50MB 的内存是 Page Cache因为作为 Page Cache 的内存在系统需要新申请物理内存的时候(作为 RSS是可以被释放的。</p>
<p>知道了这个答案,那么我们怎么来验证呢?验证的方法也挺简单的,在 Memory Cgroup 中有一个参数 memory.stat可以显示在当前控制组里各种内存类型的实际的开销。</p>
<p>那我们还是拿这一讲的容器例子,再跑一遍代码,这次要查看一下 memory.stat 里的数据。</p>
<p>第一步,我们还是用同样的脚本来启动容器,并且设置好容器的 Memory Cgroup 里的 memory.limit_in_bytes 值为 100MB。</p>
<p>启动容器后,这次我们不仅要看 memory.usage_in_bytes 的值,还要看一下 memory.stat。虽然 memory.stat 里的参数有不少,但我们目前只需要关注&quot;cache&quot;&quot;rss&quot;这两个值。</p>
<p>我们可以看到容器启动后cache也就是 Page Cache 占的内存是 99508224bytes大概是 99MB而 RSS 占的内存只有 1826816bytes也就是 1MB 多一点。</p>
<p>这就意味着,在这个容器的 Memory Cgroup 里大部分的内存都被用作了 Page Cache而这部分内存是可以被回收的。</p>
<p><img src="assets/b5c30dec6f760fbafd81de264b323dfe.png" alt="img" /></p>
<p>那么我们再执行一下我们的mem_alloc 程序,申请 50MB 的物理内存。</p>
<p>我们可以再来查看一下 memory.stat这时候 cache 的内存值降到了 46632960bytes大概 46MB而 rss 的内存值到了 54759424bytes54MB 左右吧。总的 memory.usage_in_bytes 值和之前相比,没有太多的变化。</p>
<p><img src="assets/c6835495bd44817cb5c60069d491c38a.png" alt="img" /></p>
<p>从这里我们发现Page Cache 内存对我们判断容器实际内存使用率的影响,目前 Page Cache 完全就是 Linux 内核的一个自动的行为,只要读写磁盘文件,只要有空闲的内存,就会被用作 Page Cache。</p>
<p>所以,判断容器真实的内存使用量,我们不能用 Memory Cgroup 里的 memory.usage_in_bytes而需要用 memory.stat 里的 rss 值。这个很像我们用 free 命令查看节点的可用内存,不能看&quot;free&quot;字段下的值,而要看除去 Page Cache 之后的&quot;available&quot;字段下的值。</p>
<h2>重点总结</h2>
<p>这一讲我想让你知道,每个容器的 Memory Cgroup 在统计每个控制组的内存使用时包含了两部分RSS 和 Page Cache。</p>
<p>RSS 是每个进程实际占用的物理内存,它包括了进程的代码段内存,进程运行时需要的堆和栈的内存,这部分内存是进程运行所必须的。</p>
<p>Page Cache 是进程在运行中读写磁盘文件后,作为 Cache 而继续保留在内存中的,它的目的是为了提高磁盘文件的读写性能。</p>
<p>当节点的内存紧张或者 Memory Cgroup 控制组的内存达到上限的时候Linux 会对内存做回收操作,这个时候 Page Cache 的内存页面会被释放,这样空出来的内存就可以分配给新的内存申请。</p>
<p>正是 Page Cache 内存的这种 Cache 的特性对于那些有频繁磁盘访问容器我们往往会看到它的内存使用率一直接近容器内存的限制值memory.limit_in_bytes。但是这时候我们并不需要担心它内存的不够 我们在判断一个容器的内存使用状况的时候,可以把 Page Cache 这部分内存使用量忽略,而更多的考虑容器中 RSS 的内存使用量。</p>
<h2>思考题</h2>
<p>在容器里启动一个写磁盘文件的程序,写入 100MB 的数据,查看写入前和写入后,容器对应的 Memory Cgroup 里 memory.usage_in_bytes 的值以及 memory.stat 里的 rss/cache 值。</p>
<p>欢迎在留言区写下你的思考或疑问,我们一起交流探讨。如果这篇文章让你有所收获,也欢迎你分享给更多的朋友,一起学习进步。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/容器实战高手课/08 容器内存:我的容器为什么被杀了?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/容器实战高手课/10 Swap容器可以使用Swap空间吗.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":"70997792ab3d3cfa","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>