project reflection

Zookeeper伪集群搭建

  1. 下载zookeeper

  2. 解压修改config中的dataDir and dataLogDir

  3. 复制三份加压安装好后的文件, 方便运行不同server。(若不复制, 则直接创建三份不同的config文件, 运行时指明conf文件)。

  4. 分别修改三个复制后zookeeper中的config, 修改各自的clientport + dataDir + logDir + server.1 = .. server.2 = …

  5. 在data文件夹中创建myid, 内容为1,2, 3, … 目的为指明各个server的id

  6. 分别运行即可

SpringBoot整合zookeeper为注册中心 (跟Eureka一个作用)

Reflection:

  1. Eureka vs Zookeeper

实现:

  1. zookeeper注册中心配置必须在boostrap.yml, why application.yml不生效?

  2. 在application.yml中配置service信息

关于支付模块修改订单状态的问题

Confusion:

首先, 该confusion以 用户跳转支付页面与orderGeneration模块 parallel为前提。

创建订单的模块从MQ接受消息, 然后创建订单, 这个创建订单的处理速度受MQ限制。

那么会不会有一种情况, 当1000个用户一瞬间抢完商品, 之后1000个用户同时点击“支付完成”, 点击该button会ping到修改订单状态的API。 即, 这1000 个订单可能在 抢购结束后, 立刻, 且同时, 被要求更改订单状态。 但是, 此时1000个订单可能还没有都被创建到数据库。 那么有的用户在点击”支付完成“时可能会返回报错, 即修改订单失败,因为后端无法修改到订单状态(该raw还未被创建)。

solution:

  1. 假设该问题无法解决, 则修改订单失败时返回降级页面, 提示用户等下再支付。 若如此解决, project需添加购物车, 进而有需要分布式Session

关于Confusion中的状况实际上应该不会发生的猜想:

实际业务中, 由于有支付环节, 该环节中需要:

​ a) 检测安全环境

​ b) ping支付宝/微信/paypal… 的API

​ c) 输入密码

​ d) 支付API处理请求

这些过程耗时至少10s - 20s。

在实际业务中, 用户抢购商品以后 -> 后端开始创建订单, 同时, 另一个支付Model 被parallel的运行, 引导用户付款。 ->付款结束才能点击支付完成, 或者支付完成,在payment model中修改订单状态。

由于支付业务耗时长, 所以订单应该一定已经被创建。

同时, 为了加快订单创建速度, 保证修改某订单状态时, 订单状态一定要保证完成, 可以增加 order Model的数量, 同时rabbitMQ中多建几个queue, 多个ordermodel 分发处理以加快订单创建速度。

关于能否通过先在Redis缓存订单来解决该问题:

应该不现实, 因为订单数量极大的情况下, redis内存抗不住, 只能增加redis服务器数量, 不值得。因为在秒杀情况下, users大概率可以接受服务降级, 或服务速度减慢。

实际上的架构

payment_orderGeneration

记录一次RabbitMQ 重复消费 “cannot ack/nack” 产生的超卖

原因

忘记在 RabbitQueue Consumer中, 即 order module 的 application.yml 中配置 RabbitMQ。 进而导致默认使用了自动ACK模式。

具体原因待研究。

表象

懒得截图:

100个库存, 压测并发100 个请求时, 查询MySQL发现下了199个订单一次, 201个订单一次, flash_sale表跟 order表数据是一致的, 即同时 减少/增加相同的订单量。 但是Redis中库存始终正确, 未出现超卖现象。

同时order model报错 cannot ack/nack。

MyBatis @Update(….. ${} )引发的报错: “There is no getter for property named ‘’ in ‘class java.lang.String

原因:

${} and #{}的使用错误, 修改为 #{}后报错消失, 但是其他接口方法中(@Insert)使用 ${}没有报错, 正常运行。

待仔细学习MyBatis后解答。

关于Order Module中的疑问

Current Version of Code:

  1. recievge message from MQ
  2. write order to db
  3. change stock in db
  4. return ack to MQ to tell MQ delete the message

整个Order Module没有任何返回值, 即状态返回。

Confusion:

测试发现, 数据库操作即使失败, 依然会返回ack删除数据。如何解决?

同时, write正常工作时, 如果 change stock方法出错, 也是依然返回了ACK。

是因为加了try catch才没有在方法执行失败时 卡住程序? 怎么解决? 不能把ack返回写在finally中, 因为这样就是无论对错消息都删除了。

