跨地域复制听着很高大上,但咱们今天不聊概念,直接聊一个让人抓头的问题:TLS握手太慢。很多团队把Pulsar集群分布在两个甚至三个城市,数据需要实时互相同步。为了保证传输安全,大家都会把TLS打开。可一打开,发现复制延迟涨了好几倍,甚至连接频繁超时。今天我根据自己的踩坑经历,把证书优化和握手性能这点事掰开揉碎讲明白。

一、为什么要关心跨地域复制里的TLS握手

先搞清楚一个事实:跨地域复制本质上是让一个集群的Broker,变成另一个集群的Producer/Consumer。它跟普通客户端连服务端没什么两样,同样要建立TCP连接、TLS握手、然后开始收发消息。只不过这两端可能相隔几百公里,网络往返延迟(RTT)可能是机房内部的几十倍。

TLS握手可不是一次简单的打招呼。以目前最常用的握手流程为例,客户端发ClientHello,服务器回ServerHello并带上证书链,客户端要校验证书,然后双方交换密钥参数,最后完成握手。这里面牵扯到好几个RTT。如果网络延迟是50毫秒,一次握手光网络耗时就有两三个RTT,加起来就是100到150毫秒。再加上证书签名验证、密钥协商计算,一次新连接没有200毫秒下不来。如果复制任务需要频繁重建连接,性能当然就垮了。

有人可能会说,Pulsar不是有长连接吗?对,长连接确实能解决一部分问题。但跨地域复制有很多topic和partition,连接数量其实很大。而且一旦Broker滚动升级、网络抖动、负载均衡调整,连接就会断掉重连。在重连高峰期,大量握手请求同时涌过来,Broker的CPU还要做非对称解密运算,这时候瓶颈就特别突出。

所以,我们要做的不是关掉TLS,而是想尽办法让TLS握手变快、变少、变轻。

二、先搞清楚瓶颈在哪里

在动手优化之前,得先度量。别凭感觉说慢,要用数据说话。最直接的办法就是看一次TLS握手花费的时间。如果你用的是Linux或者macOS,可以用openssl命令行工具来测试。

# 技术栈:Bash(所有命令在Linux/macOS终端执行)

# 使用openssl s_client连接Pulsar的TLS端口
# -connect 指定Broker地址和端口,6651是Pulsar TLS端口
# -servername 用于SNI扩展,如果你的证书绑定了域名,这里要填对应域名
# -brief 简化输出,只看关键信息
# 注意:这里会真实完成TCP和TLS握手,然后进入输入阶段,我们用echo直接关闭

echo "Q" | openssl s_client -connect broker1.example.com:6651 \
    -servername broker1.example.com -brief 2>&1

这条命令输出里能看到TLS协议版本、加密套件,还有证书链的信息。但要用时间说话,我们可以用time命令包裹。

# 测量一次完整握手耗时,这里的real字段就是总耗时
time bash -c 'echo "Q" | openssl s_client -connect broker1.example.com:6651 -servername broker1.example.com >/dev/null 2>&1'

输出类似:

real    0m0.183s
user    0m0.102s
sys     0m0.004s

如果real在0.15秒以上,而且你确认网络RTT本身只有50毫秒左右,那说明证书验证或密钥协商占了很大比例。这时候再进一步拆分瓶颈。可以考虑两个方向:一个是证书本身太重,另一个是握手流程太长。接下来要做的优化,也正是围绕这两点。

三、证书优化手段逐个看

3.1 优先用椭圆曲线(ECDSA)证书

很多团队还在用RSA证书,因为以前教程都这么写的。RSA证书没什么不好,但它的公钥和签名都比较大,验签和密钥交换的计算成本也比较高。在跨地域这种高延迟场景下,哪怕是本地计算节省1毫秒,也会让整体性能上限高不少。

椭圆曲线证书(ECDSA)的密钥位数短,签名体积小,验签速度快。比如P-256曲线的安全强度约等于RSA 3072,但签名只有64字节,握手时传输的数据也更少。

生成一张自签名的ECDSA证书很简单:

