所谓微服务,就是把一个比较大的单个应用程序或服务拆分为若干个独立的、粒度很小的服务或组件。
微服务的拆解业务,这一策略,可扩展单个组件,而不需要整个的应用程序堆栈做修改,从而满足服务等级协议。微服务带来的好处是,它们更快且更容易更新。当开发者对一个传统的单体应用程序进行变更时,他们必须做详细、完整的 QA 测试,以确保变更不会影响其他特性或功能。但有了微服务,开发者可以更新应用程序的单个组件,而不会影响其他的部分。测试微服务应用程序仍然是必需的,但使得其更容易被识别和隔离,从而加快开发速度并支持 DevOps 和持续应用程序开发。
这几年的快速发展,微服务已经变得越来越流行。其中,Spring Cloud 一直在更新,并被大部分公司所使用。代表性的有 Alibaba,2018 年 11 月左右,Spring Cloud 联合创始人 Spencer Gibb 在 Spring 官网的博客页面宣布:阿里巴巴开源 Spring Cloud Alibaba,并发布了首个预览版本。随后,Spring Cloud 官方 Twitter 也发布了此消息。Spring Cloud 的版本也很多:
以 Spring Boot1.x 为例,主要包括 Eureka、Zuul、Config、Ribbon、Hystrix 等。而在 Spring Boot2.x 中,网关采用了自己的 Gateway。当然在 Alibaba 版本中,其组件更是丰富:使用 Alibaba 的 Nacos 作为注册中心和配置中心。使用自带组件 Sentinel 作为限流、熔断神器。
目前,在 Spring Boot1.x 中,用到的比较多的网关就是 Zuul。Zuul 是 Netflix 公司开源的一个网关服务,而 Spring Boot2.x 中,采用了自家推出的 Spring Cloud Gateway。
API 网关的主要作用是反向路由、安全认证、负载均衡、限流熔断、日志监控。在 Zuul 中,我们可以通过注入 Bean 的方式来配置路由,也可以在直接通过配置文件来配置:
zuul.routes.api-d.sensitiveHeaders="*"
zuul.routes.api-d.path=/business/api/**
zuul.routes.api-d.serviceId=business-web
我们可以通过网关来做一些安全的认证:如统一鉴权。在 Zuul 中:
Zuul 的工作原理
过滤器机制
zuul 的核心是一系列的 filters, 其作用可以类比 Servlet 框架的 Filter,或者 AOP。zuul 把 Request route 到用户处理逻辑的过程中,这些 filter 参与一些过滤处理,比如 Authentication,Load Shedding 等。几种标准的过滤器类型:
(1) PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
(2) ROUTING:这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpClient 或 Netfilx Ribbon 请求微服务。
(3) POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
(4) ERROR:在其他阶段发生错误时执行该过滤器。
过滤器的生命周期
filterOrder:通过 int 值来定义过滤器的执行顺序,越小优先级越高。
shouldFilter:返回一个 boolean 类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关。在上例中,我们直接返回 true,所以该过滤器总是生效。
run:过滤器的具体逻辑。需要注意,这里我们通过 ctx.setSendZuulResponse(false) 令 zuul 过滤该请求,不对其进行路由,然后通过 ctx.setResponseStatusCode(401) 设置了其返回的错误码。
代码示例:
@Component
public class AccessFilter extends ZuulFilter {
private static Logger logger = LoggerFactory.getLogger(AccessFilter.class);
@Autowired
RedisCacheConfiguration redisCacheConfiguration;
@Autowired
EnvironmentConfig env;
private static final String[] PASS_PATH_ARRAY = { "/login", "openProject" };
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
response.setCharacterEncoding("UTF-8");
response.setHeader("content-type", "text/html;charset=UTF-8");
logger.info("{} request to {}", request.getMethod(), request.getRequestURL());
for (String path : PASS_PATH_ARRAY) {
if (StringUtils.contains(request.getRequestURL().toString(), path)) {
logger.debug("request path: {} is pass", path);
return null;
}
}
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
logger.warn("access token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(404);
ctx.setResponseBody(JSONObject.toJSONString(
Response.error(200, -3, "header param error", null)));
return ctx;
}
Jedis jedis = null;
try {
JedisPool jedisPool = redisCacheConfiguration.getJedisPool();
jedis = jedisPool.getResource();
logger.debug("zuul gateway service get redisResource success");
String key = env.getPrefix() + token;
String value = jedis.get(key);
if (StringUtils.isBlank(value)) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody(JSONObject.toJSONString(Response.error(200, -1, "login timeout",null)));
return ctx;
} else {
logger.debug("access token ok");
return null;
}
} catch (Exception e) {
logger.error("get redisResource failed");
logger.error(e.getMessage(), e);
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(500);
ctx.setResponseBody(JSONObject.toJSONString(
Response.error(200, -8, "redis connect failed", null)));
return ctx;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
目前常见的几种注册中心有:Eureka、Consul、Nacos,但其实 Kubernetes 也可以实现服务的注册与发现功能,且听下面讲解。
Eureka 的高可用
在注册中心部署时,有可能出现节点问题,我们先看看 Eureka 集群如何实现高可用,首先配置基础的 Eureka 配置:
spring.application.name=eureka-server
server.port=1111
spring.profiles.active=dev
eureka.instance.hostname=localhost
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
logging.path=/data/${spring.application.name}/logs
eureka.server.enable-self-preservation=false
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.server.eviction-interval-timer-in-ms=5000
eureka.server.responseCacheUpdateInvervalMs=60000
eureka.instance.lease-expiration-duration-in-seconds=10
eureka.instance.lease-renewal-interval-in-seconds=3
eureka.server.responseCacheAutoExpirationInSeconds=180
server.undertow.accesslog.enabled=false
server.undertow.accesslog.pattern=combined
配置好后,新建一个 application-peer1.properties 文件:
spring.application.name=eureka-server
server.port=1111
eureka.instance.hostname=peer1
eureka.client.serviceUrl.defaultZone=http://peer2:1112/eureka/
application-peer2.properties 文件:
spring.application.name=eureka-server
server.port=1112
eureka.instance.hostname=peer2
eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/
这样通过域名 peer1、peer2 的形式来实现高可用,那么如何配置域名呢?有几种方式:
通过 hosts 来配置域名,vi /etc/hosts:
10.12.3.2 peer1
10.12.3.5 peer2
通过 kubernetes 部署服务时来配置域名:
hostAliases:
- ip: "10.12.3.2"
hostnames:
- "peer1"
- ip: "10.12.3.5"
hostnames:
- "peer2"
Nacos 实现服务注册、发现
Nacos 是 Alibaba 推出来的,目前最新版本是 v1.2.1。其功能可以实现服务的注册、发现,也可以作为配置管理来提供配置服务。
执行,Linux/Unix/Mac:
sh startup.sh -m standalone
Windows:
cmd startup.cmd -m standalone
当我们引入 Nacos 相关配置时,即可使用它:
org.springframework.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-alibaba-nacos-config
注意:下面这个配置文件需要是 bootstrap,否则可能失败,至于为什么,大家可以自己试试。
spring:
application:
name: oauth-cas
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
refreshable-dataids: actuator.properties,log.properties
配置完成后,完成 main:
package com.damon;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackages = {"com.damon"})
@EnableDiscoveryClient
public class CasApp {
public static void main(String[] args) {
SpringApplication.run(CasApp.class, args);
}
}
Kubernetes 服务注册与发现
接下来,请允许我为大家引入 Kubernetes 的服务注册与发现功能,spring-cloud-kubernetes 的 DiscoveryClient 服务将 Kubernetes 中的 "Service" 资源与 Spring Cloud 中的服务对应起来了,有了这个 DiscoveryClient,我们在 Kubernetes 环境下就不需要 Eureka 等来做注册发现了,而是直接使用 Kubernetes 的服务机制。
在 pom.xml 中,有对 spring-cloud-kubernetes 框架的依赖配置:
org.springframework.cloud
spring-cloud-kubernetes-core
org.springframework.cloud
spring-cloud-kubernetes-discovery
为何 spring-cloud-kubernetes 可以完成服务注册发现呢?首先,创建一个 Spring Boot 项目的启动类,且引入服务发现注解 @EnableDiscoveryClient,同时需要开启服务发现:
spring:
application:
name: edge-admin
cloud:
kubernetes:
discovery:
all-namespaces: true