【发布时间】: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