go

[转载]golang和传统语言的网络层处理的对比

Posted by Zeusro on August 19, 2018
👈🏻 Select language

前言

原文写的挺好的,我决定节选一部分过来

节选自Golang服务器的网络层实现

传统语言的网络层处理

服务需要同时服务N个客户端,所以传统的编程方式是采用IO复用,这样在一个线程中对N个套接字进行事件捕获,当读写事件产生后再真正read()或者write(),这样才能提高吞吐:

image

上图中:

绿色线程为接受客户端TCP链接的线程,使用阻塞的调用socket.accept(),当有新的连接到来后,将socket对象conn加入IO复用队列。

紫色线程为IO复用的阻塞调用,通常采用epoll等系统调用实现IO复用。当IO复用队列中的任意socket有数据到来,或者写缓冲区空闲时可触发epoll调用的返回,否则阻塞epoll调用。数据的实际发送和接收都在紫色线程中完成。所以为了提高吞吐,对某个socketreadwrite都应该使用非阻塞的模式,这样才能最大限度的提高系统吞吐。例如,假设正在对某个socket调用阻塞的write,当数据没有完全发送完成前,write将无法返回,从而阻止了整个epoll进入下一个循环,如果这个时候其他的socket有读就绪的话,将无法第一时间响应。所以非阻塞的读写将在某个fd读写较慢的时候,立刻返回,而不会一直等到读写结束。这样才能提高吞吐。然而,采用非阻读写将大大提高编程难度。

紫色线程负责将数据进行解码并放入队列中,等待工作线程处理;工作线程有数据要发送时,也将数据放入发送队列,并通过某种机制通知紫色线程对应的socket有数据要写,进而使得数据在紫色线程中写入socket

这种模型的编程难度主要体现在:

  1. 线程少(也不能太多),导致一个线程需要处理多个描述符,从而存在对描述符状态的维护问题。甚至,业务层面的会话等都需要小心维护
  2. 非阻塞IO调用,使描述符的状态更为复杂
  3. 队列的同步处理

Golang如何实现网络层

通过参考多个Golang的开源程序,笔者得出的结论是:肆无忌惮的用goroutine吧。于是一个Golang版的网络模型大致是这样的:

image

上图是单个客户端连接的服务器模块结构,同样的一个颜色代表一个协程:

绿色goroutine依然是接受TCP链接

当完成握手accept返回conn对象后,使用一个单独的goroutine来阻塞读(紫色),使用一个单独的goroutine来阻塞写(红色)

读到的数据通过解码后放入读channel,并由蓝色的goroutine来处理

需要写数据时,蓝色的goroutine将数据写入写channel,从而触发红色的goroutine编码并写入conn

可以看到,针对一个客户端,服务端至少有3个goroutine在单独为这个客户端服务。如果从线程的角度来看,简直是浪费啊,然而这就是协程的好处。这个模型很容易理解,因为跟人们的正常思维方式是一致的。并且都是阻塞的调用,所以无需维护状态。

再来看看多个客户端的情况:

image

在多个客户端之间,虽然用了相同的颜色表示goroutine,但实际上他们都是独立的goroutine,可以想象goroutine的数量将是惊人的。然而,根本不用担心!这样的应用程序可能真正的线程只有几个而已。

Preface

The original article was well written, so I decided to excerpt a portion of it.

Excerpted from Golang Server Network Layer Implementation

Traditional Language Network Layer Processing

Services need to serve N clients simultaneously, so the traditional programming approach is to use IO multiplexing, which captures events for N sockets in a single thread, and then actually read() or write() when read/write events occur, in order to improve throughput:

image

In the above diagram:

The green thread is the thread that accepts client TCP connections, using blocking calls to socket.accept(). When a new connection arrives, the socket object conn is added to the IO multiplexing queue.

The purple thread is the blocking call for IO multiplexing, usually implemented using system calls like epoll. When any socket in the IO multiplexing queue has data arriving, or when the write buffer becomes free, the epoll call can return, otherwise the epoll call blocks. The actual sending and receiving of data are completed in the purple thread. So to improve throughput, read and write for a socket should use non-blocking mode, which maximizes system throughput. For example, suppose you’re calling a blocking write on a socket. Before the data is completely sent, write cannot return, which prevents the entire epoll from entering the next loop. If other sockets have read-ready at this time, they cannot respond immediately. So non-blocking read/write will return immediately when a fd is slow to read/write, rather than waiting until the read/write is complete. This improves throughput. However, using non-blocking read/write greatly increases programming difficulty.

The purple thread is responsible for decoding data and putting it into a queue, waiting for worker threads to process. When worker threads have data to send, they also put the data into a send queue and notify the purple thread through some mechanism that the corresponding socket has data to write, so that data is written to the socket in the purple thread.

The programming difficulty of this model is mainly reflected in:

  1. Few threads (and not too many), causing one thread to need to handle multiple descriptors, thus there are issues with maintaining descriptor state. Even business-level sessions need to be carefully maintained.
  2. Non-blocking IO calls make descriptor state more complex
  3. Synchronous processing of queues

How Golang Implements the Network Layer

By referring to multiple Golang open source programs, the author’s conclusion is: use goroutines recklessly. So a Golang version of the network model is roughly like this:

image

The above diagram shows the server module structure for a single client connection, with the same color representing a coroutine:

The green goroutine still accepts TCP connections

After the handshake completes and accept returns a conn object, use a separate goroutine to block read (purple), and use a separate goroutine to block write (red)

Read data is decoded and put into a read channel, and processed by the blue goroutine

When data needs to be written, the blue goroutine writes data to the write channel, which triggers the red goroutine to encode and write to conn

It can be seen that for a single client, the server has at least 3 goroutines serving this client alone. If viewed from a thread perspective, this is a waste, but this is the benefit of coroutines. This model is easy to understand because it’s consistent with normal thinking. And all calls are blocking, so there’s no need to maintain state.

Let’s look at the case of multiple clients:

image

Among multiple clients, although the same color is used to represent goroutines, they are actually independent goroutines. You can imagine the number of goroutines will be astonishing. However, there’s no need to worry! Such an application may only have a few actual threads.

Предисловие

Оригинальная статья была хорошо написана, поэтому я решил выдержать из нее часть.

Выдержка из Реализация сетевого уровня сервера Golang

Обработка сетевого уровня в традиционных языках

Сервисам нужно обслуживать N клиентов одновременно, поэтому традиционный подход к программированию — использовать мультиплексирование IO, которое захватывает события для N сокетов в одном потоке, а затем фактически read() или write(), когда происходят события чтения/записи, чтобы повысить пропускную способность:

image

На приведенной выше диаграмме:

Зеленый поток — это поток, который принимает TCP-соединения клиентов, используя блокирующие вызовы socket.accept(). Когда приходит новое соединение, объект socket conn добавляется в очередь мультиплексирования IO.

Фиолетовый поток — это блокирующий вызов для мультиплексирования IO, обычно реализованный с использованием системных вызовов, таких как epoll. Когда любой socket в очереди мультиплексирования IO получает данные, или когда буфер записи становится свободным, вызов epoll может вернуться, иначе вызов epoll блокируется. Фактическая отправка и получение данных завершаются в фиолетовом потоке. Поэтому для повышения пропускной способности read и write для socket должны использовать неблокирующий режим, что максимизирует пропускную способность системы. Например, предположим, что вы вызываете блокирующий write на socket. Прежде чем данные будут полностью отправлены, write не может вернуться, что предотвращает вход всего epoll в следующий цикл. Если в это время другие sockets готовы к чтению, они не могут ответить немедленно. Поэтому неблокирующее чтение/запись вернется немедленно, когда fd медленно читает/пишет, а не будет ждать завершения чтения/записи. Это повышает пропускную способность. Однако использование неблокирующего чтения/записи значительно увеличивает сложность программирования.

Фиолетовый поток отвечает за декодирование данных и помещение их в очередь, ожидая обработки рабочими потоками. Когда рабочие потоки имеют данные для отправки, они также помещают данные в очередь отправки и уведомляют фиолетовый поток через какой-то механизм, что соответствующий socket имеет данные для записи, чтобы данные были записаны в socket в фиолетовом потоке.

Сложность программирования этой модели в основном отражается в:

  1. Мало потоков (и не слишком много), что заставляет один поток обрабатывать несколько дескрипторов, таким образом существуют проблемы с поддержанием состояния дескриптора. Даже сессии на уровне бизнеса нужно тщательно поддерживать.
  2. Неблокирующие вызовы IO делают состояние дескриптора более сложным
  3. Синхронная обработка очередей

Как Golang реализует сетевой уровень

Ссылаясь на несколько программ с открытым исходным кодом Golang, вывод автора: используйте goroutines безрассудно. Таким образом, версия сетевой модели Golang примерно такая:

image

Приведенная выше диаграмма показывает структуру модуля сервера для одного клиентского соединения, с тем же цветом, представляющим корутину:

Зеленая goroutine по-прежнему принимает TCP-соединения

После завершения рукопожатия и возврата conn object из accept, используйте отдельную goroutine для блокирующего чтения (фиолетовый), и используйте отдельную goroutine для блокирующей записи (красный)

Прочитанные данные декодируются и помещаются в канал чтения channel, и обрабатываются синей goroutine

Когда нужно записать данные, синяя goroutine записывает данные в канал записи channel, что запускает красную goroutine для кодирования и записи в conn

Видно, что для одного клиента сервер имеет по крайней мере 3 goroutines, обслуживающих этого клиента отдельно. Если рассматривать с точки зрения потока, это расточительно, но это преимущество корутин. Эта модель легко понять, потому что она согласуется с нормальным мышлением. И все вызовы блокирующие, поэтому нет необходимости поддерживать состояние.

Давайте посмотрим на случай нескольких клиентов:

image

Среди нескольких клиентов, хотя используется тот же цвет для представления goroutines, они фактически являются независимыми goroutines. Можно представить, что количество goroutines будет поразительным. Однако не нужно беспокоиться! Такое приложение может иметь только несколько фактических потоков.



💬 讨论 / Discussion

对这篇文章有想法?欢迎在 GitHub 上发起讨论。
Have thoughts on this post? Start a discussion on GitHub.

在 GitHub 参与讨论 / Discuss on GitHub