相信也不用再多介绍,近几年最火爆的全家桶式微服务框架。这套框架里面已经囊括了微服务的注册与发布,服务的自动发现与治理,负载均衡与路由,服务降级与熔断,分布式配置中心,网关服务,消息总线等等一大堆子项目。(截止本文编写时间为止,spring cloud的二级子项目数已经达到了丧心病狂的31个)。所有你需要的、不需要的,这里都应有尽有。
这里并不想一一罗列spring cloud下面的所有子项目的功能的描述,结合国内企业项目的实际需求,这里只介绍几个个人认为使用频率最高的几个子项目:
后面将会逐一介绍这些框架的使用方法,优缺点,如何进行整合。
Eureka是spring cloud netflix 中的服务注册中心,功能类似于dubbo的dubbo admin。但与dubbo admin不同的是,Eureka不仅限于提供一般的远程服务注册,Eureka可以和Spring cloud config server配合,提供高可用的分布式配置中心服务。
要启动一个Eureka注册中心也是十分简单。只要在spring boot 程序中加入@EnableEurekaServer注解即可。
@EnableEurekaServer
@SpringBootApplication
public class EurakaServer {
public static void main(String[] args) {
SpringApplication.run(EurakaServer.class, args);
}
}
Euraka提供了高可用方案,Euraka在为其它的Eureka客户端提供注册中心服务的同时,自己也会向其它的Euraka注册中心注册自己的服务,也就是说,通过Euraka服务器之间互相注册服务,互相监听心跳,可以保证即便某一个Eureka服务挂了,也不会影响整个集群。
配置这种点对点监控,十分简单,下面是实现了Eureka服务之间互相监控的application.yml配置
server:
port: 8890
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
leaseRenewalIntervalInSeconds: 5
lease-expiration-duration-in-seconds: 90
prefer-ip-address: true
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
配置的关键在于最后的三行,如果要运行单例模式,就必须把eureka的自动注册功能,自动获取注册信息功能禁用掉。否则启动的时候Erueka会默认搜索defaultzone配置的url,来找其它的erueka服务器。当然,生产环境上面,一般是同时部署多个eureka,所以默认打开也是可以理解的。本文的关注点,主要是快速搭建,详细配置详情请参看spring cloud neflix官网.
如果需要用到erueka的集群模式,需要把自己也声明成一个eureka客户端,同时在defaultZone配置其它的eureka实例的url即可。如果存在多个实例,可以使用逗号隔开。
Eureka与dubbo不同,Eureka并不依赖像zookeeper这样的服务协作中间件来达到数据一致性。Eureka实例之间会自动同步注册信息。Eureka实例之间,可以跨越多个不同的IDC和网络,甚至物理上隔离的网络。
可能是出于对其高可用特性的自信,Eureka并不提供持久化方案,也就是说Eureka的所有状态数据都是保存在内存中,同样,Eureka客户端也是。这样的好处是,Eureka的响应速度很快,一个服务一旦被注册到Eureka服务中,马上就能被其它Eureka客户端所感知。但不好的地方,显然就是万一所有的 eureka实例全部都挂了,所有的服务要慢慢地一个个地注册回来。
Eureka注册中心自带一个比较简陋的GUI界面。
可以非常直观地在首页看到当前有多少个服务,如上图所示,有一个名叫CONFIG-SERVER的应用注册到服务中心。关于这个CONFIG-SERVER的更多的细节。将在下一节介绍
在分布式环境里,应用的配置往往是比较麻烦的。如果统一在编译打包阶段,设置好所有的配置参数(如在配置文件中配置),这对一些需要动态更新的配置项非常不友好,而且随着项目规模的变大,需要管理的配置也会越来越多,配置文件的维护也会变得越来越困难。
如果放在统一的外部配置服务中心,也会遇到诸如服务瓶颈,单点失效,应用之间的数据一致性等问题。于是开源世界就涌现了一些分布式配置中心方案,常见的有阿里的diamond,百度的disconf(后面会重点讲述)。作为微服务全家桶的spring cloud,当然也不会缺失这一块拼图,于是就有了spring cloud config这个专门用于解决分布式配置中心的方案。
Spring cloud 的配置中心提供多种后端配置源,如Git,数据库,redis。下面将简单介绍使用git作为配置源的方案。
首先需要在github(企业内部一般使用私有gitlab)新建一个仓库,例子中使用了我个人的公开git仓库https://github.com/uniqueleon/spring-cloud-demo-config,然后在application.yml中进行相庆的配置
server:
port: 8891
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://github.com/uniqueleon/spring-cloud-demo-config
freemarker:
template-loader-path: classpath:/templates/
prefer-file-system-access: false
resources:
add-mappings: false
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8890/eureka/
为了让配置中心服务器能通过服务发现的形式,被其它使用到配置中心的微服务能感应到,配置中把config-server声明成了一个eureka的客户端。
在Springboot启动类中加入相应的注解。
@EnableConfigServer
@EnableEurekaClient
@SpringBootApplication
public class ConfigServer {
public static void main(String[] args) {
SpringApplication.run(ConfigServer.class, args);
}
}
这样一个基于Eureka进行服务注册/发现的分布式配置中心就搭建完成了。由于git本身就支持分布式,所以配置中心本身不需要维护任何状态信息,因此是无状态的,可以很轻易实现水平扩展。
客户端获取配置信息也是十分简单,只需要在启动类中声明使用Eureka客户端和自动配置功能即可。
@SpringBootApplication
@EnableAutoConfiguration
@EnableEurekaClient
public class Client1Application {
public static void main(String[] args) {
SpringApplication.run(Client1Application.class, args);
}
}
代码中,只要添加@Value注解,就可以通过自动配置功能注入配置项的值
@RestController
@RequestMapping("/web/test")
public class TestWebController {
@Value("${message.hello}")
String name = "World";
@RequestMapping("/")
public String home() {
return "Hello " + name ;
}
@RequestMapping("/person")
public Person getPerson(@RequestParam("personID")Long persionID) {
return new Person(persionID,"");
}
}
配置文件application-dev.properties添加对应的配置项
message.hello=heelp wordl
启动客户端,访问前面定义的http服务:
至此, 分布式配置中心搭建完成。
由于分布式配置中心后端的配置管理是使用git来完成的,所以配置中心只需要通过git工具实时拉取配置文件数据即可,自身并不保存任何状态数据,所以很容易实现集群化。
当然,使用git的缺点也是显然。例如,不能对配置文件进行有效的拆分与管理,没有直观的图形化界面进行配置,依赖于版本管理工具。
微服务(microservice)这个词虽然大家听得耳根都烂了,但是不同人对于微服务,还是有不同的见解。有人认为微服务其实就是新瓶装旧酒,其本则还是服务封装,重用代码。有人认为是进行更细粒度的项目拆分。总之,就是一百个人就有一百个哈姆雷特。我个人认为,微服务本身并没有带来什么技术上的变革,只是人们思维方式的一种转变。就好像几百年前牛顿告诉咱们,时间是恒定的,空间是均匀的一样。但现在我们都知道牛顿是错的。
过去我们搭服务,更看重的是每个服务之间的协同工作,每个服务就像机器里面的零件一样,必须整齐划一,互相之间有严格的接口规范与定义,相互之间不可替代。但我眼中的微服务就不一样,它打破了这种定式。为什么我要这么说?大家看下面的例子,就可以明白了。
下面继续前面的例子,我创建一个新的测试项目中,先在POM文件中声明一些依赖。
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.8.RELEASE
org.aztec
spring-client2
0.0.1-SNAPSHOT
jar
spring-client2
http://maven.apache.org
1.8
Greenwich.SR3
UTF-8
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
junit
junit
3.8.1
test
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
配置啥的,没有什么特点,都是对spring的依赖。
然后,我在项目里定义了一个Restful Web Service。
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloRestService {
@RequestMapping("/web/test/")
public String sayHello() {
return "{\"name\":\"good\",\"message\":\"hello\"}";
}
}
声明了同一个hello service。
然后配置文件从上一个示例项目上复制下来,因为没用到配置中心,所以把spring cloud config的部分去掉,改一下端口号
server:
port: 8896
spring:
application:
name: apptest
profiles:
active: dev
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8890/eureka/
spring:
cloud:
config:
discovery:
enabled: true
serviceId: config-server
好,我把这个项目启动一下。接下来,就可以看到Eureka界面中, app test那一栏有两个不同的url了,表明这个应用在两个服务器在提供服务。
现在服务端基本上准备就绪了,可以来开发客户端。开发基于Web service的微服务客户端,有一个很好用的东西,就是openfeign项目。这个子项目极大的简化了开发流程。
我们先在新项目引入一些依赖。
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.8.RELEASE
org.aztec
spring-cloud-demo
0.0.1-SNAPSHOT
spring-cloud-demo
Demo project for Spring Boot
jar
1.8
Greenwich.SR3
3.0.0.M2
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-starter-netflix-ribbon
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
mysql
mysql-connector-java
5.1.44
io.shardingsphere
sharding-jdbc
3.0.0.M3
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-dbcp2
com.baidu.disconf
disconf-client
2.6.36
org.springframework.boot
spring-boot-starter-cache
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
这个项目虽然乱入了很多别的东西(这在后面系列文章将陆续介绍),但没有看到对前面两个服务端项目的依赖,核心还是spring cloud自己 的东西。
下面我们声明调用方接口
package org.aztec.spring.client.demo2.feign;
import org.aztec.spring.client.demo2.entity.People;
import org.aztec.spring.client.demo2.feign.fallback.WebTestFallBack;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name="apptest",fallback = WebTestFallBack.class)
public interface WebTestService {
@RequestMapping(method = RequestMethod.GET,value="/web/test/")
public String getHelloString();
@RequestMapping(method = RequestMethod.POST,value="/web/test/person")
public People findByID(@RequestParam("personID")Long id);
}
然后再声明一个RestController来引用这个接口,并提供对外访问的能力。
package org.aztec.spring.client.demo2.web.controller;
import org.aztec.spring.client.demo2.entity.People;
import org.aztec.spring.client.demo2.feign.WebTestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/feign")
public class TestFeignController {
@Autowired
private WebTestService service;
@RequestMapping("/hello")
public String getFeignHelloMsg() {
return "Fetching msg from remote fiegn:" + service.getHelloString();
//return "hello";
}
@RequestMapping("/person")
public String showPerson(@RequestParam("personid") Long paramID) {
People person = service.findByID(paramID);
return person.toString();
}
}
配置文件大同小异,我这里就不粘贴。项目中的示例代码都将在我的github中公布。
打开测试url访问接口:
可以明显看到,feign客户端自带的ribbon负载均衡器已经发挥作用。调用会随机地分发到两个不同的服务中。
自至,关于使用spring cloud 进行快速微服务开发的入门级介绍到这为止了。后面还会陆续更新与其它第三方组件的整合方案,以供各位看客大大参考。
通过上面的例子,可以很清晰看到微服务具备下面几个很重要的特征
以上特征中,我主要想谈谈3-5点
软件工程方法论经常把一个大型的软件工程项目拆分成很多个不同的部件,这是受其它工程技术的启发。把软件项目可管理的逻辑单元定义为“部件”,这最重要的意义在于可以进行替换。就像一台汽车,如果每一个零件都是焊死的话,那么一旦某一个零件出问题,整台车都会废掉。发明“可替换部件”技术的意义在于,当某一个零部件损坏了,能马上找到相同的零件进行替换。软件其实也一样,当使用的时间长了,一样会变“坏”。但软件部件的“坏”通常不是发自自身的,更多的是来源于业务上的压力。所以当部件发生了故障,代表着的可能不仅是替换,还可能是重构。我们经得起这样的拆腾吗?
我经常觉得写代码(特别是底层框架代码)更像是写一份合同,如果合同条款清晰,所有情况都分析得清清楚楚,这代码很好写。但事实上,做得久了会发现,越清晰的合同,后面越难改。因为你会发现,你之前做的所有工作都是基于之前已有的条款,一旦这样的假定被打破,就必须在原来的基础上修修补补。久了就会发现在原来的条款上面绕来绕去(有时还吃力不讨好),还不如重写一份。这就是所谓的“重构”。于是程序员间就流传着这样一句话:“所有的架构,不管前面的设计多么精妙,过了两三年后就必须重构。”
老实说,我并不觉得这种自我更新,自我否定的过程不好。人类科学技术的进步就是一个不断否定自我的过程,但问题是由这种颠覆所带来的冲击,人们接受得过来吗?举个例子,如果有朝一日有人告诉你,支付宝的支付功能其实是有bug的,到了某一天,这个bug发作,所有人的帐号资金都不安全。你接受得了吗?或者换个说法,如果现在的移动支付这么方便的前提是,我们有支付宝。一旦没了支付宝,人们受得了吗?哦!还好,我们还有微信支付。
很多东西重复做“两遍”,未必不是好事。当出现危机的时候,有替代方案总比什么都没有好。所以个人觉得微服务对于传统开发理念的冲击在于如果我们把某个功能做得足够小了,我们开发替代方案的成本就会变小。更退一步来说,每个微服务都不是不可替代,替代方案随时都准备着。微服务+服务治理(降级、融断)这套组合拳,可以给这个想法提供技术支撑。
很多时候,开发一个模块,会发现业务之间的牵连甚广, 例如订单模块需要调用库存模块,库存模块又会调用商品模块。一级级往下调,就会形成所谓的“扇出”现象。只要模块拆得足够小,这种现象越严重,一旦其中一个模块出问题,整个调用链就会崩溃。有时不光是调用失败的问题,如果调用的过程进行了一些状态的修改(例如库存占用),还要想擦屁股的问题。于是,越复杂的系统,处理故障的难度就越大。到最后,只能人工修复。久而久之,程序员大部分时间就被浪费在救火上面。
能不能逆转这个思维呢?为什么会牵一发动全身呢?有没有一些更“温柔”的方案。在我看来,应该是有的。最好的办法,就是把每个模块对整个调用的影响,限制在本模块内。
举个简单的例子,你去银行存款,柜台小姐发现验钞机出问题,但前期的打印单据,盖章什么的都做好了,就差最后一步。柜台小姐应该怎么对你说呢?对不起,存款失败吗?妈呀!都坐在那快一个小时了,资料什么都提交了一大堆,你才告诉我存款失败,这不是逼我骂娘吗?
所以更合理的做法是,柜台小姐钱照收,然后在银行的系统中记录一个异常。并告知你,虽然存款成功了,但由于验钞机的原因,系统有权力划走你存入的部分,并在帐款出现异常时,通知你进行补办。这样,用户体验明显舒服多了。因为所有由异常导致的问题都限制在合理的范围内。
采用微服务架构,还有一点也很重要,就是系统的上线和异常响应速度必须把过去的做法要快得多。否则,所有的东西都是扯蛋。因此微服务搭配容器技术,实现devops是必须的。光谈微服务,不谈如何快速部署的,都是耍流氓。
最后附上示例代码github地址:https://github.com/uniqueleon/spring-cloud-demo