统计复用系统为什么避免不了排队

排队论建模描述排队机理,但某些结论与直觉相悖,并不形象直观,理论如果不能直观解释,只剩下数学推导,并不十分有趣。本文尝试用有趣的方式解释排队。本文虽以数据包为例,但任何可以排队的主体都可作为主语。

所有统计复用系统,均需要buffer用来支持队列,这些系统中,队列是必然存在的。队列的成因有两个:

  • 统计波动造成到达与处理时间差。
    到达时间不是均匀的,会有突发,带来波动。
    服务时间不是均匀的,有长有短,放大波动。
  • 处理机无法突破时间墙。

一般用sequence/time坐标系来描述队列的产生和消除:
在这里插入图片描述

但这类图中无法看到动力学机理,它要么宏观地描述大趋势,要么显示特定时间点的微观细节,对于深入理解队列背后看不见的东西,并不十分有用。

我并非在贬低sequence/time图,相反,我非常喜欢。实际工作中,wireshark的tcptrace图给过我很大的帮助,对于我而言它已经必不可少。只不过在对于理解队列的本质这个单独的层面,我有比sequence/time图更好的东西。

类似于描述无标度系统选用双对数坐标系,可以对“到达/服务”过程做坐标变换,这里采用双时间坐标系:

  • 横轴表示到达时间序列,离散的不均匀坐标点表示到达时间。
  • 纵轴表示服务时间序列,离散的不均匀坐标点表示服务时间。
  • 坐标系中的点坐标表示数据包到达的时间点和处理的时间点。
    在这里插入图片描述
    在双时间坐标系中, L : T 2 = T 1 L:T_2=T_1 L:T2=T1将平面分为3个区域, L L L本身作为一堵时间墙,意味着 P ( t a r r i v , t s e r v ) P(t_{arriv},t_{serv}) P(tarriv,tserv)组成的处理线只能位于 T 2 ≥ T 1 T_2\geq T_1 T2T1区域。处理线是所有离散点连接成的包络线:
    在这里插入图片描述
    这意味着即使平均服务速率比平均到达速率高,由于时间墙,无法提前处理潜在产生队列的数据包,只能在统计波动实际发生,队列实际堆积时,才可处理。这就是队列成因的解释。

现在看一个队列产生和消除的实际例子:
在这里插入图片描述
这是一张事后分析图,以7号数据包到达时间点为参考, P 7 P_7 P7表示其到达和被处理时间,直到时间点 m m m,双时间坐标一致,处理线收敛到 T 2 = T 1 T_2=T_1 T2=T1直线,队列消除。

在上述背景下,可以很容易得出结论:
在这里插入图片描述
该结论不假设到达过程以及服务过程的任何分布,这是个普遍的结论,在我看来这是位于M/M/1模型之上的。

当看到排队论M/M/1模型结论时,可能有悖于直觉,为什么 λ \lambda λ不能等于 μ \mu μ呢?
W q = λ μ ( μ − λ ) W_q=\dfrac{\lambda}{\mu(\mu-\lambda)} Wq=μ(μλ)λ

分母不能为0只是数学上的限制,直觉上二者相等是OK的,到达率等于服务率,队列收敛到0。这个看似的悖论在双时间坐标系中却非常显然:

  • λ = μ \lambda=\mu λ=μ时靠统计波动清除队列,属于随机游走常返性,不可预期。
  • 排队论M/M/1模型结论描述的是稳定行为,而 λ = μ \lambda=\mu λ=μ不是稳定行为。

预备储备就绪,最后我写了个代码仿真排队情况,和排队论教程不同的是,除了指数分布,我还用正态分布做了实验。

仿真逻辑很简单:

  • 子线程1循环对全局变量cnt递增1,模拟排队,中间睡眠随机间隔,可按正态分布,指数分布两种方式随机。
  • 子线程2循环对全局变量cnt递减1,模拟出队,中间睡眠随机间隔,可按正态分布,指数分布两种方式随机。
  • 主线程每隔1秒打印全局变量cnt的值,该值即当前的队列长度。

代码写的不好,但管用:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <math.h>
#include <float.h>

pthread_mutex_t _mutex, _mutex2;
pthread_cond_t cond1,cond2;

static int cnt = 0, sum1 = 0, sum2 = 0;
static int cnt_add = 0, cnt_dec = 0;

double mean = 0.0, var = 0.0;
double lambda = 0.0;
int type = 0;
int delta = 0;

// Box–Muller
// https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform#Implementation
double normal(double mean, double var)
{
        double epsilon = DBL_EPSILON;
        double two_pi = 2*3.141592653589;
        double u1, u2;
        static double z0, z1;
        static unsigned int phase = 0;
        phase ++;
        if (phase % 2 == 0)
                return z1 * var + mean;
        do {
                u1 = ((double)rand())*(1.0/RAND_MAX);
                u2 = ((double)rand())*(1.0/RAND_MAX);
        } while (u1 <= epsilon);
        z0 = sqrt(-2.0*log(u1)) * cos(two_pi*u2);
        z1 = sqrt(-2.0*log(u1)) * sin(two_pi*u2);

        return z0 * var + mean;
}

// https://zh.wikipedia.org/wiki/%E6%8C%87%E6%95%B0%E5%88%86%E5%B8%83
double exp(double lambda)
{
        double x = 0;
        do {
                x = (double)(rand() % 1000)/1000;
        } while(x == 0);
        x = (-1/lambda)*log(1.0 - x);

        return x;
}

