如何部署activemq以及部署时遇到的相关问题

如何部署activemq以及部署时遇到的相关问题

如何部署activemq以及部署时遇到的相关问题

部署

以centos为例,下载jar包,tar -zxvf xxx解压后,进入activemq解压文件夹,执行./bin/activemq console即可在shell前端启动,想后台运行的话执行./bin/activemq start即可。

官方文档在这里,很简单:http://activemq.apache.org/getting-started

遇到的问题

1、activemq启动失败,报错

报错信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Caused by: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
at org.springframework.context.support.AbstractRefreshableApplicationContext.getBeanFactory(AbstractRefreshableApplicationContext.java:171)
at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1090)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:487)
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:64)
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:52)
at org.apache.activemq.xbean.XBeanBrokerFactory$1.<init>(XBeanBrokerFactory.java:104)
at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
... 10 more

解决

  • 1.确认计算机主机名名称没有下划线

  • 2.如果是win7,停止ICS(运行–>services.msc找到Internet Connection Sharing (ICS)服务,改成手动启动或禁用)

2、部署在VPS或是虚拟机上,也关闭了防火墙,但无法访问web页面xx.xx.xx.xx:8161/admin

修改conf/jetty.xml,注释掉host属性这行:

1
2
3
4
5
6

<bean id="jettyPort" class="org.apache.activemq.web.WebConsolePort" init-method="start">
          <!--默认是127.0.0.1, 注释掉这一行-->
       <!-- <property name="host" value="127.0.0.1"/> -->
       <property name="port" value="8161"/>
</bean>

重新启动activemq即可

参考资料

spring cloud gateway基础

spring cloud gateway基础

spring cloud gateway(后续简称SCG)本身就是spring cloud体系的一员,在对于java技术栈的系统使用起来比较方便,此外,SCG扩展性比较好,适合作为业务网关的基础、根据自身需求进行二次开发。

关键组成部分

  • 谓词 predicate: 将请求匹配到对应的route上
  • 路由 route: 网关的基本构建块。它由ID,目标URI,谓词集合和过滤器集合定义
  • 过滤器 filter: 由特定工厂构造生成

工作过程

    1. SCG接收客户端请求。
    1. 请求与谓词匹配,获得对应的路由,匹配成功后,继续往下执行。
    1. 请求交给前置filter处理
    1. 请求经过代理服务(类似与RPC),发送给目标URI
    1. 响应交给后置filter处理
    1. SCG将响应返回给客户端

应用场景

  • 灰度发布

    • 可以使用Weight Route Predicate实现
  • 请求限流

  • 服务容错

    • 结合Sentinel或是Hystrix,基于令牌桶算法

其他

  • 监控

  • 故障排查

    • 通过日志
    • Wiretap

参考资料

maven编译大型项目时简化编译过程的命令

maven编译大型项目时简化编译过程的命令

如果项目较大,需要尽可能跳过无关痛痒的环节(比如自己本地编译、使用的话,可能没必要编译javadoc)、只要最终jar的时候,可以用下面命令

1
mvn clean package install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Drat.skip=true -Dcheckstyle.skip=true

centos下如何启动mysql多实例

centos下如何启动mysql多实例

mysql安装有多种方式:

