2025年12月10日 星期三

在樹莓派 P3 A+ 虛擬環境下安裝 Streamlit 的空間不足問題

我的樹莓派 Pi 3A+ 主機由於記憶體僅 512MB, 最近將作業系統升級為 Trixie Lite 後因為 Selenium 與 Chronium 都會吃較多記憶體, 使得執行 Selenium 爬蟲程式時常會用光記憶體而導致爬取失敗, 看來只能用在執行靜態網頁爬蟲程式了. 但這樣一來又覺得沒有充分利用到這台主機, 想說 Lite 版適合當小型伺服器, 那就拿來跑 Streamlit 網站應用程式吧! 

首先建立一個虛擬環境 stremlit_venv, 然後在裡面安裝 Streamlit, 但是執行到一半, 到下載相依的 pyarrow 套件時就出現 OSError 28 錯誤而失敗 : 

pi@pi3aplus:~ $ python -m venv streamlit_venv     
pi@pi3aplus:~ $ source ~/streamlit_venv/bin/activate    
(streamlit_venv) pi@pi3aplus:~ $ pip install streamlit   
Collecting streamlit
  Downloading streamlit-1.52.1-py3-none-any.whl.metadata (9.8 kB)
Collecting altair!=5.4.0,!=5.4.1,<7,>=4.0 (from streamlit)
  Downloading altair-6.0.0-py3-none-any.whl.metadata (11 kB)
Collecting blinker<2,>=1.5.0 (from streamlit)
  Downloading blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
Collecting cachetools<7,>=4.0 (from streamlit)
... (略) ...
Downloading jsonschema_specifications-2025.9.1-py3-none-any.whl (18 kB)
Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (24 kB)
Downloading narwhals-2.13.0-py3-none-any.whl (426 kB)
Downloading packaging-25.0-py3-none-any.whl (66 kB)
Using cached pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl (45.0 MB)
ERROR: Could not install packages due to an OSError: [Errno 28] 裝置上已無多餘空間

先用 df -h 檢查 SD 卡空間, 還有超大的 18GB 空間 : 

(streamlit_venv) pi@pi3aplus:~ $ df -h   
檔案系統        容量  已用  可用 已用% 掛載點
udev             74M     0   74M    0% /dev
tmpfs            84M  9.0M   75M   11% /run
/dev/mmcblk0p2   29G   11G   18G   38% /
tmpfs           512M     0  512M    0% /dev/shm
tmpfs           5.0M   12K  5.0M    1% /run/lock
tmpfs           1.0M     0  1.0M    0% /run/credentials/systemd-journald.service
tmpfs           209M  138M   71M   67% /tmp
/dev/mmcblk0p1  510M   66M  445M   13% /boot/firmware
tmpfs           1.0M     0  1.0M    0% /run/credentials/getty@tty1.service
tmpfs            42M   16K   42M    1% /run/user/1000

再用 df -i 檢查 inode 使用情形, 發現 inode 只用了 14%, 顯然不是 inode 不足 :

(streamlit_venv) pi@pi3aplus:~ $ df -i   
檔案系統        Inodes  I已用   I可用 I已用% 掛載點
udev             18807    467   18340     3% /dev
tmpfs            53266    827   52439     2% /run
/dev/mmcblk0p2 1890720 259286 1631434    14% /
tmpfs            53266      1   53265     1% /dev/shm
tmpfs            53266      5   53261     1% /run/lock
tmpfs             1024      1    1023     1% /run/credentials/systemd-journald.service
tmpfs          1048576   1673 1046903     1% /tmp
/dev/mmcblk0p1       0      0       0      - /boot/firmware
tmpfs             1024      1    1023     1% /run/credentials/getty@tty1.service
tmpfs            10653     41   10612     1% /run/user/1000

從 SD 卡還有 18GB, inode 使用率僅 14% 可知問題不是 inode 或 SD 卡容量不足, 而是記憶體的 /tmp 不足所致. 通常 /tmp 預設只切 DRAM (512MB) 的一半約 200MB, 但 Streamlit 的最大依賴套件 pyarrow-22.0.0 檔案高達 45.0 MB, 實際需求空間是檔案大小 2~3 倍, 安裝 意即安裝其 wheel 約需要 120–150 MB 的 /tmp 空間, 但目前 /tmp 只剩 71 MB 所以直接觸發 Errno 28. 

Gemini 的建議是在檔案系統中建立一個資料夾做為儲存 pip 下載安裝所需檔案過程中要用到的暫存空間, 然後將環境變數 TMPDIR 指向此資料夾以取代預設的 /tmp 記憶體, 且在用 pip install 安裝時加上 --no-cache-dir 參數, 安裝完即丟棄檔案 :

