再看 NekoBox:迁移、重构、展望

再看 NekoBox:迁移、重构、展望

技术 4017 字 / 8 分钟

NekoBox 匿名提问箱于 2020 年 3 月上线以来,至今已有五位数的注册用户并产生了六位数的提问。

这个数据大大的出乎了我的意料,要知道 NekoBox 从未对外公开宣传过,纯靠用户间口口相传。我很喜欢这种无心插柳柳成荫的事情,自己默默做得事情能够被看见,对我来说是很幸福的事。

说来惭愧,一直以来我都是“放养式”运营,每次只有自己手头的工作不忙了,才会登录上兔小巢看一下用户的反馈,然后将一些恶性 Bug 或实现起来简单的需求给做了。很感谢使用 NekoBox 的各位能包容我的懒惰,依旧不离不弃。🧎🏻

本文记录了近期 NekoBox 迁移与重构时踩过得坑,以及我对 NekoBox 的定位与后续展望。技术向的内容会有些多,不感兴趣可以直接跳到文末。

2023.02.23 那天发生了什么?

NekoBox 最开始是我 2020 年花三天时间写出来的,作为一个小玩具部署在我的国内服务器上,并且使用我个人的备案域名。网站可以使用任意的邮箱注册,并不需要用户输入手机号并验证实名,这其实是不符合我国《互联网信息服务管理办法》的。

但当时抱有一定的侥幸心理,想着提问和回答都接了云服务商的文本内容审核 API,违规评论都会被拦截,再加上自己也没对外宣传这个站,应该不会有问题。

但这在无形中给我埋下了一个大雷。

2023 年 2 月 23 日上午 11 点左右,我在家接到了网信办的电话,对方说有人在 NekoBox 发布违法言论,我作为站长,需要配合调查。当时我吓坏了,马上光速注销备案 + 关站,并认真配合警察叔叔的工作。

最后好在我没有利用 NekoBox 进行盈利,且我事先也接了相关文本审核的功能,在配合工作提供了相关材料后,这件事便告一段落了。还好没留下什么案底,已经是万幸了。

事后复盘发现,那名用户使用谐音和表情符号绕过了文本内容审核功能。这让我意识到机器审核 API 也会有严重的漏报,但一方面因为成本原因又无法做到每条信息都接人工审核服务。

互联网不是法外之地!

这件事给我的打击挺大的,我最初的想法是将网站代码开源出来,大家能够一起共建,可惜 GitHub 上一直没有多少贡献者,还被炸弹人给爆破了。原先的国内网站下线后,我收到了很多用户的反馈,纷纷询问站点怎么无法访问了,甚至还有一位网友因为 NekoBox 了解到了我的技术博客,受到触动也开始尝试建站写博客。能成为他人的光真的是很开心的事。

因此后续 NekoBox 便迁移到了境外服务器,并且没再使用备案域名了 —— 正如 v2ex、Go 语言中文网等站点那样。希望它能在广袤互联网的一角,继续安静地存在下去。

云原生迁移

抒情的话聊完了,该聊点技术了。

NekoBox 部署在境外的 2C2G 轻量服务器上,由于配置的原因,只能使用 Docker Swarm 进行粗糙的服务编排调度。每次需要更新线上版本时,都是 GitHub Actions 通过 SSH 连上服务器,再执行 docker service update 命令。

我想将 NekoBox 接入现有的 K3s 集群,使用 GitOps 实现更好的版本管理和平滑更新。我的 K3s Master 节点位于腾讯云上海区域,经测试发现腾讯云东京区域的线路比中国香港区域好些,因此买了台 2C4G 的境外东京区域的机器,作为 NekoBox 新的部署机器。

关于如何跨地域甚至是跨云组件 K3s 集群,这篇文章介绍的很详细:《基于K3S和zerotier-planet实现跨云搭建K8S集群》

首先使用 ZeroTier 将不同可用区的机器加入到同一 ZeroTier 网络中,这样在 K3s 看来它们就在同一内网里了。

zerotier-node-list

节点均开启 IPv4 Forwarding 后,修改 Master 节点 /etc/systemd/system/k3s.service 中的 K3s 参数,显示指定 node-ip 为节点在 ZeroTier 中的 IP,并设置使用 ZeroTier 的网卡:

ExecStart=/usr/local/bin/k3s \
    server --node-ip 10.243.xxx.xxx --flannel-iface ztcxxxxxxx --flannel-backend=host-gw \

同理,修改 Worker 节点 /etc/systemd/system/k3s-agent.service 的 K3s 参数:

ExecStart=/usr/local/bin/k3s \
    agent --node-ip 10.243.xxx.xxx --flannel-iface ztcxxxxxxx \

