얼떨결에 JMeter로 부하테스트를 진행하고 있었는데, 연결 시간 초과나 소켓 닫힘 같은 기본적인 네트워크 오류가 특정 시점부터 많이 발생하길래 (1) 스위치나 보안장비에서 동일 IP의 동시 요청 건수를 제한하거나 또는 그런 설정이 있는지 확인하였으나 본인측 요청에 의해 보안장비는 필터링 제외를 시켰고, 스위치는 기본 셋팅이라 제한이 업다고 한다. (2) 당시 Apache의 mpm 설정은 worker로 되어있었고, MaxRequestWorkers는 4,096으로 설정되어 있었다. 모니터링 했을 때 Busy Worker가 폭등하다가 폭락하는 그래프를 보였고 폭락 지점이 네트워크 오류가 다수 발생하는 시점이었다.

 

여기서 부하발생기가 2,000 스레드로 요청을 발생하고 있었는데, 모든 요청을 온전하게 받아들이지 못하고 있었다고 판단을 하였다. 부하발생기의 자원사용률이나 TCP 소켓의 TIME_WAIT 상태는 크게 많지 않았음에도 불구하고 왜 이런 그래프가 반복되는가 고민하는 찰나에 작년 일이 생각났다. Apache를 운영하는 서버의 커널 파라미터를 수정했었다. 당시 사용하던 서버는 Destroyed 되었기 때문에 설정값은 기억나지 않지만(기록이란 참 중요하다) TCP 소켓 reuse와 recycle에 관련했던거 같다.

 

하지만 이게 왠걸 tcp_tw_reusetcp_tw_recycl\은 기본적으로 Outbound 연결에 대해서 효과가 있고 Web 서버 같이 Inbound 연결이 많은 환경에서는 아무 의미가 없다고 한다. 심지어 tcp_tw_recycle은 최신 커널에서는 폐기된 파라미터다. 도대체 작년에는 왜 그런일을 하면서 해골물을 마셨었는지 알 수가 없다.

 

각설하고 이번에는 net.core.somaxconn 파라미터를 수정했다. 연결에 대한 대기열(Queue)인데 기본값인 512로 설정되어 있었다. 부하발생기의 스레드는 반복해서 요청을 보내고 있는데 일부 응답이 늦어지며 연결을 유지하고 있었던 것들에 의해 대기열에 들어갔어야 했다. 하지만 대기열의 크기가 512에 불과하니 연결에 실패하는 등의 오류가 발생했던 것으로 판단되었다. net.core.somaxconn의 값은 Apache의 MaxRequestWorkers의 값과 동일하거나 그 이상으로 설정 해주면 된다.

sudo vim /etc/sysctl.conf

### 파일 내용에서 값을 찾아 수정하거나, 없으면 추가
net.core.somaxconn = 8192

# 바로 적용
sudo sysctl -p

 

그리고 Apache 설정도 바꿔줘야한다. 나중에 헷갈리지 않게 mpm.conf 쪽에다 추가했다. 해당 값은 net.core.somaxconn보다 작거나 같고, MaxRequestWorkers보다 큰 값으로 설정하면 된다. 나는 net.core.somaxconn과 같은 값으로 설정했다.

ListenBacklog 8192

 

Apache도 재기동하고 다시 요청을 보내니 관련 오류가 현저하게 줄어들어 Busy Worker 그래프가 폭락하지는 않았고, 코끼리를 삼킨 보아뱀처럼 그래프가 그려졌다. 아무튼 이를 통해 일부 문제를 해결할 수 있었다. JMeter 다루기도 무엇보다 쉽지가 않네.

주말에 압축했던 패키지를 서버로 옮겨서 설치를 시도했다.

# 패키지를 저장할 폴더
$ mkdir /opt/zabbix

# 압축 풀기
$ tar -zxvf zop.tar.gz -C /opt/zabbix

# createrepo_c 먼저 설치
$ sudo rpm -ivh /opt/zabbix/createrepo_c-*.rpm

 

libdrpm.so.0()이 없다면서 오류가 발생했다. 노트북을 챙겨오지 않아서 pkgs.org에서 drpm 패키지를 다운로드 했다.

