微服务(一)
作者:互联网
单体应用
各大互联网公司的应用技术栈大致可分为 LAMP(Linux + Apache + MySQL + PHP)和 MVC(Spring + iBatis/Hibernate + Tomcat)两大流派。无论是 LAMP 还是 MVC,都是为单体应用架构设计的,其优点是学习成本低,开发上手快,测试、部署、运维也比较方便,甚至一个人就可以完成一个网站的开发与部署。
存在问题
1、部署效率低下
当单体应用的代码越来越多,依赖的资源越来越多时,应用编译打包、部署测试一次,甚至需要 10 分钟以上。这也经常被新加入的同学吐槽说,部署测试一次的时间,都可以去楼下喝杯咖啡了
2、团队协作开发成本高
早期在团队开发人员只有两三个人的时候,协作修改代码,最后合并到同一个 master 分支,然后打包部署,尚且可控。但是一旦团队人员扩张,超过 5 人修改代码,然后一起打包部署,测试阶段只要有一块功能有问题,就得重新编译打包部署,然后重新预览测试,所有相关的开发人员又都得参与其中,效率低下,开发成本极高。
3、系统高可用性差
因为所有的功能开发最后都部署到同一个 WAR 包里,运行在同一个 Tomcat 进程之中,一旦某一功能涉及的代码或者资源有问题,那就会影响整个 WAR 包中部署的功能。比如我经常遇到的一个问题,某段代码不断在内存中创建大对象,并且没有回收,部署到线上运行一段时间后,就会造成 JVM 内存泄露,异常退出,那么部署在同一个 JVM 进程中的所有服务都不可用,后果十分严重。
4、线上发布变慢
特别是对于 Java 应用来说,一旦代码膨胀,服务启动的时间就会变长,有些甚至超过 10 分钟以上,如果机器规模超过 100 台以上,假设每次发布的步长为 10%,单次发布需要就需要 100 分钟之久。因此,急需一种方法能够将应用的不同模块的解耦,降低开发和部署成本
什么是服务化?
在我看来,用通俗的话来讲,服务化就是把传统的单机应用中通过 JAR 包依赖产生的本地方法调用,改造成通过 RPC 接口产生的远程方法调用。一般在编写业务代码时,对于一些通用的业务逻辑,我会尽力把它抽象并独立成为专门的模块,因为这对于代码复用和业务理解都大有裨益。
什么是微服务?
得益于以 Docker 为代表的容器化技术的成熟以及 DevOps 文化的兴起,服务化的思想进一步演化,演变为今天我们所熟知的微服务
服务拆分粒度更细。微服务可以说是更细维度的服务化,小到一个子模块,只要该模块依赖的资源与其他模块都没有关系,那么就可以拆分为一个微服务。
服务独立部署。每个微服务都严格遵循独立打包部署的准则,互不影响。比如一台物理机上可以部署多个 Docker 实例,每个 Docker 实例可以部署一个微服务的代码。
服务独立维护。每个微服务都可以交由一个小团队甚至个人来开发、测试、发布和运维,并对整个生命周期负责。
**服务治理能力要求高。**因为拆分为微服务之后,服务的数量变多,因此需要有统一的服务治理平台,来对各个服务进行管理。
总结
它是由单体应用进化到服务化拆分部署,后期随着移动互联网规模的不断扩大,敏捷开发、持续交付、DevOps 理论的发展和实践,以及基于 Docker 容器化技术的成熟,微服务架构开始流行,逐渐成为应用架构的未来演进方向。
服务化拆分的两种姿势
这种服务化拆分方式是纵向拆分,是从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。还有一种服务化拆分方式是横向拆分,是从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。
服务调用主要依赖下面几个基本组件:
服务描述
服务调用首先要解决的问题就是服务如何对外描述。比如,你对外提供了一个服务,那么这个服务的服务名叫什么?调用这个服务需要提供哪些信息?调用这个服务返回的结果是什么格式的?该如何解析?这些就是服务描述要解决的问题。
常用的服务描述方式包括 RESTful API、XML 配置以及 IDL 文件三种。
其中,RESTful API 方式通常用于 HTTP 协议的服务描述,并且常用 Wiki 或者Swagger来进行管理。下面是一个 RESTful API 方式的服务描述的例子。
注册中心
有了服务的接口描述,下一步要解决的问题就是服务的发布和订阅,就是说你提供了一个服务,如何让外部想调用你的服务的人知道。这个时候就需要一个类似注册中心的角色,服务提供者将自己提供的服务以及地址登记到注册中心,服务消费者则从注册中心查询所需要调用的服务的地址,然后发起请求。
服务提供者在启动时,根据服务发布文件中配置的发布信息向注册中心注册自己的服务。
服务消费者在启动时,根据消费者配置文件中配置的服务信息向注册中心订阅自己所需要的服务。
注册中心返回服务提供者地址列表给服务消费者。
当服务提供者发生变化,比如有节点新增或者销毁,注册中心将变更通知给服务消费者。
服务框架
通过注册中心,服务消费者就可以获取到服务提供者的地址,有了地址后就可以发起调用。但在发起调用之前你还需要解决以下几个问题。
服务通信采用什么协议?就是说服务提供者和服务消费者之间以什么样的协议进行网络通信,是采用四层 TCP、UDP 协议,还是采用七层 HTTP 协议,还是采用其他协议?
数据传输采用什么方式?就是说服务提供者和服务消费者之间的数据传输采用哪种方式,是同步还是异步,是在单连接上传输,还是多路复用。
数据压缩采用什么格式?通常数据传输都会对数据进行压缩,来减少网络传输的数据量,从而减少带宽消耗和网络传输时间,比如常见的 JSON 序列化、Java 对象序列化以及 Protobuf 序列化等。
服务监控
一旦服务消费者与服务提供者之间能够正常发起服务调用,你就需要对调用情况进行监控,以了解服务是否正常。通常来讲,服务监控主要包括三个流程。
指标收集。就是要把每一次服务调用的请求耗时以及成功与否收集起来,并上传到集中的数据处理中心。数据处理。
有了每次调用的请求耗时以及成功与否等信息,就可以计算每秒服务请求量、平均耗时以及成功率等指标。
数据展示。数据收集起来,经过处理之后,还需要以友好的方式对外展示,才能发挥价值。通常都是将数据展示在 Dashboard 面板上,并且每隔 10s 等间隔自动刷新,用作业务监控和报警等。
最常见的服务发布和引用的方式有三种:
RESTful API
@Path("/rest")
public interface RestfulService {
@GET
@Produces(MediaType.APPLICATION_JSON)
List<User> getUsers(@QueryParam("uid") int uid);
@GET
@Path("/primitive")
@Produces(MediaType.TEXT_PLAIN)
String testPrimitiveType();
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
Response add(@FormParam("id") int id, @FormParam("name") String name);
public class RestfulServerDemo implements RestfulService {
@Override
public List<User> getUsers(@CookieParam("uid") int uid) {
return Arrays.asList(new User(uid, "name" + uid));
}
@Override
public String testPrimitiveType() {
return "helloworld!";
}
@Override
public Response add(@FormParam("id") int id, @FormParam("name") String name) {
return Response.ok().cookie(new NewCookie("ck", String.valueOf(id))).entity(new User(id, name)).build();
}
服务提供者这一端通过部署代码到 Tomcat 中,并配置 Tomcat 中如下的 web.xml,就可以通过 servlet 的方式对外提供 RESTful API。
<listener>
<listener-class>com.weibo.api.motan.protocol.restful.support.servlet.RestfulServletContainerListener</listener-class>
</listener>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>resteasy.servlet.mapping.prefix</param-name>
<param-value>/servlet</param-value> <!-- 此处实际为servlet-mapping的url-pattern,具体配置见resteasy文档-->
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/servlet/*</url-pattern>
</servlet-mapping>
XML 配置
服务提供者定义接口,并实现接口。
服务提供者进程启动时,通过加载 server.xml 配置文件将接口暴露出去。服务消费者进程启动时,通过加载 client.xml 配置文件来引入要调用的接口
首先,服务提供者定义接口。
public interface FooService {
public String hello(String name);
}
然后服务提供者实现接口。
public class FooServiceImpl implements FooService {
public String hello(String name) {
System.out.println(name + " invoked rpc service");
return "hello " + name;
}
}
最后服务提供者进程启动时,加载 server.xml 配置文件,开启 8002 端口监听。
server.xml 配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:motan="http://api.weibo.com/schema/motan"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://api.weibo.com/schema/motan http://api.weibo.com/schema/motan.xsd">
<!-- service implemention bean -->
<bean id="serviceImpl" class="quickstart.FooServiceImpl" />
<!-- exporting service by Motan -->
<motan:service interface="quickstart.FooService" ref="serviceImpl" export="8002" />
</beans>
服务提供者加载 server.xml 的代码如下:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Server {
public static void main(String[] args) throws InterruptedException {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:motan_server.xml");
System.out.println("server start...");
}
}
IDL 文件
主要是用作跨语言平台的服务之间的调用,有两种最常用的 IDL:一个是 Facebook 开源的 Thrift 协议,另一个是 Google 开源的 gRPC 协议。无论是 Thrift 协议还是 gRPC 协议,它们的工作原理都是类似的。
比如文件 helloword.proto 定义了一个接口 SayHello 方法,它的请求参数是 HelloRequest,它的返回值是 HelloReply。
// The greeter service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
假如服务提供者使用的是 Java 语言,那么利用 protoc 插件即可自动生成 Server 端的 Java 代码。
private class GreeterImpl extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
@Override
public void sayHelloAgain(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage("Hello again " + req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
假如服务消费者使用的也是 Java 语言,那么利用 protoc 插件即可自动生成 Client 端的 Java 代码。
public void greet(String name) {
logger.info("Will try to greet " + name + " ...");
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
HelloReply response;
try {
response = blockingStub.sayHello(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
logger.info("Greeting: " + response.getMessage());
try {
response = blockingStub.sayHelloAgain(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
logger.info("Greeting: " + response.getMessage());
}
标签:调用,服务,name,服务提供者,public,String 来源: https://blog.csdn.net/hukehukehukehuke/article/details/120519492