Home Tags Posts tagged with "集群"

集群

0 51

如果你问我,实习期间最紧张最害怕的时刻是什么?

那一定是上线发版的时候。

 

年少轻狂三连击,服务全挂我挨批

公司的服务采用集群部署,发版时需要部署多台机器。当我第一次发版前,导师曾再三叮嘱我,项目发慢点,一台一台的发。那时我年少轻狂,觉得呆呆等着很无聊,于是一个三连击,仅有的三台机器同时发布,结果服务调用异常的报错铺天盖地而来

服务器部署服务的流程

服务部署在机器上,那么该机器就成了服务器,所有的用户请求都会发送到服务器上被处理。而当前互联网环境下,服务基本都是分布式/集群部署,也就是有多台服务器。各互联网企业在分布式/集群部署的环境下发布版本时,一定不会像我一样三连击,因为服务器部署服务的流程如下:

旧服务正在运行—>停掉旧服务—>部署新服务—>新服务启动

试想,三台服务器A,B,C工作的好好的,我先点击发布A机器,此时A机器先向注册中心发送消息——”我先走了哈,别把请求路由到我这里了”,然后Dubbo会将A机器从负载均衡策略中排除,如果A机器还有未处理的请求,它会先进行处理,之后服务彻底停止。(详细可参考Dubbo-优雅停机)停止后,A机器会部署新服务,也就是你发布的新版本,但是服务不是瞬间启动的它需要启动服务提供者,服务提供者在启动时还要向注册中心注册自己提供的服务,同时订阅自己需要的服务,等完成这些步骤后,A机器上的新服务才算是正式启动,才可以接收用户的请求(详细可参考Dubbo的简要执行流程)。显而易见,【停掉旧服务—>新服务启动】之间是需要耗费不少时间的,大概几分钟。

三连击为什么会导致服务挂掉呢?

原因就是 【停掉旧服务—>新服务启动】的这几分钟。A机器的服务还在启动中,此时A服务器不可用,而我又点击发布B机器,B机器的服务也进入了【停掉旧服务—>新服务启动】这个阶段,B服务器不可用,C机器同理。三连击导致A,B,C三台机器全部不可用,用户请求一过来就会报服务调用异常的错误

正确的流程应该是发布一台,观察一台的日志。A机器发布后,观察A机器的日志,确保A机器上的服务已经启动且有流量进入,这样的话不仅可以保证A机器可用,如果你的新版本出现了问题还可以立刻发现,此时就算需要回滚也只会影响A机器上的请求,B,C机器的服务是不受影响的,当然,最好的情况是任意一台机器都没有事故。

之所以服务器要进行分布式/集群部署,正是为了避免服务不可用。试想,如果淘宝618时用户无法下单,短短的几分钟可能会造成数以亿计的损失。所以一定要牢记,服务不可用是非常严重的事故。因此,涉及到集群环境下的服务部署,我们必须要考虑可用性及可靠性。

一台一台的发,我有一千台机器岂不是要发好几天?

非也,此处的一台一台发,强调的是一个百分比。我们知道,客户端请求会通过Dubbo的负载均衡策略选择路由到哪一台具体的服务器,如果你有A,B,C三台机器提供服务,一台一台的发可以保证任意时刻有2/3的机器可用,只要2/3的机器可以顶住当前的客户端流量,那就没有问题。(当然,如果2/3的机器顶不住,那你必须要加机器,不然的话流量会击垮B,C两台机器,服务彻底不可用)如果你有一千台机器,你完全可以一批10台或者20台的去发,尽管有10台或20台的机器不可用,但仍剩有99%或98%的机器可用,只要剩下的99%的机器可以顶得住当前流量,那就一点问题都没有。

一句话,多少台机器一起发,取决于发布时剩下可用的机器能否顶得住当前客户端流量。顶得住,没有任何问题;大大顶得住,甚至可以加大同时发布的机器数量,比如1000台只需要500台就足够顶住流量的话,直接50台50台的发布;顶不住的话,对不起,你还是老老实实的减少同时发布的机器数量吧~。

