3

我在 eu-west-1c 中有一个 AWS ec2 实例,它与目标服务器 - www.bitmex.com是同一个数据中心。如果我$ping bitmex.com从实例运行,平均往返时间约为 0.4 毫秒。对于$ping bitmex.com/api/v1(专门测试 API 端点)也是如此。但是,当我从一些 c++ 代码运行 http 请求时,bitmex.com/api/v1/order/bulk往返时间永远不会超过 ~10 毫秒,但这是一种罕见的情况,大多数请求所花费的时间在数百毫秒或更多。这使得这些调用和 ping 时间之间的差异会慢 1000 倍。

这些消息专门来自和来自订单引擎(由于 /order/bulk 端点),所以我知道一些延迟可能与另一端的服务器有一些处理要做的事实相关联。但是,我以前从未见过它这么慢 - 之前在对此进行分析时,我观察到对同一端点的请求与 ping 时间在同一区域的时间,如果有时不是更快的话。无论市场活动如何,因此也会出现这种延迟,因此 bitmex 端点有多忙。

这让我想知道问题是否出在我的代码、正在使用的 http 库、AWS 实例的设置或其他方面。我认为这里的一个关键点是,我使用 pycurl 作为 http 库在 python 版本中测试了相同的程序,它实际上在往返时间上略微优于 c++ 版本,我知道这与 python 与 c++ 无关现在是消息在网络上传输并再次返回的时候了(忽略内核和 http 库在幕后所做的事情),目的只是测试不同的语言和 http 库以查看问题是否存在。python 代码设法在 1 毫秒或接近 1 毫秒内返回一些消息,因此它的最佳情况下的性能比 C++ 代码的最佳情况下快 10 倍左右。

我在 C++ 中尝试的下一件事是先读取标题,然后读取正文,以防 Beast(我正在使用的 http 库)等待正文结束一段时间。分析表明,延迟完全是在发送请求和实际再次接收任何字节之间。我发现这一点是通过观察读取标题所花费的时间在 100 毫秒内,然后读取正文最多需要几微秒。因此,主体的数据必须已经在机器上,并且在调用 read 时准备就绪,但是等待消息的标头/初始字节。

因此,我想知道是什么原因造成的 - 是否有其他人对 bitmex API 的这个端点有同样的延迟?任何人都可以就如何调试或可能导致延迟的原因提供任何建议吗?

设置:

  • AWS 实例正在运行 Ubuntu Server 18
  • c++17 在 ubuntu 20(我的本地机器)上使用 g++9 编译,然后 scp 到 AWS 实例并在那里运行
  • 仅使用异步调用的 http Boost Beast 库
  • 所有电话都是https

我尝试和测试过的其他事情:

  • Ubuntu 20 与 18 - 没有区别
  • Boost asio 处理程序跟踪器 - 无法让它工作/不知道在哪里或如何查看它的输出
  • cURL c++ http 库 - 没有区别

这是我的应用程序的精简版本,它应该使用第 1 行中包含的用于编译它的命令来编译和运行 - 为了让它在交易所成功打开订单,您需要添加一个 API 密钥和秘密声明为类的成员变量。此代码连接到 bitmex.com,保持连接有效并向 bitmex.com/api/v1/bulk 发送 5 个相同的订单,并将响应时间打印到控制台:

// g++ -std=c++17 -pthread -o http_test.out http_test.cpp -lssl -lcrypto && ./http_test.out

//Boost & Beast headers
#include <boost/bind.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/beast/websocket/ssl.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/optional.hpp>

//REST headers
#include <sstream>
#include <openssl/evp.h>
#include <openssl/hmac.h>

//Misc. headers
#include <iomanip>
#include <iostream>
#include <string>

namespace beast     = boost::beast;         // from <boost/beast.hpp>
namespace http      = beast::http;          // from <boost/beast/http.hpp>
namespace websocket = beast::websocket;     // from <boost/beast/websocket.hpp>
namespace net       = boost::asio;          // from <boost/asio.hpp>
namespace ssl       = boost::asio::ssl;     // from <boost/asio/ssl.hpp>
using     tcp       = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>

using namespace std;


class BitMEX_MM : public std::enable_shared_from_this<BitMEX_MM> {
    
    int n_tests = 1;
    
    //REST
    tcp::resolver rest_resolver;
    beast::ssl_stream<beast::tcp_stream> rest_stream;
    beast::flat_buffer rest_buffer;
    
    http::request<http::string_body>  post_req;
    http::response<http::string_body> post_res;
    
    string limit_order_msg  = "{\"orders\":[";
    
    // Timing
    struct timespec start, end;
    
    //MEMBER VARIABLES
    string apiKey    = ""; //FILL IN API KEY
    string apiSecret = ""; //FILL IN API SEC
    int    apiKeyLen = apiKey.length();
    const char* apiKeyCStr = apiKey.c_str();
    int    apiSecLen = apiSecret.length();
    const char* apiSecCStr = apiSecret.c_str();
    int    expiry_t  = 5;
    
