专栏名称: 分布式实验室
最专业的Docker文章,最权威的Docker新闻。关注容器生态圈的发展。
目录
相关文章推荐
51好读  ›  专栏  ›  分布式实验室

爱油科技基于Spring Cloud的微服务实践

分布式实验室  · 公众号  · 后端  · 2016-11-28 07:50

正文


本次分享主要介绍了爱油科技基于Docker和Spring Cloud将整体业务微服务化的一些实践经验。

从单体应用到微服务

单体应用

对于单体应用来说,优点很多,例如:

  • 小而美,结构简单易于开发实现

  • 部署门槛低,单个Jar包或者网站打包即可部署

  • 可快速实现多实例部署

然而随着业务复杂性的上升,业务规模的扩大,缺点也显现出来,例如:

  • 随着业务发展更多的需求被塞进系统,体系结构逐渐被侵蚀反应堆林立

  • 被技术绑架,难以为特定业务选择平台或框架,尽管可能有更适宜的技术做这件事

  • 协作困难,不同业务的团队在一个系统上进行开发相互冲突

  • 难以扩展,为了热点业务而不得不同时扩容全部业务,或者难以继续扩容

因此微服务技术作为一项对分布式服务治理的架构模式,逐渐被大家认识了。

架构拆分

实施微服务,首先对我们的架构进行了拆分:按行分层,按列分业务。

在我们的微服务体系中,所有的服务被划分为了三个层次:

  1. 基础设施层:为所有业务提供基础设施,包括服务注册、数据库和NoSQL、对象存储、消息队列等基础设施服务,这一层通常是由成熟组件、第三方服务组成。

  2. 业务服务层:业务微服务,根据业务领域每个子域单独一个微服务,分而治之。

  3. 接入层:直接对外提供服务,例如网站、API接口等。接入层不包含复杂的业务逻辑,只做呈现和转换。

实践中我们主要关注业务服务层和接入层,对于没有足够运维力量的我们,基础设施使用云服务是省事省力的选择。

业务服务层我们给他起名叫作Epic,接入层我们起名Rune,建立之初便订立了如下原则:

  1. 业务逻辑层内所有服务完全对等,可相互调用

  2. 业务逻辑层所有服务必须是无状态的

  3. 接入层所有服务可调用业务逻辑层所有服务,但接入层内部同层服务之间不可调用

  4. 接入层不能包含业务逻辑代码

  5. 所有微服务必须运行在Docker容器里

业务逻辑层我们主要使用使用Java,接入层我们主要使用PHP或Node。后来随着团队的成长,逐步将接入层全部迁移至Node。

框架选型

爱油科技作为一家成品油行业的初创型公司,需要面对非常复杂的业务场景,而且随着业务的发展,变化的可能性非常高。所以在微服务架构设计之初,我们就期望我们的微服务体系能:

  • 不绑定到特定的框架、语言

  • 服务最好是Restful风格

  • 足够简单,容易落地,将来能扩展

  • 和Docker相容性好

目前常见的微服务相关框架:

  • Dubbo、DubboX

  • Spring Cloud

  • Motan

  • Thrift、gRPC

这些常见的框架中,Dubbo几乎是唯一能被称作全栈微服务框架的“框架”,它包含了微服务所需的几乎所有内容,而DubboX作为它的增强,增加了REST支持。

它优点很多,例如:

  • 全栈,服务治理的所有问题几乎都有现成答案

  • 可靠,经过阿里实践检验的产品

  • 实践多,社区有许多成功应用Dubbo的经验

不过遗憾的是:

  • 已经停止维护

  • 不利于裁剪使用

  • “过于Java”,与其他语言相容性一般

Motan是微博平台微服务框架,承载了微博平台千亿次调用业务。

优点是:

  • 性能好,源自于微博对高并发和实时性的要求

  • 模块化,结构简单,易于使用

  • 与其他语言相容性好

不过:

  • 为“短平快”业务而生,即业务简单,追求高性能高并发。

Apache Thrift、gRPC等虽然优秀,并不能算作微服务框架,自身并不包括服务发现等必要特性。

如果说微服务少不了Java,那么一定少不了Spring,如果说少不了Spring,那么微服务“官配”Spring Cloud当然是值得斟酌的选择。

Spring Cloud优点:

  • “不做生产者,只做搬运工”

  • 简单方便,几乎零配置

  • 模块化,松散耦合,按需取用

  • 社区背靠Spring大树

