xml地图|网站地图|网站标签 [设为首页] [加入收藏]
编写一个轻量级的异步写日志的实用工具类,让
分类:编程

(erbqi)导语 QQ图全称 Quantile-Quantile图,也就是分位数-分位数图,简单理解就是把两个分布相同分位数的值,构成点(x,y)绘图;如果两个分布很接近,那个点(x,y)会分布在y=x直线附近;反之则不;可以通过QQ图从整体评估回归模型的预测效果

功能说明

LongIntervalRetries是基于Quartz.Net的一个长时间间隔重试的类库,其主要解决何时执行以及执行结果反馈的问题。

一说到写日志,大家可能推荐一堆的开源日志框架,如:Log4Net、NLog,这些日志框架确实也不错,比较强大也比较灵活,但也正因为又强大又灵活,导致我们使用他们时需要引用一些DLL,同时还要学习各种用法及配置文件,这对于有些小工具、小程序、小网站来说,有点“杀鸡焉俺用牛刀”的感觉,而且如果对这些日志框架不了解,可能输出来的日志性能或效果未毕是与自己所想的,鉴于这几个原因,我自己重复造轮子,编写了一个轻量级的异步写日志的实用工具类(LogAsyncWriter),这个类还是比较简单的,实现思路也很简单,就是把消息日志先入内存队列,然后由异步监听线程从队列中取出日志并批量输出到本地文件中,同时参照各大日志框架,对单个文件过大采取分割生成多个日志文件。

 

产生的原因

简单的说,我们提供了一系列的API供第三方调用,但因为实际API对应的业务处理时间较长,所以为了增加吞吐量,实际的业务逻辑并没包含在API服务器,而是分布在了不同的服务器上进行处理,在业务处理结束后,再通过调用第三方提供的回调Url通知处理结果,为了保证回调正确,那么我们需要有这么一个策略:如果回调失败,我们需要在指定时间间隔之后再次回调,如此反复直至回调成功或者达到重复次数上限。上述回调方案是不是很熟悉?嗯,好吧,直白的讲,回调这一部分我们借鉴(抄袭)了支付宝支付时的回调方案,所以我们要解决的,就是通过代码来实现回调这部分的业务场景,于是也就有了LongIntervalRetries

经测试发现性能非常不错,先看示例使用代码:(采取并发多线程同时写入1000000万条日志)

QQ图一般有两种,正态QQ图和普通QQ图,区别在于正态QQ图中其中有一个分布是正态分布,下面来看下这两种分布

为什么不使用Polly

Polly是.NET基金会下的弹性和瞬态故障处理库,其解决的问题天然符合我们回调的业务场景,但为什么在此处却不被采用呢,原因如下:

  • Polly的重试机制是个短时间内的机制,在其重试机制时间内,当前Task一般是通过await阻塞的,而对于我们的场景来说,这明显是不合适的,我们的场景并不应该发生在当前线程内
  • Polly并不支持程序重启时的重试恢复,这一点在我们的业务场景中及其重要,总不能服务器重启后,我们还没回调成功的业务就全部丢了吧

 

正态QQ图

 下图来自这里                                                  使用Filliben's estimate来确定n分点

图片 1图片 2

下面我们尝试绘制正态QQ图

使用开源库自带函数,很简单,但是可能一些细节看不到

import numpy as np 
from matplotlib import pyplot as plt
import matplotlib
matplotlib.style.use('ggplot')
# 用正态分布随机生死100个数据
x = np.round(np.random.normal(loc=0.0, scale=1.0, size=100),2)
from scipy.stats import probplot
f = plt.figure(figsize=(8, 6))
ax = f.add_subplot(111)
probplot(x, plot=ax)
plt.show()

 

 下面展开一些细节,为下面我们的普通QQ做点铺垫

import sys,os
import pandas as pd 
import numpy as np 
from scipy.stats import norm,linregress
from matplotlib import pyplot as plt
# 返回长度为len(x)的order_statistic_medians
def calc_uniform_order_statistic_medians(x):
    N = len(x)
    osm_uniform = np.zeros(N, dtype=np.float64)
    osm_uniform[-1] = 0.5**(1.0 / N)
    osm_uniform[0] = 1 - osm_uniform[-1]
    i = np.arange(2, N)
    osm_uniform[1:-1] = (i - 0.3175) / (N + 0.365)
    return osm_uniform
# 用正态分布随机生死100个数据
x = np.round(np.random.normal(loc=0.0, scale=1.0, size=100),2)
osm_uniform = calc_uniform_order_statistic_medians(x)
# ppf(Percent point function) 是 cdf(Cumulative distribution function) 的逆函数,就是取对应分位数对应的值
osm = norm.ppf(osm_uniform)
osr = np.sort(x)
# 计算osm和osr组合的样本的线性回归的 斜率 截距 等信息
slope, intercept, rvalue, pvalue, stderr = linregress(osm, osr)