# drpm 설치
$ sudo dnf install drpm-*.rpm

# createrepo_c 다시 설치
$ sudo dnf install createrepo_c-*.rpm

# 설치 성공 했으니 RPM 파일들이 있는 폴더를 리포지토리로 만들기
$ sudo createrepo_c /opt/zabbix


# 리포지토리 파일 작성
sudo nano /etc/yum.repos.d/zabbix-local.repo

# 파일 내용
[zabbix-local]
name=Zabbix Local Repository
baseurl=file:///opt/zabbix/
enabled=1
gpgcheck=0
# 파일 내용

# 나머지 파일 설치
$ sudo dnf install zabbix-server-mysql zabbix-web-mysql zabbix-apache-conf zabbix-sql-scripts zabbix-agent mariadb-server

 

실행하니까 Updating Subscription Management repositories. Unable to read consumer identity. This system is not registered with an entitlement server. You can use subscription-manager to register. AppStream Errors During downloading metadata for repository 'AppStream': .. 오류가 발생했는데 폐쇄망이라 그런가보다.

# disablerepo="*": 모든 리포지토리 비활성화
# enablerepo="zabbix-local": 방금 만든 리포지토리만 활성화
$ sudo dnf --disablerepo="*" --enablerepo="zabbix-local" install zabbix-server-mysql zabbix-web-mysql zabbix-apache-conf zabbix-sql-scripts zabbix-agent mariadb-server

 

이것도 실행하니까 Error: No available modular metadata for modular package 오류가 발생했다.

# /opt/zabbix 에서 모든 rpm 설치
$ sudo dnf --disablerepo="*" install *.rpm

 

몰랐는데 서버에 이미 MySQL이 설치되어있어서 오류가 났다. 관련 패키지 파일 삭제 후 설치에 성공했다. 근데 나는 MySQL이 설치되어있는지 몰랐기 때문에 계정과 패스워드도 모른다. 그래서 root 패스워드를 초기화 했다.

# 안전모드로 MySQL 시작
$ sudo systemctl stop mysqld
$ sudo systemctl set-environment MYSQLD_OPTS="--skip-grant-tables"
$ sudo systemctl start mysqld

# 접속 성공
$ sudo mysql -uroot
> FLUSH PRIVILEGES;
> ALTER USER 'root'@'localhost' IDENTIFIED BY 'root';
> quit;
# 안전모드 제거
$ sudo systemctl stop mysqld
$ sudo systemctl unset-environment MYSQLD_OPTS
$ sudo systemctl start mysqld

# 변경한 비밀번호로 접속 성공
$ sudo mysql -uroot -p
# Zabbix 데이터베이스 생성(utf8mb4 형식 필수)
> create database zabbix character set utf8mb4 collate utf8mb4_bin;

# Zabbix 사용자 생성
> create user zabbix@localhost identified by 'zabbix';

# Zabbix 사용자에게 zabbix 데이터베이스의 모든 권한을 부여
> grant all privileges on zabbix.* to zabbix@localhost;

# Zabbix 6.0 이상은 SUPERUSER 권한 필요(스키마 가져오고 0으로 초기화)
> set global log_bin_trust_function_creators = 1;

# 변경사항을 적용 후 종료
> flush privileges;
> quit;
# Zabbix 초기 스키마 가져오기
$ zcat /usr/share/zabbix-sql-scripts/mysql/server.sql.gz | mysql --default-character-set=utf8mb4 -uzabbix -p zabbix

# Zabbix 설정 파일 수정(DBPassword)
$ sudo vim /etc/zabbix/zabbix_server.conf

# 파일 수정
# 주석 풀고 비밀번호 입력
DBPassword=zabbix
# 파일 수정

# 서비스 재시작 및 자동 실행 등록
$ sudo systemctl restart zabbix-server zabbix-agent httpd
$ sudo systemctl enable zabbix-server zabbix-agent httpd

 

Zabbix 설치하면서 Apache도 하나 더 설치가 되었는데, 기존에 설치가 된게 있어서 포트 충돌이 났다.

# Apache 포트 수정
$ sudo vim /etc/httpd/conf/httpd.conf

