通过Java 16 Unix域套接字通道与Postgres交谈

2021-02-06 19:53:51

在阅读有关最近JDK 16中即将发生的事情的博客文章时,我了解到新功能之一是对Unix域套接字(JEP 380)的支持。在Java 16之前,您必须诉诸于诸如jnr-unixsocket之类的第三方库为了使用它们。如果您以前从未听说过Unix域套接字,则它们是"数据通信[端点],用于在同一主机操作系统上执行的进程之间交换数据。从版本10开始关闭; macOS甚至Windows 10以后也支持Unix域套接字。

诸如Postgres或MySQL之类的数据库使用它们为与数据库在同一台机器上运行的客户端应用程序提供基于TCP / IP的连接的替代方法。在这种情况下,Unix域套接字都更安全(对数据库的远程访问是不安全的)。完全公开;文件系统权限可用于访问控制),并且比TCP / IP环回连接更有效。

一个常见的用例是用于访问基于云的数据库的代理,例如GCP Cloud SQL Proxy。与客户端应用程序在同一台计算机上运行(例如,在Kubernetes部署中,在Sidecar容器中运行),它们提供对云数据库的安全访问。托管数据库,例如负责SSL处理。

令我好奇的是,我想知道如何使用新的Java 16 Unix域套接字连接到Postgres。那是大流行期间的例行晚上,无事可做,所以我想"让我们尝试一下。为了进行测试,我首先在Fedora 33上安装了Postgres13。Fedora可能并不总是已经打包了最新的Postgres版本,但是按照Postgres的官方说明,它很直接安装较新的版本。

为了通过Unix域套接字与用户名和密码连接,需要对/var/lib/pgsql/13/data/pg_hba.conf进行一些小的调整:必须将本地连接类型的访问方法从默认值切换为将对等值(将尝试使用客户端进程的操作系统用户名进行身份验证)为md5。

...#类型数据库用户地址方法#"本地"用于Unix域套接字连接onlylocal所有所有md5 ...

确保通过重新启动数据库来应用更改的配置(systemctl restart postgresql-13),一切准备就绪。

我研究的第一件事是Postgres JDBC驱动程序。自9.4-1208版本(2016年发布)以来,它允许您配置自定义套接字工厂,该功能明确考虑了Unix域套接字而添加。驱动程序本身不附带一个实际上支持Unix域套接字的套接字工厂实现,但是存在一些外部开源实现。最著名的junixsocket提供了这种套接字工厂。

自定义套接字工厂必须扩展javax.net.SocketFactory,并且必须使用socketFactory驱动程序参数指定其完全合格的类名称。因此,基于新的UnixDomainSocketAddress类创建SocketFactory实现应该很容易,对吗?

