码恋 码恋

ALL YOUR SMILES, ALL MY LIFE.

目录
Service Mesh 与 Feign client 服务统一封装
/    

Service Mesh 与 Feign client 服务统一封装

一、引言与牢骚

在微服务盛行的当下,会有很多 JAVA 开发者去选择 RPC 框架来做服务之间的调用,如 Dubbo、GRPC 等等,当然也许会有开发者使用 Spring Cloud 来构建微服务。

在微服务的初级阶段,开发者需要在自己的工程代码里边处理一系列由于微服务化带来的问题,如服务发现、负载均衡、熔断、重试、限流等等,其直接结果就是导致在业务代码以外增加了许多额外的工作,增加了一堆非业务的代码。

如 JEE 一样,随着微服务的持续发展,出现了一些类库和框架来针对这些问题的处理做封装,如我们所了解的 Spring Cloud 。只需要几行配置,几个注解,就能帮助开发者完成很多工作。

所以下一代微服务应运而生,Service Mesh 正汹涌而来。

A service mesh is a dedicated infrastructure layer for handling service-to-service communication. It’s responsible for the reliable delivery of requests through the complex topology of services that comprise a modern, cloud native application. In practice, the service mesh is typically implemented as an array of lightweight network proxies that are deployed alongside application code, without the application needing to be aware.

服务网格是一个基础设施层,用于处理服务间通信。云原生应用有着复杂的服务拓扑,服务网格负责在这些拓扑中实现请求的可靠传递。 在实践中,服务网格通常实现为一组轻量级网络代理,它们与应用程序部署在一起,而对应用程序透明。

Service Mesh 在以一种网络代理的形式,对服务的流量做控制,实现了服务限流、熔断等。

关于 Service Mesh 的更多,可以参考文末的 PPT 。

题外音

各种框架的出现,大大提升了开发效率。在框架用的很爽的同时,其实自己也有思考这样到底是好是坏。一方面,框架带来的生产力的提升是毋庸置疑的,可以让开发者更加专注于业务。而另一方面,框架表面看起来实在是太简单了,导致许多开发者只会写业务,而很难了解其工作原理。很多时候都会想,自己除了 CRUD 还会些什么?

完成一件事的路有长有短,一大部分人都会选择短的路。就比如开发这件事来说,很多人会选择用现有的框架。谁会为了构建一个微服务去开发一个框架呢?
条件不允许,时间也不允许,当然,你老板也不会允许,最重要的是,,,实力不允许,哈哈哈 😝。

这样就导致了业务在写了一段时间后,到达一个技术瓶颈。所以今后希望自己在陷入无穷无尽的业务的同时,能够多阅读一些源码叭。

二、使用 Feign 做声明式服务调用

Service Mesh 为我们解决了一堆业务以外乱七八糟的问题,现在开发者只需要专注业务的开发。通常 Web 服务以 HTTP 形式对外暴露服务,通过使用 Feign client ,可以简化我们通过 HTTP 对服务的调用。

现在我们要做的就是类似于使用 Dubbo 一样,抽象出一个公共的基础服务包,定义统一的响应格式,对服务认证等进一步封装,后面调用这些服务只需要引入这个 Jar 包,做一些简单的配置就可以实现各微服务之间的调用。

github地址:https://github.com/wangning1018/feign

三、使用时注意的问题

  1. 正确使用 Spring boot 以及 Spring cloud feign 的不同版本,随着版本的不同会引入一些由于依赖造成的问题。
  • spring boot 1.x
    对应1.x版本的,比如
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
            <version>1.4.7.RELEASE</version>
        </dependency>
  • spring boot 2.x后,依赖变更,目前使用其他更高版本有的会造成nested exception is java.lang.NoClassDefFoundError: feign/Request$HttpMethod错误,下面是可以使用的版本,比如:
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.0.1.RELEASE</version>
        </dependency>
  1. Feign 在默认情况下使用的是 JDK 原生的 URLConnection 发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用 HTTP 的 persistence connection。我们可以用 Apache 的 HttpClient 替换 Feign 原始的 HTTP Client,通过设置连接池、超时时间等对服务之间的调用调优。所以在这个包中,使用了 HttpClient ,除此之外还可以使用 OkHttpClient 。
  • 使用 http client
    引入 http client 对应依赖,并添加配置feign.httpclient.enabled=true