Dubbo-优雅停机

背景

对于任何一个线上应用,如何在服务更新部署过程中保证客户端无感知是开发者必须要解决的问题,即从应用停止到重启恢复服务这个阶段不能影响正常的业务请求。理想条件下,在没有请求的时候再进行更新是最安全可靠的,然而互联网应用必须要保证可用性,因此在技术层面上优化应用更新流程来保证服务在更新时无损是必要的。

传统的解决方式是通过将应用更新流程划分为手工摘流量、停应用、更新重启三个步骤,由人工操作实现客户端无对更新感知。这种方式简单而有效,但是限制较多:不仅需要使用借助网关的支持来摘流量,还需要在停应用前人工判断来保证在途请求已经处理完毕。这种需要人工介入的方式运维复杂度较高,只能适用规模较小的应用,无法在大规模系统上使用。

因此,如果在容器/框架级别提供某种自动化机制,来自动进行摘流量并确保处理完以到达的请求,不仅能保证业务不受更新影响,还可以极大地提升更新应用时的运维效率。

这个机制也就是优雅停机,目前Tomcat/Undertow/Dubbo等容器/框架都有提供相关实现。下面给出正式一些的定义:优雅停机是指在停止应用时,执行的一系列保证应用正常关闭的操作。这些操作往往包括等待已有请求执行完成、关闭线程、关闭连接和释放资源等,优雅停机可以避免非正常关闭程序可能造成数据异常或丢失,应用异常等问题。优雅停机本质上是JVM即将关闭前执行的一些额外的处理代码。