# 파일 수정
Listen 80 → 8080
# 파일 수정

# Apache 서비스 시작
$ sudo systemctl start httpd

 

이제 {서버 IP}:8080/zabbix으로 접근은 되지만 'Minimum required PHP version is 8.0.0' 메시지가 나왔다. 기존에 7 버전대 패키지가 다운로드 되었고, 그걸 설치해서 그런가보다. 또 pkgs에서 PHP 8.0.0 필요한 패키지 다운로드했다.

# 7 버전 삭제
$ sudo dnf remove 'php*'

# 8 버전 설치
$ sudo dnf --disablerepo="*" install php*.rpm

# PHP 재시작 및 자동 실행 등록
$ sudo systemctl restart php-fpm
$ sudo systemctl enable php-fpm

# Apache 서비스 재시작
$ sudo systemctl restart httpd

 

다시 {서버 IP}:8080/zabbix로 접근하면 Zabbix 설치 화면이 나오고, Check of pre-requisites의 모든 항목이 OK로 표시되는지 확인 후 다음으로 넘어가서 DB 정보 입력하고 완료되었다.

어찌저찌 마무리 하였지만.. 에이전트 설치 과정은 다음에

 

'DevOps > 모니터링' 카테고리의 다른 글

Zabbix 설치기 - 1  (2) 2025.08.23

WAS 서버들은 다른 모니터링 툴로 잘 보고 있는데, WEB 서버는 자연스럽게 모니터링을 하지 않고 있음을 문득 깨달아버렸다. 조만간 부하 테스트를 진행해야하는데 putty 여러개 띄워 놓는 것도 그만둘 때가 되었다고 생각했고, Grafana + Prometheus와 Zabbix 중에서 비교하다가 그래도 빠르게 시작할 수 있는 Zabbix로 선택했다.

 

빠르게 Zabbix Appliance(모든 구성 요소가 미리 설치된 가상 머신 이미지)로 시작하려했으나, Zabbix 서버로 사용하려 했던 서버도 이미 가상화 서버이고 중첩 가상화도 지원하지 않아서 포기했다. Docker Container도 제공하고 있어 이쪽으로 할까 했지만 Docker 설정도 안 되어 있어서 직접 설치하기로 했다. 하지만 외부 리포지토리에 접근이 불가한 폐쇄망 서버여서 외부에서 설치 패키지를 가져와 옮겨야한다.

 

https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.9/x86_64/product-software

 

The world's open source leader

Red Hat is the world’s leading provider of open source solutions, using a community-powered approach to provide reliable and high-performing cloud, virtualization, storage, Linux, and middleware technologies. Red Hat also offers award-winning support, tr

www.redhat.com

우선 동일한 환경에서 의존성을 맞추는게 중요하니 RHEL 8.9 이미지를 다운로드했다.

VirtualBox에 무인 설치라는게 생겨서 이미지만 넣으면 자동으로 설치해주더라...

 

VirtualBox 화면에서 하면 복사/붙여넣기가 자유롭지 않으니 파워쉘에서 ssh 연결해서 명령어 입력했다.

# RHEL 8.9용 Zabbix 7.0 LTS 버전 repo 설치
$ sudo rpm -Uvh https://repo.zabbix.com/zabbix/7.0/rhel/8/x86_64/zabbix-release-7.0-1.el8.noarch.rpm
$ sudo dnf clean all

# Zabbix 관련 패키지 및 의존성을 모두 다운로드
$ sudo dnf install --downloadonly --downloaddir=/home/vboxuser/zop \
zabbix-server-mysql \
zabbix-web-mysql \
zabbix-apache-conf \
zabbix-sql-scripts \
zabbix-agent \
mariadb-server \
createrepo_c

# mariadb-server랑 createrepo_c가 No match for argument 가 나오면서 실패

$ sudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms
This system has no repositories available through subscriptions.

# repo 구독도 실패해서 구독 처리
$ sudo subscription-manager register
Registering to: subscription.rhshttp://m.redhat.com:443/subscription
Username: 
Password:
The system has been registered with ID:
The registered system name is: 

