InfoGrab DocsInfoGrab Docs

데이터베이스 문제 해결 및 디버깅

요약

이 절에서는 골치 아픈 데이터베이스 문제를 만났을 때 참고해 그대로 복사해 쓸 수 있는 내용을 제공합니다. 먼저 Slack에서 오류를 검색하거나 Google에서 GitLab <my error>로 검색해 봅니다.

이 절에서는 골치 아픈 데이터베이스 문제를 만났을 때 참고해 그대로 복사해 쓸 수 있는 내용을 제공합니다.

먼저 Slack에서 오류를 검색하거나 Google에서 GitLab <my error>로 검색해 봅니다.

사용할 수 있는 RAILS_ENV 값은 다음과 같습니다.

  • production(주 GDK 데이터베이스에는 보통 쓰지 않지만 Omnibus 같은 다른 설치 환경에서는 필요할 수 있습니다).
  • development(주 GDK 데이터베이스입니다).
  • test(RSpec 같은 테스트에 사용합니다).

전체 삭제 후 처음부터 시작#

모든 것을 지우고 빈 데이터베이스(DB)로 다시 시작하려면 다음과 같이 실행합니다(약 1분 소요).

bundle exec rake db:reset RAILS_ENV=development

빈 DB에 샘플 데이터를 넣으려면 다음과 같이 실행합니다(약 4분 소요).

bundle exec rake dev:setup

모든 것을 지우고 샘플 데이터로 다시 시작하려면 다음과 같이 실행합니다(약 4분 소요). 이 명령은 db:reset도 수행하고 DB 별 마이그레이션도 실행합니다.

bundle exec rake db:setup RAILS_ENV=development

테스트 DB에 문제가 있다면 중요한 데이터가 들어 있지 않으므로 전부 삭제해도 안전합니다.

bundle exec rake db:reset RAILS_ENV=test

마이그레이션 다루기#

  • bundle exec rake db:migrate RAILS_ENV=development: MR에서 가져왔을 수 있는 대기 중인 마이그레이션을 실행합니다
  • bundle exec rake db:migrate:status RAILS_ENV=development: 모든 마이그레이션이 up 인지 down 인지 확인합니다
  • bundle exec rake db:migrate:down:main VERSION=20170926203418 RAILS_ENV=development: 마이그레이션을 되돌립니다
  • bundle exec rake db:migrate:up:main VERSION=20170926203418 RAILS_ENV=development: 마이그레이션을 적용합니다
  • bundle exec rake db:migrate:redo:main VERSION=20170926203418 RAILS_ENV=development: 특정 마이그레이션을 다시 실행합니다

위 명령에서 main을 바꾸면 main 대신 ci 데이터베이스를 대상으로 실행할 수 있습니다.

데이터베이스에 직접 접근#

다음 명령 중 하나로 데이터베이스에 접근합니다. 어느 명령을 쓰든 결과는 같습니다.

gdk psql -d gitlabhq_development
bundle exec rails dbconsole -e development
bundle exec rails db -e development
  • \q: 종료합니다
  • \dt: 모든 테이블을 나열합니다
  • \d+ issues: issues 테이블의 칼럼을 나열합니다
  • CREATE TABLE board_labels();: board_labels라는 테이블을 만듭니다
  • SELECT * FROM schema_migrations WHERE version = '20170926203418';: 마이그레이션이 실행되었는지 확인합니다
  • DELETE FROM schema_migrations WHERE version = '20170926203418';: 마이그레이션을 직접 제거합니다

GUI로 데이터베이스 접근#

대부분의 GUI(DataGrip, RubyMine, DBeaver)는 데이터베이스에 대한 TCP 연결을 요구합니다. 기본적으로 GDK PostgreSQL 인스턴스는 UNIX 소켓만 수신합니다. TCP로 노출하려면 다음과 같이 진행합니다.

  1. GDK 루트 디렉터리에서 호스트를 소켓 경로에서 localhost로 바꿉니다.

    gdk config set postgresql.host localhost
    
  2. gdk.yml에 다음 내용이 있는지 확인합니다.

    postgresql:
      host: localhost
    
  3. 서비스 정의를 다시 생성합니다.

    gdk reconfigure
    
  4. 새 실행 인수가 적용되도록 PostgreSQL을 재시작합니다.

    [!warning] PostgreSQL을 재시작하면 Rails, Sidekiq, 열려 있는 gdk psql 세션을 포함해 활성 연결이 모두 끊깁니다. 의존 서비스를 중지하거나 GDK가 유휴 상태일 때 재시작합니다.

    gdk restart postgresql
    
  5. GUI에서 호스트 localhost, 포트 5432, 데이터베이스 gitlabhq_development로 연결합니다. 연결 문자열 postgresql://localhost:5432/gitlabhq_development를 사용해도 됩니다.

