本文梳理了个人在三次后端架构上升级的心路历程,对现代化的理解如何逐步现代化。 记录了从在新监控系统的搭建上,不同方案的优劣对比,到最终做出合适的选择。 可以说新技术的尝试,同时也是思维上的开拓。

心目中现代化概念的演进

从软件工程的角度来说,现代化这个词可以有很多含义,因为这一行的发展速度日新月异,有太多东西跟以前不同了。

本文的视角来自一个迟钝的后端新人,之前对后端的印象还停留在:“盘根错节的依赖导致软件不敢轻易升级”,“需要熟悉大量的运维命令来排查问题”,“改了行代码服务突然挂了半天都修不好”…… 种种惨痛经历让我曾经对这一块敬而远之,绝不会自己去做运维,而是大量的使用 BaaS 服务,只写业务,不管环境。

云函数

那时我认为的现代化就是 Serverless,无服务器的理念。业务逻辑有云函数处理,数据库有 RDS,存文件有 OSS。一切都是自动伸缩,不用管底层的稳定和高并发时的扩容,看起来多么美好。

直到当我开始构建一些复杂的,带状态的功能,我才意识到由于函数拆分的过细,调试整个系统时是多么的痛苦。你几乎没法完全在本地模拟线上的环境,测试之前需要先把函数部署到云端,然后再调用查看日志。开发效率和排查问题的效率都被拖慢了。更别提冷启动和相互调用时无法避免的延迟,简单的功能都平均 200ms 的返回时间,再复杂高频一点就不可想象。

云原生

用着用着,各个厂商开始吹云原生这个词了,主要就是解决开发和生产不一致的问题,号称可以把传统的单体服务直接搬到云上,同样不需要担心硬件限制和运维问题。这里本质上就是基于 Docker 了,但当时视野狭隘,只会用厂商提供的自用配置文件,没有去查一下实现原理,导致理解又慢了一步。

厂商推广时很喜欢说,租服务器不用的时候也在花钱,而用云服务只需要为消耗付费。确实,看到文档里定价每秒钟只有小数点后面好几位的价格时,瞬间感觉跟不要钱似的,这也太省了吧!用计算器算过之后就发现,单位时间的价格虽然低,但它是用秒来计啊!除了要买运行时间,还要买资源的消耗,内存和 CPU 都被抽象成了计算单元。

有一说一,这种方式是没问题的,用多少付多少,相当公平明确。但很遗憾以当前的定价,服务多起来之后价格也难以承受了。

基于 Docker 的服务器

学习和使用 Docker 是这两月最大的收获。只恨自己以前认为它就是另一种裁剪版虚拟机的偏见,让我迟迟没有了解这么好的东西。

一言以蔽之,Docker 能屏蔽系统和环境依赖的差异,让程序真正完全一致的跑在不同机器上。它恰好解决了后端最大的问题:运行环境被污染,执行的结果不一致。也就是说很多运维的难题没有了,可以同时跑好几个不同版本的 node 服务,不担心彼此干扰。本地完成测试,不论分发到任何地方,执行的结果也是保证一样的。

那性能呢?不错,Docker 确实是虚拟化的技术,但它并非完全虚拟化,而是基于 Linux 容器的操作系统级虚拟化,可以直接使用硬件资源。因此 Docker 的性能非常好,对系统额外带来的开销微乎其微,多数情况下基本没有明显损耗。使用方式也简单,都塞进容器里了,无非就是挂载个配置重启一下。

既然本身就是云原生的实现基础,为啥还要单独拿出来说?哈哈,因为直接用厂商的容器服务太贵了。考虑到项目的后端对扩容的要求其实远没有那么高,基础配置的服务器就足够了。所以不如直接买个便宜的服务器,装个 Docker,后续所有服务都走容器的形式。管理运维又方便,开发效率又高,岂不美哉!

没有最好,只有最合适

必须专门强调的是,云函数和云原生也是很好的解决方案。抛开场景谈最优就是耍流氓,不同的业务有不同的需求,还是要根据情况来选择,而不是妄图有一个银弹能一网打尽,处处最优。

虽然选择了服务器+Docker的方式来做新后端,但有些云函数还是保留了下来。毕竟大厂的 SLA 要比咱高得多,留一个意外时候的保底也很必要。

监控系统的搭建

之前用云函数时,为了方便定位问题,给自己写过好几个监控 App。原理都是记 Log 打点,在数据库中留下不同时刻的统计数据。 实时的数据还好说,当想要做一些不同时段的指标对比时,就不知道怎么写合适,每次都硬查过于粗暴,所有记录边存边算又有些愚蠢。

现在要上服务器了,有个监控系统心里更踏实,维护起来也更有依靠。

