当前位置: 首页 > 科技观察

如何轻松设计一个亿级规模的高可用微服务系统?

时间:2023-03-23 10:09:29 科技观察

当涉及到大规模微服务系统时,它们通常是24/7全天候运行的在线系统。那么如何设计一个大规模的微服务系统呢?图片来自Pexels这样的系统往往有如下需求:高可用性。这类系统往往需要维护一定的SLA。7*24小时不间断运行并不是说完全不停机,而是有一定的比例。比如我们常说的可用性需要达到4个9(99.99%),全年总停机时间不能超过1小时,也就是53分钟左右。也就是说,如果服务宕机时间小于53分钟,就说明高可用设计是合格的。用户分布在全国各地。大型微服务系统支撑的用户一般分布在全国各地,因此每个地区的人都希望能够就近访问。因此,一般情况下,一个系统不会服务于全国,每个地区都必须有相应的业务单元。方便用户就近访问。并发量大,有峰有谷。之所以微服务的规模比较大,是因为承受的压力比较大,需要根据请求的波峰波谷进行弹性伸缩。有故障性能诊断和快速恢复机制。在大规模的微服务场景下,运维人员很难进行命令式的手动运维来控制应用的生命周期,因此应该采用声明式的运维方式。此外,一旦出现性能瓶颈或故障点,应有自动发现和定位机制,快速发现瓶颈和故障点并及时修复,以保证SLA。策略设计为了满足以上需求,这个系统不是靠运维团队或者开发团队的努力就能解决的。它是所有部门共同实现的一个端到端的目标,所以我们常称它为战略设计。要开发一个能够支持高并发和高可用的系统,需要从研发阶段就开始下功夫。首先,每个微服务都有很好的无状态处理。幂等服务接口设计状态分为分发、处理、存储。如果一个用户的所有信息都存储在一个进程中,那么从分发阶段,就必须将用户分发到这个进程中,否则无法处理该用户。但是,当一个进程压力很大时,它根本无法扩展,新启动的进程根本无法处理原进程存储的用户数据,无法分担压力。因此,整个架构应该分为两部分,无状态部分和有状态部分,业务逻辑部分往往作为无状态部分,状态保存在有状态的中间件中,比如缓存,数据库,对象存储,大数据平台,消息队列等。这样无状态的部分就可以方便的横向扩展。用户分发时,可以方便的分发到新的进程中处理,状态保存到后台。后端中间件是有状态的。这些中间件在设计之初就考虑了扩展时的状态迁移、复制、同步等机制,业务层无需关心。对于数据存储,主要包括几类数据:session数据等,主要存储在内存中。对于存储在内存中的数据,比如Session,可以放在外部统一缓存中。结构化数据主要与业务逻辑相关。对于业务相关的数据,应该统一存储在数据库中。文件图片数据比较大,往往通过CDN进行分发。对于文件、照片等数据,应该存储在统一的对象存储中。非结构化数据,如文本、评论等。对于非结构化数据,可以有一个统一的搜索引擎,比如ElasticSearch。但是还有一个遗留的问题,就是已经分发和正在处理,但还没有存储的数据,肯定会在内存中。当进程重新启动时,一些数据仍然会丢失。这部分数据呢?这部分需要重试解决。当本次调用失败时,之前的流程会重试。比如Dubbo有重试机制。由于重试,接口需要幂等,即在同一笔交易中,调用两次转账1元不能最终转出2元。接口分为查询、插入、更新、删除等操作:对于查询接口本身,是幂等的,不需要特殊判断。对于插入接口,如果每条数据都有唯一的主键,也可以保证插入的唯一性。如果不唯一,则会报错。对于更新操作,就比较复杂了,有两种情况。一种情况是同一接口前后多次调用的幂等性。另一种情况是同一个接口,并发环境下多次调用的正确性。为了保持幂等性,往往需要有一个幂等表。通过传入幂等参数来匹配幂等表中的ID,保证每个操作只执行一次,在实现最终一致性的时候,可以不断的通过Retry来保证最终的接口调用成功。在并发情况下,谁先调用谁后调用需要使用分布式锁,如Redis、ZooKeeper等,实现同一时间只执行一个请求。如何保证多次执行的结果仍然一致?经常需要通过状态机,每个状态只流一次。还有乐观锁,即分布式CAS操作,将状态判断和更新集成到一条语句中,可以保证状态流的原子性。乐观锁不保证更新一定会成功,需要相应的机制来处理更新失败。其次,根据服务的重要程度,实现熔断降级,限流保护策略服务拆分较多,在应用层面会遇到如下问题:服务雪崩:即如果某个服务挂了up,整个呼叫链路上的所有业务都会受到影响。影响。大量请求堆积,故障恢复慢:即一个服务慢卡死,整个调用链路大量超时,等待慢服务返回需要很长时间一个正常的状态。为了解决这些问题,我们在应用层面实现了以下解决方案:通过熔断机制,当一个服务挂掉时,可以及时断掉受影响的服务,通过Fallback数据来保证进程当非关键服务不可用时,仍然可以进行。.通过线程池和消息队列机制实现异步,让服务快速失败。当一个服务因为太慢而被阻塞时,受影响的服务可以在超时后迅速失败,而不影响整个调用链路。当发现整个系统的负载确实过高时,可以选择降级某些功能或某些调用,以保证最重要的交易流程的通过,将最重要的资源全部用于保证核心流程.另一种方法是限流。当熔断策略和降级策略都设置好后,通过全链路的压测应该可以知道整个系统的支撑能力。因此,需要制定限流策略,保证系统在测试的支持能力范围内提供服务,超过支持能力的服务可以拒绝。下单时,系统弹出对话框说“系统忙,请重试”,这并不是说系统挂了,而是系统正常工作,只是限流策略起到了一定的作用角色。第三,每个服务都要设计一个有效的检测接口,让健康检查能够感知服务状态。我们在部署一个服务的时候,对于运维部门来说,我们可以监控机器的状态或者容器的状态。还可以监控进程是否启动,端口是否监听等,但是运维部门无法感知启动的进程是否可以正常服务。每个服务需要开发的时候,都应该设计一个有效的检测接口,让运维监控系统调用这个接口来判断进程是否正常。提供服务。该接口不应直接返回,而应检查提供服务的线程在进程内部是否处于正常状态,然后返回相应的状态码。只有这样,开发的服务和运维才能相互配合,使服务保持在一定的副本数。否则,如果某些服务启动了,但处于假死状态,其他正常的服务将无法承受压力。四、通过制定良好的代码检查规范和静态扫描工具,最大限度地限制代码问题导致的系统不可用要保持线上代码的高可用性,代码质量是关键,大多数线上问题,不管是性能问题,还是稳定性问题,是由代码引起的,而不是基础设施引起的。而且基础设施的可用率是99.95%,但是服务层要求的可用率要高于这个值,所以必须要靠业务层的高可用来弥补。除了下面的高可用架构部分,对于每一个服务,都需要制定好的代码检查规范和静态扫描工具,通过大量的测试用例,最大限度地限制代码问题导致的系统不可用。基地可用。高可用的架构设计避免了系统各部分的单点。系统冗余往往分为管控平面和数据平面,分为多个层次,每个层次往往都需要进行高可用设计。在机房层面,为了高可用,应该部署在多地域,或者多云,每个地域部署在多个可用区。对于云来说,云的管控需要多个机房的高可用部署,这样即使任何一个机房出现故障,管控仍然可以使用。这就要求受控组件至少分布在两个机房,托管数据库和消息队列跨机房进行数据同步。对于云的数据平面,入口的网关要配合机房网络实现跨机房的高可用,使得入口公网IP和负载均衡器在发生故障时可以切换到另一个机房。一个机房出现故障。Kubernetes平台需要部署在云端。在管控层面,Kubernetes需要高可用部署,etcd需要跨机房高可用部署,Kubernetes的管控组件也需要跨机房部署。当然还有一种情况就是机房之间的距离比较远,每个机房都需要部署一套Kubernetes。在这种情况下,Kubernetes的管控还是需要实现高可用,但是跨机房的高可用需要应用层来实现。在应用层,注册发现、ZooKeeper或Euraka、APM、配置中心等微服务治理平台都需要实现跨机房的高可用。此外,业务需要跨机房部署,实现市级机房的故障迁移能力。运维一个大规模的微服务系统也有不同的挑战。首先,推荐使用Kubernetes编排的声明式运维方式,而不是Ansible等命令式运维方式。另外,对于系统的发布,应该进行灰度和蓝绿发布,降低系统上线的风险。有了这样的概念,任何新推出的系统都是靠不住的。因此,我们可以通过分流的方式逐步切换到新的服务,从而保证系统的稳定性。三是完善监控响应机制,全面监控系统各节点、应用、组件,能够第一时间快速发现问题、解决问题。监控绝不仅限于基础设施CPU、网络、磁盘监控,应用、业务、调用链监控。对于紧急情况,应该有应急计划。应急方案是在考虑到高可用后出现异常情况时应该采用的应急方案,比如三个etcd都宕机的情况。第四,持续关注线上系统网络使用情况、服务器性能、硬件存储、中间件、数据库指标等,重点关注临界状态,即目前健康但可能即将出问题的状态。比如当网关的PPS达到临界值,下一步就会开始丢包,数据库快满了,大量消息堆积等等。DBA对于一个在线业务系统,数据库是重中之重,很多性能瓶颈最终可能是数据库的问题。因此,DBA团队必须检查数据库的使用情况。造成数据库性能问题,一方面是SQL语句的问题,另一方面是容量的问题。比如查询没有被索引覆盖,或者索引建立在几乎没有区别的字段上,是否持有锁时间过长,是否存在锁冲突等,都会造成数据库慢的问题.因此,所有在线的SQL语句都需要DBA提前审核,还要持续监控数据库的性能,比如慢SQL语句。此外,必须持续监控数据库中的数据量。当达到一定数量时,需要改变分布式数据库DDB,分库分表,分库分表。在某个阶段,分布式数据库的容量需要扩展。故障演练和性能压测再好的规划也不如演练,再好的性能评估也不如在线性能压测。性能问题往往是通过在线性能压力测试发现的。在线压测需要一个性能测试平台,可以进行各种形式的压测。比如容量测试,通过梯度加压,看什么时候真的不行。摸高度测试,测试量能承受多少在最大限度之上,会有一定的余量比较安全,相对有信心。然后是稳定性测试,测试峰值的稳定性,看峰值能不能持续一分钟,两分钟,三十分钟。还有秒杀场景测试、限流降级演练测试等,只有通过性能压测才能找到线上系统的瓶颈。通过对瓶颈的不断修复和扩展,我们最终可以知道服务应该以不同比例的副本进行部署,才能承载预期的QPS。对于可能出现的故障,可以进行故障演练,刻意模拟一些故障,看看系统是如何响应的,以及这些故障是否会因为自修复、多副本、容错等机制而对客户端没有影响。战术设计下面,我们将从架构的各个层面进行战术设计。我们先来看看高可用部署架构的选择及其优缺点:高可用需求与系统负载和成本强相关。架构越简单,部署成本越低,高可用性越低,比如上面的单体应用。但微服务化、单元化、异地多活动必然导致架构复杂难维护,机房成本相对较高。因此,使用多少成本来实现高可用性是一个权衡。高可用的实现需要多层次考虑:首先是应用层,可以通过异地多活单元保证城市级的高可用,当一个城市因灾难宕机时,另一个城市可以提供服务。另外,每个多活单元采用双机房,保证机房的高可用,即同城双机房,做到一个城一个机房宕机,另一个机房可以提供服务。另外,每个机房都采用了多副本的方式来保证实例级别的高可用,这样当一个副本宕机的时候,其他副本可以提供服务。第二个是数据库层。数据中心之间,通过主从复制或MGR实现数据异步复制。每个集群单元都使用DDB分库分表,分库分表中的每个实例由数据库同步复制。.第三个是缓存层。数据中心之间,缓存采用多集群单元复制,每个集群单元采用多副本主从复制。它的四个微服务管理着平台层。平台组件的远程多活单元确保了城市级别的高可用性。平台组件各多活单元采用双机房,保证机房级别的高可用。平台组件各机房采用多副本,保证实例级高可用。可用的。有了上面的高可用方案,下面的故障级别和影响时间如下:接下来我们详细讨论每个级别。下面这张应用层图使用了最复杂的场景,假设有三个城市,每个城市有两个完全对等的数据中心。三座城市的数据中心也完全是点对点的。我们将整个业务数据按照一定的维度划分为A、B、C三部分。这样,任何一个部分完全宕机,其他部分仍然可以提供服务。对于一些业务来说,如果省级服务中断完全无法承受,市级服务中断需要相对较短的恢复时间,而区县级服务中断的恢复时间可以相对延长。在这个场景中,可以按照地域来区分维度,使得一个区县和另一个区县的数据属于不同的单元。为了节省成本,模型可能会更加简化。中心节点和单元化节点是不对称的。中心节点可实现同城双活,异地单元化部分只需部署一个机房。这可以满足大多数高可用性要求。这种架构需要实现中间件层和数据库层的单元化,后面会详细讨论。接入层单元化需要App层或机房入口区域的接入层实现中心单元与其他单元节点之间的流量分发。如果初始请求没有路由标记,可以随机分发到任意一个单元,也可以根据地区或运营商分发到GSLB中就近的单元。应用层收到请求后,根据自己的单元生成路由信息,并将路由信息返回给接入层或App。接下来,来自App或者接入层的请求会携带路由信息,选择相应的单元发送,从而实现请求的处理都集中在这个单元。中间件层在中间件层。我们以ZooKeeper为例,分为以下两种场景:场景一:ZooKeeper单元化主从多活在这个场景中,宿主机房和单元化机房距离比较近,延迟很小,可以当电脑房。它可以通过使用ZooKeeper高可用性保证部署多个ZooKeeper实例来实现。如图所示,宿主机房的ZooKeeper有Leader和Follower,单元机房的ZooKeeper只有Observer。场景二:ZooKeeper单元化多集群复制两个机房相距较远,每个机房部署一个ZooKeeper集群,集群之间进行数据同步。每个机房应用都连接到机房内的ZooKeeper集群,注册信息可以通过数据同步被其他机房应用获取。单个机房ZooKeeper集群不可用,其余机房不受影响。目前不考虑不同机房之间的集群切换。数据库层在数据库层。首先要解决的问题是分布式数据库DDB集群在多个机房的同步复制。单元内采用同城主从复制模式,跨单元采用DTS/NDC,实现应用层数据的双向同步能力。对于数据的ID分配,应采用全局唯一ID分配。有两种实现方法。如果主机房和单元化机房距离比较近,ID分配仍然可以集中,机房所有单元向同一个中心服务申请ID。道路。如果主机房和单元化机房相距较远,可以将每个单元单独分配,通过特定的规则保证每个机房最终得到的ID不冲突。缓存层在缓存层,有两种方式:第一种方式是集群热备,新增一个Redis集群作为热备集群。主备集群之间在服务器端进行数据同步,同步过程通过RedisReplication协议进行。离线监控主集群状态,检测到故障时进行主备切换。信息通过配置中心发送给客户端,以类似哨兵的方式进行检测。在这个场景下,集群之间的数据是在服务器端同步的,正常情况下,集群之间的数据会保持一致。但是会有一定的复制延迟。如果发生故障转移,可能会出现很短的数据丢失时间。如果缓存只作为缓存使用,不作为内存数据库使用,是没有问题的。第二种方式,集群更活跃。新添加的集群用作多活集群。一般情况下,客户端根据Key哈希策略选择分发到不同的集群。客户端通过代理连接到集群中的每个节点。Proxy的目的是区分客户端写入和集群复制写入。集群间在服务器端进行双向数据复制,通过RedisReplication协议获取数据变化。离线监控主集群状态,检测到故障时进行切换。信息通过配置中心发送给客户端,以类似哨兵的方式进行监控。当该方案应用于纯集群间高可用时,同一Key只会在同一时间段内被路由到同一集群,可以保证数据的一致性。在故障转移情况下,极端时间可能会丢失数据。微服务治理平台作为面向大规模微服务的微服务治理平台,一方面要实现单元化,另一方面要实现不同单元之间流量的染色和穿梭。从API网关、NSF服务治理与管理中心、APM性能管理、GXTS分布式事务管理、容器平台管控等都需要跨机房部署。当一个请求到达一个单元时,API网关携带单元的路由信息??,NSF服务治理和管理平台在服务之间相互调用时,也插入单元的路由信息??。当一个单元的某个实例完全宕机时,它可以穿梭到另一个单元进行呼叫,并回调到下一跳的单元。这种方法称为流量着色。