이제 새 연결이 동작합니다.

Visual Studio Code로 GDK 데이터베이스 접근#

Visual Studio Code의 PostgreSQL 확장으로 데이터베이스 연결을 만들면 GDK 데이터베이스에 접근해 내용을 살펴볼 수 있습니다.

사전 요구 사항:

데이터베이스 연결을 만들려면 다음과 같이 진행합니다.

  1. 활동 표시줄에서 PostgreSQL Explorer 아이콘을 선택합니다.

  2. 열린 창에서 **+**를 선택해 새 데이터베이스 연결을 추가합니다.

  3. 데이터베이스의 hostname을 입력합니다. GDK 디렉터리에 있는 PostgreSQL 폴더 경로를 사용합니다.

    • 예: /dev/gitlab-development-kit/postgresql
  4. PostgreSQL user to authenticate as를 입력합니다. PostgreSQL 설치 중에 따로 지정하지 않았다면 로컬 사용자 이름을 사용합니다. PostgreSQL 사용자 이름을 확인하는 방법은 다음과 같습니다.

    1. gitlab 디렉터리에 있는지 확인합니다.

    2. PostgreSQL 데이터베이스에 접근합니다. rails db를 실행합니다. 출력은 다음과 같습니다.

      psql (14.9)
      Type "help" for help.
      
      gitlabhq_development=#
      
    3. 표시된 PostgreSQL 프롬프트에서 \conninfo를 실행해 접속한 사용자와 연결에 사용한 포트를 확인합니다. 예를 들면 다음과 같습니다.

      You are connected to database "gitlabhq_development" as user "root" on host "localhost" (address "127.0.0.1") at port "5432".
      
  5. password of the PostgreSQL user를 입력하라는 메시지가 표시되면 설정한 비밀번호를 입력하거나 비워 둡니다.

    • Postgres 서버가 실행 중인 머신에 이미 로그인되어 있으므로 비밀번호는 필요하지 않습니다.
  6. Port number to connect to를 입력합니다. 기본 포트 번호는 5432 입니다.

  7. use an SSL connection? 필드에서 설치 환경에 맞는 연결 방식을 선택합니다. 선택지는 다음과 같습니다.

    • Use Secure Connection
    • Standard Connection(기본값)
  8. 선택 항목인 database to connect to 필드에 gitlabhq_development를 입력합니다.

  9. display name for the database connection 필드에 gitlabhq_development를 입력합니다.

이제 PostgreSQL Explorer 창에 gitlabhq_development 데이터베이스 연결이 표시됩니다. 화살표를 사용해 GDK 데이터베이스의 내용을 펼쳐 살펴봅니다.

연결되지 않으면 먼저 GDK가 실행 중인지 확인하고 다시 시도합니다. VS Code 용 PostgreSQL Explorer 확장 사용법에 대한 자세한 안내는 확장 문서의 사용법 절을 참고합니다.

자주 묻는 질문#

Spring 사용 시 ActiveRecord::PendingMigrationError#

Spring 프리로더로 스펙을 실행하면 테스트 데이터베이스가 손상된 상태가 될 수 있습니다. 마이그레이션을 실행하거나 테스트 데이터베이스를 삭제·초기화해도 효과가 없습니다.

$ bundle exec spring rspec some_spec.rb
...
Failure/Error: ActiveRecord::Migration.maintain_test_schema!

ActiveRecord::PendingMigrationError:

  Migrations are pending. To resolve this issue, run:

    bin/rake db:migrate RAILS_ENV=test
