【问题标题】:Openssl handshake (SSL_accept) is slow in millisecondsOpenssl 握手 (SSL_accept) 以毫秒为单位慢
【发布时间】:2018-08-31 02:43:22
【问题描述】:

我正在用 C++ 编写一个服务器程序。 我为 linux 使用了 epoll 套接字多路复用,为安全层使用了 openssl 1.0.2

在 Centos 6.9 final 上(4 核,2GB 内存,x64 架构)。

这个服务器程序的目的是为我的项目提供一个 websocket/socket 服务器库。

我的问题是, SSL_accept 和 SSL_do_handshake 函数有点慢。

我为我的问题做了一些静态分析;

对于 50k EPOLL_CTL_ADD 使用(我的意思是,50k 用户同时/重复连接)我的主要阅读 函数(SSL_read、SSL_accept 逻辑)总共耗时 92.8 秒。

对于这 92.8 秒,SSL_accept(或 SSL_do_handshake [我试过])函数花费了 81.9 秒。

但是当我关闭我的 ssl 算法(使用定义)并重新强调时,读取主函数用法 [read(function] 在此静态中使用相同的函数用法花费 10.4 秒 50k。

平均值是;

  • 无 SSL:我的读取主函数从 epoll 循环 (10.4/50000) 中的每个读取调用(并执行我的上层函数)平均花费 0.2 毫秒

  • 使用 SSL:我的读取主函数从 epoll 循环 (92.8/50000) 中的每个读取调用(并执行我的上层函数)平均花费 1.85 毫秒

在这些条件下,我的服务器只能同时接受 18~19k 用户(我尝试了很多次)(SSL 版本比非 SSL 版本慢 9.25 倍)。在这些时候,我的服务器本身有很多锁定。

注意事项:

  • 在同一个VPS下,我试过nodejs、nginx等,没有像我的服务器那样锁定自己。
  • SSL 证书是从 comodo 购买的 ssl。
  • 我研究了数千次,我查看了大量服务器源代码数千次。

我的问题是,为什么我的 SSL_accept/SSL_do_handshake 函数很慢。

我在 3~5 个月内尝试了很多东西。但我想不通。

我的时间花费逻辑是:

###
timeMs = CurTime()
SSL_accept(...)
timeTotalVariable += (CurTime() - timeMs)
###

这是我的伪代码:

"call" ctx = SSL_CTX_new(SSLv23_method())
; close sslv2 & sslv3 protos
; set SSL_MODE_RELEASE_BUFFERS | SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_OP_NO_COMPRESSION | SSL_OP_CIPHER_SERVER_PREFERENCE | SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION
; set SSL_SESS_CACHE_OFF (if i use built-in openssl server cache, there is nothing big different)
; SSL_CTX_set_session_id_context ...
; SSL_CTX_set_verify(cx, SSL_VERIFY_NONE, NULL);
; SSL_CTX_set_options(ctx, SSL_OP_SINGLE_DH_USE);
; SSL_CTX_set_tmp_dh(ctx, ...
; SSL_CTX_use_certificate_chain_file(...
; SSL_CTX_use_PrivateKey_file(...
; SSL_CTX_check_private_key(...
; SSL_CTX_set_cipher_list(...
; SSL_CTX_set_options(ctx, SSL_OP_SINGLE_ECDH_USE);
; SSL_CTX_set_ecdh_auto(ctx, 1); (or set curve with "prime256v1")
"call" bind(srvSock, ...
"call" srvSock = listen(...

"call" add_epoll(srvSock, ReadFlag

###

"func" Read_Handle(Sock // from my epoll( loops
  if (Sock == srvSock)
    "call" Accept_Handle(Sock
  else
    "call" Real_Read_Handle(Sock
"end func"

"func" Real_Read_Handle(Sock
  if (!SSL_is_init_finished(ssl)) {
    timeMs = CurTime()
    "call" ret = SSL_accept(ssl);
    timeTotalVariable += (CurTime() - timeMs)
    "call" err = SSL_get_error(ssl, ret);
    if (err == SSL_ERROR_WANT_READ) {
      set_epoll(Sock, ReadFlag
    return;
    }
    if (err == SSL_ERROR_WANT_WRITE) {
      set_epoll(Sock, WriteFlag
    return;
    }
    ...
  }
  "call" SSL_read(ssl, ...
  ...
"end func"

"func" Accept_Handle(Sock
  "call" newSock = accept(Sock, ...
  "call" MyNonBlockingSocketSetFunction(Sock
  "call" ssl = SSL_new(ctx
  "call" SSL_set_fd(ssl, newSock);
  "call" SSL_set_accept_state(ssl
  "call" BIO_set_nbio(SSL_get_rbio(ssl), 0);
  "call" BIO_set_nbio(SSL_get_wbio(ssl), 0);
  "call" add_epoll(newSock, ReadFlag // handshake will do at main read function.
"end func"

我的代码非常庞大(我在这个服务器上编码了很长时间),所以我给出了伪代码。 正如我所说,我尝试了很多东西,但根本没有成功。

strace 有问题(我认为);

read(9898, "\26\3\3\0F", 5)             = 5
read(9898, "\20\0\0BA\4@/\241|9\325\247\351S\265\7<\204\5\260\203H\\\314\212\301\324B\f\353+"..., 70) = 70
read(9898, "\24\3\3\0\1", 5)            = 5
read(9898, "\1", 1)                     = 1
read(9898, "\26\3\3\0(", 5)             = 5
read(9898, ">\205\203\0006\354\337\264{\214\6\0\4\343\311Ai%\30\347\20\307\300\253+\200}B\\\326:\211"..., 40) = 40
write(9898, "\26\3\3\0\312\4\0\0\306\0\0\1,\0\300\17\225!A4\310\312(\231.\263\23\t\265\3}\211"..., 258) = 258
read(9896, "\27\3\3\1\347", 5)          = 5
read(9896, "t()R\2\347pvE\341\n\272C\231\267\352.\21G\212u\203wO\v\16\326\4\352\205\326\340"..., 487) = 487
read(9895, "\26\3\3\0F", 5)             = 5
read(9895, "\20\0\0BA\4\213\335\242OV\2154\371\345\2321\257\217\377\236\"xN\206\322\205I{\242\307\276"..., 70) = 70
read(9895, "\24\3\3\0\1", 5)            = 5
read(9895, "\1", 1)                     = 1
read(9895, "\26\3\3\0(", 5)             = 5
read(9895, "^\341Ii\273\35\nMe\214\303\352aE\236\26q\333\274\366\375\255@;\275Ad\204ko\223\377"..., 40) = 40
write(9895, "\26\3\3\0\312\4\0\0\306\0\0\1,\0\300\17\225!A4\310\312(\231.\263\23\t\265\3}d"..., 258) = 258
read(9894, "\27\3\3\1\346", 5)          = 5
read(9894, "\n+e\16\\\330\305\322\364\367j\356b\336\3T3\va\200\324\03107_\320,\22H\33\3\350"..., 486) = 486
read(9892, "\26\3\3\0F", 5)             = 5
read(9892, "\20\0\0BA\4Q\214t7$\276$\3744\247\364,\320\376\225\2623z\204\254U\355\17\323\214S"..., 70) = 70
read(9892, "\24\3\3\0\1", 5)            = 5
read(9892, "\1", 1)                     = 1
read(9892, "\26\3\3\0(", 5)             = 5
read(9892, "\201\214T8\257nBM{\210\202V\25\340R\315)\320\343'h\341\353\351\6f!$\314\230\300\221"..., 40) = 40
write(9892, "\26\3\3\0\312\4\0\0\306\0\0\1,\0\300\17\225!A4\310\312(\231.\263\23\t\265\3}h"..., 258) = 258

这些是在我的服务器的 SSL_accept 上的握手读/写调用。

但我测试了 nginx、openlitespeed、nodejs 等... 那些服务器的 strace 日志是这样的;

read(8247, "\26\3\1\1.\1\0\1*\3\3T\214\201\305\2\365\260\r\210\212\232j\1778\315\301\312\235\354e\4"..., 1024) = 307
write(8247, "\26\3\3\0=\2\0\0009\3\3\16\241r76\203\245e\373\10rF\330l\235-\3\2436Y\326"..., 1821) = 1821
read(8178, "\26\3\1\1.\1\0\1*\3\3\367._\207\353\316\262j\277\332\352\237\363\343\30\351\370X\210\34x"..., 1024) = 307
write(8178, "\26\3\3\0=\2\0\0009\3\3\347\322\200\222\206\336y\2\243\213<\235\265\230\33\315\375\306H5\7"..., 1821) = 1821
read(8280, "\26\3\1\1.\1\0\1*\3\3\35\214\374)Vg7\225\36\340\251*\234j\35>O\234\3Dw"..., 1024) = 307
write(8280, "\26\3\3\0=\2\0\0009\3\3\240q\207T\331\262\314\261o\3\307L{U\20c\270\232\377(\364"..., 1821) = 1821
read(8567, "\26\3\1\1.\1\0\1*\3\3\255\270\3447\362\226\17\276x\205\305\334C\16$@Zd\2\353\304"..., 1024) = 307
write(8567, "\26\3\3\0=\2\0\0009\3\3\355\314fko\25C\235\260\213\273\263o`\2\234:\344\27\341]"..., 1821) = 1821

对于握手,在我的服务器上读取 8 次 ???

为什么我的 SSL_accept 函数很慢?

PS。 : 对不起,我的语法不好。

编辑; SSL设置部分(ssl设置部分很容易复现,但是socket复用部分很难复现)

SSL_CTX *ctx = NULL;
void Init_SSL() {
    OPENSSL_config(NULL);
    SSL_library_init();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();
}

void Init_Server() {
    #if OPENSSL_VERSION_NUMBER < 0x10100000L
        ctx = SSL_CTX_new(SSLv23_method());
    #else
        ctx = SSL_CTX_new(TLS_method());
    #endif
    if (ctx == NULL) {
        cout << "can not create ctx" << endl;
    return;
    }
    SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2);
    SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv3);
    SSL_CTX_set_mode(ctx, SSL_MODE_RELEASE_BUFFERS);
    SSL_CTX_set_mode(ctx, SSL_MODE_ENABLE_PARTIAL_WRITE);
    SSL_CTX_set_mode(ctx, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
    SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION);
    SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE);
    #ifdef SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION
    SSL_CTX_set_options(ctx, SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
    #endif
    SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF);
    const unsigned char *TmpStr = "SslSrv";
    SSL_CTX_set_session_id_context(ctx, TmpStr, strlen(TmpStr));
    SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
}

bool LoadCert() {
    DH *dh;
    BIO *bio = BIO_new_file("server.dh", "r");
        if (bio == NULL) {
            cout << "dh file error" << endl;
        return false;
        }
        dh = PEM_read_bio_DHparams(bio, NULL, NULL, NULL);
        BIO_free(bio);
        if (dh == NULL) {
            cout << "dh params error" << endl;
        return false;
        }
        const int size = BN_num_bits(dh->p);
        if (size < 1024) {
            cout << "dh bits size error" << endl;
        return false;
        }
        SSL_CTX_set_options(ctx, SSL_OP_SINGLE_DH_USE);
        int r = SSL_CTX_set_tmp_dh(ctx, dh);
        if (!r) {
            cout << "cannot set tmp dh" << endl;
        return false;
        }
    }
    if (SSL_CTX_use_certificate_chain_file(ctx, "server.crt") <= 0) {
        cout << "cannot load cert" << endl;
    return false;
    }
    if (SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM) <= 0) {
        cout << "cannot load priv key file" << endl;
    return false;
    }
    if (!SSL_CTX_check_private_key(ctx)) {
        cout << "priv key file is wrong" << endl;
    return false;
    }
    // this list from nodejs (default list)
    if (SSL_CTX_set_cipher_list(ctx, "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA") == 0) {
        cout << "cannot set cipher list" << endl;
    return false;
    }
    SSL_CTX_set_options(ctx, SSL_OP_SINGLE_ECDH_USE);
    #if SSL_CTRL_SET_ECDH_AUTO
    SSL_CTX_set_ecdh_auto(ctx, 1);
    #endif
return true;
}

此设置中是否有任何遗漏或无效的内容? 谢谢。

【问题讨论】:

  • ssl部分很容易,但是socket(多路复用)部分很难产生简单的代码。
  • 以防万一,您检查是否有足够的随机位可用? cat /proc/sys/kernel/random/entropy_avail,应该返回大约 1k~2k 的值。
  • # cat /proc/sys/kernel/random/entropy_avail 140 嗯

标签: c++ performance openssl epoll handshake


【解决方案1】:

SSL 握手包括关键的操作逻辑,包括加密。 我列出了一些处理缓慢的原因:-

  1. 客户端请求访问受保护的资源。
  2. 服务器将其证书提供给客户端。
  3. 客户端验证服务器的证书。
  4. 如果成功,客户端将其证书发送到服务器。
  5. 服务器验证客户端的凭据。
  6. 如果成功,服务器授予对客户端请求的受保护资源的访问权限

因此,如果您的应用程序中没有 SSL 握手,则您的应用程序会避免上述所有操作,并且显然会提高操作速度。

速度慢的一个最重要的原因是,如果您考虑以上所有因素,客户端和服务器之间的通信非常多,并且数据解密/加密是同步完成的,这也减少了并行处理。

【讨论】:

  • 我知道,我用远程机器上的“openssl s_client -verify -showcerts -connect”命令测试了几次。我用这个命令和我的压力工具测试了很多不同的服务器程序。 ssl 设置中缺少某些内容,或者我认为还有更多内容。但我不明白什么...
猜你喜欢
  • 2012-03-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-08
  • 2010-11-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多