目前用的解决方案:

Service方法中返回值, 即更新数据库的方法的返回值 == 1时才return ACK。 有无更好方法?

并且实际业务中, Order模块没有任何返回状态的话, 如果出了问题如何发现?

關於Bloom Filter

zuul中加了bloom filter 解決緩存擊穿問題及過濾非法請求。

在product scan scheduler中反復刷新bloom filter。

之所以要刷新, 而不是反復添加, 理由如下:

1
2
3
4
5
6
7
8
// rBloomFilter doesn't have method to remove a elements, so if do not refresh, then with time increase
// all bits in the bloom filter will be filled out, which means all retrieve will be returned true, the bloom
// filter will lose its meanning.
// so here every time scanning the flash sale DB, we refresh the bloom filter to maintain the precision.
// Although this may cause a problem that during this task is refreshing the bloom filter,
// there might be a short period that bloom filter lacks some elements, so some impossible requests may not be
// filtered by the gateway, the number of such requests should not too much.
// More importantly by now I don't see other solutions.

待看

  1. 启动类中@FeignClients注解不加参数就找不到bean

  2. 细看 Hystrix, Ribbon, Feign

  3. Netty

  4. 细看操作系统 -> NIO, 虚拟内存

  5. java异常 classes

  6. SpringCloud Gateway VS Zuul

  7. Zuul 配置timeout时间, 默认时间多少? 不配置则得到了

  8. RabbitMQ unacked Message caused lots of message blocked:

    ​ 表象:

    ​ OrderGenerate Module中收不到新的订单消息。

    ​ RabbitMQ中始终显示一条unacked Message

    ​ 原因:

          1. 逻辑错误: 设定 if(updateState and changeState == 1) 才返回ACK, ”但是module刚启动时, 同时更新了多条消息? **两个state值不为0**,导致没有返回ACK, 更新为 if (xxxx,xxxx != 0) 后不在有ACK “
    
    
    
     8. 2 ordermodel的application.yml中只配置了 web_order的分表, 由于 web_seckill未使用分表, 所以没有配置, 但是报错了 FlashSale_2.web_seckill不存在。
    

    1
    2
    3
    4
    com.netflix.zuul.exception.ZuulException: Forwarding error
    +
    readtimeout 报错;
    应该时默认超时时间过短导致

Data Sharding and RW Splitting Archetecture

data_sarding_RWSplitting

Web_seckill 未做分表/分库。 不知道是否规范, 未做原因在于判断 秒杀商品数量不多, 对于该表访问压力不大 ;

Specificly, 该项目逻辑为 —- 创建订单的同时扣减库存。 创建订单Module从MQ消费消息, 消费QPS均固定, 并且可控。 Moreover, 秒杀过程中对于库存的查询皆走的Redis, 进一步减小了对该表的访问频率。 MoreMoreover, 对该表的查询,只在product scanning 定时进行, 频率也可控, 且访问量不大, 且Master-Slave的读写分离顺带着给web_seckill库存表也做了读写分离, 又进一步减小数据库压力。 因此判断数据库压力不大, 故未做分表/分库。

一句话概括上面内容:

web_seckill 的访问压力 可控, 并且不大, 所以不作。 不大的原因: 1. 访问QPS由 消费MQ的速度决定,所以”写“ 速度 可控。 2. 读写分离的设定, 又进一步减小了压力。 并且对于web_seckill表的读取, 每xx分钟才由 product scanning scheduler进行一次, 读取频率很低。

PS:

web_seckill秒杀库存表应该可以做成broadcast table, 未作。

Shardingj 配置 – 分库分表Only:

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
shardingsphere:
# 1. Configure DataSource, same as normal datasource configuration
datasource:
# Allocate name to different databases
names: ds1,ds2
# Configure each databases
ds1:
# configure type first -> can use almost any connection pool
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
# This has to be jdbc-url instead of url, otherwise the system cannot find the url address
jdbc-url: jdbc:p6spy:mysql://192.168.100.128:3306/FlashSale
username: root
password: 1122110
hikari:
maximum-pool-size: 30
minimum-idle: 10
ds2:
# configure type first -> can use almost any connection pool
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
jdbc-url: jdbc:p6spy:mysql://192.168.100.128:3306/FlashSale_2?serverTimezone=GMT%2B10
username: root
password: 1122110
hikari:
maximum-pool-size: 30
minimum-idle: 10
# 2. Configure Table/DB selection strategies
sharding:
tables:
web_order_:
actual-data-nodes: ds$->{1..2}.web_order_$->{1..2}
table-strategy:
inline:
sharding-column: id
algorithm-expression: web_order_$->{id % 2 +1}
database-strategy:
inline:
sharding-column: saleId
algorithm-expression: ds$->{saleId % 2 +1}
web_seckill:
actual-data-nodes: ds1.web_seckill
props:
sql:
show: true