# ~/.rvm/gems/ruby-2.3.3/gems/activerecord-4.2.10/lib/active_record/migration.rb:392:in `check_pending!'
...
0 examples, 0 failures, 1 error occurred outside of examples

이를 해결하려면 스펙 실행 사이에 살아 있는 spring 서버와 앱을 종료합니다.

$ ps aux | grep spring
eric             87304   1.3  2.9  3080836 482596   ??  Ss   10:12AM   4:08.36 spring app    | gitlab | started 6 hours ago | test mode
eric             37709   0.0  0.0  2518640   7524 s006  S    Wed11AM   0:00.79 spring server | gitlab | started 29 hours ago
$ kill 87304
$ kill 37709

db:migrate 오류: database version is too old to be migrated#

현재 스키마 버전이 Gitlab::Database 라이브러리 모듈에 정의된 MIN_SCHEMA_VERSION 보다 오래되었다고 db:migrate가 판단하면 이 오류가 발생합니다.

시간이 지나면서 코드베이스의 오래된 마이그레이션을 정리하거나 합치므로, 모든 이전 버전에서 GitLab을 마이그레이션할 수 있는 것은 아닙니다.

이 검사를 건너뛰어야 하는 경우도 있습니다. 예를 들어 MIN_SCHEMA_VERSION보다 나중의 GitLab 스키마 버전을 사용하다가 그 이전의 마이그레이션으로 롤백한 경우입니다. 이때 다시 앞으로 마이그레이션하려면 SKIP_SCHEMA_VERSION_CHECK 환경 변수를 설정합니다.

bundle exec rake db:migrate SKIP_SCHEMA_VERSION_CHECK=true

성능 문제#

커넥션 풀링으로 연결 오버헤드 감소#

새 데이터베이스 연결을 만드는 데는 비용이 들며, 특히 PostgreSQL은 새 연결마다 프로세스 전체를 포크해야 합니다. 연결이 아주 오래 유지된다면 문제가 되지 않습니다. 그러나 작은 쿼리 몇 개를 위해 프로세스를 포크하는 것은 비용이 클 수 있습니다. 그대로 두면 새 데이터베이스 연결이 몰릴 때 성능이 저하되거나 전면 장애로 이어질 수도 있습니다.

작고 수명이 짧은 데이터베이스 연결이 급증하는 인스턴스에서 검증된 해법은 커넥션 풀러로 PgBouncer를 도입하는 것입니다. 이 풀은 거의 오버헤드 없이 수천 개의 연결을 유지할 수 있습니다. 단점은 약간의 지연이 더해진다는 점이며, 사용 패턴에 따라 성능은 90% 이상까지 개선됩니다.

PgBouncer는 설치 환경에 맞게 세밀하게 조정할 수 있습니다. 자세한 내용은 PgBouncer 세부 조정 문서를 참고합니다.

ANALYZE 실행으로 데이터베이스 통계 재생성#

ANALYZE 명령은 많은 성능 문제를 푸는 좋은 출발점입니다. 테이블 통계를 다시 생성하면 쿼리 플래너가 더 효율적인 실행 경로를 만듭니다.

최신 통계는 언제나 도움이 됩니다.

  • Linux 패키지에서는 다음과 같이 실행합니다.

    gitlab-psql -c 'SET statement_timeout = 0; ANALYZE VERBOSE;'
    
  • SQL 프롬프트에서는 다음과 같이 실행합니다.

    -- needed because this is likely to run longer than the default statement_timeout
    SET statement_timeout = 0;
    ANALYZE VERBOSE;
    

ACTIVE 워크로드 데이터 수집#

데이터베이스 자원을 실제로 크게 소비하는 것은 활성 쿼리뿐입니다.

다음 쿼리는 실행 중인 모든 활성 쿼리에서 메타 정보를 수집하며, 함께 확인할 수 있는 항목은 다음과 같습니다.

  • 쿼리 수행 시간
  • 요청을 보낸 서비스
  • wait_event(대기 상태인 경우)
  • 그 밖에 관련이 있을 수 있는 정보
-- long queries are usually easier to read with the fields arranged vertically
\x

