根据流经系统的消息大小设置这3个缓冲区大小就可以减少数据复制。 永远不会复制低于 4KB 的消息。 对于一百万并发连接,导致 100*10000 x 4KB = 4GB,今天的大多数服务器中是能够满足这个内存值的。 4KB 到 128KB 之间的消息将被复制一次,并且只需要将 4KB 数据复制到 128KB 缓冲区中。 128KB 和最大消息大小之间的消息将被复制两次。 第一次 4KB 将被复制,第二次 128KB 将被复制,因此共有 132KB 复制为最大的消息。 如果没有那么多 128KB 以上的消息,这还可以接受。
消息完全处理完毕后,应再次释放已分配的内存。 这样,从同一连接接收的下一条消息再次以最小的缓冲区大小开始,这可以确保在连接之间更有效地共享内存。 并不是所有的连接都会在同一时间需要大的缓冲区。
2. 通过追加消息调整大小另一种调整缓冲区大小的方法是使缓冲区由多个数组组成,当需要调整缓冲区大小时,只需继续分配另一个字节数组并将数据写入其中。
有两种方法来增加这样的缓冲区。 一种方法是分配单独的字节数组,并将这些字节数组的保存到一个列表中。 另一种方法是分配较大的共享字节数组的片段,然后将分配给缓冲区的每一个片段保存到一个列表。 就个人而言,我觉得第二种片段方法略好一些,但差别不大。
通过向其添加单独的数组或切片来增加缓冲区的优点是在写入期间不需要复制数据。 所有数据都可以直接从套接字(Channel)复制到数组或切片中。
以这种方式增长缓冲区的缺点是数据不存储在单个连续的数组中。 这使得消息解析更加困难,因为解析器需要同时查找每个单独数组的末尾和所有数组的末尾。 由于需要在写入的数据中查找消息的结尾,因此该模型不易使用。
TLV 编码消息一些协议消息格式使用 TLV 格式(type,length,value)进行编码。 这意味着,当消息到达时,消息的总长度存储在消息的开头,这样就可以立即知道为整个消息分配多少内存。
TLV 编码使得内存管理更容易,因为可以知道要为消息分配多少内存,不会存在只有部分被使用的缓冲区,所以没有内存被浪费。
TLV 编码的一个缺点是在消息的所有数据到达之前为消息分配所有内存。 因此,发送大消息的一些慢连接可以分配可用的所有内存,从而使服务器无响应。
此问题的解决方法是使用包含多个 TLV 字段的消息格式。 因此,为每个字段分配内存,而不是为整个消息分配内存,并且仅在字段到达时分配内存。 但是,一个大字段可能会对内存管理产生与大消息相同的影响。
另一种解决方法是对未收到的消息设置超时时间,例如 10-15 秒,这可以使服务器从许多大的同时到达的消息中恢复过来,但它仍然会使服务器一段时间无响应。 此外,故意的 DoS(拒绝服务)攻击仍然可以导致服务器的内存被耗尽。
TLV 编码存在不同的形式。实际使用字节数,指定字段类型和长度取决于每个单独的 TLV 编码。 还有 TLV 编码先放置字段的长度,然后是类型,然后是值(LTV编码)。 虽然字段的顺序不同,但它仍然是 TLV 变体。
实际上,TLV 编码使内存管理更容易,是使得 HTTP 1.1 协议如此糟糕的原因之一。 这也是为什么在HTTP2.0 中在数据传输时使用 TLV 来编码帧的原因。
写入部分消息在非阻塞 IO 管道中,写入数据也是一个挑战,在通道上调用 write(ByteBuffer)时,无法保证写入ByteBuffer 中的字节数。好在 write(ByteBuffer) 方法会返回写入的字节数,因此可以跟踪写入的字节数。 这就是挑战:跟踪部分写入的消息,最终发送消息的所有字节。
和管理读取部分消息一样,为了管理部分消息写入 Channel,我们将创建一个 Message Writer。 就像使用Message Reader 一样,我们需要为每个 Channel 关联一个 Message Writer 来编写消息。 在每个 Message Writer 中,跟踪它正在写入的消息的实际写入字节数。
如果有更多消息到达会先被 Message Writer 处理,而不是直接写入 Channel,消息需要在 Message Writer 内部排队,然后,Message Writer 尽可能快地将消息写入 Channel。
下图显示了到目前为止如何设计部分消息:
为使 Message Writer 能够发送之前仅部分发送的消息,需要时不时调用 Message Writer 让它发送更多数据。
如果有很多连接,对应就会有很多 Message Writer 实例。 例如有一百万个 Message Writer 实例,查看他们是否可以写数据也是很慢的。 首先,许多 Message Writer 实例中没有任何消息要发送,我们不想检查那些 Message Writer 实例。 其次,并非所有 Channel 实例都已准备好将数据写入,我们不想浪费时间尝试将数据写入无法接受任何数据的 Channel 。