微信公众平台目前一共推出了三种公众账号:服务号、订阅号与企业号,那么这三种账号之间有什么区别和联系呢?微信官方的解释比较概括,只讲了这三种公众账号的功能,并没有细讲它们之间的区别,那么接下来笔者根据自己的经验详细展开介绍一下。
在整个微信账号体系中,订阅号是发布信息的平台,服务号是为用户提供服务的平台,而企业号则是企业进行 “内部管理” 的平台,通过加载第三方应用,可以实现员工的沟通、协作和管理。现在很多公司已基于微信企业号做了应用开发,例如企微云平台等。
订阅号
:侧重于信息传播服务号
:侧重于对用户进行服务企业号
:侧重于生产运营管理订阅号
:主要适用于个人、媒体、企业、政府或其他有需求的组织服务号
:主要适用于媒体、企业、政府或其他有需求的组织企业号
:主要适用于企业、政府、事业单位或其他有需求的组织订阅号
:推送的消息,显示在微信对话列表中的 “订阅号” 文件夹里服务号
:推送的消息,直接显示在微信对话列表中企业号
:推送的消息,直接显示在微信对话列表中订阅号
:每天可群发 1 条消息服务号
:每月可群发 4 条消息企业号
:每分钟可群发 200 次订阅号
:消息可以转发和分享服务号
:消息可以转发和分享企业号
:消息可以转发和分享,但加密的消息禁止转发和分享订阅号
:可以被任何微信用户扫码关注服务号
:可以被任何微信用户扫码关注企业号
:只有企业通讯录里的成员可关注订阅号
:通过认证之后可使用自定义菜单功能(目前无需认证也支持自定义菜单)服务号
:无需认证即可使用自定义菜单功能企业号
:无需认证即可使用自定义菜单功能订阅号
:不支持高级接口权限服务号
:通过认证之后,支持高级接口权限企业号
:通过认证之后,支持高级接口权限订阅号
:不支持微信支付功能服务号
:通过认证之后,支持微信支付功能企业号
:通过认证之后,支持微信支付功能订阅号
:不支持定制应用服务号
:不支持定制应用企业号
:支持定制应用JMM 是 Java 内存模型 (Java Memory Model),本身是一种抽象的概念,实际上并不存在。它描述的是一组规则或规范,通过这组规范定义了程序中各个变量 (包括实例字段,静态字段和构成数组对象的元素) 的访问方式。
由于 JVM 运行程序的实体是线程,而每个线程在创建时 JVM 都会为其创建一个工作内存 (有些地方称为栈空间),工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作 (读取、赋值等) 必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信 (传值) 必须通过主内存来完成。线程、工作内存、主内存工作交互图 (基于 JMM 规范),如下:
JMM 内存模型与 JVM 内存模型是两个完全不同的概念。JVM 内存模型是处于 Java 的 JVM 虚拟机层面的,实际上对于操作系统来说,本质上 JVM 还是存在于主内存中;而 JMM 是 Java 语言与 OS 和硬件架构层面的,主要作用是规定硬件架构与 Java 语言的内存模型,而本质上不存在 JMM 这个东西,JMM 只是一种规范,并不能说是某些技术的实现。进一步的讲,JMM 与 JVM 内存模型是不同的概念层次,在理解 JMM 的时候不要带着 JVM 内存模型去理解,更恰当说 JMM 描述的是一组规则,通过这组规则控制 Java 程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM 是围绕原子性、有序性、可见性拓展延伸的。JMM 与 JVM 内存模型唯一相似点,都存在共享数据区域和私有数据区域,在 JMM 中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,可能会看见主内存被描述为堆内存,工作内存被称为栈空间,实际上它们表达的都是同一个含义。
JMM 内存模型的可见性
JMM 内存模型的可见性指的是当主内存区域中的变量值被某个线程写入更改后,其它线程会马上知晓更改后的变量值,并重新得到更改后的变量值。
主内存主要存储的是 Java 实例对象,所有线程创建的实例对象都存放在主内存中 (除了开启了逃逸分析和标量替换的栈上分配和 TLAB 分配),不管该实例对象是成员变量还是方法中的本地变量 (也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多个线程对同一个变量进行非原子性操作时,可能会存在线程安全问题。
工作内存主要存储当前方法的所有本地变量信息 (工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关 Native 方法的信息。注意,由于工作内存是每个线程的私有数据,线程间无法相互访问彼此的工作内存,线程之间的通讯还是需要依赖于主内存,因此存储在工作内存的数据不存在线程安全问题。
这里简单介绍一下主内存与工作内存的数据存储类型以及操作方式。根据虚拟机规范,对于一个实例对象中的成员方法而言,如果方法中包含本地变量 (也称局部变量) 是基本数据类型 (boolean,byte,char,short,int,long,float,double),将直接存储在工作内存的帧栈结构中的局部变量表,但倘若本地变量是引用类型,那么该对象的在内存中的具体引用地址将会被存储在工作内存的帧栈结构中的局部变量表,而对象实例将存储在主内存 (共享数据区域,堆) 中。但对于实例对象的成员变量,不管它是基本数据类型或者包装类型 (Integer、Double 等) 还是引用类型,都会被存储到堆区 (栈上分配与 TLAB 分配除外)。至于 static 变量以及类本身相关信息将会存储在主内存中。需要注意的是,在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个类的同一个方法,那么两个线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新回主内存,简单示意图如下所示:
1 | Integer num = new Integer(100); |
1 | public void add() { |
上图是经过简化 CPU 与内存操作的简易图,实际上没有这么简单,这里为了方便理解,省去了南北桥。就目前计算机而言,一般拥有多个 CPU 并且每个 CPU 都可能存在多个核心,多核是指在一枚处理器 (CPU) 中集成两个或多个完整的计算引擎 (内核), 这样就可以支持多任务并行执行,从多线程的调度来说,每个线程都会映射到各个 CPU 核心中并行运行。在 CPU 内部有一组 CPU 寄存器,寄存器存储的是 CPU 可以直接访问和处理的数据,是一个临时放数据的空间。一般 CPU 都会从内存取数据到寄存器,然后进行处理,但由于内存的处理速度远远低于 CPU,导致 CPU 在处理指令时往往花费很多时间在等待内存做准备工作,于是在寄存器和主内存间添加了 CPU 缓存,CPU 缓存比较小,但访问速度比主内存快得多。如果 CPU 总是操作主内存中的同一地址的数据,很容易影响 CPU 执行速度,此时 CPU 缓存就可以把从内存读取到的数据暂时保存起来,如果寄存器要取内存中同一位置的数据,就可以直接从 CPU 缓存中提取,无需从主内存取。需要注意的是,寄存器并不是每次都可以从缓存中取得数据,万一不是同一个内存地址中的数据,那寄存器还必须直接绕过缓存从内存中取数据。所以并不是每次都可以从缓存取数据,这种现象有个专业的名称叫做 “缓存命中率”,可以从缓存中取就命中,不可以从缓存中取而从内存中取,就没命中,可见缓存命中率的高低也会影响 CPU 执行性能,这就是 CPU、缓存以及主内存间的简要交互过程。总而言之,当一个 CPU 需要访问主内存时,会先读取一部分主内存数据到 CPU 缓存 (当然,如果 CPU 缓存中存在需要的数据就会直接从缓存获取),进而再读取 CPU 缓存的数据到寄存器,当 CPU 需要写数据到主内存时,同样会先刷新寄存器中的数据到 CPU 缓存,然后再将数据刷新到主内存中。
提示
上面介绍的 CPU 寄存器 --> CPU 缓存 --> 主内存的关系,实则就类似于 Appcalition (Java) --> Cache (Redis) --> DB (MySQL) 的关系,Java 程序的性能由于 DB 需要走磁盘受到了影响,导致 Java 程序在处理请求时需要等到 DB 的处理结果,而此时负责处理该请求的线程一直处于阻塞等待状态,只有当 DB 处理结果返回了才能继续工作,那么实际上整个模型中的问题是:DB 的速度跟不上 Java 程序的性能,导致整个请求处理起来变的很慢,但是实际上在 DB 处理的过程 Java 的线程是处于阻塞不工作的状态的,那么实际上是没有必要的,因为这样最终会导致整体系统的吞吐量下降,此时可以加入 Cache (Redis) 来提升程序响应效率,从而整体提升系统吞吐和性能。实际上做性能优化的目的就是让系统的每个层面处理的速度加快,而架构实际上就是设计一套能够吞吐更大量的请求的系统。
在以上的阐述中,大致介绍了 Java 内存模型和硬件的内存架构之后,接着介绍 Java 中线程的实现原理,理解线程的实现原理,有助于了解 Java 内存模型与硬件内存架构的关系。在 Windows OS 和 Linux OS 上,Java 线程的实现是基于一对一的线程模型,所谓的一对一模型,实际上就是通过语言级别层面的程序去间接调用系统内核的线程模型,即在使用 Java 线程时,比如 new Thread(Runnable)
,JVM 内部是转而调用当前操作系统的内核线程来完成当前 Runnable 任务。这里需要了解一个术语,内核线程 (Kernel-Level Thread,KLT),它是由操作系统内核 (Kernel) 支持的线程,这种线程是由操作系统内核来完成线程切换,内核通过操作调度器进而对线程执行调度,并将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这也就是操作系统可以同时处理多任务的原因。由于编写的多线程程序属于语言层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程 (Light Weight Process),也是通常意义上的线程,由于每个轻量级进程都会映射到一个内核线程,因此可以通过轻量级进程调用内核线程,进而由操作系统内核将任务映射到各个处理器,这种轻量级进程与内核线程间 1 对 1 的关系就称为 Java 程序中的线程与 OS 的一对一模型。如下图所示:
Java 程序中的每个线程都会经过 OS 被映射到 CPU 中进行处理,当然,如果 CPU 存在多核,那么一个 CPU 可同时并行调度执行多个线程。
通过对前面的 Java 内存模型、硬件内存架构以及 Java 多线程的实现原理,可以发现多线程的执行最终都会映射到硬件处理器上进行执行,但 Java 内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存 (线程私有数据区域) 和主内存 (堆内存) 之分,也就是说 Java 内存模型对内存的划分对硬件内存并没有任何影响,因为 JMM 只是一种抽象的概念,是一组规则,并不实际存在,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到 CPU 缓存或者寄存器中。因此总体上来说,Java 内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。
Java 语言规范规定 JVM 线程内部维持顺序化语义,即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫做 “指令重排序”。指令重排序的意义是什么?JVM 能根据处理器特性(CPU 多级缓存、多核处理器等)适当地对机器指令进行重排序,使机器指令更符合 CPU 的执行特性,最大限度的发挥机器性能。下图为从源码到最终执行的指令序列示意图:
重点内容
计算机在执行程序时,为了提高性能,编译器和处理器 (CPU) 往往会对指令做重排序,一般分以下 3 种:
编译器优化重排序
指令级并行重排序
内存系统重排序
as-if-serial
语义的意思是不管怎么重排序(编译器和处理器为了提高并行度),程序(单线程)的执行结果不能被改变。编译器、Runtime 和处理器都必须遵守 as-if-serial
语义。为了遵守 as-if-serial
语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
1 | public class Test { |
由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,线程如果想要操作主内存中的某个变量,那么必须通过工作内存间接完成,主要过程是将变量从主内存拷贝到每个线程各自的工作内存,然后在工作内存中对变量进行操作,操作完成后再将变量写回主内存。如果存在两个线程同时对一个主内存中的实例对象的变量进行操作,这就有可能诱发线程安全问题。
第一种情况,在上图中假设主内存中存在一个共享变量 int i = 0
。现在有 A 和 B 两个线程分别对变量 i 进行操作,A、B 线程各自都先将主内存中的变量 i 拷贝到自己的工作内存中,也就是将其存储为共享变量副本 i,然后再对工作内存中的 i 进行自增操作。那么假设此时 A、B 线程同时将主内存中 i = 0
拷贝到自己的工作内存中进行操作,那么 A 在自己工作内存中对 i 进行自增的操作对 B 的工作内存的副本 i 是不可见的。当 A 执行完自增操作之后会将结果 1 刷写回主内存,此时 B 也执行完了 i++ 操作,那么实际上 B 刷写回主内存的值也是基于之前从主内存中拷贝到自己工作内存的值 i = 0
,那么实际上 B 刷写回主内存的值也是 1。理论上两个线程都对主内存中 i 进行了自增操作,正确的结果应该是 i = 2
,但是此时的情况结果却是 i = 1
,这就产生了线程安全问题。
第二种情况,在上图中假设现在 A 线程想要修改 i 的值为 2,而 B 线程却想要读取 i 的值,那么 B 线程读取到的值是 A 线程更新后的值 2 还是更新前的值 1 呢?答案是不确定,即 B 线程有可能读取到 A 线程更新前的值 1,也有可能读取到 A 线程更新后的值 2。这是因为工作内存是每个线程私有的数据区域,而线程 A 修改变量 i 时,首先是将变量从主内存拷贝到 A 线程的工作内存中,然后对变量进行操作,操作完成后再将变量 i 写回主内存,而对于 B 线程的也是类似的,这样就有可能造成主内存与工作内存间数据存在一致性问题,假如 A 线程修改完后正在将数据写回主内存,而 B 线程此时正在读取主内存,即将 i = 1
拷贝到自己的工作内存中,这样 B 线程读取到的值就是 i = 1
;但如果 A 线程将 i = 2
写回主内存后,B 线程才开始读取主内存的话,那么此时 B 线程读取到的就是 i = 2
,但到底是哪种情况先发生呢?这是不确定的。
为了解决类似如上阐述的线程安全问题,JVM 定义了一组规则,通过这组规则来决定一个线程对共享变量的写入何时对另一个线程可见,这组规则也称为 Java 内存模型(JMM)。JMM 整体就是围绕着程序执行的原子性、可见性、有序性展开的。
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。比如对于一个静态变量 int i = 0
,两个线程同时对它赋值,线程 A 的赋值操作为 i = 1
,而线程 B 的赋值操作为 i = 2
,不管线程如何运行,最终 i 的值要么是 1,要么是 2,线程 A 和线程 B 之前的操作是互不干扰的,这就是原子性操作,不可被中断的特点。特别注意的是,对于 32 位系统的来说,double 类型数据和 long 类型数据 (对于基本数据类型,byte,short,int,float,boolean,char 的读写是原子操作) 的读写并非原子性的,也就是说如果存在两个线程同时对 double 类型或者 long 类型的数据进行读写是存在相互干扰的。因为对于 32 位虚拟机来说,每次原子读写是 32 位的,而 double 和 long 则是 64 位的存储单元,这样会导致一个线程在写时,操作完前 32 位的原子操作后,轮到 B 线程读取时,恰好只读取到了后 32 位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它可能是 “半个变量” 的数值,即 64 位数据被两个线程分成了两次读取。但也不必太担心,因为读取到 “半个变量” 的情况比较少见,至少在目前的商用虚拟机中,几乎都把 64 位的数据的读写操作作为原子操作来执行,因此对于这个问题不必太在意,知道有这么回事即可。 那么其实本质上原子性操作指的就是一组大操作要么就全部执行成功,要么就全部失败,举个例子:下单:{增加订单,减库存} 那么对于用户来说下单是一个操作,那么系统就必须保证下单操作的原子性,要么就增加订单和减库存全部成功,不存在增加订单成功,减库存失败,那么这个例子从宏观上来就就是一个原子性操作,非原子性操作反之,线程安全问题产生的根本原因也是由于多线程对一个共享资源进行非原子性操作导致的。
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。对于串行(单线程)程序来说,可见性是不存在的,因为在任何一个操作中修改了某个变量的值,后续的操作中都能读取到这个变量值,并且是修改过的新值。但在多线程环境中就不一定了,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程 A 修改了共享变量 i 的值,还未写回主内存时,另外一个线程 B 又对主内存中同一个共享变量 i 进行操作,但此时 A 线程工作内存中的共享变量 i 对线程 B 来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题。另外,无论是编译器优化还是处理器优化的重排现象,在多线程环境下,都可能会导致程序乱序执行的问题,从而也可能会导致可见性问题。
有序性是指对于单线程的执行代码,通常认为代码的执行是按顺序依次执行的,这样的理解如果是放在单线程环境下没有问题,毕竟对于单线程而言确实如此,代码由编码的顺序从上往下执行,就算发生指令重排序,由于所有硬件优化的前提都是必须遵守 as-if-serial
语义,所以不管怎么排序,都不会且不能影响单线程程序的执行结果,将这称之为 “有序执行”。反之,对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排序后的指令与原指令的顺序未必一致。要明白的是,在 Java 程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。
Java 程序在执行的过程中本质上就是操作系统在调度 JVM 的 “线程” 执行,而在执行的过程中是与内存的交互操作,而内存交互操作有 8 种。特别注意,虚拟机实现必须保证每一个操作都是原子的,不可拆分的,但对于 double 和 long 类型的变量来说,load、store、read 和 write 操作在某些平台上允许非原子性。一个共享变量如何从主内存拷贝到工作内存,又如何从工作内存同步到主内存之间的实现细节,由 Java 内存模型定义的八种操作来完成。
lock(锁定)
:作用于主内存的变量,把一个变量标记为一个线程独占状态;unlock(解锁)
:作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;read(读取)
:作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 工作使用;load(载入)
:作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量;use(使用)
:作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎;assign(赋值)
:作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量;store(存储)
:作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作;wirte(写入)
:作用于工作内存的变量,它把 store 操作从工作内存中的一个变量值传送到主内存的变量中。总结
JMM 对上述八大原子操作制定了如下的使用规则:
提示
JMM 通过这八种操作规则和对 volatile 的一些特殊规则就能确定哪些操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析,所以一般也不会通过上述规则进行分析。更多的时候,会使用 JMM 中的 happens-before 规则来进行分析。
假如在多线程开发过程中,都需要通过 sychronized、volatile 来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,而且加锁其实本质上是让多线程的并行执行变为了串行执行,这样会大大影响程序的性能。幸运的是,从 JDK 5 开始,Java 使用新的 JSR-133 内存模型,提供了 happens-before 原则(也叫先行发生原则)来辅助保证程序执行的原子性、可见性和有序性,它是判断数据是否存在竞争、线程是否安全的依据。
happens-before (先行发生原则) 是 Java 内存模型中定义的两个操作之间的偏序关系。比如说操作 A 先行发生于操作 B,那么在 B 操作发生之前,A 操作产生的 “影响” 都会被操作 B 感知到。这里的影响是指修改了内存中的共享变量、发送了消息、调用了方法等。
程序顺序原则
:即在单个线程内必须保证语义串行,也就是说按照代码顺序执行。锁规则
:解锁(unlock)操作必须发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。volatile 规则
:volatile 变量的写,先发生于读,这保证了 volatile 变量的可见性。简单理解就是,volatile 变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。线程启动规则
:线程的 start()
方法先于它的每一个动作,即如果线程 A 在执行线程 B 的 start()
方法之前修改了共享变量的值,那么当线程 B 执行 start()
方法时,线程 A 对共享变量的修改对线程 B 可见。线程终止原则
:线程的所有操作先于线程的终结,Thread.join()
方法的作用是等待当前执行的线程终止。假设在线程 B 终止之前,修改了共享变量,线程 A 从线程 B 的 join()
方法成功返回,线程 B 对共享变量的修改将对线程 A 可见。线程中断规则
:对线程 interrupt()
方法的调用先行发生于被中断线程的代码检查到中断事件的发生,可以通过 Thread.interrupted()
方法检测线程是否中断。对象终结规则
:对象的构造函数执行,结束先于 finalize()
方法。传递性规则
:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。finalize () 方法说明
finalize()
是 Object 中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它 finalize()
方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运),比如释放资源或者关闭连接等。
在下述的代码中,如果有两个线程 A 和 B,线程 A 先调用 setValue()
方法,然后线程 B 调用 getValue()
方法,那么线程 B 执行方法返回的结果是什么?
1 | private int value = 0; |
对照先行发生原则一个一个来对比。首先是程序次序规则,这里是多线程,不在一个线程中,不适用;然后是锁规则,这里没有 synchronized,自然不会发生 lock 和 unlock,不适用;后面对于线程启动规则、线程终止规则、线程中断规则也不适用;这里与对象终结规则、传递性规则也没有关系。线程 A 和线程 B 的启动时间虽然有先后,但上述代码没有符合 8 条原则中的任意一条,也没有使用任何同步手段,因此线程 B 执行结果是不确定的,即上述的操作不是线程安全的。如何修改呢,一个是对 get、set 方法加入 synchronized 关键字,即可以使用锁规则;要么对 value 加 volatile 修饰,可以使用 volatile 变量规则。
通过上面的例子可知,一个操作时间上先发生并不代表这个操作先行发生,那么一个操作先行发生是不是代表这个操作在时间上先发生?也不是,如下面的例子:
1 | int i = 2; |
在同一个线程内,对 i 的赋值先行发生于对 j 赋值的操作,但是代码重排序优化,也有可能是 j 的赋值先发生,我们无法感知到这一变化。综上所述,时间先后顺序与先行发生原则之间基本没有太大关系。我们衡量并发安全的问题的时候不要受到时间先后顺序的干扰,一切以先行发生原则为准。
volatile 是 Java 虚拟机提供的轻量级的同步机制,它有如下两个作用:
volatile 有三大特性:
提示
volatile 保证可见性和禁止指令重排的两大特性,其内存语义都是通过内存屏障实现的。
关于 volatile 的可见性作用,必须意识到被 volatile 修饰的共享变量对所有线程总是立即可见的,即对于 volatile 共享变量的所有写操作总是能立刻反应到其他线程中。JMM 是如何实现让 volatile 共享变量对其他线程立即可见的呢?实际上,当某个线程对一个 volatile 共享变量执行写操作时,JMM 会把该线程对应的工作内存中的共享变量副本刷新到主内存中,并通知其他线程将自己工作内存中的共享变量副本设置为无效;当某个线程对自己工作内存中的共享变量副本进行读写操作时,该线程会从主内存中重新读取共享变量的值到工作内存中。volatile 共享变量正是通过这种写 - 读方式实现对其他线程可见,但其内存语义则是通过内存屏障实现的。
1 | public class VolatileTest { |
程序运行输出的结果:
1 | refresh data....... |
结合前面介绍的数据同步八大原子操作来分析上述的代码:
initFlag = true
吗?不等于,此时循环会一直执行。while(!initFlag)
,然后执行循环体;inifFlag = true
;initFlag = true
传递给主内存;为什么某个线程将共享变量的值更改后,其它线程可以马上知晓呢?其实这里是使用 “总线嗅探技术” 来保证可见性的。
缓存一致性
在介绍总线嗅探技术之前,首先谈谈缓存一致性的问题,就是当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一致。为了解决缓存一致性的问题,需要各个处理器在访问缓存时都遵循一些协议,在读写时要根据协议进行操作,这类协议主要有 MSI、MESI 等等。
MESI 协议
当 CPU 写入数据时,如果发现操作的变量是共享变量,即在其它 CPU 中也存在该变量的副本,就会发出信号通知其它 CPU 将该共享变量的缓存(副本)设置为无效。因此当其它 CPU 读取这个变量的缓存时,发现自己缓存的该变量是无效的,那么它就会从内存中重新读取。
总线嗅探
那么 CPU 是如何发现缓存数据是否失效呢?这里用到了 “总线嗅探技术”,就是每个处理器通过嗅探在总线上传播的数据来检查自己的缓存数据是否失效了。当处理器发现自己的缓存数据对应的内存地址被修改,就会将当前处理器的缓存数据设置为无效状态;当处理器对这个缓存数据进行修改的时候,会重新从内存中把数据读取到处理器缓存中,然后再执行修改操作。
总线风暴
总线嗅探技术有哪些缺点?由于 volatile 的 MESI 缓存一致性协议,需要不断的从主内存嗅探和 CAS 循环,无效的交互会导致总线带宽达到峰值。因此不要大量使用 volatile 关键字,至于什么时候使用 volatile、什么时候用锁以及 syschonized 都是需要根据实际场景衡量的。
1 | public class VolatileTest { |
在并发场景下,上述代码中变量 i 的任何改变都会立刻反应到其他线程中,但是如果存在多个线程同时调用 increase ()
方法的话,就会出现线程安全问题。因为 i++ 并不是原子性操作,i++ 实际上是由三个操作组成,包括从主内存读取值、在工作内存中执行加 1 操作、将操作结果刷写回主内存,它们三步中其中一个线程在执行任何一步的时候都有可能被打断,所以会出现线程安全问题。如果第二个线程在第一个线程读取旧值和写回新值期间读取了 i 的值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加 1 操作,这也就造成了线程安全问题,因此需要使用 synchronized、Lock 或者原子类来保证原子性,以确保线程安全。特别注意,一旦使用 synchronized 修饰方法后,由于 sunchronized 本身也具备与 volatile 一样的可见性,因此在这样的情况下就完全可以不使用 volatile 关键字来修饰变量。
1 | public class VolatileAtomic { |
程序运行输出的结果:
1 | 9615 |
可以发现上述程序实际输出的结果不到 10000,原因是因为存在并发操作,并且 volatile 不能保证原子性。每个线程只执行 counter++
操作,那为什么不能保证原子性呢?这是因为 counter++
操作不是一步完成的(非原子性),它分为三个步骤完成,包括从主内存读取值、在工作内存中执行加 1 操作、将操作结果刷写回主内存。假设现在有三个线程同时执行自加运算操作,三个线程都读取到主内存中的 counter 共享变量,然后三个线程在各自的工作内存中对共享变量的副本进行加 1 操作,但它们并发执行加 1 之后,因为同一时刻只能有一个线程刷写回主内存,所以其它线程的写操作会被挂起。假设线程 A 先执行写操作,在写操作执行完之后,由于 volatile 的可见性,JMM 会主动通知其它两个线程主内存中共享变量的值已经被修改了;但是由于 CPU 的调度速度实在太快了,其它两个线程还没来得及接收到通知,就陆续将加 1 的结果写入主内存,这就造成其他线程覆盖了线程 A 写入的值,从而导致出现写丢失的现象,这样也就让最终的计算结果少于 10000。
volatile 关键字的其中一个作用是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。关于指令重排优化前面已经介绍过,这里主要重点介绍 volatile 是如何使用内存屏障实现禁止指令重排优化的。
内存屏障(Memory Barrier),又称内存栅栏,是一个 CPU 指令,其作用有两个:
Intel 硬件提供了一系列的内存屏障,主要有以下几种类型:
lfence
:是一种 Load Barrier 读屏障;sfence
:是一种 Store Barrier 写屏障;mfence
:是一种全能型的屏障,具备 lfence 和 sfence 的能力;Lock 前缀
:Lock 不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock 会对 CPU 总线和高速缓存加锁,可以理解为 CPU 指令级的一种锁。它后面可以跟 ADD、ADC、AND、BTC、BTR、BTS、CMPXCHG、CMPXCH8B、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR、XADD、and XCHG 等指令。不同硬件实现内存屏障的方式不同,Java 内存模型屏蔽了这些底层硬件平台的差异,由 JVM 来为不同平台生成相应的机器码。JVM 中提供了四类内存屏障指令:
由于编译器和处理器 (CPU) 都能执行指令重排优化,如果在指令间插入一条内存屏障,则会告诉编译器和处理器,不管什么指令都不能和这条内存屏障指令重排序,也就是说可以通过插入内存屏障来禁止在内存屏障前后的指令执行重排序优化。内存屏障的另外一个作用是强制刷出各种处理器的缓存数据,因此任何处理器上的线程都能读取到这些数据的最新版本。总之,volatile 变量正是通过内存屏障实现其内存中的语义,即可见性和禁止指令重排优化。
这里将介绍一个非常典型的禁止指令重排优化的例子:单例模式(DCL - 双端检锁)。
1 | public class DoubleCheckLock { |
提示
上述代码在单线程环境下并没什么问题,但如果在多线程环境下就可能会出现线程安全的问题。因为当某一线程执行到第一次检测,读取到 instance 不为 null 时,instance 实例可能还没有完成初始化。
因为 instance = new DoubleCheckLock ();
可以分为以下 3 个步骤完成(伪代码):
1 | memory = allocate(); // 1. 分配对象内存空间 |
步骤 1 和步骤 2 之间可能会重排序,如下:
1 | memory = allocate(); // 1. 分配对象内存空间 |
由于步骤 2 和步骤 3 不存在数据依赖关系,而且无论重排序前还是重排序后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一个线程访问 instance 实例不为 null 时,由于 instance 实例未必已经初始化完成,这就会造成线程安全问题。也就是,当没有使用 volatile 关键字时,在某些情况下会出现多次初始化实例的情况(存疑),这是由于指令重排序导致的。那么该如何解决呢,很简单,使用 volatile 禁止 instance 变量被执行指令重排优化即可。
1 | // 禁止指令重排优化 |
前面提到过重排序分为编译器重排序和处理器重排序。为了实现 volatile 内存语义,JMM 会分别限制这两种类型的重排序类型。下面是 JMM 针对编译器制定的 volatile 重排序规则表。
为了实现内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优配置来最小化插入屏障的总数几乎不可能。为此,JMM 采取保守策略。下面是 JMM 基于保守策略的内存屏障插入策略:
上述内存屏障插入策略非常保守,但它可以保证在任一处理器平台,任意的程序中都能得到正确的 volatile 内存语义。
上图是在保守策略下,volatile 写操作插入内存屏障后生成的指令序列示意图。StoreStore 屏障可以保证在 volatile 写操作之前,其前面的所有普通写操作已经对任意处理器可见。这是因为 StoreStore 屏障将保障前面所有的普通写在 volatile 写之前刷新到主内存。这里比较有意思的是,volatile 写后面的 StoreLoad 屏障。此屏障的作用是避免 volatile 写与后面可能有的 volatile 读 / 写操作重排序。因为编译器常常无法准确判断在一个 volatile 写的后面是否需要插入一个 StoreLoad 屏障(比如,一个 volatile 写之后方法立即 return)。为了保证能正确实现 volatile 的内存语义,JMM 在采取了保守策略:在每个 volatile 写的后面,或者在每个 volatile 读的前面插入一个 StoreLoad 屏障。从整体执行效率的角度考虑,JMM 最终选择了在每个 volatile 写的后面插入一个 StoreLoad 屏障,因为 volatile 写 - 读内存语义的常见使用模式是:一个写线程写 volatile 变量,多个线程读同一个 volatile 变量。当读线程的数量大大超过写线程时,选择在 volatile 写之后插入 StoreLoad 屏障将带来可观的执行效率的提升。从这里可以看到 JMM 在实现上的一个特点:首先确保正确性,然后再去追求执行效率。
上图是在保守策略下,volatile 读操作插入内存屏障后生成的指令序列示意图。LoadLoad 屏障用来禁止处理器把前面的 volatile 读与后面的普通读重排序。LoadStore 屏障用来禁止处理器把前面的 volatile 读与后面的普通写重排序。
上述介绍的 volatile 写 和 volatile 读的内存屏障插入策略非常保守。在实际执行时,只要不改变 volatile 写 - 读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面通过具体的示例代码进行说明。
1 | class VolatileBarrierExample { |
针对 readAndWrite()
方法,编译器在生成字节码时可以做如下的优化:
特别注意
在上图中,最后的 StoreLoad 屏障不能省略。因为第二个 volatile 写之后,方法立即 return,此时编译器可能无法准确判断后面是否会有 volatile 读或写,为了安全起见,编译器通常会在这里插入一个 StoreLoad 屏障。
上面的优化针对任意处理器平台,由于不同的处理器有不同 “松紧度” 的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以 X86 处理器为例,上图中除最后的 StoreLoad 屏障外,其他的屏障都会被省略。前面保守策略下的 volatile 读和写操作,在 X86 处理器平台可以继续被优化,如下图所示。X86 处理器仅会对读 - 写操作做重排序。X86 不会对读 - 读、读 - 写 和 写 - 写 做重排序,因此在 X86 处理器中会省略掉这 3 种操作类型对应的内存屏障。在 X86 中,JMM 仅需在 volatile 写操作后面插入一个 StoreLoad 屏障即可正确实现 volatile 写 - 读的内存语义,这意味着在 X86 处理器中,volatile 写的开销比 volatile 读的开销会大很多,因为执行 StoreLoad 的屏障开销会比较大。
从 Qt 5.15
开始,Qt 的开源版本只支持在线安装,不再提供离线安装包。使用在线安装器可以安装 Qt 5.9
之后 Qt 5
和 Qt 6
的各个子版本。
Qt 的长期技术支持版本
5.15
。6.2
,包含了 Qt 框架中的所有模块。Qt 官方有一个专门的资源下载网站,所有的开发环境和相关工具都可以从这里下载,具体地址是:
http://download.qt.io/
目录 | 说明 |
---|---|
archive | 各种 Qt 开发工具安装包,新旧都有(可以下载 Qt 开发环境和源代码)。 |
community_releases | 社区定制的 Qt 库,Tizen 版 Qt 以及 Qt 附加源码包。 |
development_releases | 开发版,有新的和旧的不稳定版本,在 Qt 开发过程中的非正式版本。 |
learning | 有学习 Qt 的文档教程和示范视频。 |
ministro | 迷你版,目前是针对 Android 的版本。 |
official_releases | 正式发布版,即最新稳定版的 Qt 库和开发工具(可以下载 Qt 开发环境和源代码)。 |
online | Qt 在线资源。 |
snapshots | 预览版,最新的开发测试中的 Qt 库和开发工具。 |
archive
和official_releases
两个目录都有最新的 Qt 开发环境安装包,这里以archive
目录里的内容为例来说明。点击进入archive
目录,会看到多个子目录:
目录 | 说明 |
---|---|
vsaddin | Qt 针对 Visual Studio 集成的插件。 |
qtcreator | Qt 官方的集成开发工具。 |
qt | Qt 开发环境的下载目录。 |
online_installers | 在线安装器,国内用户的下载速度较慢。 |
additional_libraries | QT 框架的一些附加模块。 |
再进入
qt
子目录 ,会看到所有的 Qt 版本,从1.0
到目前的6.6
进入
6.6
目录,会看到各种子版本
进入
6.6.1
子版本,会看到多个目录
目录 | 说明 |
---|---|
submodules | Qt 各个子模块的源码包 |
single | Qt 完整的源码包 |
Qt 版本号的命名规则
这里解释一下 Qt 的版本号,比如 6.5.3
是完整的 Qt 版本号,第一个数字 6 是大版本号(major),第二个数字 5 是小版本号(minor),第三个数字 3 是补丁号(patch)。只要前面两个数字相同,Qt 的特性就是一致的,最后的数字是对该版本的补丁更新。
在国内,Qt 的官方下载速度较慢,建议使用国内镜像网站下载。这里推荐几个国内著名的 Qt 镜像网站,主要是各个高校的:
镜像网站名称 | 下载地址 |
---|---|
中国科学技术大学 | http://mirrors.ustc.edu.cn/qtproject/ |
清华大学 | https://mirrors.tuna.tsinghua.edu.cn/qt/ |
北京理工大学 | http://mirror.bit.edu.cn/qtproject/ |
中国互联网络信息中心 | https://mirrors.cnnic.cn/qt/ |
值得一提的是,国内镜像网站的资源目录结构和 Qt 官网是类似的,这里不再赘述。
在 Qt 官网 下载在线安装器,如
qt-unified-windows-x86-4.2.0-online.exe
双击在线安装器的
EXE
文件,开始安装 Qt,然后根据自己实际需求安装所需的组件
提示
MSVC 2019 64-bit
开发套件,若不需要,可以选择不安装该组件。5.15
与 Qt 6.2
,若不需要使用 Qt 5,可以选择不安装上述图中 Qt 5.15.2
相关的组件,只安装 Qt 6 相关的组件。一开始,开箱即用的 MySQL,一定是企业的首选。不仅仅因为用的人多,更重要的是生态成熟。随着业务的飞速发展(虽然现在这种机会比较少了),对于 MySQL 来说,很快就会遇到各种性能问题。这个时候,就需要由单机 MySQL 向分布式发展了。
单机 MySQL 面临很多问题:
很长时间以来,国内互联网的做法普遍是采用加入一个中间件的方式来解决,但随着分布式数据库的技术越来越成熟,这些魔法逐渐下沉到它本应该解决的层面 – 数据库实现层。留给分库分表技术的时间已经不多,它的存量市场越来越少了。分库分表技术,退出历史舞台,也是迟早的事情了。解决上面三个单机 MySQL 问题,有很多种切入层面,常见的有框架层、驱动层、代理层。
简单地在 MyBatis 或者 JPA 之上使用 AOP 或者拦截器封装一层,也可以实现,这也是最傻的方式。
再进一步,就可以在 JDBC 之上的驱动层来实现,把分库分表的路由维护在内存里,通过重写的 DataSource、Connection、Statment、ResultSet 等,对业务进行无侵入的改进。但可惜的是,这类方案还必须要维护与逻辑表相对应的物理表,而且功能也是阉割的,不确定性依然不小。更要命的是,JDBC 只支持 Java,对于某些公司来说,就非常的不适用。
再就是采用中间件的传统模式,引入 Proxy 中间件,即把自己伪装成一个 MySQL Server,接受 Client 的请求。至于它后面怎么去操作真实的数据库,开发者都不需要知道。但 Proxy 本身也是一套服务,需要保证高可用,且有运维成本在里面,同时功能依然是阉割的。
框架层、驱动层、代理层,在过去很长一段时间里,有无数的互联网公司前赴后继的试水,从 TDDL、Cobar,到 MyCat、ShardingSphere,各种层面的中间件也是层出不穷。但最近几年,这种争相斗艳的场面逐渐不再,到最后剩下来的,也就 ShardingSphere 这一枝独秀了。是问题不存在了么?不,正好相反,问题越来越严重。并不是问题消失了,而是它被转化成其他解决方式了。
抛开关系型数据库不说,很久之前,类似于 ElasticSearch、Cassandra 这样的 NoSQL 存储,分片和副本的概念,就已经非常成熟了,而且它们是内置的,并不需要 DBA 去人工维护它们的物理位置。对于关系型数据库来说,走向分布式也终将成为必然。随着 Raft 等协议应用越来越广泛,分布式数据库的可靠性也逐渐得到了保证。如果以前因为事务问题而拒绝采用某些 NoSQL 产品,那么如今完全兼容 MySQL 的分布式数据库,没有理由再拒绝。
云厂商,直接提供了像 Aurora、PolarDB 之类的 MySQL 增强,更有类似 TiDB、OceanBase 这样纯粹的分布式数据库,越来越多的业务走向了这个终途。当团队加班加点验证着分库分表中间件的时候,却发现其实换个兼容的存储就能玩得转,你会怎么选,简直不用再多说。当然,一旦选用了分布式数据库,以前的 DBA 经验可能就不管用了,比如说索引及其二级索引。开发团队不得不学习新的知识,来应对分布式环境。但这些都是阵痛,长远看来,分布式数据库是趋势,而分库分表中间件只能吃存量业务。
分库分表中间件并不是消失了。它摇身一变,变成了分布式数据库的一部分。你可能会听到很多切到分布式数据库,又从分布式数据库切回到 MySQL 的案例,这属于想吃螃蟹但并没有吃到。目前来看,分布式数据库越来越稳定,生态建设也越来越好。而分库分表,则适用于存量业务,终将会退出历史的舞台。
]]>PDF 可以在任何屏幕上完美地显示内容,并且可以轻松阅读、存档或分发文件,而 Office 格式是文档创建和编辑的必备格式,因此 PDF 转换对于进一步编辑或重复使用变得很常见。Cisdem PDF Converter OCR 支持快速无缝地将 PDF 转换为多种文档格式。
支持 Windows 7、Windows 8、Windows 10、Windows 11 的 64 位操作系统。
Cisdem PDF Converter OCR 是一款 PDF 格式转换软件,可以轻松地将正常和可扫描的 PDF 文档转换为可编辑文本格式的文件,比如转换为可编辑和可搜索的 PDF、Word、Excel、PPT、ePub、HTML、TXT、Rtfd、图片 (JPEG,BMP,PNG,GIF,TIFF) 等格式,支持 OCR 技术,同时保持原始布局和文件质量。
Cisdem PDF Converter OCR 将尽力保留文本、图像、表格元素,并尽可能准确地保持原始格式、布局。例如,它可以在 Word 文档中保留复杂 PDF 文件的原始外观和感觉,将表格数据放入 Excel 电子表格中的正确单元格中,并在 PowerPoint 中保留布局,您无需花费数小时调整输出文件。OCR (光学字符识别) 用于根据形状和外观识别文本字符,它可以帮助从扫描的 PDF 或图像文件中提取文本内容,是归档和重新扫描 PDF 的必备功能。Cisdem PDF Converter OCR 不仅可以通过启用 OCR 功能快速批量处理扫描的 PDF 和图像文件,还可以通过手动标记文本、图像和表格来微调 OCR 应用区域,以实现更准确的识别。Cisdem PDF Converter OCR 可以识别 200 多种语言,包括英语、中文、西班牙语、阿拉伯语、法语、俄语、葡萄牙语、德语、日语、韩语等。
https://pan.baidu.com/s/1jls5BTI_kinz5e70wbbQHg
7exm
Setup.exe
和 Crack
等文件,双击 Setup.exe
文件开始安装next
按钮install
按钮开始安装软件打开文件位置
即可获得软件的安装目录Crack
文件夹,将里面的破解补丁文件复制到软件安装目录中替换About
选项,查看是否激活成功OK
按钮开始下载。如果下载失败,可以关闭并重启软件,拖拽 PDF 文件到主界面,然后重新安装 OCR 模块OCR PDF
,并点击 齿轮
图标,选择语言为 Chinese(Simplified)
和 English
,然后点击 OK
按钮Convert
按钮开始转换 PDF 文件本文主要介绍 Kafka 在生产实践中存在的问题,如运维操作、负载均衡、故障恢复等各方面,并简单介绍字节跳动、小红书是如何使用消息队列的云原生化来解决这些问题的。
随着业务快速增长,经典消息队列 Kafka 的劣势开始逐渐暴露,在弹性、规模、成本及运维方面都无法满足业务需求。在本中,将介绍 Kafka 在生产实践中存在的问题,如运维操作、负载均衡、故障恢复等各方面。
Kafka 的重启、扩缩容、分区迁移,这些运维操作都比较复杂。
Kafka 自身支持处理单机故障,但对多机故障却无能为力。
BMQ 是字节跳动自研的一款消息队列,基于 C++ 开发,兼容 Kafka 协议,采用 HDFS 分布式存储来存放消息数据。
小红书在 Kafka 原有的基础上,引入了分层存储、弹性扩容、消费隔离等特性。
由 AutoMQ 开源的新一代消息流存储平台,面向开发者提供低成本、无状态、 100% 兼容 Apache Kafka 的消息服务。目前有两种版本,分别是商业版与开源版本。
由 AutoMQ 基于 Apache RocketMQ 5.0 的云原生开源实现。目前有两种版本,分别是商业版与开源版本。
Xinetd 是新一代的网络守护进程服务程序,又叫超级 Internet 服务器,经常用来管理保护多种轻量级 Internet 服务。它在 Linux 的安全中有着举足轻重的地位,它管理的服务都是一些不是很常用,但是系统中偶尔也会用到的小服务或者该服务没什么好的安全机制,比如:Ftp、Rsync、Telnet、SSH 等。它并不是一真正意义上的服务,Xinetd 相当于 Rync、SSH 等服务的代理人,比如代理了 sshd
,那就可以关闭 SSH 服务,22 端口就由 Xinetd 服务代理了。它的作用大致可以分为以下几个:访问控制、防止 DOS 攻击、服务转发、用户交互式体验等。
超级守护进程
:多个服务统一由一个进程管理,该进程可以管理多个服务。独立启动的守护进程
:每个特定服务都有单独的守护进程(stand-alone),这个保证单一服务始终存活的进程就是独立启动的守护进程。强大的存取控制功能
libwrap
支持,其效能更甚于 tcpd
;有效防止 DoS 攻击
强大的日志功能
syslog
设定日志等级;syslog
,也可以为每个服务建立日志文件;转向功能
支持 IPv6
2.1.8.8 pre*
版本开始就支持 IPv6,另外 IPv4 仍然被支持。与客户端的交互功能
Xinetd 当前最大的缺点是对 RPC 支持的不稳定,但是可以启用 protmap
,使它与 Xinetd 共存来解决这个问题。
原则上任何系统服务都可以使用 Xinetd,然而最适合的应该是那些常见的网络服务,并且这些服务的请求数目和频繁程度不会太高。像 DNS 和 Apache 就不适合采用 Xinetd 进行管理,而像 FTP、Telnet、SSH 等就适合使用 Xinetd 进行管理。
具体可以使用 Xinetd 进行管理的服务都在 /etc/services
配置文件中定义,该配置文件记录了网络服务名和它们对应使用的端口号及协议。文件中的每一行对应一种服务,它由 4 个字段组成,中间用 Tab 键或空格键分隔,分别表示 服务名称
、使用端口
、协议名称
及 别名
。在一般情况下,不要修改该配置文件的内容,因为这些设置都是 Internet 标准的设置。一旦修改,可能会造成系统冲突,使用户无法正常访问资源。Linux 系统的端口号范围为 0 ~ 65535,不同范围的端口号有不同的意义:
0
:不使用。1 ~ 1023
:系统保留,只能由 root
用户使用。1024 ~ 4999
:由客户端程序自由分配。5000 ~ 65535
:由服务器程序自由分配。 Xinetd 的配置文件是 /etc/xinetd.conf
,但是它只包括默认值,并包含 /etc/xinetd.d
目录中的配置文件。如果要启用或禁用某项 Xinetd 服务,可以编辑位于 /etc/xinetd.d
目录中的配置文件。例如,disable
属性被设为 yes
,表示该项服务已禁用;disable
属性被设为 no
,表示该项服务已启用。参数和值之间的操作符可以是 =
、+=
或 -=
。所有属性可以使用 =
,其作用是分配一个或多个值。某些属性可以使用 +=
或 -=
,其作用分别是将其值增加到某个现存的值表中,或将其值从现存值表中删除。详细的配置参数说明如下:
配置参数 | 说明 |
---|---|
enabled | 是否启用该服务或服务列表 |
disabled | 是否停用该服务或服务列表 |
server | 启动脚本的位置 |
server_args | |
socket_type | 服务的数据包类型 |
log_type | 包括:日志类型、路径、报警最大容量、停止服务的最大容量 |
log_on_success | 成功后要将哪些值记录到日志中 |
log_on_failure | 失败后要将哪些值记录到日志中 |
only_from | 只有指定 IP 可以访问 |
no_access | 指定 IP 不可以访问 |
access_times | 允许连接的时间 |
user | 运行此服务进程的用户 |
wait | 服务将以多线程的方式运行 |
max_load | 系统最大负载系数 |
cps m n | 限制每秒 m 个入站连接,如果超过 m,则等待 n 秒,主要用于对付服务攻击 |
port | 连接的端口 |
nice | |
protocol | 连接使用的协议 |
instances | 最大连接进程数 |
per_source | 限制每个主机的最大连接数 |
bind | |
mdns | |
v6only | |
passenv | |
groups | |
umask | |
banner | |
banner_fail | |
banner_success | |
rlimit_as | 最多可用内存 |
rlimit_cpu | CPU 每秒最多处理的进程数 |
1 | # 安装 |
1 | # 启动 |
本文主要介绍如何在 Debian 系统上设置时区与同步时间,适用于 Debian 11 Bullseye、Debian 12 Bookworm 发行版。
一般全自动安装好的 Debian 是 UTC 时间,与北京时间差 8 小时,所以最好将时区设置为常用的时区,这样方便使用与阅读。timedatectl
是一个新工具,它作为 systemd
系统和服务管理器的一部分,代替旧的传统的用在基于 Linux 分布式系统的 sysvinit
守护进程的 date
命令。timedatectl
命令可以查询和更改系统时钟和设置,可以使用此命令来设置或更改当前的日期、时间和时区,或实现与远程 NTP 服务器的自动系统时钟同步。
1 | timedatectl list-timezones |
1 | sudo timedatectl set-timezone "Asia/Shanghai" |
1 | timedatectl status |
一般重新设置系统时区后,现实时间会与系统时间之间会有误差,这时候建议使用 systemd-timesyncd
相关工具来解决时间差异的问题。特别注意:timedatectl
并不兼容 ntpd
等组件,请不要安装 ntpd
等组件,以免时间同步失效。
systemd-timesyncd
服务 1 | sudo apt install systemd-timesyncd |
1 | sudo vim /etc/systemd/timesyncd.conf |
1 | [Time] |
systemd-timesyncd
服务 1 | sudo systemctl restart systemd-timesyncd |
systemd-timesyncd
服务的运行状态 1 | sudo systemctl status systemd-timesyncd |
1 | sudo timedatectl set-ntp true |
1 | timedatectl status |
软件 | 版本 |
---|---|
Debian | 12 |
autoconf | 2.6.9 |
OpenSSL | 1.1.1 |
Erlang | 23.2 |
1 | sudo apt-get install -y build-essential perl unzip flex bison fop xsltproc unixodbc libssl-dev unixodbc-dev libncurses5-dev libgl1-mesa-dev libglu1-mesa-dev libxml2-utils |
特别注意
这里必须安装 2.69
版本的 autoconf
,否则 Erlang 23.2
在编译前的配置操作会执行失败。
1 | # 卸载已安装的版本 |
特别注意
这里必须安装 1.1.1
版本的 OpenSSL,否则 Erlang 23.2
会编译失败。
1 | # 下载 |
提示
wxWidgets
没有安装的警告信息,可以忽略该警告。wxWidegts
组件(GUI),因此下面使用了 --without-wx
命令行参数进行配置。wxWidegts
组件(GUI),则需要安装 libwxgtk3.0-gtk3-dev
、libwxgtk-webview3.0-gtk3-dev
软件包,目前 Debian 12 的软件仓库里并没有这两个软件包,可能需要使用源码进行编译安装。1 | # 创建安装目录 |
1 | # 配置环境变量 |
Erlang 在安装过程中使用 fop
来生成 PDF 文档,而 fop
依赖了 OpenJDK,因此在上面安装依赖的步骤里,默认已经安装了最新版本的 OpenJDK。若希望在 Erlang 编译安装成功后卸载 OpenDJK,可以使用以下命令:
1 | # 查看已安装的OpenJDK版本 |
本文主要介绍 Deian 11 如何安装 Percona XtraDB Cluster 8.0 集群(三个物理节点),并基于 Haproxy + Keepalived (两个物理节点)实现双机热备方案。
软件 | 版本 | 描述 |
---|---|---|
Haproxy | 1.5.18 | |
Keepalived | 1.3.5 | |
Percona XtraDB Cluster (PXC) | 8.0 |
节点名称 | 主机名 | IP | 系统 | 说明 |
---|---|---|---|---|
PXC 节点一 | pxc-node-1 | 192.168.1.188 | Debian 11 (Bullseye) | Percona XtraDB Cluster (PXC) |
PXC 节点二 | pxc-node-2 | 192.168.1.193 | Debian 11 (Bullseye) | Percona XtraDB Cluster (PXC) |
PXC 节点三 | pxc-node-3 | 192.168.1.223 | Debian 11 (Bullseye) | Percona XtraDB Cluster (PXC) |
Haproxy 节点一 | haproxy-node-1 | 192.168.1.235 | Debian 11 (Bullseye) | Haproxy + Keepalived |
Haproxy 节点二 | haproxy-node-2 | 192.168.1.239 | Debian 11 (Bullseye) | Haproxy + Keepalived |
提示
在每个节点服务器上编辑 /etc/hosts
文件,加入以下内容:
1 | 192.168.1.188 pxc-node-1 |
在每个节点服务器上设置主机名。若由于其他限制导致不允许更改主机名,则可以跳过以下步骤。
1 | # 查看主机名 |
在配置系统防火墙之前,在每个服务器上分别安装 UFW 防火墙工具。
1 | # 安装ufw |
在本节中,将在每个 PXC 集群节点服务器上使用 UFW 配置防火墙,使 PXC 集群节点之间可以互相通信。
PXC 集群默认会使用到以下端口
3306
4444
4567
4568
基于 UFW 配置防火墙
1 | # 开放 PXC 端口,192.168.1.0/24 是所有 PXC 集群节点的子网地址 |
1 | # 查看ufw的运行状态 |
1 | Status: active |
在本节中,将在每个 Haproxy 节点服务器上使用 UFW 配置防火墙,使外部可以正常访问 Haproxy。
Haproxy 默认会使用到以下端口
3306
:Haproxy 代理 MySQL 的端口8888
:Haproxy 监控页面的 Web 端口基于 UFW 配置防火墙
1 | # 开放 Haproxy 端口,192.168.1.0/24 是所有 Haproxy 节点的子网地址 |
1 | # 查看ufw的运行状态 |
1 | Status: active |
在本节中,将在每个 Keepalived 节点服务器上使用 UFW 配置防火墙,使 Keepalived 节点之间可以互相进行心跳通信。
Keepalived 心跳通信
112
)224.0.0.18
基于 UFW 配置防火墙
1 | # 开放 VRRP 广播地址,enp0s3 是网卡接口,192.168.1.0/24 是所有 Keepalived 节点的子网地址 |
1 | Status: active |
在本节中,将在每个节点服务器上永久关闭 SeLinux,保证 PXC 集群节点可以互相通信。值得一提的是,如果系统没有安装 SeLinux,则可以跳过以下步骤。
1 | # 编辑配置文件 |
SELINUX
的值设置为 disabled
1 | # This file controls the state of SELinux on the system. |
在本节中,将在每个节点服务器上更改系统的最大打开文件描述符数,且更改永久生效。
提示
/lib/systemd/system/mysql.service
和 /lib/systemd/system/mysql@.service
中,默认都已经配置了 LimitNOFILE=16364
。1 | ulimit -n |
1 | # 第一步 |
1 | ulimit -n |
检查每个节点服务器上的 Debian 系统是否已经安装过 MySQL、MariaDB、Percona Server 数据库,如果安装过请先卸载掉,然后再安装 PXC 集群。
检查每个节点服务器上的交换分区大小,如果 Debian 系统没有交换分区,则建议手动创建并挂载,详细教程请看 这里。
在本节中,将在每个节点服务器上,基于 Debian 11 添加并设置 Percona XtraDB Cluster 存储库,然后安装 Percona XtraDB Cluster 软件包。此外,在安装过程中,系统会提示设置 MySQL 的 root
账号的密码,并为 Percona XtraDB Cluster 设置默认身份验证插件。最后,将通过配置的 root
用户和密码登录数据库,验证 Percona XtraDB Cluster 的安装。值得一提的,PXC 集群的部署规划如下表所示:
节点名称 | 主机名 | IP | 系统 | 说明 |
---|---|---|---|---|
PXC 节点一 | pxc-node-1 | 192.168.1.188 | Debian 11 (Bullseye) | Percona XtraDB Cluster (PXC) |
PXC 节点二 | pxc-node-2 | 192.168.1.193 | Debian 11 (Bullseye) | Percona XtraDB Cluster (PXC) |
PXC 节点三 | pxc-node-3 | 192.168.1.223 | Debian 11 (Bullseye) | Percona XtraDB Cluster (PXC) |
1 | # 安装依赖 |
1 | # 下载存储库包 |
1 | sudo percona-release setup pxc80 |
1 | # 安装PXC软件包 |
root
账号的密码(强)root
账号的密码(强)1 | # 启动服务 |
在每个节点服务器上执行以下命令,然后输入 root
账号的密码,若能成功登录 MySQL,则说明数据库正常运行。
1 | # 登录MySQL |
特别注意,在开始配置 Percona XtraDB Cluster 集群之前,必须确保所有节点上的 MySQL 服务器都已停止运行。
1 | # 关闭服务 |
1 | # 关闭服务 |
Percona XtraDB Cluster 有两种流量加密:客户端 / 服务器连接和复制流量。在最新的 Percona XtraDB Cluster 8.0 上,默认情况下会启用所有复制流量的加密以增强安全性。因此在创建和设置 Percona XtraDB Cluster 时,所有服务器都必须具有相同的 CA 和 Server 证书,即必须将默认的 CA 和 Server 证书从 pxc-node-1
节点拷贝到 pxc-node-2
节点和 pxc-node-3
节点。
/var/lib/mysql
下自动生成了,包括 Client、Server、CA 三种类型的证书 1 | # 查看SSL/TLC证书列表 |
1 | /var/lib/mysql/ca.pem |
pxc-node-1
节点上,拷贝 CA 和 Server 证书到 pxc-node-2
节点和 pxc-node-3
节点。1 | # 进入证书目录 |
在本节中,将在第一个节点服务器 pxc-node-1
上初始化 Percona XtraDB Cluster 集群(即引导 PXC 集群启动)。请确保以下步骤都是在节点一服务器上执行。
1 | # 备份配置文件 |
1 | [client] |
1 | 核心参数说明: |
1 | # 启动节点一的MySQL服务(初始化集群) |
特别注意
mysql@bootstrap
是一个用于运行 Percona XtraDB Cluster 的 Systemd 服务,这与普通的 mysql
服务有本质的区别,主要用于 PXC 集群的初始化(即引导 PXC 集群启动)。mysql@bootstrap
服务进行管理,包括启动、关闭、重启、查看状态等操作(如下所示),而其他节点则可以直接使用普通的 mysql
服务进行管理。systemctl start mysql@bootstrap.service
。systemctl stop mysql@bootstrap.service
。systemctl restart mysql@bootstrap.service
。systemctl status mysql@bootstrap.service
。1 | # 登录MySQL |
1 | # 查看集群状态 |
提示
当 PXC 集群初始化成功后,应该可以看到下述的状态信息。wsrep_cluster_size
的值是 1
,这意味着 Percona XtraDB Cluster 是用一台服务器初始化的。wsrep_incoming_address
的值是节点一服务器的 IP 地址。最后,节点处于 Synced
状态,这意味着它已完全连接并准备好进行写集复制。
1 | # 备份配置文件 |
1 | [client] |
特别注意
server-id
、wsrep_node_address
、wsrep_node_name
这三个参数必须跟其他节点一、节点三不一样。
1 | # 启动MySQL服务 |
1 | # 登录MySQL |
1 | # 查看集群状态 |
1 | # 备份配置文件 |
1 | [client] |
特别注意
server-id
、wsrep_node_address
、wsrep_node_name
这三个参数必须跟其他节点一、节点二不一样。
1 | # 启动MySQL服务 |
1 | # 登录MySQL |
1 | # 查看集群状态 |
在本节中,将对 PXC 集群的复制进行测试,包括创建数据库和表、插入数据。
percona
数据库 1 | # 创建数据库 |
1 | # 查看数据库列表 |
example
数据库表 1 | # 切换数据库 |
example
数据库表插入数据 1 | # 切换数据库 |
example
表的数据 1 | # 切换数据库 |
登录任意一个 PXC 集群节点的数据库,执行以下命令创建新用户,并授权用户可以远程访问指定的数据库。
1 | # 登录MySQL |
1 | # 创建用户 |
提示
这里为了兼容 MySQL 5 的认证方式,建议在创建数据库用户时,指定加密规则为 mysql_native_password
。值得一提的是,MySQL 8 默认使用的加密规则是 caching_sha2_password
。
在检测 PXC 集群各指标之前,先登录任意一个 PXC 集群节点的数据库,然后再执行其他操作。
1 | # 登录MySQL |
1 | show global status where variable_name in ('wsrep_cluster_state_uuid','wsrep_cluster_conf_id','wsrep_cluster_size','wsrep_cluster_status'); |
1 | +--------------------------+--------------------------------------+ |
特别注意,在正常情况下以下指标值,在所有节点应该都是一致的。
指标 | 说明 |
---|---|
wsrep_cluster_state_uuid | 在集群所有节点中该值应该是相同的,若有不同值,说明该节点没有连入集群。 |
wsrep_cluster_conf_id | 在集群所有节点中该值应该是相同的,若有不同值,说明该节点被临时 分区 了,当节点之间网络连接恢复后,该值应该恢复成一致。 |
wsrep_cluster_size | 如果与集群中的节点数一致,说明所有节点已经连接。 |
wsrep_cluster_status | 集群状态,若不为 Primary ,说明出现 分区 或是 split-brain 状况。 |
1 | show global status where variable_name in ('wsrep_ready','wsrep_connected','wsrep_local_state_comment'); |
1 | +---------------------------+--------+ |
特别注意,在正常情况下以下指标值,在所有节点应该都是一致的。
指标 | 说明 |
---|---|
wsrep_ready | 该值为 ON ,则说明可以接受 SQL 负载;如果为 OFF ,则需要检查 wsrep_connected 。 |
wsrep_connected | 如果该值为 OFF ,且 wsrep_ready 的值也为 OFF ,则说明该节点没有连入集群,可能是 wsrep_cluster_address 或 wsrep_cluster_name 等配置错误造成的,具体需要排查 MySQL 的错误日志。 |
wsrep_local_state_comment | 若 wsrep_connected 为 ON ,但 wsrep_ready 为 OFF ,则可以从该项查找错误原因。 |
1 | show global status where variable_name in ('wsrep_flow_control_paused','wsrep_cert_deps_distance','wsrep_flow_control_sent','wsrep_local_recv_queue_avg'); |
1 | +----------------------------+-------+ |
指标 | 说明 |
---|---|
wsrep_flow_control_paused | 表示数据复制停止了多长时间(即因 Slave 延迟而慢的程度,取值范围为 0 ~ 1,越靠近 0 越好,值为 1 表示数据复制完全停止(停止广播),可优化 wsrep_slave_threads 的值来改善)。 |
wsrep_cert_deps_distance | 表示有多少事务可以并行应用处理,wsrep_slave_threads 设置的值不应该高出该值太多。 |
wsrep_flow_control_sent | 表示该节点已经停止复制了多少次。 |
wsrep_local_recv_queue_avg | 表示 Slave 事务队列的平均长度,可作为 Slave 瓶颈的预兆。 |
在本节中,将使用 Haproxy + Keepalived 实现双机热备方案,其中 Haproxy 负责将请求转发给 PXC 集群各个节点。值得一提的,Haproxy 与 Keepalived 的部署规划如下表所示:
节点名称 | 主机名 | IP | 系统 | 说明 |
---|---|---|---|---|
Haproxy 节点一 | haproxy-node-1 | 192.168.1.235 | Debian 11 (Bullseye) | Haproxy + Keepalived |
Haproxy 节点二 | haproxy-node-2 | 192.168.1.239 | Debian 11 (Bullseye) | Haproxy + Keepalived |
在本节中,将创建 MySQL 用户,Haproxy 后续会使用这个用户对 PXC 集群节点进行心跳检测。
1 | # 登录MySQL |
haproxy
,不指定密码和权限,只允许远程访问 1 | # 创建用户 |
特别注意
MySQL 8.0 默认使用的加密规则是 caching_sha2_password
,为了让 Haproxy 可以对 PXC 集群节点进行心跳检测,在创建数据库用户时,必须指定加密规则为 mysql_native_password
,否则 Haproxy 无法正常检测 PXC 集群节点的运行状态。
在本节中,将在两个单独的服务器节点上分别安装 Haproxy 服务(两个节点的安装步骤和配置内容基本一致),实现对 PXC 集群的负载均衡。
1 | # 安装 |
1 | # 备份Haproxy的配置文件 |
1 | global |
Haproxy 配置说明
bind 0.0.0.0:8888
: 8888
端口用于 Haproxy 提供监控界面的 Web 服务。bind 0.0.0.0:3306
: 3306
端口用于 Haproxy 转发请求给 PXC 集群节点。option mysql-check user haproxy
: 指定 Haproxy 使用 MySQL 的 haproxy
账户对 PXC 集群节点进行心跳检测。1 | # 重启 |
使用浏览器访问不同节点的 Haproxy 监控页面(如下图所示),登录用户名是 admin
,登录密码是 admin
。如果可以正常访问 Haproxy 的监控界面,则说明 Haproxy 成功部署。
http://192.168.1.235:8888/dbs
http://192.168.1.239:8888/dbs
在本节中,将在两个单独的 Haproxy 服务器节点上分别安装 Keepalived 服务,实现 Haproxy + Keepalived 的双机热备方案。
提示
Keepalived 安装完成后,默认会将配置模板文件存放在 /usr/share/doc/keepalived/samples/
目录下。
1 | # 安装 |
1 | # 创建或编辑Keepalived的配置文件,写入以下配置内容(请自行更改网卡设备参数) |
1 | vrrp_instance VI_1 { |
1 | # 启动 |
1 | # 安装 |
1 | # 创建或编辑Keepalived的配置文件,写入以下配置内容(请自行更改网卡设备参数) |
1 | vrrp_instance VI_1 { |
1 | # 启动 |
192.168.1.173
正常访问 Haproxy 的 8888
与 3306
端口,则说明 PXC + Haproxy + Keepalived 的高可用集群搭建成功。测试内容 | 虚拟 IP | 端口 | 测试命令 |
---|---|---|---|
Haproxy 的监控页面 | 192.168.1.173 | 8888 | curl -basic -u admin:admin -I http://192.168.1.173:8888/dbs |
Haproxy 的 MySQL 负载均衡 | 192.168.1.173 | 3306 | mysql -h 192.168.1.173 -u uatOption -P 3306 -p |
1 | # 查看IP地址 |
http://192.168.1.173:8888/dbs
。如果可以正常访问 Haproxy 的监控页面,则说明 Haproxy + Keepalived 的双机热备方案生效了。1 | # 关闭Keepalived服务 |
ip addr
命令查看 IP 地址,可以观察到 Haproxy 节点二服务器已经抢占到虚拟 IP。1 | # 查看IP地址 |
第一个问题:上述两个 Haproxy 服务器内的 Keepalived 服务,彼此仅仅是基于心跳检测来实现双机热备(故障切换)。如果第一个 Haproxy 服务器内的 Keepalived 服务(Master)正常运行,而 Haproxy 自身运行异常,那么将会出现 Haproxy 负载均衡服务失效,无法切换到备用的 Haproxy 负载均衡器上,最终导致后端的 Web 服务无法收到响应。所以,应该是要基于 Shell 脚本每隔一段时间检测 Haproxy 服务是否正常运行,而不是仅仅依靠 Keepalived 主备节点之间的心跳检测。比如,当检测到 Haproxy 服务不是正常运行,首先尝试启动 Haproxy 服务;若 Haproxy 服务重启失败,就应该关闭掉该节点上的 Keepalived 服务,并发送报警邮件,这样才能自动切换到 Keepalived 服务的 Backup 节点上。详细的解决方案建议参考 这里 的教程。
第二个问题:Haproxy 代理 MySQL 的时候,事务持久性的问题必须解决。这个事务持久性不是 ACID 的 D(持久性,Durability),而是 Transaction Persistent,这里简单描述一下此处的事务持久性。
1 | start transaction |
当客户端显式开启一个事务,然后执行上述几个数据库操作,然后提交或回滚。如果使用代理软件(如 Haproxy)对 MySQL 进行代理,必须要保证这 5 个语句全都路由到同一个 MySQL 节点上,即使后端的 MySQL 采用的是多主模型(MGR、Galera 都提供多主模型),否则事务中各语句分散,轻则返回失败,重则数据不一致、提交混乱。这就是 Transaction Persistent 的概念,即让同一个事务路由到同一个后端节点。Haproxy 如何保证事务持久性呢?对于非 MySQL 协议感知的代理(LVS、Nginx、Haproxy 等),要保证事务持久性,只能通过间接的方法实现,比较通用的方法是在代理软件上监听不同的端口(实现读写分离)。具体的思路如下:
3307
端口的请求作为写端口,3306
端口的请求作为读端口。3307
端口的请求全都路由给这个节点。3307
端口在某一时刻,路由到的必须只能有一个写节点。这样能保证事务的持久性,也能解决一些乐观锁问题。但是,如果后端是多主模型的 MGR(组复制)或 Galera,这样的代理方式将强制变为单主模型,虽然是逻辑上的强制。当然,这并非什么问题,至少到目前为止的开源技术,都建议采用单主模型。Haproxy 保证事务持久性的配置示例如下:
1 | listen haproxy_3306_read_multi |
上面的配置通过 3306
端口和 3307
端口进行读写分离,并且在负责写的 3307
端口中只有一个节点可写,其余两个节点作为 Backup 节点。对于 MySQL 的负载来说,更建议采用 MySQL 协议感知的程序来实现,例如 MySQL Router、ProxySql,MaxScale、MyCat 等数据库中间件。
PXC 集群使用的是 Percona Server 数据库,它是 MySQL 的衍生版本,因此 PXC 集群节点的很多操作跟 MySQL 一样,其错误日志信息存放在每个节点的 /var/log/mysql/error.log
文件里面。
1 | # 查看错误日志文件 |
如果第一个节点(负责集群初始化)不是最后一个离开集群的,那么它在一般情况下就不能再以第一个节点的形式启动了。这是因为从这个节点引导集群启动可能是不安全的,即这个节点可能不包含所有更新的数据。综上所述,PXC 集群节点的正确关闭顺序,应该与它们的启动顺序相反(类似栈结构 - 先进后出),即最先启动的节点应该最后关闭。
1 | # 启动第一个节点 |
1 | # 关闭第三个节点 |
PXC 集群允许动态下线节点,但需要注意的是节点的启动命令和关闭命令必须一致;比如使用 mysql@bootstrap.service
服务启动的第一个节点服务器(负责集群初始化),在它关闭时也必须使用 mysql@bootstrap.service
服务来操作(该结论有待验证)。
1 | # 第一个节点下线 |
1 | # 其他节点下线 |
提示
由于 PXC 集群的所有节点都是对等的,所以下线第一个节点和下线其他节点在效果上都是相同的。
特别注意,当 PXC 集群中某个节点所在的 Debian 系统重启后,PXC 会自动将该节点的 MySQL 服务从集群中剔除(因为节点不可用了),以此保证高可用,但该节点的 MySQL 服务后续是不会自动启动的。也就是说,等 Debian 系统重启完成后,必须手动启动该节点上的 MySQL 服务。此时只要确保 PXC 集群中至少有一个节点存活着,那么就不需要再重新初始化 PXC 集群(引导 PXC 集群启动),因此无论等待重启的节点是第一个节点(负责集群初始化),还是其他节点,都可以直接使用 mysql
服务进行启动。
1 | # 重启意外关闭的节点 |
1 | # 启动第一个节点 |
1 | # 第一个节点启动的错误信息 |
1 | # 查看系统日志信息 |
1 | 2023-10-26T22:13:35.822653Z 0 [Note] [MY-000000] [Galera] ####### Assign initial position for certification: e4bb8bf0-73d5-11ee-b279-3a5ebcc11ef7:29, protocol version: -1 |
意思是从这个节点引导集群启动可能是不安全。由于该节点不是最后一个离开集群的节点(最后停掉的节点),可能不包含所有更新的数据。要强制使用该节点进行集群引导,请手动编辑该节点的 grastate.dat
文件,并将 safe_to_bootstrap
参数设置为 1
。当然了,一般情况下不需要强制从该节点启动,可以逐一排查每个节点下的 grastate.dat
文件,找到 safe_to_bootstrap=1
的节点,然后在该节点上引导 PXC 集群启动即可。如果所有节点的 safe_to_bootstrap
都为 0
,那么只能任意选择一个节点,更改该节点下的 grastate.dat
文件,将 safe_to_bootstrap
设置为 1
,然后在该节点上引导 PXC 集群启动。特别注意,引导 PXC 集群启动(第一个节点)使用的是 sudo systemctl start mysql@bootstrap.service
命令,而启动其他节点使用的则是 sudo systemctl start mysql
命令。必须等待第一个节点启动成功,也就是 PXC 集群初始化完成之后,才能接着启动其他节点,最后再检查集群的数据是否可以正常同步。
提示
grastate.dat
文件的完整路径是 /var/lib/mysql/grastate.dat
。sudo systemctl start mysql@bootstrap.service
,其他节点的 MySQL 服务启动命令则是 sudo systemctl start mysql
。本文主要介绍如何使用 Docker 部署 Percona XtraDB Cluster 8.0 集群(单机三个节点),并详细介绍集群可用性的验证。
提示
Percona Xtradb Cluster (PXC) 的详细介绍请看 这里 的教程。
本文主要介绍如何使用 Docker 部署 Percona XtraDB Cluster 5.7 集群(单机三个节点),并基于 Haproxy + Keepalived 实现双机热备方案。
提示
PXC + Haproxy + Keepalived 双机热备架构的介绍可以阅读 这里 的内容。
软件 | 版本 | 描述 |
---|---|---|
PXC 镜像 | 5.7.43 | |
Haproxy 镜像 | 2.8.3 | |
Keepalived 服务 | 1.3.5 |
节点名称 | 容器名称 | 容器 IP | 容器数据卷 | 容器数据卷目录 | 操作系统 |
---|---|---|---|---|---|
PXC 节点一 | pxc-node1 | 172.30.0.2 | pxc-v1 | /var/lib/docker/volumes/pxc-v1/_data/ | Debian 11 |
PXC 节点二 | pxc-node2 | 172.30.0.3 | pxc-v2 | /var/lib/docker/volumes/pxc-v2/_data/ | Debian 11 |
PXC 节点三 | pxc-node3 | 172.30.0.4 | pxc-v3 | /var/lib/docker/volumes/pxc-v3/_data/ | Debian 11 |
Haproxy 节点一 | haproxy-node1 | 172.30.0.5 | haproxy-v1 | /var/lib/docker/volumes/haproxy-v1/_data/ | Debian 11 |
Haproxy 节点二 | haproxy-node2 | 172.30.0.6 | haproxy-v2 | /var/lib/docker/volumes/haproxy-v2/_data/ | Debian 11 |
服务器名称 | 服务器角色 | 虚拟 IP | 说明 | 操作系统 |
---|---|---|---|---|
Keepalived 服务器一 | 主服务器(MASTER) | 172.30.0.7 | 安装在 Haproxy 节点一的容器内(haproxy-node1 ) | Debian 11 |
Keepalived 服务器二 | 备服务器(BACKUP) | 172.30.0.7 | 安装在 Haproxy 节点二的容器内(haproxy-node2 ) | Debian 11 |
Keepalived 服务器三 | 192.168.1.160 | 安装在宿主机内,为了实现外网可以正常访问 Docker 容器内的虚拟 IP | Centos 7 |
Linux 系统支持在一个网卡中定义多个 IP 地址,并将这些地址分配给多个应用程序,这些地址就是虚拟 IP,基于 Haproxy + Keepalived 的双机热备方案最关键的技术就是虚拟 IP。
Keepalived 利用了上述 Linux 系统的特性,让多台服务器去获取同一个虚拟 IP,获取到的服务器将虚拟 IP 绑定到自身的网卡,然后接受外部流量;没有抢占到虚拟 IP 的则作为备用服务器,并进行心跳检测,一旦检测到主服务器宕机,则立刻抢占虚拟 IP。
PXC 集群有三个节点,如果每次都是第一个节点处理请求,那么就存在负载高、性能差、其他节点利用率不高等问题,所以更优的方案是对不同的节点都进行请求。这就需要有负载均衡中间件负责请求转发,主流的中间件有 Nginx、Haproxy 等,两者都支持 TCP/IP 协议,Nginx 额外支持插件,Haproxy 属于是老牌的中间件。在数据库集群的负载均衡领域,Haproxy 会使用的要多一些。不同中间件的对比如下图所示:
在本节中,将通过 Docker 部署 PXC 5.7 集群,其中包含三个集群节点。
提示
Percona Xtradb Cluster (PXC) 的详细介绍请看 这里 的教程。
节点名称 | 容器名称 | 容器 IP | 容器数据卷 | 容器数据卷目录 | 操作系统 |
---|---|---|---|---|---|
PXC 节点一 | pxc-node1 | 172.30.0.2 | pxc-v1 | /var/lib/docker/volumes/pxc-v1/_data/ | Debian 11 |
PXC 节点二 | pxc-node2 | 172.30.0.3 | pxc-v2 | /var/lib/docker/volumes/pxc-v2/_data/ | Debian 11 |
PXC 节点三 | pxc-node3 | 172.30.0.4 | pxc-v3 | /var/lib/docker/volumes/pxc-v3/_data/ | Debian 11 |
提示
1 | # 拉取镜像 |
/var/lib/docker/volumes/
1 | # 创建数据卷 |
1 | # 创建网络 |
XTRABACKUP_PASSWORD
是 XtraBackup 工具备份数据库数据的密码 1 | # 创建第一个节点 |
1 | # 启动节点一 |
1 | # 启动节点二 |
1 | # 启动节点三 |
1 | sudo docker exec -it pxc-node1 /usr/bin/mysql -uroot -p123456 |
1 | show status like 'wsrep_cluster%'; |
1 | -- 创建数据库 |
在本节中,将介绍如何使用 Haproxy 作为负载均衡服务器,将请求转发给 PXC 集群中的各个节点。特别注意,在执行以下操作之前,请先启动 PXC 集群,并确保集群可以正常运行。
节点名称 | 容器名称 | 容器 IP | 容器数据卷 | 容器数据卷目录 | 操作系统 |
---|---|---|---|---|---|
Haproxy 节点一 | haproxy-node1 | 172.30.0.5 | haproxy-v1 | /var/lib/docker/volumes/haproxy-v1/_data/ | Debian 11 |
当使用 PXC 集群的单个节点处理所有请求时,存在负载高、性能差、其他节点利用率不高等问题。
使用 Haproxy 做负载均衡,将请求均匀地分配给 PXC 集群中的每一个节点,单节点负载低、性能高,且所有节点都能利用起来。
在本节中,将创建 MySQL 用户,Haproxy 后续会使用这个用户对 PXC 集群节点进行心跳检测。
1 | sudo docker exec -it pxc-node1 /usr/bin/mysql -uroot -p123456 |
haproxy
,不指定密码和权限,只允许远程访问 1 | # 创建用户 |
1 | # 拉取镜像 |
/var/lib/docker/volumes/
1 | # 创建数据卷 |
1 | # 在数据卷目录下创建Haproxy的配置文件 |
1 | global |
特别注意
在上述 Haproxy 的配置文件中,不能启用 chroot /usr/local/etc/haproxy
,否则 Haproxy 容器会启动失败(日志信息如下),暂时不清楚其原因。
1 | [NOTICE] (1) : New worker (8) forked |
8888
是 Haproxy 监听的 HTTP 端口(用于提供监控界面的 Web 服务),3306
是 Haproxy 监听的 MySQL 端口(用于转发请求给 PXC 集群节点)1 | # 创建并运行容器 |
在宿主机内使用浏览器访问 http://192.168.1.221:4001/dbs
,打开 Haproxy 的监控界面(如下图所示),登录用户名是 admin
,登录密码是 admin
。如果可以正常访问 Haproxy 的监控界面,则说明 Haproxy 成功部署。
提示
值得一提的是,上述的 192.168.1.221
是宿主机的 IP 地址,4001
是 Haproxy 容器映射的 HTTP 端口。
通过 Haproxy 容器映射的数据库代理端口 4402
登录 PXC 集群的 MySQL 节点。如果可以正常登录,则说明 Haproxy 成功将请求转发给 PXC 集群。
1 | mysql -h 192.168.1.221 -u root -P 4002 -p123456 |
提示
值得一提的是,上述的 192.168.1.221
是宿主机的 IP 地址,4002
是 Haproxy 容器映射的 MySQL 端口。
Haproxy 每隔一段时间会对 PXC 集群的节点进行心跳检测,当某个集群节点下线后,在 Haproxy 的监控界面可以观察到(如下图所示)。
提示
当 Haproxy 检测到有 PXC 集群节点处于不可用状态时,它会将该节点从负载均衡的服务器列表中剔除掉,直到该节点重新恢复到可用状态后。因此 Haproxy 可以做到一定程度的高可用,它的负载均衡跟 Nginx 有比较大的区别,后者默认不会剔除不用的服务器节点,而是会直接转发请求给故障节点。
1 | # 关闭节点二 |
在本节中,将会创建多一个 Haproxy 容器(最终 Haproxy 会有两个容器),并在宿主机和每个 Haproxy 容器内单独安装 Keepalived 服务器,以此实现 Haproxy + Keepalived 双机热备方案。特别注意,在执行以下操作之前,请先启动 PXC 集群,并确保集群可以正常运行。
节点名称 | 容器名称 | 容器 IP | 容器数据卷 | 容器数据卷目录 | 操作系统 |
---|---|---|---|---|---|
Haproxy 节点一 | haproxy-node1 | 172.30.0.5 | haproxy-v1 | /var/lib/docker/volumes/haproxy-v1/_data/ | Debian 11 |
Haproxy 节点二 | haproxy-node2 | 172.30.0.6 | haproxy-v2 | /var/lib/docker/volumes/haproxy-v2/_data/ | Debian 11 |
服务器名称 | 服务器角色 | 虚拟 IP | 说明 | 操作系统 |
---|---|---|---|---|
Keepalived 服务器一 | 主服务器(MASTER) | 172.30.0.7 | 安装在 Haproxy 节点一的容器内(haproxy-node1 ) | Debian 11 |
Keepalived 服务器二 | 备服务器(BACKUP) | 172.30.0.7 | 安装在 Haproxy 节点二的容器内(haproxy-node2 ) | Debian 11 |
Keepalived 服务器三 | 192.168.1.160 | 安装在宿主机内,为了实现外网可以正常访问 Docker 容器内的虚拟 IP | Centos 7 |
单节点的 Haproxy 不具备真正的高可用性,必须要有冗余设计,否则 Haproxy 宕机后,会造成整个集群不可用,如下图所示:
对 Haproxy 进行集群部署(两个节点),并使用 Keepalived 实现双机热备架构,当其中一个 Haproxy 节点宕机后,另一个 Haproxy 节点可以顶上,保证整个集群的可用性,如下图所示:
在本节中,将在上面创建第一个的 Haproxy 容器内,安装 Keepalived 服务器。
1 | # 连接Haproxy容器 |
1 | # 更新包索引 |
1 | # 创建Keepalived的配置文件 |
1 | vrrp_instance VI_1 { |
1 | service keepalived start |
1 | docker top haproxy-node1 |
1 | ping 172.30.0.7 |
在本节中,将另外多部署一个 Haproxy 容器,并在容器内安装 Keepalived 服务器。
/var/lib/docker/volumes/
1 | # 创建数据卷 |
1 | # 在数据卷目录下创建Haproxy的配置文件 |
1 | global |
特别注意
在上述 Haproxy 的配置文件中,不能启用 chroot /usr/local/etc/haproxy
,否则 Haproxy 容器会启动失败(日志信息如下),暂时不清楚其原因。
1 | [NOTICE] (1) : New worker (8) forked |
8888
是 Haproxy 监听的 HTTP 端口(用于提供监控界面的 Web 服务),3306
是 Haproxy 监听的 MySQL 端口(用于转发请求给 PXC 集群节点)1 | # 创建并运行容器 |
http://192.168.1.221:4003/dbs
,打开 Haproxy 的监控界面(如下图所示),登录用户名是 admin
,登录密码是 admin
。如果可以正常访问 Haproxy 的监控界面,则说明 Haproxy 成功部署。提示
值得一提的是,上述的 192.168.1.221
是宿主机的 IP 地址,4003
是 Haproxy 容器映射的 HTTP 端口。
1 | # 连接Haproxy容器 |
1 | # 更新包索引 |
1 | # 创建Keepalived的配置文件 |
1 | vrrp_instance VI_1 { |
1 | service keepalived start |
1 | docker top haproxy-node2 |
在本节中,将在宿主机内安装第三个 Keepalive 服务器,目的是为了实现外网可以正常访问 Docker 内的虚拟 IP。特别注意,Docker 内的虚拟 IP 默认是不能被外网访问的,所以需要借助宿主机的 Keepalived 映射成外网可以正常访问的虚拟 IP。
8888
和 3306
端口 1 | # 查看防火墙运行状态 |
1 | # 安装nmap |
1 | # 安装Keepalive |
1 | # 备份Keepalived的配置文件 |
1 | vrrp_instance VI_1 { |
1 | # 启动 |
192.168.1.160
正常访问宿主机 Haproxy 容器中的虚拟 IP 172.30.0.7
及相应端口,则说明 PXC + Haproxy + Keepalived 的高可用集群搭建成功。验证内容 | 宿主机的虚拟 IP | 宿主机的端口 | 验证命令 |
---|---|---|---|
Haproxy 的监控页面 | 192.168.1.160 | 8888 | curl -basic -u admin:admin -I http://192.168.1.160:8888/dbs |
Haproxy 的 MySQL 负载均衡 | 192.168.1.160 | 3306 | mysql -h 192.168.1.160 -u root -P 3306 -p123456 |
1 | # 使用root权限连接Haproxy容器一 |
1 | # 使用root权限连接Haproxy容器二 |
http://192.168.1.160:8888/dbs
,如果可以正常访问 Haproxy 监控页面,则说明 Haproxy + Keepalived 的双机热备方案生效了。1 | # 关闭Haproxy节点一的容器 |
ip addr
命令查看 IP 地址,可以看见已经抢占到的虚拟 IP1 | # 使用root权限连接容器 |
在本节中,将介绍上述操作完成之后,Haproxy + Keepalived 双机热备方案仍需要改进的地方。
目前集群里有两个 Keepalived 服务分别安装在不同的 Haproxy 容器内,但它们默认都没有配置自启动,也就是说 Keepalived 没有随 Haproxy 容器启动而启动。为了日后方便维护集群,建议将 Haproxy 容器内的 Keepalived 服务统一配置成自启动。可以尝试通过 Dockerfile 自主构建包含有 Haproxy + Keepalived 的 Docker 镜像。由于篇幅有限,这里不再累述。
Haproxy 代理 MySQL 的时候,事务持久性的问题必须解决。这个事务持久性不是 ACID 的 D(持久性,Durability),而是 Transaction Persistent,这里简单描述一下此处的事务持久性。
1 | start transaction |
当客户端显式开启一个事务,然后执行上述几个数据库操作,然后提交或回滚。如果使用代理软件(如 Haproxy)对 MySQL 进行代理,必须要保证这 5 个语句全都路由到同一个 MySQL 节点上,即使后端的 MySQL 采用的是多主模型(MGR、Galera 都提供多主模型),否则事务中各语句分散,轻则返回失败,重则数据不一致、提交混乱。这就是 Transaction Persistent 的概念,即让同一个事务路由到同一个后端节点。Haproxy 如何保证事务持久性呢?对于非 MySQL 协议感知的代理(LVS、Nginx、Haproxy 等),要保证事务持久性,只能通过间接的方法实现,比较通用的方法是在代理软件上监听不同的端口(实现读写分离)。具体的思路如下:
3307
端口的请求作为写端口,3306
端口的请求作为读端口。3307
端口的请求全都路由给这个节点。3307
端口在某一时刻,路由到的必须只能有一个写节点。这样能保证事务的持久性,也能解决一些乐观锁问题。但是,如果后端是多主模型的 MGR(组复制)或 Galera,这样的代理方式将强制变为单主模型,虽然是逻辑上的强制。当然,这并非什么问题,至少到目前为止的开源技术,都建议采用单主模型。Haproxy 保证事务持久性的配置示例如下:
1 | listen haproxy_3306_read_multi |
上面的配置通过 3306
端口和 3307
端口进行读写分离,并且在负责写的 3307
端口中只有一个节点可写,其余两个节点作为 Backup 节点。对于 MySQL 的负载来说,更建议采用 MySQL 协议感知的程序来实现,例如 MySQL Router、ProxySql,MaxScale、MyCat 等数据库中间件。
上述两个 Haproxy 容器内的 Keepalived 服务,彼此仅仅是基于心跳检测来实现双机热备(故障切换)。如果第一个 Haproxy 容器内的 Keepalived 服务(Master)正常运行,而 Haproxy 自身运行异常,那么将会出现 Haproxy 负载均衡服务失效,无法切换到备用的 Haproxy 负载均衡器上,最终导致后端的 Web 服务无法收到响应。所以,应该是要基于 Shell 脚本每隔一段时间检测 Haproxy 服务是否正常运行,而不是仅仅依靠 Keepalived 主备节点之间的心跳检测。比如,当检测到 Haproxy 服务不是正常运行,首先尝试启动 Haproxy 服务;若 Haproxy 服务重启失败,就应该关闭掉该节点上的 Keepalived 服务,并发送报警邮件,这样才能自动切换到 Keepalived 服务的 Backup 节点上。详细的解决方案建议参考 这里 的教程。
冷备份
热备份
LVM
和 XtraBackup
两种方案。LVM
热备份方案XtraBackup
热备份方案全量备份与增量备份
全量备份
:备份全部数据。备份过程时间长,占用空间大。第一次备份要使用全量备份。增量备份
: 只备份变化的那部分数据。备份的时间短,占用空间小。第二次以后可以使用增量备份1 | # 创建数据卷,用于存储备份数据 |
pxc-node2
,将其容器关闭并删除掉,然后重新创建一个挂载了 backup
数据卷的 pxc-node2
容器 1 | # 关闭容器 |
pxc-node2
容器中安装 xtrabackup
工具 1 | # 启动容器 |
1 | # 安装存储库包 |
1 | # 创建存放备份数据的目录 |
数据库可以热备份,但是不能热还原,否则会造成业务数据和还原数据的冲突。针对 PXC 集群,为了避免在还原过程中可能出现各节点数据同步冲突的问题,需要先解散原来的集群(删除所有集群节点),然后重新创建空白数据库节点,再执行数据库冷还原操作,最后再创建其他集群节点。还原前还要将热备份保存的未提交的事务回滚,还原之后重启 MySQL 服务器。
1 | # 关闭容器 |
pxc-node1
,并进入容器内,执行冷还原操作 1 | # 创建第一个节点的数据卷 |
1 | # 准备还原 |
1 | # 关闭第一个节点的容器 |
1 | # 创建第二个节点的数据卷 |
1 | # 创建第二个节点(增加了CLUSTER_JOIN参数) |
1 | # 启动第二节点(首次启动比较耗时间,因为需要同步第一个节点的数据) |
特别注意
对于 PXC 集群节点的启动、关闭等操作,区分第一个节点(负责集群初始化)和其他节点,两者的操作步骤是不同的。
如果第一个节点(负责集群初始化)不是最后一个离开集群的,那么它在一般情况下就不能再以第一个节点的形式启动了。这是因为从这个节点引导集群启动可能是不安全的,即这个节点可能不包含所有更新的数据。综上所述,PXC 集群节点的正确关闭顺序,应该与它们的启动顺序相反(类似栈结构 - 先进后出),即最先启动的节点应该最后关闭。
1 | # 关闭节点三 |
1 | # 启动节点一 |
如果是希望 PXC 集群关闭某个节点(非第一个节点),正确的步骤如下:
1 | # 关闭节点 |
某个节点(非第一个节点)关闭之后,其他维护工作也完成了,若希望将该节点重新加入 PXC 集群,可以执行以下命令:
1 | # 启动节点 |
如果第一个节点(负责集群初始化)不是最后一个离开集群的,不能再以第一个节点的形式启动了。
sudo docker start pxc-node1
命令,会发现第一个节点的容器没有正常启动grastate.dat
文件,并找到 safe_to_bootstrap=1
的节点safe_to_bootstrap=1
的节点,且它不是第一个节点(负责集群初始化)safe_to_bootstrap=1
的节点,且它是第一个节点(负责集群初始化),或者根本找不到 safe_to_bootstrap=1
的节点sudo docker volume inspect pxc-v1
得到第一个节点的数据卷目录路径,找到数据卷目录下的 grastate.dat
文件grastate.dat
文件,将 safe_to_bootstrap
设置为 1
sudo docker start pxc-node1
命令强制从第一个节点启动sudo docker stop pxc-node1
、sudo docker rm pxc-node1
sudo docker create -p 13306:3306 -v pxc-v1:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -e XTRABACKUP_PASSWORD=123456 -e CLUSTER_NAME=pxc -e CLUSTER_JOIN=pxc-node3 --name=pxc-node1 --net=pxc-network --ip 172.30.0.2 pxc
-e CLUSTER_JOIN=pxc-node3
,这里的 pxc-node3
是集群中存活着的节点三sudo docker start pxc-node1
Docker 所在的 Windows/Linux 操作系统重启后,导致所有 PXC 集群节点都意外关闭了,此时选择 PXC 集群中的第一个节点容器进行重启,出现以下的错误信息:
1 | [ERROR] WSREP: It may not be safe to bootstrap the cluster from this node. It was not the last one to leave the cluster and may not contain all the updates. To force cluster bootstrap with this node, edit the grastate.dat file manually and set safe_to_bootstrap to 1 . |
grastate.dat
文件,设置 safe_to_bootstrap=1
。grastate.dat
文件,找到 safe_to_bootstrap=1
的节点。safe_to_bootstrap=1
的节点是第一个节点,那么可以直接在该节点上引导 PXC 集群启动,然后再启动其他节点。safe_to_bootstrap=1
的节点不是第一个节点,此时为了方便操作,建议先将所有节点容器关闭并删除掉,然后再按照 上面的步骤,重新创建并启动每一个 PXC 集群节点容器。safe_to_bootstrap
都为 0
,那么只能任意选择一个节点,更改该节点下的 grastate.dat
文件,将 safe_to_bootstrap
设置为 1
,然后在该节点上引导 PXC 集群启动,最后再启动其他节点。提示
grastate.dat
文件的路径是 /var/lib/docker/volumes/xxxx/_data/grastate.dat
,其中的 xxx
是容器数据卷的目录名称。假设由于各种原因导致整个 PXC 集群无法正常启动,此时可以将所有节点容器关闭并删除掉(必须确保数据卷目录的数据不被误删,否则会丢失所有数据库数据),然后再按照 上面的步骤,重新创建并启动每一个 PXC 集群节点容器。
数据可靠性方案 | 说明 |
---|---|
RAID 10 | 适用于对数据冗余性和性能要求较高的应用场景,如数据库服务器、虚拟化环境和高性能计算等。 |
SAN 存储网络 | 除了价格贵,没有太多缺点。 |
DRBD 磁盘复制 | Linux 内核模块实现的磁盘块级别的同步复制技术。 |
RAID 10(Redundant Array of Independent Disks 10)是一种存储方案,它结合了 RAID 1(镜像)和 RAID 0(条带化)的特性。RAID 10 通过将多个磁盘组合在一起,提供了数据冗余和性能增强的优势。在 RAID 10 中,磁盘被分为两组,每组至少有两个磁盘。其中一组磁盘使用 RAID 1(镜像)技术,即数据被同时写入两个磁盘,提供了数据的冗余备份。另一组磁盘使用 RAID 0(条带化)技术,即数据被分块地写入多个磁盘,提供了更好的读写性能。
提示
优点
数据冗余
:RAID 10 通过镜像技术提供了数据的冗余备份。如果一个磁盘发生故障,数据仍然可以从镜像磁盘中恢复,保证了数据的可靠性和可用性。高性能
:RAID 10 通过条带化技术提供了更好的读写性能。数据可以同时从多个磁盘读取或写入,提高了数据访问的速度和吞吐量。故障容忍
:由于 RAID 10 具有数据冗余性,当一个磁盘发生故障时,系统可以继续正常运行,并且可以在更换故障磁盘后进行数据恢复,减少了系统停机时间。缺点
成本较高
:由于 RAID 10 需要使用多个磁盘进行数据镜像和条带化,所以成本较高。相比其他 RAID 级别,RAID 10 需要更多的磁盘。容量利用率较低
:RAID 10 的容量利用率较低,因为数据被同时写入两个磁盘。例如,如果有 4 个 1TB 的磁盘组成 RAID 10,实际可用的存储容量只有 2TB。SAN(Storage Area Network)是一种专门用于存储数据的高速网络架构。它将存储设备(如磁盘阵列、磁带库等)与服务器连接起来,提供高性能、高可用性和可扩展性的共享存储解决方案。
提示
优点
存储共享
:SAN 允许多台服务器共享存储设备,使得数据可以在不同的服务器之间共享和访问。这样可以提高数据的灵活性和共享性,减少存储资源的浪费。高性能
:SAN 使用高速的网络连接(如光纤通道、以太网等),提供了高带宽和低延迟的数据传输。这使得存储设备可以提供更高的读写性能,满足对存储性能要求较高的应用场景。可扩展性
:SAN 具有良好的可扩展性,可以根据需求灵活地扩展存储容量和性能。通过添加新的存储设备或扩展现有设备的容量,可以满足不断增长的存储需求。管理简便
:SAN 提供了集中管理和监控的功能,使得存储资源的配置、监控和管理变得更加简便和高效。管理员可以通过集中的管理界面对存储设备进行配置和管理,提高了管理效率。数据强一致性
:可以很好地保证数据的强一致性,不会因为 MySQL 的逻辑错误发生数据不一致的情况。部署简单
:部署两节点即可,不依赖数据库实现,保障数据安全。缺点
不具备故障转移
:需要考虑共享存储的高可用性。成本较高
:相比于其他存储解决方案,SAN 的成本较高。它需要专用的硬件设备和高速网络连接,这增加了部署和维护的成本。配置复杂性
:SAN 的配置和管理相对复杂,需要专业的知识和技能。对于不熟悉 SAN 的用户来说,配置和管理可能是一项具有挑战性的任务。DRBD(Distrubuted Replicated Block Device)是一种构建高可用分布式网络存储解决方案的专业工具(由 Linux 内核提供),可用于对服务器之间的磁盘、分区、逻辑卷等进行数据同步。当用户将数据写入本地磁盘时,会将数据发送到网络中另一台主机的磁盘上,这样的本地主机(主节点)与远程主机(备节点)的数据就可以保证实时同步。DRBD 主要用于数据传输、复制和同步,可以在网络存储节点之间实现可靠性更高的数据备份,也常用于构建高可用的存储节点及其他组件,如集群、负载均衡和存储服务器等。
DRBD 结合 MySQL 使用
DRBD 与 MySQL 结合使用可以实现高可用性的数据库方案。通过将 MySQL 数据库的数据目录配置为 DRBD 设备,加上额外的配置和管理工具(如 Pacemaker),可以为数据库提供实时复制和故障转移的能力,从而提高数据库的可靠性和可用性。当主节点发生故障时,系统可以自动切换到备节点,减少数据库服务的中断时间。DRBD 经典架构的组合是 MySQL + DRBD + Heartbeat。
在 MySQL 与 DRBD 方案中,通常会有两个节点:一个主节点和一个备节点。主节点负责处理所有的读写操作,并将数据实时复制到备节点上。备节点会持续地从主节点复制数据,以保持数据的一致性。当主节点发生故障时,备节点可以接管主节点的角色,成为新的主节点,继续提供数据库服务。这种故障转移过程是自动的,可以通过配置和管理工具(如 Pacemaker)来实现。需要注意的是,配置和管理 MySQL 与 DRBD 方案需要一定的技术知识和经验。此外,对网络的稳定性和带宽要求较高,以确保数据的实时复制和同步。因此,在实施该方案之前,建议进行充分的规划和测试,以确保系统的稳定性和可靠性。
优点
缺点
高可用方案 | 保证数据强一致性 | 使用说明 | 描述 |
---|---|---|---|
主从复制 | 否 | 支持单主 | 只适用于对可用性和数据一致性要求较低的业务场景。 |
MMM | 否 | 支持单主 | 基本淘汰了,在一致性和高并发稳定性等方面有些问题。 |
MHA | 否 | 支持单主 | 有少数开发者还在用,但也有些问题,也是趋于淘汰的 MySQL 主从高可用方案。 |
MGR | 是 | 支持单主 / 多主 | 基于 MySQL 官方从 5.7.17 版本开始引入的组复制技术。 |
MySQL Cluster | 是 | 支持多主 | MySQL 官方提供的一种分布式数据库解决方案,只支持 NDB 引擎。 |
Galera Cluster | 是 | 支持多主 | 引领时代的主从复制高可用技术。 |
Galera Cluster for MySQL | 是 | 支持多主 | MySQL 对 Galera Cluster 的实现。 |
MariaDB Galera Cluster (MGC) | 是 | 支持多主 | MariaDB 对 Galera Cluster 的实现。 |
Percona XtraDB Cluster (PXC) | 是 | 支持多主 | Percona 对 Galera Cluster 的实现,目前业界使用 PXC 的会多一些。 |
MySQL InnoDB Cluster | 是 | 支持单主 / 多主 | MySQL 官方推出的一套完整高可用性解决方案。 |
Galera Cluster 是由 Codership 开源的一套基于同步多主复制的 MySQL 集群解决方案。目前 Galera Cluster 有三种版本(实现方案),分别是 Galera Cluster for MySQL、MariaDB Galera Cluster (MGC) 及 Percona Xtradb Cluster (PXC)。Galera Cluster 使用 Galera Replication 插件,通过在多个 MySQL 节点之间同步数据来实现高可用性和负载均衡。其本身具有 Multi-Master (多主) 特性,支持多点写入(所有节点都可以同时读写数据库),Galera Cluster 中每个实例都是对等的,互为主从。当客户端读写数据的时候,可以选择任一 MySQL 实例,对于读操作,每个实例读取到的数据都是相同的。对于写操作,当数据写入某一节点后,集群会将其同步到其它节点。这种架构不共享任何数据,是一种高冗余架构。
如何选择版本?
建议采用 Percona XtraDB Cluster,因为技术比较成熟,而且国内很多企业在生产线上用的更多一些。
Galera Cluster for MySQL、MariaDB Galera Cluster (MGC) 与 Percona Xtradb Cluster (PXC) 三者的区别如下:
版本不同
发行版不同
:
功能不同
:
许可证不同
:
MariaDB 与 Percona Server 的关系
MariaDB 数据库是由原 MySQL 创始人开发,Percona Server 数据库由 Percona 公司开发(使用 XtraDB 存储引擎),两者都是从 MySQL 衍生出来的数据库分支。
Galera Replication 插件
:Galera Replication 是一个基于同步复制的插件,用于实现数据的多主复制和一致性。它使用了多主复制协议,确保在集群中的所有节点之间的数据同步和一致性。Primary Component
:Primary Component 是 Galera Cluster 中的主组件,负责处理所有的写操作和读操作。Primary Component 接收来自应用程序的写请求,并将数据复制到其他节点(Secondary Component)上。Secondary Component
:Secondary Component 是 Galera Cluster 中的从组件,负责复制 Primary Component 上的数据。Secondary Component 通过与 Primary Component 进行通信,接收并应用 Primary Component 上的写操作,以保持数据的一致性。初始化集群
:在 Galera Cluster 中,首先需要配置和启动一个节点作为初始 Primary Component,并将其配置为 Galera Replication 插件的成员。然后,其他节点可以加入到集群中,并通过与 Primary Component 进行通信,获取数据并成为 Secondary Component。数据同步和复制
:一旦集群初始化完成,Primary Component 开始接收来自应用程序的写请求,并将数据复制到其他节点上。Secondary Component 通过与 Primary Component 进行通信,接收并应用 Primary Component 上的写操作,以保持数据的一致性。自动故障切换
:如果 Primary Component 发生故障,Galera Cluster 会自动选择一个 Secondary Component 作为新的 Primary Component,并将其他节点重新配置为新的 Secondary Component。这个过程是自动的,无需人工干预。高可用性
:Galera Cluster 通过数据的多主复制和自动故障切换,支持自动添加和剔除节点,实现了高可用性。即使某个节点发生故障,集群仍然可以继续提供服务。数据强一致性
:Galera Cluster 使用多主复制协议,确保在集群中的所有节点之间的数据同步和一致性。在写操作提交之前,集群中的成员会达成一致,确保数据在所有节点上的复制是一致的。简化配置和管理
:Galera Cluster 提供了简单易用的配置选项和管理工具,使得集群的配置和管理变得更加简单和方便。可扩展性
:Galera Cluster 支持水平扩展,可以通过增加节点来扩展存储容量和处理能力。同时,由于数据的多主复制和负载均衡,可以实现更好的性能和吞吐量。拥有成熟的社区
:Galera Cluster 拥有成熟的社区,国内有互联网公司在大规模使用。使用体验一致
:用户可以直接连接 Galera Cluster 集群,使用感受上与 MySQL 完全一致。同步复制
:Galera Cluster 使用了同步复制,且支持真正的并行复制(行级)。网络稳定性
:Galera Cluster 对网络的稳定性要求较高,因为节点之间需要进行频繁的通信和数据同步。如果网络不稳定,可能会导致数据同步延迟或节点之间的通信故障。写冲突
:由于 Galera Cluster 支持多主复制,如果应用程序在不同的节点上同时进行写操作,可能会导致写冲突和一致性问题。因此,需要在应用程序层面进行合理的设计和处理。配置复杂性
:尽管 Galera Cluster 提供了简化的配置选项和管理工具,但对于不熟悉 Galera Cluster 的用户来说,配置可能是一项具有挑战性的任务。需要安装补丁
:使用 Galera Cluster 时,需要提前为原生 MySQL 节点安装 Wsrep 补丁。节点数需求
:搭建 Galera Cluster 时,要求至少有三个服务器节点,且多节点写入开销大。使用限制
提示
在使用 Galera Cluster 之前,建议进行充分的测试和评估,以确保它能够满足系统的可用性、性能和扩展性要求,并根据具体的应用场景和需求进行适当的配置和调整。
Percona XtraDB Cluster(PXC)由 Percona 公司开发,是一个基于 Galera Cluster 实现的高可用性和高性能的 MySQL 集群解决方案。它是由 Percona 开发的,建立在 Galera Replication 插件之上,提供了多主复制和数据同步的功能。
mysql
)里面有些表是使用 MyISAM 存储引擎,因此不能直接对系统库的表进行 DML 操作,比如 INSERT INTO mysql.user
,但使用 CREATE USER
是没有问题的,它也是正确的使用方式。wsrep_max_ws_rows
和 wsrep_max_ws_size
变量定义,LOAD DATA INFILE
方式处理每 10000 行提交一次,对于大的事务将被分解众多小型事务。ALTER TABLE
、IMPORT
、EXPORT
等操作,因为如果这些操作未在所有节点上同步执行,可能会导致节点不一致。LOCK TABLES
以及 UNLOCK TABLES
锁定功能,如 GET_LOCK ()
,RELEASE_LOCK ()
等也不被支持。enforce_storage_engine=InnoDB
与 wsrep_replicate_myisam=OFF(默认)
不兼容。binlog_rows_query_log_events
变量不受支持。数据一致性对比
写入性能对比
两者的区别总结
MySQL InnoDB Cluster 是 MySQL 官方推出的一套完整高可用性解决方案。
MySQL Group Replication
:简称 MGR,是 MySQL 的主从同步高可用方案,包括数据同步及角色选举。MySQL Router
:业务流量的统一入口,支持对 MGR 的主从角色判断,可以配置不同的端口分别对外提供读写服务,实现读写分离等功能。MySQL Shell
:MySQL InnoDB Cluster 的管理工具,用来创建和管理集群。高可用方案 | 保证数据强一致性 | 使用说明 | 描述 |
---|---|---|---|
主从复制 | 否 | 支持单主 | 只适用于对可用性和数据一致性要求较低的业务场景。 |
MMM | 否 | 支持单主 | 基本淘汰了,在一致性和高并发稳定性等方面有些问题。 |
MHA | 否 | 支持单主 | 有少数开发者还在用,但也有些问题,也是趋于淘汰的 MySQL 主从高可用方案。 |
MGR | 是 | 支持单主 / 多主 | 基于 MySQL 官方从 5.7.17 版本开始引入的组复制技术。 |
MySQL Cluster | 是 | 支持多主 | MySQL 官方提供的一种分布式数据库解决方案,只支持 NDB 引擎。 |
Galera Cluster | 是 | 支持多主 | 引领时代的主从复制高可用技术。 |
Galera Cluster for MySQL | 是 | 支持多主 | MySQL 对 Galera Cluster 的实现。 |
MariaDB Galera Cluster (MGC) | 是 | 支持多主 | MariaDB 对 Galera Cluster 的实现。 |
Percona XtraDB Cluster (PXC) | 是 | 支持多主 | Percona 对 Galera Cluster 的实现,目前业界使用 PXC 的会多一些。 |
MySQL InnoDB Cluster | 是 | 支持单主 / 多主 | MySQL 官方推出的一套完整高可用性解决方案。 |
主从之间一般使用异步复制,这意味无法保证数据的一致性,对于数据一致性要求比较高的业务场景是不适用的(如金融、银行业务)。
优点
缺点
MMM(Master-Master Replication Manager)是一套支持 MySQL 双主故障切换和双主日常管理的脚本程序,可以实现 MySQL 数据库的高可用性和负载均衡。MMM 基于 Perl 语言开发,主要用于监控和管理 MySQL Master-Master(双主) 复制,可以说是 MySQL 主主复制的管理器。虽然叫做双主复制,但在业务上同一时刻只能有一个主库进行数据的写入,另一台主备库会提供部分读服务,以加速在主主切换时主备库的预热。另外,主备库会在主库失效时,进行主备切换和故障转移。可以说 MMM 这套脚本程序一方面实现了主备切换的功能,另一方面其内部附加的工具脚本也可以实现多个 Slave 节点的读负载均衡。简而言之,MMM 是一套基于 MySQL 主从复制的高可用性解决方案,通过使用双主复制架构、自动故障检测与切换机制、故障恢复机制,实现了 MySQL 数据库的高可用性和数据同步。
注意
MMM 方案基本淘汰了,在生产环境中不建议使用。
工作原理
:MMM 采用了一种双主复制架构,其中有两个 MySQL 主服务器(Master1 和 Master2),它们之间通过 MySQL 的复制功能进行数据同步。在这种架构中,应用程序可以同时连接到 Master1 和 Master2,从而实现读写负载的分担和高可用性。主从复制
:MMM 利用 MySQL 的主从复制机制,将一个 MySQL 主服务器(Master1)作为主节点,另一个 MySQL 主服务器(Master2)作为从节点。主节点接收写操作并将其复制到从节点,从而保持数据的同步。当主节点发生故障时,从节点可以自动接管主节点的角色,确保数据库的高可用性。自动故障检测与切换
:MMM 具有自动检测主节点故障的能力。它通过监控主节点的心跳以及与从节点的复制延迟来确定主节点是否正常工作。如果主节点发生故障或延迟过高,MMM 会自动将从节点切换为主节点,并将所有写操作重定向到新的主节点。故障恢复
:当主节点恢复正常工作后,MMM 可以自动将其重新加入复制拓扑,并将其配置为从节点。这样,当前的主节点(之前的从节点)会将数据同步到恢复的主节点,以确保数据的一致性。特别注意
使用 MMM 可以有效地提高 MySQL 数据库的可用性和性能。特别注意的是,MMM 并不能解决所有的高可用问题,例如网络分区和数据一致性等问题。
优点
高可用性
:MMM 通过自动故障检测和故障转移机制,可以快速将一个从节点提升为新的主节点,从而实现数据库的高可用性,减少系统的停机时间。负载均衡
:MMM 可以根据节点的负载情况,将读操作分发到不同的节点上,从而实现负载均衡,提高系统的整体性能。简单易用
:MMM 提供了一些管理工具,可以方便地进行节点的添加、删除和配置修改等操作,使得系统的管理和维护变得简单易用。VIP 支持
:默认提供了读写 VIP(虚拟 IP)的支持。缺点:
无法完全保证数据一致性
:由于 MMM 默认采用了 MySQL 的异步复制机制,主节点和从节点之间的同步存在一定的延迟,可能会导致数据的不一致。在某些场景下,需要额外的措施来确保数据的一致性。单点故障
:虽然 MMM 可以自动进行故障转移,但在故障转移过程中,可能会存在一段时间的数据库不可用。如果 MMM 本身发生故障,可能会导致整个系统的不可用。配置复杂性
:MMM 的配置相对复杂,需要对 MySQL 的复制机制和 MMM 的工作原理有一定的了解。在配置过程中,需要注意各个节点的配置一致性和正确性。故障切换会丢事务
:出现故障切换时,容易丢失事务,建议主从库采用半同步复制方式解决,减少出问题的概率。不支持 GTID
:MMM 不支持基于 GTID 的复制,只支持基于日志点的复制。社区不活跃
:目前 MMM 开源社区已经缺少维护者。MHA(Master High Availability)是一种用于 MySQL 数据库的高可用性架构。它的设计目标是确保在主数据库发生故障时,能够快速自动地将备库(Slave)提升为新的主库,以保证系统的连续性和可用性。MHA 专门用于监控主库的状态,当发现 Master 节点发生故障的时候,会自动提升其中拥有最新数据的 Slave 节点成为新的 Master 节点;在此期间,MHA 会通过其他从节点获取额外的信息来避免数据一致性问题。MHA 还提供了一种在线切换 Master-Slave 节点的功能,可以根据需要进行切换。MHA 可在 30 秒内实现故障转移,同时最大程度确保数据一致性。
注意
5.7.17
的版本,如 5.5
、5.6
等。5.7.17
版本开始提供了组复制技术,因此版本号大于 5.7.17
的 MySQL,建议采用 MGR(MySQL Group Replication)或者其他高可用方案。MHA 可以扩展为多主多从的集群架构,如下图所示
目前 MHA 主要支持一主多从的架构,要搭建 MHA,则必须保证在一个 MySQL 复制集群中最少有三台数据库服务器,一主二从,即一台 Master 节点,一台充当备用 Master 节点,另外一台充当 Slave 节点,因为至少需要三台服务器。
MHA 架构的工作流程如下:
在 MHA 自动故障切换的过程中,MHA 会尝试从宕机的主服务器上最大限度的保存二进制日志,最大程度的保证数据的不丢失,但这并不总是可行的。例如,主服务器硬件故障或无法通过 SSH 访问,导致 MHA 无法保存二进制日志,只进行故障转移而丢失了最新的数据。使用从 MySQL 5.5 开始支持的半同步复制,可以大大降低数据丢失的风险。值得一提的是,MHA 很适合与半同步复制机制结合起来使用。如果只有一个 Slave 节点已经接收到了最新的二进制日志,MHA 可以将最新的二进制日志应用于其他所有的 Slave 节点上,因此可以保证所有节点的数据一致性。
优点:
自动故障切换
:MHA 能够自动检测主库的故障,并快速将备库提升为新的主库,减少了手动干预的需要,提高了系统的可用性。实时监测
:MHA 通过与 Master 节点和 Slave 节点建立 SSH 连接,实时监测它们的状态,能够及时发现故障并采取相应的措施。简化配置
:MHA 提供了简单易用的配置文件,可以轻松地配置主库和备库的信息,减少了配置的复杂性。高可扩展性
:MHA 支持多个备库,可以根据需求灵活地扩展系统的容量和性能。支持 GTID 与日志点
:支持基于 GTID 的复制模式,在进行故障转移时更不易产生数据丢失,同时还支持基于日志点的复制。支持监控多个集群
:同一个监控节点可以监控多个集群。缺点:
配置复杂性
:尽管 MHA 提供了简化的配置文件,但对于不熟悉 MHA 的用户来说,配置仍然可能是一项复杂的任务。特别是在涉及多个主库和备库的复杂环境中,配置可能变得更加困难。依赖 SSH 连接
:MHA 通过 SSH 连接与主库和备库进行通信和监控。这意味着在配置和使用 MHA 时,必须确保 SSH 连接的可用性和稳定性,否则可能会导致 MHA 无法正常工作。由于需要基于 SSH 免认证配置,存在一定的安全隐患。故障切换过程中的数据同步延迟
:在故障切换期间,MHA 需要将备库提升为新的主库,并重新配置其他备库作为新的从库。这个过程可能需要一些时间,导致在切换期间存在一定的数据同步延迟,这可能会对某些应用程序的数据一致性产生影响。依赖 MySQL 复制功能
:MHA 依赖 MySQL 的半同步复制方式来实现数据的同步和复制。如果 MySQL 的复制功能出现问题,可能会导致 MHA 无法正常工作或数据同步不完整。需要额外的硬件资源
:为了实现高可用性,MHA 需要至少一个备库来作为冗余备份。这意味着需要额外的硬件资源来支持备库的运行和数据复制,增加了系统的成本和复杂性。只监控 Master 节点
:MHA 启动后只会对主数据库进行监控,并不关注 Slave 节点的运行状态,这可能会导致 Master 节点挂掉后切换到无效的 Slave 节点,从而导致系统崩溃。需要配置 VIP
:MHA 需要编写脚本或利用第三方工具来实现 VIP(虚拟 IP)的配置。存在脑裂的问题
:可能会因网络分区导致脑裂问题的发生。特别注意
MHA 并不是万能的解决方案,它适用于大多数的 MySQL 数据库场景,但在特定的情况下可能需要根据实际需求进行定制化的配置和调整。此外,为了确保 MHA 的正常运行,还需要进行定期的监控和维护工作,以保证系统的稳定性和可靠性。
MGR(MySQL Group Replication)是 MySQL 官方在 5.7.17
版本引进的一个数据库高可用解决方案,以插件形式提供,用于实现 MySQL 数据库的主从复制和自动故障切换。MGR 基于 MySQL 的 InnoDB 存储引擎和 Group Replication 插件实现,引入组复制主要是为了解决传统异步复制和半同步复制可能产生数据不一致的问题。值得一提的是,MGR 支持单主模式与多主模式,多主模式支持多点写入,MySQL 官方推荐使用单主模式。
MGR 架构的核心组件
Group Replication 组件
:Group Replication 是 MySQL 官方提供的插件,用于实现多主复制和自动故障切换。它基于 Paxos 协议,通过在集群中的成员之间进行通信和协调,实现数据的同步和一致性。Primary 节点
:Primary 节点是 MGR 集群中的主节点,负责处理所有的写操作和读操作。Primary 节点接收来自应用程序的写请求,并将数据复制到其他节点(Secondary 节点)上。Secondary 节点
:Secondary 节点是 MGR 集群中的从节点,负责复制 Primary 节点上的数据。Secondary 节点通过与 Primary 节点进行通信,接收并应用 Primary 节点上的写操作,以保持数据的一致性。MGR 架构的工作流程
初始化集群
:在 MGR 架构中,首先需要选择一个节点作为初始 Primary 节点,并将其配置为 Group Replication 组件的成员。然后,其他节点可以加入到集群中,并通过与 Primary 节点进行通信,获取数据并成为 Secondary 节点。数据同步
:一旦集群初始化完成,Primary 节点开始接收来自应用程序的写请求,并将数据复制到其他节点上。Secondary 节点通过与 Primary 节点进行通信,接收并应用 Primary 节点上的写操作,以保持数据的一致性。自动故障切换
:如果 Primary 节点发生故障,Group Replication 组件会自动选择一个 Secondary 节点作为新的 Primary 节点,并将其他节点重新配置为新的 Secondary 节点。这个过程是自动的,无需人工干预。优点
自动故障切换
:MGR 能够自动检测 Primary 节点的故障,并快速将一个 Secondary 节点提升为新的 Primary 节点,实现自动故障切换,提高了系统的可用性。只要有 N / 2 + 1 节点可用,集群就可用。保证数据的强一致性
:MGR 使用 Paxos 协议来保证数据的强一致性。在写操作提交之前,集群中的成员会达成一致,确保数据在所有节点上的复制是一致的。简化配置和管理
:MGR 提供了简单易用的配置选项和管理工具,使得集群的配置和管理变得更加简单和方便。高可扩展性
:MGR 支持多主复制,可以根据需求灵活地扩展系统的容量和性能。支持多主模式,但目前该技术还不是很成熟
缺点
网络稳定性
:MGR 对网络的稳定性要求较高,因为节点之间需要进行频繁的通信和数据同步。如果网络不稳定,可能会导致数据同步延迟或节点之间的通信故障。数据冲突
:由于 MGR 支持多主复制,如果应用程序在不同的节点上同时进行写操作,可能会导致数据冲突和一致性问题。因此,需要在应用程序层面进行合理的设计和处理。配置复杂性
:尽管 MGR 提供了简化的配置选项和管理工具,但对于不熟悉 MGR 的用户来说,配置仍然可能是一项复杂的任务。特别是在涉及多个节点和复杂环境中,配置可能变得更加困难。存在较多限制
:--binlog-checksum=none
。gap lock(间隙锁)
,隔离级别需设置为 read_committed
。lock table
、unlock table
)。serializable(串行)
隔离级别。适用场景
提示
在使用 MGR 之前,建议进行充分的测试和评估,以确保它能够满足系统的可用性和性能要求,并根据具体的应用场景和需求进行适当的配置和调整。
MySQL Cluster (又叫 MySQL NDB Cluster)是 MySQL 官方开源的一种分布式数据库解决方案,旨在提供高可用性、可扩展性和实时性能。它基于 NDB(Network DataBase)存储引擎,使用多台服务器组成一个集群,提供数据的分片和复制,以实现高可用性和自动的读写负载均衡。值得一的是,MySQL Cluster 兼容 ACID 事务,不存在单点故障,支持自动水平扩容,可以保证数据的强一致性。
注意
由于 MySQL Cluster 的使用和配置都比较复杂,该方案在国内并没有被大规模使用。
Management 节点
:Management 节点是 MySQL Cluster 的控制节点,负责集群的管理和配置。它负责监控集群中的各个节点,并协调数据的分片和复制。Data 节点
:Data 节点是 MySQL Cluster 的数据节点,负责存储和处理数据。每个 Data 节点都运行 NDB 存储引擎,数据被分片存储在不同的 Data 节点上,以实现数据的分布和负载均衡。SQL 节点
:SQL 节点是 MySQL Cluster 的查询节点,负责处理应用程序的查询请求。SQL 节点接收来自应用程序的 SQL 查询,并将查询分发到适当的 Data 节点上进行处理。MySQL Cluster 架构的工作流程如下:
集群初始化
:在 MySQL Cluster 中,首先需要配置和启动 Management 节点,然后配置和启动 Data 节点和 SQL 节点。Management 节点负责监控和管理集群中的各个节点。数据分片和复制
:一旦集群初始化完成,Management 节点会根据配置的规则将数据分片存储在不同的 Data 节点上。数据的复制和同步由 MySQL Cluster 自动处理,以保证数据的一致性和可用性。查询处理
:当应用程序发送查询请求时,SQL 节点接收并解析查询,并将查询分发到适当的 Data 节点上进行处理。Data 节点返回查询结果给 SQL 节点,然后 SQL 节点将结果返回给应用程序。优点
高可用性
:MySQL Cluster 通过数据的分片和复制,以及自动故障检测和恢复机制,实现了高可用性。即使某个节点发生故障,集群仍然可以继续提供服务。可扩展性
:MySQL Cluster 支持水平扩展,可以通过增加 Data 节点来扩展存储容量和处理能力。同时,由于数据的分片和负载均衡,可以实现更好的性能和吞吐量。实时性能
:MySQL Cluster 的设计目标之一是提供实时性能。通过将数据存储在内存中,并使用并行处理和分布式计算,可以实现较低的延迟和更高的吞吐量。数据的强一致性
:MySQL Cluster 使用多副本复制和同步机制,以保证数据的强一致性。即使在节点故障或网络分区的情况下,数据仍然可以保持一致。缺点:
配置复杂性
:MySQL Cluster 的配置相对复杂,需要考虑数据分片、复制和负载均衡等因素。对于不熟悉 MySQL Cluster 的用户来说,配置可能是一项具有挑战性的任务。内存需求
:由于 MySQL Cluster 将数据存储在内存中,因此对内存的需求较高,需要根据数据量和性能需求来配置足够的内存资源。存储引擎需求
:MySQL Cluster 需要使用 NDB 存储引擎,与 MySQL 常用引擎(如 Innodb 引擎)存在一定的差异。网络稳定性
:MySQL Cluster 对网络的稳定性要求较高,因为节点之间需要进行频繁的通信和数据同步。如果网络不稳定,可能会导致数据同步延迟或节点之间的通信故障。重启时间长
:重启的时候,数据节点将数据加载到内存需要很长时间。备份和恢复
:MySQL Cluster 对数据备份和恢复并不友好。节点数需求
:搭建 MySQL Cluster 时,要求至少有三个服务器节点。存在较多限制
:如不支持外键,数据行不能超过 8K(不包括 BLOB 和 TEXT 中的数据)等。提示
在使用 MySQL Cluster 之前,建议进行充分的测试和评估,以确保它能够满足系统的可用性、性能和扩展性要求,并根据具体的应用场景和需求进行适当的配置和调整。