# 技术栈:Bash

# 第一步:生成P-256椭圆曲线私钥,prime256v1是OpenSSL对P-256的称呼
openssl ecparam -name prime256v1 -genkey -out broker.key

# 第二步:基于这个私钥生成自签名证书
# 注意:生产环境请使用CA签发,这里仅演示生成方式
# -addext 添加SAN,Pulsar客户端需要校验主机名,SAN不能少
openssl req -new -x509 -key broker.key -out broker.crt -days 365 \
    -subj "/CN=broker1.example.com" \
    -addext "subjectAltName=DNS:broker1.example.com,DNS:broker2.example.com"

# 第三步:查看证书详情,确认公钥算法是id-ecPublicKey
openssl x509 -in broker.crt -noout -text | grep -E "Public Key Algorithm|ASN1 OID"

注意,ECDSA证书的缺点是对老版本客户端的兼容性不如RSA。如果你的客户端环境比较老,比如JDK8早期版本或者很旧的OpenSSL,可能不支持常见的椭圆曲线。好在现代JVM和OpenSSL基本上都支持,Pulsar的客户端是Java 17,完全不用担心。

3.2 给证书链减减肥

证书链是TLS握手里很占字节的部分。一张服务器证书通常只有几KB,但中间证书和根证书加起来可能几十KB。每一次握手都要把这些证书从头传一遍。跨地域场景下,传输的每一KB都要乘以昂贵的RTT和带宽成本,所以证书链越短越好。

很多公司的证书链明明可以合并,却偏偏给服务器只配了服务器证书,中间证书让客户端自己下载。这会导致客户端在握手阶段额外发起一次HTTP请求去获取中间证书,白白多耗一个RTT。正确做法是把中间证书拼到服务器证书后面。

# 技术栈:Bash

# 假设你有三个文件:服务器证书、中间证书、根证书
# 把中间证书追加到服务器证书后面,注意顺序是:服务器证书在前,中间证书在后
# 根证书可以不用放在服务器上,因为客户端本地要有根证书
cat broker.crt intermediate.crt > bundle.crt

# 校验合并后的证书链,必须看到s:是服务器证书,i:是中间证书
openssl crl2pkcs7 -nocrl -certfile bundle.crt | openssl pkcs7 -print_certs -noout

这里额外提醒一句:证书链不是越多越好。最理想的情况是,服务器只发两张证书,一张服务器证书、一张中间证书,根证书在客户端信任库里。这样做握手传输的数据量最小。如果你的证书链有三层以上,可以考虑找一个短链的CA,或者调整签名路径。

3.3 使用OCSP Stapling,把验证环节留给自己

默认情况下,客户端收到服务器证书后,可能会去CA的在线证书状态协议(OCSP)服务查询这张证书有没有被吊销。这个查询是一个额外的网络请求。如果客户端恰好配置了必须进行OCSP查询,那么一次握手又要多一个RTT,甚至更多。

OCSP Stapling的思路是,让服务器自己定期从CA拉取OCSP响应,然后在TLS握手时把响应直接捎带给客户端。客户端就不用再联网查了。这样既能验证证书状态,又不增加握手时间。

Pulsar的Broker底层用的是Netty,它本身对OCSP Stapling的支持并不是那种开箱即用的配置项。不过我们在生产环境中通常会在Pulsar前面加一层负载均衡或者接入层,比如Nginx。可以让Nginx来提供OCSP Stapling,然后与后端Pulsar之间使用内部网络传输。这样对客户端来说,握手直接和Nginx完成,OCSP查询就被Nginx挡掉了。

如果你也打算用类似方案,可以这样开启Nginx的OCSP:

# 技术栈:Bash(下面以注释形式展示Nginx配置片段)

# 在nginx.conf的server块内配置
# listen 6651 ssl;
# ssl_certificate /etc/nginx/ssl/bundle.crt;
# ssl_certificate_key /etc/nginx/ssl/broker.key;
# 开启OCSP Stapling
# ssl_stapling on;
# ssl_stapling_verify on;
# 可选:把CA的链文件指给它
# ssl_trusted_certificate /etc/nginx/ssl/ca-chain.crt;