SELECT
    pid
    ,datname
    ,usename
    ,application_name
    ,client_hostname
    ,backend_start
    ,query_start
    ,query
    ,age(now(), query_start) AS "age"
    ,state
    ,wait_event
    ,wait_event_type
    ,backend_type
FROM pg_stat_activity
WHERE state = 'active';

이 쿼리는 한 시점의 스냅숏만 담으므로, 환경이 응답하지 않는 동안 몇 분에 걸쳐 쿼리를 3~5회 실행하는 방안을 검토합니다.

-- redirect output to a file
-- this location must be writable by `gitlab-psql`
\o /tmp/active1304.out
--
-- now execute the query above
--
-- all output goes to the file - if the prompt is = then it ran
-- cancel writing output
\o

이 Python 스크립트를 사용하면 pg_stat_activity의 출력을 이해하기 쉽고 성능 문제와 연결 짓기 쉬운 수치로 파싱할 수 있습니다.

느려 보이는 쿼리 조사#

쿼리가 끝나는 데 너무 오래 걸리거나 데이터베이스 자원을 지나치게 많이 쓴다고 판단되면, EXPLAIN으로 쿼리 플래너가 그 쿼리를 어떻게 실행하는지 확인합니다.

EXPLAIN (ANALYZE, BUFFERS) SELECT ... FROM ...

BUFFERS는 관여한 메모리 양도 대략 보여 줍니다. I/O가 원인일 수 있으므로 EXPLAIN을 실행할 때는 BUFFERS를 반드시 추가합니다.

데이터베이스가 때로는 빠르고 때로는 느리다면, 두 상태 각각에서 같은 쿼리에 대한 출력을 수집합니다.

인덱스 블로트 조사#

인덱스 블로트가 눈에 띄는 성능 문제를 일으키는 경우는 드물지만, 특히 autovacuum 문제가 있다면 디스크 사용량이 크게 늘 수 있습니다.

아래 쿼리는 PostgreSQL 내장 postgres_index_bloat_estimates 테이블에서 블로트 비율을 계산하고 그 비율값으로 결과를 정렬합니다. PostgreSQL은 정상 동작을 위해 어느 정도의 블로트가 필요하므로, 25% 안팎은 여전히 정상 범위입니다.

select  a.identifier, a.bloat_size_bytes, b.tablename, b.ondisk_size_bytes,
    (a.bloat_size_bytes/b.ondisk_size_bytes::float)*100 as percentage
from postgres_index_bloat_estimates a
join postgres_indexes b on a.identifier=b.identifier
where
   -- to ensure the percentage calculation doesn't encounter zeroes
   a.bloat_size_bytes>0 and
   b.ondisk_size_bytes>1000000000
order by  percentage desc;

인덱스 재구축#

블로트가 생긴 테이블을 찾았다면 아래 쿼리로 해당 인덱스를 다시 만들 수 있습니다. 인덱스를 다시 만들면 통계가 초기화될 수 있으므로, 이후에 ANALYZE도 다시 실행합니다.

SET statement_timeout = 0;
REINDEX TABLE CONCURRENTLY <table_name>;

인덱스 재구축 진행 상황은 아래 쿼리에서 세미콜론 뒤에 \watch 30을 붙여 실행해 확인합니다.

SELECT
  t.tablename, indexname, c.reltuples AS num_rows,
  pg_size_pretty(pg_relation_size(quote_ident(t.tablename)::text)) AS table_size,
  pg_size_pretty(pg_relation_size(quote_ident(indexrelname)::text)) AS index_size,
CASE WHEN indisvalid THEN 'Y'
  ELSE 'N'
END AS VALID
FROM pg_tables t
LEFT OUTER JOIN pg_class c ON t.tablename=c.relname
LEFT OUTER JOIN
  ( SELECT c.relname AS ctablename, ipg.relname AS indexname, x.indnatts AS
  number_of_columns, indexrelname, indisvalid FROM pg_index x
JOIN pg_class c ON c.oid = x.indrelid
JOIN pg_class ipg ON ipg.oid = x.indexrelid
JOIN pg_stat_all_indexes psai ON x.indexrelid = psai.indexrelid )
AS foo
ON t.tablename = foo.ctablename
WHERE
  t.tablename in ('<comma_separated_table_names>')
  ORDER BY 1,2; \watch 30

