scservers(九)监控管理-断路器管理

前言

作为一个能上生产的系统,我觉得需要两个指标

  • 性能稳定
  • 安全运行

    前者考量设计编码能力,后者更加侧重后续监控管理,这往往被人忽略却异常重要。性能稳定需要性能优化,这个下次再讲,监控管理,我们需要对机器,应用,日志等各方面进行。这是框架应用运行良好生态系统。

    springcloud 在这方面做得很好,如springcloud admin,日志跟踪管理,甚至断路器监控管理。我们首先先补上断路器的监控管理,在前面讲熔断的时候就已经讲过部分,现在把它补全。

    ## Hystrix Dashboard

    在熔断篇已经有过,当时直接讲面板集成到了应用中,这里再说下其使用并讲面板应用独立出来。
  • 配置

    dashboard 为ui,不然不需要
    xml
    <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-actuator</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>

    </dependency>

    添加注解
@EnableCircuitBreaker
@EnableHystrixDashboard

手动开启

feign: 
  hystrix:
    enabled: true
    
management:
  endpoints:
    web:
      exposure:
        include: hystrix.stream
  • 使用

    通过/hystrix访问dashboard界面,在这里可以通过输入各个应用的hystrix.stream地址来访问监控各应用的熔断情况。

Turbine

通过hystrix dashboard 可以监控各应用熔断情况,而turbine补充了对集群进行监控。我们通过mq 收集数据流的方式来继承。

  • 配置

    依赖
 <!--集群监控-->
        <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-turbine-stream</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
注解
@EnableTurbineStream

属性

turbine:
  stream:
    port: 8030
  • 服务调用者设置

    添加依赖
    xml
    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-netflix-hystrix-stream</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>

    </dependency>

scservers(八)网关-动态路由及管理

前言

前面我们已经弄了网关的基本路由鉴权等,初步的是个可运行的poc验证demo了,那么在上实际生产之前我们还要解决一个问题——静态路由,是的,之前我们在yml文件里面配置了静态的路由,在实际生产中,不可能每次增删改路由信息都要从新启动,那么我们就得做到路由是可以在运行时动态管理的。

我们将路由信息改为从Redis里面动态加载,并且增删动作放到一个独立的应用中,gateway-admin,而不是gateway-server,gateway-admin只需要内部使用。

动态路由实现

springcloud gateway支持这个动态路由的扩展,我们只需要实现RouteDefinitionRepository 这个接口,这个接口表示从存储设备中获取路由信息,我们这里选择实现从Redis获取。

  • gateway-server 路由动态获取
@Component
@Slf4j
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
    @Autowired
    private IRouteService routeService;
    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        return routeService.getRouteDefinitions();
    }
    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return routeService.save(route);
    }
    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeService.delete(routeId);
    }
}
@Service
@Slf4j
public class RouteService implements IRouteService {

    private static final String GATEWAY_ROUTES = "gateway_routes::";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private Map<String, RouteDefinition> routeDefinitionMaps = new HashMap<>();

    private void loadRouteDefinition() {
        Set<String> gatewayKeys = stringRedisTemplate.keys(GATEWAY_ROUTES + "*");

        if (CollectionUtils.isEmpty(gatewayKeys)) {
            return;
        }

        List<String> gatewayRoutes = Optional.ofNullable(stringRedisTemplate.opsForValue().multiGet(gatewayKeys)).orElse(Lists.newArrayList());
        gatewayRoutes.forEach(value -> {
            try {
                RouteDefinition routeDefinition = new ObjectMapper().readValue(value, RouteDefinition.class);
                routeDefinitionMaps.put(routeDefinition.getId(), routeDefinition);
            } catch (IOException e) {
                log.error(e.getMessage());
            }
        });
    }

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        loadRouteDefinition();
        return Flux.fromIterable(routeDefinitionMaps.values());
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> routeDefinitionMono) {
        return routeDefinitionMono.flatMap(routeDefinition -> {
            routeDefinitionMaps.put(routeDefinition.getId(), routeDefinition);
            return Mono.empty();
        });
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap(id -> {
            routeDefinitionMaps.remove(id);
            return Mono.empty();
        });
    }
}
  • gateway-admin 管理动态路由

    动态路由管理我们将初始路由设置到关系型数据库,并且加载到缓存Redis中,增删改的时候同时修改落地数据库及缓存。
@RestController
@RequestMapping("/gateway/routes")
@Api("gateway routes")
@Slf4j
public class GatewayRouteController {

    @Autowired
    private IGatewayRouteService gatewayRoutService;

    @ApiOperation(value = "新增网关路由", notes = "新增一个网关路由")
    @ApiImplicitParam(name = "gatewayRoutForm", value = "新增网关路由form表单", required = true, dataType = "GatewayRouteForm")
    @PostMapping
    public Result add(@Valid @RequestBody GatewayRouteForm gatewayRoutForm) {
        log.info("name:", gatewayRoutForm);
        GatewayRoute gatewayRout = gatewayRoutForm.toPo(GatewayRoute.class);
        return Result.success(gatewayRoutService.add(gatewayRout));
    }

    @ApiOperation(value = "删除网关路由", notes = "根据url的id来指定删除对象")
    @ApiImplicitParam(paramType = "path", name = "id", value = "网关路由ID", required = true, dataType = "long")
    @DeleteMapping(value = "/{id}")
    public Result delete(@PathVariable long id) {
        gatewayRoutService.delete(id);
        return Result.success();
    }