    //REST FUNCTIONS
    static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp)
    {
        ((string*)userp)->append((char*)contents, size * nmemb);
        return size * nmemb;
    }
    
    string HMAC_SHA256_hex_POST(string valid_till)
    {
        string data = "POST/api/v1/order/bulk" + valid_till + limit_order_msg;
        
        stringstream ss;
        unsigned int len;
        unsigned char out[EVP_MAX_MD_SIZE];
        HMAC_CTX *ctx = HMAC_CTX_new();
        HMAC_Init_ex(ctx, apiSecCStr, apiSecLen, EVP_sha256(), NULL);
        HMAC_Update(ctx, (unsigned char*)data.c_str(), data.length());
        HMAC_Final(ctx, out, &len);
        HMAC_CTX_free(ctx);
        
        for (int i = 0; i < len; ++i)
        {
            ss << std::setw(2) << std::setfill('0') << hex << (unsigned int)out[i];
        }
        return ss.str();
    }
    
    void
    REST_on_resolve(
        beast::error_code ec,
        tcp::resolver::results_type results)
    {
        // Make the connection on the IP address we get from a lookup
        beast::get_lowest_layer(rest_stream).async_connect(
            results,
            beast::bind_front_handler(
                &BitMEX_MM::REST_on_connect,
                shared_from_this()));
    }

    void
    REST_on_connect(beast::error_code ec,
                    tcp::resolver::results_type::endpoint_type)
    {       
        // Perform the SSL handshake
        rest_stream.async_handshake(
            ssl::stream_base::client,
            beast::bind_front_handler(
                &BitMEX_MM::REST_on_handshake,
                shared_from_this()));
    }
    
    void
    REST_on_handshake(beast::error_code ec)
    {       
        limit_order_msg += "{\"symbol\":\"XBTUSD\",\"ordType\":\"Limit\",\"execInst\":\"ParticipateDoNotInitiate\",\"clOrdID\":\"" + to_string(n_tests) \
                        + "\",\"side\":\"Buy\",\"price\":10.0" \
                        + ",\"orderQty\":2}]}";
        REST_write_limit_order_bulk();
    }
    
    void REST_write_limit_order_bulk()
    {
        int valid_till        = time(0) + 5;
        string valid_till_str = to_string(valid_till);
        
        post_req.set("api-expires", valid_till_str);
        post_req.set("api-signature", HMAC_SHA256_hex_POST(valid_till_str));
        post_req.set("Content-Length", to_string(limit_order_msg.length()));
        post_req.body() = limit_order_msg;
        
        clock_gettime(CLOCK_MONOTONIC, &start);
        
        http::write(rest_stream, post_req);
        http::read(rest_stream, rest_buffer, post_res);
        
        beast::error_code _ec;
        std::size_t       _bt;
        process_limit_order_bulk_res(_ec, _bt);
    }
    
    void process_limit_order_bulk_res(beast::error_code ec,
                                      std::size_t bytes_transferred)
    {
        clock_gettime(CLOCK_MONOTONIC, &end);
        double time_taken;
        time_taken = (end.tv_sec  - start.tv_sec) + ((end.tv_nsec - start.tv_nsec) * 1e-9);
        cout << "response time: " << time_taken << endl;
        
        ++n_tests;
        
        if (n_tests <= 5)
        {
            limit_order_msg  = "{\"orders\":[";
            limit_order_msg += "{\"symbol\":\"XBTUSD\",\"ordType\":\"Limit\",\"execInst\":\"ParticipateDoNotInitiate\",\"clOrdID\":\"" + to_string(n_tests) \
                            + "\",\"side\":\"Buy\",\"price\":10.0" \
                            + ",\"orderQty\":2}]}";
            REST_write_limit_order_bulk();
        }
    }
    
public:
        
    explicit
    BitMEX_MM(net::io_context& rest_ioc, ssl::context& rest_ctx)
        : rest_resolver(net::make_strand(rest_ioc)),
        rest_stream(net::make_strand(rest_ioc), rest_ctx)
    { }
    
    void
    run_REST_service()
    {           
        // Set SNI Hostname (many hosts need this to handshake successfully)
        if(! SSL_set_tlsext_host_name(rest_stream.native_handle(), "www.bitmex.com"))
        {
            beast::error_code ec{static_cast<int>(::ERR_get_error()), net::error::get_ssl_category()};
            std::cerr << "ssl err " << ec.message() << "\n";
            return;
        }
        
        // Set up an HTTP GET request message
        post_req.version(11);
        post_req.method(http::verb::post);
        post_req.target("/api/v1/order/bulk");
        post_req.set(http::field::host, "www.bitmex.com");
        post_req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
        post_req.set(http::field::accept, "*/*");
        post_req.set(http::field::content_type, "application/json");
        post_req.set(http::field::connection, "Keep-Alive");
        post_req.set("api-key", "");
        post_req.insert("Content-Length", "");
        post_req.insert("api-expires", "");
        post_req.insert("api-signature", "");
        
        // Look up the domain name
        rest_resolver.async_resolve(
            "www.bitmex.com",
            "443",
            beast::bind_front_handler(
                &BitMEX_MM::REST_on_resolve,
                shared_from_this()));
                
    }
    
};


int main(int argc, char** argv)
{
    
    net::io_context rest_ioc;
    ssl::context    rest_ctx{ssl::context::tlsv12_client};
    
    auto algo = make_shared<BitMEX_MM>(rest_ioc, rest_ctx);
    
    cout << "Running http test." << endl;
    
    algo->run_REST_service();
    
    rest_ioc.run();
    
    return 0;
}

来自并置服务器的结果:

Running http test.
response time: 0.0110592
response time: 0.00464095
response time: 0.00503278
response time: 0.00440869
response time: 0.00362543

似乎如您所料,由于与服务器建立连接,第一个响应需要更长的时间。在那之后,响应时间要快得多,但仍然比 ping 时间长约 10 倍。然而,它确实是可变的,另一次运行它会给出:

Running http test.
response time: 0.288384
response time: 0.188272
response time: 0.133779
response time: 0.0540737
response time: 0.0179791

慢得多。

4

1 回答 1

0

根据我对 bitmex 引擎的了解,订单执行的延迟约为 10 毫秒是你能得到的最好的,而且在高波动期间会更糟。检查https://bonfida.com/latency-monitor了解延迟。在加密世界中,延迟远高于传统的 hft

于 2020-11-19T12:43:45.080 回答