选择一门后端语言

时间:2022-08-28 14:17:47

先说结论

我建议选择Erlang作为后端语言。
基本要求:服务端运行时具备热更新,低耦合,高容灾,高并行;开发时具备灵活,简单,CPU调度及内存使用清晰可见。
排除部分后端语种 C,C++ (静态语言容易出灾难性Bug),Nodejs(内存管理有一定缺陷GC较弱),Scala(学院派语种,不稳定版本兼容性差)
下面统计剩余可靠的语种(总分100)。

语言
性能
编码简单
热更新
低耦合度
高容灾
高并行
灵活性
性能追踪
快速反应
框架
总分
Go
10
10
2
8
5
9
8
5
4
4
65
Erlang
5
5
10
10
10
10
9
7
8
8
82
Java
10
6
5
5
7
8
6
9
6
10
72

但这些都不是主要的,大家看到Erlang都认为高并发,性能差,但Erlang相关书籍第一章语言叙述的DNA特征其实是:容错。

 
并发
Go 对高并发的支持通过 goroutine 实现。goroutine 可以理解为轻量级的 线程(thread)。同一个 Go 应用创建的 goroutine 共享地址空间。
Erlang 的高并发通过轻量级 进程(process)实现,每一个进程都有独立的状态记录。
另外,使用 goroutine 要注意,goroutine 运行完毕后,占用的内存放回内存池备用,不会释放。
对于每一个任务都需要有独立状态的场景,Erlang 的 process 更有优势。
抢占式调度
Erlang 的任务调度器有一个 reduction budget 的概念。进程的任何操作都会造成预算消耗,包括 函数调用、调用 BIF、进程堆垃圾回收、ETS 读写、发消息(目标邮箱堆积的消息越多,消耗越大)。Erlang 的 正则表达式库 也被做了修改以支持 reductions。所以如果进程在长时间执行正则表达式匹配,也一样会消耗 reductions,也会被抢占。
Go 之前的调度器只在 syscall 发生时调度,优化后可以在任何函数调用时调度。但是要注意,如果在 goroutine 里写一个死循环,Go 的调度器不能有效抢占,同一个调度器的 其他 goroutine 会被挂起。
垃圾回收
像 Java 一样,Go 的垃圾回收是全局的,这意味着一旦垃圾回收被触发,所有的 goroutine 都会被暂停,造成一段时间的业务延迟。
Erlang 的垃圾回收是 进程 级别的,每一个进程都有自己独立的垃圾回收器,一个进程的垃圾回收被触发,不会造成其他进程被挂起。相对来说带来的业务延迟小。
错误处理
Erlang 的每一个进程都有 进程 ID (PID),同时也可以给进程注册名字,也就是说每一个进程都有独立的身份,可以有效的监控每一个进程的状态。进程异常退出时,可以捕捉到退出事件,并重启进程(参见 otp 的 supervisor/worker)。
Go 的 goroutine 没有身份识别,goroutine 的状态没办法监控。
动态反射
Erlang 动态语言的特点,使它天然支持 REPL,另外 Erlang 支持 remote shell,我们可以在 Erlang 运行时,连接到 remote shell 与任何一个进程交互。这些特性对一个需要长期运行的复杂系统的维护带来了极大的便利。开发阶段也能有一些便利。
Go 是静态语言,不支持 REPL。
静态编译
Erlang 是动态语言,有所有动态语言的所有缺点:
  • 运行速度慢
  • 不能做早期错误检查,需要依赖全覆盖单元测试
  • 代码规模大了,给编写带来困扰
Erlang 现在也引入了 spec,对函数的参数返回值在编译时做类型检查,但是跟静态语言比起来效果差的很远。
不过正是因为是动态语言,Erlang 实现了运行时代码替换,这个特性对一个需要长时间运行的工业级产品,是一个非常重要的功能。
Go 是静态语言,运行速度快,编译时做严格的类型检查,可以避免很多隐患。
框架
Erlang 的 OTP 框架支持服务器端开发常见的几种模式(applications, supervisors, wokers),方便代码的组织。
Go 暂时没看到类似的框架。
第三方库支持
Go 是一个相对比较新的语言,虽说现在很多项目都开始支持 Go,但很多第三方库的成熟度暂时不如 Erlang。
总结
对于要求低延迟、高并发的后端服务,我们近期还是采用 Erlang 为主。但使用 Erlang 的过程中,Erlang 缺乏静态检查的手段,也是一个很麻烦的问题,目前的做法是要求大家都使用 IntelliJ IDEA 编写代码,可以通过 IDE 提前发现部分语言问题。