Files
David Young d0d428f3bf build site
2026-05-14 14:23:58 -06:00

993 lines
35 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>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
(function() {
const autoTheme = false;
if (autoTheme) {
document.documentElement.setAttribute('data-auto-theme', 'true');
}
const theme = localStorage.getItem('cleanwhite-theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
<meta property="og:site_name" content="David Young">
<meta property="og:type" content="article">
<meta property="og:image" content="https://davidpaulyoung.com//img/istio-canary-release/canary_bg.jpg">
<meta property="twitter:image" content="https://davidpaulyoung.com//img/istio-canary-release/canary_bg.jpg" />
<meta name="title" content="采用Istio实现灰度发布(金丝雀发布)" />
<meta property="og:title" content="采用Istio实现灰度发布(金丝雀发布)" />
<meta property="twitter:title" content="采用Istio实现灰度发布(金丝雀发布)" />
<meta name="description" content="当应用上线以后运维面临的一大挑战是如何能在不影响已上线业务的情况下进行升级。本文将介绍如何使用Istio实现应用的灰度发布金丝雀发布">
<meta property="og:description" content="当应用上线以后运维面临的一大挑战是如何能在不影响已上线业务的情况下进行升级。本文将介绍如何使用Istio实现应用的灰度发布金丝雀发布" />
<meta property="twitter:description" content="当应用上线以后运维面临的一大挑战是如何能在不影响已上线业务的情况下进行升级。本文将介绍如何使用Istio实现应用的灰度发布金丝雀发布" />
<meta property="og:url" content="https://davidpaulyoung.com/2017/11/08/istio-canary-release/" />
<meta property="twitter:card" content="summary" />
<meta name="keyword" content="Von Balthasar, Scripture, Gravel Riding, Ham Radio, Divine Office, Open Source">
<link rel="shortcut icon" href="/img/favicon.ico">
<title>采用Istio实现灰度发布(金丝雀发布) | David Young Blog</title>
<link rel="canonical" href="/2017/11/08/istio-canary-release/">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/hugo-theme-cleanwhite.css">
<link rel="stylesheet" href="/css/theme-variables.css">
<link rel="stylesheet" href="/css/zanshang.min.css">
<link rel="stylesheet" href="/css/font-awesome.all.min.css">
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/hux-blog.min.js"></script>
<script src="/js/lazysizes.min.js"></script>
</head>
<nav class="navbar navbar-default navbar-custom navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header page-scroll">
<button type="button" class="navbar-toggle">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">David Young</a>
</div>
<div id="huxblog_navbar">
<div class="navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li>
<a href="/">All Posts</a>
</li>
<li>
<a href="/categories/life/">life</a>
</li>
<li>
<a href="/categories/tech/">tech</a>
</li>
<li>
<a href="/categories/tips/">tips</a>
</li>
<li><a href="/archive//">ARCHIVE</a></li>
<li><a href="/notes//">NOTES</a></li>
<li><a href="/about//">ABOUT</a></li>
<li>
<a href="/search"><i class="fa fa-search"></i></a>
</li>
<li>
<a href="#" id="theme-toggle" title="Toggle dark mode" style="opacity: 0;">
<i class="fa fa-moon"></i>
<i class="fa fa-sun" style="display: none;"></i>
</a>
</li>
<script>
(function() {
var theme = localStorage.getItem('cleanwhite-theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
var toggleBtn = document.getElementById('theme-toggle');
if (toggleBtn) {
var moonIcon = toggleBtn.querySelector('.fa-moon');
var sunIcon = toggleBtn.querySelector('.fa-sun');
if (theme === 'dark') {
if (moonIcon) moonIcon.style.display = 'none';
if (sunIcon) sunIcon.style.display = 'inline';
toggleBtn.setAttribute('title', 'Switch to light mode');
} else {
if (moonIcon) moonIcon.style.display = 'inline';
if (sunIcon) sunIcon.style.display = 'none';
toggleBtn.setAttribute('title', 'Switch to dark mode');
}
requestAnimationFrame(function() {
toggleBtn.style.transition = 'opacity 0.2s ease';
toggleBtn.style.opacity = '1';
});
}
})();
</script>
</ul>
</div>
</div>
</div>
</nav>
<script>
var $body = document.body;
var $toggle = document.querySelector('.navbar-toggle');
var $navbar = document.querySelector('#huxblog_navbar');
var $collapse = document.querySelector('.navbar-collapse');
$toggle.addEventListener('click', handleMagic)
function handleMagic(e){
if ($navbar.className.indexOf('in') > 0) {
$navbar.className = " ";
setTimeout(function(){
if($navbar.className.indexOf('in') < 0) {
$collapse.style.height = "0px"
}
},400)
}else{
$collapse.style.height = "auto"
$navbar.className += " in";
}
}
document.addEventListener('DOMContentLoaded', function() {
var navLinks = document.querySelectorAll('.navbar-collapse a');
navLinks.forEach(function(link) {
link.addEventListener('click', function() {
if ($navbar.className.indexOf('in') > 0) {
$navbar.className = " ";
setTimeout(function(){
if($navbar.className.indexOf('in') < 0) {
$collapse.style.height = "0px"
}
},400)
}
});
});
});
</script>
<header class="intro-header" style="background-image: url('/img/home-bg-jeep.jpg')">
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1 ">
<div class="site-heading">
<h1>David Young </h1>
<span class="subheading">Bevonovo</span>
</div>
</div>
</div>
</div>
</header>
<article>
<div class="container">
<div class="row">
<div class="
col-lg-8 col-lg-offset-1
col-md-8 col-md-offset-1
col-sm-12
col-xs-12
post-container">
<h2 id="灰度发布又名金丝雀发布介绍">灰度发布(又名金丝雀发布)介绍</h2>
<p>当应用上线以后,运维面临的一大挑战是如何能够在不影响已上线业务的情况下进行升级。做过产品的同学都清楚,不管在发布前做过多么完备的自动化和人工测试,在发布后都会出现或多或少的故障。根据墨菲定律,可能会出错的版本发布一定会出错。</p>
<p>&ldquo;ANYTHING THAN CAN GO WRONG WILL GO WRONG&rdquo; &ndash;MURPHY&rsquo;S LAW</p>
<p>因此我们不能寄希望于在线下测试时发现所有潜在故障。在无法百分百避免版本升级故障的情况下,需要通过一种方式进行可控的版本发布,把故障影响控制在可以接受的范围内,并可以快速回退。</p>
<p>可以通过<a href="https://martinfowler.com/bliki/CanaryRelease.html">灰度发布(又名金丝雀发布)</a>来实现业务从老版本到新版本的平滑过渡,并避免升级过程中出现的问题对用户造成的影响。</p>
<p>“金丝雀发布”的来源于矿工们用金丝雀对矿井进行空气测试的做法。以前矿工挖煤的时候,矿工下矿井前会先把金丝雀放进去,或者挖煤的时候一直带着金丝雀。金丝雀对甲烷和一氧化碳浓度比较敏感,会先报警。所以大家都用“金丝雀”来搞最先的测试。</p>
<p>下图中左下方的少部分用户就被当作“金丝雀”来用于测试新上线的1.1版本。如果新版本出现问题,“金丝雀”们会报警,但不会影响其他用户业务的正常运行。
<img src="/img/istio-canary-release/canary-deployment.PNG" alt="Istio灰度发布示意图">
</p>
<p>灰度发布(金丝雀发布)的流程如下:</p>
<ul>
<li>准备和生产环境隔离的“金丝雀”服务器。</li>
<li>将新版本的服务部署到“金丝雀”服务器上。</li>
<li>对“金丝雀”服务器上的服务进行自动化和人工测试。</li>
<li>测试通过后,将“金丝雀”服务器连接到生产环境,将少量生产流量导入到“金丝雀”服务器中。</li>
<li>如果在线测试出现问题,则通过把生产流量从“金丝雀”服务器中重新路由到老版本的服务的方式进行回退,修复问题后重新进行发布。</li>
<li>如果在线测试顺利,则逐渐把生产流量按一定策略逐渐导入到新版本服务器中。</li>
<li>待新版本服务稳定运行后,删除老版本服务。</li>
</ul>
<h2 id="istio实现灰度发布金丝雀发布的原理">Istio实现灰度发布(金丝雀发布)的原理</h2>
<p>从上面的流程可以看到,如果要实现一套灰度发布的流程,需要应用程序和运维流程对该发布过程进行支持,工作量和难度的挑战是非常大的。虽然面对的问题类似,但每个企业或组织一般采用不同的私有化实现方案来进行灰度发布,为解决该问题导致研发和运维花费了大量的成本。</p>
<p>Istio通过高度的抽象和良好的设计采用一致的方式解决了该问题采用sidecar对应用流量进行了转发通过Pilot下发路由规则可以在不修改应用程序的前提下实现应用的灰度发布。</p>
<p>备注采用kubernetes的<a href="https://kubernetes.io/docs/tasks/run-application/rolling-update-replication-controller/">滚动升级(rolling update)</a>功能也可以实现不中断业务的应用升级,但滚动升级是通过逐渐使用新版本的服务来替换老版本服务的方式对应用进行升级,在滚动升级不能对应用的流量分发进行控制,因此无法采用受控地把生产流量逐渐导流到新版本服务中,也就无法控制服务升级对用户造成的影响。</p>
<p>采用Istio后可以通过定制路由规则将特定的流量如指定特征的用户导入新版本服务中在生产环境下进行测试同时通过渐进受控地导入生产流量可以最小化升级中出现的故障对用户的影响。并且在同时存在新老版本服务时还可根据应用压力对不同版本的服务进行独立的缩扩容非常灵活。采用Istio进行灰度发布的流程如下图所示
<img src="/img/istio-canary-release/canary-deployments.gif" alt="Istio灰度发布示意图">
</p>
<h2 id="操作步骤">操作步骤</h2>
<p>下面采用Istion自带的BookinfoInfo示例程序来试验灰度发布的流程。</p>
<h3 id="测试环境安装">测试环境安装</h3>
<p>首先参考<a href="http://zhaohuabing.com/2017/11/04/istio-install_and_example/">手把手教你从零搭建Istio及Bookinfo示例程序</a>安装Kubernetes及Istio控制面。</p>
<p>因为本试验并不需要安装全部3个版本的reviews服务因此如果已经安装了该应用先采用下面的命令卸载。</p>
<pre tabindex="0"><code>istio-0.2.10/samples/bookinfo/kube/cleanup.sh
</code></pre><h3 id="部署v1版本的服务">部署V1版本的服务</h3>
<p>首先只部署V1版本的Bookinfo应用程序。由于示例中的yaml文件中包含了3个版本的reviews服务我们先将V2和V3版本的Deployment从yaml文件istio-0.2.10/samples/bookinfo/kube/bookinfo.yaml中删除。</p>
<p>从Bookinfo.yaml中删除这部分内容:</p>
<pre tabindex="0"><code>apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: reviews-v2
spec:
replicas: 1
template:
metadata:
labels:
app: reviews
version: v2
spec:
containers:
- name: reviews
image: istio/examples-bookinfo-reviews-v2:0.2.3
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9080
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: reviews-v3
spec:
replicas: 1
template:
metadata:
labels:
app: reviews
version: v3
spec:
containers:
- name: reviews
image: istio/examples-bookinfo-reviews-v3:0.2.3
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9080
---
</code></pre><p>部署V1版本的Bookinfo程序。</p>
<pre tabindex="0"><code>kubectl apply -f &lt;(istioctl kube-inject -f istio-0.2.10/samples/bookinfo/kube/bookinfo.yaml)
</code></pre><p>通过kubectl命令行确认pod部署可以看到只有V1版本的服务。</p>
<pre tabindex="0"><code>kubectl get pods
NAME READY STATUS RESTARTS AGE
details-v1-3688945616-nhkqk 2/2 Running 0 2m
productpage-v1-2055622944-m3fql 2/2 Running 0 2m
ratings-v1-233971408-0f3s9 2/2 Running 0 2m
reviews-v1-1360980140-0zs9z 2/2 Running 0 2m
</code></pre><p>在浏览器中打开应用程序页面地址为istio-ingress的External IP。由于V1版本的reviews服务并不会调用rating服务因此可以看到Product 页面显示的是不带星级的评价信息。</p>
<p><code>http://10.12.25.116/productpage</code><br>
<img src="//img/istio-canary-release/product-page-default.PNG" alt="">
</p>
<p>此时系统中微服务的部署情况如下图所示下面的示意图均忽略和本例关系不大的details和ratings服务
<img src="//img/istio-canary-release/canary-example-only-v1.PNG" alt="">
</p>
<h3 id="部署v2版本的reviews服务">部署V2版本的reviews服务</h3>
<p>在部署V2版本的reviews服务前需要先创建一条缺省路由规则route-rule-default-reviews.yaml将所有生产流量都导向V1版本避免对线上用户的影响。</p>
<pre tabindex="0"><code>apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
name: reviews-default
spec:
destination:
name: reviews
precedence: 1
route:
- labels:
version: v1
</code></pre><p>启用该路由规则。</p>
<pre tabindex="0"><code>istioctl create -f route-rule-default-reviews.yaml -n default
</code></pre><p>创建一个V2版本的部署文件bookinfo-reviews-v2.yaml内容如下</p>
<pre tabindex="0"><code>apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: reviews-v2
spec:
replicas: 1
template:
metadata:
labels:
app: reviews
version: v2
spec:
containers:
- name: reviews
image: istio/examples-bookinfo-reviews-v2:0.2.3
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9080
</code></pre><p>部署V2版本的reviews服务。</p>
<pre tabindex="0"><code>kubectl apply -f &lt;(istioctl kube-inject -f bookinfo-reviews-v2.yaml)
</code></pre><p>此时系统中部署了V1和V2两个版本的reviews服务但所有的业务流量都被规则reviews-default导向了V1如下图所示
<img src="/img/istio-canary-release/canary-example-deploy-v2.PNG" alt="">
</p>
<h3 id="将测试流量导入到v2版本的reviews服务">将测试流量导入到V2版本的reviews服务</h3>
<p>在进行模拟测试时,由于测试环境和生产环境的网络,服务器,操作系统等环境存在差异,很难完全模拟生产环境进行测试。为了减少环境因素的对测试结果的影响,我们希望能在生产环境中进行上线前的测试,但如果没有很好的隔离措施,可能会导致测试影响已上线的业务,对企业造成损失。</p>
<p>通过采用Istio的路由规则可以在类生产环境中进行测试又完全隔离了线上用户的生产流量和测试流量最小化模拟测试对已上线业务的影响。如下图所示
<img src="/img/istio-canary-release/canary-example-route-test.PNG" alt="">
</p>
<p>创建一条规则,将用户名为 test-user 的流量导入到V2</p>
<pre tabindex="0"><code>apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
name: reviews-test-user
spec:
destination:
name: reviews
precedence: 2
match:
request:
headers:
cookie:
regex: &#34;^(.*?;)?(user=test-user)(;.*)?$&#34;
route:
- labels:
version: v2
</code></pre><p>注意precedence属性用于设置规则的优先级在同时存在多条规则的情况下优先级高的规则将先执行。这条规则的precedence设置为2以确保其在缺省规则之前运行将test-user用户的请求导流到V2版本reviews服务中。</p>
<p>启用该规则。</p>
<pre tabindex="0"><code>istioctl create -f route-rule-test-reviews-v2.yaml -n default
</code></pre><p>以test-user用户登录可以看到V2版本带星级的评价页面。
<img src="/img/istio-canary-release/product-page-test-user.PNG" alt="">
</p>
<p>注销test-user只能看到V1版本不带星级的评价页面。如下图所示
<img src="/img/istio-canary-release/product-page-default.PNG" alt="">
</p>
<h3 id="将部分生产流量导入到v2版本的reviews服务">将部分生产流量导入到V2版本的reviews服务</h3>
<p>在线上模拟测试完成后如果系统测试情况良好可以通过规则将一部分用户流量导入到V2版本的服务中进行小规模的“金丝雀”测试。</p>
<p>修改规则route-rule-default-reviews.yaml将50%的流量导入V2版本。</p>
<blockquote>
<p>备注本例只是描述原理因此为简单起见将50%流量导入V2版本在实际操作中更可能是先导入较少流量然后根据监控的新版本运行情况将流量逐渐导入如采用5%10%20%50% &hellip;的比例逐渐导入。</p>
</blockquote>
<pre tabindex="0"><code>apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
name: reviews-default
spec:
destination:
name: reviews
precedence: 1
route:
- labels:
version: v1
weight: 50
- labels:
version: v2
weight: 50
</code></pre><pre tabindex="0"><code>istioctl replace -f route-rule-default-reviews.yaml -n default
</code></pre><p>此时系统部署如下图所示:
<img src="/img/istio-canary-release/canary-example-route-production-50.PNG" alt="">
</p>
<h3 id="将所有生产流量导入到到v2版本的reviews服务">将所有生产流量导入到到V2版本的reviews服务</h3>
<p>如果新版本的服务运行正常则可以将所有流量导入到V2版本。</p>
<pre tabindex="0"><code>apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
name: reviews-default
spec:
destination:
name: reviews
precedence: 1
route:
- labels:
version: v2
weight: 100
</code></pre><pre tabindex="0"><code>istioctl replace -f route-rule-default-reviews.yaml -n default
</code></pre><p>系统部署如下图所示:
<img src="/img/istio-canary-release/canary-example-route-production-100.PNG" alt="">
</p>
<p>此时不管以任何用户登录都只能看到V2版本带星级的评价页面如下图所示
<img src="/img/istio-canary-release/product-page-default-v2.PNG" alt="">
</p>
<blockquote>
<p>备注如果灰度发布的过程中新版本的服务出现问题则可以通过修改路由规则将流量重新导入到V1版本的服务中将V2版本故障修复后再进行测试。</p>
</blockquote>
<h3 id="删除v1版本的reviews服务">删除V1版本的reviews服务</h3>
<p>待V2版本上线稳定运行后删除V1版本的reviews服务和测试规则。</p>
<pre tabindex="0"><code>kubectl delete pod reviews-v1-1360980140-0zs9z
istioctl delete -f route-rule-test-reviews-v2.yaml -n default
</code></pre><h2 id="参考">参考</h2>
<ul>
<li><a href="https://istio.io/docs/">Istio官方文档</a></li>
</ul>
<link href="https://xxx.xxx.com/dist/Artalk.css" rel="stylesheet" />
<script src="https://xxx.xxx.com/dist/Artalk.js"></script>
<div id="Comments"></div>
<script>
Artalk.init({
el: '#Comments',
pageKey: 'https:\/\/davidpaulyoung.com\/2017\/11\/08\/istio-canary-release\/',
pageTitle: '采用Istio实现灰度发布(金丝雀发布)',
server: 'https:\/\/xxx.xxx.com',
site: 'xxx blog',
})
</script>
</div>
<div class="
col-lg-3 col-lg-offset-0
col-md-3 col-md-offset-0
col-sm-12
col-xs-12
sidebar-container
">
<section class="visible-md visible-lg">
<div class="short-about">
<a href="/about">
<img src="/img/zhaohuabing.png" alt="avatar" style="cursor: pointer" />
</a>
<p>Open Source Enthusiast</p>
<ul class="list-inline">
<li>
<a href="mailto:youremail@gmail.com">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-envelope fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li>
<a target="_blank" href="/your%20wechat%20qr%20code%20image">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-weixin fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li>
<a target="_blank" href="https://github.com/yourgithub">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-github fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li>
<a target="_blank" href="https://www.linkedin.com/in/yourlinkedinid">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-linkedin fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li>
<a target="_blank" href="https://stackoverflow.com/users/yourstackoverflowid">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-stack-overflow fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
</ul>
</div>
</section>
<section>
<hr class="hidden-sm hidden-xs">
<h5>FEATURED TAGS</h5>
<div class="tags">
<a href="/tags/docker" title="docker">
docker
</a>
<a href="/tags/istio" title="istio">
istio
</a>
<a href="/tags/kubernetes" title="kubernetes">
kubernetes
</a>
<a href="/tags/microservice" title="microservice">
microservice
</a>
<a href="/tags/security" title="security">
security
</a>
<a href="/tags/service-mesh" title="service mesh">
service mesh
</a>
<a href="/tags/tips" title="tips">
tips
</a>
</div>
</section>
<section>
<hr class="hidden-sm hidden-xs">
<h5>FRIENDS</h5>
<ul class="list-inline">
<li><a target="_blank" href="https://zhaozhihan.com">Linda的博客</a></li>
</ul>
</section>
<section>
<hr class="hidden-sm hidden-xs">
<h5>LAST POSTS</h5>
<ul>
<li><a href="/2025/07/06/mathematical-formulae/">Authoring mathematical formulae</a></li>
<li><a href="/post/readme/">Clean White Theme for Hugo</a></li>
<li><a href="/2018/06/04/introducing-the-istio-v1alpha3-routing-api/">Istio v1aplha3 routing API介绍(译文)</a></li>
<li><a href="/2018/06/02/istio08/">Istio 0.8 Release发布</a></li>
<li><a href="/2018/05/24/set_up_my_ubuntu_desktop/">Everything about Setting Up My Ubuntu Desktop</a></li>
</ul>
</section>
</div>
</div>
</div>
</article>
<footer>
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<ul class="list-inline text-center">
<li>
<a href="mailto:youremail@gmail.com">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fas fa-envelope fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li>
<a target="_blank" href="/your%20wechat%20qr%20code%20image">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-weixin fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li>
<a target="_blank" href="https://github.com/yourgithub">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-github fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li>
<a target="_blank" href="https://www.linkedin.com/in/yourlinkedinid">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-linkedin fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li>
<a target="_blank" href="https://stackoverflow.com/users/yourstackoverflowid">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-stack-overflow fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li>
<a href='' rel="alternate" type="application/rss+xml" title="David Young" >
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fas fa-rss fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
</ul>
<p class="copyright text-muted">
Copyright &copy; David Young 2026
<br>
<a href="https://themes.gohugo.io/hugo-theme-cleanwhite">CleanWhite Hugo Theme</a> by <a href="https://zhaohuabing.com">Huabing</a> |
<iframe
style="margin-left: 2px; margin-bottom:-5px;"
frameborder="0" scrolling="0" width="100px" height="20px"
src="https://ghbtns.com/github-btn.html?user=zhaohuabing&repo=hugo-theme-cleanwhite&type=star&count=true" >
</iframe>
</p>
</div>
</div>
</div>
</footer>
<script>
function loadAsync(u, c) {
var d = document, t = 'script',
o = d.createElement(t),
s = d.getElementsByTagName(t)[0];
o.src = u;
if (c) { o.addEventListener('load', function (e) { c(null, e); }, false); }
s.parentNode.insertBefore(o, s);
}
</script>
<script>
if($('#tag_cloud').length !== 0){
loadAsync("/js/jquery.tagcloud.js",function(){
$.fn.tagcloud.defaults = {
color: {start: '#bbbbee', end: '#0085a1'},
};
$('#tag_cloud a').tagcloud();
})
}
</script>
<script>
(function() {
function updateTagcloudColors() {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const startColor = isDark ? '#808080' : '#bbbbee';
if($('#tag_cloud').length !== 0 && $.fn.tagcloud) {
$.fn.tagcloud.defaults = {
color: {start: startColor, end: '#0085a1'},
};
$('#tag_cloud a').tagcloud();
}
}
$(document).ready(function() {
updateTagcloudColors();
});
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'data-theme') {
updateTagcloudColors();
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
});
})();
</script>
<script>
loadAsync("https://cdn.jsdelivr.net/npm/fastclick@1.0.6/lib/fastclick.min.js", function(){
var $nav = document.querySelector("nav");
if($nav) FastClick.attach($nav);
})
</script>
<script src="/js/theme-toggle.js"></script>
<script type="text/javascript">
function generateCatalog(selector) {
_containerSelector = 'div.post-container'
var P = $(_containerSelector), a, n, t, l, i, c;
a = P.find('h1,h2,h3,h4,h5,h6');
$(selector).html('')
a.each(function () {
n = $(this).prop('tagName').toLowerCase();
i = "#" + $(this).prop('id');
t = $(this).text();
c = $('<a href="' + i + '" rel="nofollow" title="' + t + '">' + t + '</a>');
l = $('<li class="' + n + '_nav"></li>').append(c);
$(selector).append(l);
});
return true;
}
generateCatalog(".catalog-body");
$(".catalog-toggle").click((function (e) {
e.preventDefault();
$('.side-catalog').toggleClass("fold")
}))
loadAsync("\/js\/jquery.nav.js", function () {
$('.catalog-body').onePageNav({
currentClass: "active",
changeHash: !1,
easing: "swing",
filter: "",
scrollSpeed: 700,
scrollOffset: 0,
scrollThreshold: .2,
begin: null,
end: null,
scrollChange: null,
padding: 80
});
});
</script>
</body>
</html>