浅入Spring Cloud架构

1、 微服务简介



1.1 什么是微服务

  所谓微服务,就是把一个比较大的单个应用程序或服务拆分为若干个独立的、粒度很小的服务或组件。


1.2 为什么使用微服务

  微服务的拆解业务,这一策略,可扩展单个组件,而不需要整个的应用程序堆栈做修改,从而满足服务等级协议。微服务带来的好处是,它们更快且更容易更新。当开发者对一个传统的单体应用程序进行变更时,他们必须做详细、完整的 QA 测试,以确保变更不会影响其他特性或功能。但有了微服务,开发者可以更新应用程序的单个组件,而不会影响其他的部分。测试微服务应用程序仍然是必需的,但使得其更容易被识别和隔离,从而加快开发速度并支持 DevOps 和持续应用程序开发。


1.3 微服务的架构组成

  这几年的快速发展,微服务已经变得越来越流行。其中,Spring Cloud 一直在更新,并被大部分公司所使用。代表性的有 Alibaba,2018 年 11 月左右,Spring Cloud 联合创始人 Spencer Gibb 在 Spring 官网的博客页面宣布:阿里巴巴开源 Spring Cloud Alibaba,并发布了首个预览版本。随后,Spring Cloud 官方 Twitter 也发布了此消息。Spring Cloud 的版本也很多:


Spring Cloud Spring Cloud Alibaba Spring Boot
Spring Cloud Hoxton 2.2.0.RELEASE 2.2.X.RELEASE
Spring Cloud Greenwich 2.1.1.RELEASE 2.1.X.RELEASE
Spring Cloud Finchley 2.0.1.RELEASE 2.0.X.RELEASE
Spring Cloud Edgware 1.5.1.RELEASE 1.5.X.RELEASE

以 Spring Boot1.x 为例,主要包括 Eureka、Zuul、Config、Ribbon、Hystrix 等。而在 Spring Boot2.x 中,网关采用了自己的 Gateway。当然在 Alibaba 版本中,其组件更是丰富:使用 Alibaba 的 Nacos 作为注册中心和配置中心。使用自带组件 Sentinel 作为限流、熔断神器。




2、 微服务之网关



2.1 常见的几种网关

  目前,在 Spring Boot1.x 中,用到的比较多的网关就是 Zuul。Zuul 是 Netflix 公司开源的一个网关服务,而 Spring Boot2.x 中,采用了自家推出的 Spring Cloud Gateway。


2.2 API 网关的作用

  API 网关的主要作用是反向路由、安全认证、负载均衡、限流熔断、日志监控。在 Zuul 中,我们可以通过注入 Bean 的方式来配置路由,也可以在直接通过配置文件来配置:

1
2
3
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) 设置了其返回的错误码。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@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();
}
}
}
}




3、 微服务之服务注册与发现



3.1 常见的几种注册中心

  目前常见的几种注册中心有:Eureka、Consul、Nacos,但其实 Kubernetes 也可以实现服务的注册与发现功能,且听下面讲解。


Eureka 的高可用


  在注册中心部署时,有可能出现节点问题,我们先看看 Eureka 集群如何实现高可用,首先配置基础的 Eureka 配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 文件:
1
2
3
4
spring.application.name=eureka-server
server.port=1111
eureka.instance.hostname=peer1
eureka.client.serviceUrl.defaultZone=http://peer2:1112/eureka/

application-peer2.properties 文件:

1
2
3
4
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:
1
2
10.12.3.2 peer1
10.12.3.5 peer2

  • 通过 kubernetes 部署服务时来配置域名:
1
2
3
4
5
6
7
hostAliases:
- ip: "10.12.3.2"
hostnames:
- "peer1"
- ip: "10.12.3.5"
hostnames:
- "peer2"

Nacos 实现服务注册、发现


  Nacos 是 Alibaba 推出来的,目前最新版本是 v1.2.1。其功能可以实现服务的注册、发现,也可以作为配置管理来提供配置服务。可以手动去官网下载安装,Nacos 地址:https://github.com/alibaba/nacos/releases。
执行,Linux/Unix/Mac:
1
sh startup.sh -m standalone

Windows:

1
cmd startup.cmd -m standalone

当我们引入 Nacos 相关配置时,即可使用它:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

注意:下面这个配置文件需要是 bootstrap,否则可能失败,至于为什么,大家可以自己试试。
1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}
}

完成以上,我们运行启动类,我们打开 Nacos 登录后,打开服务列表,即可看到:

在这里插入图片描述


