`
liuxinglanyue
  • 浏览: 543930 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

海量数据分析:Sawzall并行处理(中文版论文 二)

阅读更多

在某些情况下,重新初始化是不需要的。例如,我们可能会创建一个很大的数组或者影射表来对每条记录进行分析。为了避免对每条记录都作这样的初始化,Sawzall有一个保留字static可以确保这个变量只初始化一次,并且是在处理每条记录的最开始的初始化的时候执行。这就是一个例子:

      static CJK: map[string] of string = {

      “zh” : “Chinese”,

      “jp”:”Japanese”,

      “ko”,”Korean”,

      };

CJK变量会在初始化的时候创建,并且作为处理每条记录的初始化的时候都保留CJK变量的值。

Sawzall没有引用类型;它是纯粹值语义的。数组和maps也可以作为值来是用(实现的时候,在大部分情况下,用copy-on-write引用计算来提高效率)。某些时候这个比较笨拙-在一个函数中修改一个数组,那么这个函数必须返回一个函数-但是在典型的Sawzall程序中,这个并没有太大的影响。但是这样的好处,就可以使得并发处理记录的时候,不需要担心同步问题或者担心交叉使用的问题,虽然实现上很少会用到这个情况。


11.语言的Domain相关特性

为了解决domain操作的问题,Sawzall有许多domain相关的特性。有一部分已经讨论过了,本节讨论的是剩下的一部分。

首先,跟大部分”小语言”[2]所不同,Sawzall是一个静态类型语言。主要是为了可靠性的考虑。Sawzall程序在一次运行中,会是用数小时,乃至好几个月的CPU时间,一个迟绑定(late-arising)动态类型错误导致的代价就有可能太大。另外,还有一个潜在的原因,聚合器使用完整的Sawzall类型,静态类型会让聚合器的实现比较容易。类似的争议也在分析输入协议buffer上;静态类型可以精确检测输入的类型。同样的,也会因为避免了运行时刻动态类型检测而提高整个的性能。最后,便以时候类型检查和强制类型转换要求程序员精确的指出类型转换。唯一的例外是在变量初始化的时候,但是就算在这个时候,类型以就是清晰而且程序也是类型安全的。

从另外的角度上看,强类型保证了变量的类型一定可知,在初始化的时候容易处理。例如:

      t: time=”Apr 1 12:00:00 PST 2005”;

这样的初始化就很容易理解,而且还是类型安全的。并且有一些基本类型的属性也是主机相关的。比如处理log记录的time stamps的时候,这个time基本类型就是依赖于log记录的time stamps的;对于它来说,如果要支持夏令时的时间处理就太过奢侈了。更重要的是(近来比较少见了),这个语言定义了用UNICODE表示string,而不是处理一组扩展字符集编码的变量。

由于处理大量数据集的需要,我们有赋予这个语言两个特性:处理未定义的值,处理逻辑量词。下两节详细描述这个特性。

11.1 未定义的值

Sawzall没有提供任何形式的异常处理机制。相反,他有自己的未定义值得处理概念,用来展示错误的或者不确定的结果,包括除0错,类型转换错误,I/O错误,等等。如果程序在初始化以外的地方,尝试去读一个未定义的值,它会崩溃掉,并且报告一个失败。

def()断言,用于检测这个值是否一定定义了;如果这个值是一个确定值,他返回true,否则返回false。他的通常用法如下:

      v: Value = maybe_undefined();

      if (def(v)) {

      compute(v);

      }

下面是一个必须处理未定义值得例子。我们在query-mapping程序中扩展一个时间轴。原始程序使用函数locationinfo()来通过外部数据库判定IP地址的位置。当IP地址不能在数据库中找到的时候,这个程序是不稳定的。在这种情况下,locationinfo()函数返回的是一个不确定的值,我们可以通过使用def()断言来防止这样的情况。

下边就是一个简单的扩展:

      proto "querylog.proto"

      static RESOLUTION: int = 5; # minutes; must be divisor of 60

      log_record: QueryLogProto = input;

      queries_per_degree: table sum[t: time][lat: int][lon: int] of int;

      loc: Location = locationinfo(log_record.ip);

      if (def(loc)) {

      t: time = log_record.time_usec;

      m: int = minuteof(t); # within the hour

      m = m – m % RESOLUTION;

      t = trunctohour(t) + time(m * int(MINUTE));

      emit queries_per_degree[t][int(loc.lat)][int(loc.lon)] <- 1;

      }

(注意,我们只是简单的扔掉我们不知道的位置,在这里是一个简单的处理)。在if后边的语句中,我们用了一些基本的内嵌函数(内嵌常数:MINUTE),来截断记录中的time stamp的微秒部分,整理成5分钟时间段。

这样,给定的查询log记录会扩展一个时间轴,这个程序会把数据构造多一个时间轴,这样我们可以构造一个动画来展示如何随着时间变化而查询位置有变化。

有经验的程序员会使用def()来保护常规错误,但是,有时候错误混杂起来会很怪异,导致程序员很难事先考虑。如果程序处理的事TB级别的数据,一般都会有一些数据不够规则;往往数据集的数据规则度超乎作分析程序的人的控制,或者包含偶尔当前分析不支持的数据。在Sawzall程序处理的情况下,通常对于这些异常数据,简单丢弃掉是最安全的。

Sawzall因此提供了一种模式,通过run-time flag的设置,可以改变未定义值得处理行为。通常,如果遇到一个未定义的值(就是说没有用def()来检测一下),将会终止程序并且会给出一个错误报告。当run-time flag设置了,那么,Sawzall简单的取消这个未定义的值相关的语句的执行。对于一个损坏的记录来说,就意味着对临时从程序处理中去除一样的效果。当这种情况发生的时候,run-time会把这个作为日志,在一个特别的预先定义的Collection table中记录。当运行结束的时候,用户可以检查错误率是否可以接受。对于这个flag的用法来说,还可以关掉这个flag用于调试-否则就看不到bug!-但是如果在一个大数据集上运行的时候,还是打开为妙,免得程序被异常数据所终止。

设置run-time flag的方法是不太常见的错误处理方法,但是在实际中非常有用。这个点子是和Rinard etal[14]在gcc编译器生成容错代码有点类似。在这样的编译器,如果程序访问超过数组下表的索引,那么生成的代码可以使得程序能够继续执行。这个特定的处理方式参考了web服务器的容错设计的模式,包括web服务器面临恶意攻击的健壮性的设计。Sawzall的未定义值得处理增加了类似的健壮性设计级别。

11.2 量词

虽然Sawzall是基于单个记录的操作,这些记录可能会包含数组或者结构,并且这些数组或者结构需要作为单个记录进行分析和处理。哪个数组元素有这个值?所有值都符合条件?为了使得这些容易表达,Sawzall提供了逻辑量词操作,一组特定的符号,类似”for each”,”for any”,”for all”量词。

在when语句的这种特定的构造中,可以定义一个量词,一个变量,和一个使用这个变量的布尔类型的条件。如果条件满足,那么就执行相关的语句。量词变量就像普通的integer变量,但是它的基础类型(通常是int)会有一个量词前缀。比如,给定数组a,语句:

      when(i: some int; B(A[i]))

      F(i);

就会当且仅当对于一些i的取值,布尔表达式B(a[i])为TRUE的情况下,执行F(i)。当F(i)执行了,他会被绑订到满足条件的值。对于一个when语句的执行来说,要求有求值范围的一个限制条件;在这个例子中,隐式的指出了关于数组的下标就是求值的范围。在系统内部实现上,如果需要,那么系统使用def()操作来检查边界。

一共有三个量词类型:some,当有任意值满足条件的时候执行(如果超过一个满足条件,那么就任选一个);each,执行所有满足条件的值;all,当所有的值都满足条件的时候执行(并且不绑定值到语句体)。

when语句可能包含多个量词,通常可能会导致逻辑编程的混淆[6]。Sawzall对量词的定义已经足够严格了,在实际运用中也不会有大问题。同样的,当多重变量出现的时候,Sawzall规定他们将按照他们定义的顺序进行绑定,这样可以让程序员有一定的控制能力,并且避免极端的情况。

下边是一些例子。第一个测试两个数组是否共享一个公共的元素:

      when(i, j: some int; a1[i] == a2[j]) {

      …

      }

第二个例子扩展了这个用法。使用数组限制,在数组的下标中使用用:符号来限制,他测试两个数组中,是否共享同样的3个或者更多元素的子数组:

      when(i0, i1, j0, j1: some int; a[i0:i1] == b[j0:j1] &&i1 >= i0+3) {

      …

      }

在类似这样的测试中,不用写处理边界条件的代码。即使数组小于三个元素,这个语句依旧可以正确执行,when语句的求值可以确保安全的执行。

原则上,when语句的求值处理是可以并行计算的,但是我们还没有研究这方面的内容。


12 性能

虽然Sawzall是解释执行的,但是这不是影响性能的主要因素。大部分Sawzall程序都只会带来很少一点的处理开销和I/O开销,而大部分的CPU时间都用于各种run-time的操作,比如分析protocol buffer等等。

不过,为了比较单CPU的Sawzall和其他解释语言的解释执行性能,我们写了一些小的测试程序。第一个是计算Mandelbrot的值,来测试基本的算术和循环性能。第二个测试函数用递归函数来计算头35个菲波纳契级数。我们在一个2.8G x86台式机上执行的测试。表1是测试结果,显示了Sawzall远比Python,Ruby或者Perl快,起码这些benchmarks上要快。另一方面,在这些测试上,Sawzall比解释执行的Java慢1.6倍,比编译执行的Java慢21倍,比C++编译的慢51倍。

sawzall

表1:Microbenchmarks.第一个Mandelbrotset计算:500×500图像,每点最多500次叠代。第二个用递归函数计算头35个菲波纳契级数。

这个系统的性能关键并非是单个机器上的性能,而是这个性能在处理大数据量时,增加机器的时候性能增加曲线。我们使用了一个450GB的压缩后的查询log数据,并且在其上运行一个Sawzall程序来统计某一个词出现的频率。这个程序的核心代码是类似这样的:

      result: table sum[key: string][month: int][day: int] of int;

      static keywords: array of string =

      { "hitchhiker", "benedict", "vytorin",

      "itanium", "aardvark" };

      querywords: array of string = words_from_query();

      month: int = month_of_query();

      day: int = day_of_query();

      when (i: each int; j: some int; querywords[i] == keywords[j])

      emit result[keywords[j]][month][day] <- 1;

我们在50到600台2.4G Xeon服务器上执行了这个测试程序。测试的时间结果在图5体现了。在600台机器的时候,汇聚器大概可以每秒处理1.06G压缩后的数据,或者3.2G未压缩的数据。如果这个性能扩展能力是比较完美的,那么随着机器的增加处理性能能近似线形增长,这就是说,每增加一台机器,都能增加一台机器的完整处理性能。在我们的测试中,增加1台机器的效率增加大约是相当于增加0.98台机器。

图5:当增加机器的时候性能变化曲线。实线是花费的时间,虚线是机器的工作时间产出。从50到600台机器的一个区间内,单机的性能产出仅仅下降了30%。

为什么需要一个新语言?

为什么我们需要在MapReduce之上增加一个新的语言?MapReduce已经很高效了;还少什么吗?为什么需要一个全新的语言?为什么不在MapReduce之上使用现成的语言比如Python?

这里给出了构造一个特殊目的语言的常见原因。为某一个问题领域构造特定的符号描述有助于程序清晰化,并且更紧凑,更有效率。在语言内嵌聚合器(包括在运行时刻内嵌聚合器)意味着程序员可以不用自己实现一个,这点不像使用MapReduce需要自己实现。同样的,它也更符合大规模并发处理超大数据集时候的处理思路,并且根据这个处理思路写出一流的程序。同样的,对协议栈buffer的支持,并且提供了平台相关的类型支持,在较低层面上简化了程序开发。总的来说,Sawzall程序要比基于MapReduce的C++小上10~20倍,并且更容易书写。

定制语言还有其他优势包括了增加平台相关的特性,定制的调试和模型界面,等等。

不过,制作这个Sawzall的原始动机完全不同:并行,拆分聚合器,并且提供不需要对记录内部作分析就可以最大程度的对记录进行并行处理。它也提供了一个分布式处理的模式,激励用户从另外的思维角度考察并行问题。在现成的语言中比如Awk[12],Python[1],用户可能要用这个语言书写聚合器,这就可能比较难以做到并行化处理。甚至就算在这些语言中提供了清晰的聚合器接口和函数库,经验老到的用户还有可能要实现他们自己的内容,用以大幅度提高处理性能。

Sawzall采用的模式已经被证明非常有效。虽然对于少数问题来说,这样的模式还不能有效处理,但是大部分海量数据的处理来说都已经很适用了,并且可以简单用程序实现,这就使得Sawzall成为google中很受欢迎的语言。

这个语言对用户编程方面的限制也带来额外的一些好处。因为用户程序的数据流是强类型化的,它很容易用来提供记录中的单独字段的访问控制。就是说,系统可以自动并且安全的在用户程序外增加一层,这个层本身也是由Sawzall实现的,它用来隐藏敏感信息。例如,产品工程师可以在不被授权业务信息的情况下,访问性能和监控信息数据。这个会在单独的论文中阐述。


14 工具

虽然Sawzall仅仅发布了18个月,他已经成为了google应用最广泛的语言之一。在我们的源码控制系统内已经有上千个Sawzall程序(虽然,天生这些程序就是短小精干的)。

Sawzall工具的一个衡量指标就是它所处理的数据量。我们监控了2005年3月的使用情况。在3月份,在一个有1500个XeonCPU的工作队列集群上,启动了32580个Sawzall job,平均每个使用220台机器。在这些作业中,产生了18636个失败(应用失败,网络失败,系统crash等等),导致重新运行作业的一部分。所有作业读取了大约3.2×10^15字节的数据(2.8PB),写了9.9×10^12字节(9.3TB)(显示了”数据合并”有些作用)。平均作业处理大概100GB数据,这些作业总共大约等于一个机器整整工作一个世纪的工作。


15 相关工作

传统的数据处理方式通常是通过关系数据库保存数据,并且通过SQL查询来进行查询。我们的系统有比较大的不同。首先,数据集通常过于巨大,不能放在关系型数据库里;而且文件直接在各个存储节点进行处理,而不用导入一个数据库服务器。同样的,我们的系统也没有预先设定的table或者索引;我们用构造一个特别的table和索引来进行这样的相关计算。

Sawzall和SQL完全不同,把高效的处理单个记录分析结果的聚合器接口结合到传统的过程语言。SQL有很高效的数据库join操作,但是Sawzall却不行。但是另一方面来说,Sawzall可以在上千台机器上运行处理超大数据集。

Brook[3]是另一个数据处理语言,特别适合图像处理。虽然在不同的应用领域,就像Sawzall一样,它也是基于一次处理一个元素的计算模式,来进行并行处理,并且通过一个聚合器内核来合并(reduce)输出。

另外一种处理大数据的方式是通过数据流的模式。这样的系统是处理数据流的输入,他们的操作是基于输入记录的顺序。比如,Aurora[4]就是一个流模式处理系统,支持单向数据流输入的数据集处理。就像Sawzall预定义的聚合器,Aurora提供了一个很小的,固定操作功能集合,两者都是通过用户定义的函数来体现的。这些操作功能可以构造很多有意思的查询。同Sawzall不同的是,部分Aurora操作功能是基于输入值得连续的序列,或者输入值得一个数据窗。Aurora只保存被处理的有限的一部分数据,并且不是为了查询超大的归档库设计的。虽然对Aurora来说,增加新的查询很容易,但是他们只能在最近的数据上进行操作。Aurora和Sawzall不同,Aurora是通过精心设计的运行时刻系统和查询优化器来保证性能,而Sawzall是通过强力的并行处理能力来保证性能。

另一种流模式处理系统是Hancock[7],对流模式的处理方式进行了扩展,提供了对每个查询的中间状态作保存。这个和Sawzall就完全不同,Sawzall完全不考虑每个输入记录的处理后的状态。Hancock和Aurora一样,专注于依靠提高单进程处理效率,而不是依靠大规模并行处理来提高性能。


16 展望

成百台机器并行处理的生产力是非常大的。因为Sawzall是一个大小适度的语言,用它写的程序通常比较小,并且是绑定I/O的。因此,虽然他是一个解释语言,实现上效率也足够了。但是,有些超大的,或者超复杂的分析可能需要编译成为机器码。那么编译器需要每台机器上执行一次,然后就可以用这些高速的二进制代码处理每条输入记录了。

有时候,程序在处理记录的时候需要查询外部数据库。虽然我们已经提供了对一些小型数据库的支持,比如什么IP地址信息之类的,我们的系统还是可以用一个接口来操作一个外部数据库。因为Sawzall对每条记录来说是单独处理的,所以当进行外部数据库操作的时候,系统会暂时停顿,当操作完成,继续处理记录。在这个处理过程中,当然有并行处理的可能。

有时候,我们对数据的分析需要多次处理,无论多次Sawzall处理或者从其他系统的处理而导致的多次Sawzall处理,比如从一个传统数据库来的,或者一个其他语言写的程序来的;由于Sawzall并不直接支持”chaining”(链式处理),所以,这些多重处理的程序很难在Sawzall中展示。所以,对这个进行语言方面的扩展,可以使得将来能够简单的表达对数据进行多次处理,就如同聚合器的扩展允许直接输出到外部系统一样。

某些分析需要联合从不同的输入源的数据进行分析,通常这些数据是在一次Sawzall处理或者两次Sawzall处理之后进行联合分析。Sawzall是支持这样的联合的,但是通常要求额外的链接步骤。如果有更直接的join支持会简化这样的设计。

更激进的系统模式可以完全消除这种批处理的模式。在激进的模式下,一个任务比如性能检测任务,这个Sawzall程序会持续的处理输入数据,并且聚合器跟进这个数据流。聚合器本身在一些在线服务器上运行,并且可以在任何时候来查询任何table或者table 条目的值。这种模式和流式数据库[4][7]比较类似,事实上这个也是基于数据流模式考虑的。不过,在研究这种模式以前,由Dean和Ghemawat构造的MapReduce库由于已经非常有效了,所以这样的模式还没有实现过。也许有一天我们会回到这样的模式下。


17 结束语

随着问题的增大,就需要有新的解决方案。为了更有效的解决海量数据集的大规模并发分析计算,就需要进一步限制编程模式来确保高并发能力。并且还要求不影响这样的并发模式下的展示/应用/扩展能力。

我们的觉得方法是引入了一个全新的语言叫做Sawzall。这种语言通过强制程序员每次考虑一条记录的方式来实现这样的编程模式,并且提供了一组强力的接口,这些接口属于常用的数据处理和数据合并聚合器。为了能方便写出能并发运行在上千台计算机上执行的简洁有效的程序,学一下这个新的语言还是很超值的。并且尤其重要的是,用户不用学习并发编程,本语言和底层架构解决了全部的并发细节。

虽然看起来在一个高效环境下使用解释语言有点夸张,但是我们发现CPU时间并不是瓶颈,语言明确指出,绝大部分程序都是小型的程序,并且大量的时间都耗费在I/O上以及run-time的本地代码。此外,解释语言所带来的扩展性是比较强大的,在语言级别和在多机分布式计算上的表达都是容易证明扩展能力。

也许对我们系统的终极测试就是扩展能力。我们发现随着机器的增加,性能增长是近似线性增长的。对于海量数据来说,能通过增加机器设备就能取得极高的处理性能。


18 致谢

Geeta Chaudhry写了第一个强大的Sawzall程序,并且给出了超强建议。Amit Pate,Paul Haahr,Greg Rae作为最早的用户给与了很多帮助。Paul Haahr创建了PageRank 例子。Dick Sites, Ren’ee French对于图示有贡献。此外Dan Bentley,Dave Hanson,John Lamping,Dick Sites,Tom Szymanski, Deborah A. Wallach 对本论文也有贡献。


19 参考资料

[1] David M. Beazley, Python Essential Reference, New Riders, Indianapolis, 2000.

[2] Jon Bentley, Programming Pearls, CACM August 1986 v 29 n 8 pp. 711-721.

[3] Ian Buck et al., Brook for GPUs: Stream Computing on Graphics Hardware, Proc. SIGGRAPH,Los Angeles, 2004.

[4] Don Carney et al., Monitoring Streams – A New Class of Data Management Applications, Brown Computer Science Technical Report TR-CS-02-04. At 
http://www.cs.brown.edu/research/aurora/aurora tr.pdf.

[5] M. Charikar, K. Chen, and M. Farach-Colton, Finding frequent items in data streams, Proc 29th Intl. Colloq. on Automata, Languages and Programming, 2002.

[6] W. F. Clocksin and C. S. Mellish, Programming in Prolog, Springer, 1994.

[7] Cortes et al., Hancock: A Language for Extracting Signatures from Data Streams, Proc. Sixth International Conference on Knowledge Discovery and Data Mining, Boston, 2000, pp. 9-17.

[8] Jeffrey Dean and Sanjay Ghemawat, MapReduce: Simplified Data Processing on Large Clusters, Proc 6th Symposium on Operating Systems Design and Implementation, San Francisco, 2004, pages 137-149.

[9] Sanjay Ghemawat, Howard Gobioff, and Shun-Tak Leung, The Google File System, Proc. 19th Symposium on Operating System Principles, Lake George, New York, 2003, pp. 29-43.

[10] M. Greenwald and S. Khanna, Space-efficient online computation of quantile summaries, Proc. SIGMOD, Santa Barbara, CA, May 2001, pp. 58-66.

[11] David R. Hanson, Fast allocation and deallocation of memory based on object lifetimes. Software–Practice and Experience, 20(1):512, January 1990.

[12] Brian Kernighan, Peter Weinberger, and Alfred Aho, The AWK Programming Language, Addison-Wesley, Massachusetts, 1988.

[13] Lawrence Page, Sergey Brin, Rajeev Motwani, and Terry Winograd, The pagerank citation algorithm: bringing order to the web, Proc. of the Seventh conference on the World Wide Web, Brisbane, Australia, April 1998.

[14] Martin Rinard et al., Enhancing Server Reliability Through Failure-Oblivious Computing, Proc. Sixth Symposium on Operating Systems Design and Implementation, San Francisco, 2004, pp. 303-316.

[15] Douglas Thain, Todd Tannenbaum, and Miron Livny, Distributed computing in practice: The Condor experience, Concurrency and Computation: Practice and Experience, 2004.

分享到:
评论

相关推荐

    海量数据分析:Sawzall并行处理.rar

    海量数据分析:Sawzall并行处理

    海量数据分析Sawzall并行处理

    海量数据分析:Sawzall并行处理 本文档是在网上搜索到了,迷失了原文档的地址,表示歉意! 译者信息,和感谢如下: 崮山路上走9遍2005-8-5于大连完稿 BLOG: sharp838.mblogger.cn EMAIL: sharp838@21cn.com;...

    google Sawzall 论文

    超大量的数据往往会采用一种平面的正则结构,存放于跨越多个计算机的多个磁盘上。这方面的例子包括了电话通话记录,网络日志,web...此外,对于这些数据集的分析可以展示成为应用简单的,便于分布式处理的计算方法:

    论文研究-Sawzall语言的实现和扩充 .pdf

    Sawzall语言的实现和扩充,王海波,,在互联网信息爆炸性增长的背景下,MapReduce作为一种解决随之而来的大规模数据处理问题的成熟方案被广泛应用于分布式grep、web访问日��

    sawzall

    Google并行计算构架

    谷歌论文经典中文版

    Google的经典论文中文版 Cluster:发表于2003 年,主要介绍Google 的集群架构,对Google 搜索系统的 架构也进行了简单介绍 GFS:发表于2003 年,介绍了Google 分布式文件系统的设计及实现。Hadoop 中 与之对应的是...

    Sawzall编译器Szl.zip

    Szl 是 Google Sawzall 语言日志数据统计聚合,是 Sawzall 语言的编译器和运行时工具。

    Google的十个核心技术

    本文主要简单介绍Google的十个核心技术,而且可以分为...2.分布式大规模数据处理:MapReduce和Sawzall。 3.分布式数据库技术:BigTable和数据库Sharding。 4.数据中心优化技术:数据中心高温化,12V电池和服务器整合。

    java餐桌点餐系统源码-GoogleBlog:谷歌实习

    的设计目标不只是检索和分析本机的某一种语言的代码,而是大规模的检索和分析 Google 的所有项目,所有语言,所有代码。这包括 Google 的“四大语言”:C++, Java, JavaScript, Python,一些工具性的语言:Sawzall,...

    典型云计算平台介绍.doc

    Sawzall是一 种建立在MapReduce基础上的领域语言,专门用于大规模的信息处理。Chubby是一个高可 用、分布式数据锁服务,当有机器失效时,Chubby使用Paxos算法来保证备份。 (2)IBM"蓝云"计算平台 "蓝云"解决方案是...

Global site tag (gtag.js) - Google Analytics