Shardingj 配置 – 分库分表 + Read Write Split:

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
server:
port: 8000

spring:
main:
allow-bean-definition-overriding: true
application:
name: orderGenerator
# updated to ShardingSephere at 2022/1/11
# datasource:
# driver-class-name: com.p6spy.engine.spy.P6SpyDriver
# url: jdbc:p6spy:mysql://192.168.100.128:3306/FlashSale
# username: root
# password: 1122110
# hikari:
# maximum-pool-size: 30
# minimum-idle: 10
shardingsphere:
# 1. Configure DataSource, same as normal datasource configuration
datasource:
# Allocate name to different databases
# Dont forgot to list any name here, other wise got error:
# java.lang.IllegalStateException: Missing the data source name: 'ds1_slave'
# at com.google.common.base.Preconditions.checkState(Preconditions.java:197)
names: ds1,ds1slave,ds2,ds2slave
# Configure each databases
ds1:
# configure type first -> can use almost any connection pool
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
# This has to be jdbc-url instead of url, otherwise the system cannot find the url address
jdbc-url: jdbc:p6spy:mysql://192.168.100.128:3306/FlashSale?serverTimezone=GMT%2B10
username: root
password: 1122110
hikari:
maximum-pool-size: 30
minimum-idle: 10
# slave for ds1
ds1slave:
# configure type first -> can use almost any connection pool
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
# This has to be jdbc-url instead of url, otherwise the system cannot find the url address
jdbc-url: jdbc:p6spy:mysql://127.0.0.1:3306/FlashSale?serverTimezone=GMT%2B10
username: root
password: 1122110
hikari:
maximum-pool-size: 30
minimum-idle: 10
ds2:
# configure type first -> can use almost any connection pool
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
jdbc-url: jdbc:p6spy:mysql://192.168.100.128:3306/FlashSale_2?serverTimezone=GMT%2B10
username: root
password: 1122110
hikari:
maximum-pool-size: 30
minimum-idle: 10
ds2slave:
# configure type first -> can use almost any connection pool
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
jdbc-url: jdbc:p6spy:mysql://127.0.0.1:3306/FlashSale_2?serverTimezone=GMT%2B10
username: root
password: 1122110
hikari:
maximum-pool-size: 30
minimum-idle: 10


# Configure Table selection strategies
sharding:
defaultDataSourceName: ds1
tables:
web_order_:
actual-data-nodes: ms_ds$->{1..2}.web_order_$->{1..2}
table-strategy:
inline:
sharding-column: id
algorithm-expression: web_order_$->{id % 2 +1}
database-strategy:
inline:
sharding-column: saleId
algorithm-expression: ms_ds$->{saleId % 2 +1}
web_seckill:
actual-data-nodes: ms_ds1.web_seckill
master-slave-rules:
ms_ds1:
masterDataSourceName: ds1
slaveDataSourceNames:
- ds1slave
ms_ds2:
masterDataSourceName: ds2
slaveDataSourceNames:
- ds2slave
props:
sql:
show: true
devtools:
restart:
enabled: true
additional-paths: src/main/java
rabbitmq: # rabbitmq configuration
host: localhost
port: 5672
virtual-host: /
username: guest
password: guest
connection-timeout: 5000
listener:
simple:
acknowledge-mode: manual
prefetch: 1
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.java.pojo