Kubernetes 服务注册与发现


  接下来,请允许我为大家引入 Kubernetes 的服务注册与发现功能,spring-cloud-kubernetes 的 DiscoveryClient 服务将 Kubernetes 中的 "Service" 资源与 Spring Cloud 中的服务对应起来了,有了这个 DiscoveryClient,我们在 Kubernetes 环境下就不需要 Eureka 等来做注册发现了,而是直接使用 Kubernetes 的服务机制。

在 pom.xml 中,有对 spring-cloud-kubernetes 框架的依赖配置:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-core</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-discovery</artifactId>
</dependency>


为何 spring-cloud-kubernetes 可以完成服务注册发现呢?首先,创建一个 Spring Boot 项目的启动类,且引入服务发现注解 @EnableDiscoveryClient,同时需要开启服务发现:

1
2
3
4
5
6
7
spring:
application:
name: edge-admin
cloud:
kubernetes:
discovery:
all-namespaces: true


开启后,我们打开spring-cloud-kubernetes-discovery的源码,地址是:https://github.com/spring-cloud/spring-cloud-kubernetes/tree/master/spring-cloud-kubernetes-discovery,看到内容:

在这里插入图片描述


为什么要看这个文件呢?因为 spring 容器启动时,会寻找 classpath 下所有 spring.factories 文件(包括 jar 文件中的),spring.factories 中配置的所有类都会实例化,我们在开发 springboot 时常用到的***-starter.jar 就用到了这个技术,效果是一旦依赖了某个 starter.jar 很多功能就在 spring 初始化时候自动执行。


spring.factories 文件中有两个类:KubernetesDiscoveryClientAutoConfiguration 和 KubernetesDiscoveryClientConfigClientBootstrapConfiguration 都会被实例化。先看 KubernetesDiscoveryClientConfigClientBootstrapConfiguration,KubernetesAutoConfiguration 和 KubernetesDiscoveryClientAutoConfiguration 这两个类会被实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 * Copyright 2013-2019 the original author or authors.

package org.springframework.cloud.kubernetes.discovery;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.kubernetes.KubernetesAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@ConditionalOnProperty("spring.cloud.config.discovery.enabled")
@Import({ KubernetesAutoConfiguration.class,
KubernetesDiscoveryClientAutoConfiguration.class })
public class KubernetesDiscoveryClientConfigClientBootstrapConfiguration {

}

再看 KubernetesAutoConfiguration 的源码,会实例化一个重要的类 DefaultKubernetesClient,如下:

1
2
3
4
5
@Bean
@ConditionalOnMissingBean
public KubernetesClient kubernetesClient(Config config) {
return new DefaultKubernetesClient(config);
}

最后我们再看 KubernetesDiscoveryClientAutoConfiguration 源码,注意 kubernetesDiscoveryClient 方法,这里面是接口实现的重点,还要重点关注的地方是 KubernetesClient 参数的值,是上面提到的 DefaultKubernetesClient 对象:
1
2
3
4
5
6
7
8
9
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "spring.cloud.kubernetes.discovery.enabled", matchIfMissing = true)
public KubernetesDiscoveryClient kubernetesDiscoveryClient(KubernetesClient client,
KubernetesDiscoveryProperties properties,
KubernetesClientServicesFunction kubernetesClientServicesFunction,
DefaultIsServicePortSecureResolver isServicePortSecureResolver) {
return new KubernetesDiscoveryClient(client, properties, kubernetesClientServicesFunction, isServicePortSecureResolver);
}

接下来,我们看 spring-cloud-kubernetes 中的 KubernetesDiscoveryClient.java,看方法:

1
2
3
4
5
public List<String> getServices(Predicate<Service> filter) {
return this.kubernetesClientServicesFunction.apply(this.client).list().getItems()
.stream().filter(filter).map(s -> s.getMetadata().getName())
.collect(Collectors.toList());
}

在 apply(this.client).list(),可以看到数据源其实就是 this.client,并且 KubernetesClientServicesFunction 实例化时:

1
2
3
4
5
6
7
8
9
@Bean
public KubernetesClientServicesFunction servicesFunction(
KubernetesDiscoveryProperties properties) {
if (properties.getServiceLabels().isEmpty()) {
return KubernetesClient::services;
}

return (client) -> client.services().withLabels(properties.getServiceLabels());
}

调用其 services 方法的返回结果,KubernetesDiscoveryClient.getServices 方法中的 this.client 是什么呢?在前面的分析时已经提到了,就是 DefaultKubernetesClient 类的实例,所以,此时要去去看 DefaultKubernetesClient.services 方法,发现 client 是 ServiceOperationsImpl:

