Nacos
注册中心原理
nacos1.x
1.提供方调用rest接口发起服务注册,注册自己的ip:端口
2.消费方每隔10秒拉取服务提供方注册列表
3.消费方提供方都需要和nacos每隔5秒进行心跳检测,15秒没有心跳,标志为不健康,30秒没有心跳标志位下线。
4.无论是消费方的上线或者是提供方的下线都会进行udp快速广播,通知全部的服务实例
5.nacos集群之间的数据同步采用Distro协议。
nacos2.x
把所有的单向的http升级为grpc协议,心跳包仍旧需要发送,但是得益于http2.0的信道复用模式,不需要在进行频繁的TCP握手挥手了,而且也不需要再额外发送 UDP 包广播了,大大提升了性能
配置中心原理
nacos客户端一启动,会根据bootstrap.yaml上配置的nacos地址,去nacos config
上拉取自己服务的配置文件信息。
之后进行长轮询,获取到变化以后采用事件发布机制,通知本地属性的变化。
短连接:请求/响应,立即结束,释放资源。长连接:建立连接以后,保持连接,传输数据。长轮询:请求服务器没数据则挂起一段时间,会超时,有数据则返回。
HTTP1.0:短连接,除非显示指定Keep-Alive。HTTP2.0,长连接,多路复用。gRPC:HTTP2.0,Protobuf压缩。
配置优先级 默认
nacos />
Dubbo
@DubboService
Provider
@DubboReference
Consumer
我从数据从消费方发送到提供方讲解,先获取到对方的ip:port
接着是代理层,默认使用jdk的动态代理,创建代理对象,代理真正的数据传输操作。
其次是序列化层,发送方将对象使用默认的protobuf,序列化。
接着是网络协议层,封装对应的Dubbo协议,可选grpc。
接着是传输策略层,可选负载均衡的策略,默认是随机策略。
接着是网络传输层,通过持续运行的socket服务容器,用了netty框架的主从Reactor多线程NIO模型的代码,
搭配上操作系统的epoll事件驱动机制,读取数据流的数据,并且解析协议,反序列化对象。
还会有Monitor,监控节点的调用次数和响应时间,提供健康运维。
Dubbo SPI
机制可以自定义负载均衡、网络协议和序列化方式。
META-INF/dubbo com.example.Robot optimusPrime=com.example.OptimusPrime
JDK SPI
src/main/resources/META-INF/services com.example.spi.GreetingService
此接口的具体实现
com.example.spi.impl.EnglishGreetingService
com.example.spi.impl.ChineseGreetingService
ServiceLoader<GreetingService>loader=ServiceLoader.load(GreetingService.class);// 遍历并调用服务提供者的方法for(GreetingServiceservice:loader){service.sayGreeting();}Spring boot 2.x resources/META-INF spring.factorys
Spring boot 3.x
resources/META-INF org.springframework.boot.autoconfigure.AutoConfiguration.imports
OpenFeign
@FeignClient(value="user-service", fallbackFactory=UserServiceFallbackFactory.class)public interface UserService{@GetMapping("/users/{id}")User getUserById(@PathVariable("id")Longid);//... 其他方法}@Component public class UserServiceFallbackFactory implements FallbackFactory<UserService>{@Override public UserService create(Throwable cause){returnnewUserService(){@Override public User getUserById(Longid){// 降级逻辑,返回一个默认值或错误信息returnnew User(0L,"Default User","Default Address");}//... 其他方法也实现降级逻辑};}}OpenFeign 内置了Ribbon作为客户端负载均衡器,可以自动对服务进行负载均衡,默认带区域的轮询。
Spring cloud GateWay
spring: cloud: gateway: routes: - id: service1_route uri: http://localhost:8081# 目标URIorder:0# 路由优先级,数字越小,优先级越高predicates: -Path=/service1/**# 断言,匹配以 /service1/ 开头的请求路径filters: -AddRequestHeader=X-Request-Id,123456# 过滤器,为请求添加一个头信息- id: service2_route uri: http://localhost:8082 order:1predicates: -Path=/service2/** filters:# 可以配置多个过滤器,以列表形式给出-AddRequestHeader=Another-Header, abcdef -RewritePath=/service2/(?<segment>.*), /$\{segment}# 过滤器,重写请求路径自定义过滤器。
@Component public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomGatewayFilterFactory.Config>{publicCustomGatewayFilterFactory(){super(Config.class);}@Override public List<String>shortcutFieldOrder(){// 定义配置参数的顺序(如果有的话)returnCollections.singletonList("myParam");}@Override public GatewayFilter apply(Config config){return(exchange, chain)->{// 自定义过滤逻辑 //... // 继续处理请求链returnchain.filter(exchange);};}public static class Config{// 可以在这里定义配置参数 private String myParam;//... getters, setters}}Sentinel
四种限流算法
1.固定窗口算法
10秒一个窗口,每个10s超过10请求就拒绝,会存在临界的第9秒9次,后一个窗口的第一秒9次,这就相当于两秒钟18次已经没有触发限流。
2.滑动窗口算法
类似Sentinel的算法,就是将固定的窗口划分的十分小,只有200ms。
5 qps
& & & & & & & & & & & & & & &
! ! ! ! !
第一个 5 个 200ms 窗口 ,最多5个请求,若超出了,则这1s内全部的请求会被拦截,向后滑动200ms一个小窗口,在计算新的1s窗口的请求量,进行限流判断。
3.漏桶算法
类似队列,消息发生填充是随机速度的,但是消费方的消费速度总是恒定的。
3.令牌漏桶算法
类似漏桶算法,消息发生填充是随机速度的,但是消费方的最大的消费速度总是恒定的,但是相比较漏桶算法,最大的不同在于,桶里面已经预置了令牌,对于短时间大流量也能匹配上一定的消费速度去处理,而漏桶算法永远都是恒定的。
页面限流截图
防范DDos。
低级的DDos,就是相同ip、客户端ua、相同账号这样发起大规模请求调用,这样是小儿科,Nginx就有针对ip请求速度的限制,网关也能进行限制。
高级的DDos,调用肉鸡,发起最真实的恶意请求,这样根本上防护不了,只能说保护服务不宕机、卡死。
3种攻击方式,
带宽攻击,比如文件上传接口,直接把服务的带宽占满,或者频繁进行静态资源的访问,这样可以借助厂商的cdn,或者是使用公共的对象存储,把图片放oss,让静态压力由云服务厂商的大带宽去处理。
协议攻击、如TCP,三次握手,直接就发送第一个syn,之后不连接了,tcp协议在服务端就会进入半连接状态,在超时时间之内打满。这个可以让运维优化TCP参数,调整半连接的队列。
后端接口攻击,直接攻击对外暴露的接口,这种只能通过高防服务器,云厂商大带宽服务器和高性能处理加上它们的ai识别算法,去做前置拦截过滤掉恶意流量。
使用Sentinel进行ToC接口的限流,防止瞬时流量过大,导致cpu,内存,io资源占满,任务队列无限排队,引起服务或者数据库宕机、卡死,因为某一个接口导致全服务不可用,防范DDos。
落地实践,我们会在Gateway,Sentinel整合网关,所有的toc接口能都暴露出来,针对文件上传之类的占带宽的接口,会有整体的限制。这个限制多少,预估算加上测试去测,比如最大上传5M图片,假设我们网络带宽最大是1000Mbps,那就是1000/8/5,这个是上限,取50%,就是限制的qps,当然准确都需要测试去测,防止由于带宽把重要的动态数据请求阻塞。
针对动态数据请求的攻击,首先让测试,测出每个服务单压每个接口最大的承受能力是多少,比如针对某一服务,发起超高的攻击qps,看服务能处理的qps是多少,两个qps不一样,差值其实都是在tomcat的等待队列等待,那这个服务响应的qps,就是需要接口限制的qps了,因为已经开始等待了,并且数据库和cpu内存都是这一个接口在使用,与其累计的等待不如直接拒绝。
拒绝策略,都是直接响应失败,并且在网关层会往mq发送消息,记录用户的操作入参和userId,如真的出现ddos,对于正常的用户下单,我们也能在后续发短信或者补偿优惠的方式进行二次挽救。
其次出现高流量,在k8s环境额外预留一些Iass资源,使用hpa动态扩容的方式,来应对,突发流量也是有效的,但是业务接口一般瓶颈在于数据库。
整体的目标就是不能让ddos,瞬时流量击垮冷系统。
客户端引入sentinel的核心依赖,配置上sentinel 服务端的地址,java -jar 启动sentinel 服务,web控制台可以进入,配置并且把流控配置写入nacos。
SkyWalking
全链路监控,配合web页面,能够显示出每个接口在各个服务包括mq,mysql,redis的调用链路以及各执行时间。
针对某个业务方法,每个方法的执行耗时,已经入参、出参。
SkyWalking我们是这样用的,用的是SkyWalking on Kubernetes,它是一个自定义CRD ,operator的方式,用过yaml增加annotation的方式,去动态的为java的pod增加skywalking的探针,不用自己去java -jar 指定agent了。
对于重要的服务,订单、支付、库存核心服务,会开启debug模式的日志级别,并且打印核心方法每个方法调用栈的入参和出参,并且每一步都打上完整详细的日志,通过skywalking的gpc上传日志到es。然后专门的EFK,日志系统也会收集一份日志。
当有用户有线上问题需排查的时候,记录下他大致操作的时间,做了什么操作,输入了什么参数,得到了什么响应。之后我们去es上根据时间索引查询大致的时间范围,以及对应的userid,操作的接口,确定下skywalking的tranreid,skywalking上很直观的就能看到调用的链路,已经那个方法那条sql出了什么问题。EFK定位大致的接口调用,skywalking直观的去找问题点。
第二点就是,SkyWalking的报警机制,监控各个服务的响应时间,比如一分钟之内平均响应时间大于原来95原则的两倍。
Discovery
全链路灰度发布。有这样的需要,就是局部的服务进行版本升级。
比如下订单 首先调用 风控服务。之后风控服务的风控规则升级了,增加了很多的限制规则。
之后经过软件生命周期uat测试内部全测完了,要上线了,会单独先部署一个deploy,只有一个升级版本镜像的pod。这个pod的app标签都是同一个service的,也就是说feign调用会有一部分流量调到这个pod,但是我们会结合Discovery,在Gateway的nacos配置文件进行header头的配置,也就是说只有具特定head头标识的流量才能分配到新版本的pod,Discovery的作用就是流量染色,灰度发布的作用。
上线生产之后,内部的测试员工,用他们的账号,拥有特别head头的账号进行测试,确定风控服务没问题之后,去除原有head头的限制,他会热加载,之后删除原有老版本的deploy,扩容新版的deploy副本实例,这样就做到无缝链路灰度了,要是有问题,那就不升级,原有的用户也不受影响。也适用于是否让用户选择是否参与内部全新版本,之后给她一个新版本的head头。
Discovery还有一种模式是百分比分配流量的方式,百分之10分配到新的版本,90分配到旧的版本,但是这样对于用户来讲还是会有风险。类似istio。
分布式全局ID
1.雪花算法
8字节8*8=64位
第一个不用,后面时间戳(41位),工作机器ID(10位),序列号(12位)
全局唯一性:由于时间戳、工作机器ID和序列号三者的组合,使得雪花算法能够在分布式系统中生成全局唯一的ID。
时间有序性:由于时间戳的存在,生成的ID大致按照时间递增排序,这对于需要按时间排序的场景非常有用。
实践方式,一次获取两批量的id,一批id使用完之后,异步的去请求第二批id,这样就避免了id单条获取,已经批量用完之后的数据拉取卡顿,两个id池交互获取。
我们的消息队列的自定义id就是这样的获取方式。
2.redis自增
我们就是用这种方式,专门定义了一个id服务,用于获取各类id。并且我们的业务id都会有相互关联关系。
比如,
现在新增了一件商品,需要新增分类、商品的spu、商品的sku,这三个id。
分类号,这个是redis 从10000自增的每次加1 ,假设取到某个值11111。 11111
那么该商品的spu,从10000000开始也每次自增加1,后四位和该分类的后四位一样1111。
100000001111 1七个0 后面四个1,这样spu可以自增到百万级别。
该商品的sku,1000000000,后四位也是1111。这样sku可以自增到千万级别。
并且当进行取模16运算分库分表的时候,由于基因法的存在,该商品的查询都能落到同一个库表里面。
并且业务上也能直观的看出来都属于统一商品。
注意前端js对于大于17位的long会丢失精度,需要转成字符串返回。
分布式事务
对于后台业务,面向运营人员的接口,直接@GlableTransition,一个注解开启全局的分布式锁,注意在锁的时候必须确保表设计了索引,尽量能直接锁到行锁级别,若出现范围更新,出现间隙锁,最好也小批量操作,因为面向运营的接口,并发几乎忽略,可以使用二阶段锁的seata AT模式。
重点是对于toc的高并发接口,
最开始,分清两种异常,需要解决的是有程序健壮性带来的异常问题,能处理的runtime Exception,最常见的,入参没校验,带来空指针,数组越界了或者是insert sql的时候,字段超长了,这种问题引起的分布式事务出现异常就很低级错误了。
第二种就是需要解决的异常引起的分布式事务问题,最常见的就是东西流量,接口调用超时,小概率是k8s网络引起的,运维的网络隔离策略写错,其次概率是程序代码没写好,某些入参的情况下需要处理很久,最大概率还是数据库被某些代码弄得卡死了,没索引直接全表扫描了,导致的超时。
连接超时可以确定对方服务没做。但是服务读取超时,最头疼的在于你都不知道对方服务,到底成功做了,还是没成功回滚了。
解决方法:
首先,从业务层面避免多层级的链式调用,其次使用中央编排模式。
就像A->B->C
转成 A->B 响应B的结果给A以后, A->C。将长事务有个中心编排器。
其次不要把微服务的数据库拆的很细,比如是否对于小体量公司业务是否可以考虑服务层代码是分布式的,但是数据库是同一个,这样就可以控制成本地事务,或者就相关联程度高的服务,如订单和支付不进行独占式数据库,控制成本地事务,就不会有这么多问题。
若是大体量Toc互联网业务,
处理长事务的方式是通过seata 的 saga模式,写事务状态机,自定义的反向补偿,加上mq,加上幂等判断去处理的,加上幂等就能很好解决对方服务到底成功了做成功没,没成功就不补偿了,成功了才去反向补偿。
比如我负责的核心交易层下单接口。我们是异步编排的方式,读操作异步并行,写操作同步执行。
全局不开启任何事务,只开启本地事务。
第一步,生成一个全局订单号,调用风控服务,获取商品最新的信息,三件事件都是读操作,可以异步并行,其中某一步出异常了,直接响应出错结果,不涉及事务。
年月日时分秒01订单来源八位会员号后四位,总共20为,这一步从redis查询,
同步阻塞至第二步,调用库存服务,库存服务也开启本地事务,
根据sku,查询此商品的销售库存是否可用,update stock set online_num = online_num -1,lock_num=lock_num+1 where sku=100 and online_num-1> -1;
若成功则,写入锁定表一条记录,订单号,以及锁定的商品号,以及锁定数量。
同步阻塞至第三步,调用优惠服务,一样去锁定优惠券,不让用户的一张优惠券在两个订单同时存在,写入优惠券的锁定表,订单号,优惠券号。
第四步,计算销售价格,调用物流服务,获得物流价格。
第五步,计算销售价格-物流价格得到支付价格和前端传过来的价格对比。若成功则生成父订单,这个时候再去把这个订单id,以及其他关联的用户、商品、库存和其他信息插入订单表。假设最后这一步insert出错了,通过saga 的补偿策略,依次进行逆向逻辑。
1.订单新增,取得订单号,往mq发送订单逻辑删除。
2.锁定优惠券,取得订单号,往mq发送优惠券和订单的关联锁定表逻辑删除,当然需要先查询是否存在记录,幂等的去做。
3.库存服务也是如此。
若是优惠券锁定失败,就从优惠券和库存开始反向补偿。
没开启任何事务,也没全局锁,不然对于某个商品id的库存id进行全局锁,直接会导致其他线程对此商品无法下单锁库存。
这里还有两个点,为什么还需要开启本地事务,因为需要二次保障,防止mq挂了,反向补偿的消息都发不了,这是就会有大量的数据是不一致的。
还有就是为什么一定要用消息队列补偿,直接rpc调用不行,因为假设我最后一步,插入订单失败了,这时候再用rpc调用其他服务的反向补偿接口,是不合理的,因为,自己的服务都出现未知异常了,进行远程rpc长事务补偿和正向流程执行复杂程度差不多,同步rpc调用大概率不可靠了,还会出错,而直接发消息安全的多,并且消息可以有状态存储,不像rpc失败,消息直接就没了。
分布式锁
核心公共服务,争夺一个共享资源,没争取到的进行队列等待。
Redisson,Rlock的共享资源就是。
在同一台,redis去设置相同的key,的一个hash 键值对,大key就是Rlock的字符串,hashkey就是java线程号的hash值,v是1,重入次数。
当其他线程尝试占有锁是,发现已经被别的线程占用了,就进行订阅,通过redis stream pub sub的方式,再将自己进行park阻塞,当次key不存在时,会唤醒全部的订阅者,强锁,抢到锁的进行unpark。
当然这是lua脚本的方式进行原子判断的。
zk
上面的。
reenlock
AQs队列,加 cas强锁,加park阻塞。
syncroizy
采用操作系统层面的monitor的源语,进行锁进入,锁释放,内部维护等待和阻塞队列,count 表示重入次数。
分布式会话
无状态jwt、
Header:签名算法。Payload:用户附加数据。Signature:对头部和有效载荷进行签名。
优点:无状态(不用存)跨平台(通用标准、多语言支持)安全性(内容不可修改)
缺点:无法撤销(提前过期)敏感信息泄露(载荷会被解密)过期问题(过期重新获取,无法主动续期)
redis的token,上面的缺点都无。
userId,ip
hash取模至同一台服务器。
分布式任务
分片任务。