블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

Locust 와 쿠버네티스를 이용한 분산 부하 테스트

조대협 (http://bcho.tistory.com)

분산 부하 테스트

locust는 여러개의 worker를 이용하여, 부하를 대량으로 발생 시키는 분산 부하 테스트가 가능하다. 특히 분산 클러스터 구성 설정이 매우 간단하다는 장점을 가지고 있다. 


마스터 노드의 경우에는 아래와 같이 --master 옵션을 지정하여 마스터 노드로 구동하면 되고, 

% locust -f {task file name} --host={target host address} --master


워커 노드의 경우에는 실행 모드를 slave로 하고, 마스터 노드의 주소만 명시해주면 된다. 

% locust -f {task file name} --host={target host address} --slave --master-host={master node의 주소}


이렇게 클러스터를 구성하면 아래와 같은 구조를 갖는다


특히 워크노드를 추가할때 별도의 설정이 필요없이 워커노드를 추가하면서 마스터 노드의 주소만 주면 되기 때문에, 스케일링이 편리하다. 

쿠버네티스에서 분산 부하 테스트

설정이 간단하고, 부하 테스트의 특성상 필요할때만 클러스터를 설치했다가 사용이 끝나면 없애는 구조로 사용을 하려면, 쿠버네티스에서 기동하면 매우 편리하다. 


특히 설치 및 설정을 Helm Chart로 만들어놓으면, 필요할때 마다 손쉽게 locust 클러스터를 설치했다가 사용이 끝나면 손쉽게 지울 수 있는데, Helm 공식 리파지토리에 등록된 locust chart는 https://github.com/helm/charts/tree/master/stable/locust 아쉽게도 locust 0.9를 기준으로 한다. 현재는 1.3 버전이 최신 버전인데, 0.9와 1.3은 설정고 TaskSet의 코딩 방식이 다소 변경되서, locust 0.9를 사용하게 되면, 현재 메뉴얼에 있는 일부 코드들을 사용할 수 없다. 

그래서, 스스로 Helm Chart를 만들어서 사용하기를 권장한다. 


좋은 소식중의 하나는 구글 클라우드에서 Locust 를 쿠버네티스에서 분산 부하 테스팅을 할 수 있는 가이드 문서를 제공하고 있으니, 이를 참조하면 손쉽게 사용이 가능하다.

https://cloud.google.com/solutions/distributed-load-testing-using-gke

단, helm chart 형태로는 제공하지 않으니, 스스로 Chart를 만들기를 권장한다. Chart를 만들때 기존 Chart 구조를 참고하기를 권장하는데, https://github.com/helm/charts/tree/master/stable/locust 를 보면, locust의 이미지 버전이나, 설정을 Helm의 values.yaml로 빼거나 또는 CLI parameter로 처리할 수 있게 하였고, 특히 아래 task files를 설정하는 방법이 매우 흥미로운데, 


task file을 도커 이미지 내에 넣는 방식이 아니라 file (또는 directory) 형태의 configmap으로 빼서, locust pod가 기동될때, 이 파일(디렉토리)를 마운트해서 스크립트를 읽어서 적용하는 구조를 가지고 있다.


위에서 언급한 구글 클라우드 플랫폼의 가이드는 task 파일을 docker 이미지로 매번 빌드하기 때문에, 다소 설치가 복잡해질 수 있다는 단점이 있다. 


그러면 구글 클라우드 플랫폼에서 제공하는 예제 파일들을 살펴보자

https://github.com/GoogleCloudPlatform/distributed-load-testing-using-kubernetes 리포에 저장이 되어 있는데, 

/docker-image 디렉토리

이 디렉토리에는 locust 도커 컨테이너를 만들기 위한 설정 파일들이 들어 있다. 

Dockerfile의 내용을 보면 아래와 같다.

# Start with a base Python 3.7.2 image

FROM python:3.7.2

 

# Add the licenses for third party software and libraries

ADD licenses /licenses

 

# Add the external tasks directory into /tasks

ADD locust-tasks /locust-tasks

 

# Install the required dependencies via pip

RUN pip install -r /locust-tasks/requirements.txt

 

# Expose the required Locust ports

EXPOSE 5557 5558 8089

 

# Set script to be executable

RUN chmod 755 /locust-tasks/run.sh

 

# Start Locust using LOCUS_OPTS environment variable

ENTRYPOINT ["/locust-tasks/run.sh"]


주의 깊게 살펴볼 부분은 아래 두줄이다. 

ADD locust-tasks /locust-tasks

RUN pip install -r /locust-tasks/requirements.txt


첫번째는 locust task 스크립트를 /locust-tasks 디렉토리에 복사하는 부분이다. 

그리고 두번째는 스크립트에서 사용하는 파이썬 라이브러리를 설치하는 부분이다. 

kubernetes-config 디렉토리


locust-master-controller.yaml

apiVersion: "extensions/v1beta1"

kind: "Deployment"

metadata:

 name: locust-master

 labels:

   name: locust-master

spec:

 replicas: 1

 selector:

   matchLabels:

     app: locust-master

 template:

   metadata:

     labels:

       app: locust-master

   spec:

     containers:

       - name: locust-master

         image: gcr.io/[PROJECT_ID]/locust-tasks:latest

         env:

           - name: LOCUST_MODE

             value: master

           - name: TARGET_HOST

             value: https://[TARGET_HOST]

         ports:

           - name: loc-master-web

             containerPort: 8089

             protocol: TCP

           - name: loc-master-p1

             containerPort: 5557

             protocol: TCP

           - name: loc-master-p2

             containerPort: 5558

             protocol: TCP


이 파일은 master node를 배포하기 위한 deployment 정의다. 주의 깊게 살펴볼 부분은 아래 두 라인이다. image는 앞에서 만든 task 스크립트가 포함된 locust 도커 이미지를 정의 하는 부분이고, 두번째는 부하 테스트 대상이 되는 타겟 호스트 주소를 정의하는 부분이다. 


image: gcr.io/[PROJECT_ID]/locust-tasks:latest

value: https://[TARGET_HOST]


다음은 locust-master-service.yaml 파일이다.

kind: Service

apiVersion: v1

metadata:

 name: locust-master

 labels:

   app: locust-master

spec:

 ports:

   - port: 8089

     targetPort: loc-master-web

     protocol: TCP

     name: loc-master-web

   - port: 5557

     targetPort: loc-master-p1

     protocol: TCP

     name: loc-master-p1

   - port: 5558

     targetPort: loc-master-p2

     protocol: TCP

     name: loc-master-p2

 selector:

   app: locust-master

 type: LoadBalancer


앞서 정의한 master node를 External IP (Load Balancer)로 밖으로 빼서, 웹 콘솔 접속등을 가능하게 하는 설정인데, 주의할점은 locust 웹 콘솔은 별도의 사용자 인증 기능이 없기 때문에, 위의 설정을 그대로 사용하면, 인증 기능이 없는 웹콘솔이 인터넷에 그래도 노출이 되기 때문에, 권장하지 않는다.


ClusterIP로 Service를 정의한 후, kubectl의 port forwarding 기능을 사용하거나 또는 앞에 인증용 프록시 서버를 두는 것을 권장한다. 구글 클라우드의 경우에는 Identity Aware Proxy(IAP)라는 기능을 이용하여, 구글 클라우드 계정으로 로그인을 해야 웹사이트를 접근할 수 있는 프록시 기능을 제공한다. 


마지막으로 locust-worker-controller.yaml 파일을 정의한다.

# Copyright 2015 Google Inc. All rights reserved.

#

# Licensed under the Apache License, Version 2.0 (the "License");

# you may not use this file except in compliance with the License.

# You may obtain a copy of the License at

#

#     http://www.apache.org/licenses/LICENSE-2.0

#

# Unless required by applicable law or agreed to in writing, software

# distributed under the License is distributed on an "AS IS" BASIS,

# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

# See the License for the specific language governing permissions and

# limitations under the License.

 

apiVersion: "extensions/v1beta1"

kind: "Deployment"

metadata:

 name: locust-worker

 labels:

   name: locust-worker

spec:

 replicas: 5

 selector:

   matchLabels:

     app: locust-worker

 template:

   metadata:

     labels:

       app: locust-worker

   spec:

     containers:

       - name: locust-worker

         image: gcr.io/[PROJECT_ID]/locust-tasks:latest

         env:

           - name: LOCUST_MODE

             value: worker

           - name: LOCUST_MASTER

             value: locust-master

           - name: TARGET_HOST

             value: https://[TARGET_HOST]

 


이 파일을 worker node 에 대항 설정으로 master node와 마찬가지로 target_host와 locust 이미지 경로만 지정해주면 된다. 


이 파일들을 배포하면, locust 가 실행 준비가 되고, 앞에서 정의한 master node의 서비스 주소로 접속하면 locust 웹 콘솔을 이용하여 부하 테스트를 진행할 수 있다. 


만약 동적으로 locust worker node의 수를 조절하고 싶으면, 

% kubectl scale deployment/locust-worker --replicas=20


명령을 이용하여 worker node pod의 수를 조절할 수 있다. 

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

부하테스트를 위한 Locust

조대협 (http://bcho.tistory.com)


백앤드 개발을 하다보면 많이 사용되는 도구 중의 하나가 부하 테스트 툴인데, 대표적인 도구로는 Apache Jmeter, nGrinder,SOAP UI 등의 도구가 있지만 다소 사용이 어렵고 스케일링을 하는데 어려움이 있는데, locust라는 도구는 설치와 사용이 편리하고, 테스트 시나리오를 파이썬 스크립트로 작성을 하기 때문에 다양한 시나리오 구현이 가능하다. 특히 쿠버네티스에 쉽게 배포할 수 있도록 Helm으로 패키지화가 되어 있기 때문에, 필요한 경우 대규모 부하테스트 환경을 설치하고 테스트가 끝나면 쉽게 지워버릴 수 있다. 

(참고 : locust는 영어로 메뚜기라는 뜻인데, 부하를 주는 것을 swarming 이라고 표현하는게 메뚜기 떼로 부하를 비교한것이 재미있다.)


퀵스타트

간단하게 설치 및 테스트를 해보자. 

설치

설치는 파이썬만 설치되어 있으면 간단하게 pip 명령을 이용해서 설치 할 수 있다.

%python -m pip install locust

명령으로 설치 하면 된다.

설치 확인은 locust --version 명령으로 확인하면 된다.  이 방식이외에도, 이미 빌드된 도커 이미지를 사용해서 테스트를 하거나 또는 쿠버네티스에서 Helm으로 설치해서 실행하는 방법도 가능하다. 쿠버네티스에서 Helm을 사용하는 방법은 나중에 자세하게 다루도록 한다. 

간단한 부하 스크립트 생성

locust 부하테스트는 파이썬 스크립트를 실행하는 방식으로 하기 때문에, 부하테스트용 스크립트 파일을 작성해야 한다. 아래는 대상 호스트의 / 에 HTTP GET으로 요청을 보내는 스크립트이다. 스크립트 작성방법은 후에 다시 설명하도록 한다. 


from locust import HttpLocust,TaskSet,task,between

class MyTaskSet(TaskSet):

   @task

   def index(self):

       self.client.get("/")

class MyLocus(HttpLocust):

   task_set = MyTaskSet

   wait_time = between(3,5)

실행하기

locust 실행은 locust -f {스크립트 파일명} {웹포트번호} 형식으로 실행하면 된다.

locust 서버가 기동되고, 부하를 줄 준비가 된 상태가 된다. 실제 부하 생성은 locust 웹콘솔에 접속해서 해야 한다.  아래 스크립트는 8080트로 locust 웹 클라이언트를 기동하도록 한 명령이다. 

%locust -f ./locustfile.py --port 8080


http://localhost:8080 으로 접속하면 아래와 같이 웹 콘솔이 나온다. 


첫번째 인자는, 몇개의 클라이언트를 사용할것인지 (메뚜기를 몇마리 만들것인지)를 정의하고, 두번째는 Hatch rate 라고 해서, 초마다 늘어나는 클라이언트 수 이다. 처음에는 클라이언트가 1대로 시작되서, 위의 설정의 경우 초마다 하나씩 최대 30개까지 늘어난다.

그리고 마지막은 테스트할 웹 사이트 주소를 입력한다.


Start Swarming 버튼을 누르게 되면 부하테스트가 시작되서 아래와 같이 부하 테스트 화면이 실시간으로 출력된다. 



몇 위의 화면은 Total Request Per Second로, 초당 처리량이고, 두번째는 응답 시간, 그리고 마지막은 현재 클라이언트 수 를 모니터링 해준다. 


No Web

위의 부하테스트는 웹 콘솔을 열어서, 웹 사이트로 부하를 주는 시나리오인데, 웹 부하 테스트가 아니라, 데이타베이스의 부하테스트와 같은 경우에는 웹 사이트의 주소를 정의할 수 없다. 이런 경우에는 CLI로 실행이 가능한데, --no-web 옵션을 주면 된다.

%locust -f ./nested-locust.py --no-web -c 2 -r 1 


위의 명령은 nested-locust.py 파일을 실행하되, --no-web으로 웹으로 부하를 주지 않고, -c 2 로 2개의 클라인트에 대해서 -r 1 1초마다 클라이언트를 (2까지) 하나씩 올리는 부하 테스트 시나리오이다. 웹 UI는 테스트 상태를 모니터링 하고 사람이 쓰기 좋지만 만약에 테스트 과정을 CI/CD 파이프라인내에 넣거나 또는 자동화하고 싶을때는 이렇게 CLI 를 이용할 수 있다. 

코딩 방법

사용방법을 이해하였으면, 이제 스크립트 작성 방법을 살펴보자.

스크립트는 Locust (클라이언트/메뚜기)를 정의해서, Locust의 행동 시나리오 (TaskSet)를 정의해야 한다. 

Locust

지원되는 Locust 종류는 HttpLocust와, Locust 두가지가 있다. HttpLocust는 웹 부하테스트용 클라이언트이고 범용으로 사용할 수 있는 클라이언트 (예를 들어 데이타 베이스 테스트)용으로 Locust라는 클래스를 제공한다. 하나의 부하테스트에서는 한 타입의 클라이언트만이 아니라 여러 타입의 클라이언트를 동시에 만들어서 실행할 수 있다.

예를 들어서 하나의 부하 테스트에서 안드로이드용 클라이언트와, iOS용 클라이언트를 동시에 정의해서 부하 비율을 정의해서 테스트(안드로이드,iOS = 7:3으로) 하는 것이 가능하다.

TaskSet

클라이언트(메뚜기)를 정의했으면, 이 클라이언트가 어떻게 부하를 줄지 시나리오를 정의해야 하는데, 이를 TaskSet이라고 한다. 

TaskSet는 Task의 집합으로 예를 들어, 테스트 시나리오에 리스트 페이지 보기, 상품 선택하기, 댓글 달기 등의 Task등을 정의할 수 있다.

Task

Task는 @task 라는 어노테이션으로 정의하면, TaskSet이 실행될때, TaskSet안에 있는 task를 random 하게 선택해서 실행한다. 아래 예제를 보자


class MyTaskSet(TaskSet):

    wait_time = between(5, 15)


    @task(3)

    def task1(self):

        pass


    @task(6)

    def task2(self):

        pass


위의 예제는 task1과, task2 를 랜덤하게 선택해서 실행하게 하는데, 괄호안의 숫자는 가중치가 된다. 즉 위의 TaskSet은 task1과 task2를 3:6의 비율로 실행하도록 한다.

wait_time

Task를 실행한 다음에 다른 Task를 수행할때 까지 delay 타임을 줄 수 있는데, TaskSet 클래스에 wait_time이라는 attribute로 정의되어 있다. 여기에 값을 지정해주면, 그 시간만 기다렸다가 다음 task를 수행한다. 위의 예제에서는 between(5,15)를 이용해서 5~15초 사이 시간 만큼 랜덤하게 기다렸다가 다음 task를 실행하도록 하였다. 

TaskSequence  & seq_task

TaskSet의 다른 종류로 TaskSequence 라는 클래스가 있는데, TaskSet이 task를 랜덤하게 실행한다고 하면, TaskSequence는 Task를 순차적으로 실행한다. 웹 테스트를 보면, 웹사이트에서 로그인하고, 초기 화면을 들어가고, 상품 목록 페이지로 이동하고 다음 상품 상세 페이지를 보는 것과 같은 순차적인 테스트가 필요할 수 있는데, 이를 지원하기 위한것이 TaskSequence 이다.

TaskSet과 다르게, TaskSequence내에서 순차적으로 실행해야하는 task는 seq_task()로 정의하고, 괄호 안에는 그 순서를 정의한다. 

아래 예제를 보자


class MyTaskSequence(TaskSequence):

    @seq_task(1)

    def first_task(self):

        pass


    @seq_task(2)

    def second_task(self):

        pass


    @seq_task(3)

    @task(10)

    def third_task(self):

        pass


위의 예제는 first_task를 실행하고, 두번째는 second_task를 실행한 후에, 세번째는 third_task가 실행되는데, @task(10) 으로 지정되어 있기 때문에, 10번이 실행된다. 

Nesting

Locust에서 TaskSet은 다른 TaskSet을 호출(Nest) 하는 것이 가능하다. 

예를 들어 부하 테스트 시나리오에서, 게시판에 글을 쓰는 시나리오와 상품을 구입하는 시나리오가 하나의 클라이언트에서 동시에 일어난다고 했늘때, 게시판에 글을 쓰는 TaskSet과, 상품을 구입하는 TaskSet을 정의하고 전체 시나리오에서 이 두 TaskSet을 호출하는 것과 같이 반복적이고 재사용적인 시나리오를 처리하는데 사용할 수 있다. 

아래 예제를 보자, 아래 예제는 UserBahavior TaskSet에서 Nested 라는 TaskSet을 호출하는 예제이다. 


from locust import Locust,TaskSet,task,between


class Nested(TaskSet):

        @task(1)

        def task1(self):

                print("task1")


        @task(1)

        def task2(self):

                print("task2")


        @task(1)

        def stop(self):

                print("stop")

                self.interrupt()



class UserBehavior(TaskSet):

        tasks = {Nested:2}


        @task

        def index(self):

                print("user behavior task")



UserBehavior에는 index라는 Task와 Nested TaskSet이 정의되어 있고, Nested TaskSet은 가중치가 2로 되어 있기 때문에, index task에 비해서 2배 많이 호출된다.

Nested TaskSet은 task1,task2,stop을 가지고 있느넫, 각각 가중치가 1로 이 셋중 하나가 랜덤으로 실행되는데, Nested TaskSet이 실행이 시작되면, 계속 Nested TaskSet 안의 task들만 실행이 되고, 그 상위 TaskSet인 UserBehavior가 원칙적으로는 다시 호출되지 않는다. 즉 Nested TaskSet의 task들로 루프를 도는데, 그래서 stop task에 self.intrrupt를 호출해서, Nested TaskSet 호출을 멈추고, UserBehavior TaskSet으로 리턴하도록 하였다. 

Hook

TaskSet은 실행 전후에, Hook을 정의할 수 있다.

setup & teardown

setup과, teardown은 TaskSet이 생성되었을때와 끝나기전에 각각 한번씩만 수행된다. 

locust가 실행되면, TaskSet 별로 정의된 setup 메서드가 실행되고, 프로그램을 종료를 하면, 종료하기전에 teardown 메서드가 실행된다. 

setup과 teardown은 테스트를 위한 준비와 클린업등에 사용할 수 있는데, 예를 들어 테스트를 위한 데이타 베이스 초기화 등에 사용할 수 있다. 주의 할점은 클라이언트를 여러개 만든다고 하더라도, TaskSet의 setup과 teardown은 각각 단 한번씩만 실행이 된다.

on_start,on_stop

setup과 teardown이 전체 클라이언트에서 한번만 실행된다면, 클라이언트마다 실행되는 Hook은 on_start와  on_stop이 된다. 

on_start는 클라이언트가 생성될때 마다 한번씩 실행된다. 


from locust import Locust,TaskSet,task,between


class UserBehavior(TaskSet):

def setup(self):

print("nested SETUP");


def teardown(self):

print("nested TEAR DOWN");


def on_start(self):

print("nested on start");


def on_stop(self):

print("nested on stop");


@task(1)

def task1(self):

print("task1")


@task(1)

def task2(self):

print("task2")



class User(Locust):

task_set = UserBehavior

wait_time = between(1,2)


코드를 

%locust -f ./locust.py --no-web -c 2 -r 1 

명령으로 2개의 클라이언트를 실행하게 되면 on_start는 단 두번 실행이 된다.

Locust setup & teardown

TaskSet의 Hook에 대해서 알아봤는데,  테스트 클라이언트인 Locust class도, setup과 teardown hook을 가지고 있다. 하나의 Locust 클라이언트는 여러개의 TaskSet을 가지고 있기 때문에, TaskSet마다 정의된 setup과 teardown이 실행되지만, Locust는 하나만 존재하기 때문에, 전역적으로 setup과  teardown은 정확하게 단 한번만 수행이 된다. 

Locust와 TaskSet의 실행 순서를 보면 다음과 같다. 


실행 순서

  • Locust setup

  • TaskSet setup

  • TaskSet on_start

  • TaskSet tasks…

  • TaskSet on_stop

  • TaskSet teardown

  • Locust teardown




본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

부하테스트 툴 검토 결과

성능과 튜닝 | 2012. 10. 29. 22:47 | Posted by 조대협

항상 성능 테스트를 준비할때 마다, 어떤 부하 테스트툴을 쓰느냐가 고민이다.

어느정도 틀이 갖춰진 조직이나, 예산이 충분한 경우에는 뒤도 안돌아보고 파트너 불러다가 Load Runner돌린다.


1. Load Runner

국내 인력도 많고, 다양한 시나리오에 대한 부하 테스트를 안정적으로 할 수 있어서 언제나 #1 옵션이지만, 비용이 살인적이다.


2. 기타 오픈소스

그래서 오픈소스를 검토해봤는데,

Apache JMeter의 경우 예전에 하도 실패를 많이 했고, 테스트 중 GC 발생도 그렇고, 툴 자체가 매우 세밀하게 사용을 해야 한다. 부하 테스트 툴이라기 보다는 부하테스트 프레임웍 정도로 보는게 맞지 않을까?

그 다음으로 예전에 Multimechnizer라는 툴을 고민했었는데, Erlang기반이고 Python으로 스크립트를 짠다. Erlang기반이라서 분산 환경을 기반으로한 대규모 부하테스트가 가능하고 신뢰성도 있었으나, Learning Curve가 커서 접어야 했던 제품

근래에 툴들을 리서치 해보니,

Grinder를 기반으로 해서, NHN이 Curbrid를 DB로 바꾸고, WEB UI를 사용한 nGrinder라는 제품을 국내에서는 꽤 많이 쓰는듯 하다. 오픈소스이긴 한데, 글로벌 개발팀(해외에 개발팀이 있는 경우)에는 기술 지원이나 자료 부족등의 이유 때문에 망설여지는 툴

다음으로 LoadUI라는 도구다. 라이센스가 대략 10,000USD정도 하는데, SOAPUI를 만들었던 회사의 제품이다. UI나 저작 도구가 매우매우 특이하다. Mashup 제품을 보는 느낌이라고 할까?

일단 기술 지원이 가능하니 심각하게 고려하고 있는 제품


이외에, 국내 블로그를 찾아보니 Apache AP나 HP HTTPerf(근래에 구글에 인수됨)이 있는데, 이 제품들은 웹서버에 성능 테스트에 최적화 되어 있다. 즉, 대규모의 HTTP 부하를 날리 수 는 있지만, HTTP message를 programatic하게 생성한다던가 (ID를 로직에 따라서 생성). HTTP Response를 비교하는 등의 기능이 없다. 단순하게 부하만 생성해서 날려 주는 거라서, 말 그대로 웹서버의 성능 측정은 가능할지 몰라도, 애플리케이션 로직에 대한 부하 테스트는 불가능 한것으로 보인다.


본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. 정경석 2017.09.14 11:03  댓글주소  수정/삭제  댓글쓰기

    안녕하세요. 글 내용 중 오타로 보이는게 있습니다. "Apache AP"가 "Apache AB"의 오타로 보입니다.