1. 程式人生 > >微服務實戰(八):落地微服務架構到直銷系統(服務高可用性)

微服務實戰(八):落地微服務架構到直銷系統(服務高可用性)

在微服務架構風格的系統中,如果單個微服務垮掉或地址不可訪問,雖然對系統的影響是有限的,但我們也必須採取一定的手段來保證每個微服務儘量可用;並且在大併發的情況下,雖然可以通過EDA訊息佇列處理的方式提高吞吐量,但仍然需要WebApi能夠更加高效的偵聽使用者請求,處理訊息,即使在某個服務短暫不可用的情況下。本篇文章主要來詳細講一講要保證微服務的高可用性,可以通過哪些手段來實現。

一、保證微服務負載可用

這裡的問題指的是當某個微服務或者微服務依賴的持久化儲存出現不可訪問時,會造成此塊服務不可用,我們需要有一定的手段能夠儘量避免這個問題;為了達到這個目標,通常可以從4個方面來解決。

1.資料庫高可用

現代的關係型資料庫系統或NoSql通常是作為微服務的持久化儲存機制的。當資料庫所在的伺服器、資料服務或資料庫故障或不可用時,會造成業務中斷;所以我們應該利用資料庫產品本身的高可用機制來解決這個問題,這裡以SQL Server 2016關係型資料庫為例。

SQL Server 2016資料庫服務提供了一種SQL AlwaysOn的高可用機制。SQL AlwaysOn是將多臺SQL Server組合成一個虛擬的SQL Server,然後通過SQL AlwaysOn的功能將需要能夠自動轉移故障的資料庫同步到多臺SQL Server上。當WebApi連線資料庫服務時,連線的是虛擬IP和埠,然後SQL AlwaysOn會自動將資料訪問請求定向到主物理SQL Server上;當主伺服器垮掉時,會自動轉移資料服務到一臺從資料庫伺服器上,從資料庫伺服器自動成為新的主資料庫伺服器,後續的WebApi連線虛擬IP和埠時,會自動連線到新的主資料庫伺服器上,這個階段對WebApi來說是完全透明的。在SQL Server 2016中,AlwaysOn的管理介面大致如下,作為開發人員或架構師,瞭解即可,通常這是由運維團隊管理的。

2.微服務高可用

通常我們會將某個微服務WebApi部署到物理主機、虛擬機器或其他形態的主機(比如docker)的Web Server服務上。這裡通常會有兩個方面的原因造成微服務無法訪問,一是微服務所在的Web Server或主機停止響應或關機、二是微服務併發訪問量太大,造成資源大量佔用,無法響應使用者請求。

除了前面系列文章講解的軟體架構解決外,我們還需要配合另一個機制能夠儘量保證微服務高可用,這個機制就是NLB(網路負載均衡)。

如果你的WebApi主機在內網,可以通過F5等硬體裝置提供NLB支援,如果你的WebApi部署在雲端,可以使用雲端供應商提供的NLB相關服務提供NLB支援。NLB是將多臺Web伺服器組合成一個虛擬的Web伺服器,當然還要通過埠組織。通過檔案複製功能,比如Windows Server自帶的DFS的功能將多臺Web伺服器承載相同的WebApi保持WebApi內容一致。當前端呼叫WebApi服務時,連線的是NLB上配置的虛擬IP和埠,然後根據NLB的配置(有根據Web伺服器負載情況路由到請求少的主機上;有根據每個請求自動輪詢每個主機;有根據某個會話總是請求到特定主機),將前端的請求路由到合適的WebApi主機上。在阿里雲上,NLB的管理介面大致如下,作為開發人員或架構師,瞭解即可,通常也是由運維團隊管理的。

3.重試策略

無論是資料庫還是WebApi,因為網路或服務等原因,可能會出現瞬間故障,也就是在很短的時間內,臨時不可訪問。如果出現這種情況,我們就應該有重試機制,無論是資料庫連線的重試,還是呼叫WebApi的重試。

a.資料連線的重試

在一些第三方的資料訪問庫或ORM框架中,通常都提供了資料連線重試的功能,這些功能通常都能實現如果資料訪問不可用,要重試連線幾次,每次重試的間隔是多長。示例程式碼如下:

protected override void OnConfiguring(DbContextOptionsBuilder optionBuilder)
        {           

            optionBuilder.UseSqlServer("Server=localhost;Database=DDD1OrderDB;User ID=DDD1user;Password=password",
                sqlServerOptionsAction:p=> {
                    p.EnableRetryOnFailure(
                        maxRetryCount:5,
                        maxRetryDelay:TimeSpan.FromSeconds(1),
                        errorNumbersToAdd:null
                        );
                });           
        }

b.呼叫WebApi的重試

無論是前端框架還是後端框架,通常都提供了一些庫和方法可以使用http的方式呼叫WebApi。我們可以按照需求擴充套件這些庫,能夠在呼叫WebApi不可用時,重試幾次。後端程式碼呼叫WebApi重試程式碼:

public interface IHttpClient
    {
        Task<HttpResponseMessage> GetAsync(string requesturi);
        Task<HttpResponseMessage> PostAsync(string requesturi, HttpContent content);
    }
    public class ReHttpClient : IHttpClient
    {
        private HttpClient client;
        private PolicyWrap policywrap;
        public ReHttpClient(Policy[] policies)
        {
            client = new HttpClient();
            policywrap = Policy.WrapAsync(policies);
        }
        private Task<T> HttpInvoker<T>(Func<Task<T>> action)
        {
            return policywrap.ExecuteAsync(() => action());
        }
        public Task<HttpResponseMessage> GetAsync(string requesturi)
        {
            return HttpInvoker(async () =>
            {
                return await client.GetAsync(requesturi);
            });
        }
        public Task<HttpResponseMessage> PostAsync(string requesturi, HttpContent content)
        {
            return HttpInvoker(async () =>
            {
                return await client.PostAsync(requesturi, content);
            });
        }
    }
 public class ReHttpClientFactory
    {
        public ReHttpClient CreateReHttpClient() =>
            new ReHttpClient(CreatePolicies());

        private Policy[] CreatePolicies() => new Policy[]
        {
            Policy.Handle<HttpRequestException>()
            .WaitAndRetryAsync(6,retry=>TimeSpan.FromSeconds(1)+TimeSpan.FromMilliseconds(new Random().Next(0,100))),
            Policy.Handle<Exception>()
            .WaitAndRetryAsync(6,retry=>TimeSpan.FromSeconds(1)),
            Policy.Handle<HttpRequestException>().            
        };
    }

4.斷路器模式

當某些故障是非瞬間故障時,一直重試通常是無意義的,而且也消耗資源。當重試到達一定的次數時,可以判斷為非瞬間故障,斷路器被觸發,則不再重試;斷路器恢復後,則可以重試。

CircuitBreakerAsync(5,TimeSpan.FromMinutes(1))

二、保證微服務地址可用

前端通常通過域名或IP地址作為字首來訪問特定的微服務WebApi的介面。在IT運維調整的情況下,微服務所在的域名或IP地址可能會發生變化,這樣前端使用者在拿到新的域名或IP地址前,將無法正常呼叫服務。

為了解決這個問題,我們就需要將微服務通過一個API閘道器組織起來。API閘道器會手工或自動配置它所管理的微服務的具體地址,當前端直接呼叫的API閘道器的服務時,API閘道器會根據配置來正確路由請求到特定域名或IP地址的服務。

1.API閘道器手工配置所路由的WebApi

這種情況需要在API閘道器手工新增某個服務請求應該路由到哪個特定的域名或IP地址的WebApi介面。手工配置的Json配置檔案內容如下:

這裡的Upstream指的就是前端呼叫API閘道器的特定服務時,Downstream指的就是路由到哪個特定的WebApi。有了配置檔案後,就可以使用相關的API閘道器庫載入配置檔案到API閘道器的WebApi中。

2.WebApi自動註冊地址資訊

如果總是通過手工配置對映資訊,還是比較麻煩。我們可以讓WebApi自己將資訊註冊到一個服務中心中,然後API閘道器利用這個服務中心的資訊實現請求的自動路由。

a.服務中心提供註冊功能

public static class AppBuilderExtension
    {
        public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app,
            IApplicationLifetime lifetime,ServiceEntity serviceEntity)
        {
            var consulClient = new ConsulClient(x => x.Address = 
            new Uri($"http://{serviceEntity.ConsulIP}:{serviceEntity.ConsulPort}"));
            var httpCheck = new AgentServiceCheck()
            {
                DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),
                Interval = TimeSpan.FromSeconds(10),
                HTTP = $"http://{serviceEntity.IP}:{serviceEntity.Port}/api/health",
                Timeout = TimeSpan.FromSeconds(5)
            };
            var registration = new AgentServiceRegistration()
            {
                Checks = new[] { httpCheck },
                ID = Guid.NewGuid().ToString(),
                Name = serviceEntity.ServiceName,
                Address = serviceEntity.IP,
                Port = serviceEntity.Port,
                Tags = new[] { $"urlprefix-/{serviceEntity.ServiceName}" }
            };
            consulClient.Agent.ServiceRegister(registration).Wait();
            lifetime.ApplicationStopped.Register(() =>
            {
                consulClient.Agent.ServiceDeregister(registration.ID).Wait();
            });
            return app;
        }
    }

b.WebApi註冊到服務中心

ServiceEntity serviceEntity = new ServiceEntity
            {
                IP = Configuration["Service:Address"],
                Port = Convert.ToInt32(Configuration["Service:Port"]),
                ServiceName=Configuration["Service:Name"],
                ConsulIP = Configuration["Consul:IP"],
                ConsulPort = Convert.ToInt32(Configuration["Consul:Port"])
            };
            app.RegisterConsul(lifetime, serviceEntity);

c.API閘道器利用服務中心資訊自動路由

好了,本篇文章關於微服務的高可用性就介紹到這裡。

QQ討論群:309287205 

微服務實戰視訊請關注微信公眾號: