2024-05-05 00:24:00
五一假期期间,我发布了 PicImpact 的第一个公开版本,是 Kamera 的下一代产品,使用 Next.js 开发,而 kamera 是用的 Nuxt3 开发的。前端的 SSR 框架,并不像后端的框架那么好用,后端的各种中间件、开发模型、面向切面编程(AOP)、统一异常处理等等,这些可以高度抽象且降低重复劳动的东西,基本上是别想太多。
Next.js 从 v12 开始 Beta 中间件,到我用的 v14 版本,已经趋于稳定了。但是它有一个最致命的限制:Middleware only supports the Edge runtime. The Node.js runtime cannot be used. 在这一点上,Nuxt3 的中间件,是要比 Next 好用许多的。但 Nuxt3 也有很多不好的地方,有太多的官方模块,迟迟没有推出。就比如我在实现用户登录的时候,要研究写入 cookies 的最佳实践,当然我最后还是选择了用 pinia 来手动管理。
我用 Nuxt3 和 Next.js 写了一些开源和 private 项目,就目前给我的感觉而言,双方的路线还是有很大的差异化的。没有说谁最好,但是选型时还是可以根据需求和对应框架提供的能力来决定,况且 Vue 和 React 生态都有我喜欢但另一边没什么平替的库。
这篇博客,基于我开发过的一些项目(PicImpact),来梳理大概的流程和实践,不管你有没有实际开发经验,当你想上手时,都会帮你绕过一些坑。
阅读前其实希望读者对 React 和 JavaScript 有基本的了解,组件、道具、状态和钩子之类的。文章可能会涉及到下面这些知识:
实践部分,主要基于 PicImpact 来写。
我们可以看到,首页的布局,采用了 2 大块布局,在 Header 部分,会在服务端渲染路由部分,点击不同的路由,跳转到不同的页面。在这里只有第一次是服务端渲染,后续的路由渲染会变为客户端渲染,这是因为 useRouter
和 useParams
都是客户端组件,要在客户端进行 active 的判断,而初次服务端渲染,可以保证首屏体验最佳。
在布局的设计上,我采用了 Route Groups,用来区分不同的业务布局,而不是单纯的父子嵌套关系。这点在 Nuxt 那边的体验就好点,Next 会更加考验你对业务的抽象能力。
获取形式主要分两大类:API(通过 api 请求 server 或者第三方服务)、数据库查询。
API 是应用程序代码和数据库之间的中间层,一般在客户端请求 API 层,然后获取数据,避免直接把数据库暴露给客户端。这在前后端分离的场景中很常见,前端请求接口,后端提供业务逻辑和数据库交互能力。
注:Next.js 中,常把 API 叫做 Route。在后端我们常说的接口,在 Next 中就是一个特殊的 route,是一个在服务器上运行的 API 层,它可以处理 req 和 res。
在 Next.js 中,我们也可以用 “服务器操作” 的形式来获取数据,也就是常说的 RSC。
export default async function Page() {
const getData = async () => {
'use server'
return await fetchDatabase()
}
const data = await getData()
return <main>{data}</main>
}
这样可以不用写 “接口” 也能直接查询数据库,并且不会暴露到客户端。
讲到这里,就不得不介绍 SWR 了,一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略。这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),最后得到最新数据。
先来看一个最简单的原生的 fetch 请求:
async function getData() {
const data = await fetch('https://baidu.com')
.then((res: any) => res.json())
console.log(data)
}
在真实的业务场景,我们必须得知道每个异步请求的状态,对吧?因为要根据状态显示不同的 UI,比如在请求时,页面上显示 Loading...
,结束请求后正常渲染结果。用专业点的话说就是,每一个异步请求,都是一个独立的状态机,每个都会在不同的状态中流转:
在 SWR 中,有一个 isValidating
状态:
isValidating
都会变为 true
。isLoading
会变为 true
。看到区别了吗?没错,实际场景中,isLoading
通常放在第一次渲染用,而 isValidating
适用于已经有数据时的自动更新 / 静默更新场景。
我开发时,由于业务并没有分这么细,所以偷懒直接
isLoading && isValidating
了。
你看哈,如果每一个异步请求都要维护状态,是不是很麻烦?所以 SWR 就很好的解决了我们的痛点,并且它还有其它更好用的 api。
const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, options)
key
: 请求的唯一 key string(或者是 function /array/null)fetcher
:(可选)一个请求数据的 Promise 返回函数options
:(可选)SWR hook 的配置选项对象
const { data, error, isLoading, isValidating, mutate } = useSWR('https://baidu.com',
(url: string) => fetch(url).then((res) => res.json()));
我们用 useSWR 改造之前的 fetch 请求,可以看到,url
直接用作了状态机的 key
,同时也可以作为参数传递给 fetcher
,fetcher
也正是请求函数。
为啥我上面把 key 叫做状态机的 key 呢?因为咱们可以在客户端的任何组件中,获取全局配置,来 mutate
触发状态机的状态,重新获取数据:
import { useSWRConfig } from 'swr'
export default function Page() {
const { mutate } = useSWRConfig()
return (
<Button
onClick={async() => await mutate()}
>
更新
</Button>
)
}
这是我觉得最好用的功能之一,同时解决了代码复用和组件 props 传递的烦恼。同时我们也可以利用自动重新请求机制来获得更好的用户体验,这就区别于 mutation
的手动控制了:
const { data, error, isLoading, isValidating, mutate } = useSWR('https://baidu.com',
(url: string) => fetch(url).then((res) => res.json())
, { refreshInterval: 1000 });
这里的 refreshInterval
用于控制定期重新请求。
图中的模式,表示在开启 keepPreviousData
选项并设置预设数据时,请求数据,再改变 key 值,并在之后进行重新请求的场景。
可以看到,我们改变了值并重新加载,也可以保留之前的数据。并且在分页场景下,点到下一页时,仍旧会保留上一页的数据,这样避免了数据的重复获取,也提升了用户体验。
文档中有推荐用 error.tsx
来捕获意外错误,并显示错误页面给用户,但我不是很喜欢这种方式(我写的错误页面不好看...),所以我常用下面的方式尽可能处理错误:
const { data, error, isLoading, isValidating, mutate } = useSWR('https://baidu.com',
(url: string) => fetch(url).then((res) => res.json()));
if (error) {
// 处理错误
}
export async function Page() {
try {
const findAll = await db.$queryRaw`
SELECT
*
FROM
"public"."Images"
`
} catch (error) {
throw new Error('查询错误');
}
}
身份验证框架,我选择了 Auth.js,它抽象化了管理会话、登录和注销以及身份验证其他方面所涉及的大部分复杂性。挺省事儿的,唯一不爽的地方,就是 middleware 在 vercel 上只能在 Edge 运行时跑,真的烦。
用 prisma 来迁移表结构就不多说了,这里主要介绍 Next.js 的 Instrumentation,可以在启动新的 Next.js 服务器实例时,将调用该函数一次。
import { PrismaClient } from '@prisma/client'
export async function register() {
try {
if (process.env.NEXT_RUNTIME === 'edge') {
return
}
const prisma = new PrismaClient()
if (prisma) {
await prisma.$transaction(async (tx) => {
// ...
})
console.log('初始化完毕!')
await prisma.$disconnect()
} else {
console.error('数据库初始化失败,请检查您的连接信息!')
}
} catch (e) {
console.error('初始化数据失败,您可能需要准备干净的数据表,请联系管理员!', e)
}
}
可访问性(Accessibility)是指设计和实现每个人(包括残障人士)都可以使用的 Web 应用程序。这是一个涵盖许多领域的广阔主题,例如键盘导航、语义 HTML、图像、颜色、视频等。
在开发时,我们可以用 next lint
命令来检查项目中的可访问性问题,在 Next.js 中包含了一个 eslint-plugin-jsx-a11y 插件。同时记得添加 aria-label 标签。
得益于 React 和 Next.js 的强大生态,以及之前进行 SSR 开发的经验,这次在设计 PicImpact 时,算是少走了不少弯路,它有着更好的设计、更优的性能,我在保留之前大部分功能的同时,进行了更多良性的可拓展设计。
当然这个项目也还有很多不足的地方,Next.js 本身也是一个很考验代码抽象能力的框架,更何况还用了 typescript。
最后感谢你能看到这里,有想法欢迎与我交流!
2024-04-03 11:43:00
PicImpact,分享你和世界!. Contribute to besscroft/PicImpact development by creating an account on GitHub.
2023-12-19 23:55:00
自从白嫖了 Oracle Cloud 的云服务之后,一直使用的 24 G 大内存云服务器跑的 Kubernetes,也有 2 年多了吧?(记不清了)上一版本的方案,可以看我这篇文章👉Dynamic k8s 集群实现方案
至于为啥我要更换方式呢?主要还是要部署的东西日渐增加,内存是越来越不够了。原先的方案是相当于备份,假设当前使用的集群挂了,那么直接指向另一个集群就能马上恢复服务,那么服务不可用的时间,主要还是 DNS 解析变更时间。(这里放上之前的图片,便于理解)
今天要聊的新的架构,则是下图所示:
虽然依旧不用担心数据丢失问题(数据库和部分存储全部在集群外部),但是相比原来的可用性方面略有下滑,毕竟集群从原来的多个,变成了现在的一个。但好处是获得了更大内存的集群,这样就可以充分利用原来因为资源不够而不太好上的一些功能,比如蓝绿部署、金丝雀发布,以及用来模拟正式环境的流量镜像(Traffic Mirroring)功能,这样我在本地开发时,也有足够的内存来通过 kt-connect 在集群内调试服务了。
而这次我选择了更轻量级的 K8S 版本 ——K3s,也是想节约一下资源。这样也正好能接入更多我在其它云厂商的大大小小的服务器了,官方的要求是最低 1 核 512 MB 内存。当然,我们也可以部署高可用 K3s,本文为了方便演示,就用一个控制平面节点,两个工作节点来演示。
期待的架构模样已经描述好了,接下来我们开始实践部署!
机器环境如下,准备了 3 台相同配置,但分别在 3 个不同国家 / 地区的云服务器,且无法通过内网连接,只有公网 IP:
OS: Debian GNU/Linux 11 (bullseye) aarch64
Host: KVM Virtual Machine virt-4.2
Kernel: 5.10.0-26-arm64
Shell: bash 5.1.4
CPU: (4)
GPU: 00:01.0 Red Hat, Inc. Virtio GPU
Memory: 24003MiB
首先,需要更改每一台主机名称:
hostnamectl set-hostname xxx
3 台主机更改完毕后,名称为:
kube-japan // 主节点
kube-seoul
kube-chuncheon
其次,我们需要在每台机器上安装 wireguard:
apt-get install wireguard -y
首先我们需要在主节点上安装一个功能齐全的 Kubernetes 集群,它包括了托管工作负载 pod 所需的所有数据存储、control plane、kubelet 和容器运行时组件。
curl -sfL https://get.k3s.io | K3S_CLUSTER_INIT=true \
INSTALL_K3S_EXEC="server" \
INSTALL_K3S_CHANNEL=v1.27.8+k3s2 \
sh -s - --node-label "topology.kubernetes.io/region=japan" \
--node-label "master-node=true" \
--tls-san <主节点公网IP> \
--advertise-address <主节点公网IP> \
--flannel-backend none \
--kube-proxy-arg "metrics-bind-address=0.0.0.0"
kubeconfig 文件会被写入到 /etc/rancher/k3s/k3s.yaml
,由 K3s 安装的 kubectl 将自动使用该文件。
K3S_CLUSTER_INIT=true
将安装为集群模式。INSTALL_K3S_EXEC="server"
表示安装为 Server 节点。INSTALL_K3S_CHANNEL=v1.27.8+k3s2
表示安装的版本。--node-label "topology.kubernetes.io/region=japan"
设置节点的 label。--node-label "master-node=true"
设置为主节点。--tls-san
和--advertise-address
要填主节点的公网 IP。--flannel-backend none
因为我们等会儿要用 Kilo,所以这里要关闭 fiannel CNI。
详细请参考环境变量
kubectl get node
节点是 NotReady
状态,别急,我们安装了 Kilo 就行了。
kubectl annotate nodes kube-japan kilo.squat.ai/location="japan"
kubectl annotate nodes kube-japan kilo.squat.ai/force-endpoint="<公网IP>:51820"
kubectl annotate nodes kube-japan kilo.squat.ai/persistent-keepalive=20
location
定义节点的位置,Kilo 将尝试从 topology.kubernetes.io/region 节点标签推断每个节点的位置。force-endpoint
定义节点的端点,因为咱们的服务器位置位于不同的云提供商或不同的专用网络中,则端点的host
部分应该是可公开访问的 IP 地址或解析为公共 IP 的 DNS 名称,以便其它位置可以将数据包路由到它。persistent-keepalive
持久保活注释参数配置,想了解可以看 WireGuard 文档
kubectl apply -f https://raw.githubusercontent.com/squat/kilo/main/manifests/crds.yaml
wget https://raw.githubusercontent.com/squat/kilo/main/manifests/kilo-k3s.yaml
注意 kilo-k3s.yaml
文件,咱们需要添加以后参数,这里我们采用的是 Full Mesh 模式:
- name: kilo
image: squat/kilo
args:
- --kubeconfig=/etc/kubernetes/kubeconfig
- --hostname=$(NODE_NAME)
- --mesh-granularity=full # 添加这一行
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
kubectl apply -f kilo-k3s.yaml
然后咱们应该可以看到,节点状态变为 Ready
了。
首先咱们现在 Server 节点上获取 server token
:
cat /var/lib/rancher/k3s/server/node-token
curl -sfL https://get.k3s.io | K3S_URL=https://<主节点公网IP>:6443 \
K3S_TOKEN=<主节点 server token> \
INSTALL_K3S_EXEC="agent" \
INSTALL_K3S_CHANNEL=v1.27.8+k3s2 \
sh -s - --node-label "topology.kubernetes.io/region=chuncheon" \
--node-label "worker-node=true" \
--kube-proxy-arg "metrics-bind-address=0.0.0.0"
这里的参数不需要过多解释了,可以往上翻翻。
kubectl get node
kubectl annotate nodes kube-chuncheon kilo.squat.ai/location="chuncheon"
kubectl annotate nodes kube-chuncheon kilo.squat.ai/persistent-keepalive=20
另一台也是同样的操作,注意参数的值略有不同!!!
至此,我们就安装配置完整个集群了,然后我们查看整个集群的网络拓扑图:
kgctl graph | circo -Tsvg > cluster.svg
现在咱们已经搞定了集群了,再来部署集群的 Web 面板吧,这里我选了 UI 好看点的 KubeSphere。
kubectl apply -f https://github.com/kubesphere/ks-installer/releases/download/v3.4.1/kubesphere-installer.yaml
kubectl apply -f https://github.com/kubesphere/ks-installer/releases/download/v3.4.1/cluster-configuration.yaml
kubectl logs -n kubesphere-system $(kubectl get pod -n kubesphere-system -l 'app in (ks-install, ks-installer)' -o jsonpath='{.items[0].metadata.name}') -f
安装完毕后,访问使用 IP 端口的方式访问面板,可以看到集群节点状态:
既然咱们集群配置好了,自然是把面板配置成域名访问最好不过了。这里就要借助 Kilo 和 Cloudflare Tunnel 来完成了,对于 Ingress-Nginx Controller 的控制,我们采用 cloudflare-tunnel-ingress-controller 来实现。
你至少得会用 Cloudflare 吧,既然都会玩 K8S 了,这对你来说应该不成问题。
helm repo add strrl.dev https://helm.strrl.dev
helm repo update
也可以直接用 KubeSphere 操作,见下图:
helm upgrade --install --wait \
-n cloudflare-tunnel-ingress-controller --create-namespace \
cloudflare-tunnel-ingress-controller \
strrl.dev/cloudflare-tunnel-ingress-controller \
--set=cloudflare.apiToken="<cloudflare-api-token>",cloudflare.accountId="<cloudflare-account-id>",cloudflare.tunnelName="<your-favorite-tunnel-name>"
cloudflare-api-token
你得自己去 Cloudflare 配置令牌,记得要下面三个权限:
Zone:Zone:Read
Zone:DNS:Edit
Account:Cloudflare Tunnel:Edit
cloudflare-account-id
你用的那个域名的账户 ID
,也是在 Cloudflare 控制台获取。your-favorite-tunnel-name
这个是通道的名称。
安装完成后,你应该能看到下面的 Pod:
接下来,咱们只需要创建对应的 Ingress,将面板通过 Cloudflare-tunnel 公开到互联网就好了:
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: dashboard-via-cf-tunnel
namespace: kubesphere-system
finalizers:
- strrl.dev/cloudflare-tunnel-ingress-controller-controlled
spec:
ingressClassName: cloudflare-tunnel
rules:
- host: example.com # 你要公开的域名
http:
paths:
- path: /*
pathType: Prefix
backend:
service:
name: ks-console # 指定的 Service 名称
port:
number: 80 # 指定的端口
如果你懒得创建 yaml 来执行,也可以:
kubectl -n kubesphere-system \
create ingress dashboard-via-cf-tunnel \
--rule="example.com/*=ks-console:80"\
--class cloudflare-tunnel
然后我们就可以在 Cloudflare 的控制台看到对应的隧道里面多了一条 Public Hostname
数据,说明已经成功了,现在可以直接通过域名访问了。
通过 Cloudflare-tunnel 来访问网站,可以搭配 Zero Trust 来实现网关配置(DNS、防火墙策略、流量出口策略等),配置身份提供商组、IP、设备、证书或服务令牌访问,以及监控等功能。
虽然这套方案可以通过一系列措施来保证高可用性,但我还是决定将数据存储配置成外置存储,不放在集群内部。这样一旦集群遇到问题,我就可以通过 YAML 文件在短时间内恢复,前提是数据库部分也得可靠。
参考资料:
2023-08-31 16:12:00
最近在设计开发嗯学英语项目,今天想简单讲点儿故事,一个关于英语学习软件背后复杂的技术设计思考,以及我打算如何实践,让它以何种方式运作。
英语类学习软件我用过不少,使用时间最长的,那自然是多邻国了。你看你学了这么久英语了哈,如果现在要你来做一款英语学习软件,你要如何设计(doge
一句话总结,就是查询单词信息,记录用户学习状态。看着很简单对吧?相信我,如果有人这么跟你提需求让你实现,还跟你说很简单,你一定想拍死他。
开个玩笑哈哈,现在我们来详细分析一下业务:
首先我们得让用户选择学习哪一个词库的,我们就拿《考研英语词汇》来说吧,一共 4533 个词。
在用户选择词库后,我们可以生成一条该用户对这个词库的学习汇总数据,也就是哪些单词用户 “学习” 了,总共学了多少个等等。
然后用户在软件上获取单词进行学习,在学习完成后,数据将会进行入库操作。这里的入库操作就涉及到比较多的点了,比如咱们要记录单词用户是否 “学过”,也就是说不管题目是答对还是答错,都算学过。然后就是单词的进度统计,总共多少个,学了多少个了。以及把用户做错的也记录下来,形成错题本。
这里有必要给大家解释一下,为什么会需要记录学习数据。相信很多小伙伴应该都听过艾宾浩斯遗忘曲线,而我们让用户学习,也不可能一个请求就给一个单词,那样不仅效率低,而且服务器也会绷不住。正确的做法是,生成一个 List,相信用 Excel 背过单词的小伙伴是会深有体会的。因为某个单词用户学习了,入库肯定是会做时间记录的,结合用户的行为,以及错题本,就可以根据一些 “记忆算法” 来生成 List。由于这个功能只是整块业务的一小部分,这里就不作展开了。
通过这个开发中的初版 UI 草图,我们可以理解一下,以上的功能映射到用户端的大致模样。
梳理完业务功能后,首先我们要设计数据库表。在消耗了几杯咖啡,挠掉了一些头发后,我设计了如下的数据库表结构(当然,开发迭代的过程中也是会做出调整的):
CREATE TABLE `enstudy_user_book_dict` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '用户 id',
`book_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '词典id',
`book_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '词典名称',
`studied` int NULL DEFAULT NULL COMMENT '已学词数',
`action` tinyint NOT NULL DEFAULT 0 COMMENT '用户使用状态:0->停用状态;1->使用状态',
`creator` bigint NULL DEFAULT NULL COMMENT '创建者',
`updater` bigint NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`del` tinyint NOT NULL DEFAULT 1 COMMENT '逻辑删除:0->删除状态;1->可用状态',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户活动词库表' ROW_FORMAT = Dynamic;
CREATE TABLE `enstudy_user_work_actions` (
`id` bigint NOT NULL AUTO_INCREMENT,
`word_id` bigint NOT NULL COMMENT '单词id',
`user_id` bigint NOT NULL COMMENT '用户 id',
`book_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '词典 id',
`state` tinyint NULL DEFAULT 0 COMMENT '学习状态:0->未学;1->已学',
`creator` bigint NULL DEFAULT NULL COMMENT '创建者',
`updater` bigint NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`del` tinyint NOT NULL DEFAULT 1 COMMENT '逻辑删除:0->删除状态;1->可用状态',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户学习行为表' ROW_FORMAT = Dynamic;
CREATE TABLE `enstudy_user_wrong_word` (
`id` bigint NOT NULL AUTO_INCREMENT,
`word_id` bigint NOT NULL COMMENT '单词id',
`user_id` bigint NOT NULL COMMENT '用户 id',
`book_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '词典 id',
`fail_count` int NULL DEFAULT 0 COMMENT '错误次数',
`creator` bigint NULL DEFAULT NULL COMMENT '创建者',
`updater` bigint NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`del` tinyint NOT NULL DEFAULT 1 COMMENT '逻辑删除:0->删除状态;1->可用状态',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户错题本' ROW_FORMAT = Dynamic;
现在咱们来看看,整套流程是怎么走的:
就是一个很典型的 Spring Boot 单体架构服务,对吧?用户端操作,请求到后端服务,然后后端服务操作数据库,并返回。有一点需要注意,服务本身是 “无状态” 的,所有的状态咱们都在数据库和分布式缓存中进行维护,否则的话就没法舒适地扩展了。
这里的 “无状态” 指的是,服务端的无状态认证(Authentication),属于 “会话状态” 的讨论范畴,不要弄混淆了。因为我把用户的状态写入了分布式内存中,而不是当前服务的缓存中,所以不会因为扩展了多个实例而导致状态隔离。
那么到这里为止,咱们就把核心业务给设计完了。
由于嗯学英语并不是离线客户端模式,所以还是很依赖于云服务的。我们不可能说,开发一个软件,就只能让一两个人用,人一多就崩溃对吧?那么我们这里做个假设,我们的用户跟多邻国一样多(可以压测模拟),这时候应该如何调整架构来支持呢?
首先说一下目前嗯学英语的整个技术栈和架构,然后我们以此为基础代入思考。
前端:Nuxt 3、Vue 3、TypeScript、Naive UI、TailwindCSS
后端:Spring Boot 3、OpenJDK 17
数据库及中间件:MySQL、MongoDB、Redis
CI/CD:GitHub Actions、Kubernetes
我的云服务资源能拿来演示的没那么多,我这里就单个服务分配 512 MB 内存来进行演示
在我们开始进行压测后,不难发现单台服务根本顶不住多少 “用户” 同时使用。好在我们一开始设计时,就考虑到了扩展的问题。我把服务部署在了 K8S 里面,而且嗯学英语服务是无状态的,那么基于 Service(服务)和有状态副本集(StatefulSet)来完成有序的、优雅的部署和扩缩,只需要增加容器组副本数量就行了。
这样一来,客户端不需要关心它们调用了哪个后端副本,通过快速扩容,我们也能容纳更多的请求,现在能够支撑上 w 用户同时使用了。
在压测了一段时间后,我们可以发现瓶颈来到了数据库这边。数据库 CPU 占用高、连接数过高(too many connections)、wait timeout 等问题全来了。
我们知道,MySQL 的最大连接数是有一个上限的,但是呢,我们不可能直接设置为上限的那个数,需要根据服务器的 CPU、内存大小等来设置一个相对合理的值。我一般默认 210,最大 1000(别问我能不能再大点儿,得加钱!!!)。
数据库咱们没法像服务那样直接更改部署副本大小就行,要考虑的因素特别的多,毕竟数据才是值钱的东西呢!这里场景的方案就是上集群,一个主实例(读写,也可以多个主实列)和多个辅助实例(只读),这里贴一张 MySQL 文档中的图片:
反映到嗯学英语架构中来,应该是这样的:
初次在 K8S 部署学习 MySQL 集群的话,可以试试 Bitnami 的 MySQL Chart,基于这个入手会更好!
在实际开发中不可能一句上集群就能上的,说起来轻松的事儿,只适合面试吹牛。作为一名开发,还是得先从代码设计层面去解决问题。仔细回想一下,单词数据、错题本之类的数据,是不是都是读多写少的场景?那么我们就没有必要每次都去数据库读了,而是读取之后做缓存,在每次更新数据之后,再更新缓存,这样能降低一些数据库的压力。
这个一般微服务场景下会比较好做一些,要么基于 K8S 的微服务,要么 Spring Cloud,由于我们的嗯学英语前期采用的是 Spring Boot 单体项目,如果用这个方案的话,是需要对架构进行调整的。但是我觉得多邻国肯定不是单体服务,所以还是像拿出来讲讲。
咱们前面不是扩容了很多服务嘛,那么假设每一个服务,我们都分配了最大 50 个连接数的数据库连接池。理论上来说,服务副本数量越多,会占用的数据库连接数也就会变多。
有一句很有名的话,“计算机科学中的所有问题都可以通过另一个间接层次来解决”,据说是大卫・惠勒说的。把这个思路引进来,我们可以发现增加服务副本,是为了缓解核心业务里面对于数据库的 “相同” 操作,那么我们可不可以把这些操作抽象出来,封装成 “间接层” 呢?那当然是没问题的了!
在加入了代理服务之后,就能极大地缓解这个问题了。注意哈这一套在微服务里面要好做一些,单体服务弄这个的话,维护起来相对麻烦。
在解决了上述问题之后,假设某一天嗯学英语突然爆火了,10w+ 的用户全部跑来试用(咱们增加压测线程来模拟),咱们的服务器还是崩啦!
虽说大部分场景下,每个用户对数据库中同一条数据的操作相对来说并不多,但是也没法保证出现死锁或者锁竞争的问题。我们的业务,也是在每次用户操作发送请求后,直接入库,等待操作完了之后,才返回的。虽然是第一次做,但是我们可以分析下多邻国看看有没有什么思路。
多邻国在每次完成一个单元的测试后,都会有一个 “短暂” 的结算动画,甚至有时候去个人档案页面,看到的数据统计和成就,并不是刷新好了的,可能要等个 1~3 秒左右。这类学习类 App 对于用户来说,是具有很高的容忍度的,配合上动画,反而体验看上去也很不错。那么在这中间缓冲的一点时间,就可以让后端服务 “慢慢地” 去入库就行了,用户感知也就没那么明显了,只需要最大程度的保证数据能保存下来就行。
这里可以在代理服务里面用线程池去处理,或者是用消息队列,然后让代理服务去消费它。在嗯学英语的架构中,我会选择使用 RabbitMQ(消息队列中这个我用的最熟,选其它的也没问题),原因也很简单:我需要能优雅的管理代理服务的集群副本的增加和减少,不能直接更改数量就什么都不管了,而引入消息队列的话,我会省心一些。
消息队列的引入,不仅异步写入降低了一些负载压力,同时也能极大的降低死锁和锁竞争出现的概率。出现这个问题,本质上还是太多的请求同时都来操作这一条数据导致的,我们让请求进入队列,就能避免啦!引入消息队列后,代理服务现在就成为了 “消费节点”,我们依旧可以通过线程池或者增加副本的方式,来提升 “消费能力”。
除了架构层面,我们在开发时,在代码层面也需要多多注意,应避免全表扫描对表中的所有行记录进行加锁。很多小伙伴开发时可能暂时不会考虑这么多,但是功能完成后,我还是建议看一看执行计划(EXPLAIN)。因为有时候光看 SQL 不一定看得出来,而且不同的数据量执行的 SQL,执行计划也可能差别很大。同时也要对 Slowest queries 进行监控,有剑不用和无剑可用那不是一回事儿。
这就是我对嗯学英语核心业务的思考、设计和改进的一个过程了,当然也有很多应该做的东西也还没做,但实际上 “黑天鹅” 事件真出现了,也是很难顶得住的。后面继续开发迭代的过程中,我可能也会做出很多调整,等开发完了开源出来,可能也会跟本文所讲的有很大的偏差。
毕竟还有很多内容没考虑进来,比如中间件都挂了怎么办?我要怎么对每一个链路进行 “观测”(以及错误追踪)?线上出了问题怎么告警(当然是咱们的老朋友 Prometheus 和 Grafana 啦)?怎么查询日志(ELK 太重了,每个 Pod 都打开看也麻烦)?依托于 K8S,我能以低成本去构建出满足这些的嗯学英语,从而更多时间能放在代码层面去思考。
最后感谢你能看到这里,有想法欢迎与我交流!
2023-05-19 08:30:00
五一前后抽时间维护了一下个人云服务,把集群全部重构了一部分。那几天访问的朋友们应该能看到一个简单的 “维护页面”。新的个人云架构,这里就不多讲了,毕竟不是本篇的内容,感兴趣的朋友可以看我之前文章(老架构)。这次改进了之前的诸多不合理的地方,顺便把 IP 和证书全部更换了一遍。
本文主要针对两类读者,一是 Cloudflare 的重度用户,在此前提下教你如何 “针对性的优化和防护”;二是没用过或刚接触 Cloudflare 的读者,当然,在阅读本文之前,你最起码要使用过云服务器,也能够理解云服务中的相关概念。
如果对 Linux 和云服务不太熟悉,又或者是对 CDN、HTTPS、DNS 之类的网络技能稍有欠缺,建议你买一个便宜的云服务器简单学习一下,每个月 4 美刀的足矣。
咱们既然是对 Cloudflare 和 WAF 扫盲,那么首先来介绍这俩吧。
Cloudflare 是一个全球网络,旨在让您连接到互联网的一切都安全、私密、快速和可靠。
这是官方介绍,说白了,它提供很多服务,但最出名的是 CDN 和 DDNS 服务。
WAF(Web Application Firewall,Web 应用程序防火墙)通过过滤和监控 Web 应用程序与 Internet 之间的 HTTP 流量来帮助保护 Web 应用程序。它通常保护 Web 应用程序免受跨站点伪造、跨站点脚本 (XSS)、文件包含和 SQL 注入等攻击。
注:本文的 WAF 主要指 Cloudflare WAF。
说到攻击面,这里我们就拿个人使用云服务来说(大规模云应用场景,我也接触的少,抱歉)。
有心之人防不住,但个人云服务也可以设置一些规则来缓解。真正的大佬,没有必要浪费时间和成本来攻击咱,实际上大多数情况下防的是 “无差别攻击” 脚本。
当然,什么常说的软件安全、数据安全、供应链安全,不在本文的讨论范围之内,有兴趣的话,也可以找我聊聊!
HTTPS 大家想必已经不陌生了,但是证书链 “信任” 问题,以及客户端到服务器的每一环,是否都进行了可信证书的加密,毕竟源站一旦暴露,各种烦人的脚本就来找你了。而且也要防止数据泄露,以及数据篡改。
这里贴一张官方的图,我们可以看到,图上有 2 把锁。其中浏览器和 Cloudflare 中间的锁,是边缘证书(Edge certificate),它是由 Cloudflare 向访问您的网站或应用程序的客户出示的证书。而 Cloudflare 和源服务器中间的锁,是源服务器证书(Origin certificate),它会加密源服务器和 Cloudflare 之间的流量。
巧用阿里云 OSS 玩转免费分布式存储,原理就是这样,保证所有流量经过 CloudFlare 来实现 “免流”。
这里的同源策略,可能和开发中大家所理解的同源,稍微有点小区别,但原理差不多。
说白了就是结合 CDN 为静态资源设置同源策略,在提高缓存命中率的同时,降低 “源站” 被发现的风险。
云安全(当然也可以扩展到数据安全等),我想我不用过多解释了,对于企业来说,关乎到 “生死存亡”,可以说是不过分的。
那对于个人来说的话,我总结了如下几点:
简单解释下吧,我不可能教别人干这个,何况我的水平有限,也教不了。
SOP,也就是同源策略,是 Web 领域最重要的安全机制之一。一般来说,针对 SOP 的攻击,都是基于浏览器的,也就是 “绕过 SOP 技术”。
我们知道,相同主机名、协议和端口,可以视为同一个来源。那么在 “云服务” 中基于 SOP 的攻击方式有哪些呢?
攻击的前提是,错误的配置了规则。比如资源允许跨域,导致其它来源可以疯狂请求资源。或者是缓存策略配置问题,导致缓存命中率不高。或者是只是对资源进行了缓存,但并没有把源站隐藏好。正确配置的话,可以极大的缓解攻击。
这个也称为绕过 HTTPS,这里有个前提,即证书可不可信。
理论上来说,HTTPS 加密的内容,是不会在 “传输过程” 中泄露的,如果密钥泄露了那就好好反思下自己。
这里说一下很多人可能会忽略的一点,就是在服务端没有设置强制访问 HTTPS,甚至保留了 HTTP 访问,这是有很大的风险的。如果用户能直接通过 HTTP 访问到服务了,那么能拿到什么 “有用” 的信息,不用我多说了吧?
很多年前,我也犯这个错而不知情,也是后来才改掉的。我上了 HTTPS 通信后,就没有理由继续使用 HTTP 了。使用 HTTP,无疑是告诉别人,嘿,我的源站就在这儿,且无任何缓解防护,快来攻击我(doge
分布式拒绝服务(DDoS)攻击是通过大规模互联网流量淹没目标服务器或其周边基础设施,以破坏目标服务器、服务或网络正常流量的恶意行为。
说白了,就是有很多 “用户”,疯狂的把流量送到你这儿来,然后你扛不住了,就宕机了(同时也影响了服务的正常访问)。
当然,我们不可能把大量访问全当成攻击。所以需要对服务进行流量监控和分析。比如可以看看是不是只有某个 API 或者页面的流量激增、访问者的 IP 地址或者 IP 范围是不是很可疑之类的。或者说用户的设备类型、地理位置、浏览器标头等等,是不是大量重复了。
这个很多人可能不太明白啥意思,我们首先来看一张图。
这里使用了 censys.io 的服务,只要输入一个域名,它就能查询所有该域名(包括子域名)下的源站信息。它会进行批量扫描,且进行证书关联(证书里面有域名),也同时会扫描你所有的端口。当然,也有其它方式,如果你实在是想防住它,可以直接在防火墙关闭它的扫描子网 IP 对你服务器的访问。
虽说关了用这个网站就查不出来的,但这种方式仍然没有杜绝,其它平台 / 个人依然可以这个扫出来。
图中扫描出了一台我的服务器信息(我只留了这一个用作演示,因为这台服务器宕机没啥影响)。有朋友可能就要问了,这里我已经基于 Cloudflare 配置好了,为什么还是泄露了源站呢?
按平台的分析也是属于证书泄露,基于我对其它的服务器的设置进行对比,我确定是 “监控塔” 把我的证书泄露了,但到底是我配置错了,还是其它什么原因,我就不管了(退一万步来说,是我配错了,我不用你了还不行吗?)。然后和 Aria2 的 HTTP 端口匹配到了相同的 Body Hash,并且指向了一个 Ngixn 的漏洞,真是防不胜防啊!
挑战黑洞攻击(CC)是一种网络攻击,类似于 DDoS 攻击,但它针对的是目标网站的特定 URL 或页面。攻击者利用大量的计算机或设备向目标 URL 或页面发送请求,使其过载并无法正常工作,从而导致服务中断或不可用。
这个攻击有什么用呢?就比如用 Cloudflare 代理 OSS 的流量,虽然不需要出流量费用了,但是请求次数是需要计费的,也就是防 D 不防 C。
IP 欺骗是指创建源地址经过修改的 Internet 协议 (IP) 数据包,目的要么是隐藏发送方的身份,要么是冒充其他计算机系统,或者两者兼具。恶意用户往往采用这项技术对目标设备或周边基础设施发动 DDoS 攻击。
终于到实战环节了,下面咱们就来看看,怎么使用 Cloudflare 的各种配置,来缓解各种攻击以及保护云服务吧。
首先让我们来了解一下,在应对 DDoS 攻击时,要先做什么?答案是:检测。没错,我们必须要先判断,到底是 “攻击”,还是说确实是网站 “太受欢迎” 了🤣,我们要能够区分攻击流量与大规模正常流量。
在提供了 DDoS 缓解的服务商平台,一般有 2 种方式来进行。其一是平台自己的自动缓解措施,也就是全托管形式,又平台自动判断是否遭受了攻击,并进行防护。其二是自定义 DDoS 托管规则,比如 CloudFlare 就是可以自定义规则用于匹配第 7 层(应用层)的攻击媒介。
按图中所示,首先我们找到 部署 DDoS 替代
。
然后进行填写和选择,并保存。“规则集操作” 和 “规则集敏感度” 就按照自己的需求来了。
需要注意的是,支持自定义的是 HTTP DDoS 攻击防护,而网络层和 SSL/TLS DDoS 攻击防护则是自动缓解的。
你说你不知道自己的需求?那你就跟我一样,使用平台自定义的就行了。
WAF 用来缓解恶意请求的流量,根据图中的位置,安全性
-> WAF
-> 创建规则
。
然后开始配置,比如我这里需要对完整的 URI 进行判断,把 api.besscroft.com
下的所有请求全部采取跳过匹配,并记录下来。因为我的 API 是配置了反向代理的,如果直接启用 “质询” 的话,会出现 403 错误,也就是说没法在页面上加载这个 “质询会话”,就是下图所示的内容:
而我们刚才勾选的记录,则会在事件栏中被记录下来。
我是直接全站启用了,因为这样省事儿,你可以理解为这样干的话,自定义配置就变成白名单模式了。而如果你不知道你需要哪些规则的话,你也可以先全站启用,然后自己访问网站看看,再根据事件中的请求详细信息,针对性的调试配置即可!全站启用在
概述
->Under Attack 模式
,直接打开即可!
WAF 的速率限制规则咱们下面会说,至于托管规则,免费用户就不需要管了,如果你想获得更全面的保护、全套的 Cloudflare 托管规则以及防火墙分析,你应该付费啦!
WAF 工具配置,可以自定义规则,声明哪些条件下的用户可以访问。比如 IP 访问规则,ChatGPT 应该就用了这个配置,拦截了某些地方的 IP 访问,很多用过的小伙伴,应该能看到 “5 秒盾”(破盾方法当然是有滴)。而在某些地方,访问 ChatGPT 甚至可以说是双向墙了,我不说是哪里🤡。
不过工具配置我都没有添加,因为我暂时还没有类似的需求,也不需要阻止爬虫之类的。
这个标题,为什么我加上了争议性呢?因为你如果认为 Cloudflare 是不可信的,那么确实风险是挺大的,尤其对于企业来说。但是对于咱们个人用户来说,还是能用用的,Cloudflare 没有必要狠狠的砸自己的招牌(大概?
首先我们找到 SSL/TLS
-> 概述
,然后把加密模式在 完全
和 完全(严格)
直接二选一。如果你信不过平台的证书,你用自己的证书也是可以的,看自己需求了。然后 SSL/TLS 建议程序的话,我推荐你打开,有时候确实会收到提示邮件(万一自己有漏配置呢,对吧?
然后图中的 配置规则
,和刚才的一个意思,我同样配置了 api 子域名跳过。
DNSSEC 可抵御伪造的 DNS 应答,也就是保护你的 DNS 解析不被恶意篡改的。
当我们把域名的解析交给 Cloudflare 管理后,在 DNS
-> 设置
里面,启用设置,并按照要求,在域名服务商哪里,配置好 DS 记录 即可!
这里看标题就知道要干啥了,我建议最好配置上。首先我们访问 Cloudflare IP Ranges,然后将所有 IP 段下的地址,添加进防火墙。
这项措施主要用于进行 API 防护,我上面说,我的 API 要进行反向代理,那么我必须加上这个配置。当然,这里我是直接偷懒了,我用了 CloudPanel 作为服务器管理面板,它有个功能,叫做 仅允许来自 Cloudflare 的流量
,直接启用即可!
我们有时候,可能需要防止暴力破解,或者说就想防止攻击。但是有的请求,看上去就是伪装的非常正常,根本分辨不出来。这个时候我们就可以限制请求速率了,你一个正常访问的普通用户,总不至于点的飞快吧...
如图,我们可以限制匹配到任意 URI 的请求,同 IP 在多少秒内只能请求多少次。我这里配置了 10 秒钟 50 次,更严格实际上也没问题。
前面说到的 Under Attack 模式
,也可以在 安全性
-> 设置
里面启用。而质询通过期,则是在设置的直接过后,要再来一次,否则就无法继续访问请求。去年下半年(2022 年)我使用 ChatGPT 时,就能察觉到开启了这个功能,因为打开了 Chat 页面一段时间后,在当前页面提问后,ChatGPT 就无法给出答案,而是说网络错误相关的提示。当时也很好解决,打开一个新的浏览器标签,过掉验证,然后再关上,就能在当前页面继续使用 Chat。而现在,ChatGPT 明显新增了不少规则,有付费的,也有其它的,有部分我能试出来,但有些还是第一次见。
HTTP 传输的数据是明文的,不安全;而 HTTPS 使用加密技术保护数据传输,更安全。HTTP 适用于传输非敏感信息,HTTPS 适用于传输敏感信息。
一般来说的话,是没法看到的。这种端到端加密,只有客户端和服务端可以看到数据的内容。但是呢,请求元数据是可以看得到的,也就是咱们经常说的,看不到你访问网站的内容,但是知道你访问了什么网站。
但是呢,Cloudflare 充当了中间人(Man-in-the-middle, MITM),而且一般会使用它颁发的证书,所以... 懂我意思吧?
源站,可以理解为 “来源” 站点,也就是咱们藏在 CDN 后面的原始服务器。用户请求时,CDN 会先看看自己有没有缓存资源,有的话直接响应给用户,如果没有的话,会向源站请求,并按照缓存规则,看是否要缓存下来,以及怎样缓存,并把资源响应给用户。
反向代理是位于一个或多个 Web 服务器前面的服务器,拦截来自客户端的请求。它会接受用户的请求,转发给源站,并 “代表” 源站,向用户发送响应。
因为流量被 Cloudflare 接管了,你看到的都是 Cloudflare IP。我们可以在 HTTP 请求标头 中查看到客户端 IP 地址,或者直接在源站日志中恢复原始访问者 IP。
本文其实只讲到了一小部分,也是我在使用过程中积累的一点点经验,希望能帮到各位读者朋友。Cloudflare 平台上还有各种各样的设置和功能,它能做的事儿远不止这些,它特别的强大。我每年的云服务费用就只有域名的费用,原本每年几十块,今年又买了个新的,现在手里一个 com 和一个 dev 后缀域名,每年就只消费一百多而已。可以说我的整个接入层,和部分基础设施,全部基于 Cloudflare 在管理,真的非常好用且方便。
文章写到这里,也欢迎各位朋友与我交流!
对爱情的渴望,对知识的追求,对人类苦难不可遏制的同情心,这三种纯洁而无比强烈的激情支配着我的一生。
Three passions,simple but overwhelmingly strong,have governed my life:the longing for love,the search for knowledge,and unbearable pity for the suffering of mankind.
—— 《我为什么而活着》罗素 (哲学家 数学家 思想家)
2023-04-26 17:07:00
前几天发布了 DiyFile v0.5.0 版本,算是做出了一个不算好看但基本能用的版本,今天就来聊一聊整个过程吧。
这个原因也很简单,当你学会一个新技术之后,把它用起来,最好的方法就是写一个项目。我也挺想开一个工作室,虽然不知道能不能走出这一步,但是为这个小梦想积累经验还是没问题的。
需要学习的东西挺多的,先是画原型设计稿,最开始是用的 Adobe XD 画了一个简单的首页,然后基本上就照着这个设计了。最近简单自学了一下 Figma,后面做产品打算基于它来做了。其次就是项目架构的选型,大白话说就是用什么处理界面、用什么处理服务、用什么存放数据。最开始是用 Nuxt3 写了一版简单的,后来还是被我换成 Vite 项目了,只是没法做服务端渲染了,算是个小遗憾。前端的组件库选型,Arco-Design-Vue、Element Plus、Vuetify3、Naive 都用过,前前后后改了三版,才是现在所看到的样子(Vuetify3 + Naive 混用)。确实也踩了不少坑,浪费了不少时间,但收获挺大。
为什么这么说呢?因为接触了这么多组件库之后,我能在知道某个需求后,正确地选择组件使用了。不知道大家有没有思考过哈,现在让我们回到生产中的需求来。咱们先来想想,UI 原型图出来之后,前后端分别要做什么(分治)?前端根据设计,将 UI 组件分为层次结构,对应着前端每一个细小的组件。后端根据 UI,返回对应的 JSON 结构数据(当然也可能是其它的),咱们这里先不管数据怎么来的,那是业务细节,我们只需要知道,这时候 JSON 数据包含了 UI 原型图上要展示的数据内容。然后(合并),前端根据后端返回的 JSON 结构,映射到页面上的组件结构上,从而完成开发。
为什么说好的设计很重要,设计合理,那么 JSON 结构便合理,也就会自然而然的对应每个组件。UI 和数据模型具有相同的结构,哪怕分了很多层,很多个小组件,也一样能和数据结构中的一部分匹配。这样不仅节约时间(前端再也不用等后端搞完再写了),优秀的设计也能保证逻辑上的 Bug 更少。但是我们都知道,软件开发没有银弹,所以真正干活儿的时候,还是得变通,毕竟这也只是我的一点小想法。
话说回来,为啥我还是放弃 Nuxt3 了呢,虽然有很多插件还在预发布,或者在下个大版本才发布出来,毕竟没有的咱还能自己实现下。真正让我远离它的原因就是:“慢”。不是我故意要诋毁它,我是真没见过不管是启动还是热更新,都比 SpringBoot 2.x 还要慢几倍的东西,我真的越写越烦,再也不想碰这玩意儿了。于是我干脆把后端改成 SpringBoot 3 了,做成前后端分离架构。数据库这边,兼容了 MySQL 和 SQLite 。
总的来说,还是一个权衡取舍的问题吧,不过我选型还有一个要求,就是保证客户能够最大程度的白嫖部署使用。
开发的流程就比较自由,因为是自己一个人开发的,没那么多讲究。前端基于 Vitesse 来开发,后端就 SpringBoot 3 一把梭了。项目托管在 GitHub 上面,分为了前后端 2 个仓库。然后双分支开发,一个 main、一个 dev,根据实际情况打 Tag 并进行版本发布。
没啥特别好讲的,就聊聊开发中可能要注意的地方吧。前后端都用了代码检查,前端由于引入了 TypeScript,写起来会舒服一些。不过我基本上只解决报错,以及用 eslint 来约束代码风格,毕竟保持一致会减少阅读代码的障碍。后端代码的话,我很喜欢上语法糖,以及引入了阿里编码规约插件和 SonarLint,结合 idea 自带的扫描,基本上够用了,缺点就是很卡(我基本上蓝屏全是因为 idea😭。不过写 Java 代码的时候,我是非常喜欢消灭所有的黄色警告,尽量写的优雅一些,当然如果是大半夜写的状态不好,可能就。。。
代码写完后,就提交并 push 到 GitHub 了,有时候 git commit 我会使用 emoji,因为很好看。然后就会触发 GitHub Actions 来进行代码扫描了,这一步还是有必要的,比如后端会进行一次预构建,保证在切了一个新环境后,代码能正常构建(我碰到过本地成功,CI 失败的情况)。
然后就是自动部署了,review 后,测试也没问题,就可以合并了。
部署流程也很方便,就第一次配置稍显麻烦。前端拿 Vercel 来举例子,它可以针对每一个分支和 commit 进行构建,构建完之后,会在 GitHub PR 页面显示 bot 发出来的预览链接,点进去就可以看到效果了。而后端的话,会由 GitHub Actions 自动构建镜像,然后集群内部更新镜像部署(这一步手动自动都可,开源项目可以考虑手动,防止恶意代码被 PR,然后在远程服务器上执行)。
重要的是配置反向代理,以及不同的前端环境,反代到不同的后端环境。数据库生产和测试可以偷懒用同一个,但是基于 PR 的(未合并),建议单独整一个,避免恶意代码执行导致脱裤,泄露 Key。
一个产品出来了,肯定得有一个文档,提供给用户查阅。在 DiyFile 发布后,我给它编写了一份文档 ,用户只要会使用部署所需的相关组件,应该是能够照着文档部署的,比如 Linux、Nginx 和 Docker。
社区维护就是比较难的,由于目前没有什么用户群体,也就没有去弄这个。只是在一些论坛发布了推广。
经历了这个个产品后,我也大致了解了一款产品需要具备三大核心:好的创意、有人用、有人反馈。其它的很多东西,我认为都可以归类在这三个里面。没有好的创意,可能开发出来了就没人用,也就不会有人反馈了,支撑你做下去的动力也就越来越小。虽然一个项目生命周期的每一步都至关重要,设计、开发、发布、维护,但也都只是过程。
补充:
有了这次经历(虽然项目还有不少问题需要解决),我学到了很多东西,可能以后在做新项目时,会更加谨慎吧。接下来要沉淀一段时间学技术,为下一个产品做准备!也感谢你能看到这里,希望我的经历对你有所帮助!