dxzmpk

endless hard working

0%

面向互联网信息的舆情检测与预警系统

面向互联网信息的舆情检测与预警系统

目录

环境搭建

配置过程

获取远程develop分支

  1. .git中config文件fetch修改为 fetch = +refs/heads/:refs/remotes/origin/
  2. 右下角刷新、选择分支
    img.png

img.png

项目组件介绍

缓存机制

  1. 返回的对象需要实现Serializable接口
  2. 返回的方法添加@Cacheable(cacheNames = “MESSAGE”, keyGenerator = “DEFAULT”)注解
  3. 默认的缓存过期时间为一小时

定时任务

定时执行某些接口
为了保证接口的数据被成功缓存,不能直接调用controller中的方法,而应该使用restTemplate的方式进行调用
定时任务采用 接口名称 + cron表达式进行设置
cron表达式用来规定执行的时间,具体用法可见https://www.cnblogs.com/yanghj010/p/10875151.html \
可以使用在线cron表达式生成器对执行时间进行配置,生成器链接:
https://cron.qqe2.com/

性能指标与参数调整

Redis单机性能测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[dx@IPIR-dx ~]$ redis-benchmark -t get -c 100 -n 1000000
====== GET ======
1000000 requests completed in 23.73 seconds
100 parallel clients
3 bytes payload
keep alive: 1

0.23% <= 1 milliseconds
98.86% <= 2 milliseconds
99.96% <= 3 milliseconds
99.99% <= 4 milliseconds
100.00% <= 5 milliseconds
100.00% <= 6 milliseconds
100.00% <= 106 milliseconds
100.00% <= 107 milliseconds
100.00% <= 107 milliseconds
42144.30 requests per second

负载测试

测试工具 :jemeter

测试规模

  1. 小测试 - 1000用户,每人发送20次,共2万个请求,
  2. 中测试 - 5000用户,每人发送20次,共10万个请求
  3. 大测试:10000用户,每人发送20次,共20万个请求

测试持续时间 : 60s, 到时之后将自动停止发送请求,但可能有未执行完的请求会继续排队直至执行完成。

测试接口:message/countMessages

测试环境:i5-6300hq, jvm heap大小为3.53gb

tomcat默认配置

1
2
3
4
5
6
tomcat:
uri-encoding: UTF-8
max-connections: 8192 # 最大连接数
threads:
max: 200
accept-count: 100 # 请求等待队列
测试配置 请求数量 吞吐量/s 最小返回耗时(ms) 最大耗时 平均耗时 错误率 接收速率
无redis 1790 6.2 4998 123975 70637 68.32% 1.59kb/s
单redis-小测试 20000 2035 5 3561 449 0.00% 443kb/s
单redis-大测试 198674 2465 1 3497 481 1.5% 1519kb/s

在单redis-大测试中,出现的异常为HttpHostConnectException。Connect to localhost:8080 failed: Connection refused: connect。这是tomcat可以容纳的同时链接数太少造成的。接下来更新tomcat的配置,将max-connection调为20000;

tomcat配置更新后测试【默认带有redis缓存功能】

tomcat中配置

1
2
3
4
5
6
tomcat:
uri-encoding: UTF-8
max-connections: 20000 # 最大连接数
threads:
max: 1000 # 最大线程数
accept-count: 1000 # 请求等待队列
测试配置 请求数量 吞吐量/s 最小返回耗时(ms) 最大耗时 平均耗时 错误率 接收速率
tomcat默认-大测试-local 198674 2465 1 3497 481 1.5% 1519kb/s
tomcat中-大测试-local 200000 2492 3 7850 1264 0.00% 542kb/s

在单redis-大测试中,出现的异常HttpHostConnectException在tomcat中-大测试-local中并没有出现,因此可以断定增加tomcat配置可以增加连接的数目,8092的默认数目不够10000的需求,因此出现了异常。

server测试

之前的测试程序和springboot程序运行在同一台机器上,主要是为了消除网络时延对于测试的影响。但是这种情况下,两个程序会争夺系统资源,可能无法将测试结果准确表现出来。因此基于tomcat中配置,在服务器上用docker搭建了测试环境。

测试环境:Intel(R) Xeon(R) CPU E5-2620 v2 @ 2.10GHz。jvm堆大小为8g。

jmeter设置:

