异步 DNS
Async DNS

原始链接: https://flak.tedunangst.com/post/async-dns

## 异步 DNS 替代方案:总结 作者探索了替代 `pthread_cancel` 以超时异步 DNS 请求的方法,优先考虑事件驱动的解决方案,无需后台线程或信号。 调查了几个选项: **`getaddrinfo`:** 一种经典方法,可能使用多个线程或进程,但通常可靠。 **`getaddrinfo_a` (glibc):** 简化了 `getaddrinfo` 的线程处理,但不可移植,并且不适合事件循环模型。 **`c-ares`:** 一个独立的 DNS 库,提供线程模式和事件驱动模式。 事件系统虽然可用,但严重依赖回调函数,这可能导致代码复杂和潜在的阻塞。 实现了一个快速演示,但回调繁多的特性被认为是不理想的。 **`asr` (来自 OpenBSD):** 这被证明是最有希望的。 它是一个非阻塞 API,调用者使用 `poll` 管理事件。 作者成功地使用 `asr` 重写了他们的 `c-ares` 演示,从而产生了更简洁、更易于管理的代码。 `asr` 提供了一个明确的信号——要么是结果,要么是稍后重试的请求——让应用程序完全控制时序和处理。 作者更喜欢 `asr` 的 API,将其与标准的 `read`/`write` 操作进行有利比较,提供了一种直接且可控的异步 DNS 解析方法。

## Hacker News 讨论总结:异步 DNS 与 `pthread_cancel()` 一个 Hacker News 讨论围绕异步 DNS 解析的挑战以及有问题的 `pthread_cancel()` 函数展开。最初的帖子链接到一篇讨论异步 DNS 的文章,引发了关于替代方案和历史背景的对话。 用户指出,`pthread_cancel()` 从一开始就是一个有缺陷的想法,Java 已经弃用了其等效功能,而 Haskell 由于其实现问题一直面临持续的错误。Musl libc 提出了一种更好的解决方案,即返回 `ECANCELED` 而不是终止线程,但由于潜在的兼容性问题,存在标准化问题。 讨论扩展到正确实现 DNS 客户端的复杂性,强调需要处理操作系统特定的配置(例如 macOS 的解析器系统和 BSD/Linux 上的 `nsswitch`),以及潜在的陷阱,例如 VPN 集成和 DNSSEC。 许多用户强调,构建一个健壮的 DNS 客户端比许多人想象的要复杂得多,使用操作系统提供的 API 或维护良好的库通常比自己编写更可取。Python 中的 Gevent 和 libuv/libevent 等现有方法也被提及。
相关文章

原文

curl experimented with using pthread_cancel to timeout async DNS requests and it blew up. What else can we do?

Out of curiosity, I decided to review some alternatives and see how they work. My personal priorities are control over events; no background threads or signals or secret mechanisms.

getaddrinfo

The tried and true classic technique is to call getaddrinfo in a thread. Probably with more than one thread so you don’t get stuck behind a single slow request, but probably not boundless either. You can also use a separate process if you don’t use threads.

This is probably good enough for many uses.

getaddrinfo_a

glibc provides getaddrinfo_a which basically does the thread dance for you. Some of it. It comes with some caveats, and it’s distinctly non portable, and probably doesn’t mesh with your idea of an event loop. Passing.

c-ares

c-ares is a standalone DNS library. It supports async queries via a threaded backend or an event driven system. I think the thread backend has the same issues, in that it uses a callback and then you need to push the results back into your application.

Alas, the event system uses lots of callbacks as well. This also includes some dire warnings in the documentation. “When the associated callback is called, it is called with a channel lock so care must be taken to ensure any processing is minimal to prevent DNS channel stalls.” Everyone knows the ideal callback just sets a flag, etc., but also everyone is inevitably tempted to do just one more thing, and hey look, it works fine, wait, why did it break. And thus I have a strong preference for library interfaces where you call into it, get some results, but any time you’re in your own code, you’re free to do what you want.

But worth a try. Based on the sample code I wrote the quickest dirtiest demo I could.

c-ares code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <arpa/inet.h>

