100W数据去重,用distinct还是group by?
distinct功能和用法
DISTINCT 是一种用于去除 SELECT 语句返回结果中重复行的关键字。在使用 SELECT 语句查询数据时,如果结果集中包含重复的行,可以使用 SELECT DISTINCT 语句来去除这些重复的行。例如,以下语句返回一个包含重复行的结果集:
SELECT name FROM users;
可以使用以下语句来去除重复的行:
SELECT DISTINCT name FROM users;
这样就可以得到一个不包含重复行的结果集。需要注意的是,DISTINCT 关键字会对查询的性能产生一定的影响,因为它需要对结果集进行排序和去重的操作。因此,在使用 DISTINCT 关键字时需要谨慎,尽可能地使用索引来优化查询,以提高查询的性能。
distinct用法
SELECT DISTINCT columns FROM table_name WHERE where_conditions;
例如:
mysql> select distinct age from student;
+------+
| age |
+------+
| 10 |
| 12 |
| 11 |
| NULL |
+------+
4 rows in set (0.01 sec)
DISTINCT 关键词用于返回唯一不同的值。放在查询语句中的第一个字段前使用,且作用于主句所有列。
注意:DISTINCT子句将所有NULL值视为相同的值
如果列具有NULL值,并且对该列使用DISTINCT子句,MySQL将保留一个NULL值,并删除其它的NULL值。
distinct多列去重
distinct多列的去重,则是根据指定的去重的列信息来进行,
即只有所有指定的列信息都相同,才会被认为是重复的信息。
SELECT DISTINCT column1,column2 FROM table_name WHERE where_conditions;
mysql> select distinct sex,age from student;
+--------+------+
| sex | age |
+--------+------+
| male | 10 |
| female | 12 |
| male | 11 |
| male | NULL |
| female | 11 |
+--------+------+
5 rows in set (0.02 sec)
Group by 的使用场景
GROUP BY 主要的使用场景是 在分组 聚合。
具体来说,GROUP BY 子句通常用于将查询结果按照一个或多个列进行分组,然后对每个组进行聚合计算。
例如,假设一个表存储了每个人的姓名、年龄和所在城市,可以使用 GROUP BY 子句按照城市对人进行分组,并计算每个城市的平均年龄或人口数量等统计信息。
GROUP BY 子句通常与聚合函数(例如 COUNT、SUM、AVG、MAX 和 MIN)一起使用,以计算每个组的聚合值。例如,可以使用 GROUP BY 子句和 COUNT 函数来计算每个城市中的人数。
但是,除了分组 聚合,GROUP BY 还可以用来 进行数据去重,并且在某些特定场景下,性能超过 distinct
需要注意的是:GROUP BY 子句会对结果集进行排序,因此可能会导致使用临时文件排序。如果查询中包含 ORDER BY 子句,使用不当会产生临时文件排序,容易产生慢 SQL 问题。
Group by底层原理
group by
语句根据一个或多个列对结果集进行分组。在分组的列上通常配合 COUNT, SUM, AVG等函数一起使用。
假定有个需求:统计每个城市的用户数量。
对应的 SQL 语句如下:
select city ,count(*) as num from user group by city;
执行部分结果如下:
先用Explain查看一下执行计划
explain select city ,count(*) as num from user group by city;
在 Extra 字段里面,我们可以看到以下信息:
-
用到了Using temporary, 表示执行时创建了一个内部临时表。
注意这里的临时表可能是内存上的临时表,也有可能是硬盘上的临时表,当然,如果临时表比较小,就是基于内存的,可以肯定的是:基于内存的临时表的性能高,时间消耗肯定要比基于硬盘的临时表的实际消耗小。
-
用到了Using filesort, 表示执行过程中没有使用索引的排序,而是使用临时文件。
"Using filesort"是MySQL的EXPLAIN输出中的一个短语,表示查询需要使用临时文件对结果集进行排序。
这可能发生在查询包括ORDER BY子句或GROUP BY子句时,而数据库无法使用索引满足排序顺序。
使用临时文件对大型结果集进行排序可能会导致磁盘I/O和内存使用方面的昂贵开销,因此最好尽可能避免“Using filesort”。
一些避免文件排序的策略包括使用适当的索引优化查询,限制结果集的大小或修改查询以使用不同的排序算法。
那么group by
语句为什么会同时用到临时表和临时文件 排序呢?
首先看下整个执行流程:
-
在执行过程中首先创建内存临时表,表里有
city
,num
两个字段,city
为主键。 -
扫描 user 表,依次取出一行数据,数据中
city
字段的值为 c;
-
如果临时表中没有主键为 c 的行, 则插入一条新纪录(c , 1);
-
如果存在,则更新该行为 (c, num + 1);
-
遍历完后,再根据
city
进行排序,最后将结果集返回给客户端。
这个流程的执行示意图如下:
然后进入到第二阶段,进行内存排序。其中对内存临时表的排序执行步骤,本质上和的order by
流程基本一致。
-
数据从内存临时表中拷贝到
sort buffer
中 -
sort buffer
进行排序,根据实际情况采用全字段排序或rowid排序 -
排序结果写回到内存临时表中
-
从内存临时表中返回结果集。
流程示意图如下所示:
Group by在去重场景的使用
分为两个去重场景进行介绍:
-
单列去重
-
多列去重
单列去重和多列去重的区别在于去重的依据不同。
单列去重是指针对某一列数据进行去重,即将该列中重复的值只保留一个。例如,如果有一个包含重复数据的姓名列表,可以使用单列去重将重复的姓名去除,只保留一个。
多列去重是指针对多列数据进行去重,即将多列数据中重复的行只保留一行。例如,如果有一个包含姓名、年龄和所在城市的列表,可以使用多列去重将重复的姓名、年龄和城市都相同的行去除,只保留一行。
单列去重
GROUP BY 子句,大部分都是用于单列去重。
例如,假设有一个表包含学生姓名和所在城市,可以使用以下 SQL 语句进行单列去重,以获取不同城市的学生数量:
SELECT city, COUNT(DISTINCT name) FROM student GROUP BY city;
在上述语句中,使用 GROUP BY 子句对城市进行分组,并使用 COUNT 和 DISTINCT 函数计算每个城市中不同姓名的数量。这将返回一个结果集,其中每个行包含一个城市和该城市中不同姓名的数量,从而达到了单列去重的目的。
对于单列去重来说,group by的使用和distinct类似:
单列去重语法:
SELECT columns FROM table_name WHERE where_conditions GROUP BY columns;
执行:
mysql> select age from student group by age;
+------+
| age |
+------+
| 10 |
| 12 |
| 11 |
| NULL |
+------+
4 rows in set (0.02 sec)
注意:和DISTINCT子句一样,group by将所有NULL值视为相同的值
如果列具有NULL值,并且对该列使用group by子句,MySQL将保留一个NULL值,并删除其它的NULL值。
多列去重
GROUP BY 子句也可以用于多列去重。
例如,假设有一个表包含学生姓名、所在城市和年龄,可以使用以下 SQL 语句进行多列去重,以获取不同城市、不同年龄的学生数量:
SELECT city, age, COUNT(DISTINCT name)
FROM student
GROUP BY city, age;
在上述语句中,使用 GROUP BY 子句对城市和年龄进行分组,并使用 COUNT 和 DISTINCT 函数计算每个城市、年龄组合中不同姓名的数量。这将返回一个结果集,其中每个行包含一个城市、一个年龄和该城市、年龄组合中不同姓名的数量,从而达到了多列去重的目的。
多列去重语法:
SELECT column1, column2 FROM table_name WHERE where_conditions GROUP BY column1, column2;
多列去重例子:
mysql> select sex,age from student group by sex,age;
+--------+------+
| sex | age |
+--------+------+
| male | 10 |
| female | 12 |
| male | 11 |
| male | NULL |
| female | 11 |
+--------+------+
5 rows in set (0.03 sec)
group by 多列去重 和 单列去重的区别
GROUP BY 子句用于将结果集按照指定的列进行分组,并对每个组进行聚合操作。在 GROUP BY 子句中指定的列将成为聚合键,用于将结果集中的行分组。在聚合操作之后,可以使用 GROUP BY 子句来去重。
单列去重是指使用 GROUP BY 子句将结果集按照单个列进行分组,并对每个组进行聚合操作。例如,以下查询将返回一个包含不同城市名称的列表:
SELECT city FROM mytable GROUP BY city;
多列去重是指使用 GROUP BY 子句将结果集按照多个列进行分组,并对每个组进行聚合操作。例如,以下查询将返回一个包含不同城市和州的列表:
SELECT city, state FROM mytable GROUP BY city, state;
group by的原理是先对结果进行分组排序,然后返回每组中的第一条数据。所以,区别在于:单列去重只按照一个列进行分组,而多列去重则按照多个列进行分组。
distinct和group by去重原理分析
在大多数例子中,DISTINCT可以被看作是特殊的GROUP BY,它们的实现都基于分组操作,且都可以通过松散索引扫描、紧凑索引扫描来实现。
松散索引扫描和紧凑索引扫描都是 MySQL 中的索引扫描方式。
松散索引扫描(Loose Index Scan)是指 MySQL 在使用索引进行查询时,如果索引中的数据不连续,MySQL 将会扫描整个索引树,直到找到符合条件的记录。这种扫描方式会增加查询的时间复杂度,因为需要扫描整个索引树。
紧凑索引扫描(Tight Index Scan)是指 MySQL 在使用索引进行查询时,如果索引中的数据连续,MySQL 将会按照顺序读取索引数据块,直到找到符合条件的记录。这种扫描方式会减少查询的时间复杂度,因为可以按照顺序读取索引数据块,避免了扫描整个索引树。
通常情况下,如果使用的是 InnoDB 存储引擎,MySQL 会自动选择使用紧凑索引扫描。但是,如果使用的是 MyISAM 存储引擎,或者查询条件中包含了不等于(<>)或不包含(NOT IN)操作符,MySQL 将会使用松散索引扫描。
总的来说,紧凑索引扫描比松散索引扫描更快,因为它可以避免扫描整个索引树。但是,如果使用的是 MyISAM 存储引擎,或者查询条件中包含了不等于或不包含操作符,MySQL 将会使用松散索引扫描,这时候就需要注意查询的效率。
DISTINCT和GROUP BY都是可以使用索引进行扫描搜索的。
例如以下两条sql,我们对这两条sql进行分析,可以看到,在extra中,这两条sql都使用了紧凑索引扫描Using index for group-by
。
所以,在一般情况下,对于相同语义的DISTINCT和GROUP BY语句,我们可以对其使用相同的索引优化手段来进行优化。
mysql> explain select int1_index from test_distinct_groupby group by int1_index;
+----+-------------+-----------------------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-----------------------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | test_distinct_groupby | NULL | range | index_1 | index_1 | 5 | NULL | 955 | 100.00 | Using index for group-by |
+----+-------------+-----------------------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
1 row in set (0.05 sec)
mysql> explain select distinct int1_index from test_distinct_groupby;
+----+-------------+-----------------------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-----------------------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | test_distinct_groupby | NULL | range | index_1 | index_1 | 5 | NULL | 955 | 100.00 | Using index for group-by |
+----+-------------+-----------------------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
1 row in set (0.05 sec)
但对于GROUP BY来说,在MYSQL8.0之前,GROUP BY默认会依据字段进行隐式排序。
可以看到,下面这条sql语句在使用了临时表的同时,还进行了filesort。
mysql> explain select int6_bigger_random from test_distinct_groupby GROUP BY int6_bigger_random;
+----+-------------+-----------------------+------------+------+---------------+------+---------+------+-------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-----------------------+------------+------+---------------+------+---------+------+-------+----------+---------------------------------+
| 1 | SIMPLE | test_distinct_groupby | NULL | ALL | NULL | NULL | NULL | NULL | 97402 | 100.00 | Using temporary; Using filesort |
+----+-------------+-----------------------+------------+------+---------------+------+---------+------+-------+----------+---------------------------------+
1 row in set (0.04 sec)
Group by的隐式排序
对于隐式排序,我们可以参考Mysql官方的解释:
GROUP BY 默认隐式排序(指在 GROUP BY 列没有 ASC 或 DESC 指示符的情况下也会进行排序)。然而,GROUP BY进行显式或隐式排序已经过时(deprecated)了,要生成给定的排序顺序,请提供 ORDER BY 子句。
参考连接:
MySQL :: MySQL 5.7 Reference Manual :: 8.2.1.14 ORDER BY Optimization (https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html)
所以,在MySQL8.0之前,Group by会默认根据作用字段(Group by的后接字段)对结果进行排序。在能利用索引的情况下,Group by不需要额外进行排序操作;但当无法利用索引排序时,MySQL优化器就不得不选择通过使用临时表然后再排序的方式来实现GROUP BY了。要命的是,当临时结果集的大小超出系统设置临时表大小时,MySQL会将临时表数据copy到磁盘上面再进行操作,语句的执行效率会变得极低。这也是Mysql选择将此操作(隐式排序)弃用的原因。
基于上述原因,MySQL在8.0时,对此进行了优化更新:
MySQL5.7版本之前,Group by会根据确定的条件进行隐式排序。在MySQL 8.0中,已经移除了这个功能,所以不再需要通过添加order by null 来禁止隐式排序了,但是,查询结果可能与以前的 MySQL 版本不同。如果要生成给定顺序的结果,请按通过ORDER BY指定需要进行排序的字段。
MySQL :: MySQL 8.0 Reference Manual :: 8.2.1.16 ORDER BY Optimization (https://dev.mysql.com/doc/refman/8.0/en/order-by-optimization.html)
因此,我们的结论也出来了:
-
在语义相同,有索引的情况下:group by和distinct都能使用索引,效率相同。因为group by和distinct近乎等价,distinct可以被看做是特殊的group by。
-
在语义相同,无索引的情况下:distinct效率高于group by。原因是:distinct 和 group by都会进行分组操作,但在MySQL8.0之前group by会进行隐式排序,导致触发filesort,sql执行效率低下。从MySQL8.0开始,MySQL就删除了隐式排序,所以,此时在语义相同,无索引的情况下,group by和distinct的执行效率也是近乎等价的。
总之,从MySQL8.0 开始, 不管有索引 、还是索引, group by和distinct的执行效率也是近乎等价的
但是, 100W级数据去重场景,优先推荐使用 group by。
为什么要优先推荐group by呢?
相比于distinct来说,group by的语义明确。
-
group by语义更为清晰
-
group by可对数据进行更为复杂的一些处理
-
由于distinct关键字会对所有字段生效,在进行复合业务处理时,group by的使用灵活性更高
-
group by能根据分组情况,对数据进行更为复杂的处理,例如通过having对数据进行过滤,或通过聚合函数对数据进行运算。
所以,不论是100W级数据去重场景,还是普通数据去重场景,建议优先选用 group by
从0开始做个后台项目的架构
后台技术栈结构
架构1:团队协助基础工具链的选型和培训
架构2:搭建微服务开发基础设施
架构3:选择合适的RPC框架
架构4:选择和搭建高可用的注册中心
架构5:选择和搭建高可用的配置中心
架构6:选择和搭建高性能的缓存中间件
架构7:选择和搭建高性能的消息中间件
架构8:选择和搭建高性能的关系数据库
架构9:CICD发布系统/部署系统的架构
架构10:360度全方位监控和维护的架构
架构11:生产环境高并发高吞吐负载均衡部署架构
整个后台技术架构,主要包括 4 个层面的内容:
-
语言:用了哪些开发语言,如:C++/Java/Go/PHP/Python/Ruby 等等;
-
组件:用了哪些组件,如:MQ 组件,数据库组件等等;
-
流程:怎样的流程和规范,如:开发流程,项目流程,发布流程,监控告警流程,代码规范等等;
-
系统:系统化建设,上面的流程需要有系统来保证,如:规范发布流程的发布系统,代码管理系统等等;
结合以上的的 4 个层面的内容,整个后台技术栈的结构如图1 所示:
图1 后台技术栈结构
团队协助基础工具链的选型和培训
团队协助基础工具链, 主要是三大管理
-
项目管理
-
任务管理
-
问题管理
项目管理软件是整个业务的需求,问题,流程等等的集中地,大家的跨部门沟通协同大多依赖于项目管理工具。
有一些 SaaS 的项目管理服务可以使用,但是很多时间不满足需求,此时我们可以选择一些开源的项目,这些项目本身有一定的定制能力,有丰富的插件可以使用,一般的创业公司需求基本上都能得到满足,常用的项目如下:
-
Jira:用 Java 开发的,有用户故事,task 拆分,燃尽图等等,可以做项目管理,也可以应用于跨部门沟通场景,较强大;
-
Redmine:用 Ruby 开发的,有较多的插件可以使用,能自定义字段,集成了项目管理,Bug 问题跟踪,WIKI 等功能,不过好多插件 N 年没有更新了;
-
Phabricator:用 PHP 开发的,Facebook 之前的内部工具,开发这工具的哥们离职后自己搞了一个公司专门做这个软件,集成了代码托管, Code Review,任务管理,文档管理,问题跟踪等功能,强烈推荐较敏捷的团队使用;
目前建议是 Jira
搭建微服务开发基础设施
搭建微服务开发基础设施需要考虑多个方面,包括但不限于以下几点:
-
选择合适的微服务框架和技术栈:目前比较流行的微服务框架有 Spring Cloud、Go-Micro、gRPC 等,选择适合自己团队技术栈的框架非常重要。
-
选择合适的RPC框架
-
构建基础设施:包括但不限于服务注册与发现、负载均衡、API 网关、分布式配置中心、分布式锁、消息队列等。
-
安全:包括但不限于服务间通信的加密、访问控制、身份认证等。
在搭建微服务开发基础设施之前,需要对自己的业务场景进行分析和规划,确定需要哪些基础设施和技术栈,然后再逐步实现。同时,需要注重可扩展性和可维护性,以便在业务发展过程中能够快速适应变化。
选择合适的微服务框架和技术栈
选择合适的微服务框架和技术栈需要考虑多个因素,包括以下几个方面:
-
业务需求:不同的业务需求需要不同的技术栈和框架来支持。比如,如果需要高并发和高可用性,可以选择使用 Go 语言和 Kubernetes 等技术来构建微服务。
-
开发团队技能:选择的技术栈和框架应该符合开发团队的技能水平,以便开发人员能够快速上手并高效开发。
-
社区支持:选择流行的技术栈和框架可以获得更好的社区支持,能够更快地解决问题和获得更新的功能。
-
性能和稳定性:选择的技术栈和框架应该具有良好的性能和稳定性,以便能够支持高负载和长时间运行。
常见的微服务框架和技术栈包括:
-
Spring Cloud:适用于 Java 开发团队,具有丰富的功能和社区支持。
-
Go Micro:适用于 Go 开发团队,具有高性能和简单易用的特点。
-
Node.js + Express:适用于 JavaScript 开发团队,具有轻量级和快速开发的特点。
-
Kubernetes:适用于需要高可用性和弹性的微服务架构,可以支持多种编程语言和框架。
-
Istio:适用于需要服务网格功能的微服务架构,可以提供流量管理、安全性和可观察性等功能。
在选择时,需要根据具体的业务需求和开发团队技能来选择合适的微服务框架和技术栈。
建议选用 SpringCloud Alibaba+ Dubbo RPC + Dubbo-Go,两个原因:
(1) 高性能:性能测试案例中, Dubbo比Feign性能 强10倍
(2) 兼顾团队技术栈:可以跨Go 和Java 多语言微服务架构,Java技术栈的同学们,可以基于 Java开发业务微服务,这块侧重业务开发。Go 技术栈的同学们,可以基于 Go 开发高性能的 技术微服务,这块侧重技术开发和性能优化。
(3)功能和性能兼顾:Java侧重功能的快速开发, Go侧重性能的快速提升。
选择合适的RPC框架
维基百科对 RPC 的定义是:远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。
通俗来讲,一个完整的 RPC 调用过程,就是 Server 端实现了一个函数,客户端使用 RPC 框架提供的接口,调用这个函数的实现,并获取返回值的过程。
业界 RPC 框架大致分为两大流派,一种侧重跨语言调用,另一种是偏重服务治理。
跨语言调用型 RPC:
跨语言调用型的 RPC 框架有 Thrift、gRPC、Hessian、Hprose 等。这类 RPC 框架侧重于服务的跨语言调用,能够支持大部分的语言进行语言无关的调用,非常适合多语言调用场景。但这类框架没有服务发现相关机制,实际使用时需要代理层进行请求转发和负载均衡策略控制。
其中,gRPC 是 Google 开发的高性能、通用的开源 RPC 框架,其由 Google 主要面向移动应用开发并基于 HTTP/2 协议标准而设计,基于 ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。本身它不是分布式的,所以要实现框架的功能需要进一步的开发。
Hprose(High Performance Remote Object Service Engine)是一个 MIT 开源许可的新型轻量级跨语言跨平台的面向对象的高性能远程动态通讯中间件。
冶理型 RPC:
服务治理型的 RPC 框架的特点是功能丰富,提供高性能的远程调用、服务发现及服务治理能力,适用于大型服务的服务解耦及服务治理,对于特定语言(Java)的项目可以实现透明化接入。缺点是语言耦合度较高,跨语言支持难度较大。
国内常见的冶理型 RPC 框架如下:
-
Dubbo:Dubbo 是阿里巴巴公司开源的一个 Java 高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring 框架无缝集成。当年在淘宝内部,Dubbo 由于跟淘宝另一个类似的框架 HSF 有竞争关系,导致 Dubbo 团队解散,最近又活过来了,有专职同学投入。
-
DubboX:DubboX 是由当当在基于 Dubbo 框架扩展的一个 RPC 框架,支持 REST 风格的远程调用、Kryo/FST 序列化,增加了一些新的feature。Motan:Motan 是新浪微博开源的一个 Java 框架。它诞生的比较晚,起于 2013 年,2016 年 5 月开源。Motan 在微博平台中已经广泛应用,每天为数百个服务完成近千亿次的调用。
-
rpcx:rpcx 是一个类似阿里巴巴 Dubbo 和微博 Motan 的分布式的 RPC 服务框架,基于 Golang net/rpc 实现。
但是 rpcx 基本只有一个人在维护,没有完善的社区,使用前要慎重。
建议选用Dubbo,两个原因:
(1) 高性能:性能测试案例中, Dubbo比Feign性能 强10倍
(2) 跨语言:可以跨Go 和Java 进行 双语言的 RPC调用,从而实现 多语言微服务架构。
选择和搭建高可用的注册中心
名字发现和服务发现分为两种模式,一个是客户端发现模式,一种是服务端发现模式。框架中常用的服务发现是客户端发现模式。
所谓服务端发现模式是指客户端通过一个负载均衡器向服务发送请求,负载均衡器查询服务注册表并把请求路由到一台可用的服务实例上。现在常用的负载均衡器都是此类模式,常用于微服务中。
所有的名字发现和服务发现都要依赖于一个可用性非常高的服务注册表,业界常用的服务注册表有如下三个:
etcd,一个高可用、分布式、一致性、key-value 方式的存储,被用在分享配置和服务发现中。两个著名的项目使用了它:Kubernetes 和 Cloud Foundry。Consul,一个发现和配置服务的工具,为客户端注册和发现服务提供了API,Consul还可以通过执行健康检查决定服务的可用性。Apache ZooKeeper,是一个广泛使用、高性能的针对分布式应用的协调服务。Apache ZooKeeper 本来是 Hadoop 的子工程,现在已经是顶级工程了。除此之外还有eureka, nacos等,大家可以根据相关的组件特性,选择适合自己的组件。
选择和搭建高可用的注册中心,需要考虑以下几个方面:
-
功能需求:选择注册中心时,需要根据自己的业务需求来选择,比如服务发现、负载均衡、配置管理等。
-
性能要求:注册中心需要具备高性能,能够支持高并发、高吞吐量的请求。
-
可用性要求:注册中心需要具备高可用性,能够保证24小时不间断运行,避免因为单点故障导致整个系统不可用。
-
安全要求:注册中心需要具备一定的安全性,能够保证数据的机密性和完整性,避免数据泄露和篡改。
常见的注册中心有 ZooKeeper、Etcd、Consul 等,它们都具备高可用性和安全性,并且都支持服务发现和配置管理等功能。其中,ZooKeeper 是最早的分布式协调服务,具备成熟的生态系统和广泛的应用场景;Etcd 是 CoreOS 推出的开源分布式键值存储系统,具备高可用性和一致性保证;Consul 是 HashiCorp 推出的服务发现和配置管理工具,具备易用性和可扩展性。
在搭建高可用的注册中心时,需要采用集群部署的方式,避免单点故障。同时,为了保证数据的安全性,可以启用 SSL/TLS 加密功能,并采用访问控制机制来限制访问权限。
目前建议是 高可用的nacos,也就是 nacos+mysql的版本
选择和搭建统一配置中心
随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关、降级开关,灰度开关,参数的配置、服务器的地址、数据库配置等等,除此之外,对后台程序配置的要求也越来越高:配置修改后实时生效,灰度发布,分环境、分用户,分集群管理配置,完善的权限、审核机制等等,在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配置管理的需求,需要统一的、基础的配置系统
统一配置系统是指在一个大型系统中,将所有的配置信息集中管理,以便于对系统进行管理和维护。常见的统一配置系统架构包括以下几个组件:
-
配置中心:用于存储和管理所有的配置信息,提供配置查询、修改、删除等功能。
-
配置客户端:用于从配置中心获取配置信息,并将其应用到系统中。
-
配置发布工具:用于将配置信息发布到配置中心,以便于配置客户端获取。
-
配置管理工具:用于对配置信息进行管理和维护,包括配置的新增、修改、删除等操作。
-
配置监控工具:用于监控配置信息的变化,及时发现并处理配置信息的异常情况。
在实际应用中,可以选择使用开源的配置中心工具,如 ZooKeeper、Etcd、Consul 、Nacos、Apollo等,也可以自己开发一套配置中心系统。
同时,还需要根据实际情况选择合适的配置客户端和配置发布工具。在配置管理和监控方面,可以使用一些开源的工具或者自己开发一套系统。总之,统一配置系统的架构需要根据实际需求进行设计和选择。
目前建议是 高可用的nacos,也就是 nacos+mysql的版本
选择和搭建高性能的缓存中间件
选择和搭建高性能的缓存中间件需要考虑多个因素,包括性能、可靠性、可扩展性、易用性等。以下是一些常见的高性能缓存中间件:
-
Redis:Redis 是一个开源的高性能缓存和键值存储系统,支持多种数据结构,包括字符串、哈希、列表、集合和有序集合等。Redis 通过将数据存储在内存中来提高性能,同时支持数据持久化和集群模式。
-
Memcached:Memcached 是一个开源的高性能分布式内存对象缓存系统,可以缓存任何可序列化的数据,如数据库查询结果、API 响应等。Memcached 可以通过多个节点组成的集群来提高可扩展性和可靠性。
-
Hazelcast:Hazelcast 是一个开源的分布式内存数据网格系统,支持缓存、分布式数据结构和分布式计算等功能。Hazelcast 可以通过多个节点组成的集群来提高可扩展性和可靠性。
-
Couchbase:Couchbase 是一个开源的分布式 NoSQL 数据库和缓存系统,可以缓存任何类型的数据,包括 JSON 文档、键值对和二进制数据等。Couchbase 支持多个节点组成的集群和数据持久化等功能。
在搭建高性能缓存中间件时,需要考虑以下几个方面:
-
硬件配置:缓存中间件需要占用大量内存,因此需要配置足够的内存和处理器资源。
-
部署架构:需要考虑缓存中间件的部署架构,如单节点、主从复制、集群等。
-
数据持久化:需要考虑数据持久化的方式,如内存快照、AOF 日志、RDB 文件等。
-
安全性:需要考虑缓存中间件的安全性,如访问控制、数据加密等。
-
监控和管理:需要考虑缓存中间件的监控和管理,如性能监控、故障诊断等。
总之,选择和搭建高性能缓存中间件需要综合考虑多个因素,根据具体需求和场景进行选择和配置。
目前建议是 高可用的redis cluster
要特别注意的是, redis 关系到系统的 高可用, 很容易出生产事故。
如果 redis 出现bigkey,在 高并发场景下, 很容易出现 系统瘫痪, 严重影响系统的可用性, 昨天有小伙伴来求助,他们的redis bigkey问题,导致他们的系统瘫痪一小时, 经济损失 几百万,间接损失 几千万。
如果对big key 进行探测和解决
选择和搭建高性能的消息中间件
消息中间件在后台系统中是必不可少的一个组件,一般我们会在以下场景中使用消息中间件:
-
异步处理:
异步处理是使用消息中间件的一个主要原因,在工作中最常见的异步场景有用户注册成功后需要发送注册成功邮件、缓存过期时先返回老的数据,然后异步更新缓存、异步写日志等等;通过异步处理,可以减少主流程的等待响应时间,让非主流程或者非重要业务通过消息中间件做集中的异步处理。
-
系统解耦:
比如在电商系统中,当用户成功支付完成订单后,需要将支付结果给通知ERP系统、发票系统、WMS、推荐系统、搜索系统、风控系统等进行业务处理;这些业务处理不需要实时处理、不需要强一致,只需要最终一致性即可,因此可以通过消息中间件进行系统解耦。通过这种系统解耦还可以应对未来不明确的系统需求。
-
削峰填谷:
当系统遇到大流量时,监控图上会看到一个一个的山峰样的流量图,通过使用消息中间件将大流量的请求放入队列,通过消费者程序将队列中的处理请求慢慢消化,达到消峰填谷的效果。最典型的场景是秒杀系统,在电商的秒杀系统中下单服务往往会是系统的瓶颈,因为下单需要对库存等做数据库操作,需要保证强一致性,此时使用消息中间件进行下单排队和流控,让下单服务慢慢把队列中的单处理完,保护下单服务,以达到削峰填谷的作用。
业界消息中间件是一个非常通用的东西,大家在做选型时有使用开源的,也有自己造轮子的,甚至有直接用 MySQL 或 Redis 做队列的,关键看是否满足你的需求.
选择合适的消息中间件需要考虑多个因素,包括但不限于:
-
需要处理的消息数量和频率
-
消息的大小和格式
-
可用性和容错性要求
-
数据安全性和加密需求
-
扩展性和灵活性要求
-
开发语言和技术栈的兼容性 常见的消息中间件包括RocketMQ、Kafka、 RabbitMQ、Kafka、ActiveMQ、Redis、NATS 等,每种中间件都有其特点和适用场景。
如果需要处理大量的消息并且需要高吞吐量和低延迟,可以考虑使用 Kafka。如果需要实时处理消息并且需要高可用性和容错性,可以考虑使用 RabbitMQ。如果需要处理轻量级的消息,并且需要高性能和低延迟,可以考虑使用 Redis。
在选择消息中间件时,需要根据具体的业务需求和技术栈进行综合考虑,选择最合适的中间件。
目前建议 kafka + RocketMQ
选择和搭建高性能的关系数据库
关系数据库分为两种,一种是传统关系数据,如 Oracle,MySQL,Maria,DB2,PostgreSQL 等等,另一种是 NewSQL,即至少要满足以下五点的新型关系数据库:
-
完整地支持 SQL,支持 JOIN / GROUP BY /子查询等复杂 SQL 查询。
-
支持传统数据标配的 ACID 事务,支持强隔离级别。
-
具有弹性伸缩的能力,扩容缩容对于业务层完全透明。
-
真正的高可用,异地多活、故障恢复的过程不需要人为的接入,系统能够自动地容灾和进行强一致的数据恢复。
-
具备一定的大数据分析能力。
传统关系数据库用得最多的是 MySQL,成熟,稳定,一些基本的需求都能满足,在一定数据量级之前基本单机传统数据库都可以搞定,而且现在较多的开源系统都是基于 MySQL,开箱即用,再加上主从同步和前端缓存,百万 pv 的应用都可以搞定了。
不过 CentOS 7 已经放弃了 MySQL,而改使用 MariaDB。MariaDB 数据库管理系统是 MySQ L的一个分支,主要由开源社区在维护,采用 GPL 授权许可。开发这个分支的原因之一是:甲骨文公司收购了 MySQL 后,有将 MySQL 闭源的潜在风险,因此社区采用分支的方式来避开这个风险。
在 Google 发布了 F1: A Distributed SQL Database That Scales 和 Spanner: Google’s Globally-Distributed Databasa 之后,业界开始流行起 NewSQL。于是有了 CockroachDB,于是有了奇叔公司的 TiDB。
国内已经有比较多的公司使用 TiDB,之前在创业公司时在大数据分析时已经开始应用 TiDB,当时应用的主要原因是 MySQL 要使用分库分表,逻辑开发比较复杂,扩展性不够。
选择和搭建高性能的NoSQL
NoSQL 顾名思义就是 Not-Only SQL,也有人说是 No – SQL,个人偏向于 Not-Only SQL,它并不是用来替代关系库,而是作为关系型数据库的补充而存在。
常见 NoSQL 有4个类型:
-
键值,适用于内容缓存,适合混合工作负载并发高扩展要求大的数据集,其优点是简单,查询速度快,缺点是缺少结构化数据,常见的有 Redis,Memcache,BerkeleyDB 和 Voldemort 等等;
-
列式,以列簇式存储,将同一列数据存在一起,常见于分布式的文件系统,其中以 Hbase,Cassandra 为代表。Cassandra 多用于写多读少的场景,国内用得比较多的有 360,大概 1500 台机器的集群,国外大规模使用的公司比较多,如 eBay,Instagram,Apple 和沃尔玛等等;
-
文档,数据存储方案非常适用承载大量不相关且结构差别很大的复杂信息。性能介于 kv 和关系数据库之间,它的灵感来于 lotus notes,常见的有 MongoDB,CouchDB 等等;
-
图形,图形数据库擅长处理任何涉及关系的状况。社交网络,推荐系统等。专注于构建关系图谱,需要对整个图做计算才能得出结果,不容易做分布式的集群方案,常见的有 Neo4J,InfoGrid 等。
除了以上4种类型,还有一些特种的数据库,如对象数据库,XML 数据库,这些都有针对性对某些存储类型做了优化的数据库。
在实际应用场景中,何时使用关系数据库,何时使用 NoSQL,使用哪种类型的数据库,这是我们在做架构选型时一个非常重要的考量,甚至会影响整个架构的方案。
CICD发布系统/部署系统的架构
软件生产的层面看,代码到最终服务的典型流程如图2 所示:
图2 流程图
从上图中可以看出,从开发人员写下代码到服务最终用户是一个漫长过程,整体可以分成三个阶段:
-
从代码(Code)到制品库(Artifact):这个阶段主要对开发人员的代码做持续构建,并把构建产生的制品集中管理,是为部署系统准备输入内容的阶段。
-
从制品到可运行服务:这个阶段主要完成制品部署到指定环境,是部署系统的最基本工作内容。
-
从开发环境到最终生产环境:这个阶段主要完成一次变更在不同环境的迁移,是部署系统上线最终服务的核心能力。
发布系统集成了制品管理,发布流程,权限控制,线上环境版本变更,灰度发布,线上服务回滚等几方面的内容,是开发人员工作结晶最终呈现的重要通道。
CI/CD 发布系统/部署系统的架构通常包括以下组件:
-
源代码管理系统:例如 Git、SVN 等,用于管理代码库。
-
持续集成工具:例如 Jenkins、GitLab CI、Travis CI 等,用于自动化构建、测试和打包应用程序。
-
制品仓库:例如 Docker Hub、Harbor、Aliyun Container Registry 等,用于存储应用程序的镜像。
-
部署工具:例如 Kubernetes、Docker Swarm、Mesos 等,用于自动化部署应用程序。
这些组件可以根据实际需求进行选择和组合,形成一个完整的 CI/CD 发布系统/部署系统。
其中,持续集成工具和部署工具是核心组件,它们负责自动化构建、测试、打包和部署应用程序,从而实现快速、可靠、可重复的软件发布流程。
项目初期可以集成 Jenkins + Gitlab + Harbor,以上方案基本包括制品管理,发布流程,权限控制,线上环境版本变更,灰度发布(需要自己实现),线上服务回滚等功能。
代码管理工具选型
代码是项目的命脉之一,代码管理很重要,常见的考量点包括两块:
安全和权限管理,将代码放到内网并且对于关系公司命脉的核心代码做严格的代码控制和机器的物理隔离;代码管理工具,Git 作为代码管理的不二之选,你值得拥有。
GitLab 是当今最火的开源 Git 托管服务端,没有之一,虽然有企业版,但是其社区版基本能满足我们大部分需求,结合 Gerrit 做 Code review,基本就完美了。
当然 GitLab 也有代码对比,但没 Gerrit 直观。
Gerrit 比 GitLab 提供了更好的代码检查界面与主线管理体验,更适合在对代码质量有高要求的文化下使用。
持续集成工具选型
持续集成简,称 CI(continuous integration),是一种软件开发实践,即团队开发成员经常集成他们的工作,每天可能会发生多次集成。
每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。
持续集成为研发流程提供了代码分支管理/比对、编译、检查、发布物输出等基础工作,为测试的覆盖率版本编译、生成等提供统一支持。
业界免费的持续集成工具中系统我们有如下一些选择:
-
Jenkins:Java 写的有强大的插件机制,MIT 协议开源 (免费,定制化程度高,它可以在多台机器上进行分布式地构建和负载测试)。Jenkins 可以算是无所不能,基本没有 Jenkins 做不了的,无论从小型团队到大型团队 Jenkins 都可以搞定。不过如果要大规模使用,还是需要有人力来学习和维护。
-
TeamCity:TeamCity 与 Jenkins 相比使用更加友好,也是一个高度可定制化的平台。但是用的人多了,TeamCity就要收费了。
-
Strider:Strider 是一个开源的持续集成和部署平台,使用 Node.js 实现,存储使用的是 MongoDB,BSD 许可证,概念上类似 Travis 和Jenkins。
-
GitLab CI:从GitLab 8.0开始,GitLab CI 就已经集成在 GitLab,我们只要在项目中添加一个 .gitlab-ci.yml 文件,然后添加一个 Runner,即可进行持续集成。并且 GitLab 与 Docker 有着非常好的相互协作的能力。
免费版与付费版本不同可以参见这里:https://about.gitlab.com/products/feature-comparison/。
-
Travis:Travis 和 GitHub 强关联;闭源代码使用 SaaS 还需考虑安全问题;不可定制;开源项目免费,其它收费。
-
Go:Go 是 ThoughtWorks 公司最新的 Cruise Control 的化身。除了 ThoughtWorks 提供的商业支持,Go 是免费的。它适用于 Windows,Mac 和各种 Linux 发行版。
自动化测试平台的架构
接下来,就是自动化测试平台的搭建。
搭建自动化测试平台需要考虑以下几个方面:
-
选择合适的测试框架和工具:可以选择一些流行的测试框架和工具,如Selenium、Appium、JMeter等,根据需要选择适合自己的工具。
-
搭建测试环境:需要搭建测试环境,包括测试服务器、测试数据库、测试数据等。可以使用虚拟机或者容器来搭建测试环境,以便进行测试。
-
编写测试用例:需要编写测试用例,测试用例应该覆盖系统的各个功能点,以便发现潜在的问题。
-
集成测试工具和测试用例:将测试工具和测试用例集成到自动化测试平台中,以便进行自动化测试。
-
运行测试用例:编写好测试用例后,需要运行测试用例,收集测试结果,并生成测试报告。
-
定期维护和更新:自动化测试平台需要定期维护和更新,以保证测试环境的稳定性和测试用例的有效性。
以上是搭建自动化测试平台的一般步骤,具体实现方式还需要根据实际情况进行调整。
可以结合 SpringBoot + TestNG 测试框架,搭建自己的 自动化测试平台
TestNG 是一个开源自动化测试框架;
TestNG 灵感来自 JUnit 和 NUnit,但引入了一些新的功能,使其功能更强大,使用更方便。
TestNG表示下一代(Next Generation的首字母)。
TestNG类似于 JUnit (特别是 JUnit 4),但它不是JUnit框架的扩展。它的目的是优于 JUnit ,尤其是在用于测试集成多类时。
TestNG的创始人是 Cedric Beust (塞德里克·博伊斯特)。
TestNG消除了大部分的旧框架的限制,使开发人员能够编写更加灵活和强大的测试。因为它在很大程度上借鉴了Java注解( JDK5.0引入的)来定义测试,它也可以显示如何使用这个新功能在真实的 Java 语言生产环境中。
40岁老架构师尼恩提示,不用自己从0到1 去搭建自动化测试平台,可以基于开源的自动化测试平台进行改造。
下面的两个 测试平台,就是非常好的 改造项目:
-
接口自动化测试框架(java httpClient + testNg)
-
ChenSen5/api_autotest (https://github.com/ChenSen5/api_autotest)
-
-
基于SpringBoot的高效模板化自动化测试框架
-
jinganglong123/jg-api-autotest (https://github.com/jinganglong123/jg-api-autotest)
-
360度全方位监控和维护的架构
360度全方位监控和维护的架构包括
-
日志系统
-
监控系统
日志系统
日志系统一般包括打日志,采集,中转,收集,存储,分析,呈现,搜索还有分发等。
一些特殊的如染色,全链条跟踪或者监控都可能需要依赖于日志系统实现。
日志系统的建设不仅仅是工具的建设,还有规范和组件的建设,最好一些基本的日志在框架和组件层面加就行了,比如全链接跟踪之类的。
对于常规日志系统ELK能满足大部分的需求,ELK 包括如下组件:
ElasticSearch 是个开源分布式搜索引擎,它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,RESTful 风格接口,多数据源,自动搜索负载等。
Logstash 是一个完全开源的工具,它可以对你的日志进行收集、分析,并将其存储供以后使用。Kibana 是一个开源和免费的工具,它可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助汇总、分析和搜索重要数据日志。
Filebeat 已经完全替代了 Logstash-Forwarder 成为新一代的日志采集器,同时鉴于它轻量、安全等特点,越来越多人开始使用它。
因为免费的 ELK 没有任何安全机制,所以这里使用了 Nginx 作反向代理,避免用户直接访问 Kibana 服务器。
加上配置 Nginx 实现简单的用户认证,一定程度上提高安全性。
另外,Nginx 本身具有负载均衡的作用,能够提高系统访问性能。
ELK 架构如图3 所示:
图3 ELK 流程图
对于有实时计算的需求,可以使用 Flume + Kafka + Storm + MySQL 方案,一 般架构如图4 所示:
图4 实时分析系统架构图
其中:
Flume 是一个分布式、可靠、和高可用的海量日志采集、聚合和传输的日志收集系统,支持在日志系统中定制各类数据发送方,用于收集数据;同时,Flume 提供对数据进行简单处理,并写到各种数据接受方(可定制)的能力。Kafka 是由 Apache 软件基金会开发的一个开源流处理平台,由 Scala 和 Java 编写。其本质上是一个“按照分布式事务日志架构的大规模发布/订阅消息队列”,它以可水平扩展和高吞吐率而被广泛使用。
Kafka 追求的是高吞吐量、高负载,Flume 追求的是数据的多样性,二者结合起来简直完美。
监控系统
监控系统只包含与后台相关的,这里主要是两块,一个是操作系统层的监控,比如机器负载,IO,网络流量,CPU,内存等操作系统指标的监控。
另一个是服务质量和业务质量的监控,比如服务的可用性,成功率,失败率,容量,QPS 等等。
常见业务的监控系统先有操作系统层面的监控(这部分较成熟),然后扩展出其它监控,如 Zabbix,小米的 Open-Falcon,也有一出来就是两者都支持的,如 Prometheus。
如果对业务监控要求比较高一些,在创业选型中建议可以优先考虑 Prometheus。
这里有一个有趣的分布,如图5 所示。
图5 监控系统分布
亚洲区域使用 Zabbix 较多,而美洲和欧洲,以及澳大利亚使用 Prometheus 居多,换句话说,英文国家地区(发达国家?)使用 Prometheus 较多。
Prometheus 是由 SoundCloud 开发的开源监控报警系统和时序列数据库(TSDB)。
Prometheus 使用 Go 语言开发,是 Google BorgMon 监控系统的开源版本。
相对于其它监控系统使用的 push 数据的方式,Prometheus 使用的是 pull 的方式,其架构如图6 所示:
图6 Prometheus 架构图
如上图所示,Prometheus 包含的主要组件如下:
-
Prometheus Server:主要负责数据采集和存储,提供 PromQL 查询语言的支持。
-
Server:通过配置文件、文本文件、ZooKeeper、Consul、DNS SRV Lookup 等方式指定抓取目标。
根据这些目标会,Server 定时去抓取 metrics 数据,每个抓取目标需要暴露一个 http 服务的接口给它定时抓取。
-
客户端 SDK:官方提供的客户端类库有 Go、Java、Scala、Python、Ruby,其他还有很多第三方开发的类库,支持 Nodejs、PHP、Erlang 等。
-
Push Gateway:支持临时性 Job 主动推送指标的中间网关。
-
Exporter Exporter:是 Prometheus 的一类数据采集组件的总称。它负责从目标处搜集数据,并将其转化为 Prometheus 支持的格式。
与传统的数据采集组件不同的是,它并不向中央服务器发送数据,而是等待中央服务器主动前来抓取。
-
Prometheus:提供多种类型的 Exporter 用于采集各种不同服务的运行状态。目前支持的有数据库、硬件、消息中间件、存储系统、HTTP 服务器、JMX 等。
-
Alertmanager:是一个单独的服务,可以支持 Prometheus 的查询语句,提供十分灵活的报警方式。
-
Prometheus HTTP API 的查询方式,自定义所需要的输出。
-
Grafana:是一套开源的分析监视平台,支持 Graphite,InfluxDB,OpenTSDB,Prometheus,Elasticsearch,CloudWatch 等数据源,其 UI 非常漂亮且高度定制化。
创业公司选择 Prometheus + Grafana 的方案,再加上统一的服务框架(如 gRPC),可以满足大部分中小团队的监控需求。
生产环境高并发高吞吐负载均衡部署架构
高并发高吞吐负载均衡链路架构,包括:
-
DNS的选型和使用设计
-
LB(负载均衡)的选型和使用设计
-
CDN的选型和使用设计
DNS的选型和使用设计
DNS 是一个很通用的服务,创业公司基本上选择一个合适的云厂商就行了,国内主要是两家:
阿里万网:阿里 2014 年收购了万网,整合了其域名服务,最终形成了现在的阿里万网,其中就包含 DNS 这块的服务;
腾讯 DNSPod:腾讯 2012 年以 4000 万收购 DNSPod 100% 股份,主要提供域名解析和一些防护功能;
如果你的业务是在国内,主要就是这两家,选 一个就好,像今日头条这样的企业用的也是 DNSPod 的服务,除非一些特殊的原因才需要自建,比如一些 CDN 厂商,或者对区域有特殊限制的。
要实惠一点用阿里最便宜的基础版就好了,要成功率高一些,还是用 DNSPod 的贵的那种。
在国外还是选择亚马逊吧,阿里的 DNS 服务只有在日本和美国有节点,东南亚最近才开始部点, DNSPod 也只有美国和日本,像一些出海的企业,其选择的云服务基本都是亚马逊。
如果是线上产品,DNS 强烈建议用付费版,阿里的那几十块钱的付费版基本可以满足需求。如果还需要一些按省份或按区域调试的逻辑,则需要加钱,一年也就几百块,省钱省力。
如果是国外,优先选择亚马逊,如果需要国内外互通并且有自己的 APP 的话,建议还是自己实现一些容灾逻辑或者智能调度,因为没有一个现成的 DNS 服务能同时较好的满足国内外场景,或者用多个域名,不同的域名走不同的 DNS 。
LB(负载均衡)的选型和使用设计
LB(负载均衡)是一个通用服务,一般云厂商的 LB 服务基本都会如下功能:
-
支持四层协议请求(包括 TCP、UDP 协议);
-
支持七层协议请求(包括 HTTP、HTTPS 协议);
-
集中化的证书管理系统支持 HTTPS 协议;
-
健康检查;
如果你线上的服务机器都是用的云服务,并且是在同一个云服务商的话,可以直接使用云服务商提供的 LB 服务,如阿里云的 SLB,腾讯云的 CLB,亚马逊的 ELB 等等。如果是自建机房基本都是 LVS + Nginx。
CDN的选型和使用设计
CDN 现在已经是一个很红很红的市场,基本上只能挣一些辛苦钱,都是贴着成本在卖。国内以网宿为龙头,他们家占据整个国内市场份额的 40% 以上,后面就是腾讯,阿里。网宿有很大一部分是因为直播的兴起而崛起。
国外,Amazon 和 Akamai合起来占比大概在 50%,曾经的国际市场老大 Akamai 拥有全球超一半的份额,在 Amazon CDN入局后,份额跌去了将近 20%,众多中小企业都转向后者,Akamai 也是无能为力。
国内出海的 CDN 厂商,更多的是为国内的出海企业服务,三家大一点的 CDN 服务商里面也就网宿的节点多一些,但是也多不了多少。阿里和腾讯还处于前期阶段,仅少部分国家有节点。
就创业公司来说,CDN 用腾讯云或阿里云即可,其相关系统较完善,能轻松接入,网宿在系统支持层面相对较弱一些,而且还贵一些。并且,当流量上来后,CDN 不能只用一家,需要用多家,不同的 CDN 在全国的节点覆盖不一样,而且针对不同的客户云厂商内部有些区分客户集群,并不是全节点覆盖(但有些云厂商说自己是全网节点),除了节点覆盖的问题,多 CDN 也在一定程度上起到容灾的作用。
怎么防接口被恶刷10Wqps
接口被狂刷的严重后果
恶意攻击者通常会通过自动化工具进行攻击,尤其是会针对一些高频接口、核心接口进行恶意的访问,恶意的攻击,比如:
-
注册登录接口
-
秒杀抢购接口
-
等等
接口被狂刷会带来很高的瞬时吞吐量,很容易超过1Wqps,甚至10WQPS。这样的超高并发,会导致系统的瞬时雪崩,严重的可能会导致线上系统 瘫痪。
接口狂刷的主要防护措施
-
交互式验证
-
安全参数校验
-
使用 HTTPS
-
用户访问认证
-
资源访问授权
-
访问限流
-
IP封禁
-
日志监控和异步分析
-
升级硬件设备
-
基于时序的统计预警
交互式验证
主要包括:
-
验证码验证:在发送验证码之前,可以要求用户输入一个验证码,以验证用户的身份。这种方式可以有效地防止自动化攻击。
-
人机验证:人机验证是一种更高级的验证方式,可以检测用户行为是否类似于自动化攻击。例如,可以要求用户在发送验证码之前完成一个简单的任务,如拖动滑块或识别图片中的文字。
安全参数校验
当接口被恶意狂刷时,可以通过安全参数校验来防止这种攻击。安全参数校验是指在接口请求中添加一些校验参数,例如时间戳、随机字符串、签名等,来验证请求的合法性。这样可以防止攻击者通过恶意程序进行大量的请求攻击。
具体来说,可以通过以下步骤来实现安全参数校验:
-
在接口请求中添加时间戳参数,例如:
timestamp=1622945123
。 -
在接口请求中添加随机字符串参数,例如:
nonce=abc123
。 -
将所有请求参数按照参数名的字母顺序排序,例如:
nonce=abc123×tamp=1622945123
。 -
将排序后的参数按照“参数名=参数值”的格式拼接成一个字符串,例如:
nonce=abc123×tamp=1622945123
。 -
将拼接后的字符串加上一个密钥(可以是预先约定好的密钥),例如:
nonce=abc123×tamp=1622945123&key=secret
。 -
对加密后的字符串进行哈希计算,例如使用 MD5 算法,得到一个签名值,例如:
c0c3f9a2a4c4c4dcd6d5b7b2a2e4d7b1
。将签名值添加到接口请求中,例如:nonce=abc123×tamp=1622945123&signature=c0c3f9a2a4c4c4dcd6d5b7b2a2e4d7b1
。 -
在接口服务端对接口请求进行校验时,按照相同的算法计算签名值,并与请求中的签名值进行比对,如果一致,则说明请求合法,否则说明请求不合法。
通过以上步骤,可以有效地防止接口被恶意狂刷的攻击。
理论上,哈希计算很难破解,但是如果攻击者知道了hash算法和盐,攻击者就有可能伪造出带有正确校验位的签名值,从而绕过Java接口的限流和安全机制。
因此,该方案主要适用于需要简单防范一些低强度攻击的场景,例如防范垃圾请求或非法爬虫等。
对于高强度攻击,建议采取更为复杂的验证策略,例如使用使用用户访问认证,资源访问授权、IP白名单、签名算法等。
使用 HTTPS
使用 HTTPS 可以保护数据传输的安全性,可以防止恶意攻击者窃取数据。HTTPS 使用 SSL/TLS 协议对数据进行加密,可以确保数据在传输过程中不被篡改或窃取。
因此,在一些恶意狂刷的高频接口,比如短信验证码接口、登录注册入口等敏感区域使用 HTTPS 是必要的。
当然,尽量在客户端和服务端在全部通讯,都使用HTTPS协议进行加密,防止数据被窃听或篡改。
用户访问认证
用户访问认证是指在系统中验证用户身份以授权其访问系统资源的过程。
用户访问认证是信息安全中非常重要的一环,可以保护系统免受未经授权的访问和攻击。
常见的用户访问认证方式包括用户名密码认证、双因素认证、证书认证等
这里来看最为简单的 用户访问认证:用户名密码认证
用户名密码认证 的方式,要求用户提供 用户名和密码,换取 访问的令牌。
参考的代码如下:
@RequestMapping("/api/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password){
if(!checkUser(username, password)){
return "用户名或密码错误";
}
String token = getToken();
saveToken(token);
return token;
}
private boolean checkUser(String username, String password){
//校验用户是否合法
}
private String getToken(){
//生成token
}
private void saveToken(String token){
//保存token
}
在上述代码中,当用户调用login接口时,需要提供用户名和密码。
此时会进行用户校验,若校验失败则返回错误信息,否则生成token并保存,最终返回给用户。
生成Token的作用是为了在接口请求时验证用户身份。
具体来说,当用户第一次登录系统后,该接口可以根据用户信息生成一个Token字符串,并将其保存至服务端或客户端。
-
当此用户访问其他需要鉴权的接口时,需要在请求头中带上这个Token字符串,以便服务器进行身份验证。
-
由于Token是由服务端生成的,攻击方无法自己生成有效的Token,因此只有拥有合法Token的用户才能成功调用相关接口。
对于Java接口被恶意狂刷问题,Token的作用是防止非法请求。
如果Token验证失败,则返回错误信息并拦截该请求。
-
关于Token的验证,可以通过拦截器实现。
-
拦截器可以在接口调用前检查请求头中是否包含合法的Token,并验证Token是否过期、是否被篡改等。
下面是使用拦截器进行令牌校验的示例代码:
// 鉴权拦截器
public class AuthInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !checkToken(token)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
return true;
}
private boolean checkToken(String token) {
// 验证Token是否合法,是否过期等
}
}
AuthInterceptor类是拦截器类,用于检查请求头中的Token是否合法。
如果Token验证失败,则返回401错误码并拦截该请求。
资源访问授权
如果接口的安全性要求非常高,只有特定的用户才能访问。
或者说,如果遇到要对资源进行更细粒度的 防刷处理,可以对资源进行访问权限的管理和授权。主要的策略有RBAC的机制。
主要的思路为:使用 访问控制策略 对资源权限进行 精准控制。场景的访问控制策略 为RBAC策略。
RBAC(Role-Based Access Control,基于角色的访问控制)是一种常见的访问控制策略,它将用户分配到不同的角色中,每个角色具有一组权限,从而控制用户对系统资源的访问。
在 RBAC 中,管理员可以根据用户的职责和职位,将用户分配到适当的角色中,从而控制用户对系统资源的访问权限。RBAC 还可以提高系统的安全性和可管理性,减少权限管理的复杂性。
Shiro 是一个强大且易于使用的 RBAC 访问的安全框架,提供了身份验证、授权、加密、会话管理等安全功能。其中授权是 Shiro 的核心功能之一,它可以帮助我们实现资源访问授权。
在 Shiro 中,授权是通过授权信息和角色信息来实现的。授权信息是指哪些用户可以访问哪些资源,角色信息是指用户可以拥有哪些权限。
Shiro 中的授权流程如下:
-
用户登录系统,进行身份认证。
-
身份验证成功后,Shiro 将用户信息存储在 Subject 中。
-
用户请求访问某个资源。
-
Shiro 从 Subject 中获取用户信息,并根据用户信息和授权信息判断用户是否有权限访问该资源。
-
如果用户有权限访问该资源,则允许访问;否则拒绝访问。
在 Shiro 中,授权信息和角色信息可以通过配置文件或数据库来管理。我们可以在配置文件或数据库中定义哪些用户可以访问哪些资源,以及哪些角色可以拥有哪些权限。在程序运行时,Shiro 会从配置文件或数据库中读取授权信息和角色信息,并根据这些信息进行授权判断。
除了配置文件和数据库,Shiro 还提供了编程式授权方式,即通过编写代码来实现授权。这种方式可以实现更加灵活的授权,但需要开发人员自己编写授权逻辑。
下面是 Shiro 的使用入门步骤:
1. 引入 Shiro 依赖
在 Maven 项目中,可以通过在 pom.xml 文件中添加以下依赖来引入 Shiro:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.7.1</version>
</dependency>
2. 配置 Shiro
在 Shiro 中,可以通过配置文件或编程方式来配置安全策略。下面是一个简单的 Shiro 配置文件示例:
[main]
# 定义一个默认的 Realm,用于认证和授权
myRealm = com.example.MyRealm
# 定义一个默认的加密算法
passwordService = org.apache.shiro.authc.credential.DefaultPasswordService
passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher
passwordMatcher.passwordService = $passwordService
# 配置安全管理器
securityManager = org.apache.shiro.mgt.DefaultSecurityManager
securityManager.realm = $myRealm
# 配置加密器
securityManager.passwordService = $passwordService
securityManager.authenticator.passwordMatcher = $passwordMatcher
# 配置会话管理器
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
securityManager.sessionManager = $sessionManager
# 配置缓存管理器
cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
securityManager.cacheManager = $cacheManager
[users]
# 定义用户及其密码和角色
admin = admin, admin_role
user1 = password1, user_role1
user2 = password2, user_role2
[roles]
# 定义角色及其权限
admin_role = *
user_role1 = user:read, user:write
user_role2 = user:read
3. 创建 Realm
在 Shiro 中,Realm 是用于认证和授权的核心组件。可以通过实现 org.apache.shiro.realm.Realm 接口来创建自定义的 Realm,或者使用 Shiro 提供的现成的 Realm 实现。下面是一个简单的自定义 Realm 示例:
public class MyRealm implements Realm {
@Override
public String getName() {
return "myRealm";
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
String password = new String((char[]) token.getCredentials());
if (!"admin".equals(username)) {
throw new UnknownAccountException("Unknown user");
}
if (!"password".equals(password)) {
throw new IncorrectCredentialsException("Incorrect password");
}
return new SimpleAuthenticationInfo(username, password, getName());
}
@Override
public AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole("admin_role");
info.addStringPermission("user:read");
info.addStringPermission("user:write");
return info;
}
}
4. 认证和授权
在应用程序中,可以通过调用 Shiro 提供的 Subject 类的方法来进行认证和授权。下面是一个简单的示例:
// 获取当前用户的 Subject
Subject currentUser = SecurityUtils.getSubject();
// 创建一个用户名和密码的 Token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "password");
try {
// 进行认证
currentUser.login(token);
// 进行授权
if (currentUser.hasRole("admin_role")) {
System.out.println("User has admin role");
}
if (currentUser.isPermitted("user:read")) {
System.out.println("User has read permission");
}
if (currentUser.isPermitted("user:write")) {
System.out.println("User has write permission");
}
} catch (UnknownAccountException e) {
System.out.println("Unknown user");
} catch (IncorrectCredentialsException e) {
System.out.println("Incorrect password");
} catch (LockedAccountException e) {
System.out.println("Account is locked");
} catch (AuthenticationException e) {
System.out.println("Authentication error");
}
以上就是 Shiro 的入门使用步骤。当然,Shiro 还提供了很多其他的功能和配置选项,需要根据具体的应用场景进行选择和使用。
和Shiro类似,SpringSecurit 也是个资源访问授权的框架。
访问限流
访问限流是一种常见的保护机制,用于控制对某个资源的访问速率,以防止过多的请求导致系统负载过高或崩溃。
访问限流包括两个维度:
-
访问限流策略
-
访问限流算法
维度一:访问限流策略
-
面向接口限流
-
面向用户限流
维度二:访问限流算法
-
令牌桶算法
-
漏桶算法
在令牌桶算法中,系统会按照一定速率往令牌桶中添加令牌,每个令牌代表一个请求的访问权限。当请求到来时,系统会从令牌桶中取出一个令牌,如果令牌桶中没有令牌,则拒绝该请求。
在漏桶算法中,系统会按照一定速率从漏桶中释放请求,当请求到来时,如果漏桶中还有空余容量,则将该请求放入漏桶中,否则拒绝该请求。
接口被恶意狂,可以使用 基于 漏桶算法 + 基于用户限流的 综合性限流策略。
可以结合黑名单策略,对恶意用户进行有效人工管理。如果用户被限流,甚至可以加入黑名单,封掉这个用户。
https://blog.csdn.net/crazymakercircle/article/details/130035504
IP封禁
IP封禁是常见的网络安全措施,用于保护服务器免受恶意攻击。
IP封禁是指将某个IP地址列入黑名单,禁止其访问服务器。
在实际应用中,可以通过配置防火墙规则、使用反向代理服务器、使用专业的防火墙软件等方式来实现IP封禁和防刷。
也可以在应用层代码中,通过过滤器的方式,进行IP封禁
参考代码如下:
public class IpFilter extends OncePerRequestFilter {
private static final Set<String> IP_SET = new HashSet<>();
static {
IP_SET.add("192.168.1.100");
IP_SET.add("127.0.0.1");
//添加其他需要封禁的IP
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String ipAddress = request.getRemoteAddr();
if(IP_SET.contains(ipAddress)){
response.setStatus(HttpStatus.FORBIDDEN.value());
return;
}
filterChain.doFilter(request, response);
}
}
在上述代码中,通过IpFilter过滤器来阻止特定的IP地址访问接口。其中,IP_SET为需要封禁的IP地址集合。
日志监控和异步分析
访问日志监控是一种常见的监控方式,用于监控网站、应用程序等的访问情况,可以帮助我们了解用户的行为和需求,以便做出相应的优化和改进。
常见的访问日志监控工具有 Apache 的 AccessLog、Nginx 的 AccessLog、ELK Stack 、Java 请求日志监控等。
这些工具、框架帮助我们收集、分析和可视化访问日志数据,从而更好地了解用户的需求和行为。
同时,我们也可以通过访问日志监控来检测和排查一些常见的安全问题,如 SQL 注入、XSS 攻击等。
监控访问日志可以帮助发现未经授权的访问请求。可以使用日志记录工具来记录每个请求的 IP 地址、时间戳和请求参数。
如果发现异常请求,可以及时采取措施,以防止攻击。
下面是一个参考的,进行响应记录的 过滤器。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestURI = httpRequest.getRequestURI();
try {
log.info("Request Received. URI: {}", requestURI);
chain.doFilter(request, response);
} catch(Exception e) {
log.error("Exception occurred while processing request. URI: {}", requestURI, e);
throw e;
} finally {
log.info("Request Completed. URI: {} Response Status: {}", requestURI, httpResponse.getStatus());
}
}
在上述代码中,通过Filter过滤器来实现日志监控。
当请求进入时记录请求URI,当请求结束时记录响应状态码,如此可及时发现异常情况。
有了日志仅仅是第一步,还需要结合定时任务或者 流式计算工具,进行异步分析,甚至是离线分析。
异步分析或者离线分析的,最终得到恶意请求的用户或者ip,然后进行拉黑或者IP封禁。
升级硬件设备
如果服务器无法承受恶意攻击,可以通过升级硬件设备来增加服务器的承载能力。
例如,可以增加CPU或内存等硬件资源,降低服务器的响应时间。
基于时序的统计预警
当Java接口被恶意狂刷时,及时通知相关管理人员或安全团队是非常重要的。他们可以采取更加有效的措施,如封禁IP地址、加强认证机制等,从而保障接口的安全。
系统监控和预警通知是保持系统稳定和可靠性的重要手段。通常,我们需要对系统的各种指标进行监控和预警,如 CPU 使用率、内存使用率、磁盘空间、网络流量等等。当这些指标超过预设的阈值时,系统就会触发警报,并通知相关人员进行处理。
对于系统的 恶意狂刷,也可以基于 时序进行 统计和预警。
为了实现系统监控和预警通知,可以使用一些开源的工具,如 Prometheus、Grafana、Alertmanager 等等。
其中,Prometheus 是一个广泛使用的监控系统,它支持多种数据源,如本地文件、HTTP、JMX、SNMP 等等。
Grafana 则是一个可视化监控工具,它可以将 Prometheus 收集的数据进行可视化展示。
Alertmanager 则是一个通知管理器,它可以根据不同的警报级别和通知方式,将警报发送给不同的人员或团队。
结合 Prometheus +Grafana +Alertmanager , 可以对 Java接口被恶意狂刷 进行时序统计,一旦超过一定的阈值,比如 1分钟被狂刷 10W次,进行预警, 方便开发和运维进行防范。
怎么防止系统崩溃?
三高架构的三原则
原则1:高性能意味着系统拥有更大流量的处理能力,更低的响应延迟。
例如 1 秒可处理 10W 并发请求,接口响应时间 5 ms 等等。
原则2:高并发表示系统在迭代新功能时,能以最小的代价去扩展,系统遇到流量压力时,可以在不改动代码的前提下,去扩容系统。
原则3:高可用通常用 2 个指标来衡量:
-
平均故障间隔 MTBF(Mean Time Between Failure):表示两次故障的间隔时间,也就是系统「正常运行」的平均时间,这个时间越长,说明系统稳定性越高
-
故障恢复时间 MTTR(Mean Time To Repair):表示系统发生故障后「恢复的时间」,这个值越小,故障对用户的影响越小
可用性与这两者的关系:
可用性(Availability)= MTBF / (MTBF + MTTR) * 100%
这个公式得出的结果是一个比例,通常我们会用「N 个 9」来描述一个系统的可用性。
系统可用性 | 年故障时间 | 日故障时间 |
---|---|---|
90% (1个9) | 36.5天 | 2.4小时 |
99% (2个9) | 3.65天 | 14分钟 |
99.9% (3个9) | 8小时 | 86秒 |
99.99%(4个9) | 52分钟 | 8.6秒 |
99.999%(5个9) | 5分钟 | 0.86秒 |
99.9999%(6个9) | 32秒 | 86毫秒 |
从表中,可以看到,要想达到 4 个 9 以上的可用性,一年的不可以时间为 52分钟,平均每天故障时间必须控制在 10 秒以内。
什么是优秀的架构?
原则1告诉大家:要用最少的资源,得到最大的受益。
原则2告诉大家:要能高扩展、自伸缩,能够承担高吞吐、高并发,能够自动扩容缩容
原则3告诉大家:一年的可用性要达到至少 4个9,不可用时间不能超过 52分钟。
故障的根因分析
一般来讲,单机房内部的链路,一定是高可用的。
单机房内部高可用,有架构师保障。
而机房的高可用,由机房厂商保障:建设一个机房的要求其实是很高的,地理位置、温湿度控制、备用电源等等,机房厂商会在各方面做好防护。
关键是,怎么确保 地域维度的不出现基础设施问题呢?
比如:
-
2015 年 5 月 27 日,杭州市某地光纤被挖断,近 3 亿用户长达 5 小时无法访问支付宝
-
2021 年 7 月 13 日,B 站部分服务器机房发生故障,造成整站持续 3 个小时无法访问
-
2021 年 10 月 9 日,富途证券服务器机房发生电力闪断故障,造成用户 2 个小时无法登陆、交易
-
2023 年 3月29日,唯品会遭遇了一场灾难性的机房故障, 南沙IDC冷冻系统故障导致机房设备温度快速升高宕机,造成线上商城停止服务。事故影响时间持续12个小时,导致唯品会业绩损失超亿元,影响客户达800多万
-
都是P0级事故
可见,即使机房级别的防护已经做得足够好,但只要地域维度的基础设施问题(网络问题、电力问题、地震问题、水灾问题),系统就不可用了。
如何解决地域维度的基础设施问题呢?作为应用架构师,在这块只能规避,只能规避,只能规避。
如何规避呢?核心的措施就是 :异地多活。
什么是异地多活
异地多活的概念很多,像什么同城双活、两地三中心、三地五中心等等概念。
要想理解异地多活,需要从架构设计的3高原则说起。
常见的多活方案
4 个 9 高可用的核心方案就是异地多活
异地多活指分布在异地的多个站点同时对外提供服务的业务场景。
异地多活是高可用架构设计的一种,与传统的灾备设计的最主要区别在于“多活”,即所有站点都是同时在对外提供服务的。
常见的多活方案有同城双活、两地三中心、三地五中心等多种技术方案,
方案1:同城双活
同城双活是在同城或相近区域内建立两个机房。
同城双机房距离比较近,通信线路质量较好,比较容易实现数据的同步复制 ,保证高度的数据完整性和数据零丢失。
同城两个机房各承担一部分流量,一般入口流量完全随机,内部RPC调用尽量通过就近路由闭环在同机房,相当于两个机房镜像部署了两个独立集群,数据仍然是单点写到主机房数据库,然后实时同步到另外一个机房。
下图展示了同城双活简单部署架构,当然一般真实部署和考虑问题要远远比下图复杂。
服务调用基本在同机房内完成闭环,数据仍然是单点写到主机房数据储存,然后实时同步复制到同城备份机房。
当机房A出现问题时候运维人员只需要通过GSLB或者其他方案手动更改路由方式将流量路由到B机房。
同城双活可有效用于防范火灾、建筑物破坏、供电故障、计算机系统及人为破坏引起的机房灾难。
同城双活关键:
(1)如何双机房切流
(2) 如何保证数据一致性
如何双机房切流
那怎么让 B 机房也接入流量呢?
最为简单的措施,就是进行 DNS切流。把 B 机房的接入层 IP 地址,加入到 DNS 中,这样,B 机房从上层就可以有流量进来了。
如何保证数据一致性
业务应用在操作数据库时,需要区分「读写分离」,假设A主B从,
-
「读」流量,可以读任意机房的存储,
-
「写」流量,只允许写 A 机房,因为主库在 A 机房。
-
然后进行A -B 机房的数据同步
这种架构,涉及用的所有存储,例如项目中用到了 MySQL、Redis、MongoDB 等等,
操作这些数据库,都需要区分读写请求,所以这块需要一定的业务「改造」成本。
最好的方式:是通过 proxy 中间组件,完成 统一的 读写改造。
为啥不是立即做到异城多活?
上面的方案,仅仅是同城多活,不是异城多活。
同城多活可以解决 机房级别的不可抗拒灾难,但是 地域级别的不可抗拒灾难,就没有办法搞定。
同城多活的机房,放到两个城市,不就变成异城多活了吗?
没有那么简单,来看看问题吧。
一般来说,多活机房的网络是通过「跨城专线」连通的。
如果两个机房距离较远,受到物理距离的限制,现在,两地之间的网络延迟就变成了「不可忽视」的因素了。
比如:北京到上海的距离大约 1300 公里,即使架设一条高速的「网络专线」,光纤以光速传输,一个来回也需要近 10ms 的延迟。
况且,网络线路之间还会经历各种路由器、交换机等网络设备,实际延迟可能会达到 30ms ~ 100ms,如果网络发生抖动,延迟甚至会达到 1 秒。
此时两个机房都接入流量,那上海机房的请求,可能要去读写北京机房的存储,这里存在一个很大的问题:网络延迟、用户体验差、数据存在丢失风险。
也就是说,如果是异城多活,距离太远,网络延迟太大。
这个时候, 如果A机房挂了,而数据还没有完成同步,就会出现1秒的数据丢失。
再来个用户体验差的案例:一个客户端请求打到上海机房,上海机房要去读写北京机房的存储,一次跨机房访问延迟就达到了 30ms,这大致是机房内网网络(0.5 ms)访问速度的 60 倍(30ms / 0.5ms),一次请求慢 60 倍,来回往返就要慢 100 倍以上。
而我们在 App 打开一个页面,可能会访问后端几十个 API,每次都跨机房访问,整个页面的响应延迟有可能就达到了秒级,这个性能简直惨不忍睹,难以接受。
所以:同城多活的机房,放到两个城市,不就变成异城多活了吗? 这种想法,太肤浅了。
方案2:两地三中心
所谓两地三中心是指 同城双中心 + 异地灾备中心。
异地灾备中心是指在异地的城市建立一个备份的灾备中心,用于双中心的数据备份,数据和服务平时都是冷的,当双中心所在城市或者地区出现异常而都无法对外提供服务的时候,异地灾备中心可以用备份数据进行业务的恢复。
两地三中心方案特点
优势
服务同城双活,数据同城灾备,同城不丢失数据情况下跨机房级别容灾。
架构方案较为简单,核心是解决底层数据双活,由于双机房距离近,通信质量好,底层储存例如mysql可以采用同步复制,有效保证双机房数据一致性。
灾备中心能防范同城双中心同时出现故障时候利用备份数据进行业务的恢复。
劣势
数据库写数据存在跨机房调用,在复杂业务以及链路下频繁跨机房调用增加响应时间,影响系统性能和用户体验。
服务规模足够大(例如单体应用超过万台机器),所有机器链接一个主数据库实例会引起连接不足问题。
出问题不敢轻易将流量切往异地数据备份中心,异地的备份数据中心是冷的,平时没有流量进入,因此出问题需要较长时间对异地灾备机房进行验证。
同城双活和两地三中心建设方案建设复杂度都不高,两地三中心相比同城双活有效解决了异地数据灾备问题,但是依然不能解决同城双活存在的多处缺点,想要解决这两种架构存在的弊端就要引入更复杂的解决方案去解决这些问题。
方案3:单元化+异地多活
阿里在实施这种方案时,给它起了个名字,叫做「单元化」。
单元化+异地多活结合的策略,是真正的异地多活策略,核心点有两个:
-
用户单元化
-
数据全局化
什么是用户单元化
同一个用户只会落在同一个机房内。之后的所有业务操作,都在这一个机房内完成,从根源上避免「跨机房」。
正常情况下,但用户的请求处理不会在两个机房「漂移」。
用户单元化的核心措施:要在最上层就把用户「区分」开,部分用户请求固定打到北京机房,其它用户请求固定打到上海 机房,进入某个机房的用户请求,之后的所有业务操作,都在这一个机房内完成,从根源上避免「跨机房」。
安全起见,每个机房在写存储时,还需要有一套机制,能够检测「数据归属」,应用层操作存储时,需要通过中间件来做「兜底」,避免不该写本机房的情况发生。
用户单元化之后,就可以进行用户分片。
分片的核心思路在于,让同一个用户的相关请求,只在一个机房内完成所有业务「闭环」,不再出现「跨机房」访问。
什么是数据全局化
多个机房,数据都是全量数据。当然,也可以部分全量,部分进行分区域存储。
架构的本质,都不是一刀切。
怎么做到数据全局化呢?
多个机房在接收「读写」流量(做好分片的请求),底层存储保持「双向」同步,两个机房都拥有全量数据
当任意机房故障时,另一个机房就可以「接管」全部流量,实现快速切换
用户单元化场景的流量路由
单元化+异地多活策略中,在接入层之上,再部署一个「路由层」(通常部署在云服务器上)
流量路由层的职责,就是把用户「分流」到不同的机房内。
多机房流量路由的规则
但这个路由规则,具体怎么定呢?
大致的路由规则有:
-
按业务类型分片
-
直接哈希分片
-
按地理位置分片
1、按业务类型分片
假设有 4 个应用,北京和上海机房都部署这些应用。
但应用 1、2 只在北京机房接入流量,在上海机房只是热备。
应用 3、4 只在上海机房接入流量,在北京机房是热备。
这样一来,应用 1、2 的所有业务请求,只读写北京机房存储,应用 3、4 的所有请求,只会读写上海机房存储。
这里按业务类型在不同机房接入流量,还需要考虑多个应用之间的依赖关系,要尽可能的把完成「相关」业务的应用部署在同一个机房,避免跨机房调用。
2、直接哈希分片
比如路由层会根据用户 ID 计算「哈希」取模,然后从路由表中找到对应的机房,之后把请求转发到指定机房内。
举例:一共 200 个用户,根据用户 ID 计算哈希值,然后根据路由规则,
用户 1 - 100 路由到北京机房,
用户101 - 200 用户路由到上海机房,
这样,就避免了同一个用户修改同一条数据的情况发生。
3、按地理位置分片
按地理位置分片方案,非常适合与地理位置密切相关的业务,例如打车、外卖服务就非常适合这种方案。
卖肯定是「就近」点餐,整个业务范围相关的有商家、用户、骑手,它们都是在相同的地理位置内的。
针对这种特征,就可以在最上层,按用户的「地理位置」来做分片,分散到不同的机房。
举例:北京、河北地区的用户点餐,请求只会打到北京机房,而上海、浙江地区的用户,请求则只会打到上海机房。这样的分片规则,也能避免数据冲突。
总之,至此,我们才算实现了真正的「异地双活」!
完成这样一套架构,需要投入的成本是巨大的。路由规则、路由转发、数据同步中间件、数据校验兜底策略,不仅需要开发强大的中间件,同时还要业务配合改造(业务边界划分、依赖拆分)等一些列工作,没有足够的人力物力,这套架构很难实施。
异地多活3大挑战
1、数据同步延迟挑战
(1)应用要走向异地,首先要面对的便是物理距离带来的延时。
如果某个应用请求需要在异地多个单元对同一行记录进行修改,为满足异地单元间数据库数据的一致性和完整性,需要付出高昂的时间成本。
(2)解决异地高延时即要做到单元内数据读写封闭,不能出现不同单元对同一行数据进行修改,所以我们需要找到一个维度去划分单元。
(3)某个单元内访问其他单元数据需要能正确路由到对应的单元,例如A用户给B用户转账,A用户和B用户数据不在一个单元内,对B用户的操作能路由到相应的单元。
(4)面临的数据同步挑战,对于单元封闭的数据需全部同步到对应单元,对于读写分离类型的,我们要把中心的数据同步到单元。
2、单元化解耦挑战
所谓单元(下面我们用RZone代替),是指一个能完成所有业务操作的自包含集合,在这个集合中包含了所有业务所需的所有服务,以及分配给这个单元的数据。
单元化架构就是把单元作为系统部署的基本单位,在全站所有机房中部署数个单元,每个机房里的单元数目不定,任意一个单元都部署了系统所需的所有的应用。
单元化架构下,服务仍然是分层的,不同的是每一层中的任意一个节点都属于且仅属于某一个单元,上层调用下层时,仅会选择本单元内的节点。
选择什么维度来进行流量切分,要从业务本身入手去分析。
例如电商业务和金融的业务,最重要的流程即下单、支付、交易流程,通过对用户id进行数据切分拆分是最好的选择,买家的相关操作都会在买家所在的本单元内完成。
对于商家相关操作则无法进行单元化,需要按照下面介绍的非单元化模式去部署。
当然用户操作业务并非完全能避免跨单元甚至是跨机房调用,例如两个买家A和B转账业务,A和B所属数据单元不一致的时候,对B进行操作就需要跨单元去完成,后面我们会介绍跨单元调用服务路由问题。
3、流量的路由挑战
-
流量调度,系统部署过去后流量怎么跟着怎么过去。
-
流量自闭环。由于距离的原因,跨地域的物理延时是没法避免的,流量过去之后怎么保证所有的操作都在本地完成,如果做不到那怎么将这种延时影响降到最低。
-
容灾切流。当某个机房出现故障时,如何快速把流量无损地切至其他机房。这里并不是说简单把流量切过去就完事,由于数据在多区域同步,流量切过去之后能否保证数据的一致性?