1
2
3
4
5
Number of Threads: 1000
Ramp-up period: 0
Loop Count: 20
Dutation: 120
对于中测试,Number of Threads = 5000, 其余不变。
测试配置 请求数量 吞吐量/s 最小返回耗时(ms) 最大耗时 平均耗时 错误率 接收速率
单redis-小测试 20000 2035 5 3561 449 0.00% 443kb/s
tomcat中-小测试-server 20000 311 3 5998 814 0.00% 67.78kb/s
tomcat中-中测试-server 100000 818.2 5 31532 4924 2.22% 218.90

tomcat中-中测试-server出现了错误。全部为java.net.SocketException,Non HTTP response message: Connection reset,主要问题在于网络。

本地测试和server测试吞吐量对比:

本地测试吞吐量更高,而且由于网络带来的时延,平均耗时约为server的一半。接收速率也高一些。

server测试中的问题

  1. Non HTTP response code: java.net.BindException,Non HTTP response message: Address already in use: connect

    ephemeral TCP ports使用量到达了上限,通过增加ephemeral ports的最大数量解决。方案链接

  2. Non HTTP response code: java.net.SocketException,Non HTTP response message: Connection reset

  3. Non HTTP response code: java.net.SocketTimeoutException,Non HTTP response message: Read timed out

  4. Non HTTP response code: org.apache.http.conn.HttpHostConnectException,Non HTTP response message: Connect to 192.168.55.215:8080 [/192.168.55.215] failed: Connection timed out: connect

  5. Non HTTP response code: java.net.SocketException,Non HTTP response message: Software caused connection abort: connect

If your Jmeter is eating all resources will your application will get anything (Simple answer is NO) Thus application will go down and you will start getting timeout errors or socket exceptions.

If you have jmeter and application on diff machines then still 10000 users in 1 second is very high load for a normal application and it is obvious that you will face such errors. Try running test with realistic load that is expected for your application with given hw. Maybe 100 users in 1 second and gradually increase them to expected value.

查询优化-分库分表

打开慢查询log, 将慢查询记录阈值设置为5秒。

1
2
3
4
-- show variables like '%slow%'; 
-- set global slow_query_log = on;
-- show variables like '%long_query_time%';
-- set global long_query_time = 5;

未分库分表迁移到分库分表的方法

在分库上配置适用于mybatis的sharding-jdbc方案。

img

​ 现在需要将旧库的数据迁移到新库上。主要的方案有停机迁移和非停机迁移两种。为了能在迁移的过程中不影响原数据库的使用,采用双写迁移方法。

表拆分示意图

方案1 使用ResourceDatabasePopulator进行迁移

首先使用mysqldump将原库中的数据导出为sql脚本格式,然后利用Springboot自带的ResourceDatabasePopulator将sql脚本导入。这里导出的sql脚本大小为4.7GB。

1
2
3
4
5
6
7
8
9
10
@Component
public class InitializeData {
@Autowired
private DataSource dataSource;
@EventListener(ApplicationReadyEvent.class)
public void loadData() {
ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator(false, false, "UTF-8", new FileSystemResource("posys_message.sql"));
resourceDatabasePopulator.execute(dataSource);
}
}

在内存大小为16g的电脑上启动Springboot程序,报错为Failed to execute database script; nested exception is java.lang.OutOfMemoryError : heap,堆内存溢出了。然后试图将堆内存调大,发现还是会出现内存溢出异常——系统内存大小无法满足需要分配的堆内存大小。

方案2 从旧库读取,然后插入新库中

根据mysql开发文档中查询的优化,在插入语句中级联多条记录会增加查询的速度。插入一条语句时需要的时间主要由以下因素决定,后面的数字代表耗费的时间所占的比例。

  • Connecting: (3)
  • Sending query to server: (2)
  • Parsing query: (2)
  • Inserting row: (1 × size of row)
  • Inserting indexes: (1 × number of indexes)
  • Closing: (1)

因此,在一条语句中包含更多条数据可以节省建立连接、解析语句、关闭连接的时间。同时,在新库中将索引暂时关闭,也是有益于数据插入的快速进行的。

不同数据量测试5次,结果如下:

单独插入50000条数据平均耗时:233748ms
批量插入50000条数据平均耗时:2590ms
对比:效率差50倍
单独插入10000条数据平均耗时:22036ms
批量插入10000条数据平均耗时:3330ms
对比:效率差6倍
单独插入1000条数据平均耗时:3122ms
批量插入1000条数据平均耗时:374ms
对比:效率差8倍