#include <ares.h>

struct server {
    char name[32];
    char ip[16];
    int status;
};

struct everything {
    struct server servers[1];
    int nservers;
    struct pollfd pfds[4];
    int npfds;
};

static void
addrinfo_cb(void *arg, int status, int timeouts, struct ares_addrinfo *result)
{
    struct server *server = arg;
    server->status = 3;
    if (!result)
        return;
    for (struct ares_addrinfo_node *node = result->nodes; node != NULL; node = node->ai_next) {
        if (node->ai_family == AF_INET) {
            struct sockaddr_in *in_addr = (void *)node->ai_addr;
            inet_ntop(node->ai_family, &in_addr->sin_addr, server->ip, sizeof(server->ip));        }
    }
}

static void
socket_cb(void *arg, ares_socket_t fd, int readable, int writable)
{
    struct everything *state = arg;
    printf("socket: %d r/w: %d %d\n", fd, readable, writable);

    int idx = -1;
    for (int i = 0; i < 4; i++) {
        if (state->pfds[i].fd == fd) {
            idx = i;
            break;
        }
    }
    if (idx == -1) {
        for (int i = 0; i < 4; i++) {
            if (state->pfds[i].fd == -1) {
                idx = i;
                state->pfds[idx].fd = fd;
                state->npfds++;
                break;
            }
        }
    }
    if (idx == -1)
        abort();

    if (!readable && !writable) {
        state->pfds[idx].fd = -1;
        state->npfds--;
        return;
    }
    state->pfds[idx].fd = fd;
    state->pfds[idx].events = 0;
    if (readable)
        state->pfds[idx].events |= POLLIN;
    if (writable)
        state->pfds[idx].events |= POLLOUT;
}

int
main(int argc, char **argv)
{
    struct everything state;
    memset(&state, 0, sizeof(state));
    strlcpy(state.servers[0].name, argv[1], sizeof(state.servers[0].name));
    state.servers[0].status = 1;
    state.nservers = 1;
    for (int i = 0; i < 4; i++)
        state.pfds[i].fd = -1;

    ares_library_init(ARES_LIB_INIT_ALL);

    struct ares_options options;
    memset(&options, 0, sizeof(options));
    int optmask = 0;
    options.flags = ARES_FLAG_EDNS | ARES_FLAG_DNS0x20;
    optmask |= ARES_OPT_FLAGS;
    options.sock_state_cb = socket_cb;
    options.sock_state_cb_data = &state;
    optmask |= ARES_OPT_SOCK_STATE_CB;

    ares_channel_t *channel;
    ares_init_options(&channel, &options, optmask);

    ares_fd_events_t ares_fds[1];

    while (1) {
        printf("top of loop\n");
        for (int i = 0; i < state.nservers; i++) {
            printf("processing server %d\n", i);
            struct server *server = &state.servers[i];
            switch (server->status) {
            case 1:
                {
                    struct ares_addrinfo_hints hints;
                    memset(&hints, 0, sizeof(hints));
                    hints.ai_family = AF_UNSPEC;
                    hints.ai_flags  = ARES_AI_CANONNAME;
                    ares_getaddrinfo(channel, argv[1], NULL, &hints, addrinfo_cb, server);
                    server->status = 2;
                }
                break;
            case 2:
                printf("woke up while working\n");
                break;
            case 3:
                printf("got it, done: %s -> %s\n", server->name, server->ip);
                return 0;
            }
        }
        if (state.npfds == 0) {
            printf("confused. nothing to poll\n");
            return 1;
        }
        int res = poll(state.pfds, 4 /* state.npfds */, 2000);
        printf("poll results: %d\n", res);
        if (res > 0) {
            ares_fd_events_t events[4];
            int nevents = 0;
            for (int i = 0; i < 4 /* state.npfds */; i++) {
                if (!state.pfds[i].revents)
                    continue;
                events[nevents].fd = state.pfds[i].fd;
                events[nevents].events = 0;
                if (state.pfds[i].revents & (POLLERR|POLLHUP|POLLIN))
                    events[nevents].events |= ARES_FD_EVENT_READ;
                if (state.pfds[i].revents & (POLLOUT))
                    events[nevents].events |= ARES_FD_EVENT_WRITE;
                nevents++;
            }
            ares_process_fds(channel, events, nevents, 0);
        }
    }
}

