Thrift API网关-第1部分

Thrift API Gateway — Part 1

微服务,无论采取什么措施,都是最近几年发明的最重要的概念之一。可能会尽可能长时间地抵制SOAP 2.0,但是它们迟早会来找您,并将您变成他们的信仰,或者您会来找他们,请用火和剑为自己洗礼。以及任何架构概念,微服务都有缺点。您需要在每个微服务中来自外部系统或其他微服务的请求中包含一些授权逻辑。该逻辑可以在微服务中直接"硬编码"(并且不重要的是一个单独的库),或者可以委派给其他微服务,也可以声明。"可以宣布"是什么意思?例如,可能同意特殊的HTTP标头或带有用户信息的某些数据结构出现在对每个微服务的每个请求中。并且必须绝对信任此结构中的数据。这三个选项都有缺点,但是在文章中我们将讨论最后一个。为了实现,通常使用API??网关模式:API

通常,API网关将请求的数量限制为内部服务,授权客户端的请求,进行日志记录和审核,在客户端之间分配请求,并在必要时转换数据。 甚至Nginx都可以用于API网关。 考虑针对用户请求的授权功能。 如果使用HTTP协议,则标准做法考虑在Authorization标头中添加特定令牌(不重要,因为我们收到了它):

1
Authorization: Bearer <some token>

在网关端,检查此标头,然后将标头交换到另一个标头,该标头包含向其写出令牌的用户的知识,例如其标识符。 另一个令牌将被转发到内部微服务:

1
Customer: <id>

看起来似乎很简单明了,但麻烦的是Apache Thrift由多个部分组成,例如馅饼:

1
2
3
4
5
6
7
8
9
10
11
12
13
+-------------------------------------------+
| Server                                    |
| (single-threaded, event-driven etc)       |
+-------------------------------------------+
| Processor                                 |
| (compiler generated)                      |
+-------------------------------------------+
| Protocol                                  |
| (JSON, compact, binary etc)               |
+-------------------------------------------+
| Transport                                 |
| (raw TCP, HTTP etc)                       |
+-------------------------------------------+

一般而言,我们不会陷入协议或传输中。 当然,可以选择某一项。 我们可以同意我们仅使用HTTP,但是它限制了传输转换的机会,并且它强制在微服务中执行某些外部处理器/过滤器(HTTP头不是Thrift的本机)。

这时出现了一个疯狂的主意:如果在通过我们的网关传递请求时,如果使用协议的可能性通过内部替代外部授权令牌,该怎么办?

约定优于配置

好的,让我们提供这样的服务:

1
2
3
4
5
6
service InternalTestService {  
    SomeReturnData getSomeData(
        1: UserData userData,
        2: RequestData requestData
    ) throws (1: SomeException e);
}

UserData 具有有关用户的某些信息。 据此,微服务返回特定用户的数据。 而且(我想您理解)不能从外界调用此服务。 但是有可能吗? 也许这样:

1
2
3
4
5
6
service ExternalTestService {  
    SomeReturnData getSomeData(
        1: AuthToken authData,
        2: RequestData requestData
    ) throws (1: SomeException e, 99: UnauthorizedException ue);
}

如您所见,第一个参数中的两个服务之间的区别和未授权异常为99字段(我希望没有人需要超过98个异常:))。 因此,我们只需要用内部令牌替换外部令牌即可。

内脏

不幸的是,Thrift的文档非常少。 所有指南,包括其中最好的指南,都没有触及内部协议的实现。 难过,但是很清楚。 在99%的情况下,开发人员不需要知道这一点,但是我们需要。

