关于 Eason Yang | 杨宇晨

长期从事后端研发和架构设计方面的工作,对技术和产品有一些自己的感悟和沉淀,这也是本博客所包含的主要内容。 

RSS 地址: https://easonyang.com/atom.xml

请复制 RSS 到你的阅读器,或快速订阅到 :

Eason Yang | 杨宇晨 RSS 预览

2022年终总结

2023-01-02 00:47:14

这一年我经历了很多,看到听到的更多。工作生活都没什么大的变化,但心情总是很差。21 年没有做合格的年终总结,23 年不一定会有总结,但 2022 这魔幻的一年不能没有一份详实的年度回顾记录。

工作还顺利吗

《晚点 LatePost》在「中国十二大互联网公司年终盘点」一文中有这样一段话:

中国的互联网行业集体进入了一个新阶段 —— 不再寄望于无穷尽的增长,而是确保自己能在停止增长之后,通过提高管理和经营效率取得更高的利润。

这简直是对我 2022 工作体验的完美概括。过于关注局部的数据指标、外行领导内行盛行、向上管理几乎成了政治正确,即使没被裁员和降薪,我也要被迫体验到这些中高层老板从冲量到盈利过渡期对工作方向「艰难探索」所带来的副作用。不巧的是这些都是我所厌恶的东西,以至于时不时地我就会怀疑自己的工作到底能带来什么价值,全年可以说是毫无工作乐趣。而这令人沮丧的工作氛围也耗尽了我开拓副业的热情,甚至几度让我没了发言交流的欲望,如果你发现我几天都没在 Twitter 上瞎侃,那大概率我又因为工作的原因而怀疑人生了。

在工作内容方面,今年我的角色承担了更多的管理责任,一如《卓有成效的管理者》一书所言:

管理者的时间往往只属于别人,不属于自己。

无穷无尽的会议、代码及方案审核和各种花式汇报文档的编写,再加上前面所说的「艰难探索」带来的各种自上而下的奇葩指示中有不少还需要我来推动或落地,让我很少有时间能静下心来写代码。而又由于我对本组的项目实在过于熟悉,在架构和技术方案的设计上又很难遇到什么新的挑战,所以也谈不上什么技术成长。总体而言这一年我的时间和人基本是被事情推着走,所谓的管理大多也只是做了些顺水推舟的事情,好在有一些技术和业务层面的决策还算有少许价值,不然真可谓是在工作层面碌碌无为了一整年。

在主业之外,今年的内容和副业产出少得可怜。首先没能达成 21 年所设想的本年参与开源工作的目标,连小玩具都没动手做。其次博客也只写了三篇文章(把Notion变为个人网站再见Hexo——从Hexo迁移至Hugo旁路由的原理与配置一文通),虽然从搜索引擎排名情况来看文章质量应该还算可以,但三篇这个数量着实是太少了,而且基本都是我所熟悉的舒适区,可以概括为没有持续地用心写作。

最过分的是,Newsletter 被我鸽了一整年没更,鸽到平台 Revue 都倒闭了,实在是对不住 21 年以来订阅的这几十位读者了。不过下半年运营了下 Telegram Channel 并获得了一些关注,感谢各位的厚爱,这个应该不会再鸽了(吧)。Twitter 倒是说了不少话,涨了一点关注,但好像也没产出多少有价值的内容,惭愧惭愧。

生活还开心吗

1、心情很差

从 21 年年末的西安封城开始,我的心情就一直很差。其中原因,一方面是我对同胞受苦的不忍,另一方面则是对方舱隔离和宠物无害化处理的恐惧。这一切愤恨、恐惧和绝望都在 22 年的年末戛然而止,我也不出意外地在放开后感染上了新冠病毒发了两天烧。

回看这一荒诞的一年,上海封城的乱象让我对其管理水平之低下有了新的认识,戾气日长的日常生活也让我看到了底层互害的可怕。我练出了进门前随手打开健康宝的扫码反射,也习惯了调低期望居家工作的生活。这些记忆既是历史的现身说法,也是对我认知的宝贵教训。我在自己的 Obsidian 知识库中整理了由新闻、图片和感想组成的 2022 清零防疫笔记图谱,以后每年都会翻出来看一看,提醒自己不要忘记这沉痛的一年中发生的骇人的事情。

2、书影音的荒漠之年

由于电影院几乎歇菜了一整年,再加上我心情不好和工作内卷,这一年看的影视剧大部分都是以前看过内容的二刷、三刷,其中又以老港片为主。这些内容有情怀加持,剧情和桥段我又烂熟于心,看起来没有心理负担和理解成本,可以说是纯纯地摆烂了。

不过在状态好的时候我也延迟了几周到几月不等地看了一些热度比较高的新剧和新电影,这里列举下红黑榜。

(1)年度佳片

Netflix Better Call Saul Poster

  1. Bullet Train:很多年没看到这样的暴力美学爽片了,一度让我想起了《杀死比尔》,是难得的一部我不想聊剧情漏洞,只想看电影本身的影片。
  2. Dahmer – Monster: The Jeffrey Dahmer Story:Netflix 版本的达莫案件改编,前半段重口得恰到好处,后半段又令人唏嘘。
  3. Archive 81:这两年 Netflix 难得一见的正常水平科幻惊悚新剧,剧情紧凑节奏鲜明。由于是年初的剧,让我一度对 Netflix 的制作水平又燃起了希望。
  4. Stranger Things S4:个人对这一季不是很满意,苏联那一段完全没必要。不过本季填了不少坑,也有很多感人片段,还是值得一看的。
  5. Better Call Saul S6:大部分时间仍然秉持了神剧标准。但作为一个无脑厌恶 Chuck 的观众,我觉得 Jimmy 值得一个更温暖的结局。

(2)年度烂片

  1. Choose or Die:好标题、好题材,可能想拍成黑镜,但最终拍出了个剧情稀烂的四不像。
  2. Resident Evil(Netflix 版本):期望越大,失望越大!生化危机系列最差改编,没有之一。
  3. 四海:22 年好像只在电影院看了这一部电影,如果再给我一次机会,我宁愿不去看。
  4. Alice in Borderland S2:「嘴遁+说教+回忆」,但还没博人燃。
  5. Keep Breathing:看完了我简直气得呼吸困难。

不难看出,这又是我被 Netflix 「坑害」的一年,除了续集尚有些能保持水准,新剧有大量的平庸之作甚至烂片,电影就更差了。23 年真的要考虑换一家来订阅了。

(3)不读书吗

今年读的书仍然是工具导向为主的技术或管理类书籍,而且数量不多,大多也是跳着读的,一般只有某几章是精读的,所以很难做出推荐和总结。不过在人文类书籍方面,22 年读的这本《午夜将至:核战边缘的肯尼迪、赫鲁晓夫与卡斯特罗》很有意思,联想今年国内的种种怪状和 21 年的一部我超爱的剧 Inside Job ,着实是让我重新审视了我对所谓高层的认知。

(4)还听播客吗

今年播客听得也少了很多,主力播客工具 Pocket Casts 的统计是 3days 9hours ,不过今年用有音转文功能的 Snipd 听了不少英文播客,所以这个统计有些失真。《忽左忽右》应该是我本年最爱的播客节目了,几乎每期必听,还会做很多的延伸阅读,新的一年要多多购买 JustPod 的周边产品和付费节目来做支持才行。

(5)不听音乐吗

Masuerade Album. From Apple Music

今年周杰伦听得少了很多,变得超级爱听伍佰的歌。以至于《再度重相逢》和《Last Dance》两首经典金曲在我的 Apple Music Replay 2022 中高居榜首,可见我的怀旧情绪有多高涨。我的年度歌曲是下面这两首:

  1. Running Up That Hill (A Deal With God):上半年时这首老歌着实是再次爆火了一把,相比起原版我更喜欢怪奇物语版本,搭配剧一起食用味道更佳。
  2. Masquerade:尚未大火的音乐人 Stellar 今年的作品,编曲、歌词都很棒,封面选择有股浓浓的 AI 味也很合我的胃口,他的其它单曲也很好听,推荐收藏。

(6)不玩游戏吗

Red Dead Redemption 2. From Steam

年初时终于把《荒野大镖客2》的剧情通关了,好一部快意恩仇的西部史。是的你没看错,这样的大作我总是延迟几年才去玩,计划明年玩完《战神》。

Faze Clan IEM Champion. From Faze Clan Twitter

今年机缘巧合间熬夜看了 CS:GO IEM 科隆总决赛,见证了 Faze Clan 的夺冠。后面又断断续续看了 Major 、Blast 等比赛,不得不说 CS:GO 比赛的观赏性是真的高。可惜入门和观赛门槛都挺高,所以国内外的人气都比较一般,这一点从与其他游戏的奖金池规模对比上也可见一斑。

题外话顺便聊两句足球。今年除了世界杯就没怎么看球,西甲和中超似乎都没看满整场的时候,欧冠由于巴萨早早出局也就看了个集锦。主队巴萨今年已经衰落成了标准的 1.5 流强队,并正在向二流继续滑坡,这几个赛季不因为杠杆和债务而破产或被私有化我就已经烧高香了,好在大连人今年终于不再在降级区苦苦挣扎了。足球在我生活中的比重越来越小,以前不理解我爸年轻时那么爱看球怎么中年之后就不太关注了,现在似乎逐渐领悟了一些。

3、投资理财赔钱了吗

今年的投资策略非常保守,股票、基金几乎完全不碰,更别提什么期货期权了。对这种经济下行状态下的认知不自信让我在今年变成了一个极端的风险厌恶者。

这样的好处是今年不仅没亏,而且还靠着美元升值和黄金价格波动取得了一些盈余。坏处是现在不抄底可能几年后肠子会悔青。不过我想这个时间点还是手握现金流更稳妥些,留得青山在不怕没柴烧,以后的悔就让以后的我品尝吧,不跟风不梭哈就不会破产。

4、也有一些好事情

敲敲的猫片

我家小猫在 21 年做了个大手术,令人欣慰的是 22 年她活蹦乱跳瞎作了一整年什么事也没有,希望明年能继续作继续闹,铲屎官承受得起!

今年最大的成就可能就是把烟给戒掉了。我倒不是一个数年烟龄的老烟民,总计也就抽了两年左右,但后期随着工作和清零带来的沉闷心情,我的吸烟量达到了惊人的两包一天。Q4 在给自己重新灌输了一遍尼古丁不解决问题只会带来问题的理念后(其实就是《这本书能让你戒烟》一书的核心主旨),我趁着十一假期撑过了戒断反应的最高峰并彻底把烟戒掉了。

我的戒断反应主要集中在牙龈微痛和类似于醉氧的头晕,第一周还是挺难熬的,好在三周后就完全没有任何生理层面的不适和吸烟欲望了。不过,即使是在三个月后的今天,我也仍偶尔会产生想吸烟的情绪苗头。虽然比起戒烟时期,这些缺乏生理机制支撑的小火苗很容易就可以被扑灭,但我不知道这样的情况还会持续多久。所以尼古丁这类东西能不碰就不碰,不要迷信你所谓的意志力。

Argentina the Champion. From FIFA Twitter

从下半年乌克兰变为优势方,到阿根廷在卡塔尔世界杯夺冠梅老板圆梦,再到清零政策的戛然而止,我的世界和生活似乎又有了些光明。我又想提起那句被说烂了的电影台词:

生活就像巧克力,你永远不知道下一颗是什么味道。

买了什么好东西

今年几乎所有的大件都是在春节期间买的,原因是那时还没对宏观经济情况的恶化作过多的思考。春节后就开始避免大额开销,非必要不购物了。所以比起 21 年,今年的购物清单算是寒酸了不少。

1、电子产品

(1)DJI Pocket 2

推荐指数:★★★★☆

买它是因为去环球影城前想到了在迪士尼乐园用手机录像的糟糕体验,就想购入一个更轻量的录像装备。在调研了 GoPro 、Insta360 和 DJI 的其它几款产品后,我最后选了这款没什么运动细胞、中规中矩的 Pocket 2 。除了价格不便宜、配件更贵外,整体能力很均衡,当然光线不佳时还是要用 iPhone 来补上。不过在去了一趟环球影城后因为封控的原因我就没再怎么出门游玩,希望明年可以让它发挥更大的价值吧。

(2)Xbox Series S

推荐指数:★★★★☆

本想买了它再搭配 XGP 订阅能让我重新拾起玩游戏的心情,不曾想没玩几个 XGP 里的游戏,玩的时间最长的 FIFA 22 还是单独购买的,可谓是陪了夫人又折兵。不过对于喜欢宅在家玩主机游戏而家里又只有 1080P 电视的用户来说,Series S 绝对是最高性价比的选择,配合 XGP 整体的费用要比 NS 生态低不少。

(3)米家胶囊咖啡机

推荐指数:★★★★★

没想到 2022 年了,小米贴牌在这类几百块价位的产品中仍然是性价比之选。简单、好看、小巧,不需要复杂的清洗。既然都喝胶囊咖啡了我想也就没必要追求各种咖啡机的高级功能了。

(4)文石 Leaf 阅读器

推荐指数:★★★★★

文石产品现在的文字渲染效果已经是一线水准了,今年用它搭配微信读书粗读了不少书。而作为曾经的 Kindle 用户,Android 系统之方便让我不禁怀疑此前我的 Kindle 吃灰是不是就是源自它封闭的生态而非我的问题(显然不是)。

(5)Beats Fit Pro

推荐指数:★★★☆☆

可能是本年度最让我又爱又恨的一次购物选择。年初的时候,我手中那款做工极差的初代 AirPods Pro 又出现了各种奇怪的小毛病,忍无可忍的我在送修的同时,决定不再等待下半年大概率会出现的新款 AirPods Pro ,转投当时刚发布不久的 Beats Fit Pro 。说爱,是因为这款耳机完全没有 AirPods Pro 那些恼人的如单耳突然没声音、充不进电等问题,立体空间声等 AirPods Pro 有的功能它也都有,降噪能力我觉得也不输当时的同代产品。恨的是其通话能力很拉胯,那羸弱的麦克需要我保持静止才能较好地收音,再加上 Beats 产品发售即价格跳水的传统艺能,我很快就觉得它不香了。2023 年的各位如果想买新的入耳式降噪耳机,那还是考虑 AirPods Pro 2 吧。

(6)飞利浦电动牙刷 HX6806

推荐指数:★★★☆☆

买来替代我满身水渍的欧乐B牙刷,因为是便宜版本所以整体表现中规中矩吧。声音比欧乐B的安静不少,电量要更持久些,也没出现当年欧乐B把我牙龈刷出血的情况,无功无过的一次购物选择。分别使用过这两家的中低端产品后,我对于电动牙刷的购买建议是买个便宜的、电量持久的就行了,刷牙这件事情更重要的还是每天都刷且刷够时间。

2、软件与订阅

在统计了我每年的软件和订阅开销上万后,去年的 flomo/秘塔写作猫/Bear/DigitalOcean Spaces 等产品的订阅都被我停掉了,替换为了免费产品或与其它平台型产品合并。这是一个开源节流的痛苦过程,但也是一个重新审视我真实软件需求的好机会。最后截至年末幸存的软件和服务清单如下。

服务 推荐指数 总结
Apple Music 美区 ★★★★☆ 继续订阅的一年,只要 Spotify 不支持 Homepod 一天,我大概率就会一直订阅
Netflix ★★☆☆☆ 今年继续涨价+烂片更多,由于不怎么在电脑和 iPad 上看它了所以降级至了 1080P 版本,也算是省了点钱
Pocket Casts Plus ★★★☆☆ 继续订阅的一年,也继续没什么大的迭代。不过这家伙 23 年就要从 $9.99 涨价到 $14.99 了,加价不加量着实有些夸张,新的一年大概率是要被换掉了
iCloud 2TB ★★★★★ 今年拍猫的 4K 视频过多,以至于 200GB 版本已经不够用了,不得已只能升级到了 2TB 版本。好在 iCloud 的域名邮箱现在也勉强能用了,省了一笔 Google Workspace 的钱,正负得零,还算可以接受
Setapp ★★★☆☆ 这一年 Setapp 软件列表的扩充还是挺给力的,所以评价由去年的两星变为今年的三星。不过之前的优惠折扣结束了只能原价购买了,钱包有一点受伤
Grammarly ★★★☆☆ 因为几次偶然的正式英文使用场景头脑一热就订阅了一年。虽然功能还是挺好用的,但现在再看实在太冲动了,$12/month 的价格完全被我的低频使用浪费了,我想大部分在国内做代码开发工作的人可能都不需要订阅这款产品
TickTick ★★★★☆ 订阅了滴答清单的国际版用以替换 Microsoft To Do ,收集箱和四象限等接地气的功能大大缓解了我因堆积任务而产生的焦虑感,年末时我还用它替代了一众稍后读工具和随手记类型的笔记工具,这钱花得挺值
Craft ★★★☆☆ 受不了 Notion 的渲染和加载速度,再加上 Pro 版本支持使用 iCloud 存储才决定订阅这款 native 实现的笔记工具。然而重度使用后发现其对 CJK 的支持存在一些隐蔽的 Bug ,到了 Q4 加入 Setapp 的操作着实让我感觉交了两份钱。可能是本年度仅次于 Grammarly 的不值得订阅了
腾讯云香港轻量 VPS ★★★★★ 良心云今年风评不太良心,不过倒尚未影响我的使用,今年继续订阅
搬瓦工 SoftBank VPS ★★★☆☆ 今年莫名其妙被墙了一次,明年可能会改用中继方案而不再订阅了
Jetbrains 全家桶 ★★★☆☆ 去年忘记写这个收费大户了,今年依然继续订阅,不过新版本的卡顿问题比之前更严重了,评分一般

有什么思考

  1. 高增长的 C 端产品千篇一律,而体验好的 B 端产品却是各有千秋。
  2. 要注重发挥长处的优势,而不是急于补全短板。于人于己都是如此。
  3. 绝对化的结果导向大概率会导致南辕北辙的执行。
  4. 润为上计,躺为中计,卷为下下策。躺最难,卷最累,润没那么难。
  5. 外行领导内行是常态,草台班子你我他。

明年怎么看

复制粘贴下上一年的个人 OKR ,把标题改成 23 年。内容变化不大,明年继续加油。

把Notion变为个人网站

2022-08-15 12:43:11

「The all-in-one workspace」是 Notion 的自称,它肯定不是最优秀的笔记软件,但一定是当今最强大的内容生产工具之一。除了可玩性很高的数据库功能,Notion 还支持将页面对外公开,即可以通过简单地启用分享来获取到可在互联网上直接访问的链接。不过免费版不支持为被分享页面配置 SEO ,而且即使是付费版也尚未支持自定义域名。

如果想建立一个可高度自定义的个人网站,或希望把 Notion 的页面作为已有网站的附加内容,那就还是要基于 Notion 建立一个拥有独立域名、支持自定义设置的个人网站。本文就来聊聊基于 Notion 的个人网站有几样建法。

建站方案

基于SaaS产品建站

Notion 如今已经形成了生态,既然官方没提供完整的自定义个人网站方式,那各种 SaaS 产品就会补足市场的需求。当前已经出现了很多这类产品,通常是免费版提供另一个二级域名和自定义主题功能,付费订阅版才有自定义域名、支持 SEO 等高级功能。

这些产品的界面都很简洁,操作也简单易懂。以 Popsy 为例,选择新建站点:

创建 Notion 站点

最后填入站点名称和 Notion 页面的分享链接即可:

完成初始化配置

站点新建完成后,我们可以在左侧的导航栏里完成自定义设置,最后点击 Publish 完成发布即可。不过 Popsy 没有免费套餐,需要先在 Stripe 完成支付配置才可以试用 7 天。

SaaS 类产品的操作确实非常简单,功能上也可以满足用户大部分的自定义需求,很适合非技术人员或不愿意折腾的用户。唯一的缺点只有一个字,那就是「贵」,订阅价格通常在每月 $8~12 不等。贴几张不同产品价格功能表的截图,大家感受下:

Popsy 价格功能表

Simple.ink 价格功能表

Potion 价格功能表

基于FaaS产品建站

除了付费产品,在流量不大时我们还可以借助 FaaS 产品的免费额度来实现建站。目前比较常见的思路分为 Cloudflare Workers 和 Vercel 两类,希望快速可用建议选 Workers ,打算通过开发来实现自定义的则可以使用 Vercel 方案。

基于Cloudflare Workers建站

这个方案的原理是通过 Workers 的转发和响应重写功能对 Notion 进行代理。由于这套逻辑大同小异,所以有网友开发了 Workers 代码的生成器。可视化界面的简单配置即可生成对应的代码,再拷贝到 Cloudflare Workers 中执行即可。

以最为著名的 Fruition 为例,我们首先完成 Step0 和 Step1 中的域名注册、Cloudflare 注册和 DNS 配置,随后在 Step2 中填写好期望的域名和 Notion 页面的分享链接,最后把生成的代码拷贝到新建的 Cloudflare Workers 函数中,再将该域名的路径设置为 Worker 的路由即可。官网的教程非常详细,按指导一步步执行即可。在 Step3 中我们可以做很多自定义工作,而由于该 Worker 的实现主要依赖重写响应,因此我们也可以对生成的代码进行深度修改以满足各种自定义需求。

不过我们从 Fruition 官网教程的截图也能看到,所使用的 Cloudflare 截图已经是改版前的页面了。无独有偶,该教程的细节也存在一些需要与时俱进的调整。在 Step1 的第 4 小节,教程中要求为目标域名添加 A 记录并将 IP 指向 1.1.1.1

