前言
Nodejs 在蚂蚁和阿里已经发展了四、五年时间,从最开始「前端工程师的玩具」,到 Web、BFF 场景的破局,逐步走到线上甚至是一些核心业务,非常不容易。回头想想 Nodejs 为什么能活下来?依靠的绝不仅仅是:非阻塞I/O、事件驱动、轻量这些官方宣传的特性,我认为更重要一点是我们打通了和 Java 的桥梁,实现了互联互通,这才让它真正融入阿里的技术体系。
伴随蚂蚁 SOFA(Scalable Open Financial Architecture)技术栈的开源,我们也开源了两个 Nodejs RPC 相关模块,希望能填补 Nodejs 社区这块的空白,也将我们几年来在 Nodejs 基础技术的一些经验做个总结和分享。
sofa-bolt-node:蚂蚁通讯协议 Bolt 的 Nodejs 实现
https://github.com/alipay/sofa-bolt-node
sofa-rpc-node:一个通用的 Nodejs RPC 模块
https://github.com/alipay/sofa-rpc-node
上一篇我们介绍了 RPC 通讯协议,它是实现 RPC 的第一步,接下来我们要讨论一下 RPC 的服务发现(Service Discovery)
什么是服务发现?
概念上讲,服务发现就是通过服务唯一标识来获取服务地址的过程,它在 RPC 里扮演了重要角色。下面我用一个点外卖的例子来通俗解释服务发现到底做些什么?它为什么重要?
假设我是一家外卖店的老板,我要考虑的一个问题是:如何让客户能够找到我的店,并且点我的外卖呢?最先想到的是发小广告,客户通过广告里的订餐热线就可以找到我们,这个过程其实就是最简单的服务发现。
这个方案是有效的,但是营运了一段时间后,我发现一些问题:
小广告的传播力有限,投放的精准度也不够,很多人可能随手扔进垃圾桶
客户可能因为丢失卡片或忘记号码而无法下单
一旦留的电话停机了,整个服务不可用
缺货、或者停业还是会接到客户电话
我的生意越来越好,很快开了分店,但是老客户并不知道新店的热线
后面,我听说有一个叫饿了么的点餐平台,抱着试一试态度在上面注册了我的店。没想到这个平台给我带来了大量的订单,而我不再需要到处发小广告,只需要专心做好饭菜、提高服务质量、维护良好的口碑就可以得到稳定的客源。
其次,我也不用担心电话停机、缺货、停业、开新店等服务变更带来的麻烦,我只需要在平台上修改服务信息即可。对于消费者来说他们也不需要收集一大堆外卖卡片,只要安装一个 app,就可以找到丰富的美食、并且可以根据评分选择更加优质的服务。这已经是相当高级的服务发现实现,可以看出无论对于提供者还是消费者,服务发现都是至关重要的。
服务发现的分类
硬负载
硬负载顾名思义是依靠硬件设备做负载,在调用链路上加一个独立部署的硬件设备(一般就是我们所熟知的 F5/LVS/HAproxy 集群),通过它们对后端的服务进行发现,对流量进行负载均衡。
+----------+ invoke +---------------+ | Services |-+
| Consumer | --------> | Load Balancer | -----> | Providers | |-+
+----------+ +---------------+ +------------+ | |
|-------------+ |
+-------------+
- 优点
存在一个统一的流量集中化节点,可以实现一些全局性的掌控,比如路由、鉴权、安全防控等等
- 缺点
硬负载设备的成本高,不易维护
在调用主链路上有一定性能损耗
硬负载设备需要实现集群化部署的模式以解决单点故障的问题
软负载
同理,软负载是依靠软件方式进行服务发现和负载均衡,这种方式具有以下特点:
没有了中心化的硬负载设备,把 LB 的功能以 SDK 的模式集成到服务消费方进程里
引入了注册中心(Servcie Registry),用来动态管理所有的服务地址
注册中心不在调用的主链路上,它在旁路
+------------------+
| Service Registry |
+------------------+
/ ^
/ \
Discover Register & Keep Alive
/ \
/ \
v \
+----------+ +----------+
| Consumer | ---- Load Balance & Invoke --> | Provider |
+----------+ +----------+
优点
Consumer 直接调用 Provider,不再有中间节点
不需独立的负载均衡设备,也就不存在成本和运维的问题
缺点
对 Consumer 端有侵入性,存在接入成本
去中心化,所以弱管控
虽然注册中心在旁路,但也是一个关键的基础设施,需要确保高可用
业界常见的服务发现解决方案
硬负载
阿里云的 SLB
AWS 的 ELB
软负载
Eureka
zookeeper/etcd/consul
阿里和蚂蚁的 ConfigServer
这些方案都各有场景,但在 RPC 里我们通常采用软负载来做服务发现
Node.js 如何做服务发现?
接口抽象
这里主要讨论 Node.js 接入软负载的一些经验和套路。在典型的软负载模式下包含三个角色:
服务提供者(Service Provider)
服务消费者(Service Consumer)
服务注册中心(Service Registry)
Node.js 主要承担前两种角色,所以我们要做的是开发服务注册中心的客户端 SDK。虽然注册中心有多种实现,但我们可以将其接口抽象为:
服务注册
服务注销
服务订阅
服务去订阅
健康检查(可选)
服务治理相关查询(可选)
由此我们可以创建一个 RegistryBase 基类,它的 API 定义如下:
interface RegistryBase {
async register(config: any): void;
async unRegister(config: any): void;
subscribe(config: any, listener: function): void;
unSubscribe(config: any, listener: function): void;
async close(): void;
}
针对不同的服务端,会有其对应的实现,比如:ZookeeperRegistry、EurekaRegistry 等等。
实际例子可以参考:ZookeeperRegistry 的实现
https://github.com/alipay/sofa-rpc-node/blob/master/lib/registry/zk/data_client.js
服务发现自己的服务发现
调用注册中心接口本身也需要有一个服务发现的过程,这里感觉有点鸡蛋问题。一般来说这个服务发现我们需要依赖一个更加基础的地址服务(比如:DNS),然后通过轮训或其他策略来更新注册中心的地址列表,最后从中选择一台发起请求,完整的时序图如下:
+--------+ +-----------+ +--------------+
| Client | | DNS | | Registry |
+--------+ +-----------+ +--------------+
| | |
| -- 1. 查询注册中心地址 --> | |
| <--- 返回注册中心地址 ---- | |
| | |
| |
| ---------------- 2. 注册消费者 / 发布者 ----------------> |
| <-------------------- 注册结果反馈 --------------------- |
| |
| |
| ------------------ 3. 订阅服务发布者 ------------------> |
| <-------------------- 订阅结果反馈 --------------------- |
| |
| |
| <----------------- 4. 推送服务地址 --------------------- |
| ----------------------- 反馈收到 ---------------------- |
| |
关于健康检查
服务注册中心不同于一般的动态配置系统,因为服务是有状态的(至少包含可用和不可用两种状态)。在服务发布成功以后,还需要持续通过健康检查来确保服务是可用的。
健康检查的方式一般分两种:
1、通过心跳
服务提供方和注册中心通过定时发送心跳包来维护一个长连接,只要长连接不断,就代表服务可用。
优点
对业务透明,实现也比较简单
可以确保至少网络连接是通的
缺点
粒度较粗,无法检查实际业务是否健康
对于注册中心来说需要维护大量长连接
Zookeeper, 阿里的 ConfigServer 都采用这种方式来做健康检查
2、暴露接口用于定时检查
服务提供方单独暴露一个接口给注册中心来轮训,根据接口的返回状态来判断服务是否可用
优点
业务可以自定健康标准,做更精确的健康检查
不用维护长连接
缺点
对业务有一定侵入
K8s 里的 Health Checks 就是这种方式