systemd for Developers I

    原文systemd for Developers I

    systemd not only brings improvements for administrators and users, it also brings a (small) number of new APIs with it. In this blog story (which might become the first of a series) I hope to shed some light on one of the most important new APIs in systemd: Socket Activation

    systemd 不仅为管理员和用户带来了改进,还带来了少量的新 api。在这个博客故事中(可能成为本系列文章的第一篇),我希望能对 systemd 中最重要的一个新 api 做一个分享:Socket Activation

    In the original blog story about systemd I tried to explain why socket activation is a wonderful technology to spawn services. Let’s reiterate the background here a bit.

    在关于 systemd 的最开始的博客故事中,我试图解释为什么 socket activation 是一种神奇的服务创建技术。让我们在这里再介绍一下背景。

    The basic idea of socket activation is not new. The inetd superserver was a standard component of most Linux and Unix systems since time began: instead of spawning all local Internet services already at boot, the superserver would listen on behalf of the services and whenever a connection would come in an instance of the respective service would be spawned. This allowed relatively weak machines with few resources to offer a big variety of services at the same time. However it quickly got a reputation for being somewhat slow: since daemons would be spawned for each incoming connection a lot of time was spent on forking and initialization of the services – once for each connection, instead of once for them all.

    Socket Activation 的基本思想并不是新的。inetd 从一开始就是大多数 Linux 和 Unix 系统的一个标准组件:它不在系统启动时生成所有本地 Internet 服务,而是代表服务进行监听,每当连接到来时,都会生成相应服务的实例。这使得资源较少的相对较弱的机器能够同时提供各种各样的服务。然而,它很快就以速度慢而闻名:因为每个传入的连接都会产生守护进程,所以在服务创建和初始化上花费了大量时间 —— 每个新的连接都会创建和初始化一次,而不是所有连接只创建一次。

    Spawning one instance per connection was how inetd was primarily used, even though inetd actually understood another mode: on the first incoming connection it would notice this via poll() (or select()) and spawn a single instance for all future connections. (This was controllable with the wait/nowait options.) That way the first connection would be slow to set up, but subsequent ones would be as fast as with a standalone service. In this mode inetd would work in a true on-demand mode: a service would be made available lazily when it was required.

    每个连接生成一个实例是 inetd 的主要使用方式,尽管 inetd 实际上也知道另一种模式:在第一个连接传入时,它会通过 poll()(或 select())感知到,并为将来的所有连接生成一个实例。(可以通过 wait/nowait 选项来控制。)这样一来,第一个连接的建立速度会很慢,但随后的连接将与独立服务一样快。在这种模式下,inetd 将在真正的在按需模式下工作:当服务被需要的时候,它才被创建,延迟的创建。

    inetd’s focus was clearly on AF_INET (i.e. Internet) sockets. As time progressed and Linux/Unix left the server niche and became increasingly relevant on desktops, mobile and embedded environments inetd was somehow lost in the troubles of time. Its reputation for being slow, and the fact that Linux’ focus shifted away from only Internet servers made a Linux machine running inetd (or one of its newer implementations, like xinetd) the exception, not the rule.

    inetd 的关注点显然是 AF_INET(即互联网)socket。随着时间的推移,Linux/Unix 离开了服务器领域,在台式机,移动和嵌入式环境上变得越来越多。inetd 慢慢地陷入了麻烦。它的速度慢的特性,以及 Linux 的重心从互联网服务器移走的事实,使得运行 inetd(或其更新的实现之一,如 xinetd)的 Linux 机器成为少数,而不是标准。

    When Apple engineers worked on optimizing the MacOS boot time they found a new way to make use of the idea of socket activation: they shifted the focus away from AF_INET sockets towards AF_UNIX sockets. And they noticed that on-demand socket activation was only part of the story: much more powerful is socket activation when used for all local services including those which need to be started anyway on boot. They implemented these ideas in launchd, a central building block of modern MacOS X systems, and probably the main reason why MacOS is so fast booting up.

    当苹果工程师致力于优化 MacOS 引导时间时,他们发现了一种利用 socket 激活思想的新方法:他们将注意力从 AF_INET sockets 转移到 AF_UNIX sockets。他们注意到,按需的 socket 激活只是故事的一部分:当把 socket 激活用于所有本地服务(包括那些必须在启动时启动的服务)时,它的功能要强大得多。他们在 launchd 中实现了这些想法,launchd 是现代 macosx 系统的核心构建块,这可能是 MacOS 启动速度如此之快的主要原因。

    But, before we continue, let’s have a closer look what the benefits of socket activation for non-on-demand, non-Internet services in detail are. Consider the four services Syslog, D-Bus, Avahi and the Bluetooth daemon. D-Bus logs to Syslog, hence on traditional Linux systems it would get started after Syslog. Similarly, Avahi requires Syslog and D-Bus, hence would get started after both. Finally Bluetooth is similar to Avahi and also requires Syslog and D-Bus but does not interface at all with Avahi. Sinceoin a traditional SysV-based system only one service can be in the process of getting started at a time, the following serialization of startup would take place: Syslog → D-Bus → Avahi → Bluetooth (Of course, Avahi and Bluetooth could be started in the opposite order too, but we have to pick one here, so let’s simply go alphabetically.). To illustrate this, here’s a plot showing the order of startup beginning with system startup (at the top).

    但是,在继续之前,让我们更详细地了解一下非按需、非互联网服务的 socket 激活的好处是什么。考虑一下 Syslog、D-Bus、Avahi 和蓝牙守护进程这四种服务。D-Bus 将日志记录到 Syslog,因此在传统的 Linux 系统上,它将在 Syslog 之后启动。类似地,Avahi 需要 Syslog 和 D-Bus,因此在这两者之后都会启动。最后,蓝牙类似于 Avahi,也需要 Syslog 和 D-Bus,但根本不与 Avahi 交互。由于在传统的基于 SysV 的系统中,一次只能启动一个服务,因此会发生以下启动序列化:Syslog→D-Bus→Avahi→Bluetooth(当然,Avahi 和 Bluetooth 也可以按相反的顺序启动,但我们必须在这里选择一个,所以我们就按字母顺序来吧。为了说明这一点,这里有一个图,显示了从系统启动开始的启动顺序。

    !Parallelization plot

    Certain distributions tried to improve this strictly serialized start-up: since Avahi and Bluetooth are independent from each other, they can be started simultaneously. The parallelization is increased, the overall startup time slightly smaller. (This is visualized in the middle part of the plot.)

    某些发行版试图改进这种严格序列化的启动方式:由于 Avahi 和 Bluetooth 彼此独立,所以它们可以同时启动。并行化程度提高,整体启动时间略短。(这在图的中间部分可见。)

    Socket activation makes it possible to start all four services completely simultaneously, without any kind of ordering. Since the creation of the listening sockets is moved outside of the daemons themselves we can start them all at the same time, and they are able to connect to each other’s sockets right-away. I.e. in a single step the /dev/log and /run/dbus/system_bus_socket sockets are created, and in the next step all four services are spawned simultaneously. When D-Bus then wants to log to syslog, it just writes its messages to /dev/log. As long as the socket buffer does not run full it can go on immediately with what else it wants to do for initialization. As soon as the syslog service catches up it will process the queued messages. And if the socket buffer runs full then the client logging will temporarily block until the socket is writable again, and continue the moment it can write its log messages. That means the scheduling of our services is entirely done by the kernel: from the userspace perspective all services are run at the same time, and when one service cannot keep up the others needing it will temporarily block on their request but go on as soon as these requests are dispatched. All of this is completely automatic and invisible to userspace. Socket activation hence allows us to drastically parallelize start-up, enabling simultaneous start-up of services which previously were thought to strictly require serialization. Most Linux services use sockets as communication channel. Socket activation allows starting of clients and servers of these channels at the same time.

    Socket 激活可以完全同时启动所有四个服务,不用按顺序启动。listening sockets 的创建在守护进程本身之外,我们可以同时启动进程,并且它们能够立即连接到彼此的socket。比如说, 在一个独立的步骤中,创建了 /dev/log 和 /run/dbus/system_bus_socket,下一步将同时生成所有四个服务。当 D-Bus 想要记录到 syslog 时,它只将其消息写入 /dev/log。只要socket缓冲区没有满负荷运行,它就可以立即执行它想进行初始化的其他操作。一旦 syslog 服务赶上,它就会处理排队的消息。如果 socket 缓冲区满了,那么客户机日志记录将暂时阻塞,直到 socket 再次可写为止,并在可以写入日志消息的那一刻继续。这意味着服务的调度完全由内核来完成:从用户空间的角度来看,所有服务都是同时运行的,当一个服务不能跟上依赖它的服务时,它将暂时阻塞它们的请求,但一旦这些请求被处理掉,它们就会继续运行。所有这些都是完全自动的,对用户空间是透明的。因此,Socket 激活允许我们极大地并行化启动,从而能够同时启动以前被认为严格要求序列化的服务。大多数 Linux 服务使用 socket 作为通信通道。 socket 激活允许同时启动这些通道的客户端和服务器。

    But it’s not just about parallelization. It offers a number of other benefits:

    但这不仅仅是并行化。它还提供了许多其他好处:

    • We no longer need to configure dependencies explicitly. Since the sockets are initialized before all services they are simply available, and no userspace ordering of service start-up needs to take place anymore. Socket activation hence drastically simplifies configuration and development of services.

      我们不再需要显式地配置依赖关系。由于 socket 是在所有服务之前初始化的,它们是可用的,所以不再需要对服务启动进行排序。因此, socket 激活大大简化了服务的配置和开发。

    • If a service dies its listening socket stays around, not losing a single message. After a restart of the crashed service it can continue right where it left off.

      如果一个服务死了,它的侦听 socket 会一直存在,而不会丢失一条消息。重新启动崩溃的服务后,它可以继续在它停止的地方继续。

    • If a service is upgraded we can restart the service while keeping around its sockets, thus ensuring the service is continously responsive. Not a single connection is lost during the upgrade.

      如果一个服务升级了,我们可以重新启动该服务,同时保持它的 socket ,从而确保该服务持续响应。升级过程中没有一个连接丢失。

    • We can even replace a service during runtime in a way that is invisible to the client. For example, all systems running systemd start up with a tiny syslog daemon at boot which passes all log messages written to /dev/log on to the kernel message buffer. That way we provide reliable userspace logging starting from the first instant of boot-up. Then, when the actual rsyslog daemon is ready to start we terminate the mini daemon and replace it with the real daemon. And all that while keeping around the original logging socket and sharing it between the two daemons and not losing a single message. Since rsyslog flushes the kernel log buffer to disk after start-up all log messages from the kernel, from early-boot and from runtime end up on disk.

      我们甚至可以在运行时以客户端看不见的方式替换服务。例如,所有运行 systemd 的系统在启动时都会使用一个很小的 syslog 守护进程来启动,该守护进程将写入 /dev/log 的所有日志消息传递到内核消息缓冲区。这样我们就可以从启动的第一个瞬间开始提供可靠的用户空间日志记录。然后,当实际的 rsyslog 守护进程准备好启动时,我们终止迷你守护进程,并将其替换为真正的守护进程。所有这一切,同时保留原始日志 socket ,并在两个守护进程之间共享它,而不会丢失一条消息。由于 rsyslog 在启动后会将内核日志缓冲区刷新到磁盘上,因此来自内核的所有日志消息、从早期引导到运行时的日志消息都会在磁盘上结束。

    For another explanation of this idea consult the original blog story about systemd.

    关于这个想法的另一个解释,请参考关于 systemd 的原始博客故事

    Socket activation has been available in systemd since its inception. On Fedora 15 a number of services have been modified to implement socket activation, including Avahi, D-Bus and rsyslog (to continue with the example above).

    systemd 从一开始就使用了 socket 激活。在 Fedora15 上,许多服务更改为利用 socket 激活,包括 Avahi、D-Bus 和 rsyslog。

    systemd’s socket activation is quite comprehensive. Not only classic sockets are support but related technologies as well:

    systemd 的 socket 激活非常全面。不仅支持经典 socket ,还支持其他相关技术:

    • AF_UNIX sockets, in the flavours SOCK_DGRAM, SOCK_STREAM and SOCK_SEQPACKET; both in the filesystem and in the abstract namespace

      AF_UNIX sockets, in the flavours SOCK_DGRAM, SOCK_STREAM and SOCK_SEQPACKET; 不管是文件系统或者是抽象命名空间

    • AF_INET sockets, i.e. TCP/IP and UDP/IP; both IPv4 and IPv6

      AF_INET socket ,即 TCP/IP 和 UDP/IP;包括 IPv4 和 IPv6

    • Unix named pipes/FIFOs in the filesystem

      文件系统中的 Unix 命名管道 / FIFO

    • AF_NETLINK sockets, to subscribe to certain kernel features. This is currently used by udev, but could be useful for other netlink-related services too, such as audit.

      AF_NETLINK sockets,用于订阅某些内核功能。目前 udev 正在使用这一功能,但它也可以用于其他与 netlink 相关的服务,例如 audit。

    • Certain special files like /proc/kmsg or device nodes like /dev/input/*.

      某些特殊文件,如 /proc/kmsg 或设备节点,如 /dev/input/*。

    • POSIX Message Queues

      POSIX 消息队列

    A service capable of socket activation must be able to receive its preinitialized sockets from systemd, instead of creating them internally. For most services this requires (minimal) patching. However, since systemd actually provides inetd compatibility a service working with inetd will also work with systemd – which is quite useful for services like sshd for example.

    使用 socket 激活的服务必须使用从 systemd 预初始化的 socket,而不是在服务内部创建它们。对于大多数服务,这需要(最小)修补程序。不过呢,由于 systemd 提供了 inetd 兼容能力,一个使用 inetd 的服务也可以使用 systemd 工作 —— 这对于 sshd 这样的服务非常有用。

    So much about the background of socket activation, let’s now have a look how to patch a service to make it socket activatable. Let’s start with a theoretic service foobard. (In a later blog post we’ll focus on real-life example.)

    关于 socket 激活的背景,我们就说到这里了. 现在让我们来看看如何对服务进行修补,使其能够使用socket 激活。让我们从一个理论服务 foobard 开始。(在稍后的博客文章中,我们将关注真实生活中的例子。)

    Our little (theoretic) service includes code like the following for creating sockets (most services include code like this in one way or another):

    我们的小型(理论上)服务包含以下代码用来创建 socket(大多数服务都以某种方式包含这样的代码):

    /* Source Code Example #1: ORIGINAL, NOT SOCKET-ACTIVATABLE SERVICE */
    ...
    union {
            struct sockaddr sa;
            struct sockaddr_un un;
    } sa;
    int fd;
    
    fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd < 0) {
            fprintf(stderr, "socket(): %m\n");
            exit(1);
    }
    
    memset(&sa, 0, sizeof(sa));
    sa.un.sun_family = AF_UNIX;
    strncpy(sa.un.sun_path, "/run/foobar.sk", sizeof(sa.un.sun_path));
    
    if (bind(fd, &sa.sa, sizeof(sa)) < 0) {
            fprintf(stderr, "bind(): %m\n");
            exit(1);
    }
    
    if (listen(fd, SOMAXCONN) < 0) {
            fprintf(stderr, "listen(): %m\n");
            exit(1);
    }
    ...
    

    A socket activatable service may use the following code instead:

    端口激活服务可能会用下面这样的代码:

    /* Source Code Example #2: UPDATED, SOCKET-ACTIVATABLE SERVICE */
    ...
    #include "sd-daemon.h"
    ...
    int fd;
    
    if (sd_listen_fds(0) != 1) {
            fprintf(stderr, "No or too many file descriptors received.\n");
            exit(1);
    }
    
    fd = SD_LISTEN_FDS_START + 0;
    ...
    

    systemd might pass you more than one socket (based on configuration, see below). In this example we are interested in one only. sd_listen_fds() returns how many file descriptors are passed. We simply compare that with 1, and fail if we got more or less. The file descriptors systemd passes to us are inherited one after the other beginning with fd #3. (SD_LISTEN_FDS_START is a macro defined to 3). Our code hence just takes possession of fd #3.

    systemd 可能会向你传递多个 socket(这个依赖于配置,请参见下文)。在这个例子中,我们只对一个感兴趣。sd_listen_fds 返回传递了多少个文件描述符。我们只需将其与1进行比较,如果得到更多或更少,则失败。systemd 传递给我们的文件描述符从fd#3开始, 往上递增。(SD_LISTEN_FDS_START是一个定义为3的宏)。因此我们的代码把fd 设置成 3。 (这里我有点不明白, 写程序的人如果自己先创建了Socket呢? 还是说程序员要自己注意不要这样做?)

    As you can see this code is actually much shorter than the original. This of course comes at the price that our little service with this change will no longer work in a non-socket-activation environment. With minimal changes we can adapt our example to work nicely both with and without socket activation:

    如你所见,这段代码实际上比之前的代码(不使用socket activation的)要短得多。当然,这样做的代价是我们的服务在非socket激活环境中不再工作。只需稍加修改,我们就可以调整我们的示例,无论有没有socket激活都能很好地工作:

    /* Source Code Example #3: UPDATED, SOCKET-ACTIVATABLE SERVICE WITH COMPATIBILITY */
    ...
    #include "sd-daemon.h"
    ...
    int fd, n;
    
    n = sd_listen_fds(0);
    if (n > 1) {
            fprintf(stderr, "Too many file descriptors received.\n");
            exit(1);
    } else if (n == 1)
            fd = SD_LISTEN_FDS_START + 0;
    else {
            union {
                    struct sockaddr sa;
                    struct sockaddr_un un;
            } sa;
    
            fd = socket(AF_UNIX, SOCK_STREAM, 0);
            if (fd < 0) {
                    fprintf(stderr, "socket(): %m\n");
                    exit(1);
            }
    
            memset(&sa, 0, sizeof(sa));
            sa.un.sun_family = AF_UNIX;
            strncpy(sa.un.sun_path, "/run/foobar.sk", sizeof(sa.un.sun_path));
    
            if (bind(fd, &sa.sa, sizeof(sa)) < 0) {
                    fprintf(stderr, "bind(): %m\n");
                    exit(1);
            }
    
            if (listen(fd, SOMAXCONN) < 0) {
                    fprintf(stderr, "listen(): %m\n");
                    exit(1);
            }
    }
    ...
    

    With this simple change our service can now make use of socket activation but still works unmodified in classic environments. Now, let’s see how we can enable this service in systemd. For this we have to write two systemd unit files: one describing the socket, the other describing the service. First, here’s foobar.socket:

    通过这个简单的更改,我们的服务现在可以使用socket激活,但在经典环境中仍然可以正常工作。现在,让我们看看如何在systemd中启用此服务。为此,我们必须编写两个systemd单元文件:一个描述socket,另一个描述服务。首先看foobar.socket:

    [Socket]
    ListenStream=/run/foobar.sk
    
    [Install]
    WantedBy=sockets.target
    

    下面是对应的服务描述文件 foobar.service:

    [Service]
    ExecStart=/usr/bin/foobard
    

    If we place these two files in /etc/systemd/system we can enable and start them:

    我们将这两个文件放在/etc/systemd/system中,就可以激活并启动它们:

    # systemctl enable foobar.socket
    # systemctl start foobar.socket
    

    Now our little socket is listening, but our service not running yet. If we now connect to /run/foobar.sk the service will be automatically spawned, for on-demand service start-up. With a modification of foobar.service we can start our service already at startup, thus using socket activation only for parallelization purposes, not for on-demand auto-spawning anymore:

    现在我们的 socket 正在监听,但是我们的服务还没有运行。如果我们现在连接 /run/foobar.sk 服务将自动创建,以便按需启动服务。像下面这样修改foobar.service 我们可以在启动时就启动服务,这种情况下只是将socket激活用于并行化目的,不再用于按需创建服务:

    [Service]
    ExecStart=/usr/bin/foobard
    
    [Install]
    WantedBy=multi-user.target
    

    And now let’s enable this too:

    现在激活它:

    # systemctl enable foobar.service
    # systemctl start foobar.service
    

    Now our little daemon will be started at boot and on-demand, whatever comes first. It can be started fully in parallel with its clients, and when it dies it will be automatically restarted when it is used the next time.

    现在,我们的守护进程将在引导和按需启动时启动,无论哪个先启动。它完全可以和它的Client并行启动,当它挂了后,它将在下次被使用时自动重新启动。

    A single .socket file can include multiple ListenXXX stanzas, which is useful for services that listen on more than one socket. In this case all configured sockets will be passed to the service in the exact order they are configured in the socket unit file. Also, you may configure various socket settings in the .socket files.

    一个.socket文件可以包含多个 ListenXXX ,这对于侦听多个 socket 的服务很有用。在这种情况下,所有配置的 socket 都将按照它们在 socket 单元文件中的配置顺序传递给服务。此外,您可以在.socket文件中配置各种socket参数。

    In real life it’s a good idea to include description strings in these unit files, to keep things simple we’ll leave this out of our example. Speaking of real-life: our next installment will cover an actual real-life example. We’ll add socket activation to the CUPS printing server.

    在现实生活中,在这些单元文件中包含描述字符串(这是啥?注释?)是一个好主意,为了简单起见,我们在示例中先不说这个。说到现实生活:我们的下一期将介绍一个实际的例子: 我们将向CUPS打印服务器添加socket激活。

    The sd_listen_fds() function call is defined in sd-daemon.h and sd-daemon.c. These two files are currently drop-in .c sources which projects should simply copy into their source tree. Eventually we plan to turn this into a proper shared library, however using the drop-in files allows you to compile your project in a way that is compatible with socket activation even without any compile time dependencies on systemd. sd-daemon.c is liberally licensed, should compile fine on the most exotic Unixes and the algorithms are trivial enough to be reimplemented with very little code if the license should nonetheless be a problem for your project. sd-daemon.c contains a couple of other API functions besides sd_listen_fds() that are useful when implementing socket activation in a project. For example, there’s sd_is_socket() which can be used to distuingish and identify particular sockets when a service gets passed more than one.

    sd_listen_fds()函数定义在 sd-daemon.h 和 sd-daemon.c 。目前,这两个文件是drop-in 源文件,只需将其复制到你的源代码中即可。最终,我们计划将其做成一个共享库,但是使用drop-in文件可以让您以一种与socket激活兼容的方式编译您的项目,即使在systemd上没有任何编译时依赖性。sd-daemon.c 是自由授权的,应该在最奇特的unix上也能编译得很好,如果许可证对你的项目是个问题的话,算法非常简单,你可以用很少的代码重新实现。sd-daemon.c除了sd_listen_fds()之外,还包含两个其他API函数,这些函数在项目中实现 socket 激活时非常有用。例如,有一个sd_is_socket(),当一个服务被传递到多个 socket 时,它可以用来区分和标识特定的 socket。

    Let me point out that the interfaces used here are in no way bound directly to systemd. They are generic enough to be implemented in other systems as well. We deliberately designed them as simple and minimal as possible to make it possible for others to adopt similar schemes.

    我要指出,这里使用的接口绝不直接捆绑到 systemd。它们具有足够的通用性,可以在其他系统中实现。我们特意设计了尽可能简单和最小的方案,使其他人能够采用类似的方案。

    Stay tuned for the next installment. As mentioned, it will cover a real-life example of turning an existing daemon into a socket-activatable one: the CUPS printing service. However, I hope this blog story might already be enough to get you started if you plan to convert an existing service into a socket activatable one. We invite everybody to convert upstream projects to this scheme. If you have any questions join us on #systemd on freenode.

    请继续关注下一期。如前所述,本文将介绍一个将现有守护进程转换为可激活 socket 的守护进程的实例:CUPS打印服务。但是,如果您计划将现有服务转换为 socket 激活的服务,我希望这篇博客故事已经足够让你开始使用。我们邀请大家把 upstream 项目转化为这个方案。如果你有任何问题,请在 freenode #systemd 与我们联系。

    Read More...

    Coc.nvim

    今天看了一下 coc.nvim 的配置和使用, 挺不错的 VIM 插件, 记录一下.

    我之前也使用到了这个插件, 主要是用来做补全的. 今天想使用 git blame 功能的时候, 又看到了它. 想搞搞明白, COC 到底是个什么东西.

    什么是 COC

    COC 就是一个 VIM 的插件啦. 但它是一个管理插件的插件.

    和 Plug 不同的是, 它只能管理 COC 系列的插件~ 严格来说, 应该不能算是 VIM 插件吧, 应该算是 COC 的插件, 我的感觉是这样. 因为只能用 COC 来安装, 而且是安装在 COC 自己的目录下面的.

    COC 系列的插件有不少, 除了自动补全外, 还有比如 coc-go , coc-python, coc-git 啥的, 可能可以取代 vimgo , python-mode, vim-fugitive 等插件, 但我只装了 git 插件. 作者也说了, 用不用 COC 系列插件, 还是要看你的需求是什么, 看 COC 的插件是不是能满足你的需求, 如果不行的话, 还可以给作者提需求~~

    为什么需要它

    作者的说法是: 快,稳定,功能全, 而且和 VSCode 插件一样好用易用(这一点让我有点诧异, 因为我觉得 VIM 插件挺好用啊)

    这里需要提一个”功能全”这点, 之所以这么说, 因为它实现了完全的对LSP的支持. 那什么是 LSP 呢?

    LSP 是 language-server-protocol

    我简单说一下吧(其实我还没有搞懂其中一些细节,比如每种语言的协议是不是一样的,在哪里有具体的定义), 在没有 LSP 之前呢, 如果我们写 Python 程序, 需要编辑器提供各种功能, 像补全啊, 格式化啊, 纠错啊等等. 每种编辑器可能都有自己的实现方式.

    LSP 呢, 提供了一套协议和标准. 对补全啊等等功能放在 Server(不是 HTTP Server 啊) 这里, client 通过标准统一的接口去 server 取得结果. 这样一个 server 实现之后, 所有的工作, 不管是 Vscode, 还是 Vim, 或者其他工具, 都可以方便的使用这个 Server 来接口.

    而且 LSP 发展很快, 我看 Python 已经有三个实现了.

    安装使用

    安装使用挺简单, Readme 里面都有说, 我这里就略过吧.

    COC-List

    最后需要提一下COC-List, coc 主页上面专门提到了它, 看来作者应该也觉得这是一个挺好, 而且重要的功能.

    很多时候呢, 我们需要对一个列表进行操作, 比如 buffers 列表, 我们可能需要选中, 删除等操作. 但是 VIM 自带的 buffers 列表功能太单一了, 就是展示出来, 也不能过滤搜索. 还有其他一些列表(比如marks) 也是如此, 还有一些没有列表, 像 searchhistory

    COC-List 就是增强了列表的功能, 提供了丰富的列表, 见https://github.com/neoclide/coc-lists, 而且其他一些插件, 像 coc-git 也提供了一些它自己的列表功能, 都可以使用 Coc-List 功能来展示和操作.

    Read More...

    [译] Why Should We Separate A and AAAA DNS Queries

    看了朋友一篇文章https://leeweir.github.io/posts/wget-curl-does-not-resolve-domain-properly/, 直接拉到最后的根因, 看到里面写:

    curl 不指定协议访问的时候,为什么直接从(DNS)缓存里返回了结果而不做dns解析,还是要具体从libcurl的实现上去分析

    我的第一反应是不太可能, curl 的请求不会因为指定使用 ipv4 就做 DNS 解析, ipv6 不做. libcurl 不可能这么实现.

    然后去翻了 libcurl 的代码, 他是使用另外一个叫 ARES 的库去做的 DNS 解析. 在 ARES 中看到有解析 ipv6 的时候, 使用了多线程(细节有些记不清了). 然后搜索文章, 就看到了下面这篇文章: 为什么我们要分开请求 A 和 AAAA DNS记录?

    [原文地址] Why Should We Separate A and AAAA DNS Queries?

    Imagine what it was like to be an ancient mariner navigating the ocean blue at night using nothing more than stars, a sextant and a marine chronometer. Thankfully, navigating the Internet is not as daunting. The method that networked devices use to find their way around the digital ocean is the Domain Name System (DNS), which translates human-readable host and domain names into numerical IP addresses (and vice versa).

    想像一下古代的水手在夜晚的海洋航行, 除了星星,六分仪和天文钟之外, 没有其他东西可以指引方向. 还好, 在因特网上冲浪没有这么可怕. 网络设备使用域名系统(DNS) 在数字海洋里面找路, DNS 可以把人类可读的域名转成数字的 IP (或者反过来)

    Separate DNS Queries

    分开的 DNS 请求

    One aspect of dual-protocol behavior that often surprises peoples is that hosts send two separate DNS queries to their resolver. And today, frankly, all hosts are dual-protocol bilingual and can use either IP version (4 or 6) for their DNS traffic or for the DNS queries and responses contained within. The reason that there are separate IPv4 A record and IPv6 AAAA record DNS queries is that early IPv6 deployments occasionally encountered problems with older IPv4-only resolvers.

    让很多惊讶的一点是: 解析双协议(ipv4 ipv6)的时候, 会发送两个独立的 DNS 请求到解析服务器. 坦率的讲, 现在如今所有主机都可以支持双协议, 可以使用ipv4或者ipv6来传输数字, 或者响应 DSN 请求. 那为什么要分开请求呢? 因为一些早期的解析器只支持 IPV4, 这会导致一些问题.

    If a host sent an ANY query or an IPv6 AAAA DNS query to a resolver which was not IPv6-literate, the resolver would return an erroneous response code (RCODE) such as NXDOMAIN. The would lead the host to believe that the domain did not exist, when in fact there was a perfectly valid IPv4 A record that, if returned, would have resulted in the host at least making a connection over IPv4.

    如果一个主机发送一个 ANY 类型的请求, 或者是 AAAA IPv6 类型的请求. 解析服务器恰巧不能正确处理 IPV6, 它可能会返回一个错误码, 比如说 NXDOMAIN. 这可能会导致主机认为这个域名不存在, 但实际上域名可能有一个合法的 IPV4地址. 如果我们能拿到这个 IPV4 地址, 我们还可以使用 IPV4 连接.

    Because these older DNS resolvers could not handle a AAAA query or response correctly, the IETF issued RFC 4074 “Common Misbehavior Against DNS Queries for IPv6 Addresses”. Now, hosts issue separate AAAA and A queries and if the AAAA query fails, it is likely that the A query will succeed and the host can connect.

    因为这些早期的 DNS 解析器不能正确处理 AAAA 请求, IETE 还在 RFC 4074 中专门讨论了这个问题, 在这个 Issue 中讨论了一些已知的现象及其影响. 现在, 分开发送 AAAA 和 A 请求, 如果 AAAA 失败了, 很可能 A 请求还能正确返回, 我们也可以继续建连.

    For example, here is a Wireshark packet capture showing that a simple DNS query for www.rmv6tf.org resulted four packets on the network. The DNS query started with an A record query (packet 74) followed by an A record response (packet 75). Then an AAAA record query (packet 76) was sent and an AAAA record response (packet 79) was returned. The AAAA query is expanded in the frame packet decode window.

    来看个例子, 下面是 Wireshark 抓包, 显示了一个到 www.rmv6tf.org 的 DNS 请求和返回, 一共4个包. 先是发起了一个 A 记录请求, 接着一个 A 记录的返回. 然后是 AAAA 记录的请求和返回.

    !Wireshark Packet Capture

    [译者注] 后面已经和主题没有关系了, 是其他的一些东西, 奇怪的知识又增加了..

    In 2011, when World IPv6 Day was approaching, there was significant work performed to improve how hosts operated in dual-protocol environments and recovered from failures of either IP version. The IETF issued RFC 6555 “Happy Eyeballs”, which outlined a more aggressive algorithm that would provide connection resiliency and make the Internet users/customers/eyeballs happier with their connectivity. This happy eyeballs technique can be implemented in a web browser like Chrome, or the algorithm can be implemented in the host OS like with Microsoft Network Connectivity Status Indicator (NCSI) or in Apple iOS or OS X. Regardless, the outcome is that hosts can operate effectively in dual-protocol environments and can recover and establish IP connections using the version that provides the best end-user experience.

    2011 年, 世界 IPV6日临近之际, 为了改善主机在双协议环境中的运行方式以及从任一IP版本的故障中恢复的能力,进行了大量工作. IETF 提了 RFC 6555 “[Happy Eyeballs]”(https://tools.ietf.org/html/rfc6555), 这个提议提出了一个激进算法, 可以提供链接快速恢复能力, 让我们的互联网使用者/客户/EyeBalls 更 Happy. 这个技术可以在浏览器比如说 Chrome 中使用, 也可以在操作系统, 比如 Windows 或者 IOS 或 OS X中使用. 不管哪种, 结果就是可以在双协议环境下恢复以及使用正确的协议建连, 以给用户提供更好的体验.

    [译者注] Eyeball 指终端, 也就是代表互联网上的人类用户, 与此对应的是服务器端. 简单说下这个算法, 就是同时发起 IPV4 和 IPV6 的建连请求, 如果一个没返回, 就只使用另外一个; 如果都返回了, 就使用 IPV6, 把 IPV4 Reset掉. 同时要缓存这个结果, 以便后续直接使用. 建议缓存大概10分钟.

    Dangers of ANY Queries

    There are other issues with DNS queries for Query Class (QCLASS) ANY besides causing problems for very old DNS resolvers that don’t understand IPv6 AAAA records. A DNS ANY query can result in a lot of data returned from the authoritative name server. The DNS server that receives an ANY query will simply respond with all the information it has on the subject including A records, AAAA records, DNSSEC key material, etc. If a DNS server is acting as an Open DNS Resolver and not restricting who can query it, then it may be participating as an unknowing contributor to a DDoS attack. These same types of DDoS attacks can take place, on only with DNS, but with NTP, and may leverage insecure IoT devices.

    ANY 类型请求的危险

    除了一些老的解析服务器不能正确处理 AAAA 记录外, 请求 ANY 类型还有其他问题, 这样的请求可能会从权威域名服务器返回大量的数据. 域名服务器会把所有这些信息返回给请求者, 包括 A 记录, AAAA 记录, DNSSEC 等. 如果一个域名服务器提供公开服务, 不限制请求者, 那它可能会在不知情的情况下推演 DDoS 攻击的参与者. 同样类型的 DDos 攻击也可能发生成 NTP 服务中, 起到一个杠杠的作用.

    [译者注] DDoS的原理是, 往权威服务器写一个巨大的数据, 然后伪装受害者的地址发起 DNS 请求, 这会导致大量的数据涌向受害者.

    Today, the legitimate uses of an ANY query are almost non-existent, but the nefarious uses of ANY are numerous. Now there are organizations that want to stop answering ANY queries altogether. Among these organizations is CloudFlare, one of the largest IPv6-enabled Content Delivery Networks (CDNs). CDNs are another way that we can circumnavigate the Internet seas. CloudFlare stopped answering ANY DNS queries over one year ago. If you send a query for ANY to CloudFlare you will receive back a NotImp (Not Implemented) RCODE. CloudFlare’s team has also worked on two IETF DNSOP working group drafts on this topic, “DNS Meta-Queries restricted” and “Providing Minimal-Sized Responses to DNS Queries that have QTYPE=ANY”.

    如今,几乎不存在对 ANY 查询的合法使用,但对 ANY 的恶意使用却很多。 现在,有些组织希望完全停止回答 ANY 查询。 在这些组织中,CloudFlare是最大的启用 IPv6 的内容交付网络(CDN)之一。 CDN是我们可以遨游互联网海洋的另一个途径。 一年多以前,CloudFlare 停止回答ANY DNS 查询。 如果您向CloudFlare发送ANY查询,您将收到NotImp(未实现)RCODE。 CloudFlare的团队还就此主题制定了两个IETF DNSOP工作组草案,即“ DNS元数据查询受限制”和“对ANY DNS查询提供最小化的响应”。

    Future DNS Improvements The Internet, DNS servers, host operating systems, service providers and content providers have significantly progressed since RFC 4074 was written to address old IPv4-only resolvers. Now, few of us worry about misbehaving resolvers, other than the concern that they might be too permissive in allowing DNS DDoS packet amplification. At this middle-stage of IPv6 adoption, should the DNS behavior be changed again? Or would making a mid-voyage course correction lead us toward an Internet Bermuda Triangle?

    自从 RFC4074 被用来解决早期服务器的只能解析 IPv4 问题以来,互联网、DNS 服务器、主机操作系统、服务提供商和内容提供商都有了长足的进步。现在,我们很少有人担心错误的解析器,除了担心他们在允许 DNS-DDoS 数据包放大方面可能过于宽容,在 IPv6 应用的中间阶段,DNS 的行为是否应该再次改变?或者,中途修正航向会使我们走向互联网百慕大三角?

    During the recent 2017 North American IPv6 Summit, Dani Grant from CloudFlare (@thedanigrant), gave a presentation about their IPv6 deployment experiences. She mentioned the non-response to DNS ANY queries described above and mentioned how we may want to optimize DNS queries for IPv6.

    在最近的 2017 年北美 IPv6 峰会上,来自 CloudFlare(@thedanigrant)的 Dani Grant 介绍了他们的 IPv6 部署体验。她提到了上文说的 ANY DNS 查询没有响应的情况,并提到我们可能希望如何优化 IPv6 的 DNS 查询。

    Marek Vavrusa (@vavrusam) and Olafur Gudmundsson (@OGudm) from CloudFlare have put forward an IETF draft titled “Providing AAAA records for free with QTYPE=A”. This proposal eliminates the separate A and AAAA query we use today, and return an AAAA record response along with the A record response. This would cut the number of queries and responses in half.

    CloudFlare 的 Marek Vavrusa(@vavrusam)和 Olafur Gudmundsson(@OGudm)提出了一个 IETF 草案,标题是 “对 A 查询提供额外的 AAAA 记录”。这项提议取消了我们今天使用的单独的 A 和 AAAA 查询,并返回一个 AAAA 记录响应和一个A 记录响应,这样可以将查询和响应的数量减少一半。

    Providing both an A record response and AAAA record response could be thought of as a theoretical “AAAAA response”. Jokingly, the term “Quint-A” was coined by Cody Christman (of Wipro and the RMv6TF), while at the 2017 North American IPv6 Summit.

    提供 A 记录响应和 AAAA 记录响应可以被视为理论上的 “AAAAA 响应”。开玩笑地说,“Quint-A” 一词是由 Cody Christman(Wipro 和 RMv6TF 的)在 2017 年北美 IPv6 峰会上提出的。

    Research and work continues in this area to explore how DNS meta-CLASSes can be used to carry additional information such as AAAA records responses. These potential directions may include adding an ADDR meta-query. This would require changes to servers and hosts and would take years to gain wide-scale adoption. The intent here is that these changes could result in continuing to drive higher IPv6 adoption rates and reduce the DNS traffic on networks.

    在这一领域的研究和工作仍在继续,以探索如何使用 DNS 元类来承载附加信息,如 AAAA 记录响应。潜在的方向可能包括添加一个 ADDR 元查询。这将需要对服务器和主机进行更改,并需要数年时间才能获得广泛采用。目的是这些变化可以使 IPv6 采用率继续提高,并减少网络上的 DNS 流量。

    Summary Just like IPv4, IPv6 will never stop evolving as a network protocol. Even though we have standards for global Internet behavior, we are constantly seeking out ways to improve IP networking. Even though IPv6 is now firmly deployed on the Internet and its adoption continues to grow, there is still time to optimize IPv6 behavior. What might have worked well when we were embarking on the IPv6 voyage may not be the way we want our systems to behave when we move to a predominantly IPv6-only Internet. Even though IPv6 is a “work in progress”, there is no reason to let this slow down your IPv6 deployment plans. Full steam ahead! But we will use our rudder to make some subtle course corrections as we cruise onward.

    就像 IPv4 一样,IPv6 作为一种网络协议永远不会停止发展,尽管我们有了全球互联网行为的标准,但我们仍在不断寻找改善 IP 网络的方法。尽管 IPv6 现在已经牢固地部署在互联网上,而且其采用率也在不断增长,我们仍有时间优化 IPv6 的行为。当我们开始 IPv6 之旅时,可能效果良好的可能并不是我们希望我们的系统在移动到仅 IPv6 为主的 Internet 时的行为方式。尽管 IPv6 是一个 “正在进行的工作”,没有理由让这件事拖慢你的 IPv6 部署计划。全力以赴吧!但当我们继续航行时,我们会用我们的舵进行一些细微的航向修正。

    Read More...

    多线程一起读stdin

    一个进程里面开两个线程读取 stdin , 会是哪一个能读到呢? 来测下看.

    package main
    
    import (
    	"bufio"
    	"fmt"
    	"os"
    )
    
    func s(i int) {
    	for {
    		scanner := bufio.NewScanner(os.Stdin)
    		scanner.Scan()
    		t := scanner.Text()
    		fmt.Printf("%v %v\n", i, t)
    	}
    }
    
    func main() {
    	c := make(chan struct{})
    	for i := 0; i < 2; i++ {
    		go s(i)
    	}
    	<-c
    }
    

    在 MAC 上的输出:

    % while true ; do date ; sleep 1 ; done | go run a.go                                              1 ↵
    1 Wed Aug 12 11:04:51 CST 2020
    0 Wed Aug 12 11:04:52 CST 2020
    1 Wed Aug 12 11:04:53 CST 2020
    0 Wed Aug 12 11:04:54 CST 2020
    1 Wed Aug 12 11:04:55 CST 2020
    0 Wed Aug 12 11:04:56 CST 2020
    

    在 MAC 上跑 Docker, alpha:3.9.4 输出

    /tmp # while true ; do date ; sleep 1; done | ./a
    1: Thu Aug 13 03:04:17 UTC 2020
    0: Thu Aug 13 03:04:18 UTC 2020
    0: Thu Aug 13 03:04:19 UTC 2020
    1: Thu Aug 13 03:04:20 UTC 2020
    1: Thu Aug 13 03:04:21 UTC 2020
    1: Thu Aug 13 03:04:22 UTC 2020
    0: Thu Aug 13 03:04:23 UTC 2020
    1: Thu Aug 13 03:04:24 UTC 2020
    0: Thu Aug 13 03:04:25 UTC 2020
    1: Thu Aug 13 03:04:26 UTC 2020
    0: Thu Aug 13 03:04:27 UTC 2020
    0: Thu Aug 13 03:04:28 UTC 2020
    1: Thu Aug 13 03:04:29 UTC 2020
    ^C
    

    os.Stdin 其实是一个 pty – pseudo terminal driver.

    man page 里面写到, pty 是一对 character devices , 一个称之为 master device , 一个叫 slave device. slave device 给程序提供了一个接口. 真正的 terminal 后面是有一个设备处理输入的, 但 pty 后面是 master device 来处理.

    对 pty 做了三个测试, 有些不明白.

    测试1

    SSH 登陆到一台机器 , tty 看到他的是 pty 是 /dev/pts/0 . 我就在当前屏幕敲字母, 会一个个打印出来(废话). 我的理解是, 我在键盘敲字母, 这是输入, 输入到 /dev/pts/0 . 输出到 /dev/pts/0 , 表现就是在当前屏幕打印出来. 这也是我们平时司空见惯的表现.

    测试2

    另外开一个 SSH 登陆到同一台机器, 运行 cat /dev/pts/0 . 然后在前一个SSH 里面敲字母, 这时候可以看到敲的字母会一个出现在当前屏幕, 下一个出现在新的 SSH 屏幕, 交替出现. 我的理解是, 我在键盘敲字母, 这是输入, 输入到 /dev/pts/0 . 输出到 /dev/pts/0, 但 /dev/pts/0 被两个进程读取(一个是前一个 SSH 的 Bash, 另外一个是 CAT), 所以交替出现在两边.

    测试3

    再开一个 SSH 登陆到同一台机器, 运行 date > /dev/pts/0 , 可以看到日期输出到了第一个 SSH 里面. 此时第二个 SSH 里面的 CAT 还在运行着, 但并没有捕获任何输出. 我的理解: 没能理解.

    Read More...

    二分查找拐点的一个问题

    题目: 一个先升序再降序的数组, 二分查找拐点位置

    package main
    
    import (
    	"testing"
    	"time"
    )
    
    func binsearch(nums []int) (idx int, count int) {
    	s := 0
    	e := len(nums) - 1
    	var m int
    	for s < e {
    		m = (s + e + 1) / 2
    		count++
    		if nums[m] > nums[m-1] {
    			s = m
    		} else {
    			e = m - 1
    		}
    	}
    
    	return s, count
    }
    
    func binsearch2(nums []int) (idx int, count int) {
    	s := 0
    	e := len(nums) - 1
    	var ee int = e
    	var m int = 0
    	for s < e {
    		m = (s + e + 1) / 2
    		count++
    		if nums[m] > nums[m-1] {
    			count++
    			if m == ee || nums[m] > nums[m+1] {
    				return m, count
    			}
    			s = m
    		} else {
    			e = m - 1
    		}
    	}
    
    	return s, count
    }
    
    func createTestData(n int) [][]int {
    	testData := make([][]int, n)
    	for i := range testData {
    		sortNums := make([]int, n)
    		for j := 0; j < i; j++ {
    			sortNums[j] = j
    		}
    		for j := i; j < n; j++ {
    			sortNums[j] = i + n - j
    		}
    		testData[i] = sortNums
    	}
    	return testData
    }
    
    func main() {
    	testData := createTestData(1024)
    
    	var s, e int64
    
    	s = time.Now().UnixNano()
    	var totalCount int = 0
    	for i := 0; i < 1000; i++ {
    		for _, nums := range testData {
    			_, count := binsearch(nums)
    			totalCount += count
    		}
    	}
    	e = time.Now().UnixNano()
    
    	println(totalCount)
    	println(e - s)
    
    	s = time.Now().UnixNano()
    	totalCount = 0
    	for i := 0; i < 1000; i++ {
    		for _, nums := range testData {
    			_, count := binsearch2(nums)
    			totalCount += count
    		}
    	}
    
    	e = time.Now().UnixNano()
    	println(totalCount)
    	println(e - s)
    }
    
    func BenchmarkBinSearch(b *testing.B) {
    	testData := createTestData(1024)
    
    	b.ResetTimer()
    	var totalCount int = 0
    	for i := 0; i < b.N; i++ {
    		for _, nums := range testData {
    			_, count := binsearch(nums)
    			totalCount += count
    		}
    	}
    	//println(totalCount)
    }
    
    func BenchmarkBinSearch2(b *testing.B) {
    	testData := createTestData(1024)
    
    	b.ResetTimer()
    	var totalCount int = 0
    	for i := 0; i < b.N; i++ {
    		for _, nums := range testData {
    			_, count := binsearch2(nums)
    			totalCount += count
    		}
    	}
    	//println(totalCount)
    }
    

    方法2跑了更多的分支, 但是时间却比较短. 为什么呢?

    Read More...

    Kotlin Lombok Data

    刚刚发现一个 Kotlin 文件不能访问 lombok.Data 里面的private属性, 而另外一个可以.

    之前遇到过类似的问题, 所以原因我也一下就想到了~ :) 但是到网上搜索了一下解决方法, 却发现解决不了哈哈. 记录一下.

    之前遇到的问题好像是这样的, 一个 Java 方法没办法访问 Kotlin Data Class 里面的属性. 解决方法 G 了一下就找到了, 在官网也有说明, 是编译顺序的问题, 需要先编译 Kotlin 再 Java

    那这次遇到的问题, 恰恰反了过来, 需要先编译 Java, 那就没得搞了.

    前面说一个Kt 能访问 lombok.Data class, 另外一个不能, 是因为他们不在一个 Module

    Read More...

    一起来打牌

  • {{ totalScore(person) }} {{ person.name }} {{ score }}
  • Read More...

    Graphql Java

    写在最前

    我不是写一个 GraphQL 的使用指南, 是在写自己对 GraphQL 的一些使用感受. 我自己是非常喜欢它, 愿意给他打10分, 虽然还有些小瑕疵. 如果你用过 GraphQL 了, 可以看看我们是不是有同感.

    另外多提两句. 1 GraphQL 本身是一个协议, 每种语言都有自己的框架实现, 我自己只用过 Java 的. 2 他是一个执行引擎, 不是 HTTP 框架, 你可以在任何地方使用他们, 当然现在最常见的是提供 HTTP 接口.

    当前开发模式(未使用 GraphQL 前)

    先说 API 协议设计上面.

    请求(Request): Client 给需要的一些参数. 拿取文章列表这个接口举例: 请求中给分页大小 当前是第几页 排序方法(热度/发布时间/打分等).

    响应(Response): 就是按需要给 Client 返回他需要的字段.

    然后简单说一下代码实现上面.

    从数据库,或者是别的服务接口取回需要的数据, 然后按一定的逻辑做一些过滤, 做一些字段的组装, 返回给调用者.

    痛点

    1. 难以做到按需提供 API. 这是 GraphQL 着重宣传的点, 但我的觉得后面两点才是 GraphQL 更爽的地方.

      想像一样, 我们给客户 A 提供了一个文章列表的 API. 后面客户 B 说我们还需要文章作者的昵称. 我们就要在 API Response 里面再添加一个字段, 但这个字段 A 并不需要. 后面客户 C 又说我们需要另外一个字段, 字段可能会越加越多, 而其他人可能并不需要.

      取有些字段可能还会比较费时, 那么给 C 加的字段导致 A 调用时长增加了. 这时候, 我们可能会在 Request 里面增加一个参数, 可以默认不返回某字段, 只有 C 调用的时候才返回这个字段.

      又比如说, 用户 A 需要对每篇文章返回10条评论,C 需要20条.我们又需要一个参数,这些参数基本上都是最外层的,没有规范的命名标准.

      总的来说, 没有一套规范, 这点和 GraphQL 有鲜明的对比.可能每个点都可以用自己的方法解决掉,但 GraphQL 是降维打击

    2. Do Dto Ao Vo 等各层对象转来转去. 增加一个字段, 可能会在每个转换的地方都要修改代码, 非常繁琐.

    3. 飞线代码多, 逻辑分散在各个地方, 特别是多人开发, 项目多次转交后.

      就我自己的实际经验, 来举个例子, 一个新接手的项目. 文章作者下面有标签字段, 包含标签类型和标签名字. 新需求想把一个接口A 里面的作者标签做些过滤, 某种类型的标签就不要返回了.

      AuthorService 生成 Author 以及标签的函数, 被多个其他函数引用, 这些函数又被引用, 最后被多个接口使用. 那对我来说, 完成需求的最简单的方法, 就是在接口 A 最后返回的时候, 把标签过滤一下. 我把这段代码叫做飞线代码. 时间长了, 这种情况多了, 飞线越来越多, 字段的值可能会在多个地方, 在不同的逻辑下被修改. 给 Debug 和代码可读性带来痛苦.

    GraphQL 优势

    减少 dto - ao - vo 转换

    各种 O(Object) 的定义: 阿里巴巴Java开发手册中的DO、DTO、BO、AO、VO、POJO定义

    这些各种 O 的转换, 本意大概是想让代码更清楚, 减少耦合. 但实际上, 给我的体验非常糟糕, 添加一个字段需要在多个 O 之间转来转去. 我接过一个项目, 只是数据库里面多一个字段返回给前端, 需要修改大概10处代码, 就只是在 OOO 之间Get/Set

    GraphQL 里面(特指 Java 框架,其他语言的没有用过), 因为 data mapping 和 DataFetcher 的存在, 实际操作下来, 并不需要这些 OOO 之间的转换, 而且逻辑反而更清楚.

    代码复用

    虽然这里是写了”代码复用”, 但更像一种配置的复用. 我觉得比代码复用更简单, 更清楚, 写起来更简单, 别人看起来也一目了然.

    这个是 GraphqL 的框架本身的优势 (这里也特指 Java 的). 因为他的逻辑在字段和 DataFetcher 的绑定这里, 而不在具体的业务代码里面.

    举下例子吧.

    文章里面有返回作者字段(里面有头像,昵称等字段). 评论(Comment)现在也要加作者字段. 在之前的开发模式下呢, 就是找到评论这块代码, 通过评论里面的 authorId 去调用 AuthorService 代码, 返回到 Comment 对象里. 这里我这里只用了一句话来说明需要做什么, 但实际上, article->comment->author 一层层找下去, 并不是很愉快的事, 这里还只三层而已. 另外还有dto - ao - vo 转换让我非常头疼.

    GraphQL (Java框架) 里面, 只要把 Comment 的 Author 字段绑定到 AuthorFetcher 就好了, 应该就一行代码.

    逻辑清晰

    这个是针对前面提到的飞线代码. 因为 GraphQL 天然的一个字段对应一个 DataFetcher, 所以逻辑再怎么飞也飞不多远. 在 GraphQL 里面做一些逻辑的修改是很愉快的事.

    更自然的并行

    也许吧, 我只是列在这里了, 其实对这一点, 我自己的感受并不深刻. 可能是我们的 QPS 太低, 对响应时间也不太苛刻, 不用并行也无所谓, 感受不到.

    规范化的批量处理

    比如说有好多地方需要使用 imageId 去取 image 的具体信息, 你会把这些地方合并起来批量去取吗? 你可能会很纠结, 因为代码写起来会麻烦一些, 而且充满了回调这样的东西. GraphQL 里面的 DataLoader (java 特有? 不确定) 为你提供了一个规范的批量处理的方式, 而且使用非常自然.

    但他未必能把所有 imameId 收集到一起再批量去取. 请阅读和实践一下 Dispatch 的概念, 还有 dataLoader 那一篇官方文档. 我觉得这是 GraphQL 的一个缺陷, 但已经够好了, 不是吗?

    一些实践经验

    设计好schema

    很想把这句放到文章最前面, 怎么强调都不过分. 要按照天然的数据结构和层次来设计 Schema, 天然的结构是指按 DB 里面的表结构, 以及其他接口返回的数据结构等等. 我觉得这是自然而然的事情, 但还是要说一下, 以免有人会有老一套思维定势, 觉得这样不合理.

    如果你或者前端同学觉得这样不合理, 比如说觉得这样导致一些字段的获取层次太深了, 或者是你认为把一些字段抽取出来放在一个 Struct 里才更”合理”. 我给的建议是, 不要用 GraphQL 了.

    平级和跨级依赖

    这是GraphQL 的一个痛点.

    比如说, 我们有一个 Atuhor Struct, Author 里面有 photoId 这个字段, 我们需要通过这个字段去取头像(Photo)的具体数据. 也就是说, Photo 依赖 PhotoId.

    photoId 是数据库里面一起返回的, 这样没问题. 因为运行到 Photo 绑定的 DataFetcher 的时候, photoId 必然已经存在了, 只要取 source.photoId 就可以.

    但是但是, 如果 photoId 不在 DB 里面一个字段, 而是绑定了一个非默认的 DataFetcher. 这样不能用source.photoId. 可以使用上下文(Context)来传递这个数据, 但问题是运行到 Photo 绑定的 DataFetcher 的时候, 因为两个 DataFetcher 是并行的, 这时photoId 可能还没有取到, Context 传过来的数据可能是一个空值.

    我只能用 Future 这种东西来处理这个问题, 但是代码又绕又丑. 具体做法是在 Context 里面放一个 Future, 然后在 photoId 的 DataFetcher 里面 Complete, 在 Photo 的 DataFetcher 里面 Apply

    我是希望 GraphQL 可以在框架上解决这个问题, 比如说提供类似 source.getPhotoId() 的方法, 他会自动等待 photoId 的 DataFetcher 完成.

    上面是说平级的依赖.

    跨级的依赖是说上层的上层(或者更上层)里面的属性, 不能方便的拿到, 只能走 Contxt 来传递, 不方便, 在 IDE 里面也很难做到跳转.

    尽量使用 DataLoader

    如果有批量处理的需求, 使用 DataLoader

    手动 Dispatch

    TODO

    dataLoader 注册名字规范

    需要定义一个规范, 大家都按同样的规范来取名字. 但依然不够好, 不方便跳转, 我取一个 DataLoader 的时候, 没办法方便的跳转到他的定义. 也是 GraphQL 的一个痛点.

    async

    默认执行策略是”异步执行策略”, 但在 DataFetcher 里面要用 Async 包一下才会真正的异步执行.

    enum

    Schema 里面尽量使用 Enum, 更加语义化, 使用方一眼就能看明白怎么用, 也可以避免一些笔误带来的错误.

    GraphQL-Java 框架的不足

    依赖

    如上面提到的

    无奈使用 Context 传递数据

    如上面提到的

    说在最后

    像 JAVA 框架现在还在开发迭代中, 希望他能在框架层面解决一些问题.

    另外如果有人能提供 IDE 的插件也可以解决一些问题, 比如说 IDE 里面的跳转. 毕竟更好的语义分析能力的也是大家使用 IDE 的一个重要原因.

    Read More...

    Contains Not Working In Elasticsearch

    elastic search painless script 里面的 contains “不生效”, 还好有 Google

    参考资料 https://discuss.elastic.co/t/painless-collection-contains-not-working/178944/2

    上面的链接 实在是太慢了, 摘抄一下

    Because Elasticsearch treats those numbers as Longs by default, you need to make sure that you pass a Long to the contains method. The following should work:

    GET testdatatype_unit_tests/_search
    {
      "size": 100,
      "query": {
        "script": {
          "script": {
            "source": "doc['IntCollection'].values.contains(1L)",
            "lang": "painless"
          }
        }
      }
    }
    
    Read More...