LongLong's Blog

分享IT技术,分享生活感悟,热爱摄影,热爱航天。

MySQL Group Replication集群

传统的MySQL的主从复制是主库通过binlog的形式将数据更新发送给从库,其中包含异步的复制,即主库完全不关心从库复制的情况就更新自身的数据,另外半同步复制则是主库需要等待至少一个从库将发送的binlog写入到其relaylog中才会更新自身的数据。

在此种复制模式下当主库发生故障时,从库的数据可能会存在不一致的问题,此时确定数据完全正确的从库(如果使用异步复制则可能会导致数据直接丢失,而不存在数据正确的从库)再重新调整各个从库之间的复制关系是个相对复杂的过程(尽管有一些工具能够自动处理这一过程)。

传统的异步复制

5.5中加入的半同步复制

在MySQL 5.7中增加了Group Replication的复制方式,其实现了一种去中心化的复制方式,每个节点都具有相同的数据和地位(在Single-Primary模式下会决议确定一个写入的入口节点),当事务提交时,需要由多数节点决议来确定事务是否进行提交,以保证复制组内数据的一致性。当某个节点发生故障时Group Replication会自动剔除这个节点,如果故障的是Primary节点,则其他节点会决议来确定一个新的Primary节点。

5.7中加入的组复制

1. Group Replication的配置要求

使用Group Replication功能是需要保证满足以下条件

  1. MySQL版本需要在5.7.17以上
  2. 存储引擎全部使用InnoDB
  3. 全部表都必须有主键
  4. 网络为IPV4网络

使用Group Replication功能需要进行以下配置

#全部节点开启binlog
log-bin=binlog

#binlog格式必须为row格式
binlog-format=row

#开启全局事务标识
gtid-mode=ON

#复制信息存入系统表中
master-info-repository=TABLE
relay-log-info-repository=TABLE

transaction-write-set-extraction=XXHASH64

#开启复制日志
log-slave-updates=ON

binlog-checksum=NONE

2. Group Replication的配置

配置组复制至少配置3个节点,需要进行的配置如下

#复制组的UUID标识
loose-group_replication_group_name="17da1ce4-040d-462e-a444-bfbf55a0f948"
#启动后不自动加入复制组
loose-group_replication_start_on_boot=off
#实例的组复制IP和端口
loose-group_replication_local_address= "127.0.0.1:24901"
#复制组的成员IP和端口,不能使用主机名,用于新的成员加入复制组
loose-group_replication_group_seeds= "127.0.0.1:24901,127.0.0.1:24902,127.0.0.1:24903"
#启动后初始化复制组,仅可在一个节点上设置为on
loose-group_replication_bootstrap_group=off
#单Primary节点模式
group_replication_single_primary_mode=on

配置组复制使用的帐号,并开启组复制,需要注意每个节点的用户和密码需要相同

SET SQL_LOG_BIN=0;
CREATE USER rpl_user@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO rpl_user@'%';
FLUSH PRIVILEGES;
SET SQL_LOG_BIN=1;
CHANGE MASTER TO MASTER_USER='rpl_user', MASTER_PASSWORD='password' FOR CHANNEL 'group_replication_recovery';

3. 启动Group Replication

连接进入其中一个MySQL实例,执行以下操作开启组复制

INSTALL PLUGIN group_replication SONAME 'group_replication.so';
SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;

在另外的实例中执行开启组复制

START GROUP_REPLICATION;

完成后查看状态可以看到

SELECT * FROM performance_schema.replication_group_members;
+---------------------------+--------------------------------------+-------------+-------------+--------------+
| CHANNEL_NAME              | MEMBER_ID                            | MEMBER_HOST | MEMBER_PORT | MEMBER_STATE |
+---------------------------+--------------------------------------+-------------+-------------+--------------+
| group_replication_applier | 01b7de10-81e0-11e8-b5e0-080027f3c535 | archlinux   |       24803 | ONLINE       |
| group_replication_applier | f5e1928b-81df-11e8-bda0-080027f3c535 | archlinux   |       24801 | ONLINE       |
| group_replication_applier | fa8af292-81df-11e8-8566-080027f3c535 | archlinux   |       24802 | ONLINE       |
+---------------------------+--------------------------------------+-------------+-------------+--------------+

4. 多Primary节点模式(Multi-Primary)

如果希望能够从任何一个节点都能进行读写操作,则需要将全部节点都增加配置

group_replication_single_primary_mode=off

注意全部节点的这个配置必须全部都是相同的,否则配置不一致的节点启动组复制时会发生以下报错

Variables such as single_primary_mode or enforce_update_everywhere_checks must have the same value on every server in the group.

另外在两个不同的节点同时进行锁定(SELECT FOR UPDATE),由于节点之间的锁时不能共享的,会造成两个节点上都能够锁定成功,但在事务提交时,后提交的事务会发生失败。

