1. 程式人生 > >retry重試常見場景及實現

retry重試常見場景及實現

  當我們的程式碼是有訪問網路相關的操作時,比如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原始碼來深入分析重試機制的實現原理。