$ sudo subscription-manager attach --auto
Ignoring the request to auto-attach. Attaching subscriptions is disabled for organization "" because Simple Content Access (SCA) is enabled.

# 위의 다운로드 다시 실행하면 정상적으로 다운로드 완료

 

45개 정도의 패키지가 다운로드 되는데 압축해서 서버로 옮기면 된다. 월요일에 출근해서 처리해야한다. 참고로 뒤에 구독 같은거 안 하려면 Rocky Linux 8.9로 하면 된다고 한다.

'DevOps > 모니터링' 카테고리의 다른 글

Zabbix 설치기 - 2  (1) 2025.08.25
 

지난 주 토요일 시험으로 AWS 기초 중의 기초인 'Certified Cloud Practitioner(CLF-002)'를 취득하였다. 합격으로 받은 50% 할인 바우처로 'Certified Solutions Architect - Associate'를 취득할 생각이다. Practitioner의 경우 AWS Builders에서 제공하는 에센셜 위주로 듣고, 샘플 시험이나 기타 문서를 참고하였다.

아파치를 Docker로 돌리고 싶지만 사정상 서버에 직접 설치해서 운영 중이다. 이번에 다른 목적으로 WEB 서버 6대가 추가되었는데, 6개의 콘솔을 열어서 설정파일을 수정하고 재기동하는건 말도 안 되게 귀찮기 때문에 Bash 스크립트를 작성했다.

ssh 키 쌍 생성하여 패스워드 입력 없이 서버 연결 설정

# 1.키 쌍 생성
$ ssh-keygen -t rsa
# Enter file in which to save the key : 엔터 (기본 경로 ~/.ssh 사용)
# Enter passphrase : 엔터 (패스워드 없이 사용)
# Enter same passphrase again : 엔터

# 2. 생성된 공개키를 대상 서버에 복사
$ ssh-copy -id -p 22 username@remote.server.ip

# 3. 연결 확인
$ ssh -p 22 username@remote.server.ip

ssh 연결하여 명령 실행

# 한 개의 명령 실행
$ ssh -p 22 username@remote.server.ip "touch test"

# 여러 개의 명령 실행(세미콜론으로 구분)
$ ssh -p 22 username@remote.server.ip "touch test; mv test tset"

스크립트 작성

  1. 설정 파일의 수정이 있을 때 파일명.YYYYMMDD 형태로 파일을 백업한다.
  2. conf.d의 ssl.conf, vhost.conf, workers.properties 위주로 수정한다.

스크립트 작성의 전제 조건이다.

#!/bin/bash

APACHE_CONFD_PATH=/etc/httpd/conf.d
DATE=`date +%Y%m%d`
# 오늘 날짜로 수정된 파일명
MODIFIED_CONF_LIST=`ls -1 ${APACHE_CONFD_PATH} | grep ${DATE} | sed 's/\.[^.]*$//`
# 대상 서버 목록
SERVERS="192.168.0.2 192.168.0.3 192.168.0.4 192.168.0.5 192.168.0.6"

# 수정된 파일별로 반복
for CONF in ${MODIEFIED_CONF_LIST}
do
    # 대상 서버별로 파일 전송
    for SERVER in ${SERVERS}
    do
        scp -P 22 ${APACHE_CONFD_PATH}/${CONF} apache@${SERVER}:${APACHE_CONFD_PATH}/${CONF}
        scp -P 22 ${APACHE_CONFD_PATH}/${CONF}.${DATE} apache@${SERVER}:${APACHE_CONFD_PATH}/${CONF}.${DATE}
    done
done

# 아파치 재기동
for SERVER in ${SERVERS}
do
    ssh -n -p 22 apache@${SERVER} "sudo /home/apache/apache_restart.sh"
    sleep 5
done

sudo /home/apache/apache_restart.sh

 

기존에는 apache_stop.shapache_start.sh를 따로 실행해서 재기동을 했으나, 한 번의 명령으로 재기동 하기 위해 각 서버에 apache_restart.sh를 추가 작성했다.

#!/bin/bash

# 아파치 종료
sh /home/apache/apache_stop.sh

