07-Dubbo真实生产环境思考
在互联网环境下,理想的注册中心
摘自于阿里专家(2019年文章):曹胜利
CAP 理论:现在大部分主流而且在使用中的注册中心都是满足 CP 的,但是在互联网大集群环境下,期望的结果是满足 AP 的同时,能够满足最终一致性。在大集群环境下,可用性往往比强一致性的优先级更高。以 Zookeeper 为例,Zookeeper 能够为分布式系统提供协调功能的服务,默认提供强一致性的数据服务,但是它在某些情况下是允许 Zookeeper 是不可用的。列举一个场景,Zookeeper Leader 失效了,这时需要重新选举 Leader,而这个选举过程需要 30 秒以上(数据来自于网上的文章),这段时间内 Zookeeper 对外是不可用的。
去中心化:Zookeeper 是有 Leader 机制,往 Zookeeper 里写数据都是往 Leader 里面写,这个 Leader 其实就是一个单点。所以整个写的过程是中心化的。而且 Zookeeper 对跨城跨机房的方案上,支持非常有限。
数据推送的强控制:期望对推送的有更加强的灵活性。还是以 Zookeeper 为例,Zookeeper 中有 watch 机制,每个数据节点发生变更的时候,就会往外推送变更的通知。但是作为注册中心,我们期望能够控制它的推送频率,针对新增节点只需要一分钟里面推送 6 次就可以了,每十秒推送一次,这样可以合并一些变更通知,减少网络数据请求的数据量。
容量:Dubbo 是单进程多服务的方式来注册服务的。这也就意味着注册中心中需要存储的数据量较大,所以要有足够的容量来支撑这种场景。
那些注册中心产品:Zookeeper 作为服务注册中心的公司在减少,那么现在有哪些方案,可以来替代呢?
Eureka 是一个 AP 的应用,而且它是去中心化的。但是它有几点不足:
- 在我们的内部的性能测试中,它性能表现非常一般,性能大概只有 Zookeeper 的 60%左右。
- Eureka 内有一种契约机制,它每隔 30 秒会发起一个续约的请求,如果 3 次没有接收到,它才会过期失效;如果一个服务非正常退出(没有发起解约请求),那么就存在这个超时的间隙期,服务是不可用的。所以在生产环境,对服务敏感的相关应用方是无法满足需求的。
- Eureka 是应用维度的服务注册,当前的 dubbo 是服务维度的注册,如果要匹配的话,需要大范围改造。
- Netflix 宣布了停止更新 Eureka 2.0。
Etcd 是 Zookeeper 的升级版,它参考了 Zookeeper 的很多实现,同时进行了较多优化。Etcd 的强一致性协议和代码实现更加简单,它的部署方式也更加简单,它支持了 Rest 的方式进行相关访问,它的性能相对 Zookeeper 来说也有了一定的提升。但是它还是一个 CP 的系统,它也是要求数据的强一致性,而牺牲部分的可用性。
Consul 相对前面几个产品来说,更加专注服务注册发现本身,它是一个比较专业的服务注册中心。Consul 有了后台管理页面,它有了健康检查,Consul 原生支持多数据中心。但它的性能上有瓶颈的,它和 Zookeeper 和 ETCD 进行对比,它性能是稍微差一点的;同时 Consul 也要求数据的强一致性而牺牲部分可用性。
Nacos 是阿里巴巴开源的一个产品,内部系统也在使用,它已经经受了一定流量和用户的考验。现在阿里巴巴集团内部的 Provider 和 Consumer 数量已经到达了亿的级别,它现在能够支撑上亿级别的订阅量,整体经受了一定的实践检验。Nacos 整体设计是去中心化的,而且设计上满足 AP 和最终一致性,性能上和 Zookeeper 比较接近。
前段时间和网易考拉在沟通过程中也发现,他们也在做一个自己的注册中心;新浪也有一个自己的服务注册中心。所以许多大的互联网公司,因为定制或者差异化的需求,都在自研注册中心。
Dubbo 生产环境问题排查案例
相关生产案例都搜集于网络,并非原创,在此声明!
一次关于 Dubbo 服务 IP 注册错误的踩坑经历
简介: 这不最近又遇到个问题,Dubbo 服务 IP 注册错误,好了,下面进入正题。
踩坑
阿粉公司最近新建一个机房,需要将现有系统同步部署到新机房,部署完成之后,两地机房同时对提供服务。系统架构如下图:
98d2abcc03bb445e85f1d6408eac6503
这个系统当前对外采用 Restful 接口,内部远程采用 Dubbo,服务注册中心使用 zookeeper。服务当前设定只会调用本机房内服务。
原先服务都在 A 机房,B 机房为新建机房。B 机房部署完成之后,需要测试 B 机房系统可用性。生产测试的发现 B 机房竟然调用 A 机房服务。
A/B 机房网络互相打通,可以互相访问
通过排查 B 机房服务日志,发现 Service B 一个服务节点注册 IP 解析错误,将 B 机房机器 IP 解析成 A 机房机器 IP。
于是当测试流量进入 B 机房时,openapi服务通过注册中心获取到错误的 Service B 服务地址,从而调用了 A 机房的服务。调用方式简化成如下图。
c68ecb3d56a1446da2876b1f9f226fde
知识点:Dubbo 服务提供者启动时将会将服务地址(IP+端口)注册到注册中心,消费者启动时将会通过注册中心获取服务提供者地址(IP+端口),后续服务调用将会直接通过服务地址直接调用。
问题分析
Debug Dubbo 源码,定位到 IP 解析代码,位于 ServiceConfig#findConfigedHosts,源码如下:
Dubbo 版本为 2.6.7
0edf3ed1380d4de2a862b64313bf4a1c
这个方法源码比较长,看起来比较费劲,不过好在这个方法注释上已经写明白 IP 地址查找顺序。
Register & bind IP address for service provider, can be configured separately. Configuration priority: environment variables -> java system properties -> host property in config file -> /etc/hosts -> default network address -> first available network address
解析过程,Dubbo 将会过滤无用 IP,过滤规则如下:
97ab50d4873f4cb288122fb3201fd179
下面将结合图示讲解查找顺序,只要其中一步读取 IP 符合上述规则,方法就会返回。
第一步将会调用 ServiceConfig#getValueFromConfig 从 environment variables 或 java system properties 配置 IP 地址。
71.jpg
这种方式通过在 JVM 启动参数中显示指定 IP 。
-DDUBBO_IP_TO_BIND=1.2.3.4
第二步通过读取 Dubbo 配置文件配置变量获取 IP。
<!-- protocol 指定整个 Dubbo 应用服务默认 IP -->
<dubbo:protocol host="1.2.3.4"/>
<!-- provider 指定 Dubbo 应用具体某个服务默认 IP -->
<dubbo:provider host="1.2.3.4"/>
第三步通过调用 InetAddress.getLocalHost().getHostAddress() 获取本地 IP。该方法将会获取机器 hostname,然后再在 /etc/hosts 配置文件中查找 hostname 对应的配置 IP。
84380e640efa47058e2d652a4ad39604
第四步通过 socket 连接注册中心从而获取本机 IP。
如果上述几步都不成功,Dubbo 将会轮询本机所有网卡,直到找到合适的 IP 地址。
b79f2b277cee41bea25c15bf4ff6e590
问题原因
通过排查上述几个规则,最后发现本地 /etc/hosts 文件 IP 配置错误, hostname 配置成了 A 机房的 IP 。
总结
这次的问题其实不大,就是 hosts 文件配置错误,但是整个查找问题的过程还是值得学习的,深入到了源码层面,跟踪代码,最终发现问题。毕竟运维人员和开发人员在一定的程度上还是会出现沟通问题,而且还是生产环境,所以更加需要仔细。
我们可以看到 Dubbo 在 IP 解析上花费很大功夫,最大程度上帮我们自动获取正确 IP。但是现实还是很残酷,真实环境下机器可能存在多网卡,内外网 IP,VPN ,或者应用采用 Docker 部署,这些情况下Dubbo 有可能就会获取到错误 IP,从而导致消费者调用失败。如果真遇到这种情况,读者首先通过上面顺序排查 IP 读取来源,若最后确定 IP 读取自网卡 。这种情况下就只能根据下面几种方式显示指定 IP。
配置方式一:在 JVM 启动参数中加入如下配置
-DDUBBO_IP_TO_BIND=1.2.3.4
配置方式二:在 /etc/hosts 设置 hostname 对应的 IP。
配置方式三:Dubbo 配置文件显示指定 IP。
<!-- protocol 指定整个 Dubbo 应用服务默认 IP -->
<dubbo:protocol host="1.2.3.4"/>
<!-- provider 指定 Dubbo 应用具体某个服务默认 IP -->
<dubbo:provider host="1.2.3.4"/>
阿里专家讲 Dubbo 真实生产案例:Dubbo3 中应用级注册的前身
文章来源于阿里专家(2019年文章):曹胜利
特说明:该文章完成时 Dubbo3 还没有完成!
背景知识介绍
img
注册中心注册介绍
在 RPC 整个链路中,需要的元素有 Provider、Consumer,以及注册中心(中间 Zookeeper 是作为注册中心来使用的)。整个注册过程如下:
- Provider 会把一长串 URL(dubbo://xxx 的字符串)写入到 Zookeeper 里面某个节点里面去。
- Consumer 的注册也是类似,会写到 Zookeeper 里面某个节点(Consumer 写入的原因,是因为 OPS 服务治理的时候需要实时的消费者数据)。
- Consumer 发起一个订阅,订阅相关的服务。
- 当某个服务的 Provider 列表有变化的时候,Zookeeper 会将对应的变化通知到订阅过这个服务的 Consumer 列表。
从图中我们可以看到 Provider 端的 URL 非常长,特别是当一个服务有大量方法的时候。Provider 端的 URL 会先从 Provider 到 Zookeeper,再往 Consumer 传递,这样导致了单次传输的网络开销比较大。
那么再来看一下集群的情形,图中左边有 N 个 Provider,右边有 M 个 Consumer,那么 Provider 发布的时候,会遇到什么情形呢?Provider 每次发布它会先下线再上线,所以每个 Provider 发布的时候,Provider 会发送两次通知,也就是发送 2N 次;接收数据方有 M 个 Consumer,最后算出在整个网络里面的推送数据的次数是 2N×M。
案例
来看一个真实的案例,在杭州有一家中等规模的电商公司,公司内部有 4000+个服务,以 Zookeeper 作为注册中心,Zookeeper 有 100w 个节点,在发布日的时候,公司内部网络的网卡被打爆了,进而导致服务变更的推送失败,新的服务注册也失败。整个集群基本上处于不可用状态。同样的也收到了一些中小公司的反馈,每次在发布的时候,网络也会有个抖动。
分析一下为什么会出现这种情形。
Zookeeper 的 100 万节点中,大约有 10 万个 Provider 节点和 50 万个 Consumer 节点。按照前面的算法,在所有 Provider 同时发布的极端情况下,有 2×10 万×50 万次推送,也就是说会产生 1000 亿条的数据推送。针对每次推送的数据进行了一个统计,每条 URL 大小大概有 1KB,那么计算出来的极端的推送数据量是 1KB 再乘以 1000 亿,已经是 100TB 的级别了。
上面说的是极端情形,就算是发布日也不可能同时进行发布:有的应用发布日不发版本,不同应用不可能同时发布,同一个应用也需要分批发布。假设同一时刻发布的量在千分之一,那么推送的数据量也在 100GB,所以出现发布日的时候间断性地网卡爆掉的现象就不足为奇了。每次发布的时候,都会想着要跟别的应用发布时间错开,争取单独发布,作为程序员还要纠结这个事情真是一个悲剧。
案例分析
来分析下现在的问题和需求:
首先,根据上述案例中的数据分析得知,性能出现了问题。推送的数据量非常大,存储的数据量大,网络传输量大,服务推送延迟,网卡堵塞,服务注册不可用。
接着对 Provider 端那个很长的 URL 进行分析之后发现,不需要把整个 URL 写到注册中心里,只需要把 IP 的端口写进去就可以了,因为只有 IP 的端口需要实时变化。把其他信息放到一个类似的 KEY-VALUE 结构的持久化存储里去,而且这个 KEY-VALUE 结构只要是应用级别就行了,节省了大量的存储空间。
社区中对服务测试的需求非常强烈。要支持服务测试需求,就需要知道调用的服务方法名,入参出参的详细信息。所以这部分信息也是需要存储下来的。但是这部分信息非常大,每个服务中可能有 10 多个方法,每个方法可能有三四个方法入参,入参和出参的完整数据结构往往非常复杂。这部分数据信息也叫做服务的元数据信息。
首先来看一下怎么解决性能的问题。主要有两种方式可以解决:
- 怎么减少当次的注册量,就像前面分析的,只存储 IP 的端口到注册中心;
- 是否可以减少推送的次数,现在推送次数太大了。
减少单次推送量
img
查看上图可知,Provider 端 URL 还是很长,期望简化往注册中心注册的信息;同时服务测试需求,又同时期望能将更丰富的元数据信息进行持久化的存储。
Provider 端写入的改造。Provider 往注册中心写的时候,将整个数据的写入分成两部分:
- 写入注册中心;
- 写入元数据中心。
注册中心作为服务的注册和发现,更加关注数据的实时性和有效性(watch 机制),整个 URL 中 IP 和端口就能判断某个服务是否可用,其他信息都是相对固定不变的。所以注册中心中,只需要存储 IP 和端口。元数据中心中存储 URL 中除 IP 和端口外的其他信息,加上服务测试需要的服务方法名,服务方法的出入参信息。元数据是一个 KEY-VALUES 的持久化存储,是独立于注册中心的存储,它不需要有 watch 的机制,而只需要提供持久化存储。图中使用的的 KEY VALUE 存储是 Redis,但是元数据中心定义了一套 SPI,开发者可以去扩展,可以自己实现 DB 存储,或者其他持久化存储的方式。
Consumer 端获取 Provider 列表信息的改造。Dubbo 之前的版本中,直接从注册中心里面获取 Provider 端的服务信息,获取到的信息已经是一个完整的可调用的服务信息。但是 Provider 端写入改造之后,原有 Consumer 端获取的 Provider 服务信息的方式不可用了。除了从注册中心获取到的数据之外,还需要从元数据中心里拿到元数据信息,然后对这两部分数据做一个 Merge 之后才能构建出完整的可调用的服务信息。
当前 Dubbo2.7 版本还没有完全去除所有参数,而是采用先去除部分参数的方式来验证;后续会逐渐迭代完善,同时在 2.6.x 版本中也会进行一些兼容方案的支持。
应用级服务注册
上面的改造针对的是怎么减少单次的推送数据量,针对的还是服务维度。期望中最理想地给注册中心减负的方式是应用维度的服务注册和发现,可以参考 Spring Cloud 体系下的 Eureka 实现。一旦实现这种方案,服务注册中心就再也不会成为 RPC 领域的瓶颈,而且可以认为这种方案是服务注册的终极方案。
当然这种实现方式做的改动相对比较大,不仅需要将服务执行和运维完全分开,而且需要一定的架构体系改造来支撑具体服务的发现。到目前为止还没有形成成熟可靠的方案,团队内部也只是在探讨阶段。
服务变更推送开关
所谓服务变更推送开关,就是针对任何的服务信息的变更,不进行推送。
到底哪种情形需要这种开关呢?阿里巴巴整个集群的机器数非常大,所以宿主挂掉或者虚拟机挂掉出现的概率比较高。在每年双十一的时候,大部分消费者都会去淘宝天猫上购物。在 11 月 10 号 11 点 50 几分开始,大量买家在拼命地刷新购物车或者商品详情页面,这时候阿里巴巴内部的系统负载是非常高的,网络负载也非常高。如果这时候,有一台机器因为宿主机挂了的原因而导致部分服务下线,这时候需要推送相关应用服务下线的变更给对应的服务 Consumer。这时候就需要占用网络带宽,可能对服务调用产生影响,进而还会对双十一造成很大的压力。所以这时候就希望有一个开关,能够把整个服务推送关掉。
但是这时候也会带来一些问题,当服务 Provider 不可用的时候,注册中心没有向服务 Consumer 推送变更通知,服务 Consumer 请求的时候可能会报错,这时候的小部分服务出错可以允许的;保证整个集群上万台机器,特别是整个双十一核心链路的稳定性才是双十一最重要的使命。
服务分组
在一个大的集群环境中,在没有路由规则的情况下,Consumer 集群会调用整个 Provider 集群中的任何机器。服务分组,就是对 Consumer 集群和 Provovider 集群进行分组,将大的服务级分成几个子集。
举个例子,集群中有 8 个 Consumer 实例,有 8 个 Provider 实例,按照正常流程 Consumer 这 8 个实例会调用 Provider 任何一台,任何一个 Provider 的变更通知也会通知到这 8 个 Consumer 实例。但是如果对它进行分组呢,Consumer 实例集群分成 A 和 B 两个组,Provider 集群也分成 A 和 B 两个组。Consumer 中 A 的组只能调到 Provider 中 A 组的服务;Provider 的 A 组中的实例在发布过程中,也只会推送到 Consumer 的 A 组中,而不会推动 Consumer 的 B 组。最终通过推送的范围,来减少了推送的数据总量。
获取更多干货内容,记得关注我哦。