由于所有的服务都尽可能使用 Docker,方便部署维护和升级,查资料时就自然查到了目前最火的 Prometheus + Grafana 这一套。

时序数据的可观测性

监控系统是非常通用的东西,人人都有,但是如何把它弄的更专业,了解 Prometheus 这类先进的解决方案后才知道。简单来说,通过专门的时间序列数据库而非关系型数据库,在性能和查询能力上就有了质的变化。基础的数据只有一份,而通过不同的标签等对其进行多维度划分,就能够满足各种指标的要求了。

比如容器的内存占用,传统方式是每增加一个指标,就要多一个字段来记录。而 Prometheus 能够对每条数据进行不同的维度标记,配合上自己专门的查询语言,这样就能让各种指标的计算变得非常灵活。比如只统计了时间点上容器的内存占用量,就能轻松计算平均值,一段时间某个容器的95%分位值等等。

全 Docker 本地部署

整套方案是这样的:

  • Prometheus,监控数据引擎。负责储存和处理时序数据。
  • 各种Exporter,收集和导出端点。负责收集各项目标数据,并提供被读取的接口。
  • Grafana,可视化面板。负责把数据展示为各种大屏图表。

其中 Exporter 有很多,只要能按标准接口提供数据的都算。比较常用的有统计服务器自身指标的 node-exporter,统计当前运行的 Docker 各个容器指标的 cadvisor 等。常见的数据库和服务基本都有写好的,加关键词在 GitHub 搜索即可。如果你对这些指标有定制处理的需求或者想给自己的业务加一套,可以试试 Vector,能够将各种指标和 Log 进行单项无环图的拓扑组合,最终提供 Prometheus 读取接口,非常厉害。

部署完成后,可以在 Grafana 的 Dashboards 页面里搜索对应的仪表盘,导入添加后就能看到效果了。看着专业的图表布满屏幕,心里的满足感还是挺强的,哈哈。熟悉之后还可以创建自己的图表,Grafana 的查询语言也很简单,类似于 SQL,对着现有的图表配置照猫画虎就能搞定。

Prometheus 那一套更倾向于基础设施的监控,追踪大量的指标。如果是简单的检测服务是否正常在线,就有点大材小用了。 这时可以部署一个 uptime-kuma,定时去访问某个端点,进行主动监控。这类服务就比较自成一体了,功能比较简单,基本不需要其他组件依赖。

内存占用优化

Docker 虽然没啥性能损失,但也有一个弊端,就是无法共用系统的依赖了。如果是 python 或者 node 这种需要运行时且尺寸比较大的,那就只能包含在自己的镜像里。跑起来之后,就会占用比直接部署更多的资源。

最好的解决方法,就是换用 Go 或者 Dart 这种可以 AOT 静态编译的语言,打包成品尺寸非常的小。例如上文中提到的 uptime-kuma,跑起来之后占据 200M 的内存,在小服务器上很难接受,因此换用了更小巧的 Gatus。云原生里用这些语言真是太舒服了,有个服务端用 Dart 写的,镜像打包后只有30M,拉取更新重启部署只要几秒钟,非常流畅。

虽然 Prometheus 也是 Go 写的,但这一整套组件下来也要吃掉近 500M 的内存,有点头重脚轻了。如果你监控的指标不多,也可以选择使用其官方的云服务 Prometheus Cloud,提供的免费额度足够监控一两个主机。这样就不用本地部署它和 Grafana 了,内存占用直接砍半。

另外 Prometheus 官方也提供了一个通用的 Exporter,叫做 prometheus-agent,内置了常见的各类数据库,本机和 Docker 的指标收集导出,就不用部署多个 Exporter 了。用一个大的代替几个小的,尺寸总共好像也没小多少,就当是方便管理了吧。

还有个小插曲,跑了两天之后,看统计图发现 prometheus-agent 的内存一路水涨船高,从开始的 260M 缓缓涨到了 300M,这样下去一个月不是要把内存挤爆了😂赶紧写了个脚本,每天定时重启一下,不影响使用,但会把内存占用重置了。如果有其他的 Docker 容器出现类似的情况,也可以通过定时重启容器来解决。

总结

絮絮叨叨写了一堆,中间踩坑其实也不少,通过查文档和搜索都解决了,就省略掉了。

现在想来,其实这些新技术在思想上都有类似的地方:向下抹平差异,向上提供更友善的使用方式。 比如 JVM 就是抹平了不同系统的底层调用差异,让 Java 可以一份代码到处运行。作为 UI 框架的 Flutter 也是如此,通过自绘消除了不同平台的表现差异,然后用高效的声明式语法+树形结构提高了开发效率。

也许可以看作是“高内聚,低耦合”这一原则更高的表现形式,即隐藏底层的复杂度,提供友善的使用接口。以后设计模块时,也要向着类似的标准靠近,奥力给!