PHPBB에서 Discourse로 마이그레이션 속도 최적화

안녕하세요, 2004년부터 phpbb를 사용해 왔습니다. 그래서 상당한 규모의 포럼을 이전하고 있습니다. 현재 이틀째 진행 중이며, 303139/2167314 (14%)까지 완료했고, 분당 446건을 처리하고 있습니다. 속도를 높이는 방법이 있을까요? Digital Ocean을 사용 중이며 CPU 사용률은 4%에 불과합니다. Redis가 계속 타임아웃되고 있습니다. 첨부 파일은 꺼둔 상태입니다.

이 문제를 좁혀 파악하는 데 도움이 될 몇 가지 유용한 세부 정보가 있습니다:

  • Droplet 크기: RAM, vCPU 수, 디스크 유형/크기
  • 실행 중인 정확한 import 명령어
  • 기밀 정보는 제거한 상태의 app.yml 리소스 관련 설정, 특히:
    • UNICORN_WORKERS
    • db_shared_buffers
    • db_work_mem
  • import가 표준 Discourse Docker 컨테이너 내부에서 실행되는지 여부
  • Redis 타임아웃 메시지/로그 발췌
  • 데이터베이스가 동일한 컨테이너 내부의 로컬인지, 아니면 외부인지 여부

참고할 점이 하나 있습니다. phpBB import기는 주로 웹 워커를 통해 처리되지 않으므로 UNICORN_WORKERS를 늘려도 import 속도가 크게 빨라지지 않을 가능성이 높습니다. 이 값을 늘리면 RAM 사용량이 더 증가할 수 있습니다. PostgreSQL 메모리 설정과 디스크 I/O가 더 중요한 요인이 될 가능성이 큽니다.

CPU 사용량이 약 4% 수준에 불과하므로, CPU 부족보다는 I/O 병목, 데이터베이스 병목, 또는 Redis 대기 상태일 수 있습니다. Redis 타임아웃 문제는 별도로 조사해 볼 가치가 있습니다.

image

