我又滚回来写文章了,从去年11月底到今年2月底,算起来整整1/4年没有写博客了,自己的博客站都长满了坟头草,我在上面跳舞。在这3月来临之际,我们还是来聊聊技术,就当给自己做个阶段性总结。
这几个月我经历了一次对自己前后端栈的一次彻底的重构。由于日益增长的需求,旧有的技术架构很多变成了技术债,拖累着后续的开发维护工作,来看看我具体做了哪些吧:
mongodb
-> postgres
koajs
-> nestjs
mongodb native client
-> typeorm
serverless
-> serverless
& docker compose swarm mode
restful API
-> restful API
& graphQL
github action
CI/CDtensorflow
内容合规性检查(说白了就是鉴黄)mongodb
是个好东西,很多场景下使用起来非常方便。然而一天,我阅读几个月前写的代码的时候,突然感觉它很丑陋,尤其是聚合查询,一个aggregate
管道,中间还夹杂着几个 $facet
几个 $replaceRoot
,里面作为参数的索引字符串里,还套了许许多多的$
与预留关键字。
阅读完后,我开始思考人生,我想起了那天在夕阳下的感叹,啊!那个少年竟然写出了如此精妙的聚合代码!而如今再与它两眼相望,竟无语凝噎。于是我果断把这段狗屎代码注释了。
当然我知道这也不怪 mongodb
,这马是好马,只是骑的人没本事而已。经过考虑之后,为了代码的 TypeStrong
考虑,还是选择了 RDS
。于是选用了postgres
,依靠之前一定的 MS SQL Server
和 mysql
的使用经验,还是非常好上手的。加上使用了ORM
框架,隐藏了一部分直接写SQL
的数据库操作。也写了一些脚本,定时pg_dump
备份啥的,暂时够用,其他高阶用法,比如像 supabase
里什么行数据权限啥的就不是很了解了。
把后端框架换了,为啥换 Nestjs
而不是基于koa
的 egg
,midway
?究其根本是生态好,加上不是KPI项目。
生态好,体现在高质量的官方包与丰富的第三方最佳实践,另外其他开发人员踩过的坑多,好借鉴(抄)也是一个重要因素。
另外,我以前写 ASP.NET MVC/Core
的时候,就很喜欢 IOC
DI
,Nestjs
的核心就是这套机制。何况即使我可以利用 inversify
自己设计一套,但这哪有白嫖的香呢?
而且相比koa-compose
那种,遇事不决middleware
的开发方式。Nestjs
提供了更多细粒度的切面方法,帮助我们更好的 aop
,开发体验相当舒服。
当时在选型的时候,两边都看了一下文档,并且也都fork
了examples
项目跑了一下,最终选择 typeorm
了,原因有以下几点:
prisma
定义实体对象,靠的是它自己的DSL
。虽然这个DSL
通俗易懂,但是schema.prisma
这玩意实际上和 schema.gql
这类一样,在应用调试和测试的时候,都需要修修补补的。与其多个 DSL
一起修修补补,不如我一份 entity
利用 meta data
同时生成 db schema
和 gql schema
。typeorm
和 nestjs
的适配性相当的好prisma client
需要初始化,同时在 schema
发生改变后,需要重新生成 (prisma generate
)。出于这些原因,选择了 bug
依然大堆的 typeorm
,还妄想着通过定义一份 entity
,可以派生出 dto
,input
,model
…这些其他的对象。实践事实证明,是可行的,但是需要你对元数据有比较精确的掌控,比如:
import { OmitType, PartialType, PickType, IntersectionType } from '@nestjs/mapped-types';
import { OmitType, PartialType, PickType, IntersectionType } from '@nestjs/swagger';
import { OmitType, PartialType, PickType, IntersectionType } from '@nestjs/graphql';
这些方法,它们各自取,或者说利用的元数据(meta data
)是不咋一样的,(根本的原因来自于取的元数据的key
的不同,这个藏在每个包的 *.constants.ts
文件中),你需要根据你的需求,来调用对应包的中的对应方法。
更换后端框架的背后,也付出了一些代价。
当我把 Nestjs
应用部署到阿里云函数计算/腾讯云云函数的时候,这个冷启动时间远超 koa
/express
这类应用,后来我又把整个 Nestjs app
经过 webpack
进行打包压缩,再次进行部署测试。结果也只是稍稍优化了一点点冷启动时间,总的时间还是差强人意。这也是我选择自己买服务器部署的原因所在了。
对于目前 serverless
情况,很多实践还都是把传统的 web server
, 或者通过http trigger
转 event
的 shim
,或者通过端口监听,又或者通过 docker image
部署的方式部署到单个函数的。这种遇到业务规模,体量稍微大点的,就很难满足实时性的需求。
假如你说保留实例,那我为啥不自建? 另外多函数部署运维的serverless
框架也有,可是投入这么大精力搞这些玩意,效果和产出还不如用k8s
,你请得起高薪的程序员,难道不舍得花钱买十几二十台服务器吗?
但是这不意味着我要抛弃 serverless
,只是让它从打二号位,转移到了五号位,打打辅助或者快速成型还是相当好用的。何况只要大家还在一个 VPC
(私有网络) 里面,通信是比较方便的,打打辅助更是得心应手。另外我没有上 k8s
的原因,核心原因是swarm够用,根本原因是我不会。
这次总算把 GraphQL
加上了,前端爽歪歪,后端爽歪歪,什么?前端后端都是我?这下苦逼了。
首先,由于 GraphQL SDL schema
会按照图这样的数据结构进行分析来调用我们定义的字段节点处理方法,导致我们在定义通信 model
的时候,一旦遇到相互关联的其他 model
,就需要定义这个关系的处理方式,即 ResolveField Function
。然而这种处理方式会带来一个问题,原先我们写 restful
的时候,去取有关联的数据,几个 LEFT JOIN
就完事了,但是到了 GraphQL
的 resolver
里面,由于 ResolveField Function
的独立性,它从一次数据库查询,变成了多次数据库查询,获取结果后再进行处理组合,变成前端想要的对象结构。
在这种情况下,数据缓存就显得及其重要了,另外缓存的key
也必须很好的体现数据的特征。于是我果断拉取了 redis/redis-stack-server:latest
,还是相当好用的。
另外有一点,个人不是很理解,感觉应该分开定义有可能很大的字段在不同的 model
里。举个例子,数据库有张表,表里面有个字段,每一数据行,这个字段的数据大小都超过了 1MB
。然后后端去取数据的时候,又是 SELECT *
,这很明显数据一多,内存就爆掉了呗。但是对于前端来说,我又没有去取你这个字段,你为啥要去取这个字段呢?所以后端也应该根据前端传过来的 gql
语句,去预估数据的projection
。
目前我用的是云主机部署的,代码在 Github 私有仓库,那么有啥办法可以稍稍优化一下目前的部署方式呢?
这里我的做法不是很好,但是我必须说出来,这样大牛看到我的方案之后,才能对我进行降维打击。
首先我的前后端部署方案,都是依赖各个云的,出于历史原因,我的前端OSS,CDN这些在腾讯云上。我的云函数,域名,服务器,还有一些其他在阿里云上。另外手里还有一台华为云云主机。
首先是后端部署,我肯定不会在云服务器把后端的代码全部拉下来,打包编译运行的,这样又蠢又笨重。
在后端的代码仓库里,我仅仅保留 Dockerfile
和 docker-compose.yml
,一个用来打镜像,一个用来起本地配套的服务,比如我会拉 redis
和 postgres
的镜像,本地起几个用来开发和调试,还有数据库 schema
的同步和本地 migration
的预演。
另外我还有一个部署仓库,里面用来存放配置,比如 nginx.conf
,各种ssl
证书,环境变量,加上一大坨 shell
脚本和不同的 compose
文件,这些 compose
会映射各种容器卷这个目录下。
这样,我拿到一台机器之后,首先要做的,就是生成 ssh key
,然后把 key
注册进 github
,给它对应仓库的访问权限。拉下来之后,直接 docker compose up -d
就部署成功了。
当然,情况肯定不这么简单的,因为部署仓库里,存放的compose
文件,里面都写的都是阿里云私有镜像的地址,所以还要 login
一下。
另外我还使用 Github Action
来进行镜像的打包,并推送到私有仓库,这一块非常简单,懒得说了。还有一个注意点就是,每次推送镜像,都会生成一个新的镜像 tag
。那如何在推送之后,动态改变另外那个部署仓库的 IMAGE_TAG
呢? 实际上也很简单,在部署仓库也建立一个 Github Action
用来被动的接收,后端仓库推送完成后的 repository_dispatch
里的参数就行。
参数(payload
)里,存放着推送完成后的 tag
,然后执行一行:
sed -i.bak '/^IMAGE_TAG=/s/=\w*/='${{ github.event.client_payload.tag }}'/' .env
然后 git commit
就搞定了。这样每次后端仓库镜像一推送,部署仓库的IMAGE_TAG
就实时变化,云主机只要重新 git pull
然后 docker compose up -d
部署就完成了。
至于前端部署,这就看是 ssr
,还是 spa/ssg
这类的,这块之前搞 vue/react
应用开发,用 nuxt/next
的比较多,这里就不继续说了。
之前我的技术群里,有小伙伴说它的小程序因为涉黄被封了,原因是用户自己上传了黄色图片,然后自己截了图,举报了这个小程序,然后就被微信封了。
于是我决定还是要防一手的,咱训练模型不会,工程化能力还是有一点点的,所以起码用这种还是非常容易的。
简单到,在 github
里找到并下载模型,然后本地加载调用就ok了。不过我在使用中,遇到了一个坑点就是,不能使用 alpine
作为基座,具体原因可以看这个 issue/1425,于是更换成了 debian-bullseye-slim
后运行良好。