retry重試常見場景及實現
阿新 • • 發佈:2018-11-09
當我們的程式碼是有訪問網路相關的操作時,比如http請求或者訪問遠端資料庫,經常可能會發生一些錯誤,有些錯誤可能重新去傳送請求就會成功,本文分析常見可能需要重試的場景,並最後給出python程式碼實現。
常見異常分成兩種,一種是請求傳輸過程出錯,另一種是服務端負載過高導致錯誤。
對於第一種錯誤,可能請求還未到服務端處理程式就已經返回。
HTTP請求錯誤:
- DNSError:域名不能解析出ip地址,可能是服務端重新部署到其它地方。
- ConnectionError:請求建立握手連線的過程出錯,可能是請求時的網路質量比較差。
訪問資料庫錯誤:
- OperationalError:與資料庫伺服器的連線丟失或連線失敗時。比如訪問PostgreSQL返回碼
1 Class 08 — Connection Exception 2 08000 connection_exception 3 08003 connection_does_not_exist 4 08006 connection_failure 5 08001 sqlclient_unable_to_establish_sqlconnection 6 08004 sqlserver_rejected_establishment_of_sqlconnection
- ProtocolError:屬於Redis中的一種常見錯誤, 當Redis伺服器收到一個位元組序列並轉換為無意義的操作時,會引發異常。由於您在部署之前測試了軟體,因此編寫錯誤的程式碼不太可能發生錯誤。可能是傳輸層出現了錯誤。
對於第二類錯誤,伺服器負載過高導致。對於HTTP請求,可根據狀態碼識別:
- 408 Request Timeout: 當伺服器花費很多時間處理您的請求而不能在等待時間返回。可能的原因:資源被大量傳入請求所淹沒。一段時間後等待和重試可能是最終完成資料處理的好策略。
- 429 Too Many Requests: 在一段時間內傳送的請求數量超過伺服器允許的數量。伺服器使用的這種技術稱為速率限制。良好的服務端應該返回Retry-After標頭,它提供建議在下一個請求之前需要等待多長時間。
- 500 Internal Server Error: 這是最臭名昭著的HTTP伺服器錯誤。錯誤原因多樣性,對於發生的所有未捕獲的異常,都返回這種錯誤。對於這種錯誤,應瞭解背後的原因再決定是否重試。
- 503 Service Unavailable:由於臨時過載,服務當前無法處理請求。經過一段時間的推遲,能得到緩解。
- 504 Gateway Timeout:類似於408請求超時,閘道器或反向代理不能及時從上游伺服器得到響應。
對於資料庫訪問:
- OperationalError. 對於PostgreSQL和MySQL,它還包括不受軟體工程師控制的故障。例如:處理期間發生記憶體分配錯誤,或無法處理事務。我建議重試它們。
- IntegrityError: 當違反外來鍵約束時可以引發它,例如當您嘗試插入依賴於記錄B的記錄A時。由於系統的非同步性質,可能還沒有新增記錄B.在這種情況下,進行重試。另一方面,當您嘗試新增記錄導致重複唯一鍵時,也會引發這種異常,這種情況下不需要重試。那麼如何去識別這種情況,DBMS能返回狀態碼,假如mysql驅動能在狀態碼和異常類之間對映,就能識別這種需要重試的場景,在python3中,庫pymysql可以在資料庫返回碼和異常之間對映。地址如下:
constants for MySQL errors
the mapping between exception types in PyMYSQL and error codes.
本文以網路IO為例,利用python裝飾器實現重試機制。用fetch函式去傳送http請求下載網頁
# Example is taken from http://aiohttp.readthedocs.io/en/stable/#getting-started import aiohttp import asyncio async def fetch(session, url): async with session.get(url) as response: return await response.text() # Client code, provided for reference async def main(): async with aiohttp.ClientSession() as session: html = await fetch(session, 'http://python.org') print(html) loop = asyncio.get_event_loop() loop.run_until_complete(main())
fetch函式並不是可靠的服務,可能存在失敗的情況,這時候根據上文所列的情況實現重試機制,程式碼如下:
import aiohttp @retry(aiohttp.DisconnectedError, aiohttp.ClientError, aiohttp.HttpProcessingError) async def fetch(session, url): async with session.get(url) as response: return await response.text()
retry實現如下,利用裝飾器模式
import logging from functools import wraps log = logging.getLogger(__name__) def retry(*exceptions, retries=3, cooldown=1, verbose=True): """Decorate an async function to execute it a few times before giving up. Hopes that problem is resolved by another side shortly. Args: exceptions (Tuple[Exception]) : The exceptions expected during function execution retries (int): Number of retries of function execution. cooldown (int): Seconds to wait before retry. verbose (bool): Specifies if we should log about not successful attempts. """ def wrap(func): @wraps(func) async def inner(*args, **kwargs): retries_count = 0 while True: try: result = await func(*args, **kwargs) except exceptions as err: retries_count += 1 message = "Exception during {} execution. " \ "{} of {} retries attempted". format(func, retries_count, retries) if retries_count > retries: verbose and log.exception(message) raise RetryExhaustedError( func.__qualname__, args, kwargs) from err else: verbose and log.warning(message) if cooldown: await asyncio.sleep(cooldown) else: return result return inner return wrap
基本思想是在達到重試次數限制之前捕獲預期的異常。在每次執行之間,等待固定時間。此外,如果我們想要詳細,會寫每個失敗嘗試的日誌。當然,本例子只提供了幾個重試選項,一個完備的重試庫應該提供更多重試配置,比如指數退避時間、根據返回結果重試等,這裡推薦幾個第三方庫:
本文翻譯自
Never Give Up, Retry: How Software Should Deal with Failures
下一篇博文將通過分析retrying原始碼來深入分析重試機制的實現原理。