    @ApiOperation(value = "修改网关路由", notes = "修改指定网关路由信息")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "网关路由ID", required = true, dataType = "long"),
            @ApiImplicitParam(name = "gatewayRoutForm", value = "网关路由实体", required = true, dataType = "GatewayRouteForm")
    })
    @PutMapping(value = "/{id}")
    public Result update(@PathVariable long id, @Valid @RequestBody GatewayRouteForm gatewayRoutForm) {
        GatewayRoute gatewayRout = gatewayRoutForm.toPo(GatewayRoute.class);
        gatewayRout.setId(id);
        gatewayRoutService.update(gatewayRout);
        return Result.success();
    }

    @ApiOperation(value = "获取网关路由", notes = "根据id获取指定网关路由信息")
    @ApiImplicitParam(paramType = "path", name = "id", value = "网关路由ID", required = true, dataType = "long")
    @GetMapping(value = "/{id}")
    public Result get(@PathVariable long id) {
        log.info("get with id:{}", id);
        return Result.success(new GatewayRouteVo(gatewayRoutService.get(id)));
    }

    @ApiOperation(value = "根据uri获取网关路由", notes = "根据uri获取网关路由信息,简单查询")
    @ApiImplicitParam(paramType = "query", name = "name", value = "网关路由路径", required = true, dataType = "string")
    @ApiResponses(
            @ApiResponse(code = 200, message = "处理成功", response = Result.class)
    )
    @GetMapping
    public Result get(@RequestParam String uri) {
        List<GatewayRouteVo> gatewayRoutesVo = gatewayRoutService.query(new GatewayRouteQueryParam(uri)).stream().map(GatewayRouteVo::new).collect(Collectors.toList());
        return Result.success(gatewayRoutesVo.stream().findFirst());
    }

    @ApiOperation(value = "搜索网关路由", notes = "根据条件查询网关路由信息")
    @ApiImplicitParam(name = "gatewayRoutQueryForm", value = "网关路由查询参数", required = true, dataType = "GatewayRouteQueryForm")
    @ApiResponses(
            @ApiResponse(code = 200, message = "处理成功", response = Result.class)
    )
    @PostMapping(value = "/conditions")
    public Result search(@Valid @RequestBody GatewayRouteQueryForm gatewayRouteQueryForm) {
        List<GatewayRoute> gatewayRoutes = gatewayRoutService.query(gatewayRouteQueryForm.toParam(GatewayRouteQueryParam.class));
        List<GatewayRouteVo> gatewayRoutesVo = gatewayRoutes.stream().map(GatewayRouteVo::new).collect(Collectors.toList());
        return Result.success(gatewayRoutesVo);
    }

    @ApiOperation(value = "重载网关路由", notes = "将所以网关的路由全部重载到redis中")
    @ApiResponses(
            @ApiResponse(code = 200, message = "处理成功", response = Result.class)
    )
    @PostMapping(value = "/overload")
    public Result overload() {
        return Result.success(gatewayRoutService.overload());
    }

}
@Service
@Slf4j
public class GatewayRouteService implements IGatewayRouteService {

    private static final String GATEWAY_ROUTES = "gateway_routes::";

    @Autowired
    private GatewayRouteMapper gatewayRouteMapper;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public long add(GatewayRoute gatewayRoute) {
        long gatewayId = gatewayRouteMapper.insert(gatewayRoute);
        stringRedisTemplate.opsForValue().set(GATEWAY_ROUTES + gatewayRoute.getId(), toJson(new GatewayRouteVo(gatewayRoute)));
        return gatewayId;
    }

    @Override
    public void delete(long id) {
        gatewayRouteMapper.delete(id);
        stringRedisTemplate.delete(GATEWAY_ROUTES + id);
    }

    @Override
    public void update(GatewayRoute gatewayRoute) {
        stringRedisTemplate.delete(GATEWAY_ROUTES + gatewayRoute.getId());
        stringRedisTemplate.opsForValue().set(GATEWAY_ROUTES, toJson(new GatewayRouteVo(get(gatewayRoute.getId()))));
    }

    @Override
    public GatewayRoute get(long id) {
        return gatewayRouteMapper.select(id);
    }

    @Override
    public List<GatewayRoute> query(GatewayRouteQueryParam gatewayRouteQueryParam) {
        return gatewayRouteMapper.query(gatewayRouteQueryParam);
    }

    @Override
    public boolean overload() {
        List<GatewayRoute> gatewayRoutes = gatewayRouteMapper.query(new GatewayRouteQueryParam());
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        gatewayRoutes.forEach(gatewayRoute ->
                opsForValue.set(GATEWAY_ROUTES + gatewayRoute.getId(), toJson(new GatewayRouteVo(gatewayRoute)))
        );
        return true;
    }

    /**
     * GatewayRoute转换为json
     *
     * @param gatewayRouteVo redis需要的vo
     * @return json string
     */
    private String toJson(GatewayRouteVo gatewayRouteVo) {
        String routeDefinitionJson = Strings.EMPTY;
        try {
            routeDefinitionJson = new ObjectMapper().writeValueAsString(gatewayRouteVo);
        } catch (JsonProcessingException e) {
            log.error("网关对象序列化为json String", e);
        }
        return routeDefinitionJson;
    }
}

oauth2 with spring cloud security (转载)

因项目需要,需要和三方的oauth2服务器进行集成。网上关于spring cloud security oauth2的相关资料,一般都是讲如何配置,而能把这块原理讲透彻的比较少,这边自己做一下总结和整理,顺带介绍一下JWT的使用场景。

什么是OAuth2?

OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该令牌在限定时间、限定范围访问指定资源。主要涉及的RFC规范有RFC6749(整体授权框架),RFC6750(令牌使用),RFC6819(威胁模型)这几个,一般我们需要了解的就是RFC6749。获取令牌的方式主要有四种,分别是授权码模式,简单模式,密码模式和客户端模式,如何获取token不在本篇文章的讨论范围,我们这里假定客户端已经通过某种方式获取到了access_token,想了解具体的oauth2授权步骤可以移步阮一峰老师的理解OAuth 2.0,里面有非常详细的说明。

这里要先明确几个OAuth2中的几个重要概念:

  • resource owner: 拥有被访问资源的用户

  • user-agent: 一般来说就是浏览器

  • client: 第三方应用

Authorization server: 认证服务器,用来进行用户认证并颁发token

Resource server:资源服务器,拥有被访问资源的服务器,需要通过token来确定是否有权限访问

明确概念后,就可以看OAuth2的协议握手流程,摘自RFC6749



Abstract Protocol Flow.png

什么是Spring Security?

Spring Security是一套安全框架,可以基于RBAC(基于角色的权限控制)对用户的访问权限进行控制,核心思想是通过一系列的filter chain来进行拦截过滤,以下是ss中默认的内置过滤器列表,当然你也可以通过custom-filter来自定义扩展filter chain列表

Alias

Filter Class

Namespace Element or Attribute

CHANNEL_FILTER

ChannelProcessingFilter

http/intercept-url@requires-channel

SECURITY_CONTEXT_FILTER

SecurityContextPersistenceFilter

http

CONCURRENT_SESSION_FILTER

ConcurrentSessionFilter

session-management/concurrency-control

HEADERS_FILTER

HeaderWriterFilter

http/headers

CSRF_FILTER

CsrfFilter