plt.figure(figsize=(10,8))
plt.plot(osm, osr, 'bo', osm, slope*osm + intercept, 'r-')
plt.legend()
plt.show()

 

左图是100个采样点,右图是1000个采样点,对比可以发现 ,1000个采样点的分布更接近直线y=x,也就是更拟合正态分布

图片 3

普通QQ图和正态不同的地方在于参考系不是正态分布而可能是任意分布的数据集,这正是我们要用的

下图来自这里       

图片 4

下图是一个场景,虚线是真实的网络变化,实线是简单的平滑预测的结果,我希望通过普通QQ图看下简单的平滑预测的拟合效果

图片 5

先看下两个曲线的cdf图( Fx(x)=P(X≤x) ),

这个图的累计分布点是np.linspace(min(X), max(X), len(X))计算来的,看起来有点怪

图片 6

我们重新计算以原始数据为累计分布点的cdf图,发现有趣的地方了吗?

图片 7

在两个曲线的数量一致的情况下,我们把两组数据从小到大排序之后,相同位置对应的cdf的值是一样的,

所以两个曲线的数量一致的情况下,QQ图只需要从小到大排序即可

图片 8

可以看到,正式的网络曲线和平滑预测曲线的QQ图的斜率只有0.79,说明平滑预测的分布和源数据的分布差别还是挺大的。

最后是代码

httpspeedavg = np.array([1821000, 2264000, 2209000, 2203000, 2306000, 2005000, 2428000,
       2246000, 1642000,  721000, 1125000, 1335000, 1367000, 1760000,
       1807000, 1761000, 1767000, 1723000, 1883000, 1645000, 1548000,
       1608000, 1372000, 1532000, 1485000, 1527000, 1618000, 1640000,
       1199000, 1627000, 1620000, 1770000, 1741000, 1744000, 1986000,
       1931000, 2410000, 2293000, 2199000, 1982000, 2036000, 2462000,
       2246000, 2071000, 2220000, 2062000, 1741000, 1624000, 1872000,
       1621000, 1426000, 1723000, 1735000, 1443000, 1735000, 2053000,
       1811000, 1958000, 1828000, 1763000, 2185000, 2267000, 2134000,
       2253000, 1719000, 1669000, 1973000, 1615000, 1839000, 1957000,
       1809000, 1799000, 1706000, 1549000, 1546000, 1692000, 2335000,
       2611000, 1855000, 2092000, 2029000, 1695000, 1379000, 2400000,
       2522000, 2140000, 2614000, 2399000, 2376000])

def smooth_(squences,period=5):
    res = []
    gap = period/2
    right = len(squences)
    for i in range(right):
        res.append(np.mean(squences[i-gap if i-gap > 0 else 0:i+gap if i+gap < right else right]))
    return res 

httpavg = np.round((1.0*httpspeedavg/1024/1024).tolist(),2)
smooth = np.round(smooth_((1.0*httpspeedavg/1024/1024).tolist(),5),2)

f = plt.figure(figsize=(8, 6))
ax = f.add_subplot(111)
probplot(smooth, plot=ax)
# plt.show()

f = plt.figure(figsize=(8, 6))
ax = f.add_subplot(111)
probplot(httpavg, plot=ax)
# plt.show()

import statsmodels.api as sm
plt.figure(figsize=(15,8))
ecdf = sm.distributions.ECDF(httpavg)
x = np.linspace(min(httpavg), max(httpavg), len(httpavg))
y = ecdf(x)
plt.plot(x, y, label='httpavg',color='blue',marker='.')
ecdf1 = sm.distributions.ECDF(smooth)
x1 = np.linspace(min(smooth), max(smooth), len(smooth))
y1 = ecdf1(x1)
plt.plot(x1, y1, label='smooth',color='red',marker='.')
plt.legend(loc='best')
# plt.show()
def cdf(l):
    res = []
    length = len(l)
    for i in range(length):
        res.append(1.0*(i+1)/length)
    return res
plt.figure(figsize=(15,8))
x = np.sort(httpavg)
y = cdf(x)
plt.plot(x, y, label='httpavg',color='blue',marker='.')
x1 = np.sort(smooth)
y1 = cdf(x1)
plt.plot(x1, y1, label='smooth',color='red',marker='.')
plt.legend(loc='best')
# plt.show()
from scipy.stats import norm,linregress
plt.figure(figsize=(10,8))
httpavg = np.sort(httpavg)
smooth  = np.sort(smooth)
slope, intercept, rvalue, pvalue, stderr = linregress(httpavg, smooth)
plt.plot(httpavg, smooth, 'bo', httpavg, slope*httpavg + intercept, 'r-')
xmin = np.amin(httpavg)
xmax = np.amax(httpavg)
ymin = np.amin(smooth)
ymax = np.amax(smooth)
posx = xmin + 0.50 * (xmax - xmin)
posy = ymin + 0.01 * (ymax - ymin)
plt.text(posx, posy, "$R^2=%1.4f$ y = %.2f *x + %.2f"  % (rvalue,slope,intercept))
plt.plot(httpavg,httpavg,color='green',label='y=x')
plt.legend(loc='best')
# plt.show()

 

 