ERROR 1180 (HY000): Got error 149 during COMMIT

为此还是建议直接使用Single-Primary模式。

5. 总结

Group Replication主要解决了传统主从复制方式在主库故障时可能数据丢失以及从库数据不一致并且切换复杂的问题,简化了MySQL集群的高可用设计,同时使用InnoDB Cluster结合MySQL Router便可以实现对于后端Primary节点和Secondary节点的自动选择,并且通过两个MySQL Router配置Keepalived后,就可以实现对后端MySQL Group Replication集群的透明访问。

使用OpenCL进行异构计算

目前应用比较广泛的异构计算框架是nVIDIA公司的CUDA,目前无论是开源还是商业软件的GPU加速基本都采用了CUDA,主要应用在物理仿真和机器学习等运算密集的应用场景中。然而实用CUDA的条件是具有一块nVIDIA的显卡,而在Intel核显和AMD显卡下则无法使用,就可移植性来说还是存在一些缺陷,期待以后能够有开放版本的CUDA。

OpenCL作为一个开放的异构计算框架在CUDA的流行下一直都没有能够得到广泛的应用,其原理是在运行时有OpenCL的实现层将特定语法的C语言代码(称为Kernel)编译为异构计算硬件的机器代码,通过命令队列的方式将计算任务分配给每一个异构计算单元,每一个异构计算单元执行Kernel得出运算结果。CUDA总体上也是类似的逻辑。

1. 在macOS上使用OpenCL

macOS上安装XCode后就会默认安装OpenCL,引用的头文件为

#include <OpenCL/opencl.h>

如果考虑代码在其他平台的可移植性,一般可以写成

#ifdef __APPLE__
#include <OpenCL/opencl.h>
#else
#include <CL/cl.h>
#endif

编译使用了OpenCL代码的命令为

gcc -framework opencl hello.c

2. 使用OpenCL实现矩阵乘法

以下将使用OpenCL实现一个矩阵乘法的算法,其代码如下

#include <OpenCL/opencl.h>
#include <stdio.h>
#include <stdlib.h>
#define KERNEL(...)#__VA_ARGS__
//Kernel代码,计算乘积矩阵i行j列的值
const char* KernelSource = KERNEL(
    __kernel void matrix_mul(int m, int p, int n, __global float* A, __global float* B, __global float* C) {\n
        int i = get_global_id(0);\n
        int j = get_global_id(1);\n
        float sum = 0;\n
        for (int k = 0; k < p; k++) {\n
            sum += A[i * m + k] * B[p * k + j];\n
        }\n
        C[i * m + j] = sum;\n
    }
);