1
2
3
4
@Override
public MixedOperation<Service, ServiceList, DoneableService, ServiceResource<Service, DoneableService>> services() {
return new ServiceOperationsImpl(httpClient, getConfiguration(), getNamespace());
}

接着我们在实例 ServiceOperationsImpl 中看其 list 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public L list() throws KubernetesClientException {
try {
HttpUrl.Builder requestUrlBuilder = HttpUrl.get(getNamespacedUrl()).newBuilder();

String labelQueryParam = getLabelQueryParam();
if (Utils.isNotNullOrEmpty(labelQueryParam)) {
requestUrlBuilder.addQueryParameter("labelSelector", labelQueryParam);
}

String fieldQueryString = getFieldQueryParam();
if (Utils.isNotNullOrEmpty(fieldQueryString)) {
requestUrlBuilder.addQueryParameter("fieldSelector", fieldQueryString);
}

Request.Builder requestBuilder = new Request.Builder().get().url(requestUrlBuilder.build());
L answer = handleResponse(requestBuilder, listType);
updateApiVersion(answer);
return answer;
} catch (InterruptedException | ExecutionException | IOException e) {
throw KubernetesClientException.launderThrowable(forOperationType("list"), e);
}
}

接着展开上面代码的 handleResponse 函数,可见里面是一次 http 请求,至于请求的地址,可以展开 getNamespacedUrl() 方法,里面调用的 getRootUrl 方法如下:

1
2
3
4
5
6
7
8
9
10
public URL getRootUrl() {
try {
if (apiGroup != null) {
return new URL(URLUtils.join(config.getMasterUrl().toString(), "apis", apiGroup, apiVersion));
}
return new URL(URLUtils.join(config.getMasterUrl().toString(), "api", apiVersion));
} catch (MalformedURLException e) {
throw KubernetesClientException.launderThrowable(e);
}
}

我们看到逻辑中,貌似了解到其结果是这样的格式:

1
xxx/api/version 或 xxx/apis/xxx/version

看到这样的结果,感觉比较像访问 kubernetes 的 API Server 时用的 URL 标准格式,有关 API Server 服务的详情请参考官方文档,地址是: https://kubernetes.io/docs/reference/using-api/api-concepts/。


弄清楚以上,我们发现了其实最终是向 kubernetes 的 API Server 发起 http 请求,获取 Service 资源的数据列表。因此,我们在最后还得在 k8s 底层新建 Service 资源来让其获取:
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: admin-web-service
namespace: default
spec:
ports:
- name: admin-web01
port: 2001
targetPort: admin-web01
selector:
app: admin-web

当然,在部署时,不管是以 Deployment 形式,还是以 DaemonSet 来部署,其最后还是 pod,如果要实现单个服务的多节点部署,可以用:
1
kubectl scale --replicas=2 deployment admin-web-deployment

总结:


spring-cloud-kubernetes 这个组件的服务发现目的就是获取 Kubernetes 中一个或者多个 Namespace 下的所有服务列表,且在过滤列表时候设置过滤的端口号 ,这样获取到服务列表后就能让依赖它们的 Spring Boot 或其它框架的应用完成服务发现工作,让服务能够通过 http://serviceName 这种方式进行访问。



4、 微服务之配置管理



4.1 常见的配置中心


&emsp;&emsp;目前常见的几种配置中心有:Spring Cloud Config、Apollo、Nacos,但其实 Kubernetes 组件 configMap 就可以实现服务的配置管理。并且,在 Spring Boot2.x 中,就已经引入使用了。


Nacos 配置中心


&emsp;&emsp;在上面注册中心中,我们讲到 Nacos,作为注册中心,其实也可以作为配置来管理服务的环境变量。


同样,引入其以依赖:
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

同样,注意:下面这个配置文件需要是 bootstrap,否则可能失败。

1
2
3
4
5
6
7
8
9
10
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

启动类在上面的注册中心已经讲过了,现在看其配置类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.damon.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;


@Component
@RefreshScope
public class EnvConfig {

@Value("${jdbc.driverClassName:}")
private String jdbc_driverClassName;

@Value("${jdbc.url:}")
private String jdbc_url;

@Value("${jdbc.username:}")
private String jdbc_username;

@Value("${jdbc.password:}")
private String jdbc_password;

public String getJdbc_driverClassName() {
return jdbc_driverClassName;
}

public void setJdbc_driverClassName(String jdbc_driverClassName) {
this.jdbc_driverClassName = jdbc_driverClassName;
}

public String getJdbc_url() {
return jdbc_url;
}

public void setJdbc_url(String jdbc_url) {
this.jdbc_url = jdbc_url;
}

public String getJdbc_username() {
return jdbc_username;
}

public void setJdbc_username(String jdbc_username) {
this.jdbc_username = jdbc_username;
}

public String getJdbc_password() {
return jdbc_password;
}

public void setJdbc_password(String jdbc_password) {
this.jdbc_password = jdbc_password;
}

}