데이터베이스 문제 해결 및 디버깅

GitLab v19.4
원문 보기

요약

이 절에서는 골치 아픈 데이터베이스 문제를 만났을 때 참고해 그대로 복사해 쓸 수 있는 내용을 제공합니다. 먼저 Slack에서 오류를 검색하거나 Google에서 GitLab &#x3C;my error>로 검색해 봅니다.

이 절에서는 골치 아픈 데이터베이스 문제를 만났을 때 참고해 그대로 복사해 쓸 수 있는 내용을 제공합니다.

먼저 Slack에서 오류를 검색하거나 Google에서 GitLab <my error>로 검색해 봅니다.

사용할 수 있는 RAILS_ENV 값은 다음과 같습니다.

  • production(주 GDK 데이터베이스에는 보통 쓰지 않지만 Omnibus 같은 다른 설치 환경에서는 필요할 수 있습니다).
  • development(주 GDK 데이터베이스입니다).
  • test(RSpec 같은 테스트에 사용합니다).

전체 삭제 후 처음부터 시작#

모든 것을 지우고 빈 데이터베이스(DB)로 다시 시작하려면 다음과 같이 실행합니다(약 1분 소요).

bundle exec rake db:reset RAILS_ENV=development

빈 DB에 샘플 데이터를 넣으려면 다음과 같이 실행합니다(약 4분 소요).

bundle exec rake dev:setup

모든 것을 지우고 샘플 데이터로 다시 시작하려면 다음과 같이 실행합니다(약 4분 소요). 이 명령은 db:reset도 수행하고 DB 별 마이그레이션도 실행합니다.

bundle exec rake db:setup RAILS_ENV=development

테스트 DB에 문제가 있다면 중요한 데이터가 들어 있지 않으므로 전부 삭제해도 안전합니다.

bundle exec rake db:reset RAILS_ENV=test

마이그레이션 다루기#

  • bundle exec rake db:migrate RAILS_ENV=development: MR에서 가져왔을 수 있는 대기 중인 마이그레이션을 실행합니다
  • bundle exec rake db:migrate:status RAILS_ENV=development: 모든 마이그레이션이 up 인지 down 인지 확인합니다
  • bundle exec rake db:migrate:down:main VERSION=20170926203418 RAILS_ENV=development: 마이그레이션을 되돌립니다
  • bundle exec rake db:migrate:up:main VERSION=20170926203418 RAILS_ENV=development: 마이그레이션을 적용합니다
  • bundle exec rake db:migrate:redo:main VERSION=20170926203418 RAILS_ENV=development: 특정 마이그레이션을 다시 실행합니다

위 명령에서 main을 바꾸면 main 대신 ci 데이터베이스를 대상으로 실행할 수 있습니다.

데이터베이스에 직접 접근#

다음 명령 중 하나로 데이터베이스에 접근합니다. 어느 명령을 쓰든 결과는 같습니다.

gdk psql -d gitlabhq_development
bundle exec rails dbconsole -e development
bundle exec rails db -e development
  • \q: 종료합니다
  • \dt: 모든 테이블을 나열합니다
  • \d+ issues: issues 테이블의 칼럼을 나열합니다
  • CREATE TABLE board_labels();: board_labels라는 테이블을 만듭니다
  • SELECT * FROM schema_migrations WHERE version = '20170926203418';: 마이그레이션이 실행되었는지 확인합니다
  • DELETE FROM schema_migrations WHERE version = '20170926203418';: 마이그레이션을 직접 제거합니다

GUI로 데이터베이스 접근#

대부분의 GUI(DataGrip, RubyMine, DBeaver)는 데이터베이스에 대한 TCP 연결을 요구합니다. 기본적으로 GDK PostgreSQL 인스턴스는 UNIX 소켓만 수신합니다. TCP로 노출하려면 다음과 같이 진행합니다.

  1. GDK 루트 디렉터리에서 호스트를 소켓 경로에서 localhost로 바꿉니다.

    gdk config set postgresql.host localhost
    
  2. gdk.yml에 다음 내용이 있는지 확인합니다.

    postgresql:
      host: localhost
    
  3. 서비스 정의를 다시 생성합니다.

    gdk reconfigure
    
  4. 새 실행 인수가 적용되도록 PostgreSQL을 재시작합니다.

    [!warning] PostgreSQL을 재시작하면 Rails, Sidekiq, 열려 있는 gdk psql 세션을 포함해 활성 연결이 모두 끊깁니다. 의존 서비스를 중지하거나 GDK가 유휴 상태일 때 재시작합니다.

    gdk restart postgresql
    
  5. GUI에서 호스트 localhost, 포트 5432, 데이터베이스 gitlabhq_development로 연결합니다. 연결 문자열 postgresql://localhost:5432/gitlabhq_development를 사용해도 됩니다.

