一、简介

负载均衡(Load Balance),简称 LB,就是将并发的用户请求通过规则后平衡、分摊到多台服务器上进行执行,以此达到压力分摊、数据并行的效果。常见的算法也有许多随机、轮询、加权等,今天我们就使用 C# 来实现这几种算法,并讲解在实际项目中的使用。

二、应用场景

负载均衡算法在开发层面,使用场景其实并不多。通常在项目重构、转型、上线大版本新功能等,为了避免上线出现 Bug 应用功能 100% 的挂掉。可以在程序中使用负载均衡,将部分 HTTP 流量,打入项目中新的功能模块,然后进行监控,出现问题可以及时进行调整。

这样 AB 测试的场景,也可以在运维或者网关等其他层面实现流量分配。但现实是大多数公司项目因为一些原因没有这样的支持,这时开发就可以在项目中使用代码进行实现。

三、实际案例

有这样一个需求,电商系统中,有一个预估运费的微服务(ShippingCharge )。此时上面领导来了需求,预估运费要改版,开发预估了一下改动不小。经过两周的奋斗 ShippingCharge 需求终于开发测试好了,此时要上线,但是掐指一算,万一有问题不就死翘翘了,而且还和钱相关。

此时负载均衡算法就派上用场了,我们可以让 10% 的流量打入这次的改动,可以先进行监控,可以再全部切过来。实际项目中,使用的肯定是权重的,后面随机、轮询也简单进行介绍一下其实现。

假设在改动 ShippingCharge 时,没有修改旧的功能,是在 controller 下面,对 call business 层换成了这次需求的,这样我们就可以使用负载均衡,让 10% 的流量打入新的 business,其余的依然走老的 business。

四、算法实现

这里不会说的太精细,会将核心实现代码做介绍,实际项目中使用需要自己进行一下结合,举一反三哈

下面定义了一个 ServiceCenterModel 主要用作承载需要负载均衡的对象信息,可以是 call 下游的 url,也可以是程序内的某一算法标识

public class ServiceCenterModel
{
    /// <summary>
    /// Service
    /// 1. call 下游 server,可以放 url
    /// 2. 在同一个程序内,可以放一个业务标识
    /// </summary>
    public string Service { get; set; }
    public int Weight { get; set; }
}

4.1 随机

随机算法的先对来讲,较为简单一些,主要根据 Random 与 ServiceList 的数量结合实现。如下:

/// <summary>
/// 随机
/// </summary>
public class RandomAlgorithm
{
    /// <summary>
    /// Random Function
    /// </summary>
    private static readonly Random random = new Random();

    /// <summary>
    /// serviceList
    /// </summary>
    /// <param name="serviceList">service url set</param>
    /// <returns></returns>
    public static string Get(List<ServiceCenterModel> serviceList)
    {
        if (serviceList == null)
            return null;

        if (serviceList.Count == 1)
            return serviceList[0].Service;

        // 返回一个小于所指定最大值的非负随机数
        int index = random.Next(serviceList.Count);

        string url = serviceList[index].Service;

        return url;
    }
}

模拟 10 次 http request,可以看到对OldBusiness、NewBusiness进行了随机的返回

public static void Main(string[] args)
{
    // 模拟从配置中心读取 Service 
    var serviceList = new List<ServiceCenterModel>()
    {
        new ServiceCenterModel { Service ="OldBusiness"},
        new ServiceCenterModel { Service ="NewBusiness"},
    };

    // 模拟 Http 请求次数
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(RandomAlgorithm.Get(serviceList));
    }
}

负载均衡高可用 负载均衡使用场景_负载均衡高可用

4.2 轮询

轮询的实现思路,将每次读取 ServiceList 的 Index 放到静态全局变量中,当到 ServiceList 最后一个时从0开始读取。如下:

/// <summary>
/// 轮询
/// </summary>
public class PollingAlgorithm
{
    private static Dictionary<string, int> _serviceDic = new Dictionary<string, int>();
    private static SpinLock _spinLock = new SpinLock();

