实现一个随机图api
实现一个随机图api
RealXLFD浅谈一下2023.7-2024.11这段时间做的一个小事情
关于这一年的我和博客
想起来,博客建立至今也已经有一年多的时间了。当然对于我这种懒狗肯定是建好了懒得维护然后就作废的…
我不知道写什么东西放上去
也不知道怎么写。
所以除了一开始放的那几篇之后几乎从未更新
那时候我连编程都不会,更别提产出啥有价值的东西了
但是,一年时间过去…我选择重新拾起这个博客
删掉之前写的垃圾
然后记录一下一些我想写的东西
起点
目标很简单,开发(copy)一个博客,因为无聊
每天大叫完还有大把的时间,大一暑假,为何不拿这些时间做点闲事呢?
于是我在网上瞎逛,发现了这个博客主题(anzhiyu,butterfly魔改)
一眼爱上
于是开始学习
看文档
…
需求很简单: 随机背景图
看到这个需求,脑袋里最先浮现出的解法是:
1 | const pics = [ |
但是我肯定不会这么写,毕竟麻烦
假设有1w张图片呢?那光是js都要占很多空间,难以维护,拖慢网站加载速度
那我们能不能写个后端解决这个问题呢?
不想维护+低成本
那cloudflare workers就成为了首选
用不完的免费workers额度 + 10GB r2 bucket空间 + 数一数二的防攻击
于是,初版完成了:
1 | addEventListener('fetch', event => { |
具体有什么特点呢?
- 基于r2 bucket 路径的文件分类
简单来说,假设我上传了/albumA/1.png
和/albumB/2.png
那么访问https://.../albumA
就可以随机返回1.png或者该文件夹下的一张其他图片
albumB同理 - 自动索引,手动触发索引更新
访问/purge
则会遍历配置的目录,将其所有子目录中的所有图片路径载入到数据索引中
或者请求到一张索引中有但是未在r2 bucket中找到文件的图片,也会触发索引更新
当时cf d1还没出来,所以使用的是json + cf KV
- 也就是说,实际上想要添加图片,只需要我们把图片文件上传到r2 储存桶中去就行了
新的需求,新的挑战
偶然的一个机会,看到了某个陌生的博客
被首页的大图深深吸引
为什么会有渐变效果呢?
omg
原来是先请求一张小图(缩略图)再请求大图来缩短响应时间,使用模糊来规避小图导致的图片不清晰,并且使用从模糊到清晰的渐变流利的从小图切换到大图
- 具体来说:缩略图大概100kb不到,响应速度快,载入速度快,先载入小图再替换为大图,可以显著减少白屏时间
nb
看到这么牛逼的东西,我肯定要copy过来
毕竟信仰拿来主义
别人的东西,抄过来就是我的
于是copy过来了
调渐变效果调了我一年,真的是煎熬…感觉每一ms的timeout,每1px的blur都给我试了一遍
终于舒服了
但是…随机图后端呢?
需求
- 随机图中每一张图需要一张小图,一张大图
- 需要通过某种方式,能够确保返回的大图和小图是同一张图片
怎么办
V2.0
- 大图小图的问题:
创建2个文件夹,一个装大图,一个装小图。
1 | large/albumA/big_1.png |
添加咨询参数?l返回大图,?s返回小图
数据索引只索引后面的相对路径部分(albumA/xxx.png
),所以只需要拼接文件夹路径即可得到完整路径
- 返回同一张图片的问题:
rid
用rid来解决问题。rid=前端生成的随机种子id,可以无限大
也就是说,将生成随机数的职责交给了前端。前端生成一个随机数种子,后端获得了该种子,能够确保在数据索引发生变化之前指向同一张图片
也就是
1 | let index = [...] |
开搞
1 | // src/proc/refresh.js |
特点
- 流式响应,避免完整地将图片写入worker内存,加快响应速度
- 使用
?s<rid>
的参数指明了大小与rid - referer鉴权
为啥要鉴权呢?因为朋友发现一个一个一个
一个sb
我的天copy博客源码就算了,还把我的随机图api链接给copy了
这么能超一定是铁牛子把
因为太过逆天,于是直接立马加上了鉴权
加了之后大概几小时之后那位的博客就换链接了
不过我也能理解
为什么呢?因为我也是懒狗+煞笔
要我能抄我也抄,嘻嘻
一个小小的问题
使用?k=v这样的标准咨询参数,有的浏览器会缓存请求,于是兼容适配了将?
替换为!
的请求形式,其他不变
代价是什么呢?
用的时候爽,上传的时候可就遭殃了
就跟BT下载一样,下载的人满为患,做种的就那么几个,特别是冷门的东西
每次到上传的时候,都需要提前为每个图片准备好压缩+缩放后的小图
然后分类存放
上传的复杂度直接几何级增长
咋办
对不起每次遇到这种b情况脑子里想到的都是脚本
那时候我连python都不会 (当然现在更不会,我觉得python是世界上最难学的编程语言)
但是脑子里只想着快速解决问题
于是现学(抄)现卖
python写(抄)了一个上传脚本(chatgpt+google)
依稀记得用了PIL库压缩图片,然后好像是用rclone上传同步
我找找有没有记录
好吧这是一个批量图片压缩工具:
1 | from PIL import Image |
- 压缩好之后写了个bat文件直接一键上传/同步
1 | @echo off |
V2.1?
朋友说需要一个主色调功能,也就是获取随机图的主题色
加上博客为了移动端适配,需要将图片分类为宽图片和窄图片(ratio>1与ratio<1)
- 于是乎:
1 |
|
api太慢了怎么办?前端缓存
对于用户而言:
1 | index.html -> xxx.js / xxx.css -> 执行js -> 请求图片 -> 背景加载替换 |
电脑是很快的,似乎问题落在了请求图片的身上
但是,cf worker的返回速度就那么快
不如说,所有的后端就那样,因为图片响应速度不仅仅受限于两端的ping,还有用户带宽
有没有一种办法能够让刷图的速度不受api响应速度的影响呢?
似乎service worker解决了这个问题
仔细看看
嗯
这就是我要找的,一个在前端运行的后端,太棒了
开搞
- script.js
1 | // SERVICE WORKER |
- service_worker.js
1 | // Cache name: WebCache |
解决了什么
- 用户第一次访问网页时注入server worker
- 缓存所有指定host的请求,以及缓存当前图片后面的4张图片,每次消耗队列中的一张都会新缓存一张,确保队列始终有4张图片缓存
这样用户刷图直接使用缓存而避免发送请求接受响应,网站流畅度大提升,极致丝滑
似乎就这样了,时间定格在了那个暑假结束前夕
明明写了脚本,可是我几乎一次也没更新过那些图片
Refactor
时光飞逝
大概是24年年初
偶然间看到一个WebAssembly图像处理库Photon,觉得这实在是太棒了
想着能不能搬到cloudflare worker上
也就是说:
- 上传一张图片,只需要post一次,worker接收到数据后会自动压缩转换格式并且放置于正确的位置
岂不美哉?
正好那时候cf d1刚刚公测
开搞!
Github rpics-worker
上传到github上面了
新功能跟下一个版本的差不多,会在后面提到
Fatal Error
完了
完了
cf worker对每个worker有128m内存限制
这意味着什么呢?
假设一张4k的图片需要进行处理,在不支持流式处理的情况下,图片约24M,实际占用为input + output + process cache,也就是至少24M * 3 = 64M,再加上运行时本身的开销
直接给我拒绝服务
咋办?
爆大米
我真的是草了爆了大米买的workers 付费计划还是那金子一样的128M内存
说金子问题是免费也是128M,说免费问题是掏了刀乐还是那128M
遇到大图片直接崩
真的饿饿了
Go V3.0
当时在写golang,想着直接把worker弃了,沟槽的内存限制,幸亏我还有一台hk的小鸡,虽然只有1g内存,跑个golang应该没啥问题
goland,启动
go + gin + sqlite
不错
新功能
- 支持指定图片格式,比如说url配置
format=webp
则将图片转换为webp后返回,如果是第一次请求则会同时缓存转换结果,避免后续的请求再次浪费时间在转换上 - 支持更细粒度地过滤图片长宽:比如说可以通过>1.3指定返回长宽比大于1.3的图片
- 提供了去重,即自动忽略用户上传的重复图片(通过hash)
- 可以通过size指定多个不同尺寸,比如说
480p
,720p
,1080p
,2k
,4k
功能大概跟workers差不多,但是在自托管的情况下真的快了太多
打包成docker之后运行基本上也不用维护
终点:V4.0?
大学必修课教rust我是万万没想到的
本来就久仰rust大名,打算考完研再去钻研一下,没想到被强迫提前到了现在
抱着试试的心态,随便看了两眼文档
我去
蟹神
不知道为啥,真的一见钟情了——
我感觉rust比js简单多了。。别提python,因为难度排行在我心里是这样的: python
>c++
/c
>js
>java
>rust
>go
(个人观点)
那么简单,性能又那么好
为啥不用?
但是,api结构再怎么优化就那样
到底能改什么呢?
immich ,一个自托管相册库
无意中在unraid 应用中心刷到
大概在5月份?就把自己的涩图全部转移到immich相册里去了
一个点子逐渐浮现:如果…我是说如果
能把随机图api建立在immich上层呢?
以immich相册的形式组织图片,而随机图api的数据是immich相册的缓存…
我去
这样难加图的问题,难维护的问题全部迎刃而解了?
immich对于每一张图片也存储了3个版本(thumbnail/preview/original),并且提供了去重
这样图片尺寸转换、格式转换、去重不都解决了?
官方还提供了详细的api说明文档
简直就是专业对口
于是我赶紧打开rustover
呸
vscode
为什么不用rustover?
因为analyzer的速度太几把慢了,我敲个代码得等3-4s才有提示,何况我是尊贵的非13/14代无锁intel i7
vscode,启动!
找web框架,看到rocket的文档很全面喜欢,用了。
找orm框架,我去看不懂一点,稍微试了下也报错
哎
还是直接写sql吧
于是,确定下来rust
+ rocket
+ sqlx
开搞
你以为我要放仓库连接?哈哈我自己这个版本都被我删了
为啥?
因为我™用rocket、axum和actix都写了一遍…
V4.0
请求->数据库->文件->流式响应
请求->数据不存在->请求immich->流式响应+写入文件
emm
大概就这样
随便写了个初版
一压测
1w8到顶了(12700k),sqlite同时疯狂报错提示响应时间过长
哈哈
受不了
加个内存索引和内存缓存吧
请求->缓存->响应
请求->缓存(未命中)->数据库->文件->响应
…
emm
ok加个预缓存
ok加个自动索引刷新
开测
我的12700k上跑到6w6 qps到顶了,此时cpu占用大概30%左右,内存150MB
连接数不记得了,好像是1000?
我还以为能上10w
有点垃
perf 测个火焰图,handler只占了25%左右,大部分消耗全在rocket framework上,还有醒目的hyper
得换框架了…
V4.1
Mutex<HashMap<String,...>>
有点垃
换成DashMap<FastStr,...>
rocket有点垃,换成axum试试?
……
写了出来
我去axum也是hyper
rocket和axum都是hyper
哎真的别hyper了
于是用了actix-web
windows上一测,发现…
3300qps?
真奇怪,至少也得3w吧?
但我打开任务管理器,发现4个大字:“效能模式”
哈哈^_^
诶
不是哥们我压测怎么cpu占用率只有0.5%
有点无敌了
没搞懂
于是ssh连接我的12300 + unraid开的 debian + 8gb内存的虚拟机
1 | cargo build --release |
wrk,启动!
不是
19w qps了直接
1s传4.5gb数据
逆天了
这就是蟹神吗?
10000连接数也轻轻松松16w qps
于是,就有了现在的版本,也就是这个博客正在用的版本
V4.1.x
以后遇到好的灵感我也会继续更新重构的喵
谢谢你读到这里,哈哈
不放仓库,因为我是彩笔,怕有人提issue骂我
我github都不会用真的对不起了
想开源就在下面骂我,谢谢
不过我知道文章101%没人看,所以是写给自己的,嘻嘻喵