SOFAStack(Scalable Open Financial Architecture Stack,可扩展开放金融架构栈)是蚂蚁金服开源的技术栈,国内多家金融和互联网公司在生产环境使用了此技术栈。
基于Spring Boot,额外提供了以下特性:
SOFABoot需要JDK 7+和Maven 3.2.5来完成构建。
通过IntelliJ IDEA的Spring Initializer来创建项目骨架,依赖选择Web。然后修改POM,将parent改为:
<parent> <groupId>com.alipay.sofa</groupId> <artifactId>sofaboot-dependencies</artifactId> <version>${sofa.boot.version}</version> </parent>
并添加依赖:
<!-- 提供健康检查能力 --> <dependency> <groupId>com.alipay.sofa</groupId> <artifactId>healthcheck-sofa-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
SOFABoot要求提供必要的参数:
spring.application.name=Learing SOFABoot logging.path=./logs
启动上述项目后,运行命令curl http://localhost:8080/actuator/health,可以看到服务的健康状况,正常情况下输出{"status":"UP"}
运行命令curl -s http://localhost:8080/actuator/readiness | jq .可以获得Readiness探针执行结果的细节:
{ "details": { "diskSpace": { "details": { "threshold": 10485760, "free": 166560387072, "total": 358796750848 }, "status": "UP" }, "SOFABootReadinessHealthCheckInfo": { "status": "UP" } }, "status": "UP" }
检查logs目录,可以看到日志根据来源的不同,分割到logs、infra等目录中。
你可以使用SpringRunner进行单元测试,但是如果在项目中使用了SOFABoot的类隔离特性,则必须使用SofaBootRunner、SofaJUnit4Runner进行单元测试,并引入依赖:
<dependency> <groupId>com.alipay.sofa</groupId> <artifactId>test-sofa-boot-starter</artifactId> </dependency>
单元测试用例的例子:
@SpringBootTest @RunWith(SpringRunner.class) public class SofaBootWithModulesTest { // 引用SOFABoot模块发布的服务 @SofaReference private SampleJvmService sampleJvmService; @Test public void test() { Assert.assertEquals("Hello, jvm service xml implementation.", sampleJvmService.message()); } }
从2.4版本开始SOFABoot引入了基于Spring上下文隔离的模块化能力。SOFABoot定义了三个模块化隔离级别:
SOFABoot模块化开发属于第二种模块化形式 —— 基于 Spring 上下文隔离的模块化,每个模块使用独立的Spring上下文。
每个SOFABoot模块包含Java代码、Spring配置文集、SOFA模块标识等信息,打包形式为JAR。不同模块之间不能通过DI来引用,需要转而使用SOFA服务。SOFABoot支持两种形式的服务(发布/引用):
由于相互之间没有Bean依赖,SOFABoot模块可以并行的启动,这可以提升应用启动速度。
这种模块化开发,和微服务的理念是违背的,各模块仍然需要共享单个JVM的资源。
为了体验SOFABoot的模块化开发,我们直接使用其源码附带的示例应用:
git clone https://github.com/alipay/sofa-boot.git tree sofa-boot/sofaboot-samples/sofaboot-sample-with-isle -L 1 # . # ├── service-consumer 服务的消费者 # ├── service-facade 服务的API # ├── service-provider 服务的提供者 # ├── sofa-boot-run 启动包含模块的SOFABoot应用
service-facade中定义的接口如下:
public interface SampleJvmService { String message(); }
service-provider将上面的接口发布为JVM服务。发布方式有三种:
// 唯一性的ID,不设置默认为空串 @SofaService(uniqueId = "annotationImpl") public class SampleJvmServiceAnnotationImpl implements SampleJvmService { @Override public String message() { String message = "Hello, jvm service annotation implementation."; System.out.println(message); return message; } }
XML方式:
public class SampleJvmServiceImpl implements SampleJvmService { private String message; @Override public String message() { System.out.println(message); return message; } }
需要配合XML配置:
<bean id="sampleJvmService" class="com.alipay.sofa.isle.sample.SampleJvmServiceImpl"> <property name="message" value="Hello, jvm service xml implementation."/> </bean> <sofa:service ref="sampleJvmService" interface="com.alipay.sofa.isle.sample.SampleJvmService"> <sofa:binding.jvm/> </sofa:service>
@Component public class PublishServiceWithClient implements ClientFactoryAware { private ClientFactory clientFactory; @PostConstruct public void init() { ServiceClient serviceClient = clientFactory.getClient(ServiceClient.class); ServiceParam serviceParam = new ServiceParam(); serviceParam.setInstance(new SampleJvmServiceImpl( "Hello, jvm service service client implementation.")); serviceParam.setInterfaceType(SampleJvmService.class); serviceParam.setUniqueId("serviceClientImpl"); // 发布服务 serviceClient.service(serviceParam); } @Override public void setClientFactory(ClientFactory clientFactory) { this.clientFactory = clientFactory; } }
你需要在sofa-module.properties中声明模块名称、模块依赖:
# service-provider Module-Name=com.alipay.sofa.service-provider # service-consumer Module-Name=com.alipay.sofa.service-consumer Require-Module=com.alipay.sofa.service-provider
service-consumer负责消费service-provider发布的服务:
public class JvmServiceConsumer implements ClientFactoryAware { private ClientFactory clientFactory; // 引用基于XML配置的JVM服务 @Autowired private SampleJvmService sampleJvmService; // 引用基于注解配置的JVM服务,使用uniqueId @SofaReference(uniqueId = "annotationImpl") private SampleJvmService sampleJvmServiceByFieldAnnotation; public void init() { sampleJvmService.message(); sampleJvmServiceByFieldAnnotation.message(); // 编程式客户端 ReferenceClient referenceClient = clientFactory.getClient(ReferenceClient.class); ReferenceParam referenceParam = new ReferenceParam<>(); referenceParam.setInterfaceType(SampleJvmService.class); referenceParam.setUniqueId("serviceClientImpl"); SampleJvmService sampleJvmServiceClientImpl = referenceClient.reference(referenceParam); sampleJvmServiceClientImpl.message(); } @Override public void setClientFactory(ClientFactory clientFactory) { this.clientFactory = clientFactory; } }
如果需要在SOFABoot项目中使用SOFA中间件,需要依赖相应的Starter:
<!-- SOFARPC --> <dependency> <groupId>com.alipay.sofa</groupId> <artifactId>rpc-sofa-boot-starter</artifactId> </dependency> <!-- SOFATracer --> <dependency> <groupId>com.alipay.sofa</groupId> <artifactId>tracer-sofa-boot-starter</artifactId> </dependency> <!-- SOFALookout --> <dependency> <groupId>com.alipay.sofa</groupId> <artifactId>lookout-sofa-boot-starter</artifactId> </dependency>
如果需要在SOFABoot项目中使用扩展组件,需要依赖相应的Starter:
<!-- 健康检查扩展 --> <dependency> <groupId>com.alipay.sofa</groupId> <artifactId>healthcheck-sofa-boot-starter</artifactId> </dependency> <!-- 模块化隔离扩展 --> <dependency> <groupId>com.alipay.sofa</groupId> <artifactId>isle-sofa-boot-starter</artifactId> </dependency> <!-- 类隔离扩展 --> <dependency> <groupId>com.alipay.sofa</groupId> <artifactId>sofa-ark-springboot-starter</artifactId> </dependency> <!-- 测试扩展 --> <dependency> <groupId>com.alipay.sofa</groupId> <artifactId>test-sofa-boot-starter</artifactId> </dependency>
如果要使用SOFAArk提供的类加载器隔离功能,则需要依赖相应的ARK插件,并替换到对应的Starter:
ARK插件 | ArtifactId |
SOFARPC | rpc-sofa-boot-plugin |
SOFATracer | tracer-sofa-boot-plugin |
ARK插件能够让业务应用的类、SOFA中间件的类(以及它依赖的类)使用不同的类加载器,从而避免类冲突问题。
Spring Boot Actuator提供的HealthIndicator接口可以提供Liveness健康检查,SOFABoot在此基础上增加了Readiness检查功能。使用SOFA中间件时,最好配合SOFABoot的健康检查,以实现优雅上线,避免未准备好的实例过早加入服务池。
健康检查扩展提供了多个扩展点,允许用户定制其行为。
健康检查相关配置属性:
配置属性 | 说明 | 默认值 |
com.alipay.sofa.healthcheck.skip.all | 是否跳过整个 Readiness Check 阶段 | false |
com.alipay.sofa.healthcheck.skip.component | 是否跳过 SOFA 中间件的 Readiness Check | false |
com.alipay.sofa.healthcheck.skip.indicator | 是否跳过 HealthIndicator 的 Readiness Check | false |
com.alipay.sofa.healthcheck.component.check.retry.count | 组件健康检查重试次数 | 20 |
com.alipay.sofa.healthcheck.component.check.retry.interval | 组件健康检查重试间隔时间 | 1000ms |
com.alipay.sofa.healthcheck.module.check.retry.count | sofaboot 模块健康检查重试次数 | 0 |
com.alipay.sofa.healthcheck.module.check.retry.interval | sofaboot 模块健康检查重试间隔时间 | 1000ms |
SOFABoot支持异步的执行Bean的初始化方法,要使用该特性,引入依赖:
<dependency> <groupId>com.alipay.sofa</groupId> <artifactId>runtime-sofa-boot-starter</artifactId> </dependency>
并且为目标Bean提供配置属性async-init:
<bean id="testBean" class="com.alipay.sofa.runtime.beans.TimeWasteBean" init-method="init" async-init="true"/>
SOFABoot在独立的线程池中进行Bean的异步初始化,相关配置属性:
配置属性 | 说明 |
com.alipay.sofa.boot.asyncInitBeanCoreSize | 线程池默认线程数 |
com.alipay.sofa.boot.asyncInitBeanMaxSize | 线程池最大线程数 |
SOFABoot模块的JAR包遵循如下约定:
模块元数据声明在sofa-module.properties文件中,包含以下配置属性:
配置属性 | 说明 |
Module-Name | SOFABoot模块名称,唯一标识,Java包路径形式 |
Spring-Parent | 指定一个模块的名称,将其Spring上下文设置为当前模块的父上下文,这样就可以解除对目标模块的隔离 |
Require-Module | 逗号分隔的,依赖的其它模块列表 |
Module-Profile |
所属的Profile,通过Spring配置属性com.alipay.sofa.boot.active-profile,可以指定哪些SOFABoot Profile启用(逗号分隔多个值) 只有其Profile启用的模块才激活(也就是启动) 此外,在Spring配置文件中,可以嵌套beans元素,子元素可以指定profile属性,来指定仅当特定Profile启用时才激活的Bean: <?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:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" default-autowire="byName"> <beans profile="dev"> <bean id="devBeanId" class="com.alipay.cloudenginetest.sofaprofilesenv.DemoBean"> </bean> </beans> <beans profile="test"> <bean id="testBeanId" class="com.alipay.cloudenginetest.sofaprofilesenv.DemoBean"> </bean> </beans> </beans> |
参考SOFARPC - 起步 - 整合SOFABoot一节。
SOFAArk 是一款基于 Java 实现的轻量级类隔离容器,提供类隔离和应用(模块)合并部署能力。
在大型软件开发过程中,通常会推荐底层功能插件化,业务功能模块化的开发模式,以期达到低耦合、高内聚、功能复用的优点。SOFAArk 提供了一套插件化、模块化的开发规范,它的能力包括:
Ark 包是满足特定目录格式要求的可执行扁平(Flat,也就是打包了所有依赖)Jar,使用Maven 插件 sofa-ark-maven-plugin可以将单个或多个应用打包成标准格式的 Ark 包,Ark包包含三类构件:
执行Ark包时,Ark Container优先启动,然后启动Plugin,最后启动Biz。它们的逻辑关系图如下:
在解决类冲突(两个运行在单个JVM中的模块依赖某个类的两个不兼容版本)方面,SOFAArk比OSGI简单的多。SOFAArk 提出了一种特殊的包结构Ark Plugin,在存在包冲突时,用户可以使用 Maven 插件将若干冲突包打包成 Plugin,运行时由独立的 PluginClassLoader 加载,从而解决包冲突。
将复杂项目拆分到多个工程的主要动机包括避免VCS提交冲突、规避技术栈导致的依赖冲突。SOFA支持模块化,可以将多个业务模块打包为Biz,在运行时合并部署到单个JVM,各模块基于同一的API进行交互。
这种模式下,Biz之间的依赖可以通过Maven管理,当应用打为扁平化JAR时其依赖的Biz可以合并进来。每个Biz使用独立的BizClassLoader加载,Biz之间通过JVM服务(SofaService/SofaRefernece)进行交互。
这种模式下,Biz可以在运行时,通过API或者配置中心(ZooKeeper)来动态部署/卸载。
这里需要提到主应用(Master Biz)的概念,其实不管静态/动态合并,此概念都是存在的。如果Ark包打了单个Biz则它就是主应用,如果打了多个Biz包则需要配置指定主应用。主应用不得卸载。
通常情况下,各模块的实现放在动态Biz中,供主应用调用。主应用通过两种方式部署/卸载动态Biz:
Ark Plugin可以导入、导出类或资源:
导入类:插件启动时,优先委托给导出该类的插件负责加载,如果加载不到,才会尝试从本插件内部加载
导出类:其他插件如果导入了该类,优先从本插件加载
导入资源:插件在查找资源时,优先委托给导出该资源的插件负责加载,如果加载不到,才会尝试从本插件内部加载
导出资源:其他插件如果导入了该资源,优先从本插件加载
SOFAArk的代码库提供了一个样例项目。其结构如下:
├── sample-ark-plugin │ ├── common # 此模块包含了插件导出类 │ ├── plugin # 包含插件服务的实现、PluginActivator接口实现 │ ├── pom.xml
plugin项目的POM中包含如下配置:
<build> <plugins> <plugin> <groupId>com.alipay.sofa</groupId> <artifactId>sofa-ark-plugin-maven-plugin</artifactId> <version>${project.version}</version> <executions> <execution> <id>default-cli</id> <goals> <goal>ark-plugin</goal> </goals> <configuration> <!-- Ark 容器启动插件的入口类,最多只能配置一个。一般在此执行初始化操作,比如发布插件服务 --> <activator>com.alipay.sofa.ark.sample.activator.SamplePluginActivator</activator> <!-- 导出的包、类、资源列表 --> <exported> <packages> <package>com.alipay.sofa.ark.sample.common</package> </packages> <classes> <class>com.alipay.sofa.ark.sample.facade.SamplePluginService</class> </classes> </exported> <!-- 打包后的插件的存放位置,默认${project.build.directory} --> <outputDirectory>../target</outputDirectory> </configuration> </execution> </executions> </plugin> </plugins> </build>
此配置声明了当前插件的一切必要信息。
SamplePluginActivator负责在插件启动时,发布JVM服务:
package com.alipay.sofa.ark.sample.activator; import com.alipay.sofa.ark.exception.ArkRuntimeException; import com.alipay.sofa.ark.sample.facade.SamplePluginService; import com.alipay.sofa.ark.sample.impl.SamplePluginServiceImpl; import com.alipay.sofa.ark.spi.model.PluginContext; import com.alipay.sofa.ark.spi.service.PluginActivator; public class SamplePluginActivator implements PluginActivator { // 插件启动时的回调 public void start(PluginContext context) throws ArkRuntimeException { // 通过插件上下文发布服务 context.publishService(SamplePluginService.class, new SamplePluginServiceImpl()); } // 插件停止时的回调 public void stop(PluginContext context) throws ArkRuntimeException { System.out.println("stopping in ark plugin activator"); } }
除了发布服务之外,你还可以引用其他插件或Ark容器发布的服务:
public class SamplePluginServiceImpl implements SamplePluginService { // 引用Ark容器发布的,事件管理服务 @ArkInject private EventAdminService eventAdminService; public String service() { return "I'm a sample plugin service published by ark-plugin"; } public void sendEvent(ArkEvent arkEvent) { eventAdminService.sendEvent(arkEvent); } }
使用sofa-ark-maven-plugin也可以把普通的Spring Boot项目打为Ark包:
<build> <plugins> <plugin> <groupId>com.alipay.sofa</groupId> <artifactId>sofa-ark-maven-plugin</artifactId> <executions> <execution> <id>default-cli</id> <!-- 此目标生成可执行Ark包 --> <goals> <goal>repackage</goal> </goals> <configuration> <!-- 可执行Ark包的输出目录 --> <outputDirectory>./target</outputDirectory> <!-- 可以指定Ark包的Maven坐标的classifier字段 --> <arkClassifier>executable-ark</arkClassifier> </configuration> </execution> </executions> </plugin> </plugins> </build>
添加以下依赖即可:
<dependency> <groupId>com.alipay.sofa</groupId> <artifactId>sofa-ark-springboot-starter</artifactId> <version>${sofa.ark.version}</version> </dependency>
需要添加依赖:
<dependency> <groupId>com.alipay.sofa</groupId> <artifactId>sofa-ark-support-starter</artifactId> <version>${sofa.ark.version}</version> </dependency>
并且在入口点方法中启动Ark容器:
public class Application{ public static void main(String[] args) { SofaArkBootstrap.launch(args); } }
SOFAArk提供了org.junit.runner.Runner的实现类ArkJUnit4Runner,用于集成JUnit4单元测试框架:
@RunWith(ArkJUnit4Runner.class) public class JUnitTest { @Test public void test() { Assert.assertTrue(true); }
上面的用例会在Ark容器之上运行。
SOFAArk提供了org.junit.runner.Runner的实现类ArkBootRunner,用于在Spring Boot下进行集成测试:
@RunWith(ArkBootRunner.class) @SpringBootTest(classes = SpringbootDemoApplication.class) public class IntegrationTest { // 可以注入依赖 @Autowired private SampleService sampleService; @Test public void test() { sampleService.service(); } }
这是一个Java的RPC框架,提供了负载均衡,流量转发,链路追踪,链路数据透传,故障剔除等特性,兼容 bolt,RESTful,dubbo,H2C协议。
SOFARPC的基本工作原理和Dubbo类似:
需要加入依赖:
<dependency> <groupId>com.alipay.sofa</groupId> <artifactId>sofa-rpc-all</artifactId> <version>5.6.0-SNAPSHOT</version> </dependency>
需要发布的服务接口:
public interface HelloService { String sayHello(String string); }
接口实现HelloServiceImpl这里略去。
SOFARPC服务器:
public class QuickStartServer { public static void main(String[] args) { ServerConfig serverConfig = new ServerConfig() .setProtocol("bolt") // 设置一个协议,默认bolt .setPort(12200) // 设置一个端口,默认12200 .setDaemon(false); // 非守护线程方式运行 // 使用ZK作为注册表 RegistryConfig registryConfig = new RegistryConfig() .setProtocol("zookeeper") .setAddress("127.0.0.1:2181"); ProviderConfig providerConfig = new ProviderConfig() .setInterfaceId(HelloService.class.getName()) // 指定接口 .setRef(new HelloServiceImpl()) // 指定实现 .setServer(serverConfig) // 指定服务端 .setRegistry(registryConfig); // 服务注册表 providerConfig.export(); // 发布服务 } }
SOFARPC客户端:
public class QuickStartClient { private final static Logger LOGGER = LoggerFactory.getLogger(QuickStartClient.class); public static void main(String[] args) { ConsumerConfig consumerConfig = new ConsumerConfig() .setInterfaceId(HelloService.class.getName()) // 指定接口 .setProtocol("bolt") // 指定协议 .setDirectUrl("bolt://127.0.0.1:12200") // 指定服务端的直连地址 .setConnectTimeout(10 * 1000); HelloService helloService = consumerConfig.refer(); helloService.sayHello("world"); } }
SpringBoot应用的名字必须配置:
spring.application.name=test
你需要为SOFABoot工程引入SOFARPC的starter:
<dependency> <groupId>com.alipay.sofa</groupId> <artifactId>rpc-sofa-boot-starter</artifactId> </dependency>
你可以以POJO的形式定义服务接口和它的实现。
发布服务时,可以使用下面的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:sofa="http://sofastack.io/schema/sofaboot" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://sofastack.io/schema/sofaboot http://sofastack.io/schema/sofaboot.xsd" default-autowire="byName"> <!-- 服务的Bean定义 --> <bean id="personServiceImpl" class="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonServiceImpl"/> <!-- 发布SOFARPC服务 --> <sofa:service ref="personServiceImpl" interface="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonService"> <!-- 绑定的通信协议 --> <sofa:binding.bolt> <sofa:global-attrs timeout="3000" address-wait-time="2000"/> <!-- 调用超时;地址等待时间 --> <sofa:route target-url="127.0.0.1:22000"/> <!-- 直连到提供者,不走负载均衡 --> <sofa:method name="sayName" timeout="3000"/> <!-- 方法级别超时配置 --> </sofa:binding.bolt> <sofa:binding.rest/> </sofa:service> <!-- 订阅SOFARPC服务 --> <sofa:reference id="personReferenceBolt" interface="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonService"> <sofa:binding.bolt/> </sofa:reference> <sofa:reference id="personReferenceRest" interface="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonService"> <sofa:binding.rest/> </sofa:reference> </beans>
// 发布服务 @SofaService(interfaceType = AnnotationService.class, bindings = { @SofaServiceBinding(bindingType = "bolt"), @SofaServiceBinding(bindingType = "bolt") }) @Component public class AnnotationServiceImpl implements AnnotationService { @Override public String sayAnnotation(String stirng) { return stirng; } } // 订阅服务 @Component public class AnnotationClientImpl { @SofaReference(interfaceType = AnnotationService.class, binding = @SofaReferenceBinding(bindingType = "bolt")) private AnnotationService annotationService; public String sayClientAnnotation(String str) { String result = annotationService.sayAnnotation(str); return result; } }
SOFARPC支持过滤器。实现过滤器非常简单:
public class PersonServiceFilter extends Filter { @Override public SofaResponse invoke(FilterInvoker invoker, SofaRequest request) throws SofaRpcException { // 前置钩子 System.out.println("PersonFilter before"); try { // 执行RPC调用 return invoker.invoke(request); } finally { // 后置钩子 System.out.println("PersonFilter after"); } } }
过滤器需要配置才能生效。
Bolt是一个高性能的TCP协议,其性能比HTTP好。
Bolt支持同步、异步、回调、单向等多种调用类型:
调用类型 | 说明 |
Synchronous | 默认的调用类型,调用发起后,当前线程会阻塞以等待结果 |
Asynchronous |
调用发起后,当前线程立即处理后续逻辑,当调用结果到达后SOFAGRPC会缓存之,你可以异步的调用API以获得结果 XML配置: <sofa:reference interface="com.example.demo.SampleService" id="sampleService"> <sofa:binding.bolt> <sofa:global-attrs type="future"/> </sofa:binding.bolt> </sofa:reference> 注解配置: @SofaReference(binding = @SofaReferenceBinding(bindingType = "bolt", invokeType = "future")) private SampleService sampleService; 编程式,使用Spring的情况下: BoltBindingParam boltBindingParam = new BoltBindingParam(); boltBindingParam.setType("future"); 编程式,不使用Spring的情况下: ConsumerConfig consumerConfig = new ConsumerConfig() .setInterfaceId(SampleService.class.getName()) .setRegistry(registryConfig) .setProtocol("bolt") .setInvokeType("future"); 要获得异步响应,调用: // 第一个参数为超时,第二个参数提示是否删除线程上下文中的调用结果缓存 String result = (String)SofaResponseFuture.getResponse(0, true); JDK的Future对象也可以得到: Future future = SofaResponseFuture.getFuture(true); |
Callback |
调用类型名称:callback 类似Asynchronous,不需要等待结果。当结果到达后,自动调用注册的回调函数。你需要实现如下的回调接口: /** * 面向用户的Rpc请求结果监听器 * */ public interface SofaResponseCallback { /** * SOFA RPC 会在调用成功的响应到达后回调此方法 * * @param appResponse 响应对象 * @param methodName 被调用的方法 * @param request 请求对象 */ void onAppResponse(Object appResponse, String methodName, RequestBase request); /** * SOFA RPC 会在服务器端异常时回调此方法 */ void onAppException(Throwable throwable, String methodName, RequestBase request); /** * SOFA RPC 会在框架异常时回调此方法 * */ void onSofaException(SofaRpcException sofaException, String methodName, RequestBase request); } 然后,你可以使用下面的方式注册回调接口。 XML方式: <!-- 回调Bean --> <bean id="sampleCallback" class="com.example.demo.SampleCallback"/> <sofa:reference interface="com.example.demo.SampleService" id="sampleService"> <sofa:binding.bolt> <!-- 引用回调Bean --> <sofa:global-attrs type="callback" callback-ref="sampleCallback"/> </sofa:binding.bolt> </sofa:reference> 注解方式: @SofaReference(binding = @SofaReferenceBinding(bindingType = "bolt", invokeType = "callback", callbackRef = "sampleCallback")) private SampleService sampleService; 编程式,使用Spring的情况下: BoltBindingParam boltBindingParam = new BoltBindingParam(); boltBindingParam.setType("callback"); boltBindingParam.setCallbackClass("com.example.demo.SampleCallback"); 编程式,不使用Spring的情况下: ConsumerConfig consumerConfig = new ConsumerConfig() .setInterfaceId(SampleService.class.getName()) .setRegistry(registryConfig) .setProtocol("bolt") .setInvokeType("callback") .setOnReturn(new SampleCallback()); 你还可以在调用期间临时的设置: RpcInvokeContext.getContext().setResponseCallback(new SampleCallback()); |
Oneway |
调用类型名称:oneway 发送请求后就不管了,不在乎结果如何的情况下使用 |
使用Bolt协议时默认的超时是3s。你可以在多个级别设置超时。
在服务级别设置:
<sofa:reference interface="com.example.demo.SampleService" id="sampleService"> <sofa:binding.bolt> <sofa:global-attrs timeout="2000"/> </sofa:binding.bolt> </sofa:reference>
@SofaReference(binding = @SofaReferenceBinding(bindingType = "bolt", timeout = 2000)) private SampleService sampleService;
在方法级别设置:
<sofa:reference interface="com.example.demo.SampleService" id="sampleService"> <sofa:binding.bolt> <sofa:method name="hello" timeout="2000"/> </sofa:binding.bolt> </sofa:reference>
SOFARPC允许消费者在不知道服务接口的情况下发起调用。前提条件是:
SOFARPC支持Hessian 2、protobuf两个串行化协议,默认使用前者。如果要修改,参考:
sofa:service ref="sampleService" interface="com.alipay.sofarpc.demo.SampleService"> <sofa:binding.bolt> <sofa:global-attrs serialize-type="protobuf"/> </sofa:binding.bolt> </sofa:service> <!-- 提供者/消费者都需要设置 --> <sofa:reference interface="com.alipay.sofarpc.demo.SampleService" id="sampleServiceRef" jvm-first="false"> <sofa:binding.bolt> <sofa:global-attrs serialize-type="protobuf"/> </sofa:binding.bolt> </sofa:reference>
<bean id="helloService" class="com.alipay.sofa.rpc.quickstart.HelloService"/> <!-- 自定义一个线程池 --> <bean id="customExecutor" class="com.alipay.sofa.rpc.server.UserThreadPool" init-method="init"> <property name="corePoolSize" value="10" /> <property name="maximumPoolSize" value="10" /> <property name="queueSize" value="0" /> </bean> <sofa:service ref="helloService" interface="XXXService"> <sofa:binding.bolt> <!-- 引用线程池 --> <sofa:global-attrs thread-pool-ref="customExecutor"/> </sofa:binding.bolt> </sofa:service>
或者,使用注解方式:
@SofaService(bindings = {@SofaServiceBinding(bindingType = "bolt", userThreadPool = "customThreadPool")}) public class SampleServiceImpl implements SampleService { }
SOFARPC支持RESTful协议,使用此协议时,你需要为服务接口添加JAX-RS注解:
@Path("sample") public interface SampleService { @GET @Path("hello") String hello(); }
发布服务的方式如下:
@Service // 设置绑定类型 @SofaService(bindings = {@SofaServiceBinding(bindingType = "rest")}) public class RestfulSampleServiceImpl implements SampleService { @Override public String hello() { return "Hello"; } }
这种服务直接可以通过浏览器访问: http://localhost:8341/sample/hello
在SOFARPC客户端,消费服务的方式如下:
@SofaReference(binding = @SofaReferenceBinding(bindingType = "rest")) private SampleService sampleService;
SOFARPC支持多种注册中心。当前bolt、rest、duboo传输协议均支持ZooKeeper作为注册中心,bolt、rest还支持本地文件系统作为注册中心(主要用于测试)。
当前SOFARPC(SOFARPC: 5.5.2, SOFABoot: 2.6.3)已经支持SOFARegistry注册中心:
com.alipay.sofa.rpc.registry.address=sofa://127.0.0.1:9603
要使用此注册中心,配置:
com.alipay.sofa.rpc.registry.address=zookeeper://127.0.0.1:2181 # 指定身份验证信息 com.alipay.sofa.rpc.registry.address=zookeeper://xxx:2181?file=/home/admin/registry&scheme=digest&addAuth=sofazk:rpc1
要使用此注册中心,配置:
com.alipay.sofa.rpc.registry.address=local:///home/admin/registry/localRegistry.reg
服务消费者可以直接指定服务提供者的地址:
ConsumerConfig consumer = new ConsumerConfig() .setInterfaceId(HelloService.class.getName()) .setRegistry(registryConfig) .setDirectUrl("bolt://127.0.0.1:12201");
<sofa:reference interface="com.alipay.sample.HelloService" id="helloService"> <sofa:binding.bolt> <sofa:route target-url="127.0.0.1:12200"/> </sofa:binding.bolt> </sofa:reference>
@SofaReference(binding = @SofaReferenceBinding(bindingType = "bolt", directUrl = "127.0.0.1:12220")) private SampleService sampleService;
而不经过SOFARPC的负载均衡系统。
目前支持的负载均衡算法:
算法 | 说明 |
random | 随即选取服务提供者,默认 |
localPref | 如果存在本地可用的服务提供者,优先使用之 |
roundRobin | 轮询算法 |
consistentHash | 一致性哈希算法,每个相同方法级别的请求路由到同一节点 |
weightRoundRobin | 加权的轮询 |
配置负载均衡算法的方式如下:
<sofa:reference interface="com.example.demo.SampleService" id="sampleService"> <sofa:binding.bolt> <sofa:global-attrs loadBalancer="roundRobin"/> </sofa:binding.bolt> </sofa:reference>
重试的配置方式如下:
<sofa:reference jvm-first="false" id="retriesServiceReferenceBolt" interface="com.alipay.sofa.rpc.samples.retries.RetriesService"> <sofa:binding.bolt> <sofa:global-attrs retries="2"/> </sofa:binding.bolt> </sofa:reference>
@SofaReference(binding = @SofaReferenceBinding(bindingType = "bolt", retries = 2)) private SampleService sampleService;
SOFARPC目前支持以下Tracer:
如果要禁用分布式追踪特性,可以配置:
com.alipay.sofa.rpc.defaultTracer=
SOFARPC支持内置的容错机制,还可以和Hystrix进行集成。
运行机制:
使用这种单机故障剔除,需要进行如下配置:
FaultToleranceConfig faultToleranceConfig = new FaultToleranceConfig(); faultToleranceConfig.setRegulationEffective(true); faultToleranceConfig.setDegradeEffective(true); // 时间窗口 20s faultToleranceConfig.setTimeWindow(20); // 如果被判定为故障,则权重掉1/2 faultToleranceConfig.setWeightDegradeRate(0.5); FaultToleranceConfigManager.putAppConfig("appName", faultToleranceConfig);
基于Hystrix的熔断能力,目前还不健全。要使用此特性,添加Hystrix的依赖:
<dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-core</artifactId> <version>1.5.12</version> </dependency>
然后显式启用Hystrix(导致Hystrix过滤器被加载):
// 全局开启 RpcConfigs.putValue(HystrixConstants.SOFA_HYSTRIX_ENABLED, true); // 对特定 Consumer 开启 ConsumerConfig consumerConfig = new ConsumerConfig() .setInterfaceId(HelloService.class.getName()) .setParameter(HystrixConstants.SOFA_HYSTRIX_ENABLED, String.valueOf(true));
FallbackFactory接口用于注入Hystrix的Fallback,当Hystrix遇到异常、超时、线程池拒绝、熔断等情况时,指定执行此Fallback(降级)逻辑:
// 可以直接使用默认的 FallbackFactory 直接注入 Fallback 实现 SofaHystrixConfig.registerFallback(consumerConfig, new HelloServiceFallback()); // 也可以自定义 FallbackFactory 直接注入 FallbackFactory SofaHystrixConfig.registerFallbackFactory(consumerConfig, new HelloServiceFallbackFactory());
SOFARPC注册了JVM的ShutdownHook,用于实现优雅的资源清理。
这是一个服务注册中心,和ZooKeeper、Etcd等项目对比有自己的特点:
在架构上,SOFARegistry引入4种角色:
术语 | 说明 |
服务 Service |
通过网络提供的、具有特定业务逻辑处理能力的软件功能 |
服务提供者 Service Provider |
通过网络提供服务的计算机节点 |
服务消费者 Service Consumer |
通过网络调用服务的计算机节点。一个计算机节点可以既作为一些服务的提供者,又作为一些服务的消费者 |
服务发现 Service Discovery |
服务消费者获取服务提供者的网络地址的过程 |
服务注册中心 Service Registry |
一种提供服务发现功能的软件系统,帮助服务消费者获取服务提供者的网络地址 |
术语 | 说明 |
数据(Data) | 在服务发现场景下,特指服务提供者的网络地址及其它附加信息。其他场景下,也可以表示任意发布到 SOFARegistry 的信息 |
单元(Zone) | 单元化架构关键概念,在服务发现场景下,单元是一组发布与订阅的集合,发布及订阅服务时需指定单元名 |
发布者(Publisher) | 发布数据到 SOFARegistry 的节点。在服务发现场景下,服务提供者就是“服务提供者的网络地址及其它附加信息”的发布者 |
订阅者(Subscriber) | 从 SOFARegistry 订阅数据的节点。在服务发现场景下,服务消费者就是“服务提供者的网络地址及其它附加信息”的订阅者 |
数据标识(DataId) | 用来标识数据的字符串。在服务发现场景下,通常由服务接口名、协议、版本号等信息组成,作为服务的标识 |
分组标识(GroupId) | 用于为数据归类的字符串,可以作为数据标识的命名空间,即只有 DataId、GroupId、InstanceId 都相同的服务,才属于同一服务 |
实例 ID(InstanceId) | 实例 ID,可以作为数据标识的命名空间,即只有DataId、GroupId、InstanceId都相同的服务,才属于同一服务 |
会话服务器(SessionServer) | SOFARegistry 内部负责跟客户端建立 TCP 长连接、进行数据交互的一种服务器角色 |
数据服务器(DataServer) | SOFARegistry 内部负责数据存储的一种服务器角色。 |
元信息服务器(MetaServer) | SOFARegistry 内部基于 Raft 协议,负责集群内一致性协调的一种服务器角色。 |
支持独立部署、集成部署方式。后者比较简单,单节点部署,可以用于测试。
wget https://github.com/alipay/sofa-registry/releases/download/v5.2.0/registry-integration-5.2.0.tar.gz mkdir registry-integration tar -zxvf registry-integration-5.2.0.tar.gz -C registry-integration cd registry-integration # 启动脚本位于 bin/startup.sh
启动服务器后,执行下面的命令查看各服务器端角色的健康状况:
# 查看meta角色的健康检测接口: curl http://localhost:9615/health/check # {"success":true,"message":"... raftStatus:Leader"} # 查看data角色的健康检测接口: curl http://localhost:9622/health/check # {"success":true,"message":"... status:WORKING"} # 查看session角色的健康检测接口: curl http://localhost:9603/health/check # {"success":true,"message":"..."}
引入依赖:
<dependency> <groupId>com.alipay.sofa</groupId> <artifactId>registry-client-all</artifactId> </dependency>
下面的代码演示了如何发布数据到SOFARegistry上:
// 构建客户端实例 RegistryClientConfig config = DefaultRegistryClientConfigBuilder.start() // 服务器连接地址,任意一个Session节点都可以 .setRegistryEndpoint("127.0.0.1").setRegistryEndpointPort(9603).build(); DefaultRegistryClient registryClient = new DefaultRegistryClient(config); registryClient.init(); // 构造发布者注册表 // dataId是此客户端发布的服务的唯一标识 String dataId = "com.alipay.test.demo.service:1.0@DEFAULT"; PublisherRegistration registration = new PublisherRegistration(dataId); // 将注册表注册进客户端并发布数据 registryClient.register(registration, "10.10.1.1:12200?xx=yy");
下面的代码演示了如何从SOFARegistry订阅数据:
// 构建客户端实例 RegistryClientConfig config = DefaultRegistryClientConfigBuilder.start() .setRegistryEndpoint("127.0.0.1").setRegistryEndpointPort(9603).build(); DefaultRegistryClient registryClient = new DefaultRegistryClient(config); registryClient.init(); // 创建 SubscriberDataObserver SubscriberDataObserver subscriberDataObserver = new SubscriberDataObserver() { public void handleData(String dataId, UserData userData) { // 在这里处理接收到的数据 System.out.println("receive data success, dataId: " + dataId + ", data: " + userData); } }; // 构造订阅者注册表 String dataId = "com.alipay.test.demo.service:1.0@DEFAULT"; SubscriberRegistration registration = new SubscriberRegistration(dataId, subscriberDataObserver); // 订阅的范围:zone, dataCenter, global registration.setScopeEnum(ScopeEnum.global); // 将注册表注册进客户端并订阅数据,订阅到的数据会以回调的方式通知 SubscriberDataObserver registryClient.register(registration);
有数据更新后,服务器会推送并回调,你需要在回调中处理 UserData:
public interface UserData { // 以Zone分组的数据 Map<String, List> getZoneData(); // 当前Zone String getLocalZone(); }
遵循OpenTracing规范的Tracer。可以将将一个Trace的链路信息打印到日志或发送给Zipkin进行展示。 特性包括:
术语 | 说明 |
TraceId |
在SOFATracer 中代表一次请求的唯一标识,此 ID 一般由集群中第一个处理请求的系统产生,并在分布式调用下通过网络传递到下一个被请求系统 TraceId的示例: # 启动Trace的那个服务器的IP地址,十六进制 # Trace产生的时间戳 # 自增序列 # 当前进程ID 0ad1348f 1403169275002 1003 56696 |
SpanId | SpanId 代表了本次请求在整个调用链路中的位置或者说层次,比如 A 系统在处理一个请求的过程中依次调用了 B,C,D 三个系统,那么这三次调用的的 SpanId 分别是:0.1,0.2,0.3。如果 B 系统继续调用了 E,F 两个系统,那么这两次调用的 SpanId 分别是:0.1.1,0.1.2 |
配置项 | 说明 |
logging.path |
日志输出目录 SOFATracer 会优先输出到 logging.path 目录下;如果没有配置日志输出目录,那默认输出到 ${user.home} |
com.alipay.sofa.tracer.disableDigestLog |
是否关闭所有集成 SOFATracer 组件摘要日志打印 默认false |
com.alipay.sofa.tracer.disableConfiguration[${logType}] |
关闭指定 ${logType} 的 SOFATracer 组件摘要日志打印。${logType}是指具体的日志类型,如:spring-mvc-digest.log 默认false |
com.alipay.sofa.tracer.tracerGlobalRollingPolicy |
SOFATracer 日志的滚动策略 .yyyy-MM-dd:按照天滚动,默认 |
com.alipay.sofa.tracer.tracerGlobalLogReserveDay |
SOFATracer 日志的保留天数 默认保留 7 天 |
com.alipay.sofa.tracer.statLogInterval |
统计日志的时间间隔,单位:秒 默认 60 秒统计日志输出一次 |
com.alipay.sofa.tracer.baggageMaxLength |
透传数据能够允许存放的最大长度 默认值 1024 |
com.alipay.sofa.tracer.zipkin.enabled |
是否开启 SOFATracer 远程上报数据到 Zipkin true:开启上报;false:关闭上报。默认不上报 |
com.alipay.sofa.tracer.zipkin.baseUrl |
SOFATracer 远程上报数据到 Zipkin 的地址,com.alipay.sofa.tracer.zipkin.enabled=true时配置此地址才有意义 格式:http://${host}:${port} |
com.alipay.sofa.tracer.springmvc.filterOrder | SOFATracer 集成在 SpringMVC 的 Filter 生效的 Order |
com.alipay.sofa.tracer.springmvc.urlPatterns |
SOFATracer 集成在 SpringMVC 的 Filter 生效的 URL Pattern 路径 默认/*,全部生效 |
SLF4J 提供了 MDC (Mapped Diagnostic Contexts)功能,支持用户定义和修改日志的输出格式以及内容。SOFATracer支持基于MDC来输出当前Trace上下文的TraceId、SpanId。
需要引入SLF4J的API,以及日志实现包的Maven依赖:
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </dependency> <!-- Logback --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> <!-- Log4j2 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> <!--SOFABoot没有管控log4j2 版本 --> <version>1.4.2.RELEASE</version> </dependency>
配置日志输出的PatternLayout:
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%X{SOFA-TraceId}, %X{SOFA-SpanId}] -- %m%n</pattern>
%X表示,在调用appender输出日志时,将此占位符替换为当前线程上下文(MDC)中的SOFA-TraceId、SOFA-SpanId变量。
如果用户启动新线程来处理业务,则基于线程本地变量传递的Trace信息就丢失了。为了将SOFATracer日志上下文从父线程传递到子线程,可以考虑用SofaTracerRunnable:
// 使用SOFATracer提供的包装器 Thread thread = new Thread(new SofaTracerRunnable(new Runnable() { @Override public void run() { // 异步业务逻辑 } })); thread.start();
基于java.util.concurrent.Callable发动新线程时类似:
ExecutorService executor = Executors.newCachedThreadPool(); // 使用SOFATracer提供的包装器 SofaTracerCallable<Object> sofaTracerSpanSofaTracerCallable = new SofaTracerCallable<Object>(new Callable<Object>() { @Override public Object call() throws Exception { // 异步业务逻辑 return ...; } }); Future<Object> futureResult = executor.submit(sofaTracerSpanSofaTracerCallable); // ... Object objectReturn = futureResult.get();
SOFATracer支持两种采样模式:
进行以下配置即可:
# 采样率0-100之间 com.alipay.sofa.tracer.samplerPercentage=100 # 采样模式类型名称 com.alipay.sofa.tracer.samplerName=PercentageBasedSampler
此项目解决的是监控领域的问题,提供指标的埋点、收集、加工、存储、查询等服务,分为客户端、服务器两个部分。
此项目已经快一年没有更新。
扩展了Istio项目,并做了以下改进:
SOFARPC、Dubbo之类的入侵式框架可以在Istio中运行,但是无法对其进行任何管理、监控。SOFAMesh解决了这些痛点。
MOSN(Modular Observable Smart Network,模块化可观察智能网络…)是Envoy的替代品,其出现的主要原因不是Envoy不行,而是阿里系不愿引入C++技术栈。
MOSN增加了对SOFARPC、Dubbo协议的支持,后者仍然在开发中。
Leave a Reply