rpc: add execution pool metrics (#919)

* rpc: add execution pool metrics

* rpc: stop execution pool and report metrics using ticker

* fix lint

* update go.mod, update metric report interval

* handle empty workerpool case to fix tests

* fix lint

* refactor ep metrics collection based on each service

* remove log

* rpc: convert processed metric to histogram
This commit is contained in:
Manav Darji 2023-07-19 12:00:17 +05:30 committed by GitHub
parent 67878747b3
commit 896e4bbcbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 94 additions and 30 deletions

View file

@ -656,7 +656,7 @@ func signer(c *cli.Context) error {
vhosts := utils.SplitAndTrim(c.GlobalString(utils.HTTPVirtualHostsFlag.Name)) vhosts := utils.SplitAndTrim(c.GlobalString(utils.HTTPVirtualHostsFlag.Name))
cors := utils.SplitAndTrim(c.GlobalString(utils.HTTPCORSDomainFlag.Name)) cors := utils.SplitAndTrim(c.GlobalString(utils.HTTPCORSDomainFlag.Name))
srv := rpc.NewServer(0, 0) srv := rpc.NewServer("", 0, 0)
err := node.RegisterApis(rpcAPI, []string{"account"}, srv, false) err := node.RegisterApis(rpcAPI, []string{"account"}, srv, false)
if err != nil { if err != nil {
utils.Fatalf("Could not register API: %w", err) utils.Fatalf("Could not register API: %w", err)

2
go.mod
View file

@ -7,7 +7,7 @@ require (
github.com/BurntSushi/toml v1.1.0 github.com/BurntSushi/toml v1.1.0
github.com/JekaMas/crand v1.0.1 github.com/JekaMas/crand v1.0.1
github.com/JekaMas/go-grpc-net-conn v0.0.0-20220708155319-6aff21f2d13d github.com/JekaMas/go-grpc-net-conn v0.0.0-20220708155319-6aff21f2d13d
github.com/JekaMas/workerpool v1.1.5 github.com/JekaMas/workerpool v1.1.8
github.com/VictoriaMetrics/fastcache v1.6.0 github.com/VictoriaMetrics/fastcache v1.6.0
github.com/aws/aws-sdk-go-v2 v1.2.0 github.com/aws/aws-sdk-go-v2 v1.2.0
github.com/aws/aws-sdk-go-v2/config v1.1.1 github.com/aws/aws-sdk-go-v2/config v1.1.1

4
go.sum
View file

@ -54,8 +54,8 @@ github.com/JekaMas/crand v1.0.1 h1:FMPxkUQqH/hExl0aUXsr0UCGYZ4lJH9IJ5H/KbM6Y9A=
github.com/JekaMas/crand v1.0.1/go.mod h1:GGzGpMCht/tbaNQ5A4kSiKSqEoNAhhyTfSDQyIENBQU= github.com/JekaMas/crand v1.0.1/go.mod h1:GGzGpMCht/tbaNQ5A4kSiKSqEoNAhhyTfSDQyIENBQU=
github.com/JekaMas/go-grpc-net-conn v0.0.0-20220708155319-6aff21f2d13d h1:RO27lgfZF8s9lZ3pWyzc0gCE0RZC+6/PXbRjAa0CNp8= github.com/JekaMas/go-grpc-net-conn v0.0.0-20220708155319-6aff21f2d13d h1:RO27lgfZF8s9lZ3pWyzc0gCE0RZC+6/PXbRjAa0CNp8=
github.com/JekaMas/go-grpc-net-conn v0.0.0-20220708155319-6aff21f2d13d/go.mod h1:romz7UPgSYhfJkKOalzEEyV6sWtt/eAEm0nX2aOrod0= github.com/JekaMas/go-grpc-net-conn v0.0.0-20220708155319-6aff21f2d13d/go.mod h1:romz7UPgSYhfJkKOalzEEyV6sWtt/eAEm0nX2aOrod0=
github.com/JekaMas/workerpool v1.1.5 h1:xmrx2Zyft95CEGiEqzDxiawptCIRZQ0zZDhTGDFOCaw= github.com/JekaMas/workerpool v1.1.8 h1:IbDbTITrDgt1xAzdzTS9aLk4Q/4dCsjUOopiyFkDFZ4=
github.com/JekaMas/workerpool v1.1.5/go.mod h1:IoDWPpwMcA27qbuugZKeBslDrgX09lVmksuh9sjzbhc= github.com/JekaMas/workerpool v1.1.8/go.mod h1:IoDWPpwMcA27qbuugZKeBslDrgX09lVmksuh9sjzbhc=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=

View file

@ -105,7 +105,7 @@ func New(conf *Config) (*Node, error) {
node := &Node{ node := &Node{
config: conf, config: conf,
inprocHandler: rpc.NewServer(0, 0), inprocHandler: rpc.NewServer("inproc", 0, 0),
eventmux: new(event.TypeMux), eventmux: new(event.TypeMux),
log: conf.Logger, log: conf.Logger,
stop: make(chan struct{}), stop: make(chan struct{}),

View file

@ -293,7 +293,7 @@ func (h *httpServer) enableRPC(apis []rpc.API, config httpConfig) error {
} }
// Create RPC server and handler. // Create RPC server and handler.
srv := rpc.NewServer(config.executionPoolSize, config.executionPoolRequestTimeout) srv := rpc.NewServer("http", config.executionPoolSize, config.executionPoolRequestTimeout)
srv.SetRPCBatchLimit(h.RPCBatchLimit) srv.SetRPCBatchLimit(h.RPCBatchLimit)
if err := RegisterApis(apis, config.Modules, srv, false); err != nil { if err := RegisterApis(apis, config.Modules, srv, false); err != nil {
return err return err
@ -325,7 +325,7 @@ func (h *httpServer) enableWS(apis []rpc.API, config wsConfig) error {
return fmt.Errorf("JSON-RPC over WebSocket is already enabled") return fmt.Errorf("JSON-RPC over WebSocket is already enabled")
} }
// Create RPC server and handler. // Create RPC server and handler.
srv := rpc.NewServer(config.executionPoolSize, config.executionPoolRequestTimeout) srv := rpc.NewServer("ws", config.executionPoolSize, config.executionPoolRequestTimeout)
srv.SetRPCBatchLimit(h.RPCBatchLimit) srv.SetRPCBatchLimit(h.RPCBatchLimit)
if err := RegisterApis(apis, config.Modules, srv, false); err != nil { if err := RegisterApis(apis, config.Modules, srv, false); err != nil {
return err return err

View file

@ -112,7 +112,7 @@ func (c *Client) newClientConn(conn ServerCodec) *clientConn {
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, clientContextKey{}, c) ctx = context.WithValue(ctx, clientContextKey{}, c)
ctx = context.WithValue(ctx, peerInfoContextKey{}, conn.peerInfo()) ctx = context.WithValue(ctx, peerInfoContextKey{}, conn.peerInfo())
handler := newHandler(ctx, conn, c.idgen, c.services, NewExecutionPool(100, 0)) handler := newHandler(ctx, conn, c.idgen, c.services, NewExecutionPool(100, 0, "rpcclient", true))
return &clientConn{conn, handler} return &clientConn{conn, handler}
} }

View file

@ -410,7 +410,7 @@ func TestClientSubscriptionUnsubscribeServer(t *testing.T) {
t.Parallel() t.Parallel()
// Create the server. // Create the server.
srv := NewServer(0, 0) srv := NewServer("test", 0, 0)
srv.RegisterName("nftest", new(notificationTestService)) srv.RegisterName("nftest", new(notificationTestService))
p1, p2 := net.Pipe() p1, p2 := net.Pipe()
recorder := &unsubscribeRecorder{ServerCodec: NewCodec(p1)} recorder := &unsubscribeRecorder{ServerCodec: NewCodec(p1)}
@ -446,7 +446,7 @@ func TestClientSubscriptionChannelClose(t *testing.T) {
t.Parallel() t.Parallel()
var ( var (
srv = NewServer(0, 0) srv = NewServer("test", 0, 0)
httpsrv = httptest.NewServer(srv.WebsocketHandler(nil)) httpsrv = httptest.NewServer(srv.WebsocketHandler(nil))
wsURL = "ws:" + strings.TrimPrefix(httpsrv.URL, "http:") wsURL = "ws:" + strings.TrimPrefix(httpsrv.URL, "http:")
) )

View file

@ -27,7 +27,7 @@ import (
func StartIPCEndpoint(ipcEndpoint string, apis []API) (net.Listener, *Server, error) { func StartIPCEndpoint(ipcEndpoint string, apis []API) (net.Listener, *Server, error) {
// Register all the APIs exposed by the services. // Register all the APIs exposed by the services.
var ( var (
handler = NewServer(0, 0) handler = NewServer("", 0, 0) // we can skip reporting ipc metrics, hence empty service name
regMap = make(map[string]struct{}) regMap = make(map[string]struct{})
registered []string registered []string
) )

View file

@ -7,24 +7,32 @@ import (
"time" "time"
"github.com/JekaMas/workerpool" "github.com/JekaMas/workerpool"
"github.com/ethereum/go-ethereum/metrics"
) )
type SafePool struct { type SafePool struct {
executionPool *atomic.Pointer[workerpool.WorkerPool] executionPool atomic.Pointer[workerpool.WorkerPool]
sync.RWMutex sync.RWMutex
timeout time.Duration
size int size int
timeout time.Duration
service string // the service using ep
processed atomic.Int64 // keeps count of total processed requests
close chan struct{}
// Skip sending task to execution pool // Skip sending task to execution pool
fastPath bool fastPath bool
} }
func NewExecutionPool(initialSize int, timeout time.Duration) *SafePool { func NewExecutionPool(initialSize int, timeout time.Duration, service string, report bool) *SafePool {
sp := &SafePool{ sp := &SafePool{
size: initialSize, size: initialSize,
timeout: timeout, timeout: timeout,
service: service,
close: make(chan struct{}),
} }
if initialSize == 0 { if initialSize == 0 {
@ -33,11 +41,11 @@ func NewExecutionPool(initialSize int, timeout time.Duration) *SafePool {
return sp return sp
} }
var ptr atomic.Pointer[workerpool.WorkerPool] sp.executionPool.Store(workerpool.New(initialSize))
p := workerpool.New(initialSize) if metrics.Enabled && report {
ptr.Store(p) go sp.reportMetrics(3 * time.Second)
sp.executionPool = &ptr }
return sp return sp
} }
@ -51,10 +59,6 @@ func (s *SafePool) Submit(ctx context.Context, fn func() error) (<-chan error, b
return nil, true return nil, true
} }
if s.executionPool == nil {
return nil, false
}
pool := s.executionPool.Load() pool := s.executionPool.Load()
if pool == nil { if pool == nil {
return nil, false return nil, false
@ -97,3 +101,42 @@ func (s *SafePool) Size() int {
return s.size return s.size
} }
func (s *SafePool) Stop() {
close(s.close)
if s.executionPool.Load() != nil {
s.executionPool.Load().Stop()
}
}
// reportMetrics reports the metrics after every `refresh` time interval
// regarding the execution pool.
func (s *SafePool) reportMetrics(refresh time.Duration) {
var (
epWorkerCountGuage metrics.Gauge
epWaitingQueueGuage metrics.Gauge
epProcessedRequestsHistogram metrics.Histogram
)
ticker := time.NewTicker(refresh)
for {
select {
case <-ticker.C:
ep := s.executionPool.Load()
epWorkerCountGuage, epWaitingQueueGuage, epProcessedRequestsHistogram = newEpMetrics(s.service)
epWorkerCountGuage.Update(ep.GetWorkerCount())
epWaitingQueueGuage.Update(int64(ep.WaitingQueueSize()))
epProcessedRequestsHistogram.Update(s.processed.Load())
s.processed.Store(0)
case <-s.close:
ticker.Stop()
return
}
}
}

View file

@ -231,6 +231,8 @@ func (h *handler) startCallProc(fn func(*callProc)) {
defer cancel() defer cancel()
fn(&callProc{ctx: ctx}) fn(&callProc{ctx: ctx})
h.executionPool.processed.Add(1)
return nil return nil
}) })
} }
@ -269,7 +271,6 @@ func (h *handler) handleSubscriptionResult(msg *jsonrpcMessage) {
// handleResponse processes method call responses. // handleResponse processes method call responses.
func (h *handler) handleResponse(msg *jsonrpcMessage) { func (h *handler) handleResponse(msg *jsonrpcMessage) {
op := h.respWait[string(msg.ID)] op := h.respWait[string(msg.ID)]
if op == nil { if op == nil {
h.log.Debug("Unsolicited RPC response", "reqid", idForLog{msg.ID}) h.log.Debug("Unsolicited RPC response", "reqid", idForLog{msg.ID})
@ -292,6 +293,7 @@ func (h *handler) handleResponse(msg *jsonrpcMessage) {
if op.err = json.Unmarshal(msg.Result, &op.sub.subid); op.err == nil { if op.err = json.Unmarshal(msg.Result, &op.sub.subid); op.err == nil {
h.executionPool.Submit(context.Background(), func() error { h.executionPool.Submit(context.Background(), func() error {
op.sub.run() op.sub.run()
h.executionPool.processed.Add(1)
return nil return nil
}) })

View file

@ -103,7 +103,7 @@ func TestHTTPResponseWithEmptyGet(t *testing.T) {
func TestHTTPRespBodyUnlimited(t *testing.T) { func TestHTTPRespBodyUnlimited(t *testing.T) {
const respLength = maxRequestContentLength * 3 const respLength = maxRequestContentLength * 3
s := NewServer(0, 0) s := NewServer("test", 0, 0)
defer s.Stop() defer s.Stop()
s.RegisterName("test", largeRespService{respLength}) s.RegisterName("test", largeRespService{respLength})
ts := httptest.NewServer(s) ts := httptest.NewServer(s)

View file

@ -30,6 +30,7 @@ func DialInProc(handler *Server) *Client {
//nolint:contextcheck //nolint:contextcheck
handler.executionPool.Submit(initctx, func() error { handler.executionPool.Submit(initctx, func() error {
handler.ServeCodec(NewCodec(p1), 0) handler.ServeCodec(NewCodec(p1), 0)
handler.executionPool.processed.Add(1)
return nil return nil
}) })

View file

@ -38,6 +38,7 @@ func (s *Server) ServeListener(l net.Listener) error {
s.executionPool.Submit(context.Background(), func() error { s.executionPool.Submit(context.Background(), func() error {
s.ServeCodec(NewCodec(conn), 0) s.ServeCodec(NewCodec(conn), 0)
s.executionPool.processed.Add(1)
return nil return nil
}) })
} }

View file

@ -37,3 +37,11 @@ func newRPCServingTimer(method string, valid bool) metrics.Timer {
m := fmt.Sprintf("rpc/duration/%s/%s", method, flag) m := fmt.Sprintf("rpc/duration/%s/%s", method, flag)
return metrics.GetOrRegisterTimer(m, nil) return metrics.GetOrRegisterTimer(m, nil)
} }
func newEpMetrics(service string) (metrics.Gauge, metrics.Gauge, metrics.Histogram) {
epWorkerCount := fmt.Sprintf("rpc/ep/workers/%s", service)
epWaitingQueue := fmt.Sprintf("rpc/ep/queue/%s", service)
epProcessedRequests := fmt.Sprintf("rpc/ep/processed/%s", service)
return metrics.GetOrRegisterGauge(epWorkerCount, nil), metrics.GetOrRegisterGauge(epWaitingQueue, nil), metrics.GetOrRegisterHistogram(epProcessedRequests, nil, metrics.NewExpDecaySample(1028, 0.015))
}

View file

@ -56,18 +56,24 @@ type Server struct {
} }
// NewServer creates a new server instance with no registered handlers. // NewServer creates a new server instance with no registered handlers.
func NewServer(executionPoolSize uint64, executionPoolRequesttimeout time.Duration) *Server { func NewServer(service string, executionPoolSize uint64, executionPoolRequesttimeout time.Duration) *Server {
reportEpStats := true
if service == "" || service == "test" {
reportEpStats = false
}
server := &Server{ server := &Server{
idgen: randomIDGenerator(), idgen: randomIDGenerator(),
codecs: mapset.NewSet(), codecs: mapset.NewSet(),
run: 1, run: 1,
executionPool: NewExecutionPool(int(executionPoolSize), executionPoolRequesttimeout), executionPool: NewExecutionPool(int(executionPoolSize), executionPoolRequesttimeout, service, reportEpStats),
} }
// Register the default service providing meta information about the RPC service such // Register the default service providing meta information about the RPC service such
// as the services and methods it offers. // as the services and methods it offers.
rpcService := &RPCService{server} rpcService := &RPCService{server}
server.RegisterName(MetadataApi, rpcService) server.RegisterName(MetadataApi, rpcService)
return server return server
} }
@ -166,6 +172,9 @@ func (s *Server) serveSingleRequest(ctx context.Context, codec ServerCodec) {
// requests to finish, then closes all codecs which will cancel pending requests and // requests to finish, then closes all codecs which will cancel pending requests and
// subscriptions. // subscriptions.
func (s *Server) Stop() { func (s *Server) Stop() {
// Stop the execution pool
s.executionPool.Stop()
if atomic.CompareAndSwapInt32(&s.run, 1, 0) { if atomic.CompareAndSwapInt32(&s.run, 1, 0) {
log.Debug("RPC server shutting down") log.Debug("RPC server shutting down")
s.codecs.Each(func(c interface{}) bool { s.codecs.Each(func(c interface{}) bool {

View file

@ -29,7 +29,7 @@ import (
) )
func TestServerRegisterName(t *testing.T) { func TestServerRegisterName(t *testing.T) {
server := NewServer(0, 0) server := NewServer("test", 0, 0)
service := new(testService) service := new(testService)
if err := server.RegisterName("test", service); err != nil { if err := server.RegisterName("test", service); err != nil {

View file

@ -53,7 +53,7 @@ func TestSubscriptions(t *testing.T) {
subCount = len(namespaces) subCount = len(namespaces)
notificationCount = 3 notificationCount = 3
server = NewServer(0, 0) server = NewServer("test", 0, 0)
clientConn, serverConn = net.Pipe() clientConn, serverConn = net.Pipe()
out = json.NewEncoder(clientConn) out = json.NewEncoder(clientConn)
in = json.NewDecoder(clientConn) in = json.NewDecoder(clientConn)

View file

@ -26,7 +26,7 @@ import (
) )
func newTestServer() *Server { func newTestServer() *Server {
server := NewServer(0, 0) server := NewServer("test", 0, 0)
server.idgen = sequentialIDGenerator() server.idgen = sequentialIDGenerator()
if err := server.RegisterName("test", new(testService)); err != nil { if err := server.RegisterName("test", new(testService)); err != nil {
panic(err) panic(err)

View file

@ -203,7 +203,7 @@ func TestClientWebsocketPing(t *testing.T) {
// This checks that the websocket transport can deal with large messages. // This checks that the websocket transport can deal with large messages.
func TestClientWebsocketLargeMessage(t *testing.T) { func TestClientWebsocketLargeMessage(t *testing.T) {
var ( var (
srv = NewServer(0, 0) srv = NewServer("test", 0, 0)
httpsrv = httptest.NewServer(srv.WebsocketHandler(nil)) httpsrv = httptest.NewServer(srv.WebsocketHandler(nil))
wsURL = "ws:" + strings.TrimPrefix(httpsrv.URL, "http:") wsURL = "ws:" + strings.TrimPrefix(httpsrv.URL, "http:")
) )