    /// <summary>
    /// Get URL From Service List
    /// </summary>
    /// <param name="serviceList">Service URL Set</param>
    /// <param name="serviceName">Service Name</param>
    /// <returns></returns>
    public static string Get(List<ServiceCenterModel> serviceList, string serviceName)
    {
        if (serviceList == null || string.IsNullOrEmpty(serviceName))
            return null;

        if (serviceList.Count == 1)
            return serviceList[0].Service;

        bool locked = false;
        _spinLock.Enter(ref locked);//获取锁

        int index = -1;
        if (!_serviceDic.ContainsKey(serviceName)) // Not Exist
            _serviceDic.TryAdd(serviceName, index);
        else
            _serviceDic.TryGetValue(serviceName, out index);

        string url = string.Empty;
        ++index;
        if (index > serviceList.Count - 1) //当前索引 > 最新服务最大索引
        {
            index = 0;
            url = serviceList[0].Service;
        }
        else
        {
            url = serviceList[index].Service;
        }
        _serviceDic[serviceName] = index;

        if (locked) //释放锁
            _spinLock.Exit();

        return url;
    }
}

模拟 10 次 http request,可以看到对OldBusiness、NewBusiness进行了轮询返回

public static void Main(string[] args)
{
    // 模拟从配置中心读取 Service 
    var serviceList = new List<ServiceCenterModel>()
    {
        new ServiceCenterModel { Service ="OldBusiness"},
        new ServiceCenterModel { Service ="NewBusiness"},
    };

    // 模拟 Http 请求次数
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(PollingAlgorithm.Get(serviceList, "ShippingChargeBusiness"));
    }
}

负载均衡高可用 负载均衡使用场景_c#_02

4.3 权重

权重的实现思路,将配置权重的 Service 按照数量放置在一个集合中,然后按照轮询的方式进行读取,需要注意的是这的 weight 只能配置大于 0 的整数。如下:

/// <summary>
/// 权重
/// </summary>
public class WeightAlgorithm
{
    private static ConcurrentDictionary<string, WeightAlgorithmItem> _serviceDic = new ConcurrentDictionary<string, WeightAlgorithmItem>();
    private static SpinLock _spinLock = new SpinLock();

    public static string Get(List<ServiceCenterModel> serviceList, string serviceName)
    {
        if (serviceList == null)
            return null;

        if (serviceList.Count == 1)
            return serviceList[0].Service;

        bool locked = false;
        _spinLock.Enter(ref locked);//获取锁

        WeightAlgorithmItem weightAlgorithmItem = null;
        if (!_serviceDic.ContainsKey(serviceName))
        {
            weightAlgorithmItem = new WeightAlgorithmItem()
            {
                Index = -1,
                Urls = new List<string>()
            };
            BuildWeightAlgorithmItem(weightAlgorithmItem, serviceList);
            _serviceDic.TryAdd(serviceName, weightAlgorithmItem);
        }
        else
        {
            _serviceDic.TryGetValue(serviceName, out weightAlgorithmItem);
            weightAlgorithmItem.Urls.Clear();
            BuildWeightAlgorithmItem(weightAlgorithmItem, serviceList);
        }

        string url = string.Empty;
        ++weightAlgorithmItem.Index;
        if (weightAlgorithmItem.Index > weightAlgorithmItem.Urls.Count - 1) //当前索引 > 最新服务最大索引
        {
            weightAlgorithmItem.Index = 0;
            url = serviceList[0].Service;
        }
        else
        {
            url = weightAlgorithmItem.Urls[weightAlgorithmItem.Index];
        }
        _serviceDic[serviceName] = weightAlgorithmItem;

        if (locked) //释放锁
            _spinLock.Exit();

        return url;
    }

    private static void BuildWeightAlgorithmItem(WeightAlgorithmItem weightAlgorithmItem, List<ServiceCenterModel> serviceList)
    {
        serviceList.ForEach(service => //有几个权重就加几个实例
        {
            for (int i = 0; i < service.Weight; i++)
            {
                weightAlgorithmItem.Urls.Add(service.Service);
            }
        });
    }
}

public class WeightAlgorithmItem
{
    public List<string> Urls { get; set; }
    public int Index { get; set; }
}

模拟 10 次 http request,可以看到对 OldBusiness 返回了 9 次,NewBusiness 返回了一次

public static void Main(string[] args)
{
    // 模拟从配置中心读取 Service 
    var serviceList = new List<ServiceCenterModel>()
    {
        new ServiceCenterModel { Service ="OldBusiness",Weight = 9 },
        new ServiceCenterModel { Service ="NewBusiness",Weight = 1 },
    };

    // 模拟 Http 请求次数
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(WeightAlgorithm.Get(serviceList, "ShippingChargeBusiness"));
    }
}

负载均衡高可用 负载均衡使用场景_权重_03