基于nacos的分布式服务治理

微服务是什么


微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”,原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这些小应用之间通过服务完成交互和集成


微服务架构特点

1.通过服务实现组件化
开发者不再需要协调其它服务部署对本服务的影响。

2.按业务能力来划分服务和开发团队
开发者可以自由选择开发技术,提供 API 服务

3.去中心化
每个微服务有自己私有的数据库持久化业务数据,每个微服务只能访问自己的数据库,而不能访问其它服务的数据库,某些业务场景下,需要在一个事务中更新多个数据库。这种情况也不能直接访问其它微服务的数据库,而是通过对于微服务进行操作。数据的去中心化,进一步降低了微服务之间的耦合度,不同服务可以采用不同的数据库技术(SQL、NoSQL等)。在复杂的业务场景下,如果包含多个微服务,通常在客户端或者中间层(网关)处理。

4.基础设施自动化(devops、自动化部署)
的Java EE部署架构,通过展现层打包WARs,业务层划分到JARs最后部署为EAR一个大包,而微服务则打开了这个黑盒子,把应用拆分成为一个一个的单个服务,应用Docker技术,不依赖任何服务器和数据模型,是一个全栈应用,可以通过自动化方式独立部署,每个服务运行在自己的进程中,通过轻量的通讯机制联系,经常是基于HTTP资源API,这些服务基于业务能力构建,能实现集中化管理(因为服务太多啦,不集中管理就无法DevOps啦)。


微服务的组成

1、注册中心
注册中心主要是用来登记服务信息的存储中心,主要用来注册和发现已注册登记的服务,并维护它们间的通信关系。
目前主流选用Eureka、Consul、Zookeepr及nacos作为注册中心,它们都有各自的特点和适用场景,
具体在后续注册中心专题文章进行介绍。那么,登记的服务信息到底是什么?
实际上,登记的服务程序信息一般为能够找到服务的信息,比如:IP地址、端口号、服务名及通信协议等能够唯一标志服务的数据,
不论选用上面介绍的哪种技术实现,都离不开这些信息,不同的主要在选用的实现技术在集群选举和故障恢复上的异同。

2、API网关
之所以称为“API网关”,主要是因为在微服务架构体系中,服务间的通信大多选用Rest API或RPC来实现,
而往往它们的实现形式也就是API,只不过所遵循的通信协议和极限性能不同罢了。那么,微服务架构中的API网关能够做什么事昵?其实,它可以作很多事,比如:控制不论是对外还是对内的服务间的统一入口和路由转发,并能方便地监控服务间的调度、限流及容错回退等功能。可以选用Netflix系列中的Zuul,也可以选用Web服务器,如:Nginx作为代理路由服务器。

3、服务提供者
服务提供者,承载着对外提供的业务服务单元,一般仅对服务消费者开放,而实现方式没有特殊的要求,只要能够合理的实现业务功能即可,比如:可选用传统的MVC架构,或仅仅是一个实现主类。

4、服务消费者
服务消费者,负责对接和消费服务提供者所提供的服务单元,它的实现方式也没有特殊的要求,与提供者类似,但不同的是其负责与前端交互,负责将从提供者获取的数据和行为,赋予给与用户打交道的前端交互,比如:我们熟知的浏览器等。

5、配置中心
配置中心,一个负责统一配置全局参数的地方,做到不需要重新打包、发布版本才能更新数据,而仅仅通过改变统一配置文件,服务就可以动态更新最新的属性数据,大大提高程序服务的灵活性,避免繁琐的运维部署工作。而实现配置中心的方式也有很多种,以前推荐用Cloud Bus,它一般会与消息中间件,如:RabbitMQ、ActiveMQ、RocketMQ、Kfka等结合使用,通过集成消息中间件,来实现负载群发配置变更信息给相关的服务同步数据,同时它也负责与版本存储连接,如:Git/Svn等,通过监听它们的变更,来及时通知消息消息广播消息。目前的话有nacos-config功能

6、消息总线
消息总线,在上面已经介绍,主要工作是能够负载均衡地群发事件消息数据给所有相关的服务,使它们及时更新最新属性,其一般选型均为消息队列系统实现,当然,这并非是固定的标准,选用如Redis亦可。

7、服务跟踪
微服务架构中,跟踪服务调用运转的情况很重要,因为通过分析服务调用的频次和时间作为参考,有助于我们预知服务的使用度和延迟问题所发生的时机。细心的读者,会发现服务跟踪与日志跟踪有些类似,没错,传统的架构中,我们为了定位问题,通常借助ELK技术栈来跟踪和记录感兴趣的日志信息,虽然,它们类似,但日志跟踪的数据分析较困难,所记录的数据大多无规律,而服务跟踪,则从服务粒度出发,从功能整体来记录和分析程序问题,使问题定位更加清晰和高效,实际上,服务跟踪也是一种日志跟踪。如果读者选用Spring Cloud栈搭建微服务架构,则可选用Cloud Sleuth,它往往与Zipkin等集成,来实现可视化的跟踪分析;如果采用的是非Spring Cloud,如基于dubbo搭建微服务的化,因其本身并未提供类似Cloud Sleuth的开源栈,所以需要读者自行实现,可选用高效的,且支持持久化的中间件实现,如:Redis或MongoDB等。  目前使用sentinel