업데이트된 로그
script/import_scripts/phpbb3.rb:15:in ‘’
302570 / 2167314 ( 14.0%) [448 items/min] 게시글 304683 생성 중 예외 발생. 건너뜁니다.
1.0초 대기
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection/buffered_io.rb:214:in ‘block in RedisClient::RubyConnection::BufferedIO#fill_buffer’
internal:kernel:168:in ‘Kernel#loop’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection/buffered_io.rb:197:in ‘RedisClient::RubyConnection::BufferedIO#fill_buffer’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection/buffered_io.rb:187:in ‘RedisClient::RubyConnection::BufferedIO#ensure_remaining’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection/buffered_io.rb:152:in ‘RedisClient::RubyConnection::BufferedIO#getbyte’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection/resp3.rb:113:in ‘RedisClient::RESP3.parse’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection/resp3.rb:50:in ‘RedisClient::RESP3.load’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection.rb:97:in ‘RedisClient::RubyConnection#read’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/connection_mixin.rb:37:in ‘RedisClient::ConnectionMixin#call’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client.rb:374:in ‘block (2 levels) in RedisClient#call_v’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/middlewares.rb:16:in ‘RedisClient::BasicMiddleware#call’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client.rb:373:in ‘block in RedisClient#call_v’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client.rb:781:in ‘RedisClient#ensure_connected’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client.rb:372:in ‘RedisClient#call_v’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-5.4.0/lib/redis/client.rb:90:in ‘Redis::Client#call_v’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/rack-mini-profiler-4.0.1/lib/mini_profiler/profiling_methods.rb:90:in ‘block in Redis::Client#profile_method’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-5.4.0/lib/redis.rb:152:in ‘block in Redis#send_command’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-5.4.0/lib/redis.rb:151:in ‘Monitor#synchronize’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-5.4.0/lib/redis.rb:151:in ‘Redis#send_command’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-5.4.0/lib/redis/commands/scripting.rb:110:in ‘Redis::Commands::Scripting#_eval’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-5.4.0/lib/redis/commands/scripting.rb:97:in ‘Redis::Commands::Scripting#evalsha’
/var/www/discourse/lib/discourse_redis.rb:38:in ‘Kernel#public_send’
/var/www/discourse/lib/discourse_redis.rb:38:in ‘block in DiscourseRedis#method_missing’
/var/www/discourse/lib/discourse_redis.rb:29:in ‘DiscourseRedis.ignore_readonly’
/var/www/discourse/lib/discourse_redis.rb:38:in ‘DiscourseRedis#method_missing’
/var/www/discourse/lib/discourse_redis.rb:270:in ‘DiscourseRedis::EvalHelper#eval’
/var/www/discourse/lib/distributed_mutex.rb:82:in ‘DistributedMutex#get_lock’
/var/www/discourse/lib/distributed_mutex.rb:50:in ‘block in DistributedMutex#synchronize’
/var/www/discourse/lib/distributed_mutex.rb:49:in ‘Thread::Mutex#synchronize’
/var/www/discourse/lib/distributed_mutex.rb:49:in ‘DistributedMutex#synchronize’
/var/www/discourse/lib/distributed_mutex.rb:34:in ‘DistributedMutex.synchronize’
/var/www/discourse/lib/post_creator.rb:410:in ‘PostCreator#transaction’
/var/www/discourse/lib/post_creator.rb:200:in ‘PostCreator#create’
/var/www/discourse/script/import_scripts/base.rb:611:in ‘ImportScripts::Base#create_post’
/var/www/discourse/script/import_scripts/base.rb:555:in ‘block in ImportScripts::Base#create_posts’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/rack-mini-profiler-4.0.1/lib/patches/db/mysql2/alias_method.rb:8:in ‘Mysql2::Result#each’
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/rack-mini-profiler-4.0.1/lib/patches/db/mysql2/alias_method.rb:8:in ‘Mysql2::Result#each’
/var/www/discourse/script/import_scripts/base.rb:542:in ‘ImportScripts::Base#create_posts’
/var/www/discourse/script/import_scripts/phpbb3/importer.rb:212:in ‘block in ImportScripts::PhpBB3::Importer#import_posts’
/var/www/discourse/script/import_scripts/base.rb:943:in ‘block in ImportScripts::Base#batches’
internal:kernel:168:in ‘Kernel#loop’
/var/www/discourse/script/import_scripts/base.rb:942:in ‘ImportScripts::Base#batches’
/var/www/discourse/script/import_scripts/phpbb3/importer.rb:293:in ‘ImportScripts::PhpBB3::Importer#batches’
/var/www/discourse/script/import_scripts/phpbb3/importer.rb:208:in ‘ImportScripts::PhpBB3::Importer#import_posts’
/var/www/discourse/script/import_scripts/phpbb3/importer.rb:38:in ‘ImportScripts::PhpBB3::Importer#execute’
/var/www/discourse/script/import_scripts/base.rb:47:in ‘ImportScripts::Base#perform’
/var/www/discourse/script/import_scripts/phpbb3/importer.rb:22:in ‘ImportScripts::PhpBB3::Importer#perform’
script/import_scripts/phpbb3.rb:35:in ‘module:PhpBB3
script/import_scripts/phpbb3.rb:16:in ‘module:ImportScripts
script/import_scripts/phpbb3.rb:15:in ‘’
333919 / 2167314 ( 15.4%) [441 items/min]

그럼에도 불구하고 작동하는 것 같으니 불평하지는 않아요.

현재까지

unicorn을 중지했습니다

첨부 파일과 아바타를 비활성화했습니다

해당 유형의 임포트 작업에 대해 드롭렛의 크기가 상당히 넉넉해 보이기 때문에, UNICORN_WORKERS가 여기서 주요 제한 요인일 것이라고는 생각하지 않습니다.