void *enqueue(void)
{
        int secs = 0;
        while (1) {
                double mic;
                pthread_mutex_lock(&_mutex);
                cnt ++;
                cnt_add ++;
                pthread_cond_signal(&cond2);
                pthread_mutex_unlock(&_mutex);
                if (type == 0) {
                        mic = normal(mean, var);
                        if (mic < mean - var) mic = mean - var;
                        if (mic > mean + var) mic = mean + var;
                } else {
                        mic = exp(1.0/lambda);
                }
                secs = (int)mic;
                usleep(secs);
                sum1 += mic;
        }
}

void *serve(void)
{
        int secs = 0;
        while (1) {
                double mic;
                pthread_mutex_lock(&_mutex);
                if (cnt > 0) {
                        cnt --;
                        cnt_dec ++;
                } else {
                        pthread_mutex_unlock(&_mutex);
                        pthread_cond_wait(&cond2,&_mutex2);
                        continue;
                }
                pthread_mutex_unlock(&_mutex);
                if (type == 0) {
                        mic = normal(mean, var);
                        if (mic < mean - var) mic = mean - var;
                        if (mic > mean + var) mic = mean + var;
                } else {
                        mic = exp((double)1/lambda);
                }
                if (delta != 0) {
                        mic = mic - mic/(double)delta;
                        if (mic < 0) mic = 0;
                }
                secs = (int)mic;

                usleep(secs);
                sum2 += mic;
        }
}

int main(int argc, char **argv)
{
        int ret = 0;
        pthread_t id1, id2;

        type = atoi(argv[1]);
        delta = atoi(argv[2]);
        printf("aaaaa:%d\n", delta);
        if (type == 0) {
                mean = atoi(argv[3]);
                var = atoi(argv[4]);
        } else {
                lambda = atoi(argv[3]);
        }

        pthread_mutex_init(&_mutex,NULL);
        pthread_mutex_init(&_mutex2,NULL);
        pthread_create(&id1, NULL, (void*)enqueue, NULL);

        pthread_create(&id2, NULL, (void*)serve, NULL);

        while (1) {
            printf("%d  add:%d, dec:%d sum1:%d sum2:%d\n", cnt, cnt_add, cnt_dec, sum1, sum2);
            sleep(1);
        }

        return 0;
}

结果非常有意思:

  • 如果不设置任何delta,无论什么分布,cnt均会增加到很大,看上去越来越大,然而却还是可以在某个时间突然变成0或者缓慢变成0,毫无征兆,不可预知,这就是随机游走的常返效应。
  • 但凡加一点点delta(数值上大),cnt就会维持在一个不大的值附近,归0的次数会大很多。
  • 如果加很大的delta(数值上小),cnt不会比delta小时小得多,偶尔也会很大。

这对设计交换机等设备的队列buffer提供了一些思路,但更是回答了下面的疑问:

  • 银行,医院,超市排队时,有好多服务窗口为什么只开不多的几个,为什么眼睁睁看着人们在排长队而无动于衷?

上述结论表明,把服务窗口全部打开对于整体效能是非常不经济的,但对于单独个人,却可以有效减少排队时间,这又是一个trade-off。

新开服务窗口的前提是,减少每个窗口队列的可容纳人数,这可让人们有更多机会选择窗口,平滑单个人服务时间过久引入的队头拥塞(因为人们发现队满了之后不得不选择别的窗口,或者退避,待会儿再来)。这么明显一个道理,在交换机设计者那里,却走向了相反方向。

交换机部署大的buffer是一个不正确的事情,分层网络协议栈引入了数据包出了网卡就是发送成功的假象,但也许只是进了交换机队列,真相是端到端延时增加了。这似乎非常容易理解,如果是人排队,人绝对不会将排队假装成被服务,相反,每个人都讨厌排队!同一个道理换个场景,却被忽略。

从本文的结论,我们可以回答为什么Pacing比Burstiness好。

由于主要讨论数据包转发,而数据包转发处理时延相对均匀,因此将服务台的处理波动消除,假设服务时间是均匀的,在双时间坐标和sequence/time坐标中,列举三种极端:

  • Pacing:均匀到达。
  • 真实情况:统计到达。
  • Burstiness:突发到达。

我们殊途而同归,两个图一模一样:
在这里插入图片描述
这也正是M/M/1模型里的结论:

T = 1 μ − λ T=\dfrac{1}{\mu-\lambda} T=μλ1

在这里插入图片描述
如果做不到完全Pacing,泊松分布统计到达的情况也不错, λ \lambda λ大到一定程度,wait time也不会很大,这意味着队列不会太长,也就不需要安排太多的buffer,然而如果Burstiness,即便 λ \lambda λ很小,时延也会很大,这意味着需要更多的buffer。突发导致了bufferbloat。

绳结不解,bufferbloat还是没法解决。

在写一个IDC拥塞控制算法时,无论如何也平滑不了长队列,即便是仿真了绕路算法,依然会快不过突发,Wi-Fi冲突本质上也是一种排队,坐地铁排队,结账排队,菜场一个口碑好的熟肉店买羊脸排队,…到处都在排队…我们的世界是一个分布式统计复用的世界,靠什么拥塞控制都没用,主要还是选路,但端到端原则又不允许选路,那就只有overlay了,这部分太复杂,就先聊聊排队吧,随便写写,就有了本文。

浙江温州皮鞋湿,下雨进水不会胖。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值