思路
負載平衡伺服器最出名的當數 Nginx了。Nginx伺服器通過非同步方式把串連轉寄給內網和N個伺服器,用來分解單台應用伺服器的壓力,瞭解了原理及情境後,用C#來實現一個。思路如下:
1. 使用一個網站的 Application_BeginRequest 來接收串連,轉寄串連。
2. 對各類靜態資源做單獨處理,(可轉可不轉)
3. 可以轉寄Get,Post,非同步轉寄。
4. 對指定的請求,轉寄到同一台伺服器,保持使用者的登入狀態。
實現
Vs2015建一個Mvc建站: localhost:1500/。修改Web.config,用於接收所有串連。
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
</modules>
</system.webServer>
引入 MyCmn.dll (http://code.taobao.org/svn/MyOql/libs4),MyHelper 封裝了 類型轉換函式,方便使用。
代碼如下:
public class RequestWrap
{
public HttpWebRequest Request { get; set; }
private ManualResetEvent Event { get; set; }
private Action<HttpWebResponse> Action { get; set; }
public RequestWrap(HttpWebRequest request)
{
Event = new ManualResetEvent(false);
this.Request = request;
}
public void Run(Action<HttpWebResponse> act)
{
this.Action = act;
Request.BeginGetResponse(new AsyncCallback(GetResponseCallback), this);
this.Event.WaitOne(15000);
}
private static void GetResponseCallback(IAsyncResult asyncResult)
{
RequestWrap wrap = (RequestWrap)asyncResult.AsyncState;
HttpWebResponse response = null;
try
{
response = wrap.Request.EndGetResponse(asyncResult) as HttpWebResponse;
}
catch (WebException ex)
{
response = ex.Response as HttpWebResponse;
}
wrap.Action(response);
wrap.Event.Set();
}
}
private void Application_BeginRequest(Object source, EventArgs e)
{
var lastExtName = "";
{
var lastIndex = Request.Url.LocalPath.LastIndexOf('.');
if (lastIndex > 0)
{
lastExtName = Request.Url.LocalPath.Slice(lastIndex);
}
}
// <modules runAllManagedModulesForAllRequests="true"> 設定之後,靜態資源就進來了。
if (lastExtName.IsIn(".js", ".css", ".html", ".htm", ".png", ".jpg", ".gif"))
{
return;
}
HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(GetTransferHost() + Request.RawUrl);
myRequest.Proxy = null;
myRequest.Timeout = 15000;
myRequest.ReadWriteTimeout = 3000;
myRequest.Method = Request.HttpMethod;
Request.Headers.AllKeys.All(k =>
{
if (!WebHeaderCollection.IsRestricted(k))
{
myRequest.Headers.Add(k, Request.Headers[k]);
}
else
{
var val = Request.Headers[k];
if (k.Is("Range"))
{
myRequest.AddRange(val.AsInt());
}
else if (k.Is("If-Modified-Since"))
{
myRequest.IfModifiedSince = val.AsDateTime();
}
else if (k.Is("Accept"))
{
myRequest.Accept = val;
}
else if (k.Is("Content-Type"))
{
myRequest.ContentType = val;
}
else if (k.Is("Expect"))
{
myRequest.Expect = val;
}
else if (k.Is("Date"))
{
myRequest.Date = val.AsDateTime();
}
else if (k.Is("Host"))
{
myRequest.Host = val;
}
else if (k.Is("Referer"))
{
myRequest.Referer = val;
}
else if (k.Is("Transfer-Encoding"))
{
myRequest.TransferEncoding = val;
}
else if (k.Is("User-Agent"))
{
myRequest.UserAgent = val;
}
//else if (k.Is("Connection"))
//{
// o.Connection = val;
//}
else if (k.Is("KeepAlive"))
{
myRequest.KeepAlive = val.AsBool();
}
}
return true;
});
using (var readStream = Request.InputStream)
{
while (true)
{
byte[] readBuffer = new byte[1024];
var readLength = readStream.Read(readBuffer, 0, readBuffer.Length);
if (readLength == 0) break;
myRequest.GetRequestStream().Write(readBuffer, 0, readLength);
}
}
new RequestWrap(myRequest).Run(myResponse =>
{
myResponse.Headers.AllKeys.All(k =>
{
if (k.Is("X-Powered-By"))
{
return true;
}
Response.Headers[k] = myResponse.Headers[k];
return true;
});
using (var readStream = myResponse.GetResponseStream())
{
while (true)
{
byte[] readBuffer = new byte[1024];
var read = readStream.Read(readBuffer, 0, readBuffer.Length);
if (read == 0) break;
Response.OutputStream.Write(readBuffer, 0, read);
}
}
Response.StatusCode = myResponse.StatusCode.AsInt();
});
Response.End();
}
public static string GetClientIPAddress()
{
if (HttpContext.Current == null)
return string.Empty;
HttpContext context = HttpContext.Current;//System.Web.HttpContext.Current;
string ipAddress = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
if (!string.IsNullOrEmpty(ipAddress))
{
string[] addresses = ipAddress.Split(',');
if (addresses.Length != 0)
{
return addresses[0];
}
}
return context.Request.ServerVariables["REMOTE_ADDR"]; //+ " host " + context.Request.UserHostAddress;
}
private string GetTransferHost()
{
string[] hosts = new string[] { "http://localhost" };
var index = GetClientIPAddress().Last() % hosts.Length ;
return hosts[index];
}
解釋
其中, RequestWrap 是對非同步請求封裝的請求類。封裝了一個 Run 方法進行非同步呼叫。過濾了應用伺服器的回傳頭 X-Powered-By
關於 WebHeaderCollection.IsRestricted ,是由於一個錯誤引出的: 異常處理:必須使用適當的屬性或方法修改此標題,文章地址: http://blog.useasp.net/archive/2013/09/03/the-methods-to-dispose-http-header-cannot-add-to-webrequest-headers.aspx,摘錄如下:
你可以在這裡設定其他限制的標題.
注意:
Range HTTP標題是通過AddRange來添加
If-Modified-Since HTTP標題通過IfModifiedSince 屬性設定
Accept由 Accept 屬性設定。
Connection由 Connection 屬性和 KeepAlive 屬性設定。
Content-Length由 ContentLength 屬性設定。
Content-Type由 ContentType 屬性設定。
Expect由 Expect 屬性設定。
Date由 Date屬性設定,預設為系統的目前時間。
Host由系統設定為當前主機資訊。
Referer由 Referer 屬性設定。
Transfer-Encoding由 TransferEncoding 屬性設定(SendChunked 屬性必須為 true)。
User-Agent由 UserAgent 屬性設定。
ASP.NET C# 如何做分布式負載平衡?具體思路?
曾在去年為一個客戶做過基於 nginx 的負載平衡,後來再加上一些新的實踐,總結了一下,願意分享。實際上,我目前接觸的項目,也並不算大,並沒有針對超大型項目(如事務和邏輯密集型、大量並發的應用)做過相關實踐,如果有人在相關領域有更好的方案,歡迎探討。分布式的程式與單個執行個體運行是有很大區別的,所以在分布式之前,請確保你的應用在開發階段最好有相關的準備。一個完全針對單伺服器環境開發的應用,要想在分布式均衡系統中完美運行,要做的工作之多會讓營運人員抓狂。IIS 提供了 Web Farm 部署的能力,即針對在同一台伺服器上的特定 Web 網站,為其應用程式集區配備多個進程。這種方式可以一定程式上類比應用在分布式環境下的運行情況,比如 Session、Cache 的共用和身份鑒權等。但由於分布式負載平衡通常使用反向 Proxy來完成入口伺服器與多個分布式運算伺服器之間的傳輸,因此在分布式負載平衡環境中,運行在不同伺服器(或相同伺服器上的不同 IIS 網站執行個體)可能在 Request URL 運算、檔案儲存體、相對路徑映射等這些方面與單一實例運行時存在差異。因此,在準備階段請先確定你已經解決了上述這些問題,在這裡我就不展開了,畢竟針對不同的應用會有不同的具體工作。而後,接下來的工作就顯得很從容了。分布式負載平衡的一般思路是整個系統僅有一個統一入口,再由這個統一入口 E 按配置的均衡規則將請求「反向」代理到多個運算伺服器 S,最後再將由 S 運算得到的結果輸出到用戶端。在每個 S 上,使用單一實例時一樣的配置:每一個 S 就是一個在普通部署方式下的正常伺服器:因此要將它們的資料庫連接串、Session 提供者、Cache 提供者、Form 鑒權所需的必要的 machineKey 等相關資源配備到統一位置或作統一處理,以保證所有 S 工作狀態的一致性(即當使用者的請求到達任何 S 時,其工作階段狀態、緩衝狀態等都一致)。在 E 的反向 Proxy軟體上,將所有 S 按策略配置完善(包括伺服器位置、連接埠與權值;代理 IP和原始 host 頭等)。出於安全考慮,可以將所有 S 的訪問限制為僅 E 能訪問,而 E 則應該配置為能夠公開訪問,最後將網域名稱解析到 E 上。注意,所有後續請求將先到達 E 上,然後再由 E 按照配置的策略分配給具體的某一個 S。因此,如果應用中有上傳檔案、自訂 MIME 類型、錯誤頁等可能與 IIS 預設限制/功能相關的特性,注意 E 與 S 在這些配置上的一致性。此外,關於 E 上要使用的反向 Proxy軟體,可以使用大家廣為熟悉的 nginx,也可以使用微軟與 IIS 整合的 Application Request Routing 組件(ARR)。