我们通过注解 @Component@RefreshScope,来实现其配置可被获取。注意 @Value("${jdbc.username:}")最后需要冒号的,否则启动后会报错的。


接下来可以配置属性值来,点击配置管理,查看配置:

在这里插入图片描述


如果首次打开没有配置,可以新建配置:

在这里插入图片描述


编辑配置:

在这里插入图片描述


新建完之后,可以编辑,也可以删除,这里就不操作了。


ConfigMap 作为配置管理


  spring-cloud-kubernetes 在上面提供了服务发现的功能,其实它还很强大,也提供了服务的配置管理:
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-config</artifactId>
</dependency>

在初始化时,引入注解来自动注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.damon;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import com.damon.config.EnvConfig;

@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackages = {"com.damon"})
@EnableConfigurationProperties(EnvConfig.class)
@EnableDiscoveryClient
public class AdminApp {

public static void main(String[] args) {
SpringApplication.run(AdminApp.class, args);
}

}


其中,EnvConfig 类来配置环境变量配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package com.damon.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "damon")
public class EnvConfig {

private String message = "This is a dummy message";

private String spring_mq_host;
private String spring_mq_port;
private String spring_mq_user;
private String spring_mq_pwd;
private String jdbc_driverClassName = "com.mysql.jdbc.Driver";
private String jdbc_url = "jdbc:mysql://localhost:3306/data_test?zeroDateTimeBehavior=convertToNull&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false";
private String jdbc_username = "root";
private String jdbc_password = "wwww";
private String spring_redis_host;
private String spring_redis_port;
private String spring_redis_pwd;
private String base_path;
private String chunk_size;
private Long expire_time= 600000L;

public String getMessage() {
return this.message;
}

public void setMessage(String message) {
this.message = message;
}


public String getSpring_mq_host() {
return spring_mq_host;
}

public void setSpring_mq_host(String spring_mq_host) {
this.spring_mq_host = spring_mq_host;
}

public String getSpring_mq_port() {
return spring_mq_port;
}

public void setSpring_mq_port(String spring_mq_port) {
this.spring_mq_port = spring_mq_port;
}

public String getSpring_mq_user() {
return spring_mq_user;
}

public void setSpring_mq_user(String spring_mq_user) {
this.spring_mq_user = spring_mq_user;
}

public String getSpring_mq_pwd() {
return spring_mq_pwd;
}

public void setSpring_mq_pwd(String spring_mq_pwd) {
this.spring_mq_pwd = spring_mq_pwd;
}

public String getJdbc_driverClassName() {
return jdbc_driverClassName;
}

public void setJdbc_driverClassName(String jdbc_driverClassName) {
this.jdbc_driverClassName = jdbc_driverClassName;
}

public String getJdbc_url() {
return jdbc_url;
}

public void setJdbc_url(String jdbc_url) {
this.jdbc_url = jdbc_url;
}

public String getJdbc_username() {
return jdbc_username;
}

public void setJdbc_username(String jdbc_username) {
this.jdbc_username = jdbc_username;
}

public String getJdbc_password() {
return jdbc_password;
}

public void setJdbc_password(String jdbc_password) {
this.jdbc_password = jdbc_password;
}

public String getSpring_redis_host() {
return spring_redis_host;
}

public void setSpring_redis_host(String spring_redis_host) {
this.spring_redis_host = spring_redis_host;
}

public String getSpring_redis_port() {
return spring_redis_port;
}

public void setSpring_redis_port(String spring_redis_port) {
this.spring_redis_port = spring_redis_port;
}

public String getSpring_redis_pwd() {
return spring_redis_pwd;
}

public void setSpring_redis_pwd(String spring_redis_pwd) {
this.spring_redis_pwd = spring_redis_pwd;
}


public String getBase_path() {
return base_path;
}

public void setBase_path(String base_path) {
this.base_path = base_path;
}

public String getChunk_size() {
return chunk_size;
}

public void setChunk_size(String chunk_size) {
this.chunk_size = chunk_size;
}

public Long getExpire_time() {
return expire_time;
}

public void setExpire_time(Long expire_time) {
this.expire_time = expire_time;
}


}