(streamlit_venv) pi@pi3aplus:~ $ mkdir ~/pip_tmp   
(streamlit_venv) pi@pi3aplus:~ $ TMPDIR=~/pip_tmp    
(streamlit_venv) pi@pi3aplus:~ $ pip install streamlit --no-cache-dir     
Collecting streamlit
  Downloading streamlit-1.52.1-py3-none-any.whl.metadata (9.8 kB)
Collecting altair!=5.4.0,!=5.4.1,<7,>=4.0 (from streamlit)
  Downloading altair-6.0.0-py3-none-any.whl.metadata (11 kB)
... () ...
Downloading narwhals-2.13.0-py3-none-any.whl (426 kB)
Downloading packaging-25.0-py3-none-any.whl (66 kB)
Downloading pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl (45.0 MB)
   ━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━ 20.2/45.0 MB 9.0 MB/s eta 0:00:03ERROR: Could not install packages due to an OSError: [Errno 28] 裝置上已無多餘空間

   ━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━ 20.2/45.0 MB 9.0 MB/s eta 0:00:03

雖然設定了 TMPDIR, 但 pip 在下載檔案時仍然會先佔用 DRAM 的 /tmp 而失敗, 我轉而詢問 ChatGPT, 它建議除了 TMPDIR 外, 還有 TMP 與 TEMP 這兩個環境變數也要設定為指向下載暫存資料夾 (此處改用 disk_tmp, 上面那個 pip_tmp 可以刪除) : 

(streamlit_venv) pi@pi3aplus:~ $ mkdir -p ~/disk_tmp  
(streamlit_venv) pi@pi3aplus:~ $ TMPDIR=~/disk_tmp 
(streamlit_venv) pi@pi3aplus:~ $ TMP=~/disk_tmp 
(streamlit_venv) pi@pi3aplus:~ $ TEMP=~/disk_tmp 
(streamlit_venv) pi@pi3aplus:~ $ pip install streamlit --no-cache-dir 
Collecting streamlit
  Downloading streamlit-1.52.1-py3-none-any.whl.metadata (9.8 kB)
Collecting altair!=5.4.0,!=5.4.1,<7,>=4.0 (from streamlit)
  Downloading altair-6.0.0-py3-none-any.whl.metadata (11 kB)
Collecting blinker<2,>=1.5.0 (from streamlit)
  Downloading blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
Collecting cachetools<7,>=4.0 (from streamlit)
  Downloading cachetools-6.2.2-py3-none-any.whl.metadata (5.6 kB)
Collecting click<9,>=7.0 (from streamlit)
  Downloading click-8.3.1-py3-none-any.whl.metadata (2.6 kB)
Collecting numpy<3,>=1.23 (from streamlit)
  Downloading numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl.metadata (62 kB)
Collecting packaging>=20 (from streamlit)
  Downloading packaging-25.0-py3-none-any.whl.metadata (3.3 kB)
Collecting pandas<3,>=1.4.0 (from streamlit)
  Downloading pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl.metadata (91 kB)
Collecting pillow<13,>=7.1.0 (from streamlit)
  Downloading pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl.metadata (8.8 kB)
Collecting protobuf<7,>=3.20 (from streamlit)
  Downloading protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl.metadata (593 bytes)
Collecting pyarrow>=7.0 (from streamlit)
  Downloading pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl.metadata (3.2 kB)
Collecting requests<3,>=2.27 (from streamlit)
  Downloading requests-2.32.5-py3-none-any.whl.metadata (4.9 kB)
Collecting tenacity<10,>=8.1.0 (from streamlit)
  Downloading tenacity-9.1.2-py3-none-any.whl.metadata (1.2 kB)
Collecting toml<2,>=0.10.1 (from streamlit)
  Downloading toml-0.10.2-py2.py3-none-any.whl.metadata (7.1 kB)
Collecting typing-extensions<5,>=4.4.0 (from streamlit)
  Downloading typing_extensions-4.15.0-py3-none-any.whl.metadata (3.3 kB)
Collecting watchdog<7,>=2.1.5 (from streamlit)
  Downloading watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl.metadata (44 kB)
Collecting gitpython!=3.1.19,<4,>=3.0.7 (from streamlit)
  Downloading gitpython-3.1.45-py3-none-any.whl.metadata (13 kB)
Collecting pydeck<1,>=0.8.0b4 (from streamlit)
  Downloading pydeck-0.9.1-py2.py3-none-any.whl.metadata (4.1 kB)
Collecting tornado!=6.5.0,<7,>=6.0.3 (from streamlit)
  Downloading tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (2.8 kB)