三种最受欢迎的协议:

  • 二进制-仅二进制(例如,字符串按UTF-8的原样传递)

  • 紧凑-二进制,但更紧凑

  • JSON-非常具体的JSON

  • 它们每个都有自己的实现,并由API封装。 从API的角度来看,二进制数据包如下所示:

    Protocol

    TMessage -有关消息的元信息。 它由名称,方法名称,服务中方法的类型序列号组成。 类型可能是:

  • CALL = 1-传入消息

  • 回复= 2-即将完成

  • EXCEPTION = 3-无评论

  • ONEWAY = 4-对于无效方法

  • 其他是有效载荷,打包在传入消息结构中。

    所有提出的协议都逐字节读取数据数组并存储其当前索引以继续从正确的位置读取。

    所以我们需要如下算法:

  • 读取TMessage

  • 阅读消息结构开始

  • 读取消息中第一个字段的meta

  • 存储当前数组索引

  • 读取令牌

  • 存储当前数组索引

  • 交换用户信息令牌

  • 序列化用户信息

  • 创建包含三个部分的新数组:

  • 从开始到项目4的存储索引

  • 项目8的序列化用户信息

  • 从项目6的存储索引到消息结束

  • 从开始到项目4的存储索引

  • 项目8的序列化用户信息

  • 从项目6的存储索引到消息结束

  • 测试一下

    没有测试,没有代码。 所以先写测试。 为了进行测试,我们需要以下服务:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    namespace java ru.aatarasoff.thrift

    exception SomeException {  
        1: string code
    }

    service ExternalTestService {  
        SomeReturnData getSomeData(
            1: AuthToken authData,
            2: RequestData requestData
        ) throws (1: SomeException e);
    }

    service InternalTestService {  
        SomeReturnData getSomeData(
            1: UserData userData,
            2: RequestData requestData
        ) throws (1: SomeException e);
    }

    struct SomeReturnData {  
        1: string someStringField,
        2: i32 someIntField
    }

    struct RequestData {  
        1: string someStringField,
        2: i32 someIntField
    }

    struct AuthToken {  
        1: string token,
        2: i32 checksum
    }

    struct UserData {  
        1: string id
    }

    创建外部服务并为其填充数据:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    TMemoryBuffer externalServiceBuffer = new TMemoryBufferWithLength(1024);

    ExternalTestService.Client externalServiceClient  
    = new ExternalTestService.Client(protocolFactory.getProtocol(externalServiceBuffer));

    externalServiceClient.send_getSomeData(  
        new AuthToken().setToken("sometoken").setChecksum(128),
        new RequestData().setSomeStringField("somevalue").setSomeIntField(8)
    );

    TMemoryBufferWithLength -我们需要创建新的类,因为 TMemoryBuffer 具有致命缺陷。 它不能返回实际的数组长度。 取而代之的是,它返回的缓冲区长度可能大于消息长度,因为保留了一些字节。

    方法 send_getSomeData 将消息序列化到缓冲区中。

    内部服务也是如此:

    1
    2
    3
    4
    internalServiceClient.send_getSomeData(  
      new UserData().setId("user1"),
      new RequestData().setSomeStringField("somevalue").setSomeIntField(8)
    );

    接下来,我们使用序列化数据创建字节数组:

    1
    2
    3
    4
    byte[] externalServiceMessage = Arrays.copyOf(  
        externalServiceBuffer.getArray(),
        externalServiceBuffer.length()
    );

    最后,我们创建将外部消息转换为内部消息的主类: MessageTransalator

    1
    2
    3
    4
    5
    6
    7
    8
    public MessageTransalator(TProtocolFactory protocolFactory, AuthTokenExchanger authTokenExchanger) {  
            this.protocolFactory = protocolFactory;
            this.authTokenExchanger = authTokenExchanger;
        }

    public byte[] process(byte[] thriftBody) throws TException {  
        //some actions
    }

    令牌交换的实现( AuthTokenExchanger )取决于特定的项目需求,因此我们引入以下接口:

    1
    2
    3
    4
    public interface AuthTokenExchanger<T extends TBase, U extends  TBase> {  
        T createEmptyAuthToken();
        U process(T authToken) throws TException;
    }

    方法 createEmptyAuthToken 应该返回一个对象,该对象显示为空令牌。 MessageTransalator稍后将填充它。 在方法过程中,我们必须实现令牌交换。 对于下面的简单测试实现就足够了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Override
    public AuthToken createEmptyAuthToken() {  
        return new AuthToken();
    }

    @Override
    public UserData process(AuthToken authToken) {  
        if ("sometoken".equals(authToken.getToken())) {
            return new UserData().setId("user1");
        }
        throw new RuntimeException("token is invalid");
    }

    然后添加断言:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    assert.assertTrue(  
       "Translated external message must be the same as internal message",
        Arrays.equals(
          new MessageTransalator(
              protocolFactory,
              new AuthTokenExchanger<AuthToken, UserData>() {}
          ).process(externalServiceMessage),
          internalServiceMessage
        )
    )

    运行测试,但它们不起作用。 太好了!

    绿灯

    根据算法实现过程方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    TProtocol protocol = createProtocol(thriftBody);

    int startPosition = findStartPosition(protocol);

    TBase userData = authTokenExchanger.process(  
        extractAuthToken(protocol, authTokenExchanger.createEmptyAuthToken())
    );

    int endPosition = findEndPosition(protocol);

    return  ArrayUtils.addAll(  
            ArrayUtils.addAll(
                getSkippedPart(protocol, startPosition),
                serializeUserData(protocolFactory, userData)
            ),
            getAfterTokenPart(protocol, endPosition, thriftBody.length)
    );

    我们使用 TMemoryInputTransport 传输方式,可以直接从输入缓冲区读取数据。

    1
    2
    3
    private TProtocol createProtocol(byte[] thriftBody) {  
        return protocolFactory.getProtocol(new TMemoryInputTransport(thriftBody));
    }

    实现找到令牌数据边界的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    private int findStartPosition(TProtocol protocol) throws TException {  
        skipMessageInfo(protocol);
        skipToFirstFieldData(protocol);
        return protocol.getTransport().getBufferPosition();
    }

    private int findEndPosition(TProtocol protocol) throws TException {  
        return protocol.getTransport().getBufferPosition();
    }

    private void skipToFirstFieldData(TProtocol protocol) throws TException {  
        protocol.readStructBegin();
        protocol.readFieldBegin();
    }

    private void skipMessageInfo(TProtocol protocol) throws TException {  
        protocol.readMessageBegin();
    }

    序列化用户数据:

    1
    2
    3
    4
    5
    6
    TMemoryBufferWithLength memoryBuffer = new TMemoryBufferWithLength(1024);  
    TProtocol protocol = protocolFactory.getProtocol(memoryBuffer);

    userData.write(protocol);

    return Arrays.copyOf(memoryBuffer.getArray(), memoryBuffer.length());

    运行测试,然后...

    夏洛克式

    因此,通过了针对Binary和Compact协议的测试,但未针对JSON进行测试。 怎么了 调试并查看比较的数组之间的差异:

    1
    2
    3
    4
    5
    //right JSON
    [1,"getSomeData",1,1,{"1":{"rec":{"1":{"str":"user1"}}},"2":{"rec":{"1":{"str":"somevalue"},"2":{"i32":8}}}}]

    //wrong JSON  
    [1,"getSomeData",1,1,{"1":{"rec"{"1":{"str":"user1"}}},"2":{"rec":{"1":{"str":"somevalue"},"2":{"i32":8}}}}]

    没看到区别吗? 但它是。 在第一个" rec" 符号之后被遗漏了。 我们使用一种API,但看到的结果却不同。 解决方案只有在认真阅读了 TJSONProtocol 类的代码之后才出现。 它具有字段:

    1
    TJSONProtocol.JSONBaseContext context_ = new TJSONProtocol.JSONBaseContext();

    此上下文在处理JSON结构时将不同的分隔符存储在堆栈中。 当读取结构时,它读取符号":",但不会返回分隔符,因为我们的用户数据对象没有任何上下文。

    使用 seriaizeUserData 方法手动添加符号:

    1
    2
    3
    if (protocol instanceof TJSONProtocol) {  
        memoryBuffer.write(COLON, 0, 1); //":"
    }

    然后运行测试,仅看到绿色。

    例外的兴起

    这还没有结束。 如果授权失败,我们已经忘记了异常处理。 好的,在99位置添加未授权例外:

    1
    2
    3
    4
    5
    6
    service ExternalTestService {  
        SomeReturnData getSomeData(
            1: AuthToken authData,
            2: RequestData requestData
        ) throws (1: SomeException e, 99: UnauthorizedException ue);
    }

    我们需要新的方法 processError

    1
    public byte[] processError(TException exception) throws Exception

    Thrift有两类异常,我们可以将它们序列化为输出消息。 首先是隐式声明的TApplicationException。 其次是在服务定义的 throws 部分中声明的自定义异常。 因此,如果在授权时出现意外异常,则应使用TApplicationException创建消息。 如果未授权用户并且我们知道这一点,则应使用UnauthorizedException创建消息。 我们开始做吧。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    if (TApplicationException.class.equals(exception.getClass())) {  
                protocol.writeMessageBegin(new TMessage(this.methodName, TMessageType.EXCEPTION, this.seqid));

                ((TApplicationException) exception).write(protocol);

                protocol.writeMessageEnd();
            } else {
                TStruct errorStruct = new TStruct(this.methodName +"_result");
                TField errorField = new TField("exception", TType.STRUCT, (short) 99);

                protocol.writeMessageBegin(new TMessage(this.methodName, TMessageType.REPLY, this.seqid));
                protocol.writeStructBegin(errorStruct);
                protocol.writeFieldBegin(errorField);

                exception.getClass().getMethod("write", TProtocol.class).invoke(exception, protocol);

                protocol.writeFieldEnd();
                protocol.writeFieldStop();
                protocol.writeStructEnd();
                protocol.writeMessageEnd();
            }

    一些评论。 根据节俭协议,如果TApplication升高,则应使用 TMessageType.EXCEPTION 类型的消息。 如果出现自定义异常- TMessageType.REPLY

    另外,我们需要在翻译器中引入状态,以存储在解析 TMessage 时应填写的 methodName seqid

    就这样。 现在我们可以做这样的事情:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    try {  
        byte[] processed = messageTransalator.process(request.getContentData());
        //do something
    } catch (TException e) {
        try {
          getResponse().getOutputStream().write(messageTransalator.processError(e));
        } catch (Exception e1) {
            log.error("unexpected error", e1);
        }
    }

    MessageTranslator 的完整列表在这里。

    链接

    <更多>
    Github:https://github.com/aatarasoff/thrift-api-gateway-coreBintray:https://bintray.com/aatarasoff/maven/thrift-api-gateway-core/view
    <铅>

    小吃高峰

    在下一部分中,我们将在Spring堆栈上构建网关。