重启各节点上的 K3s 服务,Lens 连上 Master 节点能在 Worker 节点启动 Node Shell 并访问 Worker 上的 Pod 日志,Worker 节点能请求到其他节点上的 Service,说明就配置成功啦~

我们再给 NekoBox 的节点加个 region=jp 的污点,防止集群里的其它服务被调度过来。

goldpinger

后续还推荐装上 goldpinger,它会建一个 Daemon Sets,在每个节点上放一个 Pod 来监测节点间的连接状态。

goldpinger-web

官方仓库里虽然提供了 Helm Charts,但比较敷衍,可扩展性差,建议是自己把有用的部分扒出来直接 GitOps 写 YAML 创建资源。官方仓库里还提供了 Grafana Dashboard 定义,导入 Grafana 后可以很直观的看到节点之间的连接延迟:

goldpinger-grafana-dashboard

数据库 MySQL -> Postgres

2020 年写 NekoBox 那会,我还很菜(虽然现在也很菜),数据库只会用 MySQL。

尝试过 Postgres 后发现真香,就想着把 NekoBox 从 MySQL 迁移到 Postgres。但 NekoBox 在线上跑着,随时会有用户访问,发布新的提问和回答往数据库插入新的数据,此举无疑是在边开飞机边换引擎。

社区的 pgloader 是一个很好用的 Postgres 数据迁移工具,但它只支持全量迁移,并不支持增量同步。换句话说我需要先给线上的 NekoBox 停机防止有新的数据写入,迁移数据,再将后端数据库配置改到新的库上。受限于老的 2C2G 服务器的性能,我需要对 pgloader 进行限速,停机迁移全量数据的时间可能会很长。

阿里云 DTS

如果要实现不停机迁移,则需要在完成全量迁移后,再将全量迁移这段时间内的增量数据,也同步到新库中。在调研了市面上几个数据库同步产品后,最后我选择使用阿里云 DTS 来完成。(这里不得不吐槽下我司,腾讯云的 DTS 产品只支持 MySQL 系之间的数据同步,不支持异构数据库,还得加强呀!)

将服务器添加为阿里云数据库网关 DG 节点后,即可在 DTS 控制台选择使用数据库网关接入非阿里云的源库与目标库,配置完后启动任务即可。

aliyun-dts

等全量迁移完了就会开始一直跑增量写入任务了,此时可以在线上写一些数据,来检查数据同步的情况。

阿里云 DTS 的坑

但是阿里云在让我失望这件事上从来没有让我失望过。

我发现阿里云 DTS 居然把 MySQL tinyint(2) 类型迁移成了 Postgres smallint 类型,而非 bool 类型!这导致 GORM 在 AutoMigrate 时直接报错了!这一点在 pgloader 中专门有一条 tinyint-to-boolean 规则进行适配:

As MySQL lacks a proper boolean type, tinyint is often used to implement that. This function transforms 0 to ‘false’ and anything else to ’true’.

提工单问了客服,客服只会照本宣科给我发产品文档链接…… 我要的是怎样解决问题,不是你告诉我产品该怎么用。

更抽象的是,阿里云 DTS 怕不是根本没什么人用,产品文档中记录的“库表列名单个映射”功能,前端的树形组件下拉是有 Bug 的,如果直接全选了整个库,则无法再细化各表的字段映射配置。

这导致不看文档,用户自己是不会知道还有这功能的。但这个字段映射也只是配置目标字段的名称,并不能修改映射类型。

我的解决办法是在迁移完后,观测到线上流量低后,关闭 DTS 同步,线上服务停机,Postgres 数据库执行 SQL 修改字段类型。

ALTER TABLE "nekobox"."questions"
ALTER COLUMN is_private TYPE BOOLEAN 
USING CASE 
    WHEN is_private = 0 THEN FALSE 
    ELSE TRUE 
END;

ALTER TABLE "nekobox"."censor_logs"
ALTER COLUMN pass TYPE BOOLEAN 
USING CASE 
    WHEN pass = 0 THEN FALSE 
    ELSE TRUE 
END;

修改类型的 SQL 执行的很快,就当我以为已经全部搞定的时候,我发现阿里云 DTS 这垃圾东西居然不会迁移 Postgres 自增序列! 这意味着每一张表的ID 字段都不会自增并自动赋值,插入数据就会报错说 ID 字段为 NULL。

没办法,赶紧执行 SQL 手动加序列……

