最近公司招聘海外后端研发,所以整理一份技术栈的资料给他们,但是想来这份整理也适用于所有后端研发,所以去掉了敏感内容,把它呈现于此,本文重在概述,毕竟篇幅有限,欢迎【关注】https://www.zhihu.com/people/wenxi.zhang,后续可能把单点拓展成文,详细地一一阐述,另外笔者见识有限,毕竟也没有可能在所有大厂工作过,所以如果有疏漏可以在留言处赐教
目录
后端开发概述
负载均衡 - Load Balance(LB)
微服务生态
Thrift
服务发现
Consul
微服务框架
数据库(Database)
Mysql
Mycat
DRC
缓存(Cache)
Redis
Redis 集群方案
KV-DB
消息队列(MQ)
RocketMQ
Kafka
对象存储
Elastic Search
何为后端开发?以一个网站为例,通常来说,前端研发注重页面的展示,交互逻辑。而后端研发,则注重在发生在前端背后(backend)的逻辑上,例如给前端返回数据,存储数据。对于一个电商网站,一个简单的下单动作,后端可能包括商品数据查询,优惠信息计算,库存维护,用户优惠券维护,订单生成,商家通知触发等等。在很多大公司前后端的配比是1:3甚至更高,因为一个复杂的业务系统,前端的展示仅仅是冰山一角,更复杂的业务逻辑都隐藏在后端。
通常来说,当用户触发某个行为后,客户端会通过http/https请求,和我们的服务器建立连接,发送请求,往往这个请求首先会被链接到负载均衡(LB)上,负载均衡根据配置,将请求转发到内部的API服务上。这些API服务,根据不同的业务逻辑会请求其他服务(Service),这些服务各司其职,例如读写某 Mysql 表、读写缓存,甚至请求搜索引擎来完成相应的任务。而API服务在完成相应的步骤后,也会将数据返回给客户端,客户端根据前端逻辑完成相关的展示。
下面这个图,简单的展示了服务端研发可能使用服务组织方式和相关技术栈,后续会对所有技术栈和大厂使用场景一一简述。
负载均衡作为连接内外的门户,在分布式系统中,有这非常核心的作用。下面的图是负载均衡最直观的呈现:
它将流量从外部转发到内部系统,对于同样的请求内容,不同时序的请求会被转发到不同的服务实例上。对每个服务实例而言,它只需要承担系统总流量的 1/N 从而降低了单个服务的负载,提升了服务整体相应速度和系统稳定性。负载均衡器会保持跟踪所有下游服务的状态,如果服务不可用,则会被从调度移除。
一个最常用的负载均衡就是Nignx反向代理,在上图中,如果使用Nginx做负载均衡,最简单的方法就是配置 upstream,例如下:
#配置负载均衡实例
upstream user_api
{
server 10.0.6.108:7080;
server 10.0.0.85:8980;
}
#配置转发到
user_api upstream location /user
{
proxy_pass http://user_api;
}
显然,这份配置中要指定 user api 服务已经部署实例的 IP 地址和端口。如果没有自动化的发布方案,意味着每次开发新的API都需要在服务发布好以后,更新 Nginx 配置,然后通过 reload nginx 的方式将API发布上线。如果API不经常变动,这种做法也能支撑业务发展;但是如果公司业务快速发展,经常频繁发布API风险就会比较大了。在服务重启切换配置的过程中,可能导致一些请求处理被丢弃,就连服务扩容和缩容(增加减少负载均衡实例),也要变更相应的nginx配置。
所以很多大厂,都会建设自己的 LB 平台,在这里你可以配置需要暴露出去的 URL,提供服务的内部服务标识,也会支持一些额外的配置,包括限流、熔断参数等。而要做到这些,往往需要对 Nginx 原生的负载均衡能力做拓展,例如使用 dyups 模块支持动态上下线配置;需要一个额外的管理平台,来管理所有对外API;需要服务注册中心维护所有的服务对应的集群和实例。
同时需要启动一个 API Watch的在线常驻服务,负责监听API配置变更和注册中心每个服务实例的上下线变更,生成 dyups 模块可以识别的 Nginx 配置,在线 load 到 Nginx 就可以完成服务动态上下线了。原理如下图:
当然,这只是一个最基本的功能和原理展示,大厂们往往根据不同的在线使用场景会有很多优化和系统设计的考量。
微服务 - 也被称为微服务架构 - 一种将整个后端服务,按照领域、模块分解为若干独立小应用的一种架构方式。微服务有如下特点
服务可以单独编写、发布、测试、部署,相比于所有功能集中于一体的单体服务,可维护性更强
服务彼此之间依赖服务通信的方式松耦合
按照业务领域来组织服务,每个团队维护各自的服务
下图直观的阐述微服务的概念:
既然微服务体系是按照组织结构、功能模块独立进行开发、测试、部署的,那么微服务架构就要解决因为独立部署带来一些问题,例如通讯协议,远程调用(RPC),服务发现,服务治理等。有能力的大厂往往会有自己的框架来解决上面的问题,例如淘宝的HSF、阿里开源的 dubbo,能力不足的小厂也可以从开源社区中选择合适技术为我所用,“拼凑”出合理的解决方案,下面主要从开源社区选择一些可用为我所用的技术来介绍。
Thrift不仅仅是一个轻量的,高性能的远程调用(RPC)通讯协议,也是一个优秀的RPC框架。Thrift 使用一种被称为 IDL 的接口定义语言,来定义远程调用的接口。使用官方提供的工具可以将IDL文件生成微服务服务端(Server)和客户端(Client)代码。这里 Server 端指提供服务的一方,而 Client 则指服务调用方,Client 通过 RPC 对 Server进行调用。
利用这份生成的代码,就可以实现Client通过指定IP和端口的调用Server服务了。个人感觉 Thrift 最大的优势是高性能,跨语言,以及足够轻量。跨语言是很好的特性,因为一个大公司的不同部门,可能语言技术栈会有差异,使用 Thrift 可以屏蔽这种差异,让彼此专注。
上面提到,如果只依赖 Thrift 我们可以实现通过指定IP端口的方式进行服务调用,但是显然这是不可行的,我们需要 Client 动态感知 Server 端服务的存在以及提供服务的所有实例。服务注册中心就是解决这个问题而诞生的概念,可以简单理解注册中心就是一个保存着服务状态的”数据库“,服务成功启动后到注册中心去注册,并且保持和注册中心的心跳以维持服务在注册中心的最新状态,当服务下线或者异常退出,服务可以主动通知注册中心下线或者被注册中心通过心跳失败感知到。
常见的服务注册中心例如 Spring Cloud 框架中官方提供的 Eureka,Dubbo 默认使用的 Zookeeper。Spring Cloud 和 Dubbo 也对 Consul 增加了原生支持,这里也主要介绍下Consul。具体对比可以参考[1]
Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能。Consul的功能都很实用,其中包括:服务注册/发现、健康检查、Key/Value存储、多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可。
如果使用 Spring Cloud 或者 Dubbo 等微服务框架,可以通过配置实现使用 Consul 作为服务注册中心,服务启动后,在Consul提供的Web界面就可以查到相应的服务。服务客户端可以在第一次调用服务端前,通过Consul进行服务端实例的查询然后按照查询奥的服务实例进行远程调用。
Consul 的 web界面:
开源社区中知名的微服务框架包括 Spring Cloud, Dubbo 等,这些框架配合生态中的一切其他组件可以解决例如服务注册&发现、负载均衡、熔断限流、调用链追踪等绝大多数问题。不过当业务发展到一定阶段,还会有更多问题要解决,例如服务调用鉴权等问题。
所以各个大厂几乎都自研自己的微服务框架,但是基本的做法都是在开源社区选择一部分,自己扩展一部分,例如通讯协议和RPC选择Thrift,服务注册中心选 Zookeeper/Consul,然后需要自研扩展的部分就是例如服务注册,服务发现,负载均衡,统一监控,鉴权等公司特色的需求。
对于一个小公司而言,可能会选择把所有数据保存在 Mysql 中,因为全部业务数据的容量可能也只有百G。但是对于大厂,每天产生的数据可能都是T级别的,如果都保存在Mysql中会有诸多问题,例如存储成本、后续的修改查询效率、高并发场景下存储的极限能力等。
数据有它不同业务特性和使用场景,业务特性很好理解,例如我们不容忍交易数据发生丢失并且在很多操作它的场景,要求强一致性;而用户评论,则能容忍很小比例丢失,并且评论计数器和评论数目之前的如果出现微小差距,用户也很难察觉到;而服务日志数据,则能容忍更大程度的丢失,只要不影响开发Debug可能就不会有人追究。
数据不同的使用场景,也对存储有不同方面的要求。例如同样是用户资料,用户资料查看自己的资料,一定要保证资料是用户最新更新的,并且不能容忍出错,哪怕是页面相应速度略微慢一点;但是用在推荐场景作为用户画像作为模型输入的时候,就能容忍这个数据不是最新的,但是要求数据访问速度要高,因为推荐场景往往对成千上万个候选排序,画像数据访问慢则直接拖累了整个推荐系统的效率。
对于Web应用而言,关系型数据库的选型中 Mysql 无疑是最佳选择。它体积小、速度快、开源授权使得它拥有成本低,支持多种操作系统,有丰富的社区支持,支持多种语言连接和操作。
Mysql是保存业务数据的理想之选,首先关系型数据库在设计之初,从概念上就在支持业务数据建模的概念,关系型数据库能结构化的描述业务需求。Mysql 对ACID属性的支持,也能满足不同业务场景下对数据操作的不同诉求。但是在大厂背景下,Mysql也有它的限制。
首先,对于在线大表DDL几乎不太可能,DDL指表结构变更之类的操作。对于一张动辄几千万数据的表,Alter Table 可能需要几小时,除非提供备案方案,否则服务在这段时间将不可用。
其次,在线查询要注意性能优化,避免慢SQL拖垮DB的场景。常见的性能问题包括查询未命中索引而触发全表扫描;使用了聚合查询(group by)触发全表扫描等
还有,大厂特别是ToC常见的大厂,每天产生的业务数据异常的大,Mysql存储超过几千万性能会下降,所以需要使用分库分表的方式来解决海量数据场景下的存储问题。
Mycat就是一个解决数据库分库分表等问题的数据库中间件,也可以理解为是数据库代理。在架构体系中是位于数据库和应用层之间的一个组件,Mycat 实现了 Mysql 的原生协议,对于应用它感知不到连接了 Mycat,因为从协议来讲,两者是一样的。而Mycat将应用的请求转发给它后面的数据库,对应用屏蔽了分库分表的细节。Mycat的三大功能:分表、读写分离、主从切换。
下图通过 Mycat schema 配置和实际 DB 部署来简要说明 Mycat 分库分表功能的实现:
例如表 A,配置中它有两个数据节点,分别是 datanode1 和 datanode2,含义是逻辑表 A实际对应了两个物理存储,分别是 db1 和 db2。对于应用而言,应用连接 Mycat 感知到的时候逻辑表 A,而在底层 A 被拆分,存储在两个 db 中。
DRC 是 Data Replication Center 的缩写,在使用Mysql作为核心存储的场景下,我们可以使用Mysql原生的主备方案,实现同城灾备。但是如果 Mysql 部署在跨国,跨洲的场景下,原生的灾备方案就有诸多问题了,所以各大厂几乎都有自己的DRC方案。
不过,虽然各自有不同的实现,但是原理和依赖的核心组件基本相同,本文从互联网上找到饿了么DRC组件阐述其原理。
本图中,异地机房分别为北京机房和上海机房。本地机房(图中为北京机房)会启动一个 DRC Replicator,它和Master节点通信并在通信协议上模拟 Mysql Slave,Replicator将Master数据库的binlog变更实时拉取到本地。然后把binlog解析,通过消息中间件将变更发送到异地机房(北京机房)。异地机房启动一个DRC Applier的应用消费数据变更消息,然后把这个变更操作同步到本机房的Master上,就完成了异地数据同步的操作。图中展示的是北京机房数据同步到上海机房的场景,实际反过来也是一样。
DRC在设计和实践中最常见的问题就是DB自增类型主键冲突,以及数据因为同步消息丢失而最后导致的不一致,前者可以通过强制使用ID生成器或者自增ID设置相同的增加值和不同的初始值等方式解决。而后者要么采用一个规则同步最终数据,或者进行人为数据干预。
如果稍微深入研究Mysql的存储原理,我们不难发现,数据是存储在磁盘中的,虽然我们可以通过索引等数据结构,降低每次查找数据的响应时间,但是对于高并发的在线应用,直接查找数据库依然很容易触碰Mysql性能瓶颈,所以我们需要缓存来缓解DB查询的压力,当要查询的数据命中缓存后,直接从缓存中获取数据,从而降低DB的访问压力。常见的缓存有两种策略:
本地缓存:不需要序列化,速度快,缓存的数量与大小受限于本机内存,很多语言提供一些框架来支持内存缓存,例如 guava cache,spring默认集成的 Ehcache。
分布式缓存:需要序列化,速度相较于本地缓存较慢,但是理论上缓存的数量与容量无限制(因为分布式缓存机器可以不断扩展),常见的分布式缓存包括 Redis 和 Memcache。本文主要介绍下 Redis。
Redis 是基于内存的缓存中间件,正因为基于内存,所以具有非常快的相应速度。支持丰富的数据结构,例如 string、hashes、list、set、sorted set等等,应用非常广泛的应用。
常见的缓存读写策略包括Cache-Aside,Read Through,Write Through,具体可以参考[2],不过文中缺少一种Cache 读写方案,这也是很多高并发在线应用比较常用的一种方式。
Write Behind Caching模式
Write Behind Caching 这种模式通常是先将数据写入到缓存里面,然后再异步地将DB更新的数据写入到cache中,这样的设计既可以直接的减少我们对于数据的database里面的直接访问,降低压力,同时对于database的多次修改可以进行合并操作,极大的提升了系统的承载能力。这个模式下,应用在读数据的时候,不感知DB,只感知Cache,优势在于简化了设计,缺点在于强依赖Cache,如果Cache出现问题例如宕机,则读会失效。
伴随着需要缓存的数据量增加和高可用的依赖,大厂的Redis都是需要集群化方式部署的。一方面通过主从模式提升了系统的高可用,另一方面通过集群模式将系统演化为可用无限扩容的模式。
Redis 从 3.0 版本开始,原生的支持了集群模式,通过 Sentinel 集群实现动态的主从模式[3];原生的集群模式,将所有的数据划分到 16384 slots中,而集群中的每个节点,会分配若干 slots。然而原生集群的方案,虽然简化了集群的设计,但是却增加了客户端的负担,需要客户端对Moved/ASK [4] 事件做封装处理,同时需要维护路由表提高访问效率,增加了客户端设计的复杂度。
所以大厂往往不会选择 Redis 原生的集群化方案,而是使用基于Proxy的集群化方案,业界比较知名开源 Proxy 有 Twemproxy 和 Codis [5],本文简要介绍下 Codis,实际上很多知名大厂的 Proxy 都来源于Codis。
Codis 引入了Group的概念,每个Group包括 1 个 Redis Master 及至少1个 Redis Slave,可以认为每个Group是一个系统分片(Shard),与 Redis-cluster 一样,Codis 也采用预分片的机制,整个集群分成 1024 个 slots,通过一致性哈希算法,将Key映射到某个 slot,再通过维护在 Zookeeper 里的分片路由表,将Key的请求转发到对应的Group上。
Codis 提供了一套运营监控界面,运维人员可通过 Dashboard “自助式”地进行主从切换。
而对于应用而言,访问 codis 和访问原生的 Redis 并没有任何区别,节点的动态上下线,slot 分配的变更都在 Proxy 层完美的对应用屏蔽了。
前文讲述了 Mysql 和 Redis,或许对于大多数公司,这两类存储已经足够。前者用于保存业务数据,后者用于集中式缓存。但是对于大厂,还有若干场景上面两种存储无法满足:例如推荐系统在线预测场景,需要将用户画像、商品画像、商家画像、用户商户交叉画像在线加载预测上下文,特征处理后给到模型做预测打分。ToC的互联网产品的注册用户很可能过亿,所以用户画像总存储很可能百G甚至T。
如果是这样规模的数据,Mysql的读性能肯定扛不住在线预测场景;Redis 是内存缓存,存储昂贵,同时在容灾恢复时候,Redis需要将AOF或者RDB数据载入内存后才能提供服务,数据量过大需要很长的恢复时间。所以需要另外一种存储能解决这个问题。
几乎所有大厂都有属于自己的KV-DB,例如360开源的Pika,饿了么通过购买Tikv封装而成Ekv,字节跳动的 Abase。Pika 和 Tikv在存储底层都使用了RocksDB作为数据存储,而RocksDB它是将数据存储在硬盘上的,Pika 和Tikv在上层构建的都是集群化方案,主从模式等,基于内存的一致性Cache等。下图是Pika架构图:
伴随着业务的复杂,我们往往会遇到这个场景,一个数据操作后,需要触发下游若干个子操作。例如外卖场景,用户下订单成功,要通知商家用户订单,要物流平台对订单进行调度和派单,要触发一些后置的风控逻辑对订单合法性进行校验等。如果是同步的设计,需要在订单完成后对后续的操作一一进行API调用,这样的做法让订单流程依赖更多外部服务,提升了业务复杂度,降低了服务的稳定性,所以我们需要消息中间件来解耦操作。依赖的服务依赖下单消息,而不是在下单结束后,通过接口调用的方式触发。
我们可以把消息队列(MQ)比作是一个存放消息的容器,Producer 负责生产消息,将消息发送到MQ,Consumer取出消息供自己使用。如图所示:
消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理降低系统耦合性,除此之外,还可以依赖消息队列提高系统性能和削峰填谷。
关于削峰填谷这里我举一个应用场景 —— 抢购。抢购是很多网站促销的重要场景,呈现方式往往是通过倒计时的方式,在某个固定时间放出限量的优惠产品。开放时间到来时,会有大量用户触发购买动作,我们知道下订单往往是一个耗时比较久的操作,需要对库存,营销信息,用户等多方面数据做查询、校验、计算操作,同时为了控制超卖,可能会引入锁机制。如果处理不好抢购的瞬时流量,可能会打垮系统。一种优化思路是可以将瞬间的购买请求转发到消息队列中,再由消息队列的消费者消费消息,进行后续的订单操作,从而对系统进行流量削峰。
从大的应用场景上,我们可以将消息队列的应用拆分成两类,一类是业务场景,例如上文提到查到用户订单消息,这类场景的吞吐量未必很大,但是需要消息中间件具有一些更高级的易于业务使用的特性,例如支持消息持久化,延迟消息等。另外一类是大数据场景,该类场景对吞吐量有极高的诉求,例如用户行为搜集(User Behavior Tracking)等。这里只介绍两种上面场景适合的消息队列中间件。
RocketMQ是一款分布式、队列模型的消息中间件,是由阿里巴巴设计的,经过多次双十一流量洪峰的洗礼,让它更有光环效应;RocketMQ是纯java编写,基于通信框架Netty这也让它拥有更好的社区基础和可改造的空间。相比于Kafka,RocketMQ支持很多业务友好的特性,具有以下特点: 支持严格的消息顺序,支持Topic与Queue两种模式,亿级消息堆积能力,比较友好的分布式特性,同时支持Push与Pull方式消费消息
Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,毫秒级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略。
对象存储是面向对象/文件的、海量的互联网存储,它也可以直接被称为“云存储”。对象尽管是文件,它是已被封装的文件,也就是说,在对象存储系统里,你不能直接打开/修改文件,但可以像ftp一样上传文件,下载文件等。另外对象存储没有像文件系统那样有一个很多层级的文件结构,而是只有一个“桶”(bucket)的概念(也就是存储空间),“桶”里面全部都是对象,是一种非常扁平化的存储方式。其最大的特点就是它的对象名称就是一个域名地址,一旦对象被设置为“公开”,所有网民都可以访问到它;它的拥有者还可以通过REST API的方式访问其中的对象。因此,对象存储最主流的使用场景,就是存储网站、移动app等互联网/移动互联网应用的静态内容(视频、图片、文件、软件安装包等)
不过自建对象存储成本很高,很多中等规模的厂,都会选择商业化对象存储方案,例如七牛云,阿里OSS等,用来降低研发和维护成本。
无论是在线应用还是管理后台,都有模糊搜索的需求,例如用户搜索感兴趣的帖子,评论,视频。管理员对命中一些关键词的内容进行批量下架等处理。这种模糊搜索,如果使用Mysql原生的查询,效率是非常低的,能想到的朴素做法可能先将用户Query进行分词处理,然后用分词的结果依次提交到Mysql做某字段的Contains条件查询。Contains操作会对表进行全扫描,如果有千万数据,效率难以想象。
针对这样的场景,倒排索引是非常理想的方案,倒排索引(Inverted Index)是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。
如果需要索引的数据不多,索引更新频率也不高,简单的做法可以使用 Apache Lucene 构建一个非常轻量的倒排索引,将需要倒排的数据,通过 Lucene 提供的API 灌到索引文件中,而该索引文件可以在后续的搜索服务中被 Lucene 加载,提供查询服务。
但是大厂场景往往是拥有海量需要索引的数据,同时要支持在线构建索引文件和灾备能力,在开源社区中 Elastic Search 就是非常好的选择之一,ES 底层也是基于 Lucene,但是它提供分布式的文档存储引擎,分布式的搜索引擎和分析引擎,支持PB级数据。
Reference:
[1] 主流微服务注册中心浅析和对比
[2] Caching Strategies and How to Choose the Right One
[4] redis 集群详解及搭建过程
[5] Redis 的 MOVED 转向与 ASK 转向
[6] 基于 Twemproxy 与 Codis 的 redis 集群方案比较
[7] Rocketmq原理&最佳实践
[8] 对象存储(云存储)概述