Problems Reflection:

  1. 配置数据源时, 官网文档配置方式为 url: xxxxxx; 但项目中报错, 修改为 jdbc-url: xxx时 解决。 不确定原因, 可能与 sharding 版本及 连接池 选取有关。

  2. sharding pom使用 4.0.0 RC 版本时, 报错 data tuncate error: Now()… 。 NOW() 不可用。 已经确定是该版本bug, 更换 4.1.1 版本后解决

  3. RabbitMQ 导致的:

    1. 消息阻塞, 无法创建订单。

      a. 逻辑错误导致返回ACK的代码未被执行

      b. 忘了配置DB的选取策略, 导致更新web_seckill的 query始终返回0

      两个错误导致ACK未被正确返回, 导致MQ中始终有unacked消息, 由于rabbitMQ的机制(待看), 导致消息被阻塞。

    2. 发送一条request, 创建多条订单。

      a. 不确定原因, 应该也与 rabbitMQ机制导致的 unack时重试机制产生, 1.a/1.b修复后没有再发生

    3. 与上两条错误相关: 再unack消息存在时, 每当重启module后有时可创建到消息, 但都是之前发送的积压订单。 新启动以后发送的请求未被创建到订单。 并且不管发送多少条都没有被创建, 但是再下次重启时, 又创建到了之前发送的请求。

      应该也是unack的阻塞导致的, 也与 重试机制等有关。

  4. 配置读写分离时报错:

    java.lang.IllegalStateException: Missing the data source name: ‘ds1_slave’

    at com.google.common.base.Preconditions.checkState(Preconditions.java:197)

    原因:

    datasources.names中忘了加两个slave的名字, 添加后解决。

  5. 修改 sharding column 时报错:

    Cause: java.lang.NullPointerException: Cannot invoke method mod() on null object

    原因: 修改了algorithm-expression: xxx 但是忘了修改对应的 sharding-column: xx

  6. sharding-column的修改:

    1. 原配置:

      sharding-column分别为 order_No and saleId, 对应分表及分库策略

      问题:

      只考虑创建订单的话,用这两个column选用表和库没问题, 但是由于payment module中使用 userId + saleId 更新数据, column的使用不同, 是否有可能retrieve不到数据?因为 写订单时, orderno 和 saleid决定了去4个表中的哪个表(2库, 每个库2个表), 那么retrieve数据时, 用orderno+saleid检索,或者只用其中一个检索就没问题, 因为一定能route到数据所在的表, 但是 若用 userId+saleId的话, 是否有可能根据 saleId 选定了库, 但是因为这个query中只有saleId, 没有orderno, 所以是否可能数据所在的表不在这个库中? 就算sharding能自动解决这个问题的话, 应该也需要检测两个库中的表1, 或一个库中的表1及表2, 应该会拖慢并发量。 因此改成根据saleId + userId选取库和表。

    2. Alternative Solution:

      Solution 1:

      仍然用 orderno + saleid 做分库分表。然后再payment module中根据orderno retrive row修改状态。 但是问题是这样payment接受的http参数就必须是orderno, 也就意味着orderno必须在用户秒杀成功后直接从deduct redis中返回。 然而问题在于目前 orderno在ordergenerate model中生成, 其从MQ接受消息, 与deduct redis异步执行, 所以没办法从ordergenerate model返回orderno 到deduct redis然后再从deduct redis返回给用户。 (该问题应该无解, 因为MQ异步)

      要解决该问题, 就得让order no在deduct redis这个秒杀module中生成, 但是这样一来该module就在秒杀过程中干了别的事, 尽管生成订单号业务不复杂, 但是应该还是会拖慢并发量。

      Solution 2:

      只用一个column做sharding-column, 即库/表的选取都是对同一个column取模。

      不确定这样是否可行, 没试(理论上觉得可行)。 同时也不确定这样是否有任何缺点。

**RW Splitting Validation ** (测试于修改sharding column之前)

MS_Write_Validation

MS_Select_Validation

分库分表验证未截图, 但对于saleId=2时, 做orderNo的测试, 分表的选择没有问题。

由于redis中没添加 奇数的saleId, 所以未测分库的选取, 但应该没有问题。

Redis Master/slave + Sentinel 的报错(大坑)

配置sentinel时, redisson及redis读到的ip始终是Localhost, 而不是配置文件中设置的VM IP。 进而导致:

Errors:

  • Cannot connect to server –> 原因1. Sentinel问题
  • Sentinel Sentinels return empty sets –> 原因1
  • Cannot connect to 127.0.0.1:Master/Slave’s port–> 原因2

