以服务网格(Service Mesh)方式整合现有服务到微服务(基于SpringCloud)架构中

为什么要以服务网格(Service Mesh)方式整合现有服务

转眼间来到了2019年,新年新的开始,来一起看看2018年还没有实现的愿望吧。作为一个SpringCloud的拥趸者,一直在心头惴惴不安的就是如何让SpringCloud的微服务架构一统天下,实现所有服务在微服务架构下的大同。
话说回来,为啥要以网格服务(Service Mesh)方式来整合现有服务呢?如果您正在使用SpringCloud构建自己的微服务架构,您可能会使用Eruka Server作为服务注册中心,使用Eureka Client进行服务注册,最后使用Ribbon进行服务调用(如下图1 服务治理机制),这些组件使用起来都非常简单易用,能使我们迅速搭建起微服务架构的应用。但在公司的架构演变过程中全部在短时间内使用基于SpringCloud进行微服务化显然是不现实,需要我们将现有服务也按照微服务架构的思想进行整合,进而逐渐下线旧服务。还有就是公司部分服务使用非JAVA语言构建(如Python),在短时间将这些服务改造成基于SpringCloud的微服务显然是不现实的,也是不符合架构演进的原则的,或者这些非JAVA服务已经很稳定无需专门进行微服务化的改造,这样同样需要采用微服务架构的思想整合这些服务。
服务治理
说到这里,您可能会问这该如何整合?如果您研究过Eureka,你可能会直接在现有服务中实现使用Eureka API进行服务注册进而给给其他系统或服务提供服务,当然还需在现有服务中实现一个类似ribbon调用客户端调用所有在Eureka注册中心已经注册的服务,这样一来显示是对现有服务改动太大了,那我们是不是能将服务注册和服务调用从现有服务中抽离出一个代理服务呢?答案是肯定。这就是提出网格服务的根本出发点,此时我们将这个代理服务叫做Sidecar。在服务网格中Sidecar还扩展了重试/超时、监控、追踪等功能。

什么是服务网格ServiceMesh

先来看下Willian Morgan的官宣说法:

服务网格(Service Mesh)是致力于解决服务间通讯的基础设施层。它负责在现代云原生应用程序的复杂服务拓扑来可靠地传递请求。实际上,Service Mesh 通常是通过一组轻量级网络代理(Sidecar proxy),与应用程序代码部署在一起来实现,而无需感知应用程序本身。

服务网格的特点

  • 应用程序间通讯的中间层
  • 轻量级网络代理
  • 应用程序无感知
  • 解耦应用程序的重试/超时、监控、追踪和服务发现

理解服务网格(Service Mesh)

如果用一句话来解释什么是 Service Mesh,可以将它比作是应用程序或者说微服务间的 TCP/IP,负责服务之间的网络调用、限流、熔断和监控。对于编写应用程序来说一般无须关心 TCP/IP 这一层(比如通过 HTTP 协议的 RESTful 应用),同样使用 Service Mesh 也就无须关系服务之间的那些原来是通过应用程序或者其他框架实现的事情,比如 Spring Cloud、OSS,现在只要交给 Service Mesh 就可以了。

服务网格架构

服务网格架构
Service Mesh 作为 sidecar 运行,对应用程序来说是透明,所有应用程序间的流量都会通过它,所以对应用程序流量的控制都可以在 Service Mesh 中实现。

如何整合现有服务到微服务架构中

对于整合现有服务,在理解了服务网格(Service Mesh)架构(如图2 服务网格架构)的基础上,我们也可以在现基于SpringCloud的微服务架构中,通过使用服务网格一样的运行方式(即Sidecar)来实现现有服务的注册与调用(即服务治理),如图3 。
服务治理使用Sidecar

什么是Sideca