其实最快的方式是从文本中直接加载表,这比INSERT语句快20多倍。但是由于这里使用了分表策略,加载时需要考虑数据的哈希定位库表的问题,因此只能选择语句插入的方式。

img

首先在数据库中记录counter, 保存下一次迁移时的起始id和终止id, 如上图所示。然后主要的工作分为三部分:

  1. 从旧库中批量读取,从起始id依次读到终止id。
  2. 向新库中批量插入。
  3. 递归调用当前函数,进行下一批数据的迁移。
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
public void migrate(){
logger.warn("开始迁移");
List<Integer> idList = new ArrayList<>();
// 取上图counter,分别为start和end
for (int i = start; i <= end; i++) {
idList.add(i);
}
List<Message> oldMessages;
// select...in子句取出源库中的所有数据
oldMessages = messageMapper.selectIdIn(idList);
if (oldMessages.size() > 0) {
// 将数据批量插入到分库分表中,这种批量插入的方式比单条插入快很多
int res = messageMapper.batchInsert(oldMessages);
logger.warn("成功插入" + res + "个");
}
// 更新计数器
int newStart = end + 1;
end = end + end - start + 1;
String newCounter = newStart + " " + end;
// 将新的计数器信息newCounter存入数据库
System.gc();
// 强制进行gc
migrate();
// 进行下一次迁移,这里使用递归调用的方式,虽然速度会慢一些,但是比较可靠,不容易出错
}
});
}

在插入时不断监看log,如果发生错误应及时修正。

img

分库分表前后接口耗时测试

countMessages(index) getKeywordCount
单库单表 26708 45476(ALL)
双库四表 9513.6(单表4.297) 15263(单表5827)(ALL)
单库单表+索引 2049(index)
双库四表+索引 2395(单表1339)(index)

利用EXPLAIN来查看上述两条语句的执行计划,并在标题栏中标注出来。ALL、index、range、 ref、eq_ref、const、system、NULL(从左到右,性能从差到好)。

分库效果小结:通过分库可以减少需要扫描全部索引的时间,因此countMessages分库分表执行时间比原来快很多。在getKeywordCount测试中,由于归并需要时间,因此在有索引的情况下,虽然单个小表查询时间比原来少,但是得到汇总结果的时间会多于单表下的查询时间。

分库分表带来的复杂性

跨库关联查询

有几种方案可以解决:

  • 字段冗余:把需要关联的字段放入主表中,避免 join 操作;
  • 数据抽象:通过ETL等将数据汇合聚集,生成新的表;
  • 全局表:比如一些基础表可以在每个数据库中都放一份;
  • 应用层组装:将基础数据查出来,通过应用程序计算组装;

分布式事务

单数据库可以用本地事务搞定,使用多数据库就只能通过分布式事务解决了。

常用解决方案有:基于可靠消息(MQ)的解决方案、两阶段事务提交、柔性事务等。

Sharding-jdbc中本地事务完全支持非跨库事务:例如仅分表,或分库但是路由的结果在单库中。同时完全支持因逻辑异常导致的跨库事务。例如:同一事务中,跨两个库更新。更新完毕后,抛出空指针,则两个库的内容都能回滚。但是不支持因网络、硬件异常导致的跨库事务。例如:同一事务中,跨两个库更新,更新完毕后、未提交之前,第一个库宕机,则只有第二个库数据提交。

因此使用两阶段事务来完全支持跨库事务。在sharding-jdbc中默认使用Atomikos,支持使用SPI的方式加载其他XA事务管理器。

不过,XA 并不是 Java 的技术规范(XA 提出那时还没有 Java),而是一套语言无关的通用规范,所以 Java 中专门定义了JSR 907 Java Transaction API,基于 XA 模式在 Java 语言中的实现了全局事务处理的标准,这也就是我们现在所熟知的 JTA。JTA 最主要的两个接口是:

  • 事务管理器的接口:javax.transaction.TransactionManager。这套接口是给 Java EE 服务器提供容器事务(由容器自动负责事务管理)使用的,还提供了另外一套javax.transaction.UserTransaction接口,用于通过程序代码手动开启、提交和回滚事务。
  • 满足 XA 规范的资源定义接口:javax.transaction.xa.XAResource,任何资源(JDBC、JMS 等等)如果想要支持 JTA,只要实现 XAResource 接口中的方法即可。