设计思路及演变

一开始我们的设计思路非常简单,就是如何定时触发回调这个业务代码,但之后发现,为什么我们要仅限于回调呢?回调只是一个业务场景,但我们完全有可能有其它业务场景,恰恰我们也的确存在这样的业务场景,我们需要向第三方服务商进行一些请求,该请求同样耗时较长,该场景是不是很熟悉?只不过与我们作为服务商不同,该服务商居然没提供回调方案,它需要我们自己定时去回调!而同样是这个第三方,其业务请求参数具有相当的定制性,其需要我们预先做很多业务性的预处理,也就是需要做一些顺序性的工作后,我们才能得到完整的请求参数。
于是我们的设计思路开始调整,最终得出该封装应当具备的功能点:

  • 其内部应该封装掉如何定时触发这个功能
  • 其应该具备同时支持多种业务策略(策略模式)
  • 其应该允许设置什么时候来触发要执行的策略
  • 其应该支持服务启动时自动恢复未结束任务的能力
  • 其应该具备最终执行结果通知的能力
            Task.Factory.StartNew(() =>
            {
                DateTime startTime = DateTime.Now;
                int logCount = 1000000;
                Parallel.For(1, logCount, (i) =>
                {
                    if (i % 2 == 0)
                    {
                        LogAsyncWriter.Default.Error("测试并发写错误日志-" + i.ToString(), "TestClass.TestLog", i.ToString());
                    }
                    else
                    {
                        LogAsyncWriter.Default.Info("测试并发写普通日志-" + i.ToString(), "TestClass.TestLog", i.ToString());
                    }
                });

                this.Invoke(new MethodInvoker(() =>
                {
                    MessageBox.Show(DateTime.Now.ToString() + "," + logCount + "条日志写完了!,耗时:" + (DateTime.Now - startTime).TotalMilliseconds + "ms");
                }));
            });

            MessageBox.Show(DateTime.Now.ToString() + ",同步方法已结束");
        }

为什么会考虑基于Quartz.Net

无论是定时触发,还是业务策略,以及设置触发时间,这些都很明显的具备Job特性,而Quartz.Net本身就是一个Job类库,而且其本身允许进行并发线程数量设置,如果基于它,明显我们可以不用考虑线程相关的问题,这可以省掉我们很大的工作量

 执行效果如下图示:

类库相关

该类库在github上的地址为:
该类库目前为v1.0.0版本,其nuget地址为:

图片 9

快速使用

此处仅是简单的代码示例,后续会有详细的使用说明
首先我们需要声明Job

    public class SomeJob: IJob
    {
        public virtual Task Execute(IJobExecutionContext context)
        {
            return Task.FromResult(1);//默认LongIntervalRetries是通过Job是否产生异常来判断是否执行成功的
        }
    }

然后我们可以将这个Job注册到LongIntervalRetries,同时设置重试策略,以及注册事件监控执行结果,完整的示例如下

var retry = new StdRetry();
//声明并注册重试规则
string simpleRuleName = "SimpleRepeatRetryRule";
var simpleRepeatRule = new SimpleRepeatRetryRule(simpleRuleName, 5, TimeSpan.FromSeconds(2));
retry.RuleManager.AddRule(simpleRepeatRule);
var registerInfo = new RetryJobRegisterInfo
{
    //指定要采用的重试规则,如果不设置,则默认使用已注册的第一项
    UsedRuleName = simpleRuleName,
    //需要传递给IJob的上下文数据
    JobMap = new Dictionary<string, object>
    {
        {"SomeKey","SomeValue" }
    },
    //开始执行时间,如果不指定则表示立刻执行
    StartAt = DateTimeOffset.UtcNow.AddSeconds(3),
};
//注册要执行的Job
retry.RegisterJob<SomeJob>(registerInfo);
//注册每次Job执行后的通知事件
retry.RegisterEvent<SomeJob>(e =>
{//Some code
});
retry.Start();//启动Quartz服务
//启动服务后仍可以RegisterJob、RegisterEvent

因为采用异步,故方法先走到结尾,输出了同步的MsgBox,随后弹出的是100W日志输出到文件后的耗时MsgBox,从截图可以看出,不足1S(当然这里的1S不是真实的输出到本地方件,而是把所有的日志推到了Queue中而矣,但不影响不阻塞业务处理),而本地日志文件的大小达到了263MB(设置最大值的MaxSizeBackup,使其不滚动备份),由此看性能是不错的;

图片 10

本文由澳门新葡亰手机版发布于编程,转载请注明出处:编写一个轻量级的异步写日志的实用工具类,让

上一篇:django静态文件,彩色插图 下一篇:常见问题,Python数据结构之四
猜你喜欢
热门排行
精彩图文