project reflection
Zookeeper伪集群搭建
下载zookeeper
解压修改config中的dataDir and dataLogDir
复制三份加压安装好后的文件, 方便运行不同server。(若不复制, 则直接创建三份不同的config文件, 运行时指明conf文件)。
分别修改三个复制后zookeeper中的config, 修改各自的clientport + dataDir + logDir + server.1 = .. server.2 = …
在data文件夹中创建myid, 内容为1,2, 3, … 目的为指明各个server的id
分别运行即可
SpringBoot整合zookeeper为注册中心 (跟Eureka一个作用)
Reflection:
- Eureka vs Zookeeper
实现:
zookeeper注册中心配置必须在boostrap.yml, why application.yml不生效?
在application.yml中配置service信息
关于支付模块修改订单状态的问题
Confusion:
首先, 该confusion以 用户跳转支付页面与orderGeneration模块 parallel为前提。
创建订单的模块从MQ接受消息, 然后创建订单, 这个创建订单的处理速度受MQ限制。
那么会不会有一种情况, 当1000个用户一瞬间抢完商品, 之后1000个用户同时点击“支付完成”, 点击该button会ping到修改订单状态的API。 即, 这1000 个订单可能在 抢购结束后, 立刻, 且同时, 被要求更改订单状态。 但是, 此时1000个订单可能还没有都被创建到数据库。 那么有的用户在点击”支付完成“时可能会返回报错, 即修改订单失败,因为后端无法修改到订单状态(该raw还未被创建)。
solution:
- 假设该问题无法解决, 则修改订单失败时返回降级页面, 提示用户等下再支付。 若如此解决, 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大概率可以接受服务降级, 或服务速度减慢。
实际上的架构
记录一次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:
- recievge message from MQ
- write order to db
- change stock in db
- 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 | // rBloomFilter doesn't have method to remove a elements, so if do not refresh, then with time increase |
待看
启动类中@FeignClients注解不加参数就找不到bean
细看 Hystrix, Ribbon, Feign
Netty
细看操作系统 -> NIO, 虚拟内存
java异常 classes
SpringCloud Gateway VS Zuul
Zuul 配置timeout时间, 默认时间多少? 不配置则得到了
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
4com.netflix.zuul.exception.ZuulException: Forwarding error
+
readtimeout 报错;
应该时默认超时时间过短导致
Data Sharding and RW Splitting Archetecture
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 | shardingsphere: |
Shardingj 配置 – 分库分表 + Read Write Split:
1 | server: |
Problems Reflection:
配置数据源时, 官网文档配置方式为 url: xxxxxx; 但项目中报错, 修改为 jdbc-url: xxx时 解决。 不确定原因, 可能与 sharding 版本及 连接池 选取有关。
sharding pom使用 4.0.0 RC 版本时, 报错 data tuncate error: Now()… 。 NOW() 不可用。 已经确定是该版本bug, 更换 4.1.1 版本后解决
RabbitMQ 导致的:
消息阻塞, 无法创建订单。
a. 逻辑错误导致返回ACK的代码未被执行
b. 忘了配置DB的选取策略, 导致更新web_seckill的 query始终返回0
两个错误导致ACK未被正确返回, 导致MQ中始终有unacked消息, 由于rabbitMQ的机制(待看), 导致消息被阻塞。
发送一条request, 创建多条订单。
a. 不确定原因, 应该也与 rabbitMQ机制导致的 unack时重试机制产生, 1.a/1.b修复后没有再发生
与上两条错误相关: 再unack消息存在时, 每当重启module后有时可创建到消息, 但都是之前发送的积压订单。 新启动以后发送的请求未被创建到订单。 并且不管发送多少条都没有被创建, 但是再下次重启时, 又创建到了之前发送的请求。
应该也是unack的阻塞导致的, 也与 重试机制等有关。
配置读写分离时报错:
java.lang.IllegalStateException: Missing the data source name: ‘ds1_slave’
at com.google.common.base.Preconditions.checkState(Preconditions.java:197)
原因:
datasources.names中忘了加两个slave的名字, 添加后解决。
修改 sharding column 时报错:
Cause: java.lang.NullPointerException: Cannot invoke method mod() on null object
原因: 修改了algorithm-expression: xxx 但是忘了修改对应的 sharding-column: xx
sharding-column的修改:
原配置:
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选取库和表。
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之前)
分库分表验证未截图, 但对于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
原因:
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的地址都不对, 所以得不到返回值。)
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处理:
- 修改sentinel.conf中的sentinel monitor mymaster IP Port Qurum, 使IP为真实VM‘s IP
- 配置sentinel.conf中的announce ip
- 配置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多, 待测多台 吞吐量是否提升
- 4000 并发量 -> 连续测3组 0 Error rate + 0 超卖
2)5000并发量 -> 连续测3组 + 1组
第三组 0.7% Error。 无超卖
第一, 二组 0 Error 无超卖, 隔2分钟第四组 0Error 0超卖
3) 6000并发量 -> 3组
第一组 0Error 无超卖(图片中总数忘了清空上次结果)
第二组0.3Error 无超卖
- 7000 1.99 Error 无超卖
5) 10000 -> 无超杀 9% Error