이제 새 연결이 동작합니다.

Visual Studio Code로 GDK 데이터베이스 접근#

Visual Studio Code의 PostgreSQL 확장으로 데이터베이스 연결을 만들면 GDK 데이터베이스에 접근해 내용을 살펴볼 수 있습니다.

사전 요구 사항:

데이터베이스 연결을 만들려면 다음과 같이 진행합니다.

  1. 활동 표시줄에서 PostgreSQL Explorer 아이콘을 선택합니다.

  2. 열린 창에서 **+**를 선택해 새 데이터베이스 연결을 추가합니다.

  3. 데이터베이스의 hostname을 입력합니다. GDK 디렉터리에 있는 PostgreSQL 폴더 경로를 사용합니다.

    • 예: /dev/gitlab-development-kit/postgresql
  4. PostgreSQL user to authenticate as를 입력합니다. PostgreSQL 설치 중에 따로 지정하지 않았다면 로컬 사용자 이름을 사용합니다. PostgreSQL 사용자 이름을 확인하는 방법은 다음과 같습니다.

    1. gitlab 디렉터리에 있는지 확인합니다.

    2. PostgreSQL 데이터베이스에 접근합니다. rails db를 실행합니다. 출력은 다음과 같습니다.

      psql (14.9)
      Type "help" for help.
      
      gitlabhq_development=#
      
    3. 표시된 PostgreSQL 프롬프트에서 \conninfo를 실행해 접속한 사용자와 연결에 사용한 포트를 확인합니다. 예를 들면 다음과 같습니다.

      You are connected to database "gitlabhq_development" as user "root" on host "localhost" (address "127.0.0.1") at port "5432".
      
  5. password of the PostgreSQL user를 입력하라는 메시지가 표시되면 설정한 비밀번호를 입력하거나 비워 둡니다.

    • Postgres 서버가 실행 중인 머신에 이미 로그인되어 있으므로 비밀번호는 필요하지 않습니다.
  6. Port number to connect to를 입력합니다. 기본 포트 번호는 5432 입니다.

  7. use an SSL connection? 필드에서 설치 환경에 맞는 연결 방식을 선택합니다. 선택지는 다음과 같습니다.

    • Use Secure Connection
    • Standard Connection(기본값)
  8. 선택 항목인 database to connect to 필드에 gitlabhq_development를 입력합니다.

  9. display name for the database connection 필드에 gitlabhq_development를 입력합니다.

이제 PostgreSQL Explorer 창에 gitlabhq_development 데이터베이스 연결이 표시됩니다. 화살표를 사용해 GDK 데이터베이스의 내용을 펼쳐 살펴봅니다.

연결되지 않으면 먼저 GDK가 실행 중인지 확인하고 다시 시도합니다. VS Code 용 PostgreSQL Explorer 확장 사용법에 대한 자세한 안내는 확장 문서의 사용법 절을 참고합니다.

자주 묻는 질문#

Spring 사용 시 ActiveRecord::PendingMigrationError#

Spring 프리로더로 스펙을 실행하면 테스트 데이터베이스가 손상된 상태가 될 수 있습니다. 마이그레이션을 실행하거나 테스트 데이터베이스를 삭제·초기화해도 효과가 없습니다.

$ bundle exec spring rspec some_spec.rb
...
Failure/Error: ActiveRecord::Migration.maintain_test_schema!

ActiveRecord::PendingMigrationError:

  Migrations are pending. To resolve this issue, run:

    bin/rake db:migrate RAILS_ENV=test
