文章目錄
回顧問題
https://blog.csdn.net/ym15229994318ym/article/details/105064094在最後提到的問題,概括一下就是分佈式服務必然要面臨的問題:
1、服務管理
如何自動註冊和發現
如何實現狀態監管
如何實現動態路由
2、服務如何實現負載均衡
3、服務如何解決容災問題
4、服務如何實現統一配置
Eureka註冊中心,認識Eureka
首先我們來解決第一問題,服務的管理。
- 問題分析
在剛纔的案例中,user-service對外提供服務,需要對外暴露自己的地址。而consumer (調用者)需要記錄服務提供者的地址。將來地址出現變更,還需要及時更新。這在服務較少的時候並不覺得有什麼,但是在現在日益複雜的互聯網環境,- -個項目肯定會拆分出十幾,甚至數十個微服務。此時如果還人爲管理地址,不僅開發困難,將來測試、發佈上線都會非常麻煩,這與DevOps的思想是背道而馳的。 - 網約車
這就好比是網約車出現以前,人們出門叫車只能叫出租車。一些私家車想做出租卻沒有資格,被稱爲車。而很多人想要約車,但是無奈出租車太少,不方便。私家車很多卻不敢攔,而且滿大街的車,誰知道哪個纔是願意載人的。一個想要,一個願意給,就是缺少引子,缺乏管理啊。此時滴滴這樣的網約車平臺出現了,所有想載客的私家車全部到滴滴注冊,記錄你的車型(服務類型),身份信息(聯繫方式)。這樣提供服務的私家車,在滴滴那裏都能找到,一目瞭然。此時要叫車的人,只需要打開APP,輸入你的目的地,選擇車型(服務類型),滴滴自動安排一個符合需求的車到你面前,爲你服務,完美! - Eureka做什麼?
Eureka就好比是滴滴,負責管理、記錄服務提供者的信息。服務調用者無需自己尋找服務,而是把自己的需求告訴Eureka,然後Eureka會把符合你需求的服務告訴你。同時,服務提供方與Eureka之間通過“心跳”機制進行監控,當某個服務提供方出現問題,Eureka自 然會把它從服務列表中剔除。這就實現了服務的自動註冊、發現、狀態監控。
Eureka的原理及配置
基礎架構
Eureka架構中的三個核心角色:
- 服務註冊中心:Eureka的服務端應用,提供服務註冊和發現功能,就是剛剛我們建立的eureka-server
- 服務提供者:提供服務的應用,可以是SpringBoot應用, 也可以是其它任意技術實現,只要對外提供的Rest風格服務即可。本例中就是我們實現的user-service
- 服務消費者:消費應用從註冊中心獲取服務列表,從而得知每個服務方的信息,知道去哪裏調用服務方。本例中就是我們實現的consumer
eureka-server子服務
增加eureka-server子服務,提供一個註冊中心,讓服務提供方user-service,在eureka-server中註冊服務,服務消費者(調用)consumer在eureka-server中拉取服務,進行調用。eureka會爲我們做好負載均衡、服務管理的工作。
- 引入依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
- 使用註解
@EnableEurekaServer
開啓eureka功能
@SpringBootApplication
@EnableEurekaServer
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class);
}
}
- application改端口號,避免衝突
server:
port: 10086
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
修改user-service子服務
依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
</dependency>
<!--引入eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
application.yaml配置
server:
port: 8081
#eureka-client配置
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
# 30秒更新一次
lease-renewal-interval-in-seconds: 30
# 最小過期時長
lease-expiration-duration-in-seconds: 90
#控制日誌級別
logging:
level:
com.baidu: debug
# 數據庫連接信息
spring:
application:
name: user-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring_study_db?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
username: root
password: admin
#配置mybatis的信息
mybatis:
#pojo別名掃描
type-aliases-package: com.baidu.user.pojo
#加載mybatis映射文件,使用通用Mapper這個就不用了
#mapper-locations: classpath:mapper/*mapper.xml
啓動類加上@EnableDiscoveryClient
註解
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.baidu.user.mapper")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class);
}
}
修改consumer-service服務
ribbon(一會就說)、依賴
<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>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
</dependencies>
application.yaml
server:
port: 8082
spring:
application:
name: consumer-server
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
根據設置的eureka端口,進入頁面,可以看見服務都已經註冊上去了。
controller
此時服務的消費方再要去調用時,就不會出現把url寫死的的情況了,而是動態的向eureka服務註冊中心去拉取服務進行調度。
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
/**
* 遠程查詢
* @param id
* @return
*/
@GetMapping("{id}")
public User findById(@PathVariable("id") Integer id) {
//DiscoveryClient方式
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
//根據實例中的id取出ip和端口
ServiceInstance instance = instances.get(0);
String path = instance.getHost() + ":" + instance.getPort();
String url = "http://" + path + "/user/findById/" + id;
System.out.println("url:" + url);*/
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
這裏的代碼雖然解決了這個問題,但是再思考一下?這裏的eureka服務、user-service、consumer-service都是註冊了一個,如果其中一個服務宕機了,那麼就會調用失敗,當然這裏也包括註冊中心服務宕機的情況。
這裏只有一個user-service服務,所以只獲取第一個即可 , 所以是 .get(0);
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
ServiceInstance instance = instances.get(0);
如果有多個,是不是就要考慮負載均衡問題了呢?
高可用的eureka配置、服務複製
高可用的Eureka Server
Eureka Server即服我的註冊中心,在上面的案例中,我們只有一個EurekaServer, 事實上EurekaServer也可以是一個集羣,形成高可用的Eureka中心。
服務同步
多個Eureka Server之間也會互相註冊爲服務,當服務提供者註冊到Eureka Server集羣中的某個節點時,該節點會把服務的信息同步給集羣中的每個節點,從而實現數據同步。因此,無論客戶端訪問到Eureka Server集羣中的任意一個節點,都可以獲取到完整的服務列表信息。
配置如下:
server:
# 本註冊中心端口號
port: 10086
spring:
application:
name: eureka-server
eureka:
client:
service-url:
# 註冊到註冊中心端口號爲10087
defaultZone: http://127.0.0.1:10087/eureka
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
在idea中,我們可以快速的複製一個相同的服務,只需要通過修改端口號即可區分。(同一個服務,不同端口號,這些服務乾的事同一件事,當其中一個宕機時,不至於一直處於等待,回去找其他服務)
此時我們的服務就如同下面所示。當然要記得改你複製的服務的端口號。(我一般是先直接複製,再改原來的那個服務端口號,效果一樣)
由於現在存在兩個註冊中心,那麼在配置服務時就要都註冊進去。
配置如下:
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
此時我們有多個服務,調用時也有了變化!因爲我們要考慮負載均衡問題了,
- 負載均衡
在剛纔的案例中,我們啓動了一個user-service,然後通過DiscoveryClient來獲取服務實例信息,然後獲取ip和端口來訪問。但是實際環境中,我們往往會開啓很多個user-service的集羣。此時我們獲取的服務列表中就會有多個,到底該訪問哪一個呢?一般這種情況下我們就需要編寫負載均衡算法,在多個實例列表中進行選擇。不過Eureka中已經幫我們集成了負載均衡組件: Ribbon, 簡單修改代碼即可使用。 - 什麼是Ribbon:
Ribbon是Netflix發佈的負載均衡器,它有助於控制HTTP和TCP客戶端的行爲。爲Ribbon配置服務提供者地址列表後,Ribbon 就可基於某種負載均衡算法,自動地幫助服務消費者去請求。Ribbon默認爲我們提供了很多的負載均衡算法,例如輪詢、隨機等。當然,我們也可爲Ribbon實現自定義的負載均衡算法。
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private RibbonLoadBalancerClient client;
/**
* 遠程查詢
*
* @param id
* @return
*/
@GetMapping("{id}")
public User findById(@PathVariable("id") Integer id) {
//RibbonLoadBalancerClient方式
ServiceInstance instance=client.choose("user-service");
String path = instance.getHost() + ":" + instance.getPort();
String url = "http://" + path + "/user/findById/" + id;
System.out.println("url:" + url);
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
其實還提供了一種註解的簡便方式,在方法上添加 @LoadBalanced
註解,其實內部實現和上面一樣。
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class);
}
}
//=============================================================================
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
/**
* 遠程查詢
* @param id
* @return
*/
@GetMapping("{id}")
public User findById(@PathVariable("id") Integer id) {
//主配置類中加了@LoadBalanced註解,底層會有攔截器解析服務id,進行負載均衡
String url="http://user-service/user/findById/"+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
測試結果
啓動所有服務。
查看端口號爲10086的註冊中心服務列表
查看端口號爲10087的註冊中心服務列表
eureka相關補充
Eureka客戶端
服務提供者要向EurekaServer註冊服務,並且完成服務續約等工作。
-
服務註冊
服務提供者在啓動時,會檢測配置屬性中的: eureka.cl ient . register -with-eruekawtrue參數是否正確,事實上默認就是true。如果值確實爲true,則會向EurekaServer發起一個Rest請求, 並攜帶自己的元數據信息,Eureka Server會把這些信息保存到一個雙層Map結構中。
●第一層Map的Key就是服務id,一般是配置中的spring.applicat ion.name屬性
●第二層Map的key是服務的實例id。 一般host+ serviceld + port,例如: locahost :user -service:8081
●值則是服務的實例對象,也就是說一個服務,可以同時啓動多個不同實例,形成集羣。 -
服務續約
在註冊服務完成以後,服務提供者會維持一個心跳(定時向EurekaServer發起Rest請求) ,告訴EurekaServer:“我還活着”。這個我們稱爲服務的續約(renew) ;有兩個重要參數可以修改服務續約的行爲:
cureka :
lnstance:
lease-expiration-durat ion- in-seconds: 90
lease-renewal- interval- in- seconds: 30
lease-renewal-interval-in-seconds: 服務續約(renew)的間隔,默認爲30秒
lease expiration-duration-in-seconds: 服務失效時間,默認值90秒
也就是說,默認情況下每個30秒服務會向註冊中心發送一次心跳, 證明自己還活着。如果超過90秒沒有發送心跳,EurekaServer就會認爲該服務宕機, 會從服務列表中移除,這兩個值在生產環境不要修改,默認即可。
- 獲取服務列表
當服務消費者啓動是,會檢測eureka.client. fetch- registry-true參數的值,如果爲true,則會從Eureka Server服務的列表只讀備份,然後緩存在本地。並且每隔30秒會重新獲取並更新數據。我們可以通過下面的參數來修改:
eureka:
client:
registry- fetch- interval- seconds: 30
失效剔除和自我保護
- 服務下線
當服務進行正常關閉操作時,它會觸發一個服務下線的REST請求給Eureka Server,告訴服務註冊中心:“我要下線了”。服務中心接受到請求之後,將該服務置爲下線狀態。 - 失效剔除
有時我們的服務可能由於內存溢出或網絡故障等原因使得服務不能正常的工作,而服務註冊中心並未收到"服務下線”的請求。相對於服務提供者的"服務續約”操作,服務註冊中心在啓動時會創建一個定時任務,默認每隔一段時間(默認爲60秒)將當前清單中超時(默認爲90秒)沒有續約的服務剔除,這個操作被稱爲失效剔除。可以通過eureka. server. eviction- interval- timer- in-ms參數對其進行修改,單位是毫秒。 - 自我保護
我們關停一個服務,就會在Eureka面板看到一條警告:這是觸發了Eureka的自我保護機制。當服務未按時進行心跳續約時,Eureka會 統計服務實例最近15分鐘心跳續約的比例是否低於了85%。在生產環境下,因爲網絡延遲等原因,心跳失敗實例的比例很有可能超標,但是此時就把服務剔除列表並不妥當,因爲服務可能沒有宕機。Eureka在這段時間內不會剔除任何服務實例,直到網絡恢復正常。生產環境下這很有效,保證了大多數服務依然可用,不過也有可能獲取到失敗的服務實例,因此服務調用者必須做好服務的失敗容錯。我們可以通過下面的配置來關停自我保護:
eureka:
server :
enable-self-preservation: false #關閉自我保護模式(缺省爲打開)