项目
2021-09-11 00:54:28 0 举报
AI智能生成
项目
作者其他创作
大纲/内容
6.824
实现了什么功能
实现了Raft算法, 保证了Raft集群的强一致性
实现了基于Raft算法的K/V数据存储系统
实现了分片的K/V数据存储系统
有哪些模块
Raft
选主
什么是选主
在 Raft 集群自身内部通过票选、心跳等机制投票选出一个大多数节点认可的主节点作为集群的 leader 去处理所有请求。
结点有三种角色
Leader
所有请求的处理者,接收客户端发起的操作请求,写入本地日志后同步至集群其它节点。
Follower
请求的被动更新者,从 leader 接收更新请求,写入本地文件。如果客户端的操作请求发送给了 follower,会首先由 follower 重定向给 leader
Candidate
如果 follower 在一定时间内没有收到 leader 的心跳,则判断 leader 可能已经故障,
此时启动 leader election 过程,本节点切换为 candidate 直到选主结束。
此时启动 leader election 过程,本节点切换为 candidate 直到选主结束。
选主过程
初始状态
所有结点都是follower
触发选主的条件
任意follower 在一段时间内没有收到任何心跳,选举超时,那么它就会主观认为 leader下线了,并发起新的选举
每个结点选举超时时间都是一定返回内的随机值, 保证了不会所有结点同时发起选举称为Candidate
少部分的Candidate数量保证了一个结点得到更多选票的可能性
选举成功的条件
Candidate向其他所有的结点发送消息, 要求投票给自己, 收到超过总结点数一半选票的Candidate当选
一定可以快速选举出来的原因
所有结点选举超时的时间一定返回随机, 导致一定是少部分结点先发起选举, 单个结点得到超过一半选票的可能性更大
一个结点一个选举的过程只能投票一次, 先到先得, 并且只会投票给日志状态比自己新的结点
保证了一次选举出的主节点的唯一性
如果 term 不同 term 更大的日志更新,否则 index 大的日志更新。
获得超过一半选票的结点转换为leader结点, 并且迅速向其他所有结点发送心跳包, 维持自己leader的状态, 防止其他节点又马上进行下一次的选举
因为每个结点只会投票给比自己结点状态更新的结点, 所以得到大部分选票的结点一定是状态比大部分新的结点,
进行日志复制的时候也是要求超出一半的结点成功复制以后就可以提交这个日志
进行日志复制的时候也是要求超出一半的结点成功复制以后就可以提交这个日志
选出的leader结点就开始接收请求
心跳机制
leader 会周期性的向所有follower结点发送心跳包来维持自己的权威
follower一段时间没有收到leader发来的心跳包就会认为leader已经下线了, 便会将自己的状态变为Candidate 并且发起一格选主的过程
日志复制
什么是日志复制
leader结点处理所有请求, 并将请求封装成日志, 向其他follower结点发起要求复制这个日志的请求的过程
raft算法是一个基于状态复制机的共识算法, 所有的结点都从初始的同一个结点出发, 经过一系列相同的日志操作到达最终的一致状态, 所以日志复制的过程就是保证结点日志一致的过程, 这样通过相同的日志操作才能达到最终一致的状态
日志复制流程
leader收到请求, 把该指令作为一条新的日志附加到自身的日志集合,
然后向其他结点发起复制这条日志的请求, 要求他们将这条日志添加到本地的日志集合中
follower收到日志复制请求
follower会查看leader日志的上一条日志是否和自己一致, 一致就会复制这条日志并且返回成功
不一致说明follower和leader的日志存在缺失, 需要先将缺失的这段日志完成复制, 才可以复制这条日志, 所以当前会拒绝
这样的检查保证了所有日志的信息最终达到一致状态
当超过一半结点成功复制这条日志后, leader会将该日志标记为commit, 并且将日志对应的操作apply到本地的状态机(数据库)
K/V Server + Raft
K/V Server提供Get/Append/Put操作
- 对于每一个KVServer结点, 都有一个与之对于的Raft结点,
- 所有的Get/Append/Put请求都会被封装成日志, 由KVServer传递给Raft结点去完成在所有的结点上实现同步
- 所以不能让所有的KVServer都处理请求, 只有KVServer结点对应的Raft是领主的服务结点才可以处理客户端的请求
- Raft领主收到服务器传递过来的请求开始要求所有的Raft结点复制当前的请求对应的日志
- 复制完成这个请求就可以被提交到本地状态机上, 并持久化到磁盘了
- 通过 Client->Server->Raft 的方式实现了各个服务器结点的状态一致性
- 所有的Get/Append/Put请求都会被封装成日志, 由KVServer传递给Raft结点去完成在所有的结点上实现同步
- 所以不能让所有的KVServer都处理请求, 只有KVServer结点对应的Raft是领主的服务结点才可以处理客户端的请求
- Raft领主收到服务器传递过来的请求开始要求所有的Raft结点复制当前的请求对应的日志
- 复制完成这个请求就可以被提交到本地状态机上, 并持久化到磁盘了
- 通过 Client->Server->Raft 的方式实现了各个服务器结点的状态一致性
ShardMaster
单一结点, 拥有哪些数据库分片属于哪些K/VServer集群负责处理的配置信息
处理分片的配置信息
服务器
replica group 询问master找到自己应该服务哪些shards
客户端
Client询问Master来找到key对应的replicate group
只负责修改配置的信息, 所有的K/VServer集群定期的从ShardMaster拉取最新的配置信息, 发现分片分片发生改变了以后自己进行分片的迁移
Shard K/V Server + Raft
因为配置可能随时变化, 所以需要定时的从ShardMaster获取最新的配置信息, 如果当前集群不再负责某部分分片就需要遍历配置信息, 查看哪个集群负责这些分片, 将这部分分片发送给负责的集群
如果请求的key不是当前集群负责的, 不处理直接返回, 让客户端找ShardMaster通过最新的配置信息找到负责的集群
处理客户端请求也是通过向对应Raft结点发起一个日志复制的请求, 同样是需要对应Raft是leader的Server结点才可以处理请求, 得到回复以后, 提交返回客户端
可能的问题
什么是Raft算法
解决什么问题
Raft算法主要是为了解决分布式系统中, 会进行结点的复制, 这也就带来了多个复制结点之间的一致性问题
具体内容
Raft算法是通过发起日志复制的机制实现数据一致性, 这里对raft集群的读写操作都会封装成一个日志
客户端请求raft集群就会发起一个日志复制的请求 这个日志必须得到>(N/2+1)结点的成功复制才可以提交这个日志并返回
成功复制的日志会将这个提案对应的日志标记为commit, 并最终应用到本地状态机
这样其他客户端在发起读请求的时候就可以从状态机读到最新的数据,
最终实现的结果就是, 一旦一个客户端成功执行了写操作, 那么之后的客户端一定能够读出刚刚写入的值, 这样整个集群就像单机一样对外提供读写服务, 即使发生网络分区故障, 少部分结点发生异常, 仍然可以提供服务
什么是强一致性
所谓线性一致性是指将一个强一致性分布式系统当成单机, 并且所有的读写操作都是原子的, 一旦某个客户端成功的执行了写操作,那么所有客户端都一定能读出刚刚写入的值,
强一致性的特性
对于调用时间存在重叠(并发)的请求,生效顺序可以任意确定。
并发发生的对一个key的读写操作, 读操作可以读到旧值或者写操作带来的新值
对于调用时间存在先后关系的请求,后一个请求不能违背前一个请求确定的结果。
当一个写操作已经成功, 那么后续的读操作一定可以读到完成的写操作的新值
任何一个客户端的读取返回新值后,所有客户端的后续读取也必须返回新值
- 并发的两个读一个写, 读1先于读2完成, 写操作时间覆盖两个读操作
- 如果读1读到了旧值, 读2可以读到新值或旧值
- 但是如果读1读到了新值, 那么即使现在写操作没有返回, 读2必须读到新值,
- 因为如果读到旧值, 说明写操作期间值在新旧之间来回翻转, 这是不符合线性一致性的
- 如果读1读到了旧值, 读2可以读到新值或旧值
- 但是如果读1读到了新值, 那么即使现在写操作没有返回, 读2必须读到新值,
- 因为如果读到旧值, 说明写操作期间值在新旧之间来回翻转, 这是不符合线性一致性的
考虑的边界条件
网络分区故障
场景描述
可能存在旧的leader仍然认为自己是leader, 但是因为网络的隔离, 处于多数派的那部分结点没有收到旧leader的心跳包, 所以开始了新一轮的选举, 并选出新leader开始后续的写请求, 这时因为旧leader误以为自己还是leader仍然处理客户端请求,导致读到旧的数据
解决方法
为了确保 leader 处理读操作时仍拥有领导权,我们可以将读请求同样作为一个提案走一遍 Raft 流程,
结点下线
leader下线以后, follower会发生选举超时, 进行新一轮选举, 选出新的主节点处理客户端请求, 只要还有超过一半的结点就仍然可以提供服务
请求到达的时候服务器正在进行分片转移
- 客户端请求的这个服务器结点处于正在转移的状态
- 返回要求客户端请求shardMaster最新的配置信息, 去请求当前分片数据迁移后所在的新的服务器结点
- 返回要求客户端请求shardMaster最新的配置信息, 去请求当前分片数据迁移后所在的新的服务器结点
遇到的困难
处理Raft结点之间的RPC
raft结点之间的通信是通过RPC, 但是一开始不了解, 没有注意到在序列化和反序列化的时候, 没有按照一致的顺序, 导致接收到反序列数据出错, 得不到正确的数据
基于Raft实现强一致性
写主, 读从
某次写操作的日志尚未被复制到一少部分 follower,但 leader 已经将其 commit。
leader收到超过一半就可以commit, 但是没有复制的少部分结点与主节点就是不一致的
leader收到超过一半就可以commit, 但是没有复制的少部分结点与主节点就是不一致的
某次写操作的日志已经被同步到所有 follower,但 leader 将其 commit 后,心跳包尚未通知到一部分 follower。
没有收到leader的commit心跳包, 这个日志对应的操作还没有被复制到follower本地状态机, 所以读取的仍然是旧数据
没有收到leader的commit心跳包, 这个日志对应的操作还没有被复制到follower本地状态机, 所以读取的仍然是旧数据
直接读follower, 前一个写操作已经成功返回了, 这时读数据一定要读到最新的数据,
但是以上情况可能读到旧的数据, 不满足线性一致性
但是以上情况可能读到旧的数据, 不满足线性一致性
写主, 读主
状态机落后于 committed log 导致脏读
leader进行日志复制的时候, 收到超过一半的成功回复就会将这个日志标记为commit, 然后马上返回客户端
但是并没有要求commit的日志马上应用到本地状态机, 所以这时候读状态机可能读到旧数据
读请求来了需要比较commit Index和apply Index, 当commit Index=apply Index读状态机就可以返回了
网络分区导致脏读
发生网络分区故障, 旧的leader刚好在小部分的分区,多数派分区因为没有收到leader的心跳包, 选出了新的leader并且开始了后续的写操作,
这个旧的leader实际已经不是leader但是仍然处理了这个请求, 可能就读到了旧的值, 不满足线性一致性
写主, 读主,
为了确保 leader 处理读操作时仍拥有领导权,我们可以将读请求同样作为一个提案走一遍 Raft 流程,
当这次读请求对应的日志可以被应用到状态机时前一个日志一定已经应用到了状态机了,leader 就可以读状态机得到最新数据并返回给用户了
重复请求
场景描述
Cleark请求leader , leader提交了这个日志, 但是马上就断线了, 没有回复Cleark, 但是这个提交的日志会被已经复制过的raft都标记为commit 并且apply到本地状态机, 所以事实上这个Op已经作用到了数据库, 但是因为没有回复Cleark, Cleark还会继续发送这个Op到其他的服务器,
解决方法
每个客户端都有自己的请求序号Seq, 每次发起请求都会在这个客户端原来Seq基础上+1
每个客户端都会生成一个唯一的UUID 和请求号一起构成一个请求的唯一标识
遇到请求会通过这个客户单的UUID找到次的请求序号
如果当前请求序号<=记录的上次序号, 说明这是一个重复请求, 返回
如果>记录的上次序号, 可以执行
幂等性
什么是幂等性?
幂等性:提交一次和多次,结果是一样的。
幂等性:提交一次和多次,结果是一样的。
分片传输过程需要复制一份分片数据, 否则接收方读取时会产生死锁
学到了什么
商城后台管理系统
前后端分离, 前端基于VUE, 后端基于SpringBoot, SpringCloud的商城后台管理系统
环境搭建
安装virtualBox
使用vagrant 虚拟linux
安装Docker
Docker安装 MySQL (5.7)
Docker安装redis
Java1.8 Maven3.6.1 Git, IDEA MybatisX插件, lombok插件
微服务环境搭建
微服务注册到Nacos注册中心
1. 配置文件中配置Nacos server的地址
2. 配置微服务的应用名称
3. 在主应用程序上加上 @EnableDiscoveryClient注解
4. 启动NacosServer服务器
5. 启动微服务
6. 登录 127.0.0.1:8848/nacos
2. 配置微服务的应用名称
3. 在主应用程序上加上 @EnableDiscoveryClient注解
4. 启动NacosServer服务器
5. 启动微服务
6. 登录 127.0.0.1:8848/nacos
微服务从Nacos配置中心获取配置
1. 引入Nacos Config starter 依赖
2. 添加bootstrap.properties配置文件
1. 配置当前服务的名称
2. 配置nacos配置中心的地址
3. 在配置中心添加一个数据集(Data Id) gulimall-coupon.properties
1. 默认命名方式为 <应用名.properties>
2. 在配置文件中可以随时修改信息发布
4. 在需要获取到配置中心配置文件内容(@Value("${properties的key}$"))的类上添加 @RefreshScope注解, 让该微服务可以动态的从配置中心配置文件获取内容
5. 如果配置中心和当前应用的配置文件都配置了相同的项,优先使用配置中心的项
2. 添加bootstrap.properties配置文件
1. 配置当前服务的名称
2. 配置nacos配置中心的地址
3. 在配置中心添加一个数据集(Data Id) gulimall-coupon.properties
1. 默认命名方式为 <应用名.properties>
2. 在配置文件中可以随时修改信息发布
4. 在需要获取到配置中心配置文件内容(@Value("${properties的key}$"))的类上添加 @RefreshScope注解, 让该微服务可以动态的从配置中心配置文件获取内容
5. 如果配置中心和当前应用的配置文件都配置了相同的项,优先使用配置中心的项
API网关
客户端发送请求到服务器路途中,设置一个网关,请求都先到达网关,网关对请求进行统一认证(合法非法)和处理等操作。他是安检。
新建gateway模块, 主启动类加@EnableDiscoveryClient 将自己注册到注册中心 这样可以找到其他的服务
配置Nacos的地址 (application.properties)
配置中心的配置文件 bootstrap.properties
编写网关服务的配置
uri: predicate满足的情况下跳转的地址
predicates: Query=请求参数, 请求参数的值
微服务功能
登录功能
katcha 谷歌验证码
传入用户的UUID为这个用户生成带有过期时间的验证码图片,
将用户UUID和验证码绑定, 检验阶段通过用户的UUID和输入验证码检验是否匹配
根据用户名获取用户信息
验证用户登录信息
用户名不存在返回
获取用户对应的盐, 和用户此次输入密码通过Sha256算法加密得到值和用户实际密码是否相等
验证完登录信息, 会用户生成Token
UserId
一个UUID随机数
当前时间
过期时间
将新生成的Token保存到数据库
token认证机制
token与session的不同主要在
①认证成功后,会对当前用户数据进行加密,生成一个加密字符串token,返还给客户端(服务器端并不进行保存)
②浏览器会将接收到的token值存储在Local Storage中,(通过js代码写入Local Storage,通过js获取,并不会像cookie一样自动携带)
③再次访问时服务器端对token值的处理:服务器对浏览器传来的token值进行解密,解密完成后进行用户数据的查询,如果查询成功,则通过认证,实现状态保持,所以,即时有了多台服务器,服务器也只是做了token的解密和用户数据的查询,它不需要在服务端去保留用户的认证信息或者会话信息,这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利,解决了session扩展性的弊端。
原文链接:https://blog.csdn.net/mydistance/article/details/84545768
token与session的不同主要在
①认证成功后,会对当前用户数据进行加密,生成一个加密字符串token,返还给客户端(服务器端并不进行保存)
②浏览器会将接收到的token值存储在Local Storage中,(通过js代码写入Local Storage,通过js获取,并不会像cookie一样自动携带)
③再次访问时服务器端对token值的处理:服务器对浏览器传来的token值进行解密,解密完成后进行用户数据的查询,如果查询成功,则通过认证,实现状态保持,所以,即时有了多台服务器,服务器也只是做了token的解密和用户数据的查询,它不需要在服务端去保留用户的认证信息或者会话信息,这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利,解决了session扩展性的弊端。
原文链接:https://blog.csdn.net/mydistance/article/details/84545768
解决跨域问题
问题描述
前端页面在8001端口登录需要访问88端口的登录, 同源策略禁止
什么是跨域
浏览器对于协议, 域名, 端口 任意一个不同的地址不允许请求
跨域的流程是发送了两个请求, 第一个为OPTIONS预检请求, 去访问服务器, 询问是否允许跨域请求, 如果服务器响应允许, 则发送真实的请求
解决方法(采用方法2)
- 方法1:设置nginx包含admin和gateway。都先请求nginx,这样端口就统一了
- 方法2:让服务器告诉预检请求能跨域
- 方法2:让服务器告诉预检请求能跨域
配置当次请求允许跨域
添加响应头, 对于预检Option请求, 表示允许
Access-Control-Allow-Origin : 支持哪些来源的请求跨域
Access-Control-Allow-Method : 支持那些方法跨域
Access-Control-Allow-Credentials :跨域请求默认不包含cookie,设置为true可以包含cookie
Access-Control-Expose-Headers : 跨域请求暴露的字段
Access-Control-Allow-Method : 支持那些方法跨域
Access-Control-Allow-Credentials :跨域请求默认不包含cookie,设置为true可以包含cookie
Access-Control-Expose-Headers : 跨域请求暴露的字段
网关模块添加配置类, 注册CorsWebFilter到Spring中
获取基于url跨域配置源类 UrlBasedCorsConfigurationSource source
生成Cors跨域配置信息类 CorsConfiguration corsConfiguration
//1.配置跨域
//允许所有的请求头类型跨域
corsConfiguration.addAllowedHeader("*");
//允许任意请求方式跨域
corsConfiguration.addAllowedMethod("*");
//允许任意请求来源跨域
corsConfiguration.addAllowedOrigin("*");
//允许携带cookies跨域
corsConfiguration.setAllowCredentials(true);
//任意路径允许跨域
source.registerCorsConfiguration("/**", corsConfiguration);
//允许所有的请求头类型跨域
corsConfiguration.addAllowedHeader("*");
//允许任意请求方式跨域
corsConfiguration.addAllowedMethod("*");
//允许任意请求来源跨域
corsConfiguration.addAllowedOrigin("*");
//允许携带cookies跨域
corsConfiguration.setAllowCredentials(true);
//任意路径允许跨域
source.registerCorsConfiguration("/**", corsConfiguration);
对任意路径拦截后设置允许跨域就不存在跨域问题了
XSS Cross Site Script(跨站脚本)
攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作
CSRF(跨站请求伪造)
1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。
2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。
设置黑客无法伪造的信息, 请求参数加rtoken 或者HTTP头中自定义属性验证
实现微服务间的RPC调用
会员服务会请求优惠券服务, 获取会员的优惠券信息
调用流程
member模块添加feign包
feign包中编写接口, 告诉SpringCloud这个接口需要调用远程服务
接口添加注解@FeignClient("需要调用的模块在注册中心的名称")
被调用服务有多个方法, 要调用哪个方法就把方法完整的写在这里
调用方法上加@RequestMapping("被调用方法的完整URL")
主启动类添加主机@EnableFeignClients("feign包的全路径, 表示这个包下的接口都是远程调用的")
商品服务
商品的增删改查
CIT594 CovidParser
CovidParser是干什么的
根据用户输入的文件解析不同类型的文件, 获取Covid相关的数据
有哪些功能
根据输入文件名类型调用不同的parser实现对不同类型文件的解析
缓存组件
CacheManager
负责创建管理不同文件parse得到的结果缓存
底层实现concurrentHashMap
key 文件名
value 文件名对应的cache对象
ICache
对应一个文件parse后得到的结果
底层实现两种
基础类型缓存 ConcurrentHashMap
LRU类型 LinkedHashMap
提供
get put remove clear
日志库
记录服务段打开文件, 解析文件, 用户与服务端交互时的日志信息
积攒一定数量日志会持久化到本地日志文件, 数量可以由用户指定
日志在不同时期带有不同的tag, 对应着打开文件, 用户输入, 服务端响应等, 通过日志可以看到服务端和用户端的行为
0 条评论
下一页