# ~/.rvm/gems/ruby-2.3.3/gems/activerecord-4.2.10/lib/active_record/migration.rb:392:in `check_pending!'
...
0 examples, 0 failures, 1 error occurred outside of examples

이를 해결하려면 스펙 실행 사이에 살아 있는 spring 서버와 앱을 종료합니다.

$ ps aux | grep spring
eric             87304   1.3  2.9  3080836 482596   ??  Ss   10:12AM   4:08.36 spring app    | gitlab | started 6 hours ago | test mode
eric             37709   0.0  0.0  2518640   7524 s006  S    Wed11AM   0:00.79 spring server | gitlab | started 29 hours ago
$ kill 87304
$ kill 37709

db:migrate 오류: database version is too old to be migrated#

현재 스키마 버전이 Gitlab::Database 라이브러리 모듈에 정의된 MIN_SCHEMA_VERSION 보다 오래되었다고 db:migrate가 판단하면 이 오류가 발생합니다.

시간이 지나면서 코드베이스의 오래된 마이그레이션을 정리하거나 합치므로, 모든 이전 버전에서 GitLab을 마이그레이션할 수 있는 것은 아닙니다.

이 검사를 건너뛰어야 하는 경우도 있습니다. 예를 들어 MIN_SCHEMA_VERSION보다 나중의 GitLab 스키마 버전을 사용하다가 그 이전의 마이그레이션으로 롤백한 경우입니다. 이때 다시 앞으로 마이그레이션하려면 SKIP_SCHEMA_VERSION_CHECK 환경 변수를 설정합니다.

bundle exec rake db:migrate SKIP_SCHEMA_VERSION_CHECK=true

성능 문제#

커넥션 풀링으로 연결 오버헤드 감소#

새 데이터베이스 연결을 만드는 데는 비용이 들며, 특히 PostgreSQL은 새 연결마다 프로세스 전체를 포크해야 합니다. 연결이 아주 오래 유지된다면 문제가 되지 않습니다. 그러나 작은 쿼리 몇 개를 위해 프로세스를 포크하는 것은 비용이 클 수 있습니다. 그대로 두면 새 데이터베이스 연결이 몰릴 때 성능이 저하되거나 전면 장애로 이어질 수도 있습니다.

작고 수명이 짧은 데이터베이스 연결이 급증하는 인스턴스에서 검증된 해법은 커넥션 풀러로 PgBouncer를 도입하는 것입니다. 이 풀은 거의 오버헤드 없이 수천 개의 연결을 유지할 수 있습니다. 단점은 약간의 지연이 더해진다는 점이며, 사용 패턴에 따라 성능은 90% 이상까지 개선됩니다.

PgBouncer는 설치 환경에 맞게 세밀하게 조정할 수 있습니다. 자세한 내용은 PgBouncer 세부 조정 문서를 참고합니다.

ANALYZE 실행으로 데이터베이스 통계 재생성#

ANALYZE 명령은 많은 성능 문제를 푸는 좋은 출발점입니다. 테이블 통계를 다시 생성하면 쿼리 플래너가 더 효율적인 실행 경로를 만듭니다.

최신 통계는 언제나 도움이 됩니다.

  • Linux 패키지에서는 다음과 같이 실행합니다.

    gitlab-psql -c 'SET statement_timeout = 0; ANALYZE VERBOSE;'
    
  • SQL 프롬프트에서는 다음과 같이 실행합니다.

    -- needed because this is likely to run longer than the default statement_timeout
    SET statement_timeout = 0;
    ANALYZE VERBOSE;
    

ACTIVE 워크로드 데이터 수집#

데이터베이스 자원을 실제로 크게 소비하는 것은 활성 쿼리뿐입니다.

다음 쿼리는 실행 중인 모든 활성 쿼리에서 메타 정보를 수집하며, 함께 확인할 수 있는 항목은 다음과 같습니다.

  • 쿼리 수행 시간
  • 요청을 보낸 서비스
  • wait_event(대기 상태인 경우)
  • 그 밖에 관련이 있을 수 있는 정보
-- long queries are usually easier to read with the fields arranged vertically
\x