这样,在部署时,我们新建 ConfigMap 类型的资源,同时,会配置其属性值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kind: ConfigMap
apiVersion: v1
metadata:
name: admin-web
data:
application.yaml: |-
damon:
message: Say Hello to the World
---
spring:
profiles: dev
damon:
message: Say Hello to the Developers
---
spring:
profiles: test
damon:
message: Say Hello to the Test
---
spring:
profiles: prod
damon:
message: Say Hello to the Prod

并且结合配置,来实现动态更新:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: admin-web
cloud:
kubernetes:
discovery:
all-namespaces: true
reload:
enabled: true
mode: polling
period: 500
config:
sources:
- name: ${spring.application.name}
namespace: default

这里是实现自动 500ms 拉取配置,也可以通过事件触发的形式来动态获取最新配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: admin-web
cloud:
kubernetes:
config:
sources:
- name: ${spring.application.name}
namespace: default
discovery:
all-namespaces: true
reload:
enabled: true
mode: event
period: 500



5、 微服务模块划分



5.1 如何划分微服务


&emsp;&emsp;微服务架构设计中,服务拆分的问题很突出,第一种,按照纵向的业务拆分,第二种,横向的功能拆分。


&emsp;&emsp;以电商业务为例,首先按照业务领域的纵向拆分,分为用户微服务、商品微服务、交易微服务、订单微服务等等。


&emsp;&emsp;思考一下: 在纵向拆分仅仅按照业务领域进行拆分是否满足所有的业务场景?结果肯定是否定的。例如用户服务分为用户注册(写)和登录(读)等。写请求的重要性总是大于读请求的,在高并发下,读写比例 10:1,甚至更高的情况下,从而导致了大量的读请求往往会直接影响写请求。为了避免大量的读对写的请求干扰,需要对服务进行读写分离,即用户注册为一个微服务,登录为另一个微服务。此时按照 API 的细粒度继续进行纵向的业务拆分。


&emsp;&emsp;在横向上,按照所请求的功能进行拆分,即对一个请求的生命周期继续进行拆分。请求从用户端发出,首先接受到请求的是网关服务(这里不考虑 nginx 代理网关分发过程),网关服务对请求进行鉴权、参数合法性检查、路由转发等。接下来业务逻辑服务对请求进行业务逻辑的编排处理。对业务数据进行存储和查询就需要数据访问服务,数据访问服务提供了基本的 CRUD 原子操作,并负责海量数据的分库分表,以及屏蔽底层存储的差异性等功能。最后是数据持久化和缓存服务,比如可以采用 MQ、Kafka、Redis Cluster 等。


&emsp;&emsp;微服务架构通过业务的纵向拆分以及功能的横向拆分,服务演化成更小的颗粒度,各服务之间相互解耦,每个服务都可以快速迭代和持续交付(CI/CD),从而在公司层面能够达到降本增效的终极目标。但是服务粒度越细,服务之间的交互就会越来越多,更多的交互会使得服务之间的治理更复杂。服务之间的治理包括服务间的发现、通信、路由、负载均衡、重试机制、限流降级、熔断、链路跟踪等。


5.2 微服务划分的粒度


&emsp;&emsp;微服务划分粒度,其最核心的六个字可能就是:“高内聚、低耦合”。高内聚:就是说每个服务处于同一个网络或网域下,而且相对于外部,整个的是一个封闭的、安全的盒子,宛如一朵玫瑰花。盒子对外的接口是不变的,盒子内部各模块之间的接口也是不变的,但是各模块内部的内容可以更改。模块只对外暴露最小限度的接口,避免强依赖关系。增删一个模块,应该只会影响有依赖关系的相关模块,无关的不应该受影响。


&emsp;&emsp;那么低耦合,这就涉及到我们业务系统的设计了。所谓低耦合:就是要每个业务模块之间的关系降低,减少冗余、重复、交叉的复杂度,模块功能划分也尽可能单一。这样,才能达到低耦合的目的。


结束福利

开源实战利用 k8s 作微服务的架构设计代码:

1
2
3
https://gitee.com/damon_one/spring-cloud-k8s
https://gitee.com/damon_one/spring-cloud-oauth2
https://gitee.com/damon_one/Springcloud-Learning-Dalston

欢迎大家 star,多多指教。


关于作者

  笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。


欢迎关注:InfoQ

欢迎关注:腾讯自媒体专栏


欢迎关注

公号:交个朋友之猿天地

公号:damon8

公号:天山六路折梅手


打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2023 交个朋友之猿天地
  • Powered By Hexo | Title - Nothing
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信