在服务网格中,已经有LinkerdEnvoyIstioConduitnginMesh等开源项目,但这些均不能直接将服务注册到Eureka Server中,所以需要用SpringCloud的方式生成运行Sidecar。
在基于SpringCloud的微服务中,有Spring Cloud Netflix Sidecar,它包含一个简单的http api去获取一个已知服务的所有实例(例如主机和端口)。你也可以通过嵌入的Zuul代理(Zuul中有一个代理功能)对代理的服务进行调用,Zuul从Eureka服务注册中心获取所有的路由记录(route entries)。通过host发现(host lookup)或者Zuul代理可以直接访问Spring Cloud Config。非jvm需要应该实现一个健康检查,Sidecar能够以此来报告给Eureka注册中心该应用是up还是down状态。总之,Sidecar是作为一个代理的服务来间接性的让其他语言可以使用Eureka等相关组件。通过与Zuul的来进行路由的映射,从而可以做到服务的获取,然后可以使用Ribbon,Feign对服务进行消费,以及对Config Server的间接性调用。

准备Sidecar应用

为了更好的理解Sidecar,我们在这里单独起一个应用叫作Sidecar,此应用只用作给其他非JVM服务做代理。构建Sidecare应用,需要添加以下依赖,如下

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-sidecar</artifactId>
<version>1.2.4.RELEASE</version><!--具体版本可自选-->
</dependency>

启用Sidecar,创建一个Spring Boot应用程序,并在在应用主类上加上@EnableSidecar注解。该注解包含@EnableCircuitBreaker, @EnableDiscoveryClient以及@EnableZuulProxy,将Sidecar和非JVM服务部署在同一台机器上(也可以部署在同一机器上,但为了好管理和理解,最好部署在同一台机器上)。
配置Sidecar,在application.yml中添加sidecar.portsidecar.health-urisidecar.port属性是非jre程序监听的端口号,这就是Sidecar可以正确注册应用到Eureka的原因。sidecar.health-uri是非jre应用提供的一个对外暴露的可访问uri地址,在该地址对应的接口中需要实现一个模仿Spring Boot健康检查指示器的功能。它需要返回如下的json文档。(注:通过返回一个json,其用status字段来标识你的应用的服务状态,是up还是down,sidecar会将该状态报告给eureka注册中心从而实现你的服务的状态可用情况。简单的说就是用来控制sidecar代理服务的状态!)

application.yml(Sidecar配置)

server:
port: 6666 ##Sidecar注册到Eureka注册中心的端口
spring:
application:
name: sidecar ## 服务的名称,在Eureka注册中心上会显示此名称(在生产环境中,此名称最好与Sidecar所代理服务的名称保持一致)

sidecar:
port: 8000 ##Sidecar监听的非JVM服务端口
health-uri: http://localhost:8000/health ##非JVM服务需要实现该接口,[响应结果](#原有服务实现健康检查API)后面会给出注册配置

至此,Sidecar应用就准备好了,具体代码可参见 《增加github链接》。
启动Sidecar应用,观察Eureka注册中心,发现Sidecar应用已经注册到注册中心但是状态显示down,这是因为Sidecar应用代理的服务还没有

原有服务实现健康检查API

对于Sidecar来说,如果能准确的代理非JVM服务,则需要实时检查非JVM服务的健康状态并实时将结果同步到Eureka注册中心,以便服务消费方能及时准确地获取到可调用的服务,所以原有非JVM服务需要实现一个简单的健康检查API,具体json结构如下

{
"status":"UP"
}

Node.js服务

接下来我们以Node.js服务作为现有服务来模拟接入SpringCloud微服务的流程。
由于Node.js社区十分活跃,可选的Rest服务框架非常多。比较主流的有express,koa, hapi等,非常轻量易扩展的也有像connect这样的,这里笔者考虑到群众基础和文档丰富度,选择使用express来开发这样一个可以接入Spring Cloud的Rest服务。

准备Node.js服务
var express = require('express');
var utils = require("./utils");
var http = require("http");
var app = express();


// 健康检查API,用于Sidecar检查本nodejs服务的存活状态
app.get('/health', function (req, res) {
res.set("Content-Type", "application/json;charset=utf-8");
console.log(utils.getNowFormatDate() + " request: " + req.url);
var response = {
"status":"UP"
};
res.send(JSON.stringify(response));
})

