跨机房微服务高可用方案:DerbySoft路由服务设计与实现
导读:在微服务中,当服务跨多个公有云的可用区时候,我们采用什么样的服务访问策略以及保障其高可用?本文是 DerbySoft 架构师朱攀在高可用架构群的分享,介绍微服务中路由服务的设计。
朱攀,德比软件架构师,2007 年 2 月加入德比软件。主要负责数据对接平台的架构和实现。作为德比软件早期员工,从无到有的主导了德比软件数据对接平台的架构设计和实现,完成了对接平台多个版本的架构改进和升级。期间设计并实现了很多必要的基础设施和服务,如内部的 RPC 框架 derbysoft-rpc,路由服务 Router,分布式存储服务 DStorage,网关服务 DGateway 等,主要编程语言 Golang,Scala,Java。目前主要关注的方向在基础架构、微服务、大数据等。
背景介绍
我来自德比软件 (DerbySoft),今天跟大家介绍我们做的微服务架构中的路由服务。德比软件是一家旅游行业 B2B 公司,为全球酒店集团及其分销渠道提供数据对接服务,构建全球旅游分销网络( GDN)。目前, 公司拥有全球超过 20 万家酒店数据,每天处理 10 亿 + API 调用,每月处理 500 万 + 间夜的订单。服务的客户包括全球重要地区的顶级分销渠道,在线旅行社,垂直搜索引擎,批发商以及众多大型旅游经销商(如: booking.com, Expedia, Google, Ctrip 等)。
为什么需要路由服务?
先来看看我们的数据对接平台的一个总体架构图:
(点击图片可放大浏览)
系统架构不是今天分享的重点,下文主要跟大家交流一下中间服务层中的路由服务( Router)。
微服务化之后,系统面临的一个突出问题是服务虽小,但是数量极多,如果这些服务全部在一个可用区内,服务之间的依赖还比较好管理,可以做服务自动注册和发现来实现依赖管理,但是如果服务分布在很多可用区域,尤其是跨国跨地区的可用区之间的依赖和调用,管理起来就比较麻烦。
下面是我画的一个服务之间的示范依赖图,实际情况的依赖可能会比图示更复杂:
如果这些服务部署分布在不同国家或地区的可用区里,它们之间的依赖调用就更难管理了,另外,跨可用区服务之间的调用还需要考虑安全认证问题。我们内部的通讯协议是 TCP,自研的 RPC 框架 derbysoft-RPC( 多语言实现 ),序列化采用 Protocol Buffers,各个服务采用的主要编程语言有 Golang, Scala, Java 等,每个服务开不同的特殊端口,所有的服务都部署在 AWS 的 EC2 上,如果服务跨区域调用,访问权限配置也是一件麻烦的事情。
为了解决这种情况下服务之间的依赖调用问题、 API 安全问题和服务的高可用问题,我们设计开发了路由服务,解耦服务的调用者和提供者,简化网络拓扑图,简化服务器的安全配置。
下面是引入路由服务之后的服务依赖图:
(点击图片可放大浏览)
引入路由之后,所有的服务只和路由通讯,服务之间不再相互依赖,每个服务只需要依赖并维护自己所在的可用区的路由节点,路由会找到调用的服务目的地,绝大部分情况下,它在本区域内就能找到相应的服务,不需要跨区,这取决于服务的部署情况,关键的服务为了提高响应速度,每个可用区都需要部署,这种情况除非本可用区的服务挂了,否则不会出现跨区访问;有些服务不是那么重要,可能只会在某个或某几个可用区部署,这时候就也可能会出现服务跨可用区调用。
同时也解决了 API 访问安全问题,具体做法是:每个可用区建立一个 VPC,所有的服务都在 VPC 内, VPC 内的 API 调用可忽略安全验证,跨 VPC 路由节点之间用安全组来限制 IP 白名单访问,只允许路由节点可以跨可用区访问其他 VPC 内的路由节点,服务访问路由或者路由访问服务都必须在同一个 VPC 用内网地址访问。
路由服务的实现
路由服务需要实现的主要功能描述:
- 当前路由表中缓存所有可用区的路由地址;
- 当前路由表中缓存下一站路由的地址,在当前路由中如果没找到所需服务,路由会转发请求到下一站路由;
- 当前路由需要检查请求在所有路由中的转发次数,若到达上限,则不再转发;
- 若在当前路由中找到所需服务,优先使用当前可用区的服务,如果当前可用区的服务不可用,调用路由表中配置的其他可用区的服务;
- 若在当前可用区内找到多个可用服务,需要相对公平的将请求分配到各个节点,也即路由服务调用其他服务要实现负载均衡;
- 路由需要在一段时间内保持到每个调用过的服务的连接,以便下次复用。
请求的消息头 (header) 包含以下主要信息:
uri, source, destination, timeout, routers
- uri:服务类型
- source:服务需求方,一般是调用者角色 ID( 服务分组 ID)
- destination:服务目的地,一般是服务者角色 ID( 服务分组 ID)
- timeout:超时时间
- routers:最大路由次数
简单介绍一下里面主要有几个重要字段,服务类型 uri 是各个服务提供者自定义的,服务需求方 source 和服务的提供方 destination,是为了提供一个服务分组的概念,有些服务可能功能相似,它可以在一组,这样可以简化我们的路由表配置。服务响应时间超过 timeout 的值会直接返回超时错误,最大路由次数 routers 是为了防止路由无限次的转发请求找目标服务。
下面是一个路由表的路由规则设计:
router.region1=host:port,host:port //可用区 1 的路由节点地址
router.region2=host:port,host:port //可用区 2 的路由节点地址
router.next=router.region1;router.region2 //下一站路由
source.destination.uri = host1:port1,host2:port2;host3:port3,host4:port4@router.region2
destination.uri = host5:port5,host6:port6@router.region3
uri = host7:port7,host8:port8
先说说“=”前面的 key, key 可以是请求 header 里的 source + destination + uri 的组合,也可以是 destination + uri 的组合,也可以是 uri,组合出来的 key 越长,路由查找时优先级越高, key 还可能是某个可用区的路由节点,每个路由表里面会存储所有可用区的路由节点,“ router.*”表示的是不同可用区的路由节点,“ router.next”表达的是下一站路由。“=”后面是服务地址,最简单的表达可以是 IP 地址+端口号,“ @”后面表示的是这个 IP 属于哪个可用区,如果没有“ @”,表示这个 IP 跟当前路由在同一个可用区;还可以有更复杂一点的表达,有些服务可能好几个可用区都有,这时用“ ;”分隔不同可用区的服务地址,写在前面的,优先级更高,最前面一般配置的是当前路由的可用区或离当前路由比较近的可用区,若当前面可用区的服务不可用,则会将请求转发到后面配置的服务地址的路由。路由服务接收到一个请求后,会用 header 里的信息生成相应的 keys,再根据 key 的优先级查找目的地服务地址,如果找到,则根据规则转发请求,若没找到,则按规则转发请求到“ router.next”配置的路由节点。
路由规则举例:
[blockquote][p]router.tokyo=54.23.20.245:9999,54.23.20.246:9999
router.oregon=44.20.22.1:9999,44.20.22.2:9999
router.next= router.tokyo;router.oregon[url=mailto:br/>ctrip.hilton.hotel_reservaion_book=10.0.0.3:6001,10.0.0.4:6002;10.1.0.3:6001,10.1.0.4:6002@router.oregonctrip.hilton.hotel_reservaion_book=10.0.0.3:6001,10.0.0.4:6002;10.1.0.3:6001,10.1.0.4:6002@router.oregon