Data Engineering/Docker

[Ubuntu/Docker] Ubuntu CLI에서 pip을 설치해보자 (의존성 설치 타임 아웃 에러 해결 방법)

seoraroong 2024. 9. 4. 23:31

 

MongoDB와 Airflow 컨테이너를 빌드하는 과정에서 위와 같은 에러가 발생했다.

에러 내용은 pip 명령어가 패키지를 다운로드 하는 동안 타임 아웃이 발생했다는 것이었다..

이 에러는 인터넷 연결이 불안정하거나, 특정 네트워크 조건에서 패키지 다운로드 시간이 오래 걸릴 때 발생할 수 있다고 한다.

 

Airflow에 관련된 Dockerfile에 requirements.txt 파일로부터 의존성을 설치하도록 하는 명령어가 있었다.

여기서 문제가 발생한 것 같다.

 

그래서 컨테이너를 빌드하기 전에 미리 의존성을 로컬 환경에서 다운로드 후, 빌드 시 해당 파일을 사용하는 방법을 사용해보기로 했다.

 

pip download -r requirements.txt -d ./packages

 

Ubuntu에 pip을 설치한 적이 없기 때문에 pip을 설치하라는 메시지가 나왔다.

 

pip 설치하기

sudo apt update

 

sudo 권한을 사용하기 위해 비밀번호를 입력하면 update가 시작된다.

 

 

sudo apt install python3-pip

 

pip을 설치하는 명령어이다.

 

위 명령어를 실행했더니 다음과 같은 에러가 발생했다.

 

 

dpkg가 중단된 상태라는 의미로, 아래 명령을 실행해 dpkg 구성을 완료해준다.

 

sudo dpkg --configure -a

 

다시 pip을 설치해준다음, 성공적으로 설치되었는지 확인해준다.

 

pip3 --version

 

 

잘 설치된 것을 확인할 수 있다.

 

다시 의존성을 로컬에 다운로드 해보자.

 

의존성 설치 중 타임 아웃 에러가 발생했다.

Collecting gensim==4.3.2
  Downloading gensim-4.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (26.5 MB)
     ━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 7.7/26.5 MB 11.1 MB/s eta 0:00:02
ERROR: Exception:
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/pip/_vendor/urllib3/response.py", line 438, in _error_catcher
    yield
  File "/usr/lib/python3/dist-packages/pip/_vendor/urllib3/response.py", line 519, in read
    data = self._fp.read(amt) if not fp_closed else b""
  File "/usr/lib/python3/dist-packages/pip/_vendor/cachecontrol/filewrapper.py", line 90, in read
    data = self.__fp.read(amt)
  File "/usr/lib/python3.10/http/client.py", line 466, in read
    s = self.fp.read(amt)
  File "/usr/lib/python3.10/socket.py", line 705, in readinto
    return self._sock.recv_into(b)
  File "/usr/lib/python3.10/ssl.py", line 1303, in recv_into
    return self.read(nbytes, buffer)
  File "/usr/lib/python3.10/ssl.py", line 1159, in read
    return self._sslobj.read(len, buffer)