몇 가지 관찰 사항이 있습니다:

  • 임포트 전용/스테이징 컨테이너의 경우 Unicorn을 중지하는 것이 괜찮을 수 있지만, 임포터 자체의 속도를 극적으로 높여 주지는 않을 가능성이 높습니다.
  • 첨부 파일과 아바타가 비활성화되어 있으므로 느린 부분 중 두 가지가 제거되었어야 합니다. 그럼에도 불구하고 여전히 분당 약 440개 정도의 게시물만 처리되고 있다면, 병목 현상은 데이터베이스 I/O, 소스 phpBB MySQL 쿼리 측, 또는 Redis/PostgreSQL 대기 시간일 수 있습니다.
  • 다음 로그 라인을 주의 깊게 살펴볼 가치가 있습니다:

Exception while creating post 304683. Skipping.

임포트가 계속 진행되더라도, 이는 해당 게시물이 최소한 건너뛰어졌음을 의미합니다. 이 상황이 반복적으로 발생한다면 최종 마이그레이션에서 일부 게시물이 누락될 수 있습니다.

Redis 부분은 PostCreator가 게시물을 생성하는 동안 Discourse의 분산 뮤텍스 락(lock) 과정에서 발생한 1초 Redis 클라이언트 타임아웃으로 보입니다. Redis가 실제로 과부하 상태인지, 아니면 긴 임포트 작업 중에 타임아웃 설정이 지나치게 공격적으로 설정되어 있는지 확인해 보시는 것이 좋습니다.

유용한 다음 점검 항목은 다음과 같습니다:

free -h
df -h
iostat -xz 1
vmstat 1
redis-cli INFO memory
redis-cli INFO stats
redis-cli SLOWLOG GET 20

또한, phpBB MySQL 데이터베이스가 같은 드롭렛에 로컬로 존재하는지, 아니면 네트워크를 통해 읽히고 있는지도 확인해 주세요. 스택 트레이스가 mysql2를 반복하고 있으므로, 느린 소스 데이터베이스나 느린 디스크는 임포터가 대기하는 동안 CPU 사용률을 낮게 유지할 수 있습니다.

현재 속도(분당 약 441개, 333919 / 2167314)로 계산하면 약 69시간이 더 소요될 것으로 보이며, 완료될 수는 있겠지만, Redis 타임아웃 예외가 게시물의 건너뛰기를 유발하고 있는지에 대해 주로 우려가 됩니다.

UNICORN_WORKERS를 높일 것을 권하지 않습니다. 임포트 전용 실행에서는 Unicorn은 대부분 관련이 없습니다. 중요한 것은 그의 임포트가 계속 진행되고 있지만 게시물을 건너뛰고 있다는 점이며, 이것이 제가 지적하고 싶은 부분입니다.

대량 가져오기 도구는 훨씬 더 빠르지만, 설정하고 정상 작동시키기는 더 어렵습니다.

대부분의 경우 이것이 원인입니다. 인덱스를 추가하고, 뷰를 생성하며, 데이터베이스 설정을 조정하여 가져오기 속도를 높일 수 있었습니다. 나중에 데이터에서 오류를 발견해 가져오기를 다시 실행해야 하는 상황이 오면 큰 골칫거리가 될 수 있으므로, 이 부분에 시간을 좀 투자하시기를 권합니다. :grimacing:

참고로, 마이그레이션 실행 속도를 훨씬 빠르게 만들어 줄 새로운 도구도 개발하고 있습니다!

그래도 진행은 되고 있습니다!