# 配置修改后,用nginx -t检查语法是否正确
nginx -t

配置好之后,可以用openssl客户端来验证OCSP Stapling是否生效:

# 技术栈:Bash

# 连接开启OCSP Stapling的服务,并且显示扩展信息
# -status 表示请求OCSP stapling响应
# -tlsextdebug 输出TLS扩展调试信息
openssl s_client -connect broker1.example.com:6651 \
    -servername broker1.example.com -status -tlsextdebug </dev/null 2>&1 | grep -i "OCSP"

如果能看到OCSP Response Status: successful,说明Stapling已经正常工作。

3.4 开启TLS会话恢复,减少重复握手

无论怎么优化证书,只要建立新连接,握手还是得跑。但如果客户端和服务器之间已经有握手记录,并且双方都同意恢复会话,那就可以跳过证书交换和密钥协商,只用一个简单的会话恢复握手就能建立连接。这样握手RTT至少省一半。

在Pulsar中,TLS会话恢复依赖客户端的实现。Java的SSLEngine默认支持会话缓存。而对于跨地域复制连接,Pulsar内部都是Java客户端,所以我们可以适当调大会话缓存的大小。

# 技术栈:Bash

# 在Broker的启动脚本中,通过JVM参数调整TLS会话缓存
# 这个参数指定了SSL会话缓存的大小,单位是会话数量
# 如果你的连接数很多,比如几千,可以设置成65536
JAVA_OPTS="$JAVA_OPTS -Djavax.net.ssl.sessionCacheSize=65536"

# 另外可以设置会话超时时间,单位是秒,默认是86400
# 这里设置成7200秒,让会话缓存更快淘汰旧会话,以免内存占用过高
JAVA_OPTS="$JAVA_OPTS -Djavax.net.ssl.sessionTimeout=7200"

这里值得注意,会话恢复有两个模式:会话ID恢复和会话票据恢复。前者需要服务器缓存会话状态,后者是服务器加密签发一个票据给客户端。Pulsar底层是Java,主要走的是会话ID恢复,所以调大缓存是有效的。缺点是如果Broker是多实例部署,会话缓存要复制到所有节点,这需要额外配置。如果集群特别大,可能反而带来复杂度。用不用,要看你的场景是不是有大量短连接。

四、跨地域复制场景下的专属优化细节

除了上面这些通用的证书优化,Pulsar跨地域复制还有一些自己的脾性。比如用于复制的连接往往要同时服务多个topic的流量。如果每个Broker到远端Broker是串行复用同一个连接,那握手次数倒不多。但现实是,Pulsar内部为了隔离故障,可能会建立多条连接。尤其当topic数量特别多的时候,连接池里的连接数就上去了。

我们可以通过配置来调整连接池大小和复用策略。比如broker.conf里面有几个参数值得看:

# 技术栈:Bash

# 在Pulsar broker.conf中调整连接池相关参数
# 这是复制连接允许的空闲时间,如果空闲超过60秒就关闭
# 调大这个值能减少连接重建次数
replicationConnectionsPerBroker=2

# 这是复制线程池线程数,如果CPU核数够多,可以适当调大
# 但这个参数本身不直接影响TLS握手,只是影响任务调度效率
replicationClientThreads=8

注意,修改配置后需要重启Broker。重启之前先用pulsar-admin检查一下当前配置是否生效。

# 技术栈:Bash

# 查看某个集群的复制配置
pulsar-admin clusters get cluster-1 --url http://localhost:8080

其实跨地域复制还有一个很有意思的点:复制用的Producer和Consumer在连接建立时会自动进行身份认证。如果你在多个机房都配置了独立的CA根证书,可能导致互相不信任。这时候不光握手里证书校验失败,还会出现反复重连的情况。所以别名区很重要,最好所有机房共用同一个根CA,或者确保对方信任了你签发证书的中间CA。

