← Home

PowerDNS-Admin 登入時因為 session 過多而失敗

2 December, 2025 - Tags: CSIT, DNS

在系資訊中心,我們內部的服務是由 PowerDNS 提供的,而 DNS TA 們為了管理方便,有另外架設一個 PowerDNS-Admin 提供 UI 好方便大家進行簡易的維護操作。然而,我們偶爾會遇到在登入時等了很久之後失敗的狀況,在之前有發現原因是因為 DB 內儲存了相當大量的 session,只要手動進去 DB 把整個 table 清空就可以解決了。

雖然前面的 workaround 可以解決,但是這看起來並不是一個足夠優雅的解法,而且我們並沒有找到 root cause,因為 session 的量跟 DNS TA 操作的量顯然沒有符合。有一天 wkhsiao 又遇到這個問題,再清完 DB 後跟 phkoan 稍微針對這個問題進行了簡短的討論,雖然說 PowerDNS-Admin 提供了 web UI 可以讓操作比較方便,但實際上直接透過 CLI 對大家來說也不是太大的負擔。當時我剛好也在場,並且這個問題引起了我的好奇,就決定要跟 wkhsiao 一起研究該問題的成因。

一開始是先到 PowerDNS-Admin 的 repo 簡單搜尋了一下是否有相關 issue 看看我們是不是可以協助貢獻,但發現這專案相當的不活躍,最近的一次 commit 是在 2024 年的 12 月,距離撰文當下差不多是一年前。issue 數量也不多,並且沒有人回報我們遇到的這個問題。稍微透過 copilot 詢問跟檢查 source code 後,我們發現它清除 expired session 的條件,是要有人使用 /users/ 底下的任何 API。 [1]

@user_bp.before_request
def before_request():
    ...

    # Clean up expired sessions in the database
    if Setting().get('session_type') == 'sqlalchemy':
        from ..models.sessions import Sessions
        Sessions().clean_up_expired_sessions()

我們本來一直以為是作者忘記開發清除 expired session 的功能,但發現這段後 code 之後,我們推測可能是觸發 user 相關 API 的頻率太低了,所以才會讓 session table 被塞爆。然而手動用 wkhsiao 帳號登入後,我們確定了它至少會透過 GET /users/image 去獲取使用者的頭像。但這時,wkhsiao 突然發現 session 上面的日期看起來很一致地都是以五秒為間隔被建立,明顯非人類使用者的自然操作。

所以我們決定尋找是什麼自動化機制在建立 session,當時推測可能是有什麼腳本在定期登入 PowerDNS-Admin,因此嘗試使用 tcpdump 來檢查流量,可惜當時我們應該是看錯輸出或下錯參數了,沒注意到有定期往該 container 送的 request。本來在懷疑有直接對 DB 的操作在猶豫要不要去聽 mariadb 的封包,後來是 wkhsiao 在 container 的 access log 找到每五秒就有一次 GET / 與 GET /login 的 request。然後才發現是 image 上的 health check HEALTHCHECK --interval=5s --timeout=5s --start-period=20s --retries=5 CMD wget --output-document=- --quiet --tries=1 http://127.0.0.1${SCRIPT_NAME:-/} [2],它會定期去執行 GET /,然後因為 wget 預設會 follow redirect,所以會再產生一筆 GET /login 的紀錄。但直覺上這件事也不該產生 session,因為這兩個 GET 並不會成功登入,但後來我們在 API controller 翻到下面這段 code。 [3]

@index_bp.before_request
def before_request():
    ...

    # Manage session timeout
    session.permanent = True
    current_app.permanent_session_lifetime = datetime.timedelta(
        minutes=int(Setting().get('session_timeout')))
    session.modified = True

我對 Flask-Session 不算熟悉,但推測應該是這邊對 session 的操作導致了它在 DB 內建立對應的 session 紀錄,不確定如果拔掉這幾行 code 是不是就不會建立了。但總之他掛在 index_bp 底下,所以不論是存取 / 跟 /login 都會建立 session。後來我們使用 oha 對測試區的 PowerDNS-Admin 進行測試,也證實了在累積了大約十萬筆 session 時就沒辦法登入了,按照每五秒新增一筆的速度來計算,只要大約一週沒有任何 DNS TA 登入,就會觸發這個 bug。

至此,我們算是真正找出這個問題背後的成因了,由於這不是一個重要的服務,後來我們決定直接把 health check 關掉,以避免產生大量 session。

  1. https://github.com/PowerDNS-Admin/PowerDNS-Admin/blob/6b73b8ff08f5b1a2c473089ca9d495261e4169b5/powerdnsadmin/routes/user.py#L21-L43 ↩

  2. https://github.com/PowerDNS-Admin/PowerDNS-Admin/blob/6b73b8ff08f5b1a2c473089ca9d495261e4169b5/docker/Dockerfile#L95C1-L95C151 ↩

  3. https://github.com/PowerDNS-Admin/PowerDNS-Admin/blob/6b73b8ff08f5b1a2c473089ca9d495261e4169b5/powerdnsadmin/routes/index.py#L63-L80 ↩