具体详细参考官网 (https://dev.mysql.com/doc/refman/5.7/en/installing.html

  • mysql的安装方法有多种,如二进制安装、源码编译安装、yum安装;
  • yum安装都是默认路径,不利于后期维护,安装相对简单;
  • 源码安装编译的过程比较长,若没有对源码进行修改且要求使用mysql较高版本;

建议使用二进制安装,比较方便后期维护。本文就是采用这种方式启动多个实例。

本文在centos 7.5下进行操作。

准备工作

下载mysql 压缩包,注意需要下载二进制包。下载地址

centos 下可以使用如下命令:

1
wget https://cdn.mysql.com//Downloads/MySQL-5.7/mysql-5.7.32-el7-x86_64.tar.gz

解压,移动到/usr/local/文件夹下:

1
2
3
tar -zxvf mysql-5.7.32-el7-x86_64.tar.gz
# 可以顺手给文件夹改个名字,比如我就已经改成了mysql,所以下面大家会看到我的目录是/usr/local/mysql
mv mysql-5.7.32-el7-x86_64 /usr/local/

初始化用户组以及用户

创建一个mysql用户组及用户,且这个用户是不可登录的 创建用户组:groupadd mysql 创建不可登录用户:useradd -g mysql -s /sbin/nologin -d /opt/mysql mysql 查看下创建后的用户信息:id msyql

1
2
id mysql
uid=501(mysql) gid=501(mysql) groups=501(mysql)

创建相关目录

此处我选择使用在/data/mysql文件夹下保存数据、日志,因此按如下结构创建目录(我打算启动2个实例),命令略去:

1
2
3
4
5
6
7
8
9
10
11
-data
-- mysql
  |-- mysql_3307
      |-- data
      |-- log
      `-- tmp
  `-- mysql_3308
      |-- data
      |-- log
      `-- tmp

更改目录权限

1
2
chown -R mysql:mysql /data/mysql/  
chown -R mysql:mysql /usr/local/mysql/  

添加环境变量

1
2
echo 'export PATH=$PATH:/usr/local/mysql/bin' >>  /etc/profile  
source /etc/profile  

添加配置文件

找一个mysql配置文件my.cnf,如果没有可以手工创建,位置放在:

1
/etc/my_3307.cnf

下面给出一个mysql配置文件参考,请按需修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# my_3307.cnf
# For advice on how to change settings please see
# http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html

[mysqld]
#
# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# innodb_buffer_pool_size = 128M
#
# Remove leading # to turn on a very important data integrity option: logging
# changes to the binary log between backups.
# log_bin
#
# Remove leading # to set options mainly useful for reporting servers.
# The server defaults are faster for transactions and fast SELECTs.
# Adjust sizes as needed, experiment to find the optimal values.
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M
port=3307
basedir=/usr/local/mysql-5.7.32
datadir=/data/mysql/mysql_3307/data
socket=/data/mysql/mysql_3307/mysql.sock
server_id=3307

sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
log-output=file
slow_query_log=1
long_query_time=1
slow_query_log_file = /data/mysql/mysql_3307/log/slow.log
log-error=/data/mysql/mysql_3307/log/mysqld.log
log-bin = /data/mysql/mysql_3307/log/mysql3307_bin
binlog-format=Row
pid-file=/var/run/mysqld/mysqld-3307.pid
collation-server = utf8_unicode_ci
init-connect='SET NAMES utf8'
character-set-server = utf8
language =/usr/local/mysql-5.7.32/share/english

[client]
default-character-set=utf8

[mysql]
default-character-set=utf8

启动mysql

1
2
3
4
5
6
cd /usr/local/mysql/
# 注意此处指定配置文件是必须的,--initialize-insecure是在初始化时不给root设置密码,方便操作;使用./bin/mysqld --defaults-file=/etc/my_3307.cnf --user=mysql --initialize也可以初始化,此时的密码会保存在mysqld.log中
./bin/mysqld --defaults-file=/etc/my_3307.cnf --user=mysql --initialize-insecure
./bin/mysqld --defaults-file=/etc/my_3307.cnf --user=mysql &
# 注意,刚初始化、启动时,如果不指定host或是-hlocalhost,无法连接,必须指定为-h127.0.0.1才能连接。并且此处注意使用-P指定端口,毕竟我们是要启动多个实例,端口别弄乱。
mysql -P3307 -uroot -h127.0.0.1

设置远程连接mysql

1
2
3
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'password' WITH GRANT OPTION;

FLUSH PRIVILEGES;

这样就能成功启动一个实例,接下来同样的操作,即可启动多个实例。

参考资料

使用sysbench对mysql进行基准测试

使用sysbench对mysql进行基准测试

测试环境说明

硬件

  • 腾讯云标准型SA2

    • 1核 2GB 1Mbps
    • 系统盘:高性能云硬盘(50G)

软件

  • centos 7.5 64位
  • mysql 5.7.31
  • sysbench 1.0.20

测试过程

使用sysbench。

1. 测试innodb 100w条记录的读写性能

1.1 生成100w数据

此处偷懒了,直接使用了sysbench默认的脚本创建数据库表,如果需要测试特定的表,需要修改/usr/share/sysbench下的脚本,基本只需要修改oltp_common.lua脚本即可,这个需要花时间细致的修改,这次就不在这方面多花时间了。本次主要目的是做基准测试、看看Mysql性能。

1
sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20  oltp_read_write prepare
1.2 进行读写测试
1.2.1 第一次直接执行100w次读写

执行命令:

1
sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write run

参数说明:

  • 指定测试mysql,默认存储引擎是innodb,指定mysql地址、端口、用户等信息
  • 选取测试的数据库为sbtest,每个表的大小为100w条数据,创建10张表
  • 测试时间300s, 20个线程同时测试
  • 输出测试结果95分位
  • 每隔10s输出一次当前测试情况
  • 选取sysbench中的oltp_read_write.lua,测试读写情况

注意此处没有预热,先观察下没有预热情况下的读写性能,得到如下执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
[root@VM_0_5_centos ~]# sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write run
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)

Running the test with following options:
Number of threads: 20
Report intermediate results every 10 second(s)
Initializing random number generator from current time


Initializing worker threads...

Threads started!

[ 10s ] thds: 20 tps: 115.28 qps: 2327.70 (r/w/o: 1633.92/461.22/232.56) lat (ms,95%): 337.94 err/s: 0.00 reconn/s: 0.00
[ 20s ] thds: 20 tps: 107.65 qps: 2154.96 (r/w/o: 1508.64/431.01/215.31) lat (ms,95%): 350.33 err/s: 0.00 reconn/s: 0.00
[ 30s ] thds: 20 tps: 107.57 qps: 2155.23 (r/w/o: 1509.13/430.97/215.13) lat (ms,95%): 337.94 err/s: 0.00 reconn/s: 0.00
[ 40s ] thds: 20 tps: 102.08 qps: 2027.14 (r/w/o: 1414.88/408.11/204.16) lat (ms,95%): 369.77 err/s: 0.00 reconn/s: 0.00
[ 50s ] thds: 20 tps: 111.17 qps: 2239.64 (r/w/o: 1571.11/446.19/222.34) lat (ms,95%): 337.94 err/s: 0.00 reconn/s: 0.00
[ 60s ] thds: 20 tps: 118.82 qps: 2376.16 (r/w/o: 1663.62/474.89/237.65) lat (ms,95%): 297.92 err/s: 0.00 reconn/s: 0.00
[ 70s ] thds: 20 tps: 118.57 qps: 2369.40 (r/w/o: 1657.88/474.48/237.04) lat (ms,95%): 325.98 err/s: 0.00 reconn/s: 0.00
[ 80s ] thds: 20 tps: 125.53 qps: 2512.64 (r/w/o: 1758.58/502.91/251.15) lat (ms,95%): 287.38 err/s: 0.00 reconn/s: 0.00
[ 90s ] thds: 20 tps: 115.44 qps: 2298.71 (r/w/o: 1609.27/458.76/230.68) lat (ms,95%): 314.45 err/s: 0.00 reconn/s: 0.00
[ 100s ] thds: 20 tps: 120.28 qps: 2412.81 (r/w/o: 1689.53/482.52/240.76) lat (ms,95%): 314.45 err/s: 0.00 reconn/s: 0.00
[ 110s ] thds: 20 tps: 123.70 qps: 2460.59 (r/w/o: 1719.30/494.00/247.30) lat (ms,95%): 287.38 err/s: 0.00 reconn/s: 0.00
[ 120s ] thds: 20 tps: 121.74 qps: 2455.40 (r/w/o: 1720.96/490.86/243.58) lat (ms,95%): 287.38 err/s: 0.00 reconn/s: 0.00
[ 130s ] thds: 20 tps: 129.32 qps: 2580.95 (r/w/o: 1808.11/514.19/258.64) lat (ms,95%): 292.60 err/s: 0.00 reconn/s: 0.00
[ 140s ] thds: 20 tps: 125.38 qps: 2508.77 (r/w/o: 1756.50/501.51/250.76) lat (ms,95%): 282.25 err/s: 0.00 reconn/s: 0.00
[ 150s ] thds: 20 tps: 128.26 qps: 2564.90 (r/w/o: 1794.64/513.74/256.52) lat (ms,95%): 287.38 err/s: 0.00 reconn/s: 0.00
[ 160s ] thds: 20 tps: 129.90 qps: 2596.47 (r/w/o: 1817.98/518.79/259.70) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00
[ 170s ] thds: 20 tps: 130.30 qps: 2597.18 (r/w/o: 1816.16/520.32/260.71) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00
[ 180s ] thds: 20 tps: 134.10 qps: 2685.40 (r/w/o: 1881.30/535.90/268.20) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00
[ 190s ] thds: 20 tps: 131.38 qps: 2636.25 (r/w/o: 1842.79/530.91/262.56) lat (ms,95%): 282.25 err/s: 0.00 reconn/s: 0.00
[ 200s ] thds: 20 tps: 134.40 qps: 2687.60 (r/w/o: 1883.30/535.30/269.00) lat (ms,95%): 282.25 err/s: 0.00 reconn/s: 0.00
[ 210s ] thds: 20 tps: 130.51 qps: 2610.40 (r/w/o: 1827.61/521.76/261.03) lat (ms,95%): 292.60 err/s: 0.00 reconn/s: 0.00
[ 220s ] thds: 20 tps: 134.91 qps: 2687.87 (r/w/o: 1880.32/537.73/269.82) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00
[ 230s ] thds: 20 tps: 131.30 qps: 2633.77 (r/w/o: 1845.48/525.69/262.60) lat (ms,95%): 277.21 err/s: 0.00 reconn/s: 0.00
[ 240s ] thds: 20 tps: 133.78 qps: 2676.67 (r/w/o: 1872.87/536.23/267.57) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
[ 250s ] thds: 20 tps: 134.10 qps: 2681.82 (r/w/o: 1876.34/537.28/268.19) lat (ms,95%): 292.60 err/s: 0.00 reconn/s: 0.00
[ 260s ] thds: 20 tps: 136.32 qps: 2711.48 (r/w/o: 1896.94/541.90/272.65) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
[ 270s ] thds: 20 tps: 134.20 qps: 2692.50 (r/w/o: 1884.80/539.30/268.40) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00
[ 280s ] thds: 20 tps: 133.03 qps: 2664.24 (r/w/o: 1866.85/531.33/266.06) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00
[ 290s ] thds: 20 tps: 134.52 qps: 2687.68 (r/w/o: 1880.73/537.90/269.05) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00
[ 300s ] thds: 20 tps: 130.74 qps: 2615.97 (r/w/o: 1830.71/523.77/261.49) lat (ms,95%): 277.21 err/s: 0.00 reconn/s: 0.00
SQL statistics:
  queries performed:
      read:                           527282
      write:                           150652
      other:                           75326
      total:                           753260
  transactions:                       37663 (125.47 per sec.)
  queries:                             753260 (2509.43 per sec.)
  ignored errors:                     0     (0.00 per sec.)
  reconnects:                         0     (0.00 per sec.)

General statistics:
  total time:                         300.1706s
  total number of events:             37663

Latency (ms):
        min:                                   7.13
        avg:                                 159.35
        max:                                 873.88
        95th percentile:                     292.60
        sum:                             6001658.82

Threads fairness:
  events (avg/stddev):           1883.1500/14.40
  execution time (avg/stddev):   300.0829/0.05
1.2.2 第二次执行100w次读写

在第一次执行完成后,再一次执行相同的压测命令:

1
sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write run

也就是说,将第一次执行当做预热,然后得到了如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
[root@VM_0_5_centos ~]# sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write run
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)

Running the test with following options:
Number of threads: 20
Report intermediate results every 10 second(s)
Initializing random number generator from current time


Initializing worker threads...

Threads started!

[ 10s ] thds: 20 tps: 134.18 qps: 2711.59 (r/w/o: 1901.91/539.32/270.36) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00
[ 20s ] thds: 20 tps: 137.73 qps: 2743.17 (r/w/o: 1918.90/549.01/275.26) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00
[ 30s ] thds: 20 tps: 135.00 qps: 2700.12 (r/w/o: 1889.21/540.70/270.20) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00
[ 40s ] thds: 20 tps: 133.61 qps: 2680.90 (r/w/o: 1880.57/533.12/267.21) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00
[ 50s ] thds: 20 tps: 133.36 qps: 2673.78 (r/w/o: 1869.10/537.96/266.73) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
[ 60s ] thds: 20 tps: 139.24 qps: 2776.62 (r/w/o: 1945.18/553.07/278.38) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
[ 70s ] thds: 20 tps: 132.31 qps: 2646.75 (r/w/o: 1852.27/529.75/264.72) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00
[ 80s ] thds: 20 tps: 138.52 qps: 2764.91 (r/w/o: 1934.31/553.56/277.03) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00
[ 90s ] thds: 20 tps: 133.20 qps: 2673.72 (r/w/o: 1874.85/532.48/266.39) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00
[ 100s ] thds: 20 tps: 139.13 qps: 2783.76 (r/w/o: 1946.06/559.43/278.27) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00
[ 110s ] thds: 20 tps: 133.60 qps: 2673.61 (r/w/o: 1872.01/534.40/267.20) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00
[ 120s ] thds: 20 tps: 137.21 qps: 2739.02 (r/w/o: 1916.55/548.04/274.42) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
[ 130s ] thds: 20 tps: 136.59 qps: 2736.29 (r/w/o: 1916.22/546.88/273.19) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00
[ 140s ] thds: 20 tps: 133.31 qps: 2667.76 (r/w/o: 1865.01/536.33/266.42) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00
[ 150s ] thds: 20 tps: 136.29 qps: 2728.42 (r/w/o: 1910.27/545.36/272.78) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 160s ] thds: 20 tps: 137.75 qps: 2747.96 (r/w/o: 1925.34/547.21/275.41) lat (ms,95%): 248.83 err/s: 0.00 reconn/s: 0.00
[ 170s ] thds: 20 tps: 136.25 qps: 2727.97 (r/w/o: 1910.08/545.29/272.60) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 180s ] thds: 20 tps: 139.96 qps: 2799.77 (r/w/o: 1958.39/561.45/279.93) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
[ 190s ] thds: 20 tps: 136.37 qps: 2724.15 (r/w/o: 1907.31/544.09/272.74) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
[ 200s ] thds: 20 tps: 137.35 qps: 2748.81 (r/w/o: 1925.74/548.38/274.69) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 210s ] thds: 20 tps: 138.32 qps: 2760.41 (r/w/o: 1930.89/552.88/276.64) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 220s ] thds: 20 tps: 136.57 qps: 2730.27 (r/w/o: 1912.63/544.89/272.75) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00
[ 230s ] thds: 20 tps: 135.90 qps: 2725.03 (r/w/o: 1905.42/547.41/272.20) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00
[ 240s ] thds: 20 tps: 139.06 qps: 2780.47 (r/w/o: 1948.78/553.56/278.13) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00
[ 250s ] thds: 20 tps: 136.23 qps: 2719.64 (r/w/o: 1902.58/544.61/272.45) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00
[ 260s ] thds: 20 tps: 136.39 qps: 2736.95 (r/w/o: 1913.82/550.35/272.77) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 270s ] thds: 20 tps: 141.91 qps: 2829.29 (r/w/o: 1980.91/564.56/283.83) lat (ms,95%): 248.83 err/s: 0.00 reconn/s: 0.00
[ 280s ] thds: 20 tps: 138.60 qps: 2776.59 (r/w/o: 1944.46/554.92/277.21) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
[ 290s ] thds: 20 tps: 137.72 qps: 2747.78 (r/w/o: 1923.26/549.08/275.44) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00
[ 300s ] thds: 20 tps: 139.27 qps: 2798.45 (r/w/o: 1958.44/561.47/278.54) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
SQL statistics:
  queries performed:
      read:                           574434
      write:                           164124
      other:                           82062
      total:                           820620
  transactions:                       41031 (136.74 per sec.)
  queries:                             820620 (2734.82 per sec.)
  ignored errors:                     0     (0.00 per sec.)
  reconnects:                         0     (0.00 per sec.)

General statistics:
  total time:                         300.0629s
  total number of events:             41031

Latency (ms):
        min:                                   6.62
        avg:                                 146.24
        max:                                 709.59
        95th percentile:                     262.64
        sum:                             6000331.95

Threads fairness:
  events (avg/stddev):           2051.5500/16.35
  execution time (avg/stddev):   300.0166/0.02
1.2.3 第三次执行100w次读写

还是这条命令,再次执行看下:

1
sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write run

最后执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
[root@VM_0_5_centos ~]# sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write run
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)

Running the test with following options:
Number of threads: 20
Report intermediate results every 10 second(s)
Initializing random number generator from current time


Initializing worker threads...

Threads started!

[ 10s ] thds: 20 tps: 137.99 qps: 2785.30 (r/w/o: 1953.79/553.54/277.97) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
[ 20s ] thds: 20 tps: 139.98 qps: 2796.53 (r/w/o: 1955.64/561.23/279.66) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 30s ] thds: 20 tps: 135.89 qps: 2721.84 (r/w/o: 1905.59/544.37/271.88) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 40s ] thds: 20 tps: 143.80 qps: 2874.52 (r/w/o: 2013.41/573.40/287.70) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 50s ] thds: 20 tps: 141.73 qps: 2838.10 (r/w/o: 1986.65/567.90/283.55) lat (ms,95%): 277.21 err/s: 0.00 reconn/s: 0.00
[ 60s ] thds: 20 tps: 142.10 qps: 2835.57 (r/w/o: 1984.28/567.09/284.20) lat (ms,95%): 248.83 err/s: 0.00 reconn/s: 0.00
[ 70s ] thds: 20 tps: 141.93 qps: 2838.68 (r/w/o: 1987.87/566.94/283.87) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00
[ 80s ] thds: 20 tps: 142.27 qps: 2841.13 (r/w/o: 1987.20/569.39/284.54) lat (ms,95%): 244.38 err/s: 0.00 reconn/s: 0.00
[ 90s ] thds: 20 tps: 144.78 qps: 2904.14 (r/w/o: 2034.24/580.63/289.27) lat (ms,95%): 231.53 err/s: 0.00 reconn/s: 0.00
[ 100s ] thds: 20 tps: 141.03 qps: 2822.14 (r/w/o: 1977.08/562.71/282.35) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 110s ] thds: 20 tps: 138.68 qps: 2775.16 (r/w/o: 1941.56/556.23/277.37) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 120s ] thds: 20 tps: 140.70 qps: 2799.50 (r/w/o: 1957.90/560.50/281.10) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 130s ] thds: 20 tps: 142.10 qps: 2847.50 (r/w/o: 1994.40/569.00/284.10) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 140s ] thds: 20 tps: 142.10 qps: 2847.17 (r/w/o: 1994.18/568.39/284.60) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00
[ 150s ] thds: 20 tps: 141.69 qps: 2834.74 (r/w/o: 1983.99/567.37/283.38) lat (ms,95%): 240.02 err/s: 0.00 reconn/s: 0.00
[ 160s ] thds: 20 tps: 142.69 qps: 2857.88 (r/w/o: 1999.55/572.96/285.38) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 170s ] thds: 20 tps: 148.01 qps: 2953.24 (r/w/o: 2067.20/590.03/296.01) lat (ms,95%): 244.38 err/s: 0.00 reconn/s: 0.00
[ 180s ] thds: 20 tps: 142.16 qps: 2845.84 (r/w/o: 1993.50/568.03/284.31) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 190s ] thds: 20 tps: 141.93 qps: 2838.87 (r/w/o: 1987.80/567.21/283.86) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00
[ 200s ] thds: 20 tps: 137.03 qps: 2742.24 (r/w/o: 1919.74/548.43/274.06) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00
[ 210s ] thds: 20 tps: 141.14 qps: 2822.53 (r/w/o: 1975.28/564.97/282.28) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
[ 220s ] thds: 20 tps: 143.06 qps: 2859.26 (r/w/o: 2001.71/571.43/286.12) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00
[ 230s ] thds: 20 tps: 145.89 qps: 2911.60 (r/w/o: 2036.39/583.54/291.67) lat (ms,95%): 248.83 err/s: 0.00 reconn/s: 0.00
[ 240s ] thds: 20 tps: 142.86 qps: 2862.32 (r/w/o: 2003.09/573.42/285.81) lat (ms,95%): 244.38 err/s: 0.00 reconn/s: 0.00
[ 250s ] thds: 20 tps: 142.36 qps: 2845.35 (r/w/o: 1992.70/567.93/284.71) lat (ms,95%): 248.83 err/s: 0.00 reconn/s: 0.00
[ 260s ] thds: 20 tps: 140.64 qps: 2816.09 (r/w/o: 1971.96/562.86/281.28) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00
[ 270s ] thds: 20 tps: 143.49 qps: 2865.80 (r/w/o: 2004.86/573.96/286.98) lat (ms,95%): 282.25 err/s: 0.00 reconn/s: 0.00
[ 280s ] thds: 20 tps: 145.81 qps: 2922.99 (r/w/o: 2047.60/583.76/291.63) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
[ 290s ] thds: 20 tps: 142.26 qps: 2838.44 (r/w/o: 1985.40/568.53/284.51) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
[ 300s ] thds: 20 tps: 137.89 qps: 2755.07 (r/w/o: 1928.14/551.15/275.78) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00
SQL statistics:
  queries performed:
      read:                           595840
      write:                           170240
      other:                           85120
      total:                           851200
  transactions:                       42560 (141.79 per sec.)
  queries:                             851200 (2835.86 per sec.)
  ignored errors:                     0     (0.00 per sec.)
  reconnects:                         0     (0.00 per sec.)

General statistics:
  total time:                         300.1547s
  total number of events:             42560

Latency (ms):
        min:                                   6.18
        avg:                                 141.02
        max:                                 687.11
        95th percentile:                     257.95
        sum:                             6001887.46

Threads fairness:
  events (avg/stddev):           2128.0000/14.78
  execution time (avg/stddev):   300.0944/0.03
1.3 清理数据

使用如下命令清理数据

1
sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300   --threads=20 --percentile=95 oltp_read_write cleanup

2. 测试myisam 100w条记录的读写性能

2.1 生成100w数据

使用

1
sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20  oltp_read_write help

可以看到oltp_read_write脚本所支持的参数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@VM_0_5_centos ~]# sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20  oltp_read_write help
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)

oltp_read_write options:
 --auto_inc[=on|off]           Use AUTO_INCREMENT column as Primary Key (for MySQL), or its alternatives in other DBMS. When disabled, use client-generated IDs [on]
 --create_secondary[=on|off]   Create a secondary index in addition to the PRIMARY KEY [on]
 --delete_inserts=N           Number of DELETE/INSERT combinations per transaction [1]
 --distinct_ranges=N           Number of SELECT DISTINCT queries per transaction [1]
 --index_updates=N             Number of UPDATE index queries per transaction [1]
 --mysql_storage_engine=STRING Storage engine, if MySQL is used [innodb]
 --non_index_updates=N         Number of UPDATE non-index queries per transaction [1]
 --order_ranges=N             Number of SELECT ORDER BY queries per transaction [1]
 --pgsql_variant=STRING       Use this PostgreSQL variant when running with the PostgreSQL driver. The only currently supported variant is 'redshift'. When enabled, create_secondary is automatically disabled, and delete_inserts is set to 0
 --point_selects=N             Number of point SELECT queries per transaction [10]
 --range_selects[=on|off]     Enable/disable all range SELECT queries [on]
 --range_size=N               Range size for range SELECT queries [100]
 --secondary[=on|off]         Use a secondary index in place of the PRIMARY KEY [off]
 --simple_ranges=N             Number of simple range SELECT queries per transaction [1]
 --skip_trx[=on|off]           Don't start explicit transactions and execute all queries in the AUTOCOMMIT mode [off]
 --sum_ranges=N               Number of SELECT SUM() queries per transaction [1]
 --table_size=N               Number of rows per table [10000]
 --tables=N                   Number of tables [1]

此处我们指定下数据库存储引擎为myisam,执行:

1
sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20  oltp_read_write --mysql_storage_engine=myisam prepare

即可创建MyIsam的表。

2.2 进行读写测试
2.2.1 第一次执行100w次读写

执行命令

1
sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write   --mysql_storage_engine=myisam run

测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
......
SQL statistics:
  queries performed:
      read:                           722036
      write:                           206294
      other:                           103150
      total:                           1031480
  transactions:                       51574 (171.84 per sec.)
  queries:                             1031480 (3436.86 per sec.)
  ignored errors:                     0     (0.00 per sec.)
  reconnects:                         0     (0.00 per sec.)

General statistics:
  total time:                         300.1224s
  total number of events:             51574

Latency (ms):
        min:                                   2.91
        avg:                                 116.36
        max:                                 2703.01
        95th percentile:                     153.02
        sum:                             6001171.45

Threads fairness:
  events (avg/stddev):           2578.7000/7.62
  execution time (avg/stddev):   300.0586/0.04
2.2.2 第二次执行100w次读写

相同命令,再来一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
......
SQL statistics:
  queries performed:
      read:                           697480
      write:                           199279
      other:                           99641
      total:                           996400
  transactions:                       49820 (165.95 per sec.)
  queries:                             996400 (3319.08 per sec.)
  ignored errors:                     0     (0.00 per sec.)
  reconnects:                         0     (0.00 per sec.)

General statistics:
  total time:                         300.2032s
  total number of events:             49820

Latency (ms):
        min:                                   2.86
        avg:                                 120.45
        max:                                 3435.92
        95th percentile:                     176.73
        sum:                             6000916.19

Threads fairness:
  events (avg/stddev):           2491.0000/9.31
  execution time (avg/stddev):   300.0458/0.06
2.2.3 第三次执行100w次读写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
······
SQL statistics:
  queries performed:
      read:                           680372
      write:                           194389
      other:                           97199
      total:                           971960
  transactions:                       48598 (161.84 per sec.)
  queries:                             971960 (3236.88 per sec.)
  ignored errors:                     0     (0.00 per sec.)
  reconnects:                         0     (0.00 per sec.)

General statistics:
  total time:                         300.2761s
  total number of events:             48598

Latency (ms):
        min:                                   2.88
        avg:                                 123.52
        max:                                 2916.81
        95th percentile:                     189.93
        sum:                             6002904.07

Threads fairness:
  events (avg/stddev):           2429.9000/10.74
  execution time (avg/stddev):   300.1452/0.07
2.3 清理数据
1
sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20  oltp_read_write cleanup

测试总结

压测指标 InnoDB MyISAM
第一次 读、写次数 读:527282 写:150652 其他:75326 事务:37663 总计:753260 读:722036 写:206294 其他:103150 总计:1031480
耗时 最小:7.13ms 平均:159.35ms 最大:873.88ms 95分位数:292.60ms 最小:2.91ms 平均:116.36ms 最大:2703.01ms 95分位数:153.02ms
吞吐量 1883.1500 2578.7000
第二次 读、写次数 读:574434 写:164124 其他:82062 事务:41031 总计:820620 读:697480 写:199279 其他:99641 总计:996400
耗时 最小:6.62ms 平均:146.24ms 最大:709.59ms 95分位数:262.64ms 最小:2.86ms 平均:120.45ms 最大:3435.92ms 95分位数:176.73ms
吞吐量 2051.5500 2491.0000
第三次 读、写次数 读:595840 写:170240 其他:85120 事务:42560 总计:851200 读:680372 写:194389 其他:97199 总计:971960
耗时 最小:6.18ms 平均:141.02ms 最大:687.11ms 95分位数:257.95ms 最小:2.88ms 平均:123.52ms 最大:2916.81ms 95分位数:189.93ms
吞吐量 2128.0000 2429.9000

分析

纵向对比

经过预热之后,可以看到InnoDB的性能越来越好,每秒能处理2000多请求。不排除预热充分后能反超MyISAM的可能性,不过这个我就没继续多测试了。

MyISAM每秒能处理2500左右。

横向对比

从目前做的测试来看,InnoDB在读写性能上还是不及MyISAM,不排除预热充分后能反超MyISAM的可能性,不过这个我就没继续多测试了。

其他

图形化

可以将sysbench压测结果输出日志、然后绘制成图片,这样看着更高大上、更加直观。可以参考这篇文章:https://yq.aliyun.com/articles/498680

遇到的问题

  1. 本来想使用sysbench的预热功能,使用如下命令
1
sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write prewarm

但有报如下错误,时间有限,没仔细排查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[root@VM_0_5_centos ~]# sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write prewarm
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)

Initializing worker threads...

Prewarming table sbtest6
Prewarming table sbtest7
Prewarming table sbtest9
Prewarming table sbtest8
Prewarming table sbtest1
Prewarming table sbtest5
Prewarming table sbtest3
Prewarming table sbtest10
Prewarming table sbtest2
Prewarming table sbtest4
FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query 'SELECT AVG(id) FROM (SELECT * FROM sbtest2 FORCE KEY (PRIMARY) LIMIT 1000000) t'
FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query 'SELECT AVG(id) FROM (SELECT * FROM sbtest3 FORCE KEY (PRIMARY) LIMIT 1000000) t'
FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query 'SELECT AVG(id) FROM (SELECT * FROM sbtest4 FORCE KEY (PRIMARY) LIMIT 1000000) t'
FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query 'SELECT AVG(id) FROM (SELECT * FROM sbtest5 FORCE KEY (PRIMARY) LIMIT 1000000) t'
FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query 'SELECT AVG(id) FROM (SELECT * FROM sbtest6 FORCE KEY (PRIMARY) LIMIT 1000000) t'
FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query 'SELECT AVG(id) FROM (SELECT * FROM sbtest7 FORCE KEY (PRIMARY) LIMIT 1000000) t'
FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query 'SELECT AVG(id) FROM (SELECT * FROM sbtest8 FORCE KEY (PRIMARY) LIMIT 1000000) t'
FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query 'SELECT AVG(id) FROM (SELECT * FROM sbtest9 FORCE KEY (PRIMARY) LIMIT 1000000) t'
FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query 'SELECT AVG(id) FROM (SELECT * FROM sbtest10 FORCE KEY (PRIMARY) LIMIT 1000000) t'
FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query 'SELECT AVG(id) FROM (SELECT * FROM sbtest1 FORCE KEY (PRIMARY) LIMIT 1000000) t'
FATAL: `sysbench.cmdline.call_command' function failed: /usr/share/sysbench/oltp_common.lua:111: SQL error, errno = 2013, state = 'HY000': Lost connection to MySQL server during query

spring中加注解方法被同一个类内部方法调用导致AOP失效的解决方案(如@Transactional注解失效)

spring中加注解方法被同一个类内部方法调用导致AOP失效的解决方案(如@Transactional注解失效)

现象

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class A {
   //......
   
   @Transactional
   public void serviceA() {
      ......
  }
   
public void serviceB() {
      ......
       serviceA()
      ......
  }
}

大概多数人都会遇到这么一个坑,如果使用spring的@Transactional给A类中的某个业务方法serviceA()加事务,在controller层直接调用serviceA()时,该方法的事务注解能正常生效;但如果在A类中的serviceB()调用serviceA(),并且servcieB没有加事务@Transactional,那么此时实际上serviceA()上的@Transactional并未生效。

原因

想讲清这个问题就必须了解spring AOP的内部实现原理。

在spring中经常使用自定义注解或是spring已封装好的注解,通过AOP的方式复用代码、避免重复劳动。而spring实现AOP是通过动态代理来实现的,默认有接口的情况使用JDK的动态代理(也可以通过配置proxyTargetClass来指定使用CGLIB)、没有接口的情况使用CGLIB。

但无论是哪一种代理,都是在原有方法的外面包一层、在方法外的代理层来实现AOP的逻辑。以上面为例,在代理后会在serviceA()外层增加事务相关逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
//原始代码
serviceA() {
......
}

//加上注解后
serviceAProxy() {
//AOP执行前逻辑
......
serviceA()
//AOP执行后逻辑
......
}

但这个前提就是spring直接调用加了注解的方法才会调用代理方法,如果serviceA被同一个类内部其他方法调用,那servcieA()就只是一段普通代码、AOP相关的信息不会被spring看到,那自然就无法执行AOP的逻辑。

解决

知道原理后,自然也就好解决了。

AOP的逻辑在代理后的方法中,那么我们就去执行spring生成的代理后的方法。获取方法如下:

方法1:通过当前类对象获取

这个有点取巧。还是以上面serviceB()为例,我们如果调用this.serviceA(),this指向对象本身、不会指向代理后的对象,因此肯定不可以,但我们可以让spring提供给我们代理后的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class A{

   //通过spring将代理后对象注入到self变量
   @Autowired
   private A self;

   public void serviceB() {
          ......
           //此处调用的就是代理后的方法
           self.serviceA()
          ......
  }
}

方法2 从ApplicationContext中直接获取

获取ApplicationContext有多种方法:

方法2.1 使用AopContext.currentProxy()

1
2
3
4
5
6
7
8
public class A {
   public void serviceB() {
          ......
           //此处调用的就是代理后的方法
          ((A)AopContext.currentProxy()).serviceA();
          ......
  }
}

使用AopContext.currentProxy()注意必须在程序启动时开启EnableAspectJAutoProxy注解,设置代理暴露方式为true,如下面所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* EnableAspectJAutoProxy注解两个参数作用分别为:
*
* 一个是控制aop的具体实现方式,为true的话使用cglib,为false的话使用java的Proxy,默认为false。
* 第二个参数控制代理的暴露方式,解决内部调用不能使用代理的场景,默认为false。
*
*
*/
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@SpringBootApplication
public class SpringAopApplication {

public static void main(String[] args) {
SpringApplication.run(SpringAopApplication.class, args);
}
}

方法2.2 使用ApplicationContextAware

通过spring生命周期,直接将ApplicationContext注入进来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class A implements ApplicationContextAware {
private ApplicationContext applicationContext;
   
   @Override
   public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
       this.applicationContext = applicationContext;
  }
   
   
   public void serviceB() {
          ......
           //此处调用的就是代理后的方法
           applicationContext.getBean(A.class).serviceA();
          ......
  }
}