http/csrf

LOGOUT_FILTER

LogoutFilter

http/logout

X509_FILTER

X509AuthenticationFilter

http/x509

PRE_AUTH_FILTER

AbstractPreAuthenticatedProcessingFilter

N/A

CAS_FILTER

CasAuthenticationFilter

N/A

FORM_LOGIN_FILTER

UsernamePasswordAuthenticationFilter

http/form-login

BASIC_AUTH_FILTER

BasicAuthenticationFilter

http/http-basic

SERVLET_API_SUPPORT_FILTER

SecurityContextHolderAwareRequestFilter

http/@servlet-api-provision

JAAS_API_SUPPORT_FILTER

JaasApiIntegrationFilter

http/@jaas-api-provision

REMEMBER_ME_FILTER

RememberMeAuthenticationFilter

http/remember-me

ANONYMOUS_FILTER

AnonymousAuthenticationFilter

http/anonymous

SESSION_MANAGEMENT_FILTER

SessionManagementFilter

session-management

EXCEPTION_TRANSLATION_FILTER

ExceptionTranslationFilter

http

FILTER_SECURITY_INTERCEPTOR

FilterSecurityInterceptor

http

SWITCH_USER_FILTER

SwitchUserFilter

N/A

这里面最核心的就是FILTER_SECURITY_INTERCEPTOR,通过FilterInvocationSecurityMetadataSource来进行资源权限的匹配,AccessDecisionManager来执行访问策略。

认证与授权(Authentication and Authorization)

一般意义来说的应用访问安全性,都是围绕认证(Authentication)和授权(Authorization)这两个核心概念来展开的。即首先需要确定用户身份,在确定这个用户是否有访问指定资源的权限。认证这块的解决方案很多,主流的有CAS、SAML2、OAUTH2等(不巧这几个都用过-_-),我们常说的单点登录方案(SSO)说的就是这块,授权的话主流的就是spring security和shiro。shiro我没用过,据说是比较轻量级,相比较而言spring security确实架构比较复杂。

Spring Cloud Security Oauth2认证流程

将OAuth2和Spring Security集成,就可以得到一套完整的安全解决方案。

为了便于理解,现在假设有一个名叫“脸盆网”的社交网站,用户在首次登陆时会要求导入用户在facebook的好友列表,以便于快速建立社交关系。具体的授权流程如下:

用户登陆脸盆网,脸盆网试图访问facebook上的好友列表

脸盆网发现该资源是facebook的受保护资源,于是返回302将用户重定向至facebook登陆页面

用户完成认证后,facebook提示用户是否将好友列表资源授权给脸盆网使用(如果本来就是已登陆facebook状态则直接显示是否授权的页面)

用户确认后,脸盆网通过授权码模式获取了facebook颁发的access_token

脸盆网携带该token访问facebook的获取用户接口https://api.facebook.com/user,facebook验证token无误后返回了与该token绑定的用户信息

脸盆网的spring security安全框架根据返回的用户信息构造出了principal对象并保存在session中

脸盆网再次携带该token访问好友列表,facebook根据该token对应的用户返回该用户的好友列表信息

该用户后续在脸盆网发起的访问facebook上的资源,只要在token有效期及权限范围内均可以正常获取(比如想访问一下保存在facebook里的相册)

不难看出,这个假设的场景中,脸盆网就是第三方应用(client),而facebook既充当了认证服务器,又充当了资源服务器。这个流程里面有几个比较重要的关键点,我需要重点说一下,而这也是其他的涉及spring security与OAuth2整合的文章中很少提及的,很容易云里雾里的地方。

细心的同学应该发现了,其实在标准的OAuth2授权过程中,5、6、8这几步都不是必须的,从上面贴的RFC6749规范来看,只要有1、2、3、4、7这几步,就完成了被保护资源访问的整个过程。事实上,RFC6749协议规范本身也并不关心用户身份的部分,它只关心token如何颁发,如何续签,如何用token访问被保护资源(facebook只要保证返回给脸盆网的就是当前用户的好友,至于当前用户是谁脸盆网不需要关心)。那为什么spring security还要做5、6这两步呢?这是因为spring security是一套完整的安全框架,它必须关心用户身份!在实际的使用场景中,OAuth2一般不仅仅用来进行被保护资源的访问,还会被用来做单点登陆(SSO)。在SSO的场景中,用户身份无疑就是核心,而token本身是不携带用户信息的,这样client就没法知道认证服务器发的token到底对应的是哪个用户。设想一下这个场景,脸盆网不想自建用户体系了,想直接用facebook的用户体系,facebook的用户和脸盆网的用户一一对应(其实在很多中小网站现在都是这种模式,可以选择使用微信、QQ、微博等网站的用户直接登陆),这种情况下,脸盆网在通过OAuth2的认证后,就希望拿到用户信息了。所以现在一般主流的OAuth2认证实现,都会预留一个用户信息获取接口,就是上面提到的https://api.facebook.com/user(虽然这不是OAuth2授权流程中必须的),这样client在拿到token后,就可以携带token通过这个接口获取用户信息,完成SSO的整个过程。另外从用户体验的角度来说,如果获取不到用户信息,则意味者每次要从脸盆网访问facebook的资源,都需要重定向一次进行认证,用户体验也不好。

OAuth2与SSO

首先要明确一点,OAuth2并不是一个SSO框架,但可以实现SSO功能。以下是一个使用github作为OAuth2认证服务器的配置文件

server:

port: 11001

security:

user:

password: user # 直接登录时的密码

ignored: /

sessions: never # session策略

oauth2:

sso:

loginPath: /login # 登录路径

client:

clientId: c40fb56cb4sdsdsdsd

clientSecret: c910ec22981daa28e1b59c778sdfjh73j3

accessTokenUri: https://github.com/login/oauth/access_token

userAuthorizationUri: https://github.com/login/oauth/authorize

resource:

userInfoUri: https://api.github.com/user

preferTokenInfo: false

可以看到accessTokenUri和userAuthorizationUri都是为了完成OAuth2的授权流程所必须的配置,而userInfoUri则是spring security框架为了完成SSO所必须要的。所以总结一下就是:通过将用户信息这个资源设置为被保护资源,可以使用OAuth2技术实现单点登陆(SSO),而Spring Security OAuth2就是这种OAuth2 SSO方案的一个实现。

