基于 Caddy 蓝绿部署实现异构博客的随机主题架构
泰裤啦!现在我的博客花里胡哨的。
我一直苦于找不到一种优雅的方案来实现 hexo & hugo & astro 的多主题配置计划。
(无须修改主题、低配置文件、节省资源)
现在我想到了一种基于容器挂载卷 + Caddy 负载均衡实现的轻量级多主题配置方案。
前置知识
- caddy
- hugo
- hexo
碎碎念
在之前,我通过:
- 用多个 docker 容器实例分别部署主站 + 从站的方式
- 通过修改主题代码实现各种的跳转到外部
- 用主站引导 + 网关路由
的方式实现个人网站的部署,但是我注意到一个问题。
每次我更新文章的时候,需要往指定从站上传,再在主站发一篇文章用来引导。
好吧,我承认这真的很蠢。那个时候我的设计重心在于如何将 wordpress、hugo、hexo 这种异构的站点部署在一起。
借着这次抛弃 wordpress 站点的机会,我又开始折腾我的博客了。
最初,我找到了一个新的 wp 主题 puork,我觉得这简直是 wp 上最优雅的主题。
久而久之我的热情就褪去了,wp 逃不了笨重的问题,而我写博客纯粹是给自己看的,不存在所谓的社交互动。
而且 puork 作为 wp 主题虽然已经足够简化了,但是它的配置界面谈不上优雅。
各种红点、推广和莫名其妙的专业配置让我看的糟心,我终于注意到了它臃肿的一面。
bye ~ Wordpress,是时候离开我的博客架构了。
我从今年开始使用 hugo,hugo 真的很快,而且我找到了我崭新的白马主题 hugo-theme-stack
哪个程序员能拒绝这么快的博客架构呢 ?
可行性猜想
在抛弃了 wordpress 以后,hugo 和 hexo 的博客都是基于 md 语法生成的,这意味着网站内容格式实现了统一。
hugo 和 hexo 的主题作者,都将 post 路径作为渲染目录。
所以我们完全可以将一个纯粹的 markdown 库挂载为存储卷。
对于不同的主题格式,他们通常都是通过 front-matter 实现的参数注入,我只需要在每次写文章的时候。
把主题需要的参数一股脑的录入即可,即使遇到兼容问题,我也可以以主站为准。
我有一些担心,hugo 和 hexo 的目录管理结构可能会导致一些问题。
hugo-theme-stack 使用 page bundles 做内容管理,直接将 title.md 直接丢进 /content/_post 是否可行呢 ?
答案是可行的。
我将 page bundles 形式直接复制到 hexo,也是可行的。
https://www.nihilx.com/post/email-architecture-analysis/
https://www.nihilx.com/email-architecture-analysis/index/
对比一下两者的 url,我心中有了一个大胆的想法:
同一篇文章(同一个 .md 文件)是否可能在网关实现用户无感的随机渲染(用不同的主题实例)呢 ?
这也是这片文章诞生的根本原因。
使用 permalinks 配置实现内容 url 对齐 / 标准化
-
问题 1 :如果是 page bundles 格式,hexo 理所当然也会连带 index 路径复制下来。
-
问题 2 :hexo 默认会用日期加入 url 的前缀。
-
问题 3 :hugo 的路径带了 /post/ 前缀
前缀都不是问题,因为 hexo 和 hugo 都提供了 permalinks 这一配置项。
https://hexo.io/zh-cn/docs/permalinks
https://gohugo.io/configuration/permalinks/
但是 page bundles 末尾的 /index/ 怎么解决呢 ?
我想要保留 page bundles 的收纳特性。
我相信 caddy 一定可以做到,我寄希望于 Caddyfile 并开始着手实际测试,并尝试解决问题 1。
基于 caddy + cookies 在网关实现状态约定
这一过程还是很有收获的,经过我 3 个小时的调试、测试和学习。
恶补了 Caddy 的各种 matcher / directives / syntax …
实现了基于 Caddy 网关会话的简单状态机,具体思路如下:
在入口处,基于 uuid 为每一个请求分配状态,在 cookie 实现会话级的持久化。
在尝试初期,我希望 hugo 和 hexo 的 url 异构能够更好的解决,我的想法是根据状态来重写 url。
令人抓狂的是,在 hugo 的 page bundles 特性下,hugo 多出的 /post/ 是头,而 hexo 多出的 /index/ 是尾巴。
而且,多个主题下还需要考虑静态资源混乱的问题,起初我甚至没有发现这个 bug。
当时我希望随机主题的交互覆盖能够最大化:在主题 1 访问文章,网关随机代理到其他主题。
(最初我没有引入网关状态)
直到我逐渐发现不对,我意识到了一些问题,并且打开了 cloudflare 的开发模式,让所有请求都走源站了。
我释怀了,原来是 CDN 在帮我们做缓存,这也让我选择用 cookie 做一个状态。
OK,我后来想起来可以在网关做缓存控制 Cache-Control: no-cache
经过漫长的 debug ,我发现交互覆盖最大化的想法是错误的,这实现起来一点儿也不优雅。
我退而求其次,决定在网关设计一个基于 cookie 的状态:
- 初始化时,根据 uuid 在客户端写入状态
- 通过路由实现简单 api,随机更新状态
这样一来,网关只需要负责判断状态,解决了静态资源混乱的问题。
客户端请求时,html 总是最先的,因此在请求 html 的时候,我们就决定好未来一段时间的服务端状态。
处理 url 尾部 /index/ 解决 page bundles 格式兼容
让 hexo 兼容 hugo 的 page bundles 格式,只需要解决 hexo 实例的 /index/ 尾巴。
1@is_kratos vars {final_theme_type} kratos
2reverse_proxy @is_kratos {
3 import kratos_service
4 @not_found status 404
5 handle_response @not_found {
6 @loop_check {
7 path /old-archives/posts/*
8 path */
9 not path */index/
10 }
11 header Location {scheme}://{host}{path}index/
12 respond @loop_check 301
13 }
14}
15
16@is_stack vars {final_theme_type} stack
17reverse_proxy @is_stack {
18 import stack_service
19 @not_found status 404
20 handle_response @not_found {
21 @loop_check {
22 path /old-archives/posts/*/index/
23 path_regexp @title /old-archives/posts/(.*)/index/
24 }
25 header Location {scheme}://{host}/old-archives/posts/{re.title.1}index/
26 respond @loop_check 301
27 }
28}
虽然实现了,但是会有问题,如果请求的路径不是一个合法的文章路径,这段逻辑就有冗余。
而且就在这时,我又注意到一些问题,不仅仅是 page bundles,文章自身携带的素材该怎么解决。
在我原本的设想中,每次请求文章意味着客户端生命周期的开始,在这一阶段网关根据状态值来判断渲染的主题。
因此,我需要通过 permalink 来让文章页具备 html 特征,已实现网关的判断。
1@is_html {
2 path /old-archives/posts/*
3 path *.html
4}
5header @is_html +Set-Cookie "theme={final_theme_type};"
考虑到防止在搜索界面、分类界面等非文章界面跳转,我直接将 /post/ 作为区分用的入口路径。
模板之间使用不同的 Tag plugins 会让 hexo 报错
我查询了相关资料,发现不同版本的 hexo,不同主题自带的 tag plugins 如果 hexo 检测不到会导致渲染停止。
即 { % something % },这类语法。许多主题都有自己的 tag plugins 语法。
marked:
disableNunjucks: true
禁用 hexo 自己的 Nunjucks 渲染即可,大多数主题都会自行渲染,实在不行只能取消了。
设计失败:放弃实现异构级别的文章级随机主题
我休息了一段时间,吃了顿饭。
我大概花了一整天在这个无聊的事情上,我从下午 4 点,直到现在,现在是 23:58 。
10 个小时,我意识到这个伟大的念头大概是无法在对主题源码无侵入下实现的。
PS:我甚至在后来又花了一个晚上的时间,来尝试这个实现,现在我浪费了 18 个小时了。
我终于意识到,网关本身设计是无状态的,我正在作困兽之斗。
我虽然通过 cookie 实现了网关与客户端的一个弱状态:
即每次请求文章 dom 的时候,更新状态
但是我在上文中,写了一个机制,来解决 hugo 和 hexo 在 permalink 尾部异构的问题。
也就是说,当这个机制生效的时候,网关会去额外请求一次文章。
那么如果我把更新状态设置在请求文章,直接会导致两次请求后状态错位。
也就是说,我不能将文章地址,作为状态更新的触发帧。
(假设一个页面有 30 个资源,即客户端会请求 30 次,每 1 次为 1 帧,第 1 帧必然是 dom)
稍微分析一下就可以知道,由于这个不确定的机制,可能会浪费整个客户端状态周期的 1 帧。
(可能会浪费掉客户端的第 2 帧,但是触发了机制,服务端打开了新的状态周期,客户端不知道服务端更新了一个新的周期,即错位)
如果不加逻辑判断或者是计数器之类的 loop-check ,这一设计永远也不可能实现。
这样的设计显然是冗余的、不优雅的、缺陷的。
所以我退求其次,实现了会话级别的异构随机主题。
会话级别的随机主题
放弃了文章级别的状态周期以后,由于不需要再考虑异构问题,非常简单。
最终实现如下:
1fluid_service) {
2 to localhost:32301
3}
4(icarus_service) {
5 to localhost:34001
6}
7(kratos_service) {
8 to localhost:34002
9}
10
11www.nihilx.com {
12 header * X-Robots-Tag "noindex, nofollow"
13
14 reverse_proxy :31313
15
16 map {cookie.theme} {theme_type} {
17 fluid fluid
18 kratos kratos
19 icarus icarus
20 default {uuid}
21 }
22
23 map {theme_type} {final_theme_type} {
24 fluid fluid
25 kratos kratos
26 icarus icarus
27 ~^[0-4] fluid
28 ~^[5-9] kratos
29 ~^[a-f] icarus
30 default fluid
31 }
32
33 @no_theme {
34 not header_regexp Cookie "theme=(fluid|kratos|icarus)"
35 }
36
37 header @no_theme +Set-Cookie "theme={final_theme_type}; Path=/;"
38
39 handle /old-archives/set-random-theme {
40 map {uuid} {new_theme_type} {
41 ~^[0-4] fluid
42 ~^[5-9] kratos
43 ~^[a-f] icarus
44 default fluid
45 }
46 header +Set-Cookie "theme={new_theme_type}; Path=/;" defer
47 redir {scheme}://{host}/old-archives/
48 }
49
50 handle /old-archives/debug {
51 respond "Raw:{cookie.theme} | Type:{theme_type} | Final:{final_theme_type} | {uuid}"
52 }
53
54 handle /old-archives/go-home/ {
55 redir {scheme}://{host}
56 }
57
58 handle /old-archives/* {
59 @is_root path /old-archives/
60 header @is_root Cache-Control "no-cache"
61
62 @is_fluid vars {final_theme_type} fluid
63 reverse_proxy @is_fluid {
64 import fluid_service
65 }
66
67 @is_kratos vars {final_theme_type} kratos
68 reverse_proxy @is_kratos {
69 import kratos_service
70 }
71
72 @is_icarus vars {final_theme_type} icarus
73 reverse_proxy @is_icarus {
74 import icarus_service
75 }
76 }
77}
docker run
一些临时笔记
1docker run -d --name hugo_i2 \
2-v hugo_v2:/app \
3-v markdown_note:/app/content/post/ \
4-p 34002:443 \
5-e baseURL=https://www.nihilx.com \
6-e port=443 \
7hugo-docker:local
8
9# volume hexo_v2 should be complete new (not exists)
10docker run -d --name hexo_i2 \
11-v hexo_v2:/app \
12-v markdown_note:/app/source/_posts/ \
13-p 34001:4000 \
14hexo-docker:local
15
16docker run -d --name hexo_i3 \
17-v hexo_v3:/app \
18-v markdown_note:/app/source/_posts/ \
19-p 34002:4000 \
20hexo-docker:local
21
22docker run -d --name hugo_i2 \
23-v hugo_v2:/app \
24-v markdown_note:/app/content/post/ \
25-p 34003:443 \
26-e baseURL=https://www.nihilx.com/old-archives/ \
27-e port=443 \
28hugo-docker:local
29
30npm install --save hexo@^7.1.1 hexo-util@^3.2.0 inferno@^8.2.3 inferno-create-element@^8.2.3 bulma-stylus@0.8.0 deepmerge@^4.3.1 hexo-component-inferno@^3.1.2 moment@^2.30.1 semver@^7.5.4 hexo-renderer-inferno@^1.0.2