网易考拉(以下简称考拉)是网易旗下以跨境业务为主的综合型电商,自 2015 年 1 月 9 日上线公测后,业务保持了高速增长,这背后离不开其技术团队的支撑。微服务化是电商 IT 架构演化的必然趋势,网易考拉的服务架构演进也经历了从单体应用走向微服务化的整个过程,以下整理自网易考拉陶杨在近期 Apache Dubbo Meetup 上的分享,通过该文,您将了解到:
考拉架构的演进过程
考拉在服务化改造方面的实践
考拉在解决注册中心性能瓶颈方面的实践
考拉未来的规划
考拉架构的演进过程
考拉在 2015 年初上线的时候,线上只有七个工程,商品详情页、购物车下单页等都耦合在中间这个 online 的工程里面。
在上线之初的时候,这种架构还是比较有优势的,因为当时考拉的开发人员也不是很多,把所有的功能都耦合在一个进程里面,利于集中开发、测试和上线,是一种比较高效和节省成本的方式。
但是随着业务的不断发展,包括需求的逐步增多,开发团队的不断扩容,这时候,单体架构的一些劣势就逐渐的暴露出来了,例如开发效率低:功能之间的相互耦合,不同需求的不同分支也经常会修改同一块代码,导致合代码的过程非常痛苦,而且经常会出问题。
再例如上线成本高:几乎所有的发布需求都会涉及到这些应用的上线,同时不断增长的业务需求,也会使得我们的代码越来越臃肿,造成维护困难、可用性差,功能之间相互耦合,都耦合在一个进程里面,导致一旦某一个业务需求涉及的代码或者资源出现问题,那么就会影响其他的业务。比如说我们曾经在 online 工程里面,因为优惠券兑换热点的问题,影响了核心的下单服务。
这个架构在考拉运行的 4 到 5 个月的时间里,从开发到测试再到上线,大家都特别痛苦。所以我们就开始进行了服务化拆分的工作。
这个是考拉现在的分布式服务架构。伴随着服务化的拆分,我们的组织架构也进行了很多调整,出现了商品中心、用户中心和订单中心等等。拆分其实是由业务驱动的,通过业务来进行一些横向拆分或者纵向拆分,同时,拆分也会面对一个拆分粒度的问题,比如怎么才算一个服务,或者说服务拆的过细,是不是会导致我们管理成本过高,又或者说是否会带来架构上的新问题。
考拉的拆分由粗到细是一个逐步演进的过程。随着服务化的拆分,使得服务架构越来越复杂,随之而来产生了各种各样的公共技术,比如说服务治理、平台配置中心、分布式事务和分布式定时任务等等。
考拉的服务化实践
微服务框架在服务化中起到了很重要的作用,是服务化改造的基石,经过严格的技术选型流程后,我们选用了 Dubbo 来作为考拉服务改造的一个重要支柱。Dubbo 可以解决服务化过程中服务的定义、服务的注册与发现、服务的调用和路由等问题,此外,Dubbo 也具有一些服务治理的功能和服务监控的功能。下面我将介绍考拉基于 Dubbo 做的一些服务化实践。
首先来说一下 熔断。
在进行服务化拆分之后,应用中原有的本地调用就会变成远程调用,这样就引入了更多的复杂性。比如说服务 A 依赖于服务 B,这个过程中可能会出现网络抖动、网络异常,或者说服务 B 变得不可用或者不好用时,也会影响到 A 的服务性能,甚至可能会使得服务 A 占满整个线程池,导致这个应用上其它的服务也受影响,从而引发更严重的雪崩效应。
因此,服务之间有这样一种依赖关系之后,需要意识到服务的依赖其实是不稳定的。此时,需要通过采取一些服务治理的措施,例如熔断、降级、限流、隔离和超时等,来保障应用不被外部的异常拖垮。Dubbo 提供了降级的特性,比如可以通过 mock 参数来配置一些服务的失败降级或者强制降级,但是 Dubbo 缺少自动熔断的特性,所以我们在 Dubbo 上引入了 Hystrix。
消费者在进行服务调用的时候会经过熔断器,当服务提供者出现异常的时候,比如暂时性的不可用,熔断器就会打开,对消费端进行调用短路,此时,消费端就不会再发起远程调用,而是直接走向降级逻辑。与此同时,消费端会持续的探测服务的可用性,一旦服务恢复,熔断器就会关闭,重新恢复调用。在 Dubbo 的服务治理平台上,可以对 Hystrix 上运行的各种动态参数进行动态的配置,包括是否允许自动熔断,是否要强制熔断,熔断的失败率和时间窗口等等。
下面再说一下 限流。
当用户的请求量,调用超过系统可承受的并发时系统 QPS 会降低、出现不可用甚至存在宕机的风险。这就需要一个机制来保护我们的系统,当预期并发超过系统可承受的范围时,进行快速失败、直接返回,以保护系统。
Dubbo 提供了一些基础的限流特性,例如可以通过信号量的配置来限制我们消费者的调用并发,或者限制提供者的执行并发。但是这些是远远不够的,考拉自研了限流框架 NFC,并基于 Dubbo filter 的形式,实现了对 Dubbo 的支持,同时也支持对 URL 等其他资源的限流。通过配置中心动态获取流控规则,对于资源的请求,比如 Dubbo 调用会经过流控客户端,进行处理并判断是否触发限流,一旦请求超出定义的阈值,就会快速失败。
同时,这些限流的结果会上报到监控平台。上图中的页面就是考拉流控平台的一个监控页面,我们在页面上可以对每一个资源(URL、Dubbo 接口)进行一个阈值的配置,并对限流进行准实时监控,包括流控比率、限流次数和当前的 QPS 等。限流框架除了实现基本的并发限流之外,也基于令牌桶和漏桶算法实现了 QPS 限流,并基于 Redis 实现了集群级别的限流。这些措施保障系统在高流量的情况下不会被打垮。
考拉在监控服务方面的改造
在服务化的过程中,系统变得越来越复杂,服务数量变得越来越多,此时需要引入更多维度的监控功能,帮助快速的去定位并解决系统中的各类问题。监控主要分为这四个方面,日志、Metrics、Trace 和 HealthCheck。
在应用程序、操作系统运行的时候,都会产生各种各样的日志,通过日志平台对这些日志进行采集、分析和展示,并支持查询和操作。Metrics 反映的是系统运行的基本状态,包括瞬时值或者聚合值,例如系统的 CPU 使用率、磁盘使用率,以及服务调用过程中的平均延时等。Trace 是对服务调用链的一个监控,例如调用过程中的耗时分析、瓶颈分析、依赖分析和异常分析等。Healthcheck 可以探测应用是否准备就绪,是否健康,或者是否还存活。
接下来,围绕 Dubbo 来介绍一下考拉在监控方面的改造实践。
第一个是服务监控。
Dubbo 提供了服务监控功能,支持定期上报服务监控数据,通过代码增强的方式,采集 Dubbo 调用数据,存储到时序数据库里面,将 Dubbo 的调用监控功能接入到考拉自己的监控平台。
上图中的页面是对 Dubbo 提供者的服务监控,包括对服务接口、源集群等不同维度的监控,除了全局的调用监控,还包括不同维度的监控,例如监控项里的调用次数。有时候我们更关心慢请求的情况,所以会将响应时间分为多个范围,比如说从 0 到 10 毫秒,或是从 10 到 50 毫秒等,这样就可以看到在各个范围内请求的数量,从而更好地了解服务质量。
同时,也可以通过各种报警规则,对报警进行定义,当服务调用出现异常时,通过邮件、短信和电话的形式通知相关人员。监控平台也会对异常堆栈进行采集,例如说这次服务调用的异常的原因,是超时还是线程满了的,可以在监控平台上直接看到。同时生成一些监控报表,帮助我们更好地了解服务的性能,推进开发去改进。
第二个是 Trace。
我们参考了 Dapper,自研了 Trace 平台,并通过代码增强的方式,实现了对 Dubbo 调用链路的采集。相关调用链参数如 TarceID,SpanID 等是通过 Dubbo 的隐式传参来传递的。Trace 可以了解在服务调用链路中的一个耗时分析和瓶颈分析等。Trace 平台上可以展示一次服务调用,经历了哪些节点,最耗时的那个节点是在哪里,从而可以有针对性的去进行性能优化。Trace 还可以进行依赖分析,这些依赖是否合理,能否通过一些业务手段或者其它手段去减少一些不合理的依赖。
Trace 对异常链路进行监控报警,及时的探测到系统异常并帮助我们快速的定位问题,同时和日志平台做了打通,通过 TraceId 可以很快的获取到关联的异常日志。
第三个是健康检查。
健康检查也是监控中很重要的一个方面,以更优雅的方式上线应用实例。我们和自动部署平台结合,实现应用的健康检查。服务启动的时候可以通过 Readiness 接口判断应用依赖的各种资源,包括数据库、消息队列等等是否已经准备就绪。只有健康检查成功的时候才会触发出注册操作。同时 Agent 也会在程序运行的过程中定时的检查服务的运行状态。
同时,也通过这些接口实现更优雅的停机,仅依赖 shutdownhook,在某些情况下不一定靠谱,比如会有 shutdownhook 执行先后顺序的问题。应用发布的时候,首先调用 offline 接口,将注册服务全部从注册中心反注册,这时不再有新的流量进来,等到一段时间后,再执行停机发布操作,可以实现更加优雅的停机。
考拉在服务测试方面的改造
下面来介绍一下考拉在服务测试方面的实践。服务测试分为接口测试、单链路压测、全链路压测和异常测试四个维度。
接口测试
通过接口测试,可以来验证对外提供的 Dubbo 服务是否正确,因此我们也有接口测试平台,帮助 QA 更好的进行接口测试,包括对接口的编辑(入参、出参),用例的编辑和测试场景的执行等,
单链路压测
单链路的压测,主要面对单个功能的压测,比如要上线一个重要功能或者比较重要的接口之前,必须通过性能测试的指标才可以上线。
全链路压测
考拉作为电商平台,在大促前都会做全链路压测,用以探测系统的性能瓶颈,和对系统容量的预估。例如,探测系统的各类服务的容量是否够,需要扩容多少,以及限流的阈值要定多少合适,都可以通过全链路压测来给出一些合理的值。
异常测试
对服务调用链路中的一些节点进行系统异常和服务异常的注入,也可以获取他们的强度依赖关系。比如一个非常重要的接口,可以从 Trace 获取的调用链路,然后对调用链的依赖的各个服务节点进行异常注入。通过接口的表现,系统就会判断这个接口的强度依赖关系,以改善这些不合理的强依赖关系。
考拉在 API 网关方面的改造
随着考拉服务化的发展,我们自研了 API 网关,API 网关可以作为外部流量的统一接口,提供了包括路由转发、流控和日志监控等一些公共的功能。
考拉的 API 网关是通过泛化调用的方式来调用后台 Dubbo 的服务的。Dubbo 原生的泛化调用的性能比普通 Api 调用要差一些,所以我们也对泛化调用性能做了一些优化,也就是去掉了泛化调用在返回结果时的一次对象转换。最终压测的结果泛化的性能甚至比正常的调用性能还要好些。
考拉在多语言方面的改造
考拉在业务发展的过程中产生了不少多语言的需求,例如,我们的前端团队希望可以用 Node 应用调用 Dubbo 服务。对比了易用性,选用了开源的 jsonrpc 方案,然后在后端的 Dubbo 服务上暴露了双协议,包括 Dubbo 协议和 json rpc 协议。
但在实施的过程中,也遇到了一些小问题,比如说,对于 Dubbo 消费者来说,不管是什么样的协议提供者,都是 invoker。通过一个负载均衡策略,选取一个 invoker 进行调用,这个时候就会导致原来的 Java 客户端选用一个 jsonrpc 协议的提供者。这样如果他们的 API 版本不一致,就有可能导致序列化异常,出现调用失败的情况。所以,我们对 Dubbo 的一些调用逻辑做了改造,例如在 Java 客户端的消费者进行调用的时候,除非显示的配置,否则默认只用 Dubbo 协议去调用。另外,考拉也为社区的 jsonrpc 扩展了隐式传参的功能,因为可以用 Dubbo 隐式传参的功能来传递一些全链路参数。
考拉在解决注册中心性能瓶颈方面的实践
注册中心瓶颈可能是大部分电商企业都会遇到的问题,考拉也不例外。我们现在线上的 Dubbo 服务实例大概有 4000 多个,但是在 ZooKeeper 中注册的节点有一百多万个,包括服务注册的 URL 和消费者订阅的 URL。
Dubbo 应用发布时的惊群效应、重复通知和消费者拉取带来的瞬时流量一下就把 ZooKeeper 集群的网卡打满,ZooKeeper 还有另外一个问题,他的强一致性模型导致 CPU 的利用率不高。
就算扩容,也解决不了 ZooKeeper 写性能的问题,ZooKeeper 写是不可扩展的,并且应用发布时有大量的请求排队,从而使得接口性能急剧下降,表现出来的现象就是应用启动十分缓慢。
因此,在今年年初的时候就我们决定把 ZooKeeper 注册中心给替换掉,对比了现有的一些开源的注册中心,包括 Consul、Eruka、etcd 等,觉得他们并不适合 Dubbo 这种单进程多服务的注册模型,同时容量能否应对未来考拉的发展,也是一个问号。于是,我们决定自研注册中心,目前正在注册中心的迁移过程当中,采用的是双注册中心的迁移方案,即服务会同时注册 ZooKeeper 注册中心,还有新的注册中心,这样对原有的架构不会产生太大的影响。
考拉新的注册中心改造方案和现在社区的差不多,比如说也做了一个注册数据的拆分,往注册中心注册的数据只包含 IP, Port 等关键数据,其它的数据都写到了 Redis 里面,注册中心实现使用了去中心化的一个架构,包括使用最终一致性来换取我们接口性能的一个提升。后面如果接入 Dubbo,会考虑使用 Nacos 而不是 ZooKeeper 作为注册中心。
未来规划
考拉最近也在进行第二机房的建设,通过两个机房独立部署相同的一套系统,以实现同城双活。针对双机房的场景,Dubbo 会做一定的改造,例如同机房优先调用,类似于即将发布的 Dubbo2.7.0 中的路由特性。在 Dubbo 在服务注册的时候,读取系统环境变量的环境标或者机房标,再将这些机房标注册到注册中心,然后消费端会做一个优先级路由,优先进行同机房的服务调用。
容器化也是我们在规划的一个方向。随着服务化进程的演进,服务数也变得越来越多,通过容器化、DevOps 可以提升测试、部署和运维效率。
Service Mesh 在今年非常火,通过 Service Mesh 将服务框架的的能力比如注册发,路由和负载均衡,服务治理等下沉到 Sidecar,使用独立进程的方式来运行。对于业务工程的一个解耦,帮助我们实现一个异构系统,对多语言支持,也可以解决中间件升级推动困难以及各种依赖的冲突,业务方也可以更好的关注于业务开发,这也会是未来探索的一个方向。
以上就是我们团队在服务化进程中的一些实践和思考,谢谢大家。
评论 1 条评论