Spring Security在调用user接口成功后,会构造一个OAuth2Authentication对象,这个对象是我们通常使用的UsernamePasswordAuthenticationToken对象的一个超集,里面封装了一个标准的UsernamePasswordAuthenticationToken,同时在detail中还携带了OAuth2认证中需要用到的一些关键信息(比如tokenValue,tokenType等),这时候就完成了SSO的登陆认证过程。后续用户如果再想访问被保护资源,spring security只需要从principal中取出这个用户的token,再去访问资源服务器就行了,而不需要每次进行用户授权。这里要注意的一点是此时浏览器与client之间仍然是通过传统的cookie-session机制来保持会话,而非通过token。实际上在SSO的过程中,使用到token访问的只有client与resource server之间获取user信息那一次,token的信息是保存在client的session中的,而不是在用户本地。这也是之前我没搞清楚的地方,以为浏览器和client之间也是使用token,绕了不少弯路,对于Spring Security来说,不管是用cas、saml2还是Oauth2来实现SSO,最后和用户建立会话保持的方式都是一样的。

OAuth2 SSO与CAS、SAML2的比较

根据前面所说,大家不难看出,OAuth2的SSO方案和CAS、SAML2这样的纯SSO框架是有本质区别的。在CAS和SAML2中,没有资源服务器的概念,只有认证客户端(需要验证客户信息的应用)和认证服务器(提供认证服务的应用)的概念。在CAS中这叫做cas-client和cas-server,SAML2中这叫做Service Providers和Identity Provider,可以看出CAS、SAML2规范天生就是为SSO设计的,在报文结构上都考虑到了用户信息的问题(SAML2规范甚至还带了权限信息),而OAuth2本身不是专门为SSO设计的,主要是为了解决资源第三方授权访问的问题,所以在用户信息方面,还需要额外提供一个接口。

Authorization Server与Resource Server分离

脸盆网的这个例子中,我们看到资源服务器和认证服务器是在一起的(都是facebook),在互联网场景下一般你很难找到一个独立的、权威的、第三方的认证中心(你很难想像腾讯的QQ空间通过支付宝的认证中心去授权,也很难想像使用谷歌服务要通过亚马逊去授权)。但是如果是在公司内部,这种场景其实是很多的,尤其在微服务架构下,有大量服务会对外提供资源访问,他们都需要做权限控制。那么最合理的当然就是建立一个统一的认证中心,而不是每个服务都做一个认证中心。我们前面也介绍了,token本身是不携带用户信息的,在分离后resouce server在收到请求后,如何检验token的真实性?又如何从token中获取对应的用户信息?这部分的介绍网上其实非常少,幸好我们可以直接从官方文档获取相关的蛛丝马迹,官方文档对于resouce server的配置是这样描述的:

security:

oauth2:

resource:

userInfoUri: https://api.github.com/user

preferTokenInfo: false

寥寥数语,但已经足够我们分析了。从这个配置可以看出,client在访问resource server的被保护资源时,如果没有携带token,则资源服务器直接返回一个401未认证的错误





Full authentication is required to access this resource



unauthorized

如果携带了token,则资源服务器会使用这个token向认证服务器发起一个用户查询的请求,若token错误或已经失效,则会返回



49e2c7d8720738cfb75f6b675d62e5ecd66

invalid_token

若token验证成功,则认证服务器向资源服务器返回对应的用户信息,此时resource server的spring security安全框架就可以按照标准的授权流程进行访问权限控制了。

认证与授权的解耦

从这个流程中我们可以看出,通过OAuth2进行SSO认证,有一个好处是做到了认证与授权的解耦。从日常的使用场景来说,认证比较容易做到统一和抽象,毕竟你就是你,走到哪里都是你,但是你在不同系统里面的角色,却可能千差万别(家里你是父亲,单位里你是员工,父母那里你是子女)。同时角色的设计,又是和资源服务器的设计强相关的。从前面的配置中不难发现,如果希望获得为不同资源服务器设计的角色,你只需要替换https://api.facebook.com/user这个配置就行了,这为我们的权限控制带来了更大的灵活性,而这是传统的比如SAML2这样的SSO框架做不到的。

JWT介绍

终于来到了著名的JWT部分了,JWT全称为Json Web Token,最近随着微服务架构的流行而越来越火,号称新一代的认证技术。今天我们就来看一下,jwt的本质到底是什么。

我们先来看一下OAuth2的token技术有没有什么痛点,相信从之前的介绍中你也发现了,token技术最大的问题是不携带用户信息,且资源服务器无法进行本地验证,每次对于资源的访问,资源服务器都需要向认证服务器发起请求,一是验证token的有效性,二是获取token对应的用户信息。如果有大量的此类请求,无疑处理效率是很低的,且认证服务器会变成一个中心节点,对于SLA和处理性能等均有很高的要求,这在分布式架构下是很要命的。

JWT就是在这样的背景下诞生的,从本质上来说,jwt就是一种特殊格式的token。普通的oauth2颁发的就是一串随机hash字符串,本身无意义,而jwt格式的token是有特定含义的,分为三部分:

头部Header

载荷Payload

签名Signature

这三部分均用base64进行编码,当中用.进行分隔,一个典型的jwt格式的token类似xxxxx.yyyyy.zzzzz。关于jwt格式的更多具体说明,不是本文讨论的重点,大家可以直接去官网查看官方文档,这里不过多赘述。

相信看到签名大家都很熟悉了,没错,jwt其实并不是什么高深莫测的技术,相反非常简单。认证服务器通过对称或非对称的加密方式利用payload生成signature,并在header中申明签名方式,仅此而已。通过这种本质上极其传统的方式,jwt可以实现分布式的token验证功能,即资源服务器通过事先维护好的对称或者非对称密钥(非对称的话就是认证服务器提供的公钥),直接在本地验证token,这种去中心化的验证机制无疑很对现在分布式架构的胃口。jwt相对于传统的token来说,解决以下两个痛点:

通过验证签名,token的验证可以直接在本地完成,不需要连接认证服务器

在payload中可以定义用户相关信息,这样就轻松实现了token和用户信息的绑定

在上面的那个资源服务器和认证服务器分离的例子中,如果认证服务器颁发的是jwt格式的token,那么资源服务器就可以直接自己验证token的有效性并绑定用户,这无疑大大提升了处理效率且减少了单点隐患。

JWT适用场景与不适用场景

