概念
我們已經知道什么是SNI,以及如何為用戶配置SNI。
[nginx] nginx使用SNI功能的方法
問題
通過觀察配置文件,可以發現,針對每一個SSL/TLS鏈接, nginx都會動態的查找(加載),找到正確的證書。
那么在這個過程中,與沒有SNI配置的情況下,有什么性能異同呢?
通過對nginx相關部分的源碼分析,將給出這個問題的答案。
(不關注源碼的話,可以直接翻到后文查看“結論”章節。)
有圖有真相

分析
如上圖所示
1 模塊
nginx使用兩個模塊(這里只針對http進行分析,stream請自行類推)來完成tls的處理,ngx_openssl_module 與
ngx_http_ssl_module。其中前者為核心模塊。后者為http模塊。
核心模塊會被master進程調用ngx_ssl_init()函數進行加載,完成全局處理,包括對openssl的初始化。
http模塊將由http框架完成處理,可以分為配置階段與connection的解析階段。
2 配置階段
見圖中的紅框“流程二”。這個階段就是逐行處理配置文件的階段。在這個階段里,merge ssl server config的時候,
http_ssl_module將判斷是否配置了SNI功能。
如果沒有配置,將調用ngx_ssl_certificate()函數加載證書鏈里邊的所有證書
,將其讀進內存並保存在全局配置文件的ctx結構體呢。ctx結構體是openssl api中的全局上下文,會作為參數傳入
給openssl的api,openssl在處理連接時,會將ctx里的證書鏈拷貝(由SSL_new接口完成)到connect結構里,
openssl的connect結構的作用域是tls連接級別。
如果配置了SNI,http_ssl_module將不會加載證書鏈,而是將配置中的變量解析編譯好備用,為運行時做好准備。
同時,還會通過openssl的SSL_CTX_set_cert_cb()函數設置一個回調函數 ngx_http_ssl_certificate(), 該函數會在
ssl握手之前進入,給用戶一個修改證書信息的機會。該回調是在SSL_do_handshake()中完成的。
# man SSL_CTX_set_cert_cb cert_cb() is the application defined callback. It is called before a certificate will be used by a client or server.
The callback can then inspect the passed ssl structure and set or clear any appropriate certificates.
3 請求階段
如圖紅框“流程三”
在請求階段,“是否啟用了SNI功能”是不被nginx感知的。所有與SNI相關的邏輯都會在前文設置的回調函數 ngx_http_ssl_certificate()
中完成。nginx框架與ssl模塊只是正常的完成業務邏輯處理。
在回調函數中,首先會申請一個request結構體,作為中間存儲。然后通過openssl的BIO接口完成證書鏈的加載,並關聯至
openssl的connect結構里。然后,剛剛申請的request會被釋放(這里邊新建了一個pool,這個pool是從操作系統申請的內存)。
該過程與配置階段共同復用了函數 ngx_ssl_load_certificate(), 整個過程都沒有全局上下文ctx的參與,鏈接結束后證書鏈會被釋放掉。
下次新的鏈接會重新再加載一次證書鏈。
4 openssl
分別觀察有SNI和沒有SNI的兩個流程,將發現api的調用序列上有如下區別,關鍵在於是否使用ctx上下文。
無sni的時候,使用SSL_CTX_use_certificate()函數,有sni的時候,使用SSL_use_certificate()函數。
查找文檔,對比一下區別:
# man SSL_CTX_use_certificate The SSL_CTX_* class of functions loads the certificates and keys into the SSL_CTX object ctx. The information is passed
to SSL objects ssl created from ctx with SSL_new(3) by copying, so that changes applied to ctx do not propagate to already existing SSL objects. The SSL_* class of functions only loads certificates and keys into a specific SSL object. The specific
information is kept, when SSL_clear(3) is called for this SSL object.
從側面印證我們的分析結果,感興趣的可以完整的閱讀手冊,這里不在贅述。
5 實驗
截止到目前所有的分析都是基於nginx的,將openssl作為黑盒。並不能嚴謹的排除openssl為此做了特別優化,去防止多次加載
證書鏈的可能。 為了印證分析,做如下實驗,配置SNI后,使用相同的證書多次請求。通過gdb觀察,是否每次都進入回調函數並重新加載
證書鏈。
616 if (ngx_strncmp(cert->data, "data:", sizeof("data:") - 1) == 0) { (gdb) n 627 if (ngx_get_full_name(pool, (ngx_str_t *) &ngx_cycle->conf_prefix, cert) (gdb) 634 bio = BIO_new_file((char *) cert->data, "r"); (gdb) 635 if (bio == NULL) { (gdb) 643 x509 = PEM_read_bio_X509_AUX(bio, NULL, NULL, NULL); (gdb) bt #0 ngx_ssl_load_certificate (pool=0x55825f636240, err=0x7ffd32750868, cert=0x7ffd32750900, chain=0x7ffd32750870) at src/event/ngx_event_openssl.c:643 #1 0x000055825ed31d79 in ngx_ssl_connection_certificate (c=0x55825f64c0c0, pool=0x55825f636240, cert=0x7ffd32750900, key=0x7ffd32750910, passwords=0x55825f0df860 <empty_passwords.29651>) at src/event/ngx_event_openssl.c:546 #2 0x000055825ee378ef in ngx_stream_ssl_certificate (ssl_conn=0x55825f632c70, arg=0x55825f63def0) at src/stream/ngx_stream_ssl_module.c:530 #3 0x00007efed0277b3c in tls_post_process_client_hello () from /opt/openssl/lib/libssl.so.1.1 #4 0x00007efed026d362 in state_machine () from /opt/openssl/lib/libssl.so.1.1 #5 0x00007efed0265991 in SSL_do_handshake () from /opt/openssl/lib/libssl.so.1.1 #6 0x000055825ed33f27 in ngx_ssl_handshake (c=0x55825f64c0c0) at src/event/ngx_event_openssl.c:1683 #7 0x000055825ed34671 in ngx_ssl_handshake_handler (ev=0x55825f66dec0) at src/event/ngx_event_openssl.c:1992 #8 0x000055825ed2dae8 in ngx_epoll_process_events (cycle=0x55825f62d4a0, timer=60000, flags=1) at src/event/modules/ngx_epoll_module.c:957 #9 0x000055825ed185cf in ngx_process_events_and_timers (cycle=0x55825f62d4a0) at src/event/ngx_event.c:242 #10 0x000055825ed2a4d4 in ngx_worker_process_cycle (cycle=0x55825f62d4a0, data=0x0) at src/os/unix/ngx_process_cycle.c:759 #11 0x000055825ed26305 in ngx_spawn_process (cycle=0x55825f62d4a0, proc=0x55825ed2a3d4 <ngx_worker_process_cycle>, data=0x0, name=0x55825ee6e4fb "worker process", respawn=-3) at src/os/unix/ngx_process.c:199 #12 0x000055825ed29018 in ngx_start_worker_processes (cycle=0x55825f62d4a0, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359 #13 0x000055825ed284fb in ngx_master_process_cycle (cycle=0x55825f62d4a0) at src/os/unix/ngx_process_cycle.c:131 #14 0x000055825ecdf1d9 in main (argc=1, argv=0x7ffd327511a8) at src/core/nginx.c:382 (gdb)
(留個調用棧,方便以后看。)
通過實驗,證書每次都會通過api BIO_new_file 進行重新加載,除非該函數做了優化,否則每次鏈接的每個證書都將發生磁盤IO操作。
結論
配置了SNI功能之后,nginx對TLS鏈接請求的處理將產生性能損失。損失的粒度為每鏈接級別。
每個ssl/tls鏈接都將額外發生:一次內存申請與釋放;一組加載證書鏈上全部證書的磁盤IO。
改進方案
我認為內存的申請可以忽略,因為原本處理request的時候也是需要向OS申請內存的。比較關鍵的在於磁盤IO。
第一個方案,可以將所有證書放置在內存文件系統中,此方案不需要調整代碼。
第二個方案,重寫ngx_http_ssl_certificate(), 雖然證書是動態選擇的,但是只要在備選集合確定的情況下,我們依然
可以進行預加載,去除掉運行時的IO。引入的限制為:1備選集合不能實時變更,2證書集合元素數量需要設置上限。
