ARTS 第十周: 找中位数,quic,nginx DNS的坑,

一、Algorithm

find-median-from-data-stream(hard)

Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.
For example,
[2,3,4], the median is 3
[2,3], the median is (2 + 3) / 2 = 2.5
Design a data structure that supports the following two operations:
void addNum(int num) - Add a integer number from the data stream to the data structure.
double findMedian() - Return the median of all elements so far.

解法一:维护一个列表,插入的时候直接放到列表最后面,然后再取中值前sort一把,排序的复杂度是O(nlogn)。这种解法时间复杂度太高了,过不了。

解法二:在add的时候排序,使用二分查找法,找到待插入的位置,然后插入。二分查找的时间复杂度为O(logn),插入由于需要后移元素,所以复杂度为O(N)
代码如下:

class MedianFinder:

    def __init__(self):
        """
        initialize your data structure here.
        """
        self.nums=[]
        

    def addNum(self, num: int) -> None:
        low=0
        high=len(self.nums)-1
        while (low <= high ):
            mid = (low+high)//2
            if (self.nums[mid]>num):
                high=mid-1
            else:
                low=mid+1
        self.nums.insert(low, num)
                

    def findMedian(self) -> float:
        l=len(self.nums)
        if l%2 != 0:
            return float(self.nums[l//2])
        else:
            return (self.nums[l//2]+self.nums[l//2-1])/2

解法三:使用大小堆
python有一个内置的数据结构堆heapq,特性是a[k]<=a[2k],a[k]<=a[2k+1],我们利用的就是堆的有序性,插入任意的数据,都能保持有序。我们创建两个堆,其中一个大顶堆,一个小顶堆。大顶堆最大的值在上面,小顶堆最小的值在上面。我们只需要控制大顶堆最大的值小于或等于小顶堆最小的值,同时大顶堆的大小==小顶堆的大小,或者只多一个。

大顶堆的实现:通过将整数取负值,排序出来,最小的值最后取负值,也就是大顶堆的最大值了。

class MedianFinder:

    def __init__(self):
        """
        initialize your data structure here.
        """
        self.maxHeap = []
        self.minHeap = []

    def addNum(self, num: int) -> None:
        heapq.heappush(self.maxHeap, -num)
        tmpMax = self.maxHeap[0] if self.maxHeap else 0
        tmpMin = self.minHeap[0] if self.minHeap else 0
        if (-tmpMax > tmpMin) or (len(self.maxHeap) > len(self.minHeap)+1):
            heapq.heappush(self.minHeap, -heapq.heappop(self.maxHeap))
        if len(self.maxHeap) < len(self.minHeap):
            heapq.heappush(self.maxHeap, -heapq.heappop(self.minHeap))

    def findMedian(self) -> float:
        if len(self.maxHeap) == len(self.minHeap):
            return (-self.maxHeap[0]+self.minHeap[0])/2.0
        else:
            return -self.maxHeap[0]*1.0

二、Review

Quic在Uber中的应用

点评

http2在网络质量变化比较大,丢包严重的情况下,质量也不好 。TCP针对这种场景,受限于协议规则,并没有太好的解决方法,只能寻求他路:Quic。Quic已经是http/3的标准了。
在Uber的实践中,使用了Quic能降低客户端10-30%的https延时。

改造的话,需要整条链路,从客户端到服务端,在Uber的应用中,并没有提到CDN,说明他们动态的域名并没有上CDN,也有可能这仅仅是测试环境。考虑都Uber需要传输的都是数据类,基本上没有媒体数据(图片,视频)等。所以不上CDN也是可行的。

腾讯在内部也已经开始灰度上线Quic了,腾讯走的路可能会曲折一点,尤其是在服务端的Quic实现上。Quic协议在开源的项目中有很多的实现,比如google独立出来的gQuic项目,从chrome项目中独立出来了,但是已经很久没有更新了,目前使用的chrome/quic中的源码,但这里面坑也比较多,具体的可以看https://myslide.cn/slides/9804

腾讯不单在腾讯云上开放了Quic,在内部也和各业务有合作,在CDN上实现了Quic,用于静态加速。

三、Tips

Nginx DNS的用法与坑(https://www.nginx.com/blog/dns-service-discovery-nginx-plus/)

Nginx作为反向代理,我们有可能需要将某些接口转发到特定的域名,比如这个样子;

location /api {
     proxy_pass http://xxx.example.com/api
}

在正常情况下是没有问题的,但是如果xxx.example.com的解析发生了变化,nginx是不会自动去刷新的,这就可能导致故障。比如需要切换服务器,原来的服务器切换DNS后可能会停服,但我们的nginx这边还是解析到老的服务器。这和Nginx的机制有关,针对有域名解析的,默认情况下,nginx会在重启或热加载的时候检查解析,如果这个时候解析失败,或导致nginx启动失败或者热加载失败。而一旦启动后,域名的解析结果将一直缓存在内存中,直到下一次重启或者热加载。

如果要使用dns解析的话,需要显示的配上resolver,然后将域名写入到变量中。

resolver 8.8.8.8 valid=60s;
location /api {
     set $domain "xxx.example.com";
     proxy_pass http://$domain;
}

这样就可以解决上面的问题。

但如果我们proxy_pass希望转发到多个域名,比如实现自动容灾与切换,使用upstream后,如果第一个sever返回异常,则会自动到下一个server重试,这样可以达到自动容灾的目的。那用法应该是这样的:

upstream backends {
    least_conn;

    server backends.example1.com:8080 max_fails=3;
    server backends.example2.com:8080 backup;
}

server {
    location / {
        proxy_pass http://backends;
    }
}

但这种情况下,同样存在dns无法刷新的问题,那解决方案呢?

resolver 8.8.8.8 valid=10s;

upstream backends {
    least_conn;

    server backends.example1.com:8080 max_fails=3 resolve;
    server backends.example2.com:8080 backup resolve;
}

server {
    location / {
        proxy_pass http://backends;
    }
}

但是可惜的upstreamre的resolve这个功能,开源版本的nginx是没有的,只能购买nginx plus版本,价格不便宜。我暂时没有找到其他方案,如果各位其他的解决方法,欢迎联系我,谢谢!

四、Share

分享一次故障定位的过程。
我们有一个拨测系统,这个系统会针对提供http服务的域名进行拨测,用于监控域名是否有异常。现象是我们部署在新加坡的一个探测点,拨测全球其他域名,成功率都很低,连拨测新加坡本地域名成功率都只有80-90%左右,这很不正常。这导致频繁告警,联系到拨测负责人说是新加坡本地网络查,从常识来看,这很不正常。新加坡的网络建设不会比中国差,偶尔的网络质量波动是可能的,但是持续性这样差,绝对不可能。

继续分析失败的请求状态码返回码是28,正常的请求状态码是0。我们看下ret code 28代表啥含义。

CURLE_OPERATION_TIMEDOUT (28)
Operation timeout. The specified time-out period was reached according to the conditions.

一开始以为是建链失败,然后用mtr排查,发现并没有问题。然后用curl来模拟拨测的过程,但没有发现返回码为28的情况。

#!/usr/bin

url='http://xxx.com.cn'
for i in {1..100}
do
    curl  $url -o /dev/null
    echo $? >> /tmp/ans
    sleep 0.5
done
exit 0

想到curl的话,有可能会配置超时时间,了解到拨测那边确实配置了超时时间:5秒。然后再curl命令中加上超时的参数:--connect-timeout 5
然后就果真能复现了,复现后然后抓包:
tcpdump -i any -v host dstIP -w /tmp/capture.pcap &

查看抓包文件发现竟然没有报错,但是发现两次请求之间的间隔有异常,命名是sleep0.5秒,但是从抓包文件来看,有些请求之间竟然间隔了5S. 而且从抓包看,源和目的之间并没有丢包问题。


ARTS 第十周: 找中位数,quic,nginx DNS的坑,_第1张图片
抓包.png

这,感觉很诡异,不正常。然后改下测试脚本,在出错的中断。

#!/usr/bin

url='http://xxx.com.cn'
for i in {1..100}
do
    curl --connect-timeout 3 $url -o /dev/null
    if [ $? == 28 ]
    then
        exit 1
    fi
    echo $i
    sleep 0.5
done
exit 0

发现具体的报错信息:

curl: (28) Resolving timed out after 3001 milliseconds

原来是dns解析的问题!难怪,抓包的没有丢包,同时间隔有5秒,这些现象都能解释得通了。然后抓dns的包,发现和nameserver会有建链失败的场景。(理论上都是同一个网段的服务器,理论上不应该有问题的,这里让系统组继续排查了,未完待续)(补充:原因是dns服务器同时配置了阿里云的slb和2台ecs,slb的后端是2台ecs,同时使用的话,在并发量请求比较大的情况下,如果使用4层协议,可能导致串包)

这里有个细节,为什么curl不带connect-timeout参数就没有报错呢,因为没有报错的话,curl自己会一直重试dns解析,直到成功或者最终超时。

你可能感兴趣的:(ARTS 第十周: 找中位数,quic,nginx DNS的坑,)