就像布鲁克斯在《人月神话》中所说的名言一样:“没有银弹”。JWT的使用上现在也有一种误区,认为传统的认证方式都应该被jwt取代。事实上,jwt也不能解决一切问题,它也有适用场景和不适用场景。

适用场景:

一次性的身份认证

api的鉴权

这些场景能充分发挥jwt无状态以及分布式验证的优势

不适用的场景:

传统的基于session的用户会话保持

不要试图用jwt去代替session。这种模式下其实传统的session+cookie机制工作的更好,jwt因为其无状态和分布式,事实上只要在有效期内,是无法作废的,用户的签退更多是一个客户端的签退,服务端token仍然有效,你只要使用这个token,仍然可以登陆系统。另外一个问题是续签问题,使用token,无疑令续签变得十分麻烦,当然你也可以通过redis去记录token状态,并在用户访问后更新这个状态,但这就是硬生生把jwt的无状态搞成有状态了,而这些在传统的session+cookie机制中都是不需要去考虑的。这种场景下,考虑高可用,我更加推荐采用分布式的session机制,现在已经有很多的成熟框架可供选择了(比如spring session)。

scservers(六) 网关-路由

前言

前面的五篇内容已经构成了微服务的基本核心框架,已经可以结合业务,加上存储提供服务,并且整个系统内部服务之间也具备相互识别调用路由能力,那么再将这些服务汇总提供给其他第三方(不管是自己客户端(如h5,mobile app)还是作为开放平台供第三方使用),还是需要一个汇总的接口出口点。网关的需求自然形成。我们需要再提供了一个外部路由的能力。

业务网关

网关我们可以分位接入网关(连接的保持、消息的解析(卸载证书等)、消息的分发)+应用网关(路由,鉴权,限流等功能性网关的能力基础安全通用能力)。方案挺多:

* nginx(f5) + zuul(or) gateway:

nginx卸载https安全证书,高并发连接能力,反向代理能力。

gateway 功能路由鉴权,限流等包含业务功能的网关能力。

* kong(nginx基础上的包含功能性网关及业务网关能力)

nginx 我们应该用得多,比较熟悉,这里主要讲gateway的业务网关附带的一些其他能力。

sprincloud gateway