spring 自定义注解时使用AliasFor注解别名时 默认值无法被覆盖

spring 自定义注解时使用AliasFor注解别名时 默认值无法被覆盖

事情是这样子的:

我想自定义一个缓存注解,用来缓存方法返回值,并且支持自定义缓存超时时间,注解定义是这样:

1
2
3
4
5
6
7
8
9
10
11
12

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface MyCache {

   @AliasFor("value")
   int expireTime() default 60;

   @AliasFor("expireTime")
   int value() default 60;
}

本来只想写一个expireTime,但想着大家肯定能偷懒尽量偷懒嘛,那最好还是支持 @MyCache(50) 这种写法、省去一步。于是也有了上面的写法,使用spring提供的@AliasFor,显式的定义两个属性互为别名,目的就是如果不在注解中指定属性,则默认就是将值给到超时时间expireTime。

这样写也是看了spring里@Transactional注解的定义,按道理,应该是我配置了 @MyCache(50) 后,我在Advice代码中取这个注解的expireTime属性值时,应该是拿到value的值,即50,但实际效果却不是这样,expireTime的值仍为默认的60。

而我取值的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Around("myCache()")
public Object around(ProceedingJoinPoint pjp) {
  ......
   //获取方法对象
   Method curMethod = getMethodByJoinPoint(pjp, methodSignature);
   //出问题的是这一句!
   Annotation annotation = curMethod.getAnnotation(MyCache.class);
   Integer expireTime = (Integer) getAnnotationConfig(annotation, "expireTime");
   Integer annotationValue = (Integer) getAnnotationConfig(annotation, "value");
   log.info("annotationValue={}, expireTime={}.", annotationValue, expireTime);
  ......
}

