前言
通过前面的几篇文章,讲解了一个短信服务的架构设计与实现。然而初始方案并非100%完美的,我们仍可以对该架构做一些优化与调整。
同时我也希望通过这篇文章与大家分享一下,我的架构设计理念。
源码地址:https://github.com/SkyChenSky/Sikiro.SMS/tree/optimize (与之前的是另外的分支)
架构是设计的还是演变的?
架构
该词出自于建筑学。软件架构定义是指软件系统的基础结构,是系统中的实体及实体(服务)之间的关系所进行的抽象描述。而架构设计的目的是为了解决软件系统复杂度带来的问题。
复杂度
系统复杂度主要有下面几点:
- 高可用
- 高性能
- 可扩展
- 安全性
- 维护成本
- 用户规模
业务规模
系统的复杂度导致的直接原因是业务规模。为了用户流畅放心的使用产品,不得不提高系统性能与安全。当系统成为人们生活不可缺一部分时,避免机房停电、挖掘机挖断电缆导致的系统不可用,不得不去思考同城跨机房同步、异地多活的高可用方案。
答案并非二选一
我认为架构,需要在已知可见的业务复杂度与用户规模的基础上进行架构设计;伴随着技术积累与成长而对系统进行架构优化;用户的日益增长,业务的不断扩充,迫使了系统的复杂度增加,为了解决系统带来新的复杂度而进行架构演变。
因此,架构方案是在已有的业务复杂度、用户规模、技术积累度、人力时间成本等几个方面的取舍决策后的结果体现。
原架构
缺点分析
- 一般情况下,调度任务轮询数据库,90%的动作是无用功,频繁的数据库访问会对数据库增加不少压力。
- 为了让调度任务服务进行轮循数据,需要在API优先进行数据持久化,这无疑是降低了API的性能。
- MongoDB的Update操作相比于Insert操作时低效的,对于日志类数据应增量添加。
因此从上述可见,调度任务服务这块是优化关键点所在。
新架构图
- 使用了RabbitMQ的队列定时任务代替调度任务来实现定时发送。
- 抛弃了调度任务,减少了调用链,同时也减少了应用服务数据量。
- 对SMS集合在MongoDB里进行按年月的时间划分,对于日志类数据可以在有效的时间范围外进行方便的归档、删除。同时也避免了同集合的数据量过大导致的查询效率缓慢。
队列定时任务
RabbitMQ自身并没有定时任务,然而可以通过消息的Time-To-Live(过期时间)与Dead Letter Exchange(死信交换机)的结合模拟定时发布的功能。具体原理如下:
- 生产者发布消息,并发布到已申明消息过期时间(TTL)的缓存队列(非真正业务消费队列)
- 消息在缓存队列等待消息过期,然后由Dead Letter Exchange将消息重新分配到实际消费队列
- 消费者再从实际消费队列消费并完成业务
Dead Letter Exchange
Dead Letter Exchange与平常的Exchange无异,主要用于消息死亡后通过Dead Letter Exchange与x-dead-letter-routing-key重新分配到新的队列进行消费处理。
消息死亡的方式有三种:
- 消息进入了一条已经达到最大长度的队列
- 消息因为设置了Time-To-Live的导致过期
- 消息因basic.reject或者basic.nack动作而拒绝
Time-To-Live
两种消息过期的方式:
队列申明x-message-ttl参数 var args = new Dictionary<string, object>(); args.Add("x-message-ttl", 60000); model.QueueDeclare("myqueue", false, false, false, args); 每条消息发布声明Expiration参数 byte[] messageBodyBytes = System.Text.Encoding.UTF8.GetBytes("Hello, world!"); IBasicProperties props = model.CreateBasicProperties(); props.ContentType = "text/plain"; props.DeliveryMode = 2; props.Expiration = "36000000" model.BasicPublish(exchangeName, routingKey, props, messageBodyBytes);
RabbitMQ.Client队列定时任务Demo
class Program { static void Main(string[] args) { var factory = new ConnectionFactory { HostName = "10.1.20.140", UserName = "admin", Password = "admin@ucsmy" }; using (var connection = factory.CreateConnection()) using (var channel = connection.CreateModel()) { var queueName = "Queue.SMS.Test"; var exchangeName = "Exchange.SMS.Test"; var key = "Route.SMS.Test"; DeclareDelayQueue(channel, exchangeName, queueName, key); DeclareReallyConsumeQueue(channel, exchangeName, queueName, key); var body = Encoding.UTF8.GetBytes("info: test dely publish!"); channel.BasicPublish(exchangeName + ".Delay", key, null, body); } } private static void DeclareDelayQueue(IModel channel, string exchangeName, string queueName, string key) { var retryDic = new Dictionary<string, object> { {"x-dead-letter-exchange", exchangeName+".dl"}, {"x-dead-letter-routing-key", key}, {"x-message-ttl", 30000} }; var ex = exchangeName + ".Delay"; var qu = queueName + ".Delay"; channel.ExchangeDeclare(ex, "topic"); channel.QueueDeclare(qu, false, false, false, retryDic); channel.QueueBind(qu, ex, key); } private static void DeclareReallyConsumeQueue(IModel channel, string exchangeName, string queueName, string key) { var ex = exchangeName + ".dl"; channel.ExchangeDeclare(ex, "topic"); channel.QueueDeclare(queueName, false, false, false); channel.QueueBind(queueName, ex, key); } }