之前springcloud 一直使用zuul1.0作为其网关,后来鉴于性能(同步模型)及zuul2跳票等可能原因,社区推出了自己的网关,名字直接明了,gateway,同时还支持webflux,整合stream流等功能特性。那么我们就直接使用它来讲解吧。

  • 配置

    依赖

    <!--api网关-->
        <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!--redis限流-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    属性配置(静态路由配置):

    spring: 
      cloud:
    gateway:
    discovery:
    locator:
    enabled: true
    routes:
    #网关路由到订单服务order-service
    - id: demo-ribbon-consumer
    uri: lb://center-ribbon-demo
    predicates:
    - Path=/test/ribbon/**
    default-filters:
    - name: Retry
    args:
    retries: 3
    statuses: BAD_GATEWAY
    - AddResponseHeader=X-Response-Default-Foo, Default-Bar
    - name: RequestRateLimiter
    args:
    redis-rate-limiter.replenishRate: 1 #令牌桶的容积
    redis-rate-limiter.burstCapacity: 1 #流速 每秒
    rate-limiter: "#{@defaultRedisRateLimiter}" #SPEL表达式去的对应的bean
    key-resolver: "#{@remoteAddressKeyResolver}" #SPEL表达式去的对应的bean
  • 使用

    限流

    限流可以在nginx接入就做.gateway 也提供了限流的组件集成,基于redis做的。基本使用:

    主要查看属性配置中的:

    default-filters:
    - name: RequestRateLimiter   
    

    限流基于令牌桶算法,参数有

    * 令牌桶总容量:redis-rate-limiter.replenishRate

    * 令牌桶每秒填充平均速率:流速 每秒:redis-rate-limiter.burstCapacity

    上面两个参数只需要配置,下面两个这两个参数

    * 限流关键字对应:key-resolver:

    * 限流

    需要编写类代码:

    @Component
    public class RequestRateLimiterConfig {
    
  • 令牌桶算法

    令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。

令牌桶的另外一个好处是可以方便的改变速度。一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如 100 毫秒)往桶中增加一定数量的令牌,有些变种算法则实时的计算应该增加的令牌的数量。

scservers(五) 服务调用——安全,熔断,限流,降级

前言

服务调用是分布式系统基础,因为微服务的理念,不管粒度如何划分,系统功能间交互调用都会复杂起来,基础服务不管什么原因挂掉,容易就造成整个系统的不可用,这就是雪崩效应。原因是服务不可用,不断重试,造成服务调用者因为同步等待造成的资源耗尽也不可用,扩大后就造成所有服务都不可用了。

解决的策略基本有如下几种:

  • 限流

    按照漏斗模型,从前向后的限流

    • 用户交互限流
    • 网关限流
    • 服务调用关闭重试
  • 改进缓存
    • 缓存预加载
    • 同步改异步刷新
  • 服务自动扩容
    • 可以在docker 容器层面解决
  • 降级

    服务调用者降级服务

    • 资源即调用服务的线程池的隔离
    • 熔断,通过阈值设置失败的服务不再继续请求
    • 快速失败:通过超时机制, 熔断器 进行快速失败
    • 快速失败根据分类可以是应答错误也可以是应答缓存or默认数据

在上述策略中,微服务系统框架可以做的主要:

  • 可用情况下预防:超时处理,进行限流,资源隔离,分类,将不重要业务降级。
  • 不可用情况下直接隔离,熔断,降级等的操作。

而Springcloud 就为我们提供了Hystrix来保护我们的系统,可以将请求隔离,针对服务限流,当服务不可用时能够熔断并降级,防止级联故障。

Hystrix处理策略

当我们使用了Hystrix时,Hystrix将所有的外部调用都封装成一个HystrixCommand或者HystrixObservableCommand对象,这些外部调用将会在一个独立的线程中运行。我们可以将出现问题的服务通过熔断、降级等手段隔离开来,这样不影响整个系统的主业务。

  • 资源隔离

    通过线程池来实现资源隔离。通常在使用的时候我们会根据调用的远程服务划分出多个线程池。例如调用产品服务的 Command 放入 A 线程池,调用账户服务的 Command 放入 B 线程池。这样做的主要优点是运行环境被隔离开了。这样就算调用服务的代码存在 bug 或者由于其他原因导致自己所在线程池被耗尽时,不会对系统的其他服务造成影响。
  • 超时机制

    如果我们加入超时机制,例如2s,那么超过2s就会直接返回了,那么这样就在一定程度上可以抑制消费者资源耗尽的问题

  • 服务限流

    通过线程池+队列的方式,通过信号量的方式。比如商品评论比较慢,最大能同时处理10个线程,队列待处理5个,那么如果同时20个线程到达的话,其中就有5个线程被限流了,其中10个先被执行,另外5个在队列中

  • 服务熔断

    这个熔断可以理解为我们自己家里的电闸。

    当依赖的服务有大量超时时,在让新的请求去访问根本没有意义,只会无畏的消耗现有资源,比如我们设置了超时时间为1s,如果短时间内有大量请求在1s内都得不到响应,就意味着这个服务出现了异常,此时就没有必要再让其他的请求去访问这个服务了,这个时候就应该使用熔断器避免资源浪费

  • 服务降级

    有服务熔断,必然要有服务降级。

    所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自己准备一个本地的fallback(回退)回调,返回一个缺省值。 例如:(备用接口/缓存/mock数据),这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景

Hystrix配置使用

  • hystrix & ribbon

    • 配置

      依赖:

       <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-hystrix</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-consul-discovery</artifactId>
      </dependency>

      属性设置:

    • 注解式使用

      • step1:使用注解 @EnableHystrix 启用豪猪断路器功能。
      • step2:在需要降级的接口上添加注解,并添加参数类型相同的降级函数,返回自定义信息

        @HystrixCommand(fallbackMethod = "ribbonfallback")
        @RequestMapping("/test/ribbon")
        public String testconfigrpc(String name) {
        return "RestTemplate+Ribbon get username <<==>> "+restTemplate.getForObject("http://center-demo/test/config/username",String.class);
        }
        public String ribbonfallback(String name) {
        return "ribbonFallback default name :ribbonFallback";
        }
    • 监控

      host:port/hystrix

      此处只是简单的单个应用监控,显然不适合大规模集群监控,后续的多个客户端监控放到后面专门的监控章节去讲吧。

  • hystrix & feign

    • 配置

      依赖

      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
      </dependency>

      属性配置

      feign: 
        hystrix:
      enabled: true
    • 注解式使用

      • step1: 引入注解 @EnableFeignClients,其已经包含启用断路器注解
      • step2:在ribbon中我们使用注解降级函数,在feign中我们使用注解降级类

        @FeignClient( name = "center-demo", path = "/test", decode404 = true,fallback = DemoFallbackFeign.class)
        public interface DemoFeignclient {  @GetMapping("/config")
        String selectconfigByName(@RequestParam String name);
        @GetMapping("/token/back")
        String gettokenback();
        }
        @Component
        public class DemoFallbackFeign implements DemoFeignclient {
        @Override
        public String selectconfigByName(String name) {
        // TODO Auto-generated method stub
        return "FeignFallbackName";
        }
        @Override
        public String gettokenback() {
        // TODO Auto-generated method stub
        return "FeignFallbackToken";
        }
        }
  • 源码

scservers(四)服务调用——接口与负载均衡

前言

前面的三篇搭起微服务的基本应用框架:服务发现,分布式动态配置,消息总线的通知能力等。就如人的身体基本上已经有了头脑四肢,基本成形。

头脑四肢之间需要互联互通,相当于服务调用,并且需要一定的协议接口及基于协议的序列化反序列化,比如dubbo,grpc甚至原生的别人不了解的协议等。目前我们这里只介绍下http的,其他基本类同。服务的调用还要负载均衡,不然只用右手,容易起茧。

http-rest

目前http协议所使用的远程调用范式基本以REST为范式,spring3之后就提供RestTemplate来执行rest http接口调用,并提供序列化反序列的能力。但没有负载均衡,负载均衡依赖于服务注册,所以springcloud 提供了Ribbon作为软负载。

而Feign,则是及大成者,包含软件负载(ribbon)的http远程调用及序列化反序列化能力。即有如下方式:

* 原生RestTemplate+Ribbon

* Feign

RestTemplate+Ribbon

  • 配置

    我们需要添加Ribbon的依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    </dependency>

    但是在示例里面我们并没有在pom.xml中看到这个依赖配置,那是因为我们已经存在如下依赖:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    前面我们说过,负载还依赖于服务注册,而如果使用eureka作为服务注册,那么其已经包含了Ribbon的依赖了。如果使用其他服务注册发现中心,如consul、zookeeper、etcd,那就要加上Ribbon的依赖。嗯,全家桶确实名不虚传。

    eureka的配置这里就不再复述了。

  • 使用

    • step1:@LoadBalanced

      在RestTemplate加上注解@LoadBalanced,让RestTemplate支持负载客户端。
    @Configuration
     class MyConfiguration {
    @LoadBalanced
    @Bean
    RestTemplate restTemplate() {
    return new RestTemplate();
    }
    }
    • step2:配置负载模式

      标明Ribbon负载的工作模式,如果是轮训可以跳过这一步(默认支持轮训),如果想该用其他方式按照如下方式。已经实现了随机模式,我们也可以通过重写类支持其他模式。
    @Configuration
    public class RibbonRuleConfiguration {
    @Bean
    public IRule ribbonRule() {
    return new RandomRule();
    }
    }

    在使用到的control或者service加上使用的ribbon客户端的注解:

    @RibbonClient(name = "center-ribbon-Rule",configuration = RibbonRestTemplateConfiguration.class)
    public class AppApplication {
    
    • step3:使用 RestTemplate远程调用
    restTemplate.getForObject("http://serviceid/restpath", String.class);
    

    Eureka根据 spring.application.name 设置 serviceId,我们只要设置serviceid就可以解析出ip:port。

Feign

Feign 通过注解及拦截器让 Java HTTP 客户端编写更方便。支持可插拔编码器和解码器,降低 HTTP API 的复杂度,通过最少的资源和代码来实现和 HTTP API 的连接。通过可定制的解码器和错误处理,可以编写任意的HTTP API。Spring Cloud Feign 封装了 Ribbon 这一组件,所以在使用 Feign 同时还能提供负载均衡的功能,这一切只需要一个 @FeignClient 即可完成。

  • 配置

    新增依赖 spring-cloud-starter-openfeign

    <dependencies>
        <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    </dependencies>
  • 使用

    1. feign client 注解引入

      在application引入 feign注解

      @EnableFeignClients
      
    2. feign client 接口

      定义rpc服务的接口

      @FeignClient( name = "center-demo", path = "/test", decode404 = true)
      public interface DemoFeignclient {
      @GetMapping("/config")
      String selectconfigByName(@RequestParam String name);
      }
    3. 调用接口

      在service中实例化该接口并使用

          @Autowired
      private DemoFeignclient feignClient;
      
      @RequestMapping("/test/feign")
      public String testconfigrpcByFeign(String name) {
      return "Feign get username <<==>> "+feignClient.selectconfigByName(name);
      }

      我们看到,所有服务提供者供外部调用的接口是一样的,如果每个不同消费者都些feign client 接口,就做了重复的工作,这就是坏味道,所以我们可以像dubbo一样在写服务提供者的时候,就可以把接口单独整个架包出来,供消费者使用。具体可见后面章节的最佳实践。

  • 拦截器

    上面是使用 Feign 来调用简单的远程服务,但实际上远程服务一般会有权限验证(这部分后续章节也会出),需要在 header 中传递 token之类的。在方法中显示传递又过于麻烦了,这时候就可以考虑使用 Feign 提供的RequestInterceptor 接口,只要实现了该接口,那么Feign每次做远程调用之前都可以被它拦截下来在进行包装。示例如下,就可以方便的传递header字段了。

    @Configuration
    public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
    requestTemplate.header("token", "ok");
    }
    }
  • 源码

  • PS注意

    Feign与显示声明的Ribbon不要混合在一个controller下使用,不然使用Feign的时候会间歇性提示404错误

          {"timestamp":"2019-03-21T05:37:41.540+0000","status":404,"error":"Not Found","message":"No message available","path":"/test/token/back"}
    

scservers(三)消息总线bus

前言

消息总线bus作为一个轻量化消息中心,通知订阅的方式,完成消息的传递,其基础的应用就是与配置中心结合,通过bus通知client来配置中心拉去更新配置。

消息总线bus

spring cloud 通过封装消息队列rabbitmq,kaffka提供我们bus的一些基础功能。发送配置更新消息。

rabbitmq

安装好rabbitmq服务端,直接使用docker-compose吧,方便。

客户端client

  • 使用bus只需要pom.xml引入如下架包配置
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
  • 同时对rabbitmq 的连接信息进行配置:
spring:
  application:
    name: bus-server
  rabbitmq:
    host: ${RABBIT_MQ_HOST:localhost}
    port: ${RABBIT_MQ_PORT:5672}
    username: ${RABBIT_MQ_USERNAME:guest}
    password: ${RABBIT_MQ_PASSWORD:guest}

即与使用其他mq方式无异。

  • 消息队列注册
@Configuration
public class RabbitMQConfig {
    @Bean
    public Queue testhelloQueue() {
        return new Queue("testhello");
    }

}
  • 消息消费
@Component
@RabbitListener(queues = "testhello")
public class ReceiverDefault {
    private final Logger logger = LoggerFactory.getLogger(ReceiverDefault.class);

    @RabbitHandler
    public void receiver(String hello){
        logger.info("接收消息=====》》》》》{}",hello);
    }
}
  • 消息推送
@Component
public class SenderDefault {
    private final Logger logger = LoggerFactory.getLogger(SenderDefault.class);
   
    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sender(String context){
        
        logger.info("发送消息=========》》》》{}",context);
        this.amqpTemplate.convertAndSend("testhello",context);
    }
}

config通知

除了常用的消息队列功能,在微服务的架构中,其与config可以深度结合进行分布式的配置变更的通知。

  • 配置

    配置中心及客户端配置见配置中心一节
  • 流程

    1. 将配置通过git发布
    2. git pull 配置http接口,像配置中心发送通知

      curl -i -X POST http://confighost:port/actuator/bus-refresh
      

      应答:

      HTTP/1.1 204
      Date: Sat, 11 Aug 2018 04:06:35 GMT
      
    3. 源码示例

      center/docker-compose-demo.yaml 文件相关内容

scservers(二) 配置中心

前言

前面介绍了服务注册发现,这个是一个比较核心的功能,还有一个服务治理相关的是配置中心,作为分布式微服务,统一设置更新成千上万的微服务配置,也已经基本成为必选项,即使你的服务没有那么多,但是热更新配置等对生产配置变动的及时更新都有帮助。

配置中心

目前配置中心这一服务比较多,国内外都有比较成熟的经过生产实践的产品,如百度的disconf,携程的apollo,springcloud 也有原生的配置中心config。国内生产用得多的应该是携程的apollo,功能强大。这里就暂时先使用springcoud全家桶,apollo后续再介绍使用。

config



configserver 依托git 完成版本管理,也有本地模式。同时为了及时更新还需要消息总线来支持其及时更新配置。

config使用

  • server

    配置中心可以将服务注册到注册中心,作为服务提供者供其他应用使用。

    1. 使用

      • 新建springboot 工程,添加config starter 傻瓜包:

        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
      • 在工程启动类添加在工程启动类中,添加注解,启用默认config配置的默认值

        @EnableConfigServer
        
    2. 配置说明

      spring:
      application:
      name: config-server
      cloud:
      config:
      server:
      git:
      uri: https://local/willen/config.git
      searchPaths: /
    3. 示例源码

  • client

  • 5

scservers(一) 注册中心eureka

  • 以前做的项目稳态应用系统一直使用公司研发的一套C++ 的rpc调用框架的esb中间件,加上一些周边插件构成的SOA生态,在此基础上开发业务系统。
  • 后来基于互联网生态的发展壮大,一些创新型的业务我们就使用java开发。并选择了dubbo rpc 框架的作为基础调用。
  • 随后在做一些后台管理应用中接触了springboot 相关内容对java 的配置的简略,约定俗成的规则,真的好用。并想将应用系统框架也升级到springcloud 的生态中去,但是耽搁了下后spring cloud 2都出来了,终于下决心迁移整改。

    # 概要
  • 你们猜对了我们想做微服务,虽然我个人认为soa这样的已经挺好了挺规范,但是趋势还是得推动我们不断学习接受新的技术思想,比如现在都已经出来了servicemesh了,但还是先把微服务搞定吧,虽然我们认为技术细节soa跟微服务差异不大,主要在服务治理上的思想差异吧。
  • 作为这种为应用系统提供框架,提供治理能力的东西,一般需要实现这样的能力:
    1. 技术基础:容器(服务集成),路由,消息通信
    2. 核心功能模块:远程调用模块,服务发现,消息队列,分布式事务
    3. 通用功能模块:对象存储,任务调度等公用的基础工具模块
    4. 服务治理:服务限流降级,分布式配置管理

      # 服务发现

      以前我就说过,原公司自己的rpc的路由,即服务发现基于纯分布式的ospf的路由协议算法+esb总线结合实现,优点是不需要中心统一注册,可以自主发现相邻节点。

      缺点的路由寻址数量有限,需要分中心由一个节点进行截断,即路由网关。同时数据流不能直接发送到目标节点,而需要层层转发(只与相邻节点建立连接),虽然会增加不必要的网络压力,却也可以给分中心的网络隔离提供机会(路由网关可以起到跳板机作用)。这种私有协议基础上的功能在特定场景合适,但是不具有广泛性,社区活跃度,eureka这种基于rest接口的c/s结构的服务注册发现服务:便于管理,易用性,适用性强。

eureka

如上图我们可以看到服务提供方注册到eureka-server上,然后服务消费方通过eureka-client从eureka获取到可用服务,然后就可以进行直接的远程调用了。这就实现了对rpc的路由能力的治理。
如果服务较多,eureka server面临的性能压力也比较大,延时等,但是最主要的还是需要高可用能够提供持续不宕机服务。

eureka使用

  • server

    springcloud基于springboot,其开箱即用的原则让我们不需要过多配置就可以傻瓜式的使用eureka,后续源码解析留待下次。

    这里就简单列出配置与说明:

    1. 使用

      • 新建springboot工程,添加eureka starter 傻瓜架包:
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
      </dependency>
      • 启动类添加注解,表明事eurekaServer应用,并启用eurekaServer默认配置:
      @EnableEurekaServer // 启用 eureka server 相关默认配置 
      
    2. 简易配置说明

      server:
      port: ${EUREKA_PORT:8761}
      eureka:
      # 生产环境中官方是不建议修改默认配置,因为那样会破坏 eureka server 的保护模式
      server:
      # 关闭保护模式(生产环境不建议修改)
      enable-self-preservation: false
      # 清理间隔(默认是60 * 1000 毫秒)(生产环境不建议修改)
      eviction-interval-timer-in-ms: 60000
      # Eureka 拉取服务列表时间(默认:30秒)(生产环境不建议修改)
      remote-region-registry-fetch-interval: 5
      client:
      # eureka server 没必要自己把自己注册上去,所以可以设置成 false
      registerWithEureka: false
      # 是否从Eureka Server上获取注册信息,默认为true,此处建议修改成 false (单机设置的意义不大,如果设置成 true 启动会去抓取一次注册表,获取不到更新缓存就会出错(该错误不影响 eureka 正常使用))
      fetchRegistry: false
      serviceUrl:
      # 默认注册地址 this.serviceUrl.put("defaultZone", "http://localhost:8761/eureka/");
      # 划重点:此处的 defaultZone 千万别写成 default-zone
      defaultZone: http://${EUREKA_ZONE_HOST:localhost}:${EUREKA_ZONE_PORT:8761}/eureka/
      spring:
      application:
      name: eureka-server
    3. 示例源码

  • client

apigateway——Kong介绍(一)

网关

一切网络的流量都需要经过网关,api网关就是所有api请求都需要经过网关。

作用

对api进行一个统一的管理及处理,让业务更加专注于业务。

整合内部接口,统一开放入口:

image——>image

对接不同客户:

image

天然的,我们可以在网关层就处理一些非业务强关联的事务,主要就涉及认证,安全,日志,流控,接口聚合,熔断处理等一系列服务能力。

Kong

kong就是一个基于nginx强力的http接入服务器,使用lua进行插件式高扩展能力,高性能的一个API网关。社区版通过插件方式提供了如下的能力,并可以通过lua编写插件的方式进行扩展:

image

image

安装

百闻不如一见,适用的情况下,我们就用最简便的方式,docker容器进行安装,快速高效,

在一个docker环境电脑中就行安装,步骤:

1.新建docker-compose.yml:内容下
version: &#39;2.2&#39;

services:

  kong-database:
    image: postgres:9.4-alpine
    container_name: kong-database
    environment:
      - POSTGRES_USER=kong
      - POSTGRES_DB=kong
    healthcheck:
      test: &quot;pg_isready -U kong &amp;&amp; psql -d kong -U kong -c \&quot;SELECT 1=1\&quot;&quot;
      interval: 10s
      timeout: 5s
      retries: 5

  kong-migration:
    image: kong:${KONG_VERSION}
    container_name: kong-migration
    depends_on:
      kong-database:
        condition: service_healthy
    environment:
      - KONG_DATABASE=postgres
      - KONG_PG_HOST=kong-database
    command: sh -c &quot;kong migrations up &amp;&amp; touch migrations_run &amp;&amp; sleep 30&quot;
    healthcheck:
      test: &quot;if [[ -f migrations_run ]] ; then exit 0; else exit 1; fi&quot;
      interval: 10s
      timeout: 5s
      retries: 5

  kong:
    image: kong:${KONG_VERSION}
    container_name: kong
    depends_on:
      kong-migration:
        condition: service_healthy
    healthcheck:
      test: &quot;kong health&quot;
      interval: 10s
      timeout: 5s
      retries: 5
    environment:
      - KONG_DATABASE=postgres
      - KONG_PG_HOST=kong-database
      - KONG_ADMIN_LISTEN=0.0.0.0:8001
    ports:
      - 8001:8001
      - 8000:8000

  kong-dashboard:
    image: pgbi/kong-dashboard
    container_name: kong-dashboard
    ports:
      - 8080:8080
    depends_on:
      kong:
        condition: service_healthy
    entrypoint: ./docker/entrypoint.sh start --kong-url http://kong:8001 
2.启动
KONG_VERSION=0.11-alpine  docker-compose -f docker-compose.yml up
3.分析

在这里我们没有进行集群,一共启动三个服务,kong的数据库,kong网关,还有一个kong的管理应用,可以直观的提供web界面浏览kong网关相关信息。我们来看看提供出来的相关参数。

端口

kong:8000业务端口(所有api进此端口),8001管理端口(kong提供了restful管理接口,可对api,插件进行各种管理操作,如新增接口,新增用户等)。

kong的数据库可以使用postgres,也可以使用cassandra。