1、SpringCloud介紹
相信也不用再多介紹,近幾年最火爆的全家桶式微服務框架。這套框架裏面已經囊括了微服務的註冊與發佈,服務的自動發現與治理,負載均衡與路由,服務降級與熔斷,分佈式配置中心,網關服務,消息總線等等一大堆子項目。(截止本文編寫時間爲止,spring cloud的二級子項目數已經達到了喪心病狂的31個)。所有你需要的、不需要的,這裏都應有盡有。
這裏並不想一一羅列spring cloud下面的所有子項目的功能的描述,結合國內企業項目的實際需求,這裏只介紹幾個個人認爲使用頻率最高的幾個子項目:
- spring cloud Netflix 。spring cloud 與 netflix的整合。該子項目提供了幾個重要的功能:服務發現(Eureka),線路熔斷器(Hytrix),聲明式RESTFUL客戶端(Feign),客戶端負載均衡器(Ribbon),路由與過濾(zuul)
- Spring cloud config。分佈式配置中心服務
- Spring cloud bus。分佈式消息總線
- Spring cloud Gateway。spring cloud的網關服務,提供外端請求到內部微服務的路由,熔斷,過濾,請求速率限制等功能。
- Spring cloud Security。顧名思義,與安全相關,提供Oauth2協議支持
- Spring cloud Task。spring cloud 任務框架
- spring cloud openfeign。 聲明式的restful客戶端
後面將會逐一介紹這些框架的使用方法,優缺點,如何進行整合。
2、Spring cloud 的服務註冊中心(Eureka)
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的更多的細節。將在下一節介紹
2、spring cloud 分佈式配置中心
在分佈式環境裏,應用的配置往往是比較麻煩的。如果統一在編譯打包階段,設置好所有的配置參數(如在配置文件中配置),這對一些需要動態更新的配置項非常不友好,而且隨着項目規模的變大,需要管理的配置也會越來越多,配置文件的維護也會變得越來越困難。
如果放在統一的外部配置服務中心,也會遇到諸如服務瓶頸,單點失效,應用之間的數據一致性等問題。於是開源世界就湧現了一些分佈式配置中心方案,常見的有阿里的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的缺點也是顯然。例如,不能對配置文件進行有效的拆分與管理,沒有直觀的圖形化界面進行配置,依賴於版本管理工具。
3、使用OpenFegin進行微服務調用
微服務(microservice)這個詞雖然大家聽得耳根都爛了,但是不同人對於微服務,還是有不同的見解。有人認爲微服務其實就是新瓶裝舊酒,其本則還是服務封裝,重用代碼。有人認爲是進行更細粒度的項目拆分。總之,就是一百個人就有一百個哈姆雷特。我個人認爲,微服務本身並沒有帶來什麼技術上的變革,只是人們思維方式的一種轉變。就好像幾百年前牛頓告訴咱們,時間是恆定的,空間是均勻的一樣。但現在我們都知道牛頓是錯的。
過去我們搭服務,更看重的是每個服務之間的協同工作,每個服務就像機器裏面的零件一樣,必須整齊劃一,互相之間有嚴格的接口規範與定義,相互之間不可替代。但我眼中的微服務就不一樣,它打破了這種定式。爲什麼我要這麼說?大家看下面的例子,就可以明白了。
下面繼續前面的例子,我創建一個新的測試項目中,先在POM文件中聲明一些依賴。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>org.aztec</groupId>
<artifactId>spring-client2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-client2</name>
<url>http://maven.apache.org</url>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<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>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置啥的,沒有什麼特點,都是對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項目。這個子項目極大的簡化了開發流程。
我們先在新項目引入一些依賴。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>org.aztec</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-demo</name>
<description>Demo project for Spring Boot</description>
<packaging>jar</packaging>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
<sharding-sphere.version>3.0.0.M2</sharding-sphere.version>
</properties>
<dependencies>
<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency> -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-client</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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<exclusions>
<exclusion>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.44</version>
</dependency>
<dependency>
<groupId>io.shardingsphere</groupId>
<artifactId>sharding-jdbc</artifactId>
<version>3.0.0.M3</version>
</dependency>
<!-- <dependency>
<groupId>io.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-sphere.version}</version>
</dependency> -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
</dependency>
<dependency>
<groupId>com.baidu.disconf</groupId>
<artifactId>disconf-client</artifactId>
<version>2.6.36</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
這個項目雖然亂入了很多別的東西(這在後面系列文章將陸續介紹),但沒有看到對前面兩個服務端項目的依賴,核心還是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負載均衡器已經發揮作用。調用會隨機地分發到兩個不同的服務中。
4、小結
自至,關於使用spring cloud 進行快速微服務開發的入門級介紹到這爲止了。後面還會陸續更新與其它第三方組件的整合方案,以供各位看客大大參考。
通過上面的例子,可以很清晰看到微服務具備下面幾個很重要的特徵
- 細粒度,松耦合,有清晰的業務邊界
- 能對服務進行有效治理,追蹤調用鏈
- 沒有不可替代性
- 不會擴散影響
- 上線快速,替換容易
以上特徵中,我主要想談談3-5點
4-1 弱化不可替代性
軟件工程方法論經常把一個大型的軟件工程項目拆分成很多個不同的部件,這是受其它工程技術的啓發。把軟件項目可管理的邏輯單元定義爲“部件”,這最重要的意義在於可以進行替換。就像一臺汽車,如果每一個零件都是焊死的話,那麼一旦某一個零件出問題,整臺車都會廢掉。發明“可替換部件”技術的意義在於,當某一個零部件損壞了,能馬上找到相同的零件進行替換。軟件其實也一樣,當使用的時間長了,一樣會變“壞”。但軟件部件的“壞”通常不是發自自身的,更多的是來源於業務上的壓力。所以當部件發生了故障,代表着的可能不僅是替換,還可能是重構。我們經得起這樣的拆騰嗎?
我經常覺得寫代碼(特別是底層框架代碼)更像是寫一份合同,如果合同條款清晰,所有情況都分析得清清楚楚,這代碼很好寫。但事實上,做得久了會發現,越清晰的合同,後面越難改。因爲你會發現,你之前做的所有工作都是基於之前已有的條款,一旦這樣的假定被打破,就必須在原來的基礎上修修補補。久了就會發現在原來的條款上面繞來繞去(有時還喫力不討好),還不如重寫一份。這就是所謂的“重構”。於是程序員間就流傳着這樣一句話:“所有的架構,不管前面的設計多麼精妙,過了兩三年後就必須重構。”
老實說,我並不覺得這種自我更新,自我否定的過程不好。人類科學技術的進步就是一個不斷否定自我的過程,但問題是由這種顛覆所帶來的衝擊,人們接受得過來嗎?舉個例子,如果有朝一日有人告訴你,支付寶的支付功能其實是有bug的,到了某一天,這個bug發作,所有人的帳號資金都不安全。你接受得了嗎?或者換個說法,如果現在的移動支付這麼方便的前提是,我們有支付寶。一旦沒了支付寶,人們受得了嗎?哦!還好,我們還有微信支付。
很多東西重複做“兩遍”,未必不是好事。當出現危機的時候,有替代方案總比什麼都沒有好。所以個人覺得微服務對於傳統開發理念的衝擊在於如果我們把某個功能做得足夠小了,我們開發替代方案的成本就會變小。更退一步來說,每個微服務都不是不可替代,替代方案隨時都準備着。微服務+服務治理(降級、融斷)這套組合拳,可以給這個想法提供技術支撐。
4-2 限制影響
很多時候,開發一個模塊,會發現業務之間的牽連甚廣, 例如訂單模塊需要調用庫存模塊,庫存模塊又會調用商品模塊。一級級往下調,就會形成所謂的“扇出”現象。只要模塊拆得足夠小,這種現象越嚴重,一旦其中一個模塊出問題,整個調用鏈就會崩潰。有時不光是調用失敗的問題,如果調用的過程進行了一些狀態的修改(例如庫存佔用),還要想擦屁股的問題。於是,越複雜的系統,處理故障的難度就越大。到最後,只能人工修復。久而久之,程序員大部分時間就被浪費在救火上面。
能不能逆轉這個思維呢?爲什麼會牽一髮動全身呢?有沒有一些更“溫柔”的方案。在我看來,應該是有的。最好的辦法,就是把每個模塊對整個調用的影響,限制在本模塊內。
舉個簡單的例子,你去銀行存款,櫃檯小姐發現驗鈔機出問題,但前期的打印單據,蓋章什麼的都做好了,就差最後一步。櫃檯小姐應該怎麼對你說呢?對不起,存款失敗嗎?媽呀!都坐在那快一個小時了,資料什麼都提交了一大堆,你才告訴我存款失敗,這不是逼我罵娘嗎?
所以更合理的做法是,櫃檯小姐錢照收,然後在銀行的系統中記錄一個異常。並告知你,雖然存款成功了,但由於驗鈔機的原因,系統有權力划走你存入的部分,並在帳款出現異常時,通知你進行補辦。這樣,用戶體驗明顯舒服多了。因爲所有由異常導致的問題都限制在合理的範圍內。
4-3 快速響應
採用微服務架構,還有一點也很重要,就是系統的上線和異常響應速度必須把過去的做法要快得多。否則,所有的東西都是扯蛋。因此微服務搭配容器技術,實現devops是必須的。光談微服務,不談如何快速部署的,都是耍流氓。
最後附上示例代碼github地址:https://github.com/uniqueleon/spring-cloud-demo