private Method getMethodByJoinPoint(ProceedingJoinPoint pjp, MethodSignature methodSignature) throws NoSuchMethodException {
   Object target = pjp.getTarget();
   return target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
}

private Object getAnnotationConfig(Annotation annotation, String name) {
   if (null == annotation || StringUtils.isEmpty(name)) {
       return null;
  }

   try {
       return annotation.annotationType().getMethod(name).invoke(annotation);
  } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
       log.error("getAnnotationConfig error:", e);
  }

   return null;
}

获取注解这一步有问题,@AliasFor是spring提供的注解,想要保证获取属性值能感知到,需要使用spring提供的工具方法获得注解,如下所示:

1
2
3
4
5
......
Annotation annotation2 = AnnotationUtils.getAnnotation(curMethod, MyCache.class);
Integer annotationVal2 = (Integer) getAnnotationConfig(annotation2, "expireTime");
log.info("annotationVal2={}.", annotationVal2);
......

进一步延伸:如果注解里的两个属性相互加别名后,使用注解时两个都设置了值,会怎样?

类似这样:

1
2
3
4
5
@MyCache(expireTime = 40, value = 50)
public String getValByCache(String key) {
   log.info("getValByCache running...");
   return key + "-" + idx.incrementAndGet();
}

**程序直接无法启动!**会报如下异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
......
2020-11-20 16:54:18.628 INFO 12076 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2020-11-20 16:54:18.878 WARN 12076 --- [ restartedMain] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [D:\GitRepository\framework-dev-learning\spring-aop-2-customzed-cache-annotation\target\classes\com\prayerlaputa\service\MyCacheDemoService.class]; nested exception is org.springframework.core.annotation.AnnotationConfigurationException: Different @AliasFor mirror values for annotation [com.prayerlaputa.annotation.MyCache] declared on com.prayerlaputa.service.MyCacheDemoService.getValByCache(java.lang.String); attribute 'expireTime' and its alias 'value' are declared with values of [40] and [50].
2020-11-20 16:54:18.945 ERROR 12076 --- [ restartedMain] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [D:\GitRepository\framework-dev-learning\spring-aop-2-customzed-cache-annotation\target\classes\com\prayerlaputa\service\MyCacheDemoService.class]; nested exception is org.springframework.core.annotation.AnnotationConfigurationException: Different @AliasFor mirror values for annotation [com.prayerlaputa.annotation.MyCache] declared on com.prayerlaputa.service.MyCacheDemoService.getValByCache(java.lang.String); attribute 'expireTime' and its alias 'value' are declared with values of [40] and [50].
at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.scanCandidateComponents(ClassPathScanningCandidateComponentProvider.java:452) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.findCandidateComponents(ClassPathScanningCandidateComponentProvider.java:315) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:276) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.annotation.ComponentScanAnnotationParser.parse(ComponentScanAnnotationParser.java:132) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:295) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:249) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:206) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:174) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:319) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:236) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:275) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:95) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:706) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:532) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747) [spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) [spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) [spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) [spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at com.prayerlaputa.SpringAopApplication.main(SpringAopApplication.java:10) [classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_161]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_161]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_161]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_161]
at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) [spring-boot-devtools-2.2.6.RELEASE.jar:2.2.6.RELEASE]
Caused by: org.springframework.core.annotation.AnnotationConfigurationException: Different @AliasFor mirror values for annotation [com.prayerlaputa.annotation.MyCache] declared on com.prayerlaputa.service.MyCacheDemoService.getValByCache(java.lang.String); attribute 'expireTime' and its alias 'value' are declared with values of [40] and [50].
at org.springframework.core.annotation.AnnotationTypeMapping$MirrorSets$MirrorSet.resolve(AnnotationTypeMapping.java:656) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.core.annotation.AnnotationTypeMapping$MirrorSets.resolve(AnnotationTypeMapping.java:611) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.core.annotation.TypeMappedAnnotation.<init>(TypeMappedAnnotation.java:136) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.core.annotation.TypeMappedAnnotation.<init>(TypeMappedAnnotation.java:120) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.core.annotation.TypeMappedAnnotation.of(TypeMappedAnnotation.java:636) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.core.annotation.MergedAnnotation.of(MergedAnnotation.java:596) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.core.type.classreading.MergedAnnotationReadingVisitor.visitEnd(MergedAnnotationReadingVisitor.java:96) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.asm.ClassReader.readElementValues(ClassReader.java:2985) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.asm.ClassReader.readMethod(ClassReader.java:1393) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.asm.ClassReader.accept(ClassReader.java:718) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.asm.ClassReader.accept(ClassReader.java:401) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.core.type.classreading.SimpleMetadataReader.<init>(SimpleMetadataReader.java:50) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.core.type.classreading.SimpleMetadataReaderFactory.getMetadataReader(SimpleMetadataReaderFactory.java:103) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.core.type.classreading.CachingMetadataReaderFactory.getMetadataReader(CachingMetadataReaderFactory.java:123) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.scanCandidateComponents(ClassPathScanningCandidateComponentProvider.java:428) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
... 25 common frames omitted


