근 2개월 정도 되는 시간동안 OSSCA(오픈소스 컨트리뷰션 아카데미)의 활동에 참여했었습니다! 해당 활동을 통해 많은 경험을 하게되었는데요. 그 중에 가장 기억에 남았던 오픈소스(python-mysql-replication)에서 발생하는 성능 문제 개선에 대해 공유하려고합니다
MySQL에서는 replication protocol 통해 마스터(Master)와 슬레이브(Slave) 간 데이터를 동기화합니다. 이러한 복제 프로토콜은 사용자(Client)가 실행한 SQL 쿼리를 통해 발생하는데, 이러한 동작을 바이너리 로그 이벤트(Binlog Event)라고 합니다. 이러한 이벤트는 MySQL 설정 파일(mysql.cnf)에서 정의된 디렉토리에 지정된 이름으로 저장됩니다.
python-mysql-replication 라이브러리는 이 바이너리 로그 파일을 읽어와 Python 객체로 변환합니다. 이러한 객체는 사용자의 요구에 따라 다른 소프트웨어와 연동하여 활용할 수 있습니다.
💡 실제 사용 중인 기업 사례
Lomio(카카오)
Streaming Changes in a Database with Amazon Kinesis
이슈의 내용은 아래와 같았습니다
요약하자면 ‘python-mysql-replication 라이브러리와 연결된 mysql master server에서 DML이 발생할 경우 Innodb_history_list_length의 값이 증가한다’라는 문제였습니다
처음으로 했던 해결과정은 ‘어떠한 경우에 발생하는가?’, ‘왜 사용자의 쿼리에 대한 응답 시간이 늘어나는가?’ 두 가지에 대한 답변을 찾는 것이었습니다. 첫 번째 두 번째 질문 모두 DB에 대해 박학다식한 다른 멘티님의 자료 제공을 통해 해결할 수 있었습니다!
I’ll review how the InnoDB history length can affect a hung MySQL...
위의 링크에서 제가 고민하고 있는 질문에 대해서 답을 찾을 수 있었습니다
해당 인용절의 마지막 줄에서와 같이 commit이 되지 않았거나 rollback이 되지 않아 지속적으로 history_list_len의 값이 증가하게 됩니다But why does the InnoDB transaction history start growing? There are 940 transactions in this state: ACTIVE 766132 sec. MySQL’s process list shows those transactions in “Sleep” state. It turns out that those transactions were “lost” or “hung”. As we can also see, each of those transactions holds two lock structures and one undo record, so they are not committed and not rolled-back.
위의 인용절에서 볼 수 있듯이 InnoDB 트랜잭션 히스토리가 늘어남에 따라 SELECT 쿼리는 더 이전 버전의 행을 계속 스캔해야 하며 성능에 영향을 미치게 됩니다As the InnoDB transaction history grows, SELECTs need to scan more and more previous versions of the rows, and performance suffers. That explains the issue: SELECT queries get slower and slower until restart. Peter also filed this bug: Major regression having many row versions.
두 번째는 이슈확인을 위한 모니터링 환경 구축과 이슈 발생 환경 구축이었습니다
모니터링 환경을 위해 mysql-exporter와 prometheus를 사용하였습니다.
...
...
while True:
cur = pymysql.connect(**MYSQL_SETTINGS)
cursor = cur.cursor()
cnt +=1
cur.execute("insert into test.r1 (c1) values ('#1'),('#2'),('#3'),('#4'),('#5'),('#6'),('#7');")
cur.commit()
cur.close()
세 번째는 DML과 연관이 있는 event를 리스트 업 하였습니다 그 후 문제를 발생시키는 event를 확인하여 하나씩 Disable 처리를 하여 문제를 일으키는 대상을 찾았습니다.
DML이 발생될 때 생성되는 event는 총 2가지였습니다
TableMapEvent
RowsEvent
모니터링 결과 TableMapEvent
에서 Innodb_history_list_length 값이 증가하는 지표를 확인할 수 있었고 pythom-mysql-replication 라이브러리를 디버깅해보면서 문제를 발생시키는 부분을 확인하였습니다.
문제를 일으키는 쿼리의 커서 객체를 생성할 때 autocommit 설정을 추가하여 문제를 해결했습니다. 이러한 수정 사항을 반영하여 Pull Request(PR)를 올렸고 성능적 이슈인 만큼 비교적 빠르게 머지되었습니다.
이후 증가하던 ‘Innodb_history_list_length’ 값의 증가폭이 0에 가깝게 줄어들었습니다 (녹색 선은 이전 버전, 노란색 선은 코드 수정 후 버전입니다)
이전에 하던 업무에서 항상 스트레스받으면서도 좋아하던 부분이 최적화에 대한 부분이었고 감사하게도 해당 이슈를 미리 진행 중이시던 멘티님이 먼저 제안해 주셔서 같이 협업하며 해결할 수 있어서 가장 기억에 남는 기여였네요.
사실 처음 제가 했던 해결은 'commit'을 그냥 실행시키는 로직으로 개선을 했었습니다 알고 보니 python-mysql 라이브러리에 cusor가 닫힐 때 자동으로 autocommit을 해주는 파라미터가 존재하더라고요(또 다른 멘티님이 도와주셨습니다!) 감사하게도 설명까지 해주시면서 도와주셔서 최선의 방법으로 해결한 것 같습니다.
또 메인테이너가 적어주신 comment가 기억에 남네요. 해당 코멘트로 좀 더 기여하는 데에 있어서 동기부여가 되었지 않았나 싶네요 ^^
‘python-mysql-replication’에 대해서 토론할 수 있는 장을 메인테이너인 줄리앙이 만들어주셨습니다 사용경험, 개선에 대한 부분, 새로운 기능에 대한 아이디어에 대해서 토론하고 기여하는 공간이 되었으면 하네요~