8、服务容错
服务容错,或称之为服务降级。我们知道,在微服务环境中,服务往往存在与不同的服务器节点,甚至是不同的网络环境,那么服务间的交互,势必会经常发生错误,如:网络抖动、服务bugs等。那么,如何实现当某个服务故障时,关联的服务程序也能正常对外提供服务昵?答案是,引入一个提高用户体验或与故障服务类似的服务作为降级服务后的服务,这就是服务降级或容错。这里以Spring Cloud方式搭建架构,那么可选用Cloud Hystrix,利用其提供的容错断路器来实现降级,并且往往它需要与API路由集成(实际上,Zuul已经集成了它),因为后者是几乎所有服务的访问控制的入口。  目前使用sentinel

技术间的对比


1.nacos 与 eureka

CAP定律
1.CP和AP不可能同时满足

2.P:代表分区容错, 在整个分布式系统中某个节点服务挂掉了,并不影响整个系统的运作和使用,因为他可以在稍后或者通过切换可用节点立即恢复使用
 
3.C:一致性,写操作之后的读操作,必须返回该值。注册中心集群中: leader的作用, 所有的写操作都依赖于leader来完成,为了保证数据的一致性,leader只有一个.假如: 没有leader,首先加入我们新加入一台数据处理服务,就会像注册中心1进行注册,注册中心1写入数据处理服务的ip等等基本信息,并且准备同步给其他注册中心节点, 结果这个在还没发生同步的过程中,注册中心1挂掉了,然后客户端准备调用数据中心写入,这个时候就因为注册中心1挂掉了,就直接切到了注册中心2,但是注册中心2没有收到数据处理服务的添加请求,所有没有这个服务,这个时候就对客户端显示不可用了.

4.A:可用性,没有leader,可以很容易的切换到可用的注册中心,对于客户端的调用总是及时反应, 在上述C操作的例子中,对于像服务注册,获取服务注册的基本信息,比如ip来说,基本不会存在,因为像Eureka来说,我们的服务可以像所有的注册中心节点发起注册请求,这样就不会存在注册中心节点服务列表不一致的情况

eureka + springCloudConfig
eureka:默认支持AP,使用节点间的相互复制,和服务端同时注册到所有注册中心节点上来保证一致性springCloudConfig:单独服务,使用git仓库拉取配置信息,然后服务器拉取config服务器里的配置缓存到本地仓库.需要结合bus等才能使用,保证数据的一致性

nacos: 默认单机支持AP 集群支持CP 同时作为注册中心和配置中心,使用推送模式来传输配置(原理:服务器使用监听器监听nacos的配置变化.如果发生变化就变更)

eureka目前停止更新 nacos处于起步阶段.新功能及bug反馈速度快.


2.Sentinel 与 Hystrix

https://www.jianshu.com/p/d1f22a555065

1.侧重点:
    (1)Hystrix: 在于以隔离和熔断为主的容错机制
    (2)Sentinel: 多样化的流量控制,熔断降级,系统负载保护,实时监控和控制台

基于nacos的分布式服务治理_第1张图片

nacos


https://nacos.io/zh-cn/docs/what-is-nacos.html

服务(Service)是 Nacos 世界的一等公民。Nacos 支持几乎所有主流类型的“服务”的发现、配置和管理:Kubernetes Service , gRPC & Dubbo RPC Service , Spring Cloud RESTful Service