JTA 原本是 Java EE 中的技术,一般情况下应该由 JBoss、WebSphere、WebLogic 这些 Java EE 容器来提供支持,但现在BittronixAtomikosJBossTM(以前叫 Arjuna)都以 JAR 包的形式实现了 JTA 的接口,称为 JOTM(Java Open Transaction Manager),使得我们能够在 Tomcat、Jetty 这样的 Java SE 环境下也能使用 JTA。

XA 将事务提交拆分成为两阶段过程:

  • 准备阶段:又叫作投票阶段,在这一阶段,协调者询问事务的所有参与的是否准备好提交,参与者如果已经准备好提交则回复 Prepared,否则回复 Non-Prepared。这里所说的准备操作跟人类语言中通常理解的准备并不相同,对于数据库来说,准备操作是在重做日志中记录全部事务提交操作所要做的内容,它与本地事务中真正提交的区别只是暂不写入最后一条 Commit Record 而已,这意味着在做完数据持久化后并不立即释放隔离性,即仍继续持有锁,维持数据对其他非事务内观察者的隔离状态。
  • 提交阶段:又叫作执行阶段,协调者如果在上一阶段收到所有事务参与者回复的 Prepared 消息,则先自己在本地持久化事务状态为 Commit,在此操作完成后向所有参与者发送 Commit 指令,所有参与者立即执行提交操作;否则,任意一个参与者回复了 Non-Prepared 消息,或任意一个参与者超时未回复,协调者将将自己的事务状态持久化为 Abort 之后,向所有参与者发送 Abort 指令,参与者立即执行回滚操作。对于数据库来说,这个阶段的提交操作应是很轻量的,仅仅是持久化一条 Commit Record 而已,通常能够快速完成,只有收到 Abort 指令时,才需要根据回滚日志清理已提交的数据,这可能是相对重负载操作。

image-20210623180351644

两段式提交原理简单,并不难实现,但有几个非常显著的缺点:

  • 单点问题:协调者在两段提交中具有举足轻重的作用,协调者等待参与者回复时可以有超时机制,允许参与者宕机,但参与者等待协调者指令时无法做超时处理。一旦宕机的不是其中某个参与者,而是协调者的话,所有参与者都会受到影响。如果协调者一直没有恢复,没有正常发送 Commit 或者 Rollback 的指令,那所有参与者都必须一直等待。
  • 性能问题:两段提交过程中,所有参与者相当于被绑定成为一个统一调度的整体,期间要经过两次远程服务调用,三次数据持久化(准备阶段写重做日志,协调者做状态持久化,提交阶段在日志写入 Commit Record),整个过程将持续到参与者集群中最慢的那一个处理操作结束为止,这决定了两段式提交的性能通常都较差。
  • 一致性风险:前面已经提到,两段式提交的成立是有前提条件的,当网络稳定性和宕机恢复能力的假设不成立时,仍可能出现一致性问题。宕机恢复能力这一点不必多谈,1985 年 Fischer、Lynch、Paterson 提出了“FLP 不可能原理#Solvability_results_for_some_agreement_problems)”,证明了如果宕机最后不能恢复,那就不存在任何一种分布式协议可以正确地达成一致性结果。该原理在分布式中是与“CAP 不可兼得原理“齐名的理论。而网络稳定性带来的一致性风险是指:尽管提交阶段时间很短,但这仍是一段明确存在的危险期,如果协调者在发出准备指令后,根据收到各个参与者发回的信息确定事务状态是可以提交的,协调者会先持久化事物状态,并提交自己的事务,如果这时候网络忽然被断开,无法再通过网络向所有参与者发出 Commit 指令的话,就会导致部分数据(协调者的)已提交,但部分数据(参与者的)既未提交,也没有办法回滚,产生了数据不一致的问题。

排序、分页、函数计算问题

使用SQL时order by, limit关键字需要特殊处理,一般来说采用分片的思路

现在某个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终得到结果。

分布式id

如果使用mysql可以在单库单表中使用id自增作为主键,分库分表就不行了,会出现id重复。

可以通过以下分布式id生成方案解决:

  • UUID
  • 基于数据库自增单独维护一张 ID表
  • 号段模式
  • Redis 缓存
  • 雪花算法(Snowflake)
  • 百度uid-generator
  • 美团Leaf
  • 滴滴Tinyid

本项目中,表插入操作在主库posys中进行,插入后会运行同步插件,实现新旧库的一致。