剑客
关注科技互联网

微服务中 HTTP 消息头的转发

微服务不再是一种趋势。不管你喜欢还是不喜欢,他们都已存在。然而,在拥抱微服务架构和正确地实现它之间,还有一个巨大的鸿沟。在此提醒,首先需要检查很多 分布式计算的错误 。在所有的需求中,要求必须满足能够遵循一个 HTTP 请求的微服务所能参与的一个特定业务场景 —— 这样做的目的是监视和调试。

一个可能的实现是使用一个专用的 HTTP 头,并用一个不可变的值传递给每个微服务参与的调用链。在 Spring 的生态系统中, Spring Cloud Sleuth 是专用的:

“Spring Cloud Sleuth 为 Spring云实现了一个分布式的追踪解决方案,并大量采用了  Dapper Zipkin  和 HTrace。对大多数用户来说,Sleuth 是不可见的,它与你所有与外部系统的交互应该都是自动检测的。你可以捕捉简单的日志,并把它发送给远程控制服务。”

在Spring Boot项目的classpath中添加 Spring Cloud Sleuth 库会自动的在HTTP请求中添加两个 header:

  • X-B3-Traceid

  • 在一个事务的所有HTTP请求中共享,即:期望中的事务标识符

  • X-B3-Spanid

  • 标识事务中某一个微服务的工作

Spring Cloud Sleuth提供了一些自定义功能,如用一些附加代码 更换 header的名称。

与开箱即用的功能产生的分歧

虽然它们对于从头开始的项目来说都是一些十分好用的功能。但不幸的是我工作的项目有着不同的 背景

  • 第一个微服务没有产生事务ID——托管的门面代理却可以

  • 事务ID不是数字的—— Sleuth只能处理数字值

  • 需要增加一个header,目的是为同一个业务场景下的 不同 的调用链做分组

  • 需要添加第三个header。在调用链上的每个新服务将其递增

一个解决方案架构师的第一步应该是浏览API管理产品,如Apigee(近期已被Google收购),并且搜索到可以提供那些需求中的功能的产品。不幸的是,当前的场景下并不允许如此。

编码需求

在最后我使用Sprint框架完成了下面的编码:

1.读取和存储最最初请求的消息头。

2.将它们写入新的微服务中。

3.读取和存储微服务响应的消息头。

4.将它们写入启动程序的最终响应中。

微服务中 HTTP 消息头的转发

第一个步骤是创建负责保存所有必需消息头的实体。我们通俗的称他为HeadersHolder。不管你怎么说,我实在想不出一个更贴切的名字了。

private const val HOP_KEY = "hop"
private const val REQUEST_ID_KEY = "request-id"
private const val SESSION_ID_KEY = "session-id"

data class HeadersHolder (var hop: Int?,
                          var requestId: String?,
                          var sessionId: String?)

有趣的是决定实例放在哪个作用域更具相关性。毫无疑问,必须有多个实例,所以使用singleton并不合适。除此之外,由于数据必在多个请求之间存储,也不能使用prototype。最后,唯一可行方法是通过ThreadLocal来管理实例。

尽管可以直接管理ThreadLocal,但是我们要利用Spring的特性,因为它能轻松的添加新的 scope。当前已经有一个开箱即用的ThreadLocal的 scope,只需要将其注册到context。直接翻译为如下代码:

internal const val THREAD_SCOPE = "thread"

@Scope(THREAD_SCOPE)
annotation class ThreadScope

@Configuration
open class WebConfigurer {

    @Bean @ThreadScope
    open fun headersHolder() = HeadersHolder()

    @Bean open fun customScopeConfigurer() = CustomScopeConfigurer().apply {
        addScope(THREAD_SCOPE, SimpleThreadScope())
    }
}

下面实现上面的需求1和4:读取request的header然后写到response。同时在完成请求响应循环之后需要重置header,为下一个做准备。

同时也托管了holder类的更新,从而更面向对象友好。