// 对外服务API,用于测试其他服务调用本nodejs服务即查询当前实例的信息
app.get('/instance_info', function(req,res){
res.set("Content-Type", "application/json;charset=utf-8");
console.log(utils.getNowFormatDate() + " request: " + req.url);
var response = {
"status":0,
"msg":"sucess",
"data":{
"instance_name":"nodejs_server"
}
};
res.send(JSON.stringify(response));
})

// 查看指定服务的实例信息API,用于测试本nodejs服务调用其他已注册到Eureka上是服务
app.get('/query/:server_name/info', function(req,res){
res.set("Content-Type", "application/json;charset=utf-8");
var response = null;
var instance_name = req.params.server_name;
console.log(utils.getNowFormatDate() + " request: %s, querying server_name :" , req.url, instance_name);
// 获取服务的实例信息
var sidecarServiceUrl = "http://localhost:6666/{instance_name}/instance_info".replace('{instance_name}',instance_name);//sidecar获取服务信息API(实际Sidecar会将此请求转发到Sidecar代理的Python服务上)
http.get(sidecarServiceUrl,function(data){
var str="";
data.on("data",function(chunk){
str+=chunk;
})
data.on("end",function(){
console.log(str.toString());
response=str.toString();
})
})
res.send(response);
})

// nodejs服务启动
var server = app.listen(8000, function () {

var host = server.address().address
var port = server.address().port

console.log("应用实例,访问地址为 http://%s:%s", host, port)
})

此时启动Node.js服务,再次观察Eureka注册中心,发现之前注册到注册中心的Sidecar应用状态变为UP正常状态了,这可以说明Sidecar确实及时准确的将被代理服务的状态反映到注册中心了。换言之,注册中心显示的Sidecar应用的状态其实是被代理服务的状态。

准备用于调用Node.js服务的测试服务

为了测试Node.js服务的在基于SpringCloud的微服务中的可用性,本文选取微服务中常用的网关作为Node.js服务的调用方来测试Node.js服务的可用性。

pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.haozi</groupId>
<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>gateway</name>
<description>MService Gateway</description>

<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Camden.SR5</version>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</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-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>1.4.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
## application.yml
server:
port: 8700

spring:
application:
name: gateway
eureka:
client:
register-with-eureka: true
fetch-registry: true
serviceUrl:
defaultZone: http://center.chuangzhi8.cn:8761/eureka/
zuul:
routes:
express_servcie: #此路由的意思是:将请求路径的一级目录是/express的请求全部转发到服务id为Sidecar的服务上
path: /express/** #路由路径
serviceId: sidecar #服务id
// InfoController.java
package com.haozi.gateway.controller;

import java.util.HashMap;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 获取应用的基础信息,用于测试node.js服务或Python服务通过Sidecar来调用其他服务
*
* @author haozi
*
*/
@RestController
public class InfoController {

@GetMapping(value = { "/instance_info" })
public Map getInstance() {
Map<String, Object> retVal = new HashMap<>();
retVal.put("staus", 0);
retVal.put("msg", "sucess");
Map<String, Object> instanceInfo = new HashMap<>();
instanceInfo.put("instance_name", "JVM_Gateway");
retVal.put("data", instanceInfo);
return retVal;
}
}
测试Node.js服务在微服务中的可用性

测试其他服务通过Sidecar调用Node.js服务
通过GET请求请求http://127.0.0.1:8700/express/instance_info ,响应结果为

{
"status": 0,
"msg": "sucess",
"data": {
"instance_name": "nodejs_server"
}
}

此结果说明请求到网关应用(8700端口)的/express/instance_info请求被转发到Sidecar上了,由于Sidecar应用上并没有/instance_info接口,所以该/instance_info接口请求被转发到Node.js服务上了,也就说Node.js服务已经可以被微服务中的其他服务正常调用了。
测试Node.js服务通过Sidecar调用其他服务
再通过GET请求请求 http://127.0.0.1:8000/query/gateway/info ,响应结果为