If you don’t have any A records imported, add one with your root domain as the Name and 1.1.1.1 as the Content. Otherwise, click Continue on the DNS Record page.

但实际上如果你真的这样配置,那在当前的 Cloudflare 机制下会出现网页无法打开的问题。原因在于现在 Cloudflare 当前已经禁止了这种 DNS 配置方式,对应的 Workers 需要使用保留 IP 地址(Reserved address)比如 192.0.2.0 ,CIDR 为 192.0.2.0/24 ,这其后的技术细节以后可以展开讲讲。

其他基于 Cloudflare Workers 的方案和 Fruition 类似,区别只在于 Worker 中代码实现思路的不同。Cloudflare 作为良心企业,免费版 Worker 每天有 100k 请求的额度,对于绝大部分的基于 Notion 的个人网站来说是完全足够的。即使真的超过了额度,多加 $5 就可以多获得 100 倍的可用请求量,不可谓不划算。

缺点在于网上比较流行的几种 Worker 实现对 SEO 的支持都不算友好,也没有直接支持评论等功能,需要做比较多的二次开发才能满足较为复杂的需求。同时近年来 Cloudflare 的国际 CDN 在国内的访问速度越来越慢,有的地区甚至还会间歇性地无法访问,因此国内用户的使用体验可能会不太好。

基于Vercel建站

既然是折腾和前端相关的东西,怎么能少了 Vercel 呢?比较著名的 Vercel 实现为 ijjk/notion-blog 项目。如果你熟悉 Vercel ,那基础配置比 Cloudflare Workers 方案要简单得多,只要找到 NOTION_TOKENBLOG_INDEX_ID 两个值(可以参考文档)就可以一键启动,然后再把实例绑定到期望的域名即可。

其实现机制和上文的 Cloudflare 方案差异较大,本质上是一个以 Notion 为数据库(直接访问 Notion 的内部 API )的基于 Next.js 的动态博客系统,这也是为什么该项目会要求相关 Notion 页面具备一个类似于其他博客系统的 front matter 配置即 table 用于记录元数据的原因。

虽然部署方式更简单了,但如果想做调整那就要对原项目进行修改。所以整体的自定义复杂度其实还是挺高的,更适合对前端项目有一定积累的用户使用。另外 Vercel 在国内的访问速度没比 Cloudflare 好多少,所以对于国内用户来说同样也可能会有体验问题。

自助反向代理建站

如果手上已经有了一台 VPS 之类的机器,那我们还可以选择通过反向代理 Notion 的方式来复用已有机器自助建站。

以 Nginx 为例,我们首先要保证 Nginx 已经启用了 ngx_http_sub_module 模块,这个模块不一定是默认开启的,有可能会需要手动编译。该模块的主要作用为按配置修改代理后的响应内容。这一点至关重要,不然我们无法解决 Notion 原始响应中各类固定域名和响应字段,进而也就无法完成反向代理。

下面给出仅考虑可正常加载页面的最简单情况下 Nginx location 的配置,并对重点指令进行解释,自定义域名使用 example.foo 指代:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    location / {
        # 由于 Notion 页面经常比较大,默认 buffer 配置容易出现 502 的情况,所以需要调大 buffer ,具体大小可以按需选择
        proxy_buffers 8 32k;
        proxy_buffer_size 64k;
        
        # rewrite 至共享空间的首页页面,以处理根路径的访问
        rewrite ^/$ https://example.foo/Foo-b1324470dd5d33d04;

        # 将流量代理至 Notion ,网上有很多实现的代理目标是 notion.so ,但 Notion 的域名规则已经发生变化,改为了 {用户名}.notion.site ,故此处也要进行修正,下同
        proxy_pass https://example.notion.site;

        # 设置一系列的代理请求头,保证对于 Notion 来说请求仍可信且可处理
        proxy_set_header Host example.notion.site;
        proxy_set_header Referer https://example.notion.site/;
        proxy_set_header User-Agent $http_user_agent;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Accept-Encoding "";
        proxy_set_header Accept-Language $http_accept_language;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 将响应中的 Notion 域名替换为自定义域名,以保证 XHR 等请求也能被正确地反向代理
        sub_filter 'https://www.notion.so' 'https://example.foo';
        sub_filter 'https://example.notion.site' 'https://example.foo';
        
        # 上述逻辑会导致 Notion 前端代码将 requestedOnPublicDomain 判断为 true 并携带此参数向 Notion 后端请求是否需要登录,最终导致强跳至登录页。由于 Notion 前端实现比较复杂,这里简单处理为对 Notion 后端返回的需要登录字段强制覆盖
        sub_filter '"requireLogin":true' '"requireLogin":false';
        
        # 上述替换涉及到了 application/javascript 和 application/json ,但实际上有些 text/plain 响应也包含了需要替换的内容。所以这里设置为替换所有 MIME 类型的响应
        sub_filter_types *;
        
        # 为 off 时表示需要扫描原始响应中的所有内容进行替换,而不是默认的替换一次即停止
        sub_filter_once off;

        # 反向代理时启用 SNI ,避免 TLS 握手出现问题
        proxy_ssl_server_name on;
    }

这类自定义反向代理方案的好处在于能复用已用机器资源,同时如果机器的网络比较优秀,还可以加速对 Notion 的访问,解决了上面几种方案在国内的尴尬使用体验。缺点是过于依赖 Notion 的前后端设计,Notion 如果做了大改动,那实现方式就需要重新探索。其实 Cloudflare Workers 方案也有这个问题,不过作为活跃的开源项目,Fruition 等的社区支持还是比较给力的。

总结

笔记工具远没有持续输出重要。对于上述几个方案的选择,我的建议是没特殊需要就使用 Cloudflare Workers 实现,舍得花钱、想省事就选 SaaS 方案,愿意折腾、乐于折腾再考虑 Vercel 或 Nginx 方案。

再见Hexo——从Hexo迁移至Hugo

2022-08-10 01:36:15

16 年的时候,博客使用的虚拟主机需要做迁移,当时所使用的 Typecho 是一个依赖于数据库的 PHP 博客系统,数据导出过程很艰辛。彼时 Gihub Pages 正大火,我也就跟风转投了静态博客系统 Hexo 。七年过去了,博客还在,但折腾 Hexo 的人是越来越少了。如今我也要和 Hexo 说声再见,拥抱 Hugo 的怀抱了。

为什么改用Hugo

依赖管理问题

静态博客的同步和备份方案一文中,我分享了基于 Github 和 iCloud 的同步备份方案,期望在保证数据安全的同时,写作环境可以在我的两台电脑上无缝切换。

然而实际上,由于 Hexo 依赖于 Node.js ,庞大的 node_modules 并不能直接同步而是需要分别安装和更新,如果忘了运行 npm install -S ,那报错和渲染异常就是家常便饭的事。而 node-sass 这样的库还对 node 版本做了显式要求,导致如果哪台设备改了 nvm 中 node 的版本就会报错。但偏偏又有大量的主题和插件都依赖于 node-sass 。

而 Hugo 是基于 Golang 的二进制程序,安装和升级都很简单。由于内置功能足够多,插件(模块)不再是必需的了,如果有需要,其也都是通过 go mod 管理,轻量而简洁。虽然大部分主题仍然依赖于 Node.js ,但那只是创建和修改主题时的事情,不会影响到写作流程。

诚然,这并不能算作 Hexo 本身的问题,但由于底层的技术选型,导致 Hexo 必然和各类主题及插件的耦合较为严重。在三方组件实现和依赖复杂的情况下,整体的复杂度也就必然会成倍地上涨。

网页生成速度

Hexo 裸安装后的网页生成速度并不算不可接受,和 Hugo 比起来也就几秒到十几秒的差异,没有网上传得那么夸张。但多加了几个像 hexo-all-minifier 这样生命周期靠后的插件后,生成速度确实会肉眼可见地下降。而 Hugo 的网页生成速度则非常稳定,总是保持在秒级别甚至毫秒级别,因此也可以真正意义上地实现本地实时预览。

Hugo的特性

Hugo 对 org-mode 、pandoc 等提供了原生支持,轻度使用体验也不错(深度使用也会遇到坑)。虽然 Hexo 等也可以通过安装插件和转换器等方式来实现,但这又会回到上面的依赖复杂的问题之中。此外,Hugo 的 shortcode 功能也非常强大,如果不考虑 md 文件的通用性,那结合 shortcodes 可以轻松实现很多本需要依赖于插件(模块)才能做到的功能。

快速上手

首先安装 Hugo 并创建站点,以 macOS 系统为例:

1
2
brew install hugo # For macOS
hugo new site sample

这里需要注意的是,如果此前系统中已经安装过旧版本的 Go ,那有可能需要升级后才能使用 homebrew 完成安装。随后选一个喜欢的主题拉取到 /themes 目录,以 even 主题为例:

1
git clone https://github.com/olOwOlo/hugo-theme-even themes/even

主题通常会带有示例配置文件 config.toml ,将其复制到站点的根目录下覆盖默认配置文件,并完成相应配置后,运行 hugo new post/test.md 新建文章,随后运行 hugo server -D 即可查看站点。对于存量的文章,需要将其复制到主题 /content/ 目录下,其中文章类的需要按主题的设计来放置于具体目录,如 even 主题使用 post 目录,则需要将文章复制到 /content/posts 目录下,重新执行上面的命令就能在站点里看到文章了。

如果运行命令时报错,则可能是存量文章的 front matter 格式有不符合 Hugo 要求的情况,此时需要按官方文档进行修改适配。

写作习惯的变化

整体写作习惯其实和 Hexo 的体验差别不大。不过 Hugo 提供了比 Hexo 更丰富的 front matter 默认配置,同时还支持 org-mode 等玩法,所以写作方式上的可玩性会更高些。

主题自定义方式的变化

与 Hexo 主题的完全前端实现不同,Hugo 的主题使用了 Go 的模板语言,有点类似于 PHP 和 JSP ,并向主题暴露了一系列的全局变量和函数,所以其实主题和 Hugo 或 Golang 还是有一定的耦合的。

不过这也使得我们可以通过自定义模板覆盖主题默认模板的方式,来既实现自己的需求,又能最低限度地避免修改主题源代码导致的升级困难。但很多主题没有提供关键位置的钩子模板(也可以说是接口),导致我们经常需要拷贝一部分主题的源代码到自定义模板中,这又对主题的升级造成了一定的影响,可以说是有得必有失了。

向前兼容

迁移工作的一个核心要求就是尽量避免引入 breaking change 。首先要保证存量页面的链接不发生变化,以避免出现 404 的情况。其次要尽量对此前在 Hexo 中使用的各项功能进行支持。

永久链接格式兼容

在 Hexo 中,通常有以下三种 URL 永久链接路径格式:

  1. 日期前缀+英文别称或文件名/2021/07/06/a-better-hexo-theme-even/
  2. 固定前缀+英文别称或文件名/posts/slidev-tutorial/
  3. 使用了 abbrlink 等插件生成 CRC/Hash 值作为路径/posts/8ccq01298/

而 Hugo 默认的永久链接格式为 /{{ 文章目录 }}/{{ 文章文件名 }} ,和上面第二种比较相似但又有所不同。那么我们该如何实现兼容呢?

首先我们要了解 Hugo 可以在根目录的 config.toml 中对永久链接进行自定义配置,例如:

1
2
[permalinks]
  '/' = "/posts/:slug"

因此我们只需要针对不同情况,对该配置进行自定义即可。

  1. 日期前缀+英文别称或文件名:仿照 Hexo 的日期格式,将值配置为 /:year/:month/:day/:title/
  2. 固定前缀+英文别称或文件名:将值改为 /posts/:title 即可。 Hugo front matter 中的 slug 变量表示自定义别名,所以如果此前在 Hexo 使用了自定义的变量,只要仿照此前的配置将 title 改为 slug 即可,例如 /posts/:title
  3. 使用 abbrlink 等插件生成的 CRC/Hash 值作为路径:Hugo 似乎没有 abbrlink 这类插件,不过我们可以仿照这篇文章,在默认内容模板 archetypes/default.md ,再将其中的 slug 配置为一段具备哈希或 CRC 功能的表达式即可。不过存量文章可能需要通过 front-matter 中的 url 变量进行完整路径的显式声明,不然如果表达式的处理结果和 Hexo 中的不同,那链接可就变了。

上面说明了 permalinks 的值,那 key 该如何配置呢?与 Hexo 不同,Hugo 中永久链接的固定前缀(对应上文的情况 2 和情况 3)是根据目录位置生成的,该位置的选择又与主题有关。有的主题使用的是 /content/post/ 目录,有的主题使用的又是 /content/posts 目录,同时这个路径在很多主题的实现中是写死的。因此如果此前你在 Hexo 中所使用的固定前缀和所选 Hugo 主题的不同(如 /articles/),那就会造成链接发生变化的问题。

所以我们需要在 permalinks 配置的 key 上做些文章,把主题所用的路径做一层指定映射,保证最终的路径以我们期望的前缀输出。以主题使用 /content/post/ 作为内容目录、原 Hexo 文章的永久链接格式为 /posts/:slug 为例,对 permalinks 进行以下配置即可:

1
2
[permalinks]
  post = "/posts/:slug"

另外,对于此前在 Hexo 中配置了 html 后缀等情况,可以开启 Hugo 的 Ugly URLs 来实现兼容,细节可以参考官方文档

归档页面路径链接的兼容

前一节提到了主题对内容目录路径的选择可能是不同的,而这也会影响到归档页面的路径。在 Hugo 的大部分主题中,如果主题使用 /content/post/ 作为内容目录,那归档页面路径则默认为 /post/ 且不支持配置。而我们在 Hexo 中通常会使用 /archives/ 作为归档页面的路径,如何才能保持不变呢?

虽然绝大部分主题(也可能是所有)都没有此项配置,但我们可以通过自定义一套模板和页面的方式来绕过主题的限制。以 even 主题的模板和 CSS 样式为例,在 /layouts/_default/ 目录下新建 archives.html 模板,随后填充以下内容,用于按年分组遍历所有文章,并在原主题的框架下输出文章标题列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{{- define "title" }}{{ T "archive" }} - {{ .Site.Title }}{{ end -}}

{{- define "content" }}

{{ $pageList := (where .Site.RegularPages "Type" "post") }}

<section id="archive" class="archive">
  {{- if .Site.Params.showArchiveCount }}
    <div class="archive-title">
      <span class="archive-post-counter">
        {{ T "archiveCounter" (len $pageList) }}
      </span>
    </div>
  {{- end -}}

  {{ range ($pageList.GroupByDate "2006") }}
    <div class="collection-title">
      <h2 class="archive-year">{{ .Key }}</h2>
    </div>

    <ul class="archive-list">
      {{ range (where .Pages "Type" "post") }}
        <div class="archive-post">
          <span class="archive-post-time">
            {{ .PublishDate.Format "01-02" }}
          </span>
          <span class="archive-post-title">
            <a href="{{ .RelPermalink }}" class="archive-post-link">
              {{ .Title }}
            </a>
          </span>
        </div>
      {{ end }}
    </ul>
  {{ end }}
</section>
{{ end }}

随后在 /content/ 目录下新建 archives.md 页面,将 type 指定为刚刚定义的 archives

1
2
3
4
5
6
7
8
---
title: "归档"
layout: "archives"
url: "/archives/"
comment: false
hidden: true
type: archives
---

这时我们就已经可以通过访问 /archives/ 路径来进入到归档页面了,接下来只要在 config.toml 中再将导航栏中的对应按钮指定为预期链接即可。

1
2
3
4
5
[[menu.main]]
  name = "归档"
  weight = 20
  identifier = "archives"
  url = "/archives/"

上文这种实现的效果和很多主题的归档页面相比,主要区别在于单页面内罗列了所有文章,即缺少分页。由于大部分主题的分页逻辑和其内部的其他模板耦合较为严重,同时 Hugo 的分页相关变量被限制不能用于自定义模板之中,所以如果希望自定义的归档页能支持分类,则可能需要对 Hugo 的原生逻辑进行包装即额外实现一套分页能力才行。这里不做展开讲述。

友情链接和自我介绍

和 Hexo 一样,Hugo 也没有直接支持友情链接和自我介绍这类常用页面。在实现上我们要么在 /content/ 目录下自定义页面也就是在页面内维护内容,要么如归档页面一般,通过自定义模板的方式来加载 config.toml 中的配置。两种实现都比较简单,我也更倾向于前者,毕竟这些是低频修改页面,是否可配置区别都不大。

标签和分类的中英文问题

在 Hexo 中,我们通常会在 _config.yml 中配置标签和分类的中英文映射,这样我们在 front matter 中可以使用任意语言标识标签和分类,但生成后两者的 URI 都是英文。然而在 Hugo 中却没有这类简易设置,也许我们可以通过修改主题和永久链接的方式来间接支持,但估计成本较高。所以如果对 URI 有强迫症的读者,还是建议把存量文章的标签和分类改为英文。而如果对此没有特殊需求,那使用中文也可。除了 URL 的分享可读性可能较差外,在 2022 年的今天其实已经不会影响搜索引擎的 SEO 效果了。

支持Git与VPS部署

不知为何 Hugo 官方没有直接支持使用 Git 搭配 Git Hooks 部署站点,对于我这种把博客部署在 VPS 的用户给出的建议方案是 rsync 。其实 rsync 方案是完全可行且成本不高的,不过本着尽量兼容的原则我还是决定在部署时执行以下 shell 脚本来通过 Git 推送生成的 /public/ 目录至 VPS ,而 VPS 上的 Git 库和 Git Hooks 配置则无需改动:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#! /bin/bash

rm -rf ./public
hugo