公共类PostgresUnixDomainSocketFactory扩展了SocketFactory {@Override public Socket createSocket()throws IOException {var socket = new Socket();插座。连接((" /var/run/postgresql/.s.PGSQL.5432")的UnixDomainSocketAddress。); (1)返回插座; } //其他创建方法...}

为Fedora和相关系统上的套接字的默认路径创建Unix域套接字地址

它可以很好地编译;但事实证明并非所有套接字地址都相等,并且java.net.Socket仅连接到InetSocketAddress类型的地址(并且PG驱动程序维护人员似乎对这些""异常&# 34;事件):

org.postgresql.util.PSQLException:发生某些异常情况导致驱动程序失败。请报告此异常。在org.postgresql.Driver.connect(Driver.java:285)...由...引起:: java.lang.IllegalArgumentException:java.base / java.net.Socket.connect(Socket.java:629)处的地址类型不受支持java.base / java.net.Socket.connect(Socket.java:595)在dev.morling.demos.PostgresUnixDomainSocketFactory.createSocket(PostgresUnixDomainSocketFactory.java:19)...

现在,JEP 380仅谈论SocketChannel,而对Socket保持沉默;但是也许从域套接字通道获取套接字是可行的吗?

实际上,看起来JEP 380仅与非阻塞SocketChannel API有关,而阻塞Socket API的用户却无法从中受益。应该有可能基于以下方面的套接字通道支持创建自定义Socket实现: JEP 380,但这超出了我的小探索范围。

如果Postgres JDBC驱动程序不能轻易地从JEP中受益,那么其他Java Postgres客户端呢?有几种非阻塞选项,包括Vert.x Postgres客户端和R2DBC。前者用于为Postgres带来Reactive功能。也进入了Quarkus堆栈,所以我将注意力转向了它。

现在,Vert.x Postgres Client已经通过向项目添加正确的Netty本机传输依赖项来支持Unix域套接字。因此从功能的角度来看,在这里并没有太多收获,但是能够使用域具有默认NIO传输的套接字仍然会很好,因为这意味着要减少一个依赖关系。因此,我对Postgres客户端和Vert.x本身的代码进行了深入研究,发现需要调整两件事:

Vert.x的基于NIO的Transport类需要了解一个事实,即SocketChannel现在还支持Unix域套接字(当前,当尝试在没有Netty本机传输的情况下使用它们时会引发异常)

Netty的NioSocketChannel需要进行一些细微的更改,因为它试图从基础SocketChannel获取一个Socket,但如上所述,它不适用于域套接字

通过创建默认Transport类的自定义子类可以快速完成第1步。需要更改两个方法:channelFactory()用于获取实际Netty传输通道的工厂,以及convert()用于将Vert.x SocketAddress转换为NIO一:

公共类UnixDomainTransport扩展了传输{@Override public ChannelFactory&lt ;?扩展频道> channelFactory(boolean domainSocket){if(!domainSocket){(1)返回super。 channelFactory(domainSocket); } else {return()-> {尝试{var sc = SocketChannel。打开(StandardProtocolFamily。UNIX); (2)返回新的UnixDomainSocketChannel(null,sc); } catch(Exception e){抛出新的RuntimeException(e); }; }} @Override public SocketAddress convert(io.vertx.core.net.SocketAddress address){if(!address。isDomainSocket()){(3)返回超级。转换(地址); } else {返回UnixDomainSocketAddress。 of(address.path()); (4)}}}

该通道工厂返回我们自己的UnixDomainSocketChannel类型的实例(请参见下文),并基于新的UNIX协议家族传递套接字通道

现在让我们看一下UnixDomainSocketChannel类,我希望通过创建基于NIO的实现的子类io.netty.channel.socket.nio.NioSocketChannel再次摆脱这种情况。 NioSocketChannel构造函数调用禁忌SocketChannel#socket()方法。在Netty本身中进行此更改当然不是问题,但出于我的小探索,我最终复制了该类并在该副本中进行了所需的调整。最终做了两个小改动:

公共UnixDomainSocketChannel(Channel parent,SocketChannel socket){super(parent,socket); config = new NioSocketChannelConfig(this,new Socket()); (1)}

传递虚拟套接字而不是socket.socket(),无论如何我们都不应该访问它

一些方法调用Socket方法isInputShutdown()和isOutputShutdown();应该可以通过自己跟踪两个关闭标志来绕过这些方法

当我在自己的命名空间而不是Netty的包中创建UnixDomainSocketChannel时,需要注释掉一些对非公共方法NioChannelOption#getOptions()的引用,这再次与域套接字的情况无关

您可以在此提交中找到完整的更改。总而言之,这不完全是一个手工的软件工程,但是这个小技巧至少足以让我们快速了解新的域套接字支持。当然,真正的实现可以在Netty项目本身中可以更正确地完成。

因此,是时候对该产品进行测试了。由于我们需要配置自定义Transport实施,因此PgPool实例的检索比平时更为冗长:

PgConnectOptions connectOptions =新的PgConnectOptions()。 setPort(5432)(1)。 setHost(" / var / run / postgresql")。 setDatabase(" test_db")。 setUser(" test_user")。 setPassword(" topsecret!"); PoolOptions poolOptions =新的PoolOptions()。 setMaxSize(5); VertxFactory fv =新的VertxFactory(); fv。传输(新的UnixDomainTransport()); (2)顶点v = fv。顶点()​​; PgPool客户端= PgPool。池(v,connectOptions,poolOptions); (3)

Vert.x Postgres客户端从给定的端口和路径(通过setHost())构造域套接字路径;完整路径将是/var/run/postgresql/.s.PGSQL.5432,与上面相同

然后我们可以照常使用客户端实例,只不过它现在将使用域套接字而不是通过TCP / IP连接到Postgres。所有这些仅使用默认的基于NIO的传输方式,而无需添加任何Netty本机依赖项,例如基于epoll的运输方式。

我目前还没有做过任何实际的性能基准测试;在对临时键执行琐碎的SELECT查询200,000次的快速即席测试中,我观察到使用Unix域套接字时的延迟约为0.11毫秒。 netty-transport-native-epoll和JDK 16 Unix域套接字(通过TCP / IP连接时)和〜0.13毫秒。因此,与其他报告相比,绝对有很大的改进,这绝对是低延迟用例的决定性因素,延迟降低约15%似乎在频谱的较低端。

应该进行一些更真诚的性能评估,例如还要检查对垃圾回收的影响。不用说,您应该仅基于自己的特定工作负载,在自己的硬件上信任自己的测量,以便确定是否是否会受益于域套接字。

数据库连接只是域套接字的用例之一;高性能的本地进程间通信可用于各种用例。我发现特别有趣的一个是基于多进程体系结构的模块化应用程序的创建。

例如,当考虑经典的Java Jakarta EE应用服务器时,您可以设想一个模型,其中应用服务器和每个部署都是独立的进程,通过域套接字进行通信。这将具有一些有趣的优点,例如更严格的隔离(例如,一个已部署的应用程序中的OutOfMemoryError不会影响其他应用程序),并且重新部署不会造成类加载器泄漏的任何风险,因为部署的JVM将重新启动。不利的是,您将面临更高的整体内存消耗(尽管这样做可能会至少部分地通过类数据共享(这也可跨JVM边界工作)和部署之间更昂贵(远程)的方法调用来缓解。

现在,由于各种原因,应用服务器模型已不受欢迎,但是这种多进程设计仍然非常有趣,例如,用于构建模块化应用程序,该应用程序应公开单个Web端点,同时由一组已开发的进程组装而成由几个独立的团队部署。另一个用例是台式机应用程序,该程序由一组出于隔离目的的进程组成,例如如今,大多数Web浏览器都采用了针对不同标签的不同处理过程。JEP380在创建Java应用程序时应简化此模型,例如考虑使用JavaFX构建的富客户端。

Unix域套接字的另一个非常有趣的功能是能够将打开文件描述符从一个进程转移到另一个进程。这允许对服务器应用程序进行无中断升级,而不会丢失任何打开的TCP连接。例如,Envoy Proxy使用了此技术。用于应用配置更改的方法:配置更改后,启动具有新配置的第二个Envoy实例,并接管前一个实例的活动套接字,并且在“排水期”之后进行。触发旧实例的关闭。这种方法可实现Envoy本身的真正不变的应用程序设计,它具有所有优点,而无需重新加载进程内配置。我强烈建议阅读上面链接的两篇文章,它们非常适合-有趣。

不幸的是,JEP 380似乎不支持文件描述符传输。因此,对于这种架构,您仍然必须避免使用前面提到的junixsocket库,该库明确列出了文件转录器传输支持作为其功能之一。不能利用Java的NIO API来利用它,而应该使用Netty等替代网络框架来实现。可能是在那些大流行周末中另一个博客文章的主题;)

这样就完成了我对Java 16对Unix域套接字的支持的小型探索。如果您要进行使用它们连接到Postgres的实验,请确保安装最新的JDK 16 EA构建并从中获取实验的源代码。这个GitHub仓库。

我希望像Netty和Vert.x这样的框架能够相当快地利用此JDK功能,因为只需要进行少量代码更改,并且用户就可以从域套接字的更高性能中受益,而不必拉扯为了保持与16之前的Java版本的兼容性,多发行版JAR提供了一种集成此功能的途径。