Process finished with exit code 0

参考资料

分布式一致性与共识之(一)Raft

分布式一致性与共识之(一)Raft

Raft基础

在raft中,服务器节点共有3种状态(或者说角色):

  • leader

    • 负责client交互和log复制,同一时刻系统中最多存在1个。
  • follower

    • 节点的初始状态就是follower,follower只能被动响应请求RPC,从不主动发起请求RPC。
    • 如果follower在一定时间范围(后面会讲,这个叫election timeout)内没有收到leader的请求,则follower可以转变成candidate(If followers don’t hear from a leader then they can become a candidate.)
  • candidate

    • 一个临时的状态,只存在于选举leader阶段。一个节点想要变成leader,那么就发起投票请求,同时自己变成candidate。如果选举成功,则变为candidate,否则退回为follower。其他节点根据当前状态回复是否,如果已经投票给其他candidate,则不会再投票给这个candidate(一个节点只有一票)。
    • 获得多数选票的candidate(n/2+1)将会变成leader。

3种状态的流转参见此图:

Raft关键问题

  • 领导选取
  • 日志复制
  • 安全
  • 成员变化

Raft采用复制状态机(state machine replication)模型,通过复制日志来保证状态的一致性,详细内容参见wiki https://en.wikipedia.org/wiki/State_machine_replication 基本思想是:

  • 日志:每台机器保存一份日志,日志来自于客户端的请求,包含一系列的命令
  • 状态机:状态机会按顺序执行这些命令
  • 一致性模型:分布式环境下,保证多机的日志是一致的,这样回放到状态机中的状态是一致的

复制状态机机制可以参考这张图:

Raft算法流程

先看 动画演示 http://thesecretlivesofdata.com/raft/

领导选举 leader election

Raft中使用心跳机制来出发leader选举。当服务器启动的时候,服务器成为follower。只要follower从leader或者candidate收到有效的RPCs就会保持follower状态。如果follower在一段时间内(该段时间被称为election timeout)没有收到消息,则它会假设当前没有可用的leader,然后开启选举新leader的流程。

1. 任期 term

Raft算法将时间划分成为任意不同长度的任期(term),任期用连续的数字进行表示。每一个任期的开始都是一次选举(election),一个或多个候选人会试图成为领导人。如果一个候选人赢得了选举,它就会在该任期的剩余时间担任领导人。在某些情况下,选票会被瓜分,有可能没有选出领导人,那么,将会开始另一个任期,并且立刻开始下一次选举。Raft 算法保证在给定的一个任期最多只有一个领导人。

2. RPC

Raft 算法中服务器节点之间通信使用远程过程调用(RPCs),并且基本的一致性算法只需要两种类型的 RPCs,为了在服务器之间传输快照增加了第三种 RPC。

RPC有三种:

  • RequestVote RPC:候选人在选举期间发起
  • AppendEntries RPC:领导人发起的一种心跳机制,复制日志也在该命令中完成
  • InstallSnapshot RPC: 领导者使用该RPC来发送快照给太落后的追随者

3. 选举流程说明

(1)follower增加当前的term,转变为candidate。 (2)candidate投票给自己,并发送RequestVote RPC给集群中的其他服务器。 (3)收到RequestVote的服务器,在同一term中只会按照先到先得投票给至多一个candidate。且只会投票给log至少和自身一样新的candidate。

一个节点node1发起选举后,会发生如下3种情况中的一种:

  • a. 该节点赢得选举。即收到大多数的节点的投票。则node1转变为leader状态。
  • b. 另一个节点node2成为了leader。即收到了leader的合法心跳包(term值等于或大于当前自身term值)。则node1转变为follower状态。
  • c. 一段时间后依然没有胜者。该种情况下会开启新一轮的选举。

4. 2个超时时间:election timeout和heartbeat timeout

election timeout

从follower状态成为candidate状态需要等待的时间。在这个时间内,节点必须等待,不能成为candidate状态。

该超时时长随机,在150ms-300ms之间,超时后,follower将成为candidate,启动一个新的election term,并投自己一票,并发送request vote 给其他节点。如果接收到的节点在当前term中还未投票(vote),就会将投票给这个candidate,并重置自己的election timeout。

heartbeat timeout

心跳超时时间。candidate获得多数选票后将会变成leader,leader将开始发送append entries消息给它的follower,这是一个心跳消息,可能为空,也可能带有需要复制的日志数据,这类消息在heartbeat timeout时间间隔内发送;follower将会响应每个append entries。如果超过心跳超时时间内,follower没收到leader的消息,将会认为leader挂掉、会开始新的选举周期。

有关append entries的解析,可以参考这篇:Raft系列文章之三:Raft RPC详解

另一方面,这也说明,日志数据实际上都是跟着心跳消息发送给follower的,并不是说client发起一个修改、leader就立马开始同步数据。参见下文“日志复制”相关。

5. 问题:split vote

上述”3.选举流程”中的情况c.,最典型的就是split vote,当两个节点同时转变为candidate、发起选举时,就会出现该情况。

假设4个节点的情况,A B分别发起选举,两个节点在同一个term中启动election,并且这两个vote都在对方之前抵达一个单独的节点,此时A B分别拿到2个投票(包括自己投自己的那一票)、且在这个term中不会有更多votes,两个节点都没拿到多数选票。这些节点将会等待、在下个term中启动选举。由于选举超时时间(election timeout)是随机的,这两个candidate节点不可能每次election timeout时间都相同,也就可以保证这个选举过程最终会选出一个leader。

日志复制 log replication

日志复制(Log Replication)主要作用是用于保证节点的一致性,这阶段所做的操作也是为了保证一致性与高可用性。

注意,在Raft中的日志,是指可能引起状态变化的操作,查看操作不引起状态变化、并不包含在下面讨论的日志范围内。

当leader选举出来后便开始负责客户端的请求,所有事务(更新操作)请求都必须先经过leader处理,日志复制(Log Replication)就是为了保证执行相同的操作序列所做的工作。

leader需要将所有数据变化复制给系统中所有节点。这需要用到在维持心跳中用到的append entries消息。

整体流程如下:

  • 在Raft中当leader接收到客户端的日志(事务请求)后先把该日志追加到本地的log中,此时的日志是uncommitted的,并不会更新节点值;
  • 接着,leader节点将复制这条日志给所有follower节点(数据会通过下一个heartbeat中append entry消息发送给所有follower),follower接收到日志后记录日志然后向leader发送ACK;
  • 当leader收到大多数(n/2+1)节点的ACK信息后,leader将这条日志设置为committed、将日志应用到本地的状态机(根据client请求修改状态),并返回给client。

可以参考这篇Raft 日志复制 Log replication

网络分区问题与解决 network partition

Raft在面对网络分区问题时仍能保证一致性。 假设,由于网络分区,导致一个Raft集群分成了两部分,有2个leader

此时若有client去更新数据,如果分区内能够达到整个集群的多数,就能commit成功,但可能出现以给一个分区提交成功、另外一个分区的节点无感知。 如

故障恢复时,重新接入的分区会比较election term,比较高版本的leader的日志,使得自身的版本与新版本一致

这样整个集群将会恢复成一致的状态。

Raft的算法实现与应用

Raft算法的论文相比Paxos直观很多,更容易在工程上实现。

目前Raft算法实现已经很多,参见: https://raft.github.io/#implementations

应用场景

