MySQL에서 PostgreSQL로 수동 마이그레이션#
MySQL 데이터베이스를 PostgreSQL로 수동 마이그레이션하는 과정에는 다음 단계가 포함됩니다:
팁
협업 플레이북과 보드를 마이그레이션하는 방법에 대한 자세한 내용은 플러그인 마이그레이션 섹션을 참조하세요.
수동 업그레이드가 올바른 방향인지 확실하지 않으신가요? Mattermost 배포를 기반으로 맞춤형 지침을 찾는 Mattermost 고객은 Mattermost 전문가 에 문의할 수 있습니다.
도구 추천#
Postgres로 수동 마이그레이션을 선호하는 경우, 마이그레이션 프로세스를 위해 다음 도구를 추천합니다:
이 페이지에는 각 도구를 설치하는 방법과 데이터베이스 마이그레이션을 진행하는 방법에 대한 지침이 포함되어 있습니다.
필요한 도구를 설치한 후에는 시스템 요구 사항 및 구성 문서를 검토하고 마이그레이션을 준비하기 위해 마이그레이션 시작 전 필요한 사항을 확인하세요.
대상 데이터베이스 준비 로 마이그레이션을 시작한 다음 데이터 마이그레이션 을 수행하고 모든 마이그레이션 후 단계 를 완료하세요.
협업 플레이북과 보드 마이그레이션에 대한 자세한 내용은 플러그인 마이그레이션 문서를 참조하세요.
pgloader#
MySQL에서 PostgreSQL로 데이터를 마이그레이션하려면 pgloader 도구를 사용하세요.
pgloader 설치#
pgloader 를 설치하려면 공식 설치 가이드 를 참조하세요.
참고
MySQL v8을 사용하는 경우: pgloader-compiled 바이너리의 알려진 버그 로 인해 소스에서 pgloader를 컴파일해야 합니다. 소스에서 빌드하려면 여기 의 단계를 따르세요.
또는 pgloader 설치나 빌드를 피하기 위해 mattermost-pgloader Docker 이미지를 사용할 수 있습니다. 자세한 내용은 아래 문서를 참조하세요.
pgloader 사용#
Docker 이미지 가져오기 및 pgloader 확인#
수동 마이그레이션을 위해 다음 명령을 실행하여 mattermost-pgloader 이미지를 가져오고 pgloader가 올바르게 작동하는지 확인하세요:
docker run -it --rm -v $(pwd):/home/migration mattermost/mattermost-pgloader:latest pgloader --version
이 명령은 mattermost/mattermost-pgloader:latest 이미지를 가져오고 pgloader 를 실행하여 버전을 확인하고 예상대로 작동하는지 확인합니다.
로컬 디렉토리 매핑#
-v $(pwd):/home/migration 플래그를 사용하여 현재 작업 디렉토리를 Docker 컨테이너에 매핑하세요. 이를 통해 로컬 디렉토리를 사용하여 로그와 기타 파일을 저장할 수 있습니다.
네트워크 구성 설정#
네트워크 요구 사항에 따라 --network 플래그를 적절히 설정하세요. 예를 들어, localhost에 접근하려면 네트워크를 host 로 설정해야 합니다.
morph#
morph 도구는 PostgreSQL 스키마를 생성합니다.
참고
morph 와 dbcmp 모두 Go 도구 체인이 필요합니다. Go 컴파일러를 설치하려면 Go 문서 를 참조하세요.
morph 설치#
다음 명령을 실행하여 morph CLI를 설치할 수 있습니다:
go install github.com/mattermost/morph/cmd/morph@v1
dbcmp (선택 사항)#
dbcmp 도구를 사용하면 모든 테이블을 비교하고 두 스키마 간에 차이가 있는지 보고하여 마이그레이션 후 데이터를 비교할 수 있습니다.
dbcmp 설치#
다음 명령을 실행하여 dbcmp 를 설치할 수 있습니다:
go install github.com/mattermost/dbcmp/cmd/dbcmp@latest
수동 마이그레이션을 위한 시스템 요구 사항 및 구성#
수동 마이그레이션 프로세스를 시작하기 전에 시스템이 원활하고 효율적인 마이그레이션을 위한 필요한 요구 사항을 충족하는지 확인하는 것이 중요합니다. 다음 시스템 사양과 조정을 강력히 권장합니다:
충분한 시스템 메모리 리소스가 있는지 확인하세요. 기본적으로 16GB RAM을 권장합니다. 시스템 메모리가 부족한 경우 사용자는
number of workers,prefetch rows, 그리고 특히concurrency가1이상으로 설정된 경우rows per range와 같은 pgloader 설정을 미세 조정할 수 있습니다. 이러한 조정은 사용 가능한 시스템 리소스를 기반으로 리소스 활용을 최적화하는 데 도움이 될 수 있습니다. 자세한 내용은 pgloader 문서 를 참조하세요.마이그레이션 프로세스, 특히 대용량 데이터셋을 처리할 때는 충분한 처리 능력을 가진 멀티코어 프로세서를 권장합니다.
MySQL과 PostgreSQL 데이터베이스, 그리고 마이그레이션 프로세스 중 생성되는 임시 파일을 저장할 수 있는 충분한 디스크 공간이 있는지 확인하세요. 필요한 디스크 공간의 양은 마이그레이션되는 데이터베이스의 크기에 따라 달라집니다.
마이그레이션 시간을 더 줄이기 위해 사용자는 마이그레이션 프로세스를 시작하기 전에 대상 PostgreSQL 데이터베이스의 인덱스를 수동으로 삭제할 수 있습니다. 이 방법은 데이터 삽입 중 인덱스 구축으로 인한 오버헤드를 줄여 마이그레이션 속도를 잠재적으로 향상시킬 수 있습니다.
수동 마이그레이션 전#
중요
이 가이드는 v7.1 ESR 이상의 스키마가 필요합니다. 따라서 이전 버전을 사용 중이고 마이그레이션을 계획 중이라면 Mattermost 서버를 최소 v7.1로 업데이트하세요. 자세한 내용은 extended support releases 문서를 참조하세요.
MySQL 데이터를 백업하세요.
Mattermost 버전을 확인하세요. 자세한 내용은 정보 모달을 참조하세요.
마이그레이션 시간을 예약하세요. 이 프로세스는 마이그레이션 중에 Mattermost 서버를 중지해야 합니다.
스키마 간의 데이터 호환성을 확인하려면 schema-diffs 섹션을 참조하세요.
데이터베이스와 사용자를 생성하여 PostgreSQL 환경을 준비하세요. 자세한 내용은 database 문서를 참조하세요.
PostgreSQL의 newer versions 에서는 새로 생성된 사용자가
public스키마에 접근할 수 없습니다.GRANT ALL ON SCHEMA public to mmuser명령을 실행하여 접근 권한을 명시적으로 부여해야 합니다.
스키마 차이점#
수동 마이그레이션 전에 두 스키마 간의 차이점으로 인해 오류 없는 마이그레이션을 위해 일부 수동 단계가 필요할 수 있습니다.
Text에서 character varying으로#
Mattermost MySQL 스키마가 PostgreSQL 스키마의 varchar 표현 대신 다양한 테이블에서 text 열 유형을 사용하므로 PostgreSQL 스키마 제한 내에서 크기가 일관되는지 확인하는 것이 좋습니다.
필요한 삭제나 업데이트가 있는지 확인할 수 있습니다. 예를 들어, Audits 테이블/Action 열에서 확인하려면 다음을 실행하세요:
SELECT FROM mattermost.Audits where LENGTH(Action) > 512;
다음 표는 추가적인 영향을 주지 않는 삭제나 업데이트를 보여줍니다.
테이블 |
열 |
데이터 타입 캐스팅 |
삭제 시 영향 |
|---|---|---|---|
감사 |
작업 |
text -> varchar(512) |
애플리케이션 작동 방식에 부작용 없음 (해당 행을 삭제해야 함). |
감사 |
ExtraInfo |
text -> varchar(1024) |
애플리케이션 작동 방식에 부작용 없음 (해당 행을 삭제해야 함). |
ClusterDiscovery |
HostName |
text -> varchar(512) |
애플리케이션 작동 방식에 부작용 없음 (해당 행을 삭제해야 함). |
명령어 |
IconURL |
text -> varchar(1024) |
필드를 삭제하거나 새 URL로 업데이트할 수 있습니다. |
명령어 |
AutoCompleteDesc |
text -> varchar(1024) |
필드를 삭제하거나 다시 작성할 수 있습니다. |
명령어 |
AutoCompleteHint |
text -> varchar(1024) |
필드를 삭제하거나 다시 작성할 수 있습니다. |
RemoteClusters |
주제 |
text -> varchar(512) |
필드를 제거할 수 있습니다. |
시스템 |
값 |
text -> varchar(1024) |
예외 케이스로, 이상적으로는 발생하지 않아야 합니다. |
다음 표는 스키마가 다를 수 있는 여러 상황과 PostgreSQL 스키마 내의 데이터 크기 제약으로 인해 오류가 발생할 수 있는 경우를 보여줍니다. 각 테이블/행은 개별 검사가 필요하므로 삭제 시 발생할 수 있는 결과를 추가했습니다.
팁
커뮤니티로부터 LinkMetadata 와 FileInfo 테이블에 일부 오버플로우가 있다는 여러 보고를 받았으므로, 특히 이 테이블들을 확인하는 것을 권장합니다. MySQL 스키마의 데이터가 이러한 제한을 초과하는지 확인해 주세요.
테이블 |
열 |
데이터 타입 캐스팅 |
삭제 시 영향 |
|---|---|---|---|
준수 |
키워드 |
text -> varchar(512) |
준수를 위한 필터를 업데이트해야 합니다. |
준수 |
이메일 |
text -> varchar(1024) |
준수를 위한 필터를 업데이트해야 합니다. |
FileInfo |
경로 |
text -> varchar(512) |
이 파일에 대한 이전 링크가 작동하지 않습니다 (해당 행을 삭제해야 합니다). |
FileInfo |
ThumbnailPath |
text -> varchar(512) |
이 파일에 대한 이전 링크가 작동하지 않습니다 (해당 행을 삭제해야 합니다). |
FileInfo |
PreviewPath |
text -> varchar(512) |
이 파일에 대한 이전 링크가 작동하지 않습니다 (해당 행을 삭제해야 합니다). |
FileInfo |
이름 |
text -> varchar(256) |
이 파일에 대한 이전 링크가 작동하지 않습니다 (해당 행을 삭제해야 합니다). |
FileInfo |
MimeType |
text -> varchar(256) |
이 파일에 대한 이전 링크가 작동하지 않습니다 (해당 행을 삭제해야 합니다). |
LinkMetadata |
URL |
text -> varchar(2048) |
이 파일에 대한 이전 링크가 작동하지 않습니다 (해당 행을 삭제해야 합니다). |
RemoteClusters |
SiteURL |
text -> varchar(512) |
이전 원격 클러스터가 제거됩니다 (해당 행을 삭제해야 합니다). |
세션 |
DeviceId |
text -> varchar(512) |
이러한 장치에서 사용자가 로그아웃됩니다 (해당 행을 삭제해야 합니다). |
UploadSessions |
FileName |
text -> varchar(256) |
업로드 세션이 손실됩니다 (해당 행을 삭제해야 합니다). |
UploadSessions |
경로 |
text -> varchar(512) |
업로드 세션이 손실됩니다 (해당 행을 삭제해야 합니다). |
전체 텍스트 인덱스#
Posts 와 FileInfo 테이블의 일부 단어가 전체 텍스트 검색 인덱싱을 위한 최대 토큰 길이 제한 을 초과할 수 있습니다. 이러한 경우 PostgreSQL 스키마에서 idx_posts_message_txt 와 idx_fileinfo_content_txt 인덱스를 삭제하고, 마이그레이션 후 다음 쿼리를 실행하여 이러한 인덱스를 생성합니다:
마이그레이션 중 오류를 방지하기 위해 다음 쿼리를 포함했습니다:
DROP INDEX IF EXISTS {{ .source_db }}.idx_posts_message_txt;
DROP INDEX IF EXISTS {{ .source_db }}.idx_fileinfo_content_txt;
지원되지 않는 유니코드 시퀀스#
PostgreSQL에서 허용되지 않는 특정 유니코드 시퀀스가 있는데, 바로 \u0000 입니다. 이 시퀀스가 MySQL 데이터베이스의 여러 테이블에 걸쳐 여러 행에 나타날 수 있습니다. 이 경우 마이그레이션 중에 다음과 같은 오류가 발생할 수 있습니다: 지원되지 않는 유니코드 이스케이프 시퀀스: \u0000를 텍스트로 변환할 수 없습니다. 이를 방지하기 위해 마이그레이션을 시작하기 전에 데이터를 정리하는 것이 좋습니다. 다음 쿼리를 사용하여 \u0000 시퀀스를 빈 문자열로 대체할 수 있습니다.
참고
이 쿼리를 스크립트에서 그대로 사용하거나, MySQL 콘솔에서 정의할 때 구분자를 다른 것으로 설정해야 할 수 있습니다 (예: DELIMITER //). 프로시저 정의가 완료되면 구분자를 원래대로 되돌려주세요 (즉, DELIMITER ;).
CREATE PROCEDURE SanitizeUnsupportedUnicode()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE curTableName text;
DECLARE curColumnName text;
DECLARE cursors CURSOR FOR
SELECT table_name, column_name
FROM information_schema.COLUMNS
WHERE data_type = 'json'
AND table_schema = DATABASE();
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cursors;
WHILE NOT DONE DO
FETCH cursors INTO curTableName, curColumnName;
SET @query_string = CONCAT('UPDATE ', curTableName, ' SET ', curColumnName, ' = REPLACE(', curColumnName, ', \'\\\\u0000\', \'\') WHERE ', curColumnName, ' LIKE \'%\\u0000%\';');
PREPARE dynamic_query FROM @query_string;
EXECUTE dynamic_query;
DEALLOCATE PREPARE dynamic_query;
END WHILE;
CLOSE cursors;
END;
CALL SanitizeUnsupportedUnicode();
DROP PROCEDURE IF EXISTS SanitizeUnsupportedUnicode;
참고
허용되지 않는 특정 바이트 시퀀스 값도 있으며, 이로 인해 마이그레이션 중에 'UTF8' 인코딩에 대한 잘못된 바이트 시퀀스: 0x00" 오류가 발생할 수 있습니다. 이 오류를 방지하기 위해 텍스트 캐스팅 규칙에 remove-null-characters 절을 추가할 수 있습니다. 그러나 pgloader가 데이터를 즉시 수정하기 때문에 비교 단계에서 테이블 간에 차이가 있을 수 있습니다(영향을 받는 테이블이 있는 경우).
이전 구성/버전의 아티팩트가 남아있을 수 있습니다#
v6.4 이전에는 Mattermost가 스키마 마이그레이션을 처리하기 위해 golang-migrate 를 사용했습니다. 더 이상 사용하지 않기 때문에 schema_migrations 테이블을 제외합니다. v6.4 이전에 Mattermost를 사용했다면 이 테이블을 삭제하고 비교에서도 제외하는 것을 고려하세요.
DROP TABLE mattermost.schema_migrations;
일부 커뮤니티 구성원들이 SharedChannelRemotes 테이블에 description 과 nextsyncat 열이 있다고 보고했습니다. 이러한 열은 테이블에서 제거해야 합니다. 다음 DDL을 실행하여 열을 삭제하는 것을 고려하세요. (이 마이그레이션은 Mattermost의 향후 버전에 추가될 예정입니다).
ALTER TABLE SharedChannelRemotes DROP COLUMN description, DROP COLUMN nextsyncat;
이전에 릴리스된 96번째 마이그레이션에서 오류가 발견되었습니다. 마이그레이션을 진행하기 전에 특정 열을 제거해야 합니다. Threads 테이블이 예상 상태에 도달하도록 하려면 다음 준비된 문을 실행하세요:
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Threads'
AND table_schema = DATABASE()
AND column_name = 'TeamId'
) > 0,
'ALTER TABLE Threads DROP COLUMN TeamId;',
'SELECT 1'
));
PREPARE alterIfExists FROM @preparedStatement;
EXECUTE alterIfExists;
DEALLOCATE PREPARE alterIfExists;
데이터베이스의 구성#
이전에 Mattermost 구성 을 처리하기 위해 데이터베이스를 사용했다면, 해당 테이블은 마이그레이션 스크립트 를 통해 MySQL 데이터베이스에서 마이그레이션되지 않습니다.
두 가지 마이그레이션이 필요합니다:
데이터베이스 구성을 파일 시스템으로 마이그레이션
파일 시스템 구성을 다시 데이터베이스로 마이그레이션
데이터베이스 구성을 파일 시스템으로 마이그레이션#
다음과 같이 mmctl config migrate 명령을 사용하여 구성을 마이그레이션 하세요:
mmctl config migrate "<DB_USER>:<DB_PASS>@tcp(<DB_HOST>:3306)/<DB_NAME>?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s&multiStatements=true" /opt/mattermost/config/config.json --local
여기서 <DB_USER>, <DB_PASS>, <DB_HOST>, <DB_NAME> 는 환경 값으로 대체됩니다. 이 명령을 실행할 때 --local 을 사용해야 합니다. 첫 번째 매개변수(<DB_USER>, <DB_PASS>) 는 구성이 저장된 데이터베이스이고, 두 번째 매개변수(<DB_HOST>, <DB_NAME>)는 구성을 저장하는 파일입니다.
새로운 변경사항을 반영하기 위해 구성 파일에서 SqlSettings.DataSource 와 SqlSettings.DriverName 필드를 업데이트하세요. 이를 위해 json 파일을 열고 해당 필드를 변경하세요.
파일 시스템 구성을 다시 데이터베이스로 마이그레이션#
구성을 다시 데이터베이스에 저장하려면 mmctl config migrate 명령을 다시 사용하고 매개변수를 반대로 지정하세요. 대상 데이터베이스로 다시 이동할 때 새 데이터베이스 자격 증명을 사용해야 합니다.
SELECT * FROM Configurations WHERE Active = 't';
SqlSettings.DataSource 와 SqlSettings.DriverName 필드를 그에 맞게 업데이트해야 합니다. 또한 마이그레이션이 완료된 후에는 MM_CONFIG 환경 변수가 새 DSN을 가리키도록 해야 합니다.
대상 데이터베이스 준비#
PostgreSQL 데이터베이스 스키마가 필요한 사양에 맞게 올바르게 구성되도록 테이블과 인덱스를 생성하는 것이 필수적입니다. Mattermost 저장소에는 이를 달성하는 데 필요한 모든 SQL 쿼리가 포함되어 있으므로 다음 단계를 실행하여 이를 활용할 수 있습니다:
특정 버전의
mattermost저장소를 복제하세요:
git clone -b <your current version (eg. release-7.8)> git@github.com:mattermost/mattermost.git --depth=1
다음 명령을 사용하여 morph CLI로 PostgreSQL 데이터베이스에서 모든 스키마 마이그레이션*을 실행하세요:
morph apply up --driver postgres --dsn "postgres://user:pass@localhost:5432/<target_db_mame>?sslmode=disable" --path ./mattermost/db/migrations/postgres --number -1
* v8 이후에는 프로젝트 재구성으로 인해 마이그레이션 디렉토리가 Mattermost 저장소를 복제한 위치를 기준으로 ./mattermost/server/channels/db/migrations/postgres/ 로 변경되었습니다. 그에 맞게 --path 플래그를 설정하세요.
데이터 마이그레이션#
스키마를 원하는 상태로 설정한 후 pgloader 를 실행하여 데이터 마이그레이션을 시작할 수 있습니다.
참고
아래 예시에서는 두 데이터베이스의 호스트가 동일한 인스턴스에 있다고 가정합니다. 다른 기계에 있는 경우 주소를 그에 맞게 업데이트하세요. 또한 --dry-run 플래그를 사용하여 pgloader 를 실행하여 .load 파일을 테스트할 수 있습니다. 예를 들어 pgloader --dry-run migration.load 명령을 사용하세요.
데이터 마이그레이션의 기준으로 다음 구성을 사용하세요:
LOAD DATABASE
FROM mysql://{{ .mysql_user }}:{{ .mysql_password }}@{{ .mysql_address }}/{{ .source_db }}
INTO pgsql://{{ .pg_user }}:{{ .pg_password }}@{{ .postgres_address }}/{{ .target_db }}
WITH data only,
workers = 8, concurrency = 1,
multiple readers per thread, rows per range = 10000,
prefetch rows = 10000, batch rows = 2500,
create no tables, create no indexes,
preserve index names
SET PostgreSQL PARAMETERS
maintenance_work_mem to '128MB',
work_mem to '12MB'
SET MySQL PARAMETERS
net_read_timeout = '120',
net_write_timeout = '120'
CAST column Channels.Type to "channel_type" drop typemod,
column Teams.Type to "team_type" drop typemod,
column UploadSessions.Type to "upload_session_type" drop typemod,
column ChannelBookmarks.Type to "channel_bookmark_type" drop typemod,
column Drafts.Priority to text,
type int when (= precision 11) to integer drop typemod,
type bigint when (= precision 20) to bigint drop typemod,
type text to varchar drop typemod using remove-null-characters,
type tinyint when (<= precision 4) to boolean using tinyint-to-boolean,
type json to jsonb drop typemod using remove-null-characters
EXCLUDING TABLE NAMES MATCHING ~<IR_>, ~<focalboard>, 'schema_migrations', 'db_migrations', 'db_lock',
'Configurations', 'ConfigurationFiles', 'db_config_migrations', 'calls'
BEFORE LOAD DO
$$ ALTER SCHEMA public RENAME TO {{ .source_db }}; $$,
$$ TRUNCATE TABLE {{ .source_db }}.systems; $$,
$$ DROP INDEX IF EXISTS {{ .source_db }}.idx_posts_message_txt; $$,
$$ DROP INDEX IF EXISTS {{ .source_db }}.idx_fileinfo_content_txt; $$
AFTER LOAD DO
$$ UPDATE {{ .source_db }}.db_migrations set name='add_createat_to_teamembers' where version=92; $$,
$$ ALTER SCHEMA {{ .source_db }} RENAME TO public; $$,
$$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
$$ ALTER USER {{ .pg_user }} SET SEARCH_PATH TO 'public'; $$;
이 구성 파일(예: migration.load)을 저장한 후 다음 명령으로 pgloader 를 실행할 수 있습니다:
pgloader migration.load > migration.log
마이그레이션 과정에서 발견한 내용을 기여하거나 보고해 주세요.
마이그레이션 후#
전체 텍스트 인덱스 복원#
Posts 및 FileInfo 테이블 접근 시 성능 저하를 방지하기 위해 마이그레이션이 완료되면 다음 쿼리를 실행해야 합니다:
CREATE INDEX IF NOT EXISTS idx_posts_message_txt ON public.posts USING gin(to_tsvector('english', message));
CREATE INDEX IF NOT EXISTS idx_fileinfo_content_txt ON public.fileinfo USING gin(to_tsvector('english', content));
참고
Posts 및 FileInfo 테이블의 항목이 위에서 언급한 제한을 초과하는 경우, 인덱스 생성 쿼리는 이러한 인덱스를 생성하려고 할 때 ERROR: string is too long for tsvector 로그로 경고를 표시합니다. 이는 tsvector``에 맞지 않는 내용이 무시되었음을 의미합니다. 잘린 내용을 인덱싱하려면 인덱스를 생성할 때 내용에 ``substring() 함수를 사용할 수 있습니다. 아래에 예시 쿼리가 있습니다. 내용의 부분 문자열로 인덱스를 생성하는 것이 계속 실패하는 경우, 인덱스가 성공적으로 생성될 때까지 값을 점진적으로 줄이는 것을 고려하세요(예: 500000).
CREATE INDEX IF NOT EXISTS idx_fileinfo_content_txt ON public.fileinfo USING gin(to_tsvector('english', substring(content,0,1000000)));
데이터 비교#
데이터베이스 비교를 단순화하기 위해 dbcmp 라는 내부 도구를 개발했습니다. 이 도구는 두 데이터베이스의 모든 테이블을 확인하고 스키마의 차이점을 보고합니다. 그러나 dbcmp 는 개별 행을 비교하지 않습니다. 대신 지정된 페이지 크기를 기반으로 체크섬 값을 계산하고 비교합니다. 이는 행 수준의 차이를 생성할 수 없음을 의미합니다.
데이터 무결성을 확인하기 위한 추가 검사로 dbcmp 를 사용하는 것을 권장합니다. 특히 마이그레이션 중에 기본값이나 우리가 제공한 것 이상의 사용자 정의 캐스팅 규칙을 사용한 경우에 더욱 그렇습니다. 그렇지 않은 경우 이 도구를 실행할 필요는 없습니다.
이 도구는 비교를 실행하기 위한 몇 가지 플래그를 포함합니다:
Usage:
dbcmp [flags]
Flags:
--exclude strings exclude tables from comparison, takes comma-separated values.
--include strings include only matching tables for comparison, takes comma-separated values.
-h, --help help for dbcmp
--source string source database dsn
--target string target database dsn
-v, --version version for dbcmp
참고
--exclude 와 --include 플래그는 상호 배타적이며 함께 사용할 수 없습니다.
이 경우 다음 명령을 간단히 실행할 수 있습니다:
dbcmp --source "${MYSQL_DSN}" --target "${POSTGRES_DSN} " --include="posts,users"
명령 예시: dbcmp --source "user:password@tcp(address:3306)/db_name --target "postgres://user:password@address:5432/db_name
참고
POSTGRES_DSN 은 postgres:// 접두사로 시작해야 합니다. 이렇게 하면 dbcmp 가 데이터베이스에 연결할 때 사용할 드라이버를 결정합니다.
우리가 제외하는 또 다른 것은 db_migrations 테이블로, 작은 차이(단일 마이그레이션 이름의 오타)가 있어 diff가 생성됩니다. morph와 공식 mattermost 소스를 사용하여 PostgreSQL 스키마를 생성했기 때문에 걱정 없이 안전하게 건너뛸 수 있습니다. 반면에 systems 테이블은 일부 마이그레이션 중에 추가 키가 추가된 경우 추가 diff를 포함할 수 있습니다. 문제가 발생하면 systems 테이블을 제외하고 systems 테이블의 데이터 크기가 상대적으로 작기 때문에 수동 비교를 수행하는 것을 고려하세요.
참고
마이그레이션 중에 remove-null-characters 변환 함수를 사용하고 MySQL 데이터베이스에 0x00 바이트 시퀀스가 있었다면, 해당 테이블은 비교 단계에서 차이가 있을 것입니다.
검색 경로 복원#
pgloader 설정 파일(예: migration.load)을 자세히 살펴보면 데이터베이스 사용자의 search_path 가 public 으로 설정되어 있음을 알 수 있습니다. 이것은 Mattermost 애플리케이션의 유일한 요구 사항입니다. 그러나 검색 경로에 다른 스키마를 포함해야 하는 경우 특정 요구 사항을 충족하도록 search_path 를 적절히 수정해야 합니다.
플러그인 마이그레이션#
플러그인 측면에서는 위에서 수행한 것과 다른 접근 방식을 취할 것입니다. 이번에는 morph 도구를 사용하여 테이블과 인덱스를 생성하지 않을 것입니다. 대신 pgloader 를 사용하여 테이블을 생성할 것입니다. 이렇게 하는 이유는 협업 플레이북과 보드가 SQL 쿼리를 촉진하기 위해 애플리케이션 로직을 활용하기 때문입니다. 하지만 이 시점에서는 어떤 수준의 애플리케이션도 사용하고 싶지 않습니다.
협업 플레이북#
플레이북용으로 제공된 pgloader 설정은 v1.38.1 을 기반으로 하며, 마이그레이션을 수행하려면 플러그인이 최소 v1.36.0 이상이어야 합니다.
마이그레이션 준비가 완료되면 pgloader 를 실행하여 스키마 와 데이터 마이그레이션을 시작할 수 있습니다.
데이터 마이그레이션의 기준으로 다음 구성을 사용하세요:
LOAD DATABASE
FROM mysql://{{ .mysql_user }}:{{ .mysql_password }}@{{ .mysql_address }}/{{ .source_db }}
INTO pgsql://{{ .pg_user }}:{{ .pg_password }}@{{ .postgres_address }}/{{ .target_db }}
WITH include drop, create tables, create indexes, no foreign keys,
workers = 8, concurrency = 1,
multiple readers per thread, rows per range = 50000,
preserve index names
SET PostgreSQL PARAMETERS
maintenance_work_mem to '128MB',
work_mem to '12MB'
SET MySQL PARAMETERS
net_read_timeout = '120',
net_write_timeout = '120'
CAST column IR_ChannelAction.ActionType to text drop typemod,
column IR_ChannelAction.TriggerType to text drop typemod,
column IR_Incident.ChecklistsJSON to "json" drop typemod
INCLUDING ONLY TABLE NAMES MATCHING
~/IR_/
BEFORE LOAD DO
$$ ALTER SCHEMA public RENAME TO {{ .source_db }}; $$
AFTER LOAD DO
$$ ALTER TABLE {{ .source_db }}.IR_ChannelAction ALTER COLUMN ActionType TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_ChannelAction ALTER COLUMN TriggerType TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ReminderMessageTemplate TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ReminderMessageTemplate SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedInvitedUserIDs TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedInvitedUserIDs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnCreationURLs TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnCreationURLs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedInvitedGroupIDs TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedInvitedGroupIDs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN Retrospective TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN Retrospective SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN MessageOnJoin TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN MessageOnJoin SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN CategoryName TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN CategoryName SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedBroadcastChannelIds TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedBroadcastChannelIds SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ChannelIDToRootID TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ChannelIDToRootID SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ReminderMessageTemplate TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ReminderMessageTemplate SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedInvitedUserIDs TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedInvitedUserIDs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnCreationURLs TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnCreationURLs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedInvitedGroupIDs TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedInvitedGroupIDs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN MessageOnJoin TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN MessageOnJoin SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN RetrospectiveTemplate TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN RetrospectiveTemplate SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedSignalAnyKeywords TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedSignalAnyKeywords SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN CategoryName TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN CategoryName SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ChecklistsJSON TYPE JSON USING ChecklistsJSON::JSON; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedBroadcastChannelIds TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedBroadcastChannelIds SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN RunSummaryTemplate TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN RunSummaryTemplate SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ChannelNameTemplate TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ChannelNameTemplate SET DEFAULT ''::text; $$,
$$ ALTER TABLE {{ .source_db }}.IR_PlaybookMember ALTER COLUMN Roles TYPE varchar(65536); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Category_Item ADD CONSTRAINT ir_category_item_categoryid FOREIGN KEY (CategoryId) REFERENCES {{ .source_db }}.IR_Category(Id); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Metric ADD CONSTRAINT ir_metric_metricconfigid FOREIGN KEY (MetricConfigId) REFERENCES {{ .source_db }}.IR_MetricConfig(Id); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Metric ADD CONSTRAINT ir_metric_incidentid FOREIGN KEY (IncidentId) REFERENCES {{ .source_db }}.IR_Incident(Id); $$,
$$ ALTER TABLE {{ .source_db }}.IR_MetricConfig ADD CONSTRAINT ir_metricconfig_playbookid FOREIGN KEY (PlaybookId) REFERENCES {{ .source_db }}.IR_Playbook(Id); $$,
$$ ALTER TABLE {{ .source_db }}.IR_PlaybookAutoFollow ADD CONSTRAINT ir_playbookautofollow_playbookid FOREIGN KEY (PlaybookId) REFERENCES {{ .source_db }}.IR_Playbook(Id); $$,
$$ ALTER TABLE {{ .source_db }}.IR_PlaybookMember ADD CONSTRAINT ir_playbookmember_playbookid FOREIGN KEY (PlaybookId) REFERENCES {{ .source_db }}.IR_Playbook(Id); $$,
$$ ALTER TABLE {{ .source_db }}.IR_Run_Participants ADD CONSTRAINT ir_run_participants_incidentid FOREIGN KEY (IncidentId) REFERENCES {{ .source_db }}.IR_Incident(Id); $$,
$$ ALTER TABLE {{ .source_db }}.IR_StatusPosts ADD CONSTRAINT ir_statusposts_incidentid FOREIGN KEY (IncidentId) REFERENCES {{ .source_db }}.IR_Incident(Id); $$,
$$ ALTER TABLE {{ .source_db }}.IR_TimelineEvent ADD CONSTRAINT ir_timelineevent_incidentid FOREIGN KEY (IncidentId) REFERENCES {{ .source_db }}.IR_Incident(Id); $$,
$$ CREATE UNIQUE INDEX IF NOT EXISTS ir_playbookmember_playbookid_memberid_key on {{ .source_db }}.IR_PlaybookMember(PlaybookId,MemberId); $$,
$$ CREATE INDEX IF NOT EXISTS ir_statusposts_incidentid_postid_key on {{ .source_db }}.IR_StatusPosts(IncidentId,PostId); $$,
$$ CREATE INDEX IF NOT EXISTS ir_playbookmember_playbookid on {{ .source_db }}.IR_PlaybookMember(PlaybookId); $$,
$$ ALTER SCHEMA {{ .source_db }} RENAME TO public; $$,
$$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
$$ ALTER USER {{ .pg_user }} SET SEARCH_PATH TO 'public'; $$;
pgloader playbooks.load > playbooks_migration.log
Focalboard#
v9.0 부터 Boards는 Focalboard 플러그인으로 완전히 커뮤니티 지원으로 전환됩니다. 따라서 이 가이드는 스키마의 v7.10.x 버전만 다룹니다. 공식 발표.
마이그레이션 준비가 완료되면 pgloader 를 실행하여 스키마 와 데이터 마이그레이션을 시작할 수 있습니다.
데이터 마이그레이션의 기준으로 다음 구성을 사용하세요:
LOAD DATABASE
FROM mysql://{{ .mysql_user }}:{{ .mysql_password }}@{{ .mysql_address }}/{{ .source_db }}
INTO pgsql://{{ .pg_user }}:{{ .pg_password }}@{{ .postgres_address }}/{{ .target_db }}
WITH include drop, create tables, create indexes, reset sequences,
workers = 8, concurrency = 1,
multiple readers per thread, rows per range = 50000,
preserve index names
SET PostgreSQL PARAMETERS
maintenance_work_mem to '128MB',
work_mem to '12MB'
SET MySQL PARAMETERS
net_read_timeout = '120',
net_write_timeout = '120'
CAST column focalboard_blocks.fields to "json" drop typemod,
column focalboard_blocks_history.fields to "json" drop typemod,
column focalboard_schema_migrations.name to "varchar" drop typemod,
column focalboard_sessions.props to "json" drop typemod,
column focalboard_teams.settings to "json" drop typemod,
column focalboard_users.props to "json" drop typemod,
type int when (= precision 11) to int4 drop typemod,
type json to jsonb drop typemod
INCLUDING ONLY TABLE NAMES MATCHING
~/focalboard/
BEFORE LOAD DO
$$ ALTER SCHEMA public RENAME TO {{ .source_db }}; $$
AFTER LOAD DO
$$ UPDATE {{ .source_db }}.focalboard_blocks SET "fields" = '{}'::json WHERE "fields"::text = ''; $$,
$$ UPDATE {{ .source_db }}.focalboard_blocks_history SET "fields" = '{}'::json WHERE "fields"::text = ''; $$,
$$ UPDATE {{ .source_db }}.focalboard_sessions SET "props" = '{}'::json WHERE "props"::text = ''; $$,
$$ UPDATE {{ .source_db }}.focalboard_teams SET "settings" = '{}'::json WHERE "settings"::text = ''; $$,
$$ UPDATE {{ .source_db }}.focalboard_users SET "props" = '{}'::json WHERE "props"::text = ''; $$,
$$ ALTER SCHEMA {{ .source_db }} RENAME TO public; $$,
$$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
$$ ALTER USER {{ .pg_user }} SET SEARCH_PATH TO 'public'; $$;
pgloader focalboard.load > focalboard_migration.log
Calls#
Mattermost v9.9 이상 또는 Calls 플러그인 v0.27 이상을 실행 중인 경우 플러그인 데이터 마이그레이션을 선택할 수 있습니다. Boards와 Playbooks 마이그레이션과 유사한 접근 방식을 취하고 pgloader가 테이블을 생성하도록 할 것입니다.
마이그레이션 준비가 완료되면 pgloader 를 실행하여 스키마 와 데이터 마이그레이션을 시작할 수 있습니다.
데이터 마이그레이션의 기준으로 다음 구성을 사용하세요:
LOAD DATABASE
FROM mysql://{{ .mysql_user }}:{{ .mysql_password }}@{{ .mysql_address }}/{{ .source_db }}
INTO pgsql://{{ .pg_user }}:{{ .pg_password }}@{{ .postgres_address }}/{{ .target_db }}
WITH include drop, create tables, create indexes, reset sequences,
workers = 8, concurrency = 1,
multiple readers per thread, rows per range = 50000,
preserve index names
SET PostgreSQL PARAMETERS
maintenance_work_mem to '128MB',
work_mem to '12MB'
SET MySQL PARAMETERS
net_read_timeout = '120',
net_write_timeout = '120'
CAST type json to jsonb drop typemod
INCLUDING ONLY TABLE NAMES MATCHING
~/calls/
BEFORE LOAD DO
$$ ALTER SCHEMA public RENAME TO {{ .source_db }}; $$
AFTER LOAD DO
$$ ALTER SCHEMA {{ .source_db }} RENAME TO public; $$,
$$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
$$ ALTER USER {{ .pg_user }} SET SEARCH_PATH TO 'public'; $$;
pgloader calls.load > calls_migration.log