mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-25 12:46:41 +08:00
410 lines
24 KiB
HTML
410 lines
24 KiB
HTML
<!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>18 练习篇:应用流量无损切换技术测验.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="/专栏/Kubernetes 实践入门指南/00 为什么我们要学习 Kubernetes 技术.md.html">00 为什么我们要学习 Kubernetes 技术</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/01 重新认识 Kubernetes 的核心组件.md.html">01 重新认识 Kubernetes 的核心组件</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/02 深入理解 Kubernets 的编排对象.md.html">02 深入理解 Kubernets 的编排对象</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/03 DevOps 场景下落地 K8s 的困难分析.md.html">03 DevOps 场景下落地 K8s 的困难分析</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/04 微服务应用场景下落地 K8s 的困难分析.md.html">04 微服务应用场景下落地 K8s 的困难分析</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/05 解决 K8s 落地难题的方法论提炼.md.html">05 解决 K8s 落地难题的方法论提炼</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/06 练习篇:K8s 核心实践知识掌握.md.html">06 练习篇:K8s 核心实践知识掌握</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/07 容器引擎 containerd 落地实践.md.html">07 容器引擎 containerd 落地实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/08 K8s 集群安装工具 kubeadm 的落地实践.md.html">08 K8s 集群安装工具 kubeadm 的落地实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/09 南北向流量组件 IPVS 的落地实践.md.html">09 南北向流量组件 IPVS 的落地实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/10 东西向流量组件 Calico 的落地实践.md.html">10 东西向流量组件 Calico 的落地实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/11 服务发现 DNS 的落地实践.md.html">11 服务发现 DNS 的落地实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/12 练习篇:K8s 集群配置测验.md.html">12 练习篇:K8s 集群配置测验</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/13 理解对方暴露服务的对象 Ingress 和 Service.md.html">13 理解对方暴露服务的对象 Ingress 和 Service</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/14 应用网关 OpenResty 对接 K8s 实践.md.html">14 应用网关 OpenResty 对接 K8s 实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/15 Service 层引流技术实践.md.html">15 Service 层引流技术实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/16 Cilium 容器网络的落地实践.md.html">16 Cilium 容器网络的落地实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/17 应用流量的优雅无损切换实践.md.html">17 应用流量的优雅无损切换实践</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/专栏/Kubernetes 实践入门指南/18 练习篇:应用流量无损切换技术测验.md.html">18 练习篇:应用流量无损切换技术测验</a>
|
||
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/19 使用 Rook 构建生产可用存储环境实践.md.html">19 使用 Rook 构建生产可用存储环境实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/20 有状态应用的默认特性落地分析.md.html">20 有状态应用的默认特性落地分析</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/21 案例:分布式 MySQL 集群工具 Vitess 实践分析.md.html">21 案例:分布式 MySQL 集群工具 Vitess 实践分析</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/22 存储对象 PV、PVC、Storage Classes 的管理落地实践.md.html">22 存储对象 PV、PVC、Storage Classes 的管理落地实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/23 K8s 集群中存储对象灾备的落地实践.md.html">23 K8s 集群中存储对象灾备的落地实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Kubernetes 实践入门指南/24 练习篇:K8s 集群配置测验.md.html">24 练习篇:K8s 集群配置测验</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>18 练习篇:应用流量无损切换技术测验</h1>
|
||
<p>经过连续 5 篇相关应用流量引流相关的技术探讨,相信大家已经对 Kubernetes 的服务引流架构有了更深入的了解。常言道好记性不如烂笔头,笔者在反复练习这些参数的过程中,也是费劲了很大的一段时间才对 Kubernetes 的集群引流技术有了一些运用。以下的练习案例都是笔者认为可以加固自身知识体系的必要练习,还请大家跟随我的记录一起练习吧。</p>
|
||
<h3>练习 1:Deployment 下实现无损流量应用更新</h3>
|
||
<p>我们在更新应用的时候,往往会发现即使发布应用的时候 Kubernetes 采用了滚动更新的策略,应用流量还是会秒断一下。这个困惑在于官方文档资料的介绍中这里都是重点说可以平滑更新的。注意这里,它是平滑更新,并不是无损流量的更新。所以到底问题出在哪里呢。笔者查阅了资料,发现核心问题是 Pod 生命周期中应用的版本更新如下图,关联对象资源如 Pod、Endpoint、IPVS、Ingress/SLB 等资源的更新操作都是异步执行的。往往流量还在处理中,Pod 容器就有可能给如下图:</p>
|
||
<p><img src="assets/d52cfb90-1146-11eb-a727-b74ce73d9726.jpg" alt="3a-sync-flow" /></p>
|
||
<p>依据 Pod 容器进程生命周期流程图中,容器进程的状态变更都是异步的,如果应用部署对象 Deployment 不增加 lifecycle 参数 preStop 的配置,即使南北向流量关闭了,进程仍然还需要几秒钟处理正在执行中的会话数据,才可以优雅退出。以下为应用部署 Deployment 对象的声明式配置:</p>
|
||
<pre><code class="language-yaml">apiVersion: apps/v1
|
||
kind: Deployment
|
||
metadata:
|
||
name: nginx
|
||
spec:
|
||
replicas: 1
|
||
selector:
|
||
matchLabels:
|
||
component: nginx
|
||
progressDeadlineSeconds: 120
|
||
strategy:
|
||
type: RollingUpdate
|
||
rollingUpdate:
|
||
maxUnavailable: 0
|
||
template:
|
||
metadata:
|
||
labels:
|
||
component: nginx
|
||
spec:
|
||
terminationGracePeriodSeconds: 60
|
||
containers:
|
||
- name: nginx
|
||
image: xds2000/nginx-hostname
|
||
ports:
|
||
- name: http
|
||
containerPort: 80
|
||
protocol: TCP
|
||
readinessProbe:
|
||
httpGet:
|
||
path: /
|
||
port: 80
|
||
httpHeaders:
|
||
- name: X-Custom-Header
|
||
value: Awesome
|
||
initialDelaySeconds: 15
|
||
periodSeconds: 3
|
||
timeoutSeconds: 1
|
||
lifecycle:
|
||
preStop:
|
||
exec:
|
||
command: ["/bin/bash", "-c", "sleep 10"]
|
||
</code></pre>
|
||
<p>就绪探测器(readinessProbe)可以知道容器什么时候准备好了并可以开始接受请求流量, 当一个 Pod 内的所有容器都准备好了,才能把这个 Pod 看作就绪。 这种信号的一个用途就是控制哪个 Pod 作为 Service 的后端。 在 Pod 还没有准备好的时候,会从 Service 的负载均衡器中剔除 Pod。periodSeconds 字段指定了 kubelet 每隔 3 秒执行一次存活探测。initialDelaySeconds 字段告诉 kubelet 在执行第一次探测前应该等待 15 秒。</p>
|
||
<p>kubectl 工具手动删除某个特定的 Pod,而该 Pod 的优雅终止限期默认值是 30 秒。preStop 回调所需要的时间长于默认的优雅终止限期,你必须修改 terminationGracePeriodSeconds 属性值来使其正常工作。</p>
|
||
<p>如果 Pod 中的容器之一定义了 preStop 回调,kubelet 开始在容器内运行该回调逻辑。如果超出体面终止限期时,preStop 回调逻辑 仍在运行,kubelet 会请求给予该 Pod 的宽限期一次性增加 2 秒钟。</p>
|
||
<p>在熟练掌握这些生命周期的配置属性后,单个 Pod 的流量就可以优雅处理,这个原子能力的处理让上层高级对象的处理也可以原生支持无损流量的切换。</p>
|
||
<h3>练习 2:Ingress-nginx 流量无损切换更新应用</h3>
|
||
<p>Ingress 对象是 Kubernetes 设计的引流对象,它直接监听 Service 的 Endpoints 接口列表的变化来更新负载均衡的接口列表,当前 ingress-nginx 的负载均衡算法已经采用 Lua 编写的<strong>数加权移动平均(EWMA)算法</strong>来实现流量的平滑处理。以下例子以 Nginx OSS 版本的 Ingress 来作为范例帮助大家理解。案例如下:</p>
|
||
<pre><code class="language-bash"># 安装 Ingress
|
||
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-0.32.0/deploy/static/provider/cloud/deploy.yaml
|
||
# 验证
|
||
❯ kubectl get pods --all-namespaces -l app.kubernetes.io/name=ingress-nginx
|
||
NAMESPACE NAME READY STATUS RESTARTS AGE
|
||
ingress-nginx ingress-nginx-admission-create-j5f8z 0/1 Completed 0 11m
|
||
ingress-nginx ingress-nginx-admission-patch-btfd4 0/1 Completed 1 11m
|
||
ingress-nginx ingress-nginx-controller-866488c6d4-snp4s 1/1 Running 0 11m
|
||
</code></pre>
|
||
<p>加载应用:</p>
|
||
<pre><code>kubectl create -f sample/apple.yaml
|
||
# sample/apple.yaml
|
||
kind: Pod
|
||
apiVersion: v1
|
||
metadata:
|
||
name: apple-app
|
||
labels:
|
||
app: apple
|
||
version: apple-v1
|
||
spec:
|
||
containers:
|
||
- name: apple-app
|
||
image: hashicorp/http-echo
|
||
args:
|
||
- "-text=apple"
|
||
---
|
||
kind: Service
|
||
apiVersion: v1
|
||
metadata:
|
||
name: apple-service
|
||
spec:
|
||
selector:
|
||
version: apple-v1
|
||
ports:
|
||
- port: 5678 # Default port for image
|
||
</code></pre>
|
||
<p>加载应用:</p>
|
||
<pre><code>kubectl create -f sample/banana.yaml
|
||
# sample/banana.yaml
|
||
kind: Pod
|
||
apiVersion: v1
|
||
metadata:
|
||
name: banana-app
|
||
labels:
|
||
app: banana
|
||
version: banana-v1
|
||
spec:
|
||
containers:
|
||
- name: banana-app
|
||
image: hashicorp/http-echo
|
||
args:
|
||
- "-text=banana"
|
||
---
|
||
kind: Service
|
||
apiVersion: v1
|
||
metadata:
|
||
name: banana-service
|
||
spec:
|
||
selector:
|
||
version: banana-v1
|
||
ports:
|
||
- port: 5678 # Default port for image
|
||
</code></pre>
|
||
<p>加载 Ingress 规则:</p>
|
||
<pre><code class="language-yaml"># sample/ingress.yaml
|
||
apiVersion: extensions/v1beta1
|
||
kind: Ingress
|
||
metadata:
|
||
name: example-ingress
|
||
annotations:
|
||
ingress.kubernetes.io/rewrite-target: /
|
||
spec:
|
||
rules:
|
||
- http:
|
||
paths:
|
||
- path: /apple
|
||
backend:
|
||
serviceName: apple-service
|
||
servicePort: 5678
|
||
- path: /banana
|
||
backend:
|
||
serviceName: banana-service
|
||
servicePort: 5678
|
||
</code></pre>
|
||
<p>当你切换 ingress.yaml 里面的路由规则的时候,反向代理就会 reload 更新一次,连接就会中断。为了解决这个问题,我们必须防止 Ingress 发生变更,只在 Service 对象通过更新 selector 的方式变更 Endpoints 对象集合,因为 Ingress 的更新机制就是监听 Endpoints,会自动热加载更改代理配置,实现无重启的流量切换。更新例子如下:</p>
|
||
<pre><code class="language-bash"># 这里 Service 的更新会触发 IPVS 更新 endpoints 的 Pod 地址,
|
||
export RELEASE_VERSION=banana-v2
|
||
kubectl patch svc default-www -p '{"spec":{"selector":{"version":"'${RELEASE_VERSION}'"}}}'
|
||
</code></pre>
|
||
<p>Ingress 监听 Endpoints 的 lua 函数如下:</p>
|
||
<pre><code class="language-lua"># https://github.com/kubernetes/ingress-nginx/blob/8f413c4231a727b28c4c137a0fb3f7a790f156c4/rootfs/etc/nginx/lua/util.lua#L100
|
||
-- diff_endpoints compares old and new
|
||
-- and as a first argument returns what endpoints are in new
|
||
-- but are not in old, and as a second argument it returns
|
||
-- what endpoints are in old but are in new.
|
||
-- Both return values are normalized (ip:port).
|
||
function _M.diff_endpoints(old, new)
|
||
local endpoints_added, endpoints_removed = {}, {}
|
||
local normalized_old = normalize_endpoints(old)
|
||
local normalized_new = normalize_endpoints(new)
|
||
for endpoint_string, _ in pairs(normalized_old) do
|
||
if not normalized_new[endpoint_string] then
|
||
table.insert(endpoints_removed, endpoint_string)
|
||
end
|
||
end
|
||
for endpoint_string, _ in pairs(normalized_new) do
|
||
if not normalized_old[endpoint_string] then
|
||
table.insert(endpoints_added, endpoint_string)
|
||
end
|
||
end
|
||
return endpoints_added, endpoints_removed
|
||
end
|
||
</code></pre>
|
||
<h3>练习 3:通过 Traefik 实现零停机时间部署</h3>
|
||
<p>因为 Traefik 可以直接和 Kubernetes Apiserver 进行交互,所以对于流量的切换和部署会比 ingress-nginx 更加便捷。Traefik 在 Kubernetes 中也是一个 Ingress 对象,在第二个练习中我们已经介绍了通过 Service 的 selector 切换实现无损流量的部署方法,第三个例子我们介绍另外三种比较流行的方法,蓝绿部署、金丝雀发布和 A/B 测试。虽然这三种方式都有关联,但也各有不同。</p>
|
||
<p>通过 Kubernetes 不可变基础设施的支持,我们可以让同一软件的多个版本实例在同一集群内服务于请求,这种模式会让试验变得非常有趣。像这样混合使用新旧版本,就可以配置路由规则来测试生产环境的最新版本。更重要的是,新版本可以逐步发布——如果出现问题,甚至可以撤回——所有这一切几乎都没有停机时间。</p>
|
||
<p>蓝绿发布模式下,"绿色 "指的是应用的当前稳定版本,而“蓝色”指的是引入新功能和修复的即将发布的版本。两个版本的实例同时在同一生产环境中运行。同时,代理服务(如 Traefik)确保只有发送到私有地址的请求才能到达蓝色实例。例子如下图:</p>
|
||
<p><img src="assets/5d56ec60-1147-11eb-b3d0-8f726182606e.jpg" alt="3a-blue-green-deploy" /></p>
|
||
<p>金丝雀发布模式将蓝绿测试又向前推进了一步,用一种谨慎的方式将新功能和补丁部署到活跃的生产环境中。路由配置让当前的稳定版本处理大多数请求,但有限比例的请求会被路由到新的“金丝雀”版本的实例。例子如下:</p>
|
||
<p><img src="assets/73c3e3e0-1147-11eb-be04-b99e4a609d35.jpg" alt="3a-canary-releases.png" /></p>
|
||
<p>A/B 测试技术有时会与前两种技术混淆,但它有自己的目的,即评估即将发布的版本的两个不同的版本,看看哪个版本会更成功。这种策略在 UI 开发中很常见。例如,假设一个新功能很快就会推出到应用程序中,但不清楚如何最好地将其暴露给用户。为了找出答案,包括该功能在内的两个版本的 UI,同时运行 A 版本和 B 版本,代理路由器向每个版本发送有限数量的请求。例子如下:</p>
|
||
<p><img src="assets/6d1b7360-1155-11eb-be04-b99e4a609d35.jpg" alt="A/B 测试" /></p>
|
||
<p>这些技术对于测试现代的云原生软件架构是非常宝贵的,尤其是与传统的瀑布式部署模型相比。如果使用得当,它们可以帮助发现生产环境中不可预见的回归、集成失败、性能瓶颈和可用性问题,但要在新代码进入稳定的生产版本之前。</p>
|
||
<p>这三种方法的共同点是,它们依靠容器和 Kubernetes 提供的部署便利性,加上云原生网络技术,将请求路由到可测试的部署,同时最大限度地减少对生产代码的干扰。这是一个强大的组合——这正是 Traefik 的优势所在——如果明智地使用,它可以有效地将整体应用程序的停机时间降至零。</p>
|
||
<h3>总结</h3>
|
||
<p>应用的流量更新复杂程度涉及到应用状态的变更,以上的例子只是在有限的环境中验证无损流量的切换思路。在真实场景中,我们还需要考虑数据库,业务系统等关联应用的影响,很难像习题中一样直接用无状态应用来随意切换。但是这些因素都不妨碍我们确认一个事实,Kubernetes 确实可以通过参数实现无损流量的切换,它是可行的一套基础设施,你需要深入理解并掌握这些基本对象的实现细节,通过合理的配置就可以实现你需要的不可变基础设施。</p>
|
||
<h3>参考资料</h3>
|
||
<ul>
|
||
<li><a href="https://kubernetes.io/zh/docs/concepts/workloads/pods/pod-lifecycle/">https://kubernetes.io/zh/docs/concepts/workloads/pods/pod-lifecycle/</a></li>
|
||
<li><a href="https://itnext.io/improving-zero-downtime-on-kubernetes-95e3a2f623ba">https://itnext.io/improving-zero-downtime-on-kubernetes-95e3a2f623ba</a></li>
|
||
<li><a href="https://github.com/kubernetes/ingress-nginx/blob/0549d9b132f54d0ac8d956bc78427cd2734ffdcd/rootfs/etc/nginx/lua/balancer/ewma.lua">https://github.com/kubernetes/ingress-nginx/blob/0549d9b132f54d0ac8d956bc78427cd2734ffdcd/rootfs/etc/nginx/lua/balancer/ewma.lua</a></li>
|
||
<li><a href="https://traefik.io/blog/achieve-zero-downtime-deployments-with-traefik-and-kubernetes/">https://traefik.io/blog/achieve-zero-downtime-deployments-with-traefik-and-kubernetes/</a></li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/专栏/Kubernetes 实践入门指南/17 应用流量的优雅无损切换实践.md.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/专栏/Kubernetes 实践入门指南/19 使用 Rook 构建生产可用存储环境实践.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":"7099727e9dfe3d60","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>
|