<dependency>
		<groupId>io.github.openfeign</groupId>
                <artifactId>feign-httpclient</artifactId>
	</dependency>
  • 使用okhttp client
    • 引入依赖
    • 添加配置
    • 增加配置类

配置项

feign.httpclient.enabled=false
	feign.okhttp.enabled: true

依赖

<dependency>
    		<groupId>io.github.openfeign</groupId>
    		<artifactId>feign-okhttp</artifactId>
	</dependency>

配置类

@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignClientOkHttpConfiguration {

    @Bean
    public OkHttpClient okHttpClient() {
        return new OkHttpClient.Builder()
                // 连接超时
                .connectTimeout(20, TimeUnit.SECONDS)
                // 响应超时
                .readTimeout(20, TimeUnit.SECONDS)
                // 写超时
                .writeTimeout(20, TimeUnit.SECONDS)
                // 是否自动重连
                .retryOnConnectionFailure(true)
                // 连接池
                .connectionPool(new ConnectionPool())
                .build();
    }

}

除此之外,还可以对最大连接数,最大路由等参数进行合理配置。

  1. 编写 Feign client 的接口时注意一定要指定 method , 支持使用 @GetMapping@PostMapping等注解,注意每个参数必须指定@RequestParam(value="xxx"),当参数不是必须时,必须指定@RequestParam(value="xxx", require=false)
  2. 设置统一的返回体时,类名和目的服务返回的类名不必相同,只要字段名称相同,Feign 即可完成相应的序列化,注意这个返回体必须要有一个无参构造函数,否则序列化时会报 no creator 的异常。
  3. 为了使用方便,在这个项目中定义了两个注解:@Host@Signature
    。可以自由配置每个 Feign client 的 host 和 验签。
  4. 在 Feign 的拦截器中做了对应的认证操作,可以自行扩展认证逻辑,对于不同的 Feign client 可以另外拓展一个签名 type ,分别做认证验签操作逻辑。
  5. 项目中使用了RequestContextHolder 来保存本次请求的额外参数,会遇到获得参数时为 null 的问题,这是由于 Feign 在使用Hystrix时,采取不同的隔离策略,传播 ThreadLocal 对象的问题。我们知道 RequestContextHolder 是使用
    ThreadLocal 来保存参数的,如下:
    image.png
  • Hystrix有隔离策略:THREAD以及SEMAPHORE。
    当隔离策略为 THREAD 时,是没办法拿到 ThreadLocal 中的值的。
    参考一个场景,使用Feign调用某个远程API,这个远程API需要传递一个Header,这个Header是动态的,与HttpRequest相关,所以项目中采用了一个拦截器来实现Header的传递,如下:
package com.aysaml.common.feign.interceptor;

import com.aysaml.common.feign.constant.Constants;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.apache.http.protocol.HTTP;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

/**
 * Host interceptor , to add host into header.
 *
 * @author wangning
 * @date 2019-10-29
 */
public class HostRequestInterceptor implements RequestInterceptor {

  @Override
  public void apply(RequestTemplate requestTemplate) {
    RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
    if (null != attributes) {
      String host = (String) attributes.getAttribute(Constants.FEIGN_HOST, 0);
      if (null != host) {
        requestTemplate.header(HTTP.TARGET_HOST, host);
      }
    }
  }
}

当开启feign.hystrix.enabled=true时,RequestContextHolder.getRequestAttributes(); 获得的是 null 。而Hystrix的默认隔离策略是 THREAD,所以在不同的线程中,获得 ThreadLocal 的值是 null。

有两种解决方案:
❀更改隔离策略:
hystrix.command.default.execution.isolation.strategy: SEMAPHORE

❀自定义并发策略:
参考Spring Cloud Sleuth以及Spring Security是如何传递 ThreadLocal 对象的,我们可以知道只需编写一个类,让其继承 HystrixConcurrencyStrategy ,并重写 wrapCallable 方法即可。

关于这块的详细内容:参见这篇博客

使用 Hystrix 进行服务的降级,只需要在 @FeignClient 中指定fallback相应的类即可。 详细的代码就不再这里做展示了,可以参见 github:https://github.com/wangning1018/feign ,如果有任何问题欢迎留言讨论。


服务网格 PPT service mesh next generationmicro service



❤ 转载请注明本文地址或来源,谢谢合作 ❤


center