rm -rf ./.deploy_git
mkdir .deploy_git
cp -r public/* ./.deploy_git
cd .deploy_git

git init --initial-branch=master
git add -A
git commit -m 'Deploy commit.' --quiet
git push -u foo@bar-server:/var/blog.git HEAD:master --force

cd ..
rm -rf ./.deploy_git

兼容Hexo的RSS形式

使用 Hexo 时博客的 RSS 是全文输出,而换到 Hugo 后 RSS 却变为了输出摘要。作为一个重度 RSS 用户,我自然是深知拉取到的文章还要二次跳转到浏览器才能看原文的体验有多差,所以还是要让 RSS 的表现和此前一致才行。

Hugo 的 RSS 是基于默认 RSS 模板生成的,所以我们只要重新定义一个模板并改为全文输出即可。Hugo 的默认实现中,决定输出内容的是如下这行代码:

1
      <description>{{ .Summary | html }}</description>

我们只需要在 /layouts/ 目录新建 index.rss.xml 覆盖默认模版并将原实现拷贝至其中,接着把代码中的 {{ .Summary | html }} 替换为代表全文内容的表达式 {{ .Content | html }} 即可:

1
      <description>{{ .Content | html }}</description>

even主题迁移

由于此前一直在使用修改过的 Hexo even 主题,为保证前端效果不变,所以主题方面也采用了 Hugo 下的 even 主题,因此会有很多主题层面的额外适配工作。

自定义导航栏

此前使用 Hexo 下 even 主题时自定义了一个用于引导用户的导航栏,那么如何在 Hugo 的 even 主题中实现兼容呢?

首先我们在 /layouts/partials/header/ 下新建 top-nav.html 模板,按需填充内容,如增加 Newsletter 、Telegram Channel 等引导:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{{- if .Site.Params.enableTopNav }}
  <div class="top-nav">
    {{- if .Site.Params.revue.enabled -}}
      <a href="{{ .Site.Params.revue.home }}" href="_blank" class="top-nav-button">Newsletter</a>
    {{- end -}}
    {{- if .Site.Params.telegram.enabled -}}
      <a href="{{ .Site.Params.telegram.link }}" href="_blank" class="top-nav-button">电报频道</a>
    {{- end -}}
    {{- if .Site.Params.wxOfficialAccount.enabled -}}
      <a href="{{ .Site.Params.wxOfficialAccount.url }}" href="_blank" class="top-nav-button">微信公众号</a>
    {{- end -}}
  </div>
{{- end -}}

随后我们需要找个位置引入该模板。受 even 主题实现的限制,我们需要将该模板放置于 header 块之后才能最低成本地保留原布局。因此我们只有一个选择,那就是将 baseof.html 这个基础模板进行覆盖。拷贝原模板内容至 /layouts/_default/baseof.html 中,并在 header 块之后、main 块之前引入此前定义的 top-nav.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
...
  <div class="container" id="mobile-panel">
    {{ if not .Params.hideHeaderAndFooter -}}
    <header id="header" class="header">
        {{ partial "header.html" . }}
    </header>
    {{- end }}
  
    {{- partial "header/top-nav.html" . -}}

    <main id="main" class="main">
...

最后在 config.toml 中完成相关参数配置即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  [params.wxOfficialAccount]
    enabled = true
    url = ""

  [params.telegram]
    enabled = true
    link = ""
    
  [params.revue]
    enabled = true
    home = ""

支持umami访问统计

博客之前一直按「使用Nginx将请求转发至Google Analytics实现后端统计」一文的方式来实现请求统计。但这个方式的问题在于,由于不要求加载 JS ,很多非真实流量(主要为 RSS 阅读器的抓取)也会被统计进来。后来看到「搭建 umami 收集个人网站统计数据」这篇文章,便也用 umami 搭建了一个轻量的统计能力。

even 主题自然没有对 umami 进行原生支持,我们需要做的是先找到一个包含 head 的模版并在 <head/> 标签中添加以下内容:

1
2
3
  {{- if (in (slice (getenv "HUGO_ENV") hugo.Environment) "production") | and .Site.Params.umami.enabled -}}
    <script async defer data-website-id="{{ .Site.Params.umami.id }}" src="{{ .Site.Params.umami.js }}"></script>
  {{- end -}}

然后在配置中完成定义即可:

1
2
3
4
  [params.umami]
    enabled = true
    id = "" # umami 统计 id
    js = "" # umami 的 JS 地址

由于 even 主题没有提供可以直接拓展 <head/> 标签的模板,我的选择是将代码加到此前不得不重写的 baseof.html 中。不得不说,这个实现很丑陋,但成本确实也是最低的。

此外,为了避免本地启动时 umami 将本地请求也进行了统计并将 Referrer 识别为 localhost ,上文的实现中对环境做了判断,即正式生成站点时才会引入 JS 依赖来上报 umami ,本地运行则不引入。

自定义文章末尾页脚

此前在 Hexo 的 even 下我也对文章末尾进行了自定义。对于 Hugo 的 even 主题,改造成本最低的方式为重写 /layouts/partials/post/copyright.html 模板。

首先要和此前展现形式对齐的是「原文链接」。even 主题本身只支持将 Markdown 原文件地址作为文章链接,所以我们需要在该模板中仿照 lionToMarkDown 部分添加以下内容:

1
2
3
4
5
6
  {{ if $.Site.Params.copyrightLink -}}
    <p class="copyright-item">
      <span class="item-title">文章链接</span>
      <span class="item-content"><a class="link-to-markdown" href="{{ .Permalink }}" target="_blank">{{ .Permalink }}</a></span>
    </p>
  {{- end }}

因为暂时没有国际化需要所以文案是固定的中文,如果想更灵活些也可以仿照原实现中的 Markdown link 来做 i18n 。

随后只要在 copyright.html 的最后引入我们自定义的文末模板即可:

1
{{- partial "post/post-footer.html" . -}}

utterances适配

even 主题本身是支持 utterances 的,但用于生成 issue 的唯一标识参数被主题写死为了 issue-term="pathname" 即根据 URI 路径生成,并没有暴露配置。而我在使用 Hexo 时该参数的值是 issueTerm="title" 即根据文章标题生成,不进行适配的话会丢失存量评论。

所以我们需要在 /layouts/partials/ 目录下新建 comments.html 覆盖主题原实现。顺便地,我们可以把另一个参数 label 也改为可配置的,这样一来,生成的 Github issues 便可以自动加上 utterances 标签方便分类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{{ if and .IsPage (ne .Params.comment false) -}}

  <!-- utterances -->
  {{- if .Site.Params.utterances.owner}}
    <script src="https://utteranc.es/client.js"
            repo="{{ .Site.Params.utterances.owner }}/{{ .Site.Params.utterances.repo }}"
            issue-term="{{ .Site.Params.utterances.issueTerm }}"
            label="{{ .Site.Params.utterances.label }}"
            theme="github-light"
            crossorigin="anonymous"
            async>
    </script>
    <noscript>Please enable JavaScript to view the <a href="https://github.com/utterance">comments powered by utterances.</a></noscript>
  {{- end }}

{{- end }}

随后我们便可以在 config.toml 中对 utterances 按需进行配置:

1
2
3
4
5
  [params.utterances]       # https://utteranc.es/
    owner = ""              # Your GitHub ID
    repo = ""               # The repo to store comments
    issueTerm = "title"     # 新增配置,可按需选择 issue 生成时的唯一标识方式
    label = "utterances"    # 新增配置,可按需指定 issue label

补齐底部社交图标

主题的社交图标使用的是托管于 iconfont 的私有实现所以直接拓展未支持的新图标较为困难。我在记hexo-theme-even主题优化一文中提到了相同的问题,文中最终选择了使用 Font Awesome 来解决,对于 Hugo 的 even 主题我们也如法炮制进行处理。

首先,在 Font Awesome 官网下载依赖并放置于 /static 目录下。例如我使用的是引入所有图标 JS 的方式,则最终路径为 /static/js/fontawesome.all.min.js 。然后在 config.toml 配置中引入该 JS 文件:

1
2
[params]
  customJS = ["fontawesome.all.min.js"]

接着,我们在 /layouts/partials/ 目录下新建 footer.html 覆盖主题原实现并保留原实现的其他代码,只对 social-links 部分进行如下修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<div class="social-links">
  {{- range $name, $config := .Site.Params.social }}
    {{- if $config.path }}
      <a href="{{ $config.path | safeURL }}" class="iconfont" title="{{ $name }}"><i class="{{ $config.icon }}"></i></a>
    {{- end }}
  {{- end }}
  {{ if .Site.LanguagePrefix -}}
    <a href="{{ .Site.LanguagePrefix | absURL }}/index.xml" type="application/rss+xml" class="iconfont" title="rss"><i class="fas fa-rss"></i></a>
  {{- else -}}
    <a href="{{ .Site.RSSLink }}" type="application/rss+xml" class="iconfont" title="rss"><i class="fas fa-rss"></i></a>
  {{- end }}
</div>

最后在 config.toml 中添加需要的图标配置即可。icon 即图标的完整 class 属性,path 即需要跳转的链接地址。需要注意的是,主题的原逻辑为了实现多语言,将 RSS 图标的逻辑隔离在了通用逻辑之外。这里也保留了原实现,即 RSS 图标是默认出现且不可去除的。如果不需要 RSS 则可以对上面的代码再进行修改,以删除独立的 RSS 逻辑。

1
2
3
4
5
6
7
  [params.social]
    a-email = { title = "Email", icon = "fas fa-envelope", path = "" }
    b-twitter = { title = "Twitter", icon = "fab fa-twitter", path = "" }
    c-github = { title = "Github", icon = "fab fa-github", path = "" }
    d-weixinOfficialAccount = { title = "微信公众号", icon = "fab fa-weixin", path = "" }
    e-telegram = { title = "Telegram", icon = "fab fa-telegram", path = "" }
    f-search = { title = "Search", icon = "fas fa-search", path = "" }

除了 Font Awesome ,最近我还看到了tabler ICONS这个库,直接支持 SVG 同时还是 MIT 协议的开源项目,也值得一试。

此外,由于我们已经覆盖了 footer 模板,那我们也可以对其他内容也进行自定义,比如将友情链接放置于 footer 等,下文的总字数统计也同样均基于自定义的 footer.html 进行处理。

支持总字数统计

even 主题自带每篇文章的字数和预计阅读时间统计,但却没有之前我借助 hexo-wordcount 所实现的全站文章字数统计。检索网络后找到了这么一篇文章 Hugo 总文章数和总字数 ,照猫画虎在 footer.html 中添加以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  {{ if .Site.Params.countAllWords.enabled }}
    {{$scratch := newScratch}}
    {{ range (where .Site.Pages "Kind" "page" )}}
    {{$scratch.Add "total" .WordCount}}
    {{ end }}

    <span style="display: block;">
      {{ .Site.Params.countAllWords.prefix }} {{$scratch.Get "total" }} {{ .Site.Params.countAllWords.suffix }}
    </span>
  {{ end }}

随后在 config.toml 中对相关参数进行配置即可:

1
2
3
4
  [params.countAllWords]
    enabled = true
    prefix = "共计"
    suffix = "字"

除了以上逻辑,还可以通过改变 range 的查询范围来按需限定需要进行总字数统计的页面集合。另外配置中的固定文案比较生硬,可以考虑加入 i18n 相关实现来满足多语言切换的需要。

总结

天下武功,唯快不破,Hugo 的速度确实让我印象深刻。但对于从 Hexo 迁移而来,同时还对 Hexo 有很多自定义配置的用户来说,迁移过程中的兼容和适配的成本其实是不低的,实际上目前的迁移仍未实现「基于 LeanCloud 的阅读计数」和「推荐阅读」两项功能的兼容。此外,无论是 Hexo 还是 Hugo ,其主题的深度自定义修改都比较麻烦,以后还是要考虑自己实现一套主题(明年一定)。

旁路由的原理与配置一文通

2022-08-02 12:18:28

最早听到旁路由这个词是在 2020 年折腾 N1 的时候,这台单网口的小盒子只能用网上所说的旁路由方案接入局域网来实现期望的功能。现在回想起来,旁路由这个词有可能就是在那个发烧友大量折腾斐讯 N1/P1/T1 的时期被发明出来的。你没办法在发烧友圈子外的互联网及各种学术材料中找到对旁路由的描述和定义,当然也找不到合适的英文翻译(导致这篇文章的 slug 定义困难);从拓扑上看,旁路由更像是杂糅了二级路由和透明网关的概念,除了实体确实多接了根网线放在主路由旁边,其本身并没有真正地开启一条旁路,做的事情也基本可以和网关的定义对齐。

不过,由于这个说法主要集中于网友的交流讨论之中,而且几年来在有相关需求的广大用户中被广为接受,所以只要这个词不变为一个学术概念,那我倒也不觉得有什么不妥。下文也仍然会用「旁路由」一词来指代在此类拓扑结构中担任代理网关角色的路由器。

此外,本文对于原理的解释偏多且为新手向,对于已经熟知相关概念的读者则可以将本文作为 cheat sheet 食用。

旁路由的适用场景

有人认为旁路由的加入(手动指定方式)不会在其出现问题时影响整个网络的可用性,所以应该把非路由功能都交给旁路由来实现。但实际上如果仅仅是为了达成这个目标,那在主路由上直接分流会是个更好的方案,因为这样不仅会大量减少疑难杂症的出现,而且还可以通过设置 fallback 的方式进一步提升网络的可用性。

所以我认为旁路由实际上只适用于一个场景,那就是出于某种考虑,主路由不能被替换或被大量修改,而主路由的固件又不能满足功能诉求,这时候旁路由便是一个可选的解决方案。

旁路由的原理

网上关于旁路由的配置教程多如牛毛,其中大部分是基于 F 大的 N1 OpenWrt 固件使用教程来写的,也有很多是结合了自己的踩坑经验的细化版本。由于版本太多、完整的说明太少,而且大部分没有讲清楚教程的环境上下文和原理,导致按照这些教程来配置的用户往往不得不一遍遍地折腾重试,甚至会遇到逐步配置最终却断网、访问不了部分网站等令人头疼的问题。

授之以鱼不如授之以渔,为了能彻底讲清楚旁路由该如何配置,我们暂且不谈具体步骤,而是先搞明白旁路由的运作原理。

名词解释

由于计算机网络的术语在不同时期、不同环境下,对于细节的含义其实有比较大的差异,故首先我们先定义好下文中将使用到的各类专有名词的含义,以免出现信息不对称的情况。同时为了方便理解,我们也尽量和 OpenWrt 的名词对齐,并尝试与现实生活场景进行类比。

  1. 路由:将数据从源地址传输到目的地址的行为。可以看出,这个行为抽象涵盖了两个动作,一是找到地址、二是转发数据。可以类比为网购时快递公司将商品从卖家发送给买家的过程。
  2. 路由表:可以简单理解为路由过程中的地址关联信息的合集,也就是现实中发货地址和收货地址的映射关系。
  3. 路由器:本文中指家用的、具备路由功能的实体网络设备,其本身并不一定需要真的承担或只承担路由功能,可以抽象理解为就是一台普通的电脑或服务器。当然,它也可以是一个容器实例、一个虚拟机。对应到现实中就是快递公司。
  4. 接口:这里特指网络接口,指两个网络设备或协议层的连接点。现实中连接路由器时,wan/lan 不同的网线插槽其实就可以理解为接口。只不过接口并不一定是物理实体,所以我们才会在 OpenWrt 的接口设置里看到甚至新增许多物理设备上所没有的接口。
  5. 网关:这里特指网络中的网关,负责执行数据转发的某个抽象设备。这里可能容易与路由器的功能混淆,毕竟路由器如果用来做路由似乎也是在做转发的工作。实际上这是由于术语的历史使用缺少规范导致的边界不够清晰,可以粗略理解为承担路由功能的路由器就是网关,但网关不一定是只能由路由器担任。
  6. IP:本身指 IP 协议,本文为方便也可能将其作为 IP 地址的简称,且默认为 IPv4 。本节所有的名词其实都是基于 IP 协议运作的,而 IP 地址即上文各类「地址」的实际值。可以理解为你用于发送和接收快递的门牌号。
  7. DHCP:IP 地址的管理和分配协议,本文中不单单指协议本身,还指负责执行该协议的设备。可以理解为给你分配门牌号的物业,只不过这个门牌号是动态变化的。
  8. DNS:上文说到数据的传输需要源地址和目的地址,而这个地址就是 IP 地址。但由于 IP 地址难以记忆,所以才会有了可以作为 IP 地址别名的域名,而 DNS 就是负责进行域名和 IP 地址映射转换的系统。本文中 DNS 也指代负责运行该系统的设备。
  9. NAT:出于各种考虑,局域网与因特网的 IP 地址是隔离的,NAT 可以理解为内外网 IP 地址转换的流程。类比网购,相当于快递公司把快递送到附近快递站,快递站的快递员再把货物送到你家门口的流程。
  10. SNAT:NAT 的一种,本质上是在修改网络包的源地址,目的是可以强制网络包返回时经过期望的地址。可以理解为支付宝的作用,即钱虽然表面上是点对点转账的,但经过支付宝后,支付宝就要求相关转账信息的回执必须由它中转一次再告知转账发起人。

网络拓扑

旁路由架构的网络拓扑图

可以看到,无论如何配置,我们都需要保证数据按图中的拓扑进行流转,才能实现我们所希望的只在旁路由增加功能而不修改主路由的目的。由于流量(至少上行流量)总会流经旁路由,所以旁路由实质上就是一层透明代理。

工作原理

那么我们该怎么实现这样的网络拓扑呢?让我们先来看下数据在网络的更底层是如何流转的。

旁路由架构的数据流转示意图

从图中可以看到,当我们从手机等终端设备发出一个数据包时,数据包总是由我们的终端设备经由网关路由至目的地址,目的地址返回数据时也是相同的路径。这是因为终端设备本身通常是不具备路由功能的,单单一个路由表终端设备就搞不定。

既然数据必然经过网关,那么我们只要强制把旁路由作为终端设备对外数据交互的第一层网关即可。至此旁路由的工作原理其实就已经解释清楚了,即在另一台路由器上实现的基于网关的透明代理。而网上各式各样的教程其实都是在教我们解决如何配置网关的问题。

在这一章节还需要说明的是为什么各个教程都要求我们把旁路由的 IP 配置在和主路由相同的 IP 网段。所谓的 IP 网段实际上就是子网,同一子网下的主机(设备)可以直接通信,跨子网则需要通过某种形式转换后才能通信,而这些转换虽然可行但比较复杂,在旁路由这个场景下显然是没有必要的。本着不改变原有网络拓扑的原则,旁路由自然也要配置在和主路由及其他设备相同的子网才行。

旁路由的配置

那么我们该如何配置网关以让数据按上文的工作原理进行流转呢?

(一)旁路由的网关设置

首先我们先来解决旁路由的网关问题。在上文的拓扑图中我们可以看到,旁路由虽然挂着路由器的名字,但它本质上也是网络链路中的一个节点,因此它也需要请求上层网关才能完成数据流转。而主路由是这个网络拓扑的出口,所以旁路由的网关自然要配置为主路由的 IP 地址。这一项配置是必须且不会随终端网关配置方式的变化而改变的,无论如何指定网关请求旁路由,旁路由本身都要依赖此配置才能完成正常的流量转发。

此外,还有子网掩码需要进行配置。前面有提到,在同一子网内的主机之间才能直接通信,而 IP 和子网掩码相组合便能确定设备当前所在的子网。旁路由并不改变网络拓扑,所以需要和主路由在同一子网内。因此将旁路由的子网掩码配置设置为主路由的子网掩码即可。同理,下文的所有子网掩码配置也均需要与主路由的保持一致。

(二)旁路由的DHCP配置

虽然配置了网关后数据流转图中的左半边已经成型,但如果不对旁路由的 DHCP 进行配置,实际上会导致各种各样的疑难杂症或直接无法联网。

原因在于 DHCP 使用了 UDP 协议,UDP 是没有连接的,如果主路由和旁路由同时开启 DHCP,则任意一个 DHCP 服务器都可能会应答终端的申请,进而导致 IP 下发和路由表的混乱造成各种无法连接的疑难杂症。当然,我们可以将两个 DHCP 的子网网段拆分开来解决共存问题,但这个行为在旁路由场景下并没有实际意义。

因此我们需要保证网络中只有一个设备承担 DHCP 功能,出于不改变原网络拓扑和避免无意义 NAT 的考虑,我们通常选择关闭旁路由的 DHCP 功能(对应到 OpenWrt 则选择「忽略此接口」)。

(三)终端网关配置

对于手机、电脑等终端,我们的目标是将其网关配置为旁路由的 IP 。实现方案很多,成本较低的主要为以下两种。

手动指定

顾名思义,只需要在终端设备的网络设置中将网关手动配置为旁路由即可。以 iOS 系统为例:

iOS 网络配置界面

手动填写一个网络上未被占用的 IP 地址,而子网掩码以主路由为准,网关则填写旁路由的 IP 地址。

手动指定的好处在于完全不影响原网络的使用,设备按需配置是否使用旁路由作为网关以实现特定功能。当旁路由故障时,未手动指定的设备仍能正常上网。

缺点在于操作烦琐。手机、电脑还好,但电视或根本没有屏幕的设备设置起来就会很麻烦。

依赖DHCP指定

DHCP 除了可以管理 IP 的分配,还会下发网关和 DNS 服务器信息,因此我们还可以借助 DHCP 的这一机制来为所有终端统一设置网关,而不再需要逐个手动修改。

前面提到,我们关闭了旁路由的 DHCP 功能,因此这个统一下发的工作就要交给主路由来完成。只需要在主路由的 DHCP 配置中将网关配置为旁路由的 IP 地址即可。

OpenWrt 的 DHCP 配置界面

以 OpenWrt 为例,将主路由 DHCP 下发的网关配置为旁路由 IP 地址即可(3 表示网关,6 表示 DNS 服务器地址)。

不过有些路由器的默认固件没有开放该配置项,对于这些设备,除非可以 SSH 连接后手动改配置,不然无法使用此种指定方式。

这种方式的好处显而易见,一次配置全家受用;缺点在于当旁路由出现故障时,所有连接的设备都会无法上网。

(四)DNS的配置

细心的读者可能发现了上文只提到了网关的配置,但未提到很多教程中的 DNS 配置。实际上单单就旁路由本身来说,网关配置完成后整个网络拓扑就已经搭建完毕了。但对于一些特定诉求,比如依赖旁路由进行统一的 DNS 劫持(很多功能的底层都依赖于此),则需要将对应位置的 DNS 也配置为旁路由的 IP 以将域名解析工作也完全交由旁路由处理。

配置步骤总结

手动指定方案

  1. 为旁路由配置和主路由同网段的静态 IP 地址,同时将旁路由的网关和 DNS 指向主路由,子网掩码与主路由保持一致。
  2. 旁路由关闭 DHCP 服务。
  3. 在主路由防火墙开启 SYN-flood 防御的情况下,关闭旁路由防火墙的 SYN-flood 防御(可选)。
  4. 在需要接入旁路由的终端设备中,将网关和 DNS 配置为旁路由 IP 地址,配置同网段的 IP 地址和与主路由相同的子网掩码。

DHCP下发方案

  1. 为旁路由配置和主路由同网段的静态 IP 地址,同时将旁路由的网关和 DNS 指向主路由,子网掩码与主路由保持一致。
  2. 旁路由关闭 DHCP 服务。
  3. 在主路由防火墙开启 SYN-flood 防御的情况下,关闭旁路由防火墙的 SYN-flood 防御(可选)。
  4. 在主路由的 DHCP 配置中,将其下发的网关和 DNS 配置为旁路由的 IP 地址。

疑难杂症的解决

虽然配置步骤看上去很简单,但很多人在实际使用中都会遇到逐步配置却上不了网或网络慢的问题,这里挑几个典型案例来解析。

(一)该不该设置iptables的MASQUERADE

这可能是争议最大的一条,有人说这条规则加上后影响性能而且没意义,但也有很多人表示不配这条就是连不上网(大多为连接不上国内网络)。

这条规则的作用本质上是在旁路由上做 SNAT,只不过修改的地址不需要指定而是动态获取旁路由对应接口网卡的 IP 地址,在 OpenWrt 里被称为「IP 伪装」。

配置 MASQUERADE 后的数据流转示意图

单从旁路由的网络拓扑来说,这条规则确实没意义,因为主路由作为对外出口必做 NAT,但旁路由本身就在局域网内且只是链路上的一环,没有必要再对内网 IP 进行耗费性能的 NAT 操作。

但不要忘了,理论和现实是两回事,物理网络拓扑中的不同设备、不同固件都有可能产生各种奇怪的兼容问题。我自己倒是没有遇到过该问题,但检索网友们的各种帖子,大概可以分为以下几种原因:

  1. 主路由固件数据包处理问题:部分路由器(似乎主要为国产品牌)的无线网在桥接和 iptables 处理过程中,当旁路由将国内流量重新转发回主路由时,主路由根据流中的首个数据包的状态做判断导致后续数据包未进行 NAT 就直接访问了互联网。这篇文章对此有比较详细的讲解(CSDN 也是有很多好文章的)。
  2. 主路由 NAT 硬件加速导致的问题:可能是由于硬件加速流程对数据包的处理出现了类似于原因 1 的问题导致无法正确完成数据交互。由于硬件加速本质上是在运行特殊驱动,而各家厂商的该驱动几乎都是闭源的,所以网上也没见到有探究深层原因的资料。这种情况下把硬件加速关闭即可(会一定程度牺牲主路由 NAT 性能)。
  3. IP 和 MAC 校验机制导致包被丢弃的推论:由旁路由的网络拓扑可知,配置网关后上行流量必然经过旁路由,但在旁路由不进行 NAT 时数据包中的 IP 仍然是终端设备的,所以理论上下行数据并不会经过旁路由而是由主路由直接转发至终端。显然,由于旁路由的存在,IP 地址和 MAC 地址在某个环节会有不匹配的情况,如果主路由对此有校验,那数据包就会被丢弃掉。但这也只是个推论没有证据佐证,同时由于网络可能的复杂性,主路由通常也不会主动做这种校验。

MASQUERADE 配置其实并没有定向地去解决上面这些具体问题,而是通过 NAT 来隐藏终端设备、只向主路由暴露旁路由 IP 的方式,一刀切地避免了上述原因导致的问题。但由于上下行流量都会经过旁路由,所有流量都会被二次 NAT 和二次转发,网络的吞吐会有不小的下降,直观感受就是下载速度变慢了。

所以比较理想的策略是,先不加 MASQUERADE 规则观察是否有问题(尤其是国内流量),如果确实有问题,在权衡可以接受性能的损失后再配上该规则。

(二)LAN和WAN是否需要绑定

具体操作是取消桥接,再设置 WAN 和 LAN 共用同一个网卡(如 eth0 )。这个操作其实和 MASQUERADE 规则的效果类似,因为绑定后经过 WAN 的流量必然会被 SNAT 。适用于确实遇到了疑难杂症且能接受性能损失场景下的备选方案。

(三)是否需要关闭旁路由桥接

在许多教程里,这个操作和添加 MASQUERADE 规则是配套的。但桥接与否实际上并不会影响整体的网络拓扑,这看上去又是一项没有意义的配置。我猜测可能是网上流传的添加 MASQUERADE 规则的方式是下面这条固定命令:

1
iptables -t nat -I POSTROUTING -o eth0 -j MASQUERADE

也就是要求 SNAT 时从 eth0 网卡动态获取 IP 。而在桥接模式下,MASQUERADE 时是需要从 br-lan (常见的桥接后的默认网卡名称,也可能是其他名称,要视实际情况而定)获取 IP 的,直接复制粘贴上面的命令会导致 IP 伪装失败。所以只要把 -o 参数的值改为 br-lan 即可:

1
iptables -t nat -I POSTROUTING -o br-lan -j MASQUERADE

但似乎确实有网友不取消桥接就无法联网,这种情况大概率是旁路由固件对桥接模式的处理有问题(多见于 ARM 架构的固件,这类固件通常要做很多的魔改适配),如果真的遇到这种情况的话则确实可以取消桥接尝试下,毕竟大多数情况下旁路由内部的桥接也没什么实际意义,去掉后由于少了流量判断流程可能还会有非常微小的性能提升。

至于 N1 这种自带无线功能但实际上没人用的设备,在关掉无线后,确实可以顺便取消掉没有意义的桥接。只是在 OpenWrt 取消桥接保存配置时,要确定选中了有线网卡对应的接口比如 LAN/eth0 ,不然如果后台的前端存在体验问题自动选择了 wlan ,那保存后路由器可就直接失联了。

(四)是否需要设置DHCP强制

如果我想通过 DHCP 下发网关但又不想改变主路由的原配置该怎么办呢?在 OpenWrt 的接口 DHCP 配置中有一个选项叫做「强制」,勾选后,此设备会忽略网络上已经存在的 DHCP 服务,强制启动本机的 DHCP 服务,所以似乎我们只需要在旁路由上配置此选项,并在旁路由 DHCP 中配置好网关,主路由不做任何变更,即可实现本段开头的诉求。

但上文提到同一子网中只能有一个用于分配指定网段的 DHCP 服务,因此旁路由强制开启 DHCP 后,还需要主路由具备主动判断网络情况停止提供 DHCP 服务的能力,这个流程才能真正运转起来。但并不是所有的路由器和固件都支持这个能力,目前来看 OpenWrt 作为主路由固件时可以正确检测并停用 DHCP ,其他固件则需要在使用时做下兼容性测试。从工程角度上讲,由于这样的操作过于依赖外部能力,属于和外部组件产生了强耦合,不利于未来维护,故不太推荐此种方案。

(五)旁路由的某些功能无法使用

各种组件内部实现大不相同,如果某个组件的代码对网络结构做了强限定(如上文所说的校验 IP 和 MAC 的匹配关系),那旁路由的加入可能就会打破组件预期的网络结构导致其无法运行。这种情况在组件本身不做适配时基本无解,只能尝试下开启 MASQUERADE 等配置,看看多加一层 NAT 后的网络拓扑是否能符合组件要求,但代价同样是会牺牲性能。

(六)是否应该关闭旁路由防火墙的SYN-flood防御

SYN-flood 是一种常见的攻击方式,SYN 指 TCP 建连三次握手中的第一步报文,flood 指大量发起该步骤的报文。由于 TCP 的实现原理要求服务端接收到 SYN 报文后回复客户端 SYN+ACK 报文表明请求被接受,并在一段时间内等待客户端回复最终的 ACK 报文,那么大量的 SYN 报文就会导致服务端出现大量等待最终资源耗尽挂掉,而攻击者并不需要真的完成建连,只要持续发送 ACK 包即可。

那路由器的防火墙又是如何作防御的呢?以 OpenWrt 为例,虽然 OpenWrt 的防火墙配置已经迁移到了 fw3 ,但翻看代码历史我们就会看到当时 OpenWrt 直接基于 iptables 的早期实现:

1
2
3
4
$IPTABLES -N syn_flood
$IPTABLES -A syn_flood -p tcp --syn -m limit --limit $rate/second --limit-burst $burst -j RETURN
$IPTABLES -A syn_flood -j DROP
$IPTABLES -A INPUT -p tcp --syn -j syn_flood

即借助 iptables 的 SYN 限流能力进行防御,同时此配置位于 default 配置中,会在 NAT 等具体网络操作之前执行。

对应到主路由,wan 口接收到攻击流量后便会进行限流,攻击者无法直接向后攻击到内网主机,那么同样为内网主机的旁路由自然理论上也就不会被攻击到。

但对于旁路由来说,理论上网络中的所有上行流量都会通过它来转发,那当流量超过防火墙的限流阈值时便会触发拦截,进而在终端上表现为网络时断时续。所以在主路由已开启 SYN-flood 防御的情况下,旁路由关闭该配置可以避免出现可能的网络不稳定问题。

旁路由与IPv6

IPv6 在家用网络中通常默认是没有 NAT 转换流程的,同时其动态地址配置方案比 IPv4 要复杂得多,比如 SLAAC 和之前的 DHCP 可以说完全不是一套机制,而 DHCPv6 又分有状态和无状态两类。而前面提到,我们实现旁路由网络拓扑的过程,其实就是在指定一个具备透明代理功能的网关的过程,但 SLAAC/DHCPv6 都没有提供网关下发能力,终端设备总是会以其所交互的主机作为网关,同时大多也不支持直接修改网关。此外,运营商不支持 DHCPv6-PD 、IPv6 子网限定范围等情况,都使得旁路由支持 IPv6 非常困难,在不同场景、不同网络下要面临不同的配置,甚至无方案可配置。

网上比较流行的旁路由 IPv6 实现是个曲线救国的折中方案,即先开启主路由的内网的 IPv6 地址分配进而让旁路由获得内网 IPv6 地址,随后再通过在旁路由开启一个 DHCPv6-Client 的方式获取到公网的 IPv6 地址,这样便可以将主路由的 DHCPv6 下发的 DNSv6 配置为旁路由的 IPv6 地址。此时除了 DNSv6 的解析是在旁路由进行,其他流程仍按原链路直连。而在需要分流的场景中,OpenWrt 的相关组件可以选择在解析域名时放过不需要处理的 IPv6 流量让其正常解析出 AAAA 记录,而对域名名单中的流量强制解析为 A 记录以继续走 IPv4 协议,从而实现和此前的类似的旁路由功能。

当然,这种解析实际上依赖于组件的能力。如果组件并不支持,那通过各种方式强制定义 IPv6 的路由表保证相关 IPv6 流量必然经过旁路由也是一种解决方案。不过无论哪种实现,由于不借助网关配置,其实都已经和本文的旁路由不相关了。

此外,还有种方案是通过 radvd 等支持配置路由单播和优先级的工具,用更高的优先级来指定终端的 IPv6 路由(可以简单理解为 IPv4 的网关),这样就替代了 IPv4 下手动配置或 DHCP 下发网关对应的功能,完美满足本文所说的旁路由网络拓扑,同时对 IPv6 动态地址配置方案的要求很低,但要求终端设备支持路由优先级配置。

总结

旁路由实际上是运行透明代理功能的网关,有人认为这个概念很民科,但我并不认同,毕竟它只是在通过已有的能力来解决特定场景的问题,和我们写代码、做产品没有本质区别,而「旁路由」这个名词也不过是个约定俗成的叫法而已,不应该被批判。

另一方面,由于旁路由在不同设备、不同网络环境有可能遇到很多奇怪问题,其实对于非专业用户来说付出的时间成本很有可能会远大于直接替换主路由的成本。但生命不息,折腾不止,如果是为了收获折腾的快乐、学习到新的知识,那又有何不可呢?

2021数码消费年终总结

2022-02-02 00:55:45

写在前面

这一年工作有了些许新变量,收入多了些,也有了更多的表达欲望和副业想法,唯一不变的大概就只有个人的后消费主义时期下的持续消费。

和往年不同的是,今年很少会看到感兴趣的东西就下单,更多的则是基于自身或合理或过度的需求调研后买入,所以买的玩具更少了,实用(大概)的东西更多了。我为今年的数码消费打上四星满意度和一星的省钱度。

硬件产品

Macbook Air M1

MBA M1

推荐指数:★★★★★

这是我近几年买过的最满意的苹果设备,虽然是最丐的版本,但在轻编程、码字和上网等中度使用场景性能完全够用,续航、发热方面有无数测评,我只追加一句:他们说的都是真的。而且由于不再烫腿,我使用它在床上躺着看视频的时间甚至超过了我的 iPad Pro ,是「真」全能设备。

在刚购入的时候,如 Golang 、VS Code 等常用开发工具还没有推出支持 M1 的正式版本,而 Final Cut Pro 及 Logic Pro 上的很多插件即使借助 Rosetta 2 也无法正常使用,但到了 2021 年下半年这些问题都基本得到了解决,我想生态的快速适配也是苹果敢于在全部产品线推进 M1 系列芯片的原因之一。

至于缺点,我认为主要是产品定位和用户的期望不一致所导致的,如果你认为它性能强大足以替代 MBP ,那 8G 的内存在重度使用(如多开 JetBrains 全家桶、剪辑大型视频)时会经常遇到内存不足的问题,低素质的屏幕也会让你在视觉相关专业场景下显得捉襟见肘,所以购买前明确好它只是你能在床上、沙发上、咖啡厅里无压力携带把玩的生产力工具才能产生愉悦的使用体验。

戴尔 U2720QM 显示器

U2720QM 显示器

推荐指数:★★★★☆

两年前年少无知的我听信网上 24 英寸左右的 4K 显示器才是最适合 MBP 的话买入了老年机型 P2415Q ,但这台显示器的固件过于古老,以至于想使用 HDMI 2.0 还要做一套仪式感十足的设置。另外 23.8 英寸的尺寸无论显示效果有多好,在看了一整天公司的 27 英寸 4K 显示器后很难不感叹家里这块屏幕实在是太小了。

在 2021 年优化(败家)预算到位之后,我做的第一项升级就是加装一台 27 英寸的 4K 显示器。由于这台显示器不用来打游戏也只接 Mac ,所以整体的要求如下:

其实如果不考虑淘宝的定制显示器,那单单高瓦数 Type-C 供电这一项要求在 5000 以下就只有 U2720 这一个选择了,巧合的是这款显示器也正好能满足其他三项要求,因此这也就是我的最终选择。

入手后发现本代 Dell 的固件已经好用多了,各项功能也和宣传无异,我认为是这个价位当前最值得购入的 4K 显示器。

那么为什么不是 5 星推荐呢,原因就在于 Dell 的祖传做工和品控问题,右下角边框缝隙能插卡也许是调侃,但我手上这台插张纸还是没问题的,另外戴尔的祖传漏光也得以延续,使用中我还发现将 MBP 以 Type-C 接入并休眠后显示器会被随机唤醒退出休眠,导致我不得不每次都要在不使用时手动关闭显示器。这些小问题对我来说都还算能接受,读者们则要好好考虑下了。

英特尔 NUC 猎豹峡谷 NUC11PAHi5

nuc

推荐指数:★★★★★

由于家用设备的用电环境差、民用机器和部件的稳定性一般的问题,我是坚定的 all-in-one 反对者,但此前我却一直将 Home lab 搭建在只有 4 核 2.0 GHz 的白群晖 DS920+ 上,性能羸弱的同时,想跑非 Docker 的服务就要承担影响系统稳定性的风险。思前想后还是决定将 Home lab 相关功能独立出来。

我首先尝试了把早年买来用来打游戏的台式 PC 装 Arch Linux 做工作站,既承担 Home lab 的功能,也作为居家工作时的主力机。但几个月后我就放弃了,一是由于屋内环境的限制台式机离床较近风扇声影响休息,几百瓦的 TDP 想 7*24 小时运行也很不环保(费钱),二是由于家中电子设备过剩,我使用台式机办公的时间少之又少,忙于工作也不想折腾 KDE 或 i3wm。重新审视后我总结出了对于我这样的非 WFH 人士所需要的 Home lab 设备标准:

这样的要求下,一票主要用途是 NAS 和软路由的一类准系统就被直接排除了,而如 Mac mini 等 ARM 架构的小机器也不合适,几经筛选后我将目光转向了 PC 届的「Mac mini」即英特尔 NUC。

玩机界的同学都知道 8 代 NUC 的口碑和可玩性最高,但目前 8 代由于炒作的原因有些溢价,另外电子产品买新不买旧,在没有黑苹果需求且手头不紧时我认为购买最新一代 NUC (当前为 11 代)才是最优解。而 NUC 这种小盒子机箱很难压住 i7 及以上的 CPU ,能标压稳定跑满 i5 就不错了,因此我最后选择了 NUC11PAHi5 这款大众型号。

京东用日常折扣价购入后配上两条三星 DDR4 3200 笔记本内存装上 Debian 跑起来,运行情况完美符合我上面提到的各个要求,而且发热比预想中好得多,至少中等负荷场景比我的 19 款 MBP 安静得多。总的来说推荐给追求稳定、懒得折腾且不打算 all-in-one 的同学买来作为 Home lab 。

微星 MAG274QRF-QD 显示器

MAG274QRF-QD

推荐指数:★★★☆☆

不怕各位看官笑话,长这么大我还没在高刷新率显示器上玩过 PC 端的游戏,进而导致我的 CSGO 水平很菜(不是),在 PC 台式机从 Home lab 场景退役后,我决定购入一款 2K 显示器让它发挥余热来提升我的 FPS 游戏水平。

我的游戏显示器购买标准如下:

几经研究才发现,2021 这个数码中庸年果然名不虚传,此前被戏称为「大金刚」、「小金刚」的显示器大多换汤不换药没什么吸引人的更新。最后还是选择了看似性价比很高的微星 MAG274QRF-QD。

这台 165Hz 2K 高刷显示器使用了 FastIPS 面板,理论上最快可以达到 1ms 的响应速度,支持 G-SYNC COMPATIBLE,附带着也支持 HDR 和 Type-C 充电,可以说从参数上来看是很优秀的水桶机了。

然而实际使用下来除了微星的固件和 Gaming OSD 对显示器配置调整来说非常方便和造型很酷外,发现了很多「小遗憾」:

总的来说显示器仍然是个一分价钱一分货的领域,2000 多块自然也不能奢求这台纸面参数优秀的显示器用起来有多完美,对于能接受画面稍微偏红且只用 DP 连接的同学可以考虑购入。

欧普照明护眼灯

Light

推荐指数:★★★★☆

买它其实主要是因为屋里的灯不是智能家居,床上关灯玩电子设备太伤视力,睡前下床关灯又是懒人噩耗,而这款灯支持米家体系,可通过米家 APP 远程控制,进而也可以用 iOS 捷径功能将相应的操作转换为语音指令添加到 Siri 中。有了它之后我躺在床上对 HomePod 喊声「台灯关」就可以关灯入睡了。

台灯是国 AA 级的,显色指数:Ra≥90,照明效果确实很适合伏案读书写字,作为屏幕灯的伪平替也颇为合适,此外色温范围是 2700K-5700K ,亮度也支持无极手触调节,使用体验上是款对得起价格的「现代台灯」。

而不是五星推荐的原因则是如下两点:

舒尔SM58动圈麦克风 + AKG-240监听耳机 + Focusrite 2i2声卡 + sE DM1动圈话筒放大器

Podcast toolkit

推荐指数:★★★★☆

原计划 2021 年要做一档播客,结果又犯了「做事前先买装备」的毛病,播客节目暂时流产于和朋友的试录,入门装备倒是集齐了。

购入前做了很多调研,在认清自家室内电器杂音较大不适合使用电容麦克风后,就一步到位入手了舒尔的 SM58 动圈话筒。不同于电容麦的自带声卡,经典的动圈麦还需要支持卡农 XLR 接口的独立声卡或调音台,对于 SM58 来说外接的声卡还需要支持 48V 幻象供电,而这套装备的入门就体现在我只选择了 2 话放版本的三代 Focusrite 2i2 声卡,而不是几千块起步的高级调音台。但选择丐版声卡搭配高级动圈麦的后果就是实测中 2i2 根本推不动 SM58 ,收到的声音非常小,不得已又入了一款颜值超高的 sE DM1 动圈话筒放大器,才达到了预期的收音水平。而装备党本性的暴露则主要体现在我又直接购入了入门级的 AKG-240 监听耳机,耳机音质比较平,好在个人佩戴起来还比较舒适。

现在再看,我仍然建议有制作播客想法的朋友为核心主播购入一些入门级的专业设备来提升音质,毕竟播客的音质是决定节目受欢迎度上限的因素之一,只是选择的时候可以考虑一些更便宜、更好推的动圈话筒,这样也能省下放大器甚至声卡的钱。监听耳机的边际效益较低,在前期则不建议购买。

GK55

推荐指数:★★★☆☆

20 年追着斐讯高性价比的尾巴,我入手了加价后的 T1 和 N1 ,T1 做电视盒子,N1 用来做旁路由。但好景不长,固件使用体验欠佳的 T1 早早就被我换成了 NVIDIA Shield TV ,而 N1 也在我使用了半年后出现了大概率也是固件问题导致的断网问题需要断电重启才能解决。Shield TV 的良好体验和这些神器的固件堪忧的稳定性让我痛定思痛,决定在路由层面也使用可扩展性和可定制性更高的 x86 架构软路由。

不巧的是那时正是 21 年年初,如果想买 J4125 的软路由就要承受较高的溢价而且没货!多番比对后我偶然看到了一款溢价较小的主打办公影院的准系统——Beelink GK55。

作为当时最新的赛扬处理器,J4125 的性能对前几代实现了碾压,到手后装上 ESXi 6.7 ,新建 OpenWrt 的同时还能再虚拟化个 Windows Server 2019 供特殊场景使用,使用体验非常丝滑,上网冲浪的速度更快了!

功耗层面没有具体测量但根据电费来看感知不强,风扇的声音不贴近几乎听不到,如果一段时间后偶现风扇起飞的情况,那有可能是主动散热结构的风扇在这个小盒子里积灰严重,清灰即可。

这款准系统还在售,我却不太推荐大家购买了,原因如下:

Nintendo Switch Pro 手柄

Switch Pro 手柄

推荐指数:★★★★★

我手上的 NS 是玩了两年的港版初代,此前已经出现了轻微的摇杆漂移问题,本着一样东西不买两次的原则,我没有选择更换 Joy Con 而是直接购入 Switch Pro 手柄。这只手柄没什么可挑剔的,个人感觉手感比我的一代 Xbox 精英手柄还要好一些,但价格却只有精英手柄的一半不到。另外终于能在主机游戏设备上吃到国行红利了,价格优惠+保修很香。

如果要说缺点,那我觉得一代 Xbox 精英手柄的电池设计仍是我最喜欢的,只要电池管够,续航就不再焦虑。

软件产品

软件 推荐指数 总结
Chevereto V3 ★★★★★ 基于 PHP 的经典图床程序,价格合适且为买断制,功能很完备
uPic ★★★★★ 又一款图床客户端,比 PicGo 的 UE 更加完整且支持 iOS 端
iA Writer ★★★☆☆ 目前的主力码字工具,但深度使用后有很多交互层面的不适应,总体而言还是 WYSIWYG 类编辑器更适合大众
DAMA ★★★★★ 八爷出品的良心买断制软件,和熊猫吃短信一样在注重用户数据安全的同时为图片打码提供了超便捷的实现
ServerCat ★★★★★ 八爷出品的良心买断制软件+1,轻度使用管理多台服务器简直不要太方便,UI 和 UE 设计加分

新增服务订阅

服务 推荐指数 总结
秘塔写作猫 ★★★☆☆ 在线文字纠正工具,对中文的支持非常完整,产品很好用但定价有些高,同时仅有在线版本无法对公司的敏感文字内容进行修正,新的一年希望能找到平替
Bear Pro ★★★★☆ 目前用来替代原生 Notes 体验不错,缺点在于虽然支持了 iCloud 同步,但没有目录管理能力,只适合轻度记录无法满足稍复杂的场景
flomo PRO ★★★☆☆ 很好用的轻量笔记工具,但当前的 PRO 订阅无法为把 flomo 当成私密微博、随手记的轻度用户创造更多价值和便捷,只推荐 flomo 的超级重度用户购买
DigitalOcean Spaces ★★★★★ 兼容 S3 的对象存储,物美价廉,不过部分 CDN 域名国内不可用,需要加一层反向代理
腾讯云香港轻量 VPS ★★★★★ 良心云诚不我欺!
搬瓦工 SoftBank VPS ★★★☆☆ 性价比很低且仅适用于我这种联通用户

服务订阅续费

服务 推荐指数 总结
Apple Music 美区 ★★★★☆ 曲库依然齐全,语言转换依然混乱,推荐依然鸡肋
Netflix ★★★★☆ 烂片烂剧越来越多,纠正了我集中注意力刷剧的毛病,再者就是价格是越来越高了
Pocket Casts Plus ★★★☆☆ 目前用来听播客的主力 APP ,Plus 订阅除了可以跨端同步进度,其他功能存在感很低
iCloud 200GB ★★★★★ 苹果设备越来越多后才能更加体会到 iCloud 的重要性
Setapp ★★☆☆☆ 曾经很实用的软件订阅集合,然而现在除了 Paste 和 Bartender ,其他软件大多用起来味同嚼蜡。推荐在第三方购买折扣码,除了便宜还比官方渠道的购买支持更多设备

总结

愈发感觉这几年可能是个可大可小的历史转折期,在这样的时期应该做的不是消费,而是「少花钱,多看书」。

该不该使用关系型数据库的物理外键

2021-09-06 00:55:20

没想到 2021 年还有很多人在争论是否该使用关系型数据库的外键,这种外键我们更习惯称其为物理外键,与之相对的是由业务逻辑控制的逻辑外键,实际上当今稍稍复杂些的业务都在使用外键,只是使用的是逻辑外键而非物理外键。

物理外键是我们学习数据库原理和设计时都会遇到的章节,它的主要优势是可以通过数据库实现强制的 Referential Integrity ,即引用完整性。但这样的完整性使用逻辑外键也完全能实现,有人认为逻辑外键由于完全依赖业务代码所以无法真正保证完整性,但这其实是个伪命题,因为物理外键也是由「人」来设置的,你只能确定已经设置过的物理外键能保证引用完整性,至于那些没考虑到的、设计错误的数据关联关系仍然是物理外键无法解决的,在这一点上物理外键和逻辑外键是没有实质区别的。而实际上当今的云原生架构在数据层面追求的是分布式和最终一致性,单个 DB 存储所有数据的时代早已过去,数据在服务间流转已经是常态,此外国内场景下很多数据也不被允许直接物理删除,物理外键的作用在现代架构下变得微乎其微。物理外键不是银弹,它甚至都没有成为银弹的实力。

而说到劣势,物理外键在现代后端架构中的缺点已经越发明显。分场景分析如下: * 对于传统企业应用,交付后几乎没有大面积迭代,使用物理外键是无可厚非的,这也是对传统软件开发模式和架构的传承。但现在有越来越多的企业选择使用 SaaS 或自主进行研发和维护,这也就意味着产品的迭代会比此前频繁得多,进而变为下文提到的流量小但迭代频繁的项目。 * 玩具型项目用不用外键都没有区别。 * 大流量项目使用物理外键无疑是在挖坑,抛开颇受争议的性能问题不谈,物理外键无法满足分库分表、单元化等现代架构设计的需要,甚至在这些架构下还会成为累赘要额外花费时间改造掉。 * 小流量项目的迭代速度可不慢,领域模型很难稳定下来,而使用了物理外键也就意味着系统是基于数据库进行的建模,那么当前的物理外键设计迟早有一天要面临变更,这所带来的维护成本(改表困难、业务拆分和聚合困难等)是巨大的,这也是为什么现在很少有人使用存储过程的原因。

可见物理外键在数据模型迭代频繁以及大流量场景下具有非常明显的劣势。

其次是职责问题,复杂的物理外键维护需要 DBA 的参与,但在人员职责上,DBA 与业务强耦合本身就是不合理的,这和为什么要做前后端分离是一个道理,这也是为什么当今很多互联网公司会选择一名 DBA 对接一个后端大组甚至事业部的原因,DBA 的职责已经下沉到更核心的数据库稳定性和性能提升上。而在架构中的分层职责上,在持久层耦合业务逻辑是非常不明智的,因为这意味着你的架构会严重依赖某个数据库选型甚至某个特定版本数据库的功能,领域模型与数据模型的耦合也会产生很多人噩梦中的一个 Service 层走天下的情况,业务逻辑很难做进一步的抽象和拆分,至于读写模型分离、CQRS 也就是更不可能的事情了。

综上,使用物理外键能带来收益非常有限,但隐性成本(只要业务还在发展,那未来早晚会变为显性成本)却非常高,其本身又可以被逻辑外键所替代,那除了个人或团队喜好,我实在找不到继续使用物理外键的理由。

公司基因与工作选择

2021-08-30 07:18:35

公司基因是一个较有争议的描述,很多人觉得这是对公司体制和运营模式的刻板印象,因为一家公司的固有模式和体制并不代表着它以后就不能涉猎并做好当前不擅长的领域。而我认为「公司基因」是对公司「路径依赖」的形象化描述,「基因」或称为路径依赖对公司发展的影响并不是确定的,而是概率性的,只是这个概率较大,可以用于「武断」地推断公司的战略调整是否会成功。那么公司基因又与普通员工的工作选择有什么联系呢?

新业务的机遇与风险

这里的新业务指的是中大型公司即将或刚刚开始拓展的新领土,这与创业公司不同,因为创业公司没有管理和技术的历史债也没有定型的体制,关心的更多的是生存和发展壮大的问题,因此不在公司基因问题的讨论范围内。而中大型公司的新业务则往往是从已有业务中抽调的人员加上新招募的员工共同组成的团队,所做的管理和决策也通常会沿用已有体系,换而言之,这类新业务通常是集团的一个事业部甚至一个小组。

对新业务团队中的员工来说,业务的发展所可能带来的收益是十分可观的,小到升职加薪,大到拆分上市,都是不小的机遇,这也是很多人愿意忍受新业务中常见的加班压力的原因之一。

但另一方面,新业务的风险是十分巨大的。虽然不需要像做存量业务时在红海中找寻新 KPI 或 OKR 做些乏善可陈的微创新,但新业务未来能否成功是一个很大的不确定因素,而如果在业务未成形时集团策略就发生了调整导致资源倾斜消失,则新业务会快速变为无利可图的老业务甚至弃子。另一方面,新业务通常追求短平快和多快好省,而业务的本身使用量在初期往往很小,这对基层员工尤其是技术人员来说在成长性上往往是大打折扣的。

公司基因与新业务的联系

风险与收益往往是相关且呈正比例关系的,因此加入新业务团队时我们也要审时度势,判断业务的成长性是否能与我们入职时的期望相匹配,而公司基因正是我们做判断时的标准之一。

前面提到,我们可以把「公司基因」理解为一家公司的「路径依赖」,而公司层面上最明显的路径依赖就是这家公司管理层以往所依赖的高效「赚钱」方式,这里说的赚钱并不一定需要盈利,因为资本市场看的是未来,只要公司的模式有赚大钱的潜力,当前运营状况是赚是赔并不是很重要。另一个主要路径依赖我认为则是开拓或适应市场的方式,一家创业公司能成长为中大型公司,好的 idea 是远远不够的,能够不断开拓和占领市场让自己的赚钱方式落地才是发展的要素之一。而面临时代的变革,如何能调整经营策略适应市场的变化则是一家公司能长期存在的决定性因素之一。

而新业务在公司路径依赖中所扮演的往往是先驱的角色,尝试的往往是公司此前不曾涉猎的业务方向。但这样的先驱也分主次,也就是所谓的核心与否: - 代表未来的新业务:作为公司为上市或财报所定的新方向,这样的业务自然是该公司各个业务中的新翘楚,也是考虑到新业务团队工作时的首要选择。但公司基因等因素会影响此类新业务的成败。 - 占位型新业务:很多时候公司管理层看到新赛道时并没有想好具体该怎么开展业务,但在新市场上占个位置总是没错的,别人都做了我当然也要跟上,万一以后有机会能发展起来呢?但此类业务的前景十分不明朗,而且常常与公司基因不符,有很大的概率会在市场上销声匿迹。 - 支撑型新业务:准确来说,虽然这类新业务的形式是公司内不曾有过的,但其定位并不能算得上新,它们更像是对已有业务基本盘的扩展,是稳固或提升存量业务的手段。由于建立此类业务的出发点就不是将其做大做强,所以资源和战略倾斜都十分有限,但只要所服务的主体业务是符合公司基因的大业务,同时自身的业务场景与公司的路径依赖差距不大,那这类新业务的存活时间通常较长,直到其定位不能再满足主体业务的发展时才有可能被取代。

从上面的分析中我们不难看出,在收益层面 代表未来的新业务 > 支撑型新业务 > 占位型新业务 ,而在风险层面 占位型新业务 > 代表未来的新业务 > 支撑型新业务 ,占位型业务均不占优,在大部分场景下都是我们在考虑加入新业务时必须避开的「坑」。而支撑型新业务虽然短期风险小,但能带来的收益却很有限,投入产出比差,而我们找工作时大多表现为风险厌恶,所以选择去支撑型新业务可能不如选择存量业务。而「代表未来的新业务」虽然在风险排名上在占位型业务之后,但这也是因为占位型业务在大部分场景下实在拉垮,以至于同样风险不小的这类代表未来的业务看上去好像风险也没那么高,是否要选择这类业务是见仁见智的事,大家应该根据自己的应聘风险喜好和对压力的承受能力做好评估。

根据公司基因分辨新业务的类型

选择建议有了,那么如何对具体的新业务进行分类呢?

首先我们要对目标公司的核心业务和盈利模式有足够的了解,既可以研究该公司的财报、招股说明书甚至新闻,也可以比对该公司与不同竞争对手的博弈历史进而从输赢中明白公司的制胜法宝和所缺乏的竞争要素,进而推断出公司长期以来在战略、管理和团队风格上的宏观基本面。虽然对于投资者来说这样的基本面是远远不足以作为判断依据的,但对于求职者来说这些已经能让我们对公司的路径依赖建立基础认知。

随后我们可以从资源投入的角度分辨出「支撑型新业务」和其他类型的新业务。投入大量人力物力,建立事业部甚至不惜对新业务团队开启高强度加班,这些真金白银都代表着公司对新业务的态度大概率是看好或先占位,而支撑型业务往往就很难有这样的「高待遇」了。

而「代表未来的新业务」和「占位型业务」的区分边界则十分模糊,例如早年百度尝试做外卖时也投入了大量的资源甚至在其官方宣传口径中外卖就是百度在 O2O 领域的先驱部队,但故事的结局今天我们也早已熟知。很多时候,公司对于「占位型业务」是没有良好的预期管理的,抱着「我做着试试看,万一做成了那就是公司未来的发展方向了」的心态来进行战略制定。这也就意味着,对于这两种新业务类型的判断要上升到业务的成长性和存活期来判断,即这个新业务做成的可能性有多大。

而当我们了解公司基因时,我们也就有了对新业务成功几率的基础判断能力。一家擅长做地推和销售的公司能做好社交吗?一家擅长做数据分析和索引的公司能做好外卖吗?一家擅长做供给侧建设的公司能做好社区团购吗?当我们有了这些疑问的时候往往也代表着我们已经了解了新业务在该公司发展中所可能面临的挑战,进而也就对该业务到底是未来的核心还是仅仅占个位置有了概率性的判断。

公司基因判断法的局限性

和股票投资一样,没有人能根据宏观情况来百分之百预测对股价的走势,对一家公司的基因有着再深的理解,我们也不能保证自己的判断是准确无误的。如果管理层的决策和管控能力足够优秀,那么哪怕企业此前的路径依赖再严重,公司也可以通过细致的资源重组和体制改革来实现公司方向的大转变。因此还是如上文所言,根据公司基因所得到的判断结果仍然只是概率性的。

另一方面,企业组建新的大型业务其实并不常见,通常是年级别的间隔,这与行业及经济的发展周期和阶段有关,更多的新业务还是已有业务线的中小型创新或重新拆分。而对于此类新业务,基于公司基因所作出的判断准确性是很低的,原因一方面在于我们很难了解此类具体业务线的路径依赖情况,另一方面则在于由于细分业务的体量相对更小,因此公司如果决心够大,那打破业务线路径依赖将业务做成功的可能也会大大增加。

如何辨别KPI驱动的技术项目

2021-08-23 03:00:07

KPI 驱动的技术项目指的是表面上是为了切实解决某一问题,但实际上却是为了满足 KPI 或 OKR 需求应运而生的项目,在开源项目和公司内部都有可能出现。

技术项目是需要进行长期维护的(尤其是开源项目),但如果 KPI 已经完成,则要么维护者的迭代动力骤降,要么项目早晚会面临交接给其他团队成为边缘项目的命运,而其中的主要原因显然是这类技术项目很难为维护者们带来持续的收益,边际成本却日益上升。同时,纯粹的 KPI 驱动也容易导致技术实现的一地鸡毛。

可见,纯 KPI 技术项目对于项目的使用者来说是很不友好的,那么当一个项目具有哪些特征时我们就应该保持警惕并思考是否还要使用该项目呢?

KPI 驱动的特征

公司特征

对于开源项目,可以纵观该公司的开源历史,如果发现该公司曾经建立很多被行业公认的 KPI 项目,那么这很可能是公司体制导致的问题,这家公司的后续项目也大概率会不断出现纯 KPI 驱动的技术项目。

团队特征

受长期的 KPI 压力、TL 甚至上层领导做事风格和内卷的影响,有的团队做技术类项目时很容易出现纯 KPI 驱动的情况,这种情况其实比公司层面的 KPI 项目更值得警惕,因为公司家大业大,可能还会为了口碑做些基础维护,而一旦某个团队的项目没了长期价值,人手有限时团队是很难再倾斜资源进行项目维护的,因此对方团队的运行状态和相关历史也是值得关注的一点。

银弹项目

我们常说「没有银弹」,但技术项目的两个核心出发点就是「创意」和「差异化」,而在技术基建日益成熟的今天,「创意」是可遇而不可求的,因此很多项目都是基于「差异化」来立项的。

而差异性在实操中意味着要比同类项目「强」,要让行业和老板觉得项目足够牛,因此在进行功能设计和文档描述时常常会把项目形容成对应垂直领域的「银弹」。

但我们知道鱼与熊掌不可兼得,大部分这样的尝试都意味着存在 tricky hack 甚至只是单纯吹吹牛,这样的项目在稳定性和安全性上都存在着不小的风险。

生态封闭

与「银弹」项目类似,短期 KPI 驱动的项目通常需要的是功能强大而不是生态欣欣向荣,原因大概是功能可以用人力成本直接换取收益,而生态的建立周期和实际效果很难量化,常常满足不了有限的 KPI 周期要求。另外公司内的项目如果定调过于开放还可能影响利益分配甚至导致竞品的快速涌现。因此很多此类项目不会主动选择开放生态。

对建议的听取力度

当一个技术项目出现在公众视野中时,通常已经有了一个较完整的形态,对于很多团队来说此时实际上已经达到了 KPI 预期,项目自然也就进入了慢速甚至减速发展期。

因此我们可以通过建 Issue 、提需求的方式测试对方团队对建议的听取力度,进而从对方的态度中对项目的性质和进度一探究竟。

解决了锦上添花的问题

这在公司内部更为常见,即以某个已有系统为基础进行二次开发来支持特定场景的需求。但由于二次开发意味着项目没有自主权,底层依赖的功能迭代可能会导致项目无法使用甚至直接被原系统取代,因此无论这类项目在立项时是不是以 KPI 为出发点,最终都很难逃脱因收益问题导致的被边缘化的结局。

技术实现细节

这需要我们耐心了解项目的具体实现,从单测、注释和代码架构上寻找蛛丝马迹。架构缺乏设计、代码逻辑混乱、大量魔改等现象都预示着该项目有很有可能是完全以 KPI 驱动的。

该一刀切地拒绝使用 KPI 项目吗

KPI 是利益这一概念的具体体现,而无利不起早,如果没有利益的驱使只靠用爱发电,那么很多技术项目实际上只会停留在想法阶段进而没有被进一步孵化的机会。

对于 KPI 驱动的技术项目,作为使用者的我们要结合我们自身的需求,考虑好使用该项目带来的收益和可能承担的风险的比例关系,正收益就用,反之则远离。

这与风投似乎有些相似,引用某个项目是看好了他的某些功能或发展潜力,但未来会如何是无法准确预测的,提前制定好退场策略会大大降低我们的风险。

基于Revue建立个人Newsletter

2021-08-16 01:37:25

Newsletter 这个名词我在十几年前刚上网时就有所耳闻,虽然能实现通过推送达到内容直接触达用户的目标,但它既没有像 RSS 一样在开放互联网上广为人知,也没能像微信公众号一般在封闭生态内成为实际上的标准,再加上国内使用邮件作为日常沟通的场景本身就弱,所以多年以来一直是个小众领域。但最近 Twitter 等大公司在 Newsletter 领域动作频频,Newsletter 可谓是老树开新花,在人们对封闭平台越发警惕的今天,相当于去中心化的 Newsletter 受到了越来越多人的青睐。

2023 年更新:Revue 随着 Twitter 被收购已经决定关闭了,很可惜也很无奈。

为什么要写 Newsletter

那么为什么要写 Newsletter 呢?从我个人订阅的 Newsletter 来看,大概可以总结为以下几个目的,如果这些目的中有一个或几个正好符合你心中的内容分发方式,那么你也应该尝试写写 Newsletter : - 去中心化的知识付费:头部的 Newsletter 作者其实通过建立付费的 Newsletter 项目并提供独家内容赚得了不少收入,而使用 Newsletter 作为知识付费渠道非常灵活,既可以省去知识星球、知乎等中间商的抽成,也可以避免严苛审查以提供更多内容。(当然本文介绍的 Revue 的收费机制并不符合这一特征) - 增加用户触达渠道:虽然今天 RSS 又有了流行的趋势,但目前仍然很小众。而微信公众号的推送能力已经日益鸡肋,看看后台数据就知道看到推送打开文章进行阅读的人少之又少,因此新增一个用户触达渠道至少不是件坏事。实际上很多博客和内容网站如 infoq 虽然没有在其 Newsletter 中提供独占内容,但经常会包含博客的最新最热文章推送,可见此渠道仍然是有挖掘价值的。 - 拉近与读者的距离:我们很难在博客或公众号的文章中以『真人』的语气向读者输出内容,而邮件由于其自带的沟通属性,在符合内容定位的情况下使用合适的语气和行文可以起到与用户拉近距离的作用,进而提升用户留存。

Newsletter 系统选型

电子邮件作为互联网最古老的通讯方式之一,其配套的营销邮件系统的种类自然也是琳琅满目,Newsletter 这个细分领域也不例外。这里介绍 3 款当下正流行的 Newsletter 方案,并讲讲为什么我最后选择了 Revue 。 - MailChimp:2001 年创建的老牌邮件营销品牌,本质上 MailChimp 适用于任何需要群发邮件的场景,只不过很多 Newsletter 维护者们都选择了这个品牌,因此它也成了 Newsletter 领域的热门服务。免费方案每月可向 2000 名订阅者发送邮件,超额后收费很昂贵。 - Sendy:邮件群发系统的开源实现,需要用户基于 Amazon SES 进行自建,基础功能还算齐备,同样也适用于所有邮件群发场景。由于没有 MailChimp 这种中间商赚差价(MailChimp 等底层使用的也是 SES)所以价格非常便宜,发送 10000 封邮件只需要 1 美元。 - Revue:一家专注于 Newsletter 的公司,刚刚被 Twitter 收购不久,与上文两家按量收费不同,Revue 的收费策略是只对付费订阅的 Newsletter 按百分比抽成,免费订阅的 Newsletter 不向发送者收取费用。

不难看出,Sendy 无论是从价格还是可掌控程度(不用担心平台跑路)来说都拔得头筹。但由于 Sendy 需要自建服务,有一定的搭建和维护成本,而且由于有订阅者邮件列表这样的重要数据,数据的备份也极为重要,不太适合个人 Newsletter 作者使用,即使是技术出身的我,也因疲于折腾其稳定性问题且担心日后服务疏于维护会导致功能不可用进而影响用户体验而没有选择 Sendy。

而 MailChimp 这只小猴子(我们常常在很多博客底部看到的猴子按钮就是 MailChimp 的订阅指引)及其类似的产品虽然在用量小时免费,但收费阶梯过于陡峭。为了避免未来订阅人数上万(这里假设一下可以达成)时需要每月付出数十上百美元的成本,在创建自己的 Newsletter 时还是应该谨慎选择这类平台。

而 Revue 只针对付费订阅项目收费的方案很符合我的理念,毕竟免费使用平台资源拉到的用户产生付费行为时让平台抽成一部分也算合理。同时很多个人维护的 Newsletter 还远远达不到推出付费项目的阶段,所以整体来看成本接近于零。此外,Revue 不同于其他平台涉猎群发邮件的所有场景,专心关注 Newsletter 领域的它也更有可能和动力将 Newsletter 作者的常用功能做得更好。最后,虽然大公司砍掉口碑高的产品线屡见不鲜(对,说的就是 Google 你),但背靠 Twitter 确实也让我对 Revue 的未来发展免去了很多如资金链等方面的基础顾虑。

基于 Revue 构建 Newsletter

配置

Revue 可以使用 Twitter 直接注册,也可以基于邮件注册,访问 Revue sign up 简单操作即可。

完成注册后,在右上角的 Accout Settings 中便可对 Newsletter 的订阅落地页和邮件进行设置。

配置页面概览

设置自定义邮箱

Revue 默认使用 @getrevue.co 后缀的邮箱作为 Newsletter 的发送方,读者的回复则会被转发到管理者设置的邮箱中。如果希望使用自己的邮箱,只需要取消选中 Profile 中 Use Revue email address 选项输入自定义邮箱即可。需要注意的是,如果是自定义域名的邮箱则最好做好 DNS 中的 spf 设置以降低被列为垃圾邮件的可能性。

设置自定义邮箱

自定义域名

Revue 提供的自定义订阅落地页使用的是 getrevue.co 域名,如果想对该页面使用自定义域名,只需要在域名的 DNS 设置中新增一条 cname 记录指向 www.getrevue.co ,随后在 Settings -> CUSTOM DOMAIN 填入新建的子域名即可。

自定义域名

自动抓取内容并创建为 Newsletter

Revue 支持将多个外部渠道的数据聚合为 Newsletter ,只要在 Settings -> Integrations 中进行简单配置,即可实现周期性的 Twitter 等社交媒体整合发布或博客新文章的自动推送,很适合不打算包含独占内容、只将邮件作为存量内容推送方式的 Newsletter 。

支持的抓取列表

在个人网站中添加订阅入口

入口这东西从来不会有人嫌少,如果想在自己的网站上新增 Newsletter 的订阅入口,可以在 Settings -> Integrations -> Signup forms 中找到对应的 HTML 代码,将其嵌入网站的合适位置并进行样式适配即可。这样一来用户就可以在你的网站上输入邮箱地址后一次跳转即可完成订阅,缩短了用户的操作路径。

订阅入口配置

开始写作

Revue 将每封 Newsletter 称为 issue ,点击左上角的 Create 按钮即可在 Revue 上开始 Newsletter 的创作。

和 MailChimp 一样,Revue 的编辑器并不支持写作时常用的 Markdown 格式直接渲染。同时 Revue 的编辑器使用起来也不太灵活,例如 H1 标题需要点击按钮单独添加、修改样式必须选中文字才可以操作。因此,我更推荐使用 Notion 这种自带齐全的排版能力且渲染美观的工具完成内容编写,复制粘贴到 Revue 的编辑器中再进行微调,效率要高很多。

撰写 Newsletter

发送 Newsletter

完成内容编写后,点击 Schedule issue 即可进行 Newsletter 的发布。出于保证落地页展示可以更加美观的考虑,建议为每封 Newsletter 添加一张封面图,分辨率最好为 1200*600px 。

Revue 支持『立即发送』和『定时发送』两种发送方式,定时发送的设置也很简单。这里需要注意的是,Revue 似乎不会为用户自动选择时区,所以在配置定时发送前建议先在右上角 Settings -> TIMEZONE 中配置好自己所在的时区,如 Asia/Shanghai 。

发送 Newsletter

查看统计数据

完成邮件的发送后,我们就可以在 Issues 列表中点击 Analytics 查看邮件的具体统计数据,当前指标基本可以满足基础的用户数据概览,如需要更详细的用户画像分析则还是建议搭配 Google Analytics 等工具一起使用,Revue 提供的具体统计指标如下: - 点击数最多的链接 - 打开邮件的订阅者排行 - 用户反馈内容 - 取消订阅情况 - 落地页站点中的文章访问情况和反链统计

查看统计数据

Newsletter 该写点什么

内容

Newsletter 一词以 News 开头,那么我们是否应该顾名思义将邮件通讯作为新闻聚合速递呢?我个人认为可以,但不建议。原因在于每周的热点新闻也就那么多,通常我们的 Newsletter 做的又是垂类内容,这样一来可以收集的内容就更少且极容易同质化,这在推荐算法盛行的今天是很难为读者提供有效价值的。所以我认为,个人维护的 Newsletter 项目至少要包含一些成段落的原创内容,让读者认识到订阅 Newsletter 并花费时间阅读是能获取到干货的。即使有新闻的聚合推送也应该加上自己的见解评论,尝试为读者提供另一审视视角,做一份轻社论『报纸』。

结构

与普通的信件一样,Newsletter 在常规情况下也应该由『称呼』、『正文』和『祝福语』组成,其中『称呼』和『祝福语』要通过简短的语句讲清为什么读者会收到这封邮件、这个 Newsletter 项目聚焦在什么内容,让用户快速回顾之前的订阅行为并与之前的印象建立联系。

此外,如果你的邮件内容既包含原创文章的正文、也包含许多内容的导读,而通常你更希望读者至少读完你的原创文章,这种情况下我认为原则上应该将原创文章正文置前,以突出你想让读者了解的内容,毕竟能读完整封邮件的读者大概率并不占多数。

语气

如果 Newsletter 中的内容是独占的,那么我更建议以第一人称进行写作,同时用语尽量生活化、富有亲和力,这样也有助于和读者建立感情联系进而增加用户对这份 Newsletter 的黏性。

当然以上只是建议,具体的语气使用还是要按照文章甚至品牌的『人设』来决定,例如一篇报道战争新闻的邮件使用亲昵的用词夹杂些非新闻性的用语显然是很突兀的。

长度

如果你写的不是那种全网唯一、不看少赚一万块的超级精品内容,那就不要试图在 Newsletter 中写万字长文,原因在于习惯使用邮箱的用户常常有很多邮件需要处理,太长的内容容易导致读者产生『太长不看』的情绪,而没有邮箱使用习惯的用户自然也没有养成阅读 Newsletter 的习惯,当内容无法快速吸引读者眼球时就很容易导致读者的放弃阅读甚至取消订阅。另一方面,开头与结尾还应该考虑加上些鼓励读者持续阅读和关注的客套话,以对读者建立一定的心理暗示。

Newsletter 能为我带来更多的读者吗?

如果之前关注你的人就不多,那答案就是不能,比如我这样的。

如果你已经有了一批数量可观的固定读者,那 Newsletter 机制可以很好地激活用户活跃度,提高用户留存,同时 Newsletter 比起博客或公众号的文章,更像是聊天,可以很好地拉近作者和读者间的距离,这也是同样具备订阅能力的 RSS 机制所做不到的。

重新定义PPT的Slidev

2021-08-14 06:44:15

作为一个每次写 VBA 都要查文档的 MS Office 小白,PowerPoint 这种被人成为神器的演示工具对我来说却是阻碍生产力的绊脚石,我总是无法沉下心做好元素的对齐,也被页码问题搞得焦头烂额。终于有一天,我意识到花费在样式和结构组织上的大量精力是浪费生命的,我应该更专注于 PPT 的核心——内容。如果能有一款基于 Markdown 的工具,让我逃脱格式调整的苦海,潜心编写内容该多好啊!而当我看到 Slidev 时,我知道问题终于有了转机。

什么是 Slidev

When working with WYSIWYG editors, it is easy to get distracted by the styling options. Slidev remedies that by separating the content and visuals. This allows you to focus on one thing at a time, while also being able to reuse the themes from the community. Slidev does not seek to replace other slide deck builders entirely. Rather, it focuses on catering to the developer community.

Slidev 的文档中如是介绍自己,虽然功能和理念强大,但它却谦卑地把自己限定在为开发者提供帮助。

简单来说,Slidev 就是一款数据与样式分离的演示文档编辑工具。文档的内容组织是以 Markdown 标记语言为基础的,使用者可以结合 CSS 甚至 Vue 对文档的样式和交互进行自定义,项目基于 Node.js 实现。

Slidev 并不是这种思路的唯一实践者,实际上它还处于项目的初期阶段,功能不完整的同时也可能存在些稳定性问题,例如 Marp 等先驱者则是更加成熟的先驱实现。但 Slidev 的优势在于,其对 Vue 的良好支持使得他的扩展和复用能力几乎具有无限的可能,很适合专业开发者在 PPT 领域实现『弯道超车』。

Slidev 适合做什么

  1. 文本文档与演示文稿间的快速转换:以技术分享场景为例,技术文档的内容往往已经足够满足分享用途,但将文档中的文字逐行粘贴到 PPT 中再进行格式微调实在是枯燥且浪费生命。而如果使用 Slidev ,我们只需要在原文档中加入分页符,再按修改文本文档的习惯对文章的组织结构进行微调即可快速创造出一份精美的 PPT ,效率不可谓不高。

  2. 深度定制化需求:尽管 PowerPoint 和 Keynote 的功能已十分强大,但其功能层面的天花板仍然被限定在了软件自身所提供的能力上。而在 Slidev 下,我们可以按开发前端页面的模式对 PPT 进行深度定制,甚至可以通过编写插件、修改 Slidev 源码的方式实现我们想要的任何功能。换而言之,只要你的前端动手能力足够强,那么你对你的 PPT 就有着 100% 的掌控力,这很适合有深度定制化需求的用户。

  3. 高度复用:借助 Slidev ,我们完全可以建立一套自己的 PPT 脚手架项目,这样一来每次制作 PPT 时我们只做好文本内容的编写即可,从而真正做到聚焦内容本身。而这种脚手架的可复用程度是 PowerPoint 下那简单的模板机制所无法比拟的。

Slidev 不适合做什么

如果你对 PPT 的动画、图表、样式细节等有着高标准的要求,那么 Slidev 可能不太适合你的使用场景。倒也不是功能上不支持,主要是想要实现与 PowerPoint 完全一致的效果需要进行一定深度的开发,这在效率上大概率是得不偿失的。

另外如果你需要进行更复杂的公式编写,那 Latex 生态下的 Beamer 是个更好的选择,毕竟 Slidev 是以 Markdown 为核心的,对 Latex 的支持只能说是聊胜于无。

除此之外,Slidev 还有以下功能上的局限性: 1. Slidev 还在持续开发中,相当于 Beta 版本,所以实际使用中可能会遇到些 bug 。 2. 不支持直接导出为常用的 MS Office 的 .ppt 格式或 Keynote 的 .key 格式,难以满足一些正式场合的要求。 3. 项目提供了许多漂亮的模板,但实际上很多公司、会议和学校会要求使用固定的内部 MS Office 或 Keynote 模板,整体适配成本较高。 4. 由于 Markdown 语法本身的局限性,表格、图表类内容的制作非常繁琐,效率远低于 PowerPoint 和 Keynote。

如何安装 Slidev

使用 Node.js 的包管理工具 npm 或 yarn 在目标目录执行以下命令即可完成安装:

1
2
3
npm init slidev@latest

yarn create slidev

在 v0.14 之后,Slidev 提供了更便捷的安装方式,全局安装后便可在任意目录执行 slidev 命令快速初始化项目,同时这也省去了此前安装方式带来的重复且巨大的 node_modules 目录:

1
2
npm i -g @slidev/cli
slidev

Slidev 使用入门

编写你的第一个 Slidev PPT

我们可以像平时编写文章一样,直接用 Markdown 的语法进行 PPT 的编写,唯一的区别在于需要使用 --- 进行分页:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 标题1

## 副标题1

正文**第一页**

<!-- 这是一段提示文字 -->

---

# 标题2

## 副标题2

正文**第二页**

---

# 标题3

## 副标题3

正文**第三页**

随后执行 slidev 命令即可在本地查看渲染好的 PPT : - 预览模式:http://localhost:3030/ - 演示者模式:http://localhost:3030/presenter

以演讲时常用的演示者模式为例,Markdown 编写的内容如期渲染,同时界面非常简洁,常用的功能一应俱全:

Slidev demo screenshot

导出 PPT

当我们希望将 PPT 导出为 PDF 时,首先要安装如下依赖:

1
npm i -D playwright-chromium

随后只需一行命令便可完成 PDF 导出:

1
slidev export

除了 PDF ,目前 Slidev 还支持以下形式的导出: - 导出为 PNG 图片:slidev export --format png - 导出为支持点击的 PDF 文件:slidev export --with-clicks - 构建项目用于 SPA 部署:正如所有现代化前端工具一样,你可以使用 slidev build 命令将 PPT 打包构建,并按前端部署的常见方式将 dist/ 目录中的内容部署在服务器上以供其他用户访问

使用 Windi CSS 美化 PPT

相信前端同学一定听说过大名鼎鼎的 Tailwind CSS 项目,该项目通过设置大量的预设 class 大大减轻了前端工程师进行样式微调的工作量的同时也很大程度上降低了不同项目间的代码重复率并为标准化提供了思路。而 Slidev 所依赖的 Windi CSS 也是同样的理念,项目作者在 README 中如是写道: > If you are already familiar with Tailwind CSS, think about Windi CSS as an on-demanded alternative to Tailwind, which provides faster load times, fully compatible with Tailwind v2.0 and with a bunch of additional cool features.

而在 Slidev 中,我们可以按照常规的前端页面代码编写方式内嵌 HTML 元素并用 Windi CSS 的预设 class 来自定义样式。例如如果我们想为 PPT 新增一个按钮,只需在 Markdown 文件中增加如下内容:

1
<button class="bg-blue-400 hover:bg-blue-500 text-sm text-white font-mono font-light py-2 px-4 rounded border-2 border-blue-200 dark:bg-blue-500 dark:hover:bg-blue-600">

预览时便可看到期望的效果: Windi CSS demo

此外,Slidev 也支持通过扩展 defineWindiSetup 这个 Vue 组件的方式将 Slidev 的默认样式也完全交给用户来进行自定义修改。

建立基于 Vue 的工程化 PPT 项目

Slidev 使用 Vue 3 进行客户端的应用渲染,因此我们可以使用标准的 Vue 项目的开发流程对 PPT 进行扩展甚至抽象,具有极高的可玩性。实践中,我们在正常完成 Vue 插件的编写后,通过扩展 Slidev 的入口函数即可完成插件整合:

1
2
3
4
5
6
import { defineAppSetup } from '@slidev/types'

export default defineAppSetup(({ app, router }) => {
  // Vue App
  app.use(YourPlugin)
})

除了对 Slidev 进行扩展时可以使用 Vue 来进行,我们在编写 PPT 内容的时候也可以借助 Vue 的 MVVM 模式对 PPT 内部的事件进行监听、对内容进行动态渲染,实际上这也是 Slidev 动画机制的实现方式。

未来可期

本文仅介绍了 Slidev 的一些基础功能,实际上这款工具的现有功能已非常强大: - 代码高亮、Latex 、流程图、动画等基础功能一应俱全。 - Slidev 服务端底层基于 Vite 实现,因此用户可以以 Vite 的开发模式对 Slidev 进行深度自定义扩展。 - Slidev 制作的 PPT 可支持 Monaco Editor 编辑器,也就是说你与 PPT 的交互方式可以从单调的鼠标点击升级为动态修改内容。 - 基于 WebRTC 的录制功能,演讲视频录制再也不用麻烦 OBS 登场了。

前有 Marp 后有 Slidev ,这些开源项目的创造者们为 PPT 制作方式的细分领域提供了强大的基础工具。『重新定义 PPT 』或许是标题党,但相信在合适的场景下,这些工具一定能为你的工作带来切实的便利。

故障处理SOP是废话吗?

2021-08-12 09:43:25

作为《空中浩劫》的忠实观众,我常常能看到飞行员在遇到各种险情时,只要还有时间和精力,就一定会拿起手边的操作手册(新机型应该已经电子化了)按指引逐条查找来尝试找到问题的标准解决方式。这份手册就是本文的主题,即 SOP (标准作业程序)。

虽然在航空业中,要么飞行员能够依靠 SOP 化险为夷,要么调查人员也常常会证明按照 SOP 操作很可能就能避免事故,但在互联网行业,SOP 虽常常被人提及和编纂,却很少能发挥出很大的作用,这又是什么原因呢?

翻译代码与滞后性

在航空、医疗等行业,SOP 的制定者是飞机或器械的研发产品团队,而阅读者则是具体的使用者,也就是说其中包含了一种较强的供需关系且两者是独立的。而在互联网领域,除去基础组件面向全公司提供的 SOP 符合这一性质,在业务团队中, SOP 的制定者与使用者却常常是同一批人。

而在制定故障处理 SOP 的编写目标时,很多团队希望这份手册能包含业务内所有逻辑的故障应对方案。这其实是种非常理想化的想法,原因就在于再简单的业务接口,其内部涉及的逻辑也至少是乘以十计的,这也就导致越细致的 SOP 就越相当于对代码的翻译。

与之类似的是理想化的单测覆盖率尤其是 TDD 模式的单测,业务运行不依赖单测,但单测与业务逻辑是强绑定的,修改业务逻辑就要协同修改单测。但故障 SOP 又与单测不完全相同,文字总结甚至系统化的 Wiki 没有 CI Pipeline 的限制,研发对代码进行修改时很难被强制要求更新 SOP ,时间一长,很容易就会出现类似于技术文档的滞后性。

正如稳定性手段一般,过期的故障处理 SOP 不仅很难帮助你,反而更有可能会害了你!

故障处理SOP的积极意义

虽然故障处理 SOP 有着很大的局限性,但在互联网和软件领域,它仍具有一定的积极意义:

  1. 加强版新人培训:只要给足团队新成员时间,理解业务、做好需求对于大部分人来说都不是问题。但故障的处理是需要经验的,此前工作经历再丰富的新人,面对一个复杂存量系统的故障时也会显得手足无措。

  2. 减少次生灾害:很多次生故障的产生都是由于操作人员处理故障时的慌乱导致的,而有了 SOP ,处理者在如禁用机器、调整路由策略等常规操作上更有底气,配合和其他成员的交叉检查即可在很大程度上避免次生故障的发生。

  3. 处理思路补位:好的故障处理 SOP 不会仅仅包含处理步骤,而会结合过往的事故,给出系统中常见故障的现象和处理思路,这对故障处理者是至关重要的。就像考试时我们有时会出现提笔忘字的情况,给处理者一份答案提示,便能为他的故障处理策略制定提供很大的帮助。

如何制订一份有点用的SOP

从上文可以看出,故障处理 SOP 并不是毫无用处,但也不可能覆盖所有问题。那么一份『有点用』的故障处理 SOP 应该具有哪些特点呢?

便于搜索

我看过很多公司和部门的故障预案,大多是在自己的 Wiki 系统中建立树状目录,而 SOP 内容则全部位于子节点页面中。

这样的文档布局看似非常清晰,很适合向 leader 作为『成果』汇报,但对使用这份手册的人来说则是灾难性的:一边是影响线上的故障,一边是使用深度搜索或广度搜索遍历各个子节点查找某个 SOP 是否能为自己提供帮助,这样的 SOP 到底是在帮助处理者还是在拖慢处理者呢?

因此编写最终面向故障处理者的 SOP 时,要么做到将内容平铺到同一份文档中,要么提供全文搜索功能,让慌乱的处理者不会因为需要查阅结构复杂的 SOP 而变得崩溃。

思路比手段更重要

故障处理 SOP 告诉我这样做,可这么做是对的吗? 按照 SOP 的步骤执行到一半,怎么现象和 SOP 说的不一样?

对于常规故障,像机器禁用等简单操作按照步骤逐步执行无可厚非。但我们知道,严重的故障很少是单个问题直接触发的,却往往是某个数据或逻辑有问题成为故障的根本原因,而更多的直接原因则常常是多个其他流程或系统由于各类问题产生故障或稳定性手段失效。这仍然与航空事故非常相似,《空中浩劫》中的 NTSB 调查员常挂在嘴边的也是类似的话。

而在复杂故障下,简单的步骤教程是没有意义的,因为阅读者根本无法决定应该选择哪个步骤指南来解决问题。所以好的故障处理 SOP 不仅要包含常见的操作步骤指南,更应该包含故障的处理思路,这些思路应该从各种历史故障或系统薄弱点出发,至少给出以下内容:

勤于故障演练

『勤能补拙』在故障处理领域并不是『鸡汤』,与其指望帮助有限的故障处理 SOP ,更有效的是通过无损或低损的故障演练来训练故障处理者的情绪控制能力、思考方式和操作熟练度,并为处理真实故障积累经验。不过虽然『混沌工程』的理念已经被提出多年,但很多公司的故障演练仍然是在过家家。对于大部分公司和团队来说,故障演练的有效落地仍然有很长的一段路需要走。

Gracias Messi

2021-08-07 05:46:06

时间回到 2009 年,年少无知的我在某天体育课后与朋友聊天,他说『梅西真强』,我说『梅西梅西肯定没戏』。但那时的我没有想到,未来的我会将这位『没戏』的梅西作为神一般的偶像。

Young Messi

两年后的 2012 年,在高中教室里不务正业的我正在偷偷翻看手头上刚刚从同学那借(抢)来的《足球周刊》,此时的我已经完全被巴萨的 Tiki-Taka 传控所征服,拉玛西亚完美中场的默契配合让人过目难忘。但更让我折服的是那名绿茵场上总能带给人无限惊喜的小个子前锋——梅西,而这一赛季梅西的俱乐部进球总数达到了惊人的 73 粒并又一次夺得了金球奖。

Messi 2012

然而那个赛季巴萨没能进入欧冠决赛,联赛上也早早就被死对头远远落下,仅仅夺得了一个乏善可陈的国王杯。当时还在使用贴吧的我,看到无数的人喷梅西散步,说梅西毒瘤,认为六冠王的巴萨走到如今的地步,责任全在梅西身上。正在读高中无缘比赛直播的我将信将疑,一方面是球队的成绩确实差,另一方面是梅西在视频片段中超神表现和完美的过人又让人无法忘怀以至于难以认同这些观点。如今我才意识到,从那时起,我已经完全迷上了这位刚刚摆脱『小跳蚤』外号的球星。

MSN

2014 年,大二的我有了更多的时间来看球,补上了数场梦二年代的比赛录像,也重温了小罗背起梅西的经典画面。我买了视频网站的会员熬夜看球,巴萨的主场几乎一场不落,听着赛后的巴萨队歌入睡。那一年,后起之秀内马尔已经完全融入了巴萨,他和梅西、苏亚雷斯一起组成的 MSN 组合是我有生以来见过的最强的锋线三叉戟,所向披靡、无可阻挡。14-15 赛季这个组合打进了 122 球,再助巴萨成为三冠王。三个人互相让点、踢快乐足球、苏牙和梅西喝玛黛茶没内马尔、梅西如当初小罗一般背起内马尔都成了足球迷们津津乐道的故事。

Messi and Neymar

那时疯传赛季初成绩不好是因为恩里克和梅西因为出场时间问题闹出了矛盾,而后来夺得三冠王是因为和梅西缓和了关系并找到了梅西的使用手册。我深知这很可能只是太阳报之类的小报编出来的小道消息,因为梅西的重要性和能力早就不需要被再次证明,梅西的球技之强已经无人可予以否定,配得上准球王的称号,黑子和喷子们挂在嘴边的也从散步帝变成了缺少国家队荣誉。那一年的梅西,过人、射门、进球、组织进攻样样在行,后撤中场的控球俨然已经有了几年后的模样。

Messi in 2014 World Cup

14 年世界杯上,替补上场的格策在 113 分钟捡漏绝杀阿根廷帮助德国夺得冠军,而带领这只中场实力堪忧、后场老化的阿根廷队一路过关斩将走上决赛赛场的梅西没有像迪马利亚一样哭泣,他只是沉默,他只是不语,他只是想为阿根廷夺得一个冠军荣誉。我常常在想,如果梅西当初没有选择为阿根廷国家队效力,而是和他的拉玛西亚梦三队友一起成为西班牙国家队的成员,那西班牙的冠军会不会来得更容易些、更多些?而梅西是否也早已不再被人称为无冕之王?

Messi and Boateng

那几年提及梅西时,人们除了在球技和荣誉上相互扯皮,还有很多人在好奇另一件事情,那就是为什么球场上的梅西会出现多次呕吐的情况。虽然网友和专家都做了各式各样的原因分析,媒体们也觉得他的身体状况是个定时炸弹,但我永远记得 2015 年的那个凌晨,梅西用『犯罪』般的过人晃倒博阿滕进球,这个进球也拉开了那场比赛闪击拜仁一报『七喜』之仇的序幕。没过几天,同样是一个凌晨,阳光刚刚洒进寝室,我在走廊里与隔壁同为巴萨球迷的朋友激动地庆祝巴萨再一次夺得了欧冠冠军!那时的我们都以为这是一个时代的开启,却没想到这是上个时代最后的余晖。

Xavi's last Barcelona La Liga match

此后的几年,巴萨中场的老化问题日益严重,哈维和伊涅斯塔陆续离开,布斯克茨已不再被称为节拍器反而因转身慢等问题被频繁吐槽,皮克倒是比之前成熟多了,但此后的巴萨再也没拿过欧冠冠军。16-17 赛季,欧冠 16 强对决中,巴萨不可思议地在首回合被大巴黎打入 4 球,赛后发布会上巴黎圣日耳曼时任主教练埃梅里的自信和开心几乎无法掩饰。不久后的一天深夜,闲来无聊的我决定打开电视和回到主场的巴萨一起面对现实。更不可思议的事情发生了,一个、两个、三个……六个!逆转了!无论别人认为这场球存在着什么争议,但在我心中这就是堪比『伊斯坦布尔奇迹』的『诺坎普奇迹』!然而这也几乎是巴萨目前为止在我心中的最后一次高光时刻。

Messi-in-Barcelona

巴萨作为硕果仅存的会员制俱乐部,与同样体制的皇马存在相同的问题,那就是俱乐部主席会为了大选垒高政绩。以政绩作为出发点,对于好的主席来说会合理规划球队的支出和发展进而为球队取得更好地竞技成绩铺路,而对于如巴托梅乌这样的人来说则会导致昏招迭出、滥买乱买。内马尔的出走如今再看的确是个双输的选择,内马尔没能借此坐上足球界的头把交椅,而拿到巨额违约金的巴萨病急乱投医买了一堆工资高、签字费高、违约金高的『三高』球员,却无一能堪重任,与此同时与老将续约时却仍无底限加薪。如今巴托梅乌早已下台,但他的无度挥霍不仅让巴萨的财政在突发的疫情下捉襟见肘,更严重的是更衣室里养了一群工资高却难以发挥真正价值的球员,这些球员的能力大部分并没有多大的硬伤,但他们却实实在在地不适合巴萨的风格。一切的一切最终导致了今天这样的局面:巴萨已然无法解决工资帽问题来续约梅西,无法让这位出场了 778 次的诺坎普国王在巴塞罗那,这座他深爱的城市,这只他效力了 21 年的球队续写传奇。

Messi-In-Copa-America-2020

一个月前,不被看好的阿根廷队击败夺冠热门巴西队,时隔 28 年再夺美洲杯冠军,而梅西仍然是这只球队的队长,仍然为这只球队的晋级之路贡献了 4 球 5 助攻。 34 岁的梅西已经不再年轻,连过数人进球的神迹不知是否还能再见,但如今任意球人墙会为了防守他而安排一名球员倒地拦截,后撤中场的进攻组织也尽显他的完美盘带和超人的球路预判,他仍然是那个能改变比赛的男人!

Messi! Messi! Messi!

在我心中,梅西就是这个时代的球王,是绿茵场上的精灵,无论他身处何处,他都是我心中永远的神。巴萨是你梦的起点,也许纽维尔老男孩会是你梦的终点,但你值得一切!感谢你为巴萨的付出,感谢你在诺坎普的精彩贡献!

Gracias Messi!

Thank you GOAT!

History has been made with you!

History has been made with you

投资、投机与赌博——我的韭菜经历回忆

2021-08-06 09:56:50

今天看到 V 站上的一个帖子,楼主老哥讲述了自己是如何能在数字货币市场通过买卖山寨币赔掉一笔学费的故事,虽然这个故事本身实在太常见了,但当看到他被收割的山寨币列表时,我的心情变得异常复杂,因为这个山寨币就是我在数字货币市场所交的第一笔学费。

本文就来讲讲我作为一颗绿油油的韭菜,在被收割的同时有哪些感悟。

新人最容易产生的幻想,就是觉得通过一番自信操作,自己也能在造福神话中占有一席之地,咸鱼翻生、财务自由和财富自由指日可待。然而实际情况是,买卖绝大部分的金融产品和金融衍生品在较好的情况下只能带来一定比例的存量财富增值,而赚钱的多少是一个非常陡峭的金字塔模型,只有极少数人在能力、运气或认知的组合加持之下才能站在塔尖。试想你在工作中做到行业翘楚了吗?如果在自己擅长的领域尚且不能做到 No.1 ,那么你是如何有自信能在陌生的金融领域飞黄腾达的呢?

在初入数字货币市场时,我已经有了一些基础的宏观经济学知识和少量的股票操作经验,实际上在入场不久前刚刚在黄金和基金上赚了笔小钱。这在当时给了我极大的自信,与黄金基金超低涨幅不同,数字货币市场 24 小时营业,现货单日涨幅 20% 以上屡见不鲜,期货期权市场零准入门槛,低仓位短线快速操作岂不是赚翻了?

然而我忽视了两个重要的问题,那就是『心态』和『交易周期的差异』。

在心态层面,当看到某个山寨币暴涨 100% ,而你在此前已经低仓位少量购入时,赌徒的心理很容易地便会冲破新手的心理防线:不由自主地加仓,熬夜盯着 K 线图盯盘,似乎所获得的收益与盯盘时间是完全成正比的。

而在交易周期层面,我虽然计划短线操作,但数字货币市场尤其是追涨山寨币的交易周期是非常短且难以把控的,而我却以操作 A 股的思想进行卖出点的时间规划却全然不觉得有问题。再加上水涨船高的币价自然而然地会让人产生纠结于『现在不卖会不会跌』和『现在卖了会不会赚得少了』的心理,买卖的操作时间点自然会完全变形。

结果可想而知,在那天我刚打开家门的时候,手机的震动令我心头一惊,随后我就看到了这样的景象:

大跌日K线图

当天的涨幅在几秒内全部跌回并持续下探,第二天继续暴跌。到了第三天,我割了肉,删了相关交流群(难以想象的是,竟然还有维权群,这场零和游戏中,谁又会在意你的权益呢?)。

很显然,在这段交学费的过程中,我的行为丝毫没有投资属性,更不是投机,而是彻头彻尾的赌博。

在交易中,我们首先要学会的就是分清『投资』、『投机』与『赌博』的区别,我们要完全避免『赌博』的心态和思想,警惕『投机』操作,坚持『投资』甚至价值投资。俗话说得好,谁的钱也不是大风刮来的,而相信大风能带来钱的人,也必然会认识到『人赚不了认知之外的钱』,所有赌博和投机行为的思想起点都是对自身认知的过度高估。

如何避免被老代码坑

2021-08-06 07:05:52

我承认有些标题党,事实上写下此文时,我还没有从被老代码坑出故障带来的低落心情中走出来。本文来简单探讨下维护历史代码尤其是历史业务代码时,有哪些原则和思考点可以帮助我们减少被坑的概率。

项目开发流程的现状决定了你本次上线的风险

代码的核心逻辑有单测覆盖吗?单测是基于 TDD 或者 BDD 编写的还是只是打印结果靠人肉 assert ?平时团队的大段代码改动会做好拆分再提 pull request 吗?团队审阅 pull requests 时有没有人认真看内容?代码结构和具体的实现问题有人提出疑问并得到作者解决了吗?QA 团队平时愿意给技术改造做测试吗?QA 团队有设计相对完善的自动化测试用例吗?

如果当你审视这些问题时,有超过 2 项问题的答案都是否定的,那你就要小心了,你改动的可能是一段除了原作者之外其他人都是知其然不知其所以然的代码,改动和上线带来的风险骤增。

永远不要无限信任历史业务代码

我们常常称历史业务代码为屎山,但不得不承认,这些代码如果没人改动,大部分都会本本分分地在线上运行数年。

然而作为历史代码,常常存在交接、奇葩需求的代码设计取舍、不规范的编程语言使用方式和较差的编程习惯。我们常常会体验到看到自己几年前所写代码时的陌生感,同样,历史代码是危机四伏的,可能仅仅一个异常类型的差异,就会给你带来不小的线上事故。因此我们在对历史代码进行修改、或迭代其底层代码依赖时,必须对其设计和逻辑进行逐行地重新审视,对于缺少单测的核心逻辑也要补全测试。不要觉得这会拖慢进度,事实上再快的进度在线上故障的影响下都是苍白无力的。

尝试让测试团队发挥真正的作用

如今国内大部分互联网公司和软件公司仍然在建设测试团队,我们必须要和测试团队做好沟通、拉近关系、讲清利弊,做 CI/CD 系统、搞稳定性建设固然比编写测试用例、测需求更容易晋升和拿好绩效,但这是以系统平稳运行为基础的,长期的忽视基础测试能力最终也会影响测试团队本身的发展。测试向来是个技术活,自测的最大问题就在于你永远也无法发现你默认没问题的问题,测试与研发协力发展才能真正地保障线上质量。

对于历史代码则更是这样,团队几经变革、代码多次易手、文档可能早已滞后,但 CI/CD 流程中的强制自动化用例检测却是提交代码的硬性门槛,只要守住了这个门槛,历史代码的维护总能少些心惊肉跳。

小心!监控可能早已无效

正如当前的稳定性手段在未来却有可能成为稳定性隐患一样,过去帮了团队大忙的监控随着业务的发展可能早已不能满足需求。而对于历史代码来说,年久失修的指标和采集都是一颗定时炸弹。

比起修改历史代码导致的故障,更可怕的是故障已然发生许久,你却没有任何感知,原因就在于历史逻辑的监控可能早已失效或根本就没设置过!

上线之心态

我常常觉得后端服务的上线犹如驾驶飞机,过度的自信总能招致各种问题的叠加酿成惨剧,而谨慎则总能为我们构筑我们能把控的一道底线。无论你是 L8 、P7 还是 2.2 甚至更高的职级,只要你面对的是历史代码且存在上文提到的风险和隐患,上线就总是危险的。端正态度,做好事前准备和事中预案,谨慎总是没错的

简易增强版hexo-theme-even

2021-08-02 04:07:59

Even 主题的简洁深得我心,它也是本博客当前的主题。不过有许多常用功能在原主题中并未直接支持,因此就自己 fork 后动手做了些小的体验优化和功能实现,供有类似功能需要的朋友直接使用。优化的部分思路可以参考前文

当然我只做了很小的一部分优化,主题作者 Yuexun Jiang 的主题代码创建和维护才是最核心的贡献。后续我也会通过 PR 的形式将本文内容中的通用部分提交到原主题中。

下文为增强版 even 主题的中文使用说明。

地址

增强版功能

原主题功能

原主题的所有功能均支持,可查看原主题文档进行配置:hexo-theme-even docs

增强功能的使用

使用 Font Awesome 图标

原主题的图标为作者自定义的 iconfont 图标库,新图标的增加需要作者进行额外支持。

增强版引入了 Font Awesome 依赖,从而实现可以使用 Font Awesome 网站 上的任意图标。

例如,增强版原生支持了 Telegram 页脚社交图标的展示,在主题配置中添加以下内容即可:

1
telegram: <Telegram chat url>

使用不蒜子进行站点统计和展示

添加以下内容到主题配置文件即可开启:

1
busuanzi: true

展示站内热点文章推荐

  1. 安装依赖:npm install hexo-related-popular-posts -S
  2. 阅读该插件的文档以了解如何进行参数配置 hexo-related-popular-posts docs
  3. 添加以下内容到主题配置文件即可开启推荐功能:

    1
    2
    3
    4
    
    popular_posts:
    enable: true
    maxCount: 5
    PPMixingRate: 0.5

页脚添加微信公众号信息

添加以下内容到主题配置文件即可开启:

1
2
3
wxOfficialAccount:
  enable: true
  url: <The QRCode image url>

设置 Twitter Cards

  1. 设置 Twitter Cards 后,在 Twitter 中发送的链接可展示预览信息,详见Twitter cards docs
  2. 添加以下内容到主题配置文件即可开启:

    1
    2
    3
    
    twitter_card:
    style: <See Twitter card docs>
    creator: <Twitter username>

设置 Open Graph

  1. 阅读文档:Open Graph docs
  2. 添加以下内容到主题配置文件即可开启:

    1
    2
    
    open_graph:
    type: <See https://ogp.me/#types>

整站字数统计与展示

  1. 安装依赖: npm install hexo-wordcount -S
  2. 添加以下内容到 hexo 配置中即可开启

    1
    
    word_count: true

在页脚展示站点地图

添加以下内容到主题配置文件即可开启:

1
footer_sitemap: true

开启搜狗和神马搜索的站点验证

添加以下内容到主题配置文件即可开启:

1
2
3
4
5
# Sogou verification
sogou_verification:

# Shenma verification
shenma_verification: 

停止百度推送

增强版默认关闭了百度推送功能,添加以下内容到主题配置文件中即可重新开启:

1
baidu_push: true

修复 Leancloud 计数器

原始实现使用了旧版的 Leancloud CDN ,其中的 API 目前已经 404 了。为保证计数器可用,主题升级和更换了 CDN 地址,同时支持了自定义域名以解决目前 Leancloud 要求中国区应用需要使用特定域名的问题。

1
2
3
4
5
# LeanCloud
leancloud:
  app_id: <Your Leancloud appId>
  app_key: <Your Leancloud appKey>
  server_url: <Your Leancloud domain>

2021常用临时网盘汇总

2021-07-28 07:01:15

当我们希望在网络上面向所有网友分享文件时,我们总希望能找到一款容量适中、上传下载较快的临时网盘(也可以叫做临时文件传输工具)。

这个场景下 Onedrive/Dropbox/Google Drive 虽然具备分享功能,但总是会暴露账号信息,同时国内的朋友访问会存在一定困难,而从各类 Tampermonkey 中各类网盘限制破解插件就可以看出百度云这类国内网盘的分享功能并不好用。本文就来盘点一下截止 2021 年的常用临时网盘(临时文件传输工具)。

临时网盘(临时文件传输工具)的特点

临时网盘的产品形态与普通网盘不同,使用体验优秀的临时网盘通常需要具有以下几个特点: - 容量限制适中:支持 500MB 以上的临时文件分享,基本能满足大部分的临时文件分享需求。 - 速度较快:不一定能跑满带宽,但也不会出现以 KB/s 为单位的下载速度。 - 匿名发布:发布者至少可以做到半匿名发布。 - 下载限制少:不对下载者做注册要求,同时分享次数限制也应较为宽松。 - 过期自动删除:不需要发布者手动维护文件的生命周期,发布时设定好有效期,到期平台自动删除。 - 平台不易跑路:有的临时文件分享的周期是会达到月级别的,所以要保证网盘平台本身相对老牌可靠,不会轻易跑路造成死链。

那么满足这些要求的临时网盘都有哪些呢?下面就来罗列些我体验过的临时文件传输工具,并以上述几点特性作为评测出发点。

汇总

城通网盘

地址

https://ctfile.com

介绍

非常老牌的临时网盘,老网民应该会在各类奇奇怪怪的网站都看到过他的身影。产品的形态其实与 115 、百度云之类很相似。

登录后才可以上传和分享文件且不支持直接设置有效期,同时下载页面会直接暴露分享者的邮箱。而下载者需要输入密码才可访问且不氪金只能使用普通下载通道。

可见城通网盘基本不具备上述特性,那么这里为什么要将它加入汇总列表呢?原因就是稳定,如果你想找一款跑路概率很低、需要长期稳定分享的临时网盘,那么城通网盘还是值得考虑的。

速度测评

奶牛快传

地址

https://cowtransfer.com

介绍

这款产品刚发布时风靡一时,然而随着其用户协议调整,尤其是对非临时分享功能进行阉割和诸多限制后,奶牛快传的口碑也有了很大程度的下降。不过由于其部署节点的问题,速度非常快,在小范围分享的场景下依然是个不错的选择。

非注册用户可传输 2GB 的内容,分享的临时文件内容可被免费下载 5 次,有效期为 1 天,注册后可以使用更多功能。

速度测评

地址

https://app.tmp.link

介绍

由微林在最近新推出的一款临时网盘产品,功能简单易懂,没有下载量的限制。注册后才可以上传文件,临时文件的大小没有任何限制也就是无限容量,不过不清楚能坚持多久。

值得一提的是,tmp.link 上的临时文件虽然可选择有效期为 24 小时、3 天和 7 天,但当有用户下载了被分享的文件后,临时文件即可被续期,所以理论上只要一直有人下载你的临时文件,这份文件的有效期就是永久的。很适合分享各类小工具和热门数据的场景使用。

速度测评

其他选择

以上是个人使用体验不错的临时网盘,当然很多人对网盘的稳定性没那么多要求但对分发渠道的多样性有着特定的需求。下面再罗列下我收藏的其他临时文件传输工具: - 无限网盘 - 草盘

为何后端限流如此之难

2021-07-25 01:28:04

压测、限流和故障演练是稳定性建设的「三把斧」,然而在实践中,限流方案的设计往往非常复杂,实际效果却通常不尽如人意,这又是为什么呢?

限流是有损的

限流的根本目的是保护己方服务不被打垮,这也就意味着超出阈值的流量会被服务端直接丢弃,这对用户体验来说是灾难性的。最明显的例子当属秒杀场景下的限流,触发限流后用户的正常请求总是被随机拒绝,这也是我们抢购券或商品时需要「拼手速」的根因之一。

而正是由于存在这样的用户体验和调用成功率问题,限流的阈值设定也就尤为重要,阈值配置准确则能在服务可用性和用户体验上找寻到一个平衡点,反之即使保护了服务也仍然算是造成了线上影响。

捉摸不透的限流阈值

限流的阈值虽然如此重要,但往往阈值方案的制定却是「拍脑袋」来定的。

我们通常会根据两个指标来确定阈值,即「单机极限容量」和「集群极限容量」,两者都需要结合日常峰值流量中服务的表现和压测的结论来得出。其中,集群容量的评估并不能简单地以单机容量乘以机器数得出。原因在于评估单机容量时采取的单机压测往往难以体现出 PaaS 层和上下游依赖的容量瓶颈,有可能集群容量达到同样的数值时,应用服务本身仍能运行,但外部依赖就已经先行无法响应了。

另一方面,高仿真的压测是困难的,其难点体现在以下几方面:

因此大部分压测的结论只能作为一个参考,实际设置限流阈值时往往需要配置者根据经验预留出一部分的 buffer 。服务越基础、调用方越多,这个问题也就越严重。

此外,限流的阈值配置并不是件一劳永逸的事情,业务逻辑和受众的调整都有可能导致预期请求量的上升或下降,进而造成原来稳定运行的限流策略失效。

单机限流的局限性

无论大厂小厂,单机限流已俨然成为标配。单机限流简单易用,假设我们确实评估出了服务的单机容量极限,加上单机限流后,我们至少能保证服务在单机层面上可以应对极端流量。

但上面提到限流是有损的,因此对于流量大、业务方众多的服务来说,粗暴的单机限流策略的误伤比例是极高的。而当我们希望按照请求参数、业务方的身份信息来做细化的限流配置来解决这一问题时,往往会发现对于只有几十 QPS 的小业务来说分配到单机的流量配额只有个位数,流量稍有不均衡就会造成误限酿成事故。而如果一段时间没有对业务容量配额进行管理,那么各个业务方的配额总额很可能已经接近或超过了单机容量极限。这时我们就只能选择扩容,但扩容又意味着要对原有的策略逐个进行调整,过程不可谓不麻烦。

因此单机限流更适合只按单机容量极限进行简单配置,作为保障系统稳定运行的兜底策略,而不适合配置精细化的限流策略。

集群限流的困境

目前的集群限流思路一般有如下两种:

  1. 非精确集群限流:原理上与单机限流无异,只是增加了中心化的配置入口,可以自动化地将集群阈值与机器数量进行计算来动态调整单机限流阈值。
  2. 精确集群限流:通常基于 Redis 这类耗时低、TPS 容量高的 KV 实现,不同的限流策略请求 KV 组件的时机也不同。当然现在也出现了基于应用层机器的实现如 Sentinel ,但本质上都是通过中心化的节点来把控是否应对某个请求进行限制。

前者并没有很好地解决单机限流中存在的问题,只是把单机限流的操作简化了。而后者虽然能真正地根据请求特征做到细粒度限流,但无论是哪种实现都会造成一定的性能损失。可见集群限流虽然能真正满足各类限流场景,但目前的技术实现并不适用于所有类型的业务,对于 TP99 只有一两毫秒的业务来说就需要对性能和稳定性进行权衡了。

应用层限流的风险

以上的内容都限定在了应用层的限流。而实际上在突发的秒级大流量之下,即使配置了应用层的限流,大量的请求堆积很有可能会将线程池、TCP 队列、堆(对于 Java 应用)瞬间打满造成服务挂掉,进而导致限流无法发挥出实际作用。

因此我们不能完全依赖应用层的限流,而应该在流量入口层,如处理 HTTP 请求常用的 Nginx 、Service Mesh 架构下的 SideCar 组件中也进行粗粒度、有 buffer 预留的限流配置。这些组件的处理流程耗时非常低、吞吐量很高且常常部署于高配容器或独立宿主机中,能够承载更高量级的突发流量,进而为应用层限流方案补位。

限流不是银弹

精确限流其实是较危险且当前仍缺乏最佳实践的操作,需要有科学的实验、细致的分析和周密的方案,同时需要由底层基础组件提供有效的能力支撑。而当上述条件得不到满足时,过于追求精确限流反而更容易导致事故。

我们必须要认识到限流并不是银弹,也不是稳定性建设的唯一「抓手」,在能力和方案都不能满足精确限流要求的情况下,以粗粒度的限流方案进行稳定性兜底,着手建设弹性扩容、流量隔离等方向的措施才是更明智的选择。

flomo-cli中文说明

2021-07-17 21:45:43

什么是 flomo-cli

这是一款可以在命令行中将笔记和想法保存到 flomo 的工具。 基于 Golang 实现,可通过 Homebrew 便捷安装。 GitHub Repo:https://github.com/MrEasonYang/flomo-cli

功能

安装

从源码编译安装

保证环境中已安装 1.16 版本以上的 Golang ,执行以下命令即可:

1
2
3
git clone [email protected]:MrEasonYang/flomo-cli.git
cd flomo-cli
go build

使用Homebrew安装

在 Homebrew 中输入以下命令即可完成安装。

1
2
brew tap MrEasonYang/taps
brew install flomo

目前支持以下平台: - Apple Intel AMD64 - Apple Silicon - Linux AMD64

手动下载安装

如果不喜欢 Homebrew 或正在使用 Windows 系统,那么你可以访问 Release 下载对应平台的最新版本并手动进行配置。

使用

配置

访问 Flomo 个人配置页面 以获取个人的开放 API ,执行以下命令配置 API 到 flomo-cli 中:

1
flomo set api ${Flomo API}

随后 flomo-cli 将会在用户目录生成名为 .flomo-cli.config 的隐藏文件,该文件的权限为 0600 。

一键保存

Memo 即 flomo 概念下的笔记,只需在各类终端工具的命令行中输入以下命令即可

1
flomo save ${Your memo content}

Shell 管道

Flomo-cli 如常见程序一样,支持以管道的数据重定向内容作为笔记内容,可借助 cat 等命令快速保存文件等内容:

1
cat memo.txt | flomo

编辑器模式

除了直接在命令行中输入,flomo-cli 也支持使用编辑器进行笔记编写和保存,只需要执行以下命令即可:

1
2
3
4
5
6
7
8
# Open vim to compose the memo.
flomo vim 

# Open neovim to compose the memo.
flomo nvim 

# Open emacs to compose the memo.
flomo emacs

目前 flomo-cli 只对 vim/neovim/emacs 进行了支持, 输入其他内容将抛出异常以避免任意执行带来的安全问题。

清理临时文件

编辑器模式的实现思路是在接收到命令时调用指定编辑器对 ~/.flomo-tmp 目录的临时文件进行编辑并一直等待。当用户退出编辑器时停止等待,接着将临时文件的内容作为笔记发送至 flomo ,最后将临时文件删除。 这样一来,如果存在并发调用或强制终止 flomo-cli 的情况,则临时文件的删除工作可能就会被中断,进而造成堆积的临时文件占用磁盘空间。对于这一问题可以执行以下命令一键清理临时文件:

1
flomo clear

设置 alias

为了防止只使用 flomo 单个命令带来的误输入风险,目前笔记的保存操作必须结合 save 关键字来进行。如果你希望简化输入,那么只需要在 zsh/bash 等 shell 的配置文件中新增 alias 即可,示例如下:

1
alias flomo="flomo save" 

贡献代码

欢迎大家通过 PR 的形式来完善本工具或加入新的想法,PR 形式不限,提 PR 前做好 lint 即可。

协议

MIT

静态博客的同步和备份方案

2021-07-11 00:36:49

目前在非工作场景下,我经常会在手头的 MBP 16 和 MBA M1 间切换,切换的时机大概就是按照就近原则,哪台离我物理距离近就用哪台。但由于我的博客是基于 Hexo 搭建的静态博客,所以在写博客时,两台设备间博客配置和文章内容的同步问题就尤为让人头疼。本文就分享下我是如何解决静态博客的多设备同步和备份问题的。

配置的同步与备份

整站

最近我的 MBP 被大雨淋坏后刚刚修好(除了 CD 面外壳,其他部分全部被换新了),这台设备之前被用来作为在公司工作时的主力设备,所以为了满足公司的数据合规要求就没开启 TimeMachine 。现在从工作中退役重新成为我的个人办公电脑后,从零搭建一套和原配置完全一致的本地博客配置会带来不小的工作量。好在我之前对博客本地内容做了定期的整站备份,因此直接拷贝一份稍加初始化即可。所以从经验来看,新建立的备份机制也应该是对本地整站备份才更加方便和安全。另一方面,不同于以前简单的定期备份,我还需要设计一套方便的同步方案,来做到近乎实时的同步,保证跨设备使用的体验。

Hexo 是基于 Node 实现的静态博客系统,这就导致如果直接使用 Dropbox 或 Onedrive 等网盘进行整站同步,那么 node_modules 这类目录的同步成本会非常高,有时甚至会阻塞正常文件的同步。虽然 Onedrive 可以在同步设置中排除指定内容,但由于莫名的原因,我在使用 Onedrive 同步博客的整站目录时出现过冲突导致的同步失败进而需要手动解决冲突甚至丢失文件的问题,因此直接使用网盘的自动同步机制进行同步是不太可行的。

而配置类内容的修改频率不高,同时几乎不会有多台设备并发或并行处理的情况,因此只需要在每次博客发布时能够触发同步即可。所以在这个问题上我决定遵循 KISS 原则,直接使用 GitHub 的私有仓库进行备份和同步。这里选择私有仓库,主要是考虑到有些配置还涉及了一些 appid 和 appsecret 相关内容,不适合公开。

主题

Hexo 的主题有两种安装方式,一种是通过 npm 以依赖的方式安装到 node_modules,另一种是直接 clone 主题内容到 themes 目录下,由于我总会对主题进行微调,所以通常会选用后一种方式安装。

但由于主题目录中本身就包含 git 信息,对整站进行 git 同步就会涉及 submobule 的麻烦问题,主题的静态依赖众多也导致同步量大增,但这些其实与博客的领域并不是直接相关的。因此对于主题,我选择 fork 原主题仓库,将对主题的修改在这个 fork 仓库中独立维护,有修改再提交代码,整站备份时则将主题目录配置在 .gitignore 中,不与整站内容耦合。

需要注意的是,通常主题的配置文件都在主题目录下,但大部分主题的 .gitignore 中都会将 _config.yml 这个配置文件排除以避免误提交,因此我们需要将主题配置文件拷贝到博客根目录并重命名,比如我将现在正在使用的 even 主题的配置文件命名为了 _config.even.yml ,Hexo 5.0 以上的版本会自动按高优先级加载这个格式命名的主题配置文件,算是约定大于配置。如果你使用了其他名称,也可以通过指定 Hexo 配置中的 theme_config 的值为文件名来达到同样的效果。

文章的同步与备份

iCloud同步

博客的核心是内容,跨设备修改最频繁的也是内容,因此将博客内容所在的 source 目录设置为实时同步也就非常有必要。

原本我们使用各类同步网盘就可以很轻松地实现实时同步,然而各类网盘只能同步其绑定目录下的内容,或对 home 目录进行全量同步,而不能灵活选择其他自定义目录。那么软链其他目录到网盘绑定目录是否可行呢?很遗憾,或许是源于操作系统的限制,目前的网盘均无法同步外部链接到网盘目录的软链,直接同步的话他们真的就只会同步软链本身,有点像早年不了解 Windows 的用户拷贝文件只拷贝了快捷方式导致在其他设备无法使用的老梗。

不过我们可以逆向思维,将 Hexo 的 source 目录转移到网盘目录中保存以实现实时同步,随后在博客根目录中再通过软链的方式将网盘中的 source 目录链接到博客目录,这样一来,文件系统在我们预览和发布博客时仍会检索软链指向的文件从而不影响博客的构建:

1
2
3
mkdir ~/Library/Mobile\ Documents/com\~apple\~CloudDocs/Blogs/
mv ./source ~/Library/Mobile\ Documents/com\~apple\~CloudDocs/Blogs/
ln -s ~/Library/Mobile\ Documents/com\~apple\~CloudDocs/Blogs/source source

我的方案直接选用了 iCloud ,尽管显然 Dropbox 的同步能力更强大而且还附有版本管理功能,但目前其免费版只支持三台设备登录(多年不用的我现在才发现这项能力缩水了,之前免费版也是可以同时登录很多台的),同时我的主力写作工具 iA Writer 在 iOS 上对 Dropbox 的支持也不太完整,所以在写作时只使用 Apple 生态产品的我就转为使用 iCloud 了。实际使用体验也还可以,有时会有几秒的同步延迟,至于版本管理功能我们可以交给下文的 GitHub 备份来处理。另一方面,iOS 上的写作工具普遍对 iCloud 支持很好,这也让我们间接实现了跨平台的写作工作流。

GitHub备份

长期以来,我都会把我的文章原文放置到 GitHub 的公共仓库中,备份之余也可供有需要的朋友直接使用,因此这一项备份能力也要加上。另外,上文提到原文内容移动到了 iCloud ,那么数据的一致性和可靠性就要完全依赖这个偶尔出现同步问题、没有版本控制能力的「知名 Apple 网络产品」了,为了避免 iCloud 出问题后的抱头痛哭,加上 GitHub 备份也是极有必要的。

同样,由于只是备份手段,所以我们只需要在部署博客的时候触发就好,触发时将 iCloud 中的博客内容拷贝到仓库目录,自动添加个时间相关 commit 直接 push:

1
2
3
4
5
6
7
8
rm -rf ./github.backup/source
mkdir ./github.backup/source
cp -r source/* ./github.backup/source/
cd ./github.backup
git pull
git add .
git commit -m "$(date)"
git push

另外有的朋友会问,前面不是已经有了整站备份吗,对内容进行额外备份岂不是多此一举?然而我们都知道 Git 对软链的处理也很不好,同时我也有公开原始文章内容的需要,所以这个处理步骤仍然是有必要的。

建立一键脚本

以上就是对静态博客进行同步和备份的方案,但我们每次部署博客或更换新设备都要执行一堆命令,这可太不友好了,所以我们还要写几个简单的一键脚本来简化我们的同步和备份操作。

一键发布

相信每个 Hexo 用户都有这样的一个脚本来快速构建和部署博客,所以这里就不再赘述逻辑了。需要注意的是,与直接构建部署不同,我们还要在部署结束后触发下文的一键同步和一键备份脚本,来实现上文提到的整站和内容的最终一致性备份:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#! /bin/bash
# deploy.sh

# Update site data and configs.
git pull

# Deploy the blog to server.
hexo clean --silent
hexo g --silent
hexo d
hexo clean --silent
hexo g --silent
hexo d --config _config.yml

# Invoke the backup and push logic.
./backup.sh
./push.sh

一键同步

这里的同步指的是上文提到整站备份能力,将备份除了主题和文章内容的所有数据:

1
2
3
4
5
6
7
8
#! /bin/bash
# push.sh

# Push to repo with date.
git pull
git add .
git commit -m "$(date)"
git push

另外为了提交的内容能符合我们的要求,我们还要在博客根目录的 .gitignore 中额外排除相应的内容:

1
2
3
4
5
6
7
8
.deploy_git
themes
public
github.backup
.DS_Store
*/.DS_Store
source
db.json

