java中使用kotlin
TLDR; Change how you fetch lists of information in your HTTP endpoints, and your application can often survive on 300–600MB of heap. Ours is usually somewhere between 200 and 350, though we have a max of 850MB to handle any high spikes.
TLDR; 更改在HTTP端点中获取信息列表的方式,您的应用程序通常可以在300–600MB的堆中存活。 我们的内存通常在200到350之间,尽管我们最大可以处理850 MB的峰值。
Doing this does not change code much and even helps against HTTP time outs. Most applications I have worked on can benefit from this, and even if you can not do this on all endpoints, it helps doing it where you can.
这样做不会改变代码,甚至可以帮助防止HTTP超时。 我从事的大多数应用程序都可以从中受益,即使您不能在所有端点上都做到这一点,也可以在可能的地方做到这一点。
It will make your application more scalable and robust.
这将使您的应用程序更具可扩展性和健壮性。
Oh yeah, and there is code at the bottom. :)
哦,是的,底部有代码。 :)
In our main back end application we are handling quite a few requests, but none that should take much memory. So when we started getting OutOfMemoryErrors we had to do some digging. We could of course increase memory, but memory in the cloud is expensive. And besides… I hate not understanding why stuff happens… ;)
在我们的主要后端应用程序,我们正在处理相当多的请求,但没有说应该采取的内存。 因此,当我们开始遇到OutOfMemoryErrors时,我们必须进行一些挖掘。 我们当然可以增加内存,但是云中的内存很昂贵。 而且……我讨厌不明白为什么会发生……;)
We use some caches, but neither of those were really big. And when consumption rose high it would always go down again on a later GC. So… Not a permanent leak, but peaks related to specific actions…
我们使用了一些缓存,但是它们都不是很大的。 当消耗量上升时,在以后的GC中它总是会再次下降。 因此...不是永久性的泄漏,而是与特定操作有关的峰值...
We zeroed in on a couple of endpoints that would get the history of orders with dates as parameters. Every once in a while the time range was increased and we could see clear issues in our back end application.
我们在两个端点上进行了归零,这些端点将获取以日期为参数的订单历史记录。 时间间隔每隔一段时间就会增加一次,我们可以在后端应用程序中看到明显的问题。
It made sense: To fetch a large list of orders from the DB we had to:
这是有道理的:要从数据库中获取大量订单,我们必须:
Read from the database 从数据库中读取 Transform into objects转化为物体Transform into JSON转换成JSONWrite JSON to the response 将JSON写入响应This basically gives at least (maybe more?) two full representations of your objects in memory at the same time. So the larger the list (the DB) gets the more memory you need. Not very scalable.:)
基本上,这一次至少可以同时(至少可以提供更多)对象的两个完整表示。 因此,列表(数据库)越大,获得的内存就越多。 伸缩性不是很好。
While you shouldn’t really fetch large lists it felt like an unnecessary scaling limitation. And techniques like paging is less effective if you really have to fetch large volumes.
尽管您不应该真正获取大型列表,但感觉像是不必要的缩放限制。 如果确实需要获取大量数据,则分页之类的技术效果会较差。
So on a hunch we started looking into streaming… If you’re not doing sum/sort/groupBy etc. it should be possible to read one item, send it to the client, then read another one. Right? Filter/map etc should also work one piece at a time. Streaming would prevent having more than one object in memory at the same time.
因此,我们突然开始考虑流式传输…如果您不进行sum / sort / groupBy等操作,则应该可以读取一项,然后将其发送给客户端,然后再读取另一项。 对? 过滤器/地图等也应该一次起作用。 进行数据删除将防止同时在内存中拥有多个对象。
So we started digging… And it was a rabbit hole…But we got back out. :)
于是我们开始挖掘……那是一个兔子洞……但是我们又退出了。 :)
Because of the Streaming API introduced in Java 8 we thought it would be possible. It lets you compose operations that will be applied first when something starts pulling items off the stream.
由于Java 8中引入了Streaming API,我们认为这是有可能的。 它使您可以编写一些操作,这些操作将在某些操作开始从流中拉出项目时首先应用。
We are using JDBI and Postgres, so there might be special variations that don’t apply to you, but I hope you can easily find similar things in your stack.
我们正在使用JDBI和Postgres ,因此可能会有一些不适用于您的特殊变体,但是我希望您可以轻松地在堆栈中找到类似的东西。
After a lot of fumbling we found these things to be key:
经过大量摸索后,我们发现这些事情很关键:
The Handle (transaction) has to be opened and closed on your view layer. The same level where you have access to the output stream to write JSON to. In our case it is in the KTor mapping code. 必须在视图层上打开和关闭句柄(事务)。 您可以访问将JSON写入其中的输出流的同一级别。 在我们的情况下,它在KTor映射代码中。 Set the fetch size of your query, it will make it use a cursor. What is the most efficient here varies a lot, but for one of our main streams we have set 500. 设置查询的提取大小,它将使用游标。 这里最有效的方法有很多,但是对于我们的主流之一,我们将其设置为500。 The connections autocommit has to be false. At least in Postgres fetching chunks (with a cursor) won’t work if autocommit is true. In JDBI this is done by starting a transaction. Usually via Handle.useTransaction { … } . 自动提交的连接必须为false。 至少在Postgres中,如果autocommit为true,则无法使用游标获取块。 在JDBI中,这是通过启动事务来完成的。 通常通过Handle.useTransaction {…}。 Use Handle.stream() (instead of execute()) in JDBI. If you use a different library, hopefully it has a similar operation. :) 在JDBI中使用Handle.stream()(而不是execute())。 如果您使用其他库,则希望它具有类似的操作。 :)Use the Jackson Streaming API to write elements. When you map one element off the DB stream, you write it to the Jackson streaming API before you pull another off the DB stream. Rinse and repeat.
使用Jackson Streaming API编写元素。 当从数据库流中映射一个元素时,在将另一个元素从数据库流中拉出之前,将其写入Jackson流API。 冲洗并重复。
We fumbled around with this, and even forgot the autocommit in a re-factoring. We noticed the next time our application crashed though. ;)
我们对此无所适从,甚至忘记了重构中的自动提交。 但是,我们注意到下一次应用程序崩溃。 ;)
With this implemented we can now stream the entire order history from our database in parallel from multiple clients. Stupid, but we can. ;)
有了这个实现,我们现在可以并行地从多个客户端的数据库中流式传输整个订单历史记录。 愚蠢的,但我们可以。 ;)
Bonus: Because the first item is sent back to the client way before the last one is fetched+converted to object+generated JSON from the DB: Bytes will start to arrive “immediately”, and your HTTP connection will not get timed out by your proxy/router.
奖励:因为在从数据库中获取最后一个项目+将其转换为对象+生成的JSON之前,第一个项目已发送回客户端,所以字节将“立即”开始到达,并且您的HTTP连接不会超时代理/路由器。
After implementing the streaming our memory consumption looks like the below image when fetching several months of orders. The resulting JSON is about 1GB and would definitely have crashed our backend before:
实施流传输后,获取几个月的订单时,我们的内存消耗如下图所示。 生成的JSON大约为1GB,并且肯定会在我们之前使后端崩溃:
It hovers around a heap size of 250MB. And you can see the load and the GC time go up when fetching, but not much happening with the memory. :)
它徘徊在250MB的堆大小附近。 并且您可以看到在读取时负载和GC时间增加了,但是内存发生的却很少。 :)
You can find a fully working example with other stuff from previous posts at https://github.com/PorterAS/dependency-injection.
您可以从https://github.com/PorterAS/dependency-injection的以前的文章中找到包含其他内容的完整示例。
Here I will extract the main parts :) First out. The repository doesn’t look very different:
在这里,我将提取主要部分:)首先。 存储库看起来没有什么不同:
override fun listOrders( handle: Handle?, from: LocalDate, to: LocalDate ): Stream<Order> { // Did not limit on dates here, just minor tweak to query return handle!!.createQuery("SELECT * FROM orders") .setFetchSize(30) // Important to make it fetch in chunks .map(OrderMapper()) .stream() }The code above looks a lot like any regular repository. But notice that the handle is passed in (transaction has to be managed from view layer), and the fetch size is set.
上面的代码很像任何常规存储库。 但是请注意,传入了句柄(必须从视图层管理事务),并且设置了提取大小。
I have made the handle nullable here, but that’s just a minor detail to be able to create stubbed repositories without mocking the whole JDBI API.
我已经在这里将句柄设置为可空的,但这只是一个很小的细节,可以在不模拟整个JDBI API的情况下创建存根存储库。
The more complex and juicy stuff is in the view layer. There is an OrderService in between here, but that basically just passes the call on to OrderRepository for this specific case.
更复杂多汁的东西在视图层中。 在此之间有一个OrderService,但是对于这种特定情况,基本上只是将调用传递给OrderRepository。
get("order/list") { this.call.respondOutputStream(ContentType.parse("application/json")) { jdbi.open().use { handle -> jdbi.transactionHandler.begin(handle) try { orderService.listOrders(handle, LocalDate.now(), LocalDate.now()).use { orderStream -> // Avoiding using .use { ... } on the generator as it will close the underlying stream. // If an exception is thrown while writing, the stream is still needed for KTor to write the // error response back. So we leave the stream to be managed by KTor and will be // closed there. factory.createGenerator(this).let { jsonGenerator -> jsonGenerator.writeStartArray() orderStream.forEach { order -> jacksonMapper.writeValue(jsonGenerator, order) } jsonGenerator.writeEndArray() jsonGenerator.flush() jsonGenerator.close() } } jdbi.transactionHandler.commit(handle) } catch (e: Exception) { jdbi.transactionHandler.rollback(handle) } } } }In the above we do:
在上面我们做:
We get a outputstream to write to from KTor (call.respondOutputStream(…)). 我们从KTor获得了要写入的输出流(call.respondOutputStream(…))。 We open a JDBI handle and start a transaction. Here we use a quite manual way of setting up the transaction as we can’t use all of the JDBI api with KTor and async. The handle/transaction is passed to the service and repository to perform operations on. 我们打开JDBI句柄并开始交易。 这里我们使用一种非常手动的方式来设置事务,因为我们无法将所有的JDBI api与KTor和async一起使用。 句柄/事务被传递到服务和存储库以对其执行操作。 We run the actual query via the repository. 我们通过存储库运行实际查询。 Once we have a resulting stream, we create a Jackson Generator and write the start of the JSON array. 获得结果流后,我们将创建一个Jackson生成器并编写JSON数组的开始。 We then iterate one by one on the orders on the stream (pulling off) and write the JSON to the generator. 然后,我们按流中的顺序一个接一个地迭代(拉出),并将JSON写入生成器。 Write the end of the JSON array and clean up by commiting the transaction and closing streams that needs to be closed. Handling streams is mainly by using .use { … }. See comments in code for exception. 编写JSON数组的末尾,并通过提交事务并关闭需要关闭的流来进行清理。 处理流主要是使用.use {…}。 请参阅代码中的注释以了解异常。Because JDBI is told to stream as well, it will pull one record, map it to an object and then pass it on before doing another one.
因为JDBI也被告知要流式传输,所以它将提取一条记录,将其映射到一个对象,然后在进行另一条记录之前将其传递。
The above code is inlined to make it clear what happens. In our real code this is separated into utility methods such that it is wrapped and handled properly everywhere we use it. Our real code would look something like this:
内联以上代码是为了清楚地说明会发生什么。 在我们的实际代码中,将其分为实用程序方法,以便可以在我们使用它的所有位置对其进行正确包装和处理。 我们的真实代码如下所示:
get("order/list") { respondWithStream(jdbi, jsonFactory) { handle -> orderService.listOrders(handle, LocalDate.now(), LocalDate.now()) } }Please let me know if there any issues or improvements to be made. :)
请让我知道是否有任何问题或需要改进的地方。 :)
If you want to work with awesome light weight technologies we are recruiting! Go to https://jobb.porterbuddy.com/ to see more. :)
如果您想使用出色的轻型技术,我们正在招募中! 转到https://jobb.porterbuddy.com/了解更多信息。 :)
翻译自: https://medium.com/porterbuddy/living-the-stream-reducing-memory-usage-in-java-and-kotlin-d87f5972be44
java中使用kotlin
相关资源:微信小程序源码-合集6.rar