세월은 빠르게 흘러, 시스템 관리에도 고전적인 perl / python 과 같은 언어를 제치고 ruby 가 등장하게 되었다.
Chef 라고 하면 이미 나온지 그렇게 오래 된 툴은 아니지만, 그렇다고 얼마 되지 않은 것도 아니다.
이 글을 쓰게 되는 것도 번역이나 샘플 코드 조사를 통한 기본적인 동작의 이해를 위한 것임을 미리 밝히며, 시스템 자동화를 구현 할 수 있는 이런 툴을 개발하고 또 미리 알아내어 접하는 분들에게 새삼 놀랄 따름이다.
우분투에서의 ruby 인스톨은 아주 쉬운 과정으로 이루어 지며, 이거 왜 안되 할 만한 부분이 있어 별도로 소개한다.
( 우분투에서는 gem update --system 을 지원하지 않는다. 패키지 매니저로 처리 하라는 친절한 시스템사마의 설명 )
하다 보면 여기서 에러를 줄줄이 뱉어 낼 것이다. 에러를 살펴보면, extconf.rb:1:in 'require': no such file to load - mkmf 라는 에러 메세지 인데, 결국 extconf.rb 는 ruby-dev 패키지를 참조하기 때문에 이 패키지를 설치 하지 않으면 정상적으로 동작하지 않는다. 따라서, 정상적으로 진행하기 위해서는
# sudo apt-get install ruby-dev
해 준다. 이후 다음의 링크에서 제공하는 Chef 를 시작하는데 안성맞춤인 내용을 따라하는데 우분투로 충분 할 것이다.
Chef 의 가장 매력적인 점은 확장 및 축소가 매우 쉽다는 점이다. 처음 시작할 때는 chef-solo 로 쉽게 시작하고, 이후 필요에 따라 chef-client 나 chef-server 를 통해 원하는 대로 구현이 가능해 진다. Chef 로 작성한 동일한 recipe 를 가지고 웹 서버, 데이터베이스, 어플리케이션 , couchdb 서버 및 기타 등등의 거의 모든 서버에 대한 수평적 확장이 가능하다.
구체적 설명은 됬고, 일단 간단한 recipe 를 통해 rubygem 의 일부 패키지를 설치 해 보기로 한다. 그리고, chef 가 베이스 시스템을 구성한 이후에 간단한 디렉토리 설정과 어플리케이션을 배포 해 보도록 하겠다 .
# gem install chef ohai --source http://gems.opcode.com --source http://gems.rubyforge.org
( 일부 패키지 의존성 문제로 인해 rubygem 이 gems.opcode.com 에 의해 서비스 받고 있음에도 불구하고 rubyforge 를 소스로 추가해 주는 것이 좋다. Rubygem 은 그런식으로 동작한단다. )
여기까지 준비 되었으면 이제 실행시켜 보면 된다. ( 이 단계는 일부 디렉토리 생성 및 어플리케이션 들을 인스톨 하기 때문에 원치 않으면 넘어가도 관계 없다. )
실행하기전, config/dna.json 의 user 설정을 변경 할 필요가 있다. 소스에는 'ez'로 되어있을 것이나, 시스템에 나와 같이 'ez' 계정이 없다면 변경 해 주는 것이 좋다.
template "/data/someservice.conf" do
owner node[:user]
mode 0644
source "someservice.conf.erb"
variables({
:applications => node[:apps]
})
end
include_recipe 로 필요한 recipe 를 순차적으로 호출하는 것을 볼 수 있는데, 이는 호출 순서에 따라 필요한 레시피가 동작하도록 설정 할 수 있게 하기 위함이다. 만약 다른 레시피를 먼저 구동하고 싶으면, 그 레시피를 다른 것보다 먼저 호출 하도록 한다.
자, 우리는 gem 레시피 이후 application 레시피를 호출하고, 이후 일부 디렉토리 작업을 수행 하는 것을 볼 수 있다. 이 이후작업에서 우리는 쉽게 템플릿을 정의하고, 네이밍 하여 해당 템플릿이 가져야 하는 속성의 정의를 구현하는 것을 볼 수 있다.
이제, gem 레시피를 보자.
node[:gems].each do |gem|
gem_package gem[:name] do
if gem[:version] && !gem[:version].empty?
version gem[:version]
end
if gem[:source]
source gem[:source]
end
action :install
end
end
여기서 우리는 loop 를 돌려가며 dna.json 에서 정의 되어 설치할 각 gem 의 버전을 가져오는 것을 볼 수 있다. Gem 은 버전과 소스를 가질 수 있으나, 주의할 점은 비어있으면 설정하지 않아야 한다. 버전을 정의하지 않으면 가장 최신의 버전을 가져오게 될 것이며, 소스를 지정하지 않으면 rubyforge.org 에서 가져오게 될 것이다.
결국 마지막 줄에, 우리는 action :install 으로서 각 gem 에 대한 인스톨을 수행한다. 이는, 시스템에 이미 해당 gem 이 설치 되어 있으면 다시 설치하지 않는다.
자 이제 application recipe 를 보면,
directory "/data" do
owner node[:user]
mode 0755
end
최근의 클라우드와 같은 대규모 분산처리가 점점 더 이슈화 되고, 발전하고 있다. 이는 각종 SnS 서비스 및 모바일 통신 환경의 발달로 인한 더 많은 사용자 층의 서비스 유입으로 인하여 자연스럽게 나타나는 시대의 플랫폼에 대한 요구가 아닌가 하는 생각이 든다.
서비스는 보다 더 복잡해 지고, 각 진영의 개발은 그 어느때 보다 활발하며, 치열하다. 기업간 경쟁은 Apple 을 필두로 Adobe 와의 분쟁, HTML5/CSS3 , 각종 확장된 Javascript 등은 플랫폼, 클라이언트 브라우저 등 모든 환경이 급변함으로 고인 물은 더욱 빨리 썩게 되고, 흐르는 빠른 물줄기를 쫒아 가자니 가랭이가 터질 지경이 아닌가.
더구나 그 물에 몸담그고 있다면, 또 그 물이 opensource 라면 각종 개발에 스크립트에 정신줄 놓는건 일도 아니다. ( 물론 학구열에 불타면 관계 없지만 매번 똑같은 일을 하지 않기 위해 짜는 스크립트는 항상 고뇌의 시간을 요구한다. )
뭐 서론의 헛소리가 길었지만, 어쨌든 제목과 같은 Framework는 예전 부터 있어왔나 보다.
다만 각종 Cloud 의 발전에 의해 좀 묻히지 않았나 하는 생각 과 함께, 기존 클라우드의 VM 으로서 Windows 서버를 굴리게 되더라도 IIS 의 성능 자체에는 MS 의 지원없이 할 수 없는 것들이 많이 있다.
그런 측면에서 이와같은 Framework 의 지원은 매우 반가운 일이며, 어떠한 형태로든 효율적인 IIS 의 Server Farm 을 구성 할 수 있다면 전단 웹 서버를 탄력적이고 확장가능하게 (MS에서 그렇다고 한다) 만들 수 있기에 서비스 구성에 고려할 만한 옵션이나 전략이 추가 되는 것이 아닌가 하는 생각이다.
물론, 요새 OpenNebula 2.0 이나 OpenStack 과 같이 마루타 할 것들이 많아져서 실제 구현을 한다거나 해 보지는 않았기에, 간단히 소개하는 정도로 하고 관계 되신 분들은 링크로 추가된 자료들을 검토해 보시면 될 듯 하다.
버전이 2.0 이니 1.0 부터 사용하신 분들도 많지 않겠나 하는 생각.
이와 같은 서버팜은 아마도 hosting 업체들에 유리하지 않겠나 하는 생각을 테스트없이 남발해 본다. 음;;;
오늘날, 웹서버 세팅과 각종 컨텐츠의 배포는 매우 고된 작업중 하나이다. 수많은 절차와 검증이 필요하며,플랫폼 관리를 위한 스크립트나 코드를 제작하여 힘겹게 운용하고 있다.
마이크로소프트 웹 팜 프레임웍 ( WFF ) 2.0 for IIS 7 은 호스팅 업체와 같이 수많은 서버를 관리해야 하는 사업체의 관리자에게 공급/확장/관리를 단순화 시켜줄 수 있는 도구이다. 관리자는 손쉽게 여러 서버를 확인할 수 있고, 컨텐츠의 배포 및 필요한 순간에 확장도 손쉽게 된다. WFF의 사용으로, 서버군에 대한 단일화된 업데이트 및 헬스체크등의 기능을 쉽게 사용 할 수 있다. 이러한 도구의 사용 잇점은 역시, 보다 적은 비용 및 리스크로 서비스를 동일하게 구현 할 수 있다는 것이다.
주요 기능.
WFF 2.0 에 포함된 주요 기능은 다음과 같다.
* Server Farm 에 서버를 한방에 추가 할 수 있다.
* Web PI ( Web Platform Installer ) 를 사용해 Platform Provisioning 을 구현 할 수 있다.
* Web Deploy 를 사용해 Application Provisioning 을 구현 할 수 있다.
* 정책기반 Provisioning
* 추가적인 Platform components 및 contents 의 설치
* ARR ( Application Request Routing ) 을 사용한 부하 분산을 통해 서비스 업타임 증가.
* Farm 에 속한 서버들의 update 상태 및 Log 추적
* 확장 가능한 모델을 통해 추가적인 Provider 들의 write 허용. ( 뭐 그냥 확장 가능하다는 말인듯 )
Key terms and concepts
간단한 개념 설명.
Server Farm
- Web Farm 으로 불리우는 관리/공급/배포의 단순화를 위해 묶인 서버의 그룹
Controller Server
- Server Farm 에 속한 서버들의 공급을 관리
Primary Server
- 플랫폼에 설치된 어플리케이션 및 컴포넌트를 정의하기 위해 설정된 서버. 여기에 설치된 것들은 Server Farm 의 다른 서버( Secondary Servers ) 로 동기화 됨.
Secondary Server
- Primary Server 로 부터 platform application / components / configuration settings / content / application 을 받아 동기화 되는 서버들. ( Primary 를 제외한 나머지 모든 서비스 서버를 말함 )
Platform Requirements
Server Farm 에 구성되는 서버들은 다음의 사항을 충족 해야 한다.
* Windows Server 2008 or Windows Server 2008 R2
* .NET 2.0 또는 그 이상버전의 설치
* 다음중 하나의 계정
- Server Farm 의 전체 서버들이 동일한 로컬 Administrator ID 와 Password 을 가지고 있거나,
- Domain Account 에 Administrator 로 등록된 계정이 각 로컬에 동일하게 존재할 것 ( 쉽게 말해서 AD 하거나 )
* Server Farm 의 전체 서버들이 Access 가능한 네트워크에 위치하고 있어야 할 것.
방화벽 설정
다음의 두가지를 방화벽에서 허용 해 주어야 한다.
* Core Networking
* Remote Administration
Controller Server Requirements
컨트롤러 서버는 다음의 요구사항을 충족 해야 한다.
* 다음중 하나의 OS 사용 : Windows Vista with SP1 / Windows 7 / Windows Server 2008 with SP1 / Windows Server R2
* Microsoft Web Platform Installer ( WEB PI ) 가 설치 되어 있을 것
* IIS 7 이 설치 되어 있을 것
* Microsoft Web Deploy module for IIS 가 설치 되어 있을 것
note : Web Deploy 모듈이 설치 되어 있지 않다면, Web PI 설치 시에 dependency 에 따라 자동으로 설치가 진행 될 것이다.
Provisioning requirements
시스템 종류에 따른 요구사항.
Secondary / Primary
32-bit (x86)
64-bit (x64)
32-bit (x86)
Supported
Not supported
64-bit (x64)
Supported
Supported
간단하게 정리하면, Primary 서버가 32bit 이면, Secondary 서버는 반드시 32bit 여야 한다.
Primary 서버가 64bit 이면 Secondary 서버는 32bit 나 64bit 모두 관계 없다.
OS 요구사항.
Secondary / Primary
Windows Server 2008
Windows Server 2008 R2
Windows Server 2008
Supported
Not Supported
Windows Server 2008 R2
Supported
Supported
동일한 관계.
WFF 2.0 for IIS 7 cmdlets for Windows Powershell
WFF 2.0 역시 파워쉘로 관리가 가능하다.
파워쉘을 사용 하기 위해선,
1. Controller 서버에서 cmd 실행
2. 파워쉘을 실행하기 위해 다음의 커맨드를 실행 ( PowerShell )
3. 파워쉘 프롬프트에서 다음의 커맨드 실행
Add-PSSnapin WebFarmSnapin
4. 다음과 같은 쉘 프롬프트가 보이면 성공
Get-Command WebFarmSnapin\*
커맨드의 리스트는 아래와 같음.
서버 관리를 위한 cmdlets
cmdlet Name
Description
Get-ActiveOperation
Returns the operations currently running on the server or server farm.
Get-AvailableOperation
Returns the operations available on the server or server farm.
Get-Server
Returns a list of servers in the farm, or the server specified.
Get-ServerProcess
Returns a list of the processes currently running on the server or server farm.
Get-ServerRequest
Returns a list of the requests currently being processed on the server or server farm.
Get-TraceMessage
Returns a list of the Trace messages from the server or server farm.
Get-WebFarm
Returns the name of the server farm or farms available.
Install-ServerProduct
Installs the specified product on the server or server farm.
New-MiniDump
Returns the dump information from the server.
New-Server
Add a server to an existing server farm.
New-WebFarm
Creates a new server farm.
Remove-Server
Removes a server from the server farm.
Remove-WebFarm
Removes a server farm.
Run-Operation
Executes the specified operation on the server or server farm.
Start-Server
Starts the specified server.
Stop-Server
Stops the specified server.
서버 팜 생성을 위한 예제.
파워쉘에서 다음의 커맨드 수행
New-WebFarm
다음과 같은 내용이 나옴
WebFarm 의 생성을 확인 하기 위해서는 다음의 커맨드 실행
Get-WebFarm
Server Farm 에 서버 추가를 위한 예제
파워쉘의 WFF cmdlets 에서 다음을 수행
New-Server
서버의 추가를 확인하기 위해서는 다음의 커맨드를 수행
Get-Server
날로 먹는 번역은 여기까지.
윈도우가 제공하는 많은 서버/시스템/플랫폼 관리 도구들은 그 최초 접근 및 구성/사용이 매우 쉬운것이 장점이다.
다만, 세부적인 문제가 발생했을때의 운용은 역시 실제 서비스에 도입 후 운용해 보아야만 알 수 있는 부분이 많으며,
장애가 발생한다 하더라도 MS 의 도움을 받아야 하는 경우가 많기 때문에 문제 처리에 지연이 발생 할 수 있다.
이러한 문제는 오히려 오픈소스 측면에서는 상당한 기술자가 존재하지 않으면 클라우드를 구성조차 ( 현시점에서 ) 하기 힘들다는 사실과 견주어 볼때 대단한 잇점이 될 수 있다. 물론 WFF 의 존재 가치가 클라우드와는 다르지만.
어쨌든, 사용하기 쉽고 장기적 관리가 용이한 신뢰할 만한 툴이 나온다는건 언제나 즐거운 일이다.
다만, 이를 사용한 서비스의 벤치마크 또는 성능 측정 및 배포시의 동기화 시간 등은 서비스 도입 전 반드시 체크해야 할 항목으로 두고 고려 해 보도록 하자.
파일 시스템의 클러스터링에 대해서는 이미 이슈화 된지 몇년이 지난 상태여서 본 내용에 대해서 이미 확인해 보거나 BMT 까지 수행 해 보신 분들도 많으시겠지만, 간략한 소개의 의미 및 nexenta.org 의 관련 내용 번역으로 간략히 포스팅 하고자 한다. pNFS 를 Nexenta 에서 서비스로 사용하는데에는 다소 신중함이 필요 하겠지만, VM 에서 여러대의 Nexenta 를 올려두고 간단히 테스트 하여 성능을 비교하는 재미는 쏠쏠하지 않겠는가.
pNFS 에서의 p 의 의미는, Parallel 이다. 전통적인 NFS v4에서 지원하는 단일 서버 - 클라이언트 서버 간의 Data 와 Meta Data 통신을 구분하여 병렬로 나누어 놓았다고 생각하면 이상적이라 할 수 있겠다.
클러스터 파일 시스템은 굉장히 여러가지 종류가 있고 구현 방법에 따라 설치가능한 패키지들이 있겠지만, 전통적으로 사용되는 NFS 에 대한 병렬 확장은 이미 2005년 경 부터 각 벤더 ( ex. EMC, IBM, NetApp ) 에 의해 연구되어 왔고, 종래에 와서 File 기반 서비스에 대해서는 어느정도 사용 할 만한 규모가 되었다 생각 한다.
물론 Block Level 의 클러스터FS 에 대해서는 여기서는 논외로 하고.
소개 ( 본 내용의 날짜는 2010년 01월 24일에 포스팅 된 wiki 를 근거로 한다 )
병렬 NFS ( pNFS )는 NFS v4.1 표준에 포함된, 클라이언트가 스토리지에 병렬로 직접 엑세스 하는 방법을 다루고 있다.
pNFS 는 오늘날 사용되고 있는 NFS의 확장성 및 성능상의 이슈를 해결하기 위하여 대두된 구조로서, 기본적인 아이디어는 데이터와 메타데이터를 분리하는 것이며, 이는 아래의 그림과 같이 전통적인 NFS 와 pNFS가 어떻게 다른지 설명 될 수 있다. ( 이미지는 pNFS 위키페이지의 것을 성능상의 이유로 본 블로그에 사진으로 첨부한다. )
Traditional NFS
How pNFS works.
차이점을 설명하면,
1. pNFS는 클라이언트들이 데이터 서버에 직접 엑세스가 가능하도록 하고 있다.
2. 데이터는 데이터 서버들 사이에서 분리되어 저장이 가능하다.
3. 데이터에 대한 엑세스는 메타 데이터 서버들을 통해 하위 호환성을 유지한다.
이로 인한 잇점은,
1. 고성능 : 확장 가능한 여러대의 서버로 파일을 분산함으로서 성능 및 확장을 노릴 수 있다.
2. 수평적 확장 : 단일 NFS 를 사용함으로서 발생 할 수 있는 제한에서 자유로울 수 있다.
3. 데이터와 메타데이터의 분리 : 데이터에 대한 변경 ( 생성, 추가, 삭제 기타 등 ) 은 메타데이터 서버에서 유지한다.
4. 전통적 NFS서버의 사용성을 유지한다.
5. 전통적 NFS서버와 같은 방법으로 마운트 할 수 있다.
pNFS 를 위한 3가지의 주요 설정 항목이 있다. 클라이언, 메타데이터 서버 ( MDS ) , 데이터 서버 ( DS )
현재, DS와 MDS는 반드시 별도의 머신에 설치 되어야 한다. 기본적으로 NCP - pNFS 설정을 위해서는 3 개의 머신을 추천한다. 한대는 클라이언트, 한대는 메타데이터 서버, 마지막 한대는 데이터 서버.
지금까지 설명 된 내용을 구동하기 위한 설정은 아래를 참고 한다.
NOTE: 본 단계에서 사용하는 파일 시스템은 임시적으로 사용하는 것이다. 데이터 서버는 지정한 파일 시스템을 사용하지 않는다. 데이터 서버의 SMF 서비스 ( svc:/network/dserv:default ) 는 NFS 서버의 SMF 서비스 ( svc:/network/nfs/server:default) 에 의존적이다. 파일 시스템의 공유는 NFS 서버의 서비스가 활성화 되었으며, 구동중임을 보장한다. ( 즉, NFS 서비스가 올라오면 관련된 서비스가 정상 동작함을 의미 한다는 표현 )
데이터 서버는 dservadm 툴을 사용하여 관리되며, 현재 man 페이지는 제공되고 있지 않으나 --help 옵션은 가능하다.
#dservadm --help
MDS 의 주소를 DS에 등록한다.
1. #dservadm addmds [MDS_IP_ADDR]
2. /etc/hosts 파일에 엔트리로 등록
NOTE: 데이터 서버에는 적어도 1개의 pNFS 데이터셋이 존재해야 한다. 따라서, 여러개의 인스턴스가 데이터 서버에 존재한다면, 인스턴스별로 반드시 1개 이상의 데이터셋을 생성해야 함을 잊지 말자.
dserv 서비스 시작 및 온라인 체크
#dservadm enable
#svcs dserv
STATE STIME FMRI
online 20:45:24 svc:/network/dserv:default
클라이언트 설정
MDS를 클라이언트로 마운트.
#mount -F nfs -o vers=4 [mds:/path] [mount_point]
현재 기능상의 문제. ( 이 부분의 번역은 다소 오류가 있을 수 있어 원문을 함께 첨부합니다. 오류 없으시길 )
클라이언트로 부터 파일이나 디렉토리가 제거될 때 주요한 병목이 발생하고 있다.
The major bottleneck is the fact that currently even though the files and directories are deleted from the client.. the dataset that is created on the withholding pool does not show any reduction in size.. This mean, only the namespace is being deleted from the client and mds.. Thus over a period of time the data server will run fill up and the only solution is to delete the entire zpool.
이에 대한 내용은 링크를 참조.
도움 요청은 IRC.frenoode.net 의 nexenta 채널로.
메일링 리스트는 gnu-sol-discuss@opensolaris.org
조금만 더 살펴보면, 이 pNFS 는 다양한 방법으로 각 스토리지 벤더에서 지원하고 있다. 이중 여기서 소개된 nexenta 의 opensolaris 진영에서도 infiniband 와 같은 고속 통신을 사용하여 pNFS over RDMA 와 같은 방법으로 구현하는 것도 있다. 물론 NetApp 의 OnTAP 에서도 그들의 독자적인 방법으로 기능을 제공한다.
주요한 것은 어떠한 파일 시스템을 사용하던, 이제는 데이터가 페타 또는 그이상의 크기를 필요로 하는 곳이 많아지게 되므로, 이와같은 병렬화된 파일레벨의 공유시스템 및 이를 사용했을때 발생하는 성능상의 이슈를 어떠한 방식의 캐싱으로 풀어나갈 것인가, 어디에 적용해야 할 것인가 등이 주요한 고민으로 작용 할 것 같다.
# Source downloads from http://ftp.novell.com/pub/mono/sources-stable/
root@mono:~# mkdir /usr/src/apps
root@mono:~# cd /usr/src/apps
root@mono:/usr/src/apps# wget http://ftp.novell.com/pub/mono/sources/mono/mono-2.6.4.tar.bz2
root@mono:/usr/src/apps# wget http://ftp.novell.com/pub/mono/sources/xsp/xsp-2.6.4.tar.bz2
root@mono:/usr/src/apps# wget http://ftp.novell.com/pub/mono/sources/mod_mono/mod_mono-2.6.3.tar.bz2
# Extract tarball
root@mono:/usr/src/apps# tar xvjf mono-2.6.4.tar.bz2
root@mono:/usr/src/apps# tar xvjf xps-2.6.4.tar.bz2
root@mono:/usr/src/apps# tar xvjf mod_mono-2.6.3.tar.bz2
# Build MONO
root@mono:/usr/src/apps# cd /usr/src/apps/mono-2.6.4
root@mono:/usr/src/apps# ./configure --prefix=/usr/local # if you wants any customize, type --help and add configure options
root@mono:/usr/src/apps# make && make install
# Build xsp
root@mono:/usr/src/apps/xsp-2.6.4# ./configure --prefix=/usr/local/
root@mono:/usr/src/apps/xsp-2.6.4# make && make install
# Build mod_mono for apache
root@mono:/usr/src/apps/mod_mono-2.6.3# ./configure --prefix=/usr/local --with-apr-config=/usr/bin/apr-config
# Add mod_mono.conf to apache2.conf ( or sites that you want to run MONO )
root@mono:/etc/apache2# echo "Include /etc/apache2/mod_mono.conf" >> /etc/apache2/apache2.conf
root@mono:/etc/apache2# tail apache2.conf
"Ipps" - 초당 패킷 유입량
"Tput" - 1M 를 전송하는데 사용된 패킷의 총 갯수 packets out of total 1M that made it
out
"txint" - 전송을 완료하는데 필요한 시스템 인터럽트 카운트
"Done" - rx ring 에서 모든 패킷이 전송되는동안 poll() 시스템 함수가 호출된 횟수.
( Note from this that the lower the load the more we could
clean up the rxring )
"Ndone" - "Done" 의 반대 개념.
( Note again, that the higher the load the more times we
couldn't clean up the rxring )
NIC가 890Kpackets/sec 을 수신할때 인터럽트는 단지 17회만 발생하게 된다. 시스템은 이와같은 부하 상황에서
1 인터럽트/패킷 을 처리 할 수 없다. 반면, rx 인터럽트가 증가하면 인터럽트/패킷의 비율이 함께 증가하는것을
볼 수있다. (표 참조)
따라서, 적당히 낮은 양의 패킷이 유입되는 상황에서는, 1 개의 패킷을 처리하기 위해 1회의 인터럽트가 필요하게
될수 있으며, 만약 시스템이 이를 처리 할 수 없는 인터럽트의 양에 도달하게 되면, 시스템은 그때부터 뻗기 시작할
것이다.
( 역자 주. 표를 그냥 vmstat 로 표시했으면 편할껀데... 어쨌든 위와 같이 인터럽트가 허용 범위 이상에 도달하는
상황은 예전 NForce Chipset 에 포함되어 나온 NIC 드라이버 문제 외에 광케이블을 크로스로 2대를 연결하여
네트워크 시뮬레이션 해 보면 알 수 있다. NAPI 가 적용되지 않은 드라이버의 경우, 인터럽트가 20만회 이상을
육박하며 , 시스템이 퍼지는 현상을 쉽게 목격 할 수 있다. 당연히 시스템의 인터럽트 능력은 CPU 및 해당 NIC 인터
페이스와 관계 되므로, 한계 성능은 시스템에 따라 다르게 나타 날 수 있다. 아무튼, 이와 같은 현상에 대한 수치적
증거로서 위의 표를 이해 하면 되겠다. )
0) 전제 조건
NAPI 변경으로 인한 잇점이 아난 네트워크 스택을 제어하기 위해 2.4 커널을 사용 할 수도 있다. ( 본문서가 마지막으로
수정된건 2002년 4월임을 양지하자 ) NAPI 추가 기능은 커널 하위 호환성을 해치지는 않지만, 다음과 같은 feature를
사용 가능하게끔 해 주어야 동작한다.
A) 소프트웨어 디바이스에 패킷을 저장하기 위한 DMA ring 또는 충분한 RAM 크기
B) 인터럽트를 끌 수 있는 기능 또는 스택에 패킷을 넣을 수 있는 이벤트.
NAPI는 패킷 이벤트를 dev->poll() 메서드를 통해 처리한다.
tulip 드라이버의 인터럽트처리를 dev->poll() 로 변경된 드라이버를 사용한 테스트에서는 약간의 지연이 나타났으며,
MII 드라이버에서는 조금 더 심했다.
본 예제는 이 문서에 사용하기위해 제작된 것이며, 인터럽트 핸들러를 dev->poll() 로 변경한 것이다. 이 코드를
보려면
아래의 포팅된 e1000 코드를 참조하기 바란다.
하지만 이와 같은 방법에는 문제가 있을 수 있다. 서로 다른 기종의 NIC 들은 각각 다르게 동작하며, 이 동작에는
status/event 와 같은 상태에 따른 다른 셋업이 요구된다.
아래와 같은 두가지의 서로 다른 ACK 이벤트 처리를 위한 매카니즘이 존재한다.
I ) Clear-on-read (COR) 로 알려진 매카니즘. status/event 레지스터를 읽는 순간 모두
비워버리는 형태.
natsemi 와 sunbmac NIC가 이 동작으로 유명하며, 이와같은 경우 모든걸 dev->poll() 로
변경해야 함.
II) Clear-on-write ( COW )
i) bit-location 에 1을 write 할때 status 가 지워지는 형태. NAPI와 가장 잘 맞는
형태이며, 많은 벤더의 NIC가
지원. 수신 이벤트 ( receive event ) 에 대해서만 dev-poll() 처리. 나머지는 기존의
인터럽트 핸들러로 처리.
ii) 무엇을 쓰던 write 액션이 발생하면 status 레지스터를 비우는 형태. 이와 같은 형태로 동작하는 리눅스
드라이버
는 보이지 않음.
C) 새로운 work 를 정확하게 감지 할 수 있는 능력.
NAPI 는 처리할 work 가 있는 상태에 shutdown 되며, 처리할 work 가 없는 상태에서 켜지게 된다.
작은 윈도우 사이즈의 새로운 패킷이 인터럽트가 재가동 되는 순간 유입된다면, 이 패킷은 인터럽트가 활성화 되는 순간
유실 될 수 있다. 따라서 우리는 이와 같은 새로 유입된 패킷은 다음번 인터럽트때 처리 될 수 있도록 분류 되어지길
원한다.
정리하면, 이와 같은 패킷은 경쟁상태(시스템이 하나의 자원에 한개 이상의 프로세스가 몰린 상태, 그냥 busy. 역자주 )
의 시스템에서는 "rotting packet" ( 쓸 수 없는 패킷 ) 으로 처리 되어야 한다.
이는 매우 중요한 프로세스이며, appendix 2 에서 추가적으로 논의 된다.
Locking 룰 과 환경에 대한 보장.
- Guarantee : 반드시 단 1개의 CPU가 dev->poll() 호출이 가능해야 한다.
- 코어 레이어에서는 패킷을 보내기 위한 디바이스 호출에 round robin 포멧을 사용해야 한다.
- contention 은 다른 CPU의 rx ring 엑세스의 결과로만 되어야 한다. ( 뭔가 번역 이상 @_@ )
이는 close() 및 suspend() 호출시에만 발생한다. ( rx ring을 비우고자 할때 )
***** guarantee : 드라이버 개발자는 이에대해 신경 쓸 필요가 없다. 이는 net의 최상위 레벨에서 구현 되기
때문에.
- local interrupts 활성화 ( dev->poll() 로 처리하고 싶지 않은 경우 )
예를 들면, link/MII , txcomplete 는 옛날 방식으로 동작한다.
본 문서의 이후로 부터는, dev->poll() 이 수신 이벤트를 위한 단 하나의 프로세스라고 가정한다.
NAPI 에서 제공하는 새로운 메서드
a) netif_rx_schedule(dev)
디바이스 poll 스케줄을 위한 IRQ 핸들러
b) netif_rx_schedule_prep(dev)
CPU 폴링리스트가 가용한 상태라면 여기에 장치 상태를 넣기 위해 사용된다.
c) __netif_rx_schedule(dev)
CPU의 폴 리스트에 넣기 위해 사용된다. _prep 가 이미 호출 되었고 1을 리턴 했음을 가정한다.
d) netif_rx_reschedule(dev, undo)
이미 호출된 장치 이외의 다른 장치를 폴링하기 위해 사용된다. 자세한 내용은 Appendix 2 를 참조한다.
e) netif_rx_complete(dev)
CPU poll 리스트에서 장치를 제거한다. 이는 반드시 현재 cpu 의 poll list 여야 한다.
이는 모든 작업이 완료 되었을때, dev->poll() 를 호출한다.
위에 기술된 내용은 아래에 반영되어 있다.
NAPI를 사용하게끔 포팅할때 변경되는 디바이스 드라이버의 내용
NAPI동작을 위해서는 아래의 내용이 변경될 필요가 있다.
1) dev->poll() 메서드의 소개
이는 새로운 패킷이 수신되었을때 네트워크 코어에서 드라이버로 호출하는 메서드다.
드라이버는 네트워크 서브 시스템의 하위 유연성을 위해 수신된 패킷을 CPU에 의해 dev->quota 로
패킷을 보낼 수 있다. ( 따라서 다른 장치들이 stack 에 접근 할 수 있다. )
dev->poll() 프로토타입은 다음과 같다.
int my_poll(struct net_device *dev, int *budget)
budget 은 네트워크 서브시스템에 남아있는 패킷의 수량을 나타낸다.
*각 드라이버는 보내진 패킷 만큼 감소된 카운트를 변경해 주어야 한다.
패킷의 총 수량은 dev->quota 보다 같거나 커질 수 없다.
dev->poll() 메서드는 시스템의 가장 높은 레벨에서 호출되며, 드라이버는 요청된 양의 패킷을 스택으로 보낼
수 있게된다. 인터럽트 이후의 dev->poll() 에 관한 상세 내용은 아래에 기술된다.
2) registering dev->poll() method
dev->poll should be set in the dev->probe() method.
e.g:
dev->open = my_open;
.
.
/* two new additions */
/* first register my poll method */
dev->poll = my_poll;
/* next register my weight/quanta; can be overriden in /proc */
dev->weight = 16;
.
.
dev->stop = my_close;
3) dev->poll() 의 스케쥴링
이 예제는 인터럽트 핸들러에 수정을 가하였으며, NIC에서 패킷을 꺼내 스택으로 보내는 동작을 보여준다.
int work_count = my_work_count;
status = read_interrupt_status_reg();
if (status == 0)
return; /* Shared IRQ: not us */
if (status == 0xffff)
return; /* Hot unplug */
if (status & error)
do_some_error_handling()
do {
acknowledge_ints_ASAP();
if (status & link_interrupt) {
spin_lock(&tp->link_lock);
do_some_link_stat_stuff();
spin_lock(&tp->link_lock);
}
if (status & rx_interrupt) {
receive_packets(dev);
}
if (status & rx_nobufs) {
make_rx_buffs_avail();
}
if (status & tx_related) {
spin_lock(&tp->lock);
tx_ring_free(dev);
if (tx_died)
restart_tx();
spin_unlock(&tp->lock);
}
status = read_interrupt_status_reg();
} while (!(status & error) || more_work_to_be_done);
status = read_interrupt_status_reg();
if (status == 0)
return; /* Shared IRQ: not us */
if (status == 0xffff)
return; /* Hot unplug */
if (status & error)
do_some_error_handling();
do {
/************************ start note *********************************/
acknowledge_ints_ASAP(); // dont ack rx and rxnobuff here
/************************ end note *********************************/
if (status & link_interrupt) {
spin_lock(&tp->link_lock);
do_some_link_stat_stuff();
spin_unlock(&tp->link_lock);
}
/************************ start note *********************************/
if (status & rx_interrupt || (status & rx_nobuffs)) {
if (netif_rx_schedule_prep(dev)) {
/* disable interrupts caused
* by arriving packets */
disable_rx_and_rxnobuff_ints();
/* tell system we have work to be done. */
__netif_rx_schedule(dev);
} else {
printk("driver bug! interrupt while in poll\n");
/* FIX by disabling interrupts */
disable_rx_and_rxnobuff_ints();
}
}
/************************ end note note *********************************/
if (status & tx_related) {
spin_lock(&tp->lock);
tx_ring_free(dev);
if (tx_died)
restart_tx();
spin_unlock(&tp->lock);
}
status = read_interrupt_status_reg();
/************************ start note *********************************/
} while (!(status & error) || more_work_to_be_done(status));
/************************ end note note *********************************/
I ) 패킷의 수신으로 발생한 어떠한 인터럽트도 동작하지 않게끔 변경한다. .
다음의 두가지 상태로 인해 하드웨어는 패킷이 수신되면 인터럽트를 유발한다. (NAPI 에서는 원하지 않는 동작 )
첫째는, 패킷이 수신되고 (rxint) , 둘째는 패킷이 도착 했으나 가용한 DMA 영역이 없음을 발견
(rxnobuff) 했을
때 이다.
이는, 위의 두가지 상황에 대해 acknowledge_ints_ASAP() 호출로 status 레지스터를 비울 수 없음을
의미한다.
NAPI를 사용하는 경우에는 work 가 어느 단계에서든 적절히 종료되면 언제든 status 레지스터를 비울 수 있다.
이에 관하여 poll() 및 refill_rx_ring() 은 아래에 논의 된다.
netif_rx_schedule_prep() 는 장치가 폴링 리스트에 정상적으로 등록이 되고 동작 가능한 상태이면 1을
리턴한다.
0이 리턴되는 경우는 장치가 이미 추가 되었거나, 장치가 동작하지 않는 경우 등으로 추정 할 수 있다.
이러한 오류를 rx 와 rxnobuf 인터럽트를 비활성화 함으로서 미연에 방지 할 수 있게 된다.
II ) receive_packets(dev) 와 make_rx_buffs_avail() 은 없어질 가능성이 있다. 아직은
존재하지만...
( 8년전 문서이기에 궁금하신 분은 최신 소스를 까 보도록 한다 )
사실, receive_packets(dev) 는 my_poll() 과 매우 비슷하며,
make_rx_buffs_avail() 은 my_poll() 에서 호출한다.
4) receive_packets() 을 dev->poll() 로 변환
D Becker 의 전통적인 receive_packet(dev) 를 my_poll() 로 변환해 줄 필요가 있다.
/* this is called by interrupt handler */
static void receive_packets (struct net_device *dev)
{
struct my_private *tp = (struct my_private *)dev->priv;
rx_ring = tp->rx_ring;
cur_rx = tp->cur_rx;
int entry = cur_rx % RX_RING_SIZE;
int received = 0;
int rx_work_limit = tp->dirty_rx + RX_RING_SIZE - tp->cur_rx;
while (rx_ring_not_empty) {
u32 rx_status;
unsigned int rx_size;
unsigned int pkt_size;
struct sk_buff *skb;
/* read size+status of next frame from DMA ring buffer */
/* the number 16 and 4 are just examples */
rx_status = le32_to_cpu (*(u32 *) (rx_ring + ring_offset));
rx_size = rx_status >> 16;
pkt_size = rx_size - 4;
/* process errors */
if ((rx_size > (MAX_ETH_FRAME_SIZE+4)) ||
(!(rx_status & RxStatusOK))) {
netdrv_rx_err (rx_status, dev, tp, ioaddr);
return;
}
if (--rx_work_limit < 0)
break;
/* grab a skb */
skb = dev_alloc_skb (pkt_size + 2);
if (skb) {
.
.
netif_rx (skb);
.
.
} else { /* OOM */
/*seems very driver specific ... some just pass
whatever is on the ring already. */
}
/* move to the next skb on the ring */
entry = (++tp->cur_rx) % RX_RING_SIZE;
received++ ;
}
/* store current ring pointer state */
tp->cur_rx = cur_rx;
/* Refill the Rx ring buffers if they are needed */
refill_rx_ring();
.
.
}
-------------------------------------------------------------------
새로운 파라메터에 주의하며 아래의 변경된 코드를 참조한다.
/* this is called by the network core */
static void my_poll (struct net_device *dev, int *budget)
{
struct my_private *tp = (struct my_private *)dev->priv;
rx_ring = tp->rx_ring;
cur_rx = tp->cur_rx;
int entry = cur_rx % RX_BUF_LEN;
/* maximum packets to send to the stack */
/************************ note note *********************************/
int rx_work_limit = dev->quota;
/************************ end note note *********************************/
do { // outer beggining loop starts here
clear_rx_status_register_bit();
while (rx_ring_not_empty) {
u32 rx_status;
unsigned int rx_size;
unsigned int pkt_size;
struct sk_buff *skb;
/* read size+status of next frame from DMA ring buffer */
/* the number 16 and 4 are just examples */
rx_status = le32_to_cpu (*(u32 *) (rx_ring + ring_offset));
rx_size = rx_status >> 16;
pkt_size = rx_size - 4;
/* process errors */
if ((rx_size > (MAX_ETH_FRAME_SIZE+4)) ||
(!(rx_status & RxStatusOK))) {
netdrv_rx_err (rx_status, dev, tp, ioaddr);
return;
}
/************************ note note *********************************/
if (--rx_work_limit < 0) { /* we got packets, but no quota */
/* store current ring pointer state */
tp->cur_rx = cur_rx;
/* Refill the Rx ring buffers if they are needed */
refill_rx_ring(dev);
goto not_done;
}
/********************** end note **********************************/
/* grab a skb */
skb = dev_alloc_skb (pkt_size + 2);
if (skb) {
.
.
/************************ note note *********************************/
netif_receive_skb (skb);
/********************** end note **********************************/
.
.
} else { /* OOM */
/*seems very driver specific ... common is just pass
whatever is on the ring already. */
}
/* move to the next skb on the ring */
entry = (++tp->cur_rx) % RX_RING_SIZE;
received++ ;
}
/* store current ring pointer state */
tp->cur_rx = cur_rx;
/* Refill the Rx ring buffers if they are needed */
refill_rx_ring(dev);
/* no packets on ring; but new ones can arrive since we last
checked */
status = read_interrupt_status_reg();
if (rx status is not set) {
/* If something arrives in this narrow window,
an interrupt will be generated */
goto done;
}
/* done! at least thats what it looks like ;->
if new packets came in after our last check on status bits
they'll be caught by the while check and we go back and clear them
since we havent exceeded our quota */
} while (rx_status_is_set);
/* If RX ring is not full we are out of memory. */
if (tp->rx_buffers[tp->dirty_rx % RX_RING_SIZE].skb == NULL)
goto oom;
/* we are happy/done, no more packets on ring; put us back
to where we can start processing interrupts again */
netif_rx_complete(dev);
enable_rx_and_rxnobuf_ints();
/* The last op happens after poll completion. Which means the following:
* 1. it can race with disabling irqs in irq handler (which are done to
* schedule polls)
* 2. it can race with dis/enabling irqs in other poll threads
* 3. if an irq raised after the begining of the outer beginning
* loop(marked in the code above), it will be immediately
* triggered here.
*
* Summarizing: the logic may results in some redundant irqs both
* due to races in masking and due to too late acking of already
* processed irqs. The good news: no events are ever lost.
*/
if (!received) {
printk("received==0\n");
received = 1;
}
dev->quota -= received;
*budget -= received;
return 1; /* not_done */
oom:
/* Start timer, stop polling, but do not enable rx interrupts. */
start_poll_timer(dev);
return 0; /* we'll take it from here so tell core "done"*/
/************************ End note note *********************************/
}
-------------------------------------------------------------------
위의 내용으로 인해 우리는,
0) rx_work_limit = dev->quota
1) refill_rx_ring() 은 정상적으로 동작하지 않게 될때 rxnobuff 비트를 지워줄 필요가 있다.
2) done / not_dont 상태가 있다.
3) netif_rx() 대신 netif_receive_skb() 가 skb 를 패스하기 위해 호출된다.
4) 새로운 OOM ( Out of memory ) 조건을 가진다.
5) 새로운 for 루프가 추가되었다. 이는 새로운 패킷의 유입의 확인, 모든 세팅의 정상 동작 및 패킷을
보내는 동안 qouta 를 초과하지 않기 위한 동작등이 포함된다.
/* If RX ring is not full we are still out of memory.
Restart the timer again. Else we re-add ourselves
to the master poll list.
*/
if (tp->rx_buffers[tp->dirty_rx % RX_RING_SIZE].skb == NULL)
restart_timer();
else netif_rx_schedule(dev); /* we are back on the poll list */
5) dev->close() , dev->suspend() 이슈
드라이버 개발자는 본 내용에 대해 신경쓰지 않아도 된다. 본 내용은 향후 채워진다.
6) /proc 에 새로운 스탯의 추가.
새로운 기능의 디버깅으로 인해 본 내용 역시 나중에 추가하기로 한다.
APPENDIX 1: HW FC 사용에 대한 논의
대부분의 FC 칩들은 Rx buffer 에 더이상 공간이 없을때 pause packet 을 전송한다. NAPI 의 softirq 에 의해 DMA ring
에서 패킷을 빼 낼때, 패킷의 유입 량에 비해 시스템이 패킷을 가져오는 속도가 느린 경우 ( 시스템이 패킷을 지우는
속도 보다 유입되는 속도가 더 빠를때 ) 이론적으로 이와 같은 packet storm 으로 발생한 모든 패킷에 대해 단 1회의
rx 인터럽트를 호출 하여 해결 할 수 있다.
낮은 부하상태에서는, 패킷 당 1회의 인터럽트가 발생이 가능하다.
FC는 충분히 빠른 속도로 시스템이 패킷을 끄집어 내지 못할때에 대비하여 프로그램 되어야 한다. ( send a pause
when out of rx buffers )
FC가 좋은 솔루션이긴 하지만, 너무 고가이다. ( 옛날 문서니까. 역자주 )
APPENDIX 2: rotting packet / race-window avoidance 스키마
( 이 부분 번역은 생략. 쓰다보니 appendix 까지 번역해야 하는 생각이 스멀스멀.. )
here are two types of associations seen here
1) status/int which honors level triggered IRQ
If a status bit for receive or rxnobuff is set and the corresponding
interrupt-enable bit is not on, then no interrupts will be generated. However,
as soon as the "interrupt-enable" bit is unmasked, an immediate interrupt is
generated. [assuming the status bit was not turned off].
Generally the concept of level triggered IRQs in association with a status and
interrupt-enable CSR register set is used to avoid the race.
If we take the example of the tulip:
"pending work" is indicated by the status bit(CSR5 in tulip).
the corresponding interrupt bit (CSR7 in tulip) might be turned off (but
the CSR5 will continue to be turned on with new packet arrivals even if
we clear it the first time)
Very important is the fact that if we turn on the interrupt bit on when
status is set that an immediate irq is triggered.
If we cleared the rx ring and proclaimed there was "no more work
to be done" and then went on to do a few other things; then when we enable
interrupts, there is a possibility that a new packet might sneak in during
this phase. It helps to look at the pseudo code for the tulip poll
routine:
--------------------------
do {
ACK;
while (ring_is_not_empty()) {
work-work-work
if quota is exceeded: exit, no touching irq status/mask
}
/* No packets, but new can arrive while we are doing this*/
CSR5 := read
if (CSR5 is not set) {
/* If something arrives in this narrow window here,
* where the comments are ;-> irq will be generated */
unmask irqs;
exit poll;
}
} while (rx_status_is_set);
------------------------
CSR5 bit of interest is only the rx status.
If you look at the last if statement:
you just finished grabbing all the packets from the rx ring .. you check if
status bit says theres more packets just in ... it says none; you then
enable rx interrupts again; if a new packet just came in during this check,
we are counting that CSR5 will be set in that small window of opportunity
and that by re-enabling interrupts, we would actually triger an interrupt
to register the new packet for processing.
[The above description nay be very verbose, if you have better wording
that will make this more understandable, please suggest it.]
2) non-capable hardware
These do not generally respect level triggered IRQs. Normally,
irqs may be lost while being masked and the only way to leave poll is to do
a double check for new input after netif_rx_complete() is invoked
and re-enable polling (after seeing this new input).
Sample code:
---------
.
.
restart_poll:
while (ring_is_not_empty()) {
work-work-work
if quota is exceeded: exit, not touching irq status/mask
}
.
.
.
enable_rx_interrupts()
netif_rx_complete(dev);
if (ring_has_new_packet() && netif_rx_reschedule(dev, received)) {
disable_rx_and_rxnobufs()
goto restart_poll
} while (rx_status_is_set);
---------
Basically netif_rx_complete() removes us from the poll list, but because a
new packet which will never be caught due to the possibility of a race
might come in, we attempt to re-add ourselves to the poll list.
Authors:
========
Alexey Kuznetsov <kuznet@ms2.inr.ac.ru>
Jamal Hadi Salim <hadi@cyberus.ca>
Robert Olsson <Robert.Olsson@data.slu.se>
Acknowledgements:
================
People who made this document better:
Lennert Buytenhek <buytenh@gnu.org>
Andrew Morton <akpm@zip.com.au>
Manfred Spraul <manfred@colorfullife.com>
Donald Becker <becker@scyld.com>
Jeff Garzik <jgarzik@mandrakesoft.com>
결국, 이런저런 내용이 많지만, NAPI 의 핵심은 다음의 몇가지로 압축 될 수 있다.
1)
고부하 상태건 저부하 상태건 패킷의 유입으로 인한 하드웨어로 부터 ( 하드웨어 드라이버로 부터) 의
직접적인 인터럽트는 시스템에 심각한 부하를 일으킬 가능성이 있다.
2)
따라서 이와 같은 인터럽트는 NAPI 및 기타 하드웨어 ( NIC )의 인터럽트 제어를 통해 부하율을 낮출 수 있으나,
다소간의 지연이 발생 할 수 있다.
3)
NAPI 가 동작하는 기본적인 방식은 Polling 이며, 이를 쉽게 말하면 유입된 패킷이 발생할때 마다 인터럽트 하여
가져오는 것이 아니라 드라이버가 원하는 때에 적절한 스케줄링을 통하여 폴링으로 한꺼번에 가져온다.
4)
따라서 이러한 패킷을 저장하기 위한 공간이 필요하며, 이러한 공간이 포화가 된 경우/ 또는 시스템에서 처리
불가능한 경우 등에 대한 대비가 필요하다.
5)
NAPI 는 기본적으로 rx ring 에 동작한다.
로 압축 할 수 있겠다.
번역한 문서가 아주 옛날 버전이기는 하지만, 최신 문서 소개해 봐야 다 기본 내용은 거기서 거기.
실제 netpipe 등과 같은 툴을 통해 packet storming 이나 하드웨어 레벨의 테스팅을 하다 보면
쉽게 접하고 튜닝이 가능한 부분이며, 힘들어하는 시스템에 idle 을 조금이나마 안겨 줄 수 있는
방법이라 하겠다. 배포판 또는 드라이버별로 적용이 되고 안되는 등의 차이가 있는 듯 하니
궁금하면 dmesg 를 확인 해 보길 바란다.