{"id":"IL-0454","org":"Woowa Brothers","date":"2016-12-08","docs":["https://github.com/MariaDB/mariadb-connector-j/commit/77834ccc7dad77f3c7dbc0c26637e12b7a3d5644"],"title":{"en":"Baemin billing servers could not open new MariaDB connections at the start of a card-discount event","ko":"배민 카드사 할인 이벤트가 시작되자 빌링 시스템이 MariaDB 에 새 커넥션을 맺지 못해 결제 요청 일부가 실패했다"},"domain":["database","networking"],"impact":{"scope":"Payment requests through the Baemin billing system at the start of the event: 140 of 14,000 requests failed in about one minute","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"A new DB connection failed because the driver did a DNS lookup of the local host name that took about 6 seconds, longer than the server's 5-second connect_timeout. Other ways to avoid it are to use localhost as the host name or to register the IP address and host name in /etc/hosts so the address is returned first. Above all, the author recommends keeping libraries on recent versions.","ko":"에러가 시간이 지나 저절로 멈췄더라도 대수롭지 않게 넘기면 큰 장애로 이어질 수 있다. 라이브러리는 최신 버전을 쓰는 쪽이 낫다. 드라이버를 올리지 않아도 이 문제를 피하는 방법이 있다. hostname 을 localhost 로 두거나, /etc/hosts 에 IP 주소와 hostname 을 등록해 DNS 를 거치지 않고 바로 IP 주소를 얻게 하면 된다."},"related":[],"sources":[{"url":"https://techblog.woowahan.com/2517/","kind":"blog","archived":"https://web.archive.org/web/20250711084846/https://techblog.woowahan.com/2517/"}],"summary":{"en":"The Baemin billing system passes users' payment requests to payment gateways and hands the responses to the order system. It runs on 7 Spring Boot application servers with MariaDB, using mariadb-connector-j and a JDBC connection pool. The team used io.spring.dependency-management 0.5.6, which picks mariadb-java-client 1.4.6 automatically. After a senior engineer on the team changed the Gradle settings to the latest mariadb-connector-j, 1.5.5, the problem no longer reproduced.\n\nTo learn why, the team went through the driver's changelog and found Jira issue CONJ-360: DriverManager.getConnection() threw SQLNonTransientConnectionException when creating a new connection, the problem disappeared with the MySQL connector, and it seemed to come from ManagementFactory.getRuntimeMXBean().getName(), which fetches the PID. Before the fix, SendHandshakeResponsePacket.java sent pid@hostname as part of the authentication data and got the host name with InetAddress.getLocalHost().getHostName(). Unless the host name is 'localhost', getLocalHost() resolves it through the name service (DNSNameService's lookupAllHostAddr()), caching the answer for 5 seconds. On the billing application servers that lookup took about 6 seconds, while the billing database's connect_timeout was 5 seconds, so the database's connect_timeout fired before the new connection finished its handshake. The fix, which returns only the PID through JNA, was released in October 2016 as version 1.5.4.","ko":"우아한형제들의 빌링 시스템은 사용자의 결제 요청을 PG사에 전달하고 응답을 정리해 주문 시스템에 넘긴다. 이 시스템은 Spring boot 애플리케이션 서버 7대와 MariaDB 로 이루어져 있고, mariadb-connector-j 와 JDBC Connection Pool 을 쓴다. 2016년 12월 8일 오후 6시 카드사 할인 이벤트가 시작되자 약 1분 동안 14,000개의 요청 중 140개가 실패했다. 에러 대부분은 PG사가 네트워크 문제로 요청을 받지 못한 경우였다. 나머지 일부는 Unable to acquire JDBC Connection 예외였고, DB 쪽에는 user: ‘unauthenticated’ Aborted connection 경고가 남았다.\n\nConnection Pool 설정과 DB 서버의 skip_name_resolve 에는 문제가 없었다. 같은 팀의 선임이 mariadb-connector-j 를 최신 1.5.5 로 바꾸자 문제가 재현되지 않았다. 팀이 쓰던 mariadb-java-client 1.4.6 은 io.spring.dependency-management 0.5.6 이 자동으로 정한 버전이었다. changelog 와 JIRA 이슈 CONJ-360 을 따라가 보니, 1.4.6 은 handshake 응답 패킷에 pid@hostname 을 넣으려고 InetAddress.getLocalHost().getHostName() 을 불렀다. hostname 이 localhost 가 아니면 getLocalHost() 는 DNS 로 호스트 이름의 IP 주소를 조회한다. 빌링 애플리케이션 서버에서는 이 조회에 약 6초가 걸렸다. 빌링 데이터베이스의 connect_timeout 은 5초였으므로, DB 서버는 인증이 끝나기 전에 커넥션을 끊었다. 이 동작은 JNA 로 pid 만 가져오도록 고쳐졌고, 그 수정은 2016년 10월에 1.5.4 로 이미 릴리스돼 있었다."},"trigger":{"en":"A card-company discount event for the first 1,000 customers started at 18:00 and sent about 14,000 requests to the billing system in about one minute.","ko":"오후 6시에 선착순 1,000명에게 3,000원을 할인해 주는 카드사 이벤트가 시작되면서 약 1분 동안 빌링 시스템에 요청 14,000개가 들어왔다."},"severity":"unknown","symptoms":["high-error-rate","intermittent-connection-failures"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"incident","detection":{"en":"The team was watching NewRelic and Pinpoint during the 18:00 event, and the Pinpoint graph turned red shortly after the event began.","ko":"할인 이벤트가 열리는 오후 6시에 NewRelic 과 Pinpoint 로 지켜보던 중, Pinpoint 그래프에 에러를 뜻하는 빨간색이 보이는 것을 알아챘다."},"components":["database","dns-resolver"],"root_cause":"software-bug","remediation":[{"text":{"en":"Changed the Gradle settings to use mariadb-connector-j 1.5.5 instead of 1.4.6","ko":"gradle 설정을 바꿔 mariadb-connector-j 를 1.4.6 에서 최신 1.5.5 로 올렸다."}}],"action_items":[],"contributing":["known-bug-unpatched"],"recurrence_of":null,"symptoms_text":{"en":"At 18:00, when a card-company event giving 3,000 won off to the first 1,000 customers started, the Pinpoint graph of the Baemin billing system turned red shortly afterwards: 140 of 14,000 requests failed in about one minute. Most errors looked like requests from the billing system that the payment gateway (PG) did not receive because of a network problem, but other errors pointed at the billing system itself.\n\nThe application threw DataAccessResourceFailureException: Unable to acquire JDBC Connection; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection, caused by java.sql.SQLNonTransientConnectionException: Could not connect to xxx.xxx.xxx.xxx unexpected end of stream, read 0 bytes from 4. The database error log showed 2016-12-08 18:00:55 xxxx [Warning] Aborted connection 37170 to db: ‘unconnected’ user: ‘unauthenticated’ host: ‘xxx.xxx.xxx.xxx’ (Unknown error).\n\nThe team first suspected the connection pool settings but found nothing wrong, and the errors stopped on their own over time. Reading the messages as new DB connections failing at the authentication step, the team searched for 'mysql unauthenticated user'. Most results pointed to the --skip-name-resolve option, which stops MySQL and MariaDB from looking up the client's host name from its IP address on each new connection, but the billing database already had skip_name_resolve set to ON.","ko":"2016년 12월 8일 오후 6시 카드사 할인 이벤트가 시작되고 얼마 지나지 않아, 빌링 시스템의 Pinpoint 그래프에 에러를 뜻하는 빨간색이 보이기 시작했다. 약 1분 동안 14,000개의 요청 가운데 140개가 에러로 끝났다.\n\n에러 대부분은 빌링 시스템이 보낸 요청을 네트워크 문제로 PG사가 받지 못한 것으로 보였다. 그 가운데 빌링 시스템 쪽 문제로 보이는 애플리케이션 예외가 섞여 있었다.\nDataAccessResourceFailureException: Unable to acquire JDBC Connection; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection\nCaused by: java.sql.SQLNonTransientConnectionException: Could not connect to xxx.xxx.xxx.xxx unexpected end of stream, read 0 bytes from 4\n\n같은 시각 DB 에러 로그에는 인증 전에 끊긴 커넥션이 남았다.\n2016-12-08 18:00:55 xxxx [Warning] Aborted connection 37170 to db: ‘unconnected’ user: ‘unauthenticated’ host: ‘xxx.xxx.xxx.xxx’ (Unknown error)\n\n에러는 시간이 지나자 저절로 멈췄다. 팀은 먼저 Connection Pool 설정을 의심해 확인했지만 문제를 찾지 못했다. 다음으로 새 DB 커넥션이 인증 단계에서 실패했다고 보고 --skip-name-resolve 옵션을 확인했다. 이 옵션은 MySQL 계열 DB 서버가 새 커넥션을 인증할 때 클라이언트 IP 주소로 hostname 을 알아내는 과정을 건너뛰게 한다. 그러나 skip_name_resolve 는 이미 ON 이었다."},"updated_at":"2026-11-14T12:34:10.310718Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0454","org_slug":"woowa-brothers"}