問題描述
在Visual Studio 2019中,通過Cloud Service模板創建了一個Worker Role的角色,在角色中使用StackExchange.Redis來連接Redis。遇見了一系列的異常:
- RedisConnectionException: No connection is available to service this operation: PING; It was not possible to connect to the redis server(s); ConnectTimeout; IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=32765,Min=8,Max=32767), Local-CPU: n/a
- RedisConnectionException: UnableToConnect on xxxxxx.redis.cache.chinacloudapi.cn:6380/Interactive, origin: ResetNonConnected, input-buffer: 0, outstanding: 0, last-read: 5s ago, last-write: 5s ago, unanswered-write: 524763s ago, keep-alive: 60s, pending: 0, state: Connecting, last-heartbeat: never, last-mbeat: -1s ago, global: 5s ago, mgr: Inactive, err: never
- IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host.
- SocketException: An existing connection was forcibly closed by the remote host
異常截圖:
問題分析
根據異常信息 Socket Exception, 在建立連接的時候被Remote Host關閉,也就是Redis服務端強制關閉了此連接。那么就需要進一步分析,為什么Redis會強制關閉連接呢? 查看Redis的連接字符串:
xxxxxx.redis.cache.chinacloudapi.cn:6380,password=<access key>,ssl=True,abortConnect=False
使用6380端口,建立SSL連接,在連接字符串中已經啟用SSL。在創建Azure Redis的資源中,會發現一段提示:TLS1.0,1.1已不被支持。需要使用TLS1.2版本。
而當前的Cloud Service使用的是.NET Framework 4.5。 而恰巧,在 .NET Framework 4.5.2 或更低版本上,Redis .NET 客戶端默認使用最低的 TLS 版本;在 .NET Framework 4.6 或更高版本上,則使用最新的 TLS 版本。
所以如果使用的是較舊版本的 .NET Framework,需要手動啟用 TLS 1.2: StackExchange.Redis: 在連接字符串中設置 ssl=true
和 sslprotocols=tls12
。
問題解決
在字符串中添加 ssl=True,sslprotocols=tls12, 完整字符串為:
string cacheConnection = "xxxxxx.redis.cache.chinacloudapi.cn:6380,password=xxxxxxxxx+xxx+xxxxxxx=,ssl=True,sslprotocols=tls12, abortConnect=False";
在Visual Studio 2019代碼中的效果如:
Could Service 與 Redis 使用的簡單代碼片段為
WorkerRole:
using Microsoft.WindowsAzure; using Microsoft.WindowsAzure.Diagnostics; using Microsoft.WindowsAzure.ServiceRuntime; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; namespace WorkerRole1 { public class WorkerRole : RoleEntryPoint { private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); private readonly ManualResetEvent runCompleteEvent = new ManualResetEvent(false); private RedisJob redisjob1 = new RedisJob(); public override void Run() { Trace.TraceInformation("WorkerRole1 is running"); try { this.RunAsync(this.cancellationTokenSource.Token).Wait(); } finally { this.runCompleteEvent.Set(); } } public override bool OnStart() { // Set the maximum number of concurrent connections ServicePointManager.DefaultConnectionLimit = 12; //ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; // For information on handling configuration changes // see the MSDN topic at https://go.microsoft.com/fwlink/?LinkId=166357. bool result = base.OnStart(); Trace.TraceInformation("WorkerRole1 has been started"); return result; } public override void OnStop() { Trace.TraceInformation("WorkerRole1 is stopping"); this.cancellationTokenSource.Cancel(); this.runCompleteEvent.WaitOne(); base.OnStop(); Trace.TraceInformation("WorkerRole1 has stopped"); } private async Task RunAsync(CancellationToken cancellationToken) { // TODO: Replace the following with your own logic. while (!cancellationToken.IsCancellationRequested) { Trace.TraceInformation("Working"); redisjob1.RunReidsCommand(); await Task.Delay(10000); } } } }
RedisJob:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using StackExchange.Redis; namespace WorkerRole1 { class RedisJob { private static Lazy<ConnectionMultiplexer> lazyConnection = CreateConnection(); public static ConnectionMultiplexer Connection { get { return lazyConnection.Value; } } private static Lazy<ConnectionMultiplexer> CreateConnection() { return new Lazy<ConnectionMultiplexer>(() => { string cacheConnection = "xxxxxx.redis.cache.chinacloudapi.cn:6380,password=xxxxxx+xxx+xxxx=,ssl=True,sslprotocols=tls12, abortConnect=False"; return ConnectionMultiplexer.Connect(cacheConnection); }); } public void RunReidsCommand() { IDatabase cache = Connection.GetDatabase(); // Perform cache operations using the cache object... // Simple PING command string cacheCommand = "PING"; Console.WriteLine("\nCache command : " + cacheCommand); Console.WriteLine("Cache response : " + cache.Execute(cacheCommand).ToString()); // Simple get and put of integral data types into the cache cacheCommand = "GET Message"; Console.WriteLine("\nCache command : " + cacheCommand + " or StringGet()"); Console.WriteLine("Cache response : " + cache.StringGet("Message").ToString()); cacheCommand = "SET Message \"Hello! The cache is working from a .NET console app!\""; Console.WriteLine("\nCache command : " + cacheCommand + " or StringSet()"); Console.WriteLine("Cache response : " + cache.StringSet("Message", "Hello! The cache is working from a .NET console app!").ToString()); // Demonstrate "SET Message" executed as expected... cacheCommand = "GET Message"; Console.WriteLine("\nCache command : " + cacheCommand + " or StringGet()"); Console.WriteLine("Cache response : " + cache.StringGet("Message").ToString()); } } }
參考資料
刪除與 Azure Cache for Redis 配合使用的 TLS 1.0 和 1.1: https://docs.microsoft.com/zh-cn/azure/azure-cache-for-redis/cache-remove-tls-10-11
將應用程序配置為使用 TLS 1.2
大多數應用程序使用 Redis 客戶端庫來處理與緩存的通信。 這里說明了如何將以各種編程語言和框架編寫的某些流行客戶端庫配置為使用 TLS 1.2。
.NET Framework
在 .NET Framework 4.5.2 或更低版本上,Redis .NET 客戶端默認使用最低的 TLS 版本;在 .NET Framework 4.6 或更高版本上,則使用最新的 TLS 版本。 如果使用的是較舊版本的 .NET Framework,則可以手動啟用 TLS 1.2:
- StackExchange.Redis: 在連接字符串中設置
ssl=true
和sslprotocols=tls12
。- ServiceStack.Redis: 請按照 ServiceStack.Redis 說明操作,並至少需要 ServiceStack.Redis v5.6。
.NET Core
Redis .NET Core 客戶端默認為操作系統默認 TLS 版本,此版本明顯取決於操作系統本身。
根據操作系統版本和已應用的任何修補程序,有效的默認 TLS 版本可能會有所不同。 有一個關於此內容的信息源,也可以訪問此處,閱讀適用於 Windows 的相應文章。
但是,如果你使用的是舊操作系統,或者只是想要確保我們建議通過客戶端手動配置首選 TLS 版本。
Java
Redis Java 客戶端基於 Java 版本 6 或更早版本使用 TLS 1.0。 如果在緩存中禁用了 TLS 1.0,則 Jedis、Lettuce 和 Redisson 無法連接到 Azure Cache for Redis。 升級 Java 框架以使用新的 TLS 版本。
對於 Java 7,Redis 客戶端默認不使用 TLS 1.2,但可以配置為使用此版本。 Jedis 允許你使用以下代碼片段指定基礎 TLS 設置:
SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); SSLParameters sslParameters = new SSLParameters(); sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); sslParameters.setProtocols(new String[]{"TLSv1.2"}); URI uri = URI.create("rediss://host:port"); JedisShardInfo shardInfo = new JedisShardInfo(uri, sslSocketFactory, sslParameters, null); shardInfo.setPassword("cachePassword"); Jedis jedis = new Jedis(shardInfo);Lettuce 和 Redisson 客戶端尚不支持指定 TLS 版本,因此,如果緩存僅接受 TLS 1.2 連接,這些客戶端將無法工作。 我們正在審查這些客戶端的修補程序,因此請檢查那些包是否有包含此支持的更新版本。
在 Java 8 中,默認情況下會使用 TLS 1.2,並且在大多數情況下都不需要更新客戶端配置。 為了安全起見,請測試你的應用程序。
Node.js
Node Redis 和 IORedis 默認使用 TLS 1.2。
PHP
Predis
低於 PHP 7 的版本:Predis 僅支持 TLS 1.0。 這些版本不支持 TLS 1.2;必須升級才能使用 TLS 1.2。
PHP 7.0 到 PHP 7.2.1:默認情況下,Predis 僅使用 TLS 1.0 或 TLS 1.1。 可以通過以下變通辦法來使用 TLS 1.2。 在創建客戶端實例時指定 TLS 1.2:
$redis=newPredis\Client([ 'scheme'=>'tls', 'host'=>'host', 'port'=>6380, 'password'=>'password', 'ssl'=>[ 'crypto_type'=>STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, ], ]);PHP 7.3 及更高版本:Predis 使用最新的 TLS 版本。
PhpRedis
PhpRedis 在任何 PHP 版本上均不支持 TLS。
Python
Redis-py 默認使用 TLS 1.2。
GO
Redigo 默認使用 TLS 1.2。
【完】