前言
在大规模应用系统接入观测云的场景中,客户通常有通过自建消息中心发送各类告警通知的需求。需要我们借助 Func 平台封装不同的消息发送接口,实现告警通知的灵活投递。这些自建消息中心往往引入了若干通信服务商,提供电话、短信、IM 消息发送等服务。不同服务商的通道质量和通信成本有所差别,这样就产生了消息通道流控的需求。需要 Func 平台基于不同通信服务商的投递成本和服务质量,结合用户主动设置的消息投递权重,动态管理某一类消息在不同服务商之间的投递调用次数。实现消息触达率和费用成本的最优平衡。
权重管理的设计
在设计权重管理机制时,通常需要考虑的两个因素是初始分配的通道权重和影响后续权重的通道质量系数。
初始通道权重可基于厂商成本或其他管理因素进行人工配置,例如某个厂商在特定时期提供的消息服务价格显著低于其他厂商时,可基于单位消息发送量的成本反比设置通道发送初始权重比例。另外,如果某些业务有特殊的管理要求,需要优先选择某个通信厂商进行消息投递,这也将影响我们对消息通道权重的设置。在代码中我们将通过相对比例关系的方式对通道权重进行预设。
第二个需要考虑的因素是单位时间内的消息投递触达率,也就是通道质量系数。作为通道发送权重管理的模块,需要依据每个通道近期的消息触达率和发送成功率,动态调整每个服务的权重系数。在下一个发送周期内,为发送触达率较高的通道分配更多的发送权限,确保发送服务质量不降低。
基于上述机制,我们需要实现两个核心逻辑:权重和质量系数的管理,以及权重*系数在流控过程中的逻辑实现。
权重实现机制------令牌桶
令牌桶是一种常用的流量控制算法,广泛应用于网络流量管理和请求限速等场景。在上述消息通知系统中,令牌桶机制可用于控制每个通信厂商的消息发送频率,确保消息发送通道的使用符合我们对各通信服务商的使用权重管理。
令牌桶模型的核心思想是通过令牌的发放和消耗来控制请求的流量。在我们的实践中,每个通信服务商都会分配一个令牌桶,桶内存放着一定数量的令牌。只有当桶中有令牌时,用户才能通过该厂商进行消息发送。令牌更新的机制有很多种,常见的方式包含:
- 固定时间间隔更新:最常见的方式是在固定的时间间隔内向令牌桶中添加一定数量的令牌。例如,每秒添加 10 个令牌。这种方法简单易懂,适合对发送速率有明确要求的场景。
- 动态速率更新:根据系统负载或网络状态的变化动态调整更新速率。比如在高峰期可以增加令牌的生成速率,而在闲置期则减少或暂停生成。这种方法能够更灵活地适应不同的业务需求。
- 基于事件的更新:在特定事件发生时触发令牌的生成。例如,当检测到发送失败率过高时,系统可以立即增加令牌数量,以提升消息发送能力。这种方法能够快速响应变化,适用于实时性要求高的场景。
- 按需更新:根据当前令牌的使用情况和未使用的令牌数量进行更新。如果当前令牌数量低于某个阈值,则增加令牌;如果令牌数量充足,则不进行更新。这种方法可以有效避免资源浪费。
在我们的实践中,可以简单的选择按需更新的方式来为所有消息通道分配流控资源。例如,当所有厂商令牌耗尽时,进入下一个周期的令牌发放。这时可以根据最新的权重配置和质量系数调整令牌数量,进而动态调整每个厂商的消息发送能力。一个令牌桶的实现示例如下:
python
class TokenBucket:
def __init__(self, capacity):
#设置令牌桶的最大容量,如按照固定比例分配,也可不检查这个值
self.capacity = capacity
#初始令牌数量
self.tokens = 0
def add_tokens(self, token_count):
#根据给定的数量token_count,更新令牌桶中的令牌
self.tokens += token_count
if self.tokens > self.capacity:
self.tokens = self.capacity # 限制令牌数量不超过桶的容量
def remove_token(self):
#尝试获取令牌,判断能否发送消息
# 如果桶中有令牌,则允许发送消息
if self.tokens >= 1:
# 移除令牌
self.tokens -= 1
return True
# 桶中没有令牌,发送失败
return False
质量系数管理
目前主流的通信服务厂商,在提供消息投递服务时,其服务结果的反馈往往是通过异步调用的方式实现,这意味着质量系数更新会稍滞后于实际通道的使用,且我们需要提供一个接口,用于接收厂商回调的消息发送统计。在这个接口中,我们需要按预设的成功率和质量系数调整步长,实现对消息发送质量的评估和质量系数的更新。
质量系数的评估除了获取特定厂商的发送成功率,还需要考虑的三个因素是权重保持阈值、惩罚步长和质量系数下限。
- 权重保持阈值用于判断当前的发送质量是否需要对该消息通道进行权重扣减,通常需要对齐到通信服务商承诺的 SLA;
- 惩罚步长没有特定的设计规则,可按照经验或者管理要求设置,但通常不应一次扣减太多,这样容易导致不同服务商之间的发送数量波动较大,进而影响主要流控因素------基于发送成本的流控管理;
- 最后是发送系数下限的设置,通常一个消息通道在服务期内应避免被完全废弃,因此在代码实现中需考虑质量系数的扣减下限,避免超扣导致对应消息通道无法被充分利用。
一个质量系数管理的实现逻辑示例如下:
python
class WeightAdjuster:
def __init__(self, weights, threshold):
self.weights = weights
# 初始化质量系数,这里具体要初始化几个系数,取决于当前需要参与流控的厂商有几家
#
self.quality_factors = [1.0] * len(weights)
# 预设成功率阈值,即发送成功率高于多少时,认为不需要调整当前的质量系数
self.threshold = threshold
def update_quality_factors(self, vendor_index, success_rate):
"""根据消息发送成功率更新对应厂商的质量系数。"""
# 如果成功率低于阈值,进行质量系数扣减
if success_rate < self.threshold:
# 这里的扣减步长为0.05,最低阈值假设为0.5
# 实际应用中可根据您的客户要求来调整。或通过配置块来统一管理
self.quality_factors[vendor_index] = max(self.quality_factors[vendor_index] - 0.05, 0.5)
def calculate_tokens(self, total_tokens):
"""根据权重和质量系数计算每个厂商的令牌数量,用于下一阶段的消息发送权重数据更新"""
adjusted_weights = [weight * quality for weight, quality in zip(self.weights, self.quality_factors)]
total_adjusted_weight = sum(adjusted_weights)
token_distribution = [int((weight / total_adjusted_weight) * total_tokens) for weight in adjusted_weights]
return token_distribution
基于权重和系数的消息流控流程
有了权重管理机制和质量系数的管理机制,我们就可以通过代码来实现对多个厂商消息通道的流控管理了,在这个管理流程中,每个消息在发送时都会检查每个厂商的令牌余额,如当前消息通道存在令牌额度,则使用该厂商的消息通道进行消息投递。当所有令牌耗尽时,则需要根据通道权重和质量系数,重新充值令牌桶并进行发送。进而实现对多个消息通道的流控策略实现。结合上述两个流程的代码,这里提供一个完整的流控过程例子供您参考和使用:
python
import time
import random
import threading
class TokenBucket:
def __init__(self, capacity):
self.capacity = capacity
self.tokens = 0
def add_tokens(self, token_count):
self.tokens += token_count
if self.tokens > self.capacity:
self.tokens = self.capacity
def remove_token(self):
if self.tokens >= 1:
self.tokens -= 1
return True
return False
'''
这里通过代码来模拟厂商的消息发送过程
'''
class Vendor:
def __init__(self, name, token_bucket):
self.name = name
self.token_bucket = token_bucket
def send_message(self, message):
"""发送消息,如果成功则输出消息内容。"""
if self.token_bucket.remove_token():
# 通常这个发送成功率是异步过程,需要单独的方法来实现
# 这里简化处理为同步过程,来演示流控管理过程
# 随机生成一个成功率,用于后面的质量系数计算,实际使用时请更换为真实的返回值处理
success_rate = random.random()
print(f"{self.name} 发送消息: {message}, 成功率: {success_rate:.2f}")
return success_rate
else:
# 发送失败
return None
class WeightAdjuster:
def __init__(self, weights, threshold):
self.weights = weights
self.quality_factors = [1.0] * len(weights)
self.threshold = threshold
def update_quality_factors(self, vendor_index, success_rate):
"""根据成功率更新对应厂商的质量系数。"""
if success_rate < self.threshold:
self.quality_factors[vendor_index] = max(self.quality_factors[vendor_index] - 0.05, 0.5)
def calculate_tokens(self, total_tokens):
"""根据权重和质量系数计算每个厂商的令牌数量。"""
adjusted_weights = [weight * quality for weight, quality in zip(self.weights, self.quality_factors)]
total_adjusted_weight = sum(adjusted_weights)
token_distribution = [int((weight / total_adjusted_weight) * total_tokens) for weight in adjusted_weights]
return token_distribution
'''
主逻辑实现,假设有3家通信服务商需要参与流控,初始权重比例为7:2:1.
质量系数的判定阈值为发送成功率0.5(当然实际不可能要求这么低。。。可根据您的场景要求来调整这个阈值)
'''
# 初始化
weights = [7, 2, 1]
# 设定成功率阈值
threshold = 0.5
# 为3个厂商创建对应的令牌桶
buckets = [TokenBucket(10) for _ in range(3)]
vendors = [Vendor("厂商 A", buckets[0]), Vendor("厂商 B", buckets[1]), Vendor("厂商 C", buckets[2])]
adjuster = WeightAdjuster(weights, threshold)
'''
模拟调用厂商接口进行消息发送的逻辑,为简化演示过程,发送成功率为同步实时返回。
实际应用中这个过程通常为异步处理,需要单独的逻辑来实现发送成功率消息的接收和质量系数的更新
'''
def send_messages():
message = "这是一个语音通知。"
while True:
all_tokens_used = True
# 简化演示,发送成功率为同步返回,需要准备一个参数来保存
success_rates = []
for i, vendor in enumerate(vendors):
success_rate = vendor.send_message(message)
if success_rate is not None:
# 记录厂商索引和成功率
success_rates.append((i, success_rate))
# 这个标识的设置用于记录有厂商成功发送消息
# 因本例使用令牌桶按需更新的机制,当所有厂商都无法发送消息时,需要通过标志来通知
# 令牌桶更新
all_tokens_used = False
# 根据本次发送消息接口返回的发送成功率更新质量系数
for index, success_rate in success_rates:
adjuster.update_quality_factors(index, success_rate)
if all_tokens_used:
'''
# 所有令牌已用完,需要重新计算并发放令牌
# 假设每次发放量为300个令牌,调用calculate_tokens方法时会根据当前最新的权重数据
# 和质量系数对每个厂商的令牌进行更新。
# 这意味着用户可以随时对各厂商的权重进行调整,每次调整的新权重比例都将在下次分配令牌时
# 对令牌数量产生影响
'''
tokens = adjuster.calculate_tokens(300)
# 按照权重计算的结果,更新每个厂商的令牌桶
for idx, bucket in enumerate(buckets):
bucket.add_tokens(tokens[idx])
print("所有厂商令牌用尽,已更新令牌并重新尝试发送消息。")
# 每秒尝试发送一次消息,确保每个消息都能正常发送
time.sleep(1)
# 主执行逻辑
if __name__ == "__main__":
send_messages()
总结
通过采用动态管理的令牌桶机制,该实践有效解决了多通道消息发送中的通道权重管理问题,在确保消息通道服务质量的前提下兼顾了通道使用成本。且具备进一步优化和扩展的潜力,为实现更智能的消息投递系统打下了基础。