典型的如etcd、nacos等。

etcd目标是构建一个高可用的分布式键值(key-value)数据库,基于 Go 语言实现。Etcd 主要用途是共享配置和服务发现,实现一致性使用了Raft算法。

etcd参见https://etcd.io/

其他

本文主要参考、整理自 https://www.cnblogs.com/binyue/p/8647733.html 这篇文章还有一些内容不错,引用如下:

三、Raft和Paxos的工程应用

……

2.Zookeeper 中的 Paxos

Zookeeper 使用了一种修改后的 Paxos 协议。

在 Zookeeper 中,始终分为两种场景:

  • Leader activation

在这个场景里,系统中缺乏 Leader(primary),通过一个类似 paxos 协议的过程完成 Leader 选举。

  • Active messaging 在 这个场景里,Leader 接收客户端发送的更新操作,以一种类似两阶段提交的过程在各个 follower (secondary)节点上进行更新操作。

在 Leader activation 场景中完成 leader 选举及数据同步后,系统转入 Active messaging 场景,在 active messaging 中 leader 异常后,系统转入 Leader activation 场景。

无论在那种场景,Zookeeper 依赖于一个全局版本号:zxid。zxid 由(epoch, count)两部分组成, 高位的 epoch 部分是选举编号,每次提议进行新的 leader 选举时 epoch 都会增加,低位的 count 部分 是 leader 为每个更新操作决定的序号。可以认为,一个 leader 对应一个唯一的 epoch,每个 leader 任期内产生的更新操作对应一个唯一的有序的 count,从而从全局的视野,一个 zxid 代表了一个更新操作的全局序号(版本号)。

Zookeeper 通过 zxid 将两个场景阶段较好的结合起来,且能保证全局的强一致性。由于同一时刻只有一个 zookeeper 节点能获得超过半数的 follower,所以同一时刻最多只存在唯一的 leader;每个 leader 利用 FIFO 以 zxid 顺序更新各个 follower,只有成功完成前一个更新操作的才会进行下一个更新操作,在同一个 leader 任期内,数据在全局满足 quorum 约束的强一致,即读超过半数的节点 一定可以读到最新已提交的数据;每个成功的更新操作都至少被超过半数的节点确认,使得新选举 的 leader 一定可以包括最新的已成功提交的数据。

3.如何解决split brain问题

分布式协议一个著名问题就是 split brain 问题。

简单说,就是比如当你的 cluster 里面有两个结点,它们都知道在这个 cluster 里需要选举出一个 master。那么当它们两之间的通信完全没有问题的时候,就会达成共识,选出其中一个作为 master。但是如果它们之间的通信出了问题,那么两个结点都会觉得现在没有 master,所以每个都把自己选举成 master。于是 cluster 里面就会有两个 master。

区块链的分叉其实类似分布式系统的split brain。

一般来说,Zookeeper会默认设置:

  • zookeeper cluster的节点数目必须是奇数。
  • zookeeper 集群中必须超过半数节点(Majority)可用,整个集群才能对外可用。

Majority 就是一种 Qunroms 的方式来支持Leader选举,可以防止 split brain出现。奇数个节点可以在相同容错能力的情况下节省资源。

四、从CAP的角度理解几种不同的算法

1.两阶段提交协议

两阶段提交系统具有完全的C,很糟糕的A,很糟糕的P。 首先,两阶段提交协议保证了副本间是完全一致的,这也是协议的设计目的。再者,协议在一个节点出现异常时,就无法更新数据,其服务可用性较低。最后,一旦协调者与参与者之间网络分化,无法提供服务。

2.Paxos和Raft算法

Paxos 协议和Raft算法都是强一致性协议。Paxos只有两种情况下服务不可用:一是超过半数的 Proposer 异常,二是出现活锁。前者可以通过增加 Proposer 的个数来 降低由于 Proposer 异常影响服务的概率,后者本身发生的概率就极低。最后,只要能与超过半数的 Proposer 通信就可以完成协议流程,协议本身具有较好的容忍网络分区的能力。

参考 Raft一致性算法 Raft 一致性算法论文译文

参考资料

源码分析之Eureka服务端源码解析

源码分析之Eureka服务端源码解析

说明

若无特别说明,本文所涉及的代码版本为spring boot/cloud 2.2.6。

eureka server基本功能

  • 接受服务注册
  • 接受服务心跳
  • 服务剔除
  • 服务下线
  • 集群同步
  • 获取注册表中服务实例信息

需要注意的是,Eureka Server同时也是一个Eureka Client,在不禁止Eureka Server的客户端行为时,它会向它配置文件中的其他Eureka Server进行拉取注册表、服务注册和发送心跳等操作

eureka架构放一张图,镇楼

启动eureka-server注册相关bean

与eureka-client类似,eureka-server启动也是依赖spring factories机制初始化、并将对象加载到spring容器中。

spring-cloud-netflix-eureka-server-2.2.2.REALEASE.jar为例,打开该包中的spring.factories,可以看到

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration

即启动时自动加载EurekaServerAutoConfiguration类,而该类的功能是向spring的bean工厂添加eureka-server相关功能的bean。

EurekaServerAutoConfiguration上有一个注解:@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class),意思是:只有在Spring容器里有Marker这个类的实例时,才会加载EurekaServerAutoConfiguration,这个就是控制是否开启Eureka Server的关键。

开启eureka server

在使用eureka-server时,我们所加的@EnableEurekaServer注解,实现如下:

1
2
3
4
5
6
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {
}

此处利用spring提供的@Import注解,将EurekaServerMarkerConfiguration动态注入到spring 容器中,这也刚好提供了上面spring factories里EurekaServerAutoConfiguration实例化的前提:必须有EurekaServerMarkerConfiguration

即启动时初始化顺序:

  • 添加EnableEurekaServer注解后,通过@Import引入EurekaServerMarkerConfiguration
  • 利用spring factories,加载EurekaServerAutoConfiguration

开启注册

在spring-cloud-netflix-eureka-server:2.2.2.RELEASE中,EurekaServerAutoConfiguration声明如下:

1
2
3
4
5
6
7
8
9
@Configuration(proxyBeanMethods = false)
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
  ......
}

其中,通过@Import引入的EurekaServerInitializerConfiguration类声明如下:

1
2
3
4
5
6
@Configuration(proxyBeanMethods = false)
public class EurekaServerInitializerConfiguration
implements ServletContextAware, SmartLifecycle, Ordered {
  ......
}
   

SmartLifecycle的作用是:初始化完之后,执行public void start()方法。

EurekaServerInitializerConfiguration中start()方法:

  • 启动一个线程;
  • 上下文初始化
  • 利用spring的事件机制,发布一个EurekaRegistryAvailableEvent事件
  • 更改running状态
  • 利用spring的事件机制,发布一个EurekaServerStartedEvent事件

在这个start() 上下文初始化这一步,还会从相邻eureka节点注册表,方法调用路径如下:

1
2
3
4
5
EurekaServerInitializerConfiguration#start()
EurekaServerBoostrap#contextInitialized(ServletContext context)
EurekaServerBoostrap#initEurekaServerContext()
// Copy registry from neighboring eureka node 此处进行复制相邻eureka注册表的操作
int registryCount = this.registry.syncUp();

在EurekaServerAutoConfiguration中会实例化EurekaServerContext,代码如下:

1
2
3
4
5
6
7
@Bean
@ConditionalOnMissingBean
public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,
PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs,
registry, peerEurekaNodes, this.applicationInfoManager);
}

DefaultEurekaServerContext中初始化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @PostConstruct
   @Override
   public void initialize() {
       logger.info("Initializing ...");
       //启动一个只拥有一个线程的线程池,第一次进去会更新一下集群其他节点信息。
       peerEurekaNodes.start();
       try {
//调用PeerAwareInstanceRegistryImpl的init方法
           registry.init(peerEurekaNodes);
      } catch (Exception e) {
           throw new RuntimeException(e);
      }
       logger.info("Initialized");
  }

在spring-cloud-netflix-eureka-server中,PeerAwareInstanceRegistryImpl的继承/实现关系参见下图:

eureka server围绕注册表管理的,也就是上面的InstanceRegistry。

注册表管理核心类 InstanceRegistry

参考上图,依次说明如下:

  • InstanceRegistry

    • 有两种InstanceRegistry,参考如下:
    • com.netflix.eureka.registry.InstanceRegistry是euraka server中注册表管理的核心接口。职责是在内存中管理注册到Eureka Server中的服务实例信息。实现类有PeerAwareInstanceRegistryImpl。
    • org.springframework.cloud.netflix.eureka.server.InstanceRegistry 对PeerAwareInstanceRegistryImpl进行了继承和扩展,使其适配Spring cloud的使用环境,主要的实现由PeerAwareInstanceRegistryImpl提供。
  • LeaseManager

    • 对注册到Eureka Server中的服务实例租约进行管理,方法有:服务注册,下线,续约,剔除。此接口管理的类目前是InstanceInfo。InstanceInfo代表服务实例信息。
  • LookupService

    • 提供服务实例的检索查询功能。
  • PeerAwareInstanceRegistryImpl

    • 增加了对peer节点的同步复制操作。使得eureka server集群中注册表信息保持一致。

服务注册

Eureka Client在发起服务注册时会将自身的服务实例元数据封装在InstanceInfo中,然后将InstanceInfo发送到Eureka Server。Eureka Server在接收到Eureka Client发送的InstanceInfo后将会尝试将其放到本地注册表中以供其他Eureka Client进行服务发现。

EurekaServerAutoConfiguration中定义了 public FilterRegistrationBean jerseyFilterRegistration ,表明eureka-server使用了Jersey实现 对外的RESTful接口。注册一个 Jersey 的 filter ,配置好相应的Filter 和 url映射。

在com.netflix.eureka.resources包下,是Eureka Server对于Eureka client的REST请求的定义。看ApplicationResource类(这是一类请求,应用类的请求),类似于应用@Controller注解:@Produces({“application/xml”, “application/json”}),接受xml和json。

例如ApplicationResource类中,有如下方法(添加实例接口):

1
2
3
4
5
6
7
8
9
    @POST
   @Consumes({"application/json", "application/xml"})
   public Response addInstance(InstanceInfo info,
                               @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
      ......
        //registry是PeerAwareInstanceRegistry类型
       registry.register(info, "true".equals(isReplication));
      ......
  }

上面的registry.register方法,可以追溯到PeerAwareInstanceRegistryImpl的方法:public void register(final InstanceInfo info, final boolean isReplication) ,中有一句:super.register(info, leaseDuration, isReplication); 将会进入:com.netflix.eureka.registry.AbstractInstanceRegistry的 register方法