原因:

  1. Sentinel地址读取错误

    原因:

    Redisson 配置Sentinel模式时, 不可使用redisson-config.yml配置, 读取不到sentinel地址,所以应该自动设置成了localhost。 改成手动配置恢复正常。之所以会这样貌似是因为 redisson的config中, 要直接配置多个地址的话, 需要nodes.toArrayList… 之类的操作。而使用哨兵模式则无可避免需要配置多个地址, 故出错。

    Error Reflection:

    之所以有Sentinel Sentinels return … 这个报错, 因为springboot连接到redis sentinel以后, 会先执行这个命令。 该报错实际就是在 redis-cli中执行 sentinel sentinels + mymaster(master配置的名字) 返回不到值。 之所以返回不到值,是因为sentinel地址没有读到, 连接到了localhost的默认端口, 所以根本没链接到sentinel。

    关于为什么执行Sentinel sentinels:

    可能性1: health check的目的, 即检测连接的目的。 应该是这个原因, google时候有看到差不多的。 而且该报错可以通过 setCheckList = False 消除。即不让它先查询。

    可能性2: 也可能是因为 Sentinel地址错了以后, 读取不到配置中的Sentinel地址, 所以想通过该指令得到其他sentinel的地址?但是又因为每个sentinel的地址都不对, 所以得不到返回值。)

  2. Master and Slave地址读取错误

    由于在springboot中使用redis的sentinel + MS 模式时, Master and Slave的地址是springboot通过sentinel得到的。 即当sentinel连接成功后, 其会像 springboot返回 master and slave’s ip + port。

    这意味着, springboot中添加的master and slave’s address, 其实是根据sentinel返回的值拼接得到的。 而sentinel返回给springboot的值, 其实就是在redis-cli中 sentinel masters / sentinel slaves mymaster 显示的 IP值和 Port值。

    而sentinel中的这两个值, 是根据sentinel.conf中的内容获取。 所以如果 sentinle中的 sentinel monitor mymaster IP Port Qurum 中的IP配置的是 127.0.0.1的话, 那么sentinel masters中的ip值就是 127.0.0.1, 所以返回给springboot的值也是localhost。 故连接不到。因此修改的第一步为把sentinel.conf中的localhost改成了VM IP。 修改后master的IP在springboot中正确得到。

    但是Slave的IP依然错误。 并且关闭master,迫使Sentinel重新选主后,新的Master的 IP再次错误。 之所以这样, 是因为:

    ​ 1) 在 sentinel.conf中, 我们只需要配置,也只能够配置 master的IP。 而slave的IP是sentinel通过master得来的(Sentinel 周期发送的那三个消息, 待复习)。 而master处的slaveIP 无法在master的conf中显示配置, 是自动得到的, 结果就是slave的IP被自动设置成了127.0.0.1。 进而slave在sentinel中存的IP就是127…., which means Springboot从sentinel得到的slaveIP依然是localhost。

    ​ 2) sentinel会自动修改sentinel.conf中的sentinel monitor…的配置, 所以例如手动配置了 xxx realIP + port1, 再重新选主以后, sentinel也会重新用存储的原slave的IP跟port将该配置项修改为 xxx localhost + newMaster’s Port。 之所以这里IP变成了xxx, 源于above reason 1)。

    解决办法: 配置 slave的conf中的announceIP为VM的真实IP。该项配置指明让server(无论是Master/Slave, 只要其conf中配置了就有效)暴露给外部的IP为设置的announce IP。 进而master得到的就是配置的IP-> sentinel 得到 -> springboot得到。

    总结原因2处理:

    1. 修改sentinel.conf中的sentinel monitor mymaster IP Port Qurum, 使IP为真实VM‘s IP
    2. 配置sentinel.conf中的announce ip
    3. 配置servers.conf中的announce ip, including master and slaves

压测记录

v1 - 2022/01/06

Archetecture:

Modules and Archetecture: 单台 秒杀接口 (未开秒杀flash sale entry module 即consumer, 因此也无 loadbalance)+ 单order module + product scan model + Message MQ;

redis 中数据结构: HashTable-> 4 kvs in the value ==> wait to be updated to store as a list (control as ziplist) and compare the performance

分布式锁: redisson

Result

单台秒杀接口吞吐量始终 200多, 待测多台 吞吐量是否提升

  1. 4000 并发量 -> 连续测3组 0 Error rate + 0 超卖

压测2022-01-06

库存

订单

2)5000并发量 -> 连续测3组 + 1组

第三组 0.7% Error。 无超卖

1

3

2

第一, 二组 0 Error 无超卖, 隔2分钟第四组 0Error 0超卖

v2-1

3) 6000并发量 -> 3组

第一组 0Error 无超卖(图片中总数忘了清空上次结果)

1

第二组0.3Error 无超卖

  1. 7000 1.99 Error 无超卖

5) 10000 -> 无超杀 9% Error