SELECT
    pid
    ,datname
    ,usename
    ,application_name
    ,client_hostname
    ,backend_start
    ,query_start
    ,query
    ,age(now(), query_start) AS "age"
    ,state
    ,wait_event
    ,wait_event_type
    ,backend_type
FROM pg_stat_activity
WHERE state = 'active';

이 쿼리는 한 시점의 스냅숏만 담으므로, 환경이 응답하지 않는 동안 몇 분에 걸쳐 쿼리를 3~5회 실행하는 방안을 검토합니다.

-- redirect output to a file
-- this location must be writable by `gitlab-psql`
\o /tmp/active1304.out
--
-- now execute the query above
--
-- all output goes to the file - if the prompt is = then it ran
-- cancel writing output
\o

이 Python 스크립트를 사용하면 pg_stat_activity의 출력을 이해하기 쉽고 성능 문제와 연결 짓기 쉬운 수치로 파싱할 수 있습니다.

느려 보이는 쿼리 조사#

쿼리가 끝나는 데 너무 오래 걸리거나 데이터베이스 자원을 지나치게 많이 쓴다고 판단되면, EXPLAIN으로 쿼리 플래너가 그 쿼리를 어떻게 실행하는지 확인합니다.

EXPLAIN (ANALYZE, BUFFERS) SELECT ... FROM ...

BUFFERS는 관여한 메모리 양도 대략 보여 줍니다. I/O가 원인일 수 있으므로 EXPLAIN을 실행할 때는 BUFFERS를 반드시 추가합니다.

데이터베이스가 때로는 빠르고 때로는 느리다면, 두 상태 각각에서 같은 쿼리에 대한 출력을 수집합니다.

인덱스 블로트 조사#

인덱스 블로트가 눈에 띄는 성능 문제를 일으키는 경우는 드물지만, 특히 autovacuum 문제가 있다면 디스크 사용량이 크게 늘 수 있습니다.

아래 쿼리는 PostgreSQL 내장 postgres_index_bloat_estimates 테이블에서 블로트 비율을 계산하고 그 비율값으로 결과를 정렬합니다. PostgreSQL은 정상 동작을 위해 어느 정도의 블로트가 필요하므로, 25% 안팎은 여전히 정상 범위입니다.

select  a.identifier, a.bloat_size_bytes, b.tablename, b.ondisk_size_bytes,
    (a.bloat_size_bytes/b.ondisk_size_bytes::float)*100 as percentage
from postgres_index_bloat_estimates a
join postgres_indexes b on a.identifier=b.identifier
where
   -- to ensure the percentage calculation doesn't encounter zeroes
   a.bloat_size_bytes>0 and
   b.ondisk_size_bytes>1000000000
order by  percentage desc;

인덱스 재구축#

블로트가 생긴 테이블을 찾았다면 아래 쿼리로 해당 인덱스를 다시 만들 수 있습니다. 인덱스를 다시 만들면 통계가 초기화될 수 있으므로, 이후에 ANALYZE도 다시 실행합니다.

SET statement_timeout = 0;
REINDEX TABLE CONCURRENTLY <table_name>;

인덱스 재구축 진행 상황은 아래 쿼리에서 세미콜론 뒤에 \watch 30을 붙여 실행해 확인합니다.

SELECT
  t.tablename, indexname, c.reltuples AS num_rows,
  pg_size_pretty(pg_relation_size(quote_ident(t.tablename)::text)) AS table_size,
  pg_size_pretty(pg_relation_size(quote_ident(indexrelname)::text)) AS index_size,
CASE WHEN indisvalid THEN 'Y'
  ELSE 'N'
END AS VALID
FROM pg_tables t
LEFT OUTER JOIN pg_class c ON t.tablename=c.relname
LEFT OUTER JOIN
  ( SELECT c.relname AS ctablename, ipg.relname AS indexname, x.indnatts AS
  number_of_columns, indexrelname, indisvalid FROM pg_index x
JOIN pg_class c ON c.oid = x.indrelid
JOIN pg_class ipg ON ipg.oid = x.indexrelid
JOIN pg_stat_all_indexes psai ON x.indexrelid = psai.indexrelid )
AS foo
ON t.tablename = foo.ctablename
WHERE
  t.tablename in ('<comma_separated_table_names>')
  ORDER BY 1,2; \watch 30