var/www/discourse/script/import_scripts/phpbb3/importer.rb:38:in 'ImportScripts::PhpBB3::Importer#execute'
/var/www/discourse/script/import_scripts/base.rb:47:in 'ImportScripts::Base#perform'
/var/www/discourse/script/import_scripts/phpbb3/importer.rb:22:in 'ImportScripts::PhpBB3::Importer#perform'
script/import_scripts/phpbb3.rb:35:in '<module:PhpBB3>'
script/import_scripts/phpbb3.rb:16:in '<module:ImportScripts>'
script/import_scripts/phpbb3.rb:15:in ''
606484 / 2167314 ( 28.0%)  \[415 items/min\]  Exception while creating post 608597. Skipping.
Waited 1.0 seconds
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection/buffered_io.rb:214:in 'block in RedisClient::RubyConnection::BufferedIO#fill_buffer'
<internal:kernel>:168:in 'Kernel#loop'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection/buffered_io.rb:197:in 'RedisClient::RubyConnection::BufferedIO#fill_buffer'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection/buffered_io.rb:187:in 'RedisClient::RubyConnection::BufferedIO#ensure_remaining'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection/buffered_io.rb:152:in 'RedisClient::RubyConnection::BufferedIO#getbyte'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection/resp3.rb:113:in 'RedisClient::RESP3.parse'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection/resp3.rb:50:in 'RedisClient::RESP3.load'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/ruby_connection.rb:97:in 'RedisClient::RubyConnection#read'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/connection_mixin.rb:37:in 'RedisClient::ConnectionMixin#call'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client.rb:374:in 'block (2 levels) in RedisClient#call_v'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client/middlewares.rb:16:in 'RedisClient::BasicMiddleware#call'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client.rb:373:in 'block in RedisClient#call_v'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client.rb:781:in 'RedisClient#ensure_connected'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-client-0.28.0/lib/redis_client.rb:372:in 'RedisClient#call_v'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-5.4.0/lib/redis/client.rb:90:in 'Redis::Client#call_v'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/rack-mini-profiler-4.0.1/lib/mini_profiler/profiling_methods.rb:90:in 'block in Redis::Client#profile_method'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-5.4.0/lib/redis.rb:152:in 'block in Redis#send_command'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-5.4.0/lib/redis.rb:151:in 'Monitor#synchronize'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-5.4.0/lib/redis.rb:151:in 'Redis#send_command'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/redis-5.4.0/lib/redis/commands/server.rb:178:in 'Redis::Commands::Server#time'
/var/www/discourse/lib/discourse_redis.rb:38:in 'Kernel#public_send'
/var/www/discourse/lib/discourse_redis.rb:38:in 'block in DiscourseRedis#method_missing'
/var/www/discourse/lib/discourse_redis.rb:29:in 'DiscourseRedis.ignore_readonly'
/var/www/discourse/lib/discourse_redis.rb:38:in 'DiscourseRedis#method_missing'
/var/www/discourse/lib/distributed_mutex.rb:55:in 'block in DistributedMutex#synchronize'
/var/www/discourse/lib/distributed_mutex.rb:49:in 'Thread::Mutex#synchronize'
/var/www/discourse/lib/distributed_mutex.rb:49:in 'DistributedMutex#synchronize'
/var/www/discourse/lib/distributed_mutex.rb:34:in 'DistributedMutex.synchronize'
/var/www/discourse/lib/post_creator.rb:410:in 'PostCreator#transaction'
/var/www/discourse/lib/post_creator.rb:200:in 'PostCreator#create'
/var/www/discourse/script/import_scripts/base.rb:611:in 'ImportScripts::Base#create_post'
/var/www/discourse/script/import_scripts/base.rb:555:in 'block in ImportScripts::Base#create_posts'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/rack-mini-profiler-4.0.1/lib/patches/db/mysql2/alias_method.rb:8:in 'Mysql2::Result#each'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/rack-mini-profiler-4.0.1/lib/patches/db/mysql2/alias_method.rb:8:in 'Mysql2::Result#each'
/var/www/discourse/script/import_scripts/base.rb:542:in 'ImportScripts::Base#create_posts'
/var/www/discourse/script/import_scripts/phpbb3/importer.rb:212:in 'block in ImportScripts::PhpBB3::Importer#import_posts'
/var/www/discourse/script/import_scripts/base.rb:943:in 'block in ImportScripts::Base#batches'
<internal:kernel>:168:in 'Kernel#loop'
/var/www/discourse/script/import_scripts/base.rb:942:in 'ImportScripts::Base#batches'
/var/www/discourse/script/import_scripts/phpbb3/importer.rb:293:in 'ImportScripts::PhpBB3::Importer#batches'
/var/www/discourse/script/import_scripts/phpbb3/importer.rb:208:in 'ImportScripts::PhpBB3::Importer#import_posts'
/var/www/discourse/script/import_scripts/phpbb3/importer.rb:38:in 'ImportScripts::PhpBB3::Importer#execute'
/var/www/discourse/script/import_scripts/base.rb:47:in 'ImportScripts::Base#perform'
/var/www/discourse/script/import_scripts/phpbb3/importer.rb:22:in 'ImportScripts::PhpBB3::Importer#perform'
script/import_scripts/phpbb3.rb:35:in '<module:PhpBB3>'
script/import_scripts/phpbb3.rb:16:in '<module:ImportScripts>'
script/import_scripts/phpbb3.rb:15:in ''
615718 / 2167314 ( 28.4%)  \[415 items/min\]
\[phpbbimpo0:docker\*

1746813 / 2167314 ( 80.6%) [353개/분]

끝은 났는데 비밀번호를 가져오는 걸 잊어버렸어요. 그래서 처음부터 다시 시작해야 할 것 같아요 :frowning: 이번에는 몇몇 게시물을 정리할 거예요. 젤리나 카운트다운 관련 게시물이 수십만 개나 쌓여 있거든요, 하하. 조언해 주셔서 감사합니다.

보안상의 이유로 비밀번호를 가져오지 않으며, 귀하도 그러시길 권합니다.

마이그레이션 후 사용자는 “비밀번호를 잊으셨나요?” 버튼을 통해 비밀번호를 재설정하도록 안내받습니다. 사용자에게는 추가 단계가 되지만, 사전에 왜 필요한지 그리고 어떻게 해야 하는지 설명해 주면 문제가 되지 않을 것입니다.

자체 호스팅을 사용하는 경우 Migrated password hashes support 플러그인을 사용할 수 있습니다. 이 플러그인은 비밀번호 자체를 가져오지 않고 비밀번호의 해시 값을 가져오기 때문에, 기존 시스템에서 비밀번호가 얼마나 안전했는지에 따라 동일한 수준의 보안성을 유지합니다.

존중하는 마음으로 말씀드리지만, 이에 대한 근거를 찾지 못하겠습니다. 저희 migratepassword 플러그인은 10년 이상 사용해 왔으며, 보안 문제가 발생한 적이 없습니다.

migratepassword_allow_insecure_passwords 설정을 비활성화 상태로 유지하는 한, Discourse 코어와 동일한 비밀번호 보안 제한이 적용됩니다.

대부분 동의합니다! 이주하려는 커뮤니티에 데이터 유출 사고가 없었고, 가져오기 과정에서 데이터가 안전하게 전송되었으며, 올바른 도구(예: 비밀번호 플러그인)를 사용한다면 비밀번호를 가져올 수 있습니다. 다만 현재 코어에 공식적으로 제공되는 가져오기 스크립트는 이를 공식적으로 지원하지는 않습니다.

많은 양의 콘텐츠가 이전되지 않아서 나중에 다시 시도할 예정입니다. 속도를 높이고 공식 비밀번호 가져오기 기능을 제공하기 위해 진행 중인 작업이 있나요? 플러그인을 사용해 보았는데 일부 비밀번호가 작동하지 않았습니다. 이메일 주소가 오래된 상태라 정말 고생이 많았어요. 그래서 나중에 다시 시도해 볼 계획입니다.

더 빠르게 만들 도구 관련해서 업데이트가 있나요?

죄송합니다, 아직 준비가 되지 않았습니다! 올해 말까지 완료하는 것을 목표로 하고 있습니다 :crossed_fingers:

다만, 현재 사용 가능한 도구를 사용하여 대용량 마이그레이션을 여러 번 수행해 본 경험이 있습니다. DB를 조정하거나 일괄 가져오기 도구를 사용해 보시는 것을 강력히 권장합니다.

대략적인 계산에 따르면 전체 가져오기를 일주일 안에 완료할 수 있습니다. 최종 가져오기는 몇 분에서 몇 시간이 걸릴 것입니다. 이는 쉬운 방법이지만, 올해 안에 새로운 가져오기 스크립트가 제공되더라도 가져온 비밀번호가 여전히 포함되지 않을 가능성이 높습니다(다만, 비밀번호를 추가하는 것은 비교적 쉬울 것입니다).