在register方法中,服务实例的InstanceInfo保存在Lease中,Lease在AbstractInstanceRegistry中统一通过ConcurrentHashMap保存在内存中。在服务注册过程中,会先获取一个读锁,防止其他线程对registry注册表进行数据操作,避免数据的不一致。然后从resgitry查询对应的InstanceInfo租约是否已经存在注册表中,根据appName划分服务集群,使用InstanceId唯一标记服务实例。

  • 如果租约存在,比较两个租约中的InstanceInfo的最后更新时间lastDirtyTimestamp,保留时间戳大的服务实例信息InstanceInfo。
  • 如果租约不存在,意味这是一次全新的服务注册,将会进行自我保护的统计,创建新的租约保存InstanceInfo。接着将租约放到resgitry注册表中。

之后将进行一系列缓存操作并根据覆盖状态规则设置服务实例的状态,缓存操作包括将InstanceInfo加入用于统计Eureka Client增量式获取注册表信息的recentlyChangedQueue和失效responseCache中对应的缓存。最后设置服务实例租约的上线时间用于计算租约的有效时间,释放读锁并完成服务注册。

流程图如下:

接收服务心跳请求

在Eureka Client完成服务注册之后,它需要定时向Eureka Server发送心跳请求(默认30秒一次),维持自己在Eureka Server中租约的有效性。

此代码实现在com.netflix.eureka.resources.InstanceResource中,参见此方法:

1
2
3
4
5
6
7
8
9
10
@PUT
public Response renewLease(
           @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
           @QueryParam("overriddenstatus") String overriddenStatus,
           @QueryParam("status") String status,
           @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
  ......
   boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
  ......
}

registry类型为PeerAwareInstanceRegistry,追溯后会发现最终实现是在AbstractInstanceRegistry#renew方法。

renew方法是对Eureka Client位于注册表中的租约的续租操作,不像register方法需要服务实例信息,仅根据服务实例的服务名和服务实例id即可更新对应租约的有效时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public boolean renew(String appName, String id, boolean isReplication) {
       RENEW.increment(isReplication);
    //根据appName获取服务集群的租约集合
       Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
       Lease<InstanceInfo> leaseToRenew = null;
       if (gMap != null) {
           leaseToRenew = gMap.get(id);
      }
       if (leaseToRenew == null) {
           RENEW_NOT_FOUND.increment(isReplication);
           logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
           return false;
      } else {
           InstanceInfo instanceInfo = leaseToRenew.getHolder();
           if (instanceInfo != null) {
               // touchASGCache(instanceInfo.getASGName());
               //查看服务实例状态
               InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
                       instanceInfo, leaseToRenew, isReplication);
               if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                   logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}"
                           + "; re-register required", instanceInfo.getId());
                   RENEW_NOT_FOUND.increment(isReplication);
                   return false;
              }
               if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
                   logger.info(
                           "The instance status {} is different from overridden instance status {} for instance {}. "
                                   + "Hence setting the status to overridden status", instanceInfo.getStatus().name(),
                                   instanceInfo.getOverriddenStatus().name(),
                                   instanceInfo.getId());
                   instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);

              }
          }
            //统计每分钟续租次数
           renewsLastMin.increment();
            //更新租约
           leaseToRenew.renew();
           return true;
      }
  }

此方法中不关注InstanceInfo,仅关注于租约本身以及租约的服务实例状态。如果根据服务实例的appName和instanceInfoId查询出服务实例的租约,并且根据#getOverriddenInstanceStatus方法得到的instanceStatus不为InstanceStatus.UNKNOWN,那么更新租约中的有效时间,即更新租约Lease中的lastUpdateTimestamp,达到续约的目的;如果租约不存在,那么返回续租失败的结果。

下图给出了eureka服务端接收心跳、租约更新流程。

服务剔除

如果Eureka Client在注册后,既没有续约,也没有下线(服务崩溃或者网络异常等原因),那么服务的状态就处于不可知的状态,不能保证能够从该服务实例中获取到回馈,所以需要服务剔除此方法定时清理这些不稳定的服务,该方法会批量将注册表中所有过期租约剔除。

剔除是定时任务,默认60秒执行一次。延时60秒,间隔60秒 evictionTimer.schedule(evictionTaskRef.get(), serverConfig.getEvictionIntervalTimerInMs(), serverConfig.getEvictionIntervalTimerInMs()); 从上面eureka server启动来看,剔除的任务,是线程启动的,执行的是下面的方法。 com.netflix.eureka.registry.AbstractInstanceRegistry#evict

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

//isLeaseExpirationEnabled在
public boolean isLeaseExpirationEnabled在() {
   if (!isSelfPreservationModeEnabled()) {
       //此处获取自我保护模式开关状态,最终对应于EurekaServerConfigBean类中的enableSelfPreservation配置项,
       // The self preservation mode is disabled, hence allowing the instances to expire.
       return true;
  }
   return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}

public void evict(long additionalLeaseMs) {
   logger.debug("Running the evict task");

   if (!isLeaseExpirationEnabled()) {
       logger.debug("DS: lease expiration is currently disabled.");
       return;
  }

   // We collect first all expired items, to evict them in random order. For large eviction sets,
   // if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it,
   // the impact should be evenly distributed across all applications.
   List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
   /*
   遍历注册表register,依次判断租约是否过期。一次性获取所有的过期租约。
   */
   for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
       Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
       if (leaseMap != null) {
           for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
               Lease<InstanceInfo> lease = leaseEntry.getValue();
               if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
                   expiredLeases.add(lease);
              }
          }
      }
  }

   // To compensate for GC pauses or drifting local time, we need to use current registry size as a base for
   // triggering self-preservation. Without that we would wipe out full registry.
   //获取注册表租约总数
   int registrySize = (int) getLocalRegistrySize();
   //计算注册表租约的阈值 (总数乘以 续租百分比),得出要续租的数量
   int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
   //总数减去要续租的数量,就是理论要剔除的数量        
   int evictionLimit = registrySize - registrySizeThreshold;
//求 上面理论剔除数量,和过期租约总数的最小值。就是最终要提出的数量。
   int toEvict = Math.min(expiredLeases.size(), evictionLimit);
   if (toEvict > 0) {
       logger.info("Evicting {} items (expired={}, evictionLimit={})", toEvict, expiredLeases.size(), evictionLimit);

       Random random = new Random(System.currentTimeMillis());
       for (int i = 0; i < toEvict; i++) {
           // Pick a random item (Knuth shuffle algorithm)
           int next = i + random.nextInt(expiredLeases.size() - i);
           Collections.swap(expiredLeases, i, next);
           Lease<InstanceInfo> lease = expiredLeases.get(i);

           String appName = lease.getHolder().getAppName();
           String id = lease.getHolder().getId();
           EXPIRED.increment();
           logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
           //执行 服务下线将服务从注册表清除掉。
           internalCancel(appName, id, false);
      }
  }
}

剔除的限制

1.自我保护期间不清除。 2.分批次清除。 3.服务是逐个随机剔除,剔除均匀分布在所有应用中,防止在同一时间内同一服务集群中的服务全部过期被剔除,造成在大量剔除服务时,并在进行自我保护时,促使程序崩溃。

剔除服务的定时任务

剔除服务是个定时任务,用EvictionTask执行,默认60秒执行一次,延时60秒执行。定时剔除过期服务。

代码:

1
2
3
4
5
6
EurekaServerInitializerConfiguration#start()
eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
EurekaServerBoostrap#initEurekaServerContext()
registry.openForTraffic(this.applicationInfoManager, registryCount);
PeerAwareInstanceRegistryImpl#openForTraffic
AbstractInstanceRegistry#postInit()

服务剔除将会遍历registry注册表,找出其中所有的过期租约,然后根据配置文件中续租百分比阀值和当前注册表的租约总数量计算出最大允许的剔除租约的数量(当前注册表中租约总数量减去当前注册表租约阀值),分批次剔除过期的服务实例租约。对过期的服务实例租约调用AbstractInstanceRegistry#internalCancel服务下线的方法将其从注册表中清除掉。

自我保护机制

自我保护机制主要在Eureka Client和Eureka Server之间存在网络分区的情况下发挥保护作用,在服务器端和客户端都有对应实现。假设在某种特定的情况下(如网络故障),Eureka Client和Eureka Server无法进行通信,此时Eureka Client无法向Eureka Server发起注册和续约请求,Eureka Server中就可能因注册表中的服务实例租约出现大量过期而面临被剔除的危险,然而此时的Eureka Client可能是处于健康状态的(可接受服务访问),如果直接将注册表中大量过期的服务实例租约剔除显然是不合理的。

针对这种情况,Eureka设计了“自我保护机制”。在Eureka Server处,如果出现大量的服务实例过期被剔除的现象,那么该Server节点将进入自我保护模式,保护注册表中的信息不再被剔除,在通信稳定后再退出该模式;在Eureka Client处,如果向Eureka Server注册失败,将快速超时并尝试与其他的Eureka Server进行通信。“自我保护机制”的设计大大提高了Eureka的可用性。

有关自我保护机制的流程图:

服务下线

Eureka Client在应用销毁时,会向Eureka Server发送服务下线请求,清除注册表中关于本应用的租约,避免无效的服务调用。在服务剔除的过程中,也是通过服务下线的逻辑完成对单个服务实例过期租约的清除工作。

对应代码:com.netflix.eureka.resources.InstanceResource 的

1
2
3
4
5
6
7
    @DELETE
   public Response cancelLease(
           @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
      ......
       boolean isSuccess = registry.cancel(app.getName(), id, "true".equals(isReplication));
      ......
  }

继续追踪:

1
2
3
4
InstanceResource#cancelLease
PeerAwareInstanceRegistryImpl#cancel //此方法中还有将取消操作复制给其他eureka节点的逻辑
AbstractInstanceRegistry#cancel
AbstractInstanceRegistry#internalCancel

