天天看點

【Azure Redis 緩存】雲服務Worker Role中調用StackExchange.Redis,遇見莫名異常(RedisConnectionException: UnableToConnect on xxx 或 No connection is available to service this operation: xxx)

問題描述

在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

異常截圖:

【Azure Redis 緩存】雲服務Worker Role中調用StackExchange.Redis,遇見莫名異常(RedisConnectionException: UnableToConnect on xxx 或 No connection is available to service this operation: xxx)

問題分析

根據異常資訊 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版本。

【Azure Redis 緩存】雲服務Worker Role中調用StackExchange.Redis,遇見莫名異常(RedisConnectionException: UnableToConnect on xxx 或 No connection is available to service this operation: xxx)

而目前的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

【Azure Redis 緩存】雲服務Worker Role中調用StackExchange.Redis,遇見莫名異常(RedisConnectionException: UnableToConnect on xxx 或 No connection is available to service this operation: xxx)

問題解決

在字元串中添加 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代碼中的效果如:

【Azure Redis 緩存】雲服務Worker Role中調用StackExchange.Redis,遇見莫名異常(RedisConnectionException: UnableToConnect on xxx 或 No connection is available to service this operation: xxx)

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。

【完】

當在複雜的環境中面臨問題,格物之道需:濁而靜之徐清,安以動之徐生。 雲中,恰是如此!

繼續閱讀