-- 1. 查看当前最大 ID(先确认数据)
SELECT MAX(id) FROM "nekobox".users;
-- 2. 创建序列并设置起始值(假设最大 id 是 1000)
CREATE SEQUENCE "nekobox".users_id_seq START WITH 1001;
-- 3. 将序列绑定到 id 列
ALTER TABLE "nekobox".users 
ALTER COLUMN id SET DEFAULT nextval('nekobox.users_id_seq');
-- 4. 将序列的所有权给表(表删除时序列也删除)
ALTER SEQUENCE "nekobox".users_id_seq OWNED BY "nekobox".users.id;

还得是阿里云,能整出这种狠活来,真牛!😅

如果再给我一次机会,我会选择自己写一个工具,先记录 MySQL 数据库 BinlogID 或 GTID,然后调用 pgloader 进行全量数据迁移,再从记录的 BinlogID/GTID 处开始增量同步添加数据。

现在 NekoBox 已经迁移到了 Postgres,凌晨 3:30 开始迁移,4:00 完成。线上运行了一天 Trace 里没看到有报错,感觉是没问题了。

前端重构

NekoBox 的前端使用 UIKit 组件库。这个组件库的风格我十分喜欢,扁平简单,美中不足的是它真的就只是一个 CSS + 一点点 JavaScript 的组件库。社区里有人开发了 vuikit 组件来将其接入 Vue 生态,但这个项目的最后一次 commit 已经是五年前了,并且还未适配 Vue3。

因此 NekoBox 的前端一直是以服务端渲染的形式呈现,稍微复杂一点的交互或者异步加载,则会使用 Alpine.js 实现。渐渐的,我发现它已经无法支撑起后续复杂的前端需求了。我写前端的经常会想:“这些响应式交互,Vue 来了可以全秒了。”

我开始尝试将 UIKit 的 CSS 引入 Vue3 项目中,发现它比我想象中的好用。由于 UIKit 大部分情况下只是在原生 HTML 标签上加上了 CSS 样式,因此我大可不必像 vuikit 那样将按钮、文本框之类的封装为 Vue 组件,直接在原生 HTML 标签用 class 指定样式即可。页面也比我想象中的少很多,因此只花了一个周末的时间就完成了 80% 的前端 Vue3 + 后端 RESTful API 的重构工作。

骨架屏

前后端分离后,由于后端部署在境外,请求 API 难免会慢一些。这里我用了 vue-loading-skeleton 来给页面加载时加上骨架屏,防止页面未加载完时布局塌陷。这个组件做得还行,可以自动识别插槽里的元素自适应调整加载的骨架元素大小。

灰度

新版的前端我不敢直接全量上线,想先小部分用户测试下。最简单的办法是将前端部署在例如 next.n3ko.cc 这样的子域下,但会导致后续主站全量上线时,子域的链接还得做重定向兼容。

复杂一点的话,在集群里搭个 Istio 服务网格来实现细粒度的流量转发,但看了下机器的配置,还是算了…… 最后简单粗暴的在 Cloudflare 上配置了回源规则:当请求 Cookies 里带 next-beta=1 时,则将请求转发到源站新版前端的端口上。后续在线上加个按钮,点一下就给 Set-Cookie 即可切换到新版前端,去掉 Cookie 就切回来。

展望

我有问过自己,NekoBox 对我而言意味着什么?

我并不指望靠着它能够发家致富,我认为 NekoBox 是一块让我实践产品运营的“试验田”。参加工作以来,我基本没有做过对性能和服务可用性有很强要求的东西,更没有什么 To C 的经验。这既是好事,好在我不用随时 on call;也是坏事,坏在我没有那些项目经验和教训。

因此我想借 NekoBox 这个用户量还算不少的平台,亲身去实践开发和运营一个产品,去踩那些前人踩过的坑,去体验边开飞机边换引擎的惊险。因此 NekoBox 的项目经历其实也一直被写在我的简历里,作为一个还算成功的 Side Project 被我拿来跟面试官吹逼。😂

对于 NekoBox 的用户,很感谢他们能包容我的“放养式”运营,更是感谢那些还会不定期支付宝打赏的朋友们。我仅通过兔小巢这个渠道接收用户的反馈,并没有尝试组建 QQ 群之类的方式,是因为我认为每个人的圈子不同,年龄和性格也不同,求同存异会比较困难。我也很害怕跟别人起纷争或者冲突,所以还是继续维持现状吧。

关于之后的更新计划嘛,等新版前端稳定后,我想先完善一下基建方面,例如 Tracing 由 Uptrace 切到更专业的腾讯云 APM,完善项目的开发和部署文档,之后就可以开始做用户反馈中提到的暗色主题、多语言、表情评价,甚至是聊天等功能。先把 flag 立了,后面慢慢填坑哈哈哈。

谢谢老板 Thanks♪(・ω・)ノ


喜欢这篇文章?为什么不打赏一下呢?