如果希望不执行部署操作也能在其他设备同步最新配置,则仍需要我们仿照代码开发流程,手动执行 git pull

一键备份

即将文章内容备份到公开的 GitHub 仓库中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#! /bin/bash
# backup.sh

# Backup source from iCloud.
rm -rf ./github.backup/source
mkdir ./github.backup/source
cp -r source/* ./github.backup/source/
cd ./github.backup
git pull
git add .
git commit -m "$(date)"
git push

# Backup the entire blog project.
cd ..
git pull
git add .
git commit -m "$(date)"
git push

一键初始化

有了以上脚本,我们就可以一键进行博客的发布、备份和同步了,不过对于新设备,我们还要整合下以上内容提供个一键初始化脚本,实现「git clone」+「执行一次脚本」即可完成博客配置,减轻我们配置新设备的工作量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#! /bin/bash

# Init source backup.
git clone {public.git.repo.path} github.backup

# Install themes.
git clone {theme-foo.git.repo.path} themes/foo

# Install Hexo and dependencies.
npm install hexo-cli -g
npm install -S

# Soft link real source from iCloud.
ln -s ~/Library/Mobile\ Documents/com\~apple\~CloudDocs/Blogs/source source

# Make shell scirpts executable.
chmod +x pull.sh
chmod +x push.sh
chmod +x deploy.sh
chmod +x backup.sh

记hexo-theme-even主题优化

2021-07-06 09:04:28

最近经常看到 Hugo 用户使用 hugo-theme-even 主题搭建的博客,简洁的外观和明亮的配色很快吸引了我。随手一搜发现 Hexo 下也有同款主题,就马上给本博客换上了。不过对我个人而言,这款简洁的主题的一些小细节不能使我满意,于是就动手做了些小优化。

更丰富的图标库

此款主题使用的是作者自定义的 iconfont 图标库,如其 FAQ 所述,加入新的图标需要作者的额外支持,目前支持如下图标:

这个列表已经很全了,但是好事不嫌多,我们其实也可以如其他主题一样使用图标超丰富且免费的 Font Awesome 来进行扩展。

从 Font Awesome 官网下载完整的文件包,解压后将该目录移动至 even/source/lib 下即可,当然我们也可以选择使用各类 CDN 来节省流量,不过由于主题自带的 iconfont 在本地调试时的加载速度会快过 CDN 进而造成视觉上不一致的观感,所以身为「强迫症患者」,我还是推荐直接本地安装。

随后在 even/layout/_partial/head.swig 中已有的 CSS 引用下新增如下内容:

1
2
3
4
...
{#- Font Awesome -#}
<link rel="stylesheet" type="text/css" href="{{ url_for('lib/fontawesome/css/all.min.css') }}">
...

这样一来我们就可以直接在需要的地方使用 Font Awesome 的海量图标了。例如页面底部的社交图标可以通过主题配置增加如下内容来新增微信公众号和 Telegram 的社交链接:

1
2
3
4
5
6
7
8
9
		...
    {%- if theme.wxOfficialAccount -%}
      <a href="{{ url_for(theme.wxOfficialAccount.url) }}" class="iconfont" title="微信公众号"><i class="fab fa-weixin"></i></a>
    {%- endif -%}

    {%- if theme.telegram -%}
      <a href="{{ url_for(theme.telegram) }}" class="iconfont" title="Telegram"><i class="fab fa-telegram"></i></a>
    {%- endif -%}
		...

支持站点访问统计

这项功能我直接使用不蒜子来实现。在主题配置中新增 busuanzi 选项并配置为 true ,在负责计数的 even/layout/_script/counter.swig 中引入不蒜子的依赖:

1
2
3
4
5
...
{%- if theme.busuanzi -%}
  <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
{%- endif -%}
...

最后在 even/layout/_partial/footer.swig 中加入如下内容即可:

1
2
3
4
5
6
7
	...
  {%- if theme.busuanzi -%}
    <span style="display: block;">
      <span id="busuanzi_container_site_pv">本站总访问量<span id="busuanzi_value_site_pv"></span>次</span>
    </span>
  {%- endif -%}
  ...

去除百度链接提交

不知为何,此款主题默认开启了百度链接提交的功能且未提供配置项,页面加载时会尝试加载 https://zz.bdstatic.com/linksubmit/push.js ,如果想关闭这一配置只需要在 even/layout/_script/push.swig 中的最外层加入一层 if 判断即可,修改后的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{%- if theme.baidu_push -%}
    <script id="baidu_push">
    (function(){
        var bp = document.createElement('script');
        var curProtocol = window.location.protocol.split(':')[0];
        if (curProtocol === 'https') {
            bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
        }
        else {
            bp.src = 'http://push.zhanzhang.baidu.com/push.js';
        }
        var s = document.getElementsByTagName("script")[0];
        s.parentNode.insertBefore(bp, s);
    })();
    </script>
{%- endif -%}

给文末加个小广告

虽然影响阅读体验,而且我个人也不喜欢微信公众号的封闭,但最近还是打算给文章的结尾都加上个微信公众号的二维码,当然目前大部分主题都没有直接支持,需要额外开发。

而对于本款主题,首先可以新增模版 even/layout/_partial/_post/custom-footer.swig ,这样我们就能在这里使用 HTML 随意定制文末的样式。

随后在 even/layout/_macro/post.swig 中的版权模块下方引入 custom-footer.swig 即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
		...
    {%- if not is_home() -%}
      {#- Post Copyright -#}
      {%- include "../_partial/_post/copyright.swig" -%}

      {#- Custom footer -#}
      {%- include "../_partial/_post/custom-footer.swig" -%}

      {#- Reward -#}
      {%- include "../_partial/_post/reward.swig" -%}
    {%- endif -%}
 		...

让文章标题更显眼些

我有个不太好的写作习惯,那就是文章的大章节标题会使用一级标题(对应到 Markdown 就是 # ),这就导致在本款主题的默认样式下,文章标题与章节标题傻傻分不清楚。

对于这个问题,作者在 even/source/css/_custom/_custom.scss 提供了 CSS 重写的功能。但由于懒得去找对应的 class 及其 CSS 定义,我直接简单粗暴地选择使用 !important 来强制覆盖:

1
2
3
.emphasized-title {
    font-weight: bolder !important;
}

接着在 even/layout/_macro/post.swig 中为 h1 标签新增这个 class 即可:

1
2
3
4
5
6
7
8
9
			...
      <h1 class="post-title emphasized-title">
        {%- if is_home() -%}
          <a class="post-link" href="{{ url_for(post.path) }}">{{ post.title }}</a>
        {%- else -%}
          {{ post.title }}
        {%- endif -%}
      </h1>
      ...

总结

总体来说这款主题选用了 hexo 主题实现中常见的 Swig 和 Sass ,对于主题场景来说比之前使用的 hexo-theme-icarus 的 React 实现要好上手得多。同时目前该项目还在不断接受 PR ,并一直有小幅迭代,未来还是值得期待的。后面我也会把本文涉及的改动通过 PR 或 fork 的形式整理出来供需要的朋友直接使用。