데이터베이스 문제 해결 및 디버깅
GitLab v19.4요약
이 절에서는 골치 아픈 데이터베이스 문제를 만났을 때 참고해 그대로 복사해 쓸 수 있는 내용을 제공합니다. 먼저 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로 노출하려면 다음과 같이 진행합니다.
-
GDK 루트 디렉터리에서 호스트를 소켓 경로에서
localhost로 바꿉니다.gdk config set postgresql.host localhost -
gdk.yml에 다음 내용이 있는지 확인합니다.postgresql: host: localhost -
서비스 정의를 다시 생성합니다.
gdk reconfigure -
새 실행 인수가 적용되도록 PostgreSQL을 재시작합니다.
[!warning] PostgreSQL을 재시작하면 Rails, Sidekiq, 열려 있는
gdk psql세션을 포함해 활성 연결이 모두 끊깁니다. 의존 서비스를 중지하거나 GDK가 유휴 상태일 때 재시작합니다.gdk restart postgresql -
GUI에서 호스트
localhost, 포트5432, 데이터베이스gitlabhq_development로 연결합니다. 연결 문자열postgresql://localhost:5432/gitlabhq_development를 사용해도 됩니다.
이제 새 연결이 동작합니다.
Visual Studio Code로 GDK 데이터베이스 접근#
Visual Studio Code의 PostgreSQL 확장으로 데이터베이스 연결을 만들면 GDK 데이터베이스에 접근해 내용을 살펴볼 수 있습니다.
사전 요구 사항:
- Visual Studio (VS) Code.
- PostgreSQL VS Code 확장.
데이터베이스 연결을 만들려면 다음과 같이 진행합니다.
-
활동 표시줄에서 PostgreSQL Explorer 아이콘을 선택합니다.
-
열린 창에서 **+**를 선택해 새 데이터베이스 연결을 추가합니다.
-
데이터베이스의 hostname을 입력합니다. GDK 디렉터리에 있는 PostgreSQL 폴더 경로를 사용합니다.
- 예:
/dev/gitlab-development-kit/postgresql
- 예:
-
PostgreSQL user to authenticate as를 입력합니다. PostgreSQL 설치 중에 따로 지정하지 않았다면 로컬 사용자 이름을 사용합니다. PostgreSQL 사용자 이름을 확인하는 방법은 다음과 같습니다.
-
gitlab디렉터리에 있는지 확인합니다. -
PostgreSQL 데이터베이스에 접근합니다.
rails db를 실행합니다. 출력은 다음과 같습니다.psql (14.9) Type "help" for help. gitlabhq_development=# -
표시된 PostgreSQL 프롬프트에서
\conninfo를 실행해 접속한 사용자와 연결에 사용한 포트를 확인합니다. 예를 들면 다음과 같습니다.You are connected to database "gitlabhq_development" as user "root" on host "localhost" (address "127.0.0.1") at port "5432".
-
-
password of the PostgreSQL user를 입력하라는 메시지가 표시되면 설정한 비밀번호를 입력하거나 비워 둡니다.
- Postgres 서버가 실행 중인 머신에 이미 로그인되어 있으므로 비밀번호는 필요하지 않습니다.
-
Port number to connect to를 입력합니다. 기본 포트 번호는
5432입니다. -
use an SSL connection? 필드에서 설치 환경에 맞는 연결 방식을 선택합니다. 선택지는 다음과 같습니다.
- Use Secure Connection
- Standard Connection(기본값)
-
선택 항목인 database to connect to 필드에
gitlabhq_development를 입력합니다. -
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