C#开发负载均衡服务器实例

作者:袖梨 2022-06-25

思路

负载均衡服务器最出名的当数 Nginx了。Nginx服务器通过异步的方式把连接转发给内网和N个服务器,用来分解单台应用服务器的压力,了解了原理及场景后,用C#来实现一个。思路如下:

1. 使用一个站点的 Application_BeginRequest 来接收连接,转发连接。

2. 对各类静态资源做单独处理,(可转可不转)

3. 可以转发Get,Post,异步转发。

4. 对指定的请求,转发到同一台服务器,保持使用者的登录状态。

实现

Vs2015建一个Mvc建站: localhost:1500/。修改Web.config,用于接收所有连接。

 
   
   

 


引入 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 Action { get; set; }
    public RequestWrap(HttpWebRequest request)
    {
        Event = new ManualResetEvent(false);
        this.Request = request;
    }

    public void Run(Action 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);
        }
    }

    // 设置之后,静态资源就进来了。
    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 的共享和身份鉴权等。但由于分布式负载均衡通常使用反向代理来完成入口服务器与多个分布式运算服务器之间的传输,因此在分布式负载均衡环境中,运行在不同服务器(或相同服务器上的不同 IIS 站点实例)可能在 Request URL 运算、文件存储、相对路径映射等这些方面与单实例运行时存在差异。因此,在准备阶段请先确定你已经解决了上述这些问题,在这里我就不展开了,毕竟针对不同的应用会有不同的具体工作。而后,接下来的工作就显得很从容了。分布式负载均衡的一般思路是整个系统仅有一个统一入口,再由这个统一入口 E 按配置的均衡规则将请求「反向」代理到多个运算服务器 S,最后再将由 S 运算得到的结果输出到客户端。在每个 S 上,使用单实例时一样的配置:每一个 S 就是一个在普通部署方式下的正常服务器:因此要将它们的数据库连接串、Session 提供程序、Cache 提供程序、Form 鉴权所需的必要的 machineKey 等相关资源配备到统一位置或作统一处理,以保证所有 S 工作状态的一致性(即当用户的请求到达任何 S 时,其会话状态、缓存状态等都一致)。在 E 的反向代理软件上,将所有 S 按策略配置完善(包括服务器位置、端口与权值;代理 IP和原始 host 头等)。出于安全考虑,可以将所有 S 的访问限制为仅 E 能访问,而 E 则应该配置为能够公开访问,最后将域名解析到 E 上。注意,所有后续请求将先到达 E 上,然后再由 E 按照配置的策略分配给具体的某一个 S。因此,如果应用中有上传文件、自定义 MIME 类型、错误页等可能与 IIS 默认限制/功能相关的特性,注意 E 与 S 在这些配置上的一致性。此外,关于 E 上要使用的反向代理软件,可以使用大家广为熟悉的 nginx,也可以使用微软与 IIS 集成的 Application Request Routing 组件(ARR)。

相关文章

精彩推荐