适用场景

  • JVM主动关闭(System.exit(int)
  • JVM由于资源问题退出(OOM);
  • 应用程序接受到SIGTERMSIGINT信号。

配置方式

服务的优雅停机

在Dubbo中,优雅停机是默认开启的,停机等待时间为10000毫秒。可以通过配置dubbo.service.shutdown.wait来修改等待时间。

例如将等待时间设置为20秒可通过增加以下配置实现:

dubbo.service.shutdown.wait=20000

容器的优雅停机

当使用org.apache.dubbo.container.Main这种容器方式来使用 Dubbo 时,也可以通过配置dubbo.shutdown.hooktrue来开启优雅停机。

通过QOS优雅上下线

基于ShutdownHook方式的优雅停机无法确保所有关闭流程一定执行完,所以 Dubbo 推出了多段关闭的方式来保证服务完全无损。

多段关闭即将停止应用分为多个步骤,通过运维自动化脚本或手工操作的方式来保证脚本每一阶段都能执行完毕。

在关闭应用前,首先通过 QOS 的offline指令下线所有服务,然后等待一定时间确保已经到达请求全部处理完毕,由于服务已经在注册中心下线,当前应用不会有新的请求。这时再执行真正的关闭(SIGTERM 或 SIGINT)流程,就能保证服务无损。

QOS可通过 telnet 或 HTTP 方式使用,具体方式请见Dubbo-QOS命令使用说明

流程

Provider在接收到停机指令后

  • 从注册中心上注销所有服务;
  • 从配置中心取消监听动态配置;
  • 向所有连接的客户端发送只读事件,停止接收新请求;
  • 等待一段时间以处理已到达的请求,然后关闭请求处理线程池;
  • 断开所有客户端连接。

Consumer在接收到停机指令后

  • 拒绝新到请求,直接返回调用异常;
  • 等待当前已发送请求执行完毕,如果响应超时则强制关闭连接。

当使用容器方式运行 Dubbo 时,在容器准备退出前,可进行一系列的资源释放和清理工。

例如使用 SpringContainer时,Dubbo 的ShutdownHook线程会执行ApplicationContextstopclose方法,保证 Bean的生命周期完整。

实现原理

Dubbo 优雅停机 | Apache Dubbo

 

Dubbo的简要启动流程

1. 服务器启动,运行服务提供者。

2. 服务提供者在启动时,向注册中心(zookeeper)注册自己提供的服务。

3. 服务消费者在启动时,向注册中心订阅自己所需的服务。

4. 注册中心返回服务提供者地址列表给消费者,(若有变更,注册中心将基于长连接推送变更数据给消费者)

5. 服务的消费者,从地址列表中,基于负载均衡,选一台提供者的服务器进行调用,若是失败,在从 地址列表中,选择另一台调用.

6. 期间Dubbo的监控中心,会记录定时消费者和提供者,的调用次数和时间

Dubbo的简要执行流程 – 简书 (jianshu.com)

0 50

在项目中用到了 Redis,为了水平扩展Redis 的处理能力(单机上的Redis能力受限于内存),保证Redis的高可用,所以需要搭建Redis集群

虽然之前已经系统的学习过Redis 的相关知识(极客时间的 Redis 课程),但对Redis的实际使用还有所欠缺,正好通过本次搭建集群整合Redis知识点,图中理论和图片来自极客时间。

 

 

关于Redis的介绍可以参考另一篇博文http://yangbili.co/wp-admin/post.php?post=1575&action=edit

 

使用Redis的什么情况下需要集群?

这个标题看上去有点好笑,Redis集群当然是在需要使用Redis集群的情况下使用;话虽如此说,但其实这句话包含很多东西。

第一,Redis不是只有一种模式的,也就是说Redis可以集群,也可以不集群

第二,你必须要使用Redis集群才去考虑它的集群(这句话用于劝告跟我一样的初学者,初学者有可能学习某项知识点,却不知道为什么要学习,或者说不知道学习了应该如何在实际中应用它)

一、实现Redis的水平扩展

好了,那么到底什么情况下需要使用Redis的集群呢?得从Redis的特点说起:redis是基于内存的,高性能,key-value,Nosql数据库;因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。基于内存却也有一个很大的缺点:数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。

因此,我的理解是,当单个Redis实例(或服务)无法满足系统的读写要求,需要进行水平扩展的时候,采用Redis集群的策略。更形象一点就是,我们需要使用Redis存储1000万的数据,而单个的Redis实例因为内存限制无法存入这么多,所以我们使用Redis集群模式,启用多个RedisServer实例以满足需求。

二、强化Redis的读写能力,实现高可靠,保证某一个Redis服务实例挂掉不会影响系统运行

Redis的高可靠有两层含义,一是数据尽量少丢失,二是服务尽量少中断;AOF 和 RDB 保证了前者,而对于后者,Redis 的做法就是增加副本冗余量,将一份数据同时保存在多个实例上。即使有一个实例出现了故障,需要过一段时间才能恢复,其他实例也可以对外提供服务,不会影响业务使用。

结合实际来说,在我的项目中使用到了Redis进行数据的读写,刚开始我采用的是单个Redis服务实例,这种情况下,如果该Redis服务实例挂掉了,整个系统就会因此出现问题,也就是整个系统都不可用了。这样的情况在实际开发中是绝不允许的,在设计系统架构的时候就要充分考虑到尽力的保证系统的高可用性,也就是系统运行不中断。为了达到这个目的,我开始考虑使用Redis集群的策略。

Redis集群中有两种类型的节点:主节点(Master)、从节点(Slave)。

可以把集群理解为策略,而集群方案则是这个策略的具体实现。

 

Redis的集群方案(主从复制模式,哨兵模式,Cluster模式)

Redis支持三种集群方案

  • 主从复制模式
  • Sentinel(哨兵)模式
  • Cluster模式

一、主从复制模式

1.主从复制模式介绍

我们已知Redis通过增加副本冗余量将一份数据同时保存在多个实例上,这样即使一个实例出故障,其他实例也可以对外提供服务,不会影响业务使用。

多实例保存同一份数据,听起来很不错,但是我们必须要考虑一个问题就是:这么多副本,它们之间的数据如何保持一致呢?数据读写操作可以发给所有的实例呢?

实际上,Redis提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式。

读操作:主库、从库都可以接收;

写操作:首先到主库执行,然后,主库将写操作同步给从库。

2.那么,为什么要采用读写分离的方式呢?

你可以设想一下,如果在上图中,不管是主库还是从库,都能接收客户端的写操作,那么,一个直接的问题就是:如果客户端对同一个数据(例如 k1)前后修改了三次,每一次的修改请求都发送到不同的实例上,在不同的实例上执行,那么,这个数据在这三个实例上的副本就不一致了(分别是 v1、v2 和 v3)。在读取这个数据的时候,就可能读取到旧的值。

如果我们非要保持这个数据在三个实例上一致,就要涉及到加锁、实例间协商是否完成修改等一系列操作,但这会带来巨额的开销,当然是不太能接受的。

而主从库模式一旦采用了读写分离,所有数据的修改只会在主库上进行,不用协调三个实例。主库有了最新的数据后,会同步给从库,这样,主从库的数据就是一致的。

 

3.主丛库间如何进行同步?(详细可看 https://time.geekbang.org/column/article/272852

具体工作机制为:

  • slave启动后,向master发送SYNC命令,master接收到SYNC命令后通过bgsave保存快照(RDB持久化),并使用缓冲区记录保存快照这段时间内执行的写命令
  • master将保存的快照文件发送给slave,并继续记录执行的写命令
  • slave接收到快照文件后,加载快照文件,载入数据(此三步为全量复制)
  • master快照发送完后开始向slave发送缓冲区的写命令,slave接收命令并执行,完成复制初始化
  • 此后master每次执行一个写命令都会同步发送给slave,保持master与slave之间数据的一致(后两步为增量复制)

总结来说,从库和主库建立连接,第一次复制采用全量复制,主库生成RDB文件(BGsave),同时记录在生成RDB期间执行的写命令,然后发送给从库,从库收到RDB文件后,首先清空数据库,在本地加载快照文件完成数据加载,因为RDB文件中不存在生成RDB期间执行的写命令,所以主库在发送完RDB文件后还需要发送这些写命令,从库接收这些写命令并重新执行这些操作,如此主从库实现同步。

三个阶段:建立连接,同步RDB文件,同步写命令

部署示例:https://www.zhihu.com/search?

type=content&q=Redis%E9%9B%86%E7%BE%A4

4.主从级联模式分担全量复制时的主库压力(主-从-从)

通过分析主从库间第一次数据同步的过程,你可以看到,一次全量复制中,对于主库来说,需要完成两个耗时的操作:生成 RDB 文件和传输 RDB 文件。

如果从库数量很多,而且都要和主库进行全量复制的话,就会导致主库忙于 fork 子进程生成 RDB 文件,进行数据全量同步。(fork完后的子线程不会阻塞,但是fork出子线程的这个动作会阻塞)

fork 这个操作会阻塞主线程处理正常请求,从而导致主库响应应用程序的请求速度变慢。此外,传输 RDB 文件也会占用主库的网络带宽,同样会给主库的资源使用带来压力。那么,有没有好的解决方法可以分担主库压力呢?

其实是有的,这就是“主 – 从 – 从”模式

在刚才介绍的主从库模式中,所有的从库都是和主库连接,所有的全量复制也都是和主库进行的。现在,我们可以通过“主 – 从 – 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。

简单来说,我们在部署主从集群的时候,可以手动选择一个从库(比如选择内存资源配置较高的从库),用于级联其他的从库。然后,我们可以再选择一些从库(例如三分之一的从库),在这些从库上执行如下命令,让它们和刚才所选的从库,建立起主从关系。

replicaof 所选从库的IP 6379

这样一来,这些从库就会知道,在进行同步时,不用再和主库进行交互了,只要和级联的从库进行写操作同步就行了,这就可以减轻主库上的压力,如下图所示:

级联的“主-从-从”模式

好了,到这里,我们了解了主从库间通过全量复制实现数据同步的过程,以及通过“主 – 从 – 从”模式分担主库压力的方式。那么,一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。

听上去好像很简单,但不可忽视的是,这个过程中存在着风险点,最常见的就是网络断连或阻塞。如果网络断连,主从库之间就无法进行命令传播了,从库的数据自然也就没办法和主库保持一致了,客户端就可能从从库读到旧数据。

接下来,我们就来聊聊网络断连后的解决办法。

 

5.主从库间网络断了怎么办?

 

 

6. 主从复制的优缺点

优点:

  • master能自动将数据同步到slave,可以进行读写分离,分担master的读压力
  • master、slave之间的同步是以非阻塞的方式进行的,同步期间,客户端仍然可以提交查询或更新请求

缺点:

  • 不具备自动容错与恢复功能,master或slave的宕机都可能导致客户端请求失败,需要等待机器重启或手动切换客户端IP才能恢复(哨兵可解决)
  • master宕机,如果宕机前数据没有同步完,则切换IP后会存在数据不一致的问题
  • 难以支持在线扩容,Redis的容量受限于单机配置

 

个人总结:

Redis集群是为了水平扩展Redis的服务能力,而主从复制可以说是Redis集群实现的方案,我现在更加觉得Cluster集群和哨兵以及主从库复制不是相互对立的,而是一个整体的不同部分,这些不同的部分实现了Redis的高可靠性,高可用性;当然,你可以只实现其中某一部分。

本篇着重讲解的就是其中一个部分—主从复制,后续会讲到的哨兵模式是基于主从复制模式的。

主从复制,是通过增加副本冗余量来实现Redis的服务尽量少终端,以达到高可靠性;实现了副本冗余,就需要考虑各个副本分别负责什么,为了避免客户端对同一个数据进行修改,而不同的修改请求都发送到不同的实例上,在不同的实例上执行而导致的在不同实例上的副本不一样的情况,Redis的主从复制采用读写分离的分工;

读操作,主从都可以接收

写操作,由主库执行,再将写操作同步给从库

那怎么进行同步呢?

先发送RDB,再发送记录的写操作(第一次全量之后是增量,增量复制的时候采用长连接),同时为了减轻主库生成RDB和传输RDB的压力,可以采用 主-从-从的级联模式

那么如果在发送过程中网络中断了怎么办呢?

repl_backlog_buffer环形缓冲区,主库记录写的位置,从库记录自己读到的位置,相减再同步

 

补充:

 

提问:

主从库间的数据复制同步使用的是 RDB 文件,前面我们学习过,AOF 记录的操作命令更全,相比于 RDB 丢失的数据更少。那么,为什么主从库间的复制不使用 AOF 呢?

回答:

1、RDB文件内容是经过压缩的二进制数据(不同数据类型数据做了针对性优化),文件很小。而AOF文件记录的是每一次写操作的命令,写操作越多文件会变得很大,其中还包括很多对同一个key的多次冗余操作。在主从全量数据同步时,传输RDB文件可以尽量降低对主库机器网络带宽的消耗,从库在加载RDB文件时,一是文件小,读取整个文件的速度会很快,二是因为RDB文件存储的都是二进制数据,从库直接按照RDB协议解析还原数据即可,速度会非常快,而AOF需要依次重放每个写命令,这个过程会经历冗长的处理逻辑,恢复速度相比RDB会慢得多,所以使用RDB进行主从全量同步的成本最低。

2、假设要使用AOF做全量同步,意味着必须打开AOF功能,打开AOF就要选择文件刷盘的策略,选择不当会严重影响Redis性能。而RDB只有在需要定时备份和主从全量同步数据时才会触发生成一次快照。而在很多丢失数据不敏感的业务场景,其实是不需要开启AOF的。

 

参考资料:

https://www.zhihu.com/search?type=content&q=Redis%E9%9B%86%E7%BE%A4

Redis哨兵、复制、集群的设计原理与区别 – 知乎 (zhihu.com)

https://blog.csdn.net/shenjianxz/article/details/59775212

https://time.geekbang.org/column/article/272852

单机结构

我想大家最最最熟悉的就是单机结构,一个系统业务量很小的时候所有的代码放在一个项目中就好了,然后这个项目部署在一台服务器上就好了。整个项目所有的服务都由这台服务器提供。这就是单机结构。

那么,单机结构有啥缺点呢?我想缺点是显而易见的,单机的处理能力毕竟是有限的,当你的业务增长到一定程度的时候,单机的硬件资源无法满足你的业务需求此时便出现了集群模式,往下接着看。

集群结构

集群模式在程序猿界有各种装逼解释,有的让你根本无法理解,其实就是一个很简单的玩意儿,且听我一一道来。

单机处理到达瓶颈的时候,你就把单机复制几份,这样就构成了一个“集群”。集群中每台服务器就叫做这个集群的一个“节点”,所有节点构成了一个集群。每个节点都提供相同的服务,那么这样系统的处理能力就相当于提升了好几倍(有几个节点就相当于提升了这么多倍)。

但问题是用户的请求究竟由哪个节点来处理呢?最好能够让此时此刻负载较小的节点来处理,这样使得每个节点的压力都比较平均。要实现这个功能,就需要在所有节点之前增加一个“调度者”的角色,用户的所有请求都先交给它,然后它根据当前所有节点的负载情况,决定将这个请求交给哪个节点处理。这个“调度者”有个牛逼了名字——负载均衡服务器。

集群结构的好处就是系统扩展非常容易。如果随着你们系统业务的发展,当前的系统又支撑不住了,那么给这个集群再增加节点就行了。但是,当你的业务发展到一定程度的时候,你会发现一个问题——无论怎么增加节点,貌似整个集群性能的提升效果并不明显了。这时候,你就需要使用微服务结构了。

分布式结构

先来对前面的知识点做个总结。

从单机结构到集群结构,你的代码基本无需要作任何修改,你要做的仅仅是多部署几台服务器,每台服务器上运行相同的代码就行了。但是,当你要从集群结构演进到微服务结构的时候,之前的那套代码就需要发生较大的改动了。所以对于新系统我们建议,系统设计之初就采用微服务架构,这样后期运维的成本更低。但如果一套老系统需要升级成微服务结构的话,那就得对代码大动干戈了。所以,对于老系统而言,究竟是继续保持集群模式,还是升级成微服务架构,这需要你们的架构师深思熟虑、权衡投入产出比。

OK,下面开始介绍所谓的分布式结构。

分布式结构就是将一个完整的系统,按照业务功能,拆分成一个个独立的子系统,在分布式结构中,每个子系统就被称为“服务”。这些子系统能够独立运行在web容器中,它们之间通过RPC方式通信。

举个例子,假设需要开发一个在线商城。按照微服务的思想,我们需要按照功能模块拆分成多个独立的服务,如:用户服务、产品服务、订单服务、后台管理服务、数据分析服务等等。这一个个服务都是一个个独立的项目,可以独立运行。如果服务之间有依赖关系,那么通过RPC方式调用。

这样的好处有很多:

  1. 系统之间的耦合度大大降低,可以独立开发、独立部署、独立测试,系统与系统之间的边界非常明确,排错也变得相当容易,开发效率大大提升。
  2. 系统之间的耦合度降低,从而系统更易于扩展。我们可以针对性地扩展某些服务。假设这个商城要搞一次大促,下单量可能会大大提升,因此我们可以针对性地提升订单系统、产品系统的节点数量,而对于后台管理系统、数据分析系统而言,节点数量维持原有水平即可。
  3. 服务的复用性更高。比如,当我们将用户系统作为单独的服务后,该公司所有的产品都可以使用该系统作为用户系统,无需重复开发。