在基于spring cloud的微服務(wù)開(kāi)發(fā)過(guò)程中,很多人一定碰到了這樣的困擾,當(dāng)發(fā)現(xiàn)部署到服務(wù)器上(開(kāi)發(fā)或者測(cè)試)的某個(gè)服務(wù)存在bug時(shí),希望本地啟動(dòng)另外一個(gè)實(shí)例,通過(guò)斷點(diǎn)調(diào)試去排除問(wèn)題,這個(gè)時(shí)候通過(guò)網(wǎng)關(guān)進(jìn)來(lái)去操作時(shí),網(wǎng)關(guān)無(wú)法保證一定路由到本地的微服務(wù),以往的操作,就是去服務(wù)器上通過(guò)kill的方式殺掉服務(wù),這樣就特別麻煩。本文通過(guò)一種官方推薦的方式更加方便的控制服務(wù)的狀態(tài)。
spring boot: 2.1.3.RELEASE
配置文件,主要是要打開(kāi)該端點(diǎn)
management:
endpoints:
web:
exposure:
include: 'service-registry'
使用方法
上線 http://ip+port/actuator/service-registry?status=UP
下線 http://ip+port/actuator/service-registry?status=DOWN
在eureka的管理頁(yè)面就能查看到服務(wù)的狀態(tài),接下來(lái)通過(guò)一個(gè)簡(jiǎn)單的方式,通過(guò)一個(gè)UI頁(yè)面去操作。
操作上下線
點(diǎn)擊服務(wù)列表后面的 UP / DOWN,提示用戶是否改變服務(wù)狀態(tài).
如果服務(wù)未暴露service-registry endpoint,彈出提示該服務(wù)未開(kāi)啟端點(diǎn).
刷新服務(wù)列表
強(qiáng)制刷新,刷新數(shù)據(jù)庫(kù)數(shù)據(jù)
創(chuàng)建服務(wù)
創(chuàng)建一個(gè)eureka client服務(wù)注冊(cè)到eureka
pom.xml內(nèi)容
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<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.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>LATEST</version>
</dependency>
</dependencies>
application.yml配置
server:
port: 11111
eureka:
client:
service-url:
defaultZone: http://10.0.0.182:2000/eureka/
spring:
application:
name: service-manager
datasource:
url: jdbc:mysql://localhost:3306/service_manage_dev?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password:
platform: mysql
driverClassName: com.mysql.cj.jdbc.Driver
hikari:
allow-pool-suspension: true
minimum-idle: 5 #最小空閑連接數(shù)量
idle-timeout: 180000 #空閑最大存活時(shí)間
maximum-pool-size: 10 #連接池最大連接數(shù)
auto-commit: true # 默認(rèn)自動(dòng)提交行為
pool-name: MallHikariCP # 連接池名字
max-lifetime: 1800000 #連接池中連接最大生命周期
connection-timeout: 30000 #連接超時(shí)時(shí)間
connection-test-query: SELECT 1
type: com.zaxxer.hikari.HikariDataSource
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
hibernate:
ddl-auto: update
properties:
hibernate:
show_sql: true
use_sql_comments: true
format_sql: true
實(shí)現(xiàn)邏輯
存儲(chǔ)model
@Entity
@Table(name = "service_instance")
@Data
public class ServiceInstance {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Integer id;
private String name;
private String ip;
private Integer port;
}
初始化增量更新服務(wù)列表
public class StartupRunner implements CommandLineRunner {
@Autowired
private ServiceInstanceRepository repository;
@Autowired
private EurekaClient eurekaClient;
@Override
public void run(String... args) throws Exception {
Applications applications = eurekaClient.getApplications();
List<Application> registeredApplications = applications.getRegisteredApplications();
Map<String, ServiceInstance> map = revertToMap(registeredApplications);
List<ServiceInstance> instanceList = repository.findAll();
Map<String, ServiceInstance> existMap = revertToMap2(instanceList);
map.forEach((key, value) -> {
if (!existMap.containsKey(key)) {
repository.save(value);
}
});
}
private Map<String, ServiceInstance> revertToMap2(List<ServiceInstance> instanceList) {
Map<String, ServiceInstance> map = new HashMap<>();
instanceList.forEach(instance -> {
ServiceInstance serviceInstance = new ServiceInstance();
serviceInstance.setName(instance.getName());
serviceInstance.setIp(instance.getIp());
serviceInstance.setPort(instance.getPort());
map.put(instance.getName() + instance.getIp() + instance.getPort(), serviceInstance);
});
return map;
}
public Map<String, ServiceInstance> revertToMap(List<Application> applications) {
Map<String, ServiceInstance> map = new HashMap<>();
applications.forEach(application -> {
application.getInstances().forEach(instanceInfo -> {
ServiceInstance serviceInstance = new ServiceInstance();
serviceInstance.setName(application.getName());
serviceInstance.setIp(instanceInfo.getIPAddr());
serviceInstance.setPort(instanceInfo.getPort());
map.put(application.getName() + instanceInfo.getIPAddr() + instanceInfo.getPort(), serviceInstance);
});
});
return map;
}
}
獲取服務(wù)列表包含狀態(tài)
- 從數(shù)據(jù)庫(kù)獲取到所有服務(wù)
- 從eurekaClient獲取所有的服務(wù)
- 如果數(shù)據(jù)庫(kù)存在,eureka也存在,直接使用服務(wù)狀態(tài)
- 如果數(shù)據(jù)庫(kù)存在,eureka不存在,要么是服務(wù)已經(jīng)移除了,要么服務(wù)是down掉了,可以通過(guò)模擬訪問(wèn)服務(wù)的比如health端點(diǎn),看是否alive,如果alive就標(biāo)記服務(wù)為DOWN,否則UNKOWN,
@Service
public class InstanceService {
@Autowired
EurekaClient eurekaClient;
@Value("${spring.application.name}")
private String applicationName;
@Autowired
private ServiceInstanceRepository instanceRepository;
public List<ServiceVo> getServices() {
List<ServiceInstance> instanceList = instanceRepository.findAll();
Map<String, List<ServiceInstance>> sericeMap = instanceList.stream().collect(Collectors.groupingBy(ServiceInstance::getName));
Applications applications = eurekaClient.getApplications();
List<Application> registeredApplications = applications.getRegisteredApplications();
List<ServiceVo> serviceVos = new ArrayList<>();
sericeMap.forEach((serviceName, list) -> {
// 為防止把自己也down掉這里過(guò)濾掉
if (!serviceName.equals(applicationName)) {
ServiceVo serviceVo = new ServiceVo();
serviceVo.setServiceName(serviceName);
serviceVos.add(serviceVo);
Application application = registeredApplications.stream().filter(app->app.getName().equals(serviceName)).findAny().orElse(null);
serviceVo.setInstanceList(list.stream().map(instance -> {
InstanceVo instanceVo = new InstanceVo();
instanceVo.setIp(instance.getIp());
instanceVo.setPort(instance.getPort());
if (application == null) {
boolean isAlive = isAlive(instance.getIp(), instance.getPort());
instanceVo.setStatus(isAlive ? InstanceInfo.InstanceStatus.DOWN.name(): InstanceInfo.InstanceStatus.UNKNOWN.name());
} else {
List<InstanceInfo> instances = application.getInstances();
InstanceInfo instanceInfo = instances.stream().filter(in -> in.getIPAddr().equals(instance.getIp()) && in.getPort() == instance.getPort()).findAny().orElse(null);
if (instanceInfo == null) {
boolean isAlive = isAlive(instance.getIp(), instance.getPort());
instanceVo.setStatus(isAlive ? InstanceInfo.InstanceStatus.DOWN.name(): InstanceInfo.InstanceStatus.UNKNOWN.name());
} else {
instanceVo.setStatus(instanceInfo.getStatus().name());
}
}
return instanceVo;
}).collect(Collectors.toList()));
}
});
return serviceVos;
}
/**
* 探知服務(wù)狀態(tài)
* @param ip
* @param port
* @return
*/
private boolean isAlive(String ip, int port) {
return true;
}
}
更改服務(wù)狀態(tài)接口
@PostMapping("/service/status")
public void changeServiceStatus(@RequestBody InstanceVo instanceVo) {
String url = "http://" + instanceVo.getIp() + ":" + instanceVo.getPort() + "/actuator/service-registry?status=" + instanceVo.getStatus();
HashMap<String, Object> hashMap = new HashMap<>();
ResponseEntity<Object> resp = restTemplate.postForEntity(url, hashMap, Object.class);
}
thymeleaf 源碼 (jquery + bootstrap)
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="../../favicon.ico"/>
<title>服務(wù)上下線管理</title>
<!-- Bootstrap core CSS -->
<link href="../static/css/bootstrap.css" rel="stylesheet" th:href="@{/css/bootstrap.css}"/>
<style>
.status {
cursor: pointer;
font-weight: 500;
text-decoration: underline;
}
.up {
color: green;
}
.down {
color: red;
}
</style>
</head>
<body>
<div class="container">
<div class="page-header">
<h1><button type="button" class="btn btn-primary" onclick="refresh()">刷新服務(wù)列表</button></h1>
</div>
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">服務(wù)名</th>
<th scope="col">服務(wù)列表</th>
</tr>
</thead>
<tbody>
<tr th:each="service, idx : ${services}">
<th scope="row" th:text="${idx.count}"></th>
<td th:text="${service.serviceName}"></td>
<td>
<p th:each="instance : ${service.instanceList}">
<span><a target="_blank" th:href="@{'http://' + ${instance.ip}+':'+${instance.port}+'/actuator/health'}">http://[[${instance.ip}]]:[[${instance.port}]]</a></span>
<span th:text="${instance.status}" th:if="${!instance.status.equals('UP')}" th:ip="${instance.ip}"
th:port="${instance.port}"
class="status down" onclick="changeStatus(this)"></span>
<span th:text="${instance.status}" th:if="${instance.status.equals('UP')}" th:ip="${instance.ip}"
th:port="${instance.port}" th:status="${instance.status}"
class="status up" onclick="changeStatus(this)"></span>
</p>
</td>
</tr>
</tbody>
</table>
</div> <!-- /container -->
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="../static/js/bootstrap.js" th:src="@{/js/bootstrap.js}"></script>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script>
$(document).ready(function () {
})
function changeStatus(obj) {
var ip = $(obj).attr('ip');
var port = $(obj).attr('port');
var status = $(obj).attr('status');
if (confirm("確定要更改服務(wù)狀態(tài)嗎?")) {
$.post({
url: 'service/status',
dataType: "json",
type: 'post',
contentType: "application/json",
data: JSON.stringify({"ip": ip, "port": port, "status": status === 'UP' ? 'DOWN' : 'UP'}),
success: function () {
$(obj).attr('status', status === 'UP' ? 'DOWN' : 'UP');
},
error: function(res) {
if (res.status !== 200) {
if (res.responseJSON.message && res.responseJSON.message.includes('404')) {
alert('沒(méi)有開(kāi)啟下線服務(wù)的endpoint,無(wú)法操作')
}
} else {
$(obj).attr('status', status === 'UP' ? 'DOWN' : 'UP');
}
}
})
}
}
function refresh() {
$.post({
url: 'service/refresh',
dataType: "json",
type: 'post',
contentType: "application/json",
success: function () {
setTimeout(window.location.reload(), 400)
}
})
}
</script>
</body>
</html>