You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

270 lines
6.6 KiB

7 years ago
7 years ago
8 years ago
8 years ago
abci: Flush socket requests and responses immediately. (#6997) The main effect of this change is to flush the socket client and server message encoding buffers immediately once the message is fully and correctly encoded. This allows us to remove the timer and some other special cases, without changing the observed behaviour of the system. -- Background The socket protocol client and server each use a buffered writer to encode request and response messages onto the underlying connection. This reduces the possibility of a single message being split across multiple writes, but has the side-effect that a request may remain buffered for some time. The implementation worked around this by keeping a ticker that occasionally triggers a flush, and by flushing the writer in response to an explicit request baked into the client/server protocol (see also #6994). These workarounds are both unnecessary: Once a message has been dequeued for sending and fully encoded in wire format, there is no real use keeping all or part of it buffered locally. Moreover, using an asynchronous process to flush the buffer makes the round-trip performance of the request unpredictable. -- Benchmarks Code: https://play.golang.org/p/0ChUOxJOiHt I found no pre-existing performance benchmarks to justify the flush pattern, but a natural question is whether this will significantly harm client/server performance. To test this, I implemented a simple benchmark that transfers randomly-sized byte buffers from a no-op "client" to a no-op "server" over a Unix-domain socket, using a buffered writer, both with and without explicit flushes after each write. As the following data show, flushing every time (FLUSH=true) does reduce raw throughput, but not by a significant amount except for very small request sizes, where the transfer time is already trivial (1.9μs). Given that the client is calibrated for 1MiB transactions, the overhead is not meaningful. The percentage in each section is the speedup for flushing only when the buffer is full, relative to flushing every block. The benchmark uses the default buffer size (4096 bytes), which is the same value used by the socket client and server implementation: FLUSH NBLOCKS MAX AVG TOTAL ELAPSED TIME/BLOCK false 3957471 512 255 1011165416 2.00018873s 505ns true 1068568 512 255 273064368 2.000217051s 1.871µs (73%) false 536096 4096 2048 1098066401 2.000229108s 3.731µs true 477911 4096 2047 978746731 2.000177825s 4.185µs (10.8%) false 124595 16384 8181 1019340160 2.000235086s 16.053µs true 120995 16384 8179 989703064 2.000329349s 16.532µs (2.9%) false 2114 1048576 525693 1111316541 2.000479928s 946.3µs true 2083 1048576 526379 1096449173 2.001817137s 961.025µs (1.5%) Note also that the FLUSH=false baseline is actually faster than the production code, which flushes more often than is required by the buffer filling up. Moreover, the timer slows down the overall transaction rate of the client and server, indepenedent of how fast the socket transfer is, so the loss on a real workload is probably much less.
3 years ago
abci: Flush socket requests and responses immediately. (#6997) The main effect of this change is to flush the socket client and server message encoding buffers immediately once the message is fully and correctly encoded. This allows us to remove the timer and some other special cases, without changing the observed behaviour of the system. -- Background The socket protocol client and server each use a buffered writer to encode request and response messages onto the underlying connection. This reduces the possibility of a single message being split across multiple writes, but has the side-effect that a request may remain buffered for some time. The implementation worked around this by keeping a ticker that occasionally triggers a flush, and by flushing the writer in response to an explicit request baked into the client/server protocol (see also #6994). These workarounds are both unnecessary: Once a message has been dequeued for sending and fully encoded in wire format, there is no real use keeping all or part of it buffered locally. Moreover, using an asynchronous process to flush the buffer makes the round-trip performance of the request unpredictable. -- Benchmarks Code: https://play.golang.org/p/0ChUOxJOiHt I found no pre-existing performance benchmarks to justify the flush pattern, but a natural question is whether this will significantly harm client/server performance. To test this, I implemented a simple benchmark that transfers randomly-sized byte buffers from a no-op "client" to a no-op "server" over a Unix-domain socket, using a buffered writer, both with and without explicit flushes after each write. As the following data show, flushing every time (FLUSH=true) does reduce raw throughput, but not by a significant amount except for very small request sizes, where the transfer time is already trivial (1.9μs). Given that the client is calibrated for 1MiB transactions, the overhead is not meaningful. The percentage in each section is the speedup for flushing only when the buffer is full, relative to flushing every block. The benchmark uses the default buffer size (4096 bytes), which is the same value used by the socket client and server implementation: FLUSH NBLOCKS MAX AVG TOTAL ELAPSED TIME/BLOCK false 3957471 512 255 1011165416 2.00018873s 505ns true 1068568 512 255 273064368 2.000217051s 1.871µs (73%) false 536096 4096 2048 1098066401 2.000229108s 3.731µs true 477911 4096 2047 978746731 2.000177825s 4.185µs (10.8%) false 124595 16384 8181 1019340160 2.000235086s 16.053µs true 120995 16384 8179 989703064 2.000329349s 16.532µs (2.9%) false 2114 1048576 525693 1111316541 2.000479928s 946.3µs true 2083 1048576 526379 1096449173 2.001817137s 961.025µs (1.5%) Note also that the FLUSH=false baseline is actually faster than the production code, which flushes more often than is required by the buffer filling up. Moreover, the timer slows down the overall transaction rate of the client and server, indepenedent of how fast the socket transfer is, so the loss on a real workload is probably much less.
3 years ago
  1. package server
  2. import (
  3. "bufio"
  4. "context"
  5. "fmt"
  6. "io"
  7. "net"
  8. "runtime"
  9. "github.com/tendermint/tendermint/abci/types"
  10. tmsync "github.com/tendermint/tendermint/internal/libs/sync"
  11. tmlog "github.com/tendermint/tendermint/libs/log"
  12. tmnet "github.com/tendermint/tendermint/libs/net"
  13. "github.com/tendermint/tendermint/libs/service"
  14. )
  15. // var maxNumberConnections = 2
  16. type SocketServer struct {
  17. service.BaseService
  18. proto string
  19. addr string
  20. listener net.Listener
  21. connsMtx tmsync.Mutex
  22. conns map[int]net.Conn
  23. nextConnID int
  24. appMtx tmsync.Mutex
  25. app types.Application
  26. }
  27. func NewSocketServer(logger tmlog.Logger, protoAddr string, app types.Application) service.Service {
  28. proto, addr := tmnet.ProtocolAndAddress(protoAddr)
  29. s := &SocketServer{
  30. proto: proto,
  31. addr: addr,
  32. listener: nil,
  33. app: app,
  34. conns: make(map[int]net.Conn),
  35. }
  36. s.BaseService = *service.NewBaseService(logger, "ABCIServer", s)
  37. return s
  38. }
  39. func (s *SocketServer) OnStart(ctx context.Context) error {
  40. ln, err := net.Listen(s.proto, s.addr)
  41. if err != nil {
  42. return err
  43. }
  44. s.listener = ln
  45. go s.acceptConnectionsRoutine(ctx)
  46. return nil
  47. }
  48. func (s *SocketServer) OnStop() {
  49. if err := s.listener.Close(); err != nil {
  50. s.Logger.Error("Error closing listener", "err", err)
  51. }
  52. s.connsMtx.Lock()
  53. defer s.connsMtx.Unlock()
  54. for id, conn := range s.conns {
  55. delete(s.conns, id)
  56. if err := conn.Close(); err != nil {
  57. s.Logger.Error("Error closing connection", "id", id, "conn", conn, "err", err)
  58. }
  59. }
  60. }
  61. func (s *SocketServer) addConn(conn net.Conn) int {
  62. s.connsMtx.Lock()
  63. defer s.connsMtx.Unlock()
  64. connID := s.nextConnID
  65. s.nextConnID++
  66. s.conns[connID] = conn
  67. return connID
  68. }
  69. // deletes conn even if close errs
  70. func (s *SocketServer) rmConn(connID int) error {
  71. s.connsMtx.Lock()
  72. defer s.connsMtx.Unlock()
  73. conn, ok := s.conns[connID]
  74. if !ok {
  75. return fmt.Errorf("connection %d does not exist", connID)
  76. }
  77. delete(s.conns, connID)
  78. return conn.Close()
  79. }
  80. func (s *SocketServer) acceptConnectionsRoutine(ctx context.Context) {
  81. for {
  82. if ctx.Err() != nil {
  83. return
  84. }
  85. // Accept a connection
  86. s.Logger.Info("Waiting for new connection...")
  87. conn, err := s.listener.Accept()
  88. if err != nil {
  89. if !s.IsRunning() {
  90. return // Ignore error from listener closing.
  91. }
  92. s.Logger.Error("Failed to accept connection", "err", err)
  93. continue
  94. }
  95. s.Logger.Info("Accepted a new connection")
  96. connID := s.addConn(conn)
  97. closeConn := make(chan error, 2) // Push to signal connection closed
  98. responses := make(chan *types.Response, 1000) // A channel to buffer responses
  99. // Read requests from conn and deal with them
  100. go s.handleRequests(ctx, closeConn, conn, responses)
  101. // Pull responses from 'responses' and write them to conn.
  102. go s.handleResponses(ctx, closeConn, conn, responses)
  103. // Wait until signal to close connection
  104. go s.waitForClose(ctx, closeConn, connID)
  105. }
  106. }
  107. func (s *SocketServer) waitForClose(ctx context.Context, closeConn chan error, connID int) {
  108. defer func() {
  109. // Close the connection
  110. if err := s.rmConn(connID); err != nil {
  111. s.Logger.Error("Error closing connection", "err", err)
  112. }
  113. }()
  114. select {
  115. case <-ctx.Done():
  116. return
  117. case err := <-closeConn:
  118. switch {
  119. case err == io.EOF:
  120. s.Logger.Error("Connection was closed by client")
  121. case err != nil:
  122. s.Logger.Error("Connection error", "err", err)
  123. default:
  124. // never happens
  125. s.Logger.Error("Connection was closed")
  126. }
  127. }
  128. }
  129. // Read requests from conn and deal with them
  130. func (s *SocketServer) handleRequests(
  131. ctx context.Context,
  132. closeConn chan error,
  133. conn io.Reader,
  134. responses chan<- *types.Response,
  135. ) {
  136. var count int
  137. var bufReader = bufio.NewReader(conn)
  138. defer func() {
  139. // make sure to recover from any app-related panics to allow proper socket cleanup
  140. r := recover()
  141. if r != nil {
  142. const size = 64 << 10
  143. buf := make([]byte, size)
  144. buf = buf[:runtime.Stack(buf, false)]
  145. err := fmt.Errorf("recovered from panic: %v\n%s", r, buf)
  146. closeConn <- err
  147. s.appMtx.Unlock()
  148. }
  149. }()
  150. for {
  151. if ctx.Err() != nil {
  152. return
  153. }
  154. var req = &types.Request{}
  155. err := types.ReadMessage(bufReader, req)
  156. if err != nil {
  157. if err == io.EOF {
  158. closeConn <- err
  159. } else {
  160. closeConn <- fmt.Errorf("error reading message: %w", err)
  161. }
  162. return
  163. }
  164. s.appMtx.Lock()
  165. count++
  166. s.handleRequest(req, responses)
  167. s.appMtx.Unlock()
  168. }
  169. }
  170. func (s *SocketServer) handleRequest(req *types.Request, responses chan<- *types.Response) {
  171. switch r := req.Value.(type) {
  172. case *types.Request_Echo:
  173. responses <- types.ToResponseEcho(r.Echo.Message)
  174. case *types.Request_Flush:
  175. responses <- types.ToResponseFlush()
  176. case *types.Request_Info:
  177. res := s.app.Info(*r.Info)
  178. responses <- types.ToResponseInfo(res)
  179. case *types.Request_DeliverTx:
  180. res := s.app.DeliverTx(*r.DeliverTx)
  181. responses <- types.ToResponseDeliverTx(res)
  182. case *types.Request_CheckTx:
  183. res := s.app.CheckTx(*r.CheckTx)
  184. responses <- types.ToResponseCheckTx(res)
  185. case *types.Request_Commit:
  186. res := s.app.Commit()
  187. responses <- types.ToResponseCommit(res)
  188. case *types.Request_Query:
  189. res := s.app.Query(*r.Query)
  190. responses <- types.ToResponseQuery(res)
  191. case *types.Request_InitChain:
  192. res := s.app.InitChain(*r.InitChain)
  193. responses <- types.ToResponseInitChain(res)
  194. case *types.Request_BeginBlock:
  195. res := s.app.BeginBlock(*r.BeginBlock)
  196. responses <- types.ToResponseBeginBlock(res)
  197. case *types.Request_EndBlock:
  198. res := s.app.EndBlock(*r.EndBlock)
  199. responses <- types.ToResponseEndBlock(res)
  200. case *types.Request_ListSnapshots:
  201. res := s.app.ListSnapshots(*r.ListSnapshots)
  202. responses <- types.ToResponseListSnapshots(res)
  203. case *types.Request_OfferSnapshot:
  204. res := s.app.OfferSnapshot(*r.OfferSnapshot)
  205. responses <- types.ToResponseOfferSnapshot(res)
  206. case *types.Request_LoadSnapshotChunk:
  207. res := s.app.LoadSnapshotChunk(*r.LoadSnapshotChunk)
  208. responses <- types.ToResponseLoadSnapshotChunk(res)
  209. case *types.Request_ApplySnapshotChunk:
  210. res := s.app.ApplySnapshotChunk(*r.ApplySnapshotChunk)
  211. responses <- types.ToResponseApplySnapshotChunk(res)
  212. default:
  213. responses <- types.ToResponseException("Unknown request")
  214. }
  215. }
  216. // Pull responses from 'responses' and write them to conn.
  217. func (s *SocketServer) handleResponses(
  218. ctx context.Context,
  219. closeConn chan error,
  220. conn io.Writer,
  221. responses <-chan *types.Response,
  222. ) {
  223. bw := bufio.NewWriter(conn)
  224. for res := range responses {
  225. if err := types.WriteMessage(res, bw); err != nil {
  226. closeConn <- fmt.Errorf("error writing message: %w", err)
  227. return
  228. }
  229. if err := bw.Flush(); err != nil {
  230. closeConn <- fmt.Errorf("error flushing write buffer: %w", err)
  231. return
  232. }
  233. }
  234. }