data class HeadersHolder private constructor (private var hop: Int?,
                                              private var requestId: String?,
                                              private var sessionId: String?) {
    constructor() : this(null, null, null)

    fun readFrom(request: HttpServletRequest) {
        this.hop = request.getIntHeader(HOP_KEY)
        this.requestId = request.getHeader(REQUEST_ID_KEY)
        this.sessionId = request.getHeader(SESSION_ID_KEY)
    }

    fun writeTo(response: HttpServletResponse) {
        hop?.let { response.addIntHeader(HOP_KEY, hop as Int) }
        response.addHeader(REQUEST_ID_KEY, requestId)
        response.addHeader(SESSION_ID_KEY, sessionId)
    }

    fun clear() {
        hop = null
        requestId = null
        sessionId = null
    }
}

为使controller免于管理header,相关的代码应该写到filter或其他相似的组件中。在Spring MVC的生态中,可以翻译为一个interceptor。

abstract class HeadersServerInterceptor : HandlerInterceptorAdapter() {

    abstract val headersHolder: HeadersHolder

    override fun preHandle(request: HttpServletRequest,
                           response: HttpServletResponse, handler: Any): Boolean {
        headersHolder.readFrom(request)
        return true
    }

    override fun afterCompletion(request: HttpServletRequest, response: HttpServletResponse,
                                 handler: Any, ex: Exception?) {
        with (headersHolder) {
            writeTo(response)
            clear()
        }
    }
}

@Configuration open class WebConfigurer : WebMvcConfigurerAdapter() {

    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(object : HeadersServerInterceptor() {
            override val headersHolder: HeadersHolder
                get() = headersHolder()
        })
    }
}

注意调用clear()方法为下一个请求重置header holder。

最重要的一点是 抽象   headersHolder 属性,在它的作用域中,线程小于适配器,它不能被直接注入,它将在  Spring上下文启动的时候被注入。因此  Spring提供了 lookup方法进行注入。上述代码被直接转换到 Kotlin中。

前面的代码假设当前的微服务是 调用链 最终环节,它读取请求的头文件并且将它写入反馈中(不要忘了增加‘hop’计数器)。然尔,一个调用链 相关的 监视器不仅仅是针对一个链接。怎样才能把头文件传递给下一个微服务呢(或是返回头文件)–需要两个或是三个以上?

Sping提供了方便的抽象来处理客户端部分, ClientHttpRequestInterceptor 可以登记成REST模板。作用域不匹配时, 相样的注入技巧被拦截器处理程序使用

abstract class HeadersClientInterceptor : ClientHttpRequestInterceptor {

    abstract val headersHolder: HeadersHolder

    override fun intercept(request: HttpRequest, 
                           body: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse {
        with(headersHolder) {
            writeTo(request.headers)
            return execution.execute(request, body).apply {
                readFrom(this.headers)
            }
        }
    }
}

@Configuration
open class WebConfigurer : WebMvcConfigurerAdapter() {

    @Bean open fun headersClientInterceptor() = object : HeadersClientInterceptor() {
        override val headersHolder: HeadersHolder
            get() = headersHolder()
    }

    @Bean open fun oAuth2RestTemplate() = OAuth2RestTemplate(clientCredentialsResourceDetails()).apply {
        interceptors = listOf(headersClientInterceptor())
    }
}

在这段代码中, 每一个利用 oAuth2RestTemplate()的 REST请求由拦截器自动管理头文件

HeadersHolder仅仅需要快速更新:

data class HeadersHolder private constructor (private var hop: Int?,
                                              private var requestId: String?,
                                              private var sessionId: String?) {

    fun readFrom(headers: org.springframework.http.HttpHeaders) {
        headers[HOP_KEY]?.let {
            it.getOrNull(0)?.let { this.hop = it.toInt() }
        }
        headers[REQUEST_ID_KEY]?.let { this.requestId = it.getOrNull(0) }
        headers[SESSION_ID_KEY]?.let { this.sessionId = it.getOrNull(0) }
    }

    fun writeTo(headers: org.springframework.http.HttpHeaders) {
        hop?.let { headers.add(HOP_KEY, hop.toString()) }
        headers.add(REQUEST_ID_KEY, requestId)
        headers.add(SESSION_ID_KEY, sessionId)
    }
}

结论

开发微服务时Spring云提供了很多组件可以开箱即用。当需求超出 Spring云可以提供的时Spring 框架底层代码的灵活性可以满足这些需求

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址