在证书轮转的时候,也要特别小心。很多人会直接替换Broker的私钥和证书,但客户端其他机房的Broker里存着旧的信任锚。如果新证书用的是新更换的根CA,旧根CA没有包含在信任库里,那握手会直接失败。因此轮转要有过渡期:先把新的根CA加到彼此的信任库,再替换Broker证书。

另一个容易忽略的优化点是TLS版本。TLS 1.3握手只需要一个RTT(加上可选的0-RTT),并且移除了旧的不安全加密套件。如果Pulsar版本和JDK版本都支持,我强烈建议开启TLS 1.3。在Pulsar中可以通过tlsProtocols配置:

# 技术栈:Bash

# broker.conf中的TLS协议配置
# 只保留TLSv1.3,如果你还需要兼容老客户端,可以写TLSv1.2,TLSv1.3
tlsProtocols=TLSv1.3

但如果客户端也支持,你会发现握手时间又缩短一大截。注意TLS 1.3的会话恢复能力更强,再加上证书优化,效果会叠加。

五、实践中的注意事项与坑

第一,别为了性能牺牲安全。有些文章建议直接把证书校验关掉,比如把tlsAllowInsecureConnection设为true。这是非常危险的做法。跨地域复制传输的是核心业务消息,如果被中间人抓到,后果不堪设想。我们要做的优化是让TLS更轻快,而不是绕过它。

第二,ECDSA证书的私钥要妥善保存。许多同学把私钥和证书放在同一个目录,权限还是755。这等于把家门钥匙挂在门上。私钥文件权限一定要设置成600或者400,并且建议用KMS或Hsm管理生产环境的私钥。

# 技术栈:Bash

# 修复私钥权限,只有属主可读
chmod 600 broker.key

# 校验私钥和证书是否匹配
# 输出结果应该是一串相同的MD5指纹(旧版OpenSSL),或者提示OK
openssl x509 -noout -modulus -in broker.crt | openssl md5
openssl rsa -noout -modulus -in broker.key | openssl md5

第三,OCSP Stapling虽然好,但必须保证能定期访问CA的OCSP响应。如果你的接入层机器不能出公网,那就别开OCSP Stapling,否则服务器会不断重试拉取响应,反而占用资源。可以用本地缓存文件手动更新,但那样其实意义不大了。

第四,会话恢复缓存不能无限大。曾经有一个生产环境,把sessionCacheSize调到10万,结果频繁GC,Broker延迟飙升。后来发现是因为每次握手都会往并发Map里写入会话,缓存太大导致锁竞争激烈。合适的值是要根据实际连接峰值乘以2或3来估计。比如峰值连接数是5000,设置15000足够了。

第五,TLS握手性能优化不是一锤子买卖。证书链长度、密钥类型、协议版本、会话缓存,这些因素会互相影响。比如你换了ECDSA证书,但证书链还是四层,那传输少了但东西还是大。要整体一起优化,才能看到显著效果。建议每次改动后都跑一下第二章节里的time命令,对比优化前后的耗时。

六、总结

跨地域复制里TLS慢,本质上是在高RTT环境下做了一场高频的密码学计算。我们能做的,就是让每次握手更轻量、更少发生。用ECDSA证书把非对称运算量降下来;把证书链压到最短,减少握手包体积;利用OCSP Stapling把证书状态验证从客户端解放出来;开启TLS会话恢复让重复握手变快;再叠加TLS 1.3,效果直接拉满。

不过也要记住,优化是乘法不是加法。证书链太长,哪怕密钥是ECDSA,传输开销依然在;会话恢复缓存太大,内存和锁竞争会成为新瓶颈。咱们要结合自己的连接数、网络带宽、RTT来调参数,不能照抄网上的配置。

我在实际处理这个问题的过程中,最大的感受是:每一毫秒都值得抠出来。跨地域复制的体验,直接取决于两地网络的质量和TLS握手的效率。希望这篇分享能帮你在Pulsar跨地域复制的路上少走几个弯路。如果哪天你又遇到握手慢,别急着换机器,先把证书拿出来看看,也许答案就藏在那张不起眼的证书里。