一、服務發現架構
服務發現架構通常具有下面 4 個概念:
- 服務註冊:服務如何使用服務發現代理進行註冊?
- 服務地址的客戶端查詢:服務客戶端查詢服務資訊的方法是什麼?
- 資訊共享:如何跨節點共享服務資訊?
- 健康監測:服務如何將它的健康資訊傳回給服務發現代理?
下圖展示了這 4 個概念的流程,以及在服務發現模式實現中通常發生的情況:
通常服務例項都只向一個服務發現例項註冊,服務發現例項之間再通過資料傳輸,讓每個服務例項註冊到所有的服務發現例項中。
服務在向服務發現例項註冊後,這個服務就能被服務消費者呼叫了。服務消費者可以使用多種模型來"發現"服務。
- 每次呼叫服務時,通過服務發現層來獲取目標服務地址並進行呼叫。這種用的比較少,弊端較多。首先是每次服務呼叫都通過服務發現層來完成,耗時會比直接呼叫高。最主要的是這種方法很脆弱,消費端完全依賴於服務發現層來查詢和呼叫服務。
- 更健壯的方法是使用所謂的客戶端負載均衡。
如下圖所示:
在這個模型中,當服務消費者需要呼叫一個服務時:
(1)聯絡服務發現層,獲取所請求服務的所有服務例項,然後放到本地快取中。
(2)每次呼叫該服務時,服務消費者從快取中取出一個服務例項的位置,通常這個'取出'使用簡單的複製均衡演算法,如“輪詢”,“隨機",以確保服務呼叫分佈在所有例項之間。
(3)客戶端將定期與服務發現層進行通訊,並重新整理服務例項的快取。
(4)如果在呼叫服務的過程中,服務呼叫失敗,那麼本地快取將從服務發現層中重新整理資料,再次嘗試。
二、spring cloud 實戰
使用 spring cloud 和 Netflix Eureka 搭建服務發現例項。
1、構建 Spring Eureka 服務
eurekasvr POM 主要配置如下:
<!-- 其他依賴省略 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
applicaiton.yml 配置如下:
server:
port: 8761
eureka:
client:
#不註冊自己
register-with-eureka: false
#不在本地快取登錄檔資訊
fetch-registry: false
server:
#接受請求前的等待實際,開發模式下不要開啟
#wait-time-in-ms-when-sync-empty: 5
最後在啟動類上加入註釋@SpringBootApplication
即可啟動服務中心。服務中心管理頁面:http://localhost:8761
2、將服務註冊到服務中心
這裡我們編寫一個新服務註冊到服務中心,organizationservice:組織服務。並將上一篇的兩個服務:confsvr:配置中心服務,licensingservice:授權服務註冊到服務中心。
a、confvr 註冊
首先修改 POM 檔案:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
然後修改配置檔案 application.yml:
server:
port: 8888
eureka:
instance:
#註冊服務的IP,而不是伺服器名
prefer-ip-address: true
client:
#向eureka註冊服務
register-with-eureka: true
#拉取登錄檔的本地副本
fetch-registry: true
service-url:
#Eureka服務的位置(如果有多個註冊中心,使用,分隔)
defaultZone: http://localhost:8761/eureka/
spring:
profiles:
# 使用檔案系統來儲存配置資訊,需要設定為native
active: native
application:
name: confsvr
cloud:
config:
server:
native:
# 使用檔案來存放配置檔案,為每個應用程式提供用逗號分隔的資料夾列表
searchLocations: file:///D:/configFolder/licensingservice,file:///D:/configFolder/organizationservice
最後在啟動類加入註解@EnableDiscoveryClient
,啟動即可在 eureka 管理頁面發現。
b、licensingservice 註冊
首先修改 POM
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
然後修改配置檔案 bootstrap.yml
spring:
application:
#指定名稱,以便spring cloud config客戶端知道查詢哪個配置
name: licensingservice
profiles:
#指定環境
active: dev
cloud:
config:
#設為true便會自動獲取從配置中心獲取配置檔案
enabled: true
eureka:
instance:
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka/
最後在啟動類加入註解@EnableDiscoveryClient
,啟動即可在 eureka 管理頁面發現本服務例項。
c、建立 organizationservice
首先在資料夾file:///D:/configFolder/organizationservice下建立兩個配置檔案:organizationservice.yml,organizationservice-dev.yml,內容分別為:
#organizationservice-dev.yml
server:
port: 10012
#organizationservice.yml
spring:
application:
name: organizationservice
主要 POM 配置如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
然後修改配置檔案,bootstrap.yml
spring:
application:
#指定名稱,以便spring cloud config客戶端知道查詢哪個配置
name: organizationservice
profiles:
#指定環境
active: dev
cloud:
config:
enabled: true
eureka:
instance:
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka/
最後在啟動類加入註解@EnableDiscoveryClient
,啟動。
3、使用服務發現來查詢服務
現在已經有兩個註冊服務了,現在來讓許可證服務呼叫組織服務,獲取組織資訊。首先在 organizationservice 服務中的 controller 包中加入一個 controller 類,讓它能夠響應請求:
//OrganizationController.java
@RestController
public class OrganizationController {
@GetMapping(value = "/organization/{orgId}")
public Object getOrganizationInfo(@PathVariable("orgId") String orgId) {
Map<String, String> data = new HashMap<>(2);
data.put("id", orgId);
data.put("name", orgId + "公司");
return data;
}
}
接下來讓許可證服務通過 Eureka 來找到組織服務的實際位置,然後呼叫該介面。為了達成目的,我們將要學習使用 3 個不同的 Spring/Netflix 客戶端庫,服務消費者可以使用它們來和 Ribbon 進行互動。從最低階別到最高階別,這些庫包含了不同的與 Ribbon 進行互動的抽象封裝層次:
- Spring DiscoveryClient
- 啟用了 RestTemplate 的 Spring DiscoveryClient
- Neflix Feign 客戶端
a、使用 Spring DiscoveryClient
該工具提供了對 Ribbon 和 Ribbon 中快取的註冊服務最低層次的訪問,可以查詢通過 Eureka 註冊的所有服務以及這些服務對應的 URL。
首先在 licensingservice 的啟動類中加入@EnableDiscoveryClient
註解來啟用 DiscoveryClient 和 Ribbon 庫。
然後在 service 包下建立 OrganizationService.java
@Service
public class OrganizationService {
private static final String SERVICE_NAME = "organizationservice";
private DiscoveryClient discoveryClient;
@Autowired
public OrganizationService(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
/**
* 使用Spring DiscoveryClient查詢
*
* @param id
* @return
*/
public Organization getOrganization(String id) {
RestTemplate restTemplate = new RestTemplate();
List<ServiceInstance> instances = discoveryClient.getInstances(SERVICE_NAME);
if (instances.size() == 0) {
throw new RuntimeException("無可用的服務");
}
String serviceUri = String.format("%s/organization/%s", instances.get(0).getUri().toString(), id);
ResponseEntity<Organization> responseEntity = restTemplate.exchange(serviceUri, HttpMethod.GET
, null, Organization.class, id);
return responseEntity.getBody();
}
}
接著在 controller 包中新建 LicensingController.java
@RestController
public class LicensingController {
private OrganizationService organizationService;
@Autowired
public LicensingController(OrganizationService organizationService) {
this.organizationService = organizationService;
}
@GetMapping("/licensing/{orgId}")
public Licensing getLicensing(@PathVariable("orgId") String orgId) {
Licensing licensing = new Licensing();
licensing.setValid(false);
licensing.setOrganization(organizationService.getOrganization(orgId));
return licensing;
}
}
啟動所有專案,訪問localhost:10011/licensing/12,可以看到返回如下結果:
{
"organization": {
"id": "12",
"name": "12公司"
},
"valid": false
}
在實際開發中,基本上是用不到這個的,除非是為了查詢 Ribbon 以獲取某個服務的所有例項資訊,才會直接使用。如果直接使用它存在以下兩個問題:
- 沒有利用 Ribbon 的客戶端負載均衡
- 和業務無關的程式碼寫得太多
b、使用帶 Ribbon 功能的 Spring RestTemplate 呼叫服務
這種方法是較為常用的微服務通訊機制之一。要啟動該功能,需要使用 Spring Cloud 註解@LoadBanced 來定義 RestTemplate bean 的構造方法。方便起見直接在啟動類中定義 bean:
#LicensingserviceApplication.java
@SpringBootApplication
@EnableDiscoveryClient //使用不帶Ribbon功能的Spring RestTemplate,其他情況下可刪除
public class LicensingserviceApplication {
/**
* 使用帶有Ribbon 功能的Spring RestTemplate,其他情況可刪除
*/
@LoadBalanced
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(LicensingserviceApplication.class, args);
}
}
接著 service 包下增加一個類:OrganizationByRibbonService.java
@Component
public class OrganizationByRibbonService {
private RestTemplate restTemplate;
@Autowired
public OrganizationByRibbonService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public Organization getOrganizationWithRibbon(String id) {
ResponseEntity<Organization> responseEntity = restTemplate.exchange("http://organizationservice/organization/{id}",
HttpMethod.GET, null, Organization.class, id);
return responseEntity.getBody();
}
}
最後就是在 LicensingController.js 中加一個訪問路徑:
//不要忘記注入OrganizationByRibbonService服務
@GetMapping("/licensingByRibbon/{orgId}")
public Licensing getLicensingByRibbon(@PathVariable("orgId") String orgId) {
Licensing licensing = new Licensing();
licensing.setValid(false);
licensing.setOrganization(organizationService.getOrganization(orgId));
return licensing;
}
}
訪問localhost:10011/licensingByRibbon/113,即可看到結果。
c、使用 Netflix Feign 客戶端呼叫
Feign 客戶端是 Spring 啟用 Ribbon 的 RestTemplate 類的替代方案。開發人員只需定義一個介面,然後使用 Spring 註解來標註介面,即可呼叫目標服務。除了編寫介面定義無需編寫其他輔助程式碼。
首先啟動類上加一個@EnableFeignClients
註解啟用 feign 客戶端。然後在 POM 中加入 Feign 的依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
然後在 client 包下新建 OrganizationFeignClient.java
@FeignClient("organizationservice")//使用FeignClient註解指定目標服務
public interface OrganizationFeignClient {
/**
* 獲取組織資訊
*
* @param orgId 組織id
* @return Organization
*/
@RequestMapping(method = RequestMethod.GET, value = "/organization/{orgId}", consumes = "application/json")
Organization getOrganization(@PathVariable("orgId") String orgId);
}
最後修改LicensingController.java
,加入一個路由呼叫 Feign。
//注入OrganizationFeignClient,使用構造注入
@GetMapping("/licensingByFeign/{orgId}")
public Licensing getLicensingByFeign(@PathVariable("orgId") String orgId) {
Licensing licensing = new Licensing();
licensing.setValid(false);
licensing.setOrganization(organizationFeignClient.getOrganization(orgId));
return licensing;
}
訪問localhost:10011/licensingByFeign/11313,即可看到結果。
總結
這一節磨磨蹭蹭寫了好幾天,雖然例子很簡單,但是相信應該是能夠看懂的。由於篇幅原因程式碼沒有全部貼上,想要檢視完整程式碼,可以訪問這個連結:點選跳轉。