{
"status": 0,
"msg": "sucess",
"data": {
"instance_name": "JVM_Gateway"
}
}

此结果表明请求到Node.js服务(8000端口)的/query/gateway/info的请求后,Node.js服务向网关发送了http://localhost:6666/gateway/instance_info (sidecar占用了6666端口)请求获取了网关的实例信息,换言之,Sidecar可以转发Node.js服务请求其他服务的所有请求。
通过以上两个方面的测试,可以看出Sidecar可以双向代理Node.js服务,也就说,Node.js可以被纳入到基于SpringCloud的微服务中提供服务了。 至此,关于Node.js服务以网格服务方式整合到现有基于SpringCloud的微服务架构中的介绍就到一段路了,下面将介绍Python服务整合到基于SpringCloud的微服务架构中。

Python服务

上一小节,我们对nodejs服务以服务网格的方式整合到现有微服务架构中进行了讲解测试,本小节我们将把Python服务同样以网格服务的方式整合到基于SpringCloud的微服务架构中。
对于Python几年也是非常火的语言,Python不仅在数据处理上应用广泛同时在Web的应用上也很多,所以Python的web框架也是百花齐放,使用上比较简单又强大的框架有Django、Flask、Bottle等,此次讲解测试选择最流行的Django作为基础框架来开发一个可以接入SpringCloud的Rest服务,以便该文章有更多的受众。

准备Python服务

具体使用Django生成一个web项目HelloWorld,这里不再赘述,大家可以参考http://www.runoob.com/django/django-first-app.html ,之后准备view.py和urls.py即可。
urls.py

from django.contrib import admin
from django.urls import path
from django.conf.urls import url
from . import view

urlpatterns = [
url(r'^health$', view.health),
url(r'^instance_info$', view.instance_info),
url(r'^query/(.+)/info$', view.query_server_info),
]

view.py

from django.http import HttpResponse,JsonResponse
import urllib
import json

# 健康检查API,用于Sidecar检查本Python服务的存活状态
def health(request):
result = {
"status":"UP"
}
print(result)
return JsonResponse(result)

# 对外服务API,用于测试其他服务调用本Python服务即查询当前实例的信息
def instance_info(request):
result = {
"status":0,
"msg":"sucess",
"data":{
"instance_name":"python_server"
}
}
print(result)
return JsonResponse(result)

# 查看指定服务的实例信息API,用于测试本Python服务调用其他已注册到Eureka上是服务
def query_server_info(request,server_name):
print(server_name)

sidecar_service_url = "http://localhost:6666/{instance_name}/instance_info".replace('{instance_name}',server_name);#sidecar获取服务信息API(实际Sidecar会将此请求转发到Sidecar代理的Python服务上)
print("request_url: "+sidecar_service_url)
req = urllib.request.urlopen(sidecar_service_url)
res_data = req.read()

print(str(res_data, encoding = "utf-8"))

return JsonResponse(json.loads(res_data))

运行python manage.py runserver 0.0.0.0:8000命令启动Python服务,同样观察Eureka注册中心,也发现之前注册到注册中心的Sidecar应用状态变为UP正常状态了,这说明Python服务已经接入基于SpringCloud的微服务中,且能对外提供服务。

准备用于调用Python服务的测试服务

此步骤与测试Node.js服务使用的测试服务相同即基于JVM的Gateway。

测试Python服务在微服务中的可用性

此步骤与测试Node.js服务可用性的操作相投,这里不再赘述。

至此,关于非JVM服务整合到基于Spring Cloud的微服务中就完成了,当然对于老的JVM服务也可以采用这个思路,来尽量少的减少对现有代码的侵入,也是个不错的选择。这样一来,可以使我们所有的现有服务平滑的演进到微服务,最后基于Spring Cloud来一统微服务的天下。

haozi wechat
扫二维码关注我
鼓励原创