int main() {
    int err;
    cl_device_id device_id;
    cl_context context;
    cl_command_queue commands;
    cl_program program;
    cl_kernel kernel;

    int gpu = 1;
    //获取计算设备,这里获取GPU设备
    err = clGetDeviceIDs(NULL, gpu ? CL_DEVICE_TYPE_GPU : CL_DEVICE_TYPE_CPU, 1, &device_id, NULL);
    if (err != CL_SUCCESS) {
        return EXIT_FAILURE;
    }
    //初始化上下文和命令队列
    context = clCreateContext(0, 1, &device_id, NULL, NULL, &err);
    if (!context) {
        return EXIT_FAILURE;
    }
    commands = clCreateCommandQueue(context, device_id, 0, &err);
    if (!commands) {
        return EXIT_FAILURE;
    }

    //编译Kernel代码
    program = clCreateProgramWithSource(context, 1, (const char**) & KernelSource, NULL, &err);
    if (!program) {
        return EXIT_FAILURE;
    }

    err = clBuildProgram(program, 0, NULL, NULL, NULL, NULL);
    if (err != CL_SUCCESS) {
        return EXIT_FAILURE;
    }

    kernel = clCreateKernel(program, "matrix_mul", &err);
    if (!kernel || err != CL_SUCCESS) {
        return EXIT_FAILURE;
    }
    int count = 800;
    int m = count;
    int p = count;
    int n = count;

    cl_mem input1;
    cl_mem input2;
    cl_mem output;

    //生成两个随机矩阵
    float A[count][count];
    float B[count][count];
    for (int i = 0; i < count; i++) {
        for (int j = 0; j < count; j++) {
            A[i][j] = (float) rand() / RAND_MAX;
            B[i][j] = (float) rand() / RAND_MAX;
        }
    }

    //创建将输入的两个矩阵和乘积结果矩阵的入缓冲区,并将随机生成的数据写入缓冲区
    input1 = clCreateBuffer(context,  CL_MEM_READ_ONLY,  sizeof(float) * m * p, NULL, NULL);
    input2 = clCreateBuffer(context,  CL_MEM_READ_ONLY,  sizeof(float) * p * n, NULL, NULL);
    output = clCreateBuffer(context,  CL_MEM_WRITE_ONLY,  sizeof(float) * m * n, NULL, NULL);
    if (!input1 || !input2 || !output) {
        return EXIT_FAILURE;
    }
    err = 0;
    err = clEnqueueWriteBuffer(commands, input1, CL_TRUE, 0, m * p * sizeof(float), A, 0, NULL, NULL);
    err |= clEnqueueWriteBuffer(commands, input2, CL_TRUE, 0, p * n * sizeof(float), B, 0, NULL, NULL);
    if (err != CL_SUCCESS) {
        return EXIT_FAILURE;
    }

    //设置Kernel参数
    err = 0;
    err  = clSetKernelArg(kernel, 0, sizeof(cl_mem), &m);
    err |= clSetKernelArg(kernel, 1, sizeof(cl_mem), &p);
    err |= clSetKernelArg(kernel, 2, sizeof(cl_mem), &n);
    err |= clSetKernelArg(kernel, 3, sizeof(cl_mem), &input1);
    err |= clSetKernelArg(kernel, 4, sizeof(cl_mem), &input2);
    err |= clSetKernelArg(kernel, 5, sizeof(cl_mem), &output);

    if (err != CL_SUCCESS) {
        return EXIT_FAILURE;
    }

    //设置计算任务,并开始计算
    //其中global中设置的两个变量对应Kernel中get_global_id中获取的两个值
    size_t local[2] = {20, 20};
    size_t global[2] = {count, count};

    err = clEnqueueNDRangeKernel(commands, kernel, 2, NULL, global, local, 0, NULL, NULL);
    if (err != CL_SUCCESS) {
        printf("%d\n", err);
        return EXIT_FAILURE;
    }

    //等待计算结果并将结果读入数组
    float C[m][n];
    clFinish(commands);

    err = clEnqueueReadBuffer(commands, output, CL_TRUE, 0, sizeof(float) * m * n, C, 0, NULL, NULL);
    if (err != CL_SUCCESS) {
        return EXIT_FAILURE;
    }

    //清理释放资源
    clReleaseMemObject(input1);
    clReleaseMemObject(input2);
    clReleaseMemObject(output);
    clReleaseProgram(program);
    clReleaseKernel(kernel);
    clReleaseCommandQueue(commands);
    clReleaseContext(context);

    //输出结果
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            printf("%f\t", C[i][j]);
        }
        printf("\n");
    }
    return 0;
}

3. 性能比较

对比OpenCL版本的矩阵乘法和普通CPU版本的矩阵乘法代码,矩阵规模为800X800的方阵

#OpenCL版本
real    0m0.568s
user    0m0.025s
sys     0m0.022s
#CPU版本
real    0m2.875s
user    0m2.862s
sys     0m0.009s

在矩阵规模较大时OpenCL版本会明显快于CPU版本,而在矩阵规模较小时会慢于CPU版本,主要是由于OpenCL版本的初始化,编译Kernel,复制数据到缓冲区这些开销在计算规模不大时会占总计算时间的很大比例。

4. 在Python中使用OpenCL

在C/C++中使用OpenCL时很多初始化的操作会相对繁琐,如果在Python等脚本语言中使用则可以简化这些处理使得代码更加简单。Python的OpenCL库为PyOpenCL,在安装了硬件所需OpenCL的SDK后编译安装PyOpenCL即可,另外通过安装Pocl(Portable Computing Language),还可以将OpenCL代码运行在CPU环境中,可以在无可用GPU环境下开发和调试OpenCL代码。

PyOpenCL主页给出的代码示例为向量相加,其中使用到了Python中常用的数值计算库Numpy,PyOpenCL可以直接对Numpy中的向量数据进行操作

from __future__ import absolute_import, print_function
import numpy as np
import pyopencl as cl

a_np = np.random.rand(50000).astype(np.float32)
b_np = np.random.rand(50000).astype(np.float32)

ctx = cl.create_some_context()
queue = cl.CommandQueue(ctx)

mf = cl.mem_flags
a_g = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=a_np)
b_g = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=b_np)

prg = cl.Program(ctx, """
__kernel void sum(
    __global const float *a_g, __global const float *b_g, __global float *res_g)
{
  int gid = get_global_id(0);
  res_g[gid] = a_g[gid] + b_g[gid];
}
""").build()

res_g = cl.Buffer(ctx, mf.WRITE_ONLY, a_np.nbytes)
prg.sum(queue, a_np.shape, None, a_g, b_g, res_g)

res_np = np.empty_like(a_np)
cl.enqueue_copy(queue, res_np, res_g)

# Check on CPU with Numpy:
print(res_np - (a_np + b_np))
print(np.linalg.norm(res_np - (a_np + b_np)))