Scale By The Bay 2019: Norman Maurer, Netty 5: Lessons Learned
Recording: Scale By The Bay 2019: Norman Maurer, Netty 5: Lessons Learned
[Music] all right so I have the last talk for today with a few other speakers I hope you are still all awake and ready yeah perfect and ready for some war stories with the jvm so I will talk a little bit more about lessons learned in Eddie's so I'm not sure if everyone knows what Nettie is I hope so if not if you use the JVM you probably use it somewhere so chances are pretty high so while this is talk us about Nettie I think a lot of things that I'm talking about here is more generic so it may be even helpful in your framework so I hope you will enjoy it alright let's go so we always have an about me slide I will keep it short so I'm the Nettie project lead at the moment I'm a traffic jam peon which means some people thought I'm very important for the Chava a con Java system ecosystem for whatever reasons mostly because I'm always complaining about Java and IO I guess I'm a Cassandra MVP which means I did some contributions and important contributions to cassandra in the past I'm a member of the patch of software foundation which probably everyone knows here and I'm usually working on large-scale network services or frameworks which I used with them so very shortly what is nerdy just in case you don't know so first of it's a channel proposed network framework what does it mean so it's not like a jetty or whatever HTTP server you can build something like this with Neddie but it's really more about you can build whatever network service or framework you want on top of it so it's all about traveling data around like bytes on the wire basically it's very low level that means while the API I think is kind of easy to use I have it as a last point as well it's not as easy as you probably would have expected when you first come to nari because honestly network programming it's not easy at all right to handle back pressure and all that kind of stuff so it gives a lot of flexibility so it's not as easy as for example Tomcat chedi whatever but you can do a lot of great thanks for that it tries to had many optimizations from the end-user which means you shouldn't care about it what does it mean so we do all kind of craziness and then really mean for incidents to make it fast we use unsafe which you should never use that's what I was told so if you think you should use it maybe you still can but it's unsafe that's the name you can do all kind of crazy things for that we do a lot of chain I because Jana is a lot of fun so if you ever look into a project to write j9 Eddy is the one we love contributions by the way right so who uses Nadya it's always nice to tell someone what companies use Neddy or what project use it so first off just to be clear the latest table trees for that one so we also followed oh and three dot o on all of them but for that one is a supported one it's deployed and used almost everywhere so if you are on the internet you probably use Neddie and some extent so if you used for example Twitter today you use Nettie because you Nagel used to and Eddie and vinegars the core of Twitter right but it's also used by Alibaba Amazon ever eBay Facebook Google Netflix Twitter Square and so on and so on so basically all of the big companies are using it in some extent if you are really interested into it and just like to you know it call out names you can go on the website and check in if your name is not there just add it as well because we love to hear you use it it's really successful in terms of API it was even ported to different languages so for example it's ported to Swift they're Swift and IO which is the de facto standard if you write network non plug-in code and Swift these days it was ported to a C++ wangle which is a project of Facebook so if you know Neddie and know the JVM but also know C++ and use when go you can just go ahead because it's very very similar but there's also thought natty which was done by Microsoft so it's really I would say the API is very very generic so we can implement it in different languages and it's the factor the standard if you write network applications on the JVM even if you don't use it directly you use it somehow or most of them right hopefully I'm not wrong with that so if Nettie fallout 1 was so great why we even need to care about a new version this is a good question right because if it works don't touch it so you don't break it well the problem is you know there's a lot of tech gap I think that's the biggest problem but we have so native fallout one is there forever basically I think it was released like eight years before maybe seven I don't remember exactly and we are basically supporting it since forever which means we have some api's which are just grown over the time we have some api's which are not as user-friendly as they should be what we found out too late as always right we would like to use usage of java 8 features internally which is a little bit of a problem because it's so old we actually still support Java 6 in the core I don't hope you use Java 6 but there are people that still use Java 6 believe me we want to reflect the sayings not only in terms of API but also to make it more performant because we made some choice that which at the end we are not good and we only found out too late right and also we want to remove some obsolete code because the best code is no code all right now with all these details we go into the more interesting things I would hope so let me give you a deep dive into things that we learned in Neddie for adore and $3 and then we plan to improve in 5.0 which may introduce may mean we have some API breakage but we also is already big ported some of them but I think they are quite interesting to understand so first off stack taps is pain what does it mean if you ever worked on reactive framework and if you did Scala for example and do a lot you know a lot of futures and change them together if you have a stack trace good luck it's 100 lines of code yeah not code like output and you try to understand what's going on and most of it is not really interesting right but there are other problems so why does it even matter in Eddy so Neddy we have the concept of a channel pipeline I make it very quick here the channel pipeline is basically the base abstraction of Neddie it just allows you to stack channel handlers that's how we call it behind each other or in front of each other that basically intercept events into something with them the thing here is that you know like we have these three channel handlers so something comes from the wire some pipes for example it goes in to the red one the red one does something with it or don't touch anything with this pass it to the next one and so on and so on so you can build up like basically chains of operations in the same is true for outbound stuff if you want to write something right which means that you end up with a lot of them the problem here is that usually in real applications you have like four to twelve of these handlers maybe you have even more if you're super happy you have less and this means that because each Channel handler basically passes event to the next one in the in the pipeline until you basically have a terminating one that you build up large chains so as you see on the right side it's probably too small to read which means it's a bad sign we end up with such deep frames that it's not only hard to debug but also the problem is that by default the JVM stops to inline at some point by default it's basically nine for like nine stacks or line methods you can adjust this but as always with all the different configuration things that you can do as a JVM the best thing is if you don't need to touch it because if you start to touch it it has also other effects right so let us look at the channel handler so that's a channel handler it has a lot of different methods basically we are just talking about channel active years so channel active is called once in your connections established right if you are server for example and if you're interested in do something with it you override this method implemented and if you're not you basically call fire channel active which passes to the next one that's when you build up the chain right so because of this chain that we have we saw like okay how can we improve this how can we remove the chain because in reality even if you have like 12 of these handlers often you're only interested in one event and not like 12 different events that the channel hunter provides you a way to extend may only be interested in channel active but not in channel read for example so what we did was basically we came up with idea okay why we don't introduce an annotation the annotation itself we put it on the default method basically and which means like if you don't implement anything it basically is there you cannot use it by yourself because we sorta like oh it's pretty dangerous because you can shoot yourself in the foot and you shouldn't need to use it the thing here is the default channel pipeline which holds all the channel handler basically looks up okay I got a channel handle which is added to the channel pipeline which methods are annotated with the Skip annotation if it attacks there's a method which is annotated with this it's basically calculates okay this handler will never be interested in this event so I shouldn't even call it at all I should just skip it directly and go to the next one which means we basically make the stack a lot you know a lot smaller in size which is kind of like we do some kind of a manual I would say right we just remove operations that we really don't need to and how that looks like is is like this the implementation just you know like just the most important part so we have this skip annotation we have the static method which basically just checks like oh is this handler type does it have this annotation on it then we have this default implementation which has to skip one that I told before and then if you want to do something it just implemented or variety to skip annotation it's not there anymore right and what you can see here because everything is more interesting with numbers is we did some benchmarks and it that's a really really simple stupidest benchmark so we basically have five channel headers in the pipeline and the last one is like the terminating one which means like the first one first four could just skip everything and did test how many operations we can do per second basically without the skip annotation are we are about like 220 230 thousand operations if we have the skip annotation we are up to about 750,000 properly which means we are more than three times faster and that's only for five handlers so in real applications you like I said you usually have 12 which means that it's a lot lot of overhead that we basically crunching out and these operations happen on each time you read something from the wire so it happens really really frequently so these messes are super hot right so after we implemented this we were like okay how we can even make this faster because faster is always good right so what we what we basically saw it like okay so why we don't cache the bit mass that we basically calculated which basically methods we want to call and just store it somewhere so first off we started with some concurrent hash map kind of thing and start there but the reality was that even if it's primarily read-only it's not for free right and Nettie has a model of an event loop so basically we have like multiple events loop one event loop is once read it always is popper once read in this event this read never changed so what we said like okay why we don't just cache it basically in a straight local look it up there with thread and then use it which basically means in the worst case you recalculate it like n times like thread times which is not too bad right because it's only happens once maybe just an interesting note here we also have something which we call fast read local I'm not talking about this today but basically it's an optimized version of thread local for a threading model which is like 35 percent faster so if you ever look into building your own thread local kind of type and your PIN to once read you maybe want to check that out so how to get this improvement well I mean first off we only did 495 but at the end we decided we can just back pour it pretty easily to 94.1 which means you get it for free and we did this and I think it was just added like six releases ago which is basically six months ago because we released like every month and we saw like great improvements in performance in general but having the user not change the code at all you get it for free which I think it's always the best perform improvement right if you don't need to change anything all right we are going to the next so like I said Nettie has an event loop and the problem was with Nettie this event loop is basically black magic it's the core of it and it's not extensible what does it mean so the event loop works like this so you have this basically endless loop running forever that's powerful one thread it's waiting for I ought to happen once the i/o is there for connection we basically pick it up process it then also process like the user tasks that the user submitted to the event loop and do it again and again until the applications basically shut down so that's a whole concept right the problem here is yet basically each of this event of implementation is very specific to the operation system or to the transport because how you select for i/o is specific to what you're using in Linux you use a port for example in a PSD or in Mac OS you use KQ and Windows you use event ports and so on and so on and also they are popping up more and more alternatives that you can use on different operation system like for example IO ring right on Linux which is like a new one so we saw like it's actually a bad thing that it's very specific right because the problem here is that even if you could adjust it an extender to add some functionality you would need to redo it every time you need to support another operation system I mean we're still supporting also niño which is basically Java kind of version which works everywhere but let me tell you this we have our specialized transports which are written in Chennai they are all a lot faster and produce a lot less garbage everyone is using that in production basically there are no that cares about scale all right and we even make this hard to impress to extend them because most of them are final because why because we didn't want users to extend it and then depend on the implementation details because as a framework as nettie which is used everywhere once people start to extend your stuff you cannot break it anymore which is pretty bad so why is this a problem well so why is it a problem that you can extend it because you don't get information out of the event loop because what do you usually do with something like a thread pool executors which a lot of people extend is you add matrix for example so you override masses to get a better idea how often is a runnable schedule how long did it take till it was scheduled to get picked up and run how long did it take to run to complete the run and so on how big are the queue sizes and you want to make smart decisions about this right in production you need this kind of insight especially if things start to go wrong I mean as long as everything's fine nobody cares about all this information but as your service gets slower and slower you want to understand Hayes anyone may be plotting my event loop and so no process can be made or it's one event look more busy than the other one because the more connections are on them or is more I all going on or is it specific to protocol and all this kind of different informations that's usually what people want to have in production kinds of systems right so how can we fix this so the very very sad news is in that ax father when we kind of fix it at all or we would need to break it which we can't so what we did in 85 and I think that's one of the features are most excited about even if it sounds maybe boring because it gives you so much more insight into what is going on at the end is basically you solve it for how it works in 84 at all what we did in 85 you basically broke up the responsibility of executing tasks for executing operations and selecting from the i/o from the operation systems so basically have now two components in a system one is responsible to select all the events and the other one is basically responsible to do something with servants so that's on the right side right so how does it look from API perspective so we have this event loop like I said the event to be specially thing that drives everything we have an inner interface which we call unsafe why we call unsafe because we don't expect users to call it it's more for us if you call it good luck you can do it but don't blame us right that's why we call it unsafe it's basically it's a sign to don't touch it then we have this i/o entler which basically extent is unsafe interface and this has different methods like I want to run something and run means I want to run some native code I owe basically I want to wake up if I'm clocking in native code I want to prepare my native stuff to be destroyed and I want to destroy it at the end right in this context that we basically give in can tell you something so you can make smart decision can I block cannot plug how I long I can block or what is a maximum amount of time I should expect before something happens basically so coming back again so we talked about why is this a problem so let us look now how the API looks like so before you basically had this event loop groups and you basically created them and you had one of the event loop implementations pair operation system or pair you know like message how you select the i/o so you create something like new niño event loop group new equal event loop group and so on and so on so now what you're doing is to just create a multi thread event loop group and pass in basically an i/o handler which is specific for the operation system that you do and everything else just stays the same and which means that basically we are armed strongly encouraged extending this multi thread event loop group which returns single thread event loops basically because then you can follow on the pattern of thread pool executors and get information out of it and customize it and do a lot of a lot of different things which also comes with idea implement once use it multiple times so you don't need to duplicate code all the time so what extension points be providing now so for example we allow you to override the run method the run method is basically the one that is called once and that runs the endless loop until we shut down so what you can do with that well you could set up thread specific informations like a straight local or whatever whatever you want to do we do have a message run i/o that you can override which is called basically each time we run some IO that we know of and basically it returns how many i/o events it processed so you can have metrics like okay the last time I called this I basically processed 100 different connections and the other time 1000 and so on so on and it makes it interesting to make smart decisions which we went to use next time we allow you to override register deregister which helps you to keep track how many active connections are sitting on the event loop and so on and so on so if there's a lot of interesting sunny thing that you can do with that and this one basically exchange a single straight event executor which also gives you all of extension points which also follows basically the abstract execution service in Java so you also have like new tasks for that you can override so you can return your own promises you can wrap your own tasks to do something like metrics and all that kind of thing so I think it's a lot more extensible than before so what can this be used for like I said smart event loop selection based on the load for example or the number of registered channels reporting an event loop does take too long or longer than expected so maybe some kind of a watchdog that you usually know from clocking code right or you can preserve context of tasks executions a little bit like a threat local but it hops with execution of the callbacks right which you usually need in your frameworks alright so now we're coming to a very interesting topic so in Eddy we have a pool pipe of a locator I will talk a little bit about this and why we have it and why this was not enough basically for us because we still had a lot of cheezy pressure which is bad so we have this pool by buffalo cater and it's basically implemented after chaima log i'm not sure if everyone is familiar with Chema log so that Jay doesn't stand for Chava it stands for Jason it's the guy that invented it he works for Facebook and that's his melech implementation and I think it's even the default in FreeBSD or it was at some point but you can actually you can flip it in if you liked you can compile it and replace a chillip c or whatever you have like it's chillip similar can't use it and the idea here is that basically to make allocations cheap you have arena based allocations so the idea is you have multiple threads right and these threads each time you want to allocate some bytes if you look up first in the cache which is thread-local which means like if there's something inside there you can basically allocated without taking any locks so that's the best case if there's nothing there okay so we must lead to look somewhere else so we look into an arena the arena itself is like a mini alligator I would say and usually an arena is shared with multiple threats which means like this all of the threats access it at the same time which means there needs to be some synchronization going on right but because the implementation is kind of smart or the idea of the implementation is kind of smart you can they even minimize the allocation the synchronization on the allocations here because how it works is you have these arenas and you may end up with two different threads or three or whatever in the same arena but the arena itself is basically chunked in size classes what does it mean so if you do an allocation which is eight bytes for example we have a site concise class which goes from 0 to 64 bytes so each allocation which is smaller than 64 goes into this bucket then we have 64 to 128 128 to 15 to 12 and so on and so on and so on and it's even configurable so it's not a fixed thing when that means like if you're allocating for multiple threads and you allocate for different size classes there's no condition going on right because you basically lock or synchronize on this size classes so it's already really optimized it's really cool like the whole idea and weari implemented in Java basically so what we proved well we pull direct memory because first off the rect memory is is actually expensive to allocate and deallocate it's actually pretty bad if you ask me because basically the DL the the the the bound of the direct memory is tied to the is tied to your extra setting which is max direct memory and the default is actually to use the same bounds as for heap memory so you basically end up with two bounds which are kind of unrelated anyway so most people don't know that so you end up with this kind of stuff and the problem here is it's even worse because Java wants to give you the impression that you know you can allocate whatever you want and you don't need to care about freeing this memory because we are no C programmers is that the garbage collector also needs to take care of your direct memory but remember it's allocated from direct memory so it has nothing to do with the heap here which means like how does it work well they pull off a trick basically so each time you know like they do like a garbage collection heap garbage collecting they also free up direct memory but if you only allocate direct they will never run a GC cycle so how does it work well there is a neat trick which is really scary so if you don't believe me you can look in the in the source code it's open JDK so it's free you know you can look inside there so what they do is if you allocate a direct memory and there's no memory left they say like okay call let us just call system GC and do a sleep for 100 milliseconds and then we try again and we do it a few times we are maybe lucky you get memory back everything's cool everything goes on or we do out of memory arrow the problem here is if you ask me is that basically if you're very unlucky you will always be able to just allocate enough one time to end up in exactly the same situation again which means like it's basically you get a lot of latency you get you know stop the world kind of thing of effects and it's very hard to debug you don't want to have to have this kind of thing I would prefer to just die you know like drop it on the floor out of memory done don't sleep done right but we also also proved for heap memory you may ask why because heap memory is basically only like a byte every allocation and you basically wrap it well kind of the problem is because of the spec of how Java is specified is it always expect that it gets a mem set pipe buffer a byte array which means the byte air needs to be filled with zeros which means like if you allocate huge white areas basically you always pay the pride for mem set all of them and it's not for free basically so here I'm staying say like it's kind of useless for something like Neddie which puts a bike container around you bytes anyway which disallow you to read more than what you wrote in before right so you have like right and reading indexes and we kind of already you know like restrict you what you can do so why we should mem said some people may say okay because of security reasons we want to override what's inside there but I think that's a weak argument because at the end if you take a heap dump you have it anyway because it sits there until it's garbage collected so I don't buy that so we do this and we even go go more on with Nettie's so if you Java Java 9 and later and open up some access to some internal unsafe so they are not too unsafe basically we can even allocate by the areas which are not members at at all whicker's they have an api for it right but people should not use it but we do anyway right so ok we have all this pooling everything is cool so why is there still a problem well the problem is because Nellie used by path and the byte buffer is a concept of Madi and you want to talk to the JVM or to JDK libraries they expect a byte buffer which is their thing basically so we need come basically adapt from a pipe app to a byte buffer the thing here is we don't do any a copy of the storage itself but we still need to allocate the object and if people tell me like that's not a big problem then they probably didn't push the system hard enough because if you start to allocate ton of objects and you need to garbage collect it basically steals your CPU and there are always problems right so let us just look at the benchmark so the benchmark is very stupid basically it's just the echo server so we you know we send some data in and we basically echo it back to the to the remote peer and what we found out is that it's basically allocation free mostly in a steady state except the byte buffers all right you see it it's marked red I'm not sure if you can read the number it doesn't matter too much but it was basically the most expensive allocation or the most objects that were allocated we said like ok cool so what can we do about this because we already pool the rest of it right so we came up with the idea why we don't actually also cache these pipe buffers because you know we allocate this chunk of memory and it's sitting in a pool and there you know it will never change and if it changed basically we chopped everything anyway we can just reduce a byte buffer as long as we make sure that they are not in use anymore and in Eddy we use this concept of reference counting which is kind of scary because now you are back in Ceylon basically right so Neddie you need to call release to release byte buffer and if you don't do that's bad because we use a pool we have a leak detector which use like very interesting implementation so if you're into like details so we use like weak references and sampling to understand if you know like if there still a reference to this and no reference to it and you didn't call release so it's very interesting but it's very specific right so what we did was we added this DQ and we basically you know we just put in buffers inside there once where you turn them and pull from them and basically reuse the buffers and the interesting thing here is that once we did this the allocations are basically gone so now we basically only allocate like I don't know something like 1,000 objects basically for the whole run and it's basically stay like this forever as long you know when a new connection come in we need to allocate again but still it's a very good improvement right so so 95 let me just talk very briefly about the roadmap because we are pretty short on time anyway so 95 is planned for 2020 depending on how many external contributions we get it goes faster or slower so if you're bored or if you just like nerdy or you're just I don't know want to do something cool you can contribute it's open source we love contributions we love PR refuse it's open source right we want to do like better and alpha releases very soon we are for a github project where we track the pro the progress of the whole thing there still a lot of to do sadly like always and now it's also the idea to bring up new ideas and concerns because we can still break stuff once we are down with 95 we will need to wait for at least six again right which I don't know when it will happen anyway so what did we complete we simplified the channel handler abstraction already so before we had because we were Java 6 we had all these different abstract classes and adapters now it's just on one interface anymore because we have default most methods we have skip annotations so it's not a problem we did the event loop stuff the XS extensibility that I talked about we provide a way to make adapt futures where our own futures and coordinate to completion stage we removed a lot of deprecated stuff we dropped speedy I don't help you you speedy you should use HP to instant and all this kind of stuff right plant work rework a cheap here - a little bit rework or cookie stuff because there are changes that we would like to do we want to support here less 1.3 early data so we always support here less 1.3 so even if you use Java 8 and you use our stuff and we also have a native implementation of SSL using j'ni because we love the pain you get here less 103 for free you should do it it's pretty cool that way we want to rework the channel life sites and just clean up in channel right alright so I'm almost done last slide almost last slide like I said we love contributions so go nuts I want to see cells and pr's today so I will try to review them all if you do it we have Stack Overflow mailing list IRC channel we on github Apache License 2 so basically you can do whatever you want with project you can do commercial stuff you can sell it whatever so you can it's a very very I think very very good open-source license because it doesn't have a lot of restrictions you just need to boot to some attributions basically alright so I'm basically done if you have any questions I'm happy to answer but I'm also happy to answer later if you are shy or if you have no questions that's fine as well anyone what kind of Java it features to you do you want to use all to you what was the motivation basically for Java 8 features in 85 ok so so you're asking why we even Nietzsche laid right what features we are looking for to use so there if you mess it handles I think it's one of them so so at the moment we have some kind of runtime checks sometimes to decide you know which is basically or usually optimize away by the JIT anyway then because the other code is dead right so we have some checks like how so we take the Java version and then based on the Java version we do something depending on on which version you run so we do some stuff for chav 11 because there were some API is for example that post edit there or we do something for Java 8 if there's something and so on and so on so we would like to remove all this hex basically and just use message handles which are also a lot faster than on reflection so we at the start we actually talked about make Java 11 the minimum but once we started with it we basically realized there's no real pressing need to be Java 11 and there's still a lot of people on Java 8 right and it's a very low-level API which means like if you use it you basically we put a burden on the user I would say I mean I think it's fine for service but well library you should try to stay old I would say but yeah it's it's basically more about you know like method handles some you know like some new constructs that we can use internal we have a lot of hex and SSL engine for example because we want to make it work with Java six seven eight nine eleven because there are different features like SNI you know matters and all that kind of stuff and also like Java it is nice because so a lot of people on use completion stage for example and we now allow to you know like convert to it so we can you know like use your functional programming kind of thing I said answer your question okay cool thank you very much for the talk it was a whirlwind of low-level JVM stuff and it was very cool thank you I had a question about and I saw the your kit little waterfall thing going down there how else how did you discover a lot of these performance bottlenecks yes that's a good question so usually yeah so I only use your kit here because it's easy to show actually so I'm not using you kid too much actually umm so I'm a big fan of azing profiler for example I'm not sure are you aware of as in profiler it's an open source tool it's pretty cool if you need more information later it can also give it to you but basically as in profiler it allows you at least so on different operation system you can do different things but you can basically do allocation profiling like CPU base you can also do like lock stuff and for example on Linux it allows you to basically um have a full view of the of the system because you see the Java stuff but you also see like C and even the kernel stacks so you get like a full-blown picture which is especially interesting for i/o stuff or SSL or whatever so we get like a good understanding and it's very low overhead it's really very low overhead so you can actually run on production system and you don't need to have an agent or nothing it just basically works right so it's I think it's a pretty cool project for example or you can also use like perf map for example which is another one but you get you can also export the data and flame curves which help so yep and also customers basically like internal customers they tell like oh this is low right and then you look into it or like open source project right you know what I mean it's yeah we have time for one one more question [Music] hi grace may I ask could you talk a bit more about the h2 rework and what's the plan for okay yeah so I will try to make it quick so we have basically three api's for h2 for various reasons so we started with one API because we wanted to do it to make less allocations and all that kind of stuff the problem with this API is basically that that it's very verbose and it's very unmet you like so it doesn't use channel handlers so you get like callbacks it's very unnatural and also it only works from the event loop so it's very hard to use I would say and it's very white the API so to fix this basically I think Twitter contributed another one which basically makes you know it could use the old API but create out of the different HP frames it creates basically poachers and fights through the pipeline so it's really nettie like right but that still is kind of only solving at half because now you have all the different frames but they belong to different streams because it's multiplexed and now we have the third which builds on the other two basically which multiplex this frames what does it mean actually we creating a new channel with a new channel pipeline per frame per stream which means like for you it just looks like channel like a connection and you can add whatever channel handle as you want to and they you know work the same way as nettie but they are specific to a stream ID which means like it's only the flow for the stream like the frames and we actually only want to have the the frame one and the multiplex one because you're not only not always need to multiplex formica system overhead by the frame one you need always but we want to remove the first one basically thank you all right then thanks a lot and enjoy the night and evening [Applause] [Music]