当然它有很多不足之处,例如:

  • 轻量并非全栈

  • 没解决RPC的问题

  • 实践案例少

根据我们的目标,我们最终选择了Spring Cloud作为我们的微服务框架,原因有4点:

  • 虽然Dubbo基础设施更加完善,但结构复杂,我们很难吃得下,容易出坑;

  • 基于 Apache Thrift gRPC 自研,投入产出比很差;

  • 不想过早引入RPC以防滥用,Restful风格本身就是一种约束;

  • 做选择时, Motan 还没有发布。

因此Spring Cloud成为了理性的选择。

Spring Cloud

Spring Cloud是一个集成框架,将开源社区中的框架集成到Spring体系下,几个重要的家族项目:

  • spring-boot ,一改Java应用程序运行难、部署难,甚至无需Web容器,只依赖JRE即可

  • spring-cloud-netflix ,集成Netflix优秀的组件Eureka、HystrixRibbon、Zuul,提供服务发现、限流、客户端负载均衡和API网关等特性支持

  • spring-cloud-config ,微服务配置管理

  • spring-cloud-consul ,集成Consul支持

当然,SpringCloud下子项目非常多,这里就不一一列出介绍了。

服务发现和配置管理

Spring Cloud Netflix提供了Eureka服务注册的集成支持,不过没选它是因为:

  • 更适合纯Java平台的服务注册和发现

  • 架构中仍然需要其他分布式KV服务,没解决我们的核心问题

Docker作为支撑平台的重要技术之一,Consul几乎也是我们的必选服务。因此我们觉得一事不烦二主,理所应当的Consul成为我们的服务注册中心。

Consul的优势:

  • 使用Raft一致性算法,能保证分布式集群内各节点状态一致

  • 提供服务注册、服务发现、服务状态检查

  • 支持HTTP、DNS等协议

  • 提供分布式一致性KV存储

也就是说,Consul可以一次性解决我们对服务注册发现、配置管理的需求,而且长期来看也更适合跟不同平台的系统,包括和Docker调度系统进行整合。

最初打算自己开发一个Consul和Spring Cloud整合的组件,不过幸运的是,我们做出这个决定的时候, spring-cloud-consul 刚刚发布了,我们可以拿来即用,这节约了很多的工作量。

因此借助Consul和 spring-cloud-consul ,我们实现了:

  • 服务注册,引用了 srping-cloud-consul 的项目可以自动注册服务,也可以通过HTTP接口手动注册,Docker容器也可以自动注册

  • 服务健康状态检查,Consul可以自动维护健康的服务列表

  • 异构系统可以直接通过Consul的HTTP接口拉取并监视服务列表,或者直接使用DNS解析服务

  • 通过分布式一致性KV存储进行微服务的配置下发

  • 为一些业务提供选主和分布式锁服务

当然也踩到了一些坑:

spring-cloud-consul 服务注册时不能正确选判本地ip地址。对于我们的环境来说,无论是在服务器上,还是Docker容器里,都有多个网络接口同时存在,而 spring-cloud-consul 在注册服务时,需要先选判本地服务的IP地址,判断逻辑是以第一个非本地地址为准,常常错判。因此在容器中我们利用entrypoint脚本获取再通过环境变量强制指定。

#!/usr/bin/env bash
set -e

if [ -n "$RUN_IN_RANCHER" ]; then
echo "Waiting for ip address..."
sleep 5

RANCHER_MS_BASE=http://rancher-metadata/2015-12-19
PRIMARY_IP=`curl -sSL $RANCHER_MS_BASE/self/container/primary_ip`
SERVICE_INDEX=`curl -sSL $RANCHER_MS_BASE/self/container/service_index`

if [ -n "$PRIMARY_IP" ]; then
    export SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME=$PRIMARY_IP
fi

echo "Starting service #${SERVICE_INDEX-1} at $PRIMARY_IP."
fi

exec "$@"

我们的容器运行在Rancher中,所以可以利用Rancher的metadata服务来获取容器的IP地址,再通过 SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME 环境变量来设置服务发现的注册地址。基于其他容器调度平台也会很相似。

服务集成

为了方便开发人员使用,微服务框架应当简单容易使用。对于很多微服务框架和RPC框架来说,都提供了很好的机制。在Spring Cloud中通过 OpenFeign 实现微服务之间的快速集成:

服务方声明一个Restful的服务接口,和普通的Spring MVC控制器几乎别无二致:

