Skip to content
isdnetworks
Go back

PHP 7 에서 8.5 로 올리기 전에 같은 입력을 두 판에 보내 동작 차이를 다섯 종으로 좁혔다

운영을 맡은 신선식품 커머스에는 아직 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 를 나란히 썼다.

  1. 운영 PHP 파일의 사본에 php -l 을 전수로 돌린다
  2. 내장 서버 php -S 로 모든 .php 에 GET 과 POST 를 한 번씩 보내고, 응답에 찍힌 오류를 모은다
  3. 같은 입력을 기준선 판에 보내 상태 코드·본문·진단 문구 수를 대조한다. display_errors=1·error_reporting=-1 로 진단 문구를 응답에 드러내고, 시각에서 나오는 64자 해시는 가린다

외부로 나가는 호출은 두 판 모두 이름 풀이에서 같은 지점에 실패하므로, 이 시험이 보는 것은 문법 호환과 그 앞 단계의 동작이다. 빈 입력으로 부르기 때문에 정상 결제 흐름의 분기, 예를 들어 결제대행 응답을 파싱하는 경로는 밟지 못한다. 그 부분은 소액 실결제 시험으로 따로 닫는다.

측정 — 289개 파일과 578건의 요청

VM 쪽 php -l 289개에서 파싱 오류는 0이었고 경고는 1건이었다. 선택 인자가 필수 인자 앞에 온 함수 선언 하나로, 8.5 에서 Deprecated 가 된다.

호출 시험 578건에서 8.5 에서만 새로 생긴 실제 동작 차이는 다섯 종이었다.

마지막 항목은 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 치명 오류
사용 중33191400
정리 대상4022774

사용 중인 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개 파일에 있었다. 선택지는 둘이었다.

  1. 고정 IP 를 그대로 둔 채, 옛 VM 을 가장 작은 인스턴스로 줄여 IP 만 붙잡고 새 서버로 중계한다
  2. 그 34줄을 호스트 이름으로 바꾸고, Cloudflare 에서 호스트별로 오리진만 새 서버로 돌린다

1번은 고칠 코드가 없는 대신 옛 VM 을 중계용으로 계속 돌려야 하고, 지원이 끝난 Debian 8.6 이 앞단에 남는다. 반면 2번은 옛 서버를 끝내 내릴 수 있다. 2번으로 선택한 이유는 새 서버가 다른 리전에 있어 고정 IP 를 옮길 수 없고, 34줄 중 바꿀 수 없는 것이 0줄이었기 때문이다. 2번의 대가는 21개 파일을 고치고, 치환 수와 대상 목록을 양방향으로 대조한 뒤에야 반영할 수 있다는 점이다. 결제 쪽 반환 주소 몇 곳은 사용하지 않는 연동이라 바꿔도 영향이 없고, 나머지는 같은 도메인 안의 화면 링크와 ajax 호출이다. 1번은 호스트화가 막힐 때 쓰는 대비책으로 남겼다. 호스트화도 7.0 에서 그대로 유효하므로 옛 VM 에 먼저 무중단으로 반영한 뒤 오리진만 바꾼다.

영향 범위 — 시험하지 않은 것과 되돌리는 방법

이 시험이 「동일」이라고 말하는 범위는 PHP 층까지다. 아래는 아직 보지 않았다.

되돌리는 방법은 층마다 따로 준비했다. 패치와 호스트화는 7.0 호환이라 옛 VM 에서도 그대로 유효하다. 오리진 전환은 Cloudflare 레코드 단위로 수 초 안에 복원하고, 옛 VM 은 정지 전에 스냅샷을 떠서 7일 동안 보존한다. 결제 시험이 범위에서 빠지면서 시험 구간이 줄었고, 전체 일정 추정은 약 7일이 됐다.

결과 — 전환 당일로 몰리지 않은 변경

정리


Share this post on:

Previous Post
운영 변경을 넣었다 뺐다 하지 않도록 결정 대조를 세 층의 장치로 나눠 걸었다
Next Post
메모리와 규칙의 정본을 파일에서 아틀라스로 옮기자 여러 머신의 세션이 같은 판을 읽게 됐다