序
高德在构建Go生态演化过程中,已经实现了QPS从0到峰值千万的飞跃,本篇文章主要介绍在此过程中积累的一些技术决策及性能优化和重构经验。阅读本文读者会有以下3点收获:
1.高德Go生态发展历程及现状分析
2.高德云原生Serverless落地情况&实战
3.高德Go生态项目落地实战( 重构&优化经验 )
一、高德Go生态发展历程及现状分析
为什么要用Go语言开发服务,说起来有些复杂,很多时候语言都不是问题,每种语言都有适应自己的场景和能够充分发挥优势的环境, 每种语言几乎都统治了一个领域或者一段时间。
高德在用Go之前也有Python、PHP、Java、C&C++等服务,接下来我会根据下面大纲来描述为什么高德服务端选择Go来做服务并简单介绍Go生态,而不是继续沿用Java,或者用Python以及新生态语言Rust等,也会从GC、内存模型和并发模型上进行剖析,了解底层原理。
本节大纲:
1.1 云原生生态适应的架构是什么
1.2 天然契合云原生架构的语言
1.3 Java和Go底层原理&云原生匹配度
1.4 Go和ErLang的并发模型区别&选择
1.5 高德Go生态衍生的中间件
1.6 Go的生态介绍
1.1 云原生生态适应的架构是什么
谈到为什么选择,首先交代下当下环境,在云原生架构时代,大家都在上云,互联网也从单体架构->分布式架构->微服务架构( 也包含宏服务 )->云原生架构。
云原生架构这个话题很大,云原生在近一两年让互联网达到了一个新的时代,大家也达成了共识,一起共建云原生基础设施。遵从以下6个特质和4个要点来考量云原生中间件,总结下来就是围绕“稳定、效率、成本”3个点。
6个特质:模块化、可观察、可部署、可测试、可替换、可处理
4个要点:DevOps、持续交付、微服务、容器
先来剖析特质,然后围绕着特质总结下云原生架构要点。
首先,云原生架构是由微服务架构衍生转化,其一定具备微服务的特性,微服务的2大核心特性为:服务单一、模块化。
这两个特性都是来约束微服务要足够小,且是无状态服务,这样在分布式并发场景下,才会游刃有余。
其次,服务具备弹性分布也是不够的,在整个服务的生命周期来看,部署也是运维一大头疼问题,解决部署之后,又要面临决策,什么时候部署,如何部署,这些问题其实也是在质问我们,你的“可观性”做的如何,可观性是相当于给服务做CT一样,直接剖析出服务的问题。
最后当你给服务做CT发现问题后又需要对症下药。一系列操作之后,最终完成部署上线,其实整个操作都在围绕云原生部署的视角,也就是上面4个要点。
1.2 天然契合云原生架构的语言
通过云原生架构内容理解,其实云原生根本没有限制用什么语言,反而还鼓励在对应的生态环境中可以用任何语言。比如现在有很多大家常见的语言。
Rust:视内存&安全如命的语言
Go:自称“很快”的语言
Java:生态圈及其庞大的语言
Python:一个掌控AI&机器学习生态的语言
...
这里没有列出Nodejs和PHP不是不好,而是这两个语言大众都非常了解。一个统治前端,一个曾经统治后端(脑海里还记得曾经那句“PHP是世界上最好的语言”)。
上面的语言简介只是形容优势,不是划定界限,Python在Server端也有很多优秀的框架,Java也可以做大数据等等。下面我们针对罗列的语言简单描述为什么适合云原生。
谈到Rust其实还是有点让人胆怯的,Rust刨除学习成本和编码成本高的前提下,它其实很适合云原生,体积小,内存安全还有很多优秀的异步网络库使得Rust安全、性能又高。强制执行Rall的机制+所有权牢牢把控内存的安全,在云原生共享资源的前提下也提高了安全性,如果再加上Tee架构简直是完美的机制。这也是为什么现在新生区块链项目很多都会选择Rust的初因。
这里简单描述下Rall和Rust如何解决内存安全的。
Rust的RAll(RAII要求,资源的有效期与持有资源的对象的生命周期严格绑定,即由对象的构造函数完成资源的分配「获取」,同时由析构函数完成资源的释放。在这种要求下,只要对象能正确地析构,就不会出现资源泄露问题)。
Rust中分配的每块内存都有其所有者,所有者负责该内存的释放和读写权限,并且每次每个值只能有唯一的所有者
引用(Reference)是Rust 提供的一种指针语义( 这里也可以称为借用,可变和不可变借用有前后要求限制 )。
Rust是如何解决内存安全问题的?
使用未定义内存
Rust中的变量必须初始化以后才可使用,否则无法通过编译器检查。
空指针
开发者没有任何办法去创建一个空指针。Rust中使用Option类型来代替空指针,Option实际是枚举体,包含两个值:Some(T)和None,分别代表两种情况,有和无。这就迫使开发者必须对这两种情况都做处理,以保证内存安全。
悬空指针
悬空指针指的是内存空间在被释放了之后,继续使用。Rust通过所有权和借用机制解决这个问题。
缓冲区溢出
Rust编译器在编译期就能检查出数组越界的问题,从而完美地避免了缓冲区溢出。
非法释放未分配的指针或已经释放过的指针
Rust中不会出现未分配的指针,所以也不存在非法释放的情况。同时,Rust的所有权机制严格地保证了析构函数只会调用一次,所以也不会出现非法释放已释放内存的情况。
通过以上内容+Rall+所有权相关机制,使得Rust内存变得很安全,但是Rust这种机制也起到反作用,有些场景在生命周期短引导下依然是被编译器认为不合法的,需要Unsafe来实现。整个项目代码写起来非常的复杂繁琐,很多人也是学着学着就被Rust给成功劝退了。但是当你掌控Rust之后,你会发现Rust在某些场景下的实现会变得更加简单。
再看Go语言, Go语言发展至今已经迭代过很多版本了,由单纯的MG模型进化为PMG模型,栈也从连续栈变成了拷贝栈,现在申请的内存也会慢慢的归还给操作系统,整体来说没毛病,以动态语言的方式写静态语言,也实现了Runtime帮大家管理GC和内存相关事宜,简直是保姆级别的语言了。很多初学者可以快速上手,以前那些潮流语言(PHP)也可以快速转到Go语言上。
说到Java,其实有点不太敢说,因为公司是主Java技术栈,而且Java的生态很大很全,公司还有很多Java的性能优化与JVM层面的优化来保证Java更加稳定,性能更加好。但是有些基因是语言天生的,就冷启动慢这个问题在云原生高并发服务的场景就不适合。
最后再看下Python吧,这里就简单描述一句,环境真是让人头疼要命,一个全局虚拟机锁(注:这里不叫锁,Python虚拟机约束全局只能有一个控制线程在执行)导致不适合做Server,但是在机器学习和AI领域中确实大放光彩。
说了这么多不太相关的内容其实就是想说明,云原生不会要求大家必须用什么语言,只会告诉大家合理的利用语言,其实我倒是觉得云原生大部分提到的都是微服务+容器的架构,或者是在加上弹性扩容后的Serverless架构。
1.3 Java和Go底层原理&云原生匹配度
1.3.1 GC层面来:
Java的GC迭代
Serial GC:完全串行的标记和整理过程,需要暂停整个程序。
ParNew GC:多线程Serial GC。
CMS (Concurrent Mark Sleep) GC:标记清理算法,并发标记。三色标记法由此开始。
G1 GC:DK9之后的默认GC。它可以设定停顿时间,将最差情况得到一定的改善(引入Region的概念)。
ZGC:ZGC主要新增了两项技术,一个是着色指针Colored Pointer,另一个是读屏障 Load Barrier,还实现了动态Region (ZGC仅支持 64 位平台)。
Go的GC也经历了很多版本的进化。
Go 1.0:完全串行的标记和清除过程,需要暂停整个程序。
Go 1.1:在多核主机并行执行垃圾收集的标记和清除阶段。
Go 1.3:运行时基于只有指针类型的值包含指针的假设,增加了对栈内存的精确扫描支持,实现了