@RestController
@RequestMapping("/users")
public class UserResource {
@RequestMapping(value = "{id}", method = RequestMethod.GET, produces = "application/json")
public UserRepresentation findOne(@PathVariable("id") String id) {
    User user = this.userRepository.findByUserId(new UserId(id));

    if (user == null || user.getDeleted()) {
        throw new NotFoundException("指定ID的用户不存在或者已被删除。");
    }

    return new UserRepresentation(user);
}
}

客户方使用一个微服务接口,只需要定义一个接口:

@FeignClient("epic-member-microservice")
public interface UserClient {

@Override
@RequestMapping(value = "/users/{id}", method = RequestMethod.GET, produces = "application/json")
User findOne(@PathVariable("id") String id);
}

在需要使用 UserClient 的Bean中,直接注入 UserClient 类型即可。事实上, UserClient 和相关VO类,可以直接作为公共接口封装在公共项目中,供任意需要使用的微服务引用,服务方Restful Controller直接实现这一接口即可。

OpenFeign 提供了这种简单的方式来使用Restful服务,这大大降低了进行接口调用的复杂程度。

对于错误的处理,我们使用HTTP状态码作为错误标识,并做了如下规定:

  • 4xx用来表示由于客户方参数错误、状态不正确、没有权限、操作冲突等种种原因导致的业务错误。

  • 5xx用来表示由于服务方系统异常、无法服务等原因服务不可用的错误。

对于服务器端,只需要在一个异常类上添加注解,即可指定该异常的HTTP响应状态码,例如:

@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {

public NotFoundException() {
    super("查找的资源不存在或者已被删除。");
}

public NotFoundException(String message) {
    super(message);
}

public NotFoundException(String message, Throwable cause) {
    super(message, cause);
}
}

对于客户端我们实现了自己的 FeignClientExceptionErrorDecoder 来将请求异常转换为对于的异常类,示例如下:

@Component
public class FeignClientExceptionErrorDecoder implements ErrorDecoder {

private final ErrorDecoder delegate = new ErrorDecoder.Default();

@Override
public Exception decode(String methodKey, Response response) {
    // Only decode 4xx errors.
    if (response.status() >= 500) {
        return delegate.decode(methodKey, response);
    }

    // Response content type must be json
    if (response.headers().getOrDefault("Content-Type", Lists.newArrayList()).stream()
        .filter(s -> s.toLowerCase().contains("json")).count() > 0) {
        try {
            String body = Util.toString(response.body().asReader());
            // 转换并返回异常对象
            ...
        } catch (IOException ex) {
            throw new RuntimeException("Failed to process response body.", ex);
        }
    }

    return delegate.decode(methodKey, response);
}
}

需要注意的是, decode 方法返回的4xx状态码异常应当是 HystrixBadRequestException 的子类对象,原因在于,我们把4xx异常视作业务异常,而不是由于故障导致的异常,所以不应当被Hystrix计算为失败请求,并引发断路器动作,这一点非常重要。

UserClient.findOne 方法的调用代码中,即可直接捕获相应的异常了:

try {
User user = this.userClient.findOne(new UserId(id));
} catch(NotFoundException ex) {
...
}

通过 OpenFeign ,我们大大降低了Restful接口进行服务集成的难度,几乎做到了无额外工作量的服务集成。

服务质量保证

微服务架构下,由于调用需要跨系统进行远程操作,各微服务独立运维,所以在设计架构时还必须考虑伸缩性和容错性,具体地说主要包括以下几点要求:

  • 服务实例可以平滑地加入、移除

  • 流量可以均匀地分布在不同的实例上

  • 接口应当资源隔离,防止因为个别接口调用时间过长导致线程池被占满而导致整个服务不可用

  • 能支持接口降级并隔离故障节点,防止集群雪崩

  • 服务能进行平滑升级

spring-cloud-netflix 和相关组件为我们提供了很好的解决方案:

  • Hystrix——实现了断路器模式,帮助控流和降级,防止集群雪崩,就像汽车的避震器

  • Ribbon——提供了客户端负载均衡器

  • Zuul——API网关模式,帮助实现接口的路由、认证等

下面主要介绍一下,各个组件在进行服务质量保证中是如何发挥作用的。

Consul

Consul中注册了一致性的可用的服务列表,并通过健康检查保证这些实例都是存活的,服务注册和检查的过程如下:







请到「今天看啥」查看全文


推荐文章
严歌苓  ·  聪明话没有任何价值
7 年前
点点星光  ·  想你,戒不了!
7 年前