说明
本文总结云盘服务端从 Node.js 转向 Java 的过程。我平时也使用 nodejs、go、python、C#、java 开发,本文只讨论当时的项目约束和工具链。
云盘 Web 前后端和服务端开发在 5 月由我接手。此前基础终端负责各端,云计算组负责服务化;云计算组已使用 Python Tornado 开发部分服务端能力。移交后,团队希望在 Node.js 上积累经验,经评审后停止维护我当时维护的 Python 版本,转为 Node.js 开发。
云盘概览

服务端功能模块

架构由 Web 前端(自研框架)、Web 后端(koajs)、服务端(koajs、postgresql)以及 PC、Mac、Android、iPhone 等终端组成。
两个月后,团队完成了一个可用的 Node.js 服务端版本。开发和维护中遇到以下问题:
- Node.js 的事务管理缺少统一方案。服务层需要将
transaction透传到 DAO 层;即使抽出 DB 层,事务上下文仍需在调用链上传递,重构会波及多个模块。基础 MySQL 包在高并发事务中还出现过难以定位和复现的异常。 - 重构依赖单元测试和功能测试覆盖。部分可由编译期发现的问题只能在测试阶段暴露,重构成本随代码规模上升。
- 服务化领域缺少稳定的第三方包。例如接入 OpenStack 存储时,需要直接查看 OpenStack 接口并为第三方包补丁。
- 公司在当年下半年开始使用 Java 服务化,许多内部能力以 jar 包提供;Node.js 需要从 wiki 查找接口后自行实现客户端。
- 云盘服务端需要处理较多数据,CPU 使用率较高。Node.js 版本上线后,CPU 出现异常波动;后续使用 C++ 实现部分处理模块后得到缓解,但 C++ 模块增加了重构成本。
- Node.js 0.12.7 的回调写法增加了控制流和问题定位的复杂度。koa.js 只能缓解部分问题,JavaScript 本身的类型和运行时问题仍需要在运行中排查。
这些问题说明,当时的团队规模、内部服务生态和业务负载不适合继续以 Node.js 作为云盘服务端的主要实现,因此需要重新评估方案。
调研国内外网盘架构后,候选方案是 Go(Dropbox、七牛)和 Java。Go 仍会遇到前述事务、第三方包和内部服务接入问题,部分包也需要自行维护。云盘当时只有我负责服务端,还要处理 Web 端工作,因此最终选择 Java,并重新设计服务端架构。
架构
架构 1

架构 2

系统拆分为多个微服务,通过 Kafka 异步通知,并通过 Kafka 维护全局事务,使系统达到最终一致性。
服务使用 Spring MVC 和 Spring Boot 实现。部分关键服务合并到微服务网关中统一处理,服务容器使用内嵌 Jetty。
项目开发约两周后上线第一个 beta 版本。相同功能的 Java 代码量比 Node.js 版本多,主要增加在配置文件。模块和分层相对独立,重构和新增功能的影响范围更容易控制。Spring 的事务能力可以通过注解使用,无需在业务代码中显式传递事务,也能接入公司提供的 mafka、tair 等服务。代价是资源消耗增加,需要多开一台服务器;Maven 编译时间也较长,修改后不能像 Node.js 一样直接运行验证。
开发方式的差异
Node.js 和 Java 的错误处理、数据处理、日志及部署方式不同。
1. 异常处理
JavaScript 是弱类型语言,可以通过函数返回值表示错误;调用方判断是否存在 error 对象,再将错误返回给终端。也可以抛出异常,并由全局异常拦截中间件处理。
Java 是强类型语言,业务错误和其他错误通常通过抛出异常处理。
2. 数据处理
JavaScript 同时支持面向对象和函数式风格。使用 lodash 可以写出如下链式调用:
_.chain(arr).filter((item)=>{return item.name.indexOf(dep_name)!==-1}).map(item=>item.count).reduce((total,n)=>{return total + n}).value()上面的代码不是纯函数式编程,而是围绕对象或容器的链式调用,类似 jquery 对象的链式调用。链上各函数可以分别优化。是否惰性执行取决于具体库和调用方式;纯函数式库【ramda.js】还会使用 compose、fuctor 等概念组合计算。
Java 也可以用 if、else、while、for 处理这类逻辑,但通常需要声明中间变量或承载类。Scala 在集合操作和函数组合方面提供了更直接的表达方式;当时也考虑过使用 Scala + Play 框架,但团队人力不支持。
3. 日志管理
Node.js 项目通常通过 pm2 查看 access log。npm 上有类似 log4j 的库,但需要在各使用位置显式埋点,配置方式不如 Java 灵活。
Java 通常使用 log4j 或 logback。声明全局 Logger 后,可以调整输出级别和输出位置。
4. 部署
Node.js 可以通过 ops 系统配置两个脚本并部署到服务器。
Java 需要编译,并且与不同版本有关。当时的 ops 使用 Docker 编译时会重新下载所有依赖,耗时较长,也需要与 SRE 一起处理环境问题。新版基于 Docker 的发布系统正在研发,预期会改善这一问题。
