안녕하세요. EOSeoul입니다.
이 글은 1) 패트로니오스의 동작 방식, 2) 벤치마크 결과, 3) 운영 및 구현에 관한 권장 사항을 공유합니다. 편의상 경어체를 사용하지 않습니다. 또한 패트로니오스의 simple/advanced 설정에 대해 충분히 이해하고 있는 독자를 가정합니다.
모든 설명은 아래의 커밋을 기준으로 삼습니다.
6501e4429f43f4de78444a227149773b914e221bvalidateMaxTransactions 추가됨48422fa05b47373ad68013f4d77d290e7fc31aae블록원은 EOSIO Dawn 4.2 출시와 함께 자사가 개발한 소프트웨어 하나를 공개한다. 이름은 패트로니오스(patroneos). 소설 해리 포터에는 혼을 빨아먹는 크리처인 디멘터가 등장하는데, 디멘터를 물리치는 마법이 '패트로누스(patronus)'다. 패트로니오스는 이 마법에서 이름을 빌려왔다.
EOS를 둘러싼 많은 사람들은 EOS 메인넷을 공격하는 다양한 공격 시도에 우려를 표해왔다. 커뮤니티와 블록원은 공격 경로별 시나리오를 도출하고 공격에도 끄덕없도록 EOSIO 소프트웨어를 개선해왔다. 패트로니오스는 이와 같은 노력의 산출물 중 하나다. 패트로니오스는 서비스 거부 공격(Denial of Service)의 기초적인 형태를 지닌 공격을 걸러내고, 정상적인 트랜잭션만 EOS RPC API Endpoint에 전달한다.
코드는 3개의 주요 파일로 구성되어 있다. main.go, filter.go, fail2ban-relay.go가 그것이다.
main.gomain.go는 다음 기능을 구현하고 있다.
Config 정의프로그램은 Filter 모드(filter)와 Relay 모드(fail2ban-relay)로 동작하도록 구현되었다. 각각을 살펴보자.
filter.gofilter.go는 Filter 모드를 구현한 파일이다.
TRANSACTION_FAILED 에러를 발생하고 연결을 종료한다./patroneos/fail2ban-relay로 HTTP 요청을 보내고, 로그를 남기고 연결을 종료한다.validateJSON() : HTTP POST의 body로 받은 JSON이 유효한지 검증. 실패할 경우 INVALID_JSON 에러 발생하고 걸러냄.validateMaxTransactions() : 하나의 JSON에 담긴 트랜잭션 수가 최대값 미만인지 검증. 실패할 경우 TOO_MANY_TRANSACTIONS 에러 발생하고 걸러냄.validateTransactionSize() : 트랜잭션에 담긴 서명의 수가 최대값보다 작은지 검증. 실패할 경우 INVALID_NUMBER_SIGNATURES 에러 발생하고 걸러냄.validateMaxSignatures() : 트랜잭션이 블랙리스트 처리된 컨트랙트의 액션이 아닌지 검증. 실패할 경우 BLACKLISTED_CONTRACT 에러 발생하고 걸러냄.validateContract() : 트랜잭션의 크기가 최대값보다 작은지 검증. 실패할 경우 INVALID_TRANSACTION_SIZE 에러 발생하고 걸러냄.validateTransactionSize(), validateMaxSignatures(), validateContract()은 HTTP Request Body의 JSON이 Object임을 가정한다. 그러나 HTTP Chain API push_transactions는 JSON Array를 사용하는데, 이를 PARSING_ERROR로 처리한다. 이에 대해서 Patroneos Issue #26로 보고했다. 이 포스트가 변경 불가 상태가 되기 전에 이슈가 해결된다면, 업데이트하겠다.fail2ban-relay.gofail2ban-relay.go는 Relay 모드를 구현한 파일이다.
/patroneos/fail2ban-relay로 받은 결과를 fail2ban이 스캔할 로그 파일에 남긴다.패트로니오스는 구글이 2009년에 만든 프로그래밍 언어인 Go로 작성되었다. Go는 비동기 메커니즘으로 Go Routine(이하 고루틴)을 제공한다. 고루틴은 Go 런타임이 관리하는 경량 쓰레드(lightweight thread)다. "go” 키워드를 이용해서 함수를 호출하면 런타임은 해당 함수를 동일한 메모리 주소 공간에서 시분할 방식으로 동시에(concurrently) 실행한다.
Go 프로그램을 다수의 CPU 또는 코어로 병렬처리할 수도 있다. runtime.GOMAXPROCS() 함수를 이용하면, Go 프로그램이 사용할 수 있는 논리 코어(logical core) 수를 정할 수 있다. Go 버전 1.5부터 머신에 장착된 논리 코어 전부를 사용하도록 변경되었다. 이로써 고루틴으로 호출되는 함수는 멀티코어 머신에서 병렬처리된다.
2018년 6월 1일을 기준으로 한다면, 일반적으로 Go 1.10.2를 설치하게 된다. Ubuntu 18.04 LTS와 macOS High Sierra 10.13.4 에서 각각 apt와 brew를 통해서 golang을 설치하면 1.10.2가 설치되었다. CentOS 7.5 에서는 1.9.4가 설치되었다.
따라서 최근 출시된 Go의 버전으로 고루틴을 사용하면 멀티코어 환경에서 자동으로 병렬처리된다.
ServMux & ListenAndServehttp의 Serve() 함수를 살펴보면, 연결이 accept된 후 고루틴으로 새로운 연결을 serve로 처리한다. HTTP 요청을 받는 부분은 멀티코어를 이용하여 병렬로 처리됨을 알 수 있다.
ListenAndServe는 서버의 timeout이 없는 디폴트 http.Server를 사용한다. 분석 시점 기준으로 패트로니오스는 Timeout 설정이 없는 Server를 사용한다. 클라이언트 또는 패트로니오스로 HTTP 요청을 전달하는 포인트에서 적절한 timeout이 없는 경우, 패트로니오스는 HTTP 연결을 맺고 데이터를 받지 못한 상황에서 무한정 대기할 것이다.
따라서 패트로니오스는 1) Client의 요청을 직접 받지 않고 2) timeout이 존재하는 HTTP 요청을 받을 수 있도록 적절한 아키텍처를 구현하기를 권장한다.
5가지 유효성 검증은 "go” 키워드로 호출되지 않고, 직렬로 처리된다. 즉, 유효성 검증은 병렬로 처리되지 않는다. 현재까지 구현된 검증 로직은 매우 단순하므로 굳이 병렬로 처리할 필요가 보이지 않는다.
클라이언트로부터 HTTP body를 모두 받은 후 ServeMux.HandleFunc가 실행되므로, 유효성 검증 로직은 timeout의 이슈는 존재하지 않는다.
그러나 Relay를 사용하도록 설정한 경우는 timeout이 발생할 수 있으나 큰 문제가 없을 것으로 보인다. 이는 아래에 이어서 설명한다.
Client많은 관련 문서에서 Go의 HTTP Client는 고루틴을 이용한 concurrency 사용이 안전하다고 확인하고 있다.
HTTP Client는 별도의 처리가 없다면 Timeout이 없다. 분석 시점 기준으로 패트로니오스는 1) Relay용 패트로니오스에 Filter 결과를 전송할 때, 2) API Endpoint에 HTTP Request를 전달할 때 HTTP Client를 사용한다. 이 두 경우 모두 Timeout이 설정되어 있지 않다.
패트로니오스가 수신한 HTTP 요청 중 검증이 완료된 요청을 전달하는 API Endpoint에 적절한 timeout이 없는 경우, 패트로니오스는 응답을 받을 때까지 무한정 대기할 것이다.
따라서 API Endpoint는 적절한 timeout을 반드시 설정하기를 권장한다.
Relay 모드의 패트로니오스를 RP라고 하고, Filter 모드의 패트로니오스를 FP라고 하자. FP가 RP를 사용하지 않는다면, 아무런 이슈가 없다. FP가 RP를 사용하도록 설정했다고 하자. 만약 RP가 꺼진 상태거나 정상적으로 응답하는 상태라면, 아무런 이슈가 없다. RP의 로직이 대단히 간단하므로 file descriptor가 부족하거나 처리가 지연되는 상황이 아닌 일반적인 상황이라면, RP의 응답이 늦어지는 경우는 대단히 드물 것이다. 또한 RP가 사용하는 포트에 무한정 응답이 없는 TCP/HTTP 서버가 있다면 큰 문제가 되겠지만, 그런 경우 역시 대단히 드물 것이다. 따라서 FP가 RP를 사용하는 경우는 timeout과 관련된 이슈가 대단히 드물 것으로 예상된다.
패트로니오스 자체의 처리 용량을 확인하는 데 초점을 두었다. 그래서 HTTP request와 API Endpoint를 간단하게 구성했다. 이 벤치마크는 일종의 실험실 벤치마크로 이해할 수 있다.
크기가 다른 두 종류의 JSON을 테스트에 사용했고, HTTP request concurrency도 100과 1000으로 나누었다. 또한 API 처리 Latency를 감안하여 0 ms와 100 ms를 주고 테스트했다.
아래는 벤치마크를 위한 테스트 구성이다. Production 환경에서는 상황에 따라 설정을 변경해야 한다.
{
"listenPort": "8081",
"nodeosProtocol": "http",
"nodeosUrl": "127.0.0.1",
"nodeosPort": "8000",
"contractBlackList": {
"currency": true
},
"maxSignatures": 10,
"maxTransactionSize": 1000000,
"logEndpoints": ["http://127.0.0.1:8080"],
"filterEndpoints": [],
"logFileLocation": "./fail2ban.log"
}
{
"listenPort": "8080",
"nodeosProtocol": "http",
"nodeosUrl": "127.0.0.1",
"nodeosPort": "8000",
"contractBlackList": {
"currency": true
},
"maxSignatures": 10,
"maxTransactionSize": 1000000,
"logEndpoints": [],
"filterEndpoints": ["http://127.0.0.1:8081"],
"logFileLocation": "./fail2ban.log"
}
package main
import (
"fmt"
"log"
"net/http"
"time"
"runtime"
"io/ioutil"
)
func main() {
runtime.GOMAXPROCS(1)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if len(r.FormValue("case-two")) > 0 {
fmt.Println("case two")
} else {
time.Sleep(time.Millisecond * 100)
b, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(b)
//fmt.Println("case one end")
}
})
if err := http.ListenAndServe(":8000", nil); err != nil {
log.Fatal(err)
}
}
{"code":"currency","type":"transfer","recipients":["initb","initc"],"authorization":[{"account":"initb","permission":"active"}],"data":"000000000041934b000000008041934be803000000000000"}
{"id":"37df4598d37bb8fdbc440e31caae07906ac90fd3fd2cd060f2ca13e59e78781e","signatures":["SIG_K1_K8ojKDxMnWy5Q3zAVQPwJANbEE2h9kStmPX4BorEGGQKCJXUYK62UiEYxGyQbaynraMX5WvzEFYaQqAf5Mdwu2yBf36HG7"],"compression":"none","packed_context_free_data":"","context_free_data":[],"packed_trx":"5f3b125b17c726f418ba000000000100a6823403ea3055000000572d3ccdcd010000000000ea305500000000a8ed32322e0000000000ea305590d5cc5865570da420a107000000000004454f53000000000d4a756e676c652046617563657400","transaction":{"expiration":"2018-06-02T06:38:23","ref_block_num":50967,"ref_block_prefix":3122197542,"max_net_usage_words":0,"max_cpu_usage_ms":0,"delay_sec":0,"context_free_actions":[],"actions":[{"account":"eosio.token","name":"transfer","authorization":[{"actor":"eosio","permission":"active"}],"data":{"from":"eosio","to":"okapitestnet","quantity":"50.0000 EOS","memo":"Jungle Faucet"},"hex_data":"0000000000ea305590d5cc5865570da420a107000000000004454f53000000000d4a756e676c6520466175636574"}],"transaction_extensions":[]}}
Test #1
Test #2
Test #3
Test #4
Test #5
Test #6
Test #7
Test #8
패트로니오스 운영 권고 사항
nodeosUrl 설정에 FQDN 대신 IP 사용하면, DNS resolving overhead를 줄일 수 있음push_transactions의 JSON Array처리에서 문제가 발생하며 Patroneos Issue #26로 버그를 보고했음. 이 버그의 대응 현황을 확인하고, 그에 따라 push_transactions을 사용하는 request에 대해 반드시 URI route 변경하거나 bypass하도록 전체 아키텍처를 구현해야 함push_transactions 역시 안전하게 사용할 수 있다.nodeos 설정 access-control-allow-origin을 *로 사용하기를 권장
패트로니오스 구현 권고 사항 (to 패트로니오스 커미터 & Block.one)
Server와 Client에 Timeout을 적절히 설정할 수 있는 Config 구조와 이를 반영할 수 있는 구현 변경을 권고한다.
Server : ReadTimeout, ReadHeaderTimeout, WriteTimeoutClient의 TimeoutClient에 HTTP Transport를 사용Transport의 파라미터 튜닝 : MaxIdleConnsPerHost, MaxIdleConns, IdleConnTimeout, ResponseHeaderTimeout, net.Dialer.Timeout 등net/http implementation recommendations결론
push_transactions에 대해서는 반드시 URI route를 변경하거나 bypass하도록 전체 아키텍처를 구현해야 한다.net/http docs and source codes
net/http implementation recommendations
EOSeoul은 EOS dApp/사용/개발/운영 등 다양한 토의를 할 수 있는 채널을 열어두고 있습니다.
Telegram(한국어) : http://t.me/eoseoul
Telegram(한국어, developer) : http://t.me/eoseoul_testnet
Steemit : http://steemit.com/@eoseoul
Github : http://github.com/eoseoul
Website : http://eoseoul.io
Twitter : http://twitter.com/eoseoul_kor
Facebook : http://www.facebook.com/EOSeoul.kr
Wechat account: neoply