背景
之前遇到個問題,發(fā)現(xiàn)一個系統(tǒng)如果拆分了太多業(yè)務(wù)類服務(wù),或者依賴于大量的第三方服務(wù),就很容易因為某個服務(wù)的故障導(dǎo)致整個系統(tǒng)不可用,比如
- 模塊中使用了 Elastic Search 進(jìn)行監(jiān)控,但是 ES 突然掛了,相關(guān)的 api 的調(diào)用報錯導(dǎo)致級聯(lián)的服務(wù)全部阻塞,那么應(yīng)該要有規(guī)避由 ES 調(diào)用 raise 出的異常或者調(diào)用超時而導(dǎo)致整個模塊或整個系統(tǒng)崩潰的保護(hù)措施。
- 使用 AWS 或 阿里云 的 ECS 服務(wù)來作為 micro-service 的載體,但是 ECS 服務(wù)故障或者過載了導(dǎo)致整個業(yè)務(wù)鏈無法正常進(jìn)行,那么應(yīng)有對應(yīng)的降級或者限制調(diào)用頻度的方案來進(jìn)行保護(hù)。
服務(wù)熔斷
服務(wù)熔斷和電路熔斷是一個道理,如果一條線路電壓過高,保險絲會熔斷,防止出現(xiàn)火災(zāi),但是過后重啟仍然是可用的。
而服務(wù)熔斷則是對于目標(biāo)服務(wù)的請求和調(diào)用大量超時或失敗,這時應(yīng)該熔斷該服務(wù)的所有調(diào)用,并且對于后續(xù)調(diào)用應(yīng)直接返回,從而快速釋放資源,確保在目標(biāo)服務(wù)不可用的這段時間內(nèi),所有對它的調(diào)用都是立即返回,不會阻塞的。再等到目標(biāo)服務(wù)好轉(zhuǎn)后進(jìn)行接口恢復(fù)。
熔斷的方式有很多,最出名的奶飛的 hystrix 項目里有很全面的實踐,這里便先列個比較偷懶的案例。
舉個栗子,
# Elastic search service decorator
def api_trend(func):
def wrapper(*args, **kwargs):
# Call elastic search service to get api trend
elastic_search_api_call()
# Custom function
return func(*args, **kwargs)
return wrapper
# Custom task to do stuff
@api_trend
def custom_func(foo):
retrun foo()
假設(shè)代碼中的 @api_trend
是個調(diào)用 Elastic Search 服務(wù)來監(jiān)控 api 執(zhí)行情況的裝飾器,那么如果 Elastic Search 服務(wù)掛了,則后續(xù)的 custom_func(foo)
也不會成功執(zhí)行或者被阻塞。所以我們需要做的就是阻止后續(xù)的程序繼續(xù)調(diào)用 @api_trend
或者 elastic_search_api_call()
這兩位老哥,把 custom_func(foo)
隔離開,這樣雖然暫時失去了監(jiān)控,但是仍能保證業(yè)務(wù)能正常執(zhí)行。
所以基于這點,我們可以簡單地加個熔斷控制器開關(guān)來隔離故障接口。
from threading import Timer
# Melt down flag
FUSE = True
# Melt down recover func
def recover():
FUSE = True
return
# Melt down decorator
def melt_down(threshold=5, inteval=60, timeout=300, recover_time=3600):
def wrap_melt(func):
def wrapper(*args, **kwargs):
is_fuse = True
while threshold > 0 and is_fuse:
try:
func(timeout, *args, **kwargs)
is_fuse = False
exception Exception, e:
is_fuse = True
threshold -= 1
continue
time.sleep(inteval)
FUSE = is_fuse
if not FUSE:
tr = threading.Timer(recover_time, recover)
tr.start()
return FUSE
return wrapper
return wrap_melt
# Elastic search service decorator
def api_trend(func):
def wrapper(*args, **kwargs):
# Call elastic search service to get api trend
if FUSE:
elastic_search_api_call()
# Custom function
return func(*args, **kwargs)
return wrapper
# Custom task to do stuff
@melt_down
@api_trend
def custom_func(foo):
return foo()
通過在調(diào)用 @api_trend
之前加上熔斷控制器,進(jìn)行目標(biāo)服務(wù)的接口調(diào)用,如果在規(guī)定的重試次數(shù)內(nèi)均未成功,則認(rèn)為該服務(wù)在這一段時間內(nèi)不可用,對于該 api 的所有調(diào)用全都用一個 FUSE_FLAG 進(jìn)行隔離,并且設(shè)置一個定時 Thread, 在一定時間后重新打開 FUSE_FLAG,恢復(fù)目標(biāo)服務(wù)的調(diào)用。
服務(wù)降級
當(dāng)服務(wù)器壓力劇增的情況下,根據(jù)當(dāng)前業(yè)務(wù)情況及流量對一些服務(wù)和頁面有策略的降級,以此釋放服務(wù)器資源以保證核心任務(wù)的正常運行。
對于復(fù)雜系統(tǒng)而言,會有很多的微服務(wù)通過 rpc 調(diào)用,從而產(chǎn)生一個業(yè)務(wù)需要一條很長的調(diào)用鏈,其中任何一環(huán)故障了都會導(dǎo)致整個調(diào)用鏈?zhǔn)』虺瑫r而導(dǎo)致業(yè)務(wù)服務(wù)不可用或阻塞。
這種情況下,可以暫時去掉調(diào)用鏈中故障的服務(wù)來進(jìn)行降級,其中降級策略又有很多種,比如限流,接口拒絕等,這里就挑個簡單的來舉栗。
比如一個電商系統(tǒng),用戶模塊,商品模塊,訂單模塊,支付模塊,物流模塊分別是5個存在相互依賴性的服務(wù),但是如果用戶要下單購買個商品則可能需要一條長調(diào)用鏈依次 Call 到這5個模塊。
# Call chain
user = UserModule.sender.get_user()
product = ProductModule.sender.get_product(user.selected)
order = OrderModule.sender.post_order(product)
payment = PaymentModule.sender.post_payment(order)
logistics = LogisticsModule.sender.post_logistics(payment)
這時候如果物流模塊崩了,那么很可能在最終購買商品的流程會被回滾,導(dǎo)致用戶購買商品不成功,然而實際上,物流模塊即便失效,仍應(yīng)允許進(jìn)行商品查看,下單,購買等,所以,坦率地講,我們應(yīng)該對這5個模塊進(jìn)行一個上下游依賴的剝離,使之變?yōu)榧儍舻?rpc 調(diào)用。
簡單地說,
from xmlrpclib import ServerProxy
MODULE_TO_ENABLE = [
'UserAgent',
'ProductAgent',
'OrderAgent',
'PaymentAgent',
'LogisticsAgent'
]
def custom_call():
return foo()
def call_nothing():
return
class LogisticsAgent(object):
self.sender = ServerProxy("http://{host}:{port}".format(host=host, port=port))
if self.__class__.__name__ in MODULE_TO_ENABLE:
self.sender.call = custom_call
else:
self.sender.call = call_nothing
pass
# Call chain
if self.current_agent not in MODULE_TO_ENABLE:
pass
這樣通過 diable Call chain 中不重要的一環(huán)來確保其他模塊可以正常使用。