ServiceComb的开放性设计
和很多微服务开发框架类似,ServiceComb的早期版本,为了追求高性能,做过非常多的尝试,比如改善编码效率,改进通信协议等。随着业务规模的递增,问题随之而来。首先面对的是和遗留系统的通信,接着是各种不同的接入终端,然后就涉及到一些更加复杂的问题,协议健壮性、防攻击等。ServiceComb准备开源的前期,用户对于与业界其他开源组件和开发框架的集成的述求又源源的涌来。在这些问题的驱动下,ServiceComb设计团队慢慢的达成了共识,系统应该全开放,使用标准协议,容易拆分和扩展,对开发人员友好,并和业界其他系统良好集成。本文将分享这些方面的设计考虑。
开放和标准
开放和标准应用到设计的不同的层面。一方面是连接组织和开发人员,一方面是连接异构系统。组织和开发人员的复杂性来源于技能的多样性,大家使用不同的开发语言,同一种开发语言存在多样的开发习惯;系统的多样性来源于系统之间的通信协议,为了实现与异构系统的通信,必须具备良好的适配不同通信协议的能力。
连接组织和开发人员
l 代码风格
每个开发人员都有自己熟练的技术和写代码的方式,使用熟悉的方式写代码往往更加高效。ServiceComb的早期版本实现了gRPC协议,推广的过程中发现大量开发人员不能熟练的书写IDL,对IDL支持哪些特性不清楚,碰到一个场景就需要翻开协议规范看一遍。加上接口语言一般缺少配套的编辑和语法检查工具,开发效率非常低。如果开发人员能够以熟悉的开发方式马上开始工作,将是一件美妙的事情,新进来的人没门槛,老系统也能快速切过来,一举两得。在JAVA世界里开发框架不胜枚举,我们从这些方式中找到了很多的共性,能够涵盖到90%以上的习惯:1. 使用RPC的方式描述对外接口。这个在gRPC、Corba、WebService等开发人员面前非常熟悉, 是最直接和有效的写代码方式; 2. 使用JAX-RS或者Spring MVC风格开发REST接口。REST风格开发随着微服务架构兴起,JAX-RS是JAVA REST开发标准,Spring MVC是JAVA开发的事实标准,Spring拥护者很熟悉。 ServiceComb选择这几种缺省的开发风格,拥抱90%的开发者,让大家能够快速开始工作。
在下面的例子中,呈现了Provider和Consumer代码的各种实现,在同一个微服务中,这些呈现方式可以同时出现;同一段Consumer代码,可以访问各种不同的Provider实现,非常灵活。
代码:RPC形式的Provider.
@RpcSchema(schemaId = "hello")
public class HelloImpl implements Hello {
@Override
public String sayHi(String name) {
return "Hello " + name;
}
@Override
public String sayHello(Person person) {
return "Hello person " + person.getName();
}
}
@RestSchema(schemaId = "jaxrsHello")
@Path("/jaxrshello")
@Produces(MediaType.APPLICATION_JSON)
public class JaxrsHelloImpl implements Hello {
@Path("/sayhi")
@POST
@Override
public String sayHi(String name) {
return "Hello " + name;
}
@Path("/sayhello")
@POST
@Override
public String sayHello(Person person) {
return "Hello person " + person.getName();
}
}
@RestSchema(schemaId = "springmvcHello")
@RequestMapping(path = "/springmvchello", produces = MediaType.APPLICATION_JSON)
public class SpringmvcHelloImpl implements Hello {
@Override
@RequestMapping(path = "/sayhi", method = RequestMethod.POST)
public String sayHi(@RequestParam(name = "name") String name) {
return "Hello " + name;
}
@Override
@RequestMapping(path = "/sayhello", method = RequestMethod.POST)
public String sayHello(@RequestBody Person person) {
return "Hello person " + person.getName();
}
}
@RpcReference(microserviceName = "hello", schemaId = "hello")
private Hello hello;
System.out.println(hello.sayHi("Java Chassis"));
看到这里,开发者或者有个疑问,Consumer既然可以通过一致的API方式访问不同的Provider,为什么还需要额外的JAX-RS和SpringMVC标签了?这里的主要考量并不仅仅是SDK的Consumer,还有浏览器等非SDK的Consumer,他们识别的是HTTP形式的消息,通过这些标签,可以更加精细的指定浏览器如何访问后台接口。这一点非常类似于Web Service的WSDL描述语言,ServiceComb称之为契约。服务契约会在运行时通过代码定义生成,并注册到服务中心。契约在运行时可以用于独立的服务治理逻辑开发,生成Consumer代码。也可以作为文档对外发布,供非SDK的Consumer参考。
l 契约
微服务强调服务自治,对外体现的功能,全部以接口提供,而且只能以通信的方式相互访问。这个原则给团队协作带来了根本的变革,一个团队通常由5~6个人的全功能团队组成,端到端的完成功能设计、开发和运维,系统的结构和组织的结构是匹配的。小团队以后的核心问题就是团队之间如何进行高效的沟通。为了更好的连接开发人员,我们让不同的开发人员能够使用自己熟悉的语言和编程习惯写代码,这样就需要一种中立的机制让每个功能团队进行有效协作。在RPC的世界里,有Corda IDL,WSDL,ProtoBuffer等可以参考的优秀实践。REST风格的接口让团队成员之间可以通过HTTP的语义进行沟通,但是不能像IDL一样描述跨语言时的数据格式。Open API的出现很好的解决了这些问题。Open API首先是一个开放的标准,并且在不断的发展壮大。Open API对于RPC、REST等不同的开发方式都完整的兼顾到了,并且吸收了大量的跨语言经验,能够在不同的语言之间解析。 对于JAVA开发者,下面的类型定义已经是毫无疑问的:
User:
type: object
properties:
age:
type: integer
如果开发人员有丰富的跨语言开发经验,可以看出Swagger在解决跨语言编程方面的努力,swagger通过format来定义数据类型的存储格式,以解决不同的语言在数据类型表示上的差异:
User:
type: object
properties:
age:
type: integer
format: int32
SerivceComb兼顾开发效率和开发规范。开发者可以先写接口定义,再写代码的方式来完成自己的开发过程,也可以直接通过自己熟悉的方式写代码。两种方式都会生成服务的契约(Open API描述文件),并且将内容注册到服务中心。使用者可以从服务中心下载相关的契约进行开发。ServiceComb的各种治理结构也是基于契约的,可以让开发者独立于业务实现对系统进行统一的管控。
连接异构系统
ServiceComb早期版本提供了gRPC、REST、SOAP等多种协议。gRPC相对于REST的最大好处就是性能,采用长连接,高效的二进制序列化方式,并提供多种语言支持。在接口定义方面,提供了IDL语言约束开发者按照标准的方式工作。一切看起来是那么的完美,实际上ServiceComb的第一轮重构,首选的也是gRPC的方式。系统上线以后,首要的问题来源于网关的压力。网关作为所有业务的接入端,必须高效的管理连接和保证公平,长连接非常容易导致拒绝服务。gRPC程序开发完成后,开发联调也变得不方便起来,特别是生产环境。开发人员无法利用系统提供的各种工具进行测试,网络包分析也变得困难。随着系统规模的扩大,gRPC再次面对更加严峻的问题,其他系统如何与它直接通信,如何跨网关与它间接通信,解决这些问题,意味着我们需要扩展和改善老的协议和程序,提供gRPC客户端支持,开发者需要提供一个额外的表示层用于业务接口的逻辑转换,造成大量的重复代码。同时由于gRPC依赖于接口定义,并根据定义生成代码,一套代码只能跑在gRPC协议上,希望业务代码使用其他更加灵活的方式,比如REST访问的时候,就得重新写一套代码。面对以上问题,gRPC在选择上,最终被定义为只能在中小型系统内部之间使用,并通过协议网关与外部系统进行沟通。
接着就是REST了。业界可用的REST实现,和gRPC比较起来,最大的痛点就是性能。有一个观点在很多设计人员和开发人员脑海里根深蒂固:”二进制编码效率远高于文本协议,采用二进制编码的系统的性能远高于采用文本的HTTP”。这个观点甚至会让多数决策止步于理论,大家甚至不愿意尝试去优化REST。可喜的是ServiceComb走出了重构REST底层通信实现的第一步,基于Netty的异步框架来替换Tomcat实现,效果大大超出我们的预期。一些基准测试数据的结果显示比gRPC还要好,gRPC最终输在了HTTP2协议上的额外报文。同时优化后的REST和业界开源的其他基于二进制的RPC实现的性能也基本持平,只有微小的差异。在一个简单的提供数据库查询的代码逻辑中,REST通信框架处理时间,占用总的处理时间远小于千分之一。这意味着在系统框架层面的大量优化,抵不上业务系统最简单的一笔操作,为了优化性能放弃的其他好处就不值得了。最终,ServiceComb选择了REST作为首选和缺省协议(http + json)。
使用REST也存在和其他系统对接,通过网关对接的场景,但是问题已经好了很多。
但是我们远没有止步于此。随着需要迁移到系统平台的服务越来越多,早期的一些遗留系统也需要进行对接。我们提供的每一种通信协议,都对应着不同的开发者接口,增加通信协议,意味着需要对业务代码进行大量的重复构建。为了进一步满足更多的场景,通信协议层被剥离了出来,做到和业务代码分离,系统运行基于契约,实现通信协议的扩展。利用协议扩展机制,用户解决了与老的gRPC框架、自定义二进制框架等很多遗留系统的通信问题。
在ServiceComb框架中,切换协议非常简单,不需要修改一行业务代码。多个协议共存也是允许的。
cse:
rest:
address: 0.0.0.0:8084
highway:
address: 0.0.0.0:8094
关于协议扩展的更多内容,将在下面的章节进行介绍。
扩展性
扩展性是系统进一步发展的基石。ServiceComb创造性的将扩展性拓展到Provider和Consumer,让它们拥有一致的开发体验。这个在现有的各种开发框架里面是独有的。
内部系统结构
连接开发者和通信协议层面已经让系统具备了很大的扩展性。微服务化给系统解耦、团队自治带来了很大的灵活性,加快了生产效率;同时也带来了服务管控的复杂性。需要用通用的管控机制来解决雪崩效应、调用跟踪、性能监控与分析等问题。基于服务契约,ServiceComb提供了动态插拔扩展的处理链机制,并且为应对这些管控,提供了默认实现,开发者可以灵活插拔这些处理模块,或者调整他们的顺序以应对不同的处理场景,增加新的处理模块等。不管Provider,还是Consumer,都会经过该处理链,这给客户端治理功能开发带来了非常大的便利,这是其他现有的微服务框架不具备的特性。ServiceComb的运行结构如下图:
ServiceComb在用户编程接口上,支持同步和异步两种方式。在通信实现上,采用了纯异步的方式,对于运行模型的扩展,也是基于异步回调接口的。这种方式提供了比同步模式(比如Filter)更加优雅和灵活的扩展方式。
在这个系统结构中,有几个核心的接口,这些接口均在core模块进行定义:
ProducerProvider:Provider编程模型的扩展,通过实现这个接口,可以适配不同的Provider编程风格;目前实现了RPC、SpringMVC、JAX-RS三种风格。
ConsumerProvider:Consumer编程模型的扩展,通过实现这个接口,可以适配不同的Consumer编程风格;目前实现了RPC、RestTemplate两种风格。RestTemplate是Spring MVC提供了REST编程接口,可以在服务层方面解除接口依赖,只需要依赖数据模型。
Handler:处理链的接口。通过扩展该接口,可以在处理过程中插入任意的逻辑。目前实现了负载均衡、服务治理、流量控制等多个处理链。开发者可以针对Consumer和Provider定义不同的处理链,并且为访问不同的微服务定制不同的处理链。
Transport:通信协议扩展。目前实现了REST over Vertx,Rest over Servlet,Highway等多个协议。
Invocation:中立的对象。所有的运行模型都面对这个中立的对象进行编程,当定义好服务接口后,对服务的治理和服务业务逻辑的开发可以并行。在编程模型和通信模型里面,也面对这个模型进行编解码。
对接外部系统
运行框架涉及到的外部系统包括服务注册发现的服务中心、配置管控和治理的配置中心、运行监控和运维的治理中心等。这些功能也预留了接口,能够让开发者灵活切换使用第三方提供的服务。 下图是不同的开发框架支持和运行的第三方系统情况,这些基础服务都给开发者预留了可以进行支持接入的接口。
下面是几个重要的扩展:
ServiceRegistryClient: 实现这个接口以对接不同的注册服务。
ConfigCenterConfigurationSource: 实现这个接口以对接不同的配置服务。
此外,ServiceComb还提供了对接Zipkin、Servo等开源系统的功能,这些可以从github官网代码中查找到对应的例子。
运行环境集成
一个完整的业务系统不是使用RPC框架就算完成了,它们还需要其他的计算资源。对于一般的业务系统都需要访问数据库,或者基于J2EE的设施进行工作。ServiceComb可以以非常轻量级的方式运行,也可以集成到其他系统框架里面工作。下面的示意图说明了ServiceComb的一些工作环境。
-
如果业务只需要提供REST接口,可以以轻量级的方式运行ServiceComb。所有的REST接口运行于ServiceComb提供的Netty HTTP之上。
-
如果业务是基于J2EE来构建,那么ServiceComb可以作为一个Servlet,运行于WEB容器里面(比如Tomcat、Jetty等)。
-
如果业务要基于Spring Boot生态构建,ServiceComb可以作为一个starter对外提供REST服务,开发者可以自由使用其他基于Spring Boot的功能。
由于ServiceComb使用了Spring,因此也能够和很多通用的组件很好的集成,比如mybatis、JPA等。各种集成方式,都可以从ServiceComb官网或者CSE示例库找到对应的例子。
总结
本文简单的介绍了一下ServiceComb开发框架开发性设计的一些考虑。除了这些特性之外,ServicComb还提供了Edge Service网关服务、Saga分布式事务等特性。
参考文献:
1. ServiceComb官网包括源码:https://github.com/apache/incubator-servicecomb-java-chassis和文档资料:http://servicecomb.incubator.apache.org/
2. 华为云使用ServiceComb作为首选微服务开发框架,商用支持参考帮助网站:http://support.huaweicloud.com/cse_dld/index.html 和资料的Preview版本:https://java.huaweicse.com/
- 点赞
- 收藏
- 关注作者
评论(0)