本文简单说明几个设计并发系统时需要考虑的问题,内容摘抄自《GO语言并发之道》
异常传递
异常是什么,什么时候发生,提供了哪些好处
首先,异常需要传达几个关键信息:
- 发生了什么:
这部分异常信息包括了岁异常时间的描述。例如:磁盘已满,连接被重置,证书过期等 - 发生在什么时间,什么位置:
异常应该包含完整的轨迹信息,从调用的启动方式开始,已异常的实例结尾。栈轨迹信息不应该被包含在异常信息中,但当需要处理栈中的异常时应该很容易被找到 - 对用户友好的信息:应当对展现给用户的信息进行自定义,应该只包含前两点的概述以及相关信息。对用户友好的信息是从用户的角度出发,给出一些信息,说明这些信息是否是暂时的,并且最好是一行以内的文本
- 告诉用户如何获取更多信息:某些情况下,用户希望知道当异常发生时具体发生了那些故障,展示给用户的异常信息应该提供一个id,利用这个id可以查到对应的详细日志,日志应该包含有完整的信息(异常的发生时间和异常时的堆栈调用)
当展示给用户的信息不包含这些信息,不是出错了就是有BUG。所以异常可以分为两类:
- BUG
- 已知信息
超时和取消
为什么要支持超时?
系统饱和
如果系统已经饱和(已经达到系统处理请求的能力),希望可以返回超时,而不是花很长的时间等待响应。
陈旧的数据
数据通常有一个窗口期,一般是在这个窗口中先处理更多相关数据或者处理数据的需求已经过期。
如果知道窗口期,那么将context.WithDeadline或context.WithTimeout创建的context传递给并发进程是有意义的,如果事先不知道窗口,我们希望并发进程的父节点能够在不再需要时取消并发进程。context.WithCancel是达到这个目的的最佳选择
试图防止死锁
通过设置超时可以将一个死锁系统转变为一个活锁系统,在系统死锁后,很可能会遇到时序配置不同步的情况。因此最好是在允许饿时间内修复活锁,好过发生死锁后只能通过重启系统才能恢复系统
这不是如何正确构建系统的建议,而是如何建立一个对时间问题有容错能力的系统
什么时候应当设置超时
- 请求在超时时不太可能重复
- 没有资源来存储请求
- 对系统的响应或请求发送数据有时效性要求
并发进程可能被取消的原因
- 超时-隐式取消
- 用户干预
- 父进程取消:如果父进程停止,那子进程也将被取消
- 复制请求:将数据发送给多个并发进程,以尝试从其中一个系统获得更快的响应。当第一个响应回来的时候,取消其余的进程。
心跳
心跳是并发进程向外界发出信号的一种方式。
心跳类型
- 在一段时间间隔内发出的心跳
- 在工作单元开始时发出的心跳
golang通过channel发送心跳的时候,需要注意有可能没有人接收发出去的心跳(因为心跳不一定重要)
复制请求
某些情况下,可以将请求分发到多个处理程序(goroutine,进程,或者服务器均可),其中一个将比其他程序更快的返回结果,这样就可以立即返回结果。但是会消耗更多的资源。当多个处理程序需要多个进程,服务器,或者数据中心时,代价会相当昂贵,所以要权衡是否值得这么做
速率限制
为什么需要速率限制
通常对系统进行限速,可以避免系统被攻击,如果恶意用户在资源允许的情况下可以频繁访问系统,他们可以做各种事情。比如:使用日志或有效请求打满服务器磁盘,或者DDos攻击。
速率限制可以将系统的性能和稳定性平衡在可控范围内
如何限速
大多数是基于令牌桶算法的,相对容易实现。golang中 golang.org/x/time/rate [github 地址] 包实现了这个功能
治愈异常的goroutine
在一个长期运行的程序中,建立一个机制来监控你的goroutine是否处于健康状态是很有用的,当goroutine异常时,可以尽快的重启。重启的过程成为“治愈”(Healing)。
如何治愈goroutine?
使用心跳模式检查我们正在监控的goroutine是否活跃。我们需要一个管理员来监视一个管理区的goroutine,如果有goroutine变得不健康,管理员负责重启这个管理区的goroutine。