It’s okay, but the callbacks are annoying. Notifying me which descriptors need watching means I’m required to pack up my poll structure so I can access it in the callbacks, etc. Everything gets bound just a little bit tighter.

wadns

Among the alternatives the c-ares project helpfully lists, is dns.c. This sounds enticing.

On the downside, it’s not clear where the demo code stops and the functional code begins. As in, there’s a getaddrinfo sample, but it incorporates a lot of other code that doesn’t seem to be public. The public header doesn’t actually expose a means to interface with an event loop. The code is meant to be integrated into a project, which is understandable and even advantageous, but it means no demo today.

asr

The asr code was written for smtpd in OpenBSD. It doesn’t use threads and requires the caller to push events. Unfortunately, a portable version currently only exists in the OpenSMTPD repo. On the plus side, it’s used as the basis for the libc resolver in OpenBSD, which means the “sample” code to replace getaddrinfo literally is getaddrinfo.c.

I rewrote the c-ares demo to use asr. It comes out quite a bit shorter, and I think clearer as well.

asr code
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <netdb.h>
#include <asr.h>
#include <arpa/inet.h>

struct server {
    char name[32];
    char ip[16];
    int status;
    struct asr_query *aq;
    int ar_fd;
};

int
main(int argc, char **argv)
{
    struct server servers[1] = {};
    strlcpy(servers[0].name, argv[1], sizeof(servers[0].name));
    servers[0].status = 1;
    int nservers = 1;

    while (1) {
        struct pollfd pfds[4];
        int npfds = 0;
        printf("top of loop\n");
        for (int i = 0; i < nservers; i++) {
            printf("processing server %d\n", i);
            struct server *server = &servers[i];
            switch (server->status) {
            case 1:
                {
                    struct addrinfo hints;
                    memset(&hints, 0, sizeof(hints));
                    hints.ai_family = AF_UNSPEC;
                    hints.ai_socktype = SOCK_STREAM;
                    server->aq = getaddrinfo_async(server->name, "80", &hints, NULL);
                    server->status = 2;
                }
                // fallthrough
          case 2:
                {
                    printf("ready to run\n");
                    struct asr_result ar;
                    int rv = asr_run(server->aq, &ar);
                    switch (rv) {
                    case 0:
                        pfds[npfds].fd = ar.ar_fd;
                        pfds[npfds].events = 0;
                        if (ar.ar_cond == ASR_WANT_READ)
                            pfds[npfds].events = POLLIN;
                        else
                            pfds[npfds].events = POLLOUT;
                        npfds++;
                        server->ar_fd = ar.ar_fd;
                        server->status = 3;
                        break;
                    case 1:
                        {
                            struct addrinfo *res;
                            for (res = ar.ar_addrinfo; res; res = res->ai_next) {
                                if (res->ai_family == AF_INET) {
                                    struct sockaddr_in *in_addr = (void *)res->ai_addr;
                                    inet_ntop(res->ai_family, &in_addr->sin_addr, server->ip, sizeof(server->ip));
                                }
                            }
                            server->status = 4;
                        }
                        break;
                    }
                }
                break;
            case 3:
                printf("woke up while working\n");
                break;
            case 4:
                printf("got it, done: %s -> %s\n", server->name, server->ip);
                return 0;
            }
        }
        if (npfds == 0)
            continue;
        int res = poll(pfds, npfds, 2000);
        printf("poll results: %d\n", res);
        if (res > 0) {
            for (int i = 0; i < npfds; i++) {
                if (!pfds[i].revents)
                    continue;
                for (int j = 0; j < nservers; j++) {
                    if (pfds[i].fd == servers[j].ar_fd)
                        servers[j].status = 2;
                }
            }
        }
    }
}

I like this API. It’s very much like read or write in that it either gives you an answer, or tells you to come back later, and then it’s up to you to decide when that is.

联系我们 contact @ memedata.com