while true
do
    # 프로세스에 httpd가 없으면(내가 실행한 grep 하나만 나오면) 탈출
    if [ `ps -ef | grep -c httpd` -eq 1 ]
    then
        break
    fi

    sleep 1
done

# 아파치 시작
sh /home/apache/apache_start.sh

연계 파일을 생성할 때 마지막 줄에 긴 공백이 들어가서 상대방이 적재 시 오류가 발생한다고 확인 요청이 왔다.

ByteBuffer buffer = ByteBuffer.allocate(data.length() * 3); //실제로는 숫자가 조금 다르다

 

ByteBuffer에 담을 데이터들은 data 변수에 들어가 있는데, 대충 한글이니 2바이트 등해서 주석에 왜 이렇게 계산을 하는지 잔뜩 적어놓았다. 물론 딱 맞는 숫자는 아니었고 무조건 미사용 공간이 생길 수 밖에 없는 구조다. 하지만 찾아보니 ByteBuffer를 allocate로 할당하면 그 capacity는 줄일 수가 없어서 꽉 안 채우면 미사용 공간이 공백으로 바뀌어 파일이 생성되는 것이었다.

buffer.flip();
byte[] bufferData = new byte[buffer.limit()];
buffer.get(bufferData);

 

flip()으로 읽기모드로 전환하면 position(시작)이 0으로, limit(끝)이 실제 쓴 데이터의 끝으로 변경되어, 이 상태로 복사하면 limit까지의 데이터만 복사되고, 뒤에 미사용 공간은 복사되지 않는다. 따라서 byte array를 생성할 때 길이를 buffer.limit()으로 설정하고, buffer.get()으로 버퍼에 담긴 데이터를 복사하면 된다.

 

문제가 있다면 다음에 buffer를 넘기는 메소드가 ByteBuffer 타입을 요구하는데, 위에서 생성한 bufferData는 byte array다.

