Xinzhao's Blog
2023-01-06T00:00:00Z
https://xinzhao.me/
Xinzhao Xu
山海心相——邵泳当代水墨展
2023-01-06T00:00:00Z
https://xinzhao.me/posts/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/
<p><img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/1.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/2.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/3.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/4.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/5.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/6.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/7.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/8.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/9.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/10.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/11.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/12.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/13.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/14.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/15.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/16.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/17.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/18.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/19.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/20.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/21.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/22.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/23.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/24.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/25.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/26.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/27.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/28.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/29.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/30.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/31.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/32.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/33.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/34.jpg" alt="" />
<img src="https://xinzhao.me/img/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/35.jpg" alt="" /></p>
<p class="caption">艺术无法实际证量它的获得或者失去。但是可以想象,小到个人,大到世界,失去这些音乐、绘画、舞蹈、装置雕塑的世界,是多么的平庸和陈昏。</p>
西安游记
2023-01-04T00:00:00Z
https://xinzhao.me/posts/2022-xian-travel-notes/
<p></p><div class="table-of-contents"><div class="toc-container-header">索引</div><ul><li><a href="https://xinzhao.me/posts/2022-xian-travel-notes/#%E5%BA%8F">序</a></li><li><a href="https://xinzhao.me/posts/2022-xian-travel-notes/#day-1-%E6%88%90%E9%83%BD%E2%80%94%E8%A5%BF%E5%AE%89">Day 1 成都—西安</a></li><li><a href="https://xinzhao.me/posts/2022-xian-travel-notes/#day-2-%E8%A5%BF%E5%AE%89">Day 2 西安</a></li><li><a href="https://xinzhao.me/posts/2022-xian-travel-notes/#day-3-%E8%A5%BF%E5%AE%89">Day 3 西安</a></li><li><a href="https://xinzhao.me/posts/2022-xian-travel-notes/#day-4-%E8%A5%BF%E5%AE%89%E2%80%94%E6%88%90%E9%83%BD">Day 4 西安—成都</a></li></ul></div><p></p>
<h2 id="%E5%BA%8F" tabindex="-1">序</h2>
<p>这次就是元旦出行,本来没放开之前没打算去哪儿,太麻烦了,我不想出去玩儿还要每天去做那个傻逼核酸检测,但后面突然就放开了,所以安排上了离成都比较近的西安,请了一天假,30 号早上从成都出发,2 号下午回成都。</p>
<h2 id="day-1-%E6%88%90%E9%83%BD%E2%80%94%E8%A5%BF%E5%AE%89" tabindex="-1">Day 1 成都—西安</h2>
<p>早上从成都出发,来回都坐高铁,一是高铁票便宜很多(机票的一半),二是坐高铁大概也只需要 4 小时,是可以接受的时间,最后也是最重要的,机票的班次太少了,而且时间也不太合适,要么太早,要么太晚。</p>
<p>下午到了就先去酒店办理入住把行李寄存了,然后先去离酒店比较近的<code>陕西历史博物馆</code>,门票免费但是一定要先在网上预约(西安大部分景点都是这样的),而且最好能找个讲解员,不然很多东西看不懂(是真的看不懂,有些东西的实际用途可能和想象中差别很大,一眼看过去都不知道是啥),也不知道它背后的故事,租的那种讲解器也行,但是讲解员体验好点,有问题可以直接问。</p>
<p><img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1574.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1579.jpg" alt="" /></p>
<p class="caption">从博物馆回来路过西安大悦城</p>
<p>晚上去了<code>大唐芙蓉园</code>,这里的夜景很好看,晚上很多穿汉服的女孩子在这儿拍照,特别专业的那种,补光灯都好几个,还有道具灯笼这些,如果你是汉服爱好者,来这里拍夜景挺合适的,我感觉比<code>大唐不夜城</code>好,<code>大唐不夜城</code>当背景的话很杂乱,毕竟它本质还是一条商业街。</p>
<p><img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1601.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1600.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1606.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1620.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1609.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1611.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1627.jpg" alt="" /></p>
<h2 id="day-2-%E8%A5%BF%E5%AE%89" tabindex="-1">Day 2 西安</h2>
<p>早上出发去<code>西安博物院</code>和<code>小雁塔</code>,它们是在同一个地方,那个地方类似于一个大公园,<code>西安博物院</code>和<code>小雁塔</code>都在里面,博物馆出来可以在里面闲逛一下。</p>
<p><code>西安博物院</code>的时候蹭了一小会儿别人的讲解,听到一些有意思的:</p>
<ul>
<li>“碑”字真正正确的写法是没有最上面那一撇的,包括“鬼”、“魂”和“魄”这些字都是,后来因为什么原因(没听清),传下来就传错了,最后就演变成现在这样了,真正的碑体都是没有那一撇的。</li>
<li>还有碑文上如果你看到“民”字的话,都会少最后一笔的,是为了避讳<code>李世民</code>。</li>
<li>唐朝官吏出行都会有很多随从,最前面还会有两个人,叫什么五百(没听清),专门负责开路的,拿着竹竿就打路上的人,让他们走开,现在骂人二百五就是从这儿来的,两个人五百,一个人就是二百五。</li>
<li>中国的瓷器工艺非常好,经常出口到国外,那个时候出口的瓷器上面就会写 <code>CHINA</code>,所以有种说法中国的英文 <code>CHINA</code> 就是瓷器的意思,还是一种说法是代表<code>秦</code>。</li>
<li>天王像脚下踩的是小鬼(有些看着比较模糊,我刚开始还以为是普通小孩儿)。
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1652.jpg" alt="" /></li>
<li>日本相机品牌<code>佳能 Canon</code>,它的日语名字其实是<code>观音</code>的意思,还有日本汽车品牌<code>马自达 MAZDA</code>,这个名字也是来源于一个宗教。</li>
</ul>
<p>中午在附近吃了饭就出发去<code>西安城墙</code>,我是从<code>永宁门</code>(南门)上去的:</p>
<p><img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1671.jpg" alt="" /></p>
<p><code>西安城墙</code>全程非常长,十多公里,要想走完其实很累,建议租一辆自行车慢慢骑,可以骑完全程:</p>
<p><img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1678.jpg" alt="" /></p>
<p>晚上就在<code>大唐不夜城</code>附近的一家烧烤店吃饭,西安终归还是算北方,这里的烧烤都是什么 5 串 10 串起点,成都可以一串一串拿,我就喜欢每样拿两串,不过这里的烤羊肉串和烤羊排是真好吃。</p>
<p>后面就在<code>大唐不夜城</code>闲逛了,夜景很漂亮,但是想要拍照的话感觉还得上无人机,从上面俯拍整条街,那天晚上人特别多,无人机拍出来应该有大唐盛世的味道。</p>
<p><img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1584.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1588.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1691.jpg" alt="" /></p>
<p>但是想吐槽一下<code>大唐不夜城</code>的建筑,漂亮是挺漂亮的,但是<code>西安美术馆</code>、<code>太平洋影城</code>、<code>陕西大剧院</code>和<code>西安音乐厅</code>,这几栋建筑长得一模一样,跟他妈复制粘贴出来的一样,我在那儿对比了半天,愣是没看出差别来,你说要统一风格我理解也认同,但是完全一样是不是太单调了点啊。</p>
<p>还有一个点,<code>大唐不夜城</code>跨年倒计时也没有,烟花也不放,真的差点意思,后来听本地的朋友说应该是为了避免人员聚集发生踩踏事件之类的,但那天晚上即便没有这些人也超级多,快 0 点的时候广场上超级多人都在那儿不动,也不知道在干啥;西安其它地方都有放烟花的,而且今年西安对烟花的管控好像挺松的,好多地方都在放烟花。</p>
<p>今天也走了四万多步,是我目前的最高记录了:
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_0320.jpg" alt="" /></p>
<p class="caption">这几天每天都两万步往上</p>
<h2 id="day-3-%E8%A5%BF%E5%AE%89" tabindex="-1">Day 3 西安</h2>
<p>今天就比较休闲了,前两天该去的景点基本都去了,本来说现在时间了再去一下<code>兵马俑</code>,但是之前没计划,现在临时买不了票索性就算了,以后有机会再去吧。</p>
<p>早上去附近的书店逛了逛,主要是找找明信片,其实之前旅游都没有寄明信片这个想法,是最近跟一个朋友学的,感觉这件事好有意义,像是把旅行的印记和记忆实体化了,希望以后出去旅行也都去找一些自己喜欢又有当地特色的明信片寄出去,给朋友,给自己;不过现在很有可能收不到,明信片容易寄丢,感觉邮政也不重视这个,有些东西还没到消亡的时候就没有生命了,总之能收到就收,收不到就算了,我在寄出去之前都会拍照(也是跟那个朋友学的),如果很久了还没收到的话就把图片发给对方。</p>
<p>下午在西安美术馆看<a href="https://xinzhao.me/posts/wisdom-beyond-the-heart-shao-yong-contemporary-ink-painting-exhibition/">山海心相——邵泳当代水墨展</a>,我个人是非常喜欢这个展览的作品,作品本身和它描述的概念和场景都非常宏大,我也拍了些作品的照片,后续可能单独发一篇博客。</p>
<p><img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1730.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1731.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_1732.jpg" alt="" /></p>
<blockquote>
<p>艺术无法实际证量它的获得或者失去。但是可以想象,小到个人,大到世界,失去这些音乐、绘画、舞蹈、装置雕塑的世界,是多么的平庸和陈昏。</p>
</blockquote>
<p>晚上约了西安的朋友吃饭,朋友请我吃了陕西菜,喝了点小酒,聊了很久,也了解了一些关于西安有意思的事情,感觉这个过程真的很棒,比走马观花式的旅游有意思很多。</p>
<p>从<code>大雁塔</code>广场路过,又聊到<code>小雁塔</code>,朋友说他也没进去过,我又想起有天我们 6 个同事吃饭,说有没有去过成都的熊猫基地,5 个成都人都没去过,唯一一个去过的是那个外地同事,后面西安的朋友说他也去过成都的熊猫基地,经典本地人不去当地的景点。</p>
<p>空气质量是我想吐槽的另一个点,成都的空气质量差到全球榜上有名,倒数 10 名以内,一般空气质量指数超过 100 我就要当场痛骂,到了西安,我说怎么感觉雾蒙蒙的,一打开手机,<strong>300</strong>,地图上一片红,红到发紫:
<img src="https://xinzhao.me/img/2022-xian-travel-notes/IMG_0286.jpg" alt="" /></p>
<p>朋友说我来的不是时候,刚好遇到可能是全年天气最差的几天了,前几天都还挺好的,然后我就好奇西安应该也没有什么重工业区,为什么空气这么差,他说大部分是地理因素导致的,西安刚好在秦岭分界线上面,因为特殊的地理结构和冬天的风向,污染物从北边过来都堆积在西安上空,无法扩散;以前最严重的时候空气质量指数就直接 500,实际上是不是 500 也没人知道,反正这个指标最高就只有 500,出去一趟回来鼻子里都是灰。朋友让我以后别冬天来了,不然就下雪的时候过来,下雪的西安才是真的绝美,一下雪,西安就变成了长安,其实可以想象,雪真的和西安的这些古建筑绝配,<code>西安城墙</code>、<code>大唐芙蓉园</code>、<code>大唐不夜城</code>、<code>大雁塔</code>和<code>钟楼鼓楼</code>这些地方肯定氛围拉满。</p>
<p>我本来还在感慨说西安的地铁其实还挺多的,比我想象中多,因为我印象中西安地下都是墓,不好修地铁,朋友说其实进度很慢了,每次一修地铁,最忙的就是文物局,一些不那么有价值的墓甚至就直接挖了,西安地下住的比地上的都多。</p>
<p>后面也聊到西安的房价,他说西安就是曲江和高新区房价高一点,曲江因为环境好一些,然后旅游资源集中在这一片,而且曲江这一片的楼都不高,应该是为了配合这边打造的旅游景区的整体风格,还有一种说法是这边的建筑物高度不能超过大雁塔,所以整体环境宜居;然后高新区那边的话就是打工人聚集的地方了,教育资源也丰富一点,学区房。</p>
<p><code>钟楼</code>那儿是西安最中心的地方,然后现在的城墙也是以它为中心建的,它是四座城门的交汇点,现在的城墙其实是近代建的,唐朝的城墙范围比现在大太多了,我们现在吃饭的地方(西安大悦城)也是在城墙范围里面的,108 坊,然后(城墙)南门到了晚上有很多年轻人在那边唱歌表演之类的,氛围很好,那儿也有很多酒吧。说起来,成都也喜欢叫南门北门的,因为成都以前也是有城墙的,但是后来基本都被毁坏了,之前说南门北门就是真的指城墙对应方位的门,现在这个说法延续了下来,代指南边北边。</p>
<h2 id="day-4-%E8%A5%BF%E5%AE%89%E2%80%94%E6%88%90%E9%83%BD" tabindex="-1">Day 4 西安—成都</h2>
<p>最后一天就没安排去哪儿玩了,早上也起得晚一点,收拾完行李就去退房,然后出去吃个午饭再出发去高铁站回成都,在西安的最后一顿是在第一顿那家店吃的,开始的地方也是结束的地方。</p>
2022 年终总结
2022-12-31T00:00:00Z
https://xinzhao.me/posts/2022-summary/
<p></p><div class="table-of-contents"><div class="toc-container-header">索引</div><ul><li><a href="https://xinzhao.me/posts/2022-summary/#%E7%94%9F%E6%B4%BB">生活</a><ul><li><a href="https://xinzhao.me/posts/2022-summary/#%F0%9F%8F%82">🏂</a></li><li><a href="https://xinzhao.me/posts/2022-summary/#%E6%97%85%E8%A1%8C">旅行</a></li></ul></li><li><a href="https://xinzhao.me/posts/2022-summary/#%E5%B7%A5%E4%BD%9C">工作</a><ul><li><a href="https://xinzhao.me/posts/2022-summary/#kcd-%E6%88%90%E9%83%BD%E7%AB%99">KCD 成都站</a></li></ul></li><li><a href="https://xinzhao.me/posts/2022-summary/#%E8%BF%99%E4%B8%80%E5%B9%B4%E4%B9%B0%E7%9A%84%E6%9C%89%E8%B6%A3%E7%9A%84%E4%B8%9C%E8%A5%BF">这一年买的有趣的东西</a><ul><li><a href="https://xinzhao.me/posts/2022-summary/#%E9%99%86%E5%9C%B0%E5%86%B2%E6%B5%AA%E6%9D%BF">陆地冲浪板</a></li><li><a href="https://xinzhao.me/posts/2022-summary/#%E7%94%B5%E5%8A%A8%E6%BB%91%E6%9D%BF">电动滑板</a></li><li><a href="https://xinzhao.me/posts/2022-summary/#be%40rbrick">BE@RBRICK</a></li><li><a href="https://xinzhao.me/posts/2022-summary/#%E7%94%B5%E8%A7%86%E5%92%8C%E8%B7%AF%E7%94%B1%E5%99%A8">电视和路由器</a></li></ul></li><li><a href="https://xinzhao.me/posts/2022-summary/#%E5%86%99%E5%9C%A8%E6%9C%80%E5%90%8E">写在最后</a></li></ul></div><p></p>
<h2 id="%E7%94%9F%E6%B4%BB" tabindex="-1">生活</h2>
<p>我还是从 Google 的 <a href="https://about.google/intl/zh_cn/stories/year-in-search/">Year in Search</a> 视频说起,今年的年度关键词是 <code>can i change</code>:</p>
<blockquote>
<p>This year, the world searched “can i change” more than ever before. From changing careers to seeking new outlooks on life, people are finding ways to reimagine themselves and reshape the world around them.</p>
<p>2022 年,随着世界重焕活力并迈入新时代,人们比以往更有热情和动力去搜寻全新的可能。</p>
</blockquote>
<p>今年于我也是改变的一年,从开始工作以来我就要每个月交钱给父母,他们帮我保管起来,他们希望我多存些钱,我知道他们是好心,但这件事真的太限制和压抑我了,我想做的事情不能做,想买的东西不能买,我也一直就逆来顺受,没想过沟通和改变,直到去年底那两个月,我换了一些电子设备,还有些杂七杂八的事情花了很多钱,两个月我都没交钱给我爸,然后他打电话来质问我,我给他解释了但他还是很生气,后面我就直接不说话把电话挂了,再后来我妈又发微信过来和我聊了下,从今年开始我就没再交钱了,自己保管,每个月剩多少存多少。</p>
<p>在夺回我的财政大权之后,我也开始真正践行<a href="https://xinzhao.me/posts/2021-summary/#%E6%B4%BB%E5%9C%A8%E5%BD%93%E4%B8%8B%E5%92%8C%E4%B8%8D%E5%8D%91%E4%B8%8D%E4%BA%A2">活在当下</a>,想买的东西就买,想做的事情马上做,把一切都献给现在,今年也尝试了很多人生中第一次做的事情。</p>
<h3 id="%F0%9F%8F%82" tabindex="-1">🏂</h3>
<p>今年年初我正式开始学习我心心念念的单板滑雪,选单板除了看起来更酷以外,还有一个很重要的原因:双板的雪杖在我看来是束缚,我不喜欢手上拿着个东西滑雪,我喜欢无拘无束的感觉。</p>
<p>到目前为止我都在融创室内雪场学习和练习单板,刚开始学起来要稍微难一点,也动不动就摔,不过千万不要因为摔跤这些气馁,我向你保证,只要稍微会一点之后,你就能体会到滑雪的快乐,贴地飞行,连风里都是自由的味道。</p>
<p>最开始图方便,我的装备都是网上随便买的,就买了那种不上不下的新手入门装备,想着等以后会点了再换好点的装备,这种想法其实不太好,就应该(在预算范围内)直接拉满,不然没玩儿多久又想换,要换的话当初买这些装备就都算额外的成本,比当初直接拉满花得更多;还有一个点,第一次买滑雪的装备最好去线下店试着买,除非你已经是老手了,否则都不建议在网上买,特别是雪鞋,一定要试试穿着是否舒适,长度是否合适,线下还可以多和店员沟通交流一下,他们见多识广,能根据你的情况给你建议和推荐。</p>
<p>说到装备还有一件事,最开始我买的固定器是一种半快穿式的,你把脚放进去,手再把背板拉起来扣上就行了,不用每次都去收紧绑带:</p>
<p><img src="https://xinzhao.me/img/2022-summary/1.jpg" alt="" /></p>
<p>听起来比传统固定器方便是吧,但其实这种不上不下的东西最垃圾,看起来两头都占,实际上两头都不占,它既没有 <a href="https://www.burton.com/us/en/content/step-on.html">Step On</a> 那种快穿方便(直接踩上去就完了,都不用弯腰),也没有传统固定器可靠和稳定,上限不高,花里胡哨的;在用了几次之后我还是决定换掉它,换成传统固定器,还是这种结构简单的东西最可靠,上限也最高。</p>
<h3 id="%E6%97%85%E8%A1%8C" tabindex="-1">旅行</h3>
<p>今年国庆长假终于下定决心要出去走走,实在不想整整 7 天又窝在家里啥也不干,再加上那段时间机会正好,成都的疫情不算太严重,至少我在的那个区是低风险的,去另一些低风险的地方不用隔离,再加上我真的挺悲观的,我担心以后的局势会越来越严格,当时不认为短期内会放开,有些人真的食髓知味,毫无代价得到了生杀予夺的权力,一条狗都以为自己是警犬吧,怎么可能轻易就放弃;所以只要有机会,我一定要出去走走。</p>
<p>最后就选了北海和涠洲岛作为目的地,内陆长大的孩子太渴望大海了,当时回来写了一篇<a href="https://xinzhao.me/posts/2022-beihai-and-weizhou-island-tour/">游记</a>,记了些流水帐、旅途中遇到的有趣的事和拍的照片,感兴趣可以移步看看,这里就不再多说了。</p>
<h2 id="%E5%B7%A5%E4%BD%9C" tabindex="-1">工作</h2>
<p>和去年不同,今年没有换工作也没有什么太大的变化,工作正常推进,没有太多值得讲的东西。</p>
<p>今年我开启了两个新的项目:</p>
<ul>
<li><a href="https://github.com/iawia002/pandora">pandora</a>: Code snippets and examples for writing Go/Kubernetes services/applications.</li>
<li><a href="https://github.com/iawia002/lia">lia</a>: Common libraries for writing Go/Kubernetes services/applications.</li>
</ul>
<p>本质上是同一类东西,这两个 repo 收集了我工作中常用到的一些 Go/Kubernetes 的开发库和代码示例,不过这还是非常个人向的项目,可能不会经常更新,我不会为了更新而更新,还是跟着我的工作节奏来,工作中有发现值得提炼成一个公共库或者值得记录的代码片段的话就会更新。</p>
<p>最初我是把所有东西都放在 <a href="https://github.com/iawia002/pandora">pandora</a> 这个 repo 里面的,后面感觉如果把它当作一个依赖库来用的话,它的描述又容易引人误解,认为这不是一个正式的项目,所以我把里面的<code>公共代码库</code>这一部分单独剥离了出来放到了 <a href="https://github.com/iawia002/lia">lia</a> 里面,<a href="https://github.com/iawia002/pandora">pandora</a> 就只包含代码片段和示例,如果有人也需要使用我的一些公共代码库的话就可以直接依赖更“正式”的 <a href="https://github.com/iawia002/lia">lia</a>。</p>
<p>这是我很早之前就应该着手做的东西,今年终于正式做起来了,代码片段这类知识库是我认为非常值得去系统构建和维护的,一方面这可以充当你的速查“词典”,当你忘记某种设计要怎么实现时,可以来翻一翻,又或者当你开始开发一个新的组件的时候,你可以直接从这个代码库来复制粘贴一些基础代码,不用从头开始写,也不用胡乱地去翻以前的项目;另一方面,归类和整理这个过程本身也会加深你对那段代码和它背后原理的理解。</p>
<h3 id="kcd-%E6%88%90%E9%83%BD%E7%AB%99" tabindex="-1">KCD 成都站</h3>
<p>今年年初的时候,那段时间我一直在参与 <a href="https://github.com/karmada-io/karmada">Karmada</a> 的贡献,华为 <a href="https://github.com/karmada-io/karmada">Karmada</a> 的人就联系我,问我要不要去 2 月底要召开的 <a href="https://community.cncf.io/kubernetes-community-days/about-kcd/">Kubernetes Community Days</a> 成都站做个关于 <a href="https://github.com/karmada-io/karmada">Karmada</a> 的演讲,我欣然地答应了,当天回去就提交了议题,最后我的议题也被选中了(虽然评分最低最后一个讲),收获了人生中第一次在技术会议上演讲的体验。</p>
<p><img src="https://xinzhao.me/img/2022-summary/2.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-summary/3.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-summary/4.jpg" alt="" /></p>
<p>不过整个过程也是非常坎坷,原定是 2 月底召开的,然后碰上疫情延期,后面就重启一次延期一次,非常离谱的是,每次看着成都没有疫情了,主办方就选日子重启,然后只要一重启,成都必再出现疫情,而且会慢慢变严重,最后就不让举办这种会议;从 2 月份一直推迟到 11 月中终于召开了,当天来的人也不多,可能是因为疫情要求 24 小时核酸,也可能是选的时间是周五的原因。</p>
<p>说到选周五还有一个插曲,那天晚上会议结束了讲师聚餐,就和主办方聊到了这件事,他们说问了一些成都人,那些人说选周六没有人会来的,因为周末成都人都在外面玩儿(这就是成都),所以才选了周五 🤷♂️。</p>
<h2 id="%E8%BF%99%E4%B8%80%E5%B9%B4%E4%B9%B0%E7%9A%84%E6%9C%89%E8%B6%A3%E7%9A%84%E4%B8%9C%E8%A5%BF" tabindex="-1">这一年买的有趣的东西</h2>
<h3 id="%E9%99%86%E5%9C%B0%E5%86%B2%E6%B5%AA%E6%9D%BF" tabindex="-1">陆地冲浪板</h3>
<p>春天的时候没再去滑雪了(虽然室内滑雪场也可以滑),当时就想着找点新的事情做,偶然了解到陆地冲浪板,感觉还挺有意思的,就买了一块来玩玩:</p>
<p><img src="https://xinzhao.me/img/2022-summary/IMG_9709.jpg" alt="" /></p>
<p>5 月初和同事们去骑<a href="http://www.cdghg.com.cn/cdsghg/c121963/2018-12/11/content_e66eb60204a54492949b201446cff5fe.shtml">天府绿道</a>,那时候我刚会一点陆冲,能 Pumping 着前进(不过姿态不好看),我不知天高地厚,直接带着陆冲板去,准备去滑天府绿道,结果刚开始就明白我这个想法有多蠢了,滑这个和他们骑自行车相比实在太慢太慢了,而且上坡上不去,下坡又刹不住(我还不会),上坡我就拉着一个同事的自行车,让他带我上去,因为拉着我,他每次就要站起来使劲蹬,事后他锐评自己:“我跟个拉黄包车的一样”,我当时真的笑不活了。</p>
<p><img src="https://xinzhao.me/img/2022-summary/IMG_1875.jpg" alt="" /></p>
<p>当天还有一个插曲,那天滑陆冲没有戴护具,当时没啥安全意识,然后在过一个桥的时候,桥每过一段就有一个拼接的缝隙,我当时没注意到那个缝隙稍微有点宽,过的时候板的轮子卡在缝里面了,板停在原地,我人直接飞出去跪在地上磨了一段,一只膝盖被磨掉一层皮,现在都还没好,那个地方新长出来的肉有点恶心,而且看起来不能恢复成正常样子了,这是真·血的教训。</p>
<h3 id="%E7%94%B5%E5%8A%A8%E6%BB%91%E6%9D%BF" tabindex="-1">电动滑板</h3>
<p>陆冲板买了没玩多久就到夏天了,太热了就没再练习,跑去游泳去了,10 月份的时候又把它翻了出来在小区里面溜达,那天刚好碰到一个玩电动滑板的,就和他聊了几句,又有点心动,然后双十一就买了一块电动滑板:</p>
<p><img src="https://xinzhao.me/img/2022-summary/IMG_9710.jpg" alt="" /></p>
<p>这个还比较好上手,和普通长板类似,适应一会儿就能满街跑了,不过还是要注意安全,这个就和电瓶车性质差不多了,头盔非常重要,一定要戴。</p>
<p>滑着电动滑板在街上和公园里面穿梭的感觉真的好棒,和滑雪那时候感受到的自由的味道非常像,御剑飞行。</p>
<p>滑着这个在外面听到的最多的一句话就是:“这是电动的吧”;在公园的时候,小朋友的家长见到我都会冲他们的小孩儿说:“小心一点,车来了”,滑板车也是车是吧。</p>
<p>当时买的时候还想着可以用来上下班代步,实践了一天,上班的时候吓死我了,早高峰各种自行车/电瓶车都比较多,我要和他们去挤,而且那天我家出来那一段地又是湿的,我那套轮胎防滑性能不怎么好,在湿地上刹车必侧滑,当时还因为这个差点摔着(当然主要是因为我注意力不集中,急刹了一次);晚上下班回去体验就非常好了,一路上人不多,可以非常自由地滑。</p>
<p>不过年底越来越冷了就代步了那一次,等明年春暖花开的时候再来,现在就用来周末去公园玩陆冲的时候代代步或者天气好的时候出去闲逛。</p>
<h3 id="be%40rbrick" tabindex="-1">BE@RBRICK</h3>
<p>今年完成了去年底年终总结立的 <a href="https://xinzhao.me/posts/2021-summary/#be%40rbrick">flag</a>,买了一个 1000% 的熊:</p>
<p><img src="https://xinzhao.me/img/2022-summary/5.jpg" alt="" /></p>
<p class="caption">BE@RBRICK x NASA Space Shuttle</p>
<h3 id="%E7%94%B5%E8%A7%86%E5%92%8C%E8%B7%AF%E7%94%B1%E5%99%A8" tabindex="-1">电视和路由器</h3>
<p>为什么提到了这两样东西呢,我并不是什么电视/路由器专家要推荐一些不错的品牌和型号之类的,事情是这样的:之前我家里面就只有客厅装了电视,但我平常一般都在自己的房间里面活动,客厅的电视也很少打开,晚上就躺床上抱着自己的电脑看些剧之类的,你说不方便吧,其实又挺方便的,躺床上放被子上面就行了,电池也基本够我晚上两三个小时不插电用,Retina 屏幕素质也不错,但你说方便吧,也没那么方便,主要是对着一块 16 寸的屏幕看剧实在谈不上什么体验感;所以今年年中我终于下手买了一台 65 寸的 4K 电视挂在房间里面,现在晚上回去就直接把电脑放桌上,连上电视,躺床上追剧,体验感不知道翻了多少倍,而且也能用来当 Switch 的外接显示器,玩游戏的体验感也直线上升。</p>
<p>再来说路由器,最开始我家里就只买了一个路由器,放在我房间的,然后入户的光猫也有 WI-FI 的功能,所以我家里就有两个不一样的 WI-FI,客厅就连光猫那个 WI-FI,在房间就连另一个,对于大部分固定的电器来说没啥问题,问题就出在手机上,我的手机只要连上了客厅的再回房间,信号不管有多弱,它也不会断开重连,虽然连接着但实际上根本没法用,就导致了我每次回房间都要手动切换一下 WI-FI,这个动作我每天都要做几次(其实很不方便,我也不知道我为什么忍了这么久);还有一个不方便的点是:有些地方刚好是两个 WI-FI 都覆盖不到的死角,比如厕所,哪个 WI-FI 都连不上,那个地方手机信号也不是很好;所以今年双 11 我终于准备好好调整一下家里的网络环境了,下单了两台路由器准备组一个网,现在一个 WI-FI 覆盖整个家,一点死角都没有,我也再不用去手动切换 WI-FI 了。</p>
<p>这两样东西都不贵,但真是我今年,甚至是近两年买过幸福感最高的东西了,没有之一;所以真的不要忍受,哪怕只有一丁点不舒服也不要忍受。</p>
<h2 id="%E5%86%99%E5%9C%A8%E6%9C%80%E5%90%8E" tabindex="-1">写在最后</h2>
<p>今年真是改变的一年,不止我自己,大环境也在年底发生巨变,百年抗疫居然提前九十七年结束,先左打死再右打死这种操作属实难以预料,既然放开了,希望明年能去更多的地方旅行,去见更多的山川河海,去体会更多的风土人情。</p>
Karmada Resource Interpreter Webhook 解析
2022-11-20T00:00:00Z
https://xinzhao.me/posts/guide-to-karmada-resource-interpreter-webhook/
<p></p><div class="table-of-contents"><div class="toc-container-header">索引</div><ul><li><a href="https://xinzhao.me/posts/guide-to-karmada-resource-interpreter-webhook/#karmada-%E4%BB%8B%E7%BB%8D">Karmada 介绍</a></li><li><a href="https://xinzhao.me/posts/guide-to-karmada-resource-interpreter-webhook/#%E4%B8%80%E4%B8%AA%E4%BE%8B%E5%AD%90%EF%BC%9A%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA-nginx-%E5%BA%94%E7%94%A8">一个例子:创建一个 nginx 应用</a></li><li><a href="https://xinzhao.me/posts/guide-to-karmada-resource-interpreter-webhook/#resource-interpreter-webhook">Resource Interpreter Webhook</a><ul><li><a href="https://xinzhao.me/posts/guide-to-karmada-resource-interpreter-webhook/#interpretreplica">InterpretReplica</a></li><li><a href="https://xinzhao.me/posts/guide-to-karmada-resource-interpreter-webhook/#revisereplica">ReviseReplica</a></li><li><a href="https://xinzhao.me/posts/guide-to-karmada-resource-interpreter-webhook/#workload-%E5%AE%9E%E7%8E%B0%E5%89%AF%E6%9C%AC%E6%95%B0%E8%B0%83%E5%BA%A6">Workload 实现副本数调度</a></li><li><a href="https://xinzhao.me/posts/guide-to-karmada-resource-interpreter-webhook/#retain">Retain</a></li><li><a href="https://xinzhao.me/posts/guide-to-karmada-resource-interpreter-webhook/#aggregatestatus">AggregateStatus</a></li></ul></li><li><a href="https://xinzhao.me/posts/guide-to-karmada-resource-interpreter-webhook/#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5">参考链接</a></li></ul></div><p></p>
<h2 id="karmada-%E4%BB%8B%E7%BB%8D" tabindex="-1">Karmada 介绍</h2>
<p>在开始讲 Resource Interpreter Webhook 之前需要对 Karmada 的基础架构以及如何分发应用等有一定的了解,但那一部分在之前的博客中已经提到过了,所以这篇文章就不再赘述了,如果需要的话可以移步到 <a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#karmada">Kubernetes 多集群项目介绍</a> 了解。</p>
<h2 id="%E4%B8%80%E4%B8%AA%E4%BE%8B%E5%AD%90%EF%BC%9A%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA-nginx-%E5%BA%94%E7%94%A8" tabindex="-1">一个例子:创建一个 nginx 应用</h2>
<p>让我们先从一个最简单的例子开始,在 Karmada 中创建并分发一个 nginx 应用;首先是准备 nginx 的资源模板,这个就是原生的 K8s <code>Deployment</code>,不需要任何改变:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> apps/v1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> Deployment<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">labels</span><span class="token punctuation">:</span><br /> <span class="token key atrule">app</span><span class="token punctuation">:</span> nginx<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">replicas</span><span class="token punctuation">:</span> <span class="token number">2</span><br /> <span class="token key atrule">selector</span><span class="token punctuation">:</span><br /> <span class="token key atrule">matchLabels</span><span class="token punctuation">:</span><br /> <span class="token key atrule">app</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">template</span><span class="token punctuation">:</span><br /> <span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">labels</span><span class="token punctuation">:</span><br /> <span class="token key atrule">app</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">containers</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">image</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx</code></pre>
<p>再准备一个 <code>PropagationPolicy</code>,用来控制 nginx 分发到哪些集群:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> policy.karmada.io/v1alpha1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> PropagationPolicy<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx<span class="token punctuation">-</span>propagation<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">resourceSelectors</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> apps/v1<br /> <span class="token key atrule">kind</span><span class="token punctuation">:</span> Deployment<br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">placement</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterAffinity</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterNames</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> member1<br /> <span class="token punctuation">-</span> member2</code></pre>
<p>这里我们就直接将它分发到 <code>member1</code> 和 <code>member2</code> 集群。</p>
<p><img src="https://xinzhao.me/img/guide-to-karmada-resource-interpreter-webhook/kubectl.png" alt="" /></p>
<p><code>member1</code> 和 <code>member2</code> 集群分别有一个副本数为 2 的 nginx <code>Deployment</code>,所以该资源一共存在 4 个 Pod。</p>
<p>上面的例子非常简单,直接在 member 集群根据模板原封不动创建 <code>Deployment</code> 就行了,但是大家知道 Karmada 是支持一些更高级的副本数调度策略的,比如下面这个例子:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">replicaScheduling</span><span class="token punctuation">:</span><br /> <span class="token key atrule">replicaDivisionPreference</span><span class="token punctuation">:</span> Weighted<br /> <span class="token key atrule">replicaSchedulingType</span><span class="token punctuation">:</span> Divided<br /> <span class="token key atrule">weightPreference</span><span class="token punctuation">:</span><br /> <span class="token key atrule">staticWeightList</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">targetCluster</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterNames</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> member1<br /> <span class="token key atrule">weight</span><span class="token punctuation">:</span> <span class="token number">1</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">targetCluster</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterNames</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> member2<br /> <span class="token key atrule">weight</span><span class="token punctuation">:</span> <span class="token number">1</span></code></pre>
<p>应用了该规则之后,会涉及到针对每个集群上资源副本数的动态调整,之后 Karmada 在 member 集群创建 <code>Deployment</code> 的时候就需要增加一个修改副本数的步骤。</p>
<p>针对 <code>Deployment</code> 这类 K8s 核心资源,因为其结构是确定的,我们可以直接编写修改其副本数的代码,但是如果我有一个功能类似 <code>Deployment</code> 的 CRD 呢?我也需要副本数调度,Karmada 能正确地修改它的副本数吗?答案是否定的,也正因此,Karmada 引入了一个新的特性来使其能深度支持自定义资源(CRD)。</p>
<h2 id="resource-interpreter-webhook" tabindex="-1">Resource Interpreter Webhook</h2>
<p>为了解决上面提到的问题,Karmada 引入了 Resource Interpreter Webhook,通过干预从 <code>ResourceTemplate</code> 到 <code>ResourceBinding</code> 到 <code>Work</code> 到 <code>Resource</code> 的这几个阶段来实现完整的自定义资源分发能力:</p>
<p><img src="https://xinzhao.me/img/guide-to-karmada-resource-interpreter-webhook/resource-interpreter-webhook.png" alt="" /></p>
<p>从一个阶段到另一个都会经过我们预定义的一个或多个接口,我们会在这些步骤中实现修改副本数等操作;用户需要增加一个单独的实现了对应接口的 webhook server,Karmada 会在执行到相应步骤时通过配置去调用该 server 来完成操作。</p>
<p>下面我们将选四个具有代表性的 hook 点来逐一介绍,接下来都使用以下 CRD 作为示例:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// Workload is a simple Deployment.</span><br /><span class="token keyword">type</span> Workload <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> metav1<span class="token punctuation">.</span>TypeMeta <span class="token string">`json:",inline"`</span><br /> metav1<span class="token punctuation">.</span>ObjectMeta <span class="token string">`json:"metadata,omitempty"`</span><br /><br /> <span class="token comment">// Spec represents the specification of the desired behavior.</span><br /> <span class="token comment">// +required</span><br /> Spec WorkloadSpec <span class="token string">`json:"spec"`</span><br /><br /> <span class="token comment">// Status represents most recently observed status of the Workload.</span><br /> <span class="token comment">// +optional</span><br /> Status WorkloadStatus <span class="token string">`json:"status,omitempty"`</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// WorkloadSpec is the specification of the desired behavior of the Workload.</span><br /><span class="token keyword">type</span> WorkloadSpec <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> <span class="token comment">// Number of desired pods. This is a pointer to distinguish between explicit</span><br /> <span class="token comment">// zero and not specified. Defaults to 1.</span><br /> <span class="token comment">// +optional</span><br /> Replicas <span class="token operator">*</span><span class="token builtin">int32</span> <span class="token string">`json:"replicas,omitempty"`</span><br /><br /> <span class="token comment">// Template describes the pods that will be created.</span><br /> Template corev1<span class="token punctuation">.</span>PodTemplateSpec <span class="token string">`json:"template" protobuf:"bytes,3,opt,name=template"`</span><br /><br /> <span class="token comment">// Paused indicates that the deployment is paused.</span><br /> <span class="token comment">// Note: both user and controllers might set this field.</span><br /> <span class="token comment">// +optional</span><br /> Paused <span class="token builtin">bool</span> <span class="token string">`json:"paused,omitempty"`</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// WorkloadStatus represents most recently observed status of the Workload.</span><br /><span class="token keyword">type</span> WorkloadStatus <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> <span class="token comment">// ReadyReplicas represents the total number of ready pods targeted by this Workload.</span><br /> <span class="token comment">// +optional</span><br /> ReadyReplicas <span class="token builtin">int32</span> <span class="token string">`json:"readyReplicas,omitempty"`</span><br /><span class="token punctuation">}</span></code></pre>
<p>它和 <code>Deployment</code> 很像,我们用来演示 Karmada 如何支持这类资源来进行副本数调度等高级特性。</p>
<h3 id="interpretreplica" tabindex="-1">InterpretReplica</h3>
<p><img src="https://xinzhao.me/img/guide-to-karmada-resource-interpreter-webhook/InterpretReplica.png" alt="" /></p>
<p>该 hook 点发生在从 <code>ResourceTemplate</code> 到 <code>ResourceBinding</code> 这个过程中,针对有 replica 功能的资源对象,比如类似 <code>Deployment</code> 的自定义资源,实现该接口来告诉 Karmada 对应资源的副本数。</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> workload.example.io/v1alpha1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> Workload<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">labels</span><span class="token punctuation">:</span><br /> <span class="token key atrule">app</span><span class="token punctuation">:</span> nginx<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">replicas</span><span class="token punctuation">:</span> <span class="token number">3</span><br /> <span class="token key atrule">template</span><span class="token punctuation">:</span><br /> <span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">labels</span><span class="token punctuation">:</span><br /> <span class="token key atrule">app</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">containers</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">image</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx</code></pre>
<p>针对我们示例的 <code>Workload</code> 资源,实现方式也非常简单,直接在 webhook server 中返回副本数的值即可:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>e <span class="token operator">*</span>workloadInterpreter<span class="token punctuation">)</span> <span class="token function">responseWithExploreReplica</span><span class="token punctuation">(</span>workload <span class="token operator">*</span>workloadv1alpha1<span class="token punctuation">.</span>Workload<span class="token punctuation">)</span> interpreter<span class="token punctuation">.</span>Response <span class="token punctuation">{</span><br /> res <span class="token operator">:=</span> interpreter<span class="token punctuation">.</span><span class="token function">Succeeded</span><span class="token punctuation">(</span><span class="token string">""</span><span class="token punctuation">)</span><br /> res<span class="token punctuation">.</span>Replicas <span class="token operator">=</span> workload<span class="token punctuation">.</span>Spec<span class="token punctuation">.</span>Replicas<br /> <span class="token keyword">return</span> res<br /><span class="token punctuation">}</span></code></pre>
<blockquote>
<p>注:所有的示例均来自 Karmada 官方文档,可以通过文章最后的 <a href="https://xinzhao.me/posts/guide-to-karmada-resource-interpreter-webhook/#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5">参考链接</a> 来查看完整的示例和代码。</p>
</blockquote>
<h3 id="revisereplica" tabindex="-1">ReviseReplica</h3>
<p><img src="https://xinzhao.me/img/guide-to-karmada-resource-interpreter-webhook/ReviseReplica.png" alt="" /></p>
<p>该 hook 点发生在从 <code>ResourceBinding</code> 到 <code>Work</code> 这个过程中,针对有 replica 功能的资源对象,需要按照 Karmada 发送的 request 来修改对象的副本数。Karmada 会通过调度策略把每个集群需要的副本数计算好,你需要做的只是把最后计算好的值赋给你的 CR 对象(因为 Karmada 并不知道该 CRD 的结构):</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>e <span class="token operator">*</span>workloadInterpreter<span class="token punctuation">)</span> <span class="token function">responseWithExploreReviseReplica</span><span class="token punctuation">(</span>workload <span class="token operator">*</span>workloadv1alpha1<span class="token punctuation">.</span>Workload<span class="token punctuation">,</span> req interpreter<span class="token punctuation">.</span>Request<span class="token punctuation">)</span> interpreter<span class="token punctuation">.</span>Response <span class="token punctuation">{</span><br /> wantedWorkload <span class="token operator">:=</span> workload<span class="token punctuation">.</span><span class="token function">DeepCopy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> wantedWorkload<span class="token punctuation">.</span>Spec<span class="token punctuation">.</span>Replicas <span class="token operator">=</span> req<span class="token punctuation">.</span>DesiredReplicas<br /> marshaledBytes<span class="token punctuation">,</span> err <span class="token operator">:=</span> json<span class="token punctuation">.</span><span class="token function">Marshal</span><span class="token punctuation">(</span>wantedWorkload<span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> interpreter<span class="token punctuation">.</span><span class="token function">Errored</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusInternalServerError<span class="token punctuation">,</span> err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> interpreter<span class="token punctuation">.</span><span class="token function">PatchResponseFromRaw</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>Object<span class="token punctuation">.</span>Raw<span class="token punctuation">,</span> marshaledBytes<span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>核心代码也只有赋值那一行。</p>
<h3 id="workload-%E5%AE%9E%E7%8E%B0%E5%89%AF%E6%9C%AC%E6%95%B0%E8%B0%83%E5%BA%A6" tabindex="-1">Workload 实现副本数调度</h3>
<p>回到我们最初的那个问题,在了解了 <code>InterpretReplica</code> 和 <code>ReviseReplica</code> 两个 hook 点之后,就能够实现自定义资源按副本数调度了,实现 <code>InterpretReplica</code> hook 点以告知 Karmada 该资源的副本总数,实现 <code>ReviseReplica</code> hook 点来修改对象的副本数,再配置一个 <code>PropagationPolicy</code> 就可以了,配置方法和 <code>Deployment</code> 等资源一样:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> policy.karmada.io/v1alpha1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> PropagationPolicy<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx<span class="token punctuation">-</span>workload<span class="token punctuation">-</span>propagation<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">resourceSelectors</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> workload.example.io/v1alpha1<br /> <span class="token key atrule">kind</span><span class="token punctuation">:</span> Workload<br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">placement</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterAffinity</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterNames</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> member1<br /> <span class="token punctuation">-</span> member2<br /> <span class="token key atrule">replicaScheduling</span><span class="token punctuation">:</span><br /> <span class="token key atrule">replicaDivisionPreference</span><span class="token punctuation">:</span> Weighted<br /> <span class="token key atrule">replicaSchedulingType</span><span class="token punctuation">:</span> Divided<br /> <span class="token key atrule">weightPreference</span><span class="token punctuation">:</span><br /> <span class="token key atrule">staticWeightList</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">targetCluster</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterNames</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> member1<br /> <span class="token key atrule">weight</span><span class="token punctuation">:</span> <span class="token number">2</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">targetCluster</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterNames</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> member2<br /> <span class="token key atrule">weight</span><span class="token punctuation">:</span> <span class="token number">1</span></code></pre>
<p>效果如下:</p>
<p><img src="https://xinzhao.me/img/guide-to-karmada-resource-interpreter-webhook/workload-replica.png" alt="" /></p>
<h3 id="retain" tabindex="-1">Retain</h3>
<p><img src="https://xinzhao.me/img/guide-to-karmada-resource-interpreter-webhook/Retain.png" alt="" /></p>
<p>该 hook 点发生在从 <code>Work</code> 到 <code>Resource</code> 这个过程中,针对 <code>spec</code> 内容会在 member 集群单独更新的情况,可以通过该 hook 告知 Karmada 保留某些字段的内容。</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> workload.example.io/v1alpha1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> Workload<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">labels</span><span class="token punctuation">:</span><br /> <span class="token key atrule">app</span><span class="token punctuation">:</span> nginx<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">replicas</span><span class="token punctuation">:</span> <span class="token number">3</span><br /> <span class="token key atrule">paused</span><span class="token punctuation">:</span> <span class="token boolean important">false</span></code></pre>
<p>以 <code>paused</code> 为例,该字段的功能是暂停 workload,member 集群的 controller 会单独更新该字段,<code>Retain</code> hook 就是为了能更好地和 member 集群的 controller 协作,可以通过该 hook 来告知 Karmada 哪些字段是需要不用更新、需要保留的。</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>e <span class="token operator">*</span>workloadInterpreter<span class="token punctuation">)</span> <span class="token function">responseWithExploreRetaining</span><span class="token punctuation">(</span>desiredWorkload <span class="token operator">*</span>workloadv1alpha1<span class="token punctuation">.</span>Workload<span class="token punctuation">,</span> req interpreter<span class="token punctuation">.</span>Request<span class="token punctuation">)</span> interpreter<span class="token punctuation">.</span>Response <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> req<span class="token punctuation">.</span>ObservedObject <span class="token operator">==</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> err <span class="token operator">:=</span> fmt<span class="token punctuation">.</span><span class="token function">Errorf</span><span class="token punctuation">(</span><span class="token string">"nil observedObject in exploreReview with operation type: %s"</span><span class="token punctuation">,</span> req<span class="token punctuation">.</span>Operation<span class="token punctuation">)</span><br /> <span class="token keyword">return</span> interpreter<span class="token punctuation">.</span><span class="token function">Errored</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusBadRequest<span class="token punctuation">,</span> err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> observerWorkload <span class="token operator">:=</span> <span class="token operator">&</span>workloadv1alpha1<span class="token punctuation">.</span>Workload<span class="token punctuation">{</span><span class="token punctuation">}</span><br /> err <span class="token operator">:=</span> e<span class="token punctuation">.</span>decoder<span class="token punctuation">.</span><span class="token function">DecodeRaw</span><span class="token punctuation">(</span><span class="token operator">*</span>req<span class="token punctuation">.</span>ObservedObject<span class="token punctuation">,</span> observerWorkload<span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> interpreter<span class="token punctuation">.</span><span class="token function">Errored</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusBadRequest<span class="token punctuation">,</span> err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// Suppose we want to retain the `.spec.paused` field of the actual observed workload object in member cluster,</span><br /> <span class="token comment">// and prevent from being overwritten by karmada controller-plane.</span><br /> wantedWorkload <span class="token operator">:=</span> desiredWorkload<span class="token punctuation">.</span><span class="token function">DeepCopy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> wantedWorkload<span class="token punctuation">.</span>Spec<span class="token punctuation">.</span>Paused <span class="token operator">=</span> observerWorkload<span class="token punctuation">.</span>Spec<span class="token punctuation">.</span>Paused<br /> marshaledBytes<span class="token punctuation">,</span> err <span class="token operator">:=</span> json<span class="token punctuation">.</span><span class="token function">Marshal</span><span class="token punctuation">(</span>wantedWorkload<span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> interpreter<span class="token punctuation">.</span><span class="token function">Errored</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusInternalServerError<span class="token punctuation">,</span> err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> interpreter<span class="token punctuation">.</span><span class="token function">PatchResponseFromRaw</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>Object<span class="token punctuation">.</span>Raw<span class="token punctuation">,</span> marshaledBytes<span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>核心代码只有一行,更新 <code>wantedWorkload</code> 的 <code>Paused</code> 字段为之前版本的内容。</p>
<h3 id="aggregatestatus" tabindex="-1">AggregateStatus</h3>
<p><img src="https://xinzhao.me/img/guide-to-karmada-resource-interpreter-webhook/AggregateStatus.png" alt="" /></p>
<p>该 hook 点发生在从 <code>ResourceBinding</code> 到 <code>ResourceTemplate</code> 这个过程中,针对需要将 <code>status</code> 信息聚合到 Resource Template 的资源类型,可通过实现该接口来更新 Resource Template 的 <code>status</code> 信息。</p>
<p>Karmada 会将各个集群 Resouce 的状态信息统一收集到 <code>ResourceBinding</code> 中:</p>
<p><img src="https://xinzhao.me/img/guide-to-karmada-resource-interpreter-webhook/nginx-rb.png" alt="" /></p>
<p><code>AggregateStatus</code> hook 需要做的事情就是将 <code>ResourceBinding</code> 中 <code>status</code> 信息更新到 Resource Template 中:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>e <span class="token operator">*</span>workloadInterpreter<span class="token punctuation">)</span> <span class="token function">responseWithExploreAggregateStatus</span><span class="token punctuation">(</span>workload <span class="token operator">*</span>workloadv1alpha1<span class="token punctuation">.</span>Workload<span class="token punctuation">,</span> req interpreter<span class="token punctuation">.</span>Request<span class="token punctuation">)</span> interpreter<span class="token punctuation">.</span>Response <span class="token punctuation">{</span><br /> wantedWorkload <span class="token operator">:=</span> workload<span class="token punctuation">.</span><span class="token function">DeepCopy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">var</span> readyReplicas <span class="token builtin">int32</span><br /> <span class="token keyword">for</span> <span class="token boolean">_</span><span class="token punctuation">,</span> item <span class="token operator">:=</span> <span class="token keyword">range</span> req<span class="token punctuation">.</span>AggregatedStatus <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> item<span class="token punctuation">.</span>Status <span class="token operator">==</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> <span class="token keyword">continue</span><br /> <span class="token punctuation">}</span><br /> status <span class="token operator">:=</span> <span class="token operator">&</span>workloadv1alpha1<span class="token punctuation">.</span>WorkloadStatus<span class="token punctuation">{</span><span class="token punctuation">}</span><br /> <span class="token keyword">if</span> err <span class="token operator">:=</span> json<span class="token punctuation">.</span><span class="token function">Unmarshal</span><span class="token punctuation">(</span>item<span class="token punctuation">.</span>Status<span class="token punctuation">.</span>Raw<span class="token punctuation">,</span> status<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> interpreter<span class="token punctuation">.</span><span class="token function">Errored</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusInternalServerError<span class="token punctuation">,</span> err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> readyReplicas <span class="token operator">+=</span> status<span class="token punctuation">.</span>ReadyReplicas<br /> <span class="token punctuation">}</span><br /> wantedWorkload<span class="token punctuation">.</span>Status<span class="token punctuation">.</span>ReadyReplicas <span class="token operator">=</span> readyReplicas<br /> marshaledBytes<span class="token punctuation">,</span> err <span class="token operator">:=</span> json<span class="token punctuation">.</span><span class="token function">Marshal</span><span class="token punctuation">(</span>wantedWorkload<span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> interpreter<span class="token punctuation">.</span><span class="token function">Errored</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusInternalServerError<span class="token punctuation">,</span> err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> interpreter<span class="token punctuation">.</span><span class="token function">PatchResponseFromRaw</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>Object<span class="token punctuation">.</span>Raw<span class="token punctuation">,</span> marshaledBytes<span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>逻辑也非常简单,根据 <code>ResourceBinding</code> 中的 <code>status</code> 信息来计算(聚合)出该资源总的 <code>status</code> 信息再更新到 Resource Template 中;效果和 <code>Deployment</code> 类似,可以直接查询到该资源在所有集群汇总后的状态信息:</p>
<p><img src="https://xinzhao.me/img/guide-to-karmada-resource-interpreter-webhook/deployment-status.png" alt="" /></p>
<h2 id="%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5" tabindex="-1">参考链接</h2>
<ul>
<li><a href="https://github.com/karmada-io/karmada/tree/master/docs/proposals/resource-interpreter-webhook">Resource Interpreter Webhook</a></li>
<li><a href="https://github.com/karmada-io/karmada/tree/master/examples#resource-interpreter">custom resource interpreter example</a></li>
</ul>
北海+涠洲岛之旅
2022-10-09T00:00:00Z
https://xinzhao.me/posts/2022-beihai-and-weizhou-island-tour/
<p></p><div class="table-of-contents"><div class="toc-container-header">索引</div><ul><li><a href="https://xinzhao.me/posts/2022-beihai-and-weizhou-island-tour/#%E5%BA%8F">序</a></li><li><a href="https://xinzhao.me/posts/2022-beihai-and-weizhou-island-tour/#day-1-%E6%88%90%E9%83%BD%E2%80%94%E5%8D%97%E5%AE%81%E2%80%94%E5%8C%97%E6%B5%B7">Day 1 成都—南宁—北海</a></li><li><a href="https://xinzhao.me/posts/2022-beihai-and-weizhou-island-tour/#day-2-%E5%8C%97%E6%B5%B7%E2%80%94%E6%B6%A0%E6%B4%B2%E5%B2%9B">Day 2 北海—涠洲岛</a></li><li><a href="https://xinzhao.me/posts/2022-beihai-and-weizhou-island-tour/#day-3-%E6%B6%A0%E6%B4%B2%E5%B2%9B">Day 3 涠洲岛</a></li><li><a href="https://xinzhao.me/posts/2022-beihai-and-weizhou-island-tour/#day-4-%E6%B6%A0%E6%B4%B2%E5%B2%9B">Day 4 涠洲岛</a></li><li><a href="https://xinzhao.me/posts/2022-beihai-and-weizhou-island-tour/#day-5-%E6%B6%A0%E6%B4%B2%E5%B2%9B%E2%80%94%E5%8C%97%E6%B5%B7">Day 5 涠洲岛—北海</a></li><li><a href="https://xinzhao.me/posts/2022-beihai-and-weizhou-island-tour/#day-6-%E5%8C%97%E6%B5%B7">Day 6 北海</a></li><li><a href="https://xinzhao.me/posts/2022-beihai-and-weizhou-island-tour/#day-7-%E5%8C%97%E6%B5%B7">Day 7 北海</a></li><li><a href="https://xinzhao.me/posts/2022-beihai-and-weizhou-island-tour/#day-8-%E5%8C%97%E6%B5%B7%E2%80%94%E5%8D%97%E5%AE%81%E2%80%94%E6%88%90%E9%83%BD">Day 8 北海—南宁—成都</a></li></ul></div><p></p>
<h2 id="%E5%BA%8F" tabindex="-1">序</h2>
<p>我旅游一般不做第一天去哪儿,第二天去哪儿那种详细的攻略,感觉每天背负那些“任务”很累,就比较随意,如果是第一次去一个地方,基本策略还是先跑一遍所有景点,之后有时间的话再根据自己喜好去随便逛逛,如果已经去过了,那就非常随意了,就完全按自己喜好,想去哪儿就去哪儿,没事儿就瞎溜达,这样轻松一点,没有心理压力。</p>
<p>然后这篇游记的话就是一些流水帐、旅途中发生/发现的一些有趣的事情和拍的一些照片。</p>
<h2 id="day-1-%E6%88%90%E9%83%BD%E2%80%94%E5%8D%97%E5%AE%81%E2%80%94%E5%8C%97%E6%B5%B7" tabindex="-1">Day 1 成都—南宁—北海</h2>
<p>早上从成都出发,中午落地南宁,这次是 9 点多的飞机,前两次都是晚上,窗外啥也看不见的那种;我们出发的时候成都还是阴天,雾蒙蒙的,但是当飞机突破云层之后,艳阳高照,万里晴空,和之前完全是两个世界,这一瞬间体验多少次都依然会觉得惊艳。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1092.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1050.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1064.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1095.jpg" alt="" /></p>
<p>去北海的高铁是晚上的,所以在南宁待了半天,逛了些美食街,吃了点当地特色,发现几件有趣的事情:</p>
<ol>
<li>广西真的很喜欢用一次性餐具,筷子必是一次性的,碗也有概率是一次性的,不然就是铁盘子</li>
<li>这里的小店餐桌上都不提供纸巾,每次去我就把我自己带的纸放桌上,有一次遇到一个来我们桌抽纸,我就给她说这是我们自己带的(还是让她用了),她说了一句:“我就说这种小店怎么可能提供纸巾”</li>
<li>这里真的很多电动自行车,而且这里的共享单车也大多数都是电单车,没想明白为啥,这里又不像重庆很多坡道</li>
</ol>
<p>去南宁高铁站的时候还看到了奇特的天空,乌云和蓝天交错,路边一个人说这个是龙凤呈祥,有龙有凤好预兆:</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/1.jpeg" alt="" /></p>
<h2 id="day-2-%E5%8C%97%E6%B5%B7%E2%80%94%E6%B6%A0%E6%B4%B2%E5%B2%9B" tabindex="-1">Day 2 北海—涠洲岛</h2>
<p>早上出发前往涠洲岛,坐船的时候又发现一件有趣的事情:这里甚至船的座位都没有第 4 排,昨天我们住的民宿也是,没有 4 楼,3 过了就是 5,他们这里好像挺信这个;然后和我同行的朋友说了这个,我说按照这个推论,这里也没有 14 排,带 4 的都没有,然后我去验证了一下,果然没有。</p>
<p>船开了一会儿之后可以去到三层甲板,我几乎全程都在那儿趴着栏杆看海,偶然间看到一个地方好像有很多鱼跳出水面,兴冲冲跑过去发现只是很多细小的浪花。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1104.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1117.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1135.jpg" alt="" /></p>
<p>到了民宿,收拾好行李,民宿老板给我们讲了一下整个岛的全貌,我们就跟老板租了辆车准备去溜达了。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/2.jpeg" alt="" /></p>
<p>因为我们到岛上还没吃饭,就先去了老板推荐的重庆胖妈面馆简单吃个饭(面),然后顺便在旁边的天主教堂逛了逛。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1142.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1151.jpg" alt="" /></p>
<p>打卡完成后就直奔最近的沙滩:贝壳沙滩,内陆长大的孩子第一次近距离接触大海,这里的沙很细软,光着腿踩很舒服,而且海浪也温柔,在这儿沿着海岸线散步很惬意。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1170.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1189.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1198.jpg" alt="" /></p>
<p>之后又去蓝桥拍夕阳,也是老板推荐的一个点位,和普通的攻略推荐石螺口不一样,老板说这里其实也很不错,刚好有蓝桥当前景,很好看,也可以顺便两个景点一起逛了,事实证明果然很出片,不过感觉运气不是很好,那个角度有很多云挡着,没能看到完整的夕阳。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1233.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1238.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1254.jpg" alt="" /></p>
<p class="caption">像跃马的云</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1237.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1274.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1248.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1252.jpg" alt="" /></p>
<p>第一天没啥时间观念,看夕阳看到很晚了,周围人都走完了,晚上在涠洲岛骑车真是太恐怖了,路上基本没人,很多路没路灯,我们的车灯又不是很亮,再加上很多那种超级偏僻的乡间小道(这可能是我们住得偏的缘故),那个氛围真的渲染得太到位了,我再也不可能在涠洲岛骑夜路了,而且导航老是导那种乡间小路,岛上信号又不好,老是走错路又重新规划,大晚上的被迫在涠洲岛上瞎逛,我们的车屏幕是坏的,看不见还有多少电,一路上还一直担心怕我们的车没电了;不过一路上发现涠洲岛上好多重庆菜,川菜饭馆,每家都说自己是正宗重庆/四川味道。</p>
<h2 id="day-3-%E6%B6%A0%E6%B4%B2%E5%B2%9B" tabindex="-1">Day 3 涠洲岛</h2>
<p>今天去了两个景点打卡,先去了滴水丹屏,可惜没退潮过不去,只能看到一部分,就是一面岩壁一直在滴水下来,那片岩壁的纹路挺好看;后面又去了鳄鱼山景区,这个是要门票的,但是上岛票包含了一次这个景区门票,也就是说可以免费去一次这个景区。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1309.jpg" alt="" /></p>
<p class="caption">在珊瑚馆看到的 Nemo</p>
<p>因为整个涠洲岛从一个角度俯瞰就像一只鳄鱼 🐊(和这个 emoji 很像),所以这个景区得名鳄鱼山(鳄鱼山当然没有鳄鱼),然后它就在鳄鱼头的那个位置,整个景区视野很开阔,看全岛风景挺好的,是一个环形路线,一路上有各种开发了(取了名)的景点;这一路上还有卖水的机器,我就一直没搞懂它的电线是怎么接下来的,从哪里接下来的,当然那个机器的水很贵,快乐水都要 10 块钱一瓶。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1322.jpg" alt="" /></p>
<p class="caption">灯塔</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1326.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1329.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1333.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1357.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1358.jpg" alt="" /></p>
<p>中午和民宿的老板娘一起去了海鲜市场买点海鲜出去加工吃,在老板娘的建议下买了野生的明虾、皮皮虾和兰花蟹,然后带去一家老板娘推荐的店(不出所料是重庆饭店,老板是重庆人)加工,也算是吃上了一顿正宗的涠洲岛海鲜(大)餐。</p>
<p>今天出了一些插曲,早上骑车出去路上胎爆了,下午从鳄鱼山景区出来车后轮和悬挂连接的地方又断掉了,这一天天的,想起早上胎爆的时候和一个路边的环卫阿姨聊天,她说我们出来玩儿怎么租这么破的车……昨天才说不可能再在涠洲岛骑夜路了,今天又因为车坏了等了好久,又骑上夜路了,不一样的夜路一样的恐怖。</p>
<p>晚上回来和民宿的其他住客一起去赶海,我全程基本都是氛围组,抓了几只小螃蟹,然后重点来了,我看到一个长方形的奇怪的东西,刚开始以为是石头之类的,但是突然看到它在缓慢移动,我就想着捡起来看看是啥,但是看着有点怕就用夹子去夹了(后面才知道这个真是明智的决定),结果一夹居然是硬的,触感就跟石头一样,夹起来仔细一看感觉像是鱼,有眼睛嘴巴,关键是有背鳍和尾鳍,但是四四方方的,除了肚子应该是软的,其它面都是硬的,实在是很奇怪的生物,于是给它也扔在桶里准备去问问民宿的老板他们看看知不知道是啥。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/3.jpeg" alt="" /></p>
<p>后面问他们也没人知道,我们在网上冲浪了一番查出来了,是箱鲀,具体是牛角鲀,剧毒,被攻击触碰时表面会释放毒素来毒死周围的鱼之类的东西,浓度高时甚至会毒死自己,想起来真有点后怕,还好当时没用手去拿,不过据说是比较温和的动物,一般不会释放毒素,而且它看着挺呆的,嘴巴和眼睛隔很远,形状也是四四方方的,越看越呆。</p>
<h2 id="day-4-%E6%B6%A0%E6%B4%B2%E5%B2%9B" tabindex="-1">Day 4 涠洲岛</h2>
<p>早上出发去看日出,天气预报也是大晴天,结果也遇到点云挡着,和那天看日落一样,这几天在岛上有点运气,但是不多。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1366.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1367.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1371.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1374.jpg" alt="" /></p>
<p>看完日出就准备出发去石螺口坐船玩儿水上项目,然后又问民宿老板娘租一辆车,我话一说出口老板娘都笑了,但还是给我们找了一辆,不算新但是比昨天那辆好,动力也强一些,电门拧到底能跑到 50 多码。</p>
<p>然后就坐船出海玩儿项目,他们在沙滩附近海域上停了一艘船,我们就在船周围活动,体验了一下摩托艇和在海里游泳,摩托艇挺有意思,坐在上面在海面上狂飙看着那个海面就跟游戏里面渲染的那种一样,一时间差点分不清现实和游戏;海里面游泳时不时会喝到一点海水,真的超级咸,比我想象中咸太多了,一斤水兑十斤盐那种程度,不过穿着救生衣躺在海面上飘荡的感觉可太舒服了 😌。</p>
<p>海面上浪还是蛮大的,我后面在船上坐了一会儿就开始晕船了,这种小船真的太晃了,本来为上岛的船买了晕船药,但是那种大船其实很稳,根本用不上,这个用得上的时候又忘记带了,晕船的时候脑袋里面就一直蹦出一句话:“对大海要有敬畏之心”。</p>
<p>下午去五彩滩溜达,也是没退潮,只有一点点能看,大部分过不去,这几天的经历下来感觉比天气更重要的是潮汐时间;最后又去了贝壳沙滩,在这里开始在这里结束,就站在海里看海浪发呆,要是搬把椅子来我能在这儿坐一天。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1394.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1399.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1410.jpg" alt="" /></p>
<h2 id="day-5-%E6%B6%A0%E6%B4%B2%E5%B2%9B%E2%80%94%E5%8C%97%E6%B5%B7" tabindex="-1">Day 5 涠洲岛—北海</h2>
<p>早上收拾好行李准备出发回北海,站在民宿门口发呆看天空时发现岛上的云都飘得好快,想起之前有次看见云飘到月亮旁边,甚至产生了月亮在快速移动的错觉。</p>
<p>这里还有个插曲,因为我们上岛这几天刚好有明星来拍综艺,《披荆斩棘的哥哥》第二季要来这里拍点东西,任贤齐和潘玮柏他们都会过来,然后我们住的这家民宿的老板就是他们的向导,他们也会在这儿拍,所以老板他们这几天都很忙,然后本来说带我们玩的也没能实现,拍的那天我们也不能待在民宿,不能靠近,走的时候老板就一直给我们道歉说不好意思这几天特殊情况没能带我们玩儿,下次来一定带我们好好玩儿。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/4.jpeg" alt="" /></p>
<p class="caption">民宿老板娘的朋友圈</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1415.jpg" alt="" /></p>
<p class="caption">从码头拍蓝桥</p>
<p>中午到北海,去民宿放了行李,民宿老板推荐了一些景点和吃饭的地方,我们就去了附近的鲨鱼湾美食。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/5.jpeg" alt="" /></p>
<p>这里的饭馆挺有意思的,没有菜单,点菜直接去选海鲜,然后称重现杀了做,我们让老板推荐了一些北海特色:</p>
<ol>
<li>沙虫,这个东西巨恶心,千万不要去搜图片看(肯定有人本来不会去搜,但是看了这句话就要去搜的),老板说这个做出来就不恶心了,而且不吃这个相当于白来北海,我们就点了一份</li>
<li>油螺,北海才有的,壳摸着滑滑的,像有一层油一样</li>
<li>黄足笛鲷,老板说这个也是北海比较特色的</li>
</ol>
<p>选完了老板还非常热情地给我们介绍他那里的其它海鲜。</p>
<p>沙虫是用蒜蓉和粉丝一起蒸的,这个真的很鲜,确实很好吃,老板娘说这个他们都生吃的,生吃的更鲜更脆;油螺是切片辣炒,这个没有啥太特别的地方;鱼也是清蒸的放一些蒸鱼豉油,老板娘说这样做才鲜。</p>
<p>吃饭的时候老板娘过来聊天,她说北海的沙虫和其它地方不一样,沙虫是不会生长在有污染的区域的,他们这儿靠海吃海,好多人都会出去挖沙虫来卖,还能做成沙虫干,用来煲汤煲粥那些很鲜的,都不用放味精。</p>
<p>后面又看上了这里的茶,回甜回甜的很好喝,问了下老板娘,她说是罗汉果和啥东西一起配的,非常热情地送了我们一点,后面说我们想要的话可以买一点,她直接给我们快递回去,20 块一斤(对,是斤),之前也有客人看上了他们这里的茶然后买的,她说他们这里都喝这种凉茶和糖水的,不会喝那种饮料,全是色素和糖精。</p>
<p>之后出发前往银滩,这里的沙子确实很细软,感觉比涠洲岛贝壳沙滩的还要细软,站在上面都会有点下陷,但是这里的海浪就一点也不温柔了,来得猛烈了许多;我们就一直沿着海岸线一直向前走,这里的沙滩就比涠洲岛商业气息重太多了,一路上全是摩托艇和叫你玩儿摩托艇的人,涠洲岛上也有,但是远没有这么多。</p>
<p>后面我们就一直往前走,一直走到了很前面,那片沙滩都没啥人了,本来打算去前面干的沙滩上坐一会儿,走近了才发现那儿全是密密麻麻的小洞,真的整片沙滩都是,整片都是,据说是小螃蟹打的洞,巨巨巨恶心 🤢,密恐患者和非密恐患者都沉默了,真的生理不适(答应我这个真的不要去搜图片了好吗,短短几秒钟要用一生来治愈,我现在写这个回想起来都还头皮发麻)。</p>
<p>今天天气不错,就决定在银滩蹲一下日落,事实证明等待是非常值得的,美中不足就是这儿人太多了,本来下午点我们来的时候人没多少,可以接受的那种程度,我们就以为今天这儿大概就只有这么多人,但是晚点了凉快了人就变多起来了,大概日落的时间达到了顶峰,如果你觉得涠洲岛海滩上的人都算多的话那这儿的人数一定是你无法接受的。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1436.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1439.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1449.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1463.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1469.jpg" alt="" />
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1477.jpg" alt="" /></p>
<p>晚上就出发去侨港风情街吃点东西,在这里买了有间冰室的炒冰,这个很好喝,但是里面加了葡萄干我觉得是败笔,那个太硬了,而且黏牙还太甜了;买了侨越世家的卷粉;买了好运烧的北海龙珠,就是那种超大号的章鱼大丸子;买了二十四幢糖水的红豆薏米糖水,这个初印象有点像凉茶,还挺好喝,但是喝多了就感觉太甜了;买了信记虾饼,这个一直有很多人在排队买,我觉得一般,吃着太腻了,味道也没啥特别的。</p>
<h2 id="day-6-%E5%8C%97%E6%B5%B7" tabindex="-1">Day 6 北海</h2>
<p>早上睡醒收拾好就出发前往银滩附近的另一个海滩:侨港海滩,很巧的是打车又打到之前的那个师傅了,他说侨港海滩和银滩一样,都是沙子比较细软的,只有金滩的不一样,那边沙粗点(我第一次听成了杀猪点,以为金滩是专门宰客的 🤷♂️)。</p>
<p>这确实和银滩差不多,不过浪比银滩的还要大,这儿的沙滩上也全是那种密密麻麻的洞,比银滩的还多还恶心,真服了,又被恶心一次。</p>
<p>中午又去侨港风情街了,又买了炒冰顺便在那儿吃了午饭,在品越越南鸡粉吃了碗粉,老板是越南回来的华侨,那个粉本身我感觉没啥太特别的,但是那个手打牛肉丸挺好吃的,太 Q 弹了。</p>
<p>下午坐公交车去冠头岭公园溜达溜达,北海的公交车可以直接刷支付宝,和南宁的地铁一样,先领一张卡就行了,挺方便的,不过北海的公交车和成都的不大一样,成都的公交车每一站都会停,不管有没有人上下,但是北海的你不告知司机他就不会停,没人上下他就直接开走了;而且北海好多用别的省市命名的路,四川路,重庆路,贵州路口,云南路口,西藏路口。</p>
<p>到了冠头岭在那个观光车师傅的劝说下坐了个观光车(不过挺值的,上坡路太难走了),非常巧师傅是重庆的,我们就直接全程四川话交流,可太亲切了。</p>
<p>师傅真的很能聊,一路上给我们介绍景点,还有些历史习俗相关的,金句频出:</p>
<ul>
<li>这边是冠岭山庄,各国领导人来这边住的地方,我们农民砍这里一棵树判两年半,他们政府一砍就砍半边山</li>
<li>本来这里规划了要修很多观景平台的,能俯瞰整个北海市,但是对面的越南非常调皮,这里就来了导弹部队,之后就不让继续开发了(确实一路上都是军事管理区,禁止前进和拍照)</li>
<li>前面有唯一一段颠簸的路,为什么呢,云贵川信佛教,这里信另一个什么教(我没听清),这里是龙头路,龙头路是不允许动水利的,所以这里就一直是这种坑坑洼洼的状态</li>
<li>本来应该是 57 个民族,南 wan(三点水一个万,没有这个字)和土家族是兄弟家族之类的,土家族进山,他们下海,他们下海回来晚了就没分到户口,现在没有这个族了,只有他们的人,但户口是汉族
<img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1488.jpg" alt="" /></li>
<li>北海市三面环海,你沿哪条路走都到北海市区,走不出去的</li>
</ul>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1487.jpg" alt="" /></p>
<p class="caption">气象塔</p>
<p>山上游览了一圈就去了冠头岭海滩,这里的浪是几个海滩里最大的,我觉得可以用惊涛骇浪来形容了,在那里坐了一会儿就说再去附近的流下村打个卡。</p>
<p>流下村就一个路修得好一点的普通村落,感觉也不怎么样,啥也没有,好像是因为王一博在这边拍了个啥广告就带火了这里,就很多人来这儿打卡拍照 🤷♂️,我们还花了 20 巨款坐车来这个地方,只能说血亏,几分钟走完了赶紧逃离这里回侨港风情街吃晚饭。</p>
<p>今晚去买了椰椰大吉,但是没买他们家招牌的椰饮,点了杯西瓜暴打柠檬茶,感觉没啥太特别的,成都也有这种;之后去了越乡小厨,感觉还好,也没有滴滴师傅说的宰客之类的,主要滴滴师傅口中的本地人开的店我们也不知道,路边那种所谓的本地人开的店感觉更宰客,每次一经过就靓仔要不要来吃饭,感觉过分热情了。</p>
<h2 id="day-7-%E5%8C%97%E6%B5%B7" tabindex="-1">Day 7 北海</h2>
<p>本来今天打算去北海老街那边住的,但是听民宿老板说那边昨天有三例疫情,新疆那边过来的游客,我们就临时改变行程,继续在银滩这儿再住一天然后直接回去。</p>
<p>早上去买了点北海特产,一些海鲜干货,之前鲨鱼湾饭店的老板娘带着逛了逛,我本来说不买的,走进去逛着逛着就买了五百多的东西,买了点沙虫干,小鲍鱼干和干贝,这些可以用来煲汤和煲粥,还买了些鱿鱼丝和小鱼干那种零食;后面老板娘还送了我们两个油螺的壳,她说现在这些东西少了,因为禁止深海捕捞了,带回去可以当个装饰品。</p>
<p>中午又去了侨港风情街吃饭顺便附近做个核酸,然后不甘心又买了一杯椰椰大吉,这次点了招牌椰饮:椰香布蕾啵啵冰,这次的就对味了,椰香奶香味很浓。</p>
<p>下午去银滩溜达了一圈,银滩面向大海的右手边那片沙滩不错,没有那些密密麻麻的洞,沙子踩着也舒服,最后又走到我们最开始来银滩那儿,开始的地方也是结束的地方。</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/IMG_1506.jpg" alt="" /></p>
<h2 id="day-8-%E5%8C%97%E6%B5%B7%E2%80%94%E5%8D%97%E5%AE%81%E2%80%94%E6%88%90%E9%83%BD" tabindex="-1">Day 8 北海—南宁—成都</h2>
<p>早上收拾好出发回成都,也是先从北海坐高铁去南宁,再从南宁坐飞机回成都,这里又有意外,出南宁高铁站的时候还要扫他们的一个通行检查码:</p>
<p><img src="https://xinzhao.me/img/2022-beihai-and-weizhou-island-tour/6.jpeg" alt="" /></p>
<p>我选了我要回去的地方,不知道为啥我的那个码是蓝色的(正常应该是绿色的),给我定义成了中高风险人员,然后上面写了我要回的区,我盲猜应该是我那个区现在有疫情之类的,但我是回去,又不是从那边来(我前几天来的时候都是正常的),他们就直接一刀切让我走中高风险人员通道,去那边又让填很多东西才放走,真的纯傻逼政策,我 24 小时核酸,行程码什么都是正常的,就这个他们自己搞的傻逼东西卡我,不过这也给了我一个教训,在这傻逼的大环境下就尽量选择直达的方式,不要去中转;还好机场不扫他们这个傻逼通行检查码了,不然我真的再也不想来这个傻逼地方了。</p>
成都双年展
2022-04-01T00:00:00Z
https://xinzhao.me/posts/2021-chengdu-biennale/
<p><img src="https://xinzhao.me/img/2021-chengdu-biennale/1.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/2.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/3.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/4.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/5.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/6.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/7.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/8.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/9.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/10.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/11.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/12.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/13.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/14.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/15.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/16.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/17.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/18.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/19.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/20.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/21.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/22.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/23.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/24.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/25.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/26.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/27.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/28.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/29.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/30.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/31.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/32.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/33.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/34.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/35.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/36.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/37.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/38.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/39.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/40.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/41.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/42.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/43.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/44.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/45.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/46.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/47.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/48.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/49.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/50.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/51.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/52.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/53.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/54.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/55.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/56.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/57.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/58.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/59.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/60.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/61.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-chengdu-biennale/62.jpg" alt="" /></p>
2021 年终总结
2021-12-31T00:00:00Z
https://xinzhao.me/posts/2021-summary/
<p></p><div class="table-of-contents"><div class="toc-container-header">索引</div><ul><li><a href="https://xinzhao.me/posts/2021-summary/#%E7%94%9F%E6%B4%BB">生活</a><ul><li><a href="https://xinzhao.me/posts/2021-summary/#%E9%80%83%E7%A6%BB%E7%A4%BE%E4%BA%A4%E7%BD%91%E7%BB%9C">逃离社交网络</a></li><li><a href="https://xinzhao.me/posts/2021-summary/#%E6%B4%BB%E5%9C%A8%E5%BD%93%E4%B8%8B%E5%92%8C%E4%B8%8D%E5%8D%91%E4%B8%8D%E4%BA%A2">活在当下和不卑不亢</a></li><li><a href="https://xinzhao.me/posts/2021-summary/#%E8%BF%87%E6%95%8F">过敏</a></li></ul></li><li><a href="https://xinzhao.me/posts/2021-summary/#%E5%B7%A5%E4%BD%9C">工作</a></li><li><a href="https://xinzhao.me/posts/2021-summary/#%E8%BF%99%E4%B8%80%E5%B9%B4%E4%B9%B0%E7%9A%84%E6%9C%89%E8%B6%A3%E7%9A%84%E4%B8%9C%E8%A5%BF">这一年买的有趣的东西</a><ul><li><a href="https://xinzhao.me/posts/2021-summary/#%E5%9B%9E%E5%BD%92%E8%8B%B9%E6%9E%9C%E7%94%9F%E6%80%81%E5%9C%88">回归苹果生态圈</a></li><li><a href="https://xinzhao.me/posts/2021-summary/#eos-rp">EOS RP</a></li><li><a href="https://xinzhao.me/posts/2021-summary/#%E7%9B%B2%E7%9B%92">盲盒</a></li><li><a href="https://xinzhao.me/posts/2021-summary/#be%40rbrick">BE@RBRICK</a></li></ul></li><li><a href="https://xinzhao.me/posts/2021-summary/#%E5%86%99%E5%9C%A8%E6%9C%80%E5%90%8E">写在最后</a></li></ul></div><p></p>
<p>拖了好久,今年终于开始写年终总结了,年中的时候重新<a href="https://xinzhao.me/posts/start-building-my-blog-again/">折腾了一下博客</a>,换成了静态博客,现在可以保证博客能一直存在,能一直被访问到,平时也不用怎么维护,基本算是稳定下来了,我也会从今年开始每年年底的时候都写写年终总结,回顾一下这一年以来的各种事情,一方面是记录下这一年印象深刻的事情,另一方面会总结一下这一年做错的事和学到的经验教训,以后可以再回头看看,看看 21 年发生了些什么,那时候的我又是什么状态。所以年终总结本身是一件非常私人的事情,按理说没必要公开,但是我还是选择发到公开的博客上,如果有认识或者不认识的朋友看到了并且我的故事和一些经验教训能对你有所帮助/启发的话,那也算是让写年终总结这件事情更有意义了。</p>
<p>由于是第一次写,所以今年的年终总结会包含一些前两年的事情,不过整体还是 21 年相关的。</p>
<h2 id="%E7%94%9F%E6%B4%BB" tabindex="-1">生活</h2>
<p>我一直都很喜欢看 Google 每年年底都会发布的 <a href="https://about.google/stories/year-in-search/">Year in Search</a> 视频,每年都会有一个年度关键词,今年的是 <code>healing</code>:</p>
<blockquote>
<p>这一年,人们都在设法克服个人和全球面临的种种挑战,“healing”(治愈)一词的全球搜索量达到了前所未有的高度。</p>
</blockquote>
<p>如果要给我这一年的工作生活选一个年度关键词,“混乱”可能是最合适的,各方面、各种意义上都很“乱”,而且我一直感觉每当我的生活快要步入正轨的时候就会跳出来一件很垃圾的事把我搞乱,然后一直恶性循环;我自己也很不稳定,感觉就一直像个无头苍蝇一样到处乱撞。</p>
<h3 id="%E9%80%83%E7%A6%BB%E7%A4%BE%E4%BA%A4%E7%BD%91%E7%BB%9C" tabindex="-1">逃离社交网络</h3>
<p>19 年有段时间非常焦虑,当时觉得是朋友圈和其它的一些社交平台加重了我的焦虑,所以就彻底关掉了朋友圈,不看也不发,删除了其它的社交平台(虽然我本来也不怎么玩儿),开启了为期一年多的社会学实验,看看是不是社交网络加重了我的焦虑,实验结果表明并没有什么本质的影响,该焦虑还是焦虑 🤷♂️。</p>
<p>在逃离社交网络的这一年多时间里,你几乎不可能在网络上看到任何我的足迹(当然除了 GitHub),我也一直在践行很早之前看到的一句话:</p>
<blockquote>
<p>最重要的还是把现实生活过好</p>
</blockquote>
<p>消失在网络中并没有马上就让我的现实生活变好,虽然进度很慢,但总归算是有一个目标了吧;今年年中的时候又重新打开了朋友圈,又回来网上冲浪了,偶尔看看和发一些生活碎片,告诉不常联系的朋友一切都好。</p>
<h3 id="%E6%B4%BB%E5%9C%A8%E5%BD%93%E4%B8%8B%E5%92%8C%E4%B8%8D%E5%8D%91%E4%B8%8D%E4%BA%A2" tabindex="-1">活在当下和不卑不亢</h3>
<p>第一次听这两个词是在高一的时候,一个同学给我提到了这两个概念,那个时候还完全不理解这两个词的含义,只是心里记着;随着时间推移愈发觉得活在当下和不卑不亢非常重要,今年也一直在践行这两个观念。</p>
<p>活在当下这一块(至少是表意上的)总体有十足的进步,想做的事马上做,想买的东西马上买,洒脱了不少,对未来的真正慷慨,是把一切都献给现在。</p>
<p>不卑不亢听起来很简单,但是做起来真的非常难,而且我大概是属于又卑又亢的类型 😶🌫️,对我来说就更难了;但是今年还在字节的时候听了一鸣在字节九周年活动上的演讲,他提到一个平常心的观点:</p>
<blockquote>
<p>平常心就是“瞄准靶心,但不期待十环”</p>
</blockquote>
<p>听了之后我很有感触,或许借由平常心可以达到真正的不卑不亢,这将是我的长期目标。</p>
<h3 id="%E8%BF%87%E6%95%8F" tabindex="-1">过敏</h3>
<p>今年体会了我人生中的第一次过敏,起因是看到一个 B 站 UP 主做的一碗满是浇头的三虾蟹黄面,加上我一直想试试秃黄油,所以去淘宝上买了一点,也做了一碗一虾蟹黄面:</p>
<p><img src="https://xinzhao.me/img/2021-summary/1.jpeg" alt="" /></p>
<p class="caption">秃黄油葱油拌面<br />(我没有河虾,就用普通的对虾代替了)</p>
<p>不是很好吃,没有啥特别的味道,吃到后面有点腻;吃完后的几个小时都没啥事,直到晚上大概 11 点的时候,我肚子开始不舒服了,就去上厕所,进厕所之前我人好好的,没有异样,在厕所里面呆了十多分钟,身上有些地方有点痒,我没在意,出来的时候一照镜子吓我一跳,整个脸通红,然后越来越痒,我开始意识到我过敏了,本来还在想要不要去医院,就在想的这段时间都变得越来越严重,身上越来越红,越来越痒,开始出现那种小疙瘩了,我赶紧打车去家旁边的医院看急诊。</p>
<p>到医院的时候挂号要排队,在排队的时候不知道怎么头又开始晕了,眼睛有点看不清东西,当时更慌了,以为是那种很严重的过敏反应,排到我的时候我就语气慌张地说我过敏了,很严重,结果他都不看我一眼,很淡定地说了一句:“不要慌”,然后给我挂号;完了又排队等着医生看病,凌晨的医院也超级多人,排了两小时队到我,就给医生说我的症状,他也非常淡定说:“过敏非常常见的,没啥事,吃点药就好了,我也经常过敏”,我当时就非常好奇为啥会过敏,因为我从小海鲜什么的都不过敏的,属于百毒不侵的那种,为什么这次这么严重,我就一直问医生,结果他也不理我 😥,埋头开药,排队两小时,看病两分钟。</p>
<p>凌晨 3 点都还在外面,8 点又滚起来上班,今年最新奇的体验;后来和一个朋友聊到这个,她说过敏就是这样的,来得很快,但是去得也很快,普通的过敏都没啥,她的朋友海鲜过敏都习惯了,左手海鲜,右手药。</p>
<h2 id="%E5%B7%A5%E4%BD%9C" tabindex="-1">工作</h2>
<p>今年换了工作,7 月底正式从字节离职,结束了我为期三年的第一份工作:</p>
<p><img src="https://xinzhao.me/img/2021-summary/7.jpg" alt="" /></p>
<p class="caption">朋友再见</p>
<p>当初阴差阳错地加入了才云科技,后面才云被字节收购,我们也跟着去了字节,待了一年之后,我还是选择了离开,很多人都不理解为什么要走;在父母眼里,字节有免费的三餐,吃得都还不错,他们也不用操心我天天吃外卖之类的;在朋友眼里,字节那么大一个公司,听说福利待遇都还不错;对,其实在字节什么都好,工作环境、各种福利和免费的三餐,但我不开心,以前听过一个理论,说留在一家公司的原因有三个:钱,工作内容,一起工作的人,这其中如果有两项崩塌了,那么就该走人了;对于当时的我来说,这三项几乎都崩塌了。</p>
<p>首先是钱,被收购了一年,期间也经历了一次字节的绩效考核,在我的绩效还不错的情况下,薪资依然没有任何调整,这完全没有兑现被收购时对我们的承诺,就挺难受的,像是自己的工作成绩不被认可。</p>
<p>然后是工作内容和一起工作的人,这两点放在一起说,今年年初的时候,我从普通的业务组转到了一个新成立的工程效能小组,专门负责一些技术选型、代码规范和跨业务组的公共事务等;因为我其实还比较喜欢和擅长这类事情的,早在被收购一年以前,我就提议和建立了公司的后端公共代码库,放一些公共的函数库和框架,推动大家使用和维护这些公共的东西而不是自己耗时耗力再造轮子,也慢慢开始接手公司其它的公共仓库和后端的 Web 框架,所以后面就开始全职做这一块了,并且我自己一直觉得这件事情长期来看、对于整个团队而言一定是非常有价值的,大家都用同样的技术栈和框架,遵守同样的代码规范,所有人的代码看起来都“一样”对新老同学来说都是好事,很好阅读和理解。</p>
<p>但是随着人员的增多,推动代码规范和一些其它的公共事务变得越来越难和不被理解,很多人觉得这没什么用,只是在徒增他们的工作量,就一直抱怨;我感觉他们的理念就是“又不是不能用”,代码只要能跑就行,怎么写,写成什么样都不重要,我挺寒心的,后面就直接开始摆烂了,不想管他们了,也不再推出新的规范,再加上每天听着那些抱怨很烦,我自己本身心态也不算很好的那种,容易被影响,久而久之就是恶性循环,工作一点热情都没有了,所以最后就决定离职了。</p>
<p>当然,前面说的只是一小部分人,大部分以前的同事还是不错的,也没有过抱怨,合作起来也很愉快,不过也不都是别人的问题,直到后面看到 Envoy 创始人在 Envoy 开源五周年的时候写的一篇文章,其中也提到了推进公共事务,看了之后才恍然大悟,我们之前也做得不够好,我和我 leader 的方式都是比较强硬的那种,几乎都是“大棒”式的。</p>
<blockquote>
<p>2016 年初,我们决定推动一个 100% 覆盖的服务网格。最初,我们认为这将是一个艰难的过程,需要自上而下的授权。在实践中,团队报名参加了迁移,因为他们将得到的好处是显而易见的。“胡萝卜”式的迁移几乎总是成功的。而“大棒”式的迁移则很少成功,或者即使成功了,也会在组织内留下眼泪和愤怒。</p>
<p>—— Matt Klein <a href="https://mattklein123.dev/2021/09/14/5-years-envoy-oss/">5 years of Envoy OSS</a></p>
</blockquote>
<p>后面也反思了一下,以后可能就不会再做这类事情了,我就把自己负责的业务模块做好,先把自己管好,要写出简单、干净和容易懂的代码:</p>
<blockquote>
<p>I try to delete more code than I write.</p>
</blockquote>
<p>不过我对工程效能这个方向还是挺感兴趣的,后面有合适的机会的话还是想再试试。</p>
<p>虽然离开字节这个大平台的选择不一定是最理智、最正确的,但这是我喜欢的选择,做喜欢的选择吧,无论对错。</p>
<p>离职之后休息了一段时间开始找工作,一直也没有一个很清晰的职业规划,没有非常明确且坚定的想要做的方向,想了很久说不如去做我心心念念的开源吧,无论什么业务方向,产品是开源的就行。</p>
<p>开源一直是我想做的事,无论是工作还是自己的业余项目,或者日常用的工具都是依托于各种大大小小的开源项目,真的是受益于开源世界太多,我也想做一些微小的贡献来回馈一下开源世界,希望以后不管做什么业务方向,都能一直在开源这条路上走下去,即便是为了开源而开源,我也相信是有意义的。</p>
<p>最后选择了加入 <a href="https://github.com/kubesphere/kubesphere">KubeSphere</a> 团队,现在的业务方向是多集群管理,平常也在给 <a href="https://github.com/karmada-io/karmada">Karmada</a> 贡献一些代码,前段时间也成功“混”到了 Member,当时很开心,还发了条朋友圈:</p>
<p><img src="https://xinzhao.me/img/2021-summary/8.jpg" alt="" /></p>
<h2 id="%E8%BF%99%E4%B8%80%E5%B9%B4%E4%B9%B0%E7%9A%84%E6%9C%89%E8%B6%A3%E7%9A%84%E4%B8%9C%E8%A5%BF" tabindex="-1">这一年买的有趣的东西</h2>
<h3 id="%E5%9B%9E%E5%BD%92%E8%8B%B9%E6%9E%9C%E7%94%9F%E6%80%81%E5%9C%88" tabindex="-1">回归苹果生态圈</h3>
<p>19 年的时候我服役很久的 iPhone SE 不太行了,考虑换手机,当时三星刚刚发布了 S10 系列,鬼迷心窍想试试现在的安卓怎么样了,就买了 S10+,结果证明还是不适合我,并且安卓没办法和 macOS 协作:传文件不方便,跨设备复制粘贴和 Handoff 等都没有了。</p>
<p>所以今年双十一又换回了 iPhone,顺便也买了 AirPods,无线耳机方便了很多,并且 AirPods 和手机电脑都能配合得很好;以前到公司的第一件事就是把电脑放好然后插上耳机,现在就完全解放了,桌面也整洁了不少,电脑上就只有一根连显示器的线了。</p>
<h3 id="eos-rp" tabindex="-1">EOS RP</h3>
<p>4 月底发完年终奖之后有点闲钱,再加上之前一直对(人像)摄影很有兴趣,所以就入手了佳能的 EOS RP 和一个 RF 50mm F1.8 的镜头。</p>
<p>今年也拍到了一些好看的照片,都在<a href="https://xinzhao.me/tags/%E6%91%84%E5%BD%B1/">摄影</a>这个 tag 里面,我的主攻方向人像的也有,帮几位朋友拍了点照片,但是不太好发到博客上,希望明年能拍到更多更好看的照片。</p>
<h3 id="%E7%9B%B2%E7%9B%92" tabindex="-1">盲盒</h3>
<p>今年对盲盒有点上瘾,刚开始抱着试一试的心态买了一盒泡泡玛特和宝可梦的联名,结果完全超出我的预期,做工、细节和质感都很好,每一个都挺精致的:</p>
<p><img src="https://xinzhao.me/img/2021-summary/2.jpg" alt="" /></p>
<p class="caption">皮卡丘和可达鸭</p>
<p>宝可梦系列还有一个插曲,因为这盒我开了几个就抽到了“隐藏”款的伊布,当时很高兴,还以为幸运之神终于开始眷顾我了,结果过了好久我才知道这个伊布不是真正意义上的隐藏,它只是惊喜款,你只要买一整盒是必出这个伊布的。</p>
<p>还有一件有趣的事,之前去线下的泡泡玛特店逛的时候,看见有人一直拿着盲盒在那儿摇,我当时就很好奇,这玩意儿真能摇出来是什么吗?后面有一次和一个朋友去泡泡玛特的时候也试了下这个方法,事实证明这个方法非常科学高效!每款的大小和重量都不一样,根据拿在手里的感觉和摇的时候上下左右的空隙,有些款还蛮好判断的,而且可以再去网上搜搜攻略,有人会发每个摇起来是什么感觉,哪款最重,哪款最轻;综合这些信息,选可能是你喜欢的那些,再排除不喜欢的,剩下的里面靠运气选。</p>
<p>那一天我们在那儿摇了好久,看见有喜欢的系列就去试试,最后结果还挺好,综合准确率可能有 60%-70%,而且小黄人坐骑系列我们只买了两个,两个都猜中了,恰好就是最想要的那两个(当然也可能单纯是运气比较好)。</p>
<p><img src="https://xinzhao.me/img/2021-summary/3.jpg" alt="" /></p>
<p class="caption">小黄人坐骑系列,还有一个是独角兽那款</p>
<p>这里就不得不再点名“表扬”一下 Soap Studio 的猫和老鼠系列,本来这个系列的创意真的超级好,看图片也很不错,但是 Soap Studio 的品控不太行啊,我买了猫和老鼠泰迪熊公仔和方块猫三角鼠,这两个都有点瑕疵,泰迪熊公仔那个单独的 Jerry 真的太轻了,好像是空心的,质感不行,也不够精细,然后方块猫三角鼠两个手办上都有一些划痕,摆在那里远看倒是没问题,但是仔细看就挺不舒服的;我看泡泡玛特线下的门店里面也有摆放这个(虽然当时没问这个卖不卖),都放人家店里了就不能好好和人家学下品控吗。</p>
<p><img src="https://xinzhao.me/img/2021-summary/4.jpg" alt="" />
<img src="https://xinzhao.me/img/2021-summary/5.jpg" alt="" /></p>
<p class="caption">没拍照,就用官方的图了</p>
<h3 id="be%40rbrick" tabindex="-1">BE@RBRICK</h3>
<p>今年还买了一个皮卡丘的熊,不过只是 400% 的:</p>
<p><img src="https://xinzhao.me/img/2021-summary/6.jpg" alt="" /></p>
<p class="caption">BE@RBRICK Pikachu Flocky Ver. 100% + 400%</p>
<p>一旦你能接受 BE@RBRICK 外观的那种设定,就会发现这个皮卡丘还蛮可爱的;明年打算去买一个便宜一点的 1000% 的熊。</p>
<h2 id="%E5%86%99%E5%9C%A8%E6%9C%80%E5%90%8E" tabindex="-1">写在最后</h2>
<p>这就是今年的全部内容了,本来还想写点今年看过的不错的电影/电视剧这些(比如纸钞屋),但是今年没有刻意去记录,现在想不起来看过些啥了,看明年要不要写写,或者单独写篇博客记录一下。</p>
<p>今年真是魔幻的一年,发生了好多事情,有很多垃圾的,也有很多开心的,体验/买了很多新奇的东西,也干了很多蠢事,明年希望自己各方面都能更稳定一些,也要好好践行活在当下和不卑不亢,最后用我很喜欢的一句话来结尾:</p>
<blockquote>
<p>无所谓对错,让我们再试一次</p>
</blockquote>
安仁双年展
2021-12-19T00:00:00Z
https://xinzhao.me/posts/3rd-anren-biennale/
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0439.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0449.jpg" alt="" /></p>
<p class="caption">全场我最喜欢的装置,你靠近它就会响,从旁边走过或者跑过就像风铃一样一片一片响,赛博朋克电子风铃,是那种 100 年后人类和 AI 开战人类战败,末世废土上肯定会出现的装置</p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0452.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0453.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0454.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0483.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0493.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0497.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0499.jpg" alt="" />
<img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0500.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0507.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0529.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0530.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0531.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0532.jpg" alt="" />
<img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0534.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0533.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0535.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0536.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0537.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0543.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0545.jpg" alt="" />
<img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0546.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0547.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0549.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0552.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0556.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0557.jpg" alt="" />
<img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0559.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0561.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0563.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0565.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0566.jpg" alt="" />
<img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0567.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0568.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0571.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0573.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0576.jpg" alt="" /></p>
<p class="caption">一位很拽的小朋友</p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0579.jpg" alt="" />
<img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0580.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0585.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0587.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0588.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0600.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0601.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0604.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0607.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0608.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0609.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0610.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0611.jpg" alt="" /></p>
<p><img src="https://xinzhao.me/img/3rd-anren-biennale/IMG_0623.jpg" alt="" /></p>
Kubernetes 多集群项目介绍
2021-11-03T00:00:00Z
https://xinzhao.me/posts/kubernetes-multi-cluster-projects/
<p></p><div class="table-of-contents"><div class="toc-container-header">索引</div><ul><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E4%BD%BF%E7%94%A8%E9%9B%86%E7%BE%A4%E8%81%94%E9%82%A6">为什么要使用集群联邦</a></li><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#federation-v1">Federation v1</a></li><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#federation-v2">Federation v2</a><ul><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#cluster-configuration">Cluster Configuration</a></li><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#type-configuration">Type Configuration</a></li><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#federated-resource-crd">Federated Resource CRD</a></li><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#scheduling">Scheduling</a></li></ul></li><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#karmada">Karmada</a><ul><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#cluster">Cluster</a></li><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#propagation-policy">Propagation Policy</a></li><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#override-policy">Override Policy</a></li><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#demo">Demo</a></li></ul></li><li><a href="https://xinzhao.me/posts/kubernetes-multi-cluster-projects/#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5">参考链接</a></li></ul></div><p></p>
<h2 id="%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E4%BD%BF%E7%94%A8%E9%9B%86%E7%BE%A4%E8%81%94%E9%82%A6" tabindex="-1">为什么要使用集群联邦</h2>
<p>Kubernetes 从 1.8 版本起就声称单集群最多可支持 5000 个节点和 15 万个 Pod,实际上应该很少有公司会部署如此庞大的一个单集群,很多情况下因为各种各样的原因我们可能会部署多个集群,但是又想将它们统一起来管理,这时候就需要用到集群联邦(Federation)。</p>
<p>集群联邦的一些典型应用场景:</p>
<ul>
<li>高可用:在多个集群上部署应用,可以最大限度地减少集群故障带来的影响</li>
<li>避免厂商锁定:可以将应用负载分布在多个厂商的集群上并在有需要时直接迁移到其它厂商</li>
<li>故障隔离:拥有多个小集群可能比单个大集群更利于故障隔离</li>
</ul>
<h2 id="federation-v1" tabindex="-1"><a href="https://github.com/kubernetes-retired/federation">Federation v1</a></h2>
<p>最早的多集群项目,由 K8s 社区提出和维护。</p>
<p>Federation v1 在 K8s v1.3 左右就已经着手设计(Design Proposal),并在后面几个版本中发布了相关的组件与命令行工具(kubefed),用于帮助使用者快速建立联邦集群,并在 v1.6 时,进入了 Beta 阶段;但 Federation v1 在进入 Beta 后,就没有更进一步的发展,由于灵活性和 API 成熟度的问题,在 K8s v1.11 左右正式被弃用。</p>
<p><img src="https://xinzhao.me/img/kubernetes-multi-cluster-projects/federation-v1.jpg" alt="" /></p>
<p>v1 的基本架构如上图,主要有三个组件:</p>
<ul>
<li>Federation API Server:类似 K8s API Server,对外提供统一的资源管理入口,但只允许使用 <a href="https://github.com/kubernetes-retired/federation/tree/master/pkg/federatedtypes">Adapter</a> 拓展支持的 K8s 资源</li>
<li>Controller Manager:提供多个集群间资源调度及状态同步,类似 kube-controller-manager</li>
<li>Etcd:储存 Federation 的资源</li>
</ul>
<p>在 v1 版本中我们要创建一个联邦资源的大致步骤如下:把联邦的所有配置信息都写到资源对象 annotations 里,整个创建流程与 K8s 类似,将资源创建到 Federation API Server,之后 Federation Controller Manager 会根据 annotations 里面的配置将该资源创建到各子集群;下面是一个 ReplicaSet 的示例:</p>
<p><img src="https://xinzhao.me/img/kubernetes-multi-cluster-projects/federation-v1-replicaset.png" alt="" /></p>
<p>这种架构带来的主要问题有两个:</p>
<ul>
<li>不够灵活,每当创建一种新资源都要新增 <a href="https://github.com/kubernetes-retired/federation/tree/master/pkg/federatedtypes">Adapter</a>(提交代码再发版);并且对象会携带许多 Federation 专属的 Annotation</li>
<li>缺少独立的 API 对象版本控制,例如 Deployment 在 Kubernetes 中是 GA,但在 Federation v1 中只是 Beta</li>
</ul>
<h2 id="federation-v2" tabindex="-1"><a href="https://github.com/kubernetes-sigs/kubefed">Federation v2</a></h2>
<p>有了 v1 版本的经验和教训之后,社区提出了新的集群联邦架构:Federation v2;Federation 项目的演进也可以参考 <a href="https://kubernetes.io/blog/2018/12/12/kubernetes-federation-evolution/">Kubernetes Federation Evolution</a> 这篇文章。</p>
<p>v2 版本利用 CRD 实现了整体功能,通过定义多种自定义资源(CR),从而省掉了 v1 中的 API Server;v2 版本由两个组件构成:</p>
<ul>
<li>admission-webhook 提供了准入控制</li>
<li>controller-manager 处理自定义资源以及协调不同集群间的状态</li>
</ul>
<p>在 v2 版本中要创建一个联邦资源的大致流程如下:</p>
<p><img src="https://xinzhao.me/img/kubernetes-multi-cluster-projects/federation-v2.png" alt="" /></p>
<p>将 Federated Resource 创建到 Host 集群的 API Server 中,之后 controller-manager 会介入将相应资源分发到不同的集群,分发的规则等都写在了这个 Federated Resource 对象里面。</p>
<p>在逻辑上,Federation v2 分为两个大部分:configuration(配置)和 propagation(分发);configuration 主要包含两个配置:Cluster Configuration 和 Type Configuration。</p>
<h3 id="cluster-configuration" tabindex="-1">Cluster Configuration</h3>
<p>用来保存被联邦托管的集群的 API 认证信息,可通过 <code>kubefedctl join/unjoin</code> 来加入/删除集群,当成功加入时,会建立一个 <code>KubeFedCluster</code> CR 来存储集群相关信息,如 API Endpoint、CA Bundle 和 Token 等。后续 controller-manager 会使用这些信息来访问不同 Kubernetes 集群。</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> core.kubefed.io/v1beta1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> KubeFedCluster<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">creationTimestamp</span><span class="token punctuation">:</span> <span class="token string">"2019-10-24T08:05:38Z"</span><br /> <span class="token key atrule">generation</span><span class="token punctuation">:</span> <span class="token number">1</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> cluster1<br /> <span class="token key atrule">namespace</span><span class="token punctuation">:</span> kube<span class="token punctuation">-</span>federation<span class="token punctuation">-</span>system<br /> <span class="token key atrule">resourceVersion</span><span class="token punctuation">:</span> <span class="token string">"647452"</span><br /> <span class="token key atrule">selfLink</span><span class="token punctuation">:</span> /apis/core.kubefed.io/v1beta1/namespaces/kube<span class="token punctuation">-</span>federation<span class="token punctuation">-</span>system/kubefedclusters/cluster1<br /> <span class="token key atrule">uid</span><span class="token punctuation">:</span> 4c5eb57f<span class="token punctuation">-</span>5ed4<span class="token punctuation">-</span>4cec<span class="token punctuation">-</span>89f3<span class="token punctuation">-</span>cfc062492ae0<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">apiEndpoint</span><span class="token punctuation">:</span> https<span class="token punctuation">:</span>//172.16.200.1<span class="token punctuation">:</span><span class="token number">6443</span><br /> <span class="token key atrule">caBundle</span><span class="token punctuation">:</span> LS<span class="token punctuation">...</span>.Qo=<br /> <span class="token key atrule">secretRef</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> cluster1<span class="token punctuation">-</span>shb2x<br /><span class="token key atrule">status</span><span class="token punctuation">:</span><br /> <span class="token key atrule">conditions</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">lastProbeTime</span><span class="token punctuation">:</span> <span class="token string">"2019-10-28T06:25:58Z"</span><br /> <span class="token key atrule">lastTransitionTime</span><span class="token punctuation">:</span> <span class="token string">"2019-10-28T05:13:47Z"</span><br /> <span class="token key atrule">message</span><span class="token punctuation">:</span> /healthz responded with ok<br /> <span class="token key atrule">reason</span><span class="token punctuation">:</span> ClusterReady<br /> <span class="token key atrule">status</span><span class="token punctuation">:</span> <span class="token string">"True"</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> Ready<br /> <span class="token key atrule">region</span><span class="token punctuation">:</span> <span class="token string">""</span></code></pre>
<p><img src="https://xinzhao.me/img/kubernetes-multi-cluster-projects/federation-v2-sync-controller.png" alt="" /></p>
<h3 id="type-configuration" tabindex="-1">Type Configuration</h3>
<p>定义了哪些 Kubernetes API 资源要被用于联邦管理;比如说想将 <code>ConfigMap</code> 资源通过联邦机制建立在不同集群上时,就必须先在 Host 集群中,通过 CRD 建立新资源 <code>FederatedConfigMap</code>,接着再建立名称为 <code>configmaps</code> 的 Type configuration(<code>FederatedTypeConfig</code>)资源,然后描述 <code>ConfigMap</code> 要被 <code>FederatedConfigMap</code> 所管理,这样 Kubefed controller-manager 才能知道如何建立 Federated 资源,一个示例如下:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> core.kubefed.k8s.io/v1beta1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> FederatedTypeConfig<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> configmaps<br /> <span class="token key atrule">namespace</span><span class="token punctuation">:</span> kube<span class="token punctuation">-</span>federation<span class="token punctuation">-</span>system<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">federatedType</span><span class="token punctuation">:</span><br /> <span class="token key atrule">group</span><span class="token punctuation">:</span> types.kubefed.k8s.io<br /> <span class="token key atrule">kind</span><span class="token punctuation">:</span> FederatedConfigMap<br /> <span class="token key atrule">pluralName</span><span class="token punctuation">:</span> federatedconfigmaps<br /> <span class="token key atrule">scope</span><span class="token punctuation">:</span> Namespaced<br /> <span class="token key atrule">version</span><span class="token punctuation">:</span> v1beta1<br /> <span class="token key atrule">propagation</span><span class="token punctuation">:</span> Enabled<br /> <span class="token key atrule">targetType</span><span class="token punctuation">:</span><br /> <span class="token key atrule">kind</span><span class="token punctuation">:</span> ConfigMap<br /> <span class="token key atrule">pluralName</span><span class="token punctuation">:</span> configmaps<br /> <span class="token key atrule">scope</span><span class="token punctuation">:</span> Namespaced<br /> <span class="token key atrule">version</span><span class="token punctuation">:</span> v1</code></pre>
<h3 id="federated-resource-crd" tabindex="-1">Federated Resource CRD</h3>
<p>其中还有一个关键的 CRD:Federated Resource,如果想新增一种要被联邦托管的资源的话,就需要建立一个新的 FederatedXX 的 CRD,用来描述对应资源的结构和分发策略(需要被分发到哪些集群上);Federated Resource CRD 主要包括三部分:</p>
<ul>
<li>Templates 用于描述被联邦的资源</li>
<li>Placement 用来描述将被部署的集群,若没有配置,则不会分发到任何集群中</li>
<li>Overrides 允许对部分集群的部分资源进行覆写</li>
</ul>
<p>一个示例如下:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> types.kubefed.k8s.io/v1beta1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> FederatedDeployment<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> test<span class="token punctuation">-</span>deployment<br /> <span class="token key atrule">namespace</span><span class="token punctuation">:</span> test<span class="token punctuation">-</span>namespace<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">template</span><span class="token punctuation">:</span> <span class="token comment"># 定义 Deployment 的所有內容,可理解成 Deployment 与 Pod 之间的关联。</span><br /> <span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">labels</span><span class="token punctuation">:</span><br /> <span class="token key atrule">app</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token punctuation">...</span><br /> <span class="token key atrule">placement</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusters</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> cluster2<br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> cluster1<br /> <span class="token key atrule">overrides</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">clusterName</span><span class="token punctuation">:</span> cluster2<br /> <span class="token key atrule">clusterOverrides</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">path</span><span class="token punctuation">:</span> spec.replicas<br /> <span class="token key atrule">value</span><span class="token punctuation">:</span> <span class="token number">5</span></code></pre>
<p>这些 FederatedXX CRD 可以通过 <code>kubefedctl enable <target kubernetes API type></code> 来创建,也可以自己生成/编写对应的 CRD 再创建。</p>
<p>结合上面介绍了的 Cluster Configuration、Type Configuration 和 Federated Resource CRD,再来看 v2 版本的整体架构和相关概念就清晰很多了:</p>
<p><img src="https://xinzhao.me/img/kubernetes-multi-cluster-projects/federation-v2-concepts.png" alt="" /></p>
<h3 id="scheduling" tabindex="-1">Scheduling</h3>
<p>Kubefed 目前只能做到一些简单的集群间调度,即手工指定,对于手工指定的调度方式主要分为两部分,一是直接在资源中制定目的集群,二是通过 <code>ReplicaSchedulingPreference</code> 进行比例分配。</p>
<p>直接在资源中指定可以通过 <code>clusters</code> 指定一个 <code>cluster</code> 列表,或者通过 <code>clusterSelector</code> 来根据集群标签选择集群,不过有两点要注意:</p>
<ul>
<li>如果 <code>clusters</code> 字段被指定,<code>clusterSelector</code> 将会被忽略</li>
<li>被选择的集群是平等的,该资源会在每个被选中的集群中部署一个无差别副本</li>
</ul>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">placement</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusters</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> cluster2<br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> cluster1<br /> <span class="token key atrule">clusterSelector</span><span class="token punctuation">:</span><br /> <span class="token key atrule">matchLabels</span><span class="token punctuation">:</span><br /> <span class="token key atrule">foo</span><span class="token punctuation">:</span> bar</code></pre>
<p>如果需要在多个集群间进行区别调度的话就需要引入 <code>ReplicaSchedulingPreference</code> 进行按比例的调度了:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> scheduling.kubefed.io/v1alpha1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> ReplicaSchedulingPreference<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> test<span class="token punctuation">-</span>deployment<br /> <span class="token key atrule">namespace</span><span class="token punctuation">:</span> test<span class="token punctuation">-</span>ns<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">targetKind</span><span class="token punctuation">:</span> FederatedDeployment<br /> <span class="token key atrule">totalReplicas</span><span class="token punctuation">:</span> <span class="token number">9</span><br /> <span class="token key atrule">clusters</span><span class="token punctuation">:</span><br /> <span class="token key atrule">A</span><span class="token punctuation">:</span><br /> <span class="token key atrule">minReplicas</span><span class="token punctuation">:</span> <span class="token number">4</span><br /> <span class="token key atrule">maxReplicas</span><span class="token punctuation">:</span> <span class="token number">6</span><br /> <span class="token key atrule">weight</span><span class="token punctuation">:</span> <span class="token number">1</span><br /> <span class="token key atrule">B</span><span class="token punctuation">:</span><br /> <span class="token key atrule">minReplicas</span><span class="token punctuation">:</span> <span class="token number">4</span><br /> <span class="token key atrule">maxReplicas</span><span class="token punctuation">:</span> <span class="token number">8</span><br /> <span class="token key atrule">weight</span><span class="token punctuation">:</span> <span class="token number">2</span></code></pre>
<p><code>totalReplicas</code> 定义了总副本数,<code>clusters</code> 描述不同集群的最大/最小副本以及权重。</p>
<p>目前 ReplicaSchedulingPreference 只支持 <a href="https://github.com/kubernetes-sigs/kubefed/blob/224fe9501b0709fb2999451339d7e756a8a94e9f/pkg/schedulingtypes/replicascheduler.go#L47-L54">deployments 和 replicasets 两种资源</a>。</p>
<h2 id="karmada" tabindex="-1"><a href="https://github.com/karmada-io/karmada">Karmada</a></h2>
<p>Karmada 是由华为开源的多云容器编排项目,这个项目是 Kubernetes Federation v1 和 v2 的延续,一些基本概念继承自这两个版本。</p>
<p><img src="https://xinzhao.me/img/kubernetes-multi-cluster-projects/karmada-architecture.png" alt="" /></p>
<p>Karmada 主要有三个组件:</p>
<ul>
<li>Karmada API Server:本质就是一个普通的 K8s API Server,绑定了一个单独的 etcd 来存储那些要被联邦托管的资源</li>
<li>Karmada Controller Manager:多个 controller 的集合,监听 Karmada API Server 中的对象并与成员集群 API server 进行通信</li>
<li>Karmada Scheduler:提供高级的多集群调度策略</li>
</ul>
<p>和 Federation v1 类似,我们下发一个资源也是要写入到 Karmada 自己的 API Server 中,之前 controller-manager 根据一些 policy 把资源下发到各个集群中;不过这个 API Server 是 K8s 原生的,所以支持任何资源,不会出现之前 Federation v1 版本中的问题,然后联邦托管资源的分发策略也是由一个单独的 CRD 来控制的,也不需要配置 v2 中的 Federated Resource CRD 和 Type Configure。</p>
<p>Karmada 的一些基本概念:</p>
<ul>
<li>资源模板(Resource Template):Karmada 使用 K8s 原生 API 定义作为资源模板,便于快速对接 K8s 生态工具链</li>
<li>分发策略(Propagation Policy):Karmada 提供独立的策略 API,用来配置资源分发策略</li>
<li>差异化策略(Override Policy):Karmada 提供独立的差异化 API,用来配置与集群相关的差异化配置,比如配置不同集群使用不同的镜像</li>
</ul>
<p><img src="https://xinzhao.me/img/kubernetes-multi-cluster-projects/karmada-resource-relation.png" alt="" /></p>
<h3 id="cluster" tabindex="-1">Cluster</h3>
<p>Cluster 资源记录的内容和 Federation v2 类似,就是访问被纳管集群的一些必要信息:API Endpoint、CA Bundle 和访问 Token。</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">apiEndpoint</span><span class="token punctuation">:</span> https<span class="token punctuation">:</span>//172.31.165.66<span class="token punctuation">:</span><span class="token number">55428</span><br /> <span class="token key atrule">secretRef</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> member1<br /> <span class="token key atrule">namespace</span><span class="token punctuation">:</span> karmada<span class="token punctuation">-</span>cluster<br /> <span class="token key atrule">syncMode</span><span class="token punctuation">:</span> Push</code></pre>
<p>但是有一个不一样的点是,Karmada 的 <code>Cluster</code> 资源有两种 sync 模式:<code>Push</code> 和 <code>Pull</code>;<code>Push</code> 就是最普通、最常见的方式,host 集群的 Karmada 组件会负责同步并更新这类集群的状态;<code>Pull</code> 模式的 member 集群上会运行一个 <code>karmada-agent</code> 组件,这个组件会负责收集自己的状态并且更新 host 集群的相应的 <code>Cluster</code> 资源状态。</p>
<h3 id="propagation-policy" tabindex="-1">Propagation Policy</h3>
<p>在 Karmada 中分发资源到 member 集群需要配置这个单独 <code>PropagationPolicy</code> CR;以下面的 nginx 应用为例,首先是 Resource Template,这个就是普通的 K8s <code>Deployment</code>:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> apps/v1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> Deployment<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">labels</span><span class="token punctuation">:</span><br /> <span class="token key atrule">app</span><span class="token punctuation">:</span> nginx<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">replicas</span><span class="token punctuation">:</span> <span class="token number">2</span><br /> <span class="token key atrule">selector</span><span class="token punctuation">:</span><br /> <span class="token key atrule">matchLabels</span><span class="token punctuation">:</span><br /> <span class="token key atrule">app</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">template</span><span class="token punctuation">:</span><br /> <span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">labels</span><span class="token punctuation">:</span><br /> <span class="token key atrule">app</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">containers</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">image</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx</code></pre>
<p>之后配置一个 <code>PropagationPolicy</code> 来控制这个 nginx <code>Deployment</code> 资源的分发策略即可,在下面的示例中,会将 nginx 应用按 1:1 的权重比分发到 member1 和 member2 集群中:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> policy.karmada.io/v1alpha1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> PropagationPolicy<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx<span class="token punctuation">-</span>propagation<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">resourceSelectors</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> apps/v1<br /> <span class="token key atrule">kind</span><span class="token punctuation">:</span> Deployment<br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">placement</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterAffinity</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterNames</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> member1<br /> <span class="token punctuation">-</span> member2<br /> <span class="token key atrule">replicaScheduling</span><span class="token punctuation">:</span><br /> <span class="token key atrule">replicaDivisionPreference</span><span class="token punctuation">:</span> Weighted<br /> <span class="token key atrule">replicaSchedulingType</span><span class="token punctuation">:</span> Divided<br /> <span class="token key atrule">weightPreference</span><span class="token punctuation">:</span><br /> <span class="token key atrule">staticWeightList</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">targetCluster</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterNames</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> member1<br /> <span class="token key atrule">weight</span><span class="token punctuation">:</span> <span class="token number">1</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">targetCluster</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterNames</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> member2<br /> <span class="token key atrule">weight</span><span class="token punctuation">:</span> <span class="token number">1</span></code></pre>
<p>在 Karmada API Server 中创建这两个资源后,可以通过 Karmada API Server 查询到该资源的状态:</p>
<pre class="language-text"><code class="language-text">$ kubectl get deploy<br />NAME READY UP-TO-DATE AVAILABLE AGE<br />nginx 2/2 2 2 51s</code></pre>
<p>但是注意,这并不代表应用运行在 Karmada API Server 所在的集群上,实际上这个集群上没有任何工作负载,只是存储了这些 Resource Template,实际的工作负载都运行在上面 <code>PropagationPolicy</code> 配置的 member1 和 member2 集群中,切换到 member1/member2 集群中可以看到:</p>
<pre class="language-text"><code class="language-text">$ kubectl get deploy<br />NAME READY UP-TO-DATE AVAILABLE AGE<br />nginx 1/1 1 1 6m26s<br /><br />$ kubectl get pod<br />NAME READY STATUS RESTARTS AGE<br />nginx-6799fc88d8-7cgfz 1/1 Running 0 6m29s</code></pre>
<p>分发策略除了上面最普通的指定集群名称外也支持 <code>LabelSelector</code>、<code>FieldSelector</code> 和 <code>ExcludeClusters</code>,如果这几个筛选项都设置了,那只有全部条件都满足的集群才会被筛选出来;除了集群亲和性,还支持 <code>SpreadConstraints</code>:对集群动态分组,按 <code>region</code>、<code>zone</code> 和 <code>provider</code> 等分组,可以将应用只分发到某类集群中。</p>
<p>针对有 <code>replicas</code> 的资源(比如原生的 <code>Deployment</code> 和 <code>StatefulSet</code>),支持在分发资源到不同集群的时候按要求更新这个副本数,比如 member1 集群上的 nginx 应用我希望有 2 个副本,member2 上的只希望有 1 个副本;策略有很多,首先是 <code>ReplicaSchedulingType</code>,有两个可选值:</p>
<ul>
<li><code>Duplicated</code>:每个候选成员集群的副本数都是一样的,从原本资源那里面复制过来的,和不设置 <code>ReplicaSchedulingStrategy</code> 的效果是一样的</li>
<li><code>Divided</code>:根据有效的候选成员集群的数量,将副本分成若干部分,每个集群的副本数由 <code>ReplicaDivisionPreference</code> 决定</li>
</ul>
<p>而这个 <code>ReplicaDivisionPreference</code> 又有两个可选值:</p>
<ul>
<li><code>Aggregated</code>:在考虑集群可用资源的情况下,将这些副本调度到尽量少的集群上,如果一个集群就能容纳所有副本,那只会调度到这一个集群上,其它集群上也会存在相应的资源,不过副本数是 0</li>
<li><code>Weighted</code>:根据 <code>WeightPreference</code> 来划分副本数,这个 <code>WeightPreference</code> 就很简单了,直接指定每个集群的权重是多少</li>
</ul>
<p>完整和详细的结构可以参考 <code>Placement</code> 的 <a href="https://github.com/karmada-io/karmada/blob/085fdd23c0390f30009d446ef96a688e8fae1b4d/pkg/apis/policy/v1alpha1/propagation_types.go#L89">API 定义</a>。</p>
<h3 id="override-policy" tabindex="-1">Override Policy</h3>
<p>Override Policy 就很简单了,通过增加 <code>OverridePolicy</code> 这个 CR 来配置不同集群的差异化配置,直接看一个例子:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> policy.karmada.io/v1alpha1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> OverridePolicy<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> example<span class="token punctuation">-</span>override<br /> <span class="token key atrule">namespace</span><span class="token punctuation">:</span> default<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">resourceSelectors</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> apps/v1<br /> <span class="token key atrule">kind</span><span class="token punctuation">:</span> Deployment<br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">targetCluster</span><span class="token punctuation">:</span><br /> <span class="token key atrule">clusterNames</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> member1<br /> <span class="token key atrule">labelSelector</span><span class="token punctuation">:</span><br /> <span class="token key atrule">matchLabels</span><span class="token punctuation">:</span><br /> <span class="token key atrule">failuredomain.kubernetes.io/region</span><span class="token punctuation">:</span> dc1<br /> <span class="token key atrule">overriders</span><span class="token punctuation">:</span><br /> <span class="token key atrule">plaintext</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">path</span><span class="token punctuation">:</span> /spec/template/spec/containers/0/image<br /> <span class="token key atrule">operator</span><span class="token punctuation">:</span> replace<br /> <span class="token key atrule">value</span><span class="token punctuation">:</span> <span class="token string">'dc-1.registry.io/nginx:1.17.0-alpine'</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">path</span><span class="token punctuation">:</span> /metadata/annotations<br /> <span class="token key atrule">operator</span><span class="token punctuation">:</span> add<br /> <span class="token key atrule">value</span><span class="token punctuation">:</span><br /> <span class="token key atrule">foo</span><span class="token punctuation">:</span> bar</code></pre>
<h3 id="demo" tabindex="-1">Demo</h3>
<p>最后看一个 Karmada 官方的例子:</p>
<p><img src="https://xinzhao.me/img/kubernetes-multi-cluster-projects/karmada-sample-nginx.svg" alt="" /></p>
<h2 id="%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5" tabindex="-1">参考链接</h2>
<ul>
<li><a href="https://blog.ihypo.net/15716465002689.html">Kubernetes 多集群管理:Kubefed(Federation v2)</a></li>
<li><a href="https://blog.ihypo.net/15718231244282.html">使用 Kubernetes 联邦(Kubefed)进行多集群管理</a></li>
<li><a href="https://jimmysong.io/kubernetes-handbook/practice/federation.html">集群联邦(Cluster Federation)</a></li>
<li><a href="https://support.huaweicloud.com/productdesc-mcp/mcp_productdesc_0001.html">什么是多云容器平台</a></li>
</ul>
Kubernetes 组件单元测试指南
2021-07-15T00:00:00Z
https://xinzhao.me/posts/k8s-unittest-guide/
<p></p><div class="table-of-contents"><div class="toc-container-header">索引</div><ul><li><a href="https://xinzhao.me/posts/k8s-unittest-guide/#%E5%89%8D%E8%A8%80">前言</a></li><li><a href="https://xinzhao.me/posts/k8s-unittest-guide/#%E4%BD%BF%E7%94%A8-fake-client">使用 fake client</a><ul><li><a href="https://xinzhao.me/posts/k8s-unittest-guide/#%E5%8E%9F%E7%94%9F-client">原生 client</a></li><li><a href="https://xinzhao.me/posts/k8s-unittest-guide/#generic-client">generic client</a></li></ul></li><li><a href="https://xinzhao.me/posts/k8s-unittest-guide/#%E6%9E%84%E9%80%A0%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%9B%86%E7%BE%A4">构造轻量级集群</a></li><li><a href="https://xinzhao.me/posts/k8s-unittest-guide/#%E5%B8%B8%E7%94%A8%E5%9C%BA%E6%99%AF%E5%AF%B9%E6%AF%94">常用场景对比</a></li></ul></div><p></p>
<h2 id="%E5%89%8D%E8%A8%80" tabindex="-1">前言</h2>
<p>单元测试相关概念和基础内容这里不过多介绍,可以参考 go 官方的一些指南和网上的其它资料:</p>
<ul>
<li><a href="https://github.com/golang/go/wiki/LearnTesting">LearnTesting</a></li>
<li><a href="https://github.com/golang/go/wiki/TableDrivenTests">TableDrivenTests</a></li>
</ul>
<p>针对需要操作 k8s 的组件,单测的关键在于如何在单测的函数中构造一个 k8s 集群出来供业务函数对相应资源进行 CRUD,构造 k8s 集群的大致思路主要分为两类:使用 fake client 构造一个假的和在单测的过程中构造一个真的、轻量级的 k8s 集群;下面将逐一介绍这两种方法。</p>
<p><em>注:根据不同的测试对象,选择合适的、能达到测试目的方法即可,不必强行使用某一种方法</em></p>
<h2 id="%E4%BD%BF%E7%94%A8-fake-client" tabindex="-1">使用 fake client</h2>
<p>fake client 基本只能用来 CRUD 各种资源(但其实这能覆盖到大部分场景了),一些其它的操作比如触发 informer 的 callback 事件等它是实现不了的,所以如果测试代码也想覆盖这类场景,需要使用下面的构造真正集群的方法;使用 fake client 测试步骤大致如下:</p>
<ol>
<li>构造测试数据
<ul>
<li>即各种测试 case 中需要的(原生和自定义)资源对象</li>
</ul>
</li>
<li>使用上面的测试数据生成 fake client
<ul>
<li>把这些测试对象 append 到 fake client 中</li>
</ul>
</li>
<li>替换业务函数使用的 client 为 fake client
<ul>
<li>这个具体看业务函数是怎么实现的,怎么获取 k8s client 的</li>
</ul>
</li>
</ol>
<p>fake client 可以再细分成下面两类:</p>
<h3 id="%E5%8E%9F%E7%94%9F-client" tabindex="-1">原生 client</h3>
<p>指原生的 <a href="https://github.com/kubernetes/client-go">client-go</a> 和使用 code-generator 生成的各 CR 的 typed client,这些 client 都提供了相应的 fake client 方法,fake client 很好构造,只用一个函数,把测试需要用到的对象都加进去就行:</p>
<pre class="language-go"><code class="language-go">client <span class="token operator">:=</span> fake<span class="token punctuation">.</span><span class="token function">NewSimpleClientset</span><span class="token punctuation">(</span>objects<span class="token operator">...</span><span class="token punctuation">)</span></code></pre>
<p>一个简单示例如下,首先业务函数定义如下:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// Add adds or updates the given Event object.</span><br /><span class="token keyword">func</span> <span class="token function">Add</span><span class="token punctuation">(</span>kubeClient kubernetes<span class="token punctuation">.</span>Interface<span class="token punctuation">,</span> eventObj <span class="token operator">*</span>corev1<span class="token punctuation">.</span>Event<span class="token punctuation">)</span> <span class="token builtin">error</span> <span class="token punctuation">{</span><br /> <span class="token operator">...</span><br /><span class="token punctuation">}</span></code></pre>
<p>我需要测试的场景就两种:增加一个事件和更新已有的事件,针对这两个场景构造测试数据:</p>
<pre class="language-go"><code class="language-go">tests <span class="token operator">:=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> name <span class="token builtin">string</span><br /> objects <span class="token punctuation">[</span><span class="token punctuation">]</span>runtime<span class="token punctuation">.</span>Object<br /> event <span class="token operator">*</span>corev1<span class="token punctuation">.</span>Event<br /> isErr <span class="token builtin">bool</span><br /> wantedEventCount <span class="token builtin">int32</span><br /><span class="token punctuation">}</span><span class="token punctuation">{</span><br /> <span class="token punctuation">{</span><br /> name<span class="token punctuation">:</span> <span class="token string">"exist test"</span><span class="token punctuation">,</span><br /> objects<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>runtime<span class="token punctuation">.</span>Object<span class="token punctuation">{</span>test<span class="token punctuation">.</span><span class="token function">GetEvent</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">,</span><br /> event<span class="token punctuation">:</span> test<span class="token punctuation">.</span><span class="token function">GetEvent</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> isErr<span class="token punctuation">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> wantedEventCount<span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> name<span class="token punctuation">:</span> <span class="token string">"not exist test"</span><span class="token punctuation">,</span><br /> event<span class="token punctuation">:</span> test<span class="token punctuation">.</span><span class="token function">GetEvent</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> isErr<span class="token punctuation">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> wantedEventCount<span class="token punctuation">:</span> <span class="token number">2</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span></code></pre>
<p>根据 case 的测试数据生成 fake client 并执行测试:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">for</span> <span class="token boolean">_</span><span class="token punctuation">,</span> tt <span class="token operator">:=</span> <span class="token keyword">range</span> tests <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span>tt<span class="token punctuation">.</span>name<span class="token punctuation">,</span> <span class="token keyword">func</span><span class="token punctuation">(</span>t <span class="token operator">*</span>testing<span class="token punctuation">.</span>T<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// 生成 fake client,把需要 CRUD 的资源加入进去</span><br /> client <span class="token operator">:=</span> fake<span class="token punctuation">.</span><span class="token function">NewSimpleClientset</span><span class="token punctuation">(</span>tt<span class="token punctuation">.</span>objects<span class="token operator">...</span><span class="token punctuation">)</span><br /><br /> <span class="token comment">// 执行函数</span><br /> err <span class="token operator">:=</span> <span class="token function">Add</span><span class="token punctuation">(</span>client<span class="token punctuation">,</span> tt<span class="token punctuation">.</span>event<span class="token punctuation">)</span><br /> <span class="token keyword">if</span> tt<span class="token punctuation">.</span>isErr <span class="token operator">!=</span> <span class="token punctuation">(</span>err <span class="token operator">!=</span> <span class="token boolean">nil</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Errorf</span><span class="token punctuation">(</span><span class="token string">"%s Add() unexpected error: %v"</span><span class="token punctuation">,</span> tt<span class="token punctuation">.</span>name<span class="token punctuation">,</span> err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// 校验结果是否符合预期</span><br /> eventObj<span class="token punctuation">,</span> err <span class="token operator">:=</span> client<span class="token punctuation">.</span><span class="token function">CoreV1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Events</span><span class="token punctuation">(</span>tt<span class="token punctuation">.</span>event<span class="token punctuation">.</span>Namespace<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Get</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span><span class="token function">TODO</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> tt<span class="token punctuation">.</span>event<span class="token punctuation">.</span>Name<span class="token punctuation">,</span> metav1<span class="token punctuation">.</span>GetOptions<span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Errorf</span><span class="token punctuation">(</span><span class="token string">"%s unexpected error: %v"</span><span class="token punctuation">,</span> tt<span class="token punctuation">.</span>name<span class="token punctuation">,</span> err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">if</span> eventObj<span class="token punctuation">.</span>Count <span class="token operator">!=</span> tt<span class="token punctuation">.</span>wantedEventCount <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Errorf</span><span class="token punctuation">(</span><span class="token string">"%s event Count = %d, want %d"</span><span class="token punctuation">,</span> tt<span class="token punctuation">.</span>name<span class="token punctuation">,</span> eventObj<span class="token punctuation">.</span>Count<span class="token punctuation">,</span> tt<span class="token punctuation">.</span>wantedEventCount<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p><em>注:测试函数不一定非得像示例这样写,重点了解下流程和构造 fake client 的方法即可</em></p>
<p>针对 controller 的测试,如果你有用到 lister 的话,还需要往对应资源的 informer 中增加需要的资源,这样业务代码里面 lister 才能读到相应的资源,简单示例如下:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// 创建 fake client</span><br />f<span class="token punctuation">.</span>client <span class="token operator">=</span> fake<span class="token punctuation">.</span><span class="token function">NewSimpleClientset</span><span class="token punctuation">(</span>f<span class="token punctuation">.</span>objects<span class="token operator">...</span><span class="token punctuation">)</span><br /><br /><span class="token comment">// 创建基于 fake client 的 informer</span><br />informer <span class="token operator">:=</span> informers<span class="token punctuation">.</span><span class="token function">NewSharedInformerFactory</span><span class="token punctuation">(</span>f<span class="token punctuation">.</span>client<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><br /><br /><span class="token comment">// 往 informer indexer 中添加对应的资源对象</span><br /><span class="token keyword">for</span> <span class="token boolean">_</span><span class="token punctuation">,</span> s <span class="token operator">:=</span> <span class="token keyword">range</span> f<span class="token punctuation">.</span>storageClasses <span class="token punctuation">{</span><br /> informer<span class="token punctuation">.</span><span class="token function">Native</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Storage</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">V1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">StorageClasses</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Informer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetIndexer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>s<span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>之后也是替换 client 和 informer 再运行测试即可。</p>
<h3 id="generic-client" tabindex="-1">generic client</h3>
<p>指 controller-runtime 提供的 <a href="https://github.com/kubernetes-sigs/controller-runtime/tree/master/pkg/client">generic client</a>,和上面的 typed client 不同的是,该 client 是一个通用的 client,可用于 CRUD 任何资源,测试方法基本和上面一样,只是构造 fake client 方法稍有不同,其它流程都一样,下面就只介绍构造 fake client 的方法,其它的内容就不再赘述了。</p>
<p>构造 generic client 的 fake client 的方法最大的一个不同点是它需要包含要 CRUD 的所有资源的 scheme:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// 配置 scheme 和需要添加的 objects</span><br />client <span class="token operator">:=</span> fake<span class="token punctuation">.</span><span class="token function">NewClientBuilder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">WithScheme</span><span class="token punctuation">(</span>scheme<span class="token punctuation">.</span>Scheme<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">WithObjects</span><span class="token punctuation">(</span>objs<span class="token operator">...</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<p>scheme 的一般构造方法如下:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">var</span> <span class="token punctuation">(</span><br /> <span class="token comment">// Scheme contains all types of custom clientset and kubernetes client-go clientset</span><br /> Scheme <span class="token operator">=</span> runtime<span class="token punctuation">.</span><span class="token function">NewScheme</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token punctuation">)</span><br /><br /><span class="token keyword">func</span> <span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// 添加你需要的资源的 scheme</span><br /> <span class="token boolean">_</span> <span class="token operator">=</span> clientgoscheme<span class="token punctuation">.</span><span class="token function">AddToScheme</span><span class="token punctuation">(</span>Scheme<span class="token punctuation">)</span><br /> <span class="token boolean">_</span> <span class="token operator">=</span> cosscheme<span class="token punctuation">.</span><span class="token function">AddToScheme</span><span class="token punctuation">(</span>Scheme<span class="token punctuation">)</span><br /> <span class="token boolean">_</span> <span class="token operator">=</span> apiextensionsv1<span class="token punctuation">.</span><span class="token function">AddToScheme</span><span class="token punctuation">(</span>Scheme<span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<h2 id="%E6%9E%84%E9%80%A0%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%9B%86%E7%BE%A4" tabindex="-1">构造轻量级集群</h2>
<p>针对使用 fake client 不能覆盖的场景可使用这种方法进行测试,在单元测试中启动一个真实集群一般使用 controller-runtime 提供的 <a href="https://github.com/kubernetes-sigs/controller-runtime/tree/master/pkg/envtest">envtest</a> 库,非常方便,由于是启动了一个真实的集群,该方法适用于任何 client。</p>
<p>测试步骤大致如下:</p>
<ol>
<li>准备集群相关配置并启动集群
<ul>
<li>一般不需要额外配置,一个函数即可启动,如果有想要预注册 CRD 等才需要配置,配置项较多,可以参考 <a href="https://github.com/kubernetes-sigs/controller-runtime/blob/master/pkg/envtest/server.go#L105">https://github.com/kubernetes-sigs/controller-runtime/blob/master/pkg/envtest/server.go#L105</a></li>
</ul>
</li>
<li>使用上面创建的集群的 kube config 来生成各种 client</li>
<li>创建测试数据(如果需要的话)</li>
<li>进行测试,完成后销毁集群</li>
</ol>
<p>启动集群方法如下:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// 生成 env,这里面有很多配置项,可以根据需要配置</span><br />testEnv <span class="token operator">:=</span> <span class="token operator">&</span>envtest<span class="token punctuation">.</span>Environment<span class="token punctuation">{</span><span class="token punctuation">}</span><br /><br /><span class="token comment">// 启动环境,返回值是环境的 rest.Config,可用于生成各 kube client</span><br />config<span class="token punctuation">,</span> err <span class="token operator">:=</span> testEnv<span class="token punctuation">.</span><span class="token function">Start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Fatal</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// 销毁集群</span><br />testEnv<span class="token punctuation">.</span><span class="token function">Stop</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<p>注意,该方法需要提前安装 kubebuilder,它依赖 kubebuilder 包提供的 apiserver 那几个 binary 文件,本地的话自己<a href="https://book.kubebuilder.io/quick-start.html#installation">下载</a>安装就好了,如果是 CI 环境需要的话,可以在基础镜像里面增加这些文件,一个示例:</p>
<pre class="language-text"><code class="language-text">RUN mkdir -p /usr/local && \<br /> wget https://go.kubebuilder.io/dl/2.3.1/linux/amd64 && \<br /> tar xvf amd64 && \<br /> mv kubebuilder_2.3.1_linux_amd64 /usr/local/kubebuilder && \<br /> rm amd64</code></pre>
<p>下面是针对普通 client 的一个简单示例,业务函数定义如下:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// Create creates the given CRD objects or updates them if these objects already exist in the cluster.</span><br /><span class="token keyword">func</span> <span class="token function">Create</span><span class="token punctuation">(</span>client clientset<span class="token punctuation">.</span>Interface<span class="token punctuation">,</span> crds <span class="token operator">...</span><span class="token operator">*</span>apiextensionsv1<span class="token punctuation">.</span>CustomResourceDefinition<span class="token punctuation">)</span> <span class="token builtin">error</span> <span class="token punctuation">{</span><br /> <span class="token operator">...</span><br /><span class="token punctuation">}</span></code></pre>
<p>测试准备:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// 启动测试集群</span><br />testEnv <span class="token operator">:=</span> <span class="token operator">&</span>envtest<span class="token punctuation">.</span>Environment<span class="token punctuation">{</span><span class="token punctuation">}</span><br />config<span class="token punctuation">,</span> err <span class="token operator">:=</span> testEnv<span class="token punctuation">.</span><span class="token function">Start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Fatal</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><span class="token keyword">defer</span> testEnv<span class="token punctuation">.</span><span class="token function">Stop</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /><span class="token comment">// 生成需要的 apiextension client</span><br />apiextensionClient<span class="token punctuation">,</span> <span class="token boolean">_</span> <span class="token operator">:=</span> apiextensionsclient<span class="token punctuation">.</span><span class="token function">NewForConfig</span><span class="token punctuation">(</span>config<span class="token punctuation">)</span></code></pre>
<p>之后再使用上面的 <code>apiextensionClient</code> 去执行测试即可。</p>
<p>针对 generic client 的一个简单示例如下:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// 启动测试集群</span><br />testEnv <span class="token operator">:=</span> <span class="token operator">&</span>envtest<span class="token punctuation">.</span>Environment<span class="token punctuation">{</span><span class="token punctuation">}</span><br />config<span class="token punctuation">,</span> err <span class="token operator">:=</span> testEnv<span class="token punctuation">.</span><span class="token function">Start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Fatal</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><span class="token keyword">defer</span> testEnv<span class="token punctuation">.</span><span class="token function">Stop</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /><span class="token comment">// 生成 generic client</span><br />cli<span class="token punctuation">,</span> err <span class="token operator">:=</span> client<span class="token punctuation">.</span><span class="token function">New</span><span class="token punctuation">(</span>config<span class="token punctuation">,</span> client<span class="token punctuation">.</span>Options<span class="token punctuation">{</span><br /> Scheme<span class="token punctuation">:</span> scheme<span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Fatal</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// 准备测试数据</span><br />sc1 <span class="token operator">:=</span> test<span class="token punctuation">.</span><span class="token function">GetStorageClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br />sc1<span class="token punctuation">.</span>Name <span class="token operator">=</span> <span class="token string">"sc1"</span><br />sc1<span class="token punctuation">.</span>Provisioner <span class="token operator">=</span> <span class="token string">"example.com/test"</span><br /><span class="token comment">// 创建测试数据</span><br /><span class="token keyword">if</span> err <span class="token operator">:=</span> cli<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span><span class="token function">TODO</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> sc1<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Fatal</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// 开始执行测试...</span></code></pre>
<h2 id="%E5%B8%B8%E7%94%A8%E5%9C%BA%E6%99%AF%E5%AF%B9%E6%AF%94" tabindex="-1">常用场景对比</h2>
<table>
<thead>
<tr>
<th></th>
<th>fake client</th>
<th>真实集群</th>
</tr>
</thead>
<tbody>
<tr>
<td>资源 CRUD</td>
<td>支持</td>
<td>支持</td>
</tr>
<tr>
<td>使用 lister</td>
<td>支持,需要额外处理</td>
<td>支持,无需额外处理</td>
</tr>
<tr>
<td>informer 事件</td>
<td>不支持,无法触发</td>
<td>支持</td>
</tr>
<tr>
<td>运行依赖</td>
<td>无</td>
<td>需要安装 kubebuilder</td>
</tr>
</tbody>
</table>
<p>fake client 使用简单,非常轻量级,执行速度快,但是它基本只能覆盖资源 CRUD 场景,其它操作的业务代码无法覆盖,还有一些场景能覆盖到但是需要一些额外的操作,稍微麻烦一点;构造真实集群完全能模拟组件运行在线上集群中的情况,几乎所有的业务代码只要想测都能覆盖到,但是执行速度较慢,对环境有特殊要求;选择哪种方案的一个简单判断方法是,用 fake client 测试能覆盖到的用 fake client,fake client 覆盖不到的再用构造真实集群的方法。</p>
Kubernetes CRD v1 介绍
2021-07-06T00:00:00Z
https://xinzhao.me/posts/crd-v1-introduction/
<p></p><div class="table-of-contents"><div class="toc-container-header">索引</div><ul><li><a href="https://xinzhao.me/posts/crd-v1-introduction/#%E5%89%8D%E8%A8%80">前言</a></li><li><a href="https://xinzhao.me/posts/crd-v1-introduction/#metadata">metadata</a></li><li><a href="https://xinzhao.me/posts/crd-v1-introduction/#versions">versions</a><ul><li><a href="https://xinzhao.me/posts/crd-v1-introduction/#schema">schema</a></li></ul></li><li><a href="https://xinzhao.me/posts/crd-v1-introduction/#subresource">subresource</a><ul><li><a href="https://xinzhao.me/posts/crd-v1-introduction/#status-subresource">status subresource</a></li><li><a href="https://xinzhao.me/posts/crd-v1-introduction/#scale-subresource">scale subresource</a></li></ul></li><li><a href="https://xinzhao.me/posts/crd-v1-introduction/#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5">参考链接</a></li></ul></div><p></p>
<h2 id="%E5%89%8D%E8%A8%80" tabindex="-1">前言</h2>
<p>CRD API 在 Kubernetes v1.16 版本 GA 了,所以打算来聊一聊关于 CRD v1 版本的一些特性和一些常见的问题;关于 CRD 基础内容可能会一笔带过,不会涉及到太多,基础内容可以在 Kubernetes 官方文档上了解一下(后面<a href="https://xinzhao.me/posts/crd-v1-introduction/#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5">参考链接</a>有),可以更多地把这篇文章当作遇到问题时的速查手册。</p>
<p>CRD 是扩展 Kubernetes 最常见和最方便的方法,除了提供定义 custom resource(CR)的基本能力外,CRD 还提供了很多扩展能力,包括 schema、多版本、版本之间的 conversion、subresource 子资源等,本文主要介绍下面四部分的内容:</p>
<ul>
<li>metadata: 定义了 CR 的基本信息,比如 API group 和名称</li>
<li>versions: 定义 CR 的版本信息</li>
<li>schema: 定义 CR 的结构和字段类型等信息</li>
<li>subresource: 定义 CR 的子资源信息</li>
</ul>
<h2 id="metadata" tabindex="-1">metadata</h2>
<p>最基础但是最重要的部分,metadata 包含的主要内容是:</p>
<ul>
<li>自定义资源 CR 的名称</li>
<li>该 CR 所属的 API group</li>
<li>scope: 资源是 Namespace 级别还是 Cluster 级别</li>
</ul>
<p>下面是一个比较完整的例子(注释基本是从 K8s 官方文档上摘抄的,就不翻译了),假设我们要定义一种新的 <code>VolumeSnapshot</code> 资源:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> apiextensions.k8s.io/v1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> CustomResourceDefinition<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token comment"># the name of this CR, must match the spec fields below, and be in the form: <plural>.<group>.</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> volumesnapshots.xinzhao.me<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token comment"># group is the API group of the defined custom resource.</span><br /> <span class="token comment"># The custom resources are served under `/apis/<group>/...`.</span><br /> <span class="token comment"># Must match the name of the CustomResourceDefinition (in the form `<names.plural>.<group>`).</span><br /> <span class="token key atrule">group</span><span class="token punctuation">:</span> xinzhao.me<br /> <span class="token key atrule">names</span><span class="token punctuation">:</span><br /> <span class="token comment"># kind is normally the CamelCased singular type. Your resource manifests use this.</span><br /> <span class="token key atrule">kind</span><span class="token punctuation">:</span> VolumeSnapshot<br /> <span class="token comment"># listKind is the serialized kind of the list for this resource. Defaults to "`kind`List".</span><br /> <span class="token key atrule">listKind</span><span class="token punctuation">:</span> VolumeSnapshotList<br /> <span class="token comment"># plural name to be used in the URL: /apis/<group>/<version>/<plural></span><br /> <span class="token key atrule">plural</span><span class="token punctuation">:</span> volumesnapshots<br /> <span class="token comment"># singular name to be used as an alias on the CLI and for display</span><br /> <span class="token key atrule">singular</span><span class="token punctuation">:</span> volumesnapshot<br /> <span class="token comment"># shortNames allow shorter string to match your resource on the CLI</span><br /> <span class="token comment"># kubectl get volumesnapshot/vss</span><br /> <span class="token key atrule">shortNames</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> vss<br /> <span class="token comment"># either Namespaced or Cluster</span><br /> <span class="token key atrule">scope</span><span class="token punctuation">:</span> Namespaced</code></pre>
<p>其中 <code>group</code> 必须是域名,不然创建的时候就会报下面的错:</p>
<pre class="language-text"><code class="language-text">The CustomResourceDefinition "volumesnapshots.test" is invalid: spec.group: Invalid value: "test": should be a domain with at least one dot</code></pre>
<h2 id="versions" tabindex="-1">versions</h2>
<p>定义了 CR 的版本信息,包括哪些版本是可用的,每个版本的结构信息,不同版本之间的转换策略等。</p>
<p>目前有两种版本转换的策略:</p>
<ul>
<li>None: 默认的策略,只改 <code>apiVersion</code>,其它字段不管,旧版本有,但是新版本没有的字段的数据会直接丢失,同名但是结构不匹配的会报错</li>
<li><a href="https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#webhook-conversion">Webhook</a>: 可以配置自定义的转换,API Server will call to an external webhook to do the conversion</li>
</ul>
<p>还是再通过一个例子来看看整体结构:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> apiextensions.k8s.io/v1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> CustomResourceDefinition<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token punctuation">...</span><br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token punctuation">...</span><br /> <span class="token comment"># list of versions supported by this CustomResourceDefinition</span><br /> <span class="token key atrule">versions</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> v1beta1<br /> <span class="token key atrule">served</span><span class="token punctuation">:</span> <span class="token boolean important">true</span><br /> <span class="token key atrule">storage</span><span class="token punctuation">:</span> <span class="token boolean important">true</span><br /> <span class="token key atrule">additionalPrinterColumns</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> PVC<br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> string<br /> <span class="token key atrule">jsonPath</span><span class="token punctuation">:</span> .spec.pvcName<br /> <span class="token key atrule">schema</span><span class="token punctuation">:</span><br /> <span class="token punctuation">...</span><br /> <span class="token comment"># The conversion section is introduced in Kubernetes 1.13+ with a default value of</span><br /> <span class="token comment"># None conversion (strategy sub-field set to None).</span><br /> <span class="token key atrule">conversion</span><span class="token punctuation">:</span><br /> <span class="token comment"># None conversion assumes the same schema for all versions and only sets the apiVersion</span><br /> <span class="token comment"># field of custom resources to the proper value</span><br /> <span class="token key atrule">strategy</span><span class="token punctuation">:</span> None</code></pre>
<p>关于多版本的常见问题:</p>
<ul>
<li>每个版本都可以通过 <code>served</code> 字段启用/禁用;有且仅有一个版本能被标记为存储版本(<code>storage = true</code>)</li>
<li>get <code>served</code> 为 <code>false</code> 的版本会直接报错:<pre class="language-text"><code class="language-text">Error from server (NotFound): Unable to list "xinzhao.me/v1, Resource=volumesnapshots": the server could not find the requested resource (get volumesnapshots.xinzhao.me)</code></pre>
</li>
<li>创建的 CR 的版本最终均为 <code>storage = true</code> 的那个版本,创建以前的版本也会被转换成新版本的</li>
<li>如果存在以前的版本的 object,在更新这个 object 时会转换成 <code>storage = true</code> 的那个版本</li>
<li>转换均是通过 <code>conversion</code> 配置的 <code>strategy</code> 来进行的</li>
<li>当你获取资源时(比如使用 <code>kubectl get</code>),如果指定的版本与对象的存储时的版本不同,k8 会处理多版本之间的转换(by conversion),返回你指定的那个版本,但是这只是显示上的转换,实际上 etcd 中的数据还是老版本的,只有更新才会真正转换;如果 conversion 出问题的话,会报错:<pre class="language-text"><code class="language-text">Error from server: conversion webhook for xinzhao.me/v1beta1, Kind=VolumeSnapshot failed: Post <https: example-conversion-webhook-server.default.svc:443="" crdconvert?timeout="30s:"> service "example-conversion-webhook-server" not found</https:></code></pre>
</li>
</ul>
<h3 id="schema" tabindex="-1">schema</h3>
<p>version 的一部分,定义了 CR 的结构信息,每个版本都有自己独立的 schema 结构,描述了 CR 有哪些字段,以及每个字段的类型等;schema 使用了 <a href="http://json-schema.org/specification.html">JSON Schema 标准</a>,一个简单的例子如下:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> apiextensions.k8s.io/v1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> CustomResourceDefinition<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token punctuation">...</span><br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token punctuation">...</span><br /> <span class="token key atrule">versions</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> v1beta1<br /> <span class="token key atrule">served</span><span class="token punctuation">:</span> <span class="token boolean important">true</span><br /> <span class="token key atrule">storage</span><span class="token punctuation">:</span> <span class="token boolean important">true</span><br /> <span class="token key atrule">schema</span><span class="token punctuation">:</span><br /> <span class="token key atrule">openAPIV3Schema</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> object<br /> <span class="token key atrule">properties</span><span class="token punctuation">:</span><br /> <span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> object<br /> <span class="token key atrule">properties</span><span class="token punctuation">:</span><br /> <span class="token key atrule">pvcName</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> string</code></pre>
<p>该 CR 我们只定义了 <code>spec</code> 并且只有一个字段 <code>pvcName</code>,所以创建一个该 CR 的 YAML 文件如下:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> xinzhao.me/v1beta1<br /><span class="token key atrule">kind</span><span class="token punctuation">:</span> VolumeSnapshot<br /><span class="token key atrule">metadata</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> s1beta1<br /> <span class="token key atrule">namespace</span><span class="token punctuation">:</span> default<br /><span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">pvcName</span><span class="token punctuation">:</span> pvc1</code></pre>
<p>每个字段都可以设置默认值,这个字段是什么类型的,在 yaml 中 <code>default</code> 的值就要是什么类型的,比如我有下面三种类型的字段:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">properties</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> array<br /> <span class="token key atrule">items</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> string<br /> <span class="token key atrule">default</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token string">"test"</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> string<br /> <span class="token key atrule">default</span><span class="token punctuation">:</span> <span class="token string">"test"</span><br /> <span class="token key atrule">size</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> number<br /> <span class="token key atrule">default</span><span class="token punctuation">:</span> <span class="token number">1</span></code></pre>
<p>字段还支持校验,比如 <code>int</code> 可以规定一个最大/最小值的范围,<code>string</code> 支持正则表达式校验和 enum 枚举。具体每种类型支持什么校验可以参考:<a href="http://json-schema.org/draft/2019-09/json-schema-validation.html">JSON Schema Validation</a>,一个例子:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">properties</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> array<br /> <span class="token key atrule">items</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> string<br /> <span class="token key atrule">minItems</span><span class="token punctuation">:</span> <span class="token number">2</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> string<br /> <span class="token key atrule">pattern</span><span class="token punctuation">:</span> <span class="token string">'\\w+'</span><br /> <span class="token key atrule">size</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> number<br /> <span class="token key atrule">minimum</span><span class="token punctuation">:</span> <span class="token number">1</span><br /> <span class="token key atrule">maximum</span><span class="token punctuation">:</span> <span class="token number">10</span></code></pre>
<p>schema 的一些常见问题:</p>
<ul>
<li>所有字段必须在 <code>schema</code> 中定义,不然会报错:<pre class="language-text"><code class="language-text">error: error validating "snapshot-v1beta1.yaml": error validating data: ValidationError(VolumeSnapshot): unknown field "spec" in ...; if you choose to ignore these errors, turn validation off with --validate=false</code></pre>
</li>
<li>如果想要存储没有事先定义的字段的话,可以给相应的父字段加上 <code>x-kubernetes-preserve-unknown-fields: true</code> 属性</li>
<li>定义了的字段可以不填,也可以配置 <code>required</code> 要求某些字段必填:<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">schema</span><span class="token punctuation">:</span><br /> <span class="token key atrule">openAPIV3Schema</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> object<br /> <span class="token key atrule">properties</span><span class="token punctuation">:</span><br /> <span class="token key atrule">spec</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> object<br /> <span class="token key atrule">properties</span><span class="token punctuation">:</span><br /> <span class="token key atrule">pvc</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> object<br /> <span class="token key atrule">required</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">"size"</span><span class="token punctuation">]</span><br /> <span class="token key atrule">properties</span><span class="token punctuation">:</span><br /> <span class="token punctuation">...</span></code></pre>
<ul>
<li>如果这个 <code>spec.pvc.size</code> 字段没填的话就会报错</li>
</ul>
</li>
<li>如果关闭校验的话(<code>kubectl create --validate=false ...</code>),未定义的字段会被直接丢弃掉,不会存储</li>
<li>type 没有 map 类型,可以使用 <code>type: object</code> 配合 <code>x-kubernetes-map-type</code> 或者 <code>x-kubernetes-preserve-unknown-fields</code> 来充当 map 类型(允许任何字段)</li>
</ul>
<h2 id="subresource" tabindex="-1">subresource</h2>
<p>支持两种类型的 subresource,一个是 <a href="https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#status-subresource">Status subresource</a>,一个是 <a href="https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#scale-subresource">Scale subresource</a>,下面的例子中,status 和 scale 两种 subresource 都启用了:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token comment"># subresources describes the subresources for custom resources.</span><br /><span class="token key atrule">subresources</span><span class="token punctuation">:</span><br /> <span class="token comment"># status enables the status subresource.</span><br /> <span class="token key atrule">status</span><span class="token punctuation">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br /> <span class="token comment"># scale enables the scale subresource.</span><br /> <span class="token key atrule">scale</span><span class="token punctuation">:</span><br /> <span class="token comment"># specReplicasPath defines the JSONPath inside of a custom resource that corresponds to Scale.Spec.Replicas.</span><br /> <span class="token key atrule">specReplicasPath</span><span class="token punctuation">:</span> .spec.replicas<br /> <span class="token comment"># statusReplicasPath defines the JSONPath inside of a custom resource that corresponds to Scale.Status.Replicas.</span><br /> <span class="token key atrule">statusReplicasPath</span><span class="token punctuation">:</span> .status.replicas<br /> <span class="token comment"># labelSelectorPath defines the JSONPath inside of a custom resource that corresponds to Scale.Status.Selector.</span><br /> <span class="token key atrule">labelSelectorPath</span><span class="token punctuation">:</span> .status.labelSelector</code></pre>
<h3 id="status-subresource" tabindex="-1">status subresource</h3>
<p>开启 status subresource 之后,整个 CR 的内容将被分为 <code>spec</code> 和 <code>status</code> 两部分,分别表示资源期望的状态和资源的实际状态,然后该资源的 API 会暴露一个新的 <code>/status</code> 接口,这个 CR 的创建/更新接口会校验整个内容(包括 <code>status</code> 的),但是 <code>status</code> 整个会被忽略,<code>status</code> 只能通过 <code>/status</code> 接口来更新,<code>/status</code> 接口只会更新/校验 <code>status</code> 的内容,其它的会被忽略。</p>
<p>针对 client-go,只要 type 定义了 <code>Status</code> 就会生成 <code>UpdateStatus</code> 方法:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// +genclient</span><br /><span class="token comment">// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object</span><br /><br /><span class="token comment">// Snapshot is the snapshot object of the specified PVC.</span><br /><span class="token keyword">type</span> Snapshot <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> metav1<span class="token punctuation">.</span>TypeMeta <span class="token string">`json:",inline"`</span><br /> <span class="token comment">// Standard object's metadata.</span><br /> <span class="token comment">// More info: <https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata></span><br /> <span class="token comment">// +optional</span><br /> metav1<span class="token punctuation">.</span>ObjectMeta <span class="token string">`json:"metadata,omitempty"`</span><br /><br /> Spec SnapshotSpec <span class="token string">`json:"spec,omitempty"`</span><br /> Status SnapshotStatus <span class="token string">`json:"status,omitempty"`</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// SnapshotInterface has methods to work with Snapshot resources.</span><br /><span class="token keyword">type</span> SnapshotInterface <span class="token keyword">interface</span> <span class="token punctuation">{</span><br /> <span class="token function">Create</span><span class="token punctuation">(</span><span class="token operator">*</span>v1beta1<span class="token punctuation">.</span>Snapshot<span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token operator">*</span>v1beta1<span class="token punctuation">.</span>Snapshot<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span><br /> <span class="token function">Update</span><span class="token punctuation">(</span><span class="token operator">*</span>v1beta1<span class="token punctuation">.</span>Snapshot<span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token operator">*</span>v1beta1<span class="token punctuation">.</span>Snapshot<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span><br /> <span class="token function">UpdateStatus</span><span class="token punctuation">(</span><span class="token operator">*</span>v1beta1<span class="token punctuation">.</span>Snapshot<span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token operator">*</span>v1beta1<span class="token punctuation">.</span>Snapshot<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span><br /> <span class="token operator">...</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// UpdateStatus was generated because the type contains a Status member.</span><br /><span class="token comment">// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().</span><br /><span class="token keyword">func</span> <span class="token punctuation">(</span>c <span class="token operator">*</span>snapshots<span class="token punctuation">)</span> <span class="token function">UpdateStatus</span><span class="token punctuation">(</span>snapshot <span class="token operator">*</span>v1beta1<span class="token punctuation">.</span>Snapshot<span class="token punctuation">)</span> <span class="token punctuation">(</span>result <span class="token operator">*</span>v1beta1<span class="token punctuation">.</span>Snapshot<span class="token punctuation">,</span> err <span class="token builtin">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> result <span class="token operator">=</span> <span class="token operator">&</span>v1beta1<span class="token punctuation">.</span>Snapshot<span class="token punctuation">{</span><span class="token punctuation">}</span><br /> err <span class="token operator">=</span> c<span class="token punctuation">.</span>client<span class="token punctuation">.</span><span class="token function">Put</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><br /> <span class="token function">Namespace</span><span class="token punctuation">(</span>c<span class="token punctuation">.</span>ns<span class="token punctuation">)</span><span class="token punctuation">.</span><br /> <span class="token function">Resource</span><span class="token punctuation">(</span><span class="token string">"snapshots"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><br /> <span class="token function">Name</span><span class="token punctuation">(</span>snapshot<span class="token punctuation">.</span>Name<span class="token punctuation">)</span><span class="token punctuation">.</span><br /> <span class="token function">SubResource</span><span class="token punctuation">(</span><span class="token string">"status"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><br /> <span class="token function">Body</span><span class="token punctuation">(</span>snapshot<span class="token punctuation">)</span><span class="token punctuation">.</span><br /> <span class="token function">Do</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><br /> <span class="token function">Into</span><span class="token punctuation">(</span>result<span class="token punctuation">)</span><br /> <span class="token keyword">return</span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="scale-subresource" tabindex="-1">scale subresource</h3>
<p>和 status subresource 类似,启用之后也会暴露一个新的 <code>/scale</code> 接口,但是我基本没使用过这个,就不过多介绍了,可以看下官方的文档,scale subresource 启用之后还可以使用 <code>kubectl scale</code> 来单独控制资源的副本数量:</p>
<pre class="language-shell"><code class="language-shell">kubectl scale <span class="token parameter variable">--replicas</span><span class="token operator">=</span><span class="token number">5</span> crontabs/my-new-cron-object<br />crontabs <span class="token string">"my-new-cron-object"</span> scaled<br /><br />kubectl get crontabs my-new-cron-object <span class="token parameter variable">-o</span> <span class="token assign-left variable">jsonpath</span><span class="token operator">=</span><span class="token string">'{.spec.replicas}'</span><br /><span class="token number">5</span></code></pre>
<h2 id="%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5" tabindex="-1">参考链接</h2>
<ul>
<li><a href="https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/">Extend the Kubernetes API with CustomResourceDefinitions</a></li>
<li><a href="https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/">Versions in CustomResourceDefinitions</a></li>
<li><a href="http://json-schema.org/specification.html">JSON Schema Specification</a></li>
</ul>
我又又又开始倒腾我的博客了
2021-05-22T00:00:00Z
https://xinzhao.me/posts/start-building-my-blog-again/
<p>上一个比较正式的博客是 17 年 1 月左右开始动手构建的,当时年轻气盛,就想造轮子,不过当时也想着锻炼下自己的能力,就选择了完全从头开始构建一个自己的博客系统(后端、前端、功能设计和 UI),虽然不是静态博客那种直接基于 Markdown 文件来直接生成博客内容,我也选择了使用 Markdown 来写作,所有文章的源内容都是 Markdown 格式的,最后再渲染成 HTML 展示出来。</p>
<p>技术层面用的都是当时很“时髦”的技术:前后端分离,后端是用 Python 写的,使用了 Flask 框架和 PostgreSQL 数据库,前端是 TypeScript + React + Sass + CSS Modules,代码开源在 <a href="https://github.com/iawia002/Diana">GitHub</a>,整个博客放在一台阿里云的机器上面。</p>
<p><img src="https://xinzhao.me/img/start-building-my-blog-again/diana.jpeg" alt="" /></p>
<p class="caption">上一代博客的截图</p>
<p>整个博客我还是很满意的,唯一的问题就在文章内容的管理上,和使用 static site generator 构建的博客不同,我的文章是单独存放在线上的 PostgreSQL 数据库中的,备份和维护都很麻烦,完全不如静态博客那样写完直接提交 git 方便,所以现在还是决定换成静态博客,文章和代码都放在 GitHub 上,不用再单独备份了,部署也非常方便,回归本质,不搞那些花里胡哨的东西了。</p>
<p>在网上冲了一圈浪之后,本来打算用 <a href="https://github.com/Track3/hermit">Track3/hermit</a> 或者 <a href="https://github.com/Huxpro/huxpro.github.io">Huxpro/huxpro.github.io</a> 来改一下,两个都是非常不错的风格,一个基于 Hugo,一个基于 Jekyll,但后面想起了之前看到过的 <a href="https://lutaonan.com/">Randy 的博客</a>,真的是非常好看的风格,很简洁,又有高级感,然后初步尝试了下这个博客用的 <a href="https://www.11ty.dev/">Eleventy</a> 和 <a href="https://tailwindcss.com/">TailwindCSS</a>,我也挺喜欢的,所以最后就基于 Randy 的 <a href="https://github.com/djyde/blog-2020">djyde/blog-2020</a> 改出了我现在这个博客,风格基本上没变,配色改成我自己喜欢的颜色,后面可能会再调整一下,功能层面的话增加了<a href="https://xinzhao.me/tags">标签</a>页面,我自己是很喜欢用标签来管理内容的,所以把这个加上了,然后还有一些很细碎的改动,具体可以看 <a href="https://github.com/iawia002/blog">iawia002/blog</a> repo 的 commit 记录。</p>
<p>这次绝对是我最后一次折腾博客系统 🤥,这个博客我不保证能经常更新,但是我保证它能一直存在,能一直被访问到,欢迎来玩儿。</p>
controller-runtime 介绍
2020-08-20T00:00:00Z
https://xinzhao.me/posts/controller-runtime/
<p></p><div class="table-of-contents"><div class="toc-container-header">索引</div><ul><li><a href="https://xinzhao.me/posts/controller-runtime/#%E6%A6%82%E8%BF%B0">概述</a></li><li><a href="https://xinzhao.me/posts/controller-runtime/#%E4%BD%BF%E7%94%A8">使用</a><ul><li><a href="https://xinzhao.me/posts/controller-runtime/#reconciler">Reconciler</a><ul><li><a href="https://xinzhao.me/posts/controller-runtime/#%E4%BB%8B%E7%BB%8D">介绍</a></li><li><a href="https://xinzhao.me/posts/controller-runtime/#%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B">使用示例</a></li></ul></li><li><a href="https://xinzhao.me/posts/controller-runtime/#builder-%E5%92%8C-manager">Builder 和 Manager</a></li></ul></li><li><a href="https://xinzhao.me/posts/controller-runtime/#%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95">单元测试</a></li><li><a href="https://xinzhao.me/posts/controller-runtime/#%E6%80%BB%E7%BB%93">总结</a></li></ul></div><p></p>
<h2 id="%E6%A6%82%E8%BF%B0" tabindex="-1">概述</h2>
<p><a href="https://github.com/kubernetes-sigs/controller-runtime">controller-runtime</a> 是 <a href="https://github.com/kubernetes-sigs/kubebuilder">Kubebuilder</a> 的子项目,提供了一系列用于构建 controller 的库;Kubebuilder 本身也是生成了大量的使用 controller-runtime 的模板代码。controller-runtime 中的几个基本概念:</p>
<ul>
<li>Controller:字面意思,一个 controller。</li>
<li>Reconciler:提供 <code>Reconcile</code> 函数,<code>Controller</code> 的主要部分和入口函数,包含这个 controller 的所有业务逻辑(等同于普通 controller 中的 <code>syncHandler</code> 函数),用于使我们关注的 object 的实际状态逐渐向期望状态逼近。<code>Reconciler</code> 还有以下特点:
<ul>
<li>通常只针对一种类型的 object,不同类型的 object 使用单独的 controller。</li>
<li>通常不关心触发 <code>Reconcile</code> 函数的事件内容和类型;比如不管 <code>ReplicaSet</code> 是创建还是更新,<code>ReplicaSet</code> <code>Reconciler</code> 总是将集群中 <code>Pod</code> 的数量与 object 中设定的数量进行比较,再做出相应的操作。</li>
</ul>
</li>
<li>Builder:基于一些配置,为 <code>Reconciler</code> 生成 <code>Controller</code>。</li>
<li>Manager:管理和启动 <code>Controller</code>,一个 <code>Manager</code> 可包含多个 <code>Controller</code>。</li>
</ul>
<h2 id="%E4%BD%BF%E7%94%A8" tabindex="-1">使用</h2>
<p>下面以官方的一个<a href="https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/builder#example-Builder">简单例子</a>来分步介绍怎么使用 controller-runtime 构建一个 controller:首先定义 <code>Reconciler</code>,其中包含 controller 的主要逻辑,然后使用 <code>Builder</code> 生成 <code>Controller</code> 并加入到 <code>Manager</code> 中,最后启动 <code>Manager</code>。</p>
<p><em>注:以 v0.5.0 版本为例,最新版 Reconcile 函数定义有变化</em></p>
<p>每一步的详细介绍如下:</p>
<h3 id="reconciler" tabindex="-1">Reconciler</h3>
<h4 id="%E4%BB%8B%E7%BB%8D" tabindex="-1">介绍</h4>
<p><code>Reconciler</code> 的定义如下,仅包含一个 <code>Reconcile</code> 函数:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">type</span> Reconciler <span class="token keyword">interface</span> <span class="token punctuation">{</span><br /> <span class="token comment">// Reconciler performs a full reconciliation for the object referred to by the Request.</span><br /> <span class="token comment">// The Controller will requeue the Request to be processed again if an error is non-nil or</span><br /> <span class="token comment">// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.</span><br /> <span class="token function">Reconcile</span><span class="token punctuation">(</span>Request<span class="token punctuation">)</span> <span class="token punctuation">(</span>Result<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p><code>Request</code> 和 <code>Result</code> 定义如下:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// Request contains the information necessary to reconcile a Kubernetes object. This includes the</span><br /><span class="token comment">// information to uniquely identify the object - its Name and Namespace. It does NOT contain information about</span><br /><span class="token comment">// any specific Event or the object contents itself.</span><br /><span class="token keyword">type</span> Request <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> <span class="token comment">// NamespacedName is the name and namespace of the object to reconcile.</span><br /> types<span class="token punctuation">.</span>NamespacedName<br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// Result contains the result of a Reconciler invocation.</span><br /><span class="token keyword">type</span> Result <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> <span class="token comment">// Requeue tells the Controller to requeue the reconcile key. Defaults to false.</span><br /> Requeue <span class="token builtin">bool</span><br /><br /> <span class="token comment">// RequeueAfter if greater than 0, tells the Controller to requeue the reconcile key after the Duration.</span><br /> <span class="token comment">// Implies that Requeue is true, there is no need to set Requeue to true at the same time as RequeueAfter.</span><br /> RequeueAfter time<span class="token punctuation">.</span>Duration<br /><span class="token punctuation">}</span></code></pre>
<p><code>Request</code> 包含本次 reconcile object 的 namespace 和 name,object 类型是生成 controller 时配置的,一个 <code>Reconciler</code> 仅能处理一种类型的 object;<code>Result</code> 基本不用关心,如果 <code>Reconcile</code> 返回 error 会自动 requeue。</p>
<h4 id="%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B" tabindex="-1">使用示例</h4>
<p>定义一个 <code>ReplicaSetReconciler</code>,其中包含一个由 controller-runtime 提供的一个 <a href="https://github.com/kubernetes-sigs/controller-runtime/tree/master/pkg/client">generic client</a>,功能同普通的 kubernetes client,能获取到集群的所有资源:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// ReplicaSetReconciler is a simple ControllerManagedBy example implementation.</span><br /><span class="token keyword">type</span> ReplicaSetReconciler <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> client<span class="token punctuation">.</span>Client<br /><span class="token punctuation">}</span></code></pre>
<p>不过和普通 kubernetes client 不同的是,这个通用 client 是一个 client 就能 CRUD 所有类型的资源,非常方便和易于使用。</p>
<p>然后是实现业务逻辑:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// Implement the business logic:</span><br /><span class="token comment">// This function will be called when there is a change to a ReplicaSet or a Pod with an OwnerReference</span><br /><span class="token comment">// to a ReplicaSet.</span><br /><span class="token comment">//</span><br /><span class="token comment">// * Read the ReplicaSet</span><br /><span class="token comment">// * Read the Pods</span><br /><span class="token comment">// * Set a Label on the ReplicaSet with the Pod count</span><br /><span class="token keyword">func</span> <span class="token punctuation">(</span>a <span class="token operator">*</span>ReplicaSetReconciler<span class="token punctuation">)</span> <span class="token function">Reconcile</span><span class="token punctuation">(</span>req reconcile<span class="token punctuation">.</span>Request<span class="token punctuation">)</span> <span class="token punctuation">(</span>reconcile<span class="token punctuation">.</span>Result<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// Read the ReplicaSet</span><br /> rs <span class="token operator">:=</span> <span class="token operator">&</span>appsv1<span class="token punctuation">.</span>ReplicaSet<span class="token punctuation">{</span><span class="token punctuation">}</span><br /> err <span class="token operator">:=</span> a<span class="token punctuation">.</span><span class="token function">Get</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span><span class="token function">TODO</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> req<span class="token punctuation">.</span>NamespacedName<span class="token punctuation">,</span> rs<span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> reconcile<span class="token punctuation">.</span>Result<span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> err<br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// List the Pods matching the PodTemplate Labels</span><br /> pods <span class="token operator">:=</span> <span class="token operator">&</span>corev1<span class="token punctuation">.</span>PodList<span class="token punctuation">{</span><span class="token punctuation">}</span><br /> err <span class="token operator">=</span> a<span class="token punctuation">.</span><span class="token function">List</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span><span class="token function">TODO</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> client<span class="token punctuation">.</span><span class="token function">InNamespace</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>Namespace<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">MatchingLabels</span><span class="token punctuation">(</span>rs<span class="token punctuation">.</span>Spec<span class="token punctuation">.</span>Template<span class="token punctuation">.</span>Labels<span class="token punctuation">)</span><span class="token punctuation">,</span> pods<span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> reconcile<span class="token punctuation">.</span>Result<span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> err<br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// Update the ReplicaSet</span><br /> rs<span class="token punctuation">.</span>Labels<span class="token punctuation">[</span><span class="token string">"pod-count"</span><span class="token punctuation">]</span> <span class="token operator">=</span> fmt<span class="token punctuation">.</span><span class="token function">Sprintf</span><span class="token punctuation">(</span><span class="token string">"%v"</span><span class="token punctuation">,</span> <span class="token function">len</span><span class="token punctuation">(</span>pods<span class="token punctuation">.</span>Items<span class="token punctuation">)</span><span class="token punctuation">)</span><br /> err <span class="token operator">=</span> a<span class="token punctuation">.</span><span class="token function">Update</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span><span class="token function">TODO</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> rs<span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> reconcile<span class="token punctuation">.</span>Result<span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> err<br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">return</span> reconcile<span class="token punctuation">.</span>Result<span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token boolean">nil</span><br /><span class="token punctuation">}</span></code></pre>
<p><code>InjectClient</code> 将 manager 真实的 client 赋给 <code>ReplicaSetReconciler</code>:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>a <span class="token operator">*</span>ReplicaSetReconciler<span class="token punctuation">)</span> <span class="token function">InjectClient</span><span class="token punctuation">(</span>c client<span class="token punctuation">.</span>Client<span class="token punctuation">)</span> <span class="token builtin">error</span> <span class="token punctuation">{</span><br /> a<span class="token punctuation">.</span>Client <span class="token operator">=</span> c<br /> <span class="token keyword">return</span> <span class="token boolean">nil</span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="builder-%E5%92%8C-manager" tabindex="-1">Builder 和 Manager</h3>
<p><code>Builder</code> 用来为 <code>Reconciler</code> 生成 <code>Controller</code>,<code>Manager</code> 用来管理和启动 <code>Controller</code>,直接用例子来介绍,首先生成一个 <code>Manager</code>:</p>
<pre class="language-go"><code class="language-go">mgr<span class="token punctuation">,</span> err <span class="token operator">:=</span> manager<span class="token punctuation">.</span><span class="token function">New</span><span class="token punctuation">(</span>config<span class="token punctuation">,</span> manager<span class="token punctuation">.</span>Options<span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> log<span class="token punctuation">.</span><span class="token function">Error</span><span class="token punctuation">(</span>err<span class="token punctuation">,</span> <span class="token string">"could not create manager"</span><span class="token punctuation">)</span><br /> os<span class="token punctuation">.</span><span class="token function">Exit</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>其中 config 为 client-go 的 <code>rest.Config</code>。</p>
<p>为 <code>ReplicaSetReconciler</code> 生成 <code>Controller</code> 并加入到 <code>Manager</code> 中:</p>
<pre class="language-go"><code class="language-go"><span class="token boolean">_</span><span class="token punctuation">,</span> err <span class="token operator">=</span> builder<span class="token punctuation">.</span><br /> <span class="token function">ControllerManagedBy</span><span class="token punctuation">(</span>mgr<span class="token punctuation">)</span><span class="token punctuation">.</span> <span class="token comment">// Create the ControllerManagedBy</span><br /> <span class="token function">For</span><span class="token punctuation">(</span><span class="token operator">&</span>appsv1<span class="token punctuation">.</span>ReplicaSet<span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span> <span class="token comment">// ReplicaSet is the Application API</span><br /> <span class="token function">Owns</span><span class="token punctuation">(</span><span class="token operator">&</span>corev1<span class="token punctuation">.</span>Pod<span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span> <span class="token comment">// ReplicaSet owns Pods created by it</span><br /> <span class="token function">Build</span><span class="token punctuation">(</span><span class="token operator">&</span>ReplicaSetReconciler<span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> log<span class="token punctuation">.</span><span class="token function">Error</span><span class="token punctuation">(</span>err<span class="token punctuation">,</span> <span class="token string">"could not create controller"</span><span class="token punctuation">)</span><br /> os<span class="token punctuation">.</span><span class="token function">Exit</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>其中 <code>For</code> 函数用于指定我们要 reconcile 的 object 类型,<code>Owns</code> 用来 watch 其 owner 是 reconcile object 类型的 object(<code>Owns</code> 可以指定多种类型),这两种类型 object 的增/删/改事件均会触发 <code>Reconcile</code> 函数。</p>
<p>最后启动 <code>Manager</code>,整个组件启动:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">if</span> err <span class="token operator">:=</span> mgr<span class="token punctuation">.</span><span class="token function">Start</span><span class="token punctuation">(</span>stopCh<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> log<span class="token punctuation">.</span><span class="token function">Error</span><span class="token punctuation">(</span>err<span class="token punctuation">,</span> <span class="token string">"could not start manager"</span><span class="token punctuation">)</span><br /> os<span class="token punctuation">.</span><span class="token function">Exit</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<h2 id="%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95" tabindex="-1">单元测试</h2>
<p>为普通的 controller 写单元测试主要还是依赖 client-go 提供的 fake client,将测试需要的各种 object append 到一个 fake client 中,使用这个 fake client 来完成测试,有用到 lister 的话需要手动往对应的 informer indexer 中添加相应的 object,示例:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// 创建 fake client</span><br />f<span class="token punctuation">.</span>client <span class="token operator">=</span> fake<span class="token punctuation">.</span><span class="token function">NewSimpleClientset</span><span class="token punctuation">(</span>f<span class="token punctuation">.</span>objects<span class="token operator">...</span><span class="token punctuation">)</span><br /><br /><span class="token comment">// 创建基于 fake client 的 informer</span><br />informer <span class="token operator">:=</span> informers<span class="token punctuation">.</span><span class="token function">NewSharedInformerFactory</span><span class="token punctuation">(</span>f<span class="token punctuation">.</span>client<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><br /><br /><span class="token comment">// 往 informer indexer 中添加 object</span><br /><span class="token keyword">for</span> <span class="token boolean">_</span><span class="token punctuation">,</span> s <span class="token operator">:=</span> <span class="token keyword">range</span> f<span class="token punctuation">.</span>storageClasses <span class="token punctuation">{</span><br /> informer<span class="token punctuation">.</span><span class="token function">Native</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Storage</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">V1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">StorageClasses</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Informer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetIndexer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>s<span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>controller-runtime 是使用了自己的 <a href="https://github.com/kubernetes-sigs/controller-runtime/tree/master/pkg/envtest">envtest</a> 包在本地启动一个真正的 apiserver 和 etcd,然后连接这个 apiserver 进行测试,我们还是需要 fake objects,不过和 fake client 不同,这些 fake objects 是要创建到 controller-runtime 启动的 apiserver 中,如果是 CR 的话,还需要先往这个新的 apiserver 中注册 CRD。一个完整的示例:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token function">TestNodeLocalStorageReconciler</span><span class="token punctuation">(</span>t <span class="token operator">*</span>testing<span class="token punctuation">.</span>T<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// 配置 testEnv,其中包含该项单测需要的 CRD 等</span><br /> testEnv <span class="token operator">:=</span> <span class="token operator">&</span>envtest<span class="token punctuation">.</span>Environment<span class="token punctuation">{</span><br /> CRDs<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>runtime<span class="token punctuation">.</span>Object<span class="token punctuation">{</span><br /> <span class="token function">newNodeLocalStorageCRD</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// 启动测试环境(etcd 和 apiserver),返回该环境的 rest config</span><br /> config<span class="token punctuation">,</span> err <span class="token operator">:=</span> testEnv<span class="token punctuation">.</span><span class="token function">Start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Fatal</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">defer</span> testEnv<span class="token punctuation">.</span><span class="token function">Stop</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> <span class="token comment">// 生成 controller-runtime 需要的 client</span><br /> c<span class="token punctuation">,</span> err <span class="token operator">:=</span> client<span class="token punctuation">.</span><span class="token function">New</span><span class="token punctuation">(</span>config<span class="token punctuation">,</span> client<span class="token punctuation">.</span>Options<span class="token punctuation">{</span><br /> Scheme<span class="token punctuation">:</span> scheme<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Fatal</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// 初始化 Reconciler</span><br /> nlsReconciler <span class="token operator">:=</span> <span class="token operator">&</span>NodeLocalStorageReconciler<span class="token punctuation">{</span><br /> Client<span class="token punctuation">:</span> c<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// 准备好测试数据</span><br /> nls1 <span class="token operator">:=</span> test<span class="token punctuation">.</span><span class="token function">GetNodeLocalStorage</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> nls1<span class="token punctuation">.</span>Name <span class="token operator">=</span> <span class="token string">"test"</span><br /> <span class="token keyword">if</span> err <span class="token operator">=</span> c<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span><span class="token function">TODO</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> nls1<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Fatal</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// test cases</span><br /> testCases <span class="token operator">:=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> describe <span class="token builtin">string</span><br /> namespace <span class="token builtin">string</span><br /> name <span class="token builtin">string</span><br /> isErr <span class="token builtin">bool</span><br /> <span class="token punctuation">}</span><span class="token punctuation">{</span><br /> <span class="token punctuation">{</span><br /> describe<span class="token punctuation">:</span> <span class="token string">"normal test"</span><span class="token punctuation">,</span><br /> namespace<span class="token punctuation">:</span> <span class="token string">""</span><span class="token punctuation">,</span><br /> name<span class="token punctuation">:</span> <span class="token string">"test"</span><span class="token punctuation">,</span><br /> isErr<span class="token punctuation">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// 测试每个 case</span><br /> <span class="token keyword">for</span> <span class="token boolean">_</span><span class="token punctuation">,</span> testCase <span class="token operator">:=</span> <span class="token keyword">range</span> testCases <span class="token punctuation">{</span><br /> <span class="token boolean">_</span><span class="token punctuation">,</span> err <span class="token operator">:=</span> nlsReconciler<span class="token punctuation">.</span><span class="token function">Reconcile</span><span class="token punctuation">(</span>reconcile<span class="token punctuation">.</span>Request<span class="token punctuation">{</span><br /> NamespacedName<span class="token punctuation">:</span> types<span class="token punctuation">.</span>NamespacedName<span class="token punctuation">{</span><br /> Namespace<span class="token punctuation">:</span> testCase<span class="token punctuation">.</span>namespace<span class="token punctuation">,</span><br /> Name<span class="token punctuation">:</span> testCase<span class="token punctuation">.</span>name<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> testCase<span class="token punctuation">.</span>isErr <span class="token operator">!=</span> <span class="token punctuation">(</span>err <span class="token operator">!=</span> <span class="token boolean">nil</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> t<span class="token punctuation">.</span><span class="token function">Fatalf</span><span class="token punctuation">(</span><span class="token string">"%s unexpected error: %v"</span><span class="token punctuation">,</span> testCase<span class="token punctuation">.</span>describe<span class="token punctuation">,</span> err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<h2 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h2>
<p>controller-runtime 框架本身提供了很多库来帮助构建 controller,让整个流程变得简单,屏蔽了很多通用的细节,能够让构建 controller 整个过程变得更简单,感兴趣的同学可以在不同的场景和需求下都尝试一下;即便不使用 controller-runtime 我们也可以单独使用它的 <a href="https://github.com/kubernetes-sigs/controller-runtime/tree/master/pkg/client">generic client</a> 和 <a href="https://github.com/kubernetes-sigs/controller-runtime/tree/master/pkg/envtest">envtest</a> 等通用库。</p>
<p>单独说下使用 <a href="https://github.com/kubernetes-sigs/controller-runtime/tree/master/pkg/envtest">envtest</a> 需要运行环境(你本地和 CI 等环境)安装 Kubebuilder 提供的一系列 bin 文件(主要是 <code>etcd</code> 和 <code>kube-apiserver</code>),本地的话自己<a href="https://book.kubebuilder.io/quick-start.html#installation">下载</a>就好了,如果是 CI 环境需要的话,可以在基础镜像里面增加这些文件,一个示例:</p>
<pre class="language-text"><code class="language-text">RUN mkdir -p /usr/local && \<br /> wget https://go.kubebuilder.io/dl/2.3.1/linux/amd64 && \<br /> tar xvf amd64 && \<br /> mv kubebuilder_2.3.1_linux_amd64 /usr/local/kubebuilder && \<br /> rm amd64</code></pre>
在使用 SharedInformerFactory 时一些很小但值得注意的问题
2020-05-11T00:00:00Z
https://xinzhao.me/posts/some-basic-but-noteworthy-points-when-using-sharedinformerfactory/
<p></p><div class="table-of-contents"><div class="toc-container-header">索引</div><ul><li><a href="https://xinzhao.me/posts/some-basic-but-noteworthy-points-when-using-sharedinformerfactory/#%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8-goroutine-%E6%9D%A5%E5%90%AF%E5%8A%A8-sharedinformerfactory">不要使用 goroutine 来启动 SharedInformerFactory</a></li><li><a href="https://xinzhao.me/posts/some-basic-but-noteworthy-points-when-using-sharedinformerfactory/#%E6%AD%A3%E7%A1%AE%E5%90%AF%E5%8A%A8-sharedinformerfactory">正确启动 SharedInformerFactory</a></li></ul></div><p></p>
<p>近期在工作中被不同的同事问了一些类似的、关于 SharedInformerFactory 的问题,有些问题非常小,很细节,虽然小但是我觉得是值得注意的问题,所以打算总结分享一下。</p>
<h2 id="%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8-goroutine-%E6%9D%A5%E5%90%AF%E5%8A%A8-sharedinformerfactory" tabindex="-1">不要使用 goroutine 来启动 SharedInformerFactory</h2>
<p>我不是说用 goroutine 来启动就一定是错的,但是用 goroutine 来启动 <code>SharedInformerFactory</code> 没有意义,因为这个方法根本就不会 block,直接启动就好了,<code>Start</code> 方法的源代码:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// Start initializes all requested informers.</span><br /><span class="token keyword">func</span> <span class="token punctuation">(</span>f <span class="token operator">*</span>sharedInformerFactory<span class="token punctuation">)</span> <span class="token function">Start</span><span class="token punctuation">(</span>stopCh <span class="token operator"><-</span><span class="token keyword">chan</span> <span class="token keyword">struct</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> f<span class="token punctuation">.</span>lock<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">defer</span> f<span class="token punctuation">.</span>lock<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> <span class="token keyword">for</span> informerType<span class="token punctuation">,</span> informer <span class="token operator">:=</span> <span class="token keyword">range</span> f<span class="token punctuation">.</span>informers <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token operator">!</span>f<span class="token punctuation">.</span>startedInformers<span class="token punctuation">[</span>informerType<span class="token punctuation">]</span> <span class="token punctuation">{</span><br /> <span class="token keyword">go</span> informer<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span>stopCh<span class="token punctuation">)</span><br /> f<span class="token punctuation">.</span>startedInformers<span class="token punctuation">[</span>informerType<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token boolean">true</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>使用 goroutine 来启动还有另一个问题:它可能导致 <code>WaitForCacheSync</code> 不再起作用,<code>WaitForCacheSync</code> 方法必须要在 informer factory 完全启动(这个方法执行完成)之后再调用,不然它会拿到错误的已启动 informers 的数据,导致它并没有等待所有 informers 启动就返回结果了,看源代码:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// WaitForCacheSync waits for all started informers' cache were synced.</span><br /><span class="token keyword">func</span> <span class="token punctuation">(</span>f <span class="token operator">*</span>sharedInformerFactory<span class="token punctuation">)</span> <span class="token function">WaitForCacheSync</span><span class="token punctuation">(</span>stopCh <span class="token operator"><-</span><span class="token keyword">chan</span> <span class="token keyword">struct</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token keyword">map</span><span class="token punctuation">[</span>reflect<span class="token punctuation">.</span>Type<span class="token punctuation">]</span><span class="token builtin">bool</span> <span class="token punctuation">{</span><br /> informers <span class="token operator">:=</span> <span class="token keyword">func</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">map</span><span class="token punctuation">[</span>reflect<span class="token punctuation">.</span>Type<span class="token punctuation">]</span>cache<span class="token punctuation">.</span>SharedIndexInformer <span class="token punctuation">{</span><br /> f<span class="token punctuation">.</span>lock<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">defer</span> f<span class="token punctuation">.</span>lock<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> informers <span class="token operator">:=</span> <span class="token keyword">map</span><span class="token punctuation">[</span>reflect<span class="token punctuation">.</span>Type<span class="token punctuation">]</span>cache<span class="token punctuation">.</span>SharedIndexInformer<span class="token punctuation">{</span><span class="token punctuation">}</span><br /> <span class="token keyword">for</span> informerType<span class="token punctuation">,</span> informer <span class="token operator">:=</span> <span class="token keyword">range</span> f<span class="token punctuation">.</span>informers <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> f<span class="token punctuation">.</span>startedInformers<span class="token punctuation">[</span>informerType<span class="token punctuation">]</span> <span class="token punctuation">{</span><br /> informers<span class="token punctuation">[</span>informerType<span class="token punctuation">]</span> <span class="token operator">=</span> informer<br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> informers<br /> <span class="token punctuation">}</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> res <span class="token operator">:=</span> <span class="token keyword">map</span><span class="token punctuation">[</span>reflect<span class="token punctuation">.</span>Type<span class="token punctuation">]</span><span class="token builtin">bool</span><span class="token punctuation">{</span><span class="token punctuation">}</span><br /> <span class="token keyword">for</span> informType<span class="token punctuation">,</span> informer <span class="token operator">:=</span> <span class="token keyword">range</span> informers <span class="token punctuation">{</span><br /> res<span class="token punctuation">[</span>informType<span class="token punctuation">]</span> <span class="token operator">=</span> cache<span class="token punctuation">.</span><span class="token function">WaitForCacheSync</span><span class="token punctuation">(</span>stopCh<span class="token punctuation">,</span> informer<span class="token punctuation">.</span>HasSynced<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> res<br /><span class="token punctuation">}</span></code></pre>
<p><code>WaitForCacheSync</code> 需要从 <code>f.startedInformers</code> 中获取已启动 informers 的数据,而 <code>f.startedInformers</code> 是在执行 <code>Start</code> 方法时填充的,使用 goroutine 来启动 <code>SharedInformerFactory</code> 之后再正常 wait for sync 往往都是 <code>WaitForCacheSync</code> 函数先于 <code>Start</code> 方法执行,它会先拿到锁,等到它执行完了 <code>Start</code> 才会开始执行,导致该函数完全没有起到 wait 的效果;可以用下面的示例代码(可自行调整用 goroutine 来启动不同的方法)来看看效果:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">package</span> main<br /><br /><span class="token keyword">import</span> <span class="token punctuation">(</span><br /> <span class="token string">"fmt"</span><br /> <span class="token string">"sync"</span><br /> <span class="token string">"time"</span><br /><span class="token punctuation">)</span><br /><br /><span class="token keyword">type</span> factory <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> lock sync<span class="token punctuation">.</span>RWMutex<br /> data <span class="token keyword">map</span><span class="token punctuation">[</span><span class="token builtin">string</span><span class="token punctuation">]</span><span class="token builtin">string</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">func</span> <span class="token punctuation">(</span>f <span class="token operator">*</span>factory<span class="token punctuation">)</span> <span class="token function">method1</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> f<span class="token punctuation">.</span>lock<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">defer</span> f<span class="token punctuation">.</span>lock<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> f<span class="token punctuation">.</span>data<span class="token punctuation">[</span><span class="token string">"k1"</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">"v1"</span><br /> f<span class="token punctuation">.</span>data<span class="token punctuation">[</span><span class="token string">"k2"</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">"v2"</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">func</span> <span class="token punctuation">(</span>f <span class="token operator">*</span>factory<span class="token punctuation">)</span> <span class="token function">method2</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> a <span class="token operator">:=</span> <span class="token keyword">func</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> f<span class="token punctuation">.</span>lock<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">defer</span> f<span class="token punctuation">.</span>lock<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> <span class="token keyword">for</span> k<span class="token punctuation">,</span> v <span class="token operator">:=</span> <span class="token keyword">range</span> f<span class="token punctuation">.</span>data <span class="token punctuation">{</span><br /> fmt<span class="token punctuation">.</span><span class="token function">Println</span><span class="token punctuation">(</span>k<span class="token punctuation">,</span> v<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token function">a</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">func</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> f <span class="token operator">:=</span> <span class="token operator">&</span>factory<span class="token punctuation">{</span><br /> data<span class="token punctuation">:</span> <span class="token function">make</span><span class="token punctuation">(</span><span class="token keyword">map</span><span class="token punctuation">[</span><span class="token builtin">string</span><span class="token punctuation">]</span><span class="token builtin">string</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /> f<span class="token punctuation">.</span><span class="token function">method1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> f<span class="token punctuation">.</span><span class="token function">method2</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> time<span class="token punctuation">.</span><span class="token function">Sleep</span><span class="token punctuation">(</span>time<span class="token punctuation">.</span>Second <span class="token operator">*</span> <span class="token number">1</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<table>
<thead><tr><th>Wrong</th><th>Correct</th></tr></thead>
<tbody>
<tr>
<td>
<pre class="language-go"><code class="language-go"><span class="token keyword">go</span> informerFactory<span class="token punctuation">.</span><span class="token function">Start</span><span class="token punctuation">(</span>stopCh<span class="token punctuation">)</span><br /><br />informerFactory<span class="token punctuation">.</span><span class="token function">WaitForCacheSync</span><span class="token punctuation">(</span>stopCh<span class="token punctuation">)</span></code></pre>
</td>
<td>
<pre class="language-go"><code class="language-go">informerFactory<span class="token punctuation">.</span><span class="token function">Start</span><span class="token punctuation">(</span>stopCh<span class="token punctuation">)</span><br /><br />informerFactory<span class="token punctuation">.</span><span class="token function">WaitForCacheSync</span><span class="token punctuation">(</span>stopCh<span class="token punctuation">)</span></code></pre>
</td>
</tr>
</tbody>
</table>
<h2 id="%E6%AD%A3%E7%A1%AE%E5%90%AF%E5%8A%A8-sharedinformerfactory" tabindex="-1">正确启动 SharedInformerFactory</h2>
<p>在调用 <code>informerFactory.Start</code> 方法之前,你必须保证直接或间接调用了你想使用的 informer 的 <code>Informer()</code> 方法,否则之后 <code>Start</code> 方法不会起作用,它不会启动任何东西;调用示例:</p>
<table>
<thead><tr><th>直接调用</th><th>间接调用</th></tr></thead>
<tbody>
<tr>
<td>
<pre class="language-go"><code class="language-go">informerFactory<span class="token punctuation">.</span><span class="token function">Core</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">V1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Pods</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Informer</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
</td>
<td>
<pre class="language-go"><code class="language-go">informerFactory<span class="token punctuation">.</span><span class="token function">Core</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">V1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Pods</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Lister</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
</td>
</tr>
</tbody>
</table>
<p>其中 <code>Lister</code> 方法会调用 <code>Informer</code> 方法,而 <code>Informer</code> 方法会调用 factory 的 <code>InformerFor</code> 方法,该方法会将当前 informer 数据写入到 factory 中,后续启动的时候会使用到:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// InternalInformerFor returns the SharedIndexInformer for obj using an internal</span><br /><span class="token comment">// client.</span><br /><span class="token keyword">func</span> <span class="token punctuation">(</span>f <span class="token operator">*</span>sharedInformerFactory<span class="token punctuation">)</span> <span class="token function">InformerFor</span><span class="token punctuation">(</span>obj runtime<span class="token punctuation">.</span>Object<span class="token punctuation">,</span> newFunc internalinterfaces<span class="token punctuation">.</span>NewInformerFunc<span class="token punctuation">)</span> cache<span class="token punctuation">.</span>SharedIndexInformer <span class="token punctuation">{</span><br /> f<span class="token punctuation">.</span>lock<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">defer</span> f<span class="token punctuation">.</span>lock<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> informerType <span class="token operator">:=</span> reflect<span class="token punctuation">.</span><span class="token function">TypeOf</span><span class="token punctuation">(</span>obj<span class="token punctuation">)</span><br /> informer<span class="token punctuation">,</span> exists <span class="token operator">:=</span> f<span class="token punctuation">.</span>informers<span class="token punctuation">[</span>informerType<span class="token punctuation">]</span><br /> <span class="token keyword">if</span> exists <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> informer<br /> <span class="token punctuation">}</span><br /><br /> resyncPeriod<span class="token punctuation">,</span> exists <span class="token operator">:=</span> f<span class="token punctuation">.</span>customResync<span class="token punctuation">[</span>informerType<span class="token punctuation">]</span><br /> <span class="token keyword">if</span> <span class="token operator">!</span>exists <span class="token punctuation">{</span><br /> resyncPeriod <span class="token operator">=</span> f<span class="token punctuation">.</span>defaultResync<br /> <span class="token punctuation">}</span><br /><br /> informer <span class="token operator">=</span> <span class="token function">newFunc</span><span class="token punctuation">(</span>f<span class="token punctuation">.</span>client<span class="token punctuation">,</span> resyncPeriod<span class="token punctuation">)</span><br /> f<span class="token punctuation">.</span>informers<span class="token punctuation">[</span>informerType<span class="token punctuation">]</span> <span class="token operator">=</span> informer<br /><br /> <span class="token keyword">return</span> informer<br /><span class="token punctuation">}</span></code></pre>
<table>
<thead><tr><th>Wrong</th><th>Correct</th></tr></thead>
<tbody>
<tr>
<td>
<pre class="language-go"><code class="language-go">informerFactory <span class="token operator">:=</span> informers<span class="token punctuation">.</span><span class="token function">NewSharedInformerFactory</span><span class="token punctuation">(</span>kubeClient<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><br />informerFactory<span class="token punctuation">.</span><span class="token function">Start</span><span class="token punctuation">(</span>stopch<span class="token punctuation">)</span><br />podLister <span class="token operator">:=</span> informerFactory<span class="token punctuation">.</span><span class="token function">Core</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">V1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Pods</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Lister</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token operator">...</span></code></pre>
</td>
<td>
<pre class="language-go"><code class="language-go">informerFactory <span class="token operator">:=</span> informers<span class="token punctuation">.</span><span class="token function">NewSharedInformerFactory</span><span class="token punctuation">(</span>kubeClient<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><br />podLister <span class="token operator">:=</span> informerFactory<span class="token punctuation">.</span><span class="token function">Core</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">V1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Pods</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Lister</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br />informerFactory<span class="token punctuation">.</span><span class="token function">Start</span><span class="token punctuation">(</span>stopch<span class="token punctuation">)</span><br /><span class="token operator">...</span></code></pre>
</td>
</tr>
</tbody>
</table>
<p>上面错误的方法中,<code>informerFactory</code> 不会启动也不会缓存任何东西,<code>podLister</code> 会永远返回空结果。</p>
<p>在一些通用框架的场景下也可以使用 <code>ForResource</code> 方法来手动“注册”一个资源,它其实也是调用了对应资源的 <code>Informer</code> 方法:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">type</span> SharedInformerFactory <span class="token keyword">interface</span> <span class="token punctuation">{</span><br /> <span class="token operator">...</span><br /> <span class="token function">ForResource</span><span class="token punctuation">(</span>resource schema<span class="token punctuation">.</span>GroupVersionResource<span class="token punctuation">)</span> <span class="token punctuation">(</span>GenericInformer<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// ForResource gives generic access to a shared informer of the matching type</span><br /><span class="token keyword">func</span> <span class="token punctuation">(</span>f <span class="token operator">*</span>sharedInformerFactory<span class="token punctuation">)</span> <span class="token function">ForResource</span><span class="token punctuation">(</span>resource schema<span class="token punctuation">.</span>GroupVersionResource<span class="token punctuation">)</span> <span class="token punctuation">(</span>GenericInformer<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">switch</span> resource <span class="token punctuation">{</span><br /> <span class="token comment">// Group=snapshot.storage.k8s.io, Version=v1beta1</span><br /> <span class="token keyword">case</span> v1beta1<span class="token punctuation">.</span>SchemeGroupVersion<span class="token punctuation">.</span><span class="token function">WithResource</span><span class="token punctuation">(</span><span class="token string">"volumesnapshots"</span><span class="token punctuation">)</span><span class="token punctuation">:</span><br /> <span class="token keyword">return</span> <span class="token operator">&</span>genericInformer<span class="token punctuation">{</span>resource<span class="token punctuation">:</span> resource<span class="token punctuation">.</span><span class="token function">GroupResource</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> informer<span class="token punctuation">:</span> f<span class="token punctuation">.</span><span class="token function">Snapshot</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">V1beta1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">VolumeSnapshots</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Informer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token boolean">nil</span><br /> <span class="token keyword">case</span> v1beta1<span class="token punctuation">.</span>SchemeGroupVersion<span class="token punctuation">.</span><span class="token function">WithResource</span><span class="token punctuation">(</span><span class="token string">"volumesnapshotclasses"</span><span class="token punctuation">)</span><span class="token punctuation">:</span><br /> <span class="token keyword">return</span> <span class="token operator">&</span>genericInformer<span class="token punctuation">{</span>resource<span class="token punctuation">:</span> resource<span class="token punctuation">.</span><span class="token function">GroupResource</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> informer<span class="token punctuation">:</span> f<span class="token punctuation">.</span><span class="token function">Snapshot</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">V1beta1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">VolumeSnapshotClasses</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Informer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token boolean">nil</span><br /> <span class="token keyword">case</span> v1beta1<span class="token punctuation">.</span>SchemeGroupVersion<span class="token punctuation">.</span><span class="token function">WithResource</span><span class="token punctuation">(</span><span class="token string">"volumesnapshotcontents"</span><span class="token punctuation">)</span><span class="token punctuation">:</span><br /> <span class="token keyword">return</span> <span class="token operator">&</span>genericInformer<span class="token punctuation">{</span>resource<span class="token punctuation">:</span> resource<span class="token punctuation">.</span><span class="token function">GroupResource</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> informer<span class="token punctuation">:</span> f<span class="token punctuation">.</span><span class="token function">Snapshot</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">V1beta1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">VolumeSnapshotContents</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Informer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token boolean">nil</span><br /><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">return</span> <span class="token boolean">nil</span><span class="token punctuation">,</span> fmt<span class="token punctuation">.</span><span class="token function">Errorf</span><span class="token punctuation">(</span><span class="token string">"no informer found for %v"</span><span class="token punctuation">,</span> resource<span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
使用 Ceph 做持久化存储的常见问题
2018-09-30T00:00:00Z
https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/
<p></p><div class="table-of-contents"><div class="toc-container-header">索引</div><ul><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#%E5%89%8D%E8%A8%80">前言</a></li><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#%E5%AE%89%E8%A3%85-ceph-%E9%9B%86%E7%BE%A4%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98">安装 Ceph 集群常见问题</a><ul><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#%E4%B8%8D%E8%A6%81%E7%94%A8-root-%E7%94%A8%E6%88%B7">不要用 root 用户</a></li><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#%E8%8A%82%E7%82%B9%E6%95%B0%E9%87%8F">节点数量</a></li><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#%E5%85%B3%E4%BA%8E-.ssh%2Fconfig-%E6%96%87%E4%BB%B6">关于 .ssh/config 文件</a></li><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#importerror%3A-no-module-named-pkg_resources">ImportError: No module named pkg_resources</a></li><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#runtimeerror%3A-nosectionerror%3A-no-section%3A-'ceph'">RuntimeError: NoSectionError: No section: 'ceph'</a></li></ul></li><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#%E4%BD%BF%E7%94%A8-ceph-%E7%9A%84%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98">使用 Ceph 的常见问题</a><ul><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#provisioner">Provisioner</a></li><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#error-creating-rbd-image%3A-executable-file-not-found-in-%24path">Error creating rbd image: executable file not found in $PATH</a></li><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#feature-set-mismatch-missing-400000000000000">feature set mismatch missing 400000000000000</a></li><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#%E8%AE%BF%E9%97%AE%E6%A8%A1%E5%BC%8F">访问模式</a></li><li><a href="https://xinzhao.me/posts/common-problems-about-using-ceph-for-persistent-storage/#%E5%85%B6%E5%AE%83%E9%97%AE%E9%A2%98">其它问题</a></li></ul></li></ul></div><p></p>
<blockquote>
<p>Ceph 是一个开源的分布式对象、块和文件存储系统。Kubernetes 支持 Ceph 的块存储(Ceph RBD)和文件存储(CephFS)作为 Kubernetes 的持久存储后端。</p>
</blockquote>
<h2 id="%E5%89%8D%E8%A8%80" tabindex="-1">前言</h2>
<p>Ceph 集群的安装过程这些就不过多阐述了,参照官网文档就行了,可以选择直接装在物理机上(我用的这种方式),也可以使用 Helm 来安装在 k8s 集群中,按需选择吧。</p>
<p>下面是一些安装参考文档:</p>
<ul>
<li><a href="http://docs.ceph.com/docs/master/start/quick-start-preflight/">Preflight Checklist — Ceph Documentation</a></li>
<li><a href="http://docs.ceph.com/docs/master/start/quick-ceph-deploy/">Storage Cluster Quick Start — Ceph Documentation</a></li>
<li><a href="http://docs.ceph.com/docs/master/start/kube-helm/">Installation (Kubernetes + Helm) — Ceph Documentation</a></li>
</ul>
<h2 id="%E5%AE%89%E8%A3%85-ceph-%E9%9B%86%E7%BE%A4%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98" tabindex="-1">安装 Ceph 集群常见问题</h2>
<p>先说一下安装环境,参考上面的官网文档的步骤进行安装的,共 4 台服务器,1 台用来部署,其他 3 台用作 Ceph 的节点,所有服务器的操作系统都是 CentOS 7.5。</p>
<h3 id="%E4%B8%8D%E8%A6%81%E7%94%A8-root-%E7%94%A8%E6%88%B7" tabindex="-1">不要用 root 用户</h3>
<p><a href="http://docs.ceph.com/docs/master/start/quick-start-preflight/#create-a-ceph-deploy-user">官网文档</a>明确指出了不要图方便就用 root 这种用户,极其不推荐。</p>
<h3 id="%E8%8A%82%E7%82%B9%E6%95%B0%E9%87%8F" tabindex="-1">节点数量</h3>
<p>Ceph 默认需要 3 个 osd 节点,如果你机器数量不够可以使用下面的配置更改默认的设置:</p>
<pre class="language-text"><code class="language-text">osd pool default size = 2</code></pre>
<h3 id="%E5%85%B3%E4%BA%8E-.ssh%2Fconfig-%E6%96%87%E4%BB%B6" tabindex="-1">关于 <code>.ssh/config</code> 文件</h3>
<pre class="language-text"><code class="language-text">Host node1<br /> Hostname 192.168.21.50<br /> User c</code></pre>
<p>Host name 要和机器的名称匹配。</p>
<p>顺便说一下,增加了这个文件之后还要更新一下 hosts 文件 <code>vim /etc/hosts</code>,把 host name 对应的 IP 地址写进去:</p>
<pre class="language-text"><code class="language-text">192.168.21.50 node1<br />...</code></pre>
<h3 id="importerror%3A-no-module-named-pkg_resources" tabindex="-1">ImportError: No module named pkg_resources</h3>
<p>更新一下机器的 python 环境:</p>
<pre class="language-text"><code class="language-text">yum install gcc python-setuptools python-devel<br /><br />easy_install pip</code></pre>
<h3 id="runtimeerror%3A-nosectionerror%3A-no-section%3A-'ceph'" tabindex="-1">RuntimeError: NoSectionError: No section: 'ceph'</h3>
<p>检查一下 yum 的配置文件 <code>/etc/yum.repos.d/</code> 里面 <code>ceph.repo</code> 这个文件中有没有 <code>ceph</code> 这一段,没有的话检查一下这个目录下面应该有一个 <code>ceph.repo.rpmnew</code> 的文件,使用这个新的文件即可:</p>
<pre class="language-text"><code class="language-text">mv ceph.repo ceph.repo.old<br /><br />mv ceph.repo.rpmnew ceph.repo</code></pre>
<h2 id="%E4%BD%BF%E7%94%A8-ceph-%E7%9A%84%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98" tabindex="-1">使用 Ceph 的常见问题</h2>
<h3 id="provisioner" tabindex="-1">Provisioner</h3>
<p>Kubernetes 自带 Ceph RBD 的 internal provisioner,可以配置动态存储提供(Dynamic Volume Provisioning),CephFS 则需要<a href="https://github.com/kubernetes-incubator/external-storage/tree/master/ceph/cephfs">外置的 provisioner</a>。</p>
<p>Kubernetes Storage Class 支持的 Provisioner 列表可以参考<a href="https://kubernetes.io/docs/concepts/storage/storage-classes/#provisioner">官网文档</a>。</p>
<h3 id="error-creating-rbd-image%3A-executable-file-not-found-in-%24path" tabindex="-1">Error creating rbd image: executable file not found in $PATH</h3>
<p>所有 k8s 节点的物理机上都要安装 <code>ceph-common</code> 这个包(<code>yum install ceph-common</code>),这个 client 的版本要和 Ceph 集群版本一致。</p>
<h3 id="feature-set-mismatch-missing-400000000000000" tabindex="-1">feature set mismatch missing 400000000000000</h3>
<blockquote>
<p>顺便说一下,如果你遇到了 <code>timeout expired waiting for volumes to attach/mount for pod</code> 这个错误,很可能也是下面的原因</p>
</blockquote>
<p>原因是 Linux 内核版本和 Ceph 当前版本支持的不匹配(内核版本太低了,不支持当前版本的 Ceph 需要的一些特性),可以使用下面的命令来使 Ceph 兼容当前的内核版本(这么说好像不太对,大概是这个意思):</p>
<pre class="language-text"><code class="language-text">ceph osd crush tunables hammer</code></pre>
<p><code>hammer</code> 是对应内核支持的 Ceph 版本,对照表如下:</p>
<pre class="language-text"><code class="language-text">> TUNABLE RELEASE CEPH_VERSION KERNEL<br />> CRUSH_TUNABLES argonaut v0.48.1 v3.6<br />> CRUSH_TUNABLES2 bobtail v0.55 v3.9<br />> CRUSH_TUNABLES3 firefly v0.78 v3.15<br />> CRUSH_V4 hammer v0.94 v4.1<br />> CRUSH_TUNABLES5 Jewel v10.0.2 v4.5</code></pre>
<p>当然,你也可以升级你的 Linux 内核来与之匹配。</p>
<h3 id="%E8%AE%BF%E9%97%AE%E6%A8%A1%E5%BC%8F" tabindex="-1">访问模式</h3>
<p>还有一个注意事项:CephFS 支持 3 种访问模式:<code>ReadWriteOnce</code>,<code>ReadOnlyMany</code>,<code>ReadWriteMany</code>,而 RBD 是块存储,仅支持两种:<code>ReadWriteOnce</code>,<code>ReadOnlyMany</code>。</p>
<h3 id="%E5%85%B6%E5%AE%83%E9%97%AE%E9%A2%98" tabindex="-1">其它问题</h3>
<p>出现了错误仔细看一下 k8s 的报错信息,如果报错信息实在看不出什么,先检查一下各个参数有没有填对,比如 monitors 地址,密码之类的。</p>
DUET
2016-09-04T00:00:00Z
https://xinzhao.me/posts/DUET/
<p>DUET 是一款非常有趣的游戏,玩法非常简单,你控制两个小球通过旋转来躲避各种形状的障碍物:</p>
<p><img src="https://xinzhao.me/img/DUET/IMG_0583.PNG" alt="" /></p>
<p>关卡设计得很棒,难度层层递进,配乐也非常不错;故事模式每一关开始的时候都有一句独白,刚开始看到的时候可能会不知道它在说啥,是什么意思,玩到后面你就会知道每句独白都是和当前关卡的主题/风格契合的,它一直在引导你去理解/适应这个游戏的节奏,你需要不断地建立起一套认识模式,然后再打破它,你的心态也会和每一章的标题一样:从无知、否定、愤怒、商谈到罪恶、沮丧、希望和接受。</p>
<h2 id="%E6%97%A0%E7%9F%A5" tabindex="-1">无知</h2>
<p><img src="https://xinzhao.me/img/DUET/IMG_0740.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0741.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0742.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0743.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0744.PNG" alt="" /></p>
<h2 id="%E5%90%A6%E5%AE%9A" tabindex="-1">否定</h2>
<p><img src="https://xinzhao.me/img/DUET/IMG_0745.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0746.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0747.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0748.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0749.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0751.PNG" alt="" /></p>
<h2 id="%E6%84%A4%E6%80%92" tabindex="-1">愤怒</h2>
<p><img src="https://xinzhao.me/img/DUET/IMG_0753.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0754.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0755.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0756.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0757.PNG" alt="" /></p>
<h2 id="%E5%95%86%E8%B0%88" tabindex="-1">商谈</h2>
<p><img src="https://xinzhao.me/img/DUET/IMG_0770.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0771.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0772.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0773.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0774.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0776.PNG" alt="" /></p>
<h2 id="%E7%BD%AA%E6%81%B6" tabindex="-1">罪恶</h2>
<p><img src="https://xinzhao.me/img/DUET/IMG_0777.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0778.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0779.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0780.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0804.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0805.PNG" alt="" /></p>
<h2 id="%E6%B2%AE%E4%B8%A7" tabindex="-1">沮丧</h2>
<p><img src="https://xinzhao.me/img/DUET/IMG_0807.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0808.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0809.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0810.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0811.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0812.PNG" alt="" /></p>
<h2 id="%E5%B8%8C%E6%9C%9B" tabindex="-1">希望</h2>
<p><img src="https://xinzhao.me/img/DUET/IMG_0813.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0818.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0819.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0820.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0821.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0825.PNG" alt="" /></p>
<h2 id="%E6%8E%A5%E5%8F%97" tabindex="-1">接受</h2>
<p><img src="https://xinzhao.me/img/DUET/IMG_0826.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0827.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0828.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0829.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0830.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0831.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0832.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0833.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0834.PNG" alt="" />
<img src="https://xinzhao.me/img/DUET/IMG_0835.PNG" alt="" /></p>