TimeoutError: The read operation timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/pip/_internal/cli/base_command.py", line 165, in exc_logging_wrapper
    status = run_func(*args)
  File "/usr/lib/python3/dist-packages/pip/_internal/cli/req_command.py", line 205, in wrapper
    return func(self, options, args)
  File "/usr/lib/python3/dist-packages/pip/_internal/commands/download.py", line 129, in run
    requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
  File "/usr/lib/python3/dist-packages/pip/_internal/resolution/resolvelib/resolver.py", line 94, in resolve
    result = self._result = resolver.resolve(
  File "/usr/lib/python3/dist-packages/pip/_vendor/resolvelib/resolvers.py", line 481, in resolve
    state = resolution.resolve(requirements, max_rounds=max_rounds)
  File "/usr/lib/python3/dist-packages/pip/_vendor/resolvelib/resolvers.py", line 348, in resolve
    self._add_to_criteria(self.state.criteria, r, parent=None)
  File "/usr/lib/python3/dist-packages/pip/_vendor/resolvelib/resolvers.py", line 172, in _add_to_criteria
    if not criterion.candidates:
  File "/usr/lib/python3/dist-packages/pip/_vendor/resolvelib/structs.py", line 151, in __bool__
    return bool(self._sequence)
  File "/usr/lib/python3/dist-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 155, in __bool__
    return any(self)
  File "/usr/lib/python3/dist-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 143, in <genexpr>
    return (c for c in iterator if id(c) not in self._incompatible_ids)
  File "/usr/lib/python3/dist-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 47, in _iter_built
    candidate = func()
  File "/usr/lib/python3/dist-packages/pip/_internal/resolution/resolvelib/factory.py", line 215, in _make_candidate_from_link
    self._link_candidate_cache[link] = LinkCandidate(
  File "/usr/lib/python3/dist-packages/pip/_internal/resolution/resolvelib/candidates.py", line 288, in __init__
    super().__init__(
  File "/usr/lib/python3/dist-packages/pip/_internal/resolution/resolvelib/candidates.py", line 158, in __init__
    self.dist = self._prepare()
  File "/usr/lib/python3/dist-packages/pip/_internal/resolution/resolvelib/candidates.py", line 227, in _prepare
    dist = self._prepare_distribution()
  File "/usr/lib/python3/dist-packages/pip/_internal/resolution/resolvelib/candidates.py", line 299, in _prepare_distribution
    return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
  File "/usr/lib/python3/dist-packages/pip/_internal/operations/prepare.py", line 487, in prepare_linked_requirement
    return self._prepare_linked_requirement(req, parallel_builds)
  File "/usr/lib/python3/dist-packages/pip/_internal/operations/prepare.py", line 532, in _prepare_linked_requirement
    local_file = unpack_url(
  File "/usr/lib/python3/dist-packages/pip/_internal/operations/prepare.py", line 214, in unpack_url
    file = get_http_url(
  File "/usr/lib/python3/dist-packages/pip/_internal/operations/prepare.py", line 94, in get_http_url
    from_path, content_type = download(link, temp_dir.path)
  File "/usr/lib/python3/dist-packages/pip/_internal/network/download.py", line 146, in __call__
    for chunk in chunks:
  File "/usr/lib/python3/dist-packages/pip/_internal/cli/progress_bars.py", line 304, in _rich_progress_bar
    for chunk in iterable:
  File "/usr/lib/python3/dist-packages/pip/_internal/network/utils.py", line 63, in response_chunks
    for chunk in response.raw.stream(
  File "/usr/lib/python3/dist-packages/pip/_vendor/urllib3/response.py", line 576, in stream
    data = self.read(amt=amt, decode_content=decode_content)
  File "/usr/lib/python3/dist-packages/pip/_vendor/urllib3/response.py", line 512, in read
    with self._error_catcher():
  File "/usr/lib/python3.10/contextlib.py", line 153, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/usr/lib/python3/dist-packages/pip/_vendor/urllib3/response.py", line 443, in _error_catcher
    raise ReadTimeoutError(self._pool, None, "Read timed out.")
pip._vendor.urllib3.exceptions.ReadTimeoutError: HTTPSConnectionPool(host='files.pythonhosted.org', port=443): Read timed out.

 

스왑 메모리 설정하기

- 패키지 다운로드 중 시스템 리소스, 특히 메모리 부족으로 인해 프로세스가 계속 Killed 되었다.

- 리소스 부족 문제를 해결하기 위해 스왑 메모리를 추가로 설정할 수 있다.

 

현재 스왑 메모리 확인하기

sudo swapon --show

 

기존에 설정된 스왑 파일이 있으면 비활성화 하기

sudo swapoff -a

 

새 스왑 파일 생성하기

-> 4GB의 스왑 파일을 생성했다. (처음에 2GB로 설정했더니 gensim 받는 도중 프로세스가 멈춰버렸다.)

sudo fallocate -l 4G /swapfile

 

스왑 파일 권한 설정하기

sudo chmod 600 /swapfile

 

스왑 파일 형식 설정하기

-> 스왑 파일을 스왑 공간으로 사용하도록 설정한다.

sudo mkswap /swapfile

 

스왑 파일 활성화하기

sudo swapon /swapfile

 

새로 설정한 스왑 파일 확인하기

sudo swapon --show

 

재부팅 후에도 스왑 파일이 유지되도록 설정하기

sudo nano /etc/fstab

파일이 열리면 마지막 줄에 다음 문장 추가하기

-> /swapfile none swap sw 0 0 

 

저장 후 종료하면 끝

 

 

 

의존성 파일 로컬에 다운로드하기

pip download -r requirements.txt -d ./packages

-> 파일 경로는 정확히 지정해주어야 한다. 

 

드디어 정상적으로 packages 파일에 의존성을 모두 다운로드 받았다.!!

 

Dockerfile 수정하기

로컬에 requirements.txt 파일에 있는 패키지들을 다운받아놨으므로, 기존의 Dockerfile에 작성한 requirements.txt 파일을 다운로드 받는 부분을 수정해주어야 한다.

 

- 기존 Dockerfile

# DockerfileAir 예시
FROM python:3.11-slim-buster

# 작업 디렉토리 설정
WORKDIR /opt/airflow

# 시스템 패키지 및 필수 툴 설치
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    && apt-get clean

# Airflow 설치
RUN pip install --no-cache-dir apache-airflow==2.5.0

# Django와 기타 필요한 패키지 설치
COPY ./Encore_Final_Project/requirements.txt /opt/airflow/requirements.txt
RUN pip install --no-cache-dir -r /opt/airflow/requirements.txt

# Django 프로젝트를 컨테이너에 복사
COPY ./Encore_Final_Project /opt/airflow/Encore_Final_Project

# 필요 시 추가 설정 (예: 환경 변수 설정)
ENV PYTHONPATH="/opt/airflow/Encore_Final_Project:$PYTHONPATH"

# Airflow와 Django를 위한 추가 설정
ENV AIRFLOW__CORE__LOAD_EXAMPLES=False
ENV AIRFLOW__CORE__EXECUTOR=LocalExecutor
ENV AIRFLOW__CORE__SQL_ALCHEMY_CONN=postgresql+psycopg2://airflow:airflow@postgres/airflow

# 기본적으로 Airflow 웹서버를 시작합니다
CMD ["airflow", "db", "init"] && ["airflow", "webserver"]

 

- 수정한 Dockerfile

# DockerfileAir 예시
FROM python:3.11-slim-buster

# 작업 디렉토리 설정
WORKDIR /opt/airflow

# 시스템 패키지 및 필수 툴 설치
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    && apt-get clean

# Airflow 설치
RUN pip install --no-cache-dir apache-airflow==2.5.0

# Django와 기타 필요한 패키지 설치
COPY ./Encore_Final_Project/requirements.txt /opt/airflow/requirements.txt

# 로컬에 있는 패키지 설치 (로컬에서 다운로드한 패키지 사용)
RUN pip install --no-index --find-links=/opt/airflow/packages -r /opt/airflow/requirements.txt

# Django 프로젝트를 컨테이너에 복사
COPY ./Encore_Final_Project /opt/airflow/Encore_Final_Project

# 필요 시 추가 설정 (예: 환경 변수 설정)
ENV PYTHONPATH="/opt/airflow/Encore_Final_Project:$PYTHONPATH"

# Airflow와 Django를 위한 추가 설정
ENV AIRFLOW__CORE__LOAD_EXAMPLES=False
ENV AIRFLOW__CORE__EXECUTOR=LocalExecutor
ENV AIRFLOW__CORE__SQL_ALCHEMY_CONN=postgresql+psycopg2://airflow:airflow@postgres/airflow

# 기본적으로 Airflow 웹서버를 시작합니다
CMD bash -c "airflow db init && airflow webserver"

 

- docker-compose.yml

version: '3.9'

services:
  mongo1:
    image: mongo
    volumes:
      - db1:/data/db
    ports:
      - 27017:27017
    networks:
      - mynetwork
    healthcheck:
      test: test $$(mongosh --port 27017 --quiet --eval "db.adminCommand('ping').ok") -eq 1
      interval: 20s
      timeout: 10s
      retries: 6

  postgres:
    image: postgres:13
    environment:
      POSTGRES_USER: airflow
      POSTGRES_PASSWORD: airflow
      POSTGRES_DB: airflow
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - mynetwork
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U airflow"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:latest
    networks:
      - mynetwork

  airflow-webserver:
    build:
      context: .
      dockerfile: DockerfileAir
    depends_on:
      mongo1:
        condition: service_healthy
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      AIRFLOW__CORE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow
      AIRFLOW__CORE__EXECUTOR: LocalExecutor
      AIRFLOW__CORE__LOAD_EXAMPLES: "False"
      AIRFLOW__WEBSERVER__RBAC: "True"
      PYTHONPATH: /opt/airflow/Encore_Final_Project # Django 프로젝트의 PYTHONPATH 설정
    volumes:
      - ./dags:/opt/airflow/dags
      - ./logs:/opt/airflow/logs
      - ./plugins:/opt/airflow/plugins
      - /mnt/c/MyDocker/Encore_Final_Project:/opt/airflow/Encore_Final_Project
      - ./packages:/opt/airflow/packages  # 다운로드한 패키지 경로를 추가

    ports:
      - "8080:8080"
    command: ["airflow", "webserver"]
    restart: always
    networks:
      - mynetwork
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5

  airflow-scheduler:
    build:
      context: .
      dockerfile: DockerfileAir
    depends_on:
      airflow-webserver:
        condition: service_healthy
    environment:
      AIRFLOW__CORE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow
      PYTHONPATH: /opt/airflow/Encore_Final_Project
    volumes:
      - ./dags:/opt/airflow/dags
      - ./logs:/opt/airflow/logs
      - ./plugins:/opt/airflow/plugins
      - /mnt/c/MyDocker/Encore_Final_Project:/opt/airflow/Encore_Final_Project
      - ./packages:/opt/airflow/packages  # 다운로드한 패키지 경로를 추가

    command: scheduler
    restart: always
    networks:
      - mynetwork

  airflow-init:
    build:
      context: .
      dockerfile: DockerfileAir
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
      mongo1:
        condition: service_healthy
    environment:
      AIRFLOW__CORE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow
      AIRFLOW__CORE__EXECUTOR: LocalExecutor
    volumes:
      - ./dags:/opt/airflow/dags
      - ./logs:/opt/airflow/logs
      - ./plugins:/opt/airflow/plugins
      - /mnt/c/MyDocker/Encore_Final_Project:/opt/airflow/Encore_Final_Project
    command: ["airflow", "db", "init"]
    restart: "no"
    networks:
      - mynetwork

  node-app:
    build:
      context: .
      dockerfile: DockerfileNode
    volumes:
      - ./app:/app
    networks:
      - mynetwork

networks:
  mynetwork:

volumes:
  db1:
  postgres-data:

 

requirements.txt로 설치하는 것 vs. 패키지를 미리 로컬에 다운로드한 후 설치하는 것

- requirements.txt 로 설치

pip이 requirements.txt 파일을 읽어 그 안에 명시된 패키지들을 인터넷(PyPl)에서 다운로드한다.

따라서 패키지를 설치하려면 반드시 인터넷 연결이 필요하며, 서버 환경에서 인터넷 연결이 제한적이거나 느릴경우 패키지 설치가 느려지거나 실패할 수 있다.

 

- 패키지를 로컬에 다운로드 후 설치 (로컬 packages 폴더 사용)

미리 다운로드한 패키지 파일을 로컬의 packages 폴더에 저장하고, 설치할 때는 인터넷 대신 로컬에서 설치한다.

인터넷 연결이 필요하지 않으며, 인터넷 환경이 제한적이거나 불안정한 환경에서 유용하게 사용할 수 있는 방법이다.