Nacos 的关键特性包括:
服务发现和服务健康监测
1.Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生SDK、OpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODO 或HTTP&API查找和发现服务。
2.Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP)和应用层 (如 HTTP、MySQL、用户自定义)的健康检查。 对于复杂的云环境和网络拓扑环境中(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了 agent 上报模式和服务端主动检测2种健康检查模式。Nacos 还提供了统一的健康检查仪表盘,帮助您根据健康状态管理服务的可用性及流量。

动态配置服务
动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。Nacos 提供了一个简洁易用的UI (控制台样例 Demo) 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。

动态 DNS 服务
动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。动态DNS服务还能让您更容易地实现以 DNS 协议为基础的服务发现,以帮助您消除耦合到厂商私有服务发现 API 上的风险。Nacos 提供了一些简单的 DNS APIs TODO 帮助您管理服务的关联域名和可用的 IP:PORT 列表.

服务及其元数据管理
Nacos 能让您从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。

nacosMap
基于nacos的分布式服务治理_第2张图片


nacos生态图
基于nacos的分布式服务治理_第3张图片

nacos基本架构
基于nacos的分布式服务治理_第4张图片

服务 (Service)
服务是指一个或一组软件功能(例如特定信息的检索或一组操作的执行),其目的是不同的客户端可以为不同的目的重用(例如通过跨进程的网络调用)。Nacos 支持主流的服务生态,如 Kubernetes Service、gRPC|Dubbo RPC Service 或者 Spring Cloud RESTful Service.

服务注册中心 (Service Registry)
服务注册中心,它是服务,其实例及元数据的数据库。服务实例在启动时注册到服务注册表,并在关闭时注销。服务和路由器的客户端查询服务注册表以查找服务的可用实例。服务注册中心可能会调用服务实例的健康检查 API 来验证它是否能够处理请求。

服务元数据 (Service Metadata)
服务元数据是指包括服务端点(endpoints)、服务标签、服务版本号、服务实例权重、路由规则、安全策略等描述服务的数据

服务提供方 (Service Provider)
是指提供可复用和可调用服务的应用方

服务消费方 (Service Consumer)
是指会发起对某个服务调用的应用方

配置 (Configuration)
在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。

配置管理 (Configuration Management)
在数据中心中,系统中所有配置的编辑、存储、分发、变更管理、历史版本管理、变更审计等所有与配置相关的活动统称为配置管理。

名字服务 (Naming Service)
提供分布式系统中所有对象(Object)、实体(Entity)的“名字”到关联的元数据之间的映射管理服务,例如 ServiceName -> Endpoints Info, Distributed Lock Name -> Lock Owner/Status Info, DNS Domain Name -> IP List, 服务发现和 DNS 就是名字服务的2大场景。

配置服务 (Configuration Service)
在服务或者应用运行过程中,提供动态配置或者元数据以及配置管理的服务提供者。

nacos逻辑架构
基于nacos的分布式服务治理_第5张图片

服务管理:实现服务CRUD,域名CRUD,服务健康状态检查,服务权重管理等功能
配置管理:实现配置管CRUD,版本管理,灰度管理,监听管理,推送轨迹,聚合数据等功能
元数据管理:提供元数据CURD 和打标能力
插件机制:实现三个模块可分可合能力,实现扩展点SPI机制
事件机制:实现异步化事件通知,sdk数据变化异步通知等逻辑
日志模块:管理日志分类,日志级别,日志可移植性(尤其避免冲突),日志格式,异常码+帮助文档
回调机制:sdk通知数据,通过统一的模式回调用户处理。接口和数据结构需要具备可扩展性
寻址模式:解决ip,域名,nameserver、广播等多种寻址模式,需要可扩展
推送通道:解决server与存储、server间、server与sdk间推送性能问题
容量管理:管理每个租户,分组下的容量,防止存储被写爆,影响服务可用性
流量管理:按照租户,分组等多个维度对请求频率,长链接个数,报文大小,请求流控进行控制
缓存机制:容灾目录,本地缓存,server缓存机制。容灾目录使用需要工具
启动模式:按照单机模式,配置模式,服务模式,dns模式,或者all模式,启动不同的程序+UI
一致性协议:解决不同数据,不同一致性要求情况下,不同一致性机制
存储模块:解决数据持久化、非持久化存储,解决数据分片问题
Nameserver:解决namespace到clusterid的路由问题,解决用户环境与nacos物理环境映射问题
CMDB:解决元数据存储,与三方cmdb系统对接问题,解决应用,人,资源关系
Metrics:暴露标准metrics数据,方便与三方监控系统打通
Trace:暴露标准trace,方便与SLA系统打通,日志白平化,推送轨迹等能力,并且可以和计量计费系统打通
接入管理:相当于阿里云开通服务,分配身份、容量、权限过程
用户管理:解决用户管理,登录,sso等问题
权限管理:解决身份识别,访问控制,角色管理等问题
审计系统:扩展接口方便与不同公司审计系统打通
通知系统:核心数据变更,或者操作,方便通过SMS系统打通,通知到对应人数据变更
OpenAPI:暴露标准Rest风格HTTP接口,简单易用,方便多语言集成
Console:易用控制台,做服务管理、配置管理等操作
SDK:多语言sdk
Agent:dns-f类似模式,或者与mesh等方案集成
CLI:命令行对产品进行轻量化管理,像git一样好用

sentinel


https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5

Sentinel 具有以下特征:
丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

基于nacos的分布式服务治理_第6张图片

sentinel分为两部分
核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。(一般需要手动改造)


nacos的使用方法

spring-boot项目

1.引入包  2个包可以单独进行使用,
 

    com.alibaba.cloud
    spring-cloud-starter-alibaba-nacos-config
    2.1.0.RELEASE



    com.alibaba.cloud
    spring-cloud-starter-alibaba-nacos-discovery
    2.1.0.RELEASE

2.使用config功能在resources里添加bootstrap.yml
spring:
  application:
    name: cloud-service
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
  main:
    allow-bean-definition-overriding: true
server:
  port: 8080
使用discovery功能在application.yml中添加
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        watch:
          enabled: false
3. 在XXApplication处增加注解  
使用config功能 @RefreshScope  正常使用@Value注解来获取nacos上的动态配置信息
使用discovery功能 @EnableDiscoveryClient  自动实现服务提供者..所有接口都可被调用
4. 服务消费者
调用方式分为两种feign调用  restTemplate调用
restTemplate为springCloud内部集成轻量,使用方便,但缺点就是有种和api调用相同的感觉,需要手动编写调用
(1)
实例化RestTemplate
/**
 * @author kuroha
 */
@Configuration
public class CloudConsumer {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

(2)调用
/**
 * @author kuroha
 */
@RestController
public class DistributeController {

    @Autowired
    private RestTemplate restTemplate;
    /**
     * 测试调用
     * @return
     */
    @GetMapping("test/user")
    public String testSpringCloud() {
        return restTemplate.getForObject("http://cloud-service/user",String.class);
    }

    /**
     * 测试调用
     * @param md5
     * @return
     */
    @GetMapping("test/MD5/{md5}")
    public String testSpringCloud(@PathVariable("md5") String md5) {
        return restTemplate.getForObject("http://cloud-service/MD5/" + md5,String.class);
    }
}

fegin是SpringCloudNetflix库的 不确定将来会不会像eureka和hystrix一样暂停维护,但目前处于可用状态
个人感觉.regin调用的思路和dubbo很像,不过一个是rpc协议.一个是rest
(1)创建接口.定义注释.和restTemplate做对比
/**
 * @author kuroha
 */
@FeignClient("cloud-service")
public interface CloudService {
    @GetMapping("/user")
    String user();
    @GetMapping("/MD5/{md5}")
    String md5(@PathVariable("md5") String md5);
}

(2)调用
/**
 * @author kuroha
 */
@RestController
public class DistributeController {

    @Autowired
    private RestTemplate restTemplate;
    /**
     * 测试调用
     * @return
     */
    @GetMapping("test/user")
    public String testSpringCloud() {
        return cloudService.user();
    }

    /**
     * 测试调用
     * @param md5
     * @return
     */
    @GetMapping("test/MD5/{md5}")
    public String testSpringCloud(@PathVariable("md5") String md5) {
        return cloudService.md5(md5);
    }
}

依据fegin的思路可以将restTemplate进行封装.也可以达到与fegin相似的效果

 

spring项目

https://github.com/kurohayan/spring-nacos

/**
 * spring-nacos的配置,用户获取namingService及configService
 * @author kuroha
 */
@Configuration
public class NacosConfiguration {

    @Value("${nacos.server-addr}")
    private String nacosServerAddr;

    @Bean
    public NamingService namingService() {
        try {
            Properties properties = new Properties();
            properties.put("serverAddr",nacosServerAddr);
            return NacosFactory.createNamingService(properties);
        } catch (NacosException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 目前维权骑士项目未使用,仅为展示用法
     * @return
     */
    @Bean
    public ConfigService configService() {
        try {
            Properties properties = new Properties();
            properties.put("serverAddr",nacosServerAddr);
            ConfigService configService = NacosFactory.createConfigService(properties);
            String config = configService.getConfig("rightkngiths.properties", "DEFAULT_GROUP", 2000);
            // 为可用properties,如果有需要可以进行改造,创建一个新的类,仿造Spring的注入方法进行属性注入
            properties.load(new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8)));
            return configService;
        }catch (NacosException | IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

cloud调用方法
/**
 * 服务调用 集成自实现路由表
 * 实现的功能
 * 1.从nacos中获取指定服务.并加载到routingMap中.
 * 2.通过监听nacos服务变化.实现服务上下线功能
 * 3.每分钟服务调用失败5次后.会自动下线指定ip,等待下次服务更新
 * 4.调用方法是否成功对用户无感知,直到服务调用成功为止,或者所以服务全部不可用为止
 * 5.几个可更改的数字getServiceList 中
 * 6.initRoutingMap可以手动配合服务上下线功能进行项目上线
 * @author kuroha
 * @date 2019-12-07 14:10:30
 */
@Slf4j
@Service
@EnableAsync
public class CloudServiceImpl implements CloudService {

    /**
     * 服务权重随机最大数
     */
    private static final int RAND_NUM = 100;
    /**
     * 服务数量(偏大即可)
     */
    private static final int SERVICE_NUM = 10;
    /**
     * 服务不可调用最大次数,超过则从服务列表中去除
     */
    private static final int SERVICE_ERROR_THROW_NUM = 3;
    /**
     * 服务不可调用的时间范围,比如 SERVICE_ERROR_TIME_OUT分钟内 SERVICE_ERROR_THROW_NUM不可调用则去除该ip
     */
    private static final int SERVICE_ERROR_TIME_OUT = 1;
    /**
     * 服务链接超时时间
     */
    private static final int SERVICE_CONNECT_TIME_OUT = 3000;
    /**
     * 服务回调读取数据时间
     */
    private static final int SERVICE_READ_TIME_OUT = 30000;
    /**
     * 服务初始化时间
     */
    private static final int SERVICE_INIT_TIME = 3600000;

    private final ConcurrentHashMap> routingMap = new ConcurrentHashMap<>();
    private final ConcurrentSkipListSet serviceSubscribeSet = new ConcurrentSkipListSet<>();
    private final RestTemplate restTemplate;
    private final Random random = new Random();
    private final ReentrantLock lock = new ReentrantLock();
    private final ReentrantLock checkLock = new ReentrantLock();
    private final LoadingCache cache;

    private final NamingService namingService;

    /**
     * 初始化
     * cache 缓存
     * restTemplate调用方法
     */
    public CloudServiceImpl(NamingService namingService) {
        cache = CacheBuilder.newBuilder().maximumSize(RAND_NUM * SERVICE_NUM * 2).expireAfterWrite(SERVICE_ERROR_TIME_OUT, TimeUnit.MINUTES)
                .build(new CacheLoader() {
                    @Override
                    public AtomicInteger load(String key) throws Exception {
                        return new AtomicInteger();
                    }
                });
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(SERVICE_CONNECT_TIME_OUT);
        factory.setReadTimeout(SERVICE_READ_TIME_OUT);
        restTemplate = new RestTemplate(factory);
        this.namingService = namingService;
    }

    /**
     * 服务调用get方法,默认返回String
     * @param serviceName 服务名
     * @param apiName 服务接口名
     * @return
     */
    @Override
    public String get(String serviceName, String apiName) {
        return this.get(serviceName,apiName,String.class);
    }
    /**
     * 服务调用post方法,默认返回String
     * @param serviceName 服务名
     * @param apiName 服务接口名
     * @param body 数据,HttpEntry或者String,Map都可
     * @return
     */
    @Override
    public String post(String serviceName, String apiName, Object body) {
        return this.post(serviceName,apiName,body,String.class);
    }
    /**
     * 服务调用get方法,返回传入的T类型
     * @param serviceName 服务名
     * @param apiName 服务接口名
     * @return
     */
    @Override
    public T get(String serviceName, String apiName, Class clazz) {
        String uri = this.getUri(serviceName);
        if (StringUtil.isBlank(uri)) {
            return null;
        }
        try {
            return restTemplate.getForObject(uri + apiName, clazz);
        }catch (Exception e) {
            log.error(e.getMessage(),e);
            Set uriSet = new HashSet<>();
            uriSet.add(uri);
            while (true) {
                try {
                    uri = checkServiceNameAndGetUri(serviceName,uri,uriSet);
                    if (StringUtil.isBlank(uri)){
                        return null;
                    }
                    return restTemplate.getForObject(uri + apiName, clazz);
                }catch (Exception ex) {
                    uriSet.add(uri);
                    log.error(ex.getMessage(),ex);
                }
            }
        }
    }
    /**
     * 服务调用post方法,返回传入的T类型
     * @param serviceName 服务名
     * @param apiName 服务接口名
     * @param body 数据,HttpEntry或者String,Map都可
     * @return
     */
    @Override
    public T post(String serviceName, String apiName, Object body, Class clazz) {
        String uri = this.getUri(serviceName);
        if (StringUtil.isBlank(uri)) {
            return null;
        }
        try {
            return restTemplate.postForObject(uri + apiName,body,clazz);
        } catch (Exception e) {
            log.error(e.getMessage(),e);
            Set uriSet = new HashSet<>();
            uriSet.add(uri);
            while (true) {
                try {
                    uri = checkServiceNameAndGetUri(serviceName,uri,uriSet);
                    if (StringUtil.isBlank(uri)){
                        return null;
                    }
                    return restTemplate.postForObject(uri + apiName,body, clazz);
                }catch (Exception ex) {
                    uriSet.add(uri);
                    log.error(ex.getMessage(),ex);
                }
            }
        }
    }
    /**
     * 服务调用put方法,直接返回调用是否成功,不返回具体的值
     * @param serviceName 服务名
     * @param apiName 服务接口名
     * @return
     */
    @Override
    public boolean put(String serviceName, String apiName, Object body) {
        String uri = this.getUri(serviceName);
        if (StringUtil.isBlank(uri)) {
            return false;
        }
        try {
            restTemplate.put(uri + apiName,body);
            return true;
        }catch (Exception e) {
            log.error(e.getMessage(),e);
            Set uriSet = new HashSet<>();
            uriSet.add(uri);
            while (true) {
                try {
                    uri = checkServiceNameAndGetUri(serviceName,uri,uriSet);
                    if (StringUtil.isBlank(uri)){
                        return false;
                    }
                    restTemplate.put(uri + apiName,body);
                    return true;
                }catch (Exception ex) {
                    uriSet.add(uri);
                    log.error(ex.getMessage(),ex);
                }
            }
        }
    }
    /**
     * 服务调用delete方法,直接返回调用是否成功,不返回具体的值
     * @param serviceName 服务名
     * @param apiName 服务接口名
     * @return
     */
    @Override
    public boolean delete(String serviceName, String apiName) {
        String uri = this.getUri(serviceName);
        if (StringUtil.isBlank(uri)) {
            return false;
        }
        try {
            restTemplate.delete(uri + apiName);
            return true;
        }catch (Exception e) {
            log.error(e.getMessage(),e);
            Set uriSet = new HashSet<>();
            uriSet.add(uri);
            while (true) {
                try {
                    uri = checkServiceNameAndGetUri(serviceName,uri,uriSet);
                    if (StringUtil.isBlank(uri)){
                        return false;
                    }
                    restTemplate.delete(uri + apiName);
                    return true;
                }catch (Exception ex) {
                    uriSet.add(uri);
                    log.error(ex.getMessage(),ex);
                }
            }
        }
    }

    /**
     * 获取实际访问的uri
     * @param serviceName 服务名
     * @return uri
     */
    private String getUri(String serviceName) {
        List list = routingMap.get(serviceName);
        // 判空
        if (list == null || list.size() == 0) {
            // 加锁
            lock.lock();
            try {
                list = routingMap.get(serviceName);
                // 再次判空
                if (list == null || list.size() == 0) {
                    // 根据服务名从nacos获取uri列表
                    list = this.getServiceList(serviceName);
                    routingMap.put(serviceName, list);
                }
                // 检测服务是否进行监听
                if (!serviceSubscribeSet.contains(serviceName)) {
                    try {
                        //服务不在监听列表的话,加入监听列表
                        namingService.subscribe(serviceName, event -> {
                            log.debug(serviceName + "服务发生变化");
                            List serviceList = this.getServiceList(serviceName);
                            routingMap.put(serviceName,serviceList);
                        });
                        serviceSubscribeSet.add(serviceName);
                    } catch (NacosException e) {
                        e.printStackTrace();
                    }
                }
            }finally {
                // 解锁
                lock.unlock();
            }
        } else {
            // 非空直接返回
            return list.get(random.nextInt(list.size()));
        }
        // 再次结束后判空名
        if (list.size()==0) {
            return null;
        }
        String uri = list.get(random.nextInt(list.size()));
        log.debug("成功寻找倒uri:" + uri);
        // 非空返回
        return uri;
    }
    /**
     * 获取除去uri的实际访问的uri
     * @param serviceName 服务名
     * @param uriSet 本次调用失败的ip地址
     * @return uri
     */
    private String getUriThrow(String serviceName, Set uriSet) {
        List list = routingMap.get(serviceName);
        list = list.stream().filter(s -> !uriSet.contains(s)).collect(Collectors.toList());
        if (list.size() == 0) {
            return null;
        }
        return list.get(random.nextInt(list.size()));
    }

    /**
     * 定时初始化路由表,用于将不健康的实例下线,或者将健康的实例再次上线
     * 初始化路由表
     */
    @Async
    @Scheduled(fixedDelay = SERVICE_INIT_TIME)
    @Override
    public void initRoutingMap() {
        log.debug("初始化路由表");
        routingMap.forEach((key,value)->{
            List uriList = this.getServiceList(key);
            if (uriList.size() == 0) {
                routingMap.remove(key);
            } else {
                routingMap.put(key, uriList);
            }
        });
    }

    /**
     * 获取全部路由表
     * @return
     */
    @Override
    public String getAllUri() {
        return JSON.toJSONString(routingMap);
    }

    /**
     * 检测服务不生效次数
     * @param serviceName
     * @param uri
     * @param uriSet
     * @return
     * @throws ExecutionException
     */
    private String checkServiceNameAndGetUri(String serviceName, String uri, Set uriSet) throws ExecutionException {
        AtomicInteger num = cache.get(serviceName);
        if (num == null) {
            cache.put(serviceName, new AtomicInteger(1));
        } else if(num.get() < SERVICE_ERROR_THROW_NUM) {
            num.incrementAndGet();
        } else {
            checkLock.lock();
            try {
                if (cache.get(serviceName).get() != 0) {
                    cache.put(serviceName, new AtomicInteger(0));
                    List collect = routingMap.get(serviceName).stream().parallel().filter(s -> !s.equals(uri)).collect(Collectors.toList());
                    routingMap.put(serviceName,collect);
                }
            }finally {
                checkLock.unlock();
            }
        }
        return getUriThrow(serviceName, uriSet);
    }

    /**
     * 获取服务列表
     * @param serviceName
     * @return
     */
    private List getServiceList(String serviceName) {
        List list = new ArrayList<>(0);
        try {
            // 获取指定serviceName的路由信息
            List instanceList = namingService.getAllInstances(serviceName);
            double sum = 0;
            // 对可能的情况进行处理
            if (instanceList.size() == 0) {
                return list;
            } else if (instanceList.size() == 1) {
                list = new ArrayList<>(1);
                Instance instance = instanceList.get(0);
                // 非启用或者非健康的跳过
                if (!(instance.isHealthy() && instance.isEnabled())) {
                    return list;
                }
                list.add(StringUtil.splicingString("http://",instance.getIp(),":",instance.getPort(),"/"));
            } else {
                list = new ArrayList<>((int)(RAND_NUM * 1.1));
                for (Instance instance : instanceList) {
                    // 非启用或者非健康的跳过
                    if (!(instance.isHealthy() && instance.isEnabled())) {
                        continue;
                    }
                    double weight = instance.getWeight();
                    sum += weight;
                }
                for (Instance instance : instanceList) {
                    // 非启用或者非健康的跳过
                    if (!(instance.isHealthy() && instance.isEnabled())) {
                        continue;
                    }
                    String uri = StringUtil.splicingString("http://",instance.getIp(),":",instance.getPort(),"/");
                    double weight = instance.getWeight();
                    int num = (int)Math.round(weight* RAND_NUM /sum);
                    for (int i = 0; i < num; i++) {
                        list.add(uri);
                    }
                }
            }
        } catch (NacosException e) {
            e.printStackTrace();
        }
        return list;
    }

}

 

sentinel改造

sentinel-dashboard

修改pom.xml


        
            com.alibaba.csp
            sentinel-datasource-nacos
            1.6.3
       

       
            com.alibaba.nacos
            nacos-api
            1.1.3
       

       
            com.alibaba.nacos
            nacos-client
            1.1.3
       


增加com.alibaba.csp.sentinel.dashboard.util.NacosConfigUtil


public final class NacosConfigUtil {
    public static final String GROUP_ID = "SENTINEL_GROUP";
    public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules";
    public static final String PARAM_FLOW_DATA_ID_POSTFIX = "-param-rules";
    public static final String CLUSTER_MAP_DATA_ID_POSTFIX = "-cluster-map";
    public static final String CLIENT_CONFIG_DATA_ID_POSTFIX = "-cc-config";
    public static final String SERVER_TRANSPORT_CONFIG_DATA_ID_POSTFIX = "-cs-transport-config";
    public static final String SERVER_FLOW_CONFIG_DATA_ID_POSTFIX = "-cs-flow-config";
    public static final String SERVER_NAMESPACE_SET_DATA_ID_POSTFIX = "-cs-namespace-set";
    private NacosConfigUtil() {}
}


增加com.alibaba.csp.sentinel.dashboard.util.FlowRuleNacosProvider


@Component("flowRuleNacosProvider")
public class FlowRuleNacosProvider implements DynamicRuleProvider> {

    @Autowired
    private ConfigService configService;
    @Autowired
    private Converter> converter;

    @Override
    public List getRules(String appName) throws Exception {
        String rules = configService.getConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
            NacosConfigUtil.GROUP_ID, 3000);
        if (StringUtil.isEmpty(rules)) {
            return new ArrayList<>();
        }
        return converter.convert(rules);
    }
}

 

增加com.alibaba.csp.sentinel.dashboard.util.FlowRuleNacosProvider


@Component("flowRuleNacosPublisher")
public class FlowRuleNacosPublisher implements DynamicRulePublisher> {

    @Autowired
    private ConfigService configService;
    @Autowired
    private Converter, String> converter;

    @Override
    public void publish(String app, List rules) throws Exception {
        AssertUtil.notEmpty(app, "app name cannot be empty");
        if (rules == null) {
            return;
        }
        configService.publishConfig(app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
            NacosConfigUtil.GROUP_ID, converter.convert(rules));
    }
}


修改com.alibaba.csp.sentinel.dashboard.controller.v2.FlowControllerV2


    // 指定实现类
    @Autowired
    @Qualifier("flowRuleNacosProvider")
    private DynamicRuleProvider> ruleProvider;
    @Autowired
    @Qualifier("flowRuleNacosPublisher")
    private DynamicRulePublisher> rulePublisher;
    
    // 从private变成public
    public void publishRules(/*@NonNull*/ String app) throws Exception {
        List rules = repository.findAllByApp(app);
        rulePublisher.publish(app, rules);
    }

 

增加com.alibaba.csp.sentinel.dashboard.config.NacosConfig


@Configuration
public class NacosConfig {

    private final String addr = "47.96.106.22:80";

    private final String namespace = "0d6121a8-6578-49d2-94f0-cb9ded23a01a";

    @Bean
    public Converter, String> flowRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    @Bean
    public Converter> flowRuleEntityDecoder() {
        return s -> JSON.parseArray(s, FlowRuleEntity.class);
    }

    @Bean
    public ConfigService nacosConfigService() throws Exception {
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, addr);
        properties.put(PropertyKeyConst.NAMESPACE, namespace);
        ConfigFactory.createConfigService(addr);
        return new NacosConfigService(properties);
    }
}

 

修改com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient


    @Autowired
    private FlowControllerV2 flowControllerV2;
private boolean setRules(String app, String ip, int port, String type, List entities) {
        if (entities == null) {
            return true;
        }
        try {
            AssertUtil.notEmpty(app, "Bad app name");
            AssertUtil.notEmpty(ip, "Bad machine IP");
            AssertUtil.isTrue(port > 0, "Bad machine port");
            String data = JSON.toJSONString(
                    entities.stream().map(r -> r.toRule()).collect(Collectors.toList()));
            Map params = new HashMap<>(2);
            params.put("type", type);
            params.put("data", data);
            String result = executeCommand(app, ip, port, SET_RULES_PATH, params, true).get();
            flowControllerV2.publishRules(app);
            logger.info("setRules: {}", result);
            return true;
        } catch (InterruptedException | ExecutionException e) {
            logger.warn("setRules api failed: {}", type, e);
            return false;
        } catch (Exception e) {
            logger.warn("setRules failed", e);
            return false;
        }
    }


修改webapp/resources/app/scripts/directives/sidebar/sidebar.html


//解开注释



  •   流控规则 V1


  • 以上完成了sentinel的改造.以后sentinel向指定服务发布的资源文件将存到nacos中,由nacos来负责进行推送规则

    使用sentinel-nacos规则

    application.yml


    spring:
      application:
        name: cloud-service
    sentinel:
      nacos:
        addr: 127.0.0.1:8848
        groupId: SENTINEL_GROUP
        namespaceId: 0d6121a8-6578-49d2-94f0-cb9ded23a01a

     

    创建一个springUtil


    /**
     * @author kuroha
     * spring初始化
     */
    @Component
    public class SpringUtil implements ApplicationContextAware {

        private static ApplicationContext applicationContext;

        private static final String ADDRESS = ConfigProperty.getInstance().getSentinelNacosAddr();
        private static final String GROUP_ID =  ConfigProperty.getInstance().getSentinelNacosGroupId();
        private static final String DATA_ID = ConfigProperty.getInstance().getApplicationName() + "-flow-rules";
        private static final String NACOS_NAMESPACE_ID = ConfigProperty.getInstance().getSentinelNacosNamespaceId();

        /**
         * 初始化sentinel规则
         */
        private static void loadNacosRules() {
            Properties properties = new Properties();
            properties.put(PropertyKeyConst.SERVER_ADDR, ADDRESS);
            properties.put(PropertyKeyConst.NAMESPACE, NACOS_NAMESPACE_ID);
            ReadableDataSource> flowRuleDataSource = new NacosDataSource<>(properties, GROUP_ID, DATA_ID,
                    source -> JSON.parseObject(source, new TypeReference>() {
                    }));
            FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
        }

        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            SpringUtil.applicationContext = applicationContext;
            loadNacosRules();
        }

        public static Object getObject(String id) {
            Object object = null;
            object = applicationContext.getBean(id);
            return object;
        }

        public static ApplicationContext getApplicationContext() {
            return applicationContext;
        }
    }

     

    至此sentinel-nacos的使用配置已经完成.接来下来只需要进行引入
    pom.xml


           
           
                com.alibaba.cloud
                spring-cloud-starter-alibaba-nacos-config
                2.1.0.RELEASE
           

           
           
                com.alibaba.cloud
                spring-cloud-starter-alibaba-sentinel
                2.1.0.RELEASE
           

           
           
                com.alibaba.cloud
                spring-cloud-alibaba-sentinel-datasource
                2.1.0.RELEASE
           

           
                com.alibaba.csp
                sentinel-datasource-nacos
                1.6.3
           

     

    在指定的controller上加入 @SentinelResource(value = "hello")  给指定资源设定指定的别名


        @GetMapping("/MD5/{data}")
        @SentinelResource(value = "hello")
        public String getMD5(@PathVariable("data") String data) {
            return "data:" + data + "--MD5:" + MD5Util.crypt(data);
        }
     

    seata

    优点:
    1.应用层基于SQL解析实现了自动补偿,从而最大程度的降低了业务的侵入性
    2.将分布式事务中的TC(事务协调者)独立部署.负责事务的注册,回滚
    3.通过全局锁实现了写隔离与读隔离


    基于nacos的分布式服务治理_第7张图片

    AT
    1.一条update语句.需要进行的步骤
    (1)获取全局事务xid(与TC通信)
    (2)解析sql,查询当前数据
    (3)执行业务update
    (4)通过id查询修改后的数据
    (5)插入回滚sql
    (6)申请提交前申请全局锁
    (7)本地事务提交
    (8)本地事务提交给过发送给TC
    又原先的一步.增加到8步操作.响应时间会增长,但对业务无入侵
    2.回滚
    (1)收到TC的分支回滚请求,开启事务
    (2)通过XID和BranchId查找到相应的UNDO LOG记录
    (3)数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处.
    (4)根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句
    (5)提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
    (6)异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

    AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:
    一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
    二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
    二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

    相应的,TCC 模式,不依赖于底层数据资源的事务支持:
    一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
    二阶段 commit 行为:调用 自定义 的 commit 逻辑。
    二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
    所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。

     

    seata防止脏数据的方法-写隔离


    1.一阶段
    tx1获取本地锁,更新操作,事务提交前获取全局锁,本地提交.释放本地锁,
    tx2获取本地锁,更新操作,尝试获取全局锁,tx1全局事务未提交,未释放全局锁,进入等待锁状态
    基于nacos的分布式服务治理_第8张图片

    2.二阶段-提交
    tx1提交全局事务,tx2获取到全局锁,提交本地事务
    2.二阶段-回滚
    tx1事务回滚.获取本地锁,但本地锁被tx2持有,进入不断重试的状态
    tx2获取全局锁超时,释放本地锁
    tx1获取到本地锁,进行全局事务回滚,释放本地锁,全局锁
    tx2获取到本地锁更新,并获取全局锁进行全局事务提交
    基于nacos的分布式服务治理_第9张图片

    seata防止脏数据的方法-读隔离

    Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
    Seata可以通过SELECT FOR UPDATE 语句的代理。达到 读已提交 
    基于nacos的分布式服务治理_第10张图片


    tx1执行事务
    tx2使用SELECT FOR UPDATE 语句进行查询数据.由于被代理,所以进行申请全局锁
    tx2如果全局锁被其他事务持有,则释放本地锁
    tx1进行事务回滚,获取本地锁,回滚后,释放本地锁和全局锁
    tx2获取本地锁及全局锁进行查询操作.
    seata并没有对普通的SELECT进行代理,只针对SELECT FOR UPDATE 进行代理

    项目组成及调用

    基于nacos的分布式服务治理_第11张图片

     

    你可能感兴趣的:(nacos)