MySQL에서 PostgreSQL로 수동 마이그레이션
Mattermost v12.0요약
MySQL 데이터베이스를 PostgreSQL로 수동으로 마이그레이션하는 절차는 다음과 같습니다. 협업 플레이북 및 Boards 마이그레이션에 대한 자세한 내용은 플러그인 마이그레이션 섹션을 참조하세요. 수동 업그레이드가 올바른 방법인지 확실하지 않으십니까?
사용 가능 플랜: Entry, Professional, Enterprise, Enterprise Advanced
MySQL 데이터베이스를 PostgreSQL로 수동으로 마이그레이션하는 절차는 다음과 같습니다.
협업 플레이북 및 Boards 마이그레이션에 대한 자세한 내용은 플러그인 마이그레이션 섹션을 참조하세요.
수동 업그레이드가 올바른 방법인지 확실하지 않으십니까? Mattermost 배포 환경에 맞는 안내가 필요한 Mattermost 고객은 Mattermost 전문가에게 문의할 수 있습니다.
도구 권장 사항#
Postgres로 수동 마이그레이션하려는 경우 마이그레이션 과정에 다음 도구를 권장합니다.
이 페이지에서는 각 도구를 설치하는 방법과 이어서 데이터베이스 마이그레이션을 진행하는 방법을 안내합니다.
필요한 도구를 설치한 후 시스템 요구 사항 및 구성 문서를 검토하고, 마이그레이션을 준비하기 위해 마이그레이션을 시작하기 전에 무엇이 필요한지 파악하세요.
대상 데이터베이스를 준비하는 것으로 마이그레이션을 시작한 다음 데이터를 마이그레이션하고, 모든 마이그레이션 후 단계를 완료하세요.
협업 플레이북 및 보드 마이그레이션에 대한 자세한 내용은 플러그인 마이그레이션 문서를 참조하세요.
pgloader#
pgloader 도구를 사용하여 MySQL에서 PostgreSQL로 데이터를 마이그레이션합니다.
pgloader 설치#
pgloader를 설치하려면 공식 설치 가이드를 참조하세요.
MySQL v8을 사용하는 경우: pgloader 컴파일 바이너리의 알려진 버그 때문에 소스에서 pgloader를 컴파일해야 합니다. 이 단계에 따라 소스에서 빌드하세요.
또는 pgloader를 설치하거나 빌드하지 않고 mattermost-pgloader Docker 이미지를 사용할 수도 있습니다. mattermost-pgloader-docker 리포지터리에서 이미지를 직접 빌드할 수도 있습니다. 자세한 내용은 아래 문서를 참조하세요.
pgloader 사용#
Docker 이미지 pull 및 pgloader 검증#
수동 마이그레이션을 위해 다음 명령을 실행하여 mattermost-pgloader 이미지를 pull하고 pgloader가 올바르게 작동하는지 확인합니다.
docker run -it --rm -v $(pwd):/home/migration mattermost/mattermost-pgloader:latest pgloader --version
이 명령은 mattermost/mattermost-pgloader:latest 이미지를 pull하고 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같은 pgloader 설정을 세부 조정할 수 있으며,concurrency가1보다 크게 설정되어 있다면 특히rows per range를 조정할 수 있습니다. 이러한 조정은 사용 가능한 시스템 리소스에 맞게 리소스 활용을 최적화하는 데 도움이 됩니다. 자세한 내용은 pgloader 문서를 참조하세요. - 마이그레이션 과정, 특히 대규모 데이터셋을 처리할 때는 충분한 처리 성능을 갖춘 멀티코어 프로세서를 권장합니다.
- MySQL 및 PostgreSQL 데이터베이스와 마이그레이션 과정에서 생성되는 임시 파일을 모두 저장할 수 있도록 디스크 공간을 충분히 확보하세요. 필요한 디스크 공간은 마이그레이션할 데이터베이스의 크기에 따라 달라집니다.
- 마이그레이션 시간을 더 줄이려면 마이그레이션을 시작하기 전에 대상 PostgreSQL 데이터베이스의 인덱스를 수동으로 삭제할 수 있습니다. 이 방법은 데이터를 삽입하는 동안 인덱스를 빌드하는 오버헤드를 줄여 마이그레이션을 가속화할 수 있습니다.
수동 마이그레이션 전#
이 가이드는 v7.1 ESR 이상의 스키마가 필요합니다. 따라서 이전 버전을 사용 중이며 마이그레이션을 계획하고 있다면 Mattermost 서버를 최소 v7.1로 업데이트하세요. 자세한 내용은 연장 지원 릴리즈 문서를 참조하세요.
- MySQL 데이터를 백업합니다.
- Mattermost 버전을 확인합니다. 자세한 내용은 정보 모달을 참조하세요.
- 마이그레이션 기간을 예약합니다. 이 과정에서는 마이그레이션하는 동안 Mattermost 서버를 중지해야 합니다.
- 스키마 간 데이터 호환성을 확인하려면 schema-diffs 섹션을 참조하세요.
- 데이터베이스와 사용자를 생성하여 PostgreSQL 환경을 준비합니다. 자세한 내용은 데이터베이스 문서를 참조하세요.
- PostgreSQL의 최신 버전에서는 새로 생성된 사용자에게
public스키마에 대한 접근 권한이 없습니다.GRANT ALL ON SCHEMA public to mmuser를 실행하여 접근 권한을 명시적으로 부여해야 합니다.
스키마 차이#
수동 마이그레이션 전에, 두 스키마의 차이로 인해 오류 없이 마이그레이션하려면 몇 가지 수동 단계가 필요할 수 있습니다.
텍스트에서 가변 문자로#
Mattermost MySQL 스키마는 여러 테이블에서 PostgreSQL 스키마의 varchar 표현 대신 text 칼럼 유형을 사용하므로, 크기가 PostgreSQL 스키마의 제한 내에서 일관되는지 확인하는 것을 권장합니다.
삭제하거나 업데이트해야 할 항목이 있는지 확인할 수 있습니다. 예를 들어 Audits 테이블의 Action 칼럼에서 확인하려면 다음을 실행합니다.
SELECT FROM mattermost.Audits where LENGTH(Action) > 512;
다음 표는 추가적인 영향 없이 진행할 수 있는 삭제 또는 업데이트를 보여 줍니다.
| 테이블 | 칼럼 | 데이터 유형 변환 | 삭제 시 결과 |
|---|---|---|---|
| Audits | Action | text -> varchar(512) | 애플리케이션 작동 방식에 부작용이 없습니다 (영향을 받는 행을 삭제해야 합니다). |
| Audits | ExtraInfo | text -> varchar(1024) | 애플리케이션 작동 방식에 부작용이 없습니다 (영향을 받는 행을 삭제해야 합니다). |
| ClusterDiscovery | HostName | text -> varchar(512) | 애플리케이션 작동 방식에 부작용이 없습니다 (영향을 받는 행을 삭제해야 합니다). |
| Commands | IconURL | text -> varchar(1024) | 필드를 삭제하거나 새 URL로 업데이트할 수 있습니다. |
| Commands | AutoCompleteDesc | text -> varchar(1024) | 필드를 삭제하거나 다시 작성할 수 있습니다. |
| Commands | AutoCompleteHint | text -> varchar(1024) | 필드를 삭제하거나 다시 작성할 수 있습니다. |
| RemoteClusters | Topics | text -> varchar(512) | 필드를 제거할 수 있습니다. |
| Systems | Value | text -> varchar(1024) | 극히 드문 경우이며, 이상적으로는 발생하지 않아야 합니다. |
다음 표는 스키마가 다를 수 있고 PostgreSQL 스키마의 데이터 크기 제약으로 인해 오류가 발생할 수 있는 여러 경우를 보여 줍니다. 각 테이블과 행은 개별적으로 검사해야 하므로 삭제 시 발생할 수 있는 결과를 함께 적었습니다.
커뮤니티에서 LinkMetadata 및 FileInfo 테이블에 오버플로가 발생했다는 보고가 여러 건 접수되었으므로 이 테이블을 특히 확인하기를 권장합니다. MySQL 스키마의 데이터가 이러한 제한을 초과하는지 확인하세요.
| 테이블 | 칼럼 | 데이터 유형 변환 | 삭제 시 결과 |
|---|---|---|---|
| Compliances | Keywords | text -> varchar(512) | 컴플라이언스 필터를 업데이트해야 합니다. |
| Compliances | Emails | text -> varchar(1024) | 컴플라이언스 필터를 업데이트해야 합니다. |
| FileInfo | Path | text -> varchar(512) | 이 파일에 대한 이전 링크가 작동하지 않습니다 (영향을 받는 행을 삭제해야 합니다). |
| FileInfo | ThumbnailPath | text -> varchar(512) | 이 파일에 대한 이전 링크가 작동하지 않습니다 (영향을 받는 행을 삭제해야 합니다). |
| FileInfo | PreviewPath | text -> varchar(512) | 이 파일에 대한 이전 링크가 작동하지 않습니다 (영향을 받는 행을 삭제해야 합니다). |
| FileInfo | Name | text -> varchar(256) | 이 파일에 대한 이전 링크가 작동하지 않습니다 (영향을 받는 행을 삭제해야 합니다). |
| FileInfo | MimeType | text -> varchar(256) | 이 파일에 대한 이전 링크가 작동하지 않습니다 (영향을 받는 행을 삭제해야 합니다). |
| LinkMetadata | URL | text -> varchar(2048) | 이 파일에 대한 이전 링크가 작동하지 않습니다 (영향을 받는 행을 삭제해야 합니다). |
| RemoteClusters | SiteURL | text -> varchar(512) | 이전 원격 클러스터가 제거됩니다 (영향을 받는 행을 삭제해야 합니다). |
| Sessions | DeviceId | text -> varchar(512) | 해당 기기의 사용자가 로그아웃됩니다 (영향을 받는 행을 삭제해야 합니다). |
| UploadSessions | FileName | text -> varchar(256) | 업로드 세션이 손실됩니다 (영향을 받는 행을 삭제해야 합니다). |
| UploadSessions | Path | text -> varchar(512) | 업로드 세션이 손실됩니다 (영향을 받는 행을 삭제해야 합니다). |
전체 텍스트 인덱스#
Posts 및 FileInfo 테이블의 일부 단어가 전체 텍스트 검색 인덱싱의 최대 토큰 길이 제한을 초과할 수 있습니다. 이 경우 PostgreSQL 스키마에서 idx_posts_message_txt 및 idx_fileinfo_content_txt 인덱스를 삭제하고, 마이그레이션 후 다음 쿼리를 실행하여 이 인덱스를 다시 생성합니다.
마이그레이션 중 오류를 방지하기 위해 다음 쿼리를 포함했습니다.
DROP INDEX IF EXISTS {}.idx_posts_message_txt;
DROP INDEX IF EXISTS {}.idx_fileinfo_content_txt;
지원되지 않는 유니코드 시퀀스#
PostgreSQL에서 허용되지 않는 특정 유니코드 시퀀스가 있으며, 바로 \u0000입니다. 이 시퀀스가 MySQL 데이터베이스의 여러 테이블에 걸쳐 여러 행에 나타날 수 있습니다. 이 경우 마이그레이션 중 다음과 같은 오류가 발생할 가능성이 높습니다. unsupported Unicode escape sequence: \u0000 cannot be converted to text.. 이를 방지하려면 마이그레이션을 시작하기 전에 데이터를 정리하는 것이 좋습니다. 다음 쿼리를 사용하여 \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;
마이그레이션 중 invalid byte sequence for encoding '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 ":@tcp(:3306)/?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s&multiStatements=true" /opt/mattermost/config/config.json --local
여기서 , , , 을 사용자 환경 값으로 바꿉니다. 이 명령을 실행할 때는 반드시 --local을 사용하세요. 첫 번째 매개변수(, )는 구성이 저장된 데이터베이스이고, 두 번째 매개변수(, )는 구성을 저장할 파일입니다.
구성 파일에서 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를 실행하여 데이터 마이그레이션을 시작할 수 있습니다.
아래 예시에서는 두 데이터베이스의 호스트가 같은 인스턴스에 있다고 가정합니다. 서로 다른 머신에 있다면 주소를 그에 맞게 업데이트하세요. 또한 pgloader를 --dry-run 플래그와 함께 실행하기만 해도 .load 파일을 테스트할 수 있습니다. 예를 들어 pgloader --dry-run migration.load 명령입니다.
데이터 마이그레이션의 기준 구성으로 다음을 사용하세요.
LOAD DATABASE
FROM mysql://{}:{}@{}/{}
INTO pgsql://{}:{}@{}/{}
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 ~, ~<focalboard>, 'schema_migrations', 'db_migrations', 'db_lock',
'Configurations', 'ConfigurationFiles', 'db_config_migrations', 'calls'
BEFORE LOAD DO
$$ ALTER SCHEMA public RENAME TO {}; $$,
$$ TRUNCATE TABLE {}.systems; $$,
$$ DROP INDEX IF EXISTS {}.idx_posts_message_txt; $$,
$$ DROP INDEX IF EXISTS {}.idx_fileinfo_content_txt; $$
AFTER LOAD DO
$$ UPDATE {}.db_migrations set name='add_createat_to_teamembers' where version=92; $$,
$$ ALTER SCHEMA {} RENAME TO public; $$,
$$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
$$ ALTER 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() 함수를 사용할 수 있습니다.
substring 길이 1000000으로 시작하고, 오류가 계속되면 점차 줄이세요.
-- Start with 1000000
CREATE INDEX IF NOT EXISTS idx_fileinfo_content_txt ON public.fileinfo USING gin(to_tsvector('english', substring(content,0,1000000)));
-- If that fails, try 800000
CREATE INDEX IF NOT EXISTS idx_fileinfo_content_txt ON public.fileinfo USING gin(to_tsvector('english', substring(content,0,800000)));
-- Continue decreasing as needed (e.g., 600000, 500000) until the index creates successfully
데이터 비교#
데이터베이스 비교를 간소화하기 위해 dbcmp라는 내부 도구를 개발했습니다. 이 도구는 두 데이터베이스의 모든 테이블을 확인하고 스키마의 차이를 보고합니다. 다만 dbcmp는 개별 행을 비교하지 않습니다. 대신 지정한 페이지 크기를 기준으로 체크섬 값을 계산하여 비교합니다. 따라서 행 수준의 diff를 생성할 수 없습니다.
특히 마이그레이션 중 (기본값이나 Mattermost에서 제공한 규칙 이외의) 사용자 지정 캐스팅 규칙을 사용한 경우, 데이터 무결성을 확인하는 추가 검사로 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가 발생합니다. PostgreSQL 스키마를 morph와 공식 mattermost 소스로 생성했으므로 문제 없이 건너뛸 수 있습니다. 한편 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://{}:{}@{}/{}
INTO pgsql://{}:{}@{}/{}
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 {}; $$
AFTER LOAD DO
$$ ALTER TABLE {}.IR_ChannelAction ALTER COLUMN ActionType TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_ChannelAction ALTER COLUMN TriggerType TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ReminderMessageTemplate TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ReminderMessageTemplate SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ConcatenatedInvitedUserIDs TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ConcatenatedInvitedUserIDs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnCreationURLs TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnCreationURLs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ConcatenatedInvitedGroupIDs TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ConcatenatedInvitedGroupIDs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN Retrospective TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN Retrospective SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN MessageOnJoin TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN MessageOnJoin SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN CategoryName TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN CategoryName SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ConcatenatedBroadcastChannelIds TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ConcatenatedBroadcastChannelIds SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ChannelIDToRootID TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Incident ALTER COLUMN ChannelIDToRootID SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ReminderMessageTemplate TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ReminderMessageTemplate SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ConcatenatedInvitedUserIDs TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ConcatenatedInvitedUserIDs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnCreationURLs TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnCreationURLs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ConcatenatedInvitedGroupIDs TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ConcatenatedInvitedGroupIDs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN MessageOnJoin TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN MessageOnJoin SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN RetrospectiveTemplate TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN RetrospectiveTemplate SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ConcatenatedSignalAnyKeywords TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ConcatenatedSignalAnyKeywords SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN CategoryName TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN CategoryName SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ChecklistsJSON TYPE JSON USING ChecklistsJSON::JSON; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ConcatenatedBroadcastChannelIds TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ConcatenatedBroadcastChannelIds SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN RunSummaryTemplate TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN RunSummaryTemplate SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ChannelNameTemplate TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Playbook ALTER COLUMN ChannelNameTemplate SET DEFAULT ''::text; $$,
$$ ALTER TABLE {}.IR_PlaybookMember ALTER COLUMN Roles TYPE varchar(65536); $$,
$$ ALTER TABLE {}.IR_Category_Item ADD CONSTRAINT ir_category_item_categoryid FOREIGN KEY (CategoryId) REFERENCES {}.IR_Category(Id); $$,
$$ ALTER TABLE {}.IR_Metric ADD CONSTRAINT ir_metric_metricconfigid FOREIGN KEY (MetricConfigId) REFERENCES {}.IR_MetricConfig(Id); $$,
$$ ALTER TABLE {}.IR_Metric ADD CONSTRAINT ir_metric_incidentid FOREIGN KEY (IncidentId) REFERENCES {}.IR_Incident(Id); $$,
$$ ALTER TABLE {}.IR_MetricConfig ADD CONSTRAINT ir_metricconfig_playbookid FOREIGN KEY (PlaybookId) REFERENCES {}.IR_Playbook(Id); $$,
$$ ALTER TABLE {}.IR_PlaybookAutoFollow ADD CONSTRAINT ir_playbookautofollow_playbookid FOREIGN KEY (PlaybookId) REFERENCES {}.IR_Playbook(Id); $$,
$$ ALTER TABLE {}.IR_PlaybookMember ADD CONSTRAINT ir_playbookmember_playbookid FOREIGN KEY (PlaybookId) REFERENCES {}.IR_Playbook(Id); $$,
$$ ALTER TABLE {}.IR_Run_Participants ADD CONSTRAINT ir_run_participants_incidentid FOREIGN KEY (IncidentId) REFERENCES {}.IR_Incident(Id); $$,
$$ ALTER TABLE {}.IR_StatusPosts ADD CONSTRAINT ir_statusposts_incidentid FOREIGN KEY (IncidentId) REFERENCES {}.IR_Incident(Id); $$,
$$ ALTER TABLE {}.IR_TimelineEvent ADD CONSTRAINT ir_timelineevent_incidentid FOREIGN KEY (IncidentId) REFERENCES {}.IR_Incident(Id); $$,
$$ CREATE UNIQUE INDEX IF NOT EXISTS ir_playbookmember_playbookid_memberid_key on {}.IR_PlaybookMember(PlaybookId,MemberId); $$,
$$ CREATE INDEX IF NOT EXISTS ir_statusposts_incidentid_postid_key on {}.IR_StatusPosts(IncidentId,PostId); $$,
$$ CREATE INDEX IF NOT EXISTS ir_playbookmember_playbookid on {}.IR_PlaybookMember(PlaybookId); $$,
$$ ALTER SCHEMA {} RENAME TO public; $$,
$$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
$$ ALTER 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://{}:{}@{}/{}
INTO pgsql://{}:{}@{}/{}
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 {}; $$
AFTER LOAD DO
$$ UPDATE {}.focalboard_blocks SET "fields" = '{}'::json WHERE "fields"::text = ''; $$,
$$ UPDATE {}.focalboard_blocks_history SET "fields" = '{}'::json WHERE "fields"::text = ''; $$,
$$ UPDATE {}.focalboard_sessions SET "props" = '{}'::json WHERE "props"::text = ''; $$,
$$ UPDATE {}.focalboard_teams SET "settings" = '{}'::json WHERE "settings"::text = ''; $$,
$$ UPDATE {}.focalboard_users SET "props" = '{}'::json WHERE "props"::text = ''; $$,
$$ ALTER SCHEMA {} RENAME TO public; $$,
$$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
$$ ALTER 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://{}:{}@{}/{}
INTO pgsql://{}:{}@{}/{}
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 {}; $$
AFTER LOAD DO
$$ ALTER SCHEMA {} RENAME TO public; $$,
$$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
$$ ALTER USER {} SET SEARCH_PATH TO 'public'; $$;
pgloader calls.load > calls_migration.log