운영을 맡은 신선식품 커머스에는 아직 PHP 7 위에서 도는 서버가 두 벌 있고, 둘 다 밖에서 직접 부를 수 있다. 런타임을 PHP 8.5 로 올리는 일의 목표는 2~3주 걸릴 일을 약 1주로 줄이고, 그동안 운영 중단을 0으로 두는 것이었다. 그 일정을 지키려면 무엇이 깨지는지를 추측이 아니라 측정으로 먼저 알아야 했다.
Table of contents
Open Table of contents
배경 — 지원이 끝난 런타임 두 벌
첫째는 오래된 웹 서버 VM 이다. PHP 7.0.12 를 fpm-fcgi 로 Apache 2.4.23 위에서 돌리고, 운영체제는 지원이 끝난 Debian 8.6 이다. 한 문서 루트에 앱 17폴더와 PHP 파일 289개, 약 67,000줄이 있고 WordPress 4.6.11 이 같은 자리에 섞여 있다.
둘째는 결제 게이트웨이 컨테이너로, php:7.2-apache 이미지 위에 .php 파일 73개가 있다. 이 73개가 곧 외부에서 부를 수 있는 경로 전부다.
VM 쪽 PHP 는 대부분 얇은 서버 코드다. mysqli·PDO 로 DB 에 직접 붙는 코드와 세션을 쓰는 코드가 하나도 없고, 외부 호출은 curl 을 쓰는 파일 4개와 file_get_contents 를 쓰는 파일 1개뿐이다. 상태를 들고 있지 않은 코드라서, 요청 하나하나를 따로 떼어 두 판에 보내 비교하는 시험이 성립한다.
시험 방법 — 같은 입력을 두 판에 보내는 기준선 대조
시험은 네트워크를 막은 컨테이너에서 했다. 공식 이미지 php:8.5-fpm-bookworm(PHP 8.5.12)을 띄우고, 같은 시험을 PHP 7.0 에서도 돌려 기준선으로 삼았다. 게이트웨이 쪽은 php:7.2-cli 와 php:8.5-cli 를 나란히 썼다.
- 운영 PHP 파일의 사본에
php -l을 전수로 돌린다 - 내장 서버
php -S로 모든.php에 GET 과 POST 를 한 번씩 보내고, 응답에 찍힌 오류를 모은다 - 같은 입력을 기준선 판에 보내 상태 코드·본문·진단 문구 수를 대조한다.
display_errors=1·error_reporting=-1로 진단 문구를 응답에 드러내고, 시각에서 나오는 64자 해시는 가린다
외부로 나가는 호출은 두 판 모두 이름 풀이에서 같은 지점에 실패하므로, 이 시험이 보는 것은 문법 호환과 그 앞 단계의 동작이다. 빈 입력으로 부르기 때문에 정상 결제 흐름의 분기, 예를 들어 결제대행 응답을 파싱하는 경로는 밟지 못한다. 그 부분은 소액 실결제 시험으로 따로 닫는다.
측정 — 289개 파일과 578건의 요청
VM 쪽 php -l 289개에서 파싱 오류는 0이었고 경고는 1건이었다. 선택 인자가 필수 인자 앞에 온 함수 선언 하나로, 8.5 에서 Deprecated 가 된다.
호출 시험 578건에서 8.5 에서만 새로 생긴 실제 동작 차이는 다섯 종이었다.
- 내장 문자열 함수에
null을 넘기는 곳 10곳. 동작은 같고 Deprecated 경고만 붙는다 fopen이 실패한 뒤 그 결과로fwrite를 부르는 곳. 7.0 은 경고로 넘어가지만 8.5 는TypeError로 죽는다- 같은 함수에서
fclose에false를 넘기는 곳. 이것도 8.5 에서TypeError다 - 선택 인자가 필수 인자 앞에 온 선언 1곳
- 정의하지 않은 변수를
curl핸들로 쓰는 곳 2곳. 7.0 은 경고뿐이지만 8.5 는TypeError를 내고 HTTP 500 이 된다
마지막 항목은 dev 클러스터의 8.5 병행 환경에서 응답을 비교하다 나왔다. 53개 경로 중 49개가 같았고, 두 경로가 7.0 의 200 에서 8.5 의 500 으로 바뀌었다.
Undefined array key 경고는 1,021건이 나왔지만 동작 차이가 아니다. 7.0 에서 Notice: Undefined index 로 같은 횟수가 나던 것이 이름만 Warning 으로 바뀌었고, 로그만 늘어난다. 오류를 총량으로 세지 않고 기준선과 같은 입력으로 대조했기 때문에 이 1,021건을 고칠 목록에서 뺄 수 있었다.
다섯 종을 고친 패치는 12개 파일에 들어갔다. 적용한 뒤 8.5.12 에서 구문 검사 오류가 0, 호출 시험의 Deprecated 가 0이었고, HTTP 500 은 26건으로 7.0 기준선과 같은 수였다. 남은 500 은 전부 7.0 에서도 나던 누락 파일 문제라 이번 전환이 만든 것이 아니므로 이번 범위에서 고치지 않는다. 고친 코드는 7.0 에서도 같은 동작을 하므로 옛 VM 에 먼저 반영해도 되고, 그만큼 전환 당일의 변경이 줄어든다.
결제 게이트웨이도 같은 방법으로 쟀다. 게이트웨이는 .php 73개에 GET·POST 를 보내 146건을 현행 판(PHP 7.2.34)과 8.5 판(PHP 8.5.12)에 같은 입력으로 비교했다. 상태 코드는 146건 모두 같았다(302 22건, 200 124건).
| 구분 | 경로 | 동일 | 진단 표기만 다름 | 본문 차이 | 8.5 치명 오류 |
|---|---|---|---|---|---|
| 사용 중 | 33 | 19 | 14 | 0 | 0 |
| 정리 대상 | 40 | 2 | 27 | 7 | 4 |
사용 중인 33개 경로에서 8.5 에서만 생긴 오류는 없었다. 정리 대상 40개의 치명 오류 4건은 모두 json_decode() 에 배열을 넘기는 코드다. 7.2 는 경고 뒤에 계속 가지만 8.x 는 TypeError 로 멈춘다. 본문 차이 7건은 진단 문구 길이와 시각 기반 서명 해시에서 나온 것이라 동작 차이가 아니다.
판단 — 옮기지 않을 것과 오리진 전환 방식
측정이 끝나고 나니 남은 문제는 「무엇을 고칠 것인가」보다 「무엇을 옮기지 않을 것인가」였다.
첫째는 쓰지 않는 결제 연동이다. 이 서비스는 결제 수단을 두 가지만 쓰므로 나머지 결제대행 연동은 시험 대상에서 뺐다. 게이트웨이의 8.5 치명 오류 4건과 본문 차이 7건이 모두 이 정리 대상에서 나왔고, 이 코드는 삭제와 함께 사라진다. 다만 VM 쪽의 구문 호환 패치는 그대로 두어, 삭제 전이라도 8.5 에서 파싱은 되게 했다.
둘째는 같은 문서 루트에 있는 WordPress 4.6.11 이다. 마지막으로 업로드·수정된 파일이 2017년 1월이고, 최근 6일 접속 기록에 wp-login·xmlrpc POST 가 0건이었다. WordPress 4.6 은 PHP 8 을 지원하지 않으므로 옮기려면 약 1,850개 파일을 따로 올려야 하는 별도의 큰 일이 된다. 그래서 새 서버로 옮기지 않고 제외하는 쪽을 권고했다. 제외의 대가는 어딘가 쓰는 곳이 남아 있으면 그 화면을 잃는다는 점이라, 쓰는 곳이 정말 없는지 한 번 확인을 받기로 했다. 쓰는 곳이 있으면 그 경로만 제외 범위에서 뺀다.
셋째는 오리진을 옮기는 방식이다. 앱 코드 안에는 서버의 고정 IP 를 직접 부르는 줄이 주석을 빼고 34줄, 21개 파일에 있었다. 선택지는 둘이었다.
- 고정 IP 를 그대로 둔 채, 옛 VM 을 가장 작은 인스턴스로 줄여 IP 만 붙잡고 새 서버로 중계한다
- 그 34줄을 호스트 이름으로 바꾸고, Cloudflare 에서 호스트별로 오리진만 새 서버로 돌린다
1번은 고칠 코드가 없는 대신 옛 VM 을 중계용으로 계속 돌려야 하고, 지원이 끝난 Debian 8.6 이 앞단에 남는다. 반면 2번은 옛 서버를 끝내 내릴 수 있다. 2번으로 선택한 이유는 새 서버가 다른 리전에 있어 고정 IP 를 옮길 수 없고, 34줄 중 바꿀 수 없는 것이 0줄이었기 때문이다. 2번의 대가는 21개 파일을 고치고, 치환 수와 대상 목록을 양방향으로 대조한 뒤에야 반영할 수 있다는 점이다. 결제 쪽 반환 주소 몇 곳은 사용하지 않는 연동이라 바꿔도 영향이 없고, 나머지는 같은 도메인 안의 화면 링크와 ajax 호출이다. 1번은 호스트화가 막힐 때 쓰는 대비책으로 남겼다. 호스트화도 7.0 에서 그대로 유효하므로 옛 VM 에 먼저 무중단으로 반영한 뒤 오리진만 바꾼다.
영향 범위 — 시험하지 않은 것과 되돌리는 방법
이 시험이 「동일」이라고 말하는 범위는 PHP 층까지다. 아래는 아직 보지 않았다.
- Apache 층 —
mod_security2(차단 규칙, 본문 1MB 상한)·/api프록시·rewrite·SSL. 내장 서버에는 Apache 가 없으므로 8.5 게이트웨이 이미지를 빌드해 같은 요청을 다시 보내야 한다 - 결제 실호출 — 승인·취소·망취소와 정기결제 키의 발급·삭제. 외부 호출이라 따로 승인을 받아 개발계에서 소액으로 시험한다
- 브라우저 흐름 — 반환 페이지의 쿠키·세션·리다이렉트와 외부 로그인 복귀
- 운영 롤링 — 무중단 확인, readiness, 되돌림
되돌리는 방법은 층마다 따로 준비했다. 패치와 호스트화는 7.0 호환이라 옛 VM 에서도 그대로 유효하다. 오리진 전환은 Cloudflare 레코드 단위로 수 초 안에 복원하고, 옛 VM 은 정지 전에 스냅샷을 떠서 7일 동안 보존한다. 결제 시험이 범위에서 빠지면서 시험 구간이 줄었고, 전체 일정 추정은 약 7일이 됐다.
결과 — 전환 당일로 몰리지 않은 변경
- 고칠 범위. 이름만 바뀐 경고 1,021건을 빼고 실제 동작 차이 다섯 종으로 좁혔고, 패치 뒤 HTTP 500 은 7.0 기준선과 같은 26건이었다
- 일정. 쓰지 않는 결제 연동과 WordPress 를 옮기지 않기로 하고 결제 시험이 범위에서 빠지면서, 2~3주로 보던 일을 약 7일로 다시 추정했다
- 전환 당일의 위험. 호환 수정과 호스트화를 옛 서버에 먼저 넣어, 전환 당일에는 Cloudflare 에서 오리진만 바꾸고 레코드 단위로 수 초 안에 되돌릴 수 있게 했다
정리
- 오류의 총량이 아니라 「8.5 에서만 생긴 동작 차이」를 센다. 이름만 바뀐 1,021건을 빼자 고칠 것은 다섯 종이었다
- 현행 판을 기준선으로 두고 같은 입력을 보내면, 전환이 만든 문제와 원래 있던 문제가 갈린다. HTTP 500 26건은 원래 있던 것이었다
- 쓰지 않는 코드는 옮기지 않는다. 고칠 범위를 줄이는 것이 일정을 줄이는 가장 큰 수단이었다
- 호환 수정과 호스트화를 7.x 에서도 유효하게 만들어 옛 서버에 먼저 넣으면, 전환 당일에는 오리진만 바뀐다
- 「동일」은 시험한 층까지만 말한다. Apache 층과 결제 실호출은 따로 닫는다