Collecting jinja2 (from altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
Collecting jsonschema>=3.0 (from altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading jsonschema-4.25.1-py3-none-any.whl.metadata (7.6 kB)
Collecting narwhals>=1.27.1 (from altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading narwhals-2.13.0-py3-none-any.whl.metadata (12 kB)
Collecting gitdb<5,>=4.0.1 (from gitpython!=3.1.19,<4,>=3.0.7->streamlit)
  Downloading gitdb-4.0.12-py3-none-any.whl.metadata (1.2 kB)
Collecting smmap<6,>=3.0.1 (from gitdb<5,>=4.0.1->gitpython!=3.1.19,<4,>=3.0.7->streamlit)
  Downloading smmap-5.0.2-py3-none-any.whl.metadata (4.3 kB)
Collecting python-dateutil>=2.8.2 (from pandas<3,>=1.4.0->streamlit)
  Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl.metadata (8.4 kB)
Collecting pytz>=2020.1 (from pandas<3,>=1.4.0->streamlit)
  Downloading pytz-2025.2-py2.py3-none-any.whl.metadata (22 kB)
Collecting tzdata>=2022.7 (from pandas<3,>=1.4.0->streamlit)
  Downloading tzdata-2025.2-py2.py3-none-any.whl.metadata (1.4 kB)
Collecting charset_normalizer<4,>=2 (from requests<3,>=2.27->streamlit)
  Downloading charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.metadata (37 kB)
Collecting idna<4,>=2.5 (from requests<3,>=2.27->streamlit)
  Downloading idna-3.11-py3-none-any.whl.metadata (8.4 kB)
Collecting urllib3<3,>=1.21.1 (from requests<3,>=2.27->streamlit)
  Downloading urllib3-2.6.1-py3-none-any.whl.metadata (6.6 kB)
Collecting certifi>=2017.4.17 (from requests<3,>=2.27->streamlit)
  Downloading certifi-2025.11.12-py3-none-any.whl.metadata (2.5 kB)
Collecting MarkupSafe>=2.0 (from jinja2->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.metadata (2.7 kB)
Collecting attrs>=22.2.0 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading attrs-25.4.0-py3-none-any.whl.metadata (10 kB)
Collecting jsonschema-specifications>=2023.03.6 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading jsonschema_specifications-2025.9.1-py3-none-any.whl.metadata (2.9 kB)
Collecting referencing>=0.28.4 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading referencing-0.37.0-py3-none-any.whl.metadata (2.8 kB)
Collecting rpds-py>=0.7.1 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (4.1 kB)
Collecting six>=1.5 (from python-dateutil>=2.8.2->pandas<3,>=1.4.0->streamlit)
  Downloading six-1.17.0-py2.py3-none-any.whl.metadata (1.7 kB)
Downloading streamlit-1.52.1-py3-none-any.whl (9.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.0/9.0 MB 9.4 MB/s eta 0:00:00
Downloading altair-6.0.0-py3-none-any.whl (795 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 795.4/795.4 kB 11.4 MB/s eta 0:00:00
Downloading blinker-1.9.0-py3-none-any.whl (8.5 kB)
Downloading cachetools-6.2.2-py3-none-any.whl (11 kB)
Downloading click-8.3.1-py3-none-any.whl (108 kB)
Downloading gitpython-3.1.45-py3-none-any.whl (208 kB)
Downloading gitdb-4.0.12-py3-none-any.whl (62 kB)
Downloading numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl (14.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14.2/14.2 MB 9.6 MB/s eta 0:00:00
Downloading pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl (11.7 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11.7/11.7 MB 7.2 MB/s eta 0:00:00
Downloading pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl (6.3 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 10.1 MB/s eta 0:00:00
Downloading protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl (324 kB)
Downloading pydeck-0.9.1-py2.py3-none-any.whl (6.9 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 10.1 MB/s eta 0:00:00
Downloading requests-2.32.5-py3-none-any.whl (64 kB)
Downloading charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (147 kB)
Downloading idna-3.11-py3-none-any.whl (71 kB)
Downloading smmap-5.0.2-py3-none-any.whl (24 kB)
Downloading tenacity-9.1.2-py3-none-any.whl (28 kB)
Downloading toml-0.10.2-py2.py3-none-any.whl (16 kB)
Downloading tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (444 kB)
Downloading typing_extensions-4.15.0-py3-none-any.whl (44 kB)
Downloading urllib3-2.6.1-py3-none-any.whl (131 kB)
Downloading watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl (79 kB)
Downloading certifi-2025.11.12-py3-none-any.whl (159 kB)
Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
Downloading jsonschema-4.25.1-py3-none-any.whl (90 kB)
Downloading attrs-25.4.0-py3-none-any.whl (67 kB)
Downloading jsonschema_specifications-2025.9.1-py3-none-any.whl (18 kB)
Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (24 kB)
Downloading narwhals-2.13.0-py3-none-any.whl (426 kB)
Downloading packaging-25.0-py3-none-any.whl (66 kB)
Downloading pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl (45.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.0/45.0 MB 9.5 MB/s eta 0:00:00
Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl (229 kB)
Downloading pytz-2025.2-py2.py3-none-any.whl (509 kB)
Downloading referencing-0.37.0-py3-none-any.whl (26 kB)
Downloading rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (389 kB)
Downloading six-1.17.0-py2.py3-none-any.whl (11 kB)
Downloading tzdata-2025.2-py2.py3-none-any.whl (347 kB)
Installing collected packages: pytz, watchdog, urllib3, tzdata, typing-extensions, tornado, toml, tenacity, smmap, six, rpds-py, pyarrow, protobuf, pillow, packaging, numpy, narwhals, MarkupSafe, idna, click, charset_normalizer, certifi, cachetools, blinker, attrs, requests, referencing, python-dateutil, jinja2, gitdb, pydeck, pandas, jsonschema-specifications, gitpython, jsonschema, altair, streamlit
Successfully installed MarkupSafe-3.0.3 altair-6.0.0 attrs-25.4.0 blinker-1.9.0 cachetools-6.2.2 certifi-2025.11.12 charset_normalizer-3.4.4 click-8.3.1 gitdb-4.0.12 gitpython-3.1.45 idna-3.11 jinja2-3.1.6 jsonschema-4.25.1 jsonschema-specifications-2025.9.1 narwhals-2.13.0 numpy-2.3.5 packaging-25.0 pandas-2.3.3 pillow-12.0.0 protobuf-6.33.2 pyarrow-22.0.0 pydeck-0.9.1 python-dateutil-2.9.0.post0 pytz-2025.2 referencing-0.37.0 requests-2.32.5 rpds-py-0.30.0 six-1.17.0 smmap-5.0.2 streamlit-1.52.1 tenacity-9.1.2 toml-0.10.2 tornado-6.5.2 typing-extensions-4.15.0 tzdata-2025.2 urllib3-2.6.1 watchdog-6.0.0
(streamlit_venv) pi@pi3aplus:~ $

哈哈! 果然成功了! 

原來使用 pip install 套件時並非只有 pip 在工作, 雖然 pip 會遵守規範將下載之檔案暫存在 TMPDIR, 但第三方依賴套件例如 pyarrow 或 C++ 擴展程式可能會用到編譯器或建構腳本, 但依賴套件的編譯器或腳本不一定會遵循, 它們可能使用 TMP 或 TEMP 環境變數, 當這些依賴程式找不到 TMP 或 TEMP 就會 fallback 到 DRAM 的 /tmp, 結果就可能因空間不足而失敗, 所以最妥當的方法就是將 TMP 與 TEMP 與 TMPDIR 一樣指向暫存資料夾, 這樣就幾乎把大部分跑回 /tmp 的可能性都堵死. 這個設定其實就是欺騙 pip, 讓它誤以為擁有 18GB 的可用暫存空間. 

測試是否可載入 streamlit : 

(streamlit_venv) pi@pi3aplus:~ $ python  
Python 3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import streamlit   
>>> streamlit.__version__   
'1.52.1'

但上面的設定只是暫時的, 當終端機關閉後就消失了. 對於 Pi 3A+ 這種只有 512MB 記憶體的主機而言, 以後每次安裝大一點的套件還是會遇到同樣問題. 如果要徹底解決, 就必須修改 Shell 啟動檔案 ~/.bashrc, 用 export 指令設定這三個環境變數指向暫存資料夾 ~/disk_tmp :

用 nano 開啟Shell 啟動檔 ~/.bashrc : 

pi@pi3aplus:~ $ nano ~/.bashrc    

把下列三個 export 指令寫在檔尾 (較安全) : 

export TMPDIR=~/disk_tmp
export TMP=~/disk_tmp
export TEMP=~/disk_tmp

按 Ctrl+O 存檔後按 Ctrl+X 退出 nano, 用 source 指令叫 Shell 重新讀取啟動檔 :

pi@pi3aplus:~ $ source ~/.bashrc   

用 echo 指令驗證此三個環境變數是否有設定進去 : 

pi@pi3aplus:~ $ echo $TMPDIR  
/home/pi/disk_tmp
pi@pi3aplus:~ $ echo $TMP  
/home/pi/disk_tmp
pi@pi3aplus:~ $ echo $TEMP  
/home/pi/disk_tmp

這樣以後每次開啟終端機進虛擬環境用 pip 安裝套件時就不會再遇到 OSError: [Errno 28] 暫存區記憶體空間不足的錯誤了. 

沒有留言:

張貼留言