問題描述

在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。

【完】