最终实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
protected boolean internalCancel(String appName, String id, boolean isReplication) {
   try {
       //先获取读锁,防止被其他线程修改
       read.lock();
       CANCEL.increment(isReplication);
       //根据appName获取服务实例集群。
       Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
       Lease<InstanceInfo> leaseToCancel = null;
       // 在内存中取消实例 id的服务
       if (gMap != null) {
           leaseToCancel = gMap.remove(id);
      }
       //添加到最近下线服务的统计队列
       recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
       InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
       if (instanceStatus != null) {
           logger.debug("Removed instance id {} from the overridden map which has value {}", id, instanceStatus.name());
      }
       // 往下判断leaseToCancel是否为空,租约不存在,返回false
       if (leaseToCancel == null) {
           CANCEL_NOT_FOUND.increment(isReplication);
           logger.warn("DS: Registry: cancel failed because Lease is not registered for: {}/{}", appName, id);
           return false;
      } else {
           //若存在,设置租约下线时间
           leaseToCancel.cancel();
           InstanceInfo instanceInfo = leaseToCancel.getHolder();
           String vip = null;
           String svip = null;
           if (instanceInfo != null) {
               //获取持有租约的服务信息,标记服务实例为instanceInfo.setActionType(ActionType.DELETED);
               instanceInfo.setActionType(ActionType.DELETED);
               //添加到租约变更记录队列
               recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));//用于eureka client的增量拉取注册表信息。
               instanceInfo.setLastUpdatedTimestamp();
               vip = instanceInfo.getVIPAddress();
               svip = instanceInfo.getSecureVipAddress();
          }
           invalidateCache(appName, vip, svip);
           logger.info("Cancelled instance {}/{} (replication={})", appName, id, isReplication);
      }
  } finally {
       read.unlock();
  }

   synchronized (lock) {
       if (this.expectedNumberOfClientsSendingRenews > 0) {
           // Since the client wants to cancel it, reduce the number of clients to send renews.
           this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;
           updateRenewsPerMinThreshold();
      }
  }

   return true;
}

上述代码主要功能: 首先通过registry根据服务名和服务实例id查询关于服务实例的租约Lease是否存在,统计最近请求下线的服务实例用于Eureka Server主页展示。如果租约不存在,返回下线失败;如果租约存在,从registry注册表中移除,设置租约的下线时间,同时在最近租约变更记录队列中添加新的下线记录,以用于Eureka Client的增量式获取注册表信息。

集群同步

如果Eureka Server是通过集群的方式进行部署,那么为了维护整个集群中Eureka Server注册表数据的一致性,势必需要一个机制同步Eureka Server集群中的注册表数据。

Eureka Server集群同步包含两个部分:

  • Eureka Server在启动过程中从它的peer节点中拉取注册表信息,并将这些服务实例的信息注册到本地注册表中;
  • Eureka Server每次对本地注册表进行操作时,同时会将操作同步到它的peer节点中,达到集群注册表数据统一的目的。

启动拉取别的peer

Eureka Server启动类中,EurekaServerAutoConfiguration通过@Import注解加载EurekaServerInitializerConfiguration

1
2
3
4
EurekaServerInitializerConfiguration#start()
eurekaServerBootstrap.contextInitialized(...)
EurekaServerBootstrap#initEurekaServerContext()
PeerAwareInstanceRegistryImpl#syncUp();

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public int syncUp() {
       // Copy entire entry from neighboring DS node
       int count = 0;

       for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
           if (i > 0) {
               try {
                   Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
              } catch (InterruptedException e) {
                   logger.warn("Interrupted during registry transfer..");
                   break;
              }
          }
           Applications apps = eurekaClient.getApplications();
           for (Application app : apps.getRegisteredApplications()) {
               for (InstanceInfo instance : app.getInstances()) {
                   try {
                       if (isRegisterable(instance)) {
                           register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
                           count++;
                      }
                  } catch (Throwable t) {
                       logger.error("During DS init copy", t);
                  }
              }
          }
      }
       return count;
  }

看循环:意思是,如果是i第一次进来,为0,不够等待的代码,直接执行下面的拉取服务实例。 将自己作为一个eureka client,拉取注册表。并通过register(instance, instance.getLeaseInfo().getDurationInSecs(), true)注册到自身的注册表中。

Eureka Server也是一个Eureka Client,在启动的时候也会进行DiscoveryClient的初始化,会从其对应的Eureka Server中拉取全量的注册表信息。在Eureka Server集群部署的情况下,Eureka Server从它的peer节点中拉取到注册表信息后,将遍历这个Applications,将所有的服务实例通过AbstractRegistry#register方法注册到自身注册表中。

EurekaServerBootstrap#initEurekaServerContext()中有如下代码:

1
2
3
4
5
。。。
// Copy registry from neighboring eureka node
int registryCount = this.registry.syncUp();
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
。。。

当执行完上面的syncUp逻辑后,在下面的openForTraffic,开启此server接受别的client注册,拉取注册表等操作。而在它首次拉取其他peer节点时,是不允许client的通信请求的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
       // Renewals happen every 30 seconds and for a minute it should be a factor of 2.
       this.expectedNumberOfClientsSendingRenews = count;
       updateRenewsPerMinThreshold();
       logger.info("Got {} instances from neighboring DS node", count);
       logger.info("Renew threshold is: {}", numberOfRenewsPerMinThreshold);
       this.startupTime = System.currentTimeMillis();
       //如果count=0,没有拉取到注册表信息,将此值设为true,表示其他peer来取空的实例信息,意味着,将不允许client从此server获取注册表信息。如果count>0,将此值设置为false,允许client来获取注册表。
       if (count > 0) {
           this.peerInstancesTransferEmptyOnStartup = false;
      }
       DataCenterInfo.Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
       boolean isAws = Name.Amazon == selfName;
       if (isAws && serverConfig.shouldPrimeAwsReplicaConnections()) {
           logger.info("Priming AWS connections for all replicas..");
           primeAwsReplicas(applicationInfoManager);
      }
       logger.info("Changing status to UP");
       applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
       super.postInit();
  }

Server之间注册表信息的同步复制

为了保证Eureka Server集群运行时注册表信息的一致性,每个Eureka Server在对本地注册表进行管理操作时,会将相应的操作同步到所有peer节点中。

在外部调用server的restful方法时,在com.netflix.eureka.resources包下的ApplicationResource资源中,查看每个服务的操作。比如服务注册public Response addInstance(,此方法中有 registry.register(info, “true”.equals(isReplication));点进去实现类:replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);这是一种情况。

在PeerAwareInstanceRegistryImpl类中,其他操作,如cancel,renew等中都有replicateToPeers, 此方法中有个peerEurekaNodes,代表一个可同步数据的eureka Server的集合,如果注册表有变化,向此中的peer节点同步。

replicateToPeers方法,它将遍历Eureka Server中peer节点,向每个peer节点发送同步请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void replicateToPeers(Action action, String appName, String id,
                                 InstanceInfo info /* optional */,
                                 InstanceStatus newStatus /* optional */, boolean isReplication) {
       Stopwatch tracer = action.getTimer().start();
       try {
           if (isReplication) {
               numberOfReplicationsLastMin.increment();
          }
           // If it is a replication already, do not replicate again as this will create a poison replication
           if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
               return;
          }

           for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
               // If the url represents this host, do not replicate to yourself.
               if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                   continue;
              }
               replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
          }
      } finally {
           tracer.stop();
      }
  }

此replicateInstanceActionsToPeers方法中,类PeerEurekaNode的实例node的各种方法,cancel,register,等,用了batchingDispatcher.process(,作用是将同一时间段内,相同服务实例的相同操作将使用相同的任务编号,在进行同步复制的时候,将根据任务编号合并操作,减少同步操作的数量和网络消耗,但是同时也造成了同步复制的延时性,不满足CAP中的C(强一致性)。 所以Eureka,只满足AP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//示例:PeerEurekaNode
public void cancel(final String appName, final String id) throws Exception {
   long expiryTime = System.currentTimeMillis() + maxProcessingDelayMs;
   batchingDispatcher.process(
       taskId("cancel", appName, id),
       new InstanceReplicationTask(targetHost, Action.Cancel, appName, id) {
           @Override
           public EurekaHttpResponse<Void> execute() {
               return replicationClient.cancel(appName, id);
          }

           @Override
           public void handleFailure(int statusCode, Object responseEntity) throws Throwable {
               super.handleFailure(statusCode, responseEntity);
               if (statusCode == 404) {
                   logger.warn("{}: missing entry.", getTaskName());
              }
          }
      },
       expiryTime
  );
}

通过Eureka Server在启动过程中初始化本地注册表信息和Eureka Server集群间的同步复制操作,最终达到了集群中Eureka Server注册表信息一致的目的。

获取注册表中服务实例信息

Eureka Server中获取注册表的服务实例信息主要通过两个方法实现:

  • AbstractInstanceRegistry#getApplicationsFromMultipleRegions从多地区获取全量注册表数据
  • AbstractInstanceRegistry#getApplicationDeltasFromMultipleRegions从多地区获取增量式注册表数据。
1、全量

上面讲到从节点复制注册信息的时候,用方法public int syncUp() ,一行Applications apps = eurekaClient.getApplications();点进去实现类,有一行getApplicationsFromAllRemoteRegions(); 下面getApplicationsFromMultipleRegions,作用从多个地区中获取全量注册表信息,并封装成Applications返回,它首先会将本地注册表registry中的所有服务实例信息提取出来封装到Applications中,再根据是否需要拉取Region的注册信息,将远程拉取过来的Application放到上面的Applications中。最后得到一个全量的Applications。

2、增量

在前面提到接受服务注册,接受心跳等方法中,都有recentlyChangedQueue.add(new RecentlyChangedItem(lease));作用是将新变动的服务放到最近变化的服务实例信息队列中,用于记录增量是注册表信息。getApplicationDeltasFromMultipleRegions,实现了从远处eureka server中获取增量式注册表信息的能力。

在EurekaServer对外restful中,在com.netflix.eureka.resources下, @GET public Response getApplication(@PathParam(“version”) String version, @HeaderParam(“Accept”) final String acceptHeader, @HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept) { 其中有一句:String payLoad = responseCache.get(cacheKey);在responseCache初始化的时候,它的构造方法ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {中,Value value = generatePayload(key);点进去有一句:registry.getApplicationDeltasFromMultipleRegions(key.getRegions()));从远程获取delta增量注册信息。但是这个只是向client提供,不向server提供,因为server可以通过每次变更自动同步到peer。

获取增量式注册表信息将会从recentlyChangedQueue中获取最近变化的服务实例信息。recentlyChangedQueue中统计了近3分钟内进行注册、修改和剔除的服务实例信息,在服务注册AbstractInstanceRegistry#registry、接受心跳请求AbstractInstanceRegistry#renew和服务下线AbstractInstanceRegistry#internalCancel等方法中均可见到recentlyChangedQueue对这些服务实例进行登记,用于记录增量式注册表信息。#getApplicationsFromMultipleRegions方法同样提供了从远程Region的Eureka Server获取增量式注册表信息的能力。

参考资料