微服务中 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 框架底层代码的灵活性可以满足这些需求

未登录用户
全部评论0
到底啦