//String foo = bar.foobar(buffer);
String foo = bar.foobar(ByteBuffer.wrap(bufferData);

 

이때 ByteBuffer.wrap(byte[])을 사용하면, 이미 존재하는 byte array를 ByteBuffer로 감싸기 때문에 예시로 쓴 foobar 메소드에서 타입 에러 없이 동작하게 된다. 이렇게 수정하여 연계 파일을 생성했을 때, 상대방의 연계 파일 적재 오류를 해결했다.

기본적으로 JBoss EAP 7.4에서 운영 중에 있으나, 특정 서비스의 요구사항으로 JBoss EAP 8.0을 설치한 서버가 있다. standalone.xml의 설정 과정에서 datasource 서브시스템의 security 절을 동일하게 사용하고, security 서브시스템으로 username과 password를 관리하려 했으나 8.0에서는 security 서브시스템을 지원하지 않고, elytron 서브시스템을 사용해야한다.

<subsystem xmlns="urn:jboss:domain:datasources:7.0">
  <datasources>
    <datasource jndi-name="java:/fooDs" pool-name="fooDs" enabled="true" use-java-context="true">
      <connection-url>...</connection-url>
      <driver>oracle</driver>
      <!-- 생략 -->
      <security>
        <security-domain>fooDs</security-domain>
      </security>
    </datasource>
  </datasources>
</subsystem>

<subsystem xmlns="urn:jboss:domain:security:2.0">
  <security-domains>
    <security-domain name="fooDs" cache-type="default">
      <authentication>
        <login-module code="org.picketbox.datasource.security.SecureIdentityLoginModule" flag="required">
          <module-option name="username" value="foo"/>
          <module-option name="password" value="bar"/>
        </login-module>
      </authentication>
    </security-domain>
  </security-domains>
</subsystem>

 

7.4에서 쓰던 설정이고, 그대로 8.0에 옮겼을 때는 앞에서 말했듯이 오류가 난다.

/subsystem=elytron/credential-store=fooDs-credstore:add( \
    path=fooDs-credstore.cs, \
    relative-to=jboss.server.config.dir, \
    create=true, \
    credential-reference={clear-text="5dOaAVafCSd;12345678;100} \
)

/subsystem=elytron/credential-store=fooDs-credstore:add-alias(\
    alias=fooDs-username, \
    secret-value="foo" \
)

/subsystem=elytron/credential-store=fooDs-credstore:add-alias(\
    alias=fooDs-password, \
    secret-value="bar" \
)

 

우선 security를 elytron으로 변경하기 위해 jboss-cli에서 elytron credential store를 생성한 뒤에, store에 username과 password를 각각 저장해준다.

<security>
  <credential-reference>
    <store>fooDs-credstore</store>
    <alias>fooDs-username</alias>
    <clear-text-credential-reference>
      <store>fooDs-credstore</store>
      <alias>fooDs-password</alias>
    </clear-text-credential-reference>
  </credential-reference>
</security>

 

설정한 datasource에서 security 구문만 바꿔줬더니 오류가 난다.

<security>
  <credential-reference store="fooDs-credstore" alias="fooDs-username"/>
  <credential-reference store="fooDs-credstore" alias="fooDs-username"/>
</security>

 

store랑 alias를 credential-reference의 속성으로 넣어봐도 오류가 난다.

<subsystem xmlns="urn:jboss:domain:datasources:7.1">

 

datasources 서브시스템을 7.0에서 7.1로 올리면 store를 사용할 수 있다고 해서 변경하였으나 오류가 난다.

<security>
  <user-name>foo</user-name>
  <credential-reference store="fooDs-credstore" alias="fooDs-username"/>
</security>

 

다시 datasources 서브시스템을 7.0으로 바꾸고 username을 user-name 태그로 변경하니 성공했다.

/subsystem=datasources/data-source=fooDs:test-connection-in-pool
{
    "outcome" => "success",
    "result" => [ture]
}

 

username은 평문으로 쓰고, password만 elytron credential store에 등록해서 설정하면 되는듯하다.

서비스 운영 중 일부 js 파일 등 static content의 노출로 인한 취약점 해결을 위해서 찾아보았다. 사용자의 직접 접근 요청을 차단하고, 설정한 referer의 경우에만 Directory에 접근하여 기능이 정상적으로 동작하도록 한다.

SetEnvIf Referer ^http(s)?:\/\/ allow_access
...
<Directory "/foo/bar">
    Require all denied
    Require env allow_access
</Directory>

 

Apache의 mod_setenvif 를 참고하면 SetEnvIf 지시어로 환경변수를 설정할 수 있다. 첫번째 인자는 attribute로 HTTP 요청 헤더나 Remote_Host, Remote_Addr 등을 지정하고, 두번째 인자로 정규표현식을 지정하는데 regex가 attribute에 대응하면 된다. 세번째 인자는 변수명이다.
 
따라서, 설정한 값은 HTTP 요청 헤더 중 Referer 헤더가 http:// 또는 https://로 시작하면 allow_access가 참이 된다. 그리고 VirtualHost에서 Directory 지시자로 지정한 경로(서버 기준의 절대경로)에 allow_access 대응했을 경우에만 접근이 가능하다. 목적은 직접 접근 요청 차단이어서 해결 완료(실제 서버에는 Referer 정규표현식을 더 상세하게 작성함).

일년에 몇 번 안 쓰는 기능을 쓰다보니 오류 때문에 진행이 안 된다고 연락이 왔다.

JBWEB002004: More than the maximum number of request parameters (GET plus POST) for a single request (10000) were detected. Any parameters beyond this limit have been ignored. To change this limit, set the maxParameterCount attribute on the Connector.

 

처음에는 제대로 안 읽고 single request 뒤의 (10000)에만 집중해서 WEB 서버에 접속해 Apache의 ssl.conf 파일을 여니 LimitRequestFields가 딱 10000이었다. 문제는 스테이징 서버도 동일 설정 값이어서 운영 서버에서 오류가 나는게 이상한 상황이었지만, 일단 10000에 꽂혔으니 늘려봤다. 당연히 결과는 허탕.

<system-properties>
    ...
    <property name="org.apache.tomcat.util.http.Parameters.MAX_COUNT" value="20000"/>
</system-properties>

 

찾아보니 JBoss의 standalone.xml에 위 내용을 추가해보라고 해서 추가하니 잘 됐다. 기본값이 10000이었다.

+ Recent posts