说到大规模微服务系统,往往是一些7*24时不间断运行的在线系统,这样的系统往往有以下的要求:
第一,高可用。这类的系统往往需要保持一定的SLA的,7*24时不间断运行不代表完全不挂,而是有一定的百分比的。例如我们常说的可用性需达到4个9(99.99%),全年停机总计不能超过1小时,约为53分钟,也即服务停用时间小于53分钟,就说明高可用设计合格。
第二,用户分布在全国。大规模微服务系统所支撑的用户一般在全国各地,因而每个地区的人,都希望能够就近访问,所以一般不会一套系统服务全国,而是每个地区都要有相应的业务单元,使得用户可以就近访问。
第三,并发量大,存在波峰波谷。微服务之所以规模比较大,其实是承载的压力比较大,而且需要根据请求的波峰波谷进行弹性伸缩。
第四,有故障性能诊断和快速恢复的机制。大规模微服务场景下,运维人员很难进行命令式手动运维来控制应用的生命周期,应该采用声明式的运维方法。另外一旦有了性能瓶颈或者故障点,应该有自动发现定位的机制,迅速找到瓶颈点和故障点,及时修复,才能保障SLA。
战略设计
为了满足以上的要求,这个系统绝不是运维组努力一把,或者开发组努力一把,就能解决的,是一个端到端的,各个部门共同完成的一个目标,所以我们常称为战略设计。
第一,研发
一个能支撑高并发,高可用的系统,一定是需要从研发环节就开始下功夫的。
首先,每一个微服务都有实现良好的无状态化处理,幂等服务接口设计。
状态分为分发,处理,存储几个过程,如果对于一个用户的所有的信息都保存在一个进程中,则从分发阶段,就必须将这个用户分发到这个进程,否则无法对这个用户进行处理,然而当一个进程压力很大的时候,根本无法扩容,新启动的进程根本无法处理那些保存在原来进程的用户的数据,不能分担压力。
所以要讲整个架构分成两个部分,无状态部分和有状态部分,而业务逻辑的部分往往作为无状态的部分,而将状态保存在有状态的中间件中,如缓存,数据库,对象存储,大数据平台,消息队列等。
这样无状态的部分可以很容易的横向扩展,在用户分发的时候,可以很容易分发到新的进程进行处理,而状态保存到后端。而后端的中间件是有状态的,这些中间件设计之初,就考虑了扩容的时候,状态的迁移,复制,同步等机制,不用业务层关心。
对于数据的存储,主要包含几类数据:
-
会话数据等,主要保存在内存中。对于保存在内存里的数据,例如Session,可以放在外部统一的缓存中。
-
结构化数据,主要是业务逻辑相关。对于业务相关的数据,则应该保存在统一的数据库中
-
文件图片数据,比较大,往往通过CDN下发。对于文件,照片之类的数据,应该存放在统一的对象存储里面
-
非结构化数据,例如文本,评论等。对于非结构化数据,可以存在在统一的搜索引擎里面,例如ElasticSearch。
但是还有一个遗留的问题,就是已经分发,正在处理,但是尚未存储的数据,肯定会在内存中有一些,在进程重启的时候,数据还是会丢一些的,那这部分数据怎么办呢?
这部分就需要通过重试进行解决,当本次调用过程中失败之后,前序的进程会进行重试,例如Dubbo就有重试机制。既然重试,就需要接口是幂等的,也即同一次交易,调用两次转账1元,不能最终转走2元。
接口分为查询,插入,更新,删除等操作。
对于查询接口来讲,本身就是幂等的,不用做特殊的判断。
对于插入接口来讲,如果每一个数据都有唯一的主键,也能保证插入的唯一性,一旦不唯一,则会报错。
对于更新操作来讲,则比较复杂,分几种情况。
一种情况是同一个接口,前后调用多次的幂等性。另一种情况是同一个接口,并发环境下调用多次的正确性。
为了保持幂等性,往往要有一个幂等表,通过传入幂等参数匹配幂等表中ID的方式,保证每个操作只被执行一次,而且在实行最终一致性的时候,可以通过不断重试,保证最终接口调用的成功。
对于并发条件下,谁先调用,谁后调用,需要通过分布式锁如Redis,Zookeeper等来实现同一个时刻只有一个请求被执行,如何保证多次执行结果仍然一致呢?则往往需要通过状态机,每个状态只流转一次。还有就是乐观锁,也即分布式的CAS操作,将状态的判断、更新整合在一条语句中,可以保证状态流转的原子性。乐观锁并不保证更新一定成功,需要有对应的机制来应对更新失败。
其次,根据服务重要度实现熔断降级、限流保护策略
服务拆分多了,在 应用层面 就遇到以下问题:
服务雪崩:即一个服务挂了,整个调用链路上的所有的服务都会受到影响;
大量请求堆积、故障恢复慢:即一个服务慢,卡住了,整个调用链路出现大量超时,要长时间等待慢的服务恢复到正常状态。
为了解决这些问题,我们在应用层面实施了以下方案:
通过熔断机制,当一个服务挂了,被影响的服务能够及时熔断,使用 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,进行分库分表,到一定的阶段需要对分布式数据库进行扩容。
第五,故障演练和性能压测
再好的规划也比不上演练,再好的性能评估也比不上在线的性能压测。
性能问题往往通过线上性能压测发现的。线上压力测试需要有一个性能测试的平台,做多种形式的压力测试。例如容量测试,通过梯度的加压,看到什么时候实在不行。摸高测试,测试在最大的限度之上还能承受多大的量,有一定的余量会保险一些,心里相对比较有底。再就是稳定性测试,测试峰值的稳定性,看这个峰值能够撑一分钟,两分钟还是30分钟。还有秒杀场景测试,限流降级演练测试等。
只有经过性能压测,才能发现线上系统的瓶颈点,通过不断的修复和扩容瓶颈点,最终才能知道服务之间应该以各种副本数的比例部署,才能承载期望的QPS。
对于可能遇到的故障,可以进行故障演练,故意模拟一些故障,来看系统如何反应,是否会因为自修复,多副本,容错等机制,使得这些故障对于客户端来讲没有影响。
战术设计
下面,我们就从架构的每个层次,进行战术设计。我们先来看一下高可用部署架构选型以及他们的优劣。
架构类型
|
可用性
|
优势
|
问题
|
单体应用
|
-
|
网络开销小
|
扩展性差,维护困难
|
单机房服务化
|
应用级高可用
|
网络开销小,解耦可扩展
|
容量受